diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0943dcc..e2e5995 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,11 +19,9 @@ jobs: - uses: actions/setup-node@v4 with: node-version: '20' - cache: npm - cache-dependency-path: package-lock.json - name: Install dependencies - run: npm ci + run: npm install --no-package-lock - name: Format check (Prettier) run: npm run format:check diff --git a/docs/qa-observed-assertions.md b/docs/qa-observed-assertions.md new file mode 100644 index 0000000..a85b40c --- /dev/null +++ b/docs/qa-observed-assertions.md @@ -0,0 +1,99 @@ +# QA: Observed Behavior Assertions + +## Problem + +AppClaw completes a task and declares success, but gives no structured record of _what it observed_ — prices, confirmation messages, order numbers, screen states. On the next run there is no way to know if the outcome was the same. + +## Concept + +After a successful run, an LLM call reads the agent's step history and extracts observable facts as assertions: + +``` +Run: "complete checkout for 1 large oat milk latte" +Observed assertions: + ✓ Order confirmation screen appeared + ✓ Item: "Oat Milk Latte, Large" shown + ✓ Price shown: $6.95 + ✓ Payment method: Apple Pay + ✓ Estimated ready time shown + ✓ Completed in 4 steps +``` + +On subsequent runs these become **soft assertions** — the agent flags any that no longer hold. + +## Assertion Types + +| Type | Example | How detected | +| --------------- | ------------------------------------ | ---------------------------------- | +| Screen appeared | "Order confirmation screen appeared" | Screen fingerprint match | +| Text present | "Price shown: $6.95" | LLM extraction from DOM/screenshot | +| Step count | "Completed in 4 steps" | `stepsInRun` from trajectory | +| Element state | "Apple Pay button was selected" | LLM extraction | + +## Proposed Design + +### Extraction (async, post-run) + +```typescript +// After successful finalize() +const assertions = await extractAssertions(stepHistory, goal, llmClient); +saveAssertions(appId, goalHash, assertions); +``` + +Prompt to LLM: + +``` +Given this agent run transcript, extract 3-6 observable facts about the outcome +as short assertion strings. Focus on: screens that appeared, values shown, +actions completed. Be specific. Format: one assertion per line. +``` + +### Storage + +`~/.appclaw/assertions//.json` + +```json +{ + "goal": "complete checkout", + "appId": "com.starbucks", + "extractedAt": 1712345678, + "assertions": ["Order confirmation screen appeared", "Price shown: $6.95", "Completed in 4 steps"] +} +``` + +### Soft assertion check on next run + +At run end, retrieve stored assertions and ask the LLM: + +``` +Previous run observed: ["Order confirmation screen appeared", "Price shown: $6.95"] +Based on the current run transcript, which of these still hold? Which do not? +``` + +Emit result in terminal and HTML report. + +### Hard assertions in YAML flows + +QA engineers can also write explicit assertions in flow files: + +```yaml +steps: + - tap checkout + - ... +assertions: + - order confirmation screen is visible + - price displayed is under $10 + - no error messages present +``` + +These run after all steps complete and fail the flow if any assertion fails. + +## Files to Touch + +- New: `src/assertions/extractor.ts` — LLM-based assertion extraction +- New: `src/assertions/checker.ts` — compare assertions against current run +- New: `src/assertions/store.ts` — persist/load assertion sets +- `src/flow/parse-yaml-flow.ts` — parse `assertions:` block from YAML +- `src/flow/run-yaml-flow.ts` — run assertion checker after steps complete +- `src/agent/loop.ts` — trigger async extraction on success +- `src/report/writer.ts` — include assertion results in HTML report diff --git a/docs/qa-personas.md b/docs/qa-personas.md new file mode 100644 index 0000000..3484652 --- /dev/null +++ b/docs/qa-personas.md @@ -0,0 +1,68 @@ +# QA: Test Persona Profiles + +## Problem + +Every QA test run requires a specific user context — free vs premium, new vs returning, admin vs regular. Currently this must be spelled out in the goal on every run, making flows verbose and hard to reuse. + +## Proposed Design + +### Persona files at `.appclaw/env/personas/.yaml` + +```yaml +# .appclaw/env/personas/premium-user.yaml +name: premium-user +credentials: + email: qa+premium@company.com + password: $SECRET_PREMIUM_PASS # interpolated from .appclaw/env/secrets +state: + subscription: active + cart: empty + onboarding: completed + notifications: denied +``` + +### CLI usage + +```bash +appclaw --flow checkout.yaml --persona premium-user +appclaw --flow onboarding.yaml --persona new-user +``` + +### YAML flow usage + +```yaml +persona: premium-user +steps: + - tap the checkout button + - ... +``` + +## How It Works + +1. Persona file is loaded at run start +2. Persona fields are injected into the LLM system prompt as context: + ``` + CURRENT USER PERSONA: premium-user + - Subscription: active + - Cart: empty + - Onboarding: completed + ``` +3. Credentials are available for interpolation in steps: + ```yaml + - type $persona.credentials.email into the email field + ``` +4. Secrets (values starting with `$`) are resolved from `.appclaw/env/secrets` before injection + +## Personas to Ship With (Examples) + +- `new-user` — no account, fresh install state +- `free-user` — logged in, free tier limits apply +- `premium-user` — logged in, all features unlocked +- `admin` — elevated permissions + +## Files to Touch + +- `src/flow/run-yaml-flow.ts` — load and inject persona at run start +- `src/config.ts` — add `--persona` CLI flag +- `src/llm/prompts.ts` — inject persona context into system prompt +- New: `src/persona/loader.ts` — load, validate, interpolate persona files diff --git a/docs/qa-regression-baseline.md b/docs/qa-regression-baseline.md new file mode 100644 index 0000000..ca9952e --- /dev/null +++ b/docs/qa-regression-baseline.md @@ -0,0 +1,65 @@ +# QA: Trajectory as Regression Baseline + +## Problem + +AppClaw's trajectory store already records the exact path a successful run took — the sequence of actions, selectors, and step counts. This is a regression baseline sitting unused. There is no way today to compare a current run against a previous one and flag changes. + +## Insight + +A regression is detectable when: + +- The same goal on the same app **took more steps** than before +- A screen that used to appear **no longer appears** +- An action that always worked **now fails** +- The completion path **diverged** from the recorded trajectory + +## Proposed Design + +### Regression report per run + +After each run, compare against the stored trajectory for the same (goal, app, platform) and emit a diff: + +``` +Regression Check: "complete checkout" app: com.starbucks +───────────────────────────────────────────────────────── +✓ Step 1: find_and_click "Add to Cart" (same) +✓ Step 2: find_and_click "Proceed to Checkout" (same) +⚠ Step 3: NEW — dismiss_popup "Enable notifications" (not in baseline) +✓ Step 4: find_and_click "Apple Pay" (same) +✗ Step 5: MISSING — order confirmation screen (appeared in baseline, not now) + +Steps: 4 (baseline: 4) ✓ | New steps: 1 | Missing steps: 1 +``` + +### CLI flag + +```bash +appclaw --flow checkout.yaml --check-regression +appclaw --flow checkout.yaml --update-baseline # overwrite stored baseline +``` + +### Baseline storage + +Extend `TrajectoryEntry` in `src/memory/types.ts` with an ordered step sequence (not just the winning final action) so full path comparison is possible. + +Or store baselines separately at `~/.appclaw/baselines//.json`. + +## Step Count Heuristic (Quick Win) + +Without full path comparison, step count delta alone is a useful signal: + +``` +⚠ Regression risk: "complete checkout" took 7 steps (baseline: 4). App may have added screens. +``` + +This requires no schema changes — `stepsInRun` is already stored in `TrajectoryEntry`. + +Surface this warning in the run summary today. + +## Files to Touch + +- `src/memory/types.ts` — extend `TrajectoryEntry` with step sequence (optional, for full diff) +- `src/memory/retriever.ts` — add baseline comparison function +- `src/agent/loop.ts` — emit regression warning at run end +- `src/report/writer.ts` — include regression diff in HTML report +- `src/config.ts` — add `--check-regression` and `--update-baseline` flags diff --git a/docs/qa-step-libraries.md b/docs/qa-step-libraries.md new file mode 100644 index 0000000..796452c --- /dev/null +++ b/docs/qa-step-libraries.md @@ -0,0 +1,79 @@ +# QA: Named Setup Steps / Step Libraries + +## Problem + +Common setup sequences (login, clear cart, reset permissions, onboard a new user) are written out in full in every flow file that needs them. When the login flow changes, every flow that embeds it must be updated. There is no reuse. + +## Concept + +Named step sequences stored as shared YAML fragments, referenced from any flow: + +```yaml +# .appclaw/steps/login-as-admin.yaml +name: login-as-admin +description: Log in using admin credentials, handle 2FA if prompted +steps: + - tap the Sign In button + - type $persona.credentials.email into the email field + - type $persona.credentials.password into the password field + - tap Login + - if OTP screen appears, wait for human input +``` + +Referenced in any flow: + +```yaml +setup: + - use: login-as-admin + - use: clear-cart + +steps: + - tap checkout + - ... +``` + +## Step Library Locations + +Resolution order (first match wins): + +1. `.appclaw/steps/` — project-level, checked into repo +2. `~/.appclaw/steps/` — user-level, shared across projects +3. Built-in steps shipped with AppClaw (login helpers, permission handlers) + +## Built-in Steps to Ship + +| Name | Description | +| ----------------------- | ------------------------------------------------- | +| `dismiss-notifications` | Deny notification permission prompt if it appears | +| `dismiss-tracking` | Deny app tracking permission if it appears | +| `clear-cart` | Navigate to cart and remove all items | +| `logout` | Navigate to account settings and log out | +| `wait-for-network` | Wait until a loading spinner disappears | + +## Composability + +Steps can reference other steps: + +```yaml +# .appclaw/steps/fresh-checkout-session.yaml +steps: + - use: logout + - use: login-as-free-user + - use: clear-cart +``` + +## Discoverable via CLI + +```bash +appclaw --list-steps # list all available named steps +appclaw --list-steps --filter login # filter by name +appclaw --run-step login-as-admin # run a single step in isolation +``` + +## Files to Touch + +- `src/flow/parse-yaml-flow.ts` — resolve `use:` references, load step files +- `src/flow/run-yaml-flow.ts` — execute referenced steps inline +- New: `src/flow/step-library.ts` — resolve step files from project + user + built-in paths +- `src/config.ts` — add `--list-steps`, `--run-step` flags +- New: `src/flow/builtin-steps/` — built-in step YAML files diff --git a/landing/usage.html b/landing/usage.html index 3b373f7..fa78ba1 100644 --- a/landing/usage.html +++ b/landing/usage.html @@ -6,7 +6,7 @@ Usage Guide — AppClaw @@ -209,45 +209,43 @@ color: var(--text); } - /* ===== PAGE HEADER ===== */ + /* ===== PAGE HEADER (compact title bar) ===== */ .page-header { - padding: 120px 0 60px; - text-align: center; - position: relative; - overflow: hidden; + position: fixed; + top: 64px; + left: 0; + right: 0; + z-index: 90; + height: 48px; + background: var(--bg); + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + padding: 0 32px; + gap: 12px; } - .page-header-orb { - position: absolute; - border-radius: 50%; - filter: blur(100px); - opacity: 0.1; - pointer-events: none; - } - .page-header-orb-1 { - width: 500px; - height: 500px; - background: var(--accent); - top: -200px; - left: -100px; + .page-header h1 { + font-size: 14px; + font-weight: 600; + color: var(--text); + letter-spacing: -0.01em; + margin: 0; } - .page-header-orb-2 { - width: 400px; - height: 400px; - background: var(--teal); - top: -100px; - right: -150px; + + .page-header .page-header-sep { + color: var(--text-tertiary); + font-size: 14px; } - .page-header h1 { - font-size: clamp(2rem, 4vw, 3rem); - font-weight: 800; - letter-spacing: -0.03em; - line-height: 1.15; - margin-bottom: 16px; + .page-header .page-header-section { + font-size: 14px; + color: var(--text-secondary); + font-weight: 400; } .page-header p { + display: none; font-size: 18px; color: var(--text-secondary); max-width: 640px; @@ -256,19 +254,38 @@ /* ===== DOCS LAYOUT ===== */ .docs-layout { - display: grid; - grid-template-columns: 220px 1fr; - gap: 64px; - padding: 60px 0 120px; + display: flex; + gap: 0; + padding: 0; + height: calc(100vh - 112px); + margin-top: 112px; + position: relative; } /* Sidebar */ .docs-sidebar { + width: 240px; + flex-shrink: 0; position: sticky; - top: 96px; - align-self: start; - max-height: calc(100vh - 120px); + top: 112px; + height: calc(100vh - 112px); overflow-y: auto; + background: var(--bg-alt); + border-right: 1px solid var(--border); + padding: 20px 0 40px; + scrollbar-width: thin; + scrollbar-color: rgba(15, 23, 42, 0.1) transparent; + } + + .docs-sidebar::-webkit-scrollbar { + width: 4px; + } + .docs-sidebar::-webkit-scrollbar-track { + background: transparent; + } + .docs-sidebar::-webkit-scrollbar-thumb { + background: rgba(15, 23, 42, 0.1); + border-radius: 2px; } .docs-sidebar ul { @@ -276,47 +293,162 @@ } .docs-sidebar li { - margin-bottom: 4px; + margin-bottom: 2px; } .docs-sidebar a { display: block; - padding: 6px 12px; + padding: 5px 16px 5px 28px; font-size: 13px; - font-weight: 500; + font-weight: 400; color: var(--text-secondary); text-decoration: none; - border-radius: 6px; - transition: all 0.15s; + border-radius: 0; + transition: all 0.12s; + border-left: 2px solid transparent; } .docs-sidebar a:hover { color: var(--text); - background: var(--bg-alt); + background: rgba(15, 23, 42, 0.04); + border-left-color: rgba(249, 115, 22, 0.3); } .docs-sidebar a.active { color: var(--accent); background: rgba(249, 115, 22, 0.08); + border-left-color: var(--accent); + font-weight: 500; + } + + /* Accordion groups */ + .sidebar-group { + margin-bottom: 4px; } - .docs-sidebar .sidebar-heading { + .sidebar-heading { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + padding: 7px 12px 7px 16px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; - color: var(--text-tertiary); - padding: 16px 12px 6px; + color: var(--text); + background: none; + border: none; + border-radius: 6px; + cursor: pointer; + text-align: left; + font-family: inherit; + transition: + background 0.15s, + color 0.15s; + margin: 4px 8px 2px; + width: calc(100% - 16px); + } + .sidebar-heading:hover { + background: rgba(15, 23, 42, 0.05); + color: var(--text); + } + + .sidebar-chevron { + width: 13px; + height: 13px; + flex-shrink: 0; + transition: transform 0.2s ease; + color: var(--text-secondary); + opacity: 0.6; + } + .sidebar-group:not(.open) .sidebar-chevron { + transform: rotate(-90deg); } - .docs-sidebar .sidebar-heading:first-child { - padding-top: 0; + + /* Accordion body */ + .sidebar-group ul { + overflow: hidden; + max-height: 600px; + transition: + max-height 0.25s ease, + opacity 0.2s ease; + opacity: 1; + } + .sidebar-group:not(.open) ul { + max-height: 0; + opacity: 0; } /* Content */ .docs-content { + flex: 1; min-width: 0; + overflow-y: auto; + height: calc(100vh - 112px); + padding: 0; + scrollbar-width: thin; + scrollbar-color: rgba(15, 23, 42, 0.15) transparent; + } + + .docs-content::-webkit-scrollbar { + width: 6px; + } + .docs-content::-webkit-scrollbar-track { + background: transparent; + } + .docs-content::-webkit-scrollbar-thumb { + background: rgba(15, 23, 42, 0.15); + border-radius: 3px; } .docs-content section { - margin-bottom: 64px; + display: none; + max-width: 820px; + padding: 48px 64px 80px; + margin: 0; + } + + /* CLI Options — wider to support 2-column table layout */ + #cli-options { + max-width: 1100px; + } + + .cli-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 40px; + margin-bottom: 8px; + } + .cli-grid .cli-col h3 { + margin-top: 0; + } + .cli-full { + margin-top: 0; + } + + @media (max-width: 860px) { + .cli-grid { + grid-template-columns: 1fr; + gap: 0; + } + #cli-options { + max-width: 820px; + } + } + + .docs-content section.active { + display: block; + animation: panelIn 0.18s ease-out; + } + + @keyframes panelIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } } .docs-content h2 { @@ -631,27 +763,40 @@ /* ===== RESPONSIVE ===== */ @media (max-width: 1024px) { .docs-layout { - grid-template-columns: 1fr; - gap: 0; + flex-direction: column; + height: auto; } .docs-sidebar { position: relative; top: 0; - max-height: none; + width: 100%; + height: auto; display: flex; flex-wrap: wrap; - gap: 4px; - padding-bottom: 32px; - border-bottom: 1px solid var(--border); - margin-bottom: 40px; + gap: 2px; + padding: 16px 16px 20px; + border-right: none; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); } .docs-sidebar .sidebar-heading { width: 100%; - padding: 12px 0 4px; + padding: 10px 4px 4px; } .docs-sidebar li { margin-bottom: 0; } + .docs-sidebar a { + padding: 4px 10px; + border-radius: 5px; + border-left: none; + } + .docs-content { + height: auto; + overflow-y: visible; + } + .docs-content section { + padding: 32px 24px 60px; + } } @media (max-width: 768px) { @@ -685,6 +830,99 @@ font-size: 16px; } } + + /* ===== SECTION NAV (prev / next) ===== */ + .section-nav { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 48px; + padding-top: 24px; + border-top: 1px solid var(--border); + gap: 16px; + } + + .section-nav-btn { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 16px; + border-radius: 8px; + border: 1px solid var(--border); + background: var(--bg); + color: var(--text-secondary); + font-size: 13px; + font-weight: 500; + cursor: pointer; + text-decoration: none; + transition: all 0.15s; + font-family: inherit; + } + .section-nav-btn:hover { + border-color: var(--accent); + color: var(--accent); + background: rgba(249, 115, 22, 0.04); + } + .section-nav-btn.prev { + margin-right: auto; + } + .section-nav-btn.next { + margin-left: auto; + } + .section-nav-btn svg { + width: 14px; + height: 14px; + flex-shrink: 0; + } + + /* ===== SECTION HEADER breadcrumb ===== */ + .section-breadcrumb { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--text-tertiary); + margin-bottom: 24px; + font-family: 'JetBrains Mono', monospace; + letter-spacing: 0.01em; + } + .section-breadcrumb span { + color: var(--accent); + } + + /* ===== DOCS SEARCH ===== */ + .sidebar-search { + padding: 12px 12px 8px; + } + .sidebar-search input { + width: 100%; + padding: 7px 10px 7px 30px; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text); + font-size: 12px; + font-family: inherit; + outline: none; + transition: border-color 0.15s; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.35-4.35'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: 10px center; + } + .sidebar-search input::placeholder { + color: var(--text-tertiary); + } + .sidebar-search input:focus { + border-color: rgba(249, 115, 22, 0.4); + color: var(--text); + } + .sidebar-search input::-webkit-search-cancel-button { + display: none; + } + + .docs-sidebar li.hidden { + display: none; + } @@ -729,38 +967,75 @@ - - + + -
-
- - + + +
+ + + +
+

Quick Start

+

Install AppClaw globally and start automating in seconds.

+ +
+
+
+ Terminal +
+
# Install
 npm install -g appclaw
 
 # Run with a natural language goal
@@ -804,81 +1138,87 @@ 

Quick Start

# Interactive playground appclaw --playground
+
+
+ +
+

CLI Options

+

All the flags you can pass to appclaw.

+ +
+
+

Platform & Device

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FlagDescription
--platform <os>Target platform: android or ios
--device-type <type>iOS only: simulator or real
--device <name>Device name (partial match, e.g. "iPhone 17 Pro")
--udid <udid>Device UDID (skips the device picker)
-
- -
-

CLI Options

-

All the flags you can pass to appclaw.

- -

Platform & Device

- - - - - - - - - - - - - - - - - - - - - - - - - -
FlagDescription
--platform <os>Target platform: android or ios
--device-type <type>iOS only: simulator or real
--device <name>Device name (partial match, e.g. "iPhone 17 Pro")
--udid <udid>Device UDID (skips the device picker)
- -

Execution

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FlagDescription
--flow <file>Run a declarative YAML flow file
--env <name>Environment for variable/secret resolution
--playgroundLaunch the interactive REPL for building flows
--recordRecord a goal execution for later replay
--replay <file>Replay a previously recorded session
--planDecompose a complex goal into sub-goals
--jsonJSON output mode (for IDE extensions)
+
+

Execution

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FlagDescription
--flow <file>Run a declarative YAML flow file
--env <name>Environment for variable/secret resolution
--playgroundLaunch the interactive REPL for building flows
--recordRecord a goal execution for later replay
--replay <file>Replay a previously recorded session
--planDecompose a complex goal into sub-goals
--jsonJSON output mode (for IDE extensions)
+
+
+

Explorer (Test Generation)

@@ -914,87 +1254,88 @@

Explorer (Test Generation)

- - -
-

Execution Modes

-

AppClaw has three distinct ways to automate mobile apps.

- -

Agent Mode

-

- Give AppClaw a goal in plain English. The AI agent takes a screenshot, reasons about - what it sees, and decides what to tap, type, or swipe — step by step until the - goal is complete. -

-
-
-
- Agent Mode -
-
appclaw "Search for 'Appium 3.0' on YouTube and find the TestMu AI video"
+
+
+ +
+

Execution Modes

+

AppClaw has three distinct ways to automate mobile apps.

+ +

Agent Mode

+

+ Give AppClaw a goal in plain English. The AI agent takes a screenshot, reasons about + what it sees, and decides what to tap, type, or swipe — step by step until the + goal is complete. +

+
+
+
+ Agent Mode
+
appclaw "Search for 'Appium 3.0' on YouTube and find the TestMu AI video"
+
-

YAML Flows

-

- Define repeatable, version-controlled test flows in YAML. Each step is a natural - language instruction — no element selectors, no brittle locators. -

-
-
-
- YAML Flow -
-
appclaw --flow tests/youtube-search.yaml --env dev
+

YAML Flows

+

+ Define repeatable, version-controlled test flows in YAML. Each step is a natural + language instruction — no element selectors, no brittle locators. +

+
+
+
+ YAML Flow
+
appclaw --flow tests/youtube-search.yaml --env dev
+
-

Playground

-

- An interactive REPL where you type one instruction at a time and see it execute - immediately. Great for exploring an app and building flows interactively. -

-
-
-
- Playground -
-
appclaw --playground --platform ios --device-type simulator
+

Playground

+

+ An interactive REPL where you type one instruction at a time and see it execute + immediately. Great for exploring an app and building flows interactively. +

+
+
+
+ Playground
-
- - - - -
-

Designing YAML Flows

+
appclaw --playground --platform ios --device-type simulator
+
+ + + + + +
+

Designing YAML Flows

+

+ YAML flows are the heart of AppClaw's repeatable automation. Write your test steps in + plain English — AppClaw figures out how to execute them on the device. No XPath, + no accessibility IDs, no brittle selectors. +

+ +
+
Key Idea

- YAML flows are the heart of AppClaw's repeatable automation. Write your test steps in - plain English — AppClaw figures out how to execute them on the device. No XPath, - no accessibility IDs, no brittle selectors. + Each step is a natural language instruction like tap Login or + wait for the home screen to be visible. AppClaw uses AI to find the right + elements on screen.

- -
-
Key Idea
-

- Each step is a natural language instruction like tap Login or - wait for the home screen to be visible. AppClaw uses AI to find the - right elements on screen. -

+
+
+ +
+

Flat Format

+

+ The simplest YAML structure — a metadata header separated by + --- from a flat list of steps. +

+ +
+
+
+ settings-wifi.yaml
-
- -
-

Flat Format

-

- The simplest YAML structure — a metadata header separated by - --- from a flat list of steps. -

- -
-
-
- settings-wifi.yaml -
-
name: Turn on Wi-Fi
+            
name: Turn on Wi-Fi
 platform: android
 ---
 - open Settings app
@@ -1003,61 +1344,61 @@ 

Flat Format

- tap Wi-Fi - verify Wi-Fi is visible - done
-
- -

Metadata Fields

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldDescription
nameDisplay name for the flow
descriptionOptional description of what the flow does
platform - android or ios — fallback if no - --platform CLI flag -
appIdApp bundle/package ID for launchApp steps
env - Environment name — resolves variables from - .appclaw/env/<name>.yaml -
-
- -
-

Phased Format

-

- For structured tests, organize your steps into three phases: setup, - steps, and assertions. This gives clearer reporting - and separates initialization from the actual test logic. -

+
-
-
-
- youtube-search.yaml -
-
name: YouTube Search
+          

Metadata Fields

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
nameDisplay name for the flow
descriptionOptional description of what the flow does
platform + android or ios — fallback if no + --platform CLI flag +
appIdApp bundle/package ID for launchApp steps
env + Environment name — resolves variables from + .appclaw/env/<name>.yaml +
+ + +
+

Phased Format

+

+ For structured tests, organize your steps into three phases: setup, + steps, and assertions. This gives clearer reporting + and separates initialization from the actual test logic. +

+ +
+
+
+ youtube-search.yaml +
+
name: YouTube Search
 description: Searches YouTube and verifies video results
 platform: android
 env: dev
@@ -1076,81 +1417,81 @@ 

Phased Format

assertions: - verify ${variables.expected_channel} is visible
-
- -

Phases Explained

- - - - - - - - - - - - - - - - - - - - - -
PhasePurpose
setup - Initialization — launch the app, navigate to starting screen, dismiss - popups. Failures here skip the test. -
stepsThe main test actions — the interactions you're actually testing.
assertions - Verification checks — confirm the expected outcome. You can also mix in - actions here if needed. -
-
- -
-

Variables & Secrets

-

Keep your flows flexible and secure with variable interpolation.

- -

Variables ${variables.X}

-

Loaded from environment files. Values appear in logs.

- -

Secrets ${secrets.X}

-

- Resolved from shell environment variables at runtime. Always shown as - *** in logs. -

- -

Environment File

-

Create .appclaw/env/<name>.yaml in your project root:

+
-
-
-
- .appclaw/env/dev.yaml -
-
variables:
+          

Phases Explained

+ + + + + + + + + + + + + + + + + + + + + +
PhasePurpose
setup + Initialization — launch the app, navigate to starting screen, dismiss + popups. Failures here skip the test. +
stepsThe main test actions — the interactions you're actually testing.
assertions + Verification checks — confirm the expected outcome. You can also mix in + actions here if needed. +
+ + +
+

Variables & Secrets

+

Keep your flows flexible and secure with variable interpolation.

+ +

Variables ${variables.X}

+

Loaded from environment files. Values appear in logs.

+ +

Secrets ${secrets.X}

+

+ Resolved from shell environment variables at runtime. Always shown as + *** in logs. +

+ +

Environment File

+

Create .appclaw/env/<name>.yaml in your project root:

+ +
+
+
+ .appclaw/env/dev.yaml +
+
variables:
   app_name: youtube
   expected_channel: TestMu AI
   timeout: 30
   locale: en-US
-
+
-

- Then reference it in your YAML header with env: dev, or pass - --env dev on the CLI. -

+

+ Then reference it in your YAML header with env: dev, or pass + --env dev on the CLI. +

-

Inline Variables

-

For self-contained flows, embed variables directly in the YAML header:

+

Inline Variables

+

For self-contained flows, embed variables directly in the YAML header:

-
-
-
- Inline env block -
-
name: Self-contained flow
+          
+
+
+ Inline env block +
+
name: Self-contained flow
 env:
   variables:
     app_name: youtube
@@ -1158,34 +1499,34 @@ 

Inline Variables

--- - open ${variables.app_name} app - type ${variables.search_term}
-
- -
-
Resolution Order
-

- --env CLI flag wins over the YAML env: field, which wins - over inline env: blocks. Secrets always come from shell environment - variables. -

-
- +
- - - -
-

Tap / Click

+
+
Resolution Order

- Tap on an element by describing its label. AppClaw matches it against visible text and - elements on screen. + --env CLI flag wins over the YAML env: field, which wins + over inline env: blocks. Secrets always come from shell environment + variables.

- -
-
-
- Natural language -
-
- tap Login
+          
+
+ + + + +
+

Tap / Click

+

+ Tap on an element by describing its label. AppClaw matches it against visible text and + elements on screen. +

+ +
+
+
+ Natural language +
+
- tap Login
 - click on the search icon
 - press Submit
 - select the first item
@@ -1196,32 +1537,32 @@ 

Tap / Click

- enable Notifications - close the popup - dismiss the dialog
+
+ +

+ All of these are equivalent — they find the element and tap it. Use whichever + reads most naturally. +

+ +
+
+
+ Structured YAML
+
- tap: "Login Button"
+
+
-

- All of these are equivalent — they find the element and tap it. Use whichever - reads most naturally. -

+
+

Type Text

+

Type text into the currently focused field, or specify a target field.

-
-
-
- Structured YAML -
-
- tap: "Login Button"
+
+
+
+ Natural language
-
- -
-

Type Text

-

Type text into the currently focused field, or specify a target field.

- -
-
-
- Natural language -
-
# Type into focused field
+            
# Type into focused field
 - type "hello world"
 - enter text "user@example.com"
 
@@ -1232,89 +1573,89 @@ 

Type Text

# Search (types the text) - search for "Appium 3.0" - look for "restaurants nearby"
+
+ +
+
+
+ Structured YAML
+
- type: "hello world"
+
+
+ +
+

Wait / Pause

+

Pause execution for a fixed duration.

-
-
-
- Structured YAML -
-
- type: "hello world"
+
+
+
+ Natural language
-
- -
-

Wait / Pause

-

Pause execution for a fixed duration.

- -
-
-
- Natural language -
-
- wait 3s
+            
- wait 3s
 - wait 1.5 seconds
 - sleep 500ms
 - pause 2 sec
 - wait               # defaults to 2 seconds
 - wait a moment      # defaults to 2 seconds
-
+ -
-
-
- Structured YAML -
-
- wait: 3          # seconds
+
+
+
+ Structured YAML
-
- -
-

Wait Until

-

- Wait dynamically until a condition is met. Polls the screen every 500ms up to a - timeout (default 10s). Uses AI vision to understand the screen — you can - describe what you expect to see in plain English. -

- -
-
-
- Wait for something to appear -
-
- wait until search icon is visible
+            
- wait: 3          # seconds
+
+
+ +
+

Wait Until

+

+ Wait dynamically until a condition is met. Polls the screen every 500ms up to a timeout + (default 10s). Uses AI vision to understand the screen — you can describe what you + expect to see in plain English. +

+ +
+
+
+ Wait for something to appear +
+
- wait until search icon is visible
 - wait for the search results to be visible
 - wait for the home screen to be loaded
 - wait until "Welcome back" appears
 - wait 15s until login button is visible  # custom timeout
-
+ -
-
-
- Wait for something to disappear -
-
- wait until loading spinner is gone
+          
+
+
+ Wait for something to disappear +
+
- wait until loading spinner is gone
 - wait for the popup to be hidden
 - wait until progress bar disappeared
-
+
-
-
-
- Wait for screen to stabilize -
-
- wait until screen is loaded
+          
+
+
+ Wait for screen to stabilize +
+
- wait until screen is loaded
 - wait until screen is stable
 - wait 5s until screen is ready
-
+
-
-
-
- Structured YAML -
-
# With custom timeout
+          
+
+
+ Structured YAML +
+
# With custom timeout
 - waitUntil: "Login button"
   timeout: 15
 
@@ -1324,145 +1665,145 @@ 

Wait Until

# Screen loaded (DOM stability check) - waitUntil: "screen loaded"
-
- -
-
Smart Vision
-

- When you write something descriptive like - wait for the search results to be visible, AppClaw uses AI vision to - understand the screen holistically — it checks whether results are actually - shown, not just whether the literal words "search results" appear. You can describe - what you expect to see naturally. -

-
-
+ -
-

Scroll / Swipe

+
+
Smart Vision

- Scroll or swipe in any direction, optionally repeating multiple times or scrolling - until an element is found. + When you write something descriptive like + wait for the search results to be visible, AppClaw uses AI vision to + understand the screen holistically — it checks whether results are actually + shown, not just whether the literal words "search results" appear. You can describe + what you expect to see naturally.

- -
-
-
- Basic scroll / swipe -
-
- scroll down
+          
+
+ +
+

Scroll / Swipe

+

+ Scroll or swipe in any direction, optionally repeating multiple times or scrolling until + an element is found. +

+ +
+
+
+ Basic scroll / swipe +
+
- scroll down
 - scroll up 3 times
 - swipe left
 - swipe right 2 times
-
+ -
-
-
- Scroll until found -
-
# Scroll until an element appears
+          
+
+
+ Scroll until found +
+
# Scroll until an element appears
 - scroll down until "Terms & Conditions" is visible
 - scroll down 5 times to find "Accept"
 - scroll down to see "Load More"
-
+
-
-
-
- Structured YAML -
-
- scrollAssert: "Terms & Conditions"
+          
+
+
+ Structured YAML +
+
- scrollAssert: "Terms & Conditions"
   direction: down
   maxScrolls: 5
+
+
+ +
+

Drag / Slider

+

+ Drag one element to another — sliders, carousels, reorderable lists, and any + drag-and-drop interaction. Requires vision mode (AGENT_MODE=vision). +

+ +
+
+
+ Natural language
-
- -
-

Drag / Slider

-

- Drag one element to another — sliders, carousels, reorderable lists, and any - drag-and-drop interaction. Requires vision mode (AGENT_MODE=vision). -

- -
-
-
- Natural language -
-
- drag the green circle slider to the +100 mark
+            
- drag the green circle slider to the +100 mark
 - slide the price handle to +80
 - move the volume knob to maximum
-
+ -
-
-
- Structured YAML — shorthand -
-
# "drag: from to to"
-- drag: "green circle slider to +100 mark"
+
+
+
+ Structured YAML — shorthand
+
# "drag: from to to"
+- drag: "green circle slider to +100 mark"
+
-
-
-
- Structured YAML — explicit from/to -
-
- drag:
+          
+
+
+ Structured YAML — explicit from/to +
+
- drag:
     from: green circle slider
     to:   +100 mark
-
- -
-
Vision required
-

- Drag uses AI vision to locate both the source and target by visual description. Set - AGENT_MODE=vision and VISION_LOCATE_PROVIDER=stark with a - valid LLM_API_KEY. -

-
-
+ -
-

Assert / Verify

+
+
Vision required

- Verify that something is visible on screen. Works with both literal text and - visual/semantic descriptions via AI vision. + Drag uses AI vision to locate both the source and target by visual description. Set + AGENT_MODE=vision and VISION_LOCATE_PROVIDER=stark with a + valid LLM_API_KEY.

- -
-
-
- Natural language -
-
- verify "Welcome back" is visible
+          
+
+ +
+

Assert / Verify

+

+ Verify that something is visible on screen. Works with both literal text and + visual/semantic descriptions via AI vision. +

+ +
+
+
+ Natural language +
+
- verify "Welcome back" is visible
 - assert Dashboard is visible
 - check that the login button is on the screen
 - verify TestMu AI is visible
-
+ -
-
-
- Structured YAML -
-
- assert: "Welcome back"
+          
+
+
+ Structured YAML +
+
- assert: "Welcome back"
 - verify: "Dashboard"    # alias for assert
 - check:  "Login button"  # alias for assert
+
+
+ + - - + +
+

Full Command Reference

+

Every supported step kind at a glance.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KindParametersDescription
openAppqueryOpen an app by name
launchAppLaunch app defined in appId metadata
taplabelTap element by visible text/label
typetext, target?Type text, optionally into a named field
enterPress Enter / Return key
backPress the Back button
homePress the Home button
waitsecondsPause for a fixed duration
waitUntilcondition, text?, timeoutPoll until visible/gone/screenLoaded
swipedirection, repeat?Swipe up/down/left/right
dragfrom, toDrag from one element to another (vision mode)
asserttextVerify text or description is visible
scrollAsserttext, direction, maxScrollsScroll until text found
getInfoqueryAsk the AI a question about the screen
donemessage?Signal flow completion
+
+ + + + +
+

Vision Modes

+

Control how AppClaw locates elements on screen.

+ +

Agent Mode AGENT_MODE

+ + + + + + + + + + + + + + + + + +
ValueBehavior
domDefault. Uses the app's DOM/accessibility tree to find elements.
vision + Uses AI vision (screenshots + LLM) as the primary strategy for all interactions. +
+ +

Vision Mode VISION_MODE

+ + + + + + + + + + + + + + + + + + + + + +
ValueBehavior
fallbackDefault. Try DOM first, fall back to vision if no match found.
alwaysSkip DOM entirely, use vision for every interaction.
neverDOM only. No vision fallback.
+
+ +
+

Environment Variables

+

+ All environment variables recognized by AppClaw. These are especially useful for CI/CD + pipelines. +

+ +

LLM Configuration

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableDescription
LLM_PROVIDER + LLM provider: anthropic, openai, gemini, + groq, ollama +
LLM_API_KEYAPI key for the chosen provider
LLM_MODELSpecific model name to use
LLM_THINKINGExtended thinking: on or off (default: on)
LLM_THINKING_BUDGETMax thinking tokens: 1–10000 (default: 128)
LLM_SCREENSHOT_MAX_EDGE_PXDownscale screenshots to this max edge (0 = disabled)
+ +

Device & Platform

+ + + + + + + + + + + + + + + + + + + + + + + + + +
VariableDescription
PLATFORMSame as --platform flag
DEVICE_TYPESame as --device-type flag
DEVICE_UDIDSame as --udid flag
DEVICE_NAMESame as --device flag
+ +

Vision

+ + + + + + + + + + + + + + + + + + + + + +
VariableDescription
VISION_MODEalways, fallback, or never
AGENT_MODEdom or vision
GEMINI_API_KEY + Gemini API key for Stark vision — only needed when LLM_PROVIDER is + not gemini and AGENT_MODE=vision. If provider is already + Gemini, LLM_API_KEY is reused automatically. +
+ +

Execution Tuning

+ + + + + + + + + + + + + + + + + + + + + + + + + +
VariableDescription
MAX_STEPSMax steps per goal (default: 30)
STEP_DELAYDelay between steps in ms (default: 500)
MAX_ELEMENTSMax DOM elements to parse (default: 40)
MAX_HISTORY_STEPSMax action history retained (default: 10)
+ +

MCP Connection

+ + + + + + + + + + + + + + + + + + + + + +
VariableDescription
MCP_TRANSPORTstdio or sse (default: stdio)
MCP_HOSTMCP server host (default: localhost)
MCP_PORTMCP server port (default: 8080)
+
+ + + + +
+

LambdaTest Cloud

+

+ Run AppClaw tests on real iOS and Android devices in the cloud — no local device or + emulator required. AppClaw integrates with + LambdaTest's real device cloud + via its Appium-compatible hub. +

+ +

Setup

+

Add your LambdaTest credentials and target device to .env:

+ +
+
+
+ .env
-
+
# Enable LambdaTest cloud
+CLOUD_PROVIDER=lambdatest
 
-          
-

Full Command Reference

-

Every supported step kind at a glance.

+# LambdaTest credentials (from app.lambdatest.com → Profile → Access Key) +LAMBDATEST_USERNAME=your_username +LAMBDATEST_ACCESS_KEY=your_access_key - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
KindParametersDescription
openAppqueryOpen an app by name
launchAppLaunch app defined in appId metadata
taplabelTap element by visible text/label
typetext, target?Type text, optionally into a named field
enterPress Enter / Return key
backPress the Back button
homePress the Home button
waitsecondsPause for a fixed duration
waitUntilcondition, text?, timeoutPoll until visible/gone/screenLoaded
swipedirection, repeat?Swipe up/down/left/right
dragfrom, toDrag from one element to another (vision mode)
asserttextVerify text or description is visible
scrollAsserttext, direction, maxScrollsScroll until text found
getInfoqueryAsk the AI a question about the screen
donemessage?Signal flow completion
-
+# Target device +LAMBDATEST_DEVICE_NAME=iPhone 14 +LAMBDATEST_OS_VERSION=16 +PLATFORM=ios - - - -
-

Vision Modes

-

Control how AppClaw locates elements on screen.

+# Your app (upload via LambdaTest portal, copy the lt:// ID) +LAMBDATEST_APP=lt://APP10xxxxxxxxxxxxxxxx -

Agent Mode AGENT_MODE

- - - - - - - - - - - - - - - - - -
ValueBehavior
domDefault. Uses the app's DOM/accessibility tree to find elements.
vision - Uses AI vision (screenshots + LLM) as the primary strategy for all interactions. -
+# LLM for AI-powered automation +LLM_PROVIDER=gemini +LLM_API_KEY=your_gemini_api_key
+ -

Vision Mode VISION_MODE

- - - - - - - - - - - - - - - - - - - - - -
ValueBehavior
fallbackDefault. Try DOM first, fall back to vision if no match found.
alwaysSkip DOM entirely, use vision for every interaction.
neverDOM only. No vision fallback.
- +

CLI Usage

+

Once .env is configured, run AppClaw exactly as you would locally:

+ +
+
+
+ Terminal +
+
# Run a natural-language goal on a cloud device
+appclaw "Open the app and navigate to the checkout screen"
+
+# Run a YAML flow on a cloud device
+appclaw --flow flows/checkout.yaml
+
+ +

SDK Usage

+

+ No SDK changes needed — when CLOUD_PROVIDER=lambdatest is set in + .env, the SDK automatically routes the session through LambdaTest: +

+ +
+
+
+ TypeScript +
+
import { AppClaw } from 'appclaw';
 
-          
-

Environment Variables

+// CLOUD_PROVIDER=lambdatest is read from .env automatically +const app = new AppClaw({ + provider: 'gemini', + apiKey: process.env.LLM_API_KEY, + reportName: 'Checkout — LambdaTest', +}); + +await app.run('open the app'); +await app.run('tap Add to Cart'); +await app.run('tap Checkout'); + +await app.teardown(); // report saved to .appclaw/runs/
+
+ +

Environment Variables

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableRequiredDescription
CLOUD_PROVIDERYesSet to lambdatest to enable cloud execution
LAMBDATEST_USERNAMEYesYour LambdaTest account username
LAMBDATEST_ACCESS_KEYYesYour LambdaTest access key (from Profile → Access Key)
LAMBDATEST_DEVICE_NAMEYesCloud device to use, e.g. iPhone 14, Galaxy S23
LAMBDATEST_OS_VERSIONYesOS version, e.g. 16 (iOS) or 13 (Android)
LAMBDATEST_APPNoApp ID from the LambdaTest portal (format: lt://APP…)
LAMBDATEST_BUILD_NAMENoBuild label shown in the LambdaTest dashboard
LAMBDATEST_PROJECT_NAMENoProject label shown in the LambdaTest dashboard
LAMBDATEST_VIDEONoRecord session video. Default: true
LAMBDATEST_NETWORKNoCapture network logs. Default: false
+ +
+
No code changes required
+

+ Switching between local and cloud execution is purely config — set + CLOUD_PROVIDER=lambdatest in your CI environment and remove it for local + runs. Your YAML flows and SDK tests stay identical. +

+
+ + + + + + +
+

Node.js / TypeScript SDK

+

+ AppClaw ships a first-class programmatic API so you can drive mobile automation directly + from Node.js or TypeScript — no CLI required. The SDK is the natural fit for QA + automation inside test runners (Vitest, Jest, Mocha), CI pipelines, and any script that + needs to control a device programmatically. +

+ +
+
When to use the SDK vs the CLI

- All environment variables recognized by AppClaw. These are especially useful for CI/CD - pipelines. + Use the CLI for one-off tasks and interactive exploration. Use the + SDK when you want to run flows inside a test suite, assert on + results, share a device connection across multiple flows, or integrate AppClaw into a + larger automation pipeline.

+
-

LLM Configuration

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
VariableDescription
LLM_PROVIDER - LLM provider: anthropic, openai, gemini, - groq, ollama -
LLM_API_KEYAPI key for the chosen provider
LLM_MODELSpecific model name to use
LLM_THINKINGExtended thinking: on or off (default: on)
LLM_THINKING_BUDGETMax thinking tokens: 1–10000 (default: 128)
LLM_SCREENSHOT_MAX_EDGE_PXDownscale screenshots to this max edge (0 = disabled)
+

Architecture

+

+ The SDK exposes a single AppClaw class that manages the full lifecycle: +

+
    +
  • + Lazy MCP connect — the Appium connection is opened on the first + runFlow() or runGoal() call, not on construction. +
  • +
  • + Connection reuse — subsequent calls share the same underlying + connection, so you pay the startup cost once per test suite. +
  • +
  • + Explicit teardown — call teardown() in your + afterAll hook to close the connection cleanly. +
  • +
  • + Silent by default — spinners and terminal colours are suppressed + automatically, keeping CI logs clean. +
  • +
+
-

Device & Platform

- - - - - - - - - - - - - - - - - - - - - - - - - -
VariableDescription
PLATFORMSame as --platform flag
DEVICE_TYPESame as --device-type flag
DEVICE_UDIDSame as --udid flag
DEVICE_NAMESame as --device flag
+
+

Installation

+

AppClaw is a single package — the SDK is built in, nothing extra to install.

-

Vision

- - - - - - - - - - - - - - - - - - - - - -
VariableDescription
VISION_MODEalways, fallback, or never
AGENT_MODEdom or vision
GEMINI_API_KEY - Gemini API key for Stark vision — only needed when LLM_PROVIDER is - not gemini and AGENT_MODE=vision. If provider is - already Gemini, LLM_API_KEY is reused automatically. -
+
+
+
+ Terminal +
+
npm install appclaw
+
-

Execution Tuning

- - - - - - - - - - - - - - - - - - - - - - - - - -
VariableDescription
MAX_STEPSMax steps per goal (default: 30)
STEP_DELAYDelay between steps in ms (default: 500)
MAX_ELEMENTSMax DOM elements to parse (default: 40)
MAX_HISTORY_STEPSMax action history retained (default: 10)
+

+ Create a .env file in your project root (or pass options directly to the + constructor — see Options Reference). +

-

MCP Connection

- - - - - - - - - - - - - - - - - - - - - -
VariableDescription
MCP_TRANSPORTstdio or sse (default: stdio)
MCP_HOSTMCP server host (default: localhost)
MCP_PORTMCP server port (default: 8080)
-
- - +
+
+
+ .env +
+
LLM_PROVIDER=anthropic
+LLM_API_KEY=sk-ant-...
+PLATFORM=android
+
+ + +
+

runFlow()

+

+ Parse and execute a YAML flow file against a connected device. Returns a + FlowResult you can assert on. +

+ +
+
+
+ TypeScript +
+
import { AppClaw } from 'appclaw';
+
+const app = new AppClaw({
+  provider: 'anthropic',
+  apiKey:   process.env.ANTHROPIC_API_KEY,
+  platform: 'android',
+});
+
+const result = await app.runFlow('./flows/checkout.yaml');
+
+console.log(result.success);    // true
+console.log(result.stepsUsed);  // 6
+console.log(result.stepsTotal); // 6
+
+await app.teardown();
+
+ +

FlowResult shape

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
successbooleanWhether all steps completed successfully
stepsUsednumberSteps executed before completion or failure
stepsTotalnumberTotal steps in the flow (including unexecuted)
failedStepnumber?1-based index of the step that failed
failedPhasestring?setup | test | assertion
errorstring?Human-readable failure reason
+
+ +
+

runGoal()

+

+ Execute a plain-English goal using the agent loop — same as passing a goal string to the + CLI. Returns an AgentResult. +

+ +
+
+
+ TypeScript +
+
import { AppClaw } from 'appclaw';
+
+const app = new AppClaw({ provider: 'anthropic', apiKey: process.env.ANTHROPIC_API_KEY });
+
+const result = await app.runGoal(
+  'Log in with email qa@company.com and password Test1234'
+);
+
+console.log(result.success);   // true
+console.log(result.stepsUsed); // 4
+console.log(result.reason);    // "Logged in successfully"
+
+await app.teardown();
+
+ +
+
Flow vs Goal
+

+ Use runFlow() for repeatable QA scenarios — structured, + deterministic, zero LLM cost. Use runGoal() for exploratory tasks or + when you need the agent to adapt to dynamic screen states. +

+
+
+ +
+

run()

+

+ Execute a single natural-language instruction directly on the device — the programmatic + equivalent of typing a command in the playground REPL. Each call is one atomic action: + parse the instruction, execute it, return the result. +

+ +
+
+
+ TypeScript +
+
import { AppClaw } from 'appclaw';
+
+const app = new AppClaw({ provider: 'gemini', apiKey: process.env.GEMINI_API_KEY, platform: 'android' });
+
+await app.run('open YouTube app');       // regex match — no LLM call
+await app.run('tap Search');             // regex match — no LLM call
+await app.run('type Appium 3.0');        // regex match — no LLM call
+await app.run('tap the search button');  // LLM fallback → tap
+await app.run('wait 2 seconds');         // regex match — no LLM call
+await app.run('scroll down');            // regex match — no LLM call
+
+await app.teardown();  // report written to .appclaw/runs/
+
+ +

How instructions are resolved

+
    +
  1. + Regex match — common patterns (open X, + tap X, type X, wait N seconds, + scroll down, …) are resolved instantly with zero LLM cost. +
  2. +
  3. + LLM fallback — anything that doesn't match a regex is sent to the + configured LLM, which classifies it into a structured action (tap, + type, swipe, etc.). +
  4. +
+ +

RunResult shape

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
successbooleanWhether the action completed successfully
actionstring + Resolved step kind: tap | type | openApp | + wait | swipe | … +
messagestringHuman-readable description of what happened
+ +
+
run() vs runGoal() vs runFlow()
+

+ Use run() when you want full control — one deterministic step at a + time, easy to integrate with any test framework. Use runGoal() when + you want the agent to figure out the steps itself. Use runFlow() for + declarative YAML test cases you want to version-control. +

+
+
+ +
+

Reports

+

+ Reports are enabled by default when using the SDK. After + teardown() is called, AppClaw writes an HTML report to + .appclaw/runs/ — one screenshot per step, plus a full execution summary. No + extra configuration needed. +

+ +
+
+
+ TypeScript +
+
const app = new AppClaw({
+  provider:   'gemini',
+  apiKey:     process.env.GEMINI_API_KEY,
+  platform:   'android',
+  reportName: 'YouTube Search',  // shown in the report viewer
+});
+
+await app.run('open YouTube app');
+await app.run('tap Search');
+await app.run('type Appium 3.0');
+await app.run('tap the search button');
+
+await app.teardown();
+// ↑ writes report to .appclaw/runs/<runId>/
+
+ +

Viewing the report

+

Run the built-in report server after your tests complete:

+ +
+
+
+ Shell +
+
npx appclaw --report
+
+ +

+ This starts a local server and opens the report in your browser. Every run is listed + with its steps, screenshots, pass/fail status, and timing. +

+ +

Report file layout

+
+
+
+ File tree +
+
.appclaw/
+  runs/
+    runs.json              # global run index
+    <runId>/
+      manifest.json        # full run data (steps, timing, success)
+      steps/
+        step-000.png       # screenshot after step 1
+        step-001.png       # screenshot after step 2
+        step-002.png
+
+ +

Disabling reports

+

+ Set report: false to skip report generation (e.g. in performance-sensitive + CI pipelines): +

+ +
+
+
+ TypeScript +
+
const app = new AppClaw({
+  provider: 'gemini',
+  apiKey:   process.env.GEMINI_API_KEY,
+  report:   false,   // disable report generation
+});
+
+
+ +
+

Using with Vitest / Jest

+

+ Create one AppClaw instance per test file, connect once in + beforeAll, and tear down in afterAll. Individual tests call + runFlow() or runGoal() and assert on the result. +

+ +
+
+
+ tests/checkout.test.ts +
+
import { describe, it, expect, afterAll } from 'vitest';
+import { AppClaw } from 'appclaw';
+
+const app = new AppClaw({
+  provider: 'anthropic',
+  apiKey:   process.env.ANTHROPIC_API_KEY,
+  platform: 'android',
+  maxSteps: 20,
+});
+
+afterAll(() => app.teardown());
+
+describe('Checkout flow', () => {
+  it('completes purchase as a logged-in user', async () => {
+    const result = await app.runFlow('./flows/checkout.yaml');
+    expect(result.success).toBe(true);
+  });
+
+  it('handles empty cart gracefully', async () => {
+    const result = await app.runFlow('./flows/checkout-empty-cart.yaml');
+    expect(result.success).toBe(true);
+  });
+
+  it('completes in under 15 steps', async () => {
+    const result = await app.runFlow('./flows/checkout.yaml');
+    expect(result.stepsUsed).toBeLessThan(15);
+  });
+});
+
+ +

Phased flows & assertion results

+

+ For flows that use setup / steps / + assertions sections, the failedPhase field tells you exactly + where execution broke down: +

+ +
+
+
+ TypeScript +
+
const result = await app.runFlow('./flows/login-phased.yaml');
+
+if (!result.success) {
+  // failedPhase: 'setup' | 'test' | 'assertion'
+  console.error(`Failed in ${result.failedPhase} phase`);
+  console.error(`Step ${result.failedStep}: ${result.error}`);
+}
+
+
+ +
+

CI Scripts

+

+ For CI pipelines that don't use a test framework, run flows sequentially and exit + non-zero on failure. The SDK's silent: true default keeps logs clean. +

+ +
+
+
+ scripts/smoke-test.ts +
+
import { AppClaw } from 'appclaw';
+
+const app = new AppClaw({
+  provider: 'google',
+  apiKey:   process.env.GEMINI_API_KEY,
+  platform: 'android',
+  silent:   true,  // no spinners in CI
+});
+
+const flows = [
+  './flows/login.yaml',
+  './flows/checkout.yaml',
+  './flows/search.yaml',
+];
+
+for (const flow of flows) {
+  const result = await app.runFlow(flow);
+
+  if (!result.success) {
+    console.error(`FAILED: ${flow} ${result.error}`);
+    await app.teardown();
+    process.exit(1);
+  }
+
+  console.log(`PASSED: ${flow} (${result.stepsUsed} steps)`);
+}
+
+await app.teardown();
+console.log('All flows passed.');
+
+ +

Run it with tsx (no compilation step needed):

+ +
+
+
+ Terminal +
+
npx tsx scripts/smoke-test.ts
+
+
+ +
+

Options Reference

+

+ All fields passed to new AppClaw(options). Every field is optional — unset + fields fall back to .env values or built-in defaults, matching CLI + behaviour exactly. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OptionTypeDefaultDescription
providerstring'gemini' + 'anthropic' | 'openai' | 'gemini' | + 'groq' | 'ollama' +
apiKeystringAPI key for the chosen LLM provider
modelstringProvider defaultModel ID override (e.g. 'claude-opus-4-6')
platformstring'android' | 'ios'
agentModestring'dom' + 'dom' uses accessibility tree; 'vision' uses AI vision +
maxStepsnumber30Maximum agent steps before giving up (applies to runGoal)
stepDelaynumber500Delay between steps in milliseconds
silentbooleantrue + Suppress spinners and terminal colour output. Set false to debug + locally. +
reportbooleantrue + Auto-generate an HTML report to .appclaw/runs/ on + teardown(). Set false to disable. +
reportNamestring'AppClaw SDK Run'Name shown in the report viewer.
mcpTransportstring'stdio' + 'stdio' (local appium-mcp) | 'sse' (remote server) +
mcpHoststring'localhost'appium-mcp host when transport is 'sse'
mcpPortnumber8080appium-mcp port when transport is 'sse'
+ +

TypeScript types

+

All public types are exported from the top-level 'appclaw' import:

+ +
+
+
+ TypeScript +
+
import {
+  AppClaw,
+  type AppClawOptions,   // constructor options
+  type FlowResult,       // returned by runFlow()
+  type RunResult,        // returned by run()
+  type AgentResult,      // returned by runGoal()
+  type RunYamlFlowOptions // second arg to runFlow()
+} from 'appclaw';
+
+
+ @@ -1876,52 +2971,185 @@

Community

diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index d2d0776..0000000 --- a/package-lock.json +++ /dev/null @@ -1,5108 +0,0 @@ -{ - "name": "appclaw", - "version": "0.1.5", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "appclaw", - "version": "0.1.5", - "dependencies": { - "@ai-sdk/anthropic": "^1.0.0", - "@ai-sdk/google": "^3.0.43", - "@ai-sdk/openai": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.22.0", - "ai": "^6.0.72", - "ai-sdk-ollama": "3.8.3", - "boxen": "^8.0.1", - "chalk": "^5.6.2", - "cli-spinners": "^3.4.0", - "cli-table3": "^0.6.5", - "df-vision": "1.1.75", - "dotenv": "^17.3.1", - "fast-xml-parser": "^4.5.0", - "gradient-string": "^3.0.0", - "marked": "^15.0.12", - "marked-terminal": "^7.3.0", - "mjpeg-consumer": "2.0.0", - "sharp": "^0.33.5", - "yaml": "^2.8.3", - "zod": "^3.23.0" - }, - "bin": { - "appclaw": "bin/appclaw.js" - }, - "devDependencies": { - "@types/bun": "^1.1.0", - "@types/gradient-string": "^1.1.6", - "prettier": "^3.8.1", - "tsx": "^4.21.0", - "typescript": "^5.6.0", - "vitest": "^4.1.2" - } - }, - "../df-vision": { - "extraneous": true - }, - "node_modules/@ai-sdk/anthropic": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-1.2.12.tgz", - "integrity": "sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "1.1.3", - "@ai-sdk/provider-utils": "2.2.8" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.0.0" - } - }, - "node_modules/@ai-sdk/gateway": { - "version": "3.0.95", - "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.95.tgz", - "integrity": "sha512-ZmUNNbZl3V42xwQzPaNUi+s8eqR2lnrxf0bvB6YbLXpLjHYv0k2Y78t12cNOfY0bxGeuVVTLyk856uLuQIuXEQ==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "3.0.8", - "@ai-sdk/provider-utils": "4.0.23", - "@vercel/oidc": "3.1.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, - "node_modules/@ai-sdk/gateway/node_modules/@ai-sdk/provider": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.8.tgz", - "integrity": "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==", - "license": "Apache-2.0", - "dependencies": { - "json-schema": "^0.4.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@ai-sdk/gateway/node_modules/@ai-sdk/provider-utils": { - "version": "4.0.23", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.23.tgz", - "integrity": "sha512-z8GlDaCmRSDlqkMF2f4/RFgWxdarvIbyuk+m6WXT1LYgsnGiXRJGTD2Z1+SDl3LqtFuRtGX1aghYvQLoHL/9pg==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "3.0.8", - "@standard-schema/spec": "^1.1.0", - "eventsource-parser": "^3.0.6" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, - "node_modules/@ai-sdk/google": { - "version": "3.0.53", - "resolved": "https://registry.npmjs.org/@ai-sdk/google/-/google-3.0.53.tgz", - "integrity": "sha512-uz8tIlkDgQJG9Js2Wh9JHzd4kI9+hYJqf9XXJLx60vyN5mRIqhr49iwR5zGP5Gl8odp2PeR3Gh2k+5bh3Z1HHw==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "3.0.8", - "@ai-sdk/provider-utils": "4.0.21" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, - "node_modules/@ai-sdk/google/node_modules/@ai-sdk/provider": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.8.tgz", - "integrity": "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==", - "license": "Apache-2.0", - "dependencies": { - "json-schema": "^0.4.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@ai-sdk/google/node_modules/@ai-sdk/provider-utils": { - "version": "4.0.21", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.21.tgz", - "integrity": "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "3.0.8", - "@standard-schema/spec": "^1.1.0", - "eventsource-parser": "^3.0.6" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, - "node_modules/@ai-sdk/openai": { - "version": "1.3.24", - "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-1.3.24.tgz", - "integrity": "sha512-GYXnGJTHRTZc4gJMSmFRgEQudjqd4PUN0ZjQhPwOAYH1yOAvQoG/Ikqs+HyISRbLPCrhbZnPKCNHuRU4OfpW0Q==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "1.1.3", - "@ai-sdk/provider-utils": "2.2.8" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.0.0" - } - }, - "node_modules/@ai-sdk/provider": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.3.tgz", - "integrity": "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==", - "license": "Apache-2.0", - "dependencies": { - "json-schema": "^0.4.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@ai-sdk/provider-utils": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-2.2.8.tgz", - "integrity": "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "1.1.3", - "nanoid": "^3.3.8", - "secure-json-parse": "^2.7.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.23.8" - } - }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/@emnapi/core": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", - "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", - "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", - "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", - "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", - "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", - "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", - "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", - "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", - "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", - "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", - "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", - "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", - "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", - "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", - "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", - "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", - "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", - "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", - "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", - "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", - "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", - "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", - "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", - "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", - "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", - "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", - "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", - "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@google/genai": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.49.0.tgz", - "integrity": "sha512-hO69Zl0H3x+L0KL4stl1pLYgnqnwHoLqtKy6MRlNnW8TAxjqMdOUVafomKd4z1BePkzoxJWbYILny9a2Zk43VQ==", - "license": "Apache-2.0", - "dependencies": { - "google-auth-library": "^10.3.0", - "p-retry": "^4.6.2", - "protobufjs": "^7.5.4", - "ws": "^8.18.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.25.2" - }, - "peerDependenciesMeta": { - "@modelcontextprotocol/sdk": { - "optional": true - } - } - }, - "node_modules/@hono/node-server": { - "version": "1.19.11", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", - "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", - "license": "MIT", - "engines": { - "node": ">=18.14.1" - }, - "peerDependencies": { - "hono": "^4" - } - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", - "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", - "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", - "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", - "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", - "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", - "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.5" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", - "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", - "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", - "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", - "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", - "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.2.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", - "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.28.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.28.0.tgz", - "integrity": "sha512-gmloF+i+flI8ouQK7MWW4mOwuMh4RePBuPFAEPC6+pdqyWOUMDOixb6qZ69owLJpz6XmyllCouc4t8YWO+E2Nw==", - "license": "MIT", - "dependencies": { - "@hono/node-server": "^1.19.9", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.2.1", - "express-rate-limit": "^8.2.1", - "hono": "^4.11.4", - "jose": "^6.1.3", - "json-schema-typed": "^8.0.2", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@cfworker/json-schema": "^4.1.1", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "@cfworker/json-schema": { - "optional": true - }, - "zod": { - "optional": false - } - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", - "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, - "node_modules/@opentelemetry/api": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", - "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", - "license": "Apache-2.0", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@oxc-project/types": { - "version": "0.122.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", - "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", - "license": "BSD-3-Clause" - }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", - "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", - "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", - "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", - "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", - "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", - "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^1.1.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", - "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", - "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", - "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "license": "MIT" - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/bun": { - "version": "1.3.11", - "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.3.11.tgz", - "integrity": "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "bun-types": "1.3.11" - } - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/gradient-string": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@types/gradient-string/-/gradient-string-1.1.6.tgz", - "integrity": "sha512-LkaYxluY4G5wR1M4AKQUal2q61Di1yVVCw42ImFTuaIoQVgmV0WP1xUaLB8zwb47mp82vWTpePI9JmrjEnJ7nQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/tinycolor2": "*" - } - }, - "node_modules/@types/node": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", - "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", - "license": "MIT", - "dependencies": { - "undici-types": "~7.18.0" - } - }, - "node_modules/@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", - "license": "MIT" - }, - "node_modules/@types/tinycolor2": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.6.tgz", - "integrity": "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==", - "license": "MIT" - }, - "node_modules/@vercel/oidc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.1.0.tgz", - "integrity": "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==", - "license": "Apache-2.0", - "engines": { - "node": ">= 20" - } - }, - "node_modules/@vitest/expect": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", - "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.2", - "@vitest/utils": "4.1.2", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz", - "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "4.1.2", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz", - "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz", - "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "4.1.2", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz", - "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.2", - "@vitest/utils": "4.1.2", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz", - "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz", - "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.2", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/ai": { - "version": "6.0.158", - "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.158.tgz", - "integrity": "sha512-gLTp1UXFtMqKUi3XHs33K7UFglbvojkxF/aq337TxnLGOhHIW9+GyP2jwW4hYX87f1es+wId3VQoPRRu9zEStQ==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/gateway": "3.0.95", - "@ai-sdk/provider": "3.0.8", - "@ai-sdk/provider-utils": "4.0.23", - "@opentelemetry/api": "1.9.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, - "node_modules/ai-sdk-ollama": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/ai-sdk-ollama/-/ai-sdk-ollama-3.8.3.tgz", - "integrity": "sha512-KId/S++eb0CgTPFTtHzCGCrO73kXZLK+hyyZx5k8LVqU2XOEHYKVbIwDiQ+hm3okHjnsGehn4zR4QNm14SUM3Q==", - "license": "MIT", - "dependencies": { - "@ai-sdk/provider": "^3.0.8", - "@ai-sdk/provider-utils": "^4.0.23", - "jsonrepair": "^3.13.3", - "ollama": "^0.6.3" - }, - "engines": { - "node": ">=22" - }, - "peerDependencies": { - "ai": "^6.0.154" - } - }, - "node_modules/ai-sdk-ollama/node_modules/@ai-sdk/provider": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.8.tgz", - "integrity": "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==", - "license": "Apache-2.0", - "dependencies": { - "json-schema": "^0.4.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/ai-sdk-ollama/node_modules/@ai-sdk/provider-utils": { - "version": "4.0.23", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.23.tgz", - "integrity": "sha512-z8GlDaCmRSDlqkMF2f4/RFgWxdarvIbyuk+m6WXT1LYgsnGiXRJGTD2Z1+SDl3LqtFuRtGX1aghYvQLoHL/9pg==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "3.0.8", - "@standard-schema/spec": "^1.1.0", - "eventsource-parser": "^3.0.6" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, - "node_modules/ai/node_modules/@ai-sdk/provider": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.8.tgz", - "integrity": "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==", - "license": "Apache-2.0", - "dependencies": { - "json-schema": "^0.4.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/ai/node_modules/@ai-sdk/provider-utils": { - "version": "4.0.23", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.23.tgz", - "integrity": "sha512-z8GlDaCmRSDlqkMF2f4/RFgWxdarvIbyuk+m6WXT1LYgsnGiXRJGTD2Z1+SDl3LqtFuRtGX1aghYvQLoHL/9pg==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "3.0.8", - "@standard-schema/spec": "^1.1.0", - "eventsource-parser": "^3.0.6" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, - "node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ansi-align": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", - "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", - "license": "ISC", - "dependencies": { - "string-width": "^4.1.0" - } - }, - "node_modules/ansi-align/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-align/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/ansi-align/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-align/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-escapes": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", - "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", - "license": "MIT", - "dependencies": { - "environment": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "license": "MIT" - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/async-retry": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", - "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", - "license": "MIT", - "dependencies": { - "retry": "0.13.1" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/bignumber.js": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", - "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/boxen": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz", - "integrity": "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==", - "license": "MIT", - "dependencies": { - "ansi-align": "^3.0.1", - "camelcase": "^8.0.0", - "chalk": "^5.3.0", - "cli-boxes": "^3.0.0", - "string-width": "^7.2.0", - "type-fest": "^4.21.0", - "widest-line": "^5.0.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" - }, - "node_modules/bun-types": { - "version": "1.3.11", - "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.11.tgz", - "integrity": "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/camelcase": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", - "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/cli-boxes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", - "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-highlight": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", - "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", - "license": "ISC", - "dependencies": { - "chalk": "^4.0.0", - "highlight.js": "^10.7.1", - "mz": "^2.4.0", - "parse5": "^5.1.1", - "parse5-htmlparser2-tree-adapter": "^6.0.0", - "yargs": "^16.0.0" - }, - "bin": { - "highlight": "bin/highlight" - }, - "engines": { - "node": ">=8.0.0", - "npm": ">=5.0.0" - } - }, - "node_modules/cli-highlight/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/cli-highlight/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/cli-spinners": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", - "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", - "license": "MIT", - "engines": { - "node": ">=18.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-table3": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", - "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", - "license": "MIT", - "dependencies": { - "string-width": "^4.2.0" - }, - "engines": { - "node": "10.* || >= 12.*" - }, - "optionalDependencies": { - "@colors/colors": "1.5.0" - } - }, - "node_modules/cli-table3/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-table3/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/cli-table3/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-table3/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/df-vision": { - "version": "1.1.75", - "resolved": "https://registry.npmjs.org/df-vision/-/df-vision-1.1.75.tgz", - "integrity": "sha512-CPUpNIz4eb+qNY22dy5Lfkl9fwrKl0vUwDiAQobxhDuHCY1wEtd4I/UQJt1f1b1/yzhfZff/oRn0pc9wkWd7GA==", - "license": "MIT", - "dependencies": { - "@google/genai": "^1.44.0", - "async-retry": "^1.3.3" - } - }, - "node_modules/dotenv": { - "version": "17.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", - "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "license": "MIT" - }, - "node_modules/emojilib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", - "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/environment": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", - "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.4", - "@esbuild/android-arm": "0.27.4", - "@esbuild/android-arm64": "0.27.4", - "@esbuild/android-x64": "0.27.4", - "@esbuild/darwin-arm64": "0.27.4", - "@esbuild/darwin-x64": "0.27.4", - "@esbuild/freebsd-arm64": "0.27.4", - "@esbuild/freebsd-x64": "0.27.4", - "@esbuild/linux-arm": "0.27.4", - "@esbuild/linux-arm64": "0.27.4", - "@esbuild/linux-ia32": "0.27.4", - "@esbuild/linux-loong64": "0.27.4", - "@esbuild/linux-mips64el": "0.27.4", - "@esbuild/linux-ppc64": "0.27.4", - "@esbuild/linux-riscv64": "0.27.4", - "@esbuild/linux-s390x": "0.27.4", - "@esbuild/linux-x64": "0.27.4", - "@esbuild/netbsd-arm64": "0.27.4", - "@esbuild/netbsd-x64": "0.27.4", - "@esbuild/openbsd-arm64": "0.27.4", - "@esbuild/openbsd-x64": "0.27.4", - "@esbuild/openharmony-arm64": "0.27.4", - "@esbuild/sunos-x64": "0.27.4", - "@esbuild/win32-arm64": "0.27.4", - "@esbuild/win32-ia32": "0.27.4", - "@esbuild/win32-x64": "0.27.4" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", - "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz", - "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==", - "license": "MIT", - "dependencies": { - "ip-address": "10.1.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fast-xml-parser": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.5.tgz", - "integrity": "sha512-cK9c5I/DwIOI7/Q7AlGN3DuTdwN61gwSfL8rvuVPK+0mcCNHHGxRrpiFtaZZRfRMJL3Gl8B2AFlBG6qXf03w9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "strnum": "^1.0.5" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gaxios": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", - "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/gcp-metadata": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", - "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^7.0.0", - "google-logging-utils": "^1.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-east-asian-width": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", - "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-tsconfig": { - "version": "4.13.7", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", - "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/google-auth-library": { - "version": "10.6.2", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", - "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", - "license": "Apache-2.0", - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^7.1.4", - "gcp-metadata": "8.1.2", - "google-logging-utils": "1.1.3", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/google-logging-utils": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", - "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gradient-string": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/gradient-string/-/gradient-string-3.0.0.tgz", - "integrity": "sha512-frdKI4Qi8Ihp4C6wZNB565de/THpIaw3DjP5ku87M+N9rNSGmPTjfkq61SdRXB7eCaL8O1hkKDvf6CDMtOzIAg==", - "license": "MIT", - "dependencies": { - "chalk": "^5.3.0", - "tinygradient": "^1.1.5" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/highlight.js": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", - "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, - "node_modules/hono": { - "version": "4.12.9", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.9.tgz", - "integrity": "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==", - "license": "MIT", - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-arrayish": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", - "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", - "license": "MIT" - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/jose": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", - "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/json-bigint": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", - "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", - "license": "MIT", - "dependencies": { - "bignumber.js": "^9.0.0" - } - }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", - "license": "(AFL-2.1 OR BSD-3-Clause)" - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "license": "BSD-2-Clause" - }, - "node_modules/jsonrepair": { - "version": "3.13.3", - "resolved": "https://registry.npmjs.org/jsonrepair/-/jsonrepair-3.13.3.tgz", - "integrity": "sha512-BTznj0owIt2CBAH/LTo7+1I5pMvl1e1033LRl/HUowlZmJOIhzC0zbX5bxMngLkfT4WnzPP26QnW5wMr2g9tsQ==", - "license": "ISC", - "bin": { - "jsonrepair": "bin/cli.js" - } - }, - "node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", - "license": "MIT", - "dependencies": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "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" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0" - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/marked": { - "version": "15.0.12", - "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", - "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/marked-terminal": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-7.3.0.tgz", - "integrity": "sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw==", - "license": "MIT", - "dependencies": { - "ansi-escapes": "^7.0.0", - "ansi-regex": "^6.1.0", - "chalk": "^5.4.1", - "cli-highlight": "^2.1.11", - "cli-table3": "^0.6.5", - "node-emoji": "^2.2.0", - "supports-hyperlinks": "^3.1.0" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "marked": ">=1 <16" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/mjpeg-consumer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mjpeg-consumer/-/mjpeg-consumer-2.0.0.tgz", - "integrity": "sha512-nyVOIMOQ7JEXwFPUkcbDgekBJGXSK/53nqhPnKZen3a4CAOM4HxqzPZQxDo/8NPhoueuXlvKN3DeDLV2OSi1fw==", - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-emoji": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", - "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^4.6.0", - "char-regex": "^1.0.2", - "emojilib": "^2.4.0", - "skin-tone": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT" - }, - "node_modules/ollama": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/ollama/-/ollama-0.6.3.tgz", - "integrity": "sha512-KEWEhIqE5wtfzEIZbDCLH51VFZ6Z3ZSa6sIOg/E/tBV8S51flyqBOXi+bRxlOYKDf8i327zG9eSTb8IJxvm3Zg==", - "license": "MIT", - "dependencies": { - "whatwg-fetch": "^3.6.20" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/p-retry": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", - "license": "MIT", - "dependencies": { - "@types/retry": "0.12.0", - "retry": "^0.13.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/parse5": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", - "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", - "license": "MIT" - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", - "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", - "license": "MIT", - "dependencies": { - "parse5": "^6.0.1" - } - }, - "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "license": "MIT" - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-to-regexp": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", - "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pkce-challenge": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", - "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", - "license": "MIT", - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prettier": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", - "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/protobufjs": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", - "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/rolldown": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", - "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.122.0", - "@rolldown/pluginutils": "1.0.0-rc.12" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.12", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", - "@rolldown/binding-darwin-x64": "1.0.0-rc.12", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/secure-json-parse": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", - "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", - "license": "BSD-3-Clause" - }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/sharp": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", - "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.6.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.5", - "@img/sharp-darwin-x64": "0.33.5", - "@img/sharp-libvips-darwin-arm64": "1.0.4", - "@img/sharp-libvips-darwin-x64": "1.0.4", - "@img/sharp-libvips-linux-arm": "1.0.5", - "@img/sharp-libvips-linux-arm64": "1.0.4", - "@img/sharp-libvips-linux-s390x": "1.0.4", - "@img/sharp-libvips-linux-x64": "1.0.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", - "@img/sharp-libvips-linuxmusl-x64": "1.0.4", - "@img/sharp-linux-arm": "0.33.5", - "@img/sharp-linux-arm64": "0.33.5", - "@img/sharp-linux-s390x": "0.33.5", - "@img/sharp-linux-x64": "0.33.5", - "@img/sharp-linuxmusl-arm64": "0.33.5", - "@img/sharp-linuxmusl-x64": "0.33.5", - "@img/sharp-wasm32": "0.33.5", - "@img/sharp-win32-ia32": "0.33.5", - "@img/sharp-win32-x64": "0.33.5" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, - "node_modules/simple-swizzle": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", - "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/skin-tone": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", - "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", - "license": "MIT", - "dependencies": { - "unicode-emoji-modifier-base": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/std-env": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", - "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strnum": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", - "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-hyperlinks": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", - "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" - }, - "engines": { - "node": ">=14.18" - }, - "funding": { - "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinycolor2": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", - "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", - "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinygradient": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/tinygradient/-/tinygradient-1.1.5.tgz", - "integrity": "sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==", - "license": "MIT", - "dependencies": { - "@types/tinycolor2": "^1.4.0", - "tinycolor2": "^1.0.0" - } - }, - "node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true - }, - "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "license": "MIT" - }, - "node_modules/unicode-emoji-modifier-base": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", - "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vite": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", - "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.8", - "rolldown": "1.0.0-rc.12", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.0", - "esbuild": "^0.27.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 - } - } - }, - "node_modules/vitest": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz", - "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "4.1.2", - "@vitest/mocker": "4.1.2", - "@vitest/pretty-format": "4.1.2", - "@vitest/runner": "4.1.2", - "@vitest/snapshot": "4.1.2", - "@vitest/spy": "4.1.2", - "@vitest/utils": "4.1.2", - "es-module-lexer": "^2.0.0", - "expect-type": "^1.3.0", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^4.0.0-rc.1", - "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.1.0", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "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.2", - "@vitest/browser-preview": "4.1.2", - "@vitest/browser-webdriverio": "4.1.2", - "@vitest/ui": "4.1.2", - "happy-dom": "*", - "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "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/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "vite": { - "optional": false - } - } - }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/whatwg-fetch": { - "version": "3.6.20", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", - "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", - "license": "MIT" - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/widest-line": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", - "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", - "license": "MIT", - "dependencies": { - "string-width": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", - "license": "MIT", - "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 - } - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "license": "MIT", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.2", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", - "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.25.28 || ^4" - } - } - } -} diff --git a/package.json b/package.json index 7f736c9..babc86b 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,19 @@ { "name": "appclaw", - "version": "0.1.5", + "version": "0.1.6", "description": "Agentic AI layer for mobile automation via appium-mcp", "type": "module", - "main": "dist/index.js", + "main": "dist/sdk/index.js", + "types": "dist/sdk/index.d.ts", + "exports": { + ".": { + "import": "./dist/sdk/index.js", + "types": "./dist/sdk/index.d.ts" + }, + "./cli": { + "import": "./dist/index.js" + } + }, "bin": { "appclaw": "bin/appclaw.js" }, @@ -20,8 +30,9 @@ "lint": "npm run typecheck", "format": "prettier --write .", "format:check": "prettier --check .", - "test": "vitest run", - "test:watch": "vitest", + "test": "vitest run tests/flow tests/sdk", + "test:e2e": "vitest run tests/e2e/", + "test:watch": "vitest tests/flow tests/sdk", "build:vsix": "cd vscode-extension && npm run build && npx vsce package", "deploy:landing": "npm run deploy --prefix landing" }, @@ -36,7 +47,7 @@ "chalk": "^5.6.2", "cli-spinners": "^3.4.0", "cli-table3": "^0.6.5", - "df-vision": "1.1.75", + "df-vision": "1.1.76", "dotenv": "^17.3.1", "fast-xml-parser": "^4.5.0", "gradient-string": "^3.0.0", diff --git a/src/agent/loop.ts b/src/agent/loop.ts index 8236a3f..bb68ea1 100644 --- a/src/agent/loop.ts +++ b/src/agent/loop.ts @@ -127,6 +127,7 @@ export async function runAgent(options: AgentOptions): Promise { const history: StepRecord[] = []; let totalInputTokens = 0; let totalOutputTokens = 0; + let totalCachedTokens = 0; let prevDom = ''; let lastResult = ''; let detectedPlatform: 'android' | 'ios' = 'android'; @@ -259,7 +260,13 @@ export async function runAgent(options: AgentOptions): Promise { const cost = (totalInputTokens / 1_000_000) * pricing[0] + (totalOutputTokens / 1_000_000) * pricing[1]; - ui.printTokenSummary(totalInputTokens, totalOutputTokens, cost, modelName); + ui.printTokenSummary( + totalInputTokens, + totalOutputTokens, + cost, + modelName, + totalCachedTokens + ); return { success: true, reason: evaluation.reason, @@ -465,7 +472,12 @@ export async function runAgent(options: AgentOptions): Promise { if (decision.usage) { totalInputTokens += decision.usage.inputTokens; totalOutputTokens += decision.usage.outputTokens; - ui.printStepTokens(decision.usage.inputTokens, decision.usage.outputTokens); + totalCachedTokens += decision.usage.cachedTokens ?? 0; + ui.printStepTokens( + decision.usage.inputTokens, + decision.usage.outputTokens, + decision.usage.cachedTokens + ); } // ─── 4c. TRACK TRIED SELECTORS ───────────────────── @@ -516,6 +528,7 @@ export async function runAgent(options: AgentOptions): Promise { if (verifyDecision.usage) { totalInputTokens += verifyDecision.usage.inputTokens; totalOutputTokens += verifyDecision.usage.outputTokens; + totalCachedTokens += verifyDecision.usage.cachedTokens ?? 0; } if (verifyDecision.toolName !== 'done') { @@ -552,7 +565,7 @@ export async function runAgent(options: AgentOptions): Promise { const pricing = MODEL_PRICING[modelName] ?? [0, 0]; const cost = (totalInputTokens / 1_000_000) * pricing[0] + (totalOutputTokens / 1_000_000) * pricing[1]; - ui.printTokenSummary(totalInputTokens, totalOutputTokens, cost, modelName); + ui.printTokenSummary(totalInputTokens, totalOutputTokens, cost, modelName, totalCachedTokens); return { success: true, reason, diff --git a/src/config.ts b/src/config.ts index e781d46..151c823 100644 --- a/src/config.ts +++ b/src/config.ts @@ -143,8 +143,9 @@ const envSchema = z.object({ export type AppClawConfig = z.infer; -export function loadConfig(): AppClawConfig { - const config = envSchema.parse(process.env); +export function loadConfig(overrides?: Record): AppClawConfig { + const env = overrides ? { ...process.env, ...overrides } : process.env; + const config = envSchema.parse(env); if (config.CLOUD_PROVIDER === 'lambdatest') { if (!config.LAMBDATEST_USERNAME || !config.LAMBDATEST_ACCESS_KEY) { throw new Error( diff --git a/src/constants.ts b/src/constants.ts index 4942f30..6ef0e8d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -30,8 +30,12 @@ export const MODEL_PRICING: Record = { 'gemini-1.5-flash': [0.075, 0.3], 'gemini-1.5-pro': [1.25, 5.0], 'gemini-2.5-pro-preview-05-06': [1.25, 10.0], - 'gemini-3.1-flash-lite-preview': [0.075, 0.3], - 'gemini-3-flash-preview': [0.1, 0.4], + 'gemini-3-flash-preview': [0.5, 3.0], + 'gemini-3-pro-image-preview': [2.0, 12.0], + 'gemini-3.1-flash-lite-preview': [0.25, 1.5], + 'gemini-3.1-flash-image-preview': [0.5, 3.0], + 'gemini-3.1-flash-live-preview': [0.75, 4.5], + 'gemini-3.1-pro-preview': [2.0, 12.0], // OpenAI 'gpt-4o': [2.5, 10.0], 'gpt-4o-mini': [0.15, 0.6], diff --git a/src/device/ios-setup.ts b/src/device/ios-setup.ts index d8865f0..2118d7c 100644 --- a/src/device/ios-setup.ts +++ b/src/device/ios-setup.ts @@ -13,110 +13,69 @@ import * as ui from '../ui/terminal.js'; /** * Full simulator setup: boot + WDA download + WDA install. - * Each step is idempotent (skips if already done). + * Uses the prepare_ios_simulator tool which handles all three steps in one call. */ export async function setupSimulator(mcp: MCPClient, udid: string): Promise { - // Step 1: Boot simulator - ui.startSpinner('Booting simulator...'); + ui.startSpinner('Preparing iOS simulator...'); + let result: any; try { - const bootResult = await mcp.callTool('boot_simulator', { udid }); - const bootText = extractText(bootResult); - - if (bootText.toLowerCase().includes('error') || bootText.toLowerCase().includes('failed')) { - throw new Error(bootText); - } - - ui.stopSpinner(); - if ( - bootText.toLowerCase().includes('already booted') || - bootText.toLowerCase().includes('already running') - ) { - ui.printSetupOk('Simulator already booted'); - } else { - ui.printSetupOk('Simulator booted'); + const mcpResult = await mcp.callTool('prepare_ios_simulator', { udid }); + const text = extractText(mcpResult); + try { + result = JSON.parse(text); + } catch { + result = null; } } catch (err: any) { ui.stopSpinner(); const msg = err instanceof Error ? err.message : String(err); - // "Already booted" is not an error - if ( - msg.toLowerCase().includes('already booted') || - msg.toLowerCase().includes('already running') - ) { - ui.printSetupOk('Simulator already booted'); - } else { - ui.printSetupError( - `Failed to boot simulator: ${msg}`, - 'Try closing other simulators or run: xcrun simctl boot ' - ); - throw err; - } + ui.printSetupError(`Failed to prepare simulator: ${msg}`, 'Run: xcrun simctl boot '); + throw err; } + ui.stopSpinner(); - // Step 2: Download/setup WDA (cached in ~/.cache/appium-mcp/wda/) - ui.startSpinner('Setting up WebDriverAgent...'); - try { - const wdaResult = await mcp.callTool('setup_wda', { platform: 'ios' }); - const wdaText = extractText(wdaResult); + if (!result) { + ui.printSetupOk('iOS simulator prepared'); + return; + } - if (wdaText.toLowerCase().includes('error') || wdaText.toLowerCase().includes('failed')) { - throw new Error(wdaText); - } + // Boot step + if (result.boot?.status === 'failed') { + ui.printSetupError( + `Failed to boot simulator: ${result.boot.detail}`, + 'Run: xcrun simctl boot ' + ); + throw new Error(result.boot.detail); + } else if (result.boot?.status === 'skipped') { + ui.printSetupOk(`Simulator already booted`); + } else if (result.boot?.status === 'completed') { + ui.printSetupOk('Simulator booted'); + } - ui.stopSpinner(); - if (wdaText.toLowerCase().includes('cached') || wdaText.toLowerCase().includes('already')) { - ui.printSetupOk('WebDriverAgent ready (cached)'); - } else { - ui.printSetupOk('WebDriverAgent downloaded'); - } - } catch (err: any) { - ui.stopSpinner(); - const msg = err instanceof Error ? err.message : String(err); + // WDA download step + if (result.wda_download?.status === 'failed') { ui.printSetupError( - `Failed to setup WebDriverAgent: ${msg}`, + `Failed to download WDA: ${result.wda_download.detail}`, 'Check network connection. WDA is downloaded from GitHub releases.' ); - throw err; + throw new Error(result.wda_download.detail); + } else if (result.wda_download?.status === 'completed') { + ui.printSetupOk('WebDriverAgent downloaded'); + } else if (result.wda_download?.status === 'skipped') { + ui.printSetupOk('WebDriverAgent ready (cached)'); } - // Step 3: Install WDA on the booted simulator - ui.startSpinner('Installing WebDriverAgent on simulator...'); - try { - const installResult = await mcp.callTool('install_wda', { simulatorUdid: udid }); - const installText = extractText(installResult); - - if ( - installText.toLowerCase().includes('error') || - installText.toLowerCase().includes('failed') - ) { - throw new Error(installText); - } - - ui.stopSpinner(); - if ( - installText.toLowerCase().includes('already installed') || - installText.toLowerCase().includes('already running') - ) { - ui.printSetupOk('WebDriverAgent already installed'); - } else { - ui.printSetupOk('WebDriverAgent installed on simulator'); - } - } catch (err: any) { - ui.stopSpinner(); - const msg = err instanceof Error ? err.message : String(err); - // "Already installed" is acceptable - if ( - msg.toLowerCase().includes('already installed') || - msg.toLowerCase().includes('already running') - ) { - ui.printSetupOk('WebDriverAgent already installed'); - } else { - ui.printSetupError( - `Failed to install WebDriverAgent: ${msg}`, - 'Try resetting the simulator: xcrun simctl erase ' - ); - throw err; - } + // WDA install step + if (result.wda_install?.status === 'failed') { + ui.printSetupError( + `Failed to install WDA: ${result.wda_install.detail}`, + 'Try resetting the simulator: xcrun simctl erase ' + ); + throw new Error(result.wda_install.detail); + } else if (result.wda_install?.status === 'completed') { + ui.printSetupOk('WebDriverAgent installed on simulator'); + } else if (result.wda_install?.status === 'skipped') { + ui.printSetupOk('WebDriverAgent already installed'); } } diff --git a/src/flow/llm-parser.ts b/src/flow/llm-parser.ts index ef043cc..9d438d8 100644 --- a/src/flow/llm-parser.ts +++ b/src/flow/llm-parser.ts @@ -72,13 +72,18 @@ const SYSTEM_PROMPT = `- "done" → done\n` + `Extract the relevant parameters. Works with any language.`; +export interface ResolvedStep { + step: FlowStep; + usage: { inputTokens: number; outputTokens: number; totalTokens: number }; +} + /** * Resolve a free-form natural language instruction into a concrete FlowStep via LLM. */ -export async function resolveNaturalStep(instruction: string): Promise { +export async function resolveNaturalStep(instruction: string): Promise { const model = buildModel(Config); - const { object } = await generateObject({ + const { object, usage } = await generateObject({ model: model as any, schema: stepSchema, system: SYSTEM_PROMPT, @@ -89,5 +94,12 @@ export async function resolveNaturalStep(instruction: string): Promise }, }); - return { ...object, verbatim: instruction } as FlowStep; + return { + step: { ...object, verbatim: instruction } as FlowStep, + usage: { + inputTokens: usage.inputTokens ?? 0, + outputTokens: usage.outputTokens ?? 0, + totalTokens: (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0), + }, + }; } diff --git a/src/flow/parse-yaml-flow.ts b/src/flow/parse-yaml-flow.ts index 568f19d..9b202af 100644 --- a/src/flow/parse-yaml-flow.ts +++ b/src/flow/parse-yaml-flow.ts @@ -227,7 +227,7 @@ async function parseRawSteps(rawSteps: unknown[], strict?: boolean): Promise 0) { + const vtPricing = MODEL_PRICING[getStarkVisionModel()] ?? [0, 0]; + const vtCost = + (vt.inputTokens / 1_000_000) * vtPricing[0] + (vt.outputTokens / 1_000_000) * vtPricing[1]; + ui.printStepTokens( + vt.inputTokens, + vt.outputTokens, + vt.cachedTokens || undefined, + vtCost, + 'vision' + ); + } onFlowStep?.( globalN, globalTotal, @@ -1347,8 +1363,22 @@ export async function runYamlFlow( } options.onFlowStep?.(n, total, step.kind, stepLabel(step), 'running'); + resetVisionTokens(); const result = await executeStep(mcp, step, meta, appResolver, tapPoll, options.deviceUdid); ui.printFlowStep(n, total, label, result.success); + const vt = getVisionTokens(); + if (vt.totalTokens > 0) { + const vtPricing = MODEL_PRICING[getStarkVisionModel()] ?? [0, 0]; + const vtCost = + (vt.inputTokens / 1_000_000) * vtPricing[0] + (vt.outputTokens / 1_000_000) * vtPricing[1]; + ui.printStepTokens( + vt.inputTokens, + vt.outputTokens, + vt.cachedTokens || undefined, + vtCost, + 'vision' + ); + } options.onFlowStep?.( n, total, diff --git a/src/flow/vision-execute.ts b/src/flow/vision-execute.ts index 913a1d9..64c7b91 100644 --- a/src/flow/vision-execute.ts +++ b/src/flow/vision-execute.ts @@ -15,6 +15,7 @@ */ import starkVision from 'df-vision'; +import { trackVisionTokenUsage } from '../vision/vision-token-tracker.js'; import type { MCPClient } from '../mcp/types.js'; import type { FlowStep } from './types.js'; import type { ActionResult } from '../llm/schemas.js'; @@ -440,6 +441,7 @@ export async function visionExecute( disableThinking: true, ...(baseUrl && { baseUrl }), ...(baseUrl && { coordinateOrder }), + onTokenUsage: trackVisionTokenUsage, }); const t0 = performance.now(); const response = await client.getElementInfo(imageBase64, pre.getInfoQuery, true); @@ -479,6 +481,7 @@ export async function visionExecute( disableThinking: true, ...(baseUrl && { baseUrl }), ...(baseUrl && { coordinateOrder }), + onTokenUsage: trackVisionTokenUsage, }); const visQuery = pre.assertQuery; const t0 = performance.now(); @@ -528,6 +531,7 @@ export async function visionExecute( model: getStarkVisionModel(), disableThinking: true, ...(baseUrl && { baseUrl }), + onTokenUsage: trackVisionTokenUsage, }); const t0 = performance.now(); diff --git a/src/llm/extract-usage.ts b/src/llm/extract-usage.ts index d840276..f1d0f6c 100644 --- a/src/llm/extract-usage.ts +++ b/src/llm/extract-usage.ts @@ -63,6 +63,23 @@ function fromProviderMetadata(meta: unknown): { input: number; output: number } return null; } +/** Extract cached token count from Gemini/Vertex provider metadata. */ +export function extractCachedTokensFromMetadata(providerMetadata: unknown): number { + if (!providerMetadata || typeof providerMetadata !== 'object') return 0; + const root = providerMetadata as Record; + for (const key of ['google', 'vertex']) { + const block = root[key]; + if (block && typeof block === 'object') { + const u = (block as Record).usageMetadata; + if (u && typeof u === 'object') { + const cached = (u as Record).cachedContentTokenCount; + if (typeof cached === 'number' && cached > 0) return cached; + } + } + } + return 0; +} + /** * Pick input/output token counts from a generateText result. */ diff --git a/src/llm/provider.ts b/src/llm/provider.ts index ae898a0..c20dcfe 100644 --- a/src/llm/provider.ts +++ b/src/llm/provider.ts @@ -29,7 +29,10 @@ import { VISION_MODE_EXCLUDED_TOOLS, } from '../mcp/tool-converter.js'; import { prepareScreenshotForLlm } from '../vision/prepare-screenshot-for-llm.js'; -import { extractUsageFromGenerateTextResult } from './extract-usage.js'; +import { + extractUsageFromGenerateTextResult, + extractCachedTokensFromMetadata, +} from './extract-usage.js'; import { buildSystemPrompt, buildUserMessage } from './prompts.js'; export interface AgentContext { @@ -57,6 +60,8 @@ export interface TokenUsage { inputTokens: number; outputTokens: number; totalTokens: number; + /** Tokens served from Gemini implicit cache (reduces billed input cost by ~75%). */ + cachedTokens?: number; } /** What the LLM decided to do — a tool call with name and arguments */ @@ -332,6 +337,11 @@ export function createLLMProvider(config: AppClawConfig, mcpTools: MCPToolInfo[] const metaTools = buildMetaTools(config.AGENT_MODE); const allTools = { ...dynamicTools, ...metaTools }; + // ─── System prompt cache ──────────────────────────────── + // Built lazily on first getDecision() call (platform not known at init time), + // then reused for every subsequent step — platform/mode never change mid-run. + let cachedSystemPrompt: string | undefined; + // ─── Action history for context injection ────────────── // Instead of multi-turn messages (which break across providers), // we inject a compact action history into each user prompt. @@ -347,13 +357,16 @@ export function createLLMProvider(config: AppClawConfig, mcpTools: MCPToolInfo[] context: AgentContext, callbacks?: StreamCallbacks ): Promise { - // Build prompt and prepare screenshot in parallel - const systemPrompt = buildSystemPrompt( - context.platform, - isVisionLocateEnabledFromConfig(config), - config.AGENT_MODE, - Object.keys(allTools).length - ); + // Build system prompt once and cache it — platform/mode never change mid-run. + if (!cachedSystemPrompt) { + cachedSystemPrompt = buildSystemPrompt( + context.platform, + isVisionLocateEnabledFromConfig(config), + config.AGENT_MODE, + Object.keys(allTools).length + ); + } + const systemPrompt = cachedSystemPrompt; let userMessage = buildUserMessage(context); if (actionHistory.length > 0) { @@ -432,10 +445,12 @@ export function createLLMProvider(config: AppClawConfig, mcpTools: MCPToolInfo[] providerMetadata: providerMeta, response: { body: (response as any)?.body }, }); + const cachedTokens = extractCachedTokensFromMetadata(providerMeta); const usage: TokenUsage = { inputTokens: extracted.inputTokens, outputTokens: extracted.outputTokens, totalTokens: extracted.totalTokens, + cachedTokens: cachedTokens || undefined, }; const toolCall = toolCalls?.[0]; @@ -472,10 +487,12 @@ export function createLLMProvider(config: AppClawConfig, mcpTools: MCPToolInfo[] // Prefer totalUsage + raw Gemini usageMetadata — some models omit fields the SDK maps to 0 const extracted = extractUsageFromGenerateTextResult(result); + const cachedTokens = extractCachedTokensFromMetadata(result.providerMetadata); const usage: TokenUsage = { inputTokens: extracted.inputTokens, outputTokens: extracted.outputTokens, totalTokens: extracted.totalTokens, + cachedTokens: cachedTokens || undefined, }; // Extract the first tool call diff --git a/src/mcp/tool-converter.ts b/src/mcp/tool-converter.ts index 6e7b845..8672966 100644 --- a/src/mcp/tool-converter.ts +++ b/src/mcp/tool-converter.ts @@ -45,9 +45,7 @@ export const EXCLUDED_MCP_TOOLS = new Set([ 'selectSession', 'select_platform', 'select_device', - 'setup_wda', - 'install_wda', - 'boot_simulator', + 'prepare_ios_simulator', // AI code-gen tools — not relevant to device control 'appium_generate_tests', 'appium_generate_locators', diff --git a/src/playground/index.ts b/src/playground/index.ts index 804c33e..899351d 100644 --- a/src/playground/index.ts +++ b/src/playground/index.ts @@ -21,6 +21,9 @@ import { AppResolver } from '../agent/app-resolver.js'; import { tryParseNaturalFlowLine } from '../flow/natural-line.js'; import { resolveNaturalStep } from '../flow/llm-parser.js'; import { visionExecute } from '../flow/vision-execute.js'; +import { resetVisionTokens, getVisionTokens } from '../vision/vision-token-tracker.js'; +import { MODEL_PRICING, DEFAULT_MODELS } from '../constants.js'; +import { getStarkVisionModel } from '../vision/locate-enabled.js'; import type { FlowStep, FlowMeta } from '../flow/types.js'; import type { MCPClient } from '../mcp/types.js'; import { @@ -54,6 +57,22 @@ const state: PlaygroundState = { appResolver: null, }; +// ─── Cost helpers ─────────────────────────────────────── + +function calcCost(inputTokens: number, outputTokens: number, modelName: string): number { + const pricing = MODEL_PRICING[modelName] ?? [0, 0]; + return (inputTokens / 1_000_000) * pricing[0] + (outputTokens / 1_000_000) * pricing[1]; +} + +function visionCost(inputTokens: number, outputTokens: number): number { + return calcCost(inputTokens, outputTokens, getStarkVisionModel()); +} + +function llmCost(inputTokens: number, outputTokens: number): number { + const modelName = Config.LLM_MODEL || DEFAULT_MODELS[Config.LLM_PROVIDER] || ''; + return calcCost(inputTokens, outputTokens, modelName); +} + // ─── Formatting helpers ───────────────────────────────── /** Short action word for a step kind */ @@ -1001,7 +1020,8 @@ export async function runPlaygroundJson(deviceArgs?: PlaygroundDeviceArgs): Prom // Two-call fallback: classify → execute let parsed: FlowStep; try { - parsed = await resolveNaturalStep(line); + const resolved = await resolveNaturalStep(line); + parsed = resolved.step; } catch (err: any) { emitJson({ event: 'step', @@ -1285,6 +1305,7 @@ async function processLine(line: string): Promise { if (state.mcp && Config.AGENT_MODE === 'vision') { try { ui.startSpinner('Executing', line); + resetVisionTokens(); const vResult = await visionExecute(state.mcp, line, undefined, undefined, { minMatchScore: MIN_MATCH_SCORE, }); @@ -1298,6 +1319,15 @@ async function processLine(line: string): Promise { : ans; console.log(); printPanel({ title: 'Answer', content: ansBody }); + const vt = getVisionTokens(); + if (vt.totalTokens > 0) + ui.printStepTokens( + vt.inputTokens, + vt.outputTokens, + vt.cachedTokens || undefined, + visionCost(vt.inputTokens, vt.outputTokens), + 'vision' + ); console.log(); return; } @@ -1314,6 +1344,15 @@ async function processLine(line: string): Promise { printStepFail(stepNum, vResult.step, execResult.message); console.log(` ${theme.dim('Step not recorded. Fix and try again.')}`); } + const vt = getVisionTokens(); + if (vt.totalTokens > 0) + ui.printStepTokens( + vt.inputTokens, + vt.outputTokens, + vt.cachedTokens || undefined, + visionCost(vt.inputTokens, vt.outputTokens), + 'vision' + ); return; } @@ -1332,6 +1371,15 @@ async function processLine(line: string): Promise { } console.log(` ${theme.dim('Step not recorded. Fix and try again.')}`); } + const vt = getVisionTokens(); + if (vt.totalTokens > 0) + ui.printStepTokens( + vt.inputTokens, + vt.outputTokens, + vt.cachedTokens || undefined, + visionCost(vt.inputTokens, vt.outputTokens), + 'vision' + ); return; } } catch (err: any) { @@ -1342,9 +1390,12 @@ async function processLine(line: string): Promise { // ── Two-call fallback: classify via LLM → execute via step runner ── let parsed: FlowStep; + let classifyUsage: { inputTokens: number; outputTokens: number; totalTokens: number } | undefined; try { ui.startSpinner('Classifying', line); - parsed = await resolveNaturalStep(line); + const resolved = await resolveNaturalStep(line); + parsed = resolved.step; + classifyUsage = resolved.usage; ui.stopSpinner(); } catch (err: any) { ui.stopSpinner(); @@ -1359,6 +1410,14 @@ async function processLine(line: string): Promise { if (parsed.kind === 'getInfo') { await handleGetInfo(parsed.query); + if (classifyUsage && classifyUsage.totalTokens > 0) + ui.printStepTokens( + classifyUsage.inputTokens, + classifyUsage.outputTokens, + undefined, + llmCost(classifyUsage.inputTokens, classifyUsage.outputTokens), + 'classify' + ); return; } @@ -1367,12 +1426,21 @@ async function processLine(line: string): Promise { if (parsed.kind === 'done') { state.steps.push(parsed); printStepSuccess(stepNum, parsed, 'recorded'); + if (classifyUsage && classifyUsage.totalTokens > 0) + ui.printStepTokens( + classifyUsage.inputTokens, + classifyUsage.outputTokens, + undefined, + llmCost(classifyUsage.inputTokens, classifyUsage.outputTokens), + 'classify' + ); return; } // Execute on device ui.startSpinner(`[${stepNum}] ${parsed.kind}`, spinnerDetail(parsed)); + resetVisionTokens(); try { const result = await runStepOnDevice(parsed); ui.stopSpinner(); @@ -1384,6 +1452,23 @@ async function processLine(line: string): Promise { printStepFail(stepNum, parsed, result.message); console.log(` ${theme.dim('Step not recorded. Fix and try again.')}`); } + if (classifyUsage && classifyUsage.totalTokens > 0) + ui.printStepTokens( + classifyUsage.inputTokens, + classifyUsage.outputTokens, + undefined, + llmCost(classifyUsage.inputTokens, classifyUsage.outputTokens), + 'classify' + ); + const vt = getVisionTokens(); + if (vt.totalTokens > 0) + ui.printStepTokens( + vt.inputTokens, + vt.outputTokens, + vt.cachedTokens || undefined, + visionCost(vt.inputTokens, vt.outputTokens), + 'vision' + ); } catch (err: any) { ui.stopSpinner(); printStepFail(stepNum, parsed, err?.message ?? String(err)); diff --git a/src/sdk/config-builder.ts b/src/sdk/config-builder.ts new file mode 100644 index 0000000..274ab15 --- /dev/null +++ b/src/sdk/config-builder.ts @@ -0,0 +1,44 @@ +/** + * Config builder — maps AppClawOptions to AppClawConfig. + * + * Single Responsibility: translates the SDK's public option surface + * into the internal env-var-keyed config object without touching process.env. + */ + +import { loadConfig, type AppClawConfig } from '../config.js'; +import type { AppClawOptions } from './types.js'; + +/** Mapping from AppClawOptions keys to their env-var equivalents. */ +const OPTION_TO_ENV_VAR: Partial> = { + provider: 'LLM_PROVIDER', + apiKey: 'LLM_API_KEY', + model: 'LLM_MODEL', + platform: 'PLATFORM', + agentMode: 'AGENT_MODE', + maxSteps: 'MAX_STEPS', + stepDelay: 'STEP_DELAY', + mcpTransport: 'MCP_TRANSPORT', + mcpHost: 'MCP_HOST', + mcpPort: 'MCP_PORT', + // `silent` is SDK-only — no env-var equivalent. +}; + +/** + * Build an AppClawConfig from the SDK options object. + * + * Explicitly-set options take priority over env vars. + * Unset options fall through to process.env (same as CLI behaviour). + */ +export function buildConfig(options: AppClawOptions): AppClawConfig { + const overrides: Record = {}; + + for (const [key, envVar] of Object.entries(OPTION_TO_ENV_VAR)) { + if (!envVar) continue; + const val = options[key as keyof AppClawOptions]; + if (val !== undefined) { + overrides[envVar] = String(val); + } + } + + return loadConfig(overrides); +} diff --git a/src/sdk/flow-runner.ts b/src/sdk/flow-runner.ts new file mode 100644 index 0000000..0de05d5 --- /dev/null +++ b/src/sdk/flow-runner.ts @@ -0,0 +1,44 @@ +/** + * Flow runner — executes a YAML flow file against a live device. + * + * Single Responsibility: parse the flow file and delegate execution + * to the existing runYamlFlow engine. + * + * Open/Closed: extend via RunYamlFlowOptions callbacks (onFlowStep, + * artifactCollector) without modifying this class. + */ + +import { parseFlowYamlFile } from '../flow/parse-yaml-flow.js'; +import { runYamlFlow, type RunYamlFlowOptions } from '../flow/run-yaml-flow.js'; +import type { MCPClient } from '../mcp/types.js'; +import type { FlowResult } from './types.js'; + +export class FlowRunner { + private readonly mcp: MCPClient; + + constructor(mcp: MCPClient) { + this.mcp = mcp; + } + + /** + * Parse and execute a YAML flow file. + * + * @param flowPath - Absolute or relative path to the .yaml flow file. + * @param options - Optional overrides forwarded to the flow engine + * (step delay, callbacks, artifact collection, etc.). + */ + async run(flowPath: string, options: RunYamlFlowOptions = {}): Promise { + const { meta, steps, phases } = await parseFlowYamlFile(flowPath); + + const result = await runYamlFlow(this.mcp, meta, steps, options, phases); + + return { + success: result.success, + stepsUsed: result.stepsExecuted, + stepsTotal: result.stepsTotal, + failedStep: result.failedAt, + failedPhase: result.failedPhase, + error: result.reason, + }; + } +} diff --git a/src/sdk/goal-runner.ts b/src/sdk/goal-runner.ts new file mode 100644 index 0000000..0ffe424 --- /dev/null +++ b/src/sdk/goal-runner.ts @@ -0,0 +1,45 @@ +/** + * Goal runner — executes a natural-language goal against a live device. + * + * Single Responsibility: wire together the LLM provider and agent loop + * for a single goal execution. + * + * Depends on MCPClient and MCPToolInfo interfaces (not concretions), + * satisfying the Dependency Inversion Principle. + */ + +import { runAgent, type AgentResult } from '../agent/loop.js'; +import { createLLMProvider } from '../llm/provider.js'; +import type { MCPClient, MCPToolInfo } from '../mcp/types.js'; +import type { AppClawConfig } from '../config.js'; + +export class GoalRunner { + private readonly mcp: MCPClient; + private readonly tools: MCPToolInfo[]; + private readonly config: AppClawConfig; + + constructor(mcp: MCPClient, tools: MCPToolInfo[], config: AppClawConfig) { + this.mcp = mcp; + this.tools = tools; + this.config = config; + } + + /** + * Execute a natural-language goal. + * + * A fresh LLM provider is created per call so history does not leak + * across independent goal executions. + */ + async run(goal: string): Promise { + const llm = createLLMProvider(this.config, this.tools); + + return runAgent({ + goal, + mcp: this.mcp, + llm, + maxSteps: this.config.MAX_STEPS, + stepDelay: this.config.STEP_DELAY, + visionMode: this.config.VISION_MODE, + }); + } +} diff --git a/src/sdk/index.ts b/src/sdk/index.ts new file mode 100644 index 0000000..0db4357 --- /dev/null +++ b/src/sdk/index.ts @@ -0,0 +1,128 @@ +/** + * AppClaw SDK — public entry point. + * + * Usage: + * import { AppClaw } from 'appclaw' + * + * const app = new AppClaw({ provider: 'anthropic', apiKey: process.env.KEY }) + * await app.run('open YouTube app') + * await app.run('tap Search') + * await app.teardown() // report written to .appclaw/runs/ + */ + +import { buildConfig } from './config-builder.js'; +import { McpSession } from './mcp-session.js'; +import { FlowRunner } from './flow-runner.js'; +import { GoalRunner } from './goal-runner.js'; +import { StepRunner } from './step-runner.js'; +import { RunArtifactCollector } from '../report/writer.js'; +import { silenceTerminalUI } from '../ui/terminal.js'; +import type { RunYamlFlowOptions, RunYamlFlowResult } from '../flow/run-yaml-flow.js'; +import type { AppClawOptions, FlowResult, RunResult } from './types.js'; +import type { AgentResult } from '../agent/loop.js'; + +export class AppClaw { + private readonly session: McpSession; + private readonly config: ReturnType; + + // ── Report state ─────────────────────────────────────────── + private readonly collector: RunArtifactCollector | null; + private runStepCounter = 0; + private runSuccess = true; + private runFailedAt: number | undefined; + private runFailureReason: string | undefined; + + constructor(options: AppClawOptions = {}) { + this.config = buildConfig(options); + this.session = new McpSession(this.config); + + // Silent by default in SDK mode — no spinners or ANSI colours in CI logs. + if (options.silent !== false) { + silenceTerminalUI(); + } + + // Report enabled by default — written to .appclaw/runs/ on teardown. + this.collector = + options.report !== false + ? new RunArtifactCollector( + 'sdk-run', + { name: options.reportName ?? 'AppClaw SDK Run' }, + (options.platform ?? 'android') as 'android' | 'ios' + ) + : null; + } + + /** + * Execute a single natural-language instruction on the device. + * + * Equivalent to the playground's per-command execution: the instruction is + * interpreted (regex → LLM fallback) and executed immediately as one step. + * Each call is captured as a step in the auto-generated report. + * + * @param instruction - e.g. "open YouTube app", "tap Search", "type Appium 3.0" + */ + async run(instruction: string): Promise { + const { client } = await this.session.connect(); + const stepIndex = ++this.runStepCounter; + const runner = new StepRunner(client, this.collector ?? undefined, stepIndex); + const result = await runner.run(instruction); + + // Track first failure for report finalization + if (!result.success && this.runSuccess) { + this.runSuccess = false; + this.runFailedAt = stepIndex; + this.runFailureReason = result.message; + } + + return result; + } + + /** + * Parse and execute a YAML flow file. + * + * The MCP connection is established on the first call and reused for all + * subsequent calls on this instance. + * + * @param flowPath - Path to the .yaml flow file (absolute or relative to cwd). + * @param options - Optional flow engine overrides (step delay, callbacks, etc.). + */ + async runFlow(flowPath: string, options: RunYamlFlowOptions = {}): Promise { + const { client } = await this.session.connect(); + const runner = new FlowRunner(client); + return runner.run(flowPath, options); + } + + /** + * Execute a natural-language goal. + * + * @param goal - Plain English description of what to accomplish on the device. + */ + async runGoal(goal: string): Promise { + const { client, tools } = await this.session.connect(); + const runner = new GoalRunner(client, tools, this.config); + return runner.run(goal); + } + + /** + * Close the MCP connection and release all resources. + * Writes the report to .appclaw/runs/ if report is enabled (default). + * Call this in afterAll() / test teardown hooks. + */ + async teardown(): Promise { + if (this.collector && this.runStepCounter > 0) { + const flowResult: RunYamlFlowResult = { + success: this.runSuccess, + stepsExecuted: this.runStepCounter, + stepsTotal: this.runStepCounter, + failedAt: this.runFailedAt, + reason: this.runFailureReason, + }; + await this.collector.finalize(process.cwd(), flowResult); + } + await this.session.release(); + } +} + +// ── Public type exports ───────────────────────────────────────────────────── +export type { AppClawOptions, FlowResult, RunResult, AgentResult } from './types.js'; +export type { RunYamlFlowOptions } from '../flow/run-yaml-flow.js'; diff --git a/src/sdk/mcp-session.ts b/src/sdk/mcp-session.ts new file mode 100644 index 0000000..ccc1983 --- /dev/null +++ b/src/sdk/mcp-session.ts @@ -0,0 +1,64 @@ +/** + * MCP session manager. + * + * Single Responsibility: own the lifecycle of the MCP client connection. + * Lazily connects on first use, reuses across multiple run calls, + * and releases cleanly on teardown. + * + * Depends on the MCPClient and SharedMCPClient interfaces (not concretions), + * satisfying the Dependency Inversion Principle. + */ + +import { acquireSharedMCPClient } from '../mcp/client.js'; +import { createPlatformSession } from '../device/session.js'; +import type { MCPClient, MCPToolInfo, SharedMCPClient } from '../mcp/types.js'; +import type { AppClawConfig } from '../config.js'; +import type { Platform } from '../index.js'; + +export interface ConnectedSession { + client: MCPClient; + tools: MCPToolInfo[]; +} + +export class McpSession { + private readonly config: AppClawConfig; + private handle: SharedMCPClient | null = null; + private scopedClient: MCPClient | null = null; + private cachedTools: MCPToolInfo[] = []; + + constructor(config: AppClawConfig) { + this.config = config; + } + + /** + * Return the active MCP client and its tool list. + * Connects on first call; subsequent calls reuse the existing connection. + */ + async connect(): Promise { + if (!this.handle) { + this.handle = await acquireSharedMCPClient({ + transport: this.config.MCP_TRANSPORT, + host: this.config.MCP_HOST, + port: this.config.MCP_PORT, + }); + const platform = (this.config.PLATFORM || 'android') as Platform; + const { scopedMcp } = await createPlatformSession(this.handle, this.config, platform); + this.scopedClient = scopedMcp; + this.cachedTools = await this.handle.listTools(); + } + return { client: this.scopedClient!, tools: this.cachedTools }; + } + + /** + * Release the MCP connection. + * The underlying appium-mcp process is closed when the last handle is released. + */ + async release(): Promise { + if (this.handle) { + await this.handle.release(); + this.handle = null; + this.scopedClient = null; + this.cachedTools = []; + } + } +} diff --git a/src/sdk/step-runner.ts b/src/sdk/step-runner.ts new file mode 100644 index 0000000..901a509 --- /dev/null +++ b/src/sdk/step-runner.ts @@ -0,0 +1,68 @@ +/** + * Step runner — executes a single natural-language instruction on device. + * + * Mirrors the playground's per-command execution path: + * instruction → tryParseNaturalFlowLine (regex, no LLM) → resolveNaturalStep (LLM fallback) → executeStep + * + * Single Responsibility: translate one natural-language string into a device action. + */ + +import type { MCPClient } from '../mcp/types.js'; +import type { RunArtifactCollector } from '../report/writer.js'; +import { screenshot } from '../mcp/tools.js'; +import { tryParseNaturalFlowLine } from '../flow/natural-line.js'; +import { resolveNaturalStep } from '../flow/llm-parser.js'; +import { executeStep } from '../flow/run-yaml-flow.js'; +import type { RunResult } from './types.js'; + +const DEFAULT_TAP_POLL = { maxAttempts: 3, intervalMs: 300 }; + +export class StepRunner { + constructor( + private readonly mcp: MCPClient, + private readonly collector?: RunArtifactCollector, + private readonly stepIndex?: number + ) {} + + async run(instruction: string): Promise { + // 1. Try regex-based parsing first — no LLM cost for common patterns + let step = tryParseNaturalFlowLine(instruction); + + // 2. Fall back to LLM interpretation for anything the regex can't handle + if (!step) { + const resolved = await resolveNaturalStep(instruction); + step = resolved.step; + } + + // 3. Mark step start for duration tracking + if (this.collector && this.stepIndex !== undefined) { + this.collector.startStep(this.stepIndex); + } + + // 4. Execute on device + const result = await executeStep(this.mcp, step, {}, undefined, DEFAULT_TAP_POLL); + + // 5. Record step + screenshot in report + if (this.collector && this.stepIndex !== undefined) { + this.collector.addStep({ + index: this.stepIndex, + kind: step.kind, + verbatim: instruction, + phase: 'test', + status: result.success ? 'passed' : 'failed', + message: result.message, + error: result.success ? undefined : result.message, + }); + const screenshotB64 = await screenshot(this.mcp).catch(() => null); + if (screenshotB64) { + this.collector.attachScreenshot(this.stepIndex, screenshotB64); + } + } + + return { + success: result.success, + action: step.kind, + message: result.message, + }; + } +} diff --git a/src/sdk/types.ts b/src/sdk/types.ts new file mode 100644 index 0000000..1002da5 --- /dev/null +++ b/src/sdk/types.ts @@ -0,0 +1,78 @@ +/** + * Public-facing types for the AppClaw SDK. + * + * All interfaces that consumers of `appclaw` import live here. + * Internal implementation types stay in their respective modules. + */ + +export type LLMProvider = 'anthropic' | 'openai' | 'gemini' | 'groq' | 'ollama'; +export type AgentMode = 'dom' | 'vision'; +export type MCPTransport = 'stdio' | 'sse'; +export type Platform = 'android' | 'ios'; + +/** + * Options accepted by the AppClaw constructor. + * All fields are optional — unset fields fall back to environment variables + * or AppClaw defaults, matching CLI behaviour. + */ +export interface AppClawOptions { + /** LLM provider to use. Default: 'gemini'. */ + provider?: LLMProvider; + /** API key for the chosen provider. */ + apiKey?: string; + /** Model ID override (e.g. 'claude-opus-4-6'). Defaults to the provider's recommended model. */ + model?: string; + /** Target mobile platform. */ + platform?: Platform; + /** Interaction strategy: DOM locators (default) or AI vision. */ + agentMode?: AgentMode; + /** Maximum number of agent steps before giving up. Default: 30. */ + maxSteps?: number; + /** Delay between steps in milliseconds. Default: 500. */ + stepDelay?: number; + /** + * Suppress all terminal output (spinners, colours, progress). + * Defaults to true in SDK mode — set false to re-enable for debugging. + */ + silent?: boolean; + /** + * Automatically generate an HTML report to .appclaw/runs/ on teardown. + * Defaults to true — set false to disable. + */ + report?: boolean; + /** Name shown in the report viewer. Default: 'AppClaw SDK Run'. */ + reportName?: string; + /** How to connect to appium-mcp. Default: 'stdio'. */ + mcpTransport?: MCPTransport; + /** appium-mcp host when transport is 'sse'. Default: 'localhost'. */ + mcpHost?: string; + /** appium-mcp port when transport is 'sse'. Default: 8080. */ + mcpPort?: number; +} + +/** Result returned by AppClaw.runFlow() */ +export interface FlowResult { + success: boolean; + /** Number of flow steps executed. */ + stepsUsed: number; + /** Total steps in the flow (including unexecuted ones). */ + stepsTotal: number; + /** 1-based index of the step that failed (if any). */ + failedStep?: number; + /** Which phase failed ('setup' | 'test' | 'assertion'), for phased flows. */ + failedPhase?: string; + /** Human-readable failure reason. */ + error?: string; +} + +/** Result returned by AppClaw.run() — a single natural-language instruction executed on device. */ +export interface RunResult { + success: boolean; + /** The resolved step kind (tap, type, openApp, wait, …). */ + action: string; + /** Human-readable description of what happened. */ + message: string; +} + +// Re-export core agent result so consumers get a single import surface. +export type { AgentResult } from '../agent/loop.js'; diff --git a/src/ui/terminal.ts b/src/ui/terminal.ts index c9caadc..c15cff8 100644 --- a/src/ui/terminal.ts +++ b/src/ui/terminal.ts @@ -799,11 +799,20 @@ export function printTimeout(): void { // ─── Token usage ───────────────────────────────────────── -export function printStepTokens(inputTokens: number, outputTokens: number): void { +export function printStepTokens( + inputTokens: number, + outputTokens: number, + cachedTokens?: number, + cost?: number, + label?: string +): void { if (!Config.SHOW_TOKEN_USAGE) return; const total = inputTokens + outputTokens; + const cachedStr = cachedTokens && cachedTokens > 0 ? ` cached: ${cachedTokens}` : ''; + const costStr = cost != null && cost > 0 ? ` ${chalk.green(`$${cost.toFixed(5)}`)}` : ''; + const labelStr = label ? `${chalk.dim(label.padEnd(9))} ` : ''; console.log( - ` ${' '.repeat(8)}${theme.dim(`⟠ ${total} tokens (in: ${inputTokens} out: ${outputTokens})`)}` + ` ${' '.repeat(8)}${theme.dim(`⟠ `)}${labelStr}${theme.dim(`${total} tokens (in: ${inputTokens} out: ${outputTokens}${cachedStr})`)}${costStr}` ); } @@ -811,15 +820,22 @@ export function printTokenSummary( totalInput: number, totalOutput: number, cost: number, - modelName: string + modelName: string, + totalCached?: number ): void { if (!Config.SHOW_TOKEN_USAGE) return; const total = totalInput + totalOutput; - const content = [ + const lines = [ `${chalk.hex('#9CA3AF')('Tokens')} ${chalk.white.bold(total.toLocaleString())} ${chalk.dim(`(in: ${totalInput.toLocaleString()} out: ${totalOutput.toLocaleString()})`)}`, `${chalk.hex('#9CA3AF')('Cost')} ${chalk.green.bold(`$${cost.toFixed(4)}`)}`, `${chalk.hex('#9CA3AF')('Model')} ${chalk.dim(modelName)}`, - ].join('\n'); + ]; + if (totalCached && totalCached > 0) { + lines.push( + `${chalk.hex('#9CA3AF')('Cached')} ${chalk.cyan.bold(totalCached.toLocaleString())} ${chalk.dim('tokens from implicit cache (~75% off)')}` + ); + } + const content = lines.join('\n'); console.log(); printBox(content, { padding: { left: 2, right: 2, top: 0, bottom: 0 }, diff --git a/src/vision/stark-locate.ts b/src/vision/stark-locate.ts index a17f87b..ca0a5d8 100644 --- a/src/vision/stark-locate.ts +++ b/src/vision/stark-locate.ts @@ -26,6 +26,7 @@ import { getStarkVisionCoordinateOrder, getStarkVisionModel, } from './locate-enabled.js'; +import { trackVisionTokenUsage } from './vision-token-tracker.js'; import { getScreenSizeForStark } from './window-size.js'; /** Max edge for screenshots sent to Stark/Gemini — coordinates are normalized so resolution doesn't matter. */ @@ -132,6 +133,11 @@ export async function starkLocateTapTarget( // Use raw screenshot for coordinate scaling (needs true device pixels), compressed for Gemini const screenSize = await getScreenSizeForStark(mcp, rawScreenshot); const imageBase64 = await downscaleForVision(rawScreenshot); + if (process.env.MCP_DEBUG === '1' || process.env.MCP_DEBUG === 'true') { + const rawKB = Math.round(rawScreenshot.length / 1024); + const newKB = Math.round(imageBase64.length / 1024); + console.log(` [stark] screenshot ${rawKB}KB → ${newKB}KB`); + } const client = new StarkVisionClient({ apiKey: apiKey || 'local', @@ -139,6 +145,7 @@ export async function starkLocateTapTarget( disableThinking: true, ...(baseUrl && { baseUrl }), ...(baseUrl && { coordinateOrder }), + onTokenUsage: trackVisionTokenUsage, }); const locateT0 = performance.now(); diff --git a/src/vision/vision-token-tracker.ts b/src/vision/vision-token-tracker.ts new file mode 100644 index 0000000..83c42dd --- /dev/null +++ b/src/vision/vision-token-tracker.ts @@ -0,0 +1,41 @@ +/** + * Module-level accumulator for Stark vision token usage. + * + * All StarkVisionClient instances (stark-locate, vision-execute) report here + * via the onTokenUsage callback. Call resetVisionTokens() at the start of each + * flow/run, then getVisionTokens() to read the totals. + */ + +export interface VisionTokenTotals { + inputTokens: number; + outputTokens: number; + totalTokens: number; + cachedTokens: number; +} + +let _totals: VisionTokenTotals = { + inputTokens: 0, + outputTokens: 0, + totalTokens: 0, + cachedTokens: 0, +}; + +export function resetVisionTokens(): void { + _totals = { inputTokens: 0, outputTokens: 0, totalTokens: 0, cachedTokens: 0 }; +} + +export function getVisionTokens(): VisionTokenTotals { + return { ..._totals }; +} + +export function trackVisionTokenUsage(usage: { + inputTokens: number; + outputTokens: number; + totalTokens: number; + cachedTokens?: number; +}): void { + _totals.inputTokens += usage.inputTokens; + _totals.outputTokens += usage.outputTokens; + _totals.totalTokens += usage.totalTokens; + _totals.cachedTokens += usage.cachedTokens ?? 0; +} diff --git a/tests/e2e/flows/expected-failure.yaml b/tests/e2e/flows/expected-failure.yaml new file mode 100644 index 0000000..e587a6d --- /dev/null +++ b/tests/e2e/flows/expected-failure.yaml @@ -0,0 +1,10 @@ +# E2E fixture: a flow that is EXPECTED TO FAIL. +# Tries to assert something that will never be visible on any Settings screen. +# Used to verify that runFlow() returns success=false rather than throwing. +appId: com.android.settings +name: E2E — Expected Failure +--- +- launchApp +- wait: 1 +- assert: ThisTextWillNeverAppearOnAnyScreen_XYZ_12345 +- done diff --git a/tests/e2e/flows/settings-open.yaml b/tests/e2e/flows/settings-open.yaml new file mode 100644 index 0000000..6242ce4 --- /dev/null +++ b/tests/e2e/flows/settings-open.yaml @@ -0,0 +1,9 @@ +# E2E fixture: open Android Settings and immediately exit. +# No assertions — this is purely a connectivity / runFlow smoke test. +# Always succeeds on any Android device. +appId: com.android.settings +name: E2E — Open Settings +--- +- launchApp +- wait: 2 +- done: Settings opened diff --git a/tests/e2e/flows/settings-phased.yaml b/tests/e2e/flows/settings-phased.yaml new file mode 100644 index 0000000..d1a312e --- /dev/null +++ b/tests/e2e/flows/settings-phased.yaml @@ -0,0 +1,14 @@ +# E2E fixture: phased flow against Android Settings. +# Tests setup / steps / assertions phase execution via the SDK. +appId: com.android.settings +name: E2E — Settings Phased +--- +setup: + - launchApp + - wait: 2 + +steps: + - assert: Settings + +assertions: + - assert: Settings diff --git a/tests/e2e/flows/youtube-search.yaml b/tests/e2e/flows/youtube-search.yaml new file mode 100644 index 0000000..76fd6cf --- /dev/null +++ b/tests/e2e/flows/youtube-search.yaml @@ -0,0 +1,11 @@ +# E2E fixture: search YouTube for a known video. +# Used to test a multi-step natural-language flow via the SDK. +# Requires: YouTube app installed on the device. +name: E2E — YouTube Search +--- +- open YouTube app +- wait until search icon is visible +- tap Search +- type Appium 3.0 +- wait: 3 +- done: Search results loaded diff --git a/tests/e2e/sdk.e2e.test.ts b/tests/e2e/sdk.e2e.test.ts new file mode 100644 index 0000000..7224170 --- /dev/null +++ b/tests/e2e/sdk.e2e.test.ts @@ -0,0 +1,324 @@ +/** + * AppClaw SDK — End-to-End Tests + * + * These tests exercise the full SDK stack against a real device: + * AppClaw class → McpSession → appium-mcp → Appium → device + * + * ─── Prerequisites ────────────────────────────────────────────────────────── + * • Android device or emulator connected (adb devices shows a device) + * • Appium server reachable (stdio mode starts automatically via npx) + * • LLM credentials set for runGoal() tests + * + * ─── How to run ───────────────────────────────────────────────────────────── + * LLM_API_KEY= npx vitest run tests/e2e/ + * + * With explicit provider: + * LLM_PROVIDER=anthropic LLM_API_KEY=sk-ant-... \ + * PLATFORM=android npx vitest run tests/e2e/ + * + * ─── Skip individual suites ───────────────────────────────────────────────── + * Set APPCLAW_E2E_SKIP_GOAL=1 to skip runGoal() tests (no API key needed) + * Set APPCLAW_E2E_SKIP_YOUTUBE=1 to skip YouTube tests (app may not be installed) + */ + +import 'dotenv/config'; +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { AppClaw } from '../../src/sdk/index.js'; +import type { AppClawOptions, FlowResult, AgentResult } from '../../src/sdk/index.js'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const FLOWS = resolve(__dirname, 'flows'); + +// ── Guards ──────────────────────────────────────────────────────────────────── + +const SKIP_GOAL = process.env.APPCLAW_E2E_SKIP_GOAL === '1'; +const SKIP_YOUTUBE = process.env.APPCLAW_E2E_SKIP_YOUTUBE === '1'; + +const PROVIDER = (process.env.LLM_PROVIDER ?? 'gemini') as AppClawOptions['provider']; +const API_KEY = + process.env.LLM_API_KEY ?? process.env.GEMINI_API_KEY ?? process.env.ANTHROPIC_API_KEY ?? ''; +const PLATFORM = (process.env.PLATFORM ?? 'android') as AppClawOptions['platform']; + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +function flow(name: string): string { + return resolve(FLOWS, name); +} + +function assertFlowPassed(result: FlowResult, label: string): void { + if (!result.success) { + throw new Error( + `[${label}] Flow failed in phase="${result.failedPhase ?? '?'}" ` + + `at step ${result.failedStep ?? '?'}: ${result.error ?? 'unknown error'}` + ); + } +} + +// ── Suite: Teardown Safety (no device needed) ───────────────────────────────── + +describe('SDK E2E — teardown safety (no device required)', () => { + it('teardown() before any connect() does not throw', async () => { + const app = new AppClaw({ provider: PROVIDER, apiKey: API_KEY, platform: PLATFORM }); + await expect(app.teardown()).resolves.not.toThrow(); + }); + + it('double teardown() does not throw', async () => { + const app = new AppClaw({ provider: PROVIDER, apiKey: API_KEY, platform: PLATFORM }); + await app.teardown(); + await expect(app.teardown()).resolves.not.toThrow(); + }); +}); + +// ── Suite: runFlow() ────────────────────────────────────────────────────────── + +describe('SDK E2E — runFlow()', () => { + let app: AppClaw; + + beforeAll(() => { + app = new AppClaw({ + provider: PROVIDER, + apiKey: API_KEY, + platform: PLATFORM, + silent: false, // show output so device activity is visible during debugging + }); + }, 30_000); + + afterAll(async () => { + await app.teardown(); + }, 15_000); + + // ── Basic success ──────────────────────────────────────────────────────── + + it('opens Settings and returns success=true', async () => { + const result = await app.runFlow(flow('settings-open.yaml')); + assertFlowPassed(result, 'settings-open'); + expect(result.success).toBe(true); + }, 60_000); + + it('stepsUsed and stepsTotal are positive integers', async () => { + const result = await app.runFlow(flow('settings-open.yaml')); + expect(result.stepsUsed).toBeGreaterThan(0); + expect(result.stepsTotal).toBeGreaterThan(0); + expect(Number.isInteger(result.stepsUsed)).toBe(true); + expect(Number.isInteger(result.stepsTotal)).toBe(true); + }, 60_000); + + it('stepsUsed does not exceed stepsTotal', async () => { + const result = await app.runFlow(flow('settings-open.yaml')); + expect(result.stepsUsed).toBeLessThanOrEqual(result.stepsTotal); + }, 60_000); + + it('failedStep and failedPhase are undefined on success', async () => { + const result = await app.runFlow(flow('settings-open.yaml')); + expect(result.failedStep).toBeUndefined(); + expect(result.failedPhase).toBeUndefined(); + expect(result.error).toBeUndefined(); + }, 60_000); + + // ── Phased flow ────────────────────────────────────────────────────────── + + it('phased flow (setup/steps/assertions) returns success=true', async () => { + const result = await app.runFlow(flow('settings-phased.yaml')); + assertFlowPassed(result, 'settings-phased'); + expect(result.success).toBe(true); + }, 90_000); + + it('phased flow stepsTotal covers all phases', async () => { + const result = await app.runFlow(flow('settings-phased.yaml')); + // setup=2, steps=1, assertions=1 → at least 4 steps total + expect(result.stepsTotal).toBeGreaterThanOrEqual(4); + }, 90_000); + + // ── Failure handling ───────────────────────────────────────────────────── + + it('returns success=false when an assertion fails — does NOT throw', async () => { + const result = await app.runFlow(flow('expected-failure.yaml')); + expect(result.success).toBe(false); + }, 60_000); + + it('provides error description on failure', async () => { + const result = await app.runFlow(flow('expected-failure.yaml')); + expect(result.error).toBeDefined(); + expect(typeof result.error).toBe('string'); + expect(result.error!.length).toBeGreaterThan(0); + }, 60_000); + + it('provides failedStep index on failure', async () => { + const result = await app.runFlow(flow('expected-failure.yaml')); + // The bad assert is step 3 (launchApp=1, wait=2, assert=3) + expect(result.failedStep).toBeDefined(); + expect(result.failedStep).toBeGreaterThan(0); + }, 60_000); + + // ── MCP connection reuse ───────────────────────────────────────────────── + + it('second runFlow() call reuses the MCP connection (no reconnect delay)', async () => { + const t0 = Date.now(); + await app.runFlow(flow('settings-open.yaml')); // first call — connects + const firstDuration = Date.now() - t0; + + const t1 = Date.now(); + await app.runFlow(flow('settings-open.yaml')); // second call — reuses connection + const secondDuration = Date.now() - t1; + + // The second call should not be significantly slower than the first due to reconnect. + // We allow up to 2× first call as a loose bound — the point is no new subprocess startup. + // (startup typically adds 3-8s; same-connection calls don't have that penalty) + expect(secondDuration).toBeLessThan(firstDuration + 8_000); + }, 120_000); + + it('running two different flows shares one underlying connection', async () => { + // Both flows should succeed — if connection was incorrectly closed between calls + // the second one would throw or fail to connect. + const r1 = await app.runFlow(flow('settings-open.yaml')); + const r2 = await app.runFlow(flow('settings-phased.yaml')); + expect(r1.success).toBe(true); + expect(r2.success).toBe(true); + }, 120_000); +}); + +// ── Suite: runFlow() — YouTube (skippable) ──────────────────────────────────── + +describe.skipIf(SKIP_YOUTUBE)('SDK E2E — runFlow() YouTube', () => { + let app: AppClaw; + + beforeAll(() => { + app = new AppClaw({ + provider: PROVIDER, + apiKey: API_KEY, + platform: PLATFORM, + silent: false, + }); + }, 30_000); + + afterAll(async () => { + await app.teardown(); + }, 15_000); + + it('searches YouTube and returns success=true', async () => { + const result = await app.runFlow(flow('youtube-search.yaml')); + assertFlowPassed(result, 'youtube-search'); + expect(result.success).toBe(true); + }, 120_000); + + it('YouTube flow completes in a reasonable number of steps', async () => { + const result = await app.runFlow(flow('youtube-search.yaml')); + // Simple search flow should not need more than 20 steps + expect(result.stepsUsed).toBeLessThanOrEqual(20); + }, 120_000); +}); + +// ── Suite: runGoal() ────────────────────────────────────────────────────────── + +describe.skipIf(SKIP_GOAL)('SDK E2E — runGoal()', () => { + let app: AppClaw; + + beforeAll(() => { + if (!API_KEY) { + throw new Error( + 'runGoal() tests require LLM_API_KEY (or GEMINI_API_KEY / ANTHROPIC_API_KEY). ' + + 'Set APPCLAW_E2E_SKIP_GOAL=1 to skip these tests.' + ); + } + app = new AppClaw({ + provider: PROVIDER, + apiKey: API_KEY, + platform: PLATFORM, + maxSteps: 10, + silent: false, + }); + }, 30_000); + + afterAll(async () => { + await app.teardown(); + }, 15_000); + + it('completes a simple natural-language goal', async () => { + const result = await app.runGoal('Open the Settings app'); + expect(result.success).toBe(true); + }, 90_000); + + it('returns stepsUsed as a positive integer', async () => { + const result = await app.runGoal('Open the Settings app'); + expect(result.stepsUsed).toBeGreaterThan(0); + expect(Number.isInteger(result.stepsUsed)).toBe(true); + }, 90_000); + + it('returns a non-empty reason string', async () => { + const result = await app.runGoal('Open the Settings app'); + expect(typeof result.reason).toBe('string'); + expect(result.reason.length).toBeGreaterThan(0); + }, 90_000); + + it('returns AgentResult with history array', async () => { + const result = await app.runGoal('Open the Settings app'); + expect(Array.isArray(result.history)).toBe(true); + }, 90_000); + + it('caps stepsUsed at maxSteps option', async () => { + // maxSteps=10 — stepsUsed must not exceed this + const result = await app.runGoal('Open the Settings app'); + expect(result.stepsUsed).toBeLessThanOrEqual(10); + }, 90_000); +}); + +// ── Suite: Mixed flow + goal ────────────────────────────────────────────────── + +describe.skipIf(SKIP_GOAL)('SDK E2E — mixed runFlow() + runGoal()', () => { + let app: AppClaw; + + beforeAll(() => { + if (!API_KEY) { + throw new Error('Mixed tests require LLM_API_KEY. Set APPCLAW_E2E_SKIP_GOAL=1 to skip.'); + } + app = new AppClaw({ + provider: PROVIDER, + apiKey: API_KEY, + platform: PLATFORM, + maxSteps: 10, + silent: false, + }); + }, 30_000); + + afterAll(async () => { + await app.teardown(); + }, 15_000); + + it('runFlow() then runGoal() share one MCP connection without error', async () => { + const flowResult = await app.runFlow(flow('settings-open.yaml')); + const goalResult = await app.runGoal('Open the Settings app'); + + expect(flowResult.success).toBe(true); + expect(goalResult.success).toBe(true); + }, 120_000); + + it('runGoal() then runFlow() share one MCP connection without error', async () => { + const goalResult = await app.runGoal('Open the Settings app'); + const flowResult = await app.runFlow(flow('settings-open.yaml')); + + expect(goalResult.success).toBe(true); + expect(flowResult.success).toBe(true); + }, 120_000); +}); + +// ── Suite: teardown after use ───────────────────────────────────────────────── + +describe('SDK E2E — teardown after use', () => { + it('teardown() after runFlow() does not throw', async () => { + const app = new AppClaw({ + provider: PROVIDER, + apiKey: API_KEY, + platform: PLATFORM, + }); + + await app.run('open YouTube app'); + await app.run('tap Search'); + await app.run('type Appium 3.0'); + await app.run('tap the search button'); + await app.run('wait 2 seconds'); + await app.run('scroll down'); + await app.teardown(); + }, 90_000); +}); diff --git a/tests/sdk-smoke.ts b/tests/sdk-smoke.ts new file mode 100644 index 0000000..8cd5b44 --- /dev/null +++ b/tests/sdk-smoke.ts @@ -0,0 +1,54 @@ +/** + * SDK smoke test — verifies the public API is importable and correctly wired. + * + * Does NOT require a real device or MCP connection. + * Run with: npx tsx tests/sdk-smoke.ts + */ + +import { AppClaw } from '../src/sdk/index.js'; +import type { AppClawOptions, FlowResult, AgentResult } from '../src/sdk/index.js'; + +// ── 1. Constructor accepts options without touching process.env ────────────── + +const app = new AppClaw({ + provider: 'anthropic', + apiKey: 'test-key', + platform: 'android', + agentMode: 'dom', + maxSteps: 10, + silent: true, +}); + +console.log('✓ AppClaw instantiated without errors'); + +// ── 2. Public types are exported and usable ────────────────────────────────── + +const _options: AppClawOptions = { provider: 'gemini', silent: true }; +const _flowResult: FlowResult = { + success: true, + stepsUsed: 3, + stepsTotal: 5, +}; +const _agentResult: AgentResult = { + success: true, + reason: 'Goal completed', + stepsUsed: 3, + history: [], +}; + +console.log('✓ All public types resolved correctly'); + +// ── 3. teardown is safe to call without a prior connect ───────────────────── + +await app.teardown(); +console.log('✓ teardown() is safe with no active connection'); + +// ── 4. Multiple instances are isolated ────────────────────────────────────── + +const app1 = new AppClaw({ provider: 'anthropic', apiKey: 'key-1', silent: true }); +const app2 = new AppClaw({ provider: 'openai', apiKey: 'key-2', silent: true }); +await app1.teardown(); +await app2.teardown(); + +console.log('✓ Multiple isolated instances work correctly'); +console.log('\nAll smoke tests passed.'); diff --git a/tests/sdk/appclaw.test.ts b/tests/sdk/appclaw.test.ts new file mode 100644 index 0000000..b62b8dc --- /dev/null +++ b/tests/sdk/appclaw.test.ts @@ -0,0 +1,324 @@ +import { describe, test, expect, vi, beforeEach } from 'vitest'; + +// ── Mocks — set up before any module under test is imported ────────────── + +vi.mock('../../src/mcp/client.js', () => ({ + acquireSharedMCPClient: vi.fn(), +})); + +vi.mock('../../src/flow/parse-yaml-flow.js', () => ({ + parseFlowYamlFile: vi.fn(), +})); + +vi.mock('../../src/flow/run-yaml-flow.js', () => ({ + runYamlFlow: vi.fn(), +})); + +vi.mock('../../src/llm/provider.js', () => ({ + createLLMProvider: vi.fn(), +})); + +vi.mock('../../src/agent/loop.js', () => ({ + runAgent: vi.fn(), +})); + +vi.mock('../../src/ui/terminal.js', () => ({ + silenceTerminalUI: vi.fn(), + theme: { dim: (s: string) => s, info: (s: string) => s }, +})); + +vi.mock('../../src/device/session.js', () => ({ + createPlatformSession: vi.fn(), +})); + +const { acquireSharedMCPClient } = await import('../../src/mcp/client.js'); +const { createPlatformSession } = await import('../../src/device/session.js'); +const { parseFlowYamlFile } = await import('../../src/flow/parse-yaml-flow.js'); +const { runYamlFlow } = await import('../../src/flow/run-yaml-flow.js'); +const { createLLMProvider } = await import('../../src/llm/provider.js'); +const { runAgent } = await import('../../src/agent/loop.js'); +const { silenceTerminalUI } = await import('../../src/ui/terminal.js'); +const { AppClaw } = await import('../../src/sdk/index.js'); + +// ── Shared fixture builders ─────────────────────────────────────────────── + +const mockRelease = vi.fn().mockResolvedValue(undefined); +const mockListTools = vi.fn().mockResolvedValue([{ name: 'appium_click' }]); +const mockCallTool = vi.fn().mockResolvedValue({ content: [] }); +const mockClose = vi.fn().mockResolvedValue(undefined); + +function makeSharedClient() { + return { + callTool: mockCallTool, + listTools: mockListTools, + close: mockClose, + release: mockRelease, + }; +} + +const stubParsedFlow = { + meta: { name: 'Stub Flow' }, + steps: [{ kind: 'done' as const }], + phases: [], +}; + +const stubFlowResult = { + success: true, + stepsExecuted: 1, + stepsTotal: 1, + reason: undefined, + failedAt: undefined, + failedPhase: undefined, + phaseResults: [], +}; + +const stubAgentResult = { + success: true, + reason: 'Done', + stepsUsed: 2, + history: [], +}; + +const mockLLM = { + supportsVision: false, + getDecision: vi.fn(), + feedToolResult: vi.fn(), + resetHistory: vi.fn(), +}; + +const stubScopedMcp = { + callTool: vi.fn().mockResolvedValue({ content: [] }), + listTools: vi.fn().mockResolvedValue([]), + close: vi.fn().mockResolvedValue(undefined), +}; + +beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(acquireSharedMCPClient).mockResolvedValue(makeSharedClient() as any); + vi.mocked(createPlatformSession).mockResolvedValue({ + platform: 'android', + sessionText: 'mock session', + sessionId: 'mock-session-id', + scopedMcp: stubScopedMcp, + } as any); + vi.mocked(parseFlowYamlFile).mockResolvedValue(stubParsedFlow as any); + vi.mocked(runYamlFlow).mockResolvedValue(stubFlowResult as any); + vi.mocked(createLLMProvider).mockReturnValue(mockLLM as any); + vi.mocked(runAgent).mockResolvedValue(stubAgentResult as any); +}); + +// ── Constructor ─────────────────────────────────────────────────────────── + +describe('AppClaw — constructor', () => { + test('constructs without errors with empty options', () => { + expect(() => new AppClaw()).not.toThrow(); + }); + + test('constructs without errors with all options', () => { + expect( + () => + new AppClaw({ + provider: 'anthropic', + apiKey: 'sk-test', + model: 'claude-opus-4-6', + platform: 'android', + agentMode: 'dom', + maxSteps: 20, + stepDelay: 300, + silent: true, + mcpTransport: 'stdio', + mcpHost: 'localhost', + mcpPort: 8080, + }) + ).not.toThrow(); + }); + + test('calls silenceTerminalUI when silent is not explicitly false', () => { + new AppClaw({ silent: true }); + expect(silenceTerminalUI).toHaveBeenCalledOnce(); + }); + + test('calls silenceTerminalUI when silent is omitted (default)', () => { + new AppClaw({}); + expect(silenceTerminalUI).toHaveBeenCalledOnce(); + }); + + test('does NOT call silenceTerminalUI when silent is false', () => { + new AppClaw({ silent: false }); + expect(silenceTerminalUI).not.toHaveBeenCalled(); + }); + + test('does not connect to MCP on construction', () => { + new AppClaw({ provider: 'anthropic' }); + expect(acquireSharedMCPClient).not.toHaveBeenCalled(); + }); +}); + +// ── runFlow ─────────────────────────────────────────────────────────────── + +describe('AppClaw — runFlow', () => { + test('connects to MCP on first runFlow call', async () => { + const app = new AppClaw(); + await app.runFlow('./flows/test.yaml'); + expect(acquireSharedMCPClient).toHaveBeenCalledOnce(); + await app.teardown(); + }); + + test('reuses MCP connection on subsequent runFlow calls', async () => { + const app = new AppClaw(); + await app.runFlow('./flows/a.yaml'); + await app.runFlow('./flows/b.yaml'); + expect(acquireSharedMCPClient).toHaveBeenCalledOnce(); + await app.teardown(); + }); + + test('returns FlowResult with success=true on success', async () => { + const app = new AppClaw(); + const result = await app.runFlow('./flows/test.yaml'); + expect(result.success).toBe(true); + await app.teardown(); + }); + + test('returns FlowResult with success=false on failure', async () => { + vi.mocked(runYamlFlow).mockResolvedValue({ + ...stubFlowResult, + success: false, + reason: 'Step failed', + failedAt: 1, + } as any); + const app = new AppClaw(); + const result = await app.runFlow('./flows/test.yaml'); + expect(result.success).toBe(false); + expect(result.error).toBe('Step failed'); + await app.teardown(); + }); + + test('passes flow path to parseFlowYamlFile', async () => { + const app = new AppClaw(); + await app.runFlow('./flows/checkout.yaml'); + expect(parseFlowYamlFile).toHaveBeenCalledWith('./flows/checkout.yaml'); + await app.teardown(); + }); + + test('forwards RunYamlFlowOptions to the flow engine', async () => { + const app = new AppClaw(); + const options = { stepDelayMs: 800 }; + await app.runFlow('./flows/test.yaml', options); + + const call = vi.mocked(runYamlFlow).mock.calls[0]; + expect(call[3]).toEqual(options); + await app.teardown(); + }); +}); + +// ── runGoal ─────────────────────────────────────────────────────────────── + +describe('AppClaw — runGoal', () => { + test('connects to MCP on first runGoal call', async () => { + const app = new AppClaw(); + await app.runGoal('Log in'); + expect(acquireSharedMCPClient).toHaveBeenCalledOnce(); + await app.teardown(); + }); + + test('reuses MCP connection across runGoal calls', async () => { + const app = new AppClaw(); + await app.runGoal('Log in'); + await app.runGoal('Check order status'); + expect(acquireSharedMCPClient).toHaveBeenCalledOnce(); + await app.teardown(); + }); + + test('passes goal string to runAgent', async () => { + const app = new AppClaw(); + await app.runGoal('Complete checkout'); + const call = vi.mocked(runAgent).mock.calls[0][0]; + expect(call.goal).toBe('Complete checkout'); + await app.teardown(); + }); + + test('creates a fresh LLM provider for each runGoal call', async () => { + const app = new AppClaw(); + await app.runGoal('Goal one'); + await app.runGoal('Goal two'); + expect(createLLMProvider).toHaveBeenCalledTimes(2); + await app.teardown(); + }); + + test('returns AgentResult directly', async () => { + const app = new AppClaw(); + const result = await app.runGoal('Log in'); + expect(result.success).toBe(true); + expect(result.reason).toBe('Done'); + await app.teardown(); + }); +}); + +// ── Mixed flow and goal ─────────────────────────────────────────────────── + +describe('AppClaw — mixed runFlow and runGoal', () => { + test('shares MCP connection between runFlow and runGoal', async () => { + const app = new AppClaw(); + await app.runFlow('./flows/setup.yaml'); + await app.runGoal('Check the result'); + expect(acquireSharedMCPClient).toHaveBeenCalledOnce(); + await app.teardown(); + }); +}); + +// ── teardown ────────────────────────────────────────────────────────────── + +describe('AppClaw — teardown', () => { + test('is safe to call without a prior connect', async () => { + const app = new AppClaw(); + await expect(app.teardown()).resolves.not.toThrow(); + expect(mockRelease).not.toHaveBeenCalled(); + }); + + test('releases the MCP handle after connect', async () => { + const app = new AppClaw(); + await app.runFlow('./flows/test.yaml'); + await app.teardown(); + expect(mockRelease).toHaveBeenCalledOnce(); + }); + + test('double teardown does not throw', async () => { + const app = new AppClaw(); + await app.runFlow('./flows/test.yaml'); + await app.teardown(); + await expect(app.teardown()).resolves.not.toThrow(); + expect(mockRelease).toHaveBeenCalledOnce(); // not twice + }); +}); + +// ── Instance isolation ──────────────────────────────────────────────────── + +describe('AppClaw — instance isolation', () => { + test('two instances have independent MCP connections', async () => { + const app1 = new AppClaw({ provider: 'anthropic' }); + const app2 = new AppClaw({ provider: 'openai' }); + + await app1.runFlow('./flows/test.yaml'); + await app2.runFlow('./flows/test.yaml'); + + expect(acquireSharedMCPClient).toHaveBeenCalledTimes(2); + + await app1.teardown(); + await app2.teardown(); + }); + + test('tearing down one instance does not affect the other', async () => { + const app1 = new AppClaw(); + const app2 = new AppClaw(); + + await app1.runFlow('./flows/test.yaml'); + await app2.runFlow('./flows/test.yaml'); + await app1.teardown(); + + // app2 should still be able to run + await app2.runFlow('./flows/another.yaml'); + expect(acquireSharedMCPClient).toHaveBeenCalledTimes(2); // no new connection for app2 + + await app2.teardown(); + }); +}); diff --git a/tests/sdk/config-builder.test.ts b/tests/sdk/config-builder.test.ts new file mode 100644 index 0000000..966981b --- /dev/null +++ b/tests/sdk/config-builder.test.ts @@ -0,0 +1,270 @@ +import { describe, test, expect, beforeEach, afterEach } from 'vitest'; +import { buildConfig } from '../../src/sdk/config-builder.js'; + +// Snapshot of env vars we touch — restored after each test. +const TOUCHED_VARS = [ + 'LLM_PROVIDER', + 'LLM_API_KEY', + 'LLM_MODEL', + 'PLATFORM', + 'AGENT_MODE', + 'MAX_STEPS', + 'STEP_DELAY', + 'MCP_TRANSPORT', + 'MCP_HOST', + 'MCP_PORT', +]; + +let saved: Record = {}; + +beforeEach(() => { + saved = {}; + for (const key of TOUCHED_VARS) { + saved[key] = process.env[key]; + delete process.env[key]; + } +}); + +afterEach(() => { + for (const key of TOUCHED_VARS) { + if (saved[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = saved[key]; + } + } +}); + +// ── Provider mapping ────────────────────────────────────────────────────── + +describe('buildConfig — provider', () => { + test('maps provider option to LLM_PROVIDER', () => { + const config = buildConfig({ provider: 'anthropic' }); + expect(config.LLM_PROVIDER).toBe('anthropic'); + }); + + test('supports all valid provider values', () => { + const providers = ['anthropic', 'openai', 'gemini', 'groq', 'ollama'] as const; + for (const p of providers) { + const config = buildConfig({ provider: p }); + expect(config.LLM_PROVIDER).toBe(p); + } + }); + + test('defaults to gemini when provider is not set', () => { + const config = buildConfig({}); + expect(config.LLM_PROVIDER).toBe('gemini'); + }); +}); + +// ── API key mapping ─────────────────────────────────────────────────────── + +describe('buildConfig — apiKey', () => { + test('maps apiKey to LLM_API_KEY', () => { + const config = buildConfig({ apiKey: 'sk-test-key' }); + expect(config.LLM_API_KEY).toBe('sk-test-key'); + }); + + test('falls back to empty string when not set', () => { + const config = buildConfig({}); + expect(config.LLM_API_KEY).toBe(''); + }); + + test('option takes priority over process.env', () => { + process.env.LLM_API_KEY = 'env-key'; + const config = buildConfig({ apiKey: 'option-key' }); + expect(config.LLM_API_KEY).toBe('option-key'); + }); +}); + +// ── Model mapping ───────────────────────────────────────────────────────── + +describe('buildConfig — model', () => { + test('maps model to LLM_MODEL', () => { + const config = buildConfig({ model: 'claude-opus-4-6' }); + expect(config.LLM_MODEL).toBe('claude-opus-4-6'); + }); + + test('defaults to empty string when not set', () => { + const config = buildConfig({}); + expect(config.LLM_MODEL).toBe(''); + }); +}); + +// ── Platform mapping ────────────────────────────────────────────────────── + +describe('buildConfig — platform', () => { + test('maps platform android', () => { + const config = buildConfig({ platform: 'android' }); + expect(config.PLATFORM).toBe('android'); + }); + + test('maps platform ios', () => { + const config = buildConfig({ platform: 'ios' }); + expect(config.PLATFORM).toBe('ios'); + }); + + test('defaults to empty string when not set', () => { + const config = buildConfig({}); + expect(config.PLATFORM).toBe(''); + }); +}); + +// ── Agent mode mapping ──────────────────────────────────────────────────── + +describe('buildConfig — agentMode', () => { + test('maps agentMode dom', () => { + const config = buildConfig({ agentMode: 'dom' }); + expect(config.AGENT_MODE).toBe('dom'); + }); + + test('maps agentMode vision', () => { + const config = buildConfig({ agentMode: 'vision' }); + expect(config.AGENT_MODE).toBe('vision'); + }); + + test('defaults to dom when not set', () => { + const config = buildConfig({}); + expect(config.AGENT_MODE).toBe('dom'); + }); +}); + +// ── Numeric option mapping ──────────────────────────────────────────────── + +describe('buildConfig — numeric options', () => { + test('maps maxSteps to MAX_STEPS', () => { + const config = buildConfig({ maxSteps: 15 }); + expect(config.MAX_STEPS).toBe(15); + }); + + test('maps stepDelay to STEP_DELAY', () => { + const config = buildConfig({ stepDelay: 1000 }); + expect(config.STEP_DELAY).toBe(1000); + }); + + test('defaults MAX_STEPS to 30', () => { + const config = buildConfig({}); + expect(config.MAX_STEPS).toBe(30); + }); + + test('defaults STEP_DELAY to 500', () => { + const config = buildConfig({}); + expect(config.STEP_DELAY).toBe(500); + }); +}); + +// ── MCP transport mapping ───────────────────────────────────────────────── + +describe('buildConfig — MCP options', () => { + test('maps mcpTransport stdio', () => { + const config = buildConfig({ mcpTransport: 'stdio' }); + expect(config.MCP_TRANSPORT).toBe('stdio'); + }); + + test('maps mcpTransport sse', () => { + const config = buildConfig({ mcpTransport: 'sse' }); + expect(config.MCP_TRANSPORT).toBe('sse'); + }); + + test('maps mcpHost', () => { + const config = buildConfig({ mcpHost: '192.168.1.1' }); + expect(config.MCP_HOST).toBe('192.168.1.1'); + }); + + test('maps mcpPort', () => { + const config = buildConfig({ mcpPort: 9090 }); + expect(config.MCP_PORT).toBe(9090); + }); + + test('defaults to stdio transport', () => { + const config = buildConfig({}); + expect(config.MCP_TRANSPORT).toBe('stdio'); + }); + + test('defaults to localhost', () => { + const config = buildConfig({}); + expect(config.MCP_HOST).toBe('localhost'); + }); + + test('defaults to port 8080', () => { + const config = buildConfig({}); + expect(config.MCP_PORT).toBe(8080); + }); +}); + +// ── Priority: options override process.env ──────────────────────────────── + +describe('buildConfig — option priority', () => { + test('option takes priority over process.env for provider', () => { + process.env.LLM_PROVIDER = 'gemini'; + const config = buildConfig({ provider: 'openai' }); + expect(config.LLM_PROVIDER).toBe('openai'); + }); + + test('option takes priority over process.env for maxSteps', () => { + process.env.MAX_STEPS = '50'; + const config = buildConfig({ maxSteps: 5 }); + expect(config.MAX_STEPS).toBe(5); + }); + + test('falls back to process.env when option is omitted', () => { + process.env.LLM_PROVIDER = 'groq'; + const config = buildConfig({}); + expect(config.LLM_PROVIDER).toBe('groq'); + }); +}); + +// ── silent is SDK-only, not mapped to any env var ──────────────────────── + +describe('buildConfig — silent (SDK-only flag)', () => { + test('silent option does not cause config errors', () => { + expect(() => buildConfig({ silent: true })).not.toThrow(); + expect(() => buildConfig({ silent: false })).not.toThrow(); + }); +}); + +// ── Empty options produces valid config with defaults ───────────────────── + +describe('buildConfig — defaults', () => { + test('empty options produces valid config', () => { + const config = buildConfig({}); + expect(config).toMatchObject({ + LLM_PROVIDER: 'gemini', + LLM_API_KEY: '', + LLM_MODEL: '', + PLATFORM: '', + AGENT_MODE: 'dom', + MAX_STEPS: 30, + STEP_DELAY: 500, + MCP_TRANSPORT: 'stdio', + MCP_HOST: 'localhost', + MCP_PORT: 8080, + }); + }); + + test('combined options all map correctly', () => { + const config = buildConfig({ + provider: 'anthropic', + apiKey: 'sk-ant', + model: 'claude-opus-4-6', + platform: 'ios', + agentMode: 'dom', + maxSteps: 20, + stepDelay: 200, + mcpTransport: 'sse', + mcpHost: '10.0.0.1', + mcpPort: 3000, + }); + + expect(config.LLM_PROVIDER).toBe('anthropic'); + expect(config.LLM_API_KEY).toBe('sk-ant'); + expect(config.LLM_MODEL).toBe('claude-opus-4-6'); + expect(config.PLATFORM).toBe('ios'); + expect(config.AGENT_MODE).toBe('dom'); + expect(config.MAX_STEPS).toBe(20); + expect(config.STEP_DELAY).toBe(200); + expect(config.MCP_TRANSPORT).toBe('sse'); + expect(config.MCP_HOST).toBe('10.0.0.1'); + expect(config.MCP_PORT).toBe(3000); + }); +}); diff --git a/tests/sdk/flow-runner.test.ts b/tests/sdk/flow-runner.test.ts new file mode 100644 index 0000000..5c058ea --- /dev/null +++ b/tests/sdk/flow-runner.test.ts @@ -0,0 +1,168 @@ +import { describe, test, expect, vi, beforeEach } from 'vitest'; +import type { MCPClient } from '../../src/mcp/types.js'; +import type { ParsedFlow } from '../../src/flow/types.js'; + +// ── Mocks ───────────────────────────────────────────────────────────────── + +vi.mock('../../src/flow/parse-yaml-flow.js', () => ({ + parseFlowYamlFile: vi.fn(), +})); + +vi.mock('../../src/flow/run-yaml-flow.js', () => ({ + runYamlFlow: vi.fn(), +})); + +const { parseFlowYamlFile } = await import('../../src/flow/parse-yaml-flow.js'); +const { runYamlFlow } = await import('../../src/flow/run-yaml-flow.js'); +const { FlowRunner } = await import('../../src/sdk/flow-runner.js'); + +// ── Helpers ─────────────────────────────────────────────────────────────── + +const mockMcp: MCPClient = { + callTool: vi.fn(), + listTools: vi.fn(), + close: vi.fn(), +}; + +function parsedFlow(overrides: Partial = {}): ParsedFlow { + return { + meta: { name: 'Test Flow', platform: 'android' }, + steps: [{ kind: 'tap', label: 'Login' }, { kind: 'done' }], + phases: [], + ...overrides, + }; +} + +function flowSuccess(stepsExecuted = 2, stepsTotal = 2) { + return { + success: true, + stepsExecuted, + stepsTotal, + reason: undefined, + failedAt: undefined, + failedPhase: undefined, + phaseResults: [], + }; +} + +function flowFailure(reason: string, failedAt = 1, failedPhase = 'test') { + return { + success: false, + stepsExecuted: 1, + stepsTotal: 2, + reason, + failedAt, + failedPhase, + phaseResults: [], + }; +} + +beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(parseFlowYamlFile).mockResolvedValue(parsedFlow()); + vi.mocked(runYamlFlow).mockResolvedValue(flowSuccess()); +}); + +// ── Basic execution ─────────────────────────────────────────────────────── + +describe('FlowRunner — basic execution', () => { + test('calls parseFlowYamlFile with the given path', async () => { + const runner = new FlowRunner(mockMcp); + await runner.run('./flows/checkout.yaml'); + expect(parseFlowYamlFile).toHaveBeenCalledWith('./flows/checkout.yaml'); + }); + + test('calls runYamlFlow with mcp, meta, steps, options, phases', async () => { + const flow = parsedFlow(); + vi.mocked(parseFlowYamlFile).mockResolvedValue(flow); + + const runner = new FlowRunner(mockMcp); + await runner.run('./flows/checkout.yaml'); + + expect(runYamlFlow).toHaveBeenCalledWith( + mockMcp, + flow.meta, + flow.steps, + {}, // default empty options + flow.phases + ); + }); + + test('forwards options to runYamlFlow', async () => { + const runner = new FlowRunner(mockMcp); + const options = { stepDelayMs: 1000 }; + await runner.run('./flows/checkout.yaml', options); + + const call = vi.mocked(runYamlFlow).mock.calls[0]; + expect(call[3]).toEqual(options); + }); +}); + +// ── Result mapping ──────────────────────────────────────────────────────── + +describe('FlowRunner — result mapping', () => { + test('maps success result correctly', async () => { + vi.mocked(runYamlFlow).mockResolvedValue(flowSuccess(3, 5)); + const runner = new FlowRunner(mockMcp); + const result = await runner.run('./flows/test.yaml'); + + expect(result.success).toBe(true); + expect(result.stepsUsed).toBe(3); + expect(result.stepsTotal).toBe(5); + expect(result.failedStep).toBeUndefined(); + expect(result.failedPhase).toBeUndefined(); + expect(result.error).toBeUndefined(); + }); + + test('maps failure result correctly', async () => { + vi.mocked(runYamlFlow).mockResolvedValue(flowFailure('Element not found', 2, 'test')); + const runner = new FlowRunner(mockMcp); + const result = await runner.run('./flows/test.yaml'); + + expect(result.success).toBe(false); + expect(result.stepsUsed).toBe(1); + expect(result.stepsTotal).toBe(2); + expect(result.failedStep).toBe(2); + expect(result.failedPhase).toBe('test'); + expect(result.error).toBe('Element not found'); + }); + + test('maps setup phase failure', async () => { + vi.mocked(runYamlFlow).mockResolvedValue(flowFailure('App not found', 1, 'setup')); + const runner = new FlowRunner(mockMcp); + const result = await runner.run('./flows/test.yaml'); + + expect(result.failedPhase).toBe('setup'); + expect(result.error).toBe('App not found'); + }); +}); + +// ── Error propagation ───────────────────────────────────────────────────── + +describe('FlowRunner — error propagation', () => { + test('propagates parseFlowYamlFile errors', async () => { + vi.mocked(parseFlowYamlFile).mockRejectedValue(new Error('File not found: missing.yaml')); + const runner = new FlowRunner(mockMcp); + await expect(runner.run('./missing.yaml')).rejects.toThrow('File not found: missing.yaml'); + }); + + test('propagates runYamlFlow errors', async () => { + vi.mocked(runYamlFlow).mockRejectedValue(new Error('MCP connection lost')); + const runner = new FlowRunner(mockMcp); + await expect(runner.run('./flows/test.yaml')).rejects.toThrow('MCP connection lost'); + }); +}); + +// ── Multiple flows ──────────────────────────────────────────────────────── + +describe('FlowRunner — multiple flows', () => { + test('can run multiple different flows sequentially', async () => { + const runner = new FlowRunner(mockMcp); + await runner.run('./flows/login.yaml'); + await runner.run('./flows/checkout.yaml'); + + expect(parseFlowYamlFile).toHaveBeenCalledTimes(2); + expect(parseFlowYamlFile).toHaveBeenNthCalledWith(1, './flows/login.yaml'); + expect(parseFlowYamlFile).toHaveBeenNthCalledWith(2, './flows/checkout.yaml'); + }); +}); diff --git a/tests/sdk/goal-runner.test.ts b/tests/sdk/goal-runner.test.ts new file mode 100644 index 0000000..1b72f85 --- /dev/null +++ b/tests/sdk/goal-runner.test.ts @@ -0,0 +1,186 @@ +import { describe, test, expect, vi, beforeEach } from 'vitest'; +import type { MCPClient, MCPToolInfo } from '../../src/mcp/types.js'; +import type { AgentResult } from '../../src/agent/loop.js'; + +// ── Mocks ───────────────────────────────────────────────────────────────── + +vi.mock('../../src/llm/provider.js', () => ({ + createLLMProvider: vi.fn(), +})); + +vi.mock('../../src/agent/loop.js', () => ({ + runAgent: vi.fn(), +})); + +const { createLLMProvider } = await import('../../src/llm/provider.js'); +const { runAgent } = await import('../../src/agent/loop.js'); +const { GoalRunner } = await import('../../src/sdk/goal-runner.js'); + +// ── Helpers ─────────────────────────────────────────────────────────────── + +const mockMcp: MCPClient = { + callTool: vi.fn(), + listTools: vi.fn(), + close: vi.fn(), +}; + +const mockTools: MCPToolInfo[] = [{ name: 'appium_click' }, { name: 'appium_type' }]; + +const mockLLM = { + supportsVision: false, + getDecision: vi.fn(), + feedToolResult: vi.fn(), + resetHistory: vi.fn(), +}; + +function agentSuccess(stepsUsed = 3): AgentResult { + return { success: true, reason: 'Goal completed', stepsUsed, history: [] }; +} + +function agentFailure(reason: string): AgentResult { + return { success: false, reason, stepsUsed: 5, history: [] }; +} + +function makeConfig(overrides = {}) { + return { + MAX_STEPS: 30, + STEP_DELAY: 500, + VISION_MODE: 'fallback' as const, + LLM_PROVIDER: 'anthropic', + LLM_API_KEY: 'sk-test', + ...overrides, + } as any; +} + +beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(createLLMProvider).mockReturnValue(mockLLM as any); + vi.mocked(runAgent).mockResolvedValue(agentSuccess()); +}); + +// ── LLM provider creation ───────────────────────────────────────────────── + +describe('GoalRunner — LLM provider creation', () => { + test('creates an LLM provider via createLLMProvider', async () => { + const config = makeConfig(); + const runner = new GoalRunner(mockMcp, mockTools, config); + await runner.run('Log in to the app'); + expect(createLLMProvider).toHaveBeenCalledOnce(); + }); + + test('passes config to createLLMProvider', async () => { + const config = makeConfig({ LLM_PROVIDER: 'openai' }); + const runner = new GoalRunner(mockMcp, mockTools, config); + await runner.run('Open settings'); + expect(createLLMProvider).toHaveBeenCalledWith(config, mockTools); + }); + + test('passes the tool list to createLLMProvider', async () => { + const config = makeConfig(); + const runner = new GoalRunner(mockMcp, mockTools, config); + await runner.run('Tap login'); + expect(createLLMProvider).toHaveBeenCalledWith(config, mockTools); + }); + + test('creates a fresh LLM provider on each run() call', async () => { + const config = makeConfig(); + const runner = new GoalRunner(mockMcp, mockTools, config); + await runner.run('Goal one'); + await runner.run('Goal two'); + expect(createLLMProvider).toHaveBeenCalledTimes(2); + }); +}); + +// ── Agent invocation ────────────────────────────────────────────────────── + +describe('GoalRunner — runAgent invocation', () => { + test('calls runAgent with goal, mcp, and llm', async () => { + const config = makeConfig(); + const runner = new GoalRunner(mockMcp, mockTools, config); + await runner.run('Complete checkout'); + + expect(runAgent).toHaveBeenCalledOnce(); + const call = vi.mocked(runAgent).mock.calls[0][0]; + expect(call.goal).toBe('Complete checkout'); + expect(call.mcp).toBe(mockMcp); + expect(call.llm).toBe(mockLLM); + }); + + test('passes maxSteps from config', async () => { + const config = makeConfig({ MAX_STEPS: 10 }); + const runner = new GoalRunner(mockMcp, mockTools, config); + await runner.run('Some goal'); + + const call = vi.mocked(runAgent).mock.calls[0][0]; + expect(call.maxSteps).toBe(10); + }); + + test('passes stepDelay from config', async () => { + const config = makeConfig({ STEP_DELAY: 200 }); + const runner = new GoalRunner(mockMcp, mockTools, config); + await runner.run('Some goal'); + + const call = vi.mocked(runAgent).mock.calls[0][0]; + expect(call.stepDelay).toBe(200); + }); + + test('passes visionMode from config', async () => { + const config = makeConfig({ VISION_MODE: 'never' }); + const runner = new GoalRunner(mockMcp, mockTools, config); + await runner.run('Some goal'); + + const call = vi.mocked(runAgent).mock.calls[0][0]; + expect(call.visionMode).toBe('never'); + }); +}); + +// ── Result passthrough ──────────────────────────────────────────────────── + +describe('GoalRunner — result passthrough', () => { + test('returns the AgentResult directly on success', async () => { + vi.mocked(runAgent).mockResolvedValue(agentSuccess(4)); + const runner = new GoalRunner(mockMcp, mockTools, makeConfig()); + const result = await runner.run('Do something'); + + expect(result.success).toBe(true); + expect(result.stepsUsed).toBe(4); + expect(result.reason).toBe('Goal completed'); + }); + + test('returns the AgentResult directly on failure', async () => { + vi.mocked(runAgent).mockResolvedValue(agentFailure('Max steps exceeded')); + const runner = new GoalRunner(mockMcp, mockTools, makeConfig()); + const result = await runner.run('Do something'); + + expect(result.success).toBe(false); + expect(result.reason).toBe('Max steps exceeded'); + }); + + test('propagates runAgent errors', async () => { + vi.mocked(runAgent).mockRejectedValue(new Error('MCP disconnected')); + const runner = new GoalRunner(mockMcp, mockTools, makeConfig()); + await expect(runner.run('Some goal')).rejects.toThrow('MCP disconnected'); + }); +}); + +// ── History isolation ───────────────────────────────────────────────────── + +describe('GoalRunner — history isolation', () => { + test('each run gets a fresh LLM provider so history does not bleed', async () => { + // Two distinct LLM mock instances + const llm1 = { ...mockLLM }; + const llm2 = { ...mockLLM }; + vi.mocked(createLLMProvider) + .mockReturnValueOnce(llm1 as any) + .mockReturnValueOnce(llm2 as any); + + const runner = new GoalRunner(mockMcp, mockTools, makeConfig()); + await runner.run('Goal one'); + await runner.run('Goal two'); + + const calls = vi.mocked(runAgent).mock.calls; + expect(calls[0][0].llm).toBe(llm1); + expect(calls[1][0].llm).toBe(llm2); + expect(calls[0][0].llm).not.toBe(calls[1][0].llm); + }); +}); diff --git a/tests/sdk/mcp-session.test.ts b/tests/sdk/mcp-session.test.ts new file mode 100644 index 0000000..7cd28b2 --- /dev/null +++ b/tests/sdk/mcp-session.test.ts @@ -0,0 +1,204 @@ +import { describe, test, expect, vi, beforeEach } from 'vitest'; +import type { MCPClient, MCPToolInfo, SharedMCPClient } from '../../src/mcp/types.js'; + +// ── Mock acquireSharedMCPClient ─────────────────────────────────────────── + +const mockRelease = vi.fn().mockResolvedValue(undefined); +const mockListTools = vi.fn().mockResolvedValue([ + { name: 'appium_click', description: 'Click an element' }, + { name: 'appium_type', description: 'Type text' }, +] as MCPToolInfo[]); +const mockCallTool = vi.fn().mockResolvedValue({ content: [] }); +const mockClose = vi.fn().mockResolvedValue(undefined); + +function makeSharedClient(): SharedMCPClient { + return { + callTool: mockCallTool, + listTools: mockListTools, + close: mockClose, + release: mockRelease, + }; +} + +vi.mock('../../src/mcp/client.js', () => ({ + acquireSharedMCPClient: vi.fn(), +})); + +vi.mock('../../src/device/session.js', () => ({ + createPlatformSession: vi.fn(), +})); + +const { acquireSharedMCPClient } = await import('../../src/mcp/client.js'); +const { createPlatformSession } = await import('../../src/device/session.js'); +const { McpSession } = await import('../../src/sdk/mcp-session.js'); + +function makeConfig(overrides = {}) { + return { + MCP_TRANSPORT: 'stdio' as const, + MCP_HOST: 'localhost', + MCP_PORT: 8080, + ...overrides, + } as any; +} + +const mockScopedMcp: MCPClient = { + callTool: vi.fn().mockResolvedValue({ content: [] }), + listTools: vi.fn().mockResolvedValue([]), + close: vi.fn().mockResolvedValue(undefined), +}; + +beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(acquireSharedMCPClient).mockResolvedValue(makeSharedClient()); + vi.mocked(createPlatformSession).mockResolvedValue({ + platform: 'android', + sessionText: 'mock session', + sessionId: 'mock-session-id', + scopedMcp: mockScopedMcp, + } as any); +}); + +// ── Lazy connection ─────────────────────────────────────────────────────── + +describe('McpSession — lazy connection', () => { + test('does not connect on construction', () => { + new McpSession(makeConfig()); + expect(acquireSharedMCPClient).not.toHaveBeenCalled(); + }); + + test('connects on first connect() call', async () => { + const session = new McpSession(makeConfig()); + await session.connect(); + expect(acquireSharedMCPClient).toHaveBeenCalledOnce(); + }); + + test('passes correct MCPConfig to acquireSharedMCPClient', async () => { + const session = new McpSession( + makeConfig({ + MCP_TRANSPORT: 'sse', + MCP_HOST: '10.0.0.1', + MCP_PORT: 9090, + }) + ); + await session.connect(); + expect(acquireSharedMCPClient).toHaveBeenCalledWith({ + transport: 'sse', + host: '10.0.0.1', + port: 9090, + }); + }); +}); + +// ── Connection reuse ────────────────────────────────────────────────────── + +describe('McpSession — connection reuse', () => { + test('reuses the same connection on multiple connect() calls', async () => { + const session = new McpSession(makeConfig()); + await session.connect(); + await session.connect(); + await session.connect(); + expect(acquireSharedMCPClient).toHaveBeenCalledOnce(); + }); + + test('returns the same client instance on every call', async () => { + const session = new McpSession(makeConfig()); + const first = await session.connect(); + const second = await session.connect(); + expect(first.client).toBe(second.client); + }); + + test('caches the tool list after the first connect', async () => { + const session = new McpSession(makeConfig()); + await session.connect(); + await session.connect(); + // listTools is called once during the first connect, not again + expect(mockListTools).toHaveBeenCalledOnce(); + }); +}); + +// ── Tool list ───────────────────────────────────────────────────────────── + +describe('McpSession — tool list', () => { + test('returns the tool list from the MCP client', async () => { + const session = new McpSession(makeConfig()); + const { tools } = await session.connect(); + expect(tools).toHaveLength(2); + expect(tools[0].name).toBe('appium_click'); + expect(tools[1].name).toBe('appium_type'); + }); +}); + +// ── Release ─────────────────────────────────────────────────────────────── + +describe('McpSession — release', () => { + test('calls release() on the shared client handle', async () => { + const session = new McpSession(makeConfig()); + await session.connect(); + await session.release(); + expect(mockRelease).toHaveBeenCalledOnce(); + }); + + test('release without prior connect is a no-op', async () => { + const session = new McpSession(makeConfig()); + await expect(session.release()).resolves.not.toThrow(); + expect(mockRelease).not.toHaveBeenCalled(); + }); + + test('reconnects after release', async () => { + const session = new McpSession(makeConfig()); + await session.connect(); + await session.release(); + await session.connect(); + expect(acquireSharedMCPClient).toHaveBeenCalledTimes(2); + }); + + test('double release is a no-op on the second call', async () => { + const session = new McpSession(makeConfig()); + await session.connect(); + await session.release(); + await session.release(); + expect(mockRelease).toHaveBeenCalledOnce(); + }); + + test('clears cached tools after release', async () => { + const session = new McpSession(makeConfig()); + const before = await session.connect(); + await session.release(); + + // New tools returned after reconnect + const newTools: MCPToolInfo[] = [{ name: 'appium_screenshot' }]; + mockListTools.mockResolvedValueOnce(newTools); + const after = await session.connect(); + + expect(after.tools).toHaveLength(1); + expect(after.tools[0].name).toBe('appium_screenshot'); + }); +}); + +// ── Isolation ───────────────────────────────────────────────────────────── + +describe('McpSession — isolation', () => { + test('two sessions connect independently', async () => { + const session1 = new McpSession(makeConfig()); + const session2 = new McpSession(makeConfig()); + + await session1.connect(); + await session2.connect(); + + expect(acquireSharedMCPClient).toHaveBeenCalledTimes(2); + }); + + test('releasing one session does not affect the other', async () => { + const session1 = new McpSession(makeConfig()); + const session2 = new McpSession(makeConfig()); + + await session1.connect(); + await session2.connect(); + await session1.release(); + + // session2 should still have its handle + const { client } = await session2.connect(); + expect(client).toBeDefined(); + expect(acquireSharedMCPClient).toHaveBeenCalledTimes(2); // no new connection + }); +}); diff --git a/vscode-extension/package.json b/vscode-extension/package.json index 751d928..39a1250 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -2,7 +2,7 @@ "name": "appclaw", "displayName": "AppClaw", "description": "AI-powered mobile automation agent — control Android & iOS devices from VS Code", - "version": "0.1.0", + "version": "0.1.6", "publisher": "atddevs", "engines": { "vscode": "^1.85.0"