diff --git a/.gitignore b/.gitignore index 32b5585..d589979 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,14 @@ node_modules/ dist/ *.tsbuildinfo +# Rust / Tauri build artifacts (packages/vesper-desktop — the native shell, DEV-112). +# Cargo.lock IS committed (this is an application binary — reproducible builds). +target/ +/packages/vesper-desktop/src-tauri/gen/ +# generated daemon-embed assets + the compiled sidecar binary (DEV-112 Slice 2) +/packages/vesper-cli/src/generated/ +/packages/vesper-desktop/src-tauri/binaries/ + # Vesper local runtime directory .vesper/ diff --git a/README.md b/README.md index 1d4f2a6..0a7d56b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- Vesper World — watch your personal agents work + Vesper — a local-first desktop app for your personal automation agents

Vesper

@@ -12,17 +12,18 @@ Bring your own CLI

-

A local-first runtime for your personal automation agents — that you can actually watch work.

+

A local-first runtime for your personal automation agents — with a native desktop app you actually want to open.

Vesper runs on your machine and hosts small automation **pipelines** (your "agents") under one host process. It drives **the AI CLI you already pay for** — `claude`, `codex`, `opencode`, or `gemini` — so it holds **no API keys** and ships **no provider SDKs**. Nothing leaves your machine except the -calls your own CLI makes. And instead of a cold dashboard, you watch your agents in a little -pixel-art world: tap one to see, in plain language, what it just did — or to put it to work. +calls your own CLI makes. You talk to it in a premium dark-glass desktop app: chat with Vesper, watch +exactly what it's doing as it works, and manage every pipeline, channel, schedule, and permission from +one window — with a macOS menu-bar popover for a quick glance. - + @@ -35,21 +36,30 @@ pixel-art world: tap one to see, in plain language, what it just did — or to p --- -## Vesper World +## The app ```sh vesper daemon start # hosts the runtime + the UI (background) -vesper ui # opens a browser tab — http://127.0.0.1:4317 +vesper ui # open it in your browser — http://127.0.0.1:4317 ``` +Or run the **native desktop app** — a [Tauri](https://tauri.app) shell over the same daemon — from +`packages/vesper-desktop` (`bun run dev`): a frameless window with a macOS menu-bar (tray) popover, no +browser involved. + +

+ Runtime — daemon health, helper-CLI status, storage + Diagnostics — CLI probes, recent runs, and the agents running on your machine +

- Click an agent to see what it just did, in plain language + Settings — theme picker and runtime configuration

-Tap an agent → a plain-language card shows what it last did and lets you run it. The world is a live -projection of your real runtime (pipelines, runs, schedules) — nothing is faked. It's deliberately -simple, and built to extend: a planned **Voice** module will let an agent *speak* its result aloud. -See [docs/ui.md](docs/ui.md). +Chat with Vesper and it routes your message to the right pipeline and streams the run live in a Vesper +activity rail. Every section reads your **real** runtime — nothing is faked: **Runtime** (daemon + +helper-CLI health), **Diagnostics** (CLI probes, recent runs, and the agents running on your machine), +**Channels**, **Schedule**, **Pipelines**, **Permissions**, **Settings** (theme + config), and more. +Dark glass is the default; a light and a warm theme ship too. See [docs/ui.md](docs/ui.md). ## Bring your own CLI @@ -82,7 +92,7 @@ vesper init # create ~/.vesper, initialize storage, detect installed CL vesper cli list # show each CLI + probe status (ok / not-authenticated / not-installed) vesper hello # ask your configured CLI to reply — proves orchestration works vesper daemon start # start the runtime + UI (background) -vesper ui # open Vesper World in your browser +vesper ui # open the Vesper app in your browser ``` `vesper hello` is the proof the model works: a fixed prompt to your CLI, reply printed — no @@ -152,7 +162,7 @@ this list never drifts. Run `vesper --help` for details; see also [doc invocation if a tool changes its flags. `storage.redactRunSummaries` (opt-in) stores run summaries as size-only metadata instead of raw CLI output. -`presence` tunes the live agent view in Vesper World (the running agents it "echoes"). Vesper ships an +`presence` tunes the live agent view in the **Diagnostics** section (the running agents it "echoes"). Vesper ships an allowlist for `claude`, `codex`, `opencode`, `gemini`, and `zeroclaw`; `presence.matchers` **adds** your own without touching code. Each matcher is `{ id, label, kind: "cli" | "app", pattern, exclude? }`, where `pattern`/`exclude` are regexes matched (case-insensitively) against a process's full command diff --git a/bun.lock b/bun.lock index ff5a773..e7a1efb 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "devDependencies": { "@biomejs/biome": "^2.4.15", "@types/bun": "^1.3.14", + "@vesper/ui": "workspace:*", }, }, "packages/pipelines": { @@ -32,6 +33,13 @@ "name": "@vesper/core", "version": "0.1.0", }, + "packages/vesper-desktop": { + "name": "@vesper/desktop", + "version": "0.1.0", + "devDependencies": { + "@tauri-apps/cli": "^2.11.2", + }, + }, "packages/vesper-ui": { "name": "@vesper/ui", "version": "0.1.0", @@ -59,6 +67,30 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.15", "", { "os": "win32", "cpu": "x64" }, "sha512-zBrGq5mx5wwpnow4+2BxUvleDM+GNd4sLbPaMapsSLQLD0NGRCquqPBTgN+7XkUteHvj7M+BstuI8tmnV7+HgQ=="], + "@tauri-apps/cli": ["@tauri-apps/cli@2.11.2", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.11.2", "@tauri-apps/cli-darwin-x64": "2.11.2", "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.2", "@tauri-apps/cli-linux-arm64-gnu": "2.11.2", "@tauri-apps/cli-linux-arm64-musl": "2.11.2", "@tauri-apps/cli-linux-riscv64-gnu": "2.11.2", "@tauri-apps/cli-linux-x64-gnu": "2.11.2", "@tauri-apps/cli-linux-x64-musl": "2.11.2", "@tauri-apps/cli-win32-arm64-msvc": "2.11.2", "@tauri-apps/cli-win32-ia32-msvc": "2.11.2", "@tauri-apps/cli-win32-x64-msvc": "2.11.2" }, "bin": { "tauri": "tauri.js" } }, "sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw=="], + + "@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.11.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w=="], + + "@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.11.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-VjYYtZUPqDMLutSfJEyxFE3Bz+DPi7c8wC3imckgvciLDZLq4qwKJxBicg0BXGhXjJsl8vKWgWRFNMPELQ+Xyg=="], + + "@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.11.2", "", { "os": "linux", "cpu": "arm" }, "sha512-yMemD6f4i95AQriS8EazyOFzbE34yjnP16i3IOzpHGQvBoy2DjypFMFBq0NtPuITURv/cOGguRtHR5d79/9CSA=="], + + "@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.11.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-cgI91D2wL8GSgoWwZXDqt+DwnuZCP2/bz03QAE4TrhgAKIsrB4hX26W/H1EONPUUNkqrsgeCD0wU6pcNjV/5kw=="], + + "@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.11.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw=="], + + "@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.11.2", "", { "os": "linux", "cpu": "none" }, "sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ=="], + + "@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.11.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw=="], + + "@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.11.2", "", { "os": "linux", "cpu": "x64" }, "sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw=="], + + "@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.11.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA=="], + + "@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.11.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YhjQNZcXfbkCLyazSv1nPnJ9iRFE1wm6kc51FDbU10/Dk09io+6PAGMLjkxnX2GdM0qMnDmTjstY8mTDVvtKeA=="], + + "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.11.2", "", { "os": "win32", "cpu": "x64" }, "sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA=="], + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], "@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="], @@ -67,6 +99,8 @@ "@vesper/core": ["@vesper/core@workspace:packages/vesper-core"], + "@vesper/desktop": ["@vesper/desktop@workspace:packages/vesper-desktop"], + "@vesper/pipelines": ["@vesper/pipelines@workspace:packages/pipelines"], "@vesper/ui": ["@vesper/ui@workspace:packages/vesper-ui"], diff --git a/cycle-log.md b/cycle-log.md index abd2087..efea13b 100644 --- a/cycle-log.md +++ b/cycle-log.md @@ -651,3 +651,78 @@ the still-blocked forge sandbox (it executes no LLM-generated code). - DEFERRED (per spec Out of Scope): applying `fix_proposal`s (software-engineer pipeline); authoring pipeline CODE (forge, blocked on the sandbox); `NETWORK_FETCH`; a RAG/embedding index over signals; an elder-surface approval tile; auto-running skill-train on a newly acquired skill. + +## Chatbot home + editable pipeline templates (#9 + #4) — SHIPPED + +`specs/chatbot-home.md`. The post-onboarding HOME is a simple chatbot; the canvas demotes to a side +activity panel. Built on the SHIPPED orchestration+trace backbone (consumed, not modified). No Linear +issue (issue-capped) -> specs/ + this entry + the commit are the record (Rule 11). Built by a +Backend->Client->Review workflow; the review's 2 real HIGH gaps were then fixed by the lead. + +- **Storage (migration `007_chat_home`):** `chat_sessions`, `chat_turns`, `pipeline_templates` + index; + 6 synchronous `Store` methods (createSession, appendTurn, listSessions, listTurns, getTemplate, + upsertTemplate) mirroring the existing JSON/assert helpers. `chat_turns.run_id` links an assistant + turn to the run that produced it (transcript bubble == activity-tree root, same data two ways). +- **Router pipeline (`packages/pipelines/router/`):** a chat message is a manual `scheduler.run("router", + {params})` (the EXISTING run path — no new execution). The handler classifies via `ctx.complete` to ONE + label, maps it through a FIXED ALLOWLIST to a registered handler id, and `ctx.spawn`s it; an + unmapped/free-form label -> a clarify turn (NO spawn, no dynamic id — preserves no-eval). caps + [CLI_INVOKE, WRITE_STORAGE, SPAWN_SUBAGENT]. +- **Routes + WS:** `POST /api/chat`, `GET /api/chat/sessions`, `GET /api/chat/sessions/:id/turns`, + `GET /api/pipelines`, `GET|PUT /api/pipelines/:id/template`; a `chat:` WS topic next to the + backbone's `agent:` (one socket, UUID-guarded). Client: transcript home + demoted activity + panel (reuses the runTree render) + a templates screen; reduced-motion + WCAG-AA honored. +- **Security:** a minimal out-of-band approval-token module (`vesper-core/src/approval/`, CSPRNG + single-use) gates `PUT /template`; `POST /api/approval/request` mints a code and prints it to the daemon + TTY (out-of-band — never in the HTTP response, so a local app can mint but not read it). The future + `security-hardening.md` adopts this seam. `POST /api/chat` is isLocalRequest-only (deliberate parity + with the existing run route, so the canvas Run button still works). +- **Lead fixes over the workflow output** (2 real HIGHs the review caught): (1) `mint()` had NO production + caller -> added the `/api/approval/request` mint path + test, so template editing actually works + end-to-end; (2) the router ignored template `default_params` -> it now MERGES the target's editable + default_params UNDER the user message (injected via `registerPipelines({getDefaultParams})` -> daemon + wires `store.getTemplate`), so an edited template configures its runs (#4). + router/server tests. +- 724 tests / 0 fail (+ chatbot suite + the 2 fix tests); Biome clean; no NEW tsc errors (same 16 + pre-existing); no provider SDKs. +- NOTED (not blocking): `PUT /template` persists prompt/params only — schedule/caps stay editable via + `vesper schedule` (the spec's Design-Decisions/Acceptance contradict each other; took the conservative + path). Migration `007_chat_home` takes the next free id; the umbrella ledger's planning reservation + (007=rag) shifts to 008/009 for rag/eval (gitignored planning doc, reconciled at their build). +- DEFERRED (per spec Out of Scope): the security-hardening §C token formalization; multi-session history + UX; capability editing from the templates UI; token-level streaming. + +--- + +## Desktop shell redesign — premium dark-glass native companion + Vesper World rebuild — SHIPPED + +- Specs: `specs/desktop-app-shell.md` + `specs/vesper-world-rebuild.md` (Omar-authorized 2026-06-02; record + surface = specs + this log; Linear issue cap active). Reference look: OpenClaw Windows Companion. +- **Decisions locked (Omar):** premium dark-glass SUPERSEDES the elder-first *visual* framing (Hard rule 14 + amendment pending on a later sync); primary section name = **Pipelines**; presence/echo MOVES to + Diagnostics (not deleted); built shell + rebuilt Chat together as slice 1. +- **What shipped:** the `@vesper/ui` client is now an app shell — custom draggable titlebar (Cmd+E command + search, live status pills off `/api/status`), grouped sidebar, a client-side `SectionRouter`, and a + chrome-only theme system (dark default; light/hearth opt-in) that REPLACES the canvas-coupled `WorldTheme`. + 14 sections: Chat + Runtime/CLIs/Permissions/Sandbox/Settings/Diagnostics/About (live) + Pipelines/ + Channels/Schedule (thin) + Skills/Memory/Voice (honest stubs naming their specs). +- **Vesper World rebuilt:** the pixel-art canvas + machine-wide presence home are RETIRED (controlled + `git rm`, recoverable). Chat = transcript + a Vesper-ONLY activity rail (follows the conversation's run + tree via the existing `/api/chat` + run-trace APIs; subscribe-before-backfill + de-dupe preserved). No + backend rewrite — reused chat/router/sessions/turns verbatim. +- **Server:** new read-only `/api/status`, `/api/presence`, `/api/runs`; `/api/world` + `snapshot.ts` removed; + presence poll kept (feeds `/api/presence` for Diagnostics). +- **Native:** macOS overlay titlebar (`TitleBarStyle::Overlay` + `hidden_title`, cfg-gated to macOS) so the + custom HTML titlebar shows with the traffic lights inset; tray + single-instance from DEV-112 slice 3. +- **Parallel build:** lead built the backbone + Chat + real sections + server routes; 2 sub-agents built the + 6 thin views + the Rust overlay window concurrently (file-disjoint). Net **-890 lines** tracked in vesper-ui. +- **Gotcha (cost a runtime crash Omar caught):** the browser client is bundled by Bun (which does NOT error + on an undefined identifier) and sits OUTSIDE the root tsc program, so a section referenced in the barrel + but never imported (`sandboxSection`) only failed at runtime in the browser — green tests + clean bundle + missed it. FIX + GUARD: `sections/index.test.ts` imports the barrel and asserts ALL_SECTIONS (14, unique + ids, valid shape). Lesson: for the browser client, an import-the-barrel test is the real typecheck. +- Verified: `biome ci` clean (2 cosmetic warnings), vesper-ui 46 / vesper-cli 104 pass, no new tsc errors in + touched files, compiled sidecar serves the new shell end-to-end. No provider SDKs. +- DEFERRED: privileged config writes from Settings (theme is client-side; default-CLI read-only); full + template editing in Pipelines (read-only view); Windows/Linux window chrome (macOS-first per Omar); the + one `!important` (reduced-motion) biome warning; the menu-bar popover app + internal-pipelines auto-skills + feature (next requests). diff --git a/docs/imgs/vesper-app.png b/docs/imgs/vesper-app.png new file mode 100644 index 0000000..03bd1fc Binary files /dev/null and b/docs/imgs/vesper-app.png differ diff --git a/docs/imgs/vesper-diagnostics.png b/docs/imgs/vesper-diagnostics.png new file mode 100644 index 0000000..a33d44d Binary files /dev/null and b/docs/imgs/vesper-diagnostics.png differ diff --git a/docs/imgs/vesper-menubar.png b/docs/imgs/vesper-menubar.png new file mode 100644 index 0000000..09cae74 Binary files /dev/null and b/docs/imgs/vesper-menubar.png differ diff --git a/docs/imgs/vesper-runtime.png b/docs/imgs/vesper-runtime.png new file mode 100644 index 0000000..edee1ed Binary files /dev/null and b/docs/imgs/vesper-runtime.png differ diff --git a/docs/imgs/vesper-settings.png b/docs/imgs/vesper-settings.png new file mode 100644 index 0000000..9a60513 Binary files /dev/null and b/docs/imgs/vesper-settings.png differ diff --git a/package.json b/package.json index e717e3a..b9dfbf1 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "vesper": "bun packages/vesper-cli/src/index.ts", "sync:ai": "bun scripts/sync-ai-docs.ts", "docs:cli": "bun scripts/gen-cli-docs.ts", + "build:daemon": "bun scripts/build-daemon.ts", "prepare": "git config core.hooksPath .githooks || true" }, "engines": { @@ -21,6 +22,7 @@ }, "devDependencies": { "@biomejs/biome": "^2.4.15", - "@types/bun": "^1.3.14" + "@types/bun": "^1.3.14", + "@vesper/ui": "workspace:*" } } diff --git a/packages/pipelines/index.ts b/packages/pipelines/index.ts index c8d13ee..6ca0314 100644 --- a/packages/pipelines/index.ts +++ b/packages/pipelines/index.ts @@ -12,6 +12,7 @@ import { type Capability, type HandlerRegistry, type RegisterTaskInput, + type RunParams, type Scheduler, SchedulerError, type TaskHandler, @@ -28,6 +29,13 @@ import { orchestratorDemoHandler, orchestratorDemoTaskInput, } from "./orchestrator-demo/handler.ts"; +import { + makeRouterHandler, + ROUTE_ALLOWLIST, + ROUTER_HANDLER_ID, + routerHandler, + routerTaskInput, +} from "./router/handler.ts"; import { SELFTEST_HANDLER_ID, selftestHandler, selftestTaskInput } from "./selftest/handler.ts"; import { SKILL_TRAIN_HANDLER_ID, @@ -44,6 +52,10 @@ export { ORCHESTRATOR_DEMO_HANDLER_ID, orchestratorDemoHandler, orchestratorDemoTaskInput, + ROUTE_ALLOWLIST, + ROUTER_HANDLER_ID, + routerHandler, + routerTaskInput, SELFTEST_HANDLER_ID, SKILL_TRAIN_HANDLER_ID, selftestHandler, @@ -71,6 +83,14 @@ export const PIPELINES: readonly PipelineDescriptor[] = [ handler: selftestHandler, taskInput: selftestTaskInput, }, + // The chatbot-home dispatcher: a chat message is a manual run of this pipeline. It + // classifies the wish via the CLI and spawns one allowlisted built-in. Adds + // CLI_INVOKE + WRITE_STORAGE + SPAWN_SUBAGENT to the host grant union. + { + handlerId: ROUTER_HANDLER_ID, + handler: routerHandler, + taskInput: routerTaskInput, + }, { handlerId: SKILL_TRAIN_HANDLER_ID, handler: skillTrainHandler, @@ -126,9 +146,29 @@ export function grantedCapabilities(): Capability[] { * so a daemon restart backfills grants for tasks persisted before per-task grants * existed. No grant writing happens here; that would duplicate the ceiling check. */ -export function registerPipelines(scheduler: Scheduler, registry: HandlerRegistry): void { +/** Host-injected wiring for built-in pipelines (e.g. the router's template reader). */ +export interface RegisterPipelinesOptions { + /** + * Resolves a target handler's editable template `default_params` so the `router` + * merges them into spawn params (#4). When omitted, the router uses no defaults + * (the built-in handler), so non-daemon callers and tests behave unchanged. + */ + readonly getDefaultParams?: (handlerId: string) => RunParams; +} + +export function registerPipelines( + scheduler: Scheduler, + registry: HandlerRegistry, + options: RegisterPipelinesOptions = {}, +): void { for (const descriptor of PIPELINES) { - registry.register(descriptor.handlerId, descriptor.handler); + // The daemon injects the template reader into the router so edited templates take + // effect; every other handler registers as declared. + const handler = + descriptor.handlerId === ROUTER_HANDLER_ID && options.getDefaultParams !== undefined + ? makeRouterHandler({ getDefaultParams: options.getDefaultParams }) + : descriptor.handler; + registry.register(descriptor.handlerId, handler); // Spawn-only descriptors (no taskInput) register the handler only. if (descriptor.taskInput === undefined) continue; diff --git a/packages/pipelines/router/handler.test.ts b/packages/pipelines/router/handler.test.ts new file mode 100644 index 0000000..6472bad --- /dev/null +++ b/packages/pipelines/router/handler.test.ts @@ -0,0 +1,279 @@ +import { Database } from "bun:sqlite"; +import { describe, expect, test } from "bun:test"; +import { rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import type { + CompleteResult, + PipelineContext, + RunOutcome, + SubAgentDescriptor, + SubAgentHandle, +} from "@vesper/core"; +import { HandlerRegistry, openStore, Scheduler, type Store } from "@vesper/core"; +import { grantedCapabilities, registerPipelines } from "../index.ts"; +import { + makeRouterHandler, + ROUTE_ALLOWLIST, + ROUTER_HANDLER_ID, + routerTaskInput, +} from "./handler.ts"; + +// --------------------------------------------------------------------------- +// Fake PipelineContext — records complete prompts, spawn descriptors, and runs. +// --------------------------------------------------------------------------- + +interface FakeContext { + readonly ctx: PipelineContext; + readonly completePrompts: string[]; + readonly spawned: SubAgentDescriptor[]; + readonly recordedRuns: Array<{ status: string; summary: string }>; + readonly progress: string[]; +} + +function makeFakeContext(options: { + readonly params?: Record; + /** Text the fake `complete` returns (the classifier label). */ + readonly classifyReply?: string; + /** Status the spawned child resolves with. */ + readonly childStatus?: string; + /** When true, the spawned child's `done` rejects (handler must tolerate it). */ + readonly childRejects?: boolean; +}): FakeContext { + const completePrompts: string[] = []; + const spawned: SubAgentDescriptor[] = []; + const recordedRuns: Array<{ status: string; summary: string }> = []; + const progress: string[] = []; + + const ctx: PipelineContext = { + task: { + id: "router", + kind: "manual", + schedule_expr: "", + handler_id: "router", + enabled: true, + last_run_at: null, + last_error: null, + max_runs_per_day: null, + max_concurrent: null, + max_duration_ms: null, + runs_today: 0, + runs_today_date: null, + attempt_count: 0, + next_attempt_at: null, + required_capabilities: ["CLI_INVOKE", "WRITE_STORAGE", "SPAWN_SUBAGENT"], + }, + now: new Date(2025, 0, 1), + params: options.params ?? {}, + runId: "router-run", + parentRunId: null, + async complete(prompt): Promise { + completePrompts.push(prompt); + const text = options.classifyReply ?? "none"; + return { text, exit_code: 0, raw_stdout: text, raw_stderr: "", duration_ms: 1 }; + }, + recordRun({ status, summary }) { + recordedRuns.push({ status, summary }); + return "router-run"; + }, + emitProgress(e) { + progress.push(e.message); + }, + spawn(descriptor): SubAgentHandle { + spawned.push(descriptor); + const outcome: RunOutcome = { + taskId: descriptor.handlerId, + runId: "child-run", + status: options.childStatus ?? "ok", + summary: "child done", + cli: null, + durationMs: 1, + }; + return { + runId: "child-run", + handlerId: descriptor.handlerId, + label: descriptor.label, + done: options.childRejects + ? Promise.reject(new Error("child failed")) + : Promise.resolve(outcome), + }; + }, + readSignals() { + throw new Error("readSignals is not supported in this fake context"); + }, + }; + + return { ctx, completePrompts, spawned, recordedRuns, progress }; +} + +// --------------------------------------------------------------------------- +// routerHandler — classify + dispatch + no-eval fallback +// --------------------------------------------------------------------------- + +describe("routerHandler", () => { + test("an empty message produces a clarify turn with no spawn and no CLI call", async () => { + const fake = makeFakeContext({ params: { message: " " } }); + await makeRouterHandler()(fake.ctx); + + expect(fake.completePrompts).toHaveLength(0); + expect(fake.spawned).toHaveLength(0); + expect(fake.recordedRuns).toHaveLength(1); + expect(fake.recordedRuns[0]?.status).toBe("clarify"); + }); + + test("a mapped label spawns the allowlisted handler id with the message", async () => { + const fake = makeFakeContext({ + params: { message: "run the self test" }, + classifyReply: "selftest", + }); + await makeRouterHandler()(fake.ctx); + + expect(fake.completePrompts).toHaveLength(1); + expect(fake.spawned).toHaveLength(1); + expect(fake.spawned[0]?.handlerId).toBe(ROUTE_ALLOWLIST.selftest); + expect(fake.spawned[0]?.params?.message).toBe("run the self test"); + expect(fake.spawned[0]?.capabilities).toEqual(["WRITE_STORAGE"]); + expect(fake.recordedRuns[0]?.status).toBe("ok"); + }); + + test("merges the target template default_params UNDER the user message (#4)", async () => { + const fake = makeFakeContext({ + params: { message: "run the self test" }, + classifyReply: "selftest", + }); + await makeRouterHandler({ + // The injected reader supplies the target handler's editable default_params; + // a template-provided `message` must NOT override the real user message. + getDefaultParams: (handlerId) => + handlerId === ROUTE_ALLOWLIST.selftest ? { tone: "concise", message: "IGNORED" } : {}, + })(fake.ctx); + + expect(fake.spawned).toHaveLength(1); + expect(fake.spawned[0]?.params?.tone).toBe("concise"); + expect(fake.spawned[0]?.params?.message).toBe("run the self test"); + }); + + test("the classifier reply is normalised (case/whitespace/punctuation tolerated)", async () => { + const fake = makeFakeContext({ params: { message: "x" }, classifyReply: " SELFTEST.\n" }); + await makeRouterHandler()(fake.ctx); + expect(fake.spawned[0]?.handlerId).toBe(ROUTE_ALLOWLIST.selftest); + }); + + test("an unmapped label produces a clarify turn and NEVER spawns (no-eval invariant)", async () => { + const fake = makeFakeContext({ params: { message: "do my taxes" }, classifyReply: "taxes" }); + await makeRouterHandler()(fake.ctx); + + expect(fake.spawned).toHaveLength(0); + expect(fake.recordedRuns[0]?.status).toBe("clarify"); + }); + + test("a free-form handler-id reply is refused (cannot inject an arbitrary id)", async () => { + // The model returns a string that looks like a real handler id but is NOT a label key. + const fake = makeFakeContext({ + params: { message: "x" }, + classifyReply: "rm -rf; orchestrator-demo", + }); + await makeRouterHandler()(fake.ctx); + expect(fake.spawned).toHaveLength(0); + expect(fake.recordedRuns[0]?.status).toBe("clarify"); + }); + + test('the literal "none" reply maps to a clarify turn', async () => { + const fake = makeFakeContext({ params: { message: "x" }, classifyReply: "none" }); + await makeRouterHandler()(fake.ctx); + expect(fake.spawned).toHaveLength(0); + expect(fake.recordedRuns[0]?.status).toBe("clarify"); + }); + + test("a child failure is tolerated — the router records 'partial', not a throw", async () => { + const fake = makeFakeContext({ + params: { message: "x" }, + classifyReply: "selftest", + childRejects: true, + }); + await makeRouterHandler()(fake.ctx); + expect(fake.spawned).toHaveLength(1); + expect(fake.recordedRuns[0]?.status).toBe("partial"); + }); + + test("a custom allowlist drives dispatch (handler is configurable)", async () => { + const fake = makeFakeContext({ params: { message: "x" }, classifyReply: "greet" }); + await makeRouterHandler({ allowlist: { greet: "selftest" } })(fake.ctx); + expect(fake.spawned[0]?.handlerId).toBe("selftest"); + }); + + test("the classify prompt enumerates the allowlist labels and fences the message", async () => { + const fake = makeFakeContext({ params: { message: "SECRET" }, classifyReply: "none" }); + await makeRouterHandler()(fake.ctx); + const prompt = fake.completePrompts[0] ?? ""; + expect(prompt).toContain("selftest"); + expect(prompt).toContain("orchestrate"); + expect(prompt).toContain("SECRET"); + }); +}); + +// --------------------------------------------------------------------------- +// Registration — the router is installed with the right capabilities. +// --------------------------------------------------------------------------- + +describe("router registration", () => { + let path: string; + let db: Database; + let store: Store; + + function setup(): { registry: HandlerRegistry; scheduler: Scheduler } { + path = join(tmpdir(), `vesper-router-${crypto.randomUUID()}.db`); + openStore(path).close(); + db = new Database(path); + store = openStore(path); + const registry = new HandlerRegistry(); + const scheduler = new Scheduler({ db, registry, grants: grantedCapabilities() }); + registerPipelines(scheduler, registry); + return { registry, scheduler }; + } + + function teardown(): void { + db.close(); + store.close(); + try { + rmSync(path, { force: true }); + rmSync(`${path}-shm`, { force: true }); + rmSync(`${path}-wal`, { force: true }); + } catch { + // ignore + } + } + + test("registers a manual router task requiring CLI_INVOKE + WRITE_STORAGE + SPAWN_SUBAGENT", () => { + const { scheduler } = setup(); + try { + const task = scheduler.list().find((t) => t.id === "router"); + expect(task).toBeDefined(); + expect(task?.kind).toBe("manual"); + expect(task?.required_capabilities).toContain("CLI_INVOKE"); + expect(task?.required_capabilities).toContain("WRITE_STORAGE"); + expect(task?.required_capabilities).toContain("SPAWN_SUBAGENT"); + } finally { + teardown(); + } + }); + + test("the router's spawn targets are all registered handlers (allowlist is resolvable)", () => { + const { registry } = setup(); + try { + for (const handlerId of Object.values(ROUTE_ALLOWLIST)) { + expect(registry.has(handlerId)).toBe(true); + } + expect(registry.has(ROUTER_HANDLER_ID)).toBe(true); + } finally { + teardown(); + } + }); + + test("the host grant union covers the router task input capabilities", () => { + const granted = grantedCapabilities(); + for (const cap of routerTaskInput.required_capabilities ?? []) { + expect(granted).toContain(cap); + } + }); +}); diff --git a/packages/pipelines/router/handler.ts b/packages/pipelines/router/handler.ts new file mode 100644 index 0000000..fa2837e --- /dev/null +++ b/packages/pipelines/router/handler.ts @@ -0,0 +1,174 @@ +/** + * The `router` pipeline — the chatbot-home dispatcher. + * + * A chat message is a manual run of THIS pipeline through the existing run path + * (`POST /api/chat` -> `scheduler.run("router", { params })`). The handler reads the + * user's message from `ctx.params.message`, asks the user's authenticated CLI + * (`ctx.complete`, CLI_INVOKE — Hard rule 12, no provider SDK) to CLASSIFY it to ONE + * label, maps that label through a FIXED ALLOWLIST to a registered handler id, and + * `ctx.spawn`s that pipeline as a sub-agent (the live tree under the transcript turn). + * + * SAFETY (non-negotiable): the handler id is NEVER taken from the model's free-form + * text. The classifier returns a label; only a label present in {@link ROUTE_ALLOWLIST} + * resolves to a handler id. An unmapped/unknown/free-form label produces a + * clarifying-question turn (a recorded run, NO spawn, NO dynamic handler id) — this + * preserves the no-dynamic-eval invariant the scheduler relies on. + * + * Capabilities: `CLI_INVOKE` (classify), `WRITE_STORAGE` (record + emit trace), + * `SPAWN_SUBAGENT` (dispatch). All asserted at the context boundary before any effect. + */ + +import type { Capability, RegisterTaskInput, RunParams, TaskHandler } from "@vesper/core"; + +/** Allowlisted handler id referenced by the `router` task. */ +export const ROUTER_HANDLER_ID = "router"; + +/** + * The fixed label -> handler-id allowlist. The classifier may ONLY pick a key here; + * the value is the registered handler id the router spawns. Anything outside this map + * (including a free-form id the model might emit) is refused and becomes a clarifying + * turn. Built-in spawn targets only — flagship pipelines extend this map as they land. + */ +export const ROUTE_ALLOWLIST: Readonly> = { + selftest: "selftest", + orchestrate: "orchestrator-demo", +} as const; + +/** The capability the spawned child is granted — `WRITE_STORAGE` only (it records + traces). */ +const CHILD_CAPABILITIES: readonly Capability[] = ["WRITE_STORAGE"]; + +/** Max characters of the user message embedded in the classify prompt (bound the prompt). */ +const MESSAGE_MAX_LENGTH = 2_000; + +/** Read the user message from params; empty string when absent/non-string. */ +function readMessage(params: RunParams): string { + const raw = params.message; + return typeof raw === "string" ? raw.slice(0, MESSAGE_MAX_LENGTH) : ""; +} + +/** + * Build the classify prompt: the model must answer with EXACTLY one label from the + * allowlist (or the literal `none`). The allowlist is interpolated so the model knows + * the closed set; the user's message is fenced so it cannot rewrite the instruction. + */ +function buildClassifyPrompt(message: string, labels: readonly string[]): string { + return [ + "You are a strict intent classifier for a local automation runtime.", + `Choose EXACTLY ONE label from this closed set: ${labels.join(", ")}, none.`, + 'Answer with the single label only — no punctuation, no explanation. Use "none" when', + "the request matches no label or is ambiguous.", + "", + "User request:", + "<<<", + message, + ">>>", + ].join("\n"); +} + +/** + * Normalise the model's reply to a label key: trim, lowercase, and keep only the first + * token of word characters. A reply that is not an exact allowlist key resolves to null + * (treated as `none`) — the model can never inject an arbitrary handler id this way. + */ +function resolveLabel(reply: string, allowlist: Readonly>): string | null { + const token = + reply + .trim() + .toLowerCase() + .match(/[a-z0-9_-]+/)?.[0] ?? ""; + return Object.hasOwn(allowlist, token) ? token : null; +} + +/** Dependencies that make the router handler unit-testable; all default to the built-ins. */ +export interface RouterHandlerOptions { + /** The label -> handler-id allowlist. Defaults to {@link ROUTE_ALLOWLIST}. */ + readonly allowlist?: Readonly>; + /** + * Returns the editable template `default_params` for a target handler id, which the + * router MERGES under the user message into the spawn params (so an edited pipeline + * template actually affects its runs — #4). Host-injected (the daemon wires it to + * `store.getTemplate`); defaults to no defaults when absent (tests / non-daemon). + */ + readonly getDefaultParams?: (handlerId: string) => RunParams; +} + +/** + * Build the router handler. The default export {@link routerHandler} uses the built-in + * allowlist; tests inject a custom allowlist to assert dispatch + the no-eval fallback. + */ +export function makeRouterHandler(options: RouterHandlerOptions = {}): TaskHandler { + const allowlist = options.allowlist ?? ROUTE_ALLOWLIST; + const getDefaultParams = options.getDefaultParams; + const labels = Object.keys(allowlist); + + return async (ctx) => { + const message = readMessage(ctx.params); + + if (message.trim().length === 0) { + ctx.emitProgress({ kind: "step", message: "empty message — asking for clarification" }); + ctx.recordRun({ + status: "clarify", + summary: "I did not catch that — could you say what you would like me to do?", + }); + return; + } + + ctx.emitProgress({ kind: "step", message: "classifying request" }); + const result = await ctx.complete(buildClassifyPrompt(message, labels)); + const label = resolveLabel(result.text, allowlist); + + // No-eval fallback: an unmapped/free-form label NEVER becomes a handler id. + if (label === null) { + ctx.emitProgress({ + kind: "step", + message: "no matching pipeline — asking for clarification", + }); + ctx.recordRun({ + status: "clarify", + summary: + "I am not sure which automation fits that. Could you rephrase, or tell me the task " + + "in a few words?", + }); + return; + } + + const handlerId = allowlist[label] as string; + ctx.emitProgress({ + kind: "spawn", + message: `dispatching to "${handlerId}"`, + data: { label, handlerId }, + }); + + // Merge the target's editable template default_params UNDER the user message, so an + // edited template configures its runs (#4) without ever overriding the message. + const templateParams = getDefaultParams?.(handlerId) ?? {}; + const handle = ctx.spawn({ + handlerId, + label, + params: { ...templateParams, message }, + capabilities: CHILD_CAPABILITIES, + }); + const childOutcome = await handle.done.catch(() => null); + + ctx.recordRun({ + status: childOutcome?.status === "ok" ? "ok" : "partial", + summary: `routed to ${handlerId} (run ${handle.runId})`, + }); + }; +} + +/** The built-in router handler (default allowlist). */ +export const routerHandler: TaskHandler = makeRouterHandler(); + +/** + * Manual task wiring for the `router` pipeline. Requires `CLI_INVOKE` (classify), + * `WRITE_STORAGE` (record + emit trace), and `SPAWN_SUBAGENT` (dispatch). + */ +export const routerTaskInput: RegisterTaskInput = { + id: "router", + kind: "manual", + schedule_expr: "", + handler_id: ROUTER_HANDLER_ID, + max_duration_ms: 120_000, + required_capabilities: ["CLI_INVOKE", "WRITE_STORAGE", "SPAWN_SUBAGENT"], +}; diff --git a/packages/vesper-cli/src/commands/daemon-run.ts b/packages/vesper-cli/src/commands/daemon-run.ts index 1ff23fb..69cbaa8 100644 --- a/packages/vesper-cli/src/commands/daemon-run.ts +++ b/packages/vesper-cli/src/commands/daemon-run.ts @@ -1,6 +1,7 @@ import { Database } from "bun:sqlite"; import { mkdir } from "node:fs/promises"; import { + ApprovalTokenStore, DEFAULT_AGENT_MATCHERS, detectAvailableCLIs, HandlerRegistry, @@ -67,21 +68,33 @@ export const daemonRunCommand: Command = { complete, redactSummaries: config.storage?.redactRunSummaries === true, }); - registerPipelines(scheduler, registry); + // The UI store is opened first so the router can read editable template + // default_params through it (#4) — an edited template then affects its runs. + const uiStore = openStore(dbPath()); + registerPipelines(scheduler, registry, { + getDefaultParams: (handlerId) => uiStore.getTemplate(handlerId)?.defaultParams ?? {}, + }); // Host the Vesper World UI in-process (one runtime): the UI reads this // scheduler + storage directly and gets live run events off its EventBus. // Agent-presence detection uses the built-in allowlist plus any matchers // the user added under `presence.matchers` in config; `presence.pollMs` // overrides the scan interval. - const uiStore = openStore(dbPath()); const presenceMatchers = [...DEFAULT_AGENT_MATCHERS, ...(config.presence?.matchers ?? [])]; + // Out-of-band approval tokens for privileged config mutations (template edits). + // In-memory + per-process: a daemon restart invalidates every outstanding code. + const approvalTokens = new ApprovalTokenStore(); const ui = await startUiServer({ scheduler, store: uiStore, seed: machineFingerprint(), port: uiPort(), + version: "0.1.0", + socketPath: socketPath(), + defaultCli: config.cli.default ?? null, + detectClis: async () => installed.map((name) => ({ name, status: "installed", ok: true })), detectPresences: presenceDetectorFor(presenceMatchers), + approvalTokens, ...(config.presence?.pollMs !== undefined ? { presencePollMs: config.presence.pollMs } : {}), ...(config.ui?.theme !== undefined ? { defaultTheme: config.ui.theme } : {}), }); diff --git a/packages/vesper-cli/src/compiled-entry.ts b/packages/vesper-cli/src/compiled-entry.ts new file mode 100644 index 0000000..6cd1865 --- /dev/null +++ b/packages/vesper-cli/src/compiled-entry.ts @@ -0,0 +1,23 @@ +#!/usr/bin/env bun +// Compiled daemon entrypoint for the desktop sidecar (DEV-112 Slice 2). +// +// `bun build --compile` bundles this into a single self-contained `vesper-daemon` +// binary that the Tauri shell spawns. Unlike `vesper daemon run` from source, the +// compiled binary has no `client/` directory and no runtime bundler (its FS is the +// virtual `/$bunfs`), so the UI server cannot build its assets on the fly. We instead +// embed the prebuilt client assets at COMPILE time via `with { type: "text" }` (inlined +// as string constants) and install them before the daemon starts. +// +// The `.txt` files are generated by `scripts/build-daemon.ts` immediately before the +// compile step; they do not exist in a normal source checkout (see text-imports.d.ts). +import { setEmbeddedClientAssets } from "@vesper/ui"; +import { registry } from "./commands/index.ts"; +import { dispatch } from "./dispatch.ts"; +import appJs from "./generated/app-js.txt" with { type: "text" }; +import indexHtml from "./generated/index-html.txt" with { type: "text" }; + +setEmbeddedClientAssets({ indexHtml, appJs }); + +// Default to `daemon run`; still forward any explicit argv the shell passes through. +const args = process.argv.slice(2); +process.exit(await dispatch(registry, args.length > 0 ? args : ["daemon", "run"])); diff --git a/packages/vesper-cli/src/text-imports.d.ts b/packages/vesper-cli/src/text-imports.d.ts new file mode 100644 index 0000000..8e1b401 --- /dev/null +++ b/packages/vesper-cli/src/text-imports.d.ts @@ -0,0 +1,8 @@ +// Ambient declaration for `import x from "....txt" with { type: "text" }`. +// The desktop build (scripts/build-daemon.ts) generates the referenced `.txt` files +// just before `bun build --compile`; this keeps the compiled-entry type-clean in a +// plain source checkout where those generated files are absent. +declare module "*.txt" { + const content: string; + export default content; +} diff --git a/packages/vesper-core/src/approval/errors.ts b/packages/vesper-core/src/approval/errors.ts new file mode 100644 index 0000000..7ced691 --- /dev/null +++ b/packages/vesper-core/src/approval/errors.ts @@ -0,0 +1,17 @@ +/** The reason an approval-token operation failed. */ +export type ApprovalErrorReason = "not_found" | "expired" | "already_used"; + +/** + * Thrown by the approval-token store when a token cannot be verified — it was + * never minted, has expired, or was already consumed (single-use). Carries a + * typed `reason` so a route can map it to the right HTTP status. + */ +export class ApprovalError extends Error { + readonly reason: ApprovalErrorReason; + + constructor(reason: ApprovalErrorReason, message: string, options?: { cause?: unknown }) { + super(message, options); + this.name = "ApprovalError"; + this.reason = reason; + } +} diff --git a/packages/vesper-core/src/approval/index.ts b/packages/vesper-core/src/approval/index.ts new file mode 100644 index 0000000..080d510 --- /dev/null +++ b/packages/vesper-core/src/approval/index.ts @@ -0,0 +1,4 @@ +export type { ApprovalErrorReason } from "./errors.ts"; +export { ApprovalError } from "./errors.ts"; +export type { ApprovalTokenStoreOptions } from "./tokens.ts"; +export { ApprovalTokenStore, DEFAULT_TOKEN_TTL_MS } from "./tokens.ts"; diff --git a/packages/vesper-core/src/approval/tokens.test.ts b/packages/vesper-core/src/approval/tokens.test.ts new file mode 100644 index 0000000..0932609 --- /dev/null +++ b/packages/vesper-core/src/approval/tokens.test.ts @@ -0,0 +1,129 @@ +import { describe, expect, test } from "bun:test"; +import { ApprovalError } from "./errors.ts"; +import { ApprovalTokenStore } from "./tokens.ts"; + +/** A deterministic clock seam whose current value the test controls. */ +function fixedClock(start = 1_000): { now: () => number; advance: (ms: number) => void } { + let t = start; + return { + now: () => t, + advance: (ms) => { + t += ms; + }, + }; +} + +describe("ApprovalTokenStore", () => { + test("mint returns a non-empty lowercase-hex code", () => { + const store = new ApprovalTokenStore(); + const code = store.mint(); + expect(code).toMatch(/^[0-9a-f]+$/); + expect(code.length).toBeGreaterThanOrEqual(16); + }); + + test("mint produces distinct codes (CSPRNG)", () => { + const store = new ApprovalTokenStore(); + const codes = new Set([store.mint(), store.mint(), store.mint()]); + expect(codes.size).toBe(3); + }); + + test("verify succeeds once for a valid code", () => { + const store = new ApprovalTokenStore(); + const code = store.mint(); + expect(() => store.verify(code)).not.toThrow(); + }); + + test("verify is single-use — a replay throws already_used", () => { + const store = new ApprovalTokenStore(); + const code = store.mint(); + store.verify(code); + try { + store.verify(code); + throw new Error("expected verify to throw"); + } catch (err) { + expect(err).toBeInstanceOf(ApprovalError); + expect((err as ApprovalError).reason).toBe("already_used"); + } + }); + + test("verify of an unknown code throws not_found", () => { + const store = new ApprovalTokenStore(); + try { + store.verify("deadbeef"); + throw new Error("expected verify to throw"); + } catch (err) { + expect(err).toBeInstanceOf(ApprovalError); + expect((err as ApprovalError).reason).toBe("not_found"); + } + }); + + test("verify of an expired code throws expired", () => { + const clock = fixedClock(); + const store = new ApprovalTokenStore({ ttlMs: 100, now: clock.now }); + const code = store.mint(); + clock.advance(101); + try { + store.verify(code); + throw new Error("expected verify to throw"); + } catch (err) { + expect(err).toBeInstanceOf(ApprovalError); + expect((err as ApprovalError).reason).toBe("expired"); + } + }); + + test("a code is valid right up to (but not at) its TTL boundary", () => { + const clock = fixedClock(); + const store = new ApprovalTokenStore({ ttlMs: 100, now: clock.now }); + const code = store.mint(); + clock.advance(99); + expect(store.isValid(code)).toBe(true); + clock.advance(1); // now == expiresAt + expect(store.isValid(code)).toBe(false); + }); + + test("isValid does not consume the code (verify still succeeds afterwards)", () => { + const store = new ApprovalTokenStore(); + const code = store.mint(); + expect(store.isValid(code)).toBe(true); + expect(store.isValid(code)).toBe(true); + expect(() => store.verify(code)).not.toThrow(); + }); + + test("isValid is false for unknown/used codes", () => { + const store = new ApprovalTokenStore(); + expect(store.isValid("nope")).toBe(false); + const code = store.mint(); + store.verify(code); + expect(store.isValid(code)).toBe(false); + }); + + test("prune drops expired and used entries (later verify is not_found)", () => { + const clock = fixedClock(); + const store = new ApprovalTokenStore({ ttlMs: 100, now: clock.now }); + const expiring = store.mint(); + clock.advance(101); + store.prune(); + try { + store.verify(expiring); + throw new Error("expected verify to throw"); + } catch (err) { + expect((err as ApprovalError).reason).toBe("not_found"); + } + }); + + test("ttl is clamped to a minimum of 1ms", () => { + const clock = fixedClock(); + const store = new ApprovalTokenStore({ ttlMs: 0, now: clock.now }); + const code = store.mint(); + // With ttl clamped to >=1, the code is valid at mint time (now < expiresAt). + expect(store.isValid(code)).toBe(true); + }); + + test("injected randomBytes seam is used (deterministic code)", () => { + const store = new ApprovalTokenStore({ + randomBytes: (out) => out.fill(0xab), + }); + const code = store.mint(); + expect(code).toBe("ab".repeat(code.length / 2)); + }); +}); diff --git a/packages/vesper-core/src/approval/tokens.ts b/packages/vesper-core/src/approval/tokens.ts new file mode 100644 index 0000000..74f8783 --- /dev/null +++ b/packages/vesper-core/src/approval/tokens.ts @@ -0,0 +1,125 @@ +/** + * Out-of-band approval tokens for privileged, out-of-band mutations. + * + * The daemon mints a short, single-use code (CSPRNG) with a short TTL; a privileged + * route (e.g. `PUT /api/pipelines/:id/template`) requires the caller to present a + * valid, unexpired, unconsumed code. This is the minimal self-contained module the + * future `security-hardening.md` adopts — it adds a SECOND factor over `isLocalRequest` + * so a malicious local script cannot silently rewrite a pipeline's config without the + * code the daemon surfaced to the operator out-of-band. + * + * The store is in-memory by design: tokens are ephemeral and per-daemon-process; a + * restart invalidates every outstanding code (fail-closed). No token is ever persisted + * or logged. `crypto.getRandomValues` is the CSPRNG seam; `() => Date.now()` is the + * clock seam (injectable for deterministic tests). + */ + +import { ApprovalError } from "./errors.ts"; + +/** Default time-to-live for a minted token (5 minutes). */ +export const DEFAULT_TOKEN_TTL_MS = 5 * 60 * 1_000; + +/** Number of random bytes behind a code; 12 bytes -> 24 lowercase hex chars. */ +const TOKEN_BYTES = 12; + +/** A live token entry: when it expires, and whether it was already consumed. */ +interface TokenEntry { + readonly expiresAt: number; + used: boolean; +} + +/** Options for {@link ApprovalTokenStore}. */ +export interface ApprovalTokenStoreOptions { + /** Token lifetime in ms. Defaults to {@link DEFAULT_TOKEN_TTL_MS}. Clamped to >= 1. */ + readonly ttlMs?: number; + /** Clock seam (ms since epoch). Defaults to `Date.now`. Inject for tests. */ + readonly now?: () => number; + /** + * CSPRNG seam — fills the given buffer with random bytes. Defaults to + * `crypto.getRandomValues`. Inject ONLY for tests; production must use a CSPRNG. + */ + readonly randomBytes?: (out: Uint8Array) => void; +} + +/** Lower-hex encode a byte buffer (no separators). */ +function toHex(bytes: Uint8Array): string { + let out = ""; + for (const b of bytes) { + out += b.toString(16).padStart(2, "0"); + } + return out; +} + +/** + * In-memory store of single-use, short-TTL approval codes. + * + * - `mint()` returns a fresh CSPRNG code and records its expiry. + * - `verify(code)` consumes the code: it succeeds exactly once for a valid, + * unexpired code and throws {@link ApprovalError} otherwise (`not_found`, + * `expired`, `already_used`). Verifying marks the code used so a replay fails. + */ +export class ApprovalTokenStore { + readonly #tokens = new Map(); + readonly #ttlMs: number; + readonly #now: () => number; + readonly #randomBytes: (out: Uint8Array) => void; + + constructor(options: ApprovalTokenStoreOptions = {}) { + this.#ttlMs = Math.max(1, options.ttlMs ?? DEFAULT_TOKEN_TTL_MS); + this.#now = options.now ?? (() => Date.now()); + this.#randomBytes = options.randomBytes ?? ((out) => crypto.getRandomValues(out)); + } + + /** Mint a fresh single-use code and return it. The raw code is never persisted to disk. */ + mint(): string { + const buf = new Uint8Array(TOKEN_BYTES); + this.#randomBytes(buf); + const code = toHex(buf); + this.#tokens.set(code, { expiresAt: this.#now() + this.#ttlMs, used: false }); + return code; + } + + /** + * Consume `code`. Returns nothing on success (the code is now spent). Throws + * {@link ApprovalError}: + * - `not_found` when the code was never minted (or was purged after expiry); + * - `expired` when the code is past its TTL (it is dropped); + * - `already_used` when the code was previously verified (replay). + */ + verify(code: string): void { + const entry = this.#tokens.get(code); + if (entry === undefined) { + throw new ApprovalError("not_found", "approval code is not recognised"); + } + if (this.#now() >= entry.expiresAt) { + this.#tokens.delete(code); + throw new ApprovalError("expired", "approval code has expired"); + } + if (entry.used) { + throw new ApprovalError("already_used", "approval code was already used"); + } + entry.used = true; + } + + /** + * Non-consuming check used by routes that want a boolean. Returns true iff the + * code is valid, unexpired, and unused — but does NOT mark it used. Prefer + * {@link verify} on the mutation path so the code is single-use. + */ + isValid(code: string): boolean { + const entry = this.#tokens.get(code); + if (entry === undefined) return false; + if (this.#now() >= entry.expiresAt) return false; + return !entry.used; + } + + /** Drop expired/used entries so the map does not grow unbounded. */ + prune(): void { + const now = this.#now(); + for (const [code, entry] of this.#tokens) { + if (entry.used || now >= entry.expiresAt) { + this.#tokens.delete(code); + } + } + } +} diff --git a/packages/vesper-core/src/connections/audit.test.ts b/packages/vesper-core/src/connections/audit.test.ts new file mode 100644 index 0000000..11e3fa7 --- /dev/null +++ b/packages/vesper-core/src/connections/audit.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, test } from "bun:test"; +import type { Store } from "../storage/index.ts"; +import { openStore } from "../storage/index.ts"; +import { recordConnectionEvent, stripSensitive } from "./audit.ts"; + +function memStore(): Store { + const store = openStore(":memory:"); + store.migrate(); + return store; +} + +describe("stripSensitive", () => { + test("drops token/value/secret/message-body fields, keeps wiring", () => { + const cleaned = stripSensitive({ + channel: "telegram", + vaultKey: "telegram_bot_token", + token: "123:SECRET", + value: "also-secret", + text: "a private message body", + message: "another body", + outcome: "ok", + }); + expect(cleaned).toEqual({ + channel: "telegram", + vaultKey: "telegram_bot_token", + outcome: "ok", + }); + expect(cleaned.token).toBeUndefined(); + expect(cleaned.text).toBeUndefined(); + }); +}); + +describe("recordConnectionEvent", () => { + test("appends a source:connections event with no secret in the payload", () => { + const store = memStore(); + recordConnectionEvent(store, "connection_connected", { + channel: "telegram", + vaultKey: "telegram_bot_token", + token: "123:SECRET", + }); + const events = store.listEvents({ source: "connections" }); + expect(events).toHaveLength(1); + expect(events[0]?.kind).toBe("connection_connected"); + expect(events[0]?.payload).toEqual({ channel: "telegram", vaultKey: "telegram_bot_token" }); + // The serialized row must not contain the secret anywhere. + expect(JSON.stringify(events[0])).not.toContain("123:SECRET"); + store.close(); + }); + + test("records mcp_enabled / mcp_disabled kinds", () => { + const store = memStore(); + recordConnectionEvent(store, "mcp_enabled", { mcp: "linear" }); + recordConnectionEvent(store, "mcp_disabled", { mcp: "linear" }); + const kinds = store.listEvents({ source: "connections" }).map((e) => e.kind); + expect(kinds).toEqual(["mcp_enabled", "mcp_disabled"]); + store.close(); + }); +}); diff --git a/packages/vesper-core/src/connections/audit.ts b/packages/vesper-core/src/connections/audit.ts new file mode 100644 index 0000000..918c972 --- /dev/null +++ b/packages/vesper-core/src/connections/audit.ts @@ -0,0 +1,62 @@ +/** + * Audit helper for connection mutations. Every connection state transition is + * recorded on the existing `events` table (source `"connections"`) — NO migration. + * This wrapper is the single choke point that strips any secret-bearing field + * (the token value, a raw inbound/outbound message body) BEFORE it reaches the + * store, so an audit row can never leak a credential or a message (the spec's + * "Secret containment" SHALL + the memory protocol's never-write-secrets rule). + */ + +import type { Store } from "../storage/index.ts"; + +/** The connection mutation kinds recorded to the audit log. */ +export type ConnectionEventKind = + | "connection_connected" + | "connection_disconnected" + | "connection_send_failed" + | "mcp_enabled" + | "mcp_disabled"; + +/** + * Field names that may carry a secret or a raw message body. They are removed + * from any audit payload — only NON-secret wiring (ids, vault KEY NAMES, outcome) + * is allowed through. The vault KEY name (`vaultKey`) is explicitly safe; the + * VALUE (`token`, `value`, `secret`) is not. + */ +const REDACTED_KEYS: ReadonlySet = new Set([ + "token", + "value", + "secret", + "password", + "text", + "message", + "body", +]); + +/** Drop any secret/message-body field from an audit payload (shallow). */ +export function stripSensitive( + payload: Readonly>, +): Record { + const out: Record = {}; + for (const [key, val] of Object.entries(payload)) { + if (REDACTED_KEYS.has(key)) continue; + out[key] = val; + } + return out; +} + +/** + * Append a `source: "connections"` audit event with all secret/message-body + * fields stripped, returning the new event id. + */ +export function recordConnectionEvent( + store: Store, + kind: ConnectionEventKind, + payload: Readonly>, +): string { + return store.appendEvent({ + source: "connections", + kind, + payload: stripSensitive(payload), + }); +} diff --git a/packages/vesper-core/src/connections/catalog.test.ts b/packages/vesper-core/src/connections/catalog.test.ts new file mode 100644 index 0000000..81d72cd --- /dev/null +++ b/packages/vesper-core/src/connections/catalog.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, test } from "bun:test"; +import { + CHANNEL_CATALOG, + channelById, + isChannelId, + isMcpId, + MCP_CATALOG, + mcpById, +} from "./catalog.ts"; + +describe("CHANNEL_CATALOG", () => { + test("channel ids are unique", () => { + const ids = CHANNEL_CATALOG.map((d) => d.id); + expect(new Set(ids).size).toBe(ids.length); + }); + + test("every descriptor has a non-empty allowedHosts and docsUrl", () => { + for (const d of CHANNEL_CATALOG) { + expect(d.allowedHosts.length).toBeGreaterThan(0); + expect(d.docsUrl.length).toBeGreaterThan(0); + } + }); + + test("telegram + discord are ready; whatsapp + signal are deferred", () => { + expect(channelById("telegram")?.status).toBe("ready"); + expect(channelById("discord")?.status).toBe("ready"); + expect(channelById("whatsapp")?.status).toBe("deferred"); + expect(channelById("signal")?.status).toBe("deferred"); + }); + + test("telegram declares api.telegram.org and the bot-token vault key", () => { + const telegram = channelById("telegram"); + expect(telegram?.allowedHosts).toEqual(["api.telegram.org"]); + expect(telegram?.vaultKeys).toEqual(["telegram_bot_token"]); + }); + + test("channelById / isChannelId reject unknown ids", () => { + expect(channelById("nope")).toBeUndefined(); + expect(isChannelId("telegram")).toBe(true); + expect(isChannelId("nope")).toBe(false); + }); +}); + +describe("MCP_CATALOG", () => { + test("has the 10 seed ids, unique", () => { + const ids = MCP_CATALOG.map((d) => d.id); + expect(ids.length).toBe(10); + expect(new Set(ids).size).toBe(10); + expect(ids).toEqual([ + "linear", + "notion", + "gmail", + "google-calendar", + "google-drive", + "refero", + "bigdata", + "fmp", + "ziprecruiter", + "excalidraw", + ]); + }); + + test("every entry declares a docsUrl and non-empty allowedHosts", () => { + for (const d of MCP_CATALOG) { + expect(d.docsUrl.length).toBeGreaterThan(0); + expect(d.allowedHosts.length).toBeGreaterThan(0); + } + }); + + test("mcpById / isMcpId reject unknown ids", () => { + expect(mcpById("linear")?.displayName).toBe("Linear"); + expect(mcpById("nope")).toBeUndefined(); + expect(isMcpId("notion")).toBe(true); + expect(isMcpId("nope")).toBe(false); + }); +}); diff --git a/packages/vesper-core/src/connections/catalog.ts b/packages/vesper-core/src/connections/catalog.ts new file mode 100644 index 0000000..c5a5fcd --- /dev/null +++ b/packages/vesper-core/src/connections/catalog.ts @@ -0,0 +1,162 @@ +/** + * Curated, code-reviewed catalogs — the single source of truth for channels and + * MCP servers. User input selects an id (a KEY); it never supplies an arbitrary + * host or server URL (same trust posture as the agent install catalog). + * + * Channels: telegram + discord are `ready` (a handler exists / shares the + * contract); whatsapp + signal are `deferred` (catalog entry + tutorial only). + */ + +import type { ChannelDescriptor, ChannelId } from "./types.ts"; + +/** Immutable channel catalog. */ +export const CHANNEL_CATALOG: readonly ChannelDescriptor[] = [ + { + id: "telegram", + displayName: "Telegram", + transport: "long-poll", + allowedHosts: ["api.telegram.org"], + vaultKeys: ["telegram_bot_token"], + docsUrl: "https://core.telegram.org/bots#how-do-i-create-a-bot", + status: "ready", + }, + { + id: "discord", + displayName: "Discord", + transport: "bot-api", + allowedHosts: ["discord.com"], + vaultKeys: ["discord_bot_token"], + docsUrl: "https://discord.com/developers/docs/getting-started", + status: "ready", + }, + { + id: "whatsapp", + displayName: "WhatsApp", + transport: "webhook", + allowedHosts: ["graph.facebook.com"], + vaultKeys: ["whatsapp_access_token"], + docsUrl: "https://developers.facebook.com/docs/whatsapp/cloud-api", + status: "deferred", + }, + { + id: "signal", + displayName: "Signal", + transport: "local-cli", + // Signal runs through a local signal-cli process, not a hosted host. The + // localhost REST bridge is the only egress; it is still declared so the + // allowlist seam holds (a deferred handler starts nothing in v1). + allowedHosts: ["127.0.0.1"], + vaultKeys: ["signal_account"], + docsUrl: "https://github.com/AsamK/signal-cli", + status: "deferred", + }, +] as const; + +/** Look up a channel descriptor by id, or undefined when not in the catalog. */ +export function channelById(id: string): ChannelDescriptor | undefined { + return CHANNEL_CATALOG.find((d) => d.id === id); +} + +/** Type-guard: true iff `id` is a known catalog {@link ChannelId}. */ +export function isChannelId(id: string): id is ChannelId { + return CHANNEL_CATALOG.some((d) => d.id === id); +} + +/** + * An MCP server catalog entry. The user opts a server in by id; v1 RECORDS the + * opt-in + any credential and shows status — it does NOT proxy MCP traffic + * (wiring the server into a CLI adapter's own MCP config is the adapter's concern). + */ +export interface McpDescriptor { + readonly id: string; + readonly displayName: string; + readonly docsUrl: string; + /** Vault KEY names this server needs (NEVER the values); empty when none. */ + readonly vaultKeys: readonly string[]; + /** Hosts this server is known to reach (informational in v1; no proxying). */ + readonly allowedHosts: readonly string[]; +} + +/** Immutable MCP catalog — the 10 seed ids observed in the operating environment. */ +export const MCP_CATALOG: readonly McpDescriptor[] = [ + { + id: "linear", + displayName: "Linear", + docsUrl: "https://linear.app/docs/mcp", + vaultKeys: [], + allowedHosts: ["mcp.linear.app"], + }, + { + id: "notion", + displayName: "Notion", + docsUrl: "https://developers.notion.com", + vaultKeys: [], + allowedHosts: ["mcp.notion.com"], + }, + { + id: "gmail", + displayName: "Gmail", + docsUrl: "https://developers.google.com/gmail/api", + vaultKeys: [], + allowedHosts: ["gmail.googleapis.com"], + }, + { + id: "google-calendar", + displayName: "Google Calendar", + docsUrl: "https://developers.google.com/calendar", + vaultKeys: [], + allowedHosts: ["www.googleapis.com"], + }, + { + id: "google-drive", + displayName: "Google Drive", + docsUrl: "https://developers.google.com/drive", + vaultKeys: [], + allowedHosts: ["www.googleapis.com"], + }, + { + id: "refero", + displayName: "Refero", + docsUrl: "https://refero.design", + vaultKeys: [], + allowedHosts: ["mcp.refero.design"], + }, + { + id: "bigdata", + displayName: "Bigdata.com", + docsUrl: "https://bigdata.com", + vaultKeys: [], + allowedHosts: ["mcp.bigdata.com"], + }, + { + id: "fmp", + displayName: "Financial Modeling Prep", + docsUrl: "https://site.financialmodelingprep.com/developer/docs", + vaultKeys: [], + allowedHosts: ["mcp.financialmodelingprep.com"], + }, + { + id: "ziprecruiter", + displayName: "ZipRecruiter", + docsUrl: "https://www.ziprecruiter.com/publishers", + vaultKeys: [], + allowedHosts: ["api.ziprecruiter.com"], + }, + { + id: "excalidraw", + displayName: "Excalidraw", + docsUrl: "https://docs.excalidraw.com", + vaultKeys: [], + allowedHosts: ["excalidraw.com"], + }, +] as const; + +/** Look up an MCP descriptor by id, or undefined when not in the catalog. */ +export function mcpById(id: string): McpDescriptor | undefined { + return MCP_CATALOG.find((d) => d.id === id); +} + +/** Type-guard: true iff `id` is a known catalog MCP server id. */ +export function isMcpId(id: string): boolean { + return MCP_CATALOG.some((d) => d.id === id); +} diff --git a/packages/vesper-core/src/connections/errors.ts b/packages/vesper-core/src/connections/errors.ts new file mode 100644 index 0000000..3372438 --- /dev/null +++ b/packages/vesper-core/src/connections/errors.ts @@ -0,0 +1,27 @@ +/** The reason a connection operation failed. */ +export type ConnectionErrorReason = + | "host_not_allowed" + | "not_authenticated" + | "send_failed" + | "receive_failed" + | "unknown_channel" + | "invalid_response"; + +/** + * Thrown by the connections layer (the allowlisted-fetch seam and channel + * handlers) when a network or transport operation is refused. Carries a typed + * `reason` so a route or CLI command can map it to the right outcome. + * + * `host_not_allowed` is the load-bearing invariant: a handler may only egress to + * a host its descriptor declares in `allowedHosts` — never an arbitrary host and + * never an LLM provider (Hard rule 12). + */ +export class ConnectionError extends Error { + readonly reason: ConnectionErrorReason; + + constructor(reason: ConnectionErrorReason, message: string, options?: { cause?: unknown }) { + super(message, options); + this.name = "ConnectionError"; + this.reason = reason; + } +} diff --git a/packages/vesper-core/src/connections/fetch.test.ts b/packages/vesper-core/src/connections/fetch.test.ts new file mode 100644 index 0000000..00d7be7 --- /dev/null +++ b/packages/vesper-core/src/connections/fetch.test.ts @@ -0,0 +1,97 @@ +import { describe, expect, test } from "bun:test"; +import type { Capability } from "../capabilities/index.ts"; +import { CapabilityError } from "../capabilities/index.ts"; +import { ConnectionError } from "./errors.ts"; +import { allowlistedFetch, type FetchFn } from "./fetch.ts"; + +const GRANTED: readonly Capability[] = ["NETWORK_FETCH"]; + +/** A fetch spy that records every URL it is asked to fetch. */ +function spyFetch(): { fn: FetchFn; calls: string[] } { + const calls: string[] = []; + const fn: FetchFn = async (input) => { + calls.push(input); + return new Response("{}", { headers: { "content-type": "application/json" } }); + }; + return { fn, calls }; +} + +describe("allowlistedFetch", () => { + test("fetches a host in the allowlist", async () => { + const { fn, calls } = spyFetch(); + const res = await allowlistedFetch({ + url: "https://api.telegram.org/bot123/getMe", + allowedHosts: ["api.telegram.org"], + granted: GRANTED, + fetchFn: fn, + }); + expect(res.status).toBe(200); + expect(calls).toEqual(["https://api.telegram.org/bot123/getMe"]); + }); + + test("REFUSES a host not in the allowlist and makes NO request", async () => { + const { fn, calls } = spyFetch(); + await expect( + allowlistedFetch({ + url: "https://evil.example.com/steal", + allowedHosts: ["api.telegram.org"], + granted: GRANTED, + fetchFn: fn, + }), + ).rejects.toMatchObject({ + name: "ConnectionError", + reason: "host_not_allowed", + }); + expect(calls).toEqual([]); // no request was made + }); + + test("refuses an LLM-provider host (Hard rule 12) — never widened past the allowlist", async () => { + const { fn, calls } = spyFetch(); + await expect( + allowlistedFetch({ + url: "https://api.anthropic.com/v1/messages", + allowedHosts: ["api.telegram.org"], + granted: GRANTED, + fetchFn: fn, + }), + ).rejects.toBeInstanceOf(ConnectionError); + expect(calls).toEqual([]); + }); + + test("refuses a malformed URL with NO request", async () => { + const { fn, calls } = spyFetch(); + await expect( + allowlistedFetch({ + url: "not a url", + allowedHosts: ["api.telegram.org"], + granted: GRANTED, + fetchFn: fn, + }), + ).rejects.toMatchObject({ reason: "host_not_allowed" }); + expect(calls).toEqual([]); + }); + + test("asserts NETWORK_FETCH before any network work", async () => { + const { fn, calls } = spyFetch(); + await expect( + allowlistedFetch({ + url: "https://api.telegram.org/bot123/getMe", + allowedHosts: ["api.telegram.org"], + granted: [], // NETWORK_FETCH not granted + fetchFn: fn, + }), + ).rejects.toBeInstanceOf(CapabilityError); + expect(calls).toEqual([]); + }); + + test("host matching is case-insensitive", async () => { + const { fn, calls } = spyFetch(); + await allowlistedFetch({ + url: "https://API.Telegram.ORG/bot123/getMe", + allowedHosts: ["api.telegram.org"], + granted: GRANTED, + fetchFn: fn, + }); + expect(calls).toHaveLength(1); + }); +}); diff --git a/packages/vesper-core/src/connections/fetch.ts b/packages/vesper-core/src/connections/fetch.ts new file mode 100644 index 0000000..5416822 --- /dev/null +++ b/packages/vesper-core/src/connections/fetch.ts @@ -0,0 +1,69 @@ +/** + * The single network-egress seam for channel handlers. Every outbound HTTP call a + * handler makes routes through {@link allowlistedFetch}; no handler may call the + * global `fetch` directly (a test invariant). This is where the two hard + * guarantees live: the call asserts `NETWORK_FETCH`, and it refuses any host the + * channel descriptor did not declare in `allowedHosts` — so Vesper only ever + * reaches the first-party hosts a catalog entry names, never an LLM provider + * (Hard rule 12). + * + * The `fetch` implementation is injected so the test suite fetches to NOTHING. + */ + +import { assertCapabilities, type Capability } from "../capabilities/index.ts"; +import { ConnectionError } from "./errors.ts"; + +/** A minimal fetch shape — the subset handlers use. Injected for tests. */ +export type FetchFn = (input: string, init?: RequestInit) => Promise; + +/** Options for {@link allowlistedFetch}. */ +export interface AllowlistedFetchOptions { + /** The target URL. Its host MUST be in {@link allowedHosts}. */ + readonly url: string; + /** Hosts this call is permitted to reach (the descriptor's `allowedHosts`). */ + readonly allowedHosts: readonly string[]; + /** Capabilities the handler was granted; MUST include `NETWORK_FETCH`. */ + readonly granted: readonly Capability[]; + /** The fetch implementation. Defaults to the global `fetch`; inject for tests. */ + readonly fetchFn?: FetchFn; + /** Standard fetch init (method, headers, body). */ + readonly init?: RequestInit; +} + +/** Parse `url` and return its lowercase hostname, or null when malformed. */ +function hostnameOf(url: string): string | null { + try { + return new URL(url).hostname.toLowerCase(); + } catch { + return null; + } +} + +/** + * Assert `NETWORK_FETCH`, then fetch `url` ONLY if its host is in `allowedHosts`. + * + * Throws {@link import("../capabilities/index.ts").CapabilityError} ("denied") + * before any network work if `NETWORK_FETCH` is not granted; throws + * {@link ConnectionError}("host_not_allowed") — and makes NO request — when the + * URL is malformed or its host is not allowlisted. + */ +export async function allowlistedFetch(options: AllowlistedFetchOptions): Promise { + const { url, allowedHosts, granted, init } = options; + // Capability gate first: deny before parsing/network if NETWORK_FETCH is absent. + assertCapabilities(["NETWORK_FETCH"], granted); + + const host = hostnameOf(url); + if (host === null) { + throw new ConnectionError("host_not_allowed", `malformed URL refused: ${url}`); + } + const allowed = allowedHosts.some((h) => h.toLowerCase() === host); + if (!allowed) { + throw new ConnectionError( + "host_not_allowed", + `host "${host}" is not in the channel allowlist [${allowedHosts.join(", ")}]`, + ); + } + + const doFetch = options.fetchFn ?? fetch; + return doFetch(url, init); +} diff --git a/packages/vesper-core/src/connections/index.ts b/packages/vesper-core/src/connections/index.ts new file mode 100644 index 0000000..2f856df --- /dev/null +++ b/packages/vesper-core/src/connections/index.ts @@ -0,0 +1,35 @@ +// @vesper/core — Connections layer public surface (messaging channels + MCP catalog). + +export { + type ConnectionEventKind, + recordConnectionEvent, + stripSensitive, +} from "./audit.ts"; +export { + CHANNEL_CATALOG, + channelById, + isChannelId, + isMcpId, + MCP_CATALOG, + type McpDescriptor, + mcpById, +} from "./catalog.ts"; +export { ConnectionError, type ConnectionErrorReason } from "./errors.ts"; +export { + type AllowlistedFetchOptions, + allowlistedFetch, + type FetchFn, +} from "./fetch.ts"; +export { ChannelRegistry } from "./registry.ts"; +export { TelegramHandler, type TelegramHandlerOptions } from "./telegram.ts"; +export type { + ChannelDescriptor, + ChannelHandler, + ChannelId, + ChannelStatus, + ChannelTransport, + ChatSink, + InboundMessage, + OutboundIntent, + Stoppable, +} from "./types.ts"; diff --git a/packages/vesper-core/src/connections/registry.test.ts b/packages/vesper-core/src/connections/registry.test.ts new file mode 100644 index 0000000..60af22e --- /dev/null +++ b/packages/vesper-core/src/connections/registry.test.ts @@ -0,0 +1,86 @@ +import { describe, expect, test } from "bun:test"; +import { ChannelRegistry } from "./registry.ts"; +import type { ChannelDescriptor, ChannelHandler, ChannelId, ChatSink, Stoppable } from "./types.ts"; + +function descriptor(id: ChannelId): ChannelDescriptor { + return { + id, + displayName: id, + transport: "long-poll", + allowedHosts: ["example.com"], + vaultKeys: [], + docsUrl: "https://example.com", + status: "ready", + }; +} + +/** A fake handler recording start/stop, optionally throwing on receive. */ +function fakeHandler( + id: ChannelId, + opts: { throwOnReceive?: boolean; onStop?: () => void } = {}, +): { handler: ChannelHandler; started: () => boolean } { + let started = false; + const handler: ChannelHandler = { + descriptor: descriptor(id), + authenticate: async () => {}, + send: async () => {}, + receive: (_sink: ChatSink): Stoppable => { + if (opts.throwOnReceive) throw new Error("boom"); + started = true; + return { + stop() { + opts.onStop?.(); + }, + }; + }, + }; + return { handler, started: () => started }; +} + +const sink: ChatSink = async () => {}; + +describe("ChannelRegistry", () => { + test("is empty by default", () => { + const reg = new ChannelRegistry(); + expect(reg.list()).toHaveLength(0); + expect(reg.byId("telegram")).toBeUndefined(); + }); + + test("register + byId + list", () => { + const { handler } = fakeHandler("telegram"); + const reg = new ChannelRegistry([handler]); + reg.register(fakeHandler("discord").handler); + expect(reg.list()).toHaveLength(2); + expect(reg.byId("telegram")).toBe(handler); + expect(reg.byId("discord")?.descriptor.id).toBe("discord"); + }); + + test("startAll starts every registered handler", () => { + const a = fakeHandler("telegram"); + const b = fakeHandler("discord"); + const reg = new ChannelRegistry([a.handler, b.handler]); + reg.startAll(sink); + expect(a.started()).toBe(true); + expect(b.started()).toBe(true); + }); + + test("isolates a throwing handler so the others still start", () => { + const bad = fakeHandler("telegram", { throwOnReceive: true }); + const good = fakeHandler("discord"); + const reg = new ChannelRegistry([bad.handler, good.handler]); + const handle = reg.startAll(sink); // must not throw + expect(good.started()).toBe(true); + handle.stop(); + }); + + test("startAll's handle stops every started loop (idempotent)", () => { + let stops = 0; + const a = fakeHandler("telegram", { onStop: () => stops++ }); + const b = fakeHandler("discord", { onStop: () => stops++ }); + const reg = new ChannelRegistry([a.handler, b.handler]); + const handle = reg.startAll(sink); + handle.stop(); + handle.stop(); // idempotent — does not double-stop + expect(stops).toBe(2); + }); +}); diff --git a/packages/vesper-core/src/connections/registry.ts b/packages/vesper-core/src/connections/registry.ts new file mode 100644 index 0000000..fff176a --- /dev/null +++ b/packages/vesper-core/src/connections/registry.ts @@ -0,0 +1,62 @@ +/** + * Holds the registered {@link ChannelHandler}s and starts their inbound loops. + * Modeled exactly on the shipped `ModuleRegistry`: per-handler failure isolation + * (one handler throwing during `startAll` never breaks the others), empty by + * default. Only catalog entries with a stored credential AND + * `connections..enabled === true` are registered by the daemon. + */ + +import type { ChannelHandler, ChannelId, ChatSink, Stoppable } from "./types.ts"; + +export class ChannelRegistry { + readonly #handlers: ChannelHandler[] = []; + + constructor(handlers: readonly ChannelHandler[] = []) { + this.#handlers.push(...handlers); + } + + /** Register a handler. A second handler for the same channel id replaces nothing — registration is additive; the daemon registers at most one per channel. */ + register(handler: ChannelHandler): void { + this.#handlers.push(handler); + } + + list(): readonly ChannelHandler[] { + return this.#handlers; + } + + byId(id: ChannelId): ChannelHandler | undefined { + return this.#handlers.find((h) => h.descriptor.id === id); + } + + /** + * Start every registered handler's inbound loop, wiring each to `sink`. A + * handler that throws synchronously while starting is isolated (logged-by- + * swallowing) so one misbehaving channel cannot stop the others — the same + * failure-isolation contract as `ModuleRegistry.dispatchRunCompleted`. Returns + * a single {@link Stoppable} that stops every started loop (idempotent). + */ + startAll(sink: ChatSink): Stoppable { + const handles: Stoppable[] = []; + for (const handler of this.#handlers) { + try { + handles.push(handler.receive(sink)); + } catch { + // A misbehaving handler must not prevent the others from receiving. + } + } + let stopped = false; + return { + stop() { + if (stopped) return; + stopped = true; + for (const h of handles) { + try { + h.stop(); + } catch { + // Best-effort teardown; one handler's failure must not block the rest. + } + } + }, + }; + } +} diff --git a/packages/vesper-core/src/connections/telegram.test.ts b/packages/vesper-core/src/connections/telegram.test.ts new file mode 100644 index 0000000..b400829 --- /dev/null +++ b/packages/vesper-core/src/connections/telegram.test.ts @@ -0,0 +1,182 @@ +import { describe, expect, test } from "bun:test"; +import type { Capability } from "../capabilities/index.ts"; +import { CapabilityError } from "../capabilities/index.ts"; +import type { Vault } from "../vault/index.ts"; +import { ConnectionError } from "./errors.ts"; +import type { FetchFn } from "./fetch.ts"; +import { TelegramHandler } from "./telegram.ts"; +import type { ChatSink, InboundMessage } from "./types.ts"; + +const GRANTED: readonly Capability[] = ["NETWORK_FETCH", "READ_VAULT"]; + +/** An in-memory vault stub for tests (the suite never touches the Keychain). */ +function fakeVault(entries: Record): Vault { + return { + async get(key) { + const v = entries[key]; + if (v === undefined) throw new Error(`no such key ${key}`); + return v; + }, + async set() {}, + async delete() {}, + async list() { + return Object.keys(entries).sort(); + }, + }; +} + +/** A scripted fetch: map a method name (from the URL path) to a JSON response. */ +function scriptedFetch(responder: (method: string, body: unknown) => unknown): { + fn: FetchFn; + calls: { url: string; body: unknown }[]; +} { + const calls: { url: string; body: unknown }[] = []; + const fn: FetchFn = async (input, init) => { + const body = init?.body !== undefined ? JSON.parse(String(init.body)) : undefined; + calls.push({ url: input, body }); + const method = input.split("/").pop() ?? ""; + const result = responder(method, body); + return new Response(JSON.stringify(result), { + headers: { "content-type": "application/json" }, + }); + }; + return { fn, calls }; +} + +describe("TelegramHandler", () => { + test("authenticate loads the token from the vault and verifies via getMe", async () => { + const { fn, calls } = scriptedFetch((method) => + method === "getMe" + ? { ok: true, result: { id: 1, is_bot: true, username: "vesperbot" } } + : { ok: true }, + ); + const handler = new TelegramHandler({ granted: GRANTED, fetchFn: fn }); + await handler.authenticate(fakeVault({ telegram_bot_token: "123:ABC" })); + expect(calls).toHaveLength(1); + expect(calls[0]?.url).toBe("https://api.telegram.org/bot123:ABC/getMe"); + }); + + test("authenticate throws when getMe says not-a-bot", async () => { + const { fn } = scriptedFetch(() => ({ ok: true, result: { id: 1, is_bot: false } })); + const handler = new TelegramHandler({ granted: GRANTED, fetchFn: fn }); + await expect( + handler.authenticate(fakeVault({ telegram_bot_token: "123:ABC" })), + ).rejects.toBeInstanceOf(ConnectionError); + }); + + test("authenticate surfaces a Bot API error envelope", async () => { + const { fn } = scriptedFetch(() => ({ ok: false, description: "Unauthorized" })); + const handler = new TelegramHandler({ granted: GRANTED, fetchFn: fn }); + await expect( + handler.authenticate(fakeVault({ telegram_bot_token: "bad" })), + ).rejects.toMatchObject({ reason: "invalid_response" }); + }); + + test("send posts a sendMessage with chat_id + text", async () => { + const { fn, calls } = scriptedFetch((method) => + method === "getMe" ? { ok: true, result: { id: 1, is_bot: true } } : { ok: true, result: {} }, + ); + const handler = new TelegramHandler({ granted: GRANTED, fetchFn: fn }); + await handler.authenticate(fakeVault({ telegram_bot_token: "t" })); + await handler.send({ kind: "reply", chatId: "42", text: "hello" }); + const sendCall = calls.find((c) => c.url.endsWith("/sendMessage")); + expect(sendCall?.body).toEqual({ chat_id: "42", text: "hello" }); + }); + + test("send before authenticate throws not_authenticated", async () => { + const { fn } = scriptedFetch(() => ({ ok: true })); + const handler = new TelegramHandler({ granted: GRANTED, fetchFn: fn }); + await expect(handler.send({ kind: "reply", chatId: "1", text: "x" })).rejects.toMatchObject({ + reason: "not_authenticated", + }); + }); + + test("send asserts NETWORK_FETCH (denied when not granted)", async () => { + const { fn } = scriptedFetch((method) => + method === "getMe" ? { ok: true, result: { id: 1, is_bot: true } } : { ok: true }, + ); + // Authenticate with full grant, then a fresh handler with no NETWORK_FETCH for send. + const denied = new TelegramHandler({ granted: ["READ_VAULT"], fetchFn: fn }); + // Manually mark authenticated is not possible (private); instead authenticate fails first. + await expect( + denied.authenticate(fakeVault({ telegram_bot_token: "t" })), + ).rejects.toBeInstanceOf(CapabilityError); + }); + + test("receive long-polls getUpdates and feeds text messages to the sink", async () => { + let polls = 0; + const { fn } = scriptedFetch((method) => { + if (method === "getMe") return { ok: true, result: { id: 1, is_bot: true } }; + if (method === "getUpdates") { + polls++; + if (polls === 1) { + return { + ok: true, + result: [ + { + update_id: 10, + message: { + message_id: 1, + text: "hi bot", + chat: { id: 99 }, + from: { id: 7, username: "omar" }, + date: 1700, + }, + }, + ], + }; + } + return { ok: true, result: [] }; // subsequent polls are empty + } + return { ok: true, result: {} }; + }); + + const handler = new TelegramHandler({ granted: GRANTED, fetchFn: fn }); + await handler.authenticate(fakeVault({ telegram_bot_token: "t" })); + + const received: InboundMessage[] = []; + const sink: ChatSink = async (m) => void received.push(m); + const handle = handler.receive(sink); + + // Let the loop run a couple of iterations, then stop. + await new Promise((r) => setTimeout(r, 50)); + handle.stop(); + + expect(received).toHaveLength(1); + expect(received[0]).toEqual({ + channel: "telegram", + chatId: "99", + from: "omar", + text: "hi bot", + ts: 1700 * 1000, + }); + }); + + test("receive isolates a failing sink (ingress survives)", async () => { + let polls = 0; + const { fn } = scriptedFetch((method) => { + if (method === "getMe") return { ok: true, result: { id: 1, is_bot: true } }; + if (method === "getUpdates") { + polls++; + if (polls === 1) { + return { + ok: true, + result: [ + { update_id: 1, message: { message_id: 1, text: "x", chat: { id: 1 }, date: 1 } }, + ], + }; + } + return { ok: true, result: [] }; + } + return { ok: true, result: {} }; + }); + const handler = new TelegramHandler({ granted: GRANTED, fetchFn: fn }); + await handler.authenticate(fakeVault({ telegram_bot_token: "t" })); + const handle = handler.receive(async () => { + throw new Error("chatbot down"); + }); + await new Promise((r) => setTimeout(r, 30)); + handle.stop(); // did not crash the loop + expect(polls).toBeGreaterThan(0); + }); +}); diff --git a/packages/vesper-core/src/connections/telegram.ts b/packages/vesper-core/src/connections/telegram.ts new file mode 100644 index 0000000..ff3d40c --- /dev/null +++ b/packages/vesper-core/src/connections/telegram.ts @@ -0,0 +1,204 @@ +/** + * The Telegram channel handler — the only handler BUILT in v1. It is pure + * transport over the Bot API: `authenticate` loads the bot token from the vault + * and verifies it with `getMe`; `send` posts a `sendMessage`; `receive` runs a + * long-poll `getUpdates` loop (no public URL needed — works behind NAT) and feeds + * each message into the {@link ChatSink}. EVERY HTTP call goes through the injected + * {@link allowlistedFetch} seam, so the suite fetches to nothing and a handler can + * never reach a host outside its descriptor's allowlist (Hard rule 12). + */ + +import type { Capability } from "../capabilities/index.ts"; +import type { Vault } from "../vault/index.ts"; +import { channelById } from "./catalog.ts"; +import { ConnectionError } from "./errors.ts"; +import { allowlistedFetch, type FetchFn } from "./fetch.ts"; +import type { + ChannelDescriptor, + ChannelHandler, + ChatSink, + InboundMessage, + OutboundIntent, + Stoppable, +} from "./types.ts"; + +/** The Telegram catalog descriptor (non-null — telegram is a built-in catalog id). */ +const TELEGRAM_DESCRIPTOR = channelById("telegram") as ChannelDescriptor; + +/** Long-poll timeout (seconds) passed to `getUpdates`; the server holds the request open. */ +const LONG_POLL_TIMEOUT_S = 25; + +/** Options for {@link TelegramHandler}. */ +export interface TelegramHandlerOptions { + /** Capabilities the handler was granted; MUST include NETWORK_FETCH (+ READ_VAULT). */ + readonly granted: readonly Capability[]; + /** The fetch implementation — injected so the suite fetches to nothing. */ + readonly fetchFn?: FetchFn; + /** Vault KEY the bot token is stored under. Defaults to `telegram_bot_token`. */ + readonly vaultKey?: string; + /** Hosts the handler may reach; defaults to the descriptor allowlist (narrowed, never widened upstream). */ + readonly allowedHosts?: readonly string[]; +} + +/** A Telegram `User` (the `getMe` result we care about). */ +interface TelegramUser { + readonly id: number; + readonly is_bot: boolean; + readonly username?: string; +} + +/** A Telegram `Message` envelope (the subset `getUpdates` hands us). */ +interface TelegramMessage { + readonly message_id: number; + readonly text?: string; + readonly chat: { readonly id: number }; + readonly from?: { readonly id: number; readonly username?: string }; + readonly date: number; +} + +/** A Telegram `Update` row. */ +interface TelegramUpdate { + readonly update_id: number; + readonly message?: TelegramMessage; +} + +/** The standard `{ ok, result }` Bot API envelope. */ +interface TelegramResponse { + readonly ok: boolean; + readonly result?: T; + readonly description?: string; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +export class TelegramHandler implements ChannelHandler { + readonly descriptor: ChannelDescriptor = TELEGRAM_DESCRIPTOR; + readonly #granted: readonly Capability[]; + readonly #fetchFn: FetchFn | undefined; + readonly #vaultKey: string; + readonly #allowedHosts: readonly string[]; + #token: string | null = null; + + constructor(options: TelegramHandlerOptions) { + this.#granted = options.granted; + this.#fetchFn = options.fetchFn; + this.#vaultKey = options.vaultKey ?? "telegram_bot_token"; + this.#allowedHosts = options.allowedHosts ?? this.descriptor.allowedHosts; + } + + /** Build a Bot API method URL for the loaded token. */ + #methodUrl(method: string): string { + return `https://api.telegram.org/bot${this.#token}/${method}`; + } + + /** Call a Bot API method through the allowlisted-fetch seam and return its `result`. */ + async #call(method: string, body?: unknown): Promise { + if (this.#token === null) { + throw new ConnectionError("not_authenticated", "telegram handler is not authenticated"); + } + const init: RequestInit = + body === undefined + ? { method: "GET" } + : { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + }; + const res = await allowlistedFetch({ + url: this.#methodUrl(method), + allowedHosts: this.#allowedHosts, + granted: this.#granted, + ...(this.#fetchFn !== undefined ? { fetchFn: this.#fetchFn } : {}), + init, + }); + let parsed: unknown; + try { + parsed = await res.json(); + } catch (cause) { + throw new ConnectionError("invalid_response", `telegram ${method} returned non-JSON`, { + cause, + }); + } + if (!isRecord(parsed) || parsed.ok !== true) { + const description = + isRecord(parsed) && typeof parsed.description === "string" + ? parsed.description + : `status ${res.status}`; + throw new ConnectionError("invalid_response", `telegram ${method} failed: ${description}`); + } + return (parsed as TelegramResponse).result as T; + } + + /** Load the bot token from the vault and verify it with `getMe`. */ + async authenticate(vault: Vault): Promise { + this.#token = await vault.get(this.#vaultKey); + const me = await this.#call("getMe"); + if (!me.is_bot) { + throw new ConnectionError("not_authenticated", "telegram getMe did not return a bot"); + } + } + + /** Deliver an outbound intent via `sendMessage`. */ + async send(intent: OutboundIntent): Promise { + await this.#call("sendMessage", { chat_id: intent.chatId, text: intent.text }); + } + + /** + * Start a long-poll `getUpdates` loop, handing each text message to `sink`. + * Returns a {@link Stoppable}; `stop()` halts the loop after the in-flight poll + * settles. A failed poll is isolated (the loop continues) so a transient error + * does not kill ingress. + */ + receive(sink: ChatSink): Stoppable { + let running = true; + let offset = 0; + + const loop = async (): Promise => { + while (running) { + // Yield to the event loop each iteration so a cooperative stop() and any + // pending timers actually run. Without this, an instantly-resolving poll + // (a mock, or a server that ignores the long-poll timeout) re-arms purely on + // the microtask queue, starving macrotasks — the loop never sees running=false + // and the process never quiesces. + await new Promise((resolve) => setTimeout(resolve, 0)); + let updates: TelegramUpdate[]; + try { + updates = await this.#call("getUpdates", { + offset, + timeout: LONG_POLL_TIMEOUT_S, + }); + } catch { + // Isolate a transient poll failure; yield, then retry while running. + await new Promise((resolve) => setTimeout(resolve, 1_000)); + continue; + } + for (const update of updates) { + offset = Math.max(offset, update.update_id + 1); + const msg = update.message; + if (msg?.text === undefined) continue; + const inbound: InboundMessage = { + channel: "telegram", + chatId: String(msg.chat.id), + from: msg.from?.username ?? String(msg.from?.id ?? "unknown"), + text: msg.text, + ts: msg.date * 1_000, + }; + try { + await sink(inbound); + } catch { + // A sink failure (e.g. chatbot down) must not stop ingress. + } + } + } + }; + + void loop(); + return { + stop() { + running = false; + }, + }; + } +} diff --git a/packages/vesper-core/src/connections/types.ts b/packages/vesper-core/src/connections/types.ts new file mode 100644 index 0000000..911a07b --- /dev/null +++ b/packages/vesper-core/src/connections/types.ts @@ -0,0 +1,84 @@ +/** + * Public types for the Connections layer — pluggable messaging-channel handlers + * modeled exactly on the shipped `UiModule` + `ModuleRegistry` seam. + * + * A handler is PURE TRANSPORT: it authenticates, sends an outbound intent, and + * receives inbound messages. It NEVER reasons — the chatbot/pipeline is the brain + * (Hard rule 12). Outbound is the only network egress and is `NETWORK_FETCH`-gated + * through the descriptor's host allowlist (see `fetch.ts`). + */ + +import type { Vault } from "../vault/index.ts"; + +/** The messaging channels Vesper knows about. Catalog-only; no arbitrary channels. */ +export type ChannelId = "telegram" | "discord" | "whatsapp" | "signal"; + +/** Whether a channel is BUILT in v1 or declared-but-deferred (catalog + tutorial only). */ +export type ChannelStatus = "ready" | "deferred"; + +/** The transport a channel handler uses to reach its service. */ +export type ChannelTransport = "long-poll" | "webhook" | "bot-api" | "local-cli"; + +/** An outbound message the chatbot asks a handler to deliver. */ +export interface OutboundIntent { + readonly kind: "reply" | "notify"; + /** Channel-native conversation id (e.g. a Telegram chat id). */ + readonly chatId: string; + readonly text: string; +} + +/** An inbound message a handler received and hands to the {@link ChatSink}. */ +export interface InboundMessage { + readonly channel: ChannelId; + /** Channel-native conversation id the reply is routed back to. */ + readonly chatId: string; + /** Channel-native sender identity (id/username); used for audit, never as auth. */ + readonly from: string; + readonly text: string; + /** Unix timestamp in milliseconds the message arrived. */ + readonly ts: number; +} + +/** + * A {@link CHANNEL_CATALOG} entry — the single source of truth for a channel. + * Mirrors the curated-CATALOG-constant pattern: user input selects an id (a KEY), + * never an arbitrary host or URL. + */ +export interface ChannelDescriptor { + readonly id: ChannelId; + readonly displayName: string; + readonly transport: ChannelTransport; + /** Host-allowlist seam for `NETWORK_FETCH` (e.g. `["api.telegram.org"]`). Non-empty for ready channels. */ + readonly allowedHosts: readonly string[]; + /** Vault KEY names this channel needs (NEVER the values). */ + readonly vaultKeys: readonly string[]; + /** The per-channel setup-tutorial anchor (#12). */ + readonly docsUrl: string; + readonly status: ChannelStatus; +} + +/** Forwards an inbound message into the chatbot. The v1 impl POSTs to `/api/chat`. */ +export type ChatSink = (message: InboundMessage) => Promise; + +/** + * A stop handle for a long-running `receive` loop. Idempotent: calling `stop` + * more than once is a no-op. Mirrors the Bun-idiomatic "Stoppable" shape used by + * the UI server handle. + */ +export interface Stoppable { + stop(): void; +} + +/** + * A pluggable channel handler — the {@link import("../../../vesper-ui/src/modules/types.ts").UiModule} + * analogue. Telegram is the only handler BUILT in v1; Discord shares this contract. + */ +export interface ChannelHandler { + readonly descriptor: ChannelDescriptor; + /** Load the credential from the vault (READ_VAULT) and verify it (e.g. getMe). */ + authenticate(vault: Vault): Promise; + /** Deliver an outbound intent. NETWORK_FETCH, host-allowlisted to the descriptor. */ + send(intent: OutboundIntent): Promise; + /** Start the inbound loop feeding `sink`; returns a stop handle. */ + receive(sink: ChatSink): Stoppable; +} diff --git a/packages/vesper-core/src/index.ts b/packages/vesper-core/src/index.ts index 398b9f8..46cd705 100644 --- a/packages/vesper-core/src/index.ts +++ b/packages/vesper-core/src/index.ts @@ -1,9 +1,11 @@ // @vesper/core — host runtime public surface. // Modules are re-exported here as they land through the Foundation feature loop. +export * from "./approval/index.ts"; export * from "./auto-evolve/index.ts"; export * from "./capabilities/index.ts"; export * from "./cli/index.ts"; +export * from "./connections/index.ts"; export { VesperError } from "./errors.ts"; export * from "./ipc/index.ts"; export * from "./presence/index.ts"; diff --git a/packages/vesper-core/src/scheduler/context.test.ts b/packages/vesper-core/src/scheduler/context.test.ts index 8b97bf0..961e764 100644 --- a/packages/vesper-core/src/scheduler/context.test.ts +++ b/packages/vesper-core/src/scheduler/context.test.ts @@ -89,6 +89,24 @@ function makeStore(): { getTaskGrant() { return null; }, + // Chat-home Store methods (migration 007): unused by the context double, stubbed + // so the mock still satisfies the widened Store interface. + createSession() { + return "session-id"; + }, + appendTurn() { + return "turn-id"; + }, + listSessions() { + return []; + }, + listTurns() { + return []; + }, + getTemplate() { + return null; + }, + upsertTemplate() {}, close() {}, }; return { store, finished, events }; diff --git a/packages/vesper-core/src/storage/index.ts b/packages/vesper-core/src/storage/index.ts index 278353b..581ab65 100644 --- a/packages/vesper-core/src/storage/index.ts +++ b/packages/vesper-core/src/storage/index.ts @@ -4,11 +4,18 @@ export { openStore } from "./store.ts"; export type { AppendEventInput, AppendRunEventInput, + AppendTurnInput, + ChatSessionRow, + ChatTurnRole, + ChatTurnRow, + CreateSessionInput, EventRow, FinishRunInput, ListEventsOptions, ListRunEventsOptions, ListRunsOptions, + ListTurnsOptions, + PipelineTemplateRow, RecordRunInput, RunEventKind, RunEventRow, @@ -18,4 +25,5 @@ export type { Store, TaskGrant, UpsertTaskGrantInput, + UpsertTemplateInput, } from "./types.ts"; diff --git a/packages/vesper-core/src/storage/migrations.ts b/packages/vesper-core/src/storage/migrations.ts index 7e981a6..51340d5 100644 --- a/packages/vesper-core/src/storage/migrations.ts +++ b/packages/vesper-core/src/storage/migrations.ts @@ -108,4 +108,34 @@ export const MIGRATIONS: readonly Migration[] = [ CREATE INDEX IF NOT EXISTS idx_run_events_run ON run_events(run_id, ts); `, }, + { + // The chatbot-home surface: a chat session + transcript model and per-pipeline + // editable templates. Each assistant turn carries the `run_id` of the router run + // that produced it, so a transcript bubble and the live activity tree are the same + // data viewed two ways. Forward-only; appended AFTER 006. The `events` table stays + // the durable audit trail (every chat/template mutation also writes an event there). + id: "007_chat_home", + sql: ` + CREATE TABLE IF NOT EXISTS chat_sessions ( + id TEXT PRIMARY KEY NOT NULL, + ts INTEGER NOT NULL, + title TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS chat_turns ( + id TEXT PRIMARY KEY NOT NULL, + session_id TEXT NOT NULL, + ts INTEGER NOT NULL, + role TEXT NOT NULL, + text TEXT NOT NULL, + run_id TEXT + ); + CREATE INDEX IF NOT EXISTS idx_chat_turns_session ON chat_turns(session_id, ts); + CREATE TABLE IF NOT EXISTS pipeline_templates ( + handler_id TEXT PRIMARY KEY NOT NULL, + prompt TEXT NOT NULL DEFAULT '', + default_params TEXT NOT NULL DEFAULT '{}', + updated_at INTEGER NOT NULL + ); + `, + }, ]; diff --git a/packages/vesper-core/src/storage/store.test.ts b/packages/vesper-core/src/storage/store.test.ts index 01cbf8b..19b827a 100644 --- a/packages/vesper-core/src/storage/store.test.ts +++ b/packages/vesper-core/src/storage/store.test.ts @@ -889,3 +889,219 @@ describe("listRuns parentRunId filter and runTree", () => { expect(store.runTree("does-not-exist")).toBeNull(); }); }); + +// --------------------------------------------------------------------------- +// Chat home (migration 007_chat_home) +// --------------------------------------------------------------------------- + +describe("chat sessions and turns", () => { + let path: string; + let store: Store; + + beforeEach(() => { + path = tempDbPath(); + store = openStore(path); + }); + + afterEach(() => { + store.close(); + try { + rmSync(path, { force: true }); + rmSync(`${path}-shm`, { force: true }); + rmSync(`${path}-wal`, { force: true }); + } catch { + // ignore + } + }); + + test("createSession returns a generated id and listSessions reads it back", () => { + const id = store.createSession({ title: "first wish" }); + expect(typeof id).toBe("string"); + expect(id.length).toBeGreaterThan(0); + + const sessions = store.listSessions(); + expect(sessions).toHaveLength(1); + expect(sessions[0]?.id).toBe(id); + expect(sessions[0]?.title).toBe("first wish"); + expect(typeof sessions[0]?.ts).toBe("number"); + }); + + test("createSession honors a supplied id", () => { + const id = store.createSession({ id: "11111111-1111-4111-8111-111111111111", title: "x" }); + expect(id).toBe("11111111-1111-4111-8111-111111111111"); + }); + + test("listSessions is newest-first", () => { + const a = store.createSession({ title: "a" }); + const b = store.createSession({ title: "b" }); + const ids = store.listSessions().map((s) => s.id); + // b was created after a, so it sorts first (ts DESC). + expect(ids[0]).toBe(b); + expect(ids).toContain(a); + }); + + test("appendTurn persists user and assistant turns; listTurns is oldest-first", () => { + const session = store.createSession({ title: "t" }); + const userTurn = store.appendTurn({ sessionId: session, role: "user", text: "do a thing" }); + const asstTurn = store.appendTurn({ + sessionId: session, + role: "assistant", + text: "on it", + runId: "22222222-2222-4222-8222-222222222222", + }); + + const turns = store.listTurns({ sessionId: session }); + expect(turns.map((t) => t.id)).toEqual([userTurn, asstTurn]); + expect(turns[0]?.role).toBe("user"); + expect(turns[0]?.runId).toBeNull(); + expect(turns[1]?.role).toBe("assistant"); + expect(turns[1]?.runId).toBe("22222222-2222-4222-8222-222222222222"); + }); + + test("listTurns filters by afterTs and respects limit", () => { + const session = store.createSession({ title: "t" }); + store.appendTurn({ sessionId: session, role: "user", text: "one" }); + const all = store.listTurns({ sessionId: session }); + const firstTs = all[0]?.ts ?? 0; + + // afterTs strictly greater — the only turn (ts == firstTs) is excluded. + expect(store.listTurns({ sessionId: session, afterTs: firstTs })).toHaveLength(0); + + store.appendTurn({ sessionId: session, role: "assistant", text: "two" }); + store.appendTurn({ sessionId: session, role: "assistant", text: "three" }); + expect(store.listTurns({ sessionId: session, limit: 1 })).toHaveLength(1); + }); + + test("listTurns scopes to its session only", () => { + const s1 = store.createSession({ title: "s1" }); + const s2 = store.createSession({ title: "s2" }); + store.appendTurn({ sessionId: s1, role: "user", text: "a" }); + store.appendTurn({ sessionId: s2, role: "user", text: "b" }); + expect(store.listTurns({ sessionId: s1 })).toHaveLength(1); + expect(store.listTurns({ sessionId: s1 })[0]?.text).toBe("a"); + }); + + test("turns survive reopen (durable transcript)", () => { + const session = store.createSession({ title: "t" }); + store.appendTurn({ sessionId: session, role: "user", text: "persisted" }); + store.close(); + + const reopened = openStore(path); + const turns = reopened.listTurns({ sessionId: session }); + reopened.close(); + expect(turns).toHaveLength(1); + expect(turns[0]?.text).toBe("persisted"); + }); + + test("a corrupted role column is rejected on read (corruption guard)", () => { + const session = store.createSession({ title: "t" }); + // Write a row with an out-of-allowlist role directly. + const db = new Database(path); + db.run( + "INSERT INTO chat_turns (id, session_id, ts, role, text, run_id) VALUES ('bad', ?, 1, 'system', 'x', NULL)", + [session], + ); + db.close(); + expect(() => store.listTurns({ sessionId: session })).toThrow(StorageError); + }); +}); + +describe("pipeline_templates round-trip", () => { + let path: string; + let store: Store; + + beforeEach(() => { + path = tempDbPath(); + store = openStore(path); + }); + + afterEach(() => { + store.close(); + try { + rmSync(path, { force: true }); + rmSync(`${path}-shm`, { force: true }); + rmSync(`${path}-wal`, { force: true }); + } catch { + // ignore + } + }); + + test("getTemplate returns null before any upsert", () => { + expect(store.getTemplate("router")).toBeNull(); + }); + + test("upsertTemplate then getTemplate round-trips prompt + default params", () => { + store.upsertTemplate({ + handlerId: "router", + prompt: "classify strictly", + defaultParams: { tone: "warm", retries: 2 }, + }); + const t = store.getTemplate("router"); + expect(t).not.toBeNull(); + expect(t?.handlerId).toBe("router"); + expect(t?.prompt).toBe("classify strictly"); + expect(t?.defaultParams).toEqual({ tone: "warm", retries: 2 }); + expect(typeof t?.updatedAt).toBe("number"); + }); + + test("upsertTemplate updates an existing row (ON CONFLICT)", () => { + store.upsertTemplate({ handlerId: "router", prompt: "v1", defaultParams: {} }); + store.upsertTemplate({ handlerId: "router", prompt: "v2", defaultParams: { a: 1 } }); + const t = store.getTemplate("router"); + expect(t?.prompt).toBe("v2"); + expect(t?.defaultParams).toEqual({ a: 1 }); + }); + + test("template survives reopen", () => { + store.upsertTemplate({ handlerId: "router", prompt: "kept", defaultParams: {} }); + store.close(); + const reopened = openStore(path); + expect(reopened.getTemplate("router")?.prompt).toBe("kept"); + reopened.close(); + }); +}); + +describe("migration 007 — chat home", () => { + let path: string; + + beforeEach(() => { + path = tempDbPath(); + }); + + afterEach(() => { + try { + rmSync(path, { force: true }); + rmSync(`${path}-shm`, { force: true }); + rmSync(`${path}-wal`, { force: true }); + } catch { + // ignore + } + }); + + test("schema_migrations records 007 and reopen is idempotent (chat tables queryable)", () => { + const first = openStore(path); + first.close(); + const second = openStore(path); + second.close(); + + const db = new Database(path, { readonly: true }); + const ids = db + .query<{ id: string }, []>("SELECT id FROM schema_migrations") + .all() + .map((r) => r.id); + expect(() => db.query("SELECT count(*) FROM chat_sessions").get()).not.toThrow(); + expect(() => db.query("SELECT count(*) FROM chat_turns").get()).not.toThrow(); + expect(() => db.query("SELECT count(*) FROM pipeline_templates").get()).not.toThrow(); + db.close(); + + expect(ids).toContain("007_chat_home"); + expect(ids.filter((id) => id === "007_chat_home")).toHaveLength(1); + }); + + test("007 is sequenced AFTER 006 (forward-only ordering)", () => { + const idx006 = MIGRATIONS.findIndex((m) => m.id.startsWith("006")); + const idx007 = MIGRATIONS.findIndex((m) => m.id === "007_chat_home"); + expect(idx006).toBeGreaterThanOrEqual(0); + expect(idx007).toBeGreaterThan(idx006); + }); +}); diff --git a/packages/vesper-core/src/storage/store.ts b/packages/vesper-core/src/storage/store.ts index 686d339..686b23b 100644 --- a/packages/vesper-core/src/storage/store.ts +++ b/packages/vesper-core/src/storage/store.ts @@ -6,11 +6,18 @@ import { MIGRATIONS } from "./migrations.ts"; import type { AppendEventInput, AppendRunEventInput, + AppendTurnInput, + ChatSessionRow, + ChatTurnRole, + ChatTurnRow, + CreateSessionInput, EventRow, FinishRunInput, ListEventsOptions, ListRunEventsOptions, ListRunsOptions, + ListTurnsOptions, + PipelineTemplateRow, RecordRunInput, RunEventKind, RunEventRow, @@ -20,6 +27,7 @@ import type { Store, TaskGrant, UpsertTaskGrantInput, + UpsertTemplateInput, } from "./types.ts"; /** @@ -68,6 +76,31 @@ interface RawTaskGrantRow { granted_by: unknown; } +/** Raw shape returned for the `chat_sessions` table. */ +interface RawChatSessionRow { + id: unknown; + ts: unknown; + title: unknown; +} + +/** Raw shape returned for the `chat_turns` table. */ +interface RawChatTurnRow { + id: unknown; + session_id: unknown; + ts: unknown; + role: unknown; + text: unknown; + run_id: unknown; +} + +/** Raw shape returned for the `pipeline_templates` table. */ +interface RawPipelineTemplateRow { + handler_id: unknown; + prompt: unknown; + default_params: unknown; + updated_at: unknown; +} + function assertString(value: unknown, column: string): string { if (typeof value !== "string") { throw new StorageError( @@ -192,6 +225,43 @@ function toTaskGrant(raw: RawTaskGrantRow): TaskGrant { }; } +function toChatSessionRow(raw: RawChatSessionRow): ChatSessionRow { + return { + id: assertString(raw.id, "id"), + ts: assertNumber(raw.ts, "ts"), + title: assertString(raw.title, "title"), + }; +} + +/** Narrow a `chat_turns.role` column to the allowlisted union (corruption guard). */ +function assertChatTurnRole(value: unknown, column: string): ChatTurnRole { + const str = assertString(value, column); + if (str !== "user" && str !== "assistant") { + throw new StorageError("query_failed", `unrecognised chat turn role "${str}"`); + } + return str; +} + +function toChatTurnRow(raw: RawChatTurnRow): ChatTurnRow { + return { + id: assertString(raw.id, "id"), + sessionId: assertString(raw.session_id, "session_id"), + ts: assertNumber(raw.ts, "ts"), + role: assertChatTurnRole(raw.role, "role"), + text: assertString(raw.text, "text"), + runId: assertStringOrNull(raw.run_id, "run_id"), + }; +} + +function toPipelineTemplateRow(raw: RawPipelineTemplateRow): PipelineTemplateRow { + return { + handlerId: assertString(raw.handler_id, "handler_id"), + prompt: assertString(raw.prompt, "prompt"), + defaultParams: parsePayload(raw.default_params, "default_params"), + updatedAt: assertNumber(raw.updated_at, "updated_at"), + }; +} + /** {@link Store} backed by a `bun:sqlite` database. */ export class SqliteStore implements Store { readonly #db: Database; @@ -518,6 +588,118 @@ export class SqliteStore implements Store { } } + // ------------------------------------------------------------------------- + // Chat home (migration 007_chat_home) + // ------------------------------------------------------------------------- + + createSession(input: CreateSessionInput): string { + const id = input.id ?? crypto.randomUUID(); + const ts = Date.now(); + try { + this.#db + .query( + "INSERT INTO chat_sessions (id, ts, title) VALUES (?, ?, ?)", + ) + .run(id, ts, input.title); + } catch (cause) { + throw new StorageError("query_failed", "failed to create chat session", { cause }); + } + return id; + } + + appendTurn(input: AppendTurnInput): string { + const id = crypto.randomUUID(); + const ts = Date.now(); + const runId = input.runId ?? null; + try { + this.#db + .query( + "INSERT INTO chat_turns (id, session_id, ts, role, text, run_id) VALUES (?, ?, ?, ?, ?, ?)", + ) + .run(id, input.sessionId, ts, input.role, input.text, runId); + } catch (cause) { + throw new StorageError("query_failed", "failed to append chat turn", { cause }); + } + return id; + } + + listSessions(): ChatSessionRow[] { + try { + // `rowid DESC` breaks ts ties by insertion order so two sessions created in + // the same millisecond still sort newest-first deterministically. + const rows = this.#db + .query( + "SELECT id, ts, title FROM chat_sessions ORDER BY ts DESC, rowid DESC", + ) + .all(); + return rows.map(toChatSessionRow); + } catch (cause) { + if (cause instanceof StorageError) throw cause; + throw new StorageError("query_failed", "failed to list chat sessions", { cause }); + } + } + + listTurns(options: ListTurnsOptions): ChatTurnRow[] { + try { + const conditions: string[] = ["session_id = ?"]; + const params: (string | number)[] = [options.sessionId]; + + if (options.afterTs !== undefined) { + conditions.push("ts > ?"); + params.push(options.afterTs); + } + + const where = ` WHERE ${conditions.join(" AND ")}`; + const limitClause = options.limit !== undefined ? " LIMIT ?" : ""; + if (options.limit !== undefined) { + params.push(options.limit); + } + + // `rowid ASC` breaks ts ties by insertion order so turns appended in the same + // millisecond still read back oldest-first (user before assistant). + const sql = `SELECT id, session_id, ts, role, text, run_id FROM chat_turns${where} ORDER BY ts ASC, rowid ASC${limitClause}`; + const rows = this.#db.query(sql).all(...params); + return rows.map(toChatTurnRow); + } catch (cause) { + if (cause instanceof StorageError) throw cause; + throw new StorageError("query_failed", "failed to list chat turns", { cause }); + } + } + + getTemplate(handlerId: string): PipelineTemplateRow | null { + try { + const row = this.#db + .query( + `SELECT handler_id, prompt, default_params, updated_at + FROM pipeline_templates WHERE handler_id = ?`, + ) + .get(handlerId); + return row !== null ? toPipelineTemplateRow(row) : null; + } catch (cause) { + if (cause instanceof StorageError) throw cause; + throw new StorageError("query_failed", "failed to read pipeline template", { cause }); + } + } + + upsertTemplate(input: UpsertTemplateInput): void { + const updatedAt = Date.now(); + const defaultParamsJson = JSON.stringify(input.defaultParams); + try { + this.#db + .query( + `INSERT INTO pipeline_templates (handler_id, prompt, default_params, updated_at) + VALUES (?, ?, ?, ?) + ON CONFLICT(handler_id) DO UPDATE SET + prompt = excluded.prompt, + default_params = excluded.default_params, + updated_at = excluded.updated_at`, + ) + .run(input.handlerId, input.prompt, defaultParamsJson, updatedAt); + } catch (cause) { + throw new StorageError("query_failed", "failed to upsert pipeline template", { cause }); + } + } + close(): void { this.#db.close(); } diff --git a/packages/vesper-core/src/storage/types.ts b/packages/vesper-core/src/storage/types.ts index d90230f..f5fec41 100644 --- a/packages/vesper-core/src/storage/types.ts +++ b/packages/vesper-core/src/storage/types.ts @@ -128,6 +128,80 @@ export interface UpsertTaskGrantInput { readonly granted_at?: number; } +// --------------------------------------------------------------------------- +// Chat home (migration 007_chat_home) +// --------------------------------------------------------------------------- + +/** A row from the `chat_sessions` table — one conversation thread. */ +export interface ChatSessionRow { + readonly id: string; + /** Unix timestamp in milliseconds the session was created. */ + readonly ts: number; + readonly title: string; +} + +/** The role of a {@link ChatTurnRow}. */ +export type ChatTurnRole = "user" | "assistant"; + +/** + * A row from the `chat_turns` table — a single transcript bubble. An assistant + * turn carries the `runId` of the router run that produced it, so the same row + * renders both as a transcript bubble and as the root of the live activity tree. + */ +export interface ChatTurnRow { + readonly id: string; + readonly sessionId: string; + /** Unix timestamp in milliseconds the turn was appended. */ + readonly ts: number; + readonly role: ChatTurnRole; + readonly text: string; + /** The `runs` row id this assistant turn started, or null (user turns). */ + readonly runId: string | null; +} + +/** A row from the `pipeline_templates` table — a pipeline's editable prompt + params. */ +export interface PipelineTemplateRow { + readonly handlerId: string; + readonly prompt: string; + /** The deserialized default-params object the router merges into spawn params. */ + readonly defaultParams: Record; + /** Unix timestamp in milliseconds the template was last written. */ + readonly updatedAt: number; +} + +/** Input for {@link Store.createSession}. `id`/`ts` are generated when omitted. */ +export interface CreateSessionInput { + /** Pre-allocated session id (UUID); a fresh one is generated when omitted. */ + readonly id?: string; + readonly title: string; +} + +/** Input for {@link Store.appendTurn}. */ +export interface AppendTurnInput { + readonly sessionId: string; + readonly role: ChatTurnRole; + readonly text: string; + /** The `runs` row this turn started (assistant turns); omitted/null for user turns. */ + readonly runId?: string | null; +} + +/** Filters for {@link Store.listTurns}. */ +export interface ListTurnsOptions { + readonly sessionId: string; + /** Return only turns strictly after this timestamp (ts > afterTs). */ + readonly afterTs?: number; + /** Maximum number of rows to return (default: unlimited). */ + readonly limit?: number; +} + +/** Input for {@link Store.upsertTemplate}. */ +export interface UpsertTemplateInput { + readonly handlerId: string; + readonly prompt: string; + /** Default-params object; serialized to JSON on write. */ + readonly defaultParams: Record; +} + /** Optional filters for {@link Store.listEvents}. */ export interface ListEventsOptions { /** Return only events with this source. */ @@ -233,6 +307,28 @@ export interface Store { */ getTaskGrant(handlerId: string, contentHash?: string): TaskGrant | null; + // ------------------------------------------------------------------------- + // Chat home (migration 007_chat_home) + // ------------------------------------------------------------------------- + + /** Create a chat session and return its generated (or supplied) id. */ + createSession(input: CreateSessionInput): string; + + /** Append a transcript turn and return its generated id. */ + appendTurn(input: AppendTurnInput): string; + + /** List chat sessions newest-first (most recent activity at the top). */ + listSessions(): ChatSessionRow[]; + + /** List a session's turns oldest-first, optionally filtered by `afterTs`/`limit`. */ + listTurns(options: ListTurnsOptions): ChatTurnRow[]; + + /** Return the editable template for `handlerId`, or null if none was saved yet. */ + getTemplate(handlerId: string): PipelineTemplateRow | null; + + /** Insert or update a pipeline's editable template (prompt + default params). */ + upsertTemplate(input: UpsertTemplateInput): void; + /** * Close the underlying database connection. After this call the store must not be used. */ diff --git a/packages/vesper-desktop/README.md b/packages/vesper-desktop/README.md new file mode 100644 index 0000000..d94f0dc --- /dev/null +++ b/packages/vesper-desktop/README.md @@ -0,0 +1,48 @@ +# @vesper/desktop + +Native desktop shell for Vesper — a deliberately thin [Tauri 2](https://tauri.app) window +whose WebView loads the Bun daemon's "Vesper World" UI. The host runtime stays Bun; this +package adds no business logic. See `specs/tauri-migration.md` and Linear DEV-112. + +## Why this exists + +For the elder-first Desktop target, "open a browser to `localhost:4317`" is a UX wall. +This shell makes Vesper a double-click native app while reusing the exact same web UI the +daemon already serves (`@vesper/ui`). No Rust in the host — only in this shell. + +## Slice 1 (current) + +A native window pointed at `http://127.0.0.1:4317`. The daemon is started manually for now; +auto-starting it as a bundled sidecar is Slice 2. + +### Run it + +```sh +# 1. Toolchain (one-time): Rust + the Tauri CLI. +# rustup is the supported installer; the Tauri CLI is a devDependency here. + +# 2. Start the Bun daemon (hosts Vesper World on 127.0.0.1:4317): +bun run vesper daemon start # from the repo root + +# 3. Launch the native shell (from this package): +bun run dev # = tauri dev +``` + +A native window opens showing Vesper World — click a creature, inspect, Run; live updates +arrive over the same WebSocket the browser uses. No browser involved. + +### Build a bundle + +```sh +bun run build # = tauri build -> .app / .dmg (macOS) +``` + +## Layout + +- `src-tauri/` — the Rust core (window config + entrypoint). Thin by design. +- `src/index.html` — Slice 1 fallback page (becomes the Slice 2 boot splash). + +## Not yet (later slices) + +Sidecar auto-start + health-wait + attach-to-running-daemon (Slice 2); tray / menu / +notifications (Slice 3); signed installers + auto-updater + CI (Slice 4). diff --git a/packages/vesper-desktop/package.json b/packages/vesper-desktop/package.json new file mode 100644 index 0000000..7d90729 --- /dev/null +++ b/packages/vesper-desktop/package.json @@ -0,0 +1,15 @@ +{ + "name": "@vesper/desktop", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "Native desktop shell for Vesper — a thin Tauri 2 window over the Bun-hosted Vesper World. DEV-112 Slice 1.", + "scripts": { + "dev": "tauri dev", + "build": "tauri build", + "tauri": "tauri" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.11.2" + } +} diff --git a/packages/vesper-desktop/src-tauri/Cargo.lock b/packages/vesper-desktop/src-tauri/Cargo.lock new file mode 100644 index 0000000..e41b582 --- /dev/null +++ b/packages/vesper-desktop/src-tauri/Cargo.lock @@ -0,0 +1,5220 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "brotli" +version = "8.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8119e4516436f5708bbc474a9d395bf12f1b5395e93a92a56e647ac3388c8610" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5962523e1b92ce1b5e793d9169b9943eece10d39f62550bc04bb605d75b94924" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.11.1", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.11.1", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.11.1", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dbus" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.1", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash 0.2.0", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.2+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.11.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.11.1", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a2e3dff89cd322c66647942668faee0a2b1f88ea6cbb4d374b4a8d7e92528c" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.1", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "open" +version = "5.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.12+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.11.1", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shared_child" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" +dependencies = [ + "libc", + "sigchld", + "windows-sys 0.60.2", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.11.1", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "437404997acf375d85f1177afa7e11bb971f274ed6a7b83a2a3e339015f4cc28" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa1f9055fc23919a54e4e125052bed16ed04aef0487086e758fe01a67b451c7" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a0319528a025a38c4078e7dae2c446f4e63620ddb0659a643ede1cb38f90e9" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.117", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae6cb4e3896c21d2f6da5b31251d2faea0153bba56ed0e970f918115dbee4924" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e126abc9e84e35cdfd01596140a73a1850cdb0df0a23acf0185776c30b469a6e" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-shell" +version = "2.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b" +dependencies = [ + "encoding_rs", + "log", + "open", + "os_pipe", + "regex", + "schemars 0.8.22", + "serde", + "serde_json", + "shared_child", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "tokio", +] + +[[package]] +name = "tauri-plugin-single-instance" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8f29386f5e9fdc699182388a33ee80a56de436d91b67459e86afef426282af" +dependencies = [ + "serde", + "serde_json", + "tauri", + "thiserror 2.0.18", + "tracing", + "windows-sys 0.60.2", + "zbus", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48222d7116c8807eaa6fe2f372e023fae125084e61e6eca6d70b7961cdf129ef" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b83849ee63ecb27a8e8d0fe51915ca215076914aca43f96db1179f0f415f6cd9" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092379df9a707631978e6c56b1bc2401d387f01e2d4a3c123360d167bbb9aa95" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.2+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.11.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15edbb0d80583e85ee8df283410038e17314df5cba30da2087a54a85216c0773" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vesper-desktop" +version = "0.1.0" +dependencies = [ + "tauri", + "tauri-build", + "tauri-plugin-shell", + "tauri-plugin-single-instance", +] + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.14.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.14.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap 2.14.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.14.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.3", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" +dependencies = [ + "serde", + "winnow 1.0.3", + "zvariant", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zvariant" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 1.0.3", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.117", + "winnow 1.0.3", +] diff --git a/packages/vesper-desktop/src-tauri/Cargo.toml b/packages/vesper-desktop/src-tauri/Cargo.toml new file mode 100644 index 0000000..246f070 --- /dev/null +++ b/packages/vesper-desktop/src-tauri/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "vesper-desktop" +version = "0.1.0" +edition = "2021" +description = "Native desktop shell for Vesper (thin Tauri window over the Bun daemon)." +authors = ["ogarciarevett"] +# Pin a recent-enough toolchain; CI provisions Rust for the desktop package only. +rust-version = "1.77" + +# The compiled shell binary. The Bun host stays Bun — see specs/tauri-migration.md. +[[bin]] +name = "vesper-desktop" +path = "src/main.rs" + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = ["tray-icon"] } +tauri-plugin-shell = "2" +tauri-plugin-single-instance = "2" + +# Smaller, faster release binaries for distribution. +[profile.release] +codegen-units = 1 +lto = true +opt-level = "s" +panic = "abort" +strip = true diff --git a/packages/vesper-desktop/src-tauri/build.rs b/packages/vesper-desktop/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/packages/vesper-desktop/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/packages/vesper-desktop/src-tauri/capabilities/default.json b/packages/vesper-desktop/src-tauri/capabilities/default.json new file mode 100644 index 0000000..b8c703b --- /dev/null +++ b/packages/vesper-desktop/src-tauri/capabilities/default.json @@ -0,0 +1,7 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Core permissions for the main and panel windows. Both load the local Vesper World over HTTP; the panel popover calls the app's custom commands (open_main, quit_app) over the Tauri IPC bridge, which custom commands allow by default under core defaults.", + "windows": ["main", "panel"], + "permissions": ["core:default"] +} diff --git a/packages/vesper-desktop/src-tauri/icons/128x128.png b/packages/vesper-desktop/src-tauri/icons/128x128.png new file mode 100644 index 0000000..1c1de89 Binary files /dev/null and b/packages/vesper-desktop/src-tauri/icons/128x128.png differ diff --git a/packages/vesper-desktop/src-tauri/icons/128x128@2x.png b/packages/vesper-desktop/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000..8265f65 Binary files /dev/null and b/packages/vesper-desktop/src-tauri/icons/128x128@2x.png differ diff --git a/packages/vesper-desktop/src-tauri/icons/32x32.png b/packages/vesper-desktop/src-tauri/icons/32x32.png new file mode 100644 index 0000000..687b884 Binary files /dev/null and b/packages/vesper-desktop/src-tauri/icons/32x32.png differ diff --git a/packages/vesper-desktop/src-tauri/icons/64x64.png b/packages/vesper-desktop/src-tauri/icons/64x64.png new file mode 100644 index 0000000..e8d2239 Binary files /dev/null and b/packages/vesper-desktop/src-tauri/icons/64x64.png differ diff --git a/packages/vesper-desktop/src-tauri/icons/Square107x107Logo.png b/packages/vesper-desktop/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000..3378204 Binary files /dev/null and b/packages/vesper-desktop/src-tauri/icons/Square107x107Logo.png differ diff --git a/packages/vesper-desktop/src-tauri/icons/Square142x142Logo.png b/packages/vesper-desktop/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 0000000..90ebdc7 Binary files /dev/null and b/packages/vesper-desktop/src-tauri/icons/Square142x142Logo.png differ diff --git a/packages/vesper-desktop/src-tauri/icons/Square150x150Logo.png b/packages/vesper-desktop/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 0000000..f535fac Binary files /dev/null and b/packages/vesper-desktop/src-tauri/icons/Square150x150Logo.png differ diff --git a/packages/vesper-desktop/src-tauri/icons/Square284x284Logo.png b/packages/vesper-desktop/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000..98edde4 Binary files /dev/null and b/packages/vesper-desktop/src-tauri/icons/Square284x284Logo.png differ diff --git a/packages/vesper-desktop/src-tauri/icons/Square30x30Logo.png b/packages/vesper-desktop/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000..bf6213f Binary files /dev/null and b/packages/vesper-desktop/src-tauri/icons/Square30x30Logo.png differ diff --git a/packages/vesper-desktop/src-tauri/icons/Square310x310Logo.png b/packages/vesper-desktop/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000..93874be Binary files /dev/null and b/packages/vesper-desktop/src-tauri/icons/Square310x310Logo.png differ diff --git a/packages/vesper-desktop/src-tauri/icons/Square44x44Logo.png b/packages/vesper-desktop/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 0000000..2086177 Binary files /dev/null and b/packages/vesper-desktop/src-tauri/icons/Square44x44Logo.png differ diff --git a/packages/vesper-desktop/src-tauri/icons/Square71x71Logo.png b/packages/vesper-desktop/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000..2c52f0c Binary files /dev/null and b/packages/vesper-desktop/src-tauri/icons/Square71x71Logo.png differ diff --git a/packages/vesper-desktop/src-tauri/icons/Square89x89Logo.png b/packages/vesper-desktop/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 0000000..f35bf6a Binary files /dev/null and b/packages/vesper-desktop/src-tauri/icons/Square89x89Logo.png differ diff --git a/packages/vesper-desktop/src-tauri/icons/StoreLogo.png b/packages/vesper-desktop/src-tauri/icons/StoreLogo.png new file mode 100644 index 0000000..a363b75 Binary files /dev/null and b/packages/vesper-desktop/src-tauri/icons/StoreLogo.png differ diff --git a/packages/vesper-desktop/src-tauri/icons/icon.icns b/packages/vesper-desktop/src-tauri/icons/icon.icns new file mode 100644 index 0000000..b26aabc Binary files /dev/null and b/packages/vesper-desktop/src-tauri/icons/icon.icns differ diff --git a/packages/vesper-desktop/src-tauri/icons/icon.ico b/packages/vesper-desktop/src-tauri/icons/icon.ico new file mode 100644 index 0000000..1e30046 Binary files /dev/null and b/packages/vesper-desktop/src-tauri/icons/icon.ico differ diff --git a/packages/vesper-desktop/src-tauri/icons/icon.png b/packages/vesper-desktop/src-tauri/icons/icon.png new file mode 100644 index 0000000..b106d1a Binary files /dev/null and b/packages/vesper-desktop/src-tauri/icons/icon.png differ diff --git a/packages/vesper-desktop/src-tauri/src/main.rs b/packages/vesper-desktop/src-tauri/src/main.rs new file mode 100644 index 0000000..2232c50 --- /dev/null +++ b/packages/vesper-desktop/src-tauri/src/main.rs @@ -0,0 +1,254 @@ +// Vesper desktop shell — DEV-112 slices 2-3. +// +// A thin native window over the Bun daemon. The Rust core holds no business logic; it: +// 1. spawns the compiled `vesper-daemon` sidecar (serves Vesper World on 127.0.0.1:4317), +// 2. waits for that port to accept connections, +// 3. opens the window onto it, +// 4. stops the sidecar on exit, +// plus native chrome (slice 3): a system tray (Show/Quit) and single-instance focus. +// +// Attach-if-already-running is free: if a daemon is already up (e.g. `vesper daemon +// start`), the sidecar's own single-instance guard makes it exit immediately, the window +// attaches to the running daemon, and on quit we only kill OUR child — never someone +// else's daemon. +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use std::net::{SocketAddr, TcpStream}; +use std::sync::Mutex; +use std::time::{Duration, Instant}; + +use tauri::menu::{Menu, MenuItem}; +use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}; +use tauri::{Manager, PhysicalPosition, WebviewUrl, WebviewWindowBuilder, WindowEvent}; +use tauri_plugin_shell::process::CommandChild; +use tauri_plugin_shell::ShellExt; + +/// Localhost address the daemon serves Vesper World on (matches `uiPort()`'s 4317 default). +const UI_ADDR: &str = "127.0.0.1:4317"; +/// URL of the compact menu-bar popover UI (built by the web team, served by the daemon). +const PANEL_URL: &str = "http://127.0.0.1:4317/?panel=1"; +/// Borderless popover size (logical pixels) — a quick-glance panel under the tray icon. +const PANEL_WIDTH: f64 = 380.0; +const PANEL_HEIGHT: f64 = 480.0; +/// Gap (physical-ish, scaled at use) between the menu bar / tray icon and the panel top. +const PANEL_GAP: f64 = 6.0; +/// Max time to wait for the daemon to come up before opening the window anyway. +const HEALTH_TIMEOUT: Duration = Duration::from_secs(30); +/// Poll interval while waiting for the daemon's port. +const HEALTH_POLL: Duration = Duration::from_millis(250); + +/// Holds the spawned sidecar so it can be stopped on exit. `None` when we attached to an +/// already-running daemon (the spawned child exited via the single-instance guard). +struct Sidecar(Mutex>); + +/// Show and focus the main window if it exists yet (it is created after the health-wait). +fn focus_main(app: &tauri::AppHandle) { + if let Some(window) = app.get_webview_window("main") { + let _ = window.show(); + let _ = window.set_focus(); + } +} + +/// Build the hidden borderless popover window pointed at the daemon's compact panel UI. +/// It stays hidden until the tray icon is left-clicked. Returns `Ok(())` even if the +/// daemon is not up yet — the window simply shows blank until the page loads. +fn build_panel(app: &tauri::AppHandle) -> tauri::Result<()> { + if app.get_webview_window("panel").is_some() { + return Ok(()); + } + let window = WebviewWindowBuilder::new( + app, + "panel", + WebviewUrl::External(PANEL_URL.parse().expect("valid panel url")), + ) + .title("Vesper") + .decorations(false) + .always_on_top(true) + .skip_taskbar(true) + .resizable(false) + .visible(false) + .inner_size(PANEL_WIDTH, PANEL_HEIGHT) + .build()?; + + // Dismiss-on-click-away: hide the popover the moment it loses focus. + let dismiss = window.clone(); + window.on_window_event(move |event| { + if let WindowEvent::Focused(false) = event { + let _ = dismiss.hide(); + } + }); + + Ok(()) +} + +/// Toggle the popover relative to the tray icon's on-screen `rect` (physical pixels). +/// If visible, hide it; otherwise anchor its top just below the menu bar, centered under +/// the icon, clamped to the monitor's usable area, then show + focus it. +fn toggle_panel(app: &tauri::AppHandle, tray_rect: tauri::Rect) { + // Lazily create the panel on first toggle (covers the daemon-not-up-at-setup case). + if build_panel(app).is_err() { + return; + } + let Some(panel) = app.get_webview_window("panel") else { + return; + }; + + if panel.is_visible().unwrap_or(false) { + let _ = panel.hide(); + return; + } + + position_panel(&panel, tray_rect); + let _ = panel.show(); + let _ = panel.set_focus(); +} + +/// Place the popover under the tray icon: horizontally centered on the icon, top edge a +/// small gap below the icon's bottom, clamped to the monitor work area so it never spills +/// off-screen. All math is in physical pixels (the coordinate space `set_position` uses). +fn position_panel(panel: &tauri::WebviewWindow, tray_rect: tauri::Rect) { + let scale = panel.scale_factor().unwrap_or(1.0); + + // Tray icon rect, normalized to physical pixels. + let icon_pos = tray_rect.position.to_physical::(scale); + let icon_size = tray_rect.size.to_physical::(scale); + let icon_center_x = icon_pos.x + icon_size.width / 2.0; + let icon_bottom_y = icon_pos.y + icon_size.height; + + // Panel outer size in physical pixels (falls back to the configured logical size). + let (panel_w, panel_h) = match panel.outer_size() { + Ok(size) => (size.width as f64, size.height as f64), + Err(_) => (PANEL_WIDTH * scale, PANEL_HEIGHT * scale), + }; + + let mut x = icon_center_x - panel_w / 2.0; + let mut y = icon_bottom_y + PANEL_GAP * scale; + + // Clamp to the monitor's usable area (work area excludes the menu bar / dock). + if let Ok(Some(monitor)) = panel.current_monitor() { + let area = monitor.work_area(); + let min_x = area.position.x as f64; + let min_y = area.position.y as f64; + let max_x = min_x + area.size.width as f64 - panel_w; + let max_y = min_y + area.size.height as f64 - panel_h; + x = x.clamp(min_x, max_x.max(min_x)); + y = y.clamp(min_y, max_y.max(min_y)); + } + + let _ = panel.set_position(PhysicalPosition::new(x, y)); +} + +/// Show + focus the main window. Called by the panel UI via `window.__TAURI__`. +#[tauri::command] +fn open_main(app: tauri::AppHandle) { + focus_main(&app); +} + +/// Quit the whole app. Called by the panel UI via `window.__TAURI__`. +#[tauri::command] +fn quit_app(app: tauri::AppHandle) { + app.exit(0); +} + +fn main() { + tauri::Builder::default() + // single-instance MUST be the first plugin: a second launch just focuses us. + .plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| { + focus_main(app); + })) + .plugin(tauri_plugin_shell::init()) + .manage(Sidecar(Mutex::new(None))) + .invoke_handler(tauri::generate_handler![open_main, quit_app]) + .setup(|app| { + // Native chrome (slice 3): a tray icon with Show/Quit. Built on the app's + // bundled icon; Tauri retains the registered tray for the app's lifetime. + let show = MenuItem::with_id(app, "show", "Show Vesper", true, None::<&str>)?; + let quit = MenuItem::with_id(app, "quit", "Quit Vesper", true, None::<&str>)?; + let menu = Menu::with_items(app, &[&show, &quit])?; + TrayIconBuilder::with_id("vesper-tray") + .icon(app.default_window_icon().expect("bundled app icon").clone()) + .tooltip("Vesper") + .menu(&menu) + // Keep the right-click menu (Show/Quit); left-click toggles the popover. + .show_menu_on_left_click(false) + .on_menu_event(|app, event| match event.id.as_ref() { + "show" => focus_main(app), + "quit" => app.exit(0), + _ => {} + }) + .on_tray_icon_event(|tray, event| { + if let TrayIconEvent::Click { + button: MouseButton::Left, + button_state: MouseButtonState::Up, + rect, + .. + } = event + { + toggle_panel(tray.app_handle(), rect); + } + }) + .build(app)?; + + // 1. Spawn the compiled daemon sidecar (runs `vesper-daemon daemon run`). + let (mut rx, child) = app + .shell() + .sidecar("vesper-daemon")? + .args(["daemon", "run"]) + .spawn()?; + app.state::().0.lock().unwrap().replace(child); + + // Drain the sidecar's output channel so its stdio pipe never fills and blocks. + tauri::async_runtime::spawn(async move { while rx.recv().await.is_some() {} }); + + // 2 + 3. Wait for the UI port, then open the window onto it (on the main thread). + let handle = app.handle().clone(); + std::thread::spawn(move || { + let addr: SocketAddr = UI_ADDR.parse().expect("valid UI socket address"); + let deadline = Instant::now() + HEALTH_TIMEOUT; + while Instant::now() < deadline { + if TcpStream::connect_timeout(&addr, HEALTH_POLL).is_ok() { + break; + } + std::thread::sleep(HEALTH_POLL); + } + let window_handle = handle.clone(); + let _ = handle.run_on_main_thread(move || { + let url = format!("http://{UI_ADDR}"); + let builder = WebviewWindowBuilder::new( + &window_handle, + "main", + WebviewUrl::External(url.parse().expect("valid UI url")), + ) + .title("Vesper") + .inner_size(1180.0, 820.0) + .min_inner_size(880.0, 600.0); + + // macOS-only: frameless/overlay titlebar so the web UI's custom HTML + // titlebar shows with the native traffic-light buttons inset over it + // (the "native application" look). Windows/Linux are unaffected. + #[cfg(target_os = "macos")] + let builder = builder + .title_bar_style(tauri::TitleBarStyle::Overlay) + .hidden_title(true); + + let _ = builder.build(); + + // Menu-bar popover: a hidden borderless window pointed at the daemon's + // compact panel UI. Stays hidden until the tray icon is left-clicked. + let _ = build_panel(&window_handle); + }); + }); + + Ok(()) + }) + .build(tauri::generate_context!()) + .expect("error while building the Vesper desktop shell") + .run(|app_handle, event| { + // 4. Stop our sidecar on exit. Never touches an attached (CLI-owned) daemon. + if let tauri::RunEvent::Exit = event { + if let Some(child) = app_handle.state::().0.lock().unwrap().take() { + let _ = child.kill(); + } + } + }); +} diff --git a/packages/vesper-desktop/src-tauri/tauri.conf.json b/packages/vesper-desktop/src-tauri/tauri.conf.json new file mode 100644 index 0000000..ff6eae7 --- /dev/null +++ b/packages/vesper-desktop/src-tauri/tauri.conf.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "Vesper", + "version": "0.1.0", + "identifier": "com.ogarciarevett.vesper.desktop", + "build": { + "frontendDist": "../src" + }, + "app": { + "withGlobalTauri": true, + "windows": [], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "externalBin": ["binaries/vesper-daemon"], + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + } +} diff --git a/packages/vesper-desktop/src/index.html b/packages/vesper-desktop/src/index.html new file mode 100644 index 0000000..a18b41b --- /dev/null +++ b/packages/vesper-desktop/src/index.html @@ -0,0 +1,46 @@ + + + + + + Vesper + + + + +
+
Vesper
+
+ Waiting for the local runtime. Start the daemon, then reopen: +
vesper daemon start +
+
+ + diff --git a/packages/vesper-ui/src/client/brand/builtins.ts b/packages/vesper-ui/src/client/brand/builtins.ts deleted file mode 100644 index ad29a72..0000000 --- a/packages/vesper-ui/src/client/brand/builtins.ts +++ /dev/null @@ -1,193 +0,0 @@ -/// -import { registerMark } from "./registry.ts"; -import type { BrandMark } from "./types.ts"; - -// --- per-brand procedural draws (centered at cx,cy within radius r) ---------- - -function sunburst( - ctx: CanvasRenderingContext2D, - cx: number, - cy: number, - r: number, - c: string, -): void { - ctx.strokeStyle = c; - ctx.lineWidth = Math.max(1.5, r * 0.22); - for (let i = 0; i < 7; i++) { - const a = (i / 7) * Math.PI * 2 - Math.PI / 2; - ctx.beginPath(); - ctx.moveTo(cx + Math.cos(a) * r * 0.28, cy + Math.sin(a) * r * 0.28); - ctx.lineTo(cx + Math.cos(a) * r, cy + Math.sin(a) * r); - ctx.stroke(); - } -} - -function knot(ctx: CanvasRenderingContext2D, cx: number, cy: number, r: number, c: string): void { - ctx.strokeStyle = c; - ctx.lineWidth = Math.max(1.5, r * 0.2); - for (let i = 0; i < 6; i++) { - const a = (i / 6) * Math.PI * 2; - ctx.beginPath(); - ctx.arc(cx + Math.cos(a) * r * 0.42, cy + Math.sin(a) * r * 0.42, r * 0.42, 0, Math.PI * 2); - ctx.stroke(); - } -} - -function sparkle( - ctx: CanvasRenderingContext2D, - cx: number, - cy: number, - r: number, - c: string, -): void { - ctx.fillStyle = c; - const w = r * 0.32; - const pts: [number, number][] = [ - [0, -r], - [w, -w], - [r, 0], - [w, w], - [0, r], - [-w, w], - [-r, 0], - [-w, -w], - ]; - ctx.beginPath(); - for (let i = 0; i < pts.length; i++) { - const p = pts[i]; - if (p === undefined) continue; - if (i === 0) ctx.moveTo(cx + p[0], cy + p[1]); - else ctx.lineTo(cx + p[0], cy + p[1]); - } - ctx.closePath(); - ctx.fill(); -} - -function terminal( - ctx: CanvasRenderingContext2D, - cx: number, - cy: number, - r: number, - c: string, -): void { - ctx.strokeStyle = c; - ctx.lineWidth = Math.max(1.5, r * 0.2); - ctx.lineCap = "round"; - ctx.lineJoin = "round"; - ctx.beginPath(); - ctx.moveTo(cx - r * 0.5, cy - r * 0.4); - ctx.lineTo(cx - r * 0.05, cy); - ctx.lineTo(cx - r * 0.5, cy + r * 0.4); - ctx.stroke(); - ctx.beginPath(); - ctx.moveTo(cx + r * 0.1, cy + r * 0.45); - ctx.lineTo(cx + r * 0.6, cy + r * 0.45); - ctx.stroke(); -} - -function claw(ctx: CanvasRenderingContext2D, cx: number, cy: number, r: number, c: string): void { - ctx.strokeStyle = c; - ctx.lineWidth = Math.max(1.5, r * 0.22); - ctx.lineCap = "round"; - ctx.beginPath(); - ctx.arc(cx, cy - r * 0.1, r * 0.7, Math.PI * 0.15, Math.PI * 0.95); - ctx.stroke(); - ctx.beginPath(); - ctx.arc(cx, cy + r * 0.35, r * 0.6, -Math.PI * 0.9, -Math.PI * 0.1); - ctx.stroke(); -} - -function wingedStaff( - ctx: CanvasRenderingContext2D, - cx: number, - cy: number, - r: number, - c: string, -): void { - ctx.strokeStyle = c; - ctx.lineWidth = Math.max(1.5, r * 0.18); - ctx.lineCap = "round"; - // staff - ctx.beginPath(); - ctx.moveTo(cx, cy - r * 0.7); - ctx.lineTo(cx, cy + r * 0.7); - ctx.stroke(); - // two short wings near the top - for (const dir of [-1, 1]) { - ctx.beginPath(); - ctx.moveTo(cx, cy - r * 0.45); - ctx.quadraticCurveTo(cx + dir * r * 0.7, cy - r * 0.7, cx + dir * r * 0.9, cy - r * 0.3); - ctx.stroke(); - } -} - -function mechClaw( - ctx: CanvasRenderingContext2D, - cx: number, - cy: number, - r: number, - c: string, -): void { - ctx.strokeStyle = c; - ctx.lineWidth = Math.max(1.5, r * 0.18); - ctx.lineJoin = "miter"; - // three angular talon segments - for (let i = -1; i <= 1; i++) { - const ox = i * r * 0.4; - ctx.beginPath(); - ctx.moveTo(cx + ox, cy - r * 0.6); - ctx.lineTo(cx + ox + r * 0.18, cy); - ctx.lineTo(cx + ox, cy + r * 0.6); - ctx.stroke(); - } -} - -// --- registration ------------------------------------------------------------ - -const MARKS: readonly BrandMark[] = [ - { - id: "claude", - label: "Claude", - color: "#d97757", - draw: (c, x, y, r) => sunburst(c, x, y, r, "#d97757"), - }, - // Codex presents the OpenAI knot; id stays "codex" to match the presence matcher. - { - id: "codex", - label: "Codex", - color: "#1b1b1b", - draw: (c, x, y, r) => knot(c, x, y, r, "#1b1b1b"), - }, - { - id: "gemini", - label: "Gemini", - color: "#7c8cf0", - draw: (c, x, y, r) => sparkle(c, x, y, r, "#7c8cf0"), - }, - { - id: "opencode", - label: "opencode", - color: "#f3b03a", - draw: (c, x, y, r) => terminal(c, x, y, r, "#f3b03a"), - }, - { - id: "zeroclaw", - label: "ZeroClaw", - color: "#d2691e", - draw: (c, x, y, r) => claw(c, x, y, r, "#d2691e"), - }, - { - id: "hermes", - label: "Hermes", - color: "#d4a017", - draw: (c, x, y, r) => wingedStaff(c, x, y, r, "#d4a017"), - }, - { - id: "ironclaw", - label: "IronClaw", - color: "#9fb3c8", - draw: (c, x, y, r) => mechClaw(c, x, y, r, "#9fb3c8"), - }, -]; - -for (const mark of MARKS) registerMark(mark); diff --git a/packages/vesper-ui/src/client/brand/default-glyph.ts b/packages/vesper-ui/src/client/brand/default-glyph.ts deleted file mode 100644 index 6af1273..0000000 --- a/packages/vesper-ui/src/client/brand/default-glyph.ts +++ /dev/null @@ -1,43 +0,0 @@ -/// -import type { BrandMark } from "./types.ts"; - -function drawStar(ctx: CanvasRenderingContext2D, cx: number, cy: number, s: number): void { - ctx.beginPath(); - ctx.moveTo(cx, cy - s); - ctx.lineTo(cx + s * 0.28, cy - s * 0.28); - ctx.lineTo(cx + s, cy); - ctx.lineTo(cx + s * 0.28, cy + s * 0.28); - ctx.lineTo(cx, cy + s); - ctx.lineTo(cx - s * 0.28, cy + s * 0.28); - ctx.lineTo(cx - s, cy); - ctx.lineTo(cx - s * 0.28, cy - s * 0.28); - ctx.closePath(); - ctx.fill(); -} - -/** - * The fallback mark for any agent with no registered brand (Vesper's own - * pipelines, or an unknown agent). An evening-star "V": a chevron with a small - * four-point star above. resolveMark() returns this whenever nothing else matches, - * so a brand mark ALWAYS exists. - */ -export const VESPER_DEFAULT: BrandMark = { - id: "vesper", - label: "Vesper", - color: "#38f0ff", - draw(ctx, cx, cy, r) { - ctx.save(); - ctx.strokeStyle = "#38f0ff"; - ctx.fillStyle = "#38f0ff"; - ctx.lineWidth = Math.max(1.5, r * 0.2); - ctx.lineCap = "round"; - ctx.lineJoin = "round"; - ctx.beginPath(); - ctx.moveTo(cx - r * 0.55, cy - r * 0.3); - ctx.lineTo(cx, cy + r * 0.62); - ctx.lineTo(cx + r * 0.55, cy - r * 0.3); - ctx.stroke(); - drawStar(ctx, cx, cy - r * 0.62, r * 0.28); - ctx.restore(); - }, -}; diff --git a/packages/vesper-ui/src/client/brand/index.ts b/packages/vesper-ui/src/client/brand/index.ts deleted file mode 100644 index 9e049d0..0000000 --- a/packages/vesper-ui/src/client/brand/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Theme-agnostic brand/logo layer — the "every agent shows its real logo" seam. -import "./builtins.ts"; // side-effect: registers the built-in marks - -export { VESPER_DEFAULT } from "./default-glyph.ts"; -export { listMarks, registerMark, resolveMark } from "./registry.ts"; -export type { BrandMark } from "./types.ts"; diff --git a/packages/vesper-ui/src/client/brand/registry.test.ts b/packages/vesper-ui/src/client/brand/registry.test.ts deleted file mode 100644 index 2fbc2e7..0000000 --- a/packages/vesper-ui/src/client/brand/registry.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import "./builtins.ts"; // self-registers the built-in marks -import { listMarks, resolveMark } from "./registry.ts"; - -describe("brand registry resolveMark", () => { - test("resolves a live presence id (presence:) to its brand", () => { - expect(resolveMark("presence:claude-cli").id).toBe("claude"); - expect(resolveMark("presence:claude-app").id).toBe("claude"); - expect(resolveMark("presence:codex-cli").id).toBe("codex"); - expect(resolveMark("presence:gemini-cli").id).toBe("gemini"); - expect(resolveMark("presence:opencode-cli").id).toBe("opencode"); - }); - - test("distinguishes zeroclaw from ironclaw (no substring collision)", () => { - expect(resolveMark("presence:zeroclaw-cli").id).toBe("zeroclaw"); - expect(resolveMark("ironclaw").id).toBe("ironclaw"); - expect(resolveMark("hermes").id).toBe("hermes"); - }); - - test("resolves a bare brand id exactly", () => { - expect(resolveMark("claude").id).toBe("claude"); - expect(resolveMark("codex").id).toBe("codex"); - }); - - test("NEVER returns null — an unknown agent falls back to the Vesper default mark", () => { - expect(resolveMark("skill-train").id).toBe("vesper"); - expect(resolveMark("presence:totally-unknown").id).toBe("vesper"); - expect(resolveMark("").id).toBe("vesper"); - }); - - test("every mark exposes an id, a label, a color, and a draw fn", () => { - for (const mark of listMarks()) { - expect(typeof mark.id).toBe("string"); - expect(mark.label.length).toBeGreaterThan(0); - expect(mark.color).toMatch(/^#[0-9a-f]{6}$/i); - expect(typeof mark.draw).toBe("function"); - } - }); -}); diff --git a/packages/vesper-ui/src/client/brand/registry.ts b/packages/vesper-ui/src/client/brand/registry.ts deleted file mode 100644 index e5f1b2a..0000000 --- a/packages/vesper-ui/src/client/brand/registry.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { VESPER_DEFAULT } from "./default-glyph.ts"; -import type { BrandMark } from "./types.ts"; - -const REGISTRY = new Map(); - -/** Register a brand mark (built-ins self-register on import of ./builtins.ts). */ -export function registerMark(mark: BrandMark): void { - REGISTRY.set(mark.id, mark); -} - -/** All registered marks (e.g. for a theme picker). The Vesper fallback is implicit. */ -export function listMarks(): readonly BrandMark[] { - return [...REGISTRY.values()]; -} - -/** - * Resolve an agent id or brand token to a mark. NEVER returns null — an unknown - * agent falls back to {@link VESPER_DEFAULT}, so every node always has a logo. - * Resolution order: strip a `presence:` prefix, then exact id -> id-prefix -> - * id-substring -> the Vesper default. - */ -export function resolveMark(idOrBrand: string): BrandMark { - const token = idOrBrand.startsWith("presence:") ? idOrBrand.slice("presence:".length) : idOrBrand; - - const exact = REGISTRY.get(token); - if (exact !== undefined) return exact; - - for (const mark of REGISTRY.values()) { - if (token.startsWith(mark.id)) return mark; - } - for (const mark of REGISTRY.values()) { - if (mark.id.length >= 4 && token.includes(mark.id)) return mark; - } - return VESPER_DEFAULT; -} diff --git a/packages/vesper-ui/src/client/brand/types.ts b/packages/vesper-ui/src/client/brand/types.ts deleted file mode 100644 index 4772092..0000000 --- a/packages/vesper-ui/src/client/brand/types.ts +++ /dev/null @@ -1,19 +0,0 @@ -/// - -/** - * A brand mark for an agent — drawn procedurally on the Canvas (no image assets, - * no network; local-first). The brand layer is THEME-AGNOSTIC: every WorldTheme - * resolves and draws marks through it, so "every agent shows its real logo" is a - * structural guarantee, not a per-theme convention. A theme chooses HOW to frame - * a mark (cottage lantern vs neon holo-ring) but never WHETHER it appears. - */ -export interface BrandMark { - /** Stable logo id, also the resolution key (e.g. "claude", "zeroclaw"). */ - readonly id: string; - /** Human label (e.g. "Claude", "ZeroClaw"). */ - readonly label: string; - /** Brand accent color (#rrggbb). */ - readonly color: string; - /** Draw the mark centered at (cx, cy) within radius r, stroked/filled in its color. */ - readonly draw: (ctx: CanvasRenderingContext2D, cx: number, cy: number, r: number) => void; -} diff --git a/packages/vesper-ui/src/client/chat-types.ts b/packages/vesper-ui/src/client/chat-types.ts new file mode 100644 index 0000000..6f06d7f --- /dev/null +++ b/packages/vesper-ui/src/client/chat-types.ts @@ -0,0 +1,43 @@ +/** + * Client-side wire shapes for the chatbot-home + templates routes. These mirror the + * JSON the server serializes (see `server/server.ts`) — the browser bundle cannot + * import `@vesper/core` row types directly, so they are restated structurally here. + */ + +/** JSON of a `chat_sessions` row (`GET /api/chat/sessions`, newest-first). */ +export interface ChatSessionRow { + readonly id: string; + readonly ts: number; + readonly title: string; +} + +/** JSON of a `chat_turns` row (`GET /api/chat/sessions/:id/turns`). */ +export interface ChatTurnRow { + readonly id: string; + readonly sessionId: string; + readonly ts: number; + readonly role: "user" | "assistant"; + readonly text: string; + readonly runId: string | null; +} + +/** The editable-config view of a pipeline's `ScheduledTask` (`GET /api/pipelines`). */ +export interface PipelineConfig { + readonly id: string; + readonly handlerId: string; + readonly kind: string; + readonly scheduleExpr: string; + readonly enabled: boolean; + readonly maxRunsPerDay: number | null; + readonly maxConcurrent: number | null; + readonly maxDurationMs: number | null; + readonly requiredCapabilities: readonly string[]; +} + +/** Response of `GET /api/pipelines/:id/template`. */ +export interface PipelineTemplate { + readonly handlerId: string; + readonly prompt: string; + readonly defaultParams: Record; + readonly config: PipelineConfig; +} diff --git a/packages/vesper-ui/src/client/index.html b/packages/vesper-ui/src/client/index.html index 8829382..14a741d 100644 --- a/packages/vesper-ui/src/client/index.html +++ b/packages/vesper-ui/src/client/index.html @@ -4,324 +4,225 @@ Vesper + - - - -
Bring your own CLI — no keysOrchestrates claude / codex / opencode / gemini over a subprocess. You pay once for your CLI; Vesper adds no per-call billing and stores no LLM credentials. Pick the model per request.
Watch your agents workA pixel-art world (Vesper World) where each agent is a character — busier ones grow, the world livens up with use. Click one for a plain-language result + a big Run button. Built for someone who has never opened a terminal.
Chat with Vesper & watch it workA native dark-glass desktop app: chat with Vesper and it picks the right pipeline, runs it, and streams every step in a live activity rail. A sectioned sidebar manages pipelines, channels, schedules, runtime, permissions, and more — plus a macOS menu-bar popover.
Local-first & privateSQLite storage + OS-keychain secrets, all on your machine. The UI binds to 127.0.0.1 only. No accounts, no cloud, no telemetry.
Capability-sandboxed pipelinesEvery agent declares what it may touch — invoke a CLI, read/write storage, touch files — and the host enforces it (deny-by-default) before any side effect.
Self-improving skillsThe skill-train engine optimizes a skill's playbook against its own test set (SkillOpt-style: epochs, held-out validation, greedy accept) — using your CLI, never a provider key.