From 703fc643992d63b997d9de2dfa2e1ce19bad6a0b Mon Sep 17 00:00:00 2001 From: BB-fat <1056871944@qq.com> Date: Thu, 13 Aug 2026 15:04:34 +0000 Subject: [PATCH] =?UTF-8?q?feat(packages):=20add=20dsh-plugin-browserskill?= =?UTF-8?q?=20=E2=80=94=20DeepSeek=20Harness=20tools=20for=20bsk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new monorepo package that registers BrowserSkill (bsk) browser automation as model-visible tools in DeepSeek Harness (dsh): - 11 tools mapping to bsk CLI --json commands: session start/stop/list, navigate, snapshot, observe, click, fill, press, screenshot, emulate - multi-session support: optional session arg with a current-session pointer, session id echoed in every result, configurable concurrency cap (default 5), and dispose-time cleanup of plugin-started sessions - cancellation: exec.signal aborts kill the underlying bsk child process - terminal-style UI cards (command line as the call card, output as the result card); screenshots inline the PNG through the host attachment store when the model route accepts image input, else return the path - config: bskPath / defaultTimeoutMs / maxSessions via Schemastery schema - install guidance when the bsk binary is missing; bsk JSON error envelopes (code/message/hint) surfaced to the model - unit tests mock the bsk runner (no real browser required); the root lint script now also runs the package's typecheck + tests so the existing CI frontend job covers the new package (a dedicated CI job diff is provided in the PR; the pushing token lacks workflow scope) --- package.json | 2 +- packages/dsh-plugin-browserskill/.gitignore | 1 + packages/dsh-plugin-browserskill/README.md | 88 ++ .../dsh-plugin-browserskill/cordis.patch.yml | 3 + packages/dsh-plugin-browserskill/package.json | 54 + packages/dsh-plugin-browserskill/src/image.ts | 83 ++ packages/dsh-plugin-browserskill/src/index.ts | 80 ++ .../dsh-plugin-browserskill/src/runner.ts | 194 ++++ .../dsh-plugin-browserskill/src/sessions.ts | 92 ++ packages/dsh-plugin-browserskill/src/tools.ts | 853 ++++++++++++++++ .../tests/runner.test.ts | 137 +++ .../tests/sessions.test.ts | 55 + .../tests/tools.test.ts | 525 ++++++++++ .../dsh-plugin-browserskill/tsconfig.json | 15 + .../dsh-plugin-browserskill/tsdown.config.ts | 10 + .../dsh-plugin-browserskill/vitest.config.ts | 8 + pnpm-lock.yaml | 946 +++++++++++++++++- 17 files changed, 3135 insertions(+), 11 deletions(-) create mode 100644 packages/dsh-plugin-browserskill/.gitignore create mode 100644 packages/dsh-plugin-browserskill/README.md create mode 100644 packages/dsh-plugin-browserskill/cordis.patch.yml create mode 100644 packages/dsh-plugin-browserskill/package.json create mode 100644 packages/dsh-plugin-browserskill/src/image.ts create mode 100644 packages/dsh-plugin-browserskill/src/index.ts create mode 100644 packages/dsh-plugin-browserskill/src/runner.ts create mode 100644 packages/dsh-plugin-browserskill/src/sessions.ts create mode 100644 packages/dsh-plugin-browserskill/src/tools.ts create mode 100644 packages/dsh-plugin-browserskill/tests/runner.test.ts create mode 100644 packages/dsh-plugin-browserskill/tests/sessions.test.ts create mode 100644 packages/dsh-plugin-browserskill/tests/tools.test.ts create mode 100644 packages/dsh-plugin-browserskill/tsconfig.json create mode 100644 packages/dsh-plugin-browserskill/tsdown.config.ts create mode 100644 packages/dsh-plugin-browserskill/vitest.config.ts diff --git a/package.json b/package.json index 9c72f35..f4b4313 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "ext:dev": "pnpm --filter @browser-skill/extension dev", "ext:test": "pnpm --filter @browser-skill/extension test", "vom:test": "pnpm --filter @browser-skill/vom test", - "lint": "biome check . && stylelint \"**/*.css\"", + "lint": "biome check . && stylelint \"**/*.css\" && pnpm --filter dsh-plugin-browserskill typecheck && pnpm --filter dsh-plugin-browserskill test", "format": "biome format --write ." }, "devDependencies": { diff --git a/packages/dsh-plugin-browserskill/.gitignore b/packages/dsh-plugin-browserskill/.gitignore new file mode 100644 index 0000000..c3af857 --- /dev/null +++ b/packages/dsh-plugin-browserskill/.gitignore @@ -0,0 +1 @@ +lib/ diff --git a/packages/dsh-plugin-browserskill/README.md b/packages/dsh-plugin-browserskill/README.md new file mode 100644 index 0000000..7449914 --- /dev/null +++ b/packages/dsh-plugin-browserskill/README.md @@ -0,0 +1,88 @@ +# dsh-plugin-browserskill + +A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh) tool plugin that exposes +[BrowserSkill](https://github.com/Tencent/BrowserSkill) (`bsk`) browser automation to the model. + +Each tool maps to one `bsk --json` invocation: the plugin spawns the bsk CLI, parses its +structured JSON output, and returns a canonical typed value. The bsk daemon, browser, and Chrome +extension keep owning the actual browser control — this package is a thin, well-typed bridge. + +## Tools + +| Tool | bsk command | Purpose | +| --- | --- | --- | +| `browser_session_start` | `bsk session start` | Open an Agent Window session; optional initial URL, window size, and device emulation preset. Returns the session id and makes it the current session. | +| `browser_session_stop` | `bsk session stop` | Stop a session (the current one by default) and close its Agent Window. | +| `browser_session_list` | `bsk session list` | List daemon-side sessions, marking the plugin's current session. | +| `browser_navigate` | `bsk navigate` | Navigate the active tab, with `waitUntil` / timeout control. | +| `browser_snapshot` | `bsk snapshot` | Indented aria-tree snapshot with `@eN` refs for interaction tools. | +| `browser_observe` | `bsk observe` | Semantic VOM observation (read-only) with `@eN` refs. | +| `browser_click` | `bsk click` | Click a snapshot ref or CSS selector (button / click-count options). | +| `browser_fill` | `bsk fill` | Fill an input / textarea / contenteditable (clears first by default). | +| `browser_press` | `bsk press` | Dispatch a key or combo, optionally focusing a target first. | +| `browser_screenshot` | `bsk screenshot` | PNG capture of the tab or a ref-cropped element; inlines the image when the deployment supports image input, otherwise returns a file path. | +| `browser_emulate` | `bsk emulate` | Apply or clear mobile device emulation on the active tab. | + +## Multi-session model + +One agent conversation can drive several browser sessions at once: + +- `browser_session_start` returns the session id and makes it the **current session**. +- Every operation tool accepts an optional `session` argument. When omitted, the call acts on the + current session (the one most recently started or used); when given, that session becomes current. +- Every tool result echoes the session it actually acted on, so the model never has to guess. +- The number of concurrent sessions started through the plugin is capped (`maxSessions`, default 5). +- Unloading the plugin stops every session it started and kills in-flight bsk processes. + +## Installation + +The plugin follows the standard dsh bundle layout (`dsh.bundle` manifest + `cordis.patch.yml`): + +```sh +dsh plugin --profile add +dsh --profile +``` + +Prerequisite: the `bsk` CLI must be installed and on `PATH`, and the BrowserSkill Chrome extension +must be connected — see the [BrowserSkill README](https://github.com/Tencent/BrowserSkill). When bsk +is missing, tool calls fail with install guidance instead of a bare spawn error. + +## Configuration + +All fields are optional and validated through the plugin's Schemastery `Config`: + +```yaml +# cordis.patch.yml override example +- insert: + - id: browserskill + name: dsh-plugin-browserskill + config: + bskPath: bsk # path to the bsk binary (default: resolve from PATH) + defaultTimeoutMs: 120000 + maxSessions: 5 +``` + +## Behavior notes + +- **Cancellation**: aborting a tool call (`exec.signal`) kills the underlying bsk child process, + matching BrowserSkill's cooperative tool-cancellation model. +- **UI cards**: calls render as terminal cards (command line as title, output as the completed + card). Screenshots additionally attach the image itself when the host mounts an attachment store + and the active model route declares image input; otherwise the PNG path is returned. +- **Errors**: non-zero bsk exits surface the CLI's JSON error envelope (`code`, `message`, `hint`) + so the model gets the daemon's actionable guidance. +- **Long-running work** (e.g. `bsk record`) is not backgrounded via `ctx.jobs` yet — tracked as a + follow-up. + +## Development + +```sh +pnpm install +pnpm --filter dsh-plugin-browserskill typecheck +pnpm --filter dsh-plugin-browserskill test # unit tests mock bsk; no browser needed +pnpm --filter dsh-plugin-browserskill build # tsdown -> lib/ +``` + +## License + +MIT diff --git a/packages/dsh-plugin-browserskill/cordis.patch.yml b/packages/dsh-plugin-browserskill/cordis.patch.yml new file mode 100644 index 0000000..8c04006 --- /dev/null +++ b/packages/dsh-plugin-browserskill/cordis.patch.yml @@ -0,0 +1,3 @@ +- insert: + - id: browserskill + name: dsh-plugin-browserskill diff --git a/packages/dsh-plugin-browserskill/package.json b/packages/dsh-plugin-browserskill/package.json new file mode 100644 index 0000000..430edfa --- /dev/null +++ b/packages/dsh-plugin-browserskill/package.json @@ -0,0 +1,54 @@ +{ + "name": "dsh-plugin-browserskill", + "description": "DeepSeek Harness tool plugin that exposes BrowserSkill (bsk) browser automation to the model", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "lib/index.mjs", + "types": "lib/index.d.mts", + "exports": { + ".": { + "types": "./lib/index.d.mts", + "default": "./lib/index.mjs" + }, + "./package.json": "./package.json" + }, + "files": [ + "lib", + "cordis.patch.yml" + ], + "dsh": { + "bundle": { + "patch": "./cordis.patch.yml" + } + }, + "scripts": { + "build": "tsdown", + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.6" + }, + "peerDependenciesMeta": { + "@deepseek-ai/dsh-attachment": { + "optional": true + }, + "@deepseek-ai/dsh-llm": { + "optional": true + } + }, + "devDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.6", + "@deepseek-ai/dsh-attachment": "^0.1.0-rc.6", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.6", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.6", + "@deepseek-ai/schemastery": "^3.18.1", + "@types/node": "^22.10.0", + "tsdown": "^0.22.14", + "typescript": "^5.8.0", + "vitest": "^4.1.6" + } +} diff --git a/packages/dsh-plugin-browserskill/src/image.ts b/packages/dsh-plugin-browserskill/src/image.ts new file mode 100644 index 0000000..0c050ee --- /dev/null +++ b/packages/dsh-plugin-browserskill/src/image.ts @@ -0,0 +1,83 @@ +/** + * Screenshot delivery. The canonical tool value stays plain JSON (a PNG file + * path plus pixel metadata); when the host mounts a durable attachment store + * AND the calling route declares image input, the PNG bytes are additionally + * committed through `ctx.attachments` so the render step can attach the image + * itself to the tool result. Any uncertainty (no store, unknown route, + * text-only model) falls back to the path-only form. + */ + +import type { Context } from "@deepseek-ai/cordis"; +import type { ImageAttachmentRef } from "@deepseek-ai/dsh-attachment"; +import type { ToolExecution } from "@deepseek-ai/dsh-tools"; + +/** Structural view of the attachment seam (avoids a runtime dependency). */ +interface AttachmentLike { + imageLimits: { + mediaTypes: readonly string[]; + maxImageBytes: number; + maxMessageImageBytes: number; + }; + saveImage(input: { + data: Uint8Array; + mediaType: string; + name?: string; + }): Promise; +} + +interface LlmLike { + resolveModelInfo( + provider: string, + model: string, + signal?: AbortSignal, + ): Promise<{ inputModalities?: readonly string[] }>; +} + +/** + * Commit screenshot bytes to the host attachment store when the composition + * supports durable images on the current model route. + * @returns the durable reference, or undefined to stay in path-only mode. + */ +export async function trySaveScreenshot( + ctx: Context, + exec: ToolExecution, + data: Uint8Array, + name: string, +): Promise { + const attachments = ctx.get("attachments") as AttachmentLike | undefined; + if (attachments === undefined) return undefined; + if (!attachments.imageLimits.mediaTypes.includes("image/png")) return undefined; + if ( + data.byteLength > + Math.min(attachments.imageLimits.maxImageBytes, attachments.imageLimits.maxMessageImageBytes) + ) { + return undefined; + } + if (!(await isImageCapableRoute(ctx, exec))) return undefined; + try { + return await attachments.saveImage({ data, mediaType: "image/png", name }); + } catch { + // A store hiccup must not fail the tool call; the PNG path still works. + return undefined; + } +} + +/** + * Best-effort check that the calling route's model accepts image input, + * mirroring dsh-tool-fs `read_image`. Unknown route / missing llm service + * answers false (refuse to attach) so history never gains an image block a + * text-only adapter cannot replay. + */ +async function isImageCapableRoute(ctx: Context, exec: ToolExecution): Promise { + const llm = ctx.get("llm") as LlmLike | undefined; + const provider = + exec.agent?.session.requestHeader()?.config.provider ?? exec.agent?.options.provider; + const model = exec.agent?.session.requestHeader()?.config.model ?? exec.agent?.options.model; + if (llm === undefined || provider === undefined || model === undefined) return false; + try { + const info = await llm.resolveModelInfo(provider, model, exec.signal); + return info.inputModalities?.includes("image") ?? false; + } catch { + return false; + } +} diff --git a/packages/dsh-plugin-browserskill/src/index.ts b/packages/dsh-plugin-browserskill/src/index.ts new file mode 100644 index 0000000..7e047ad --- /dev/null +++ b/packages/dsh-plugin-browserskill/src/index.ts @@ -0,0 +1,80 @@ +/** + * dsh-plugin-browserskill — a DeepSeek Harness bundle that registers + * BrowserSkill (`bsk`) browser automation as model-visible tools. + * + * Each tool spawns `bsk --json`, parses the structured output, and + * returns a canonical JSON value. The plugin tracks the sessions it starts so + * one agent conversation can drive several browsers at once; unloading the + * plugin stops every tracked session and kills any in-flight bsk children. + * + * @module dsh-plugin-browserskill + */ + +import type { Context } from "@deepseek-ai/cordis"; +import Schema from "@deepseek-ai/schemastery"; +import { createBskRunner } from "./runner"; +import { SessionRegistry } from "./sessions"; +import { type PluginConfig, registerTools } from "./tools"; + +export const name = "dsh-plugin-browserskill"; +export const inject = ["tools"]; + +/** Runtime configuration schema (validated and defaulted by Cordis). */ +export const Config = Schema.object({ + bskPath: Schema.string() + .default("bsk") + .description("Path to the bsk CLI binary (defaults to resolving `bsk` from PATH)."), + defaultTimeoutMs: Schema.number() + .default(120_000) + .description("Default per-command timeout in milliseconds."), + maxSessions: Schema.number() + .default(5) + .description("Maximum number of concurrent browser sessions started through this plugin."), +}); + +export type Config = PluginConfig; + +export function apply(ctx: Context, config: Partial = {}): void { + const resolved: PluginConfig = { + bskPath: config.bskPath ?? "bsk", + defaultTimeoutMs: config.defaultTimeoutMs ?? 120_000, + maxSessions: config.maxSessions ?? 5, + }; + const runner = createBskRunner(resolved.bskPath); + const registry = new SessionRegistry(resolved.maxSessions); + + registerTools({ ctx, runner, registry, config: resolved }); + + // Non-blocking install probe: warn early when bsk is missing instead of + // failing the first tool call with a bare spawn error. + runner.run(["status"], { timeoutMs: 10_000 }).then( + () => {}, + (error: unknown) => { + const detail = error instanceof Error ? error.message : String(error); + console.warn( + `[${name}] bsk probe failed (${detail}); browser tools will report install guidance until the bsk CLI is available`, + ); + }, + ); + + // Unload cleanup: kill in-flight children, then stop every session this + // plugin started (best effort; sessions started outside the plugin are + // left running). + ctx.effect(() => { + return () => { + runner.killAll(); + const stops = registry + .list() + .map((session) => + runner.run(["session", "stop", session.sessionId], { timeoutMs: 15_000 }).catch(() => {}), + ); + return Promise.all(stops).then(() => {}); + }; + }); +} + +export type { BskRunner, BskRunResult, SpawnImpl } from "./runner"; +export { BskError, createBskRunner } from "./runner"; +export { SessionRegistry } from "./sessions"; +export type { PluginConfig, ToolDeps } from "./tools"; +export { registerTools } from "./tools"; diff --git a/packages/dsh-plugin-browserskill/src/runner.ts b/packages/dsh-plugin-browserskill/src/runner.ts new file mode 100644 index 0000000..3f05426 --- /dev/null +++ b/packages/dsh-plugin-browserskill/src/runner.ts @@ -0,0 +1,194 @@ +/** + * Process runner for the `bsk` CLI. Every model-facing tool in this plugin + * maps to one `bsk --json` invocation: spawn the child, capture + * stdout/stderr, honor the dsh cancellation signal by killing the child, and + * map the CLI's JSON error envelope onto a thrown `BskError`. + */ + +import { type ChildProcess, spawn } from "node:child_process"; + +/** Shape of the JSON error envelope `bsk --json` prints on failure. */ +export interface BskErrorBody { + code?: string; + message?: string; + hint?: string; + exit_code?: number; + data?: unknown; +} + +/** A failed `bsk` invocation (non-zero exit, timeout, or spawn failure). */ +export class BskError extends Error { + readonly code?: string; + readonly hint?: string; + readonly exitCode?: number | null; + readonly timedOut: boolean; + + constructor( + message: string, + options: { code?: string; hint?: string; exitCode?: number | null; timedOut?: boolean } = {}, + ) { + super(message); + this.name = "BskError"; + this.code = options.code; + this.hint = options.hint; + this.exitCode = options.exitCode; + this.timedOut = options.timedOut ?? false; + } +} + +export interface BskRunResult { + /** Process exit code, or null when killed by a signal / never started. */ + code: number | null; + stdout: string; + stderr: string; + timedOut: boolean; + aborted: boolean; +} + +export interface BskRunOptions { + signal?: AbortSignal; + timeoutMs?: number; +} + +/** Minimal spawn signature so tests can substitute a fake child process. */ +export type SpawnImpl = (command: string, args: string[]) => ChildProcess; + +export interface BskRunner { + /** Run `bsk --json` and collect its output. */ + run(args: string[], options?: BskRunOptions): Promise; + /** Kill every in-flight child (used when the plugin unloads). */ + killAll(): void; +} + +const KILL_GRACE_MS = 2000; + +export function createBskRunner(bskPath: string, spawnImpl: SpawnImpl = spawn): BskRunner { + const live = new Set(); + + function killChild(child: ChildProcess): void { + if (child.exitCode !== null || child.signalCode !== null) return; + child.kill("SIGTERM"); + const force = setTimeout(() => { + if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL"); + }, KILL_GRACE_MS); + force.unref(); + } + + return { + run(args, options = {}) { + return new Promise((resolve, reject) => { + let child: ChildProcess; + try { + child = spawnImpl(bskPath, [...args, "--json"]); + } catch (error) { + reject(error); + return; + } + live.add(child); + + let stdout = ""; + let stderr = ""; + let timedOut = false; + let aborted = false; + child.stdout?.on("data", (chunk: Buffer | string) => { + stdout += chunk; + }); + child.stderr?.on("data", (chunk: Buffer | string) => { + stderr += chunk; + }); + + const timeoutMs = options.timeoutMs; + const timer = + timeoutMs !== undefined && timeoutMs > 0 + ? setTimeout(() => { + timedOut = true; + killChild(child); + }, timeoutMs) + : undefined; + timer?.unref(); + + const onAbort = () => { + aborted = true; + killChild(child); + }; + if (options.signal?.aborted) { + onAbort(); + } else { + options.signal?.addEventListener("abort", onAbort, { once: true }); + } + + const settle = () => { + if (timer !== undefined) clearTimeout(timer); + options.signal?.removeEventListener("abort", onAbort); + live.delete(child); + }; + + child.on("error", (error) => { + settle(); + reject(error); + }); + child.on("close", (code) => { + settle(); + resolve({ code, stdout, stderr, timedOut, aborted }); + }); + }); + }, + killAll() { + for (const child of live) killChild(child); + }, + }; +} + +/** True when the spawn failure means the bsk binary itself is missing. */ +export function isCommandNotFound(error: unknown): boolean { + return ( + typeof error === "object" && + error !== null && + "code" in error && + (error as { code?: unknown }).code === "ENOENT" + ); +} + +/** Install guidance shown when the bsk CLI cannot be spawned. */ +export function bskInstallMessage(bskPath: string): string { + return ( + `the bsk CLI ("${bskPath}") was not found. BrowserSkill must be installed and on PATH ` + + "for browser tools to work — install it from https://github.com/Tencent/BrowserSkill " + + "(see the README install script or `cargo install`), then retry." + ); +} + +/** + * Interpret one finished run: throw `BskError` on timeout / non-zero exit + * (parsing the CLI's JSON error envelope when present), otherwise parse and + * return the stdout JSON payload. + */ +export function parseBskJson(result: BskRunResult, commandLabel: string): unknown { + if (result.timedOut) { + throw new BskError(`bsk ${commandLabel} timed out`, { timedOut: true }); + } + const body = result.stdout.trim(); + if (result.code !== 0) { + let parsed: BskErrorBody | undefined; + try { + parsed = JSON.parse(body) as BskErrorBody; + } catch { + parsed = undefined; + } + const message = + parsed?.message ?? (result.stderr.trim() || body || `bsk ${commandLabel} failed`); + throw new BskError(`bsk ${commandLabel} failed: ${message}`, { + code: parsed?.code, + hint: parsed?.hint, + exitCode: result.code, + }); + } + try { + return JSON.parse(body); + } catch { + throw new BskError( + `bsk ${commandLabel} did not produce JSON output: ${body.slice(0, 200) || "(empty)"}`, + { exitCode: result.code }, + ); + } +} diff --git a/packages/dsh-plugin-browserskill/src/sessions.ts b/packages/dsh-plugin-browserskill/src/sessions.ts new file mode 100644 index 0000000..acf125a --- /dev/null +++ b/packages/dsh-plugin-browserskill/src/sessions.ts @@ -0,0 +1,92 @@ +/** + * Tracks the bsk sessions started through this plugin and the "current" + * session pointer used when a tool call omits its optional `session` arg. + * The pointer moves to whatever session was most recently started, stopped, + * or operated on, so a model can run several browsers side by side without + * repeating the id on every call. + */ + +export interface TrackedSession { + sessionId: string; + browserInstanceId?: string; + startedAtMs: number; +} + +export class SessionRegistry { + private readonly sessions = new Map(); + private currentId: string | undefined; + + constructor(private readonly maxSessions: number) {} + + /** + * Register a freshly started session and make it current. + * @throws when the configured concurrency cap is already reached. + */ + add(session: TrackedSession): void { + if (!this.sessions.has(session.sessionId) && this.sessions.size >= this.maxSessions) { + throw new Error( + `session limit reached (${this.maxSessions} concurrent sessions); ` + + "stop one with browser_session_stop before starting another", + ); + } + this.sessions.set(session.sessionId, session); + this.currentId = session.sessionId; + } + + /** Forget a stopped session; falls back to the most recent remaining one. */ + remove(sessionId: string): void { + this.sessions.delete(sessionId); + if (this.currentId === sessionId) { + const rest = [...this.sessions.values()]; + this.currentId = rest.length > 0 ? rest[rest.length - 1].sessionId : undefined; + } + } + + /** Mark a session as most recently used. Unknown ids are adopted. */ + touch(sessionId: string): void { + const existing = this.sessions.get(sessionId); + if (existing !== undefined) { + // Re-insert to refresh recency order. + this.sessions.delete(sessionId); + this.sessions.set(sessionId, existing); + } else { + this.sessions.set(sessionId, { sessionId, startedAtMs: Date.now() }); + } + this.currentId = sessionId; + } + + /** The current session id, if any. */ + current(): string | undefined { + return this.currentId; + } + + /** Tracked sessions in least- to most-recently-used order. */ + list(): TrackedSession[] { + return [...this.sessions.values()]; + } + + size(): number { + return this.sessions.size; + } + + /** + * Resolve the session a tool call acts on: an explicit `session` argument + * wins (and becomes current); otherwise fall back to the current session. + * @throws when neither is available. + */ + resolve(explicit: string | undefined, toolName: string): string { + if (explicit !== undefined && explicit.trim().length > 0) { + this.touch(explicit); + return explicit; + } + const current = this.current(); + if (current === undefined) { + throw new Error( + `${toolName} needs a session but none is active — start one with browser_session_start ` + + "or pass an explicit `session` id", + ); + } + this.touch(current); + return current; + } +} diff --git a/packages/dsh-plugin-browserskill/src/tools.ts b/packages/dsh-plugin-browserskill/src/tools.ts new file mode 100644 index 0000000..739bcc5 --- /dev/null +++ b/packages/dsh-plugin-browserskill/src/tools.ts @@ -0,0 +1,853 @@ +/** + * Model-facing tool definitions. Each tool maps to one `bsk --json` + * invocation: spawn via the runner, parse the JSON envelope, and return a + * canonical value whose `output.render` produces the model-facing text. + * UI cards reuse the terminal style: the pending card is the command line, + * the completed card carries the output. + */ + +import { readFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import type { Context } from "@deepseek-ai/cordis"; +import type { ContentBlock } from "@deepseek-ai/dsh-llm"; +import { defineTool, type ToolResult, type ToolRunContext } from "@deepseek-ai/dsh-tools"; +import { trySaveScreenshot } from "./image"; +import { type BskRunner, bskInstallMessage, isCommandNotFound, parseBskJson } from "./runner"; +import type { SessionRegistry } from "./sessions"; + +/** Plugin configuration resolved from the Schemastery schema in index.ts. */ +export interface PluginConfig { + bskPath: string; + defaultTimeoutMs: number; + maxSessions: number; +} + +export interface ToolDeps { + ctx: Context; + runner: BskRunner; + registry: SessionRegistry; + config: PluginConfig; +} + +/** Device presets supported by `bsk emulate --device`. */ +const DEVICE_PRESETS = [ + "iphone-14", + "iphone-14-pro-max", + "iphone-se", + "pixel-7", + "galaxy-s23", + "ipad-mini", + "galaxy-tab-s8", +] as const; + +const SESSION_PARAM = { + type: "string", + description: + "bsk session id to act on. Omit to use the current session (the one most recently started or used).", +} as const; + +/** Run one bsk command and return its parsed JSON payload, or throw. */ +async function runBsk( + deps: ToolDeps, + exec: ToolRunContext, + args: string[], + label: string, +): Promise { + let result; + try { + result = await deps.runner.run(args, { + signal: exec.signal, + timeoutMs: deps.config.defaultTimeoutMs, + }); + } catch (error) { + if (isCommandNotFound(error)) { + throw new Error(bskInstallMessage(deps.config.bskPath)); + } + throw error; + } + if (result.aborted) { + const error = new Error("tool call aborted"); + error.name = "AbortError"; + throw error; + } + return parseBskJson(result, label); +} + +/** Build the command line shown on the pending terminal card (pure). */ +function cmdline(deps: ToolDeps, args: string[]): string { + return [deps.config.bskPath, ...args].join(" "); +} + +/** Shared completed-card presenter: terminal card with the rendered text. */ +function presentTerminalResult(_args: never, result: ToolResult) { + const block = result.content.length === 1 ? result.content[0] : undefined; + if (block === undefined || block.type !== "text") return undefined; + if (result.isError) return undefined; + return { card: "terminal" as const, output: block.text, exitCode: 0 }; +} + +function abortError(): Error { + const error = new Error("tool call aborted"); + error.name = "AbortError"; + return error; +} + +/** Register the full browser tool suite. */ +export function registerTools(deps: ToolDeps): void { + const { ctx, registry } = deps; + + ctx.tools.register( + defineTool({ + name: "browser_session_start", + description: + "Start a new browser session: opens an Agent Window in the connected browser and returns " + + "its session id. The new session becomes the current session for subsequent browser_* calls. " + + "Optionally navigate to an initial URL and/or apply a mobile device emulation preset.", + parameters: { + url: { + type: "string", + description: "Initial URL to navigate to after the session starts.", + }, + width: { + type: "integer", + description: "Agent Window outer width in CSS pixels (100..=7680). Requires height.", + }, + height: { + type: "integer", + description: "Agent Window outer height in CSS pixels (100..=7680). Requires width.", + }, + noFocus: { + type: "boolean", + description: "Open the Agent Window in the background without stealing focus.", + }, + browser: { + type: "string", + description: + "Target browser instance id (only needed when multiple browsers are connected).", + }, + device: { + type: "string", + enum: DEVICE_PRESETS, + description: "Mobile device emulation preset applied to the tab after start.", + }, + }, + output: { + schema: { + type: "object", + additionalProperties: false, + properties: { + sessionId: { type: "string", required: true }, + browserInstanceId: { type: "string", required: true }, + url: { type: "string" }, + device: { type: "string" }, + }, + }, + render: (_args, value) => [ + { + type: "text", + text: + `started browser session ${value.sessionId}` + + (value.url !== undefined ? ` and navigated to ${value.url}` : "") + + (value.device !== undefined ? ` (device: ${value.device})` : ""), + }, + ], + }, + async execute(args, exec) { + if (args.url !== undefined && args.url.trim().length === 0) { + throw new Error("url must be a non-empty string"); + } + if ((args.width === undefined) !== (args.height === undefined)) { + throw new Error("width and height must be given together"); + } + const startArgs = ["session", "start"]; + if (args.width !== undefined && args.height !== undefined) { + startArgs.push("--width", String(args.width), "--height", String(args.height)); + } + if (args.noFocus === true) startArgs.push("--no-focus"); + if (args.browser !== undefined) startArgs.push("--browser", args.browser); + const reply = (await runBsk(deps, exec, startArgs, "session start")) as { + session_id: string; + browser_instance_id: string; + }; + registry.add({ + sessionId: reply.session_id, + browserInstanceId: reply.browser_instance_id, + startedAtMs: Date.now(), + }); + try { + if (args.device !== undefined) { + await runBsk( + deps, + exec, + ["emulate", "--session", reply.session_id, "--device", args.device], + "emulate", + ); + } + if (args.url !== undefined) { + await runBsk( + deps, + exec, + ["navigate", "--session", reply.session_id, args.url], + "navigate", + ); + } + } catch (error) { + // A half-initialized session must not leak: stop it before surfacing. + await deps.runner + .run(["session", "stop", reply.session_id], { timeoutMs: 30_000 }) + .catch(() => {}); + registry.remove(reply.session_id); + throw error; + } + return { + sessionId: reply.session_id, + browserInstanceId: reply.browser_instance_id, + ...(args.url !== undefined ? { url: args.url } : {}), + ...(args.device !== undefined ? { device: args.device } : {}), + }; + }, + presentCall: (args) => ({ + card: "terminal", + title: cmdline(deps, [ + "session", + "start", + ...(args.device !== undefined ? ["+ emulate", args.device] : []), + ...(args.url !== undefined ? ["+ navigate", args.url] : []), + ]), + description: "Start a browser session", + }), + presentResult: presentTerminalResult, + }), + ); + + ctx.tools.register( + defineTool({ + name: "browser_session_stop", + description: + "Stop a browser session and close its Agent Window. Stops the given session, or the " + + "current session when `session` is omitted.", + parameters: { session: SESSION_PARAM }, + output: { + schema: { + type: "object", + additionalProperties: false, + properties: { stopped: { type: "string", required: true } }, + }, + render: (_args, value) => [ + { type: "text", text: `stopped browser session ${value.stopped}` }, + ], + }, + async execute(args, exec) { + const sessionId = registry.resolve(args.session, "browser_session_stop"); + await runBsk(deps, exec, ["session", "stop", sessionId], "session stop"); + registry.remove(sessionId); + return { stopped: sessionId }; + }, + presentCall: (args) => ({ + card: "terminal", + title: cmdline(deps, ["session", "stop", args.session ?? "(current session)"]), + description: "Stop a browser session", + }), + presentResult: presentTerminalResult, + }), + ); + + ctx.tools.register( + defineTool({ + name: "browser_session_list", + description: + "List active browser sessions known to the bsk daemon. The session marked `current` is " + + "the one browser_* tools act on when no explicit `session` is passed.", + parameters: {}, + output: { + schema: { + type: "object", + additionalProperties: false, + properties: { + sessions: { + type: "array", + required: true, + items: { + type: "object", + additionalProperties: false, + properties: { + sessionId: { type: "string", required: true }, + browserInstanceId: { type: "string", required: true }, + current: { type: "boolean", required: true }, + }, + }, + }, + }, + }, + render: (_args, value) => [ + { + type: "text", + text: + value.sessions.length === 0 + ? "no active browser sessions" + : value.sessions + .map( + (s) => + `${s.sessionId} (browser ${s.browserInstanceId})${s.current ? " [current]" : ""}`, + ) + .join("\n"), + }, + ], + }, + isConcurrencySafe: () => true, + async execute(_args, exec) { + const reply = (await runBsk(deps, exec, ["session", "list"], "session list")) as { + session_id: string; + browser_instance_id: string; + }[]; + const current = registry.current(); + return { + sessions: reply.map((entry) => ({ + sessionId: entry.session_id, + browserInstanceId: entry.browser_instance_id, + current: entry.session_id === current, + })), + }; + }, + presentCall: () => ({ + card: "terminal", + title: cmdline(deps, ["session", "list"]), + description: "List browser sessions", + }), + presentResult: presentTerminalResult, + }), + ); + + ctx.tools.register( + defineTool({ + name: "browser_navigate", + description: + "Navigate the session's active tab to a URL and wait for a page lifecycle phase " + + "(default: load). Returns the final URL after redirects.", + parameters: { + url: { type: "string", required: true, description: "Destination URL." }, + session: SESSION_PARAM, + waitUntil: { + type: "string", + enum: ["load", "domcontentloaded", "networkidle", "commit"], + description: "Lifecycle phase to wait for (default: load).", + }, + timeoutMs: { + type: "integer", + description: "Navigation wait timeout in milliseconds (default 30000).", + }, + }, + output: { + schema: { + type: "object", + additionalProperties: false, + properties: { + session: { type: "string", required: true }, + tabId: { type: "integer", required: true }, + url: { type: "string", required: true }, + finalUrl: { type: "string" }, + reached: { type: "string", required: true }, + }, + }, + render: (_args, value) => [ + { + type: "text", + text: + `[session ${value.session}] navigated to ${value.finalUrl ?? value.url} (reached: ${value.reached})` + + (value.reached === "timeout" + ? " — the wait timed out before the requested phase" + : ""), + }, + ], + }, + async execute(args, exec) { + if (args.url.trim().length === 0) throw new Error("url must be a non-empty string"); + const sessionId = registry.resolve(args.session, "browser_navigate"); + const cmdArgs = ["navigate", "--session", sessionId]; + if (args.waitUntil !== undefined) cmdArgs.push("--wait-until", args.waitUntil); + if (args.timeoutMs !== undefined) cmdArgs.push("--timeout", `${args.timeoutMs}ms`); + cmdArgs.push(args.url); + const reply = (await runBsk(deps, exec, cmdArgs, "navigate")) as { + tab_id: number; + url: string; + final_url?: string; + reached: string; + }; + return { + session: sessionId, + tabId: reply.tab_id, + url: reply.url, + ...(reply.final_url !== undefined ? { finalUrl: reply.final_url } : {}), + reached: reply.reached, + }; + }, + presentCall: (args) => ({ + card: "terminal", + title: cmdline(deps, ["navigate", "--session", args.session ?? "(current)", args.url]), + description: "Navigate to a URL", + }), + presentResult: presentTerminalResult, + }), + ); + + const registerObservationTool = (kind: "snapshot" | "observe") => { + const isSnapshot = kind === "snapshot"; + const name = isSnapshot ? "browser_snapshot" : "browser_observe"; + const description = isSnapshot + ? "Capture an indented aria-tree snapshot of the session's active tab. Interactive elements " + + "carry @eN refs for browser_click / browser_fill. Prefer browser_observe for a richer semantic view." + : "Produce a semantic VOM observation of the session's active tab (roles, states, perception " + + "probes) with @eN refs for browser_click / browser_fill. Read-only: never submits input."; + ctx.tools.register( + defineTool({ + name, + description, + parameters: { + session: SESSION_PARAM, + maxDepth: { type: "integer", description: "Cap on tree depth before truncating." }, + maxTokens: { + type: "integer", + description: "Soft cap on rendered tokens (~4 chars/token).", + }, + }, + output: { + schema: { + type: "object", + additionalProperties: false, + properties: { + session: { type: "string", required: true }, + tabId: { type: "integer", required: true }, + text: { type: "string", required: true }, + refCount: { type: "integer", required: true }, + truncated: { type: "boolean", required: true }, + }, + }, + render: (_args, value) => [ + { + type: "text", + text: + value.text.length > 0 + ? value.text + (value.truncated ? "\n(truncated — re-run with looser caps)" : "") + : "(empty observation — page may still be loading)", + }, + ], + }, + isConcurrencySafe: () => true, + async execute(args, exec) { + const sessionId = registry.resolve(args.session, name); + const cmdArgs = [kind, "--session", sessionId]; + if (args.maxDepth !== undefined) cmdArgs.push("--max-depth", String(args.maxDepth)); + if (args.maxTokens !== undefined) cmdArgs.push("--max-tokens", String(args.maxTokens)); + const reply = (await runBsk(deps, exec, cmdArgs, kind)) as { + text: string; + ref_count: number; + tab_id: number; + truncated?: boolean; + }; + return { + session: sessionId, + tabId: reply.tab_id, + text: reply.text, + refCount: reply.ref_count, + truncated: reply.truncated ?? false, + }; + }, + presentCall: (args) => ({ + card: "terminal", + title: cmdline(deps, [kind, "--session", args.session ?? "(current)"]), + description: isSnapshot ? "Capture an aria snapshot" : "Observe the page semantically", + }), + presentResult: presentTerminalResult, + }), + ); + }; + registerObservationTool("snapshot"); + registerObservationTool("observe"); + + ctx.tools.register( + defineTool({ + name: "browser_click", + description: + "Click an element in the session's active tab. Target is a snapshot ref (@e3) from the " + + "last browser_snapshot/browser_observe, or a CSS selector.", + parameters: { + target: { + type: "string", + required: true, + description: "Snapshot ref (@e3 / e3) or CSS selector of the element to click.", + }, + session: SESSION_PARAM, + button: { + type: "string", + enum: ["left", "middle", "right"], + description: "Mouse button (default: left).", + }, + clickCount: { + type: "integer", + description: "Number of consecutive presses (double-click = 2).", + }, + }, + output: { + schema: { + type: "object", + additionalProperties: false, + properties: { + session: { type: "string", required: true }, + tabId: { type: "integer", required: true }, + x: { type: "number", required: true }, + y: { type: "number", required: true }, + }, + }, + render: (_args, value) => [ + { + type: "text", + text: `[session ${value.session}] clicked at (${value.x}, ${value.y}) on tab ${value.tabId}`, + }, + ], + }, + async execute(args, exec) { + if (args.target.trim().length === 0) throw new Error("target must be a non-empty string"); + const sessionId = registry.resolve(args.session, "browser_click"); + const cmdArgs = ["click", "--session", sessionId]; + if (args.button !== undefined) cmdArgs.push("--button", args.button); + if (args.clickCount !== undefined) cmdArgs.push("--click-count", String(args.clickCount)); + cmdArgs.push(args.target); + const reply = (await runBsk(deps, exec, cmdArgs, "click")) as { + tab_id: number; + x: number; + y: number; + }; + return { session: sessionId, tabId: reply.tab_id, x: reply.x, y: reply.y }; + }, + presentCall: (args) => ({ + card: "terminal", + title: cmdline(deps, ["click", args.target, "--session", args.session ?? "(current)"]), + description: "Click an element", + }), + presentResult: presentTerminalResult, + }), + ); + + ctx.tools.register( + defineTool({ + name: "browser_fill", + description: + "Fill an input / textarea / contenteditable element, clearing it first by default. " + + "Target is a snapshot ref (@e3) or a CSS selector.", + parameters: { + target: { + type: "string", + required: true, + description: "Snapshot ref (@e3 / e3) or CSS selector of the field.", + }, + value: { type: "string", required: true, description: "Text to type into the element." }, + session: SESSION_PARAM, + noClear: { + type: "boolean", + description: "Skip the default wipe-the-field-first pass (append instead).", + }, + }, + output: { + schema: { + type: "object", + additionalProperties: false, + properties: { + session: { type: "string", required: true }, + tabId: { type: "integer", required: true }, + valueLength: { type: "integer", required: true }, + }, + }, + render: (_args, value) => [ + { + type: "text", + text: `[session ${value.session}] filled field on tab ${value.tabId} (${value.valueLength} chars)`, + }, + ], + }, + async execute(args, exec) { + if (args.target.trim().length === 0) throw new Error("target must be a non-empty string"); + const sessionId = registry.resolve(args.session, "browser_fill"); + const cmdArgs = ["fill", "--session", sessionId, "--value", args.value]; + if (args.noClear === true) cmdArgs.push("--no-clear"); + cmdArgs.push(args.target); + const reply = (await runBsk(deps, exec, cmdArgs, "fill")) as { + tab_id: number; + value_length: number; + }; + return { session: sessionId, tabId: reply.tab_id, valueLength: reply.value_length }; + }, + presentCall: (args) => ({ + card: "terminal", + title: cmdline(deps, ["fill", args.target, "--session", args.session ?? "(current)"]), + description: `Fill a field with ${args.value.length} chars`, + }), + presentResult: presentTerminalResult, + }), + ); + + ctx.tools.register( + defineTool({ + name: "browser_press", + description: + "Dispatch a keyboard key or combo (Enter, Escape, ArrowDown, Ctrl+A, …) to the session's " + + "active tab, optionally focusing a target element first.", + parameters: { + key: { + type: "string", + required: true, + description: "Key spec: a CDP key name (Enter, a, ArrowLeft) or a combo (Ctrl+A).", + }, + session: SESSION_PARAM, + target: { + type: "string", + description: "Snapshot ref (@e3) or CSS selector to focus before pressing.", + }, + holdMs: { + type: "integer", + description: "Hold the key down for N milliseconds between keyDown and keyUp.", + }, + }, + output: { + schema: { + type: "object", + additionalProperties: false, + properties: { + session: { type: "string", required: true }, + tabId: { type: "integer", required: true }, + key: { type: "string", required: true }, + }, + }, + render: (_args, value) => [ + { + type: "text", + text: `[session ${value.session}] pressed ${value.key} on tab ${value.tabId}`, + }, + ], + }, + async execute(args, exec) { + if (args.key.trim().length === 0) throw new Error("key must be a non-empty string"); + const sessionId = registry.resolve(args.session, "browser_press"); + const cmdArgs = ["press", "--session", sessionId]; + if (args.target !== undefined) { + if (args.target.trim().length === 0) throw new Error("target must be a non-empty string"); + // bsk distinguishes ref vs selector via flags; positional detection is CLI-side for + // click/fill only, so pass the explicit flag that matches the target's shape. + if (/^@?e\d+$/.test(args.target)) { + cmdArgs.push("--ref", args.target); + } else { + cmdArgs.push("--selector", args.target); + } + } + if (args.holdMs !== undefined) cmdArgs.push("--hold-ms", String(args.holdMs)); + cmdArgs.push(args.key); + const reply = (await runBsk(deps, exec, cmdArgs, "press")) as { + tab_id: number; + key: string; + }; + return { session: sessionId, tabId: reply.tab_id, key: reply.key }; + }, + presentCall: (args) => ({ + card: "terminal", + title: cmdline(deps, ["press", args.key, "--session", args.session ?? "(current)"]), + description: "Press a key", + }), + presentResult: presentTerminalResult, + }), + ); + + ctx.tools.register( + defineTool({ + name: "browser_screenshot", + description: + "Capture a PNG screenshot of the session's active tab, or crop to a snapshot ref element. " + + "Returns the image itself when the deployment supports image input, otherwise a file path.", + parameters: { + session: SESSION_PARAM, + ref: { + type: "string", + description: "Snapshot ref (@e3) from the last snapshot/observe; crops to that element.", + }, + }, + output: { + schema: { + type: "object", + additionalProperties: false, + properties: { + session: { type: "string", required: true }, + tabId: { type: "integer", required: true }, + path: { type: "string", required: true }, + width: { type: "integer", required: true }, + height: { type: "integer", required: true }, + byteSize: { type: "integer", required: true }, + image: { + type: "object", + additionalProperties: false, + properties: { + attachmentId: { type: "string", required: true }, + mediaType: { type: "string", required: true, const: "image/png" }, + bytes: { type: "integer", required: true }, + width: { type: "integer", required: true }, + height: { type: "integer", required: true }, + name: { type: "string" }, + }, + }, + }, + }, + render: (_args, value) => { + const text = + value.image !== undefined + ? `[session ${value.session}] screenshot of tab ${value.tabId} (${value.width}x${value.height}px)` + : `[session ${value.session}] screenshot saved to ${value.path} (${value.width}x${value.height}px, ${value.byteSize} bytes) — this deployment cannot inline images; read the file to view it`; + const blocks: ContentBlock[] = [{ type: "text", text }]; + if (value.image !== undefined) { + blocks.push({ + type: "image", + // Structurally an ImageAttachmentRef; branded without a runtime import. + attachment: { + attachmentId: value.image.attachmentId, + mediaType: value.image.mediaType, + bytes: value.image.bytes, + width: value.image.width, + height: value.image.height, + ...(value.image.name !== undefined ? { name: value.image.name } : {}), + } as never, + }); + } + return blocks; + }, + }, + isConcurrencySafe: () => true, + async execute(args, exec) { + const sessionId = registry.resolve(args.session, "browser_screenshot"); + const outPath = join( + tmpdir(), + `bsk-screenshot-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.png`, + ); + const cmdArgs = ["screenshot", "--session", sessionId, "--out", outPath]; + if (args.ref !== undefined) cmdArgs.push("--ref", args.ref); + const reply = (await runBsk(deps, exec, cmdArgs, "screenshot")) as { + tab_id: number; + width: number; + height: number; + path: string; + byte_size: number; + }; + const data = await readFile(reply.path); + if (exec.signal.aborted) throw abortError(); + const ref = await trySaveScreenshot(deps.ctx, exec, data, `screenshot-${sessionId}.png`); + return { + session: sessionId, + tabId: reply.tab_id, + path: reply.path, + width: reply.width, + height: reply.height, + byteSize: reply.byte_size, + ...(ref !== undefined + ? { + image: { + attachmentId: String(ref.attachmentId), + mediaType: "image/png" as const, + bytes: ref.bytes, + width: ref.width, + height: ref.height, + ...(ref.name !== undefined ? { name: ref.name } : {}), + }, + } + : {}), + }; + }, + presentCall: (args) => ({ + card: "terminal", + title: cmdline(deps, [ + "screenshot", + "--session", + args.session ?? "(current)", + ...(args.ref !== undefined ? ["--ref", args.ref] : []), + ]), + description: "Capture a screenshot", + }), + presentResult: presentTerminalResult, + }), + ); + + ctx.tools.register( + defineTool({ + name: "browser_emulate", + description: + "Apply (or clear) mobile device emulation on the session's active tab: viewport metrics, " + + "user agent, and touch. Overrides are per-tab and not inherited by new tabs.", + parameters: { + session: SESSION_PARAM, + device: { + type: "string", + enum: DEVICE_PRESETS, + description: "Built-in device preset.", + }, + width: { type: "integer", description: "Viewport width in CSS pixels (requires height)." }, + height: { type: "integer", description: "Viewport height in CSS pixels (requires width)." }, + mobile: { type: "boolean", description: "Emulate a mobile viewport." }, + off: { type: "boolean", description: "Clear every emulation override on the tab." }, + }, + output: { + schema: { + type: "object", + additionalProperties: false, + properties: { + session: { type: "string", required: true }, + tabId: { type: "integer", required: true }, + cleared: { type: "boolean", required: true }, + }, + }, + render: (_args, value) => [ + { + type: "text", + text: value.cleared + ? `[session ${value.session}] cleared emulation on tab ${value.tabId}` + : `[session ${value.session}] applied emulation on tab ${value.tabId}`, + }, + ], + }, + async execute(args, exec) { + const sessionId = registry.resolve(args.session, "browser_emulate"); + if (args.off === true) { + if (args.device !== undefined || args.width !== undefined || args.height !== undefined) { + throw new Error("off is mutually exclusive with device/width/height"); + } + } else if (args.device === undefined && args.width === undefined) { + throw new Error("nothing to apply: pass device, width+height, or off"); + } + if ((args.width === undefined) !== (args.height === undefined)) { + throw new Error("width and height must be given together"); + } + const cmdArgs = ["emulate", "--session", sessionId]; + if (args.off === true) { + cmdArgs.push("--off"); + } else { + if (args.device !== undefined) cmdArgs.push("--device", args.device); + if (args.width !== undefined && args.height !== undefined) { + cmdArgs.push("--width", String(args.width), "--height", String(args.height)); + } + if (args.mobile === true) cmdArgs.push("--mobile"); + } + const reply = (await runBsk(deps, exec, cmdArgs, "emulate")) as { + tab_id: number; + cleared: boolean; + }; + return { session: sessionId, tabId: reply.tab_id, cleared: reply.cleared }; + }, + presentCall: (args) => ({ + card: "terminal", + title: cmdline(deps, [ + "emulate", + "--session", + args.session ?? "(current)", + ...(args.off === true ? ["--off"] : []), + ...(args.device !== undefined ? ["--device", args.device] : []), + ]), + description: "Emulate a device environment", + }), + presentResult: presentTerminalResult, + }), + ); +} diff --git a/packages/dsh-plugin-browserskill/tests/runner.test.ts b/packages/dsh-plugin-browserskill/tests/runner.test.ts new file mode 100644 index 0000000..35dc279 --- /dev/null +++ b/packages/dsh-plugin-browserskill/tests/runner.test.ts @@ -0,0 +1,137 @@ +import type { ChildProcess } from "node:child_process"; +import { EventEmitter } from "node:events"; +import { describe, expect, it } from "vitest"; +import { type BskRunResult, createBskRunner, isCommandNotFound, parseBskJson } from "../src/runner"; + +/** Minimal fake ChildProcess driven by the test. */ +class FakeChild extends EventEmitter { + stdout = new EventEmitter(); + stderr = new EventEmitter(); + exitCode: number | null = null; + signalCode: string | null = null; + killedWith: string[] = []; + + kill(signal: string): boolean { + if (this.exitCode !== null || this.signalCode !== null) return false; + this.killedWith.push(signal); + this.signalCode = signal; + queueMicrotask(() => this.emit("close", null)); + return true; + } + + finish(code: number, stdout = "", stderr = ""): void { + if (stdout) this.stdout.emit("data", stdout); + if (stderr) this.stderr.emit("data", stderr); + this.exitCode = code; + this.emit("close", code); + } +} + +function fakeSpawn(children: FakeChild[]) { + return () => { + const child = children.shift(); + if (child === undefined) throw new Error("no fake child queued"); + return child as unknown as ChildProcess; + }; +} + +describe("createBskRunner", () => { + it("appends --json and collects stdout", async () => { + const child = new FakeChild(); + const seen: string[][] = []; + const runner = createBskRunner("bsk", (cmd: string, args: string[]) => { + seen.push([cmd, ...args]); + return child as unknown as ChildProcess; + }); + const promise = runner.run(["session", "list"]); + child.finish(0, '{"ok":true}'); + const result = await promise; + expect(seen).toEqual([["bsk", "session", "list", "--json"]]); + expect(result).toMatchObject({ + code: 0, + stdout: '{"ok":true}', + aborted: false, + timedOut: false, + }); + }); + + it("kills the child when the abort signal fires", async () => { + const child = new FakeChild(); + const runner = createBskRunner("bsk", fakeSpawn([child])); + const controller = new AbortController(); + const promise = runner.run(["snapshot"], { signal: controller.signal }); + controller.abort(); + const result = await promise; + expect(result.aborted).toBe(true); + expect(child.killedWith).toContain("SIGTERM"); + }); + + it("kills the child on timeout", async () => { + const child = new FakeChild(); + const runner = createBskRunner("bsk", fakeSpawn([child])); + const result = await runner.run(["snapshot"], { timeoutMs: 5 }); + expect(result.timedOut).toBe(true); + expect(child.killedWith.length).toBeGreaterThan(0); + }); + + it("killAll terminates in-flight children", async () => { + const child = new FakeChild(); + const runner = createBskRunner("bsk", fakeSpawn([child])); + const promise = runner.run(["session", "list"]); + runner.killAll(); + const result = await promise; + expect(child.killedWith).toContain("SIGTERM"); + expect(result.code).toBeNull(); + }); +}); + +describe("parseBskJson", () => { + const base: BskRunResult = { code: 0, stdout: "", stderr: "", timedOut: false, aborted: false }; + + it("parses stdout JSON on success", () => { + expect(parseBskJson({ ...base, stdout: '{"a":1}' }, "status")).toEqual({ a: 1 }); + }); + + it("maps the JSON error envelope on non-zero exit", () => { + const body = JSON.stringify({ + code: "no_browser", + message: "no browser connected", + hint: "open Chrome", + }); + try { + parseBskJson({ ...base, code: 3, stdout: body }, "session start"); + expect.unreachable(); + } catch (error) { + expect(error).toMatchObject({ + name: "BskError", + code: "no_browser", + hint: "open Chrome", + exitCode: 3, + }); + expect((error as Error).message).toContain("no browser connected"); + } + }); + + it("falls back to stderr when the error body is not JSON", () => { + expect(() => parseBskJson({ ...base, code: 1, stderr: "boom" }, "x")).toThrow(/boom/); + }); + + it("reports timeouts distinctly", () => { + expect(() => parseBskJson({ ...base, timedOut: true }, "x")).toThrow(/timed out/); + }); + + it("rejects non-JSON success output", () => { + expect(() => parseBskJson({ ...base, stdout: "not json" }, "x")).toThrow( + /did not produce JSON/, + ); + }); +}); + +describe("isCommandNotFound", () => { + it("detects ENOENT spawn failures", () => { + expect( + isCommandNotFound(Object.assign(new Error("spawn bsk ENOENT"), { code: "ENOENT" })), + ).toBe(true); + expect(isCommandNotFound(new Error("other"))).toBe(false); + }); +}); diff --git a/packages/dsh-plugin-browserskill/tests/sessions.test.ts b/packages/dsh-plugin-browserskill/tests/sessions.test.ts new file mode 100644 index 0000000..c73ca63 --- /dev/null +++ b/packages/dsh-plugin-browserskill/tests/sessions.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "vitest"; +import { SessionRegistry } from "../src/sessions"; + +describe("SessionRegistry", () => { + it("tracks the current session across add and remove", () => { + const registry = new SessionRegistry(5); + expect(registry.current()).toBeUndefined(); + registry.add({ sessionId: "s1", startedAtMs: 1 }); + expect(registry.current()).toBe("s1"); + registry.add({ sessionId: "s2", startedAtMs: 2 }); + expect(registry.current()).toBe("s2"); + registry.remove("s2"); + expect(registry.current()).toBe("s1"); + registry.remove("s1"); + expect(registry.current()).toBeUndefined(); + }); + + it("enforces the concurrency cap", () => { + const registry = new SessionRegistry(2); + registry.add({ sessionId: "s1", startedAtMs: 1 }); + registry.add({ sessionId: "s2", startedAtMs: 2 }); + expect(() => registry.add({ sessionId: "s3", startedAtMs: 3 })).toThrow(/session limit/); + registry.remove("s1"); + registry.add({ sessionId: "s3", startedAtMs: 3 }); + expect(registry.size()).toBe(2); + }); + + it("resolve prefers the explicit session and adopts unknown ids", () => { + const registry = new SessionRegistry(5); + registry.add({ sessionId: "s1", startedAtMs: 1 }); + expect(registry.resolve("s9", "tool")).toBe("s9"); + expect(registry.current()).toBe("s9"); + expect(registry.list().map((s) => s.sessionId)).toContain("s9"); + }); + + it("resolve falls back to the current session", () => { + const registry = new SessionRegistry(5); + registry.add({ sessionId: "s1", startedAtMs: 1 }); + expect(registry.resolve(undefined, "tool")).toBe("s1"); + }); + + it("resolve throws actionable guidance when no session exists", () => { + const registry = new SessionRegistry(5); + expect(() => registry.resolve(undefined, "browser_click")).toThrow(/browser_session_start/); + }); + + it("touch refreshes recency so fallback picks the most recent survivor", () => { + const registry = new SessionRegistry(5); + registry.add({ sessionId: "s1", startedAtMs: 1 }); + registry.add({ sessionId: "s2", startedAtMs: 2 }); + registry.touch("s1"); + registry.remove("s1"); + expect(registry.current()).toBe("s2"); + }); +}); diff --git a/packages/dsh-plugin-browserskill/tests/tools.test.ts b/packages/dsh-plugin-browserskill/tests/tools.test.ts new file mode 100644 index 0000000..0073790 --- /dev/null +++ b/packages/dsh-plugin-browserskill/tests/tools.test.ts @@ -0,0 +1,525 @@ +import { mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import type { ToolDefinition, ToolRunContext } from "@deepseek-ai/dsh-tools"; +import { describe, expect, it } from "vitest"; +import type { BskRunOptions, BskRunResult } from "../src/runner"; +import { SessionRegistry } from "../src/sessions"; +import { type PluginConfig, registerTools } from "../src/tools"; + +const CONFIG: PluginConfig = { bskPath: "bsk", defaultTimeoutMs: 120_000, maxSessions: 5 }; + +interface FakeCall { + args: string[]; + options: BskRunOptions; +} + +/** Mark a canned answer as a per-call sequence (each call shifts one reply). */ +function seq(items: unknown[]): { __seq: unknown[] } { + return { __seq: items }; +} + +/** A runner that answers canned JSON per command prefix and records calls. */ +function fakeRunner(responses: Record) { + const calls: FakeCall[] = []; + return { + calls, + runner: { + async run(args: string[], options: BskRunOptions = {}): Promise { + calls.push({ args, options }); + if (options.signal?.aborted) { + return { code: null, stdout: "", stderr: "", timedOut: false, aborted: true }; + } + for (const [prefix, response] of Object.entries(responses)) { + if (args.join(" ").startsWith(prefix)) { + if (response instanceof Error) throw response; + const reply = + typeof response === "object" && response !== null && "__seq" in response + ? (response as { __seq: unknown[] }).__seq.shift() + : response; + return { + code: 0, + stdout: JSON.stringify(reply), + stderr: "", + timedOut: false, + aborted: false, + }; + } + } + return { + code: 2, + stdout: JSON.stringify({ + code: "unexpected", + message: `no canned response for: ${args.join(" ")}`, + }), + stderr: "", + timedOut: false, + aborted: false, + }; + }, + killAll() {}, + }, + }; +} + +function makeCtx(services: Record = {}) { + const tools = new Map(); + const ctx = { + tools: { register: (def: ToolDefinition) => tools.set(def.name, def) }, + get: (key: string) => services[key], + }; + return { ctx, tools }; +} + +function makeExec(overrides: Record = {}): ToolRunContext { + return { + callId: "test-call", + name: "test", + signal: new AbortController().signal, + ...overrides, + } as unknown as ToolRunContext; +} + +const SNAPSHOT_REPLY = { + text: '- button "OK" [ref=@e1]', + ref_count: 1, + tab_id: 7, + truncated: false, +}; + +function setup( + responses: Record, + services: Record = {}, + maxSessions = 5, +) { + const { ctx, tools } = makeCtx(services); + const { runner, calls } = fakeRunner(responses); + const registry = new SessionRegistry(maxSessions); + registerTools({ ctx: ctx as never, runner, registry, config: { ...CONFIG, maxSessions } }); + return { tools, calls, registry }; +} + +async function startSession(tools: Map, sessionId = "s1") { + const tool = tools.get("browser_session_start"); + if (tool === undefined) throw new Error("browser_session_start not registered"); + return tool.execute({}, makeExec()) as Promise<{ sessionId: string }>; +} + +const START_REPLY = (id: string) => ({ session_id: id, browser_instance_id: "chrome-1" }); + +describe("tool registration", () => { + it("registers the full browser tool suite", () => { + const { tools } = setup({}); + expect([...tools.keys()].sort()).toEqual([ + "browser_click", + "browser_emulate", + "browser_fill", + "browser_navigate", + "browser_observe", + "browser_press", + "browser_screenshot", + "browser_session_list", + "browser_session_start", + "browser_session_stop", + "browser_snapshot", + ]); + }); + + it("validates model args through defineTool (missing required param)", async () => { + const { tools } = setup({}); + const navigate = tools.get("browser_navigate"); + await expect(navigate?.execute({}, makeExec())).rejects.toThrow(/invalid arguments/); + }); +}); + +describe("browser_session_start", () => { + it("maps the start reply and tracks the session as current", async () => { + const { tools, registry } = setup({ "session start": START_REPLY("s1") }); + const value = await startSession(tools); + expect(value.sessionId).toBe("s1"); + expect(registry.current()).toBe("s1"); + }); + + it("passes window size and opens the initial URL and device preset", async () => { + const { tools, calls } = setup({ + "session start": START_REPLY("s1"), + emulate: { tab_id: 7, cleared: false }, + navigate: { tab_id: 7, url: "https://example.com", reached: "load" }, + }); + const tool = tools.get("browser_session_start"); + const value = (await tool?.execute( + { width: 1280, height: 800, url: "https://example.com", device: "iphone-14", noFocus: true }, + makeExec(), + )) as { sessionId: string; url: string; device: string }; + expect(value).toMatchObject({ + sessionId: "s1", + url: "https://example.com", + device: "iphone-14", + }); + expect(calls[0].args).toEqual([ + "session", + "start", + "--width", + "1280", + "--height", + "800", + "--no-focus", + ]); + expect(calls[1].args).toEqual(["emulate", "--session", "s1", "--device", "iphone-14"]); + expect(calls[2].args).toEqual(["navigate", "--session", "s1", "https://example.com"]); + }); + + it("requires width and height together", async () => { + const { tools } = setup({ "session start": START_REPLY("s1") }); + const tool = tools.get("browser_session_start"); + await expect(tool?.execute({ width: 1280 }, makeExec())).rejects.toThrow(/together/); + }); + + it("cleans up a half-initialized session when the initial navigate fails", async () => { + const { tools, calls, registry } = setup({ + "session start": START_REPLY("s1"), + "session stop": {}, + }); + // navigate has no canned response -> non-zero exit -> error. + const tool = tools.get("browser_session_start"); + await expect(tool?.execute({ url: "https://example.com" }, makeExec())).rejects.toThrow( + /navigate/, + ); + expect(calls.some((c) => c.args.join(" ") === "session stop s1")).toBe(true); + expect(registry.current()).toBeUndefined(); + }); +}); + +describe("multi-session behavior", () => { + const responses = { + "session start": START_REPLY("s1"), + snapshot: SNAPSHOT_REPLY, + }; + + it("operation tools default to the current session and echo it back", async () => { + const { tools, calls } = setup(responses); + await startSession(tools); + const snapshot = tools.get("browser_snapshot"); + const value = (await snapshot?.execute({}, makeExec())) as { + session: string; + refCount: number; + }; + expect(value).toMatchObject({ session: "s1", refCount: 1 }); + expect(calls[1].args).toEqual(["snapshot", "--session", "s1"]); + }); + + it("an explicit session wins and becomes the new current session", async () => { + const { tools, calls, registry } = setup(responses); + await startSession(tools); + const snapshot = tools.get("browser_snapshot"); + const value = (await snapshot?.execute({ session: "other" }, makeExec())) as { + session: string; + }; + expect(value.session).toBe("other"); + expect(calls[1].args).toEqual(["snapshot", "--session", "other"]); + expect(registry.current()).toBe("other"); + }); + + it("fails with guidance when no session exists", async () => { + const { tools } = setup(responses); + const snapshot = tools.get("browser_snapshot"); + await expect(snapshot?.execute({}, makeExec())).rejects.toThrow(/browser_session_start/); + }); + + it("enforces the configured session cap", async () => { + const { tools } = setup( + { "session start": seq([START_REPLY("s1"), START_REPLY("s2")]) }, + {}, + 1, + ); + await startSession(tools); + const start = tools.get("browser_session_start"); + await expect(start?.execute({}, makeExec())).rejects.toThrow(/session limit/); + }); +}); + +describe("browser_session_stop / list", () => { + it("stops the current session and clears the registry", async () => { + const { tools, calls, registry } = setup({ + "session start": START_REPLY("s1"), + "session stop": {}, + }); + await startSession(tools); + const stop = tools.get("browser_session_stop"); + const value = (await stop?.execute({}, makeExec())) as { stopped: string }; + expect(value.stopped).toBe("s1"); + expect(calls[1].args).toEqual(["session", "stop", "s1"]); + expect(registry.current()).toBeUndefined(); + }); + + it("lists daemon sessions with the current marker", async () => { + const { tools } = setup({ + "session start": START_REPLY("s1"), + "session list": [ + { session_id: "s1", browser_instance_id: "chrome-1" }, + { session_id: "s2", browser_instance_id: "chrome-1" }, + ], + }); + await startSession(tools); + const list = tools.get("browser_session_list"); + const value = (await list?.execute({}, makeExec())) as { + sessions: { sessionId: string; current: boolean }[]; + }; + expect(value.sessions).toEqual([ + { sessionId: "s1", browserInstanceId: "chrome-1", current: true }, + { sessionId: "s2", browserInstanceId: "chrome-1", current: false }, + ]); + }); +}); + +describe("interaction tools", () => { + const responses = { + "session start": START_REPLY("s1"), + click: { tab_id: 7, used_ref: "@e1", x: 10, y: 20 }, + fill: { tab_id: 7, used_ref: "@e2", value_length: 5 }, + press: { tab_id: 7, key: "Enter", code: "Enter", modifiers: [] }, + navigate: { tab_id: 7, url: "https://a.test", final_url: "https://b.test", reached: "load" }, + emulate: { tab_id: 7, cleared: false }, + }; + + it("browser_click maps target, button, and click count", async () => { + const { tools, calls } = setup(responses); + await startSession(tools); + const click = tools.get("browser_click"); + const value = (await click?.execute( + { target: "@e1", button: "right", clickCount: 2 }, + makeExec(), + )) as { session: string; x: number; y: number }; + expect(value).toMatchObject({ session: "s1", x: 10, y: 20 }); + expect(calls[1].args).toEqual([ + "click", + "--session", + "s1", + "--button", + "right", + "--click-count", + "2", + "@e1", + ]); + }); + + it("browser_fill passes value and --no-clear", async () => { + const { tools, calls } = setup(responses); + await startSession(tools); + const fill = tools.get("browser_fill"); + await fill?.execute({ target: "e2", value: "hello", noClear: true }, makeExec()); + expect(calls[1].args).toEqual([ + "fill", + "--session", + "s1", + "--value", + "hello", + "--no-clear", + "e2", + ]); + }); + + it("browser_press picks --ref for snapshot refs and --selector otherwise", async () => { + const { tools, calls } = setup(responses); + await startSession(tools); + const press = tools.get("browser_press"); + await press?.execute({ key: "Enter", target: "@e3" }, makeExec()); + expect(calls[1].args).toEqual(["press", "--session", "s1", "--ref", "@e3", "Enter"]); + await press?.execute({ key: "Ctrl+A", target: "#field" }, makeExec()); + expect(calls[2].args).toEqual(["press", "--session", "s1", "--selector", "#field", "Ctrl+A"]); + }); + + it("browser_navigate converts waitUntil and timeoutMs", async () => { + const { tools, calls } = setup(responses); + await startSession(tools); + const navigate = tools.get("browser_navigate"); + const value = (await navigate?.execute( + { url: "https://a.test", waitUntil: "networkidle", timeoutMs: 1500 }, + makeExec(), + )) as { finalUrl: string; reached: string }; + expect(value).toMatchObject({ finalUrl: "https://b.test", reached: "load" }); + expect(calls[1].args).toEqual([ + "navigate", + "--session", + "s1", + "--wait-until", + "networkidle", + "--timeout", + "1500ms", + "https://a.test", + ]); + }); + + it("browser_emulate rejects off combined with overrides", async () => { + const { tools } = setup(responses); + await startSession(tools); + const emulate = tools.get("browser_emulate"); + await expect(emulate?.execute({ off: true, device: "iphone-14" }, makeExec())).rejects.toThrow( + /mutually exclusive/, + ); + }); +}); + +describe("browser_screenshot", () => { + function pngFile(): string { + const dir = mkdtempSync(join(tmpdir(), "bsk-test-")); + const path = join(dir, "shot.png"); + // Minimal PNG header bytes are enough: the plugin never decodes the image. + writeFileSync(path, Buffer.from([0x89, 0x50, 0x4e, 0x47, 1, 2, 3, 4])); + return path; + } + + it("returns the PNG path when no attachment store is mounted", async () => { + const path = pngFile(); + const { tools, calls } = setup({ + "session start": START_REPLY("s1"), + screenshot: { tab_id: 7, width: 800, height: 600, format: "png", path, byte_size: 8 }, + }); + await startSession(tools); + const screenshot = tools.get("browser_screenshot"); + const value = (await screenshot?.execute({ ref: "@e1" }, makeExec())) as { + path: string; + image?: unknown; + }; + expect(value.path).toBe(path); + expect(value.image).toBeUndefined(); + expect(calls[1].args.slice(0, 3)).toEqual(["screenshot", "--session", "s1"]); + expect(calls[1].args).toContain("--ref"); + const rendered = screenshot?.output.render({ session: "s1" }, value as never); + expect(rendered?.every((block) => block.type === "text")).toBe(true); + }); + + it("commits the image through the attachment store on an image-capable route", async () => { + const path = pngFile(); + const services = { + attachments: { + imageLimits: { + mediaTypes: ["image/png"], + maxImageBytes: 10_000_000, + maxMessageImageBytes: 10_000_000, + }, + saveImage: async (input: { data: Uint8Array; mediaType: string; name?: string }) => ({ + attachmentId: "att-1", + mediaType: input.mediaType, + bytes: input.data.byteLength, + width: 800, + height: 600, + name: input.name, + }), + }, + llm: { + resolveModelInfo: async () => ({ inputModalities: ["text", "image"] }), + }, + }; + const { tools } = setup( + { + "session start": START_REPLY("s1"), + screenshot: { tab_id: 7, width: 800, height: 600, format: "png", path, byte_size: 8 }, + }, + services, + ); + await startSession(tools); + const exec = makeExec({ + agent: { + session: { requestHeader: () => ({ config: { provider: "deepseek", model: "vl" } }) }, + options: {}, + }, + }); + const screenshot = tools.get("browser_screenshot"); + const value = (await screenshot?.execute({}, exec)) as { + image?: { attachmentId: string; mediaType: string }; + }; + expect(value.image).toMatchObject({ attachmentId: "att-1", mediaType: "image/png" }); + const rendered = screenshot?.output.render({}, value as never) as { type: string }[]; + expect(rendered.map((block) => block.type)).toEqual(["text", "image"]); + }); + + it("stays path-only when the model route is text-only", async () => { + const path = pngFile(); + const services = { + attachments: { + imageLimits: { mediaTypes: ["image/png"], maxImageBytes: 1e7, maxMessageImageBytes: 1e7 }, + saveImage: async () => { + throw new Error("must not be called"); + }, + }, + llm: { resolveModelInfo: async () => ({ inputModalities: ["text"] }) }, + }; + const { tools } = setup( + { + "session start": START_REPLY("s1"), + screenshot: { tab_id: 7, width: 1, height: 1, format: "png", path, byte_size: 8 }, + }, + services, + ); + await startSession(tools); + const exec = makeExec({ + agent: { + session: { requestHeader: () => ({ config: { provider: "p", model: "m" } }) }, + options: {}, + }, + }); + const screenshot = tools.get("browser_screenshot"); + const value = (await screenshot?.execute({}, exec)) as { image?: unknown; path: string }; + expect(value.image).toBeUndefined(); + expect(value.path).toBe(path); + }); +}); + +describe("error and cancellation semantics", () => { + it("surfaces the bsk JSON error envelope with code and hint", async () => { + const { tools } = setup({ + "session start": START_REPLY("s1"), + }); + await startSession(tools); + const snapshot = tools.get("browser_snapshot"); + await expect(snapshot?.execute({}, makeExec())).rejects.toThrow(/no canned response/); + }); + + it("turns an aborted child into an AbortError", async () => { + const { tools } = setup({ "session start": START_REPLY("s1") }); + await startSession(tools); + const snapshot = tools.get("browser_snapshot"); + const controller = new AbortController(); + controller.abort(); + const exec = makeExec({ signal: controller.signal }); + await expect(snapshot?.execute({}, exec)).rejects.toMatchObject({ name: "AbortError" }); + }); + + it("maps a missing bsk binary onto install guidance", async () => { + const { ctx, tools } = makeCtx(); + const registry = new SessionRegistry(5); + const runner = { + async run(): Promise { + throw Object.assign(new Error("spawn bsk ENOENT"), { code: "ENOENT" }); + }, + killAll() {}, + }; + registerTools({ ctx: ctx as never, runner, registry, config: CONFIG }); + const list = tools.get("browser_session_list"); + await expect(list?.execute({}, makeExec())).rejects.toThrow(/BrowserSkill must be installed/); + }); +}); + +describe("presentation", () => { + it("presents calls as terminal cards with the bsk command line", () => { + const { tools } = setup({}); + const click = tools.get("browser_click"); + const view = click?.presentCall?.({ target: "@e1" }) as { + card: string; + title: string; + }; + expect(view.card).toBe("terminal"); + expect(view.title).toContain("bsk click @e1"); + }); + + it("presents results as terminal cards carrying the rendered text", () => { + const { tools } = setup({}); + const click = tools.get("browser_click"); + const view = click?.presentResult?.({ target: "@e1" }, { + content: [{ type: "text", text: "clicked" }], + isError: false, + } as never) as { card: string; output: string; exitCode: number }; + expect(view).toMatchObject({ card: "terminal", output: "clicked", exitCode: 0 }); + }); +}); diff --git a/packages/dsh-plugin-browserskill/tsconfig.json b/packages/dsh-plugin-browserskill/tsconfig.json new file mode 100644 index 0000000..299df7c --- /dev/null +++ b/packages/dsh-plugin-browserskill/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "noEmit": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "strict": true, + "skipLibCheck": true, + "types": ["node"] + }, + "include": ["src/**/*", "tests/**/*", "tsdown.config.ts", "vitest.config.ts"] +} diff --git a/packages/dsh-plugin-browserskill/tsdown.config.ts b/packages/dsh-plugin-browserskill/tsdown.config.ts new file mode 100644 index 0000000..ba59502 --- /dev/null +++ b/packages/dsh-plugin-browserskill/tsdown.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "tsdown"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["esm"], + dts: true, + outDir: "lib", + // dsh host packages are provided by the profile the plugin is installed into. + external: [/^@deepseek-ai\//], +}); diff --git a/packages/dsh-plugin-browserskill/vitest.config.ts b/packages/dsh-plugin-browserskill/vitest.config.ts new file mode 100644 index 0000000..faa6d98 --- /dev/null +++ b/packages/dsh-plugin-browserskill/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["tests/**/*.test.ts"], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b5a69f8..702e10e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,7 +65,7 @@ importers: version: 6.0.2(vite@8.0.13(@types/node@25.8.0)(esbuild@0.27.7)(jiti@2.7.0)) '@wxt-dev/module-react': specifier: ^1.1.3 - version: 1.2.2(vite@8.0.13(@types/node@25.8.0)(esbuild@0.27.7)(jiti@2.7.0))(wxt@0.20.26(@types/node@25.8.0)(jiti@2.7.0)(rolldown@1.0.1)(rollup@4.60.4)) + version: 1.2.2(vite@8.0.13(@types/node@25.8.0)(esbuild@0.27.7)(jiti@2.7.0))(wxt@0.20.26(@types/node@25.8.0)(jiti@2.7.0)(rolldown@1.2.4)(rollup@4.60.4)) happy-dom: specifier: ^15.11.7 version: 15.11.7 @@ -80,7 +80,40 @@ importers: version: 4.1.6(@types/node@25.8.0)(happy-dom@15.11.7)(vite@8.0.13(@types/node@25.8.0)(esbuild@0.27.7)(jiti@2.7.0)) wxt: specifier: ^0.20.0 - version: 0.20.26(@types/node@25.8.0)(jiti@2.7.0)(rolldown@1.0.1)(rollup@4.60.4) + version: 0.20.26(@types/node@25.8.0)(jiti@2.7.0)(rolldown@1.2.4)(rollup@4.60.4) + + packages/dsh-plugin-browserskill: + devDependencies: + '@deepseek-ai/cordis': + specifier: ^4.0.1 + version: 4.0.1 + '@deepseek-ai/dsh-agent': + specifier: ^0.1.0-rc.6 + version: 0.1.0-rc.6(b0514d7a320728b6d8f5a31eb3960e10) + '@deepseek-ai/dsh-attachment': + specifier: ^0.1.0-rc.6 + version: 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/dsh-llm': + specifier: ^0.1.0-rc.6 + version: 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))) + '@deepseek-ai/dsh-tools': + specifier: ^0.1.0-rc.6 + version: 0.1.0-rc.6(f8724372086ccc1457fc84e7becee2e0) + '@deepseek-ai/schemastery': + specifier: ^3.18.1 + version: 3.18.1 + '@types/node': + specifier: ^22.10.0 + version: 22.20.1 + tsdown: + specifier: ^0.22.14 + version: 0.22.14(typescript@5.9.3) + typescript: + specifier: ^5.8.0 + version: 5.9.3 + vitest: + specifier: ^4.1.6 + version: 4.1.6(@types/node@22.20.1)(happy-dom@15.11.7)(vite@8.0.13(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)) packages/i18n: dependencies: @@ -264,6 +297,129 @@ packages: peerDependencies: postcss-selector-parser: ^7.0.0 + '@deepseek-ai/cordis@4.0.1': + resolution: {integrity: sha512-YBdskTU2Po1kru3GgcUWUbkTsPMA9LkSQDAY8rBkFJeajdgcQad3QPJZE26JyK99Xb6HaASvoXg2DSUTeN/0Nw==} + hasBin: true + peerDependencies: + '@deepseek-ai/cordis-plugin-include': ^1.0.6 + '@deepseek-ai/cordis-plugin-loader': ^1.0.2 + peerDependenciesMeta: + '@deepseek-ai/cordis-plugin-include': + optional: true + '@deepseek-ai/cordis-plugin-loader': + optional: true + + '@deepseek-ai/cosmokit@1.8.2': + resolution: {integrity: sha512-muBOKtSrUKU5m/xpq8ZXWL6hQ/jgd4PhU2PqH97bcxIiLEJfNwZOGQEx4t/aS/GgxRAR+ra9pMHPMtTHU4sqqA==} + + '@deepseek-ai/dsh-agent@0.1.0-rc.6': + resolution: {integrity: sha512-vtqq2pWTrzn0dKfj5kREZRpP82AwtGjGx9V1lYnKvF+Uc/a8zyWbSvjDE7V1d3YQAQJzs2cWO31hURWDekDXIA==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.1 + '@deepseek-ai/dsh-invariants': ^0.1.0-rc.6 + '@deepseek-ai/dsh-llm': ^0.1.0-rc.6 + '@deepseek-ai/dsh-scope': ^0.1.0-rc.6 + '@deepseek-ai/dsh-session': ^0.1.0-rc.6 + '@deepseek-ai/dsh-system-prompt': ^0.1.0-rc.6 + '@deepseek-ai/dsh-typert-protocol': ^0.1.0-rc.6 + + '@deepseek-ai/dsh-attachment@0.1.0-rc.6': + resolution: {integrity: sha512-3P6N17NQ8jqSQGzeCs+svCIqArU8oq0YmgEAo+axN9aVuUDferWU4DLRSX59UGpmyldX4LQn81toA+c+DqMcHg==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.1 + '@deepseek-ai/dsh-brand': ^0.1.0-rc.6 + '@deepseek-ai/dsh-invariants': ^0.1.0-rc.6 + + '@deepseek-ai/dsh-brand@0.1.0-rc.6': + resolution: {integrity: sha512-E8j9Nby24qP4rfrdcfc7bpt1CHpGT3tYmycOJJkEOH4ptIdT1m2ro9nmnSd5CWYukTr64A77vjm2WGqHRI92UA==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.1 + '@deepseek-ai/dsh-invariants': ^0.1.0-rc.6 + + '@deepseek-ai/dsh-code-runtime@0.1.0-rc.6': + resolution: {integrity: sha512-aw8D4IOeMo11A3uxQeE4LFoW3bvaQnVkGFqqtS+lsINDARrOCJHXLUgecoKpys+Lc5erZZ4UQDnymJmL4OCcKA==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.1 + '@deepseek-ai/dsh-invariants': ^0.1.0-rc.6 + + '@deepseek-ai/dsh-invariants@0.1.0-rc.6': + resolution: {integrity: sha512-WfEfOi99a4cpOugRAHTBSTnesLieu3ist1q9PXDXFBHX++K1rAl9+sB7YrdnbB8LH0UOY532gS9xJUYU6w0SLw==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.1 + + '@deepseek-ai/dsh-llm@0.1.0-rc.6': + resolution: {integrity: sha512-kuFGC8bHlzGTwlRxQhXjf3CYWl8M4NzH+EYIkrW8rri4iMc9W53xrdvkil5No/DUlMm8g1u7GdeiWYFy0TMvtA==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.1 + '@deepseek-ai/dsh-attachment': ^0.1.0-rc.6 + '@deepseek-ai/dsh-brand': ^0.1.0-rc.6 + '@deepseek-ai/dsh-invariants': ^0.1.0-rc.6 + '@deepseek-ai/dsh-timeout': ^0.1.0-rc.6 + + '@deepseek-ai/dsh-scope@0.1.0-rc.6': + resolution: {integrity: sha512-UlDLV4syLoJinNg9imhXrSAHrdaTa5Ff8gg46rzjFJGPUOhAk3DZff0hryT5OhrBi0A5Tj92qVpg2pRVvxnUzQ==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.1 + '@deepseek-ai/dsh-invariants': ^0.1.0-rc.6 + + '@deepseek-ai/dsh-session@0.1.0-rc.6': + resolution: {integrity: sha512-8tu8I6VWC7050GAUXWhcEWQw4pakALQc8TlhKr52m7Y4+kIKeNt3FBgP86PaGPBtpK0p5zUPRQNkFpzZbBdxyw==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.1 + '@deepseek-ai/dsh-brand': ^0.1.0-rc.6 + '@deepseek-ai/dsh-invariants': ^0.1.0-rc.6 + '@deepseek-ai/dsh-llm': ^0.1.0-rc.6 + '@deepseek-ai/dsh-scope': ^0.1.0-rc.6 + '@deepseek-ai/dsh-typert-protocol': ^0.1.0-rc.6 + + '@deepseek-ai/dsh-system-prompt@0.1.0-rc.6': + resolution: {integrity: sha512-E7g+XChh4q4/wX++v56z1pV4SA1Rtz42xkznLPPi9FlXrrzJxwHMOUzBZ9Rz3Y1kLhQ++HJG3ZatmNx3rjFilg==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.1 + '@deepseek-ai/dsh-invariants': ^0.1.0-rc.6 + '@deepseek-ai/dsh-llm': ^0.1.0-rc.6 + '@deepseek-ai/dsh-scope': ^0.1.0-rc.6 + + '@deepseek-ai/dsh-timeout@0.1.0-rc.6': + resolution: {integrity: sha512-CUean0fAnfsJVszFEip7PsU/S26W+JfDFfsza2dCtlw8n6xlkbHA9Gjxdk2aTwqDGCgXEPkRW7mYkdJ0n6FR7w==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.1 + '@deepseek-ai/dsh-invariants': ^0.1.0-rc.6 + + '@deepseek-ai/dsh-tools@0.1.0-rc.6': + resolution: {integrity: sha512-Tu08EPK3JyK0iNjH4FGzu/1uADynNSS6SmwOLdfytUN0YNqwNuKFSt2OJUg19famNlTgy992DcHfDu0T+gLXFg==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.1 + '@deepseek-ai/dsh-agent': ^0.1.0-rc.6 + '@deepseek-ai/dsh-code-runtime': ^0.1.0-rc.6 + '@deepseek-ai/dsh-invariants': ^0.1.0-rc.6 + '@deepseek-ai/dsh-llm': ^0.1.0-rc.6 + '@deepseek-ai/dsh-scope': ^0.1.0-rc.6 + '@deepseek-ai/dsh-session': ^0.1.0-rc.6 + '@deepseek-ai/dsh-system-prompt': ^0.1.0-rc.6 + '@deepseek-ai/dsh-user-approval': ^0.1.0-rc.6 + + '@deepseek-ai/dsh-typert-protocol@0.1.0-rc.6': + resolution: {integrity: sha512-weWzN8r01YCkoDCAM7BsKw2YhRrD4zL8N2SAZu9hovYtXSq8xHXsP4Zh8RLYIlYcuotjyff/6hic+0TJPd14YA==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.1 + '@deepseek-ai/dsh-invariants': ^0.1.0-rc.6 + + '@deepseek-ai/dsh-user-approval@0.1.0-rc.6': + resolution: {integrity: sha512-9rnkSDGOpu2XUeGwbPeTzVUTFWTND1PMPM5L/ZQPptV5yyZlQiNxM2rCC6OdL+ZVerwxEqrRhZIQn/KVtQfKag==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.1 + '@deepseek-ai/dsh-agent': ^0.1.0-rc.6 + '@deepseek-ai/dsh-brand': ^0.1.0-rc.6 + '@deepseek-ai/dsh-invariants': ^0.1.0-rc.6 + '@deepseek-ai/dsh-llm': ^0.1.0-rc.6 + '@deepseek-ai/dsh-scope': ^0.1.0-rc.6 + '@deepseek-ai/dsh-session': ^0.1.0-rc.6 + '@deepseek-ai/dsh-system-prompt': ^0.1.0-rc.6 + + '@deepseek-ai/schemastery@3.18.1': + resolution: {integrity: sha512-Qn0FCSwCQnpnj6SB31I6i2sIKgKWnkbJM8O0EU91Gv2UsYVvtZTl6IA0sCwk2e2MZf5S8w5hpq9QkeVvK9qwxg==} + '@devicefarmer/adbkit-logcat@2.1.3': resolution: {integrity: sha512-yeaGFjNBc/6+svbDeul1tNHtNChw6h8pSHAt5D+JsedUrMTN7tla7B15WLDyekxsuS2XlZHRxpuC6m92wiwCNw==} engines: {node: '>= 4'} @@ -491,6 +647,9 @@ packages: '@oxc-project/types@0.130.0': resolution: {integrity: sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==} + '@oxc-project/types@0.144.0': + resolution: {integrity: sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==} + '@pnpm/config.env-replace@1.1.0': resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} engines: {node: '>=12.22.0'} @@ -503,6 +662,9 @@ packages: resolution: {integrity: sha512-h104Kh26rR8tm+a3Qkc5S4VLYint3FE48as7+/5oCEcKR2idC/pF1G6AhIXKI+eHPJa/3J9i5z0Al47IeGHPkA==} engines: {node: '>=12'} + '@quansync/fs@1.0.0': + resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} + '@remixicon/react@4.9.0': resolution: {integrity: sha512-5/jLDD4DtKxH2B4QVXTobvV1C2uL8ab9D5yAYNtFt+w80O0Ys1xFOrspqROL3fjrZi+7ElFUWE37hBfaAl6U+Q==} peerDependencies: @@ -514,72 +676,144 @@ packages: cpu: [arm64] os: [android] + '@rolldown/binding-android-arm64@1.2.4': + resolution: {integrity: sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + '@rolldown/binding-darwin-arm64@1.0.1': resolution: {integrity: sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] + '@rolldown/binding-darwin-arm64@1.2.4': + resolution: {integrity: sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + '@rolldown/binding-darwin-x64@1.0.1': resolution: {integrity: sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] + '@rolldown/binding-darwin-x64@1.2.4': + resolution: {integrity: sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + '@rolldown/binding-freebsd-x64@1.0.1': resolution: {integrity: sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] + '@rolldown/binding-freebsd-x64@1.2.4': + resolution: {integrity: sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + '@rolldown/binding-linux-arm-gnueabihf@1.0.1': resolution: {integrity: sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] + '@rolldown/binding-linux-arm-gnueabihf@1.2.4': + resolution: {integrity: sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + '@rolldown/binding-linux-arm64-gnu@1.0.1': resolution: {integrity: sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + '@rolldown/binding-linux-arm64-gnu@1.2.4': + resolution: {integrity: sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + '@rolldown/binding-linux-arm64-musl@1.0.1': resolution: {integrity: sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + '@rolldown/binding-linux-arm64-musl@1.2.4': + resolution: {integrity: sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + '@rolldown/binding-linux-ppc64-gnu@1.0.1': resolution: {integrity: sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + '@rolldown/binding-linux-ppc64-gnu@1.2.4': + resolution: {integrity: sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + '@rolldown/binding-linux-s390x-gnu@1.0.1': resolution: {integrity: sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + '@rolldown/binding-linux-s390x-gnu@1.2.4': + resolution: {integrity: sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + '@rolldown/binding-linux-x64-gnu@1.0.1': resolution: {integrity: sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + '@rolldown/binding-linux-x64-gnu@1.2.4': + resolution: {integrity: sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + '@rolldown/binding-linux-x64-musl@1.0.1': resolution: {integrity: sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + '@rolldown/binding-linux-x64-musl@1.2.4': + resolution: {integrity: sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + '@rolldown/binding-openharmony-arm64@1.0.1': resolution: {integrity: sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] + '@rolldown/binding-openharmony-arm64@1.2.4': + resolution: {integrity: sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + '@rolldown/binding-wasm32-wasi@1.0.1': resolution: {integrity: sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -591,12 +825,24 @@ packages: cpu: [arm64] os: [win32] + '@rolldown/binding-win32-arm64-msvc@1.2.4': + resolution: {integrity: sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + '@rolldown/binding-win32-x64-msvc@1.0.1': resolution: {integrity: sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] + '@rolldown/binding-win32-x64-msvc@1.2.4': + resolution: {integrity: sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} @@ -876,6 +1122,9 @@ packages: '@types/minimatch@3.0.5': resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} + '@types/node@22.20.1': + resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==} + '@types/node@25.8.0': resolution: {integrity: sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==} @@ -950,6 +1199,129 @@ packages: '@wxt-dev/storage@1.2.8': resolution: {integrity: sha512-GWCFKgF5+d7eslOxUDFC70ypA9njupmJb1nQM8uZoX0J3sWT2BO5xJLzb1sYahWAfID9p2BMtnUBN1lkWxPsbQ==} + '@yuku-codegen/binding-android-arm64@0.8.7': + resolution: {integrity: sha512-C/0zV5IhgVdYhGJTwrY0v8dknxlhiKwtVJkMUaexu9/QvRmzlV4vfU3hZlUSgqc2BxQHntL1mCVbDq8j0FRFDw==} + cpu: [arm64] + os: [android] + + '@yuku-codegen/binding-darwin-arm64@0.8.7': + resolution: {integrity: sha512-/u+REDMI4a0/lsJXTM4c53/w31OGZLOReZIyg62uhgLs0kc8NHsj/nOcxTdlQjq5gi0zhdkccD9LaLTXcdzPvw==} + cpu: [arm64] + os: [darwin] + + '@yuku-codegen/binding-darwin-x64@0.8.7': + resolution: {integrity: sha512-sMMzFOwCo4WXR+/6zIBThOocSC50iIIZZdfiIDbaLvj0Ax/rWt/iavyfEAqajyvzydLyCqR/ZItdLWSRlu1umw==} + cpu: [x64] + os: [darwin] + + '@yuku-codegen/binding-freebsd-x64@0.8.7': + resolution: {integrity: sha512-MpdpKXix9P+Y1rKgjvcNeNtGjXeL1CmttNhYINrWls8kRpm4xM/oBGTmn6w7to8lAlwj5jm8q03dQPl5mRv4Qw==} + cpu: [x64] + os: [freebsd] + + '@yuku-codegen/binding-linux-arm-gnu@0.8.7': + resolution: {integrity: sha512-rr1srFLlPAmC1vtxfc9C1YLDe3iH09YjfSeeIidBqKhzx1MATjOAq4mjlRUOnhr/L27MotWIYOFKwVsd5JZFOg==} + cpu: [arm] + os: [linux] + + '@yuku-codegen/binding-linux-arm-musl@0.8.7': + resolution: {integrity: sha512-eAufXh8qBRpiSO6ueaMDL+yyoXIGLhpUce72YbcACtZU2qhExwBIJyEtQf5kH2Ki0X2aqkSjSfog4OPtKXan3Q==} + cpu: [arm] + os: [linux] + + '@yuku-codegen/binding-linux-arm64-gnu@0.8.7': + resolution: {integrity: sha512-gw4w6wPoHObBrdIC4duVWLmJOvpdE25j5D7yrM5mACNlK4klRz/lv8hK+ssQk9EJHBgZjSaqZIJVFgqNYbfv7A==} + cpu: [arm64] + os: [linux] + + '@yuku-codegen/binding-linux-arm64-musl@0.8.7': + resolution: {integrity: sha512-L68N6Y4XkqcIaKo3Ra88JEvBEH4AHff44A4INcrxeVWZ8CZtu2tCpfVxe3hR8qQQMcvBSQlDn24KpkCKEhVvfA==} + cpu: [arm64] + os: [linux] + + '@yuku-codegen/binding-linux-x64-gnu@0.8.7': + resolution: {integrity: sha512-dzyAbltJmf3Cqlb8HcFuYIf5Yn0fl1vTr3XJ9HiVNNvOlhqPSArOqtw9vI1p6/VTXTjrMLXlU+s+/kNHiIy/Cw==} + cpu: [x64] + os: [linux] + + '@yuku-codegen/binding-linux-x64-musl@0.8.7': + resolution: {integrity: sha512-Otw4MH3404q0Bbvl+YTdW9aoUV5vXmUw8260bWvt1XlaoIX/ceSgI4ygheRDMfBPoHt6FDayQaO9OLVUZAgkFA==} + cpu: [x64] + os: [linux] + + '@yuku-codegen/binding-win32-arm64@0.8.7': + resolution: {integrity: sha512-qo/jyrzryiBuKEsFiuWaBCBe3tRMynQ0qFWFgOEjcCMQeZfBm+wKiVEUEFXXLc7bh8YezguAWp0Mtnhq4ARNyA==} + cpu: [arm64] + os: [win32] + + '@yuku-codegen/binding-win32-x64@0.8.7': + resolution: {integrity: sha512-D5lDsVDx6m00E6bWySlWdH72Ca4TPSaphDqB6QjU6MpuNLIJqoGoatYyq2rOmBE8Zv/kunot/o58KGL03P3eiA==} + cpu: [x64] + os: [win32] + + '@yuku-parser/binding-android-arm64@0.8.7': + resolution: {integrity: sha512-eGKYiUDX7Y0V7tDTmg+JTVnXnjMqfXXsorZ+EDf5kxwchQ3Or1HS14MzI2fw+jFhHR85fCWt+mtX33Yao73hIQ==} + cpu: [arm64] + os: [android] + + '@yuku-parser/binding-darwin-arm64@0.8.7': + resolution: {integrity: sha512-Re0RHelKLnjEURulY2/KxW+Ngb8zuNA4BRZuMwgGQNzVumT6u4U2N2hc01oeYVNVof0i7GrXE4UCNBgbpRRnjQ==} + cpu: [arm64] + os: [darwin] + + '@yuku-parser/binding-darwin-x64@0.8.7': + resolution: {integrity: sha512-Hn8DROtQkjlA1ACbPgj4a7eP9IuVOI504oiTwpkWPbpaDWD9KdmnVYCqW+1LfenNK/g7O9NhWGpXEdaCNX7lIA==} + cpu: [x64] + os: [darwin] + + '@yuku-parser/binding-freebsd-x64@0.8.7': + resolution: {integrity: sha512-bAP2OV8wRuzplX/jYxv9+vvqQT8JxyNphI8fLfXGL054Xs+4/J5u33cIm3y4rxY8rdoLmmdiJs2Tq7r7lrDRfA==} + cpu: [x64] + os: [freebsd] + + '@yuku-parser/binding-linux-arm-gnu@0.8.7': + resolution: {integrity: sha512-kTYwJQQgmZeAWdDIWabiReIZMpmfLueIj1tCmjStUtFGhR1Z0qwxonKVfUC4N7h/VhGGzLZ//7O1kgt1QKqgCg==} + cpu: [arm] + os: [linux] + + '@yuku-parser/binding-linux-arm-musl@0.8.7': + resolution: {integrity: sha512-uL4jE8HPT2BLlxAXyD10LqgPuXa9eDa0BKpCdSANmzIJghq/2eZo3/gQNtaxPZMupWoxjYzSad9IXrwu7aYPXQ==} + cpu: [arm] + os: [linux] + + '@yuku-parser/binding-linux-arm64-gnu@0.8.7': + resolution: {integrity: sha512-3gVN4pWSKZmXiNX7cU164dR9MPvesCHnlH6nPfpK+yQsCuYphjKKplcb4SnZBrhiqmXbgb2HR0c2TS0IZUPhgA==} + cpu: [arm64] + os: [linux] + + '@yuku-parser/binding-linux-arm64-musl@0.8.7': + resolution: {integrity: sha512-S0mwfEjoLpxzXeZw802Wa4RaELsQiPtWqG6INcy8j4GtvNFtl4LCX3eGO1XLn9pyLAISLzTRyU3zUCBUPin8lg==} + cpu: [arm64] + os: [linux] + + '@yuku-parser/binding-linux-x64-gnu@0.8.7': + resolution: {integrity: sha512-lnbWdPmerE5D1uH1G4IEZKnPzCrWCStRGrtgpSIe1RibAo5bZIjDbbbPYXmMHCEh4F+x/JaJpElh26a3r+BPbg==} + cpu: [x64] + os: [linux] + + '@yuku-parser/binding-linux-x64-musl@0.8.7': + resolution: {integrity: sha512-769uwndMvMzUvATWbAcEvyLHKA+DzhHSCl/obBUrRdYfRo26yxui6S8y3z7uJ+Naup7UKrDxrpK7OnQkxkl9KQ==} + cpu: [x64] + os: [linux] + + '@yuku-parser/binding-win32-arm64@0.8.7': + resolution: {integrity: sha512-mEB/9PlaAkisJ6KWGz0zvywXoU6+80dTlR2LwS7s/jcXXoU6fm2+sitBZXtqu3+Q4DcDgPxM45uWMCzPs0TSRw==} + cpu: [arm64] + os: [win32] + + '@yuku-parser/binding-win32-x64@0.8.7': + resolution: {integrity: sha512-8vNB2DP0ou61nGb8tc/qfi41gfyDXz1MHr2zqL3nR+cJ6CEbiuWV/l/a/vv151gCgiZLLAyGkQGENpozdg716w==} + cpu: [x64] + os: [win32] + + '@yuku-toolchain/types@0.8.7': + resolution: {integrity: sha512-2Z53dNxAJL6UvFoIrDZvYf3zlO8s4VJK4O2hhaB4mXVwwpX/7ajtss3cmfqKvamlNLWyt9FSWs4eoYdlbxpnHA==} + acorn@8.16.0: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} @@ -989,6 +1361,10 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} + ansis@4.3.1: + resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==} + engines: {node: '>=14'} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1309,6 +1685,15 @@ packages: resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} engines: {node: '>=12'} + dts-resolver@3.0.0: + resolution: {integrity: sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q==} + engines: {node: ^22.18.0 || >=24.0.0} + peerDependencies: + oxc-resolver: '>=11.0.0' + peerDependenciesMeta: + oxc-resolver: + optional: true + ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} @@ -1318,6 +1703,10 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + empathic@2.0.1: + resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==} + engines: {node: '>=14'} + enhanced-resolve@5.21.4: resolution: {integrity: sha512-wE4fDO8OjJhrPFH69HUQStq5oKvGRTNXEyW+k5C/pUQLASSsTu7obd2V3GvCDgPcY9AWjhJ4jz9Kh7iRvrxhJg==} engines: {node: '>=10.13.0'} @@ -1465,6 +1854,10 @@ packages: get-port-please@3.2.0: resolution: {integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==} + get-tsconfig@5.0.0-beta.5: + resolution: {integrity: sha512-/6gFNr0N04nob252sTQxyFLi3eKFRqIg1I87YcqAMT1i6SQrSF6KujUEQrtrjMV0H/eejTCltLdDSTEMzHbnsQ==} + engines: {node: '>=20.20.0'} + giget@3.2.0: resolution: {integrity: sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==} hasBin: true @@ -1570,6 +1963,10 @@ packages: import-meta-resolve@4.2.0: resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + import-without-cache@0.4.0: + resolution: {integrity: sha512-NkJQA7oZ4YHQhd2+H3BoRFKF3d/XNsiKpHZCQEMH9pDX27hQQLsTyOocyRgaIVtf8gHX3Nt3LPkR4e5EdtPAGQ==} + engines: {node: ^22.18.0 || >=24.0.0} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -1984,6 +2381,10 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + ofetch@1.5.1: resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} @@ -2050,6 +2451,10 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + pino-abstract-transport@2.0.0: resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} @@ -2127,6 +2532,9 @@ packages: quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + quansync@1.0.0: + resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -2203,6 +2611,9 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + restore-cursor@5.1.0: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} @@ -2214,11 +2625,35 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rolldown-plugin-dts@0.27.14: + resolution: {integrity: sha512-ZvuDDwoIpRK9RPxDXratCpklFO9QZZWndf/sd0VBFb4LEj0jj07UcHK9OCh7V4XiFz2Z89ziyBC2K6tJiDjrbw==} + engines: {node: ^22.18.0 || >=24.11.0} + peerDependencies: + '@typescript/native-preview': '*' + '@volar/typescript': ~2.4.0 + rolldown: ^1.0.0 + typescript: ^5.0.0 || ^6.0.0 || ~7.0.0 + vue-tsc: ~3.2.0 || ~3.3.0 + peerDependenciesMeta: + '@typescript/native-preview': + optional: true + '@volar/typescript': + optional: true + typescript: + optional: true + vue-tsc: + optional: true + rolldown@1.0.1: resolution: {integrity: sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + rolldown@1.2.4: + resolution: {integrity: sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + rollup@4.60.4: resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -2428,10 +2863,18 @@ packages: resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} engines: {node: '>=18'} + tinyexec@1.3.0: + resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} + engines: {node: '>=18'} + tinyglobby@0.2.16: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + tinyrainbow@3.1.0: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} @@ -2444,6 +2887,44 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + tsdown@0.22.14: + resolution: {integrity: sha512-ule7Y+fsAN2iZbLDoo7C4KYljFJNJJ+fLshyn+9gozeTspVersWHxwdGB+Dm2hzA38s6muFnUTl0jK3vJm9ifQ==} + engines: {node: ^22.18.0 || >=24.11.0} + hasBin: true + peerDependencies: + '@arethetypeswrong/core': ^0.18.1 + '@tsdown/css': 0.22.14 + '@tsdown/exe': 0.22.14 + '@vitejs/devtools': '*' + publint: ^0.3.8 + tsx: '*' + typescript: ^5.0.0 || ^6.0.0 || ^7.0.0 + unplugin-unused: ^0.5.0 + unrun: '*' + peerDependenciesMeta: + '@arethetypeswrong/core': + optional: true + '@tsdown/css': + optional: true + '@tsdown/exe': + optional: true + '@vitejs/devtools': + optional: true + publint: + optional: true + tsx: + optional: true + typescript: + optional: true + unplugin-unused: + optional: true + unrun: + optional: true + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -2472,6 +2953,12 @@ packages: uhyphen@0.2.0: resolution: {integrity: sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==} + unconfig-core@7.5.0: + resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.24.6: resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} @@ -2511,6 +2998,10 @@ packages: deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true + verkit@0.3.2: + resolution: {integrity: sha512-zj/ob3UsvJGN0whEAKFp53REA5X66hvffVqoCtVQAakJKnKlH+/PcOfMoFwIG/o4rElqLv/ycAFlx8ZlXUorCg==} + engines: {node: '>=18.12.0'} + vite-node@6.0.0: resolution: {integrity: sha512-oj4PVrT+pDh6GYf5wfUXkcZyekYS8kKPfLPXVl8qe324Ec6l4K2DUKNadRbZ3LQl0qGcDz+PyOo7ZAh00Y+JjQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2708,6 +3199,15 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yuku-ast@0.8.7: + resolution: {integrity: sha512-h6+4bDfyootiMB9vckk5uKo5r5j0GHrkr17FQTDNfEsFT3DWlN9uu1HJwQwc64pgmLCI945fWM3lbTIqxjT3GQ==} + + yuku-codegen@0.8.7: + resolution: {integrity: sha512-adwDZSh8oVDzhE6Du9PwVWxcOxeV0e2EVhUuMKWfhSY4wkrDq9eqixlxFF3l/XGUV1E7UFzhpz9393MUumkyNw==} + + yuku-parser@0.8.7: + resolution: {integrity: sha512-vRD9nwt4L3aYpxNqeSC4WqLv58xrXef0Ong1Mc45CTXTIpvLafx7JO05sczmQZwdLEZvywrLOGdNC5+Rp5N1BQ==} + zip-dir@2.0.0: resolution: {integrity: sha512-uhlsJZWz26FLYXOD6WVuq+fIcZ3aBPGo/cFdiLlv3KNwpa52IF3ISV8fLhQLiqVu5No3VhlqlgthN6gehil1Dg==} @@ -2817,6 +3317,115 @@ snapshots: dependencies: postcss-selector-parser: 7.1.1 + '@deepseek-ai/cordis@4.0.1': + dependencies: + '@deepseek-ai/cosmokit': 1.8.2 + '@standard-schema/spec': 1.1.0 + + '@deepseek-ai/cosmokit@1.8.2': {} + + '@deepseek-ai/dsh-agent@0.1.0-rc.6(b0514d7a320728b6d8f5a31eb3960e10)': + dependencies: + '@deepseek-ai/cordis': 4.0.1 + '@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1) + '@deepseek-ai/dsh-llm': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))) + '@deepseek-ai/dsh-scope': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/dsh-session': 0.1.0-rc.6(6fd26f59436a18b115f326d6060415e6) + '@deepseek-ai/dsh-system-prompt': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-llm@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))))(@deepseek-ai/dsh-scope@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))) + '@deepseek-ai/dsh-typert-protocol': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) + + '@deepseek-ai/dsh-attachment@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))': + dependencies: + '@deepseek-ai/cordis': 4.0.1 + '@deepseek-ai/dsh-brand': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1) + + '@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))': + dependencies: + '@deepseek-ai/cordis': 4.0.1 + '@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1) + + '@deepseek-ai/dsh-code-runtime@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))': + dependencies: + '@deepseek-ai/cordis': 4.0.1 + '@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1) + + '@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)': + dependencies: + '@deepseek-ai/cordis': 4.0.1 + '@deepseek-ai/schemastery': 3.18.1 + + '@deepseek-ai/dsh-llm@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))': + dependencies: + '@deepseek-ai/cordis': 4.0.1 + '@deepseek-ai/dsh-attachment': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/dsh-brand': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1) + '@deepseek-ai/dsh-timeout': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/schemastery': 3.18.1 + + '@deepseek-ai/dsh-scope@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))': + dependencies: + '@deepseek-ai/cordis': 4.0.1 + '@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1) + + '@deepseek-ai/dsh-session@0.1.0-rc.6(6fd26f59436a18b115f326d6060415e6)': + dependencies: + '@deepseek-ai/cordis': 4.0.1 + '@deepseek-ai/dsh-brand': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1) + '@deepseek-ai/dsh-llm': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))) + '@deepseek-ai/dsh-scope': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/dsh-typert-protocol': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) + + '@deepseek-ai/dsh-system-prompt@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-llm@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))))(@deepseek-ai/dsh-scope@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))': + dependencies: + '@deepseek-ai/cordis': 4.0.1 + '@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1) + '@deepseek-ai/dsh-llm': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))) + '@deepseek-ai/dsh-scope': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/schemastery': 3.18.1 + + '@deepseek-ai/dsh-timeout@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))': + dependencies: + '@deepseek-ai/cordis': 4.0.1 + '@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1) + + '@deepseek-ai/dsh-tools@0.1.0-rc.6(f8724372086ccc1457fc84e7becee2e0)': + dependencies: + '@deepseek-ai/cordis': 4.0.1 + '@deepseek-ai/dsh-agent': 0.1.0-rc.6(b0514d7a320728b6d8f5a31eb3960e10) + '@deepseek-ai/dsh-code-runtime': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1) + '@deepseek-ai/dsh-llm': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))) + '@deepseek-ai/dsh-scope': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/dsh-session': 0.1.0-rc.6(6fd26f59436a18b115f326d6060415e6) + '@deepseek-ai/dsh-system-prompt': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-llm@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))))(@deepseek-ai/dsh-scope@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))) + '@deepseek-ai/dsh-user-approval': 0.1.0-rc.6(dbdaba06c174c5e30e3a58edf3cc27a9) + '@deepseek-ai/schemastery': 3.18.1 + + '@deepseek-ai/dsh-typert-protocol@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))': + dependencies: + '@deepseek-ai/cordis': 4.0.1 + '@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1) + + '@deepseek-ai/dsh-user-approval@0.1.0-rc.6(dbdaba06c174c5e30e3a58edf3cc27a9)': + dependencies: + '@deepseek-ai/cordis': 4.0.1 + '@deepseek-ai/dsh-agent': 0.1.0-rc.6(b0514d7a320728b6d8f5a31eb3960e10) + '@deepseek-ai/dsh-brand': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/dsh-invariants': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1) + '@deepseek-ai/dsh-llm': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))) + '@deepseek-ai/dsh-scope': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/dsh-session': 0.1.0-rc.6(6fd26f59436a18b115f326d6060415e6) + '@deepseek-ai/dsh-system-prompt': 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-llm@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))))(@deepseek-ai/dsh-scope@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))) + '@deepseek-ai/schemastery': 3.18.1 + + '@deepseek-ai/schemastery@3.18.1': + dependencies: + '@deepseek-ai/cosmokit': 1.8.2 + '@standard-schema/spec': 1.1.0 + '@devicefarmer/adbkit-logcat@2.1.3': {} '@devicefarmer/adbkit-monkey@1.2.1': {} @@ -2977,6 +3586,8 @@ snapshots: '@oxc-project/types@0.130.0': {} + '@oxc-project/types@0.144.0': {} + '@pnpm/config.env-replace@1.1.0': {} '@pnpm/network.ca-file@1.0.2': @@ -2989,6 +3600,10 @@ snapshots: '@pnpm/network.ca-file': 1.0.2 config-chain: 1.1.13 + '@quansync/fs@1.0.0': + dependencies: + quansync: 1.0.0 + '@remixicon/react@4.9.0(react@19.2.6)': dependencies: react: 19.2.6 @@ -2996,39 +3611,75 @@ snapshots: '@rolldown/binding-android-arm64@1.0.1': optional: true + '@rolldown/binding-android-arm64@1.2.4': + optional: true + '@rolldown/binding-darwin-arm64@1.0.1': optional: true + '@rolldown/binding-darwin-arm64@1.2.4': + optional: true + '@rolldown/binding-darwin-x64@1.0.1': optional: true + '@rolldown/binding-darwin-x64@1.2.4': + optional: true + '@rolldown/binding-freebsd-x64@1.0.1': optional: true + '@rolldown/binding-freebsd-x64@1.2.4': + optional: true + '@rolldown/binding-linux-arm-gnueabihf@1.0.1': optional: true + '@rolldown/binding-linux-arm-gnueabihf@1.2.4': + optional: true + '@rolldown/binding-linux-arm64-gnu@1.0.1': optional: true + '@rolldown/binding-linux-arm64-gnu@1.2.4': + optional: true + '@rolldown/binding-linux-arm64-musl@1.0.1': optional: true + '@rolldown/binding-linux-arm64-musl@1.2.4': + optional: true + '@rolldown/binding-linux-ppc64-gnu@1.0.1': optional: true + '@rolldown/binding-linux-ppc64-gnu@1.2.4': + optional: true + '@rolldown/binding-linux-s390x-gnu@1.0.1': optional: true + '@rolldown/binding-linux-s390x-gnu@1.2.4': + optional: true + '@rolldown/binding-linux-x64-gnu@1.0.1': optional: true + '@rolldown/binding-linux-x64-gnu@1.2.4': + optional: true + '@rolldown/binding-linux-x64-musl@1.0.1': optional: true + '@rolldown/binding-linux-x64-musl@1.2.4': + optional: true + '@rolldown/binding-openharmony-arm64@1.0.1': optional: true + '@rolldown/binding-openharmony-arm64@1.2.4': + optional: true + '@rolldown/binding-wasm32-wasi@1.0.1': dependencies: '@emnapi/core': 1.10.0 @@ -3039,9 +3690,15 @@ snapshots: '@rolldown/binding-win32-arm64-msvc@1.0.1': optional: true + '@rolldown/binding-win32-arm64-msvc@1.2.4': + optional: true + '@rolldown/binding-win32-x64-msvc@1.0.1': optional: true + '@rolldown/binding-win32-x64-msvc@1.2.4': + optional: true + '@rolldown/pluginutils@1.0.1': {} '@rollup/rollup-android-arm-eabi@4.60.4': @@ -3248,9 +3905,14 @@ snapshots: '@types/minimatch@3.0.5': {} + '@types/node@22.20.1': + dependencies: + undici-types: 6.21.0 + '@types/node@25.8.0': dependencies: undici-types: 7.24.6 + optional: true '@types/react-dom@19.2.3(@types/react@19.2.14)': dependencies: @@ -3274,6 +3936,14 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 + '@vitest/mocker@4.1.6(vite@8.0.13(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0))': + dependencies: + '@vitest/spy': 4.1.6 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.13(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0) + '@vitest/mocker@4.1.6(vite@8.0.13(@types/node@25.8.0)(esbuild@0.27.7)(jiti@2.7.0))': dependencies: '@vitest/spy': 4.1.6 @@ -3321,11 +3991,11 @@ snapshots: '@types/filesystem': 0.0.36 '@types/har-format': 1.2.16 - '@wxt-dev/module-react@1.2.2(vite@8.0.13(@types/node@25.8.0)(esbuild@0.27.7)(jiti@2.7.0))(wxt@0.20.26(@types/node@25.8.0)(jiti@2.7.0)(rolldown@1.0.1)(rollup@4.60.4))': + '@wxt-dev/module-react@1.2.2(vite@8.0.13(@types/node@25.8.0)(esbuild@0.27.7)(jiti@2.7.0))(wxt@0.20.26(@types/node@25.8.0)(jiti@2.7.0)(rolldown@1.2.4)(rollup@4.60.4))': dependencies: '@vitejs/plugin-react': 6.0.2(vite@8.0.13(@types/node@25.8.0)(esbuild@0.27.7)(jiti@2.7.0)) vite: 8.0.13(@types/node@25.8.0)(esbuild@0.27.7)(jiti@2.7.0) - wxt: 0.20.26(@types/node@25.8.0)(jiti@2.7.0)(rolldown@1.0.1)(rollup@4.60.4) + wxt: 0.20.26(@types/node@25.8.0)(jiti@2.7.0)(rolldown@1.2.4)(rollup@4.60.4) transitivePeerDependencies: - '@rolldown/plugin-babel' - babel-plugin-react-compiler @@ -3336,6 +4006,80 @@ snapshots: async-mutex: 0.5.0 dequal: 2.0.3 + '@yuku-codegen/binding-android-arm64@0.8.7': + optional: true + + '@yuku-codegen/binding-darwin-arm64@0.8.7': + optional: true + + '@yuku-codegen/binding-darwin-x64@0.8.7': + optional: true + + '@yuku-codegen/binding-freebsd-x64@0.8.7': + optional: true + + '@yuku-codegen/binding-linux-arm-gnu@0.8.7': + optional: true + + '@yuku-codegen/binding-linux-arm-musl@0.8.7': + optional: true + + '@yuku-codegen/binding-linux-arm64-gnu@0.8.7': + optional: true + + '@yuku-codegen/binding-linux-arm64-musl@0.8.7': + optional: true + + '@yuku-codegen/binding-linux-x64-gnu@0.8.7': + optional: true + + '@yuku-codegen/binding-linux-x64-musl@0.8.7': + optional: true + + '@yuku-codegen/binding-win32-arm64@0.8.7': + optional: true + + '@yuku-codegen/binding-win32-x64@0.8.7': + optional: true + + '@yuku-parser/binding-android-arm64@0.8.7': + optional: true + + '@yuku-parser/binding-darwin-arm64@0.8.7': + optional: true + + '@yuku-parser/binding-darwin-x64@0.8.7': + optional: true + + '@yuku-parser/binding-freebsd-x64@0.8.7': + optional: true + + '@yuku-parser/binding-linux-arm-gnu@0.8.7': + optional: true + + '@yuku-parser/binding-linux-arm-musl@0.8.7': + optional: true + + '@yuku-parser/binding-linux-arm64-gnu@0.8.7': + optional: true + + '@yuku-parser/binding-linux-arm64-musl@0.8.7': + optional: true + + '@yuku-parser/binding-linux-x64-gnu@0.8.7': + optional: true + + '@yuku-parser/binding-linux-x64-musl@0.8.7': + optional: true + + '@yuku-parser/binding-win32-arm64@0.8.7': + optional: true + + '@yuku-parser/binding-win32-x64@0.8.7': + optional: true + + '@yuku-toolchain/types@0.8.7': {} + acorn@8.16.0: {} adm-zip@0.5.17: {} @@ -3367,6 +4111,8 @@ snapshots: ansi-styles@6.2.3: {} + ansis@4.3.1: {} + argparse@2.0.1: {} aria-query@5.3.0: @@ -3475,7 +4221,7 @@ snapshots: chrome-launcher@1.2.0: dependencies: - '@types/node': 25.8.0 + '@types/node': 22.20.1 escape-string-regexp: 4.0.0 is-wsl: 2.2.0 lighthouse-logger: 2.0.2 @@ -3653,6 +4399,8 @@ snapshots: dotenv@17.4.2: {} + dts-resolver@3.0.0: {} + ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer: 5.2.1 @@ -3661,6 +4409,8 @@ snapshots: emoji-regex@8.0.0: {} + empathic@2.0.1: {} + enhanced-resolve@5.21.4: dependencies: graceful-fs: 4.2.11 @@ -3753,6 +4503,10 @@ snapshots: optionalDependencies: picomatch: 4.0.4 + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + file-entry-cache@11.1.3: dependencies: flat-cache: 6.1.22 @@ -3807,6 +4561,10 @@ snapshots: get-port-please@3.2.0: {} + get-tsconfig@5.0.0-beta.5: + dependencies: + resolve-pkg-maps: 1.0.0 + giget@3.2.0: {} glob-parent@5.1.2: @@ -3904,6 +4662,8 @@ snapshots: import-meta-resolve@4.2.0: {} + import-without-cache@0.4.0: {} + imurmurhash@0.1.4: {} inherits@2.0.4: {} @@ -4256,6 +5016,8 @@ snapshots: obug@2.1.1: {} + obug@2.1.4: {} + ofetch@1.5.1: dependencies: destr: 2.0.5 @@ -4327,6 +5089,8 @@ snapshots: picomatch@4.0.4: {} + picomatch@4.0.5: {} + pino-abstract-transport@2.0.0: dependencies: split2: 4.2.0 @@ -4423,6 +5187,8 @@ snapshots: quansync@0.2.11: {} + quansync@1.0.0: {} + queue-microtask@1.2.3: {} quick-format-unescaped@4.0.4: {} @@ -4487,6 +5253,8 @@ snapshots: resolve-from@5.0.0: {} + resolve-pkg-maps@1.0.0: {} + restore-cursor@5.1.0: dependencies: onetime: 7.0.0 @@ -4496,6 +5264,20 @@ snapshots: rfdc@1.4.1: {} + rolldown-plugin-dts@0.27.14(rolldown@1.2.4)(typescript@5.9.3): + dependencies: + dts-resolver: 3.0.0 + get-tsconfig: 5.0.0-beta.5 + obug: 2.1.4 + rolldown: 1.2.4 + yuku-ast: 0.8.7 + yuku-codegen: 0.8.7 + yuku-parser: 0.8.7 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - oxc-resolver + rolldown@1.0.1: dependencies: '@oxc-project/types': 0.130.0 @@ -4517,6 +5299,26 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.1 '@rolldown/binding-win32-x64-msvc': 1.0.1 + rolldown@1.2.4: + dependencies: + '@oxc-project/types': 0.144.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.2.4 + '@rolldown/binding-darwin-arm64': 1.2.4 + '@rolldown/binding-darwin-x64': 1.2.4 + '@rolldown/binding-freebsd-x64': 1.2.4 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.4 + '@rolldown/binding-linux-arm64-gnu': 1.2.4 + '@rolldown/binding-linux-arm64-musl': 1.2.4 + '@rolldown/binding-linux-ppc64-gnu': 1.2.4 + '@rolldown/binding-linux-s390x-gnu': 1.2.4 + '@rolldown/binding-linux-x64-gnu': 1.2.4 + '@rolldown/binding-linux-x64-musl': 1.2.4 + '@rolldown/binding-openharmony-arm64': 1.2.4 + '@rolldown/binding-win32-arm64-msvc': 1.2.4 + '@rolldown/binding-win32-x64-msvc': 1.2.4 + rollup@4.60.4: dependencies: '@types/estree': 1.0.8 @@ -4768,11 +5570,18 @@ snapshots: tinyexec@1.1.2: {} + tinyexec@1.3.0: {} + tinyglobby@0.2.16: dependencies: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + tinyrainbow@3.1.0: {} tmp@0.2.5: {} @@ -4781,6 +5590,33 @@ snapshots: dependencies: is-number: 7.0.0 + tree-kill@1.2.2: {} + + tsdown@0.22.14(typescript@5.9.3): + dependencies: + ansis: 4.3.1 + cac: 7.0.0 + defu: 6.1.7 + empathic: 2.0.1 + hookable: 6.1.1 + import-without-cache: 0.4.0 + obug: 2.1.4 + picomatch: 4.0.5 + rolldown: 1.2.4 + rolldown-plugin-dts: 0.27.14(rolldown@1.2.4)(typescript@5.9.3) + tinyexec: 1.3.0 + tinyglobby: 0.2.17 + tree-kill: 1.2.2 + unconfig-core: 7.5.0 + verkit: 0.3.2 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@typescript/native-preview' + - '@volar/typescript' + - oxc-resolver + - vue-tsc + tslib@2.8.1: {} tw-animate-css@1.4.0: {} @@ -4797,9 +5633,17 @@ snapshots: uhyphen@0.2.0: {} - undici-types@7.24.6: {} + unconfig-core@7.5.0: + dependencies: + '@quansync/fs': 1.0.0 + quansync: 1.0.0 - unimport@6.3.0(rolldown@1.0.1): + undici-types@6.21.0: {} + + undici-types@7.24.6: + optional: true + + unimport@6.3.0(rolldown@1.2.4): dependencies: acorn: 8.16.0 escape-string-regexp: 5.0.0 @@ -4816,7 +5660,7 @@ snapshots: unplugin: 3.0.0 unplugin-utils: 0.3.1 optionalDependencies: - rolldown: 1.0.1 + rolldown: 1.2.4 universalify@2.0.1: {} @@ -4848,6 +5692,8 @@ snapshots: uuid@8.3.2: {} + verkit@0.3.2: {} + vite-node@6.0.0(@types/node@25.8.0)(esbuild@0.27.7)(jiti@2.7.0): dependencies: cac: 7.0.0 @@ -4869,6 +5715,19 @@ snapshots: - tsx - yaml + vite@8.0.13(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.14 + rolldown: 1.0.1 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 22.20.1 + esbuild: 0.27.7 + fsevents: 2.3.3 + jiti: 2.7.0 + vite@8.0.13(@types/node@25.8.0)(esbuild@0.27.7)(jiti@2.7.0): dependencies: lightningcss: 1.32.0 @@ -4882,6 +5741,34 @@ snapshots: fsevents: 2.3.3 jiti: 2.7.0 + vitest@4.1.6(@types/node@22.20.1)(happy-dom@15.11.7)(vite@8.0.13(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)): + dependencies: + '@vitest/expect': 4.1.6 + '@vitest/mocker': 4.1.6(vite@8.0.13(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)) + '@vitest/pretty-format': 4.1.6 + '@vitest/runner': 4.1.6 + '@vitest/snapshot': 4.1.6 + '@vitest/spy': 4.1.6 + '@vitest/utils': 4.1.6 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.1.2 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: 8.0.13(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.20.1 + happy-dom: 15.11.7 + transitivePeerDependencies: + - msw + vitest@4.1.6(@types/node@25.8.0)(happy-dom@15.11.7)(vite@8.0.13(@types/node@25.8.0)(esbuild@0.27.7)(jiti@2.7.0)): dependencies: '@vitest/expect': 4.1.6 @@ -5004,7 +5891,7 @@ snapshots: is-wsl: 3.1.1 powershell-utils: 0.1.0 - wxt@0.20.26(@types/node@25.8.0)(jiti@2.7.0)(rolldown@1.0.1)(rollup@4.60.4): + wxt@0.20.26(@types/node@25.8.0)(jiti@2.7.0)(rolldown@1.2.4)(rollup@4.60.4): dependencies: '@1natsu/wait-element': 4.2.0 '@aklinker1/rollup-plugin-visualizer': 5.12.0(rollup@4.60.4) @@ -5044,7 +5931,7 @@ snapshots: publish-browser-extension: 4.0.5 scule: 1.3.0 tinyglobby: 0.2.16 - unimport: 6.3.0(rolldown@1.0.1) + unimport: 6.3.0(rolldown@1.2.4) vite: 8.0.13(@types/node@25.8.0)(esbuild@0.27.7)(jiti@2.7.0) vite-node: 6.0.0(@types/node@25.8.0)(esbuild@0.27.7)(jiti@2.7.0) web-ext-run: 0.2.4 @@ -5089,6 +5976,45 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yuku-ast@0.8.7: + dependencies: + '@yuku-toolchain/types': 0.8.7 + + yuku-codegen@0.8.7: + dependencies: + '@yuku-toolchain/types': 0.8.7 + optionalDependencies: + '@yuku-codegen/binding-android-arm64': 0.8.7 + '@yuku-codegen/binding-darwin-arm64': 0.8.7 + '@yuku-codegen/binding-darwin-x64': 0.8.7 + '@yuku-codegen/binding-freebsd-x64': 0.8.7 + '@yuku-codegen/binding-linux-arm-gnu': 0.8.7 + '@yuku-codegen/binding-linux-arm-musl': 0.8.7 + '@yuku-codegen/binding-linux-arm64-gnu': 0.8.7 + '@yuku-codegen/binding-linux-arm64-musl': 0.8.7 + '@yuku-codegen/binding-linux-x64-gnu': 0.8.7 + '@yuku-codegen/binding-linux-x64-musl': 0.8.7 + '@yuku-codegen/binding-win32-arm64': 0.8.7 + '@yuku-codegen/binding-win32-x64': 0.8.7 + + yuku-parser@0.8.7: + dependencies: + '@yuku-toolchain/types': 0.8.7 + yuku-ast: 0.8.7 + optionalDependencies: + '@yuku-parser/binding-android-arm64': 0.8.7 + '@yuku-parser/binding-darwin-arm64': 0.8.7 + '@yuku-parser/binding-darwin-x64': 0.8.7 + '@yuku-parser/binding-freebsd-x64': 0.8.7 + '@yuku-parser/binding-linux-arm-gnu': 0.8.7 + '@yuku-parser/binding-linux-arm-musl': 0.8.7 + '@yuku-parser/binding-linux-arm64-gnu': 0.8.7 + '@yuku-parser/binding-linux-arm64-musl': 0.8.7 + '@yuku-parser/binding-linux-x64-gnu': 0.8.7 + '@yuku-parser/binding-linux-x64-musl': 0.8.7 + '@yuku-parser/binding-win32-arm64': 0.8.7 + '@yuku-parser/binding-win32-x64': 0.8.7 + zip-dir@2.0.0: dependencies: async: 3.2.6