From 7f02509319d1b7635f0dfa63c5b997dcd54d6749 Mon Sep 17 00:00:00 2001 From: owen <185341197+oxwen11@users.noreply.github.com> Date: Sun, 30 Aug 2026 00:05:57 -0400 Subject: [PATCH] feat: add Cursor harness integration --- .agents/rules/architecture.md | 2 +- CLAUDE.md | 2 +- CONTEXT.md | 2 +- .../features/chat/components/harness-icon.tsx | 13 + docs/adr/0005-cursor-acp.md | 39 + packages/contract/package.json | 1 + packages/contract/src/cursor/index.ts | 10 + packages/contract/src/cursor/tools.ts | 125 ++++ packages/contract/src/cursor/ui-message.ts | 15 + packages/contract/src/domain.ts | 8 +- packages/server/src/harness/cursor/README.md | 32 + packages/server/src/harness/cursor/adapter.ts | 325 +++++++++ packages/server/src/harness/cursor/agent.ts | 677 +++++++++++++++++ .../server/src/harness/cursor/executable.ts | 69 ++ packages/server/src/harness/cursor/index.ts | 3 + .../server/src/harness/cursor/protocol.ts | 249 +++++++ packages/server/src/harness/cursor/request.ts | 68 ++ .../server/src/harness/cursor/transform.ts | 170 +++++ .../server/src/harness/cursor/transport.ts | 327 +++++++++ packages/server/src/harness/errors.ts | 23 + packages/server/src/rpc/runtime.ts | 11 +- .../test/harness/adapter-capabilities.test.ts | 10 + .../server/test/harness/cursor/agent.test.ts | 209 ++++++ .../test/harness/cursor/executable.test.ts | 78 ++ .../test/harness/cursor/transform.test.ts | 113 +++ .../test/harness/cursor/transport.test.ts | 111 +++ pnpm-lock.yaml | 681 ++++-------------- 27 files changed, 2833 insertions(+), 540 deletions(-) create mode 100644 docs/adr/0005-cursor-acp.md create mode 100644 packages/contract/src/cursor/index.ts create mode 100644 packages/contract/src/cursor/tools.ts create mode 100644 packages/contract/src/cursor/ui-message.ts create mode 100644 packages/server/src/harness/cursor/README.md create mode 100644 packages/server/src/harness/cursor/adapter.ts create mode 100644 packages/server/src/harness/cursor/agent.ts create mode 100644 packages/server/src/harness/cursor/executable.ts create mode 100644 packages/server/src/harness/cursor/index.ts create mode 100644 packages/server/src/harness/cursor/protocol.ts create mode 100644 packages/server/src/harness/cursor/request.ts create mode 100644 packages/server/src/harness/cursor/transform.ts create mode 100644 packages/server/src/harness/cursor/transport.ts create mode 100644 packages/server/test/harness/cursor/agent.test.ts create mode 100644 packages/server/test/harness/cursor/executable.test.ts create mode 100644 packages/server/test/harness/cursor/transform.test.ts create mode 100644 packages/server/test/harness/cursor/transport.test.ts diff --git a/.agents/rules/architecture.md b/.agents/rules/architecture.md index 1290aa439..30a178be8 100644 --- a/.agents/rules/architecture.md +++ b/.agents/rules/architecture.md @@ -70,7 +70,7 @@ app-server generate-ts`) and is in the lint/format ignore lists. Don't hand-edit `packages/server/src/daemon/paths.ts` names files inside a directory it is handed and deliberately has no default of its own. - `HarnessAgentIdSchema` in `packages/contract/src/domain.ts` is the whitelist: - `claude-code`, `codex`, `pi`, `grok` and nothing else. A fifth harness needs a literal + `claude-code`, `codex`, `pi`, `grok`, `cursor` and nothing else. A new harness needs a literal there, a `packages/server/src/harness//` transform, and its adapter added to the `RegistryLayer` in `packages/server/src/rpc/runtime.ts` — all three, or it is unreachable at runtime. `harness/registry.ts` is only the lookup table's diff --git a/CLAUDE.md b/CLAUDE.md index f475c5ce6..7d79d01c4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,7 @@ # vibest In-browser tooling for AI coding agents: a web chat UI over local agents -(Claude Code, Codex, pi, Grok), served by a local Node daemon and also shipped as an +(Claude Code, Codex, pi, Grok, Cursor), served by a local Node daemon and also shipped as an Electron app. pnpm + Turborepo, TypeScript everywhere. ## Commands diff --git a/CONTEXT.md b/CONTEXT.md index 522909b6b..c07fd6c1f 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -13,7 +13,7 @@ The composite identity `{ projectId, harnessAgentId, sessionId }` that every ses _Avoid_: bare sessionId as a wire identity or client-state key **Harness session id**: -The agent-native session identity (Claude session UUID, Codex thread ID, Grok ACP session id) held in the session's metadata. Internal plumbing for resume/history — never exposed as wire identity. +The agent-native session identity (Claude session UUID, Codex thread ID, Grok ACP session id, Cursor SDK `agentId`) held in the session's metadata. Internal plumbing for resume/history — never exposed as wire identity. _Avoid_: native id **Attach**: diff --git a/apps/app/src/features/chat/components/harness-icon.tsx b/apps/app/src/features/chat/components/harness-icon.tsx index e721e0414..d7a1d9df7 100644 --- a/apps/app/src/features/chat/components/harness-icon.tsx +++ b/apps/app/src/features/chat/components/harness-icon.tsx @@ -49,6 +49,19 @@ export function HarnessIcon({ ); + case "cursor": + return ( + + ); case "grok": // 2025 Grok "G" (Jon Vio / xAI). Path from Lobe Icons' grok.svg, matching // the mark on grok.com — not a made-up glyph. diff --git a/docs/adr/0005-cursor-acp.md b/docs/adr/0005-cursor-acp.md new file mode 100644 index 000000000..95804ebd5 --- /dev/null +++ b/docs/adr/0005-cursor-acp.md @@ -0,0 +1,39 @@ +# Cursor reuses the logged-in `cursor-agent` via ACP + +Cursor is a first-class harness. The Node adapter spawns `cursor-agent acp` +and reuses the subscription the user already created with `agent login`. + +## Context + +Cursor exposes three faces: the IDE agent, the `cursor-agent` CLI (ACP over +stdio, plus a one-shot `-p` mode), and `@cursor/sdk`. A `HarnessAgentRuntime` +has to prompt, interrupt, stream, and close without exiting. + +`@cursor/sdk` looks like Claude's in-process SDK, but its stored login is +only a key minted by `Cursor.auth.login()` / `CURSOR_API_KEY`. It does not +read the desktop or CLI install. Headless `cursor-agent -p` exits after one +prompt and cannot host a runtime. + +`cursor-agent acp` is the same door the user already authenticated. The +binary name must be `cursor-agent`, never `agent`: Grok's CLI ships an +`agent` that wins PATH resolution. + +## Decision + +- Wire id `cursor`. +- Spawn `cursor-agent acp`. Availability is a PATH lookup + (`VIBEST_CURSOR_EXECUTABLE`, then `cursor-agent`, then `~/.local/bin` / + `~/.cursor/bin`). +- Handshake: `initialize`, then `authenticate` with `methodId: "cursor_login"` + when advertised. +- Probe models with `cursor/list_available_models`, never `session/new`. +- End a turn when `session/prompt` returns. Cursor ACP does not send a + Grok-style `_x.ai` `turn_completed` notice. +- Map vibest `plan` / `ask` / `full` onto ACP session modes `plan` / `ask` / + `agent`. Default is `full` (`agent`). + +## Consequences + +- Users who already ran `agent login` are available with no extra key. +- Interactive per-tool approval is `session/request_permission`, same as Grok. +- A relocated CLI needs `VIBEST_CURSOR_EXECUTABLE`. diff --git a/packages/contract/package.json b/packages/contract/package.json index f9fa7cc79..3e96af211 100644 --- a/packages/contract/package.json +++ b/packages/contract/package.json @@ -14,6 +14,7 @@ "./session-events": "./src/session-events.ts", "./claude-code": "./src/claude-code/index.ts", "./grok": "./src/grok/index.ts", + "./cursor": "./src/cursor/index.ts", "./codex": "./src/codex/index.ts", "./codex/protocol": "./src/codex/protocol/index.ts", "./codex/protocol/v2": "./src/codex/protocol/v2/index.ts" diff --git a/packages/contract/src/cursor/index.ts b/packages/contract/src/cursor/index.ts new file mode 100644 index 000000000..7e1c723fd --- /dev/null +++ b/packages/contract/src/cursor/index.ts @@ -0,0 +1,10 @@ +// Browser-safe Cursor tool schemas and UI-message types. The pure UI +// vocabulary the SPA renders against; server-side transforms import from here +// too. Runtime (ACP stdio, session lifecycle) lives in @vibest/server. +export * from "./tools"; +export type { + CursorUIMessage, + CursorUIMessageChunk, + CursorMetadata, + CursorDataTypes, +} from "./ui-message"; diff --git a/packages/contract/src/cursor/tools.ts b/packages/contract/src/cursor/tools.ts new file mode 100644 index 000000000..17d946bda --- /dev/null +++ b/packages/contract/src/cursor/tools.ts @@ -0,0 +1,125 @@ +import { tool, type InferUITools, type ToolSet, type UIToolInvocation } from "ai"; +import { z } from "zod"; + +// Cursor ACP built-in tools. Keys are the wire `title` values on +// `session/update` `tool_call` frames (`Read`, `Shell`, `Write`, …). +// Input types are observed `rawInput` shapes; `z.custom` is a typed +// pass-through — the transform forwards input and output verbatim and never +// validates. +// +// MCP and unrecognized titles stay off this registry so the part is typed as +// dynamic and the UI renders them with the generic card. + +export type ReadInput = { + readonly path?: string; + readonly offset?: number; + readonly limit?: number; +}; +export type WriteInput = { + readonly path?: string; + readonly contents?: string; +}; +export type StrReplaceInput = { + readonly path?: string; + readonly old_string?: string; + readonly new_string?: string; +}; +export type DeleteInput = { + readonly path?: string; +}; +export type ShellInput = { + readonly command?: string; + readonly working_directory?: string; + readonly block_until_ms?: number; +}; +export type GrepInput = { + readonly pattern?: string; + readonly path?: string; + readonly glob?: string; +}; +export type GlobInput = { + readonly glob_pattern?: string; + readonly target_directory?: string; +}; +export type WebSearchInput = { + readonly search_term?: string; + readonly explanation?: string; +}; +export type WebFetchInput = { + readonly url?: string; +}; +export type TaskInput = { + readonly description?: string; + readonly prompt?: string; +}; + +export const Read = tool({ + inputSchema: z.custom(), + outputSchema: z.unknown(), +}); +export const Write = tool({ + inputSchema: z.custom(), + outputSchema: z.unknown(), +}); +export const StrReplace = tool({ + inputSchema: z.custom(), + outputSchema: z.unknown(), +}); +export const Delete = tool({ + inputSchema: z.custom(), + outputSchema: z.unknown(), +}); +export const Shell = tool({ + inputSchema: z.custom(), + outputSchema: z.unknown(), +}); +export const Grep = tool({ + inputSchema: z.custom(), + outputSchema: z.unknown(), +}); +export const Glob = tool({ + inputSchema: z.custom(), + outputSchema: z.unknown(), +}); +export const WebSearch = tool({ + inputSchema: z.custom(), + outputSchema: z.unknown(), +}); +export const WebFetch = tool({ + inputSchema: z.custom(), + outputSchema: z.unknown(), +}); +export const Task = tool({ + inputSchema: z.custom(), + outputSchema: z.unknown(), +}); + +export const cursorTools = { + Read, + Write, + StrReplace, + Delete, + Shell, + Grep, + Glob, + WebSearch, + WebFetch, + Task, +} satisfies ToolSet; + +export type CursorTools = InferUITools; + +export type ReadUIToolInvocation = UIToolInvocation; +export type WriteUIToolInvocation = UIToolInvocation; +export type StrReplaceUIToolInvocation = UIToolInvocation; +export type DeleteUIToolInvocation = UIToolInvocation; +export type ShellUIToolInvocation = UIToolInvocation; +export type GrepUIToolInvocation = UIToolInvocation; +export type GlobUIToolInvocation = UIToolInvocation; +export type WebSearchUIToolInvocation = UIToolInvocation; +export type WebFetchUIToolInvocation = UIToolInvocation; +export type TaskUIToolInvocation = UIToolInvocation; + +export function isCursorTool(toolName: string): toolName is keyof typeof cursorTools { + return toolName in cursorTools; +} diff --git a/packages/contract/src/cursor/ui-message.ts b/packages/contract/src/cursor/ui-message.ts new file mode 100644 index 000000000..2619ab207 --- /dev/null +++ b/packages/contract/src/cursor/ui-message.ts @@ -0,0 +1,15 @@ +import type { InferUIMessageChunk, UIMessage } from "ai"; + +import type { CursorTools } from "./tools"; + +export type CursorMetadata = { + /** Cursor ACP session id from `session/new`. */ + sessionId: string; +}; + +// No `data-*` parts — ACP session updates that are not text/reasoning/tools +// stay off the chunk track until a data part earns its keep. +export type CursorDataTypes = Record; + +export type CursorUIMessage = UIMessage; +export type CursorUIMessageChunk = InferUIMessageChunk; diff --git a/packages/contract/src/domain.ts b/packages/contract/src/domain.ts index 031d20128..09c6a0bb9 100644 --- a/packages/contract/src/domain.ts +++ b/packages/contract/src/domain.ts @@ -4,7 +4,13 @@ import { Schema } from "effect"; export const toStandardSchema = >(schema: S) => Schema.toStandardJSONSchemaV1(Schema.toStandardSchemaV1(schema)); -export const HarnessAgentIdSchema = Schema.Literals(["claude-code", "codex", "pi", "grok"]); +export const HarnessAgentIdSchema = Schema.Literals([ + "claude-code", + "codex", + "pi", + "grok", + "cursor", +]); export type HarnessAgentId = typeof HarnessAgentIdSchema.Type; // Derived from the schema rather than written out a second time: clients that // need the ids as data (narrowing a URL param, say) would otherwise keep their diff --git a/packages/server/src/harness/cursor/README.md b/packages/server/src/harness/cursor/README.md new file mode 100644 index 000000000..c2ba00a43 --- /dev/null +++ b/packages/server/src/harness/cursor/README.md @@ -0,0 +1,32 @@ +# Cursor harness (`cursor-agent acp`) + +Verified against `cursor-agent` 2026.08.25. The adapter speaks +[ACP](https://agentclientprotocol.com) over: + +``` +cursor-agent acp +``` + +That is the same logged-in CLI the user already ran `agent login` against. +Do not spawn `agent` — Grok's CLI also ships an `agent` binary, and it wins +PATH resolution. Do not use `@cursor/sdk`: its stored login is a separately +minted API key, not the desktop / CLI subscription. + +Do not substitute `cursor-agent -p` (headless). That process exits when the +prompt ends and cannot host a `HarnessAgentRuntime`. + +Handshake is `initialize` then `authenticate` with `methodId: "cursor_login"` +when that method is advertised. Models come from `cursor/list_available_models`. +A turn ends when `session/prompt` returns — Cursor ACP has no `_x.ai` +`turn_completed` notice. + +Availability is a PATH lookup (`cursor-agent`, then `~/.local/bin` / +`~/.cursor/bin`), overridable with `VIBEST_CURSOR_EXECUTABLE`. + +Tests speak a fake ACP child over stdio. There is no live-CLI smoke: a real +turn spends tokens and is not a regression gate. + +Regenerate nothing — protocol types in `protocol.ts` are hand-written around +the ACP methods and Cursor extensions we consume. After bumping the CLI, +re-probe `initialize`, `authenticate`, `cursor/list_available_models`, +`session/new`, and `session/request_permission` before widening the types. diff --git a/packages/server/src/harness/cursor/adapter.ts b/packages/server/src/harness/cursor/adapter.ts new file mode 100644 index 000000000..5077df4d4 --- /dev/null +++ b/packages/server/src/harness/cursor/adapter.ts @@ -0,0 +1,325 @@ +import type { ModelInfo, PermissionMode } from "@vibest/contract"; +import { Effect, Queue, Ref, Scope, Stream } from "effect"; +import type * as Cause from "effect/Cause"; + +import { + type HarnessAgentAdapter, + type HarnessAgentRuntime, + type SessionInfoResult, + type UserInput, +} from "../adapter"; +import { + AgentOpenError, + AgentOperationError, + AgentRequestUnavailable, + CapabilityProbeFailed, + SessionClosed, + SessionNotResumable, + TurnAlreadyRunning, +} from "../errors"; +import type { SessionEnvelopeDraft } from "../events/framework"; +import { streamFromQueueOne } from "../queue-stream"; +import type { CursorAgent } from "./agent"; +import { checkCursorAvailability } from "./executable"; +import type { ModelInfoNative } from "./protocol"; + +const EVENT_QUEUE_CAPACITY = 1024; + +const operationError = (sessionId: string, operation: string, cause: unknown) => + new AgentOperationError({ sessionId, operation, cause }); + +const toPromptText = (input: UserInput): string => + input.parts + .map((part) => + part.type === "text" + ? part.text + : `i am current inspect target: ${part.data + .map((target) => `@${target.file}:${target.line}:${target.column}`) + .join(", ")}`, + ) + .join("\n"); + +// Cursor ACP session modes are `plan` / `ask` / `agent`. `full` is vibest's +// name for unrestricted agent mode. +const CURSOR_PERMISSION_MODES = { + plan: "plan", + ask: "ask", + full: "agent", +} as const satisfies Partial>; +const CURSOR_PERMISSION_MODE_IDS = Object.keys( + CURSOR_PERMISSION_MODES, +) as ReadonlyArray; +const toCursorPermissionMode = (mode: PermissionMode): string | undefined => + CURSOR_PERMISSION_MODES[mode as keyof typeof CURSOR_PERMISSION_MODES]; + +const toModelInfo = (model: ModelInfoNative): ModelInfo => ({ + id: model.modelId, + ...(model.name !== undefined ? { label: model.name } : {}), + ...(model.reasoningEfforts !== undefined ? { reasoningEfforts: model.reasoningEfforts } : {}), + ...(model.defaultReasoningEffort !== undefined + ? { defaultReasoningEffort: model.defaultReasoningEffort } + : {}), +}); + +const makeRuntime = ( + agent: CursorAgent, + sessionId: string, +): Effect.Effect => + Effect.gen(function* () { + const scope = yield* Scope.Scope; + const events = yield* Queue.bounded( + EVENT_QUEUE_CAPACITY, + ); + const cursor = yield* Ref.make(0); + const closed = yield* Ref.make(false); + const activeTurn = yield* Ref.make(undefined); + + const emit = (body: SessionEnvelopeDraft["body"]) => + Queue.offer(events, { harnessAgentId: "cursor", sessionId, body }).pipe( + Effect.flatMap((accepted) => + accepted + ? Ref.update(cursor, (current) => current + 1) + : Effect.fail( + operationError(sessionId, "publish-event", new Error("Event queue closed")), + ), + ), + ); + + const crash = (cause: unknown) => + Ref.getAndSet(closed, true).pipe( + Effect.flatMap((alreadyClosed) => + alreadyClosed + ? Effect.void + : Ref.getAndSet(activeTurn, undefined).pipe( + Effect.flatMap((turnId) => + emit({ type: "session.crashed", sessionId, reason: String(cause) }).pipe( + Effect.andThen( + turnId + ? emit({ + type: "session.turn.ended", + sessionId, + turnId, + outcome: "failed", + error: { message: String(cause), category: "unknown" }, + }) + : Effect.void, + ), + ), + ), + Effect.catch(() => Effect.void), + Effect.andThen( + agent.session.abort(sessionId).pipe(Effect.catch(() => Effect.void)), + ), + Effect.andThen(Queue.end(events)), + Effect.asVoid, + ), + ), + ); + + const close = Ref.getAndSet(closed, true).pipe( + Effect.flatMap((alreadyClosed) => + alreadyClosed + ? Effect.void + : Ref.getAndSet(activeTurn, undefined).pipe( + Effect.flatMap((turnId) => + turnId + ? emit({ + type: "session.turn.ended", + sessionId, + turnId, + outcome: "canceled", + }) + : Effect.void, + ), + Effect.catch(() => Effect.void), + Effect.andThen(agent.session.abort(sessionId).pipe(Effect.catch(() => Effect.void))), + Effect.andThen(Queue.end(events)), + Effect.asVoid, + ), + ), + ); + + const interrupt: HarnessAgentRuntime["interrupt"] = Effect.gen(function* () { + if (yield* Ref.get(closed)) return yield* new SessionClosed({ sessionId }); + yield* agent.session + .interrupt(sessionId) + .pipe(Effect.mapError((cause) => operationError(sessionId, "interrupt", cause))); + }); + + yield* Scope.addFinalizer(scope, close); + yield* agent.session.awaitTermination(sessionId).pipe( + Effect.catch((cause) => crash(cause)), + Effect.forkIn(scope), + ); + yield* Stream.runForEach(agent.session.requestPermission(sessionId), (request) => + emit({ type: "session.request.asked", sessionId, request }), + ).pipe(Effect.catch(crash), Effect.forkIn(scope)); + + return { + sessionId, + harnessAgentId: "cursor", + events: streamFromQueueOne(events), + prompt: (input) => + Effect.gen(function* () { + if (yield* Ref.get(closed)) return yield* new SessionClosed({ sessionId }); + const prompt = yield* agent.session + .prompt({ sessionId, text: toPromptText(input) }) + .pipe( + Effect.mapError((cause) => + cause instanceof TurnAlreadyRunning + ? cause + : operationError(sessionId, "prompt", cause), + ), + ); + const receipt = { + turnId: prompt.turnId, + cursor: yield* Ref.get(cursor), + started: prompt.started, + }; + if (prompt.started) { + yield* Ref.set(activeTurn, prompt.turnId); + yield* emit({ type: "session.turn.started", sessionId, turnId: prompt.turnId }); + } + + const finished = yield* Ref.make(false); + const pump = Stream.runForEach(prompt.output, (chunk) => + emit(chunk).pipe( + Effect.andThen( + chunk.type === "finish" + ? prompt.completion.pipe( + Effect.flatMap((completion) => + emit({ + type: "session.turn.ended", + sessionId, + turnId: prompt.turnId, + outcome: completion.outcome, + ...(completion.usage ? { usage: completion.usage } : {}), + }), + ), + Effect.andThen(Ref.set(finished, true)), + Effect.andThen( + Ref.update(activeTurn, (current) => + current === prompt.turnId ? undefined : current, + ), + ), + ) + : Effect.void, + ), + ), + ).pipe( + Effect.flatMap(() => Ref.get(finished)), + Effect.flatMap((didFinish) => + prompt.started && !didFinish + ? crash(new Error("Cursor turn ended without a finish event")) + : Effect.void, + ), + Effect.catch(crash), + ); + yield* Effect.forkIn(pump, scope); + return receipt; + }), + steer: (_expectedTurnId, _input) => + Effect.gen(function* () { + if (yield* Ref.get(closed)) return yield* new SessionClosed({ sessionId }); + return yield* Effect.fail( + operationError(sessionId, "steer", new Error("steering is not supported by Cursor")), + ); + }), + setModel: (model) => + Effect.gen(function* () { + if (yield* Ref.get(closed)) return yield* new SessionClosed({ sessionId }); + yield* agent.session + .setModel(sessionId, model) + .pipe(Effect.mapError((cause) => operationError(sessionId, "set-model", cause))); + }), + setReasoningEffort: (effort) => + Effect.gen(function* () { + if (yield* Ref.get(closed)) return yield* new SessionClosed({ sessionId }); + yield* agent.session + .setReasoningEffort(sessionId, effort) + .pipe( + Effect.mapError((cause) => operationError(sessionId, "set-reasoning-effort", cause)), + ); + }), + setPermissionMode: (mode) => + Effect.gen(function* () { + if (yield* Ref.get(closed)) return yield* new SessionClosed({ sessionId }); + const native = toCursorPermissionMode(mode); + if (!native) { + return yield* Effect.fail( + operationError( + sessionId, + "set-permission-mode", + new Error(`unknown permission mode: ${mode}`), + ), + ); + } + yield* agent.session + .setPermissionMode(sessionId, native) + .pipe( + Effect.mapError((cause) => operationError(sessionId, "set-permission-mode", cause)), + ); + }), + interrupt, + respondToAgentRequest: (requestId, response) => + agent.session.respondPermission(sessionId, requestId, response).pipe( + Effect.mapError((cause) => + cause instanceof AgentRequestUnavailable + ? cause + : operationError(sessionId, "respond-to-request", cause), + ), + Effect.andThen(emit({ type: "session.request.replied", sessionId, requestId })), + ), + getCapabilities: Effect.succeed({ + supportsResume: true, + supportsSteering: false, + supportsPermissions: true, + }), + close, + } satisfies HarnessAgentRuntime; + }); + +export const makeCursorAdapter = ( + agent: CursorAgent, + options: { readonly executablePath?: string } = {}, +): HarnessAgentAdapter => ({ + id: "cursor", + descriptor: { id: "cursor", name: "Cursor" }, + permissionModes: CURSOR_PERMISSION_MODE_IDS, + defaultPermissionMode: "full", + probeModels: (_cwd) => + agent.listModels.pipe( + Effect.map((result) => (result.availableModels ?? []).map(toModelInfo)), + Effect.mapError((cause) => new CapabilityProbeFailed({ harnessAgentId: "cursor", cause })), + ), + checkAvailability: checkCursorAvailability( + options.executablePath + ? { env: { ...process.env, VIBEST_CURSOR_EXECUTABLE: options.executablePath } } + : {}, + ), + open: (input) => + agent.session.create({ cwd: input.cwd }).pipe( + Effect.mapError((cause) => new AgentOpenError({ harnessAgentId: "cursor", cause })), + Effect.flatMap(({ sessionId }) => makeRuntime(agent, sessionId)), + ), + resume: (input) => { + const cwd = input.cwd; + if (cwd === undefined) { + return Effect.fail( + new AgentOpenError({ + harnessAgentId: "cursor", + cause: new Error("resume requires cwd"), + }), + ); + } + return agent.session.resume({ sessionId: input.sessionId, cwd }).pipe( + Effect.mapError((cause) => + cause instanceof SessionNotResumable + ? cause + : new AgentOpenError({ harnessAgentId: "cursor", cause }), + ), + Effect.flatMap(({ sessionId }) => makeRuntime(agent, sessionId)), + ); + }, + getSessionInfo: () => Effect.succeed({ _tag: "unsupported" }), +}); diff --git a/packages/server/src/harness/cursor/agent.ts b/packages/server/src/harness/cursor/agent.ts new file mode 100644 index 000000000..20e84b33d --- /dev/null +++ b/packages/server/src/harness/cursor/agent.ts @@ -0,0 +1,677 @@ +import type { AgentRequest, AgentResponse, ReasoningEffort, TokenUsage } from "@vibest/contract"; +import type { CursorUIMessageChunk } from "@vibest/contract/cursor"; +import { Deferred, Effect, Exit, FileSystem, Queue, Ref, Scope, Stream } from "effect"; +import type * as Cause from "effect/Cause"; +import type * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; +import { v7 as uuid } from "uuid"; + +import { + AgentOperationError, + AgentRequestUnavailable, + CursorRpcError, + CursorTransportError, + HarnessSessionNotFound, + SessionNotResumable, + TurnAlreadyRunning, +} from "../errors"; +import { drainQueue, streamFromQueueOne } from "../queue-stream"; +import { cursorNotFoundMessage, resolveCursorExecutable } from "./executable"; +import { + AUTH_METHOD_ID, + TURN_END_METHOD, + hasCursorLogin, + initializeParams, + isCancelledStopReason, + isRequestPermission, + promptResultOf, + sessionIdOf, + unwrapModels, + type ModelsListResult, + type RpcNotification, + type RpcServerRequest, +} from "./protocol"; +import { + buildPermissionRequest, + cancelledPermissionResult, + mapPermissionResponse, +} from "./request"; +import { createCursorTransform } from "./transform"; +import { + makeCursorTransport, + type CursorTransport, + type CursorTransportFailure, +} from "./transport"; + +const SESSION_QUEUE_CAPACITY = 1024; +const HANDSHAKE_TIMEOUT = "30 seconds"; + +type PendingRequest = { + readonly deferred: Deferred.Deferred; + readonly declineValue: unknown; + readonly settle: (response: AgentResponse) => unknown; +}; + +type CursorTurnState = + | { readonly _tag: "Idle" } + | { + readonly _tag: "Active"; + readonly turnId: string; + readonly ended: Deferred.Deferred; + }; + +type TurnEnd = { + readonly outcome: "completed" | "canceled"; + readonly usage?: TokenUsage; +}; + +export type CursorSessionFailure = CursorTransportFailure | AgentOperationError; + +type SessionState = { + readonly sessionId: string; + readonly scope: Scope.Closeable; + readonly transport: CursorTransport; + readonly termination: Deferred.Deferred; + readonly chunks: Queue.Queue; + readonly requests: Queue.Queue; + readonly pending: Ref.Ref>; + readonly turnState: Ref.Ref; + readonly turnEnd: Ref.Ref; + readonly transform: ReturnType; +}; + +export interface CursorAgentOptions { + readonly executablePath?: string; + readonly args?: ReadonlyArray; +} + +export interface CursorAgentDependencies { + readonly makeTransport: (config: { + readonly cwd?: string; + }) => Effect.Effect; +} + +export interface CursorAgent { + readonly listModels: Effect.Effect; + readonly session: { + readonly create: (config: { + readonly cwd: string; + }) => Effect.Effect<{ readonly sessionId: string }, CursorTransportFailure>; + readonly resume: (config: { + readonly sessionId: string; + readonly cwd: string; + }) => Effect.Effect< + { readonly sessionId: string }, + CursorTransportFailure | SessionNotResumable + >; + readonly prompt: (input: { + readonly sessionId: string; + readonly text: string; + }) => Effect.Effect< + { + readonly turnId: string; + readonly started: boolean; + readonly output: Stream.Stream; + readonly completion: Effect.Effect; + }, + HarnessSessionNotFound | CursorTransportFailure | AgentOperationError | TurnAlreadyRunning + >; + readonly setModel: ( + sessionId: string, + model: string, + ) => Effect.Effect; + readonly setReasoningEffort: ( + sessionId: string, + effort: ReasoningEffort, + ) => Effect.Effect; + readonly setPermissionMode: ( + sessionId: string, + modeId: string, + ) => Effect.Effect; + readonly requestPermission: ( + sessionId: string, + ) => Stream.Stream; + readonly awaitTermination: ( + sessionId: string, + ) => Effect.Effect; + readonly respondPermission: ( + sessionId: string, + requestId: string, + response: AgentResponse, + ) => Effect.Effect; + readonly interrupt: (sessionId: string) => Effect.Effect; + readonly abort: (sessionId: string) => Effect.Effect; + }; +} + +/** @internal */ +export const makeCursorAgentWithDependencies = ( + dependencies: CursorAgentDependencies, +): Effect.Effect => + Effect.gen(function* () { + const ownerScope = yield* Scope.Scope; + const buildContext = yield* Effect.context(); + const sessions = yield* Ref.make(new Map()); + + const getSession = (sessionId: string): Effect.Effect => + Ref.get(sessions).pipe( + Effect.flatMap((current) => { + const session = current.get(sessionId); + return session + ? Effect.succeed(session) + : Effect.fail(new HarnessSessionNotFound({ sessionId })); + }), + ); + + const unregister = (session: SessionState) => + Ref.modify(sessions, (current) => { + if (current.get(session.sessionId) !== session) return [false, current] as const; + const next = new Map(current); + next.delete(session.sessionId); + return [true, next] as const; + }); + + const settlePending = (session: SessionState) => + Ref.getAndSet(session.pending, new Map()).pipe( + Effect.flatMap((pending) => + Effect.forEach( + pending.values(), + (request) => Deferred.succeed(request.deferred, request.declineValue), + { discard: true }, + ), + ), + ); + + const completeTurn = (session: SessionState) => + Ref.getAndSet(session.turnState, { _tag: "Idle" }).pipe( + Effect.flatMap((turn) => + turn._tag === "Idle" + ? Effect.void + : Deferred.succeed(turn.ended, undefined).pipe(Effect.asVoid), + ), + ); + + const overflowError = (session: SessionState) => + new AgentOperationError({ + sessionId: session.sessionId, + operation: "event-queue-overflow", + cause: new Error("Cursor session event queue overflowed"), + }); + + const closeScope = (session: SessionState) => + Effect.forkIn(Scope.close(session.scope, Exit.void), ownerScope).pipe(Effect.asVoid); + + const evictOverflowedSession = (session: SessionState) => { + const error = overflowError(session); + return unregister(session).pipe( + Effect.andThen(Deferred.fail(session.termination, error)), + Effect.andThen(settlePending(session)), + Effect.andThen(completeTurn(session)), + Effect.andThen(Queue.end(session.requests)), + Effect.andThen(Queue.fail(session.chunks, error)), + Effect.andThen(closeScope(session)), + Effect.asVoid, + ); + }; + + const crashSession = (session: SessionState, failure: CursorSessionFailure) => + unregister(session).pipe( + Effect.flatMap((removed) => { + if (!removed) return Effect.void; + return Deferred.fail(session.termination, failure).pipe( + Effect.andThen(settlePending(session)), + Effect.andThen(completeTurn(session)), + Effect.andThen(Queue.end(session.requests)), + Effect.andThen( + Queue.offer(session.chunks, { type: "error", errorText: failure.message }), + ), + Effect.flatMap((accepted) => + accepted + ? Queue.end(session.chunks).pipe(Effect.asVoid) + : Queue.fail(session.chunks, overflowError(session)).pipe(Effect.asVoid), + ), + Effect.andThen(closeScope(session)), + ); + }), + ); + + const reportCrash = (session: SessionState, failure: CursorSessionFailure) => + Effect.forkIn(crashSession(session, failure), ownerScope).pipe(Effect.asVoid); + + const offerChunk = (session: SessionState, chunk: CursorUIMessageChunk) => + Queue.offer(session.chunks, chunk).pipe( + Effect.flatMap((accepted) => (accepted ? Effect.void : evictOverflowedSession(session))), + ); + + const settleTurn = (session: SessionState, turnId: string) => + Ref.modify | undefined>( + session.turnState, + (current) => { + if (current._tag !== "Active" || current.turnId !== turnId) { + return [undefined, current] as const; + } + return [current.ended, { _tag: "Idle" } as const] as const; + }, + ).pipe( + Effect.flatMap((ended) => + ended ? Deferred.succeed(ended, undefined).pipe(Effect.asVoid) : Effect.void, + ), + ); + + const routeNotification = ( + session: SessionState, + notification: RpcNotification, + ): Effect.Effect => + Effect.gen(function* () { + for (const chunk of session.transform.apply(notification)) { + if (chunk.type === "finish") { + const ended = yield* Ref.modify | undefined>( + session.turnState, + (current) => { + if (current._tag !== "Active") return [undefined, current] as const; + return [current.ended, { _tag: "Idle" } as const] as const; + }, + ); + if (!ended) continue; + yield* Deferred.succeed(ended, undefined); + } + yield* offerChunk(session, chunk); + } + }); + + const handleServerRequest = ( + session: SessionState, + request: RpcServerRequest, + ): Effect.Effect => { + if (!isRequestPermission(request)) { + return session.transport + .respondError(request.id, { + code: -32601, + message: `Unsupported ACP request '${request.method}'`, + }) + .pipe(Effect.catch(() => Effect.void)); + } + const agentRequest = buildPermissionRequest(request); + return Effect.gen(function* () { + const deferred = yield* Deferred.make(); + yield* Ref.update(session.pending, (current) => + new Map(current).set(agentRequest.id, { + deferred, + settle: mapPermissionResponse, + declineValue: cancelledPermissionResult, + }), + ); + const accepted = yield* Queue.offer(session.requests, agentRequest); + if (!accepted) { + yield* Ref.update(session.pending, (current) => { + const next = new Map(current); + next.delete(agentRequest.id); + return next; + }); + yield* session.transport + .respond(request.id, cancelledPermissionResult) + .pipe(Effect.catch(() => Effect.void)); + return; + } + const result = yield* Deferred.await(deferred).pipe( + Effect.onInterrupt(() => + Ref.update(session.pending, (current) => { + const next = new Map(current); + next.delete(agentRequest.id); + return next; + }), + ), + ); + yield* session.transport.respond(request.id, result).pipe(Effect.catch(() => Effect.void)); + }); + }; + + const handshake = (transport: CursorTransport): Effect.Effect => + transport.request("initialize", initializeParams).pipe( + Effect.timeoutOrElse({ + duration: HANDSHAKE_TIMEOUT, + orElse: () => + Effect.fail( + new CursorTransportError({ + operation: "handshake-timeout", + cause: new Error("Cursor ACP initialize timed out"), + }), + ), + }), + Effect.flatMap((result) => + hasCursorLogin(result) + ? transport.request("authenticate", { methodId: AUTH_METHOD_ID }).pipe(Effect.asVoid) + : Effect.void, + ), + ); + + const registerSession = ( + sessionId: string, + transport: CursorTransport, + scope: Scope.Closeable, + ): Effect.Effect => + Effect.gen(function* () { + const session: SessionState = { + sessionId, + scope, + transport, + termination: yield* Deferred.make(), + chunks: yield* Queue.dropping( + SESSION_QUEUE_CAPACITY, + ), + requests: yield* Queue.bounded(SESSION_QUEUE_CAPACITY), + pending: yield* Ref.make>(new Map()), + turnState: yield* Ref.make({ _tag: "Idle" }), + turnEnd: yield* Ref.make(undefined), + transform: createCursorTransform(sessionId), + }; + yield* Ref.update(sessions, (current) => new Map(current).set(sessionId, session)); + yield* Stream.runForEach(transport.notifications, (notification) => + routeNotification(session, notification), + ).pipe( + Effect.catch((error) => reportCrash(session, error)), + Effect.forkIn(scope), + ); + yield* Stream.runForEach(transport.serverRequests, (request) => + Effect.forkIn(handleServerRequest(session, request), scope).pipe(Effect.asVoid), + ).pipe( + Effect.catch((error) => reportCrash(session, error)), + Effect.forkIn(scope), + ); + yield* transport.awaitTermination.pipe( + Effect.catch((error) => reportCrash(session, error)), + Effect.forkIn(scope), + ); + return session; + }); + + const openSession = (config: { + readonly cwd: string; + readonly resumeId?: string; + }): Effect.Effect< + { readonly sessionId: string }, + CursorTransportFailure | SessionNotResumable + > => + Effect.gen(function* () { + const scope = yield* Scope.fork(ownerScope, "sequential"); + return yield* Effect.gen(function* () { + const transport = yield* dependencies + .makeTransport({ cwd: config.cwd }) + .pipe(Effect.provideService(Scope.Scope, scope), Effect.provideContext(buildContext)); + yield* handshake(transport); + const opened = config.resumeId + ? yield* transport + .request("session/load", { + sessionId: config.resumeId, + cwd: config.cwd, + mcpServers: [], + }) + .pipe( + Effect.mapError((cause) => + cause instanceof CursorRpcError + ? new SessionNotResumable({ + sessionId: config.resumeId!, + reason: cause.message, + }) + : cause, + ), + ) + : yield* transport.request("session/new", { cwd: config.cwd, mcpServers: [] }); + const sessionId = config.resumeId ?? sessionIdOf(opened); + if (sessionId === undefined) { + return yield* new CursorTransportError({ + operation: "session/new", + cause: new Error("ACP session/new did not return a sessionId"), + }); + } + yield* registerSession(sessionId, transport, scope); + return { sessionId }; + }).pipe( + Effect.mapError((error) => { + if (error instanceof SessionNotResumable) return error; + if ( + error instanceof CursorTransportError || + error instanceof CursorRpcError || + (typeof error === "object" && + error !== null && + "_tag" in error && + (error._tag === "AgentProcessExited" || error._tag === "AgentProtocolError")) + ) { + return error as CursorTransportFailure; + } + return new CursorTransportError({ operation: "open-session", cause: error }); + }), + Effect.onError(() => Scope.close(scope, Exit.void)), + ); + }); + + const interrupt = (sessionId: string): Effect.Effect => + Effect.gen(function* () { + const session = yield* getSession(sessionId); + const turn = yield* Ref.get(session.turnState); + if (turn._tag !== "Active") return; + yield* session.transport + .notify("session/cancel", { sessionId }) + .pipe(Effect.catch(() => Effect.void)); + }); + + const abort = (sessionId: string): Effect.Effect => + getSession(sessionId).pipe( + Effect.flatMap((session) => + unregister(session).pipe( + Effect.andThen( + session.transport + .request("session/close", { sessionId }) + .pipe(Effect.catch(() => Effect.void)), + ), + Effect.andThen(settlePending(session)), + Effect.andThen(completeTurn(session)), + Effect.andThen(Queue.end(session.requests)), + Effect.andThen(Queue.end(session.chunks)), + Effect.andThen(closeScope(session)), + Effect.asVoid, + ), + ), + ); + + const injectFinish = (session: SessionState, turnId: string) => + Ref.modify | undefined>( + session.turnState, + (current) => { + if (current._tag !== "Active" || current.turnId !== turnId) { + return [undefined, current] as const; + } + return [current.ended, { _tag: "Idle" } as const] as const; + }, + ).pipe( + Effect.flatMap((ended) => + ended + ? Ref.update( + session.turnEnd, + (current) => current ?? { outcome: "completed" as const }, + ).pipe( + Effect.andThen( + Effect.forEach(session.transform.endTurn(), (chunk) => + offerChunk(session, chunk), + ), + ), + Effect.andThen(Deferred.succeed(ended, undefined)), + ) + : Effect.void, + ), + ); + + return { + listModels: Effect.gen(function* () { + const scope = yield* Scope.fork(ownerScope, "sequential"); + return yield* Effect.gen(function* () { + const transport = yield* dependencies + .makeTransport({}) + .pipe(Effect.provideService(Scope.Scope, scope), Effect.provideContext(buildContext)); + yield* handshake(transport); + return unwrapModels(yield* transport.request("cursor/list_available_models", {})); + }).pipe(Effect.ensuring(Scope.close(scope, Exit.void))); + }), + session: { + create: (config) => + openSession({ cwd: config.cwd }).pipe( + Effect.catchTag("SessionNotResumable", (error) => + Effect.fail(new CursorTransportError({ operation: "session/new", cause: error })), + ), + ), + resume: (config) => openSession({ cwd: config.cwd, resumeId: config.sessionId }), + prompt: (input) => + Effect.uninterruptibleMask((restore) => + Effect.gen(function* () { + const session = yield* getSession(input.sessionId); + const turnId = uuid(); + const ended = yield* Deferred.make(); + const taken = yield* Ref.modify( + session.turnState, + (current) => { + if (current._tag !== "Idle") return [false, current] as const; + return [true, { _tag: "Active", turnId, ended }] as const; + }, + ); + if (!taken) { + return yield* new TurnAlreadyRunning({ sessionId: input.sessionId, turnId }); + } + + yield* Ref.set(session.turnEnd, undefined); + yield* drainQueue(session.chunks); + yield* Effect.forkIn( + session.transport + .request("session/prompt", { + sessionId: input.sessionId, + prompt: [{ type: "text", text: input.text }], + }) + .pipe( + Effect.flatMap((result) => { + const { stopReason } = promptResultOf(result); + return Ref.set(session.turnEnd, { + outcome: isCancelledStopReason(stopReason) + ? ("canceled" as const) + : ("completed" as const), + }).pipe( + Effect.andThen( + session.transport.enqueueNotification({ + method: TURN_END_METHOD, + params: { sessionId: input.sessionId }, + }), + ), + ); + }), + Effect.catch((error) => + offerChunk(session, { type: "error", errorText: error.message }).pipe( + Effect.andThen(injectFinish(session, turnId)), + ), + ), + ), + session.scope, + ); + + const abandonTurn = settleTurn(session, turnId); + + return { + turnId, + started: true, + output: streamFromQueueOne(session.chunks).pipe( + Stream.takeUntil((chunk) => chunk.type === "finish"), + Stream.ensuring(abandonTurn), + ), + completion: Deferred.await(ended).pipe( + Effect.andThen(Ref.get(session.turnEnd)), + Effect.map((end) => end ?? { outcome: "completed" as const }), + ), + }; + }).pipe( + Effect.onInterrupt(() => + restore(interrupt(input.sessionId)).pipe(Effect.catch(() => Effect.void)), + ), + ), + ), + setModel: (sessionId, model) => + getSession(sessionId).pipe( + Effect.flatMap((session) => + session.transport + .request("session/set_model", { sessionId, modelId: model }) + .pipe(Effect.asVoid), + ), + ), + setReasoningEffort: (sessionId, effort) => + getSession(sessionId).pipe( + Effect.flatMap((session) => + session.transport + .request("session/set_config_option", { + sessionId, + configId: "effort", + value: effort, + }) + .pipe(Effect.asVoid), + ), + ), + setPermissionMode: (sessionId, modeId) => + getSession(sessionId).pipe( + Effect.flatMap((session) => + session.transport + .request("session/set_mode", { sessionId, modeId }) + .pipe(Effect.asVoid), + ), + ), + requestPermission: (sessionId) => + Stream.unwrap( + getSession(sessionId).pipe( + Effect.map((session) => streamFromQueueOne(session.requests)), + ), + ), + awaitTermination: (sessionId) => + getSession(sessionId).pipe( + Effect.flatMap((session) => Deferred.await(session.termination)), + ), + respondPermission: (sessionId, requestId, response) => + Effect.gen(function* () { + const session = yield* getSession(sessionId); + const pending = yield* Ref.modify(session.pending, (current) => { + const request = current.get(requestId); + if (!request) return [undefined, current] as const; + const next = new Map(current); + next.delete(requestId); + return [request, next] as const; + }); + if (!pending) { + return yield* new AgentRequestUnavailable({ sessionId, requestId }); + } + yield* Deferred.succeed(pending.deferred, pending.settle(response)); + return true; + }), + interrupt, + abort, + }, + } satisfies CursorAgent; + }); + +export const makeCursorAgent = ( + options: CursorAgentOptions = {}, +): Effect.Effect< + CursorAgent, + never, + ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Scope.Scope +> => + makeCursorAgentWithDependencies({ + makeTransport: (config) => + Effect.gen(function* () { + const executablePath = options.executablePath ?? (yield* resolveCursorExecutable()); + if (!executablePath) { + return yield* Effect.fail( + new CursorTransportError({ + operation: "resolve", + cause: new Error(cursorNotFoundMessage), + }), + ); + } + return yield* makeCursorTransport({ + executablePath, + ...(options.args ? { args: options.args } : {}), + ...(config.cwd ? { cwd: config.cwd } : {}), + }); + }), + }); diff --git a/packages/server/src/harness/cursor/executable.ts b/packages/server/src/harness/cursor/executable.ts new file mode 100644 index 000000000..e73e36ad9 --- /dev/null +++ b/packages/server/src/harness/cursor/executable.ts @@ -0,0 +1,69 @@ +import os from "node:os"; +import path from "node:path"; + +import { Effect, FileSystem } from "effect"; + +import { findExecutable, isExecutableFile } from "../executable"; + +/** + * Where the Cursor CLI installer puts `cursor-agent`. Availability searches + * these in addition to PATH because a desktop host's GUI PATH is often bare; + * spawn uses the resolved absolute path, so the two cannot drift. + * + * Never fall back to `agent`: Grok's CLI ships an `agent` binary that wins + * PATH resolution and is the wrong process. + */ +function extraInstallDirs(home: string): string[] { + return [path.join(home, ".local", "bin"), path.join(home, ".cursor", "bin")]; +} + +export type ResolveCursorDeps = { + env?: NodeJS.ProcessEnv; + home?: string; + platform?: NodeJS.Platform; +}; + +export const cursorNotFoundMessage = + "Cursor Agent was not found. Install the Cursor CLI from https://cursor.com/cli, run `agent login`, or set VIBEST_CURSOR_EXECUTABLE to the `cursor-agent` binary."; + +/** + * The `cursor-agent` binary this adapter should exec, or `undefined` when it + * is not installed. `VIBEST_CURSOR_EXECUTABLE` is an explicit override (tests, + * a relocated install) and is taken as-is when it is executable. + */ +export const resolveCursorExecutable = ( + deps: ResolveCursorDeps = {}, +): Effect.Effect => + Effect.gen(function* () { + const { env = process.env, home = os.homedir(), platform = process.platform } = deps; + const override = env["VIBEST_CURSOR_EXECUTABLE"]; + if (override) { + return yield* findExecutable(override, { env, platform }); + } + + const fromPath = yield* findExecutable("cursor-agent", { env, platform }); + if (fromPath) return fromPath; + + const fs = yield* FileSystem.FileSystem; + const binary = platform === "win32" ? "cursor-agent.exe" : "cursor-agent"; + for (const dir of extraInstallDirs(home)) { + const candidate = path.join(dir, binary); + if (yield* isExecutableFile(fs, candidate, platform)) return candidate; + } + return undefined; + }); + +export const checkCursorAvailability = ( + deps: ResolveCursorDeps = {}, +): Effect.Effect< + { readonly available: true } | { readonly available: false; readonly reason: string }, + never, + FileSystem.FileSystem +> => + resolveCursorExecutable(deps).pipe( + Effect.map((found) => + found + ? { available: true as const } + : { available: false as const, reason: cursorNotFoundMessage }, + ), + ); diff --git a/packages/server/src/harness/cursor/index.ts b/packages/server/src/harness/cursor/index.ts new file mode 100644 index 000000000..bc460f4cb --- /dev/null +++ b/packages/server/src/harness/cursor/index.ts @@ -0,0 +1,3 @@ +export * from "./adapter"; +export * from "./agent"; +export * from "./transport"; diff --git a/packages/server/src/harness/cursor/protocol.ts b/packages/server/src/harness/cursor/protocol.ts new file mode 100644 index 000000000..5192e8453 --- /dev/null +++ b/packages/server/src/harness/cursor/protocol.ts @@ -0,0 +1,249 @@ +import type { ReasoningEffort } from "@vibest/contract"; +import { Schema } from "effect"; + +// ACP JSON-RPC 2.0 frames over `cursor-agent acp`. Types are local: the ACP +// TypeScript SDK would spawn with `node:child_process`. Treat unknown +// `sessionUpdate` values as skippable — the wire is non-exhaustive across +// cursor-agent releases. + +const RpcErrorBody = Schema.Struct({ + code: Schema.Number, + message: Schema.String, + data: Schema.optionalKey(Schema.Unknown), +}); + +export const RpcFrame = Schema.Union([ + Schema.Struct({ id: Schema.Number, result: Schema.Unknown }), + Schema.Struct({ id: Schema.Number, error: RpcErrorBody }), + Schema.Struct({ + method: Schema.String, + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Number])), + params: Schema.optionalKey(Schema.Unknown), + }), +]); + +export const isRpcFrame = Schema.is(RpcFrame); + +export type RpcNotification = { + readonly method: string; + readonly params?: unknown; +}; + +export type RpcServerRequest = { + readonly method: string; + readonly id: string | number; + readonly params?: unknown; +}; + +export type AcpContent = { + readonly type: string; + readonly text?: string; +}; + +export type AcpSessionUpdate = { + readonly sessionUpdate: string; + readonly content?: AcpContent; + readonly toolCallId?: string; + readonly title?: string; + readonly kind?: string; + readonly status?: string; + readonly rawInput?: unknown; + readonly rawOutput?: unknown; + readonly _meta?: unknown; +}; + +export type SessionUpdateNotification = { + readonly method: "session/update"; + readonly params: { + readonly sessionId: string; + readonly update: AcpSessionUpdate; + }; +}; + +export type PermissionOption = { + readonly optionId: string; + readonly name?: string; + readonly kind?: string; +}; + +export type RequestPermissionParams = { + readonly sessionId: string; + readonly toolCall?: { + readonly toolCallId?: string; + readonly title?: string; + readonly kind?: string; + readonly rawInput?: unknown; + readonly _meta?: unknown; + }; + readonly options?: ReadonlyArray; +}; + +export type ModelInfoNative = { + readonly modelId: string; + readonly name?: string; + readonly reasoningEfforts?: ReadonlyArray; + readonly defaultReasoningEffort?: ReasoningEffort; +}; + +export type ModelsListResult = { + readonly availableModels?: ReadonlyArray; +}; + +export const CLIENT_INFO = { name: "vibest", title: "Vibest", version: "0.0.0" } as const; +export const AUTH_METHOD_ID = "cursor_login" as const; + +export const initializeParams = { + protocolVersion: 1, + clientInfo: CLIENT_INFO, + clientCapabilities: { + _meta: { parameterizedModelPicker: true }, + }, +} as const; + +export function isSessionUpdate( + notification: RpcNotification, +): notification is SessionUpdateNotification { + return notification.method === "session/update" && notification.params !== undefined; +} + +export function isRequestPermission( + request: RpcServerRequest, +): request is RpcServerRequest & { readonly params: RequestPermissionParams } { + return request.method === "session/request_permission"; +} + +export function toolNameOf(update: { + readonly title?: string; + readonly kind?: string; + readonly _meta?: unknown; +}): string { + if (typeof update.title === "string" && update.title.length > 0) return update.title; + const meta = update["_meta"]; + if (typeof meta === "object" && meta !== null) { + const name = (meta as { name?: unknown }).name; + if (typeof name === "string" && name.length > 0) return name; + } + if (typeof update.kind === "string" && update.kind.length > 0) return update.kind; + return "tool"; +} + +const REASONING_EFFORTS = new Set([ + "minimal", + "low", + "medium", + "high", + "xhigh", + "max", +]); + +const asReasoningEffort = (value: string | undefined): ReasoningEffort | undefined => { + if (value === undefined) return undefined; + const normalized = value.trim().toLowerCase(); + if (normalized === "extra-high" || normalized === "extra high") return "xhigh"; + return REASONING_EFFORTS.has(normalized as ReasoningEffort) + ? (normalized as ReasoningEffort) + : undefined; +}; + +const ConfigOptionSchema = Schema.Struct({ + id: Schema.String, + currentValue: Schema.optionalKey(Schema.String), + options: Schema.optionalKey( + Schema.Array(Schema.Struct({ value: Schema.String, name: Schema.optionalKey(Schema.String) })), + ), +}); + +const effortFromConfig = ( + options: unknown, +): Pick => { + if (!Array.isArray(options)) return {}; + for (const entry of options) { + try { + const option = Schema.decodeUnknownSync(ConfigOptionSchema)(entry); + if (option.id.trim().toLowerCase() !== "effort") continue; + const reasoningEfforts = (option.options ?? []) + .map((item) => asReasoningEffort(item.value)) + .filter((value): value is ReasoningEffort => value !== undefined); + const defaultReasoningEffort = asReasoningEffort(option.currentValue); + return { + ...(reasoningEfforts.length > 0 ? { reasoningEfforts } : {}), + ...(defaultReasoningEffort !== undefined ? { defaultReasoningEffort } : {}), + }; + } catch { + // Skip a malformed option rather than dropping the model. + } + } + return {}; +}; + +const ModelsListResultSchema = Schema.Struct({ + models: Schema.optionalKey( + Schema.Array( + Schema.Struct({ + value: Schema.String, + name: Schema.optionalKey(Schema.String), + configOptions: Schema.optionalKey(Schema.Array(Schema.Unknown)), + }), + ), + ), +}); + +export const unwrapModels = (value: unknown): ModelsListResult => { + try { + const parsed = Schema.decodeUnknownSync(ModelsListResultSchema)(value); + return { + availableModels: (parsed.models ?? []).map((model) => ({ + modelId: model.value, + ...(model.name !== undefined ? { name: model.name } : {}), + ...effortFromConfig(model.configOptions), + })), + }; + } catch { + return {}; + } +}; + +const OpenedSessionSchema = Schema.Struct({ sessionId: Schema.NonEmptyString }); + +export const sessionIdOf = (value: unknown): string | undefined => { + try { + return Schema.decodeUnknownSync(OpenedSessionSchema)(value).sessionId; + } catch { + return undefined; + } +}; + +const InitializeResultSchema = Schema.Struct({ + authMethods: Schema.optionalKey(Schema.Array(Schema.Struct({ id: Schema.String }))), +}); + +export const hasCursorLogin = (value: unknown): boolean => { + try { + return (Schema.decodeUnknownSync(InitializeResultSchema)(value).authMethods ?? []).some( + (method) => method.id === AUTH_METHOD_ID, + ); + } catch { + return false; + } +}; + +const PromptResultSchema = Schema.Struct({ stopReason: Schema.optionalKey(Schema.String) }); + +export const promptResultOf = (value: unknown): { readonly stopReason?: string } => { + try { + return Schema.decodeUnknownSync(PromptResultSchema)(value); + } catch { + return {}; + } +}; + +export const isCancelledStopReason = (stopReason: string | undefined): boolean => + stopReason === "cancelled" || stopReason === "canceled"; + +/** Injected by the agent after `session/prompt` returns, onto the same + * notification queue as `session/update`, so finish cannot overtake deltas. */ +export const TURN_END_METHOD = "vibest/turn_end" as const; + +export function isTurnEnd(notification: RpcNotification): boolean { + return notification.method === TURN_END_METHOD; +} diff --git a/packages/server/src/harness/cursor/request.ts b/packages/server/src/harness/cursor/request.ts new file mode 100644 index 000000000..e7a71ea5b --- /dev/null +++ b/packages/server/src/harness/cursor/request.ts @@ -0,0 +1,68 @@ +import type { AgentRequest, AgentRequestAction, AgentResponse } from "@vibest/contract"; + +import { + toolNameOf as toolNameFromUpdate, + type PermissionOption, + type RequestPermissionParams, + type RpcServerRequest, +} from "./protocol"; + +const FALLBACK_ACTIONS: AgentRequestAction[] = [ + { id: "allow-once", label: "Allow", behavior: "allow", variant: "primary" }, + { id: "reject-once", label: "Deny", behavior: "deny" }, +]; + +const kindBehavior = (kind: string | undefined): AgentRequestAction["behavior"] => + kind?.startsWith("reject") || kind?.startsWith("deny") ? "deny" : "allow"; + +const toAction = (option: PermissionOption): AgentRequestAction => ({ + id: option.optionId, + label: option.name ?? option.optionId, + behavior: kindBehavior(option.kind), + ...(option.kind?.includes("always") || option.kind?.includes("session") + ? { grant: { type: "session" as const } } + : {}), +}); + +export function buildPermissionRequest(request: RpcServerRequest): AgentRequest { + const params = (request.params ?? {}) as RequestPermissionParams; + const options = params.options ?? []; + const actions = options.length > 0 ? options.map(toAction) : FALLBACK_ACTIONS; + return { + type: "tool", + id: String(request.id), + harnessAgentId: "cursor", + toolName: toolNameFromUpdate(params.toolCall ?? {}), + input: + typeof params.toolCall?.rawInput === "object" && params.toolCall.rawInput !== null + ? (params.toolCall.rawInput as Record) + : {}, + actions, + ...(params.toolCall?.title ? { title: params.toolCall.title } : {}), + native: params, + }; +} + +export function mapPermissionResponse(response: AgentResponse): unknown { + if (response.type === "question") { + return { outcome: { outcome: "cancelled" } }; + } + if (response.behavior === "deny") { + const optionId = response.type === "tool" ? response.selectedActionId : undefined; + return { + outcome: { + outcome: "selected", + optionId: optionId ?? "reject-once", + }, + }; + } + const optionId = response.type === "tool" ? response.selectedActionId : undefined; + return { + outcome: { + outcome: "selected", + optionId: optionId ?? "allow-once", + }, + }; +} + +export const cancelledPermissionResult = { outcome: { outcome: "cancelled" } } as const; diff --git a/packages/server/src/harness/cursor/transform.ts b/packages/server/src/harness/cursor/transform.ts new file mode 100644 index 000000000..7d1763dfa --- /dev/null +++ b/packages/server/src/harness/cursor/transform.ts @@ -0,0 +1,170 @@ +import { isCursorTool, type CursorUIMessageChunk } from "@vibest/contract/cursor"; +import { v7 as uuid } from "uuid"; + +import { + isSessionUpdate, + isTurnEnd, + toolNameOf, + type AcpSessionUpdate, + type RpcNotification, +} from "./protocol"; + +const TEXT_ID = "text"; +const REASONING_ID = "reasoning"; + +const isDynamicTool = (toolName: string): boolean => !isCursorTool(toolName); + +type SeenTool = { + readonly toolName: string; + readonly dynamic: boolean; +}; + +export type CursorTransform = { + readonly apply: (notification: RpcNotification) => Generator; + readonly endTurn: () => Generator; +}; + +// ACP session/update → UI-chunk transform. One factory per session; the +// returned generators hold open text/reasoning blocks so deltas and the +// turn-end close agree. Cursor ACP has no Grok-style `_x.ai` turn_completed +// notice — the agent injects `endTurn` when `session/prompt` returns. + +export function createCursorTransform(sessionId: string): CursorTransform { + let turnOpen = false; + let textOpen = false; + let reasoningOpen = false; + const seenTools = new Map(); + + function* ensureTurn(): Generator { + if (turnOpen) return; + turnOpen = true; + yield { type: "start", messageId: uuid(), messageMetadata: { sessionId } }; + } + + function* closeBlocks(): Generator { + if (textOpen) { + textOpen = false; + yield { type: "text-end", id: TEXT_ID }; + } + if (reasoningOpen) { + reasoningOpen = false; + yield { type: "reasoning-end", id: REASONING_ID }; + } + } + + function* onUpdate(update: AcpSessionUpdate): Generator { + switch (update.sessionUpdate) { + case "agent_message_chunk": { + const delta = update.content?.text; + if (typeof delta !== "string" || delta.length === 0) return; + yield* ensureTurn(); + if (reasoningOpen) { + reasoningOpen = false; + yield { type: "reasoning-end", id: REASONING_ID }; + } + if (!textOpen) { + textOpen = true; + yield { type: "text-start", id: TEXT_ID }; + } + yield { type: "text-delta", id: TEXT_ID, delta }; + return; + } + case "agent_thought_chunk": { + const delta = update.content?.text; + if (typeof delta !== "string" || delta.length === 0) return; + yield* ensureTurn(); + if (!reasoningOpen) { + reasoningOpen = true; + yield { type: "reasoning-start", id: REASONING_ID }; + } + yield { type: "reasoning-delta", id: REASONING_ID, delta }; + return; + } + case "tool_call": { + const toolCallId = update.toolCallId; + if (typeof toolCallId !== "string") return; + yield* ensureTurn(); + if (seenTools.has(toolCallId)) return; + const toolName = toolNameOf(update); + const dynamic = isDynamicTool(toolName); + seenTools.set(toolCallId, { toolName, dynamic }); + yield { + type: "tool-input-available", + toolCallId, + toolName, + input: update.rawInput ?? {}, + providerExecuted: true, + dynamic, + }; + return; + } + case "tool_call_update": { + const toolCallId = update.toolCallId; + if (typeof toolCallId !== "string") return; + yield* ensureTurn(); + let seen = seenTools.get(toolCallId); + if (!seen && update.rawInput !== undefined) { + const toolName = toolNameOf(update); + seen = { toolName, dynamic: isDynamicTool(toolName) }; + seenTools.set(toolCallId, seen); + yield { + type: "tool-input-available", + toolCallId, + toolName, + input: update.rawInput, + providerExecuted: true, + dynamic: seen.dynamic, + }; + } + const dynamic = seen?.dynamic ?? true; + if (update.status === "failed") { + yield { + type: "tool-output-error", + toolCallId, + errorText: typeof update.rawOutput === "string" ? update.rawOutput : "tool failed", + dynamic, + }; + return; + } + if (update.status === "completed" || update.rawOutput !== undefined) { + yield { + type: "tool-output-available", + toolCallId, + output: update.rawOutput ?? {}, + providerExecuted: true, + dynamic, + }; + } + return; + } + default: + return; + } + } + + function* closeAndFinish(): Generator { + yield* closeBlocks(); + seenTools.clear(); + if (turnOpen) { + turnOpen = false; + yield { type: "finish" }; + return; + } + yield { type: "finish" }; + } + + return { + *apply(notification: RpcNotification): Generator { + if (isTurnEnd(notification)) { + yield* closeAndFinish(); + return; + } + if (isSessionUpdate(notification) && notification.params.update) { + yield* onUpdate(notification.params.update); + } + }, + *endTurn(): Generator { + yield* closeAndFinish(); + }, + }; +} diff --git a/packages/server/src/harness/cursor/transport.ts b/packages/server/src/harness/cursor/transport.ts new file mode 100644 index 000000000..07553935e --- /dev/null +++ b/packages/server/src/harness/cursor/transport.ts @@ -0,0 +1,327 @@ +import { Deferred, type Duration, Effect, Queue, Ref, Stream, type Scope } from "effect"; +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; + +import { + AgentProcessExited, + AgentProtocolError, + CursorRpcError, + CursorTransportError, +} from "../errors"; +import { isRpcFrame, type RpcNotification, type RpcServerRequest } from "./protocol"; + +const DEFAULT_QUEUE_CAPACITY = 256; +const DEFAULT_FORCE_KILL_AFTER = "2 seconds"; + +export type CursorTransportFailure = + | CursorTransportError + | CursorRpcError + | AgentProcessExited + | AgentProtocolError; + +export interface CursorTransportOptions { + readonly executablePath: string; + readonly cwd?: string; + readonly args?: ReadonlyArray; + readonly queueCapacity?: number; + readonly forceKillAfter?: Duration.Input; + readonly env?: NodeJS.ProcessEnv; +} + +export interface CursorTransport { + readonly request: ( + method: string, + params?: unknown, + ) => Effect.Effect; + readonly notify: (method: string, params?: unknown) => Effect.Effect; + readonly notifications: Stream.Stream; + readonly serverRequests: Stream.Stream; + readonly respond: ( + id: string | number, + result: unknown, + ) => Effect.Effect; + readonly respondError: ( + id: string | number, + error: { readonly code: number; readonly message: string; readonly data?: unknown }, + ) => Effect.Effect; + readonly enqueueNotification: ( + notification: RpcNotification, + ) => Effect.Effect; + readonly isTerminated: Effect.Effect; + readonly awaitTermination: Effect.Effect; +} + +type PendingRequest = { + readonly method: string; + readonly deferred: Deferred.Deferred; +}; + +type RequestState = + | { + readonly _tag: "Running"; + readonly pending: ReadonlyMap; + } + | { + readonly _tag: "Terminated"; + readonly failure: CursorTransportFailure; + }; + +const transportError = (operation: string, cause: unknown) => + new CursorTransportError({ operation, cause }); + +const normalizeFailure = (operation: string, error: unknown): CursorTransportFailure => { + if (typeof error === "object" && error !== null && "_tag" in error) { + switch (error._tag) { + case "CursorTransportError": + case "CursorRpcError": + case "AgentProcessExited": + case "AgentProtocolError": + return error as CursorTransportFailure; + } + } + return transportError(operation, error); +}; + +export const makeCursorTransport = ( + options: CursorTransportOptions, +): Effect.Effect< + CursorTransport, + CursorTransportError, + ChildProcessSpawner.ChildProcessSpawner | Scope.Scope +> => + Effect.gen(function* () { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const queueCapacity = options.queueCapacity ?? DEFAULT_QUEUE_CAPACITY; + const env = { + ...process.env, + ...options.env, + }; + const child = yield* spawner + .spawn( + ChildProcess.make(options.executablePath, ["acp", ...(options.args ?? [])], { + ...(options.cwd ? { cwd: options.cwd } : {}), + env, + forceKillAfter: options.forceKillAfter ?? DEFAULT_FORCE_KILL_AFTER, + }), + ) + .pipe(Effect.mapError((cause) => transportError("spawn", cause))); + + const outgoing = yield* Queue.bounded(queueCapacity); + const notifications = yield* Queue.bounded( + queueCapacity, + ); + const serverRequests = yield* Queue.bounded( + queueCapacity, + ); + const requestState = yield* Ref.make({ + _tag: "Running", + pending: new Map(), + }); + const nextRequestId = yield* Ref.make(1); + const termination = yield* Deferred.make(); + const stderrTail = yield* Ref.make(""); + const stderrDrained = yield* Deferred.make(); + const encoder = new TextEncoder(); + + const terminate = (error: CursorTransportFailure) => + Ref.modify(requestState, (current) => { + if (current._tag === "Terminated") return [undefined, current] as const; + return [ + Array.from(current.pending.values()), + { _tag: "Terminated", failure: error } as const, + ] as const; + }).pipe( + Effect.flatMap((requests) => { + if (!requests) return Effect.void; + return Effect.forEach(requests, ({ deferred }) => Deferred.fail(deferred, error), { + discard: true, + }).pipe( + Effect.andThen(Deferred.fail(termination, error)), + Effect.andThen(Queue.fail(notifications, error)), + Effect.andThen(Queue.fail(serverRequests, error)), + Effect.andThen(Queue.shutdown(outgoing)), + Effect.asVoid, + ); + }), + ); + + const offerOutgoing = ( + frame: Record, + ): Effect.Effect => + Effect.gen(function* () { + const encoded = yield* Effect.try({ + try: () => `${JSON.stringify(frame)}\n`, + catch: (cause) => transportError("encode-frame", cause), + }); + const accepted = yield* Queue.offer(outgoing, encoded); + if (!accepted) { + return yield* transportError("write-closed", new Error("Cursor transport is closed")); + } + }); + + const removePending = (id: number) => + Ref.update(requestState, (current) => { + if (current._tag === "Terminated" || !current.pending.has(id)) return current; + const pending = new Map(current.pending); + pending.delete(id); + return { _tag: "Running", pending } as const; + }); + + const resolvePending = ( + id: number, + complete: (request: PendingRequest) => Effect.Effect, + ) => + Ref.modify(requestState, (current) => { + if (current._tag === "Terminated") return [Effect.void, current] as const; + const request = current.pending.get(id); + if (!request) return [Effect.void, current] as const; + const pending = new Map(current.pending); + pending.delete(id); + return [complete(request), { _tag: "Running", pending } as const] as const; + }).pipe(Effect.flatten); + + const handleFrame = (line: string): Effect.Effect => + Effect.gen(function* () { + if (line.trim().length === 0) return; + let decoded: unknown; + try { + decoded = JSON.parse(line) as unknown; + } catch { + // cursor-agent may print a human banner on stdout before the JSON-RPC loop. + return; + } + if (!isRpcFrame(decoded)) { + // Banners, log lines, and notices that aren't a JSON-RPC frame: skip. + // Killing the child here would turn a protocol extension into a session crash. + return; + } + + if ("method" in decoded) { + if (decoded.id === undefined) { + yield* Queue.offer(notifications, decoded as RpcNotification); + } else { + yield* Queue.offer(serverRequests, decoded as RpcServerRequest); + } + return; + } + + const id = typeof decoded.id === "number" ? decoded.id : Number(decoded.id); + if (!Number.isFinite(id)) return; + + if ("error" in decoded) { + yield* resolvePending(id, ({ deferred, method }) => + Deferred.fail( + deferred, + new CursorRpcError({ + method, + code: decoded.error.code, + errorMessage: decoded.error.message, + ...(decoded.error.data !== undefined ? { data: decoded.error.data } : {}), + }), + ), + ); + return; + } + + yield* resolvePending(id, ({ deferred }) => Deferred.succeed(deferred, decoded.result)); + }); + + yield* Stream.fromQueue(outgoing).pipe( + Stream.map((frame) => encoder.encode(frame)), + Stream.run(child.stdin), + Effect.catch((cause) => terminate(transportError("write-stdin", cause))), + Effect.forkScoped, + ); + + yield* child.stdout.pipe( + Stream.decodeText(), + Stream.splitLines, + Stream.runForEach(handleFrame), + Effect.catch((error) => terminate(normalizeFailure("read-stdout", error))), + Effect.forkScoped, + ); + + yield* child.stderr.pipe( + Stream.decodeText(), + Stream.runForEach((chunk) => + Ref.update(stderrTail, (current) => (current + chunk).slice(-8192)), + ), + Effect.catch((error) => terminate(transportError("read-stderr", error))), + Effect.ensuring(Deferred.succeed(stderrDrained, undefined)), + Effect.forkScoped, + ); + + yield* child.exitCode.pipe( + Effect.flatMap((code) => + Deferred.await(stderrDrained).pipe( + Effect.timeoutOrElse({ duration: "250 millis", orElse: () => Effect.void }), + Effect.andThen(Ref.get(stderrTail)), + Effect.flatMap((tail) => + terminate( + new AgentProcessExited({ + harnessAgentId: "cursor", + code, + ...(tail.length > 0 ? { stderrTail: tail } : {}), + }), + ), + ), + ), + ), + Effect.catch((cause) => terminate(transportError("read-exit-code", cause))), + Effect.forkScoped, + ); + + yield* Effect.addFinalizer(() => + terminate(transportError("shutdown", new Error("Cursor transport scope closed"))).pipe( + Effect.andThen(Queue.shutdown(notifications)), + Effect.andThen(Queue.shutdown(serverRequests)), + Effect.asVoid, + ), + ); + + const request: CursorTransport["request"] = (method: string, params?: unknown) => + Effect.gen(function* () { + const id = yield* Ref.getAndUpdate(nextRequestId, (current) => current + 1); + const deferred = yield* Deferred.make(); + const terminalFailure = yield* Ref.modify(requestState, (current) => { + if (current._tag === "Terminated") return [current.failure, current] as const; + const pending = new Map(current.pending).set(id, { method, deferred }); + return [undefined, { _tag: "Running", pending } as const] as const; + }); + if (terminalFailure) return yield* terminalFailure; + + return (yield* offerOutgoing({ + jsonrpc: "2.0", + method, + id, + ...(params !== undefined ? { params } : {}), + }).pipe( + Effect.tapError(() => removePending(id)), + Effect.andThen(Deferred.await(deferred)), + Effect.onInterrupt(() => removePending(id)), + )) as A; + }); + + return { + request, + notify: (method, params) => + offerOutgoing({ + jsonrpc: "2.0", + method, + ...(params !== undefined ? { params } : {}), + }), + notifications: Stream.fromQueue(notifications), + serverRequests: Stream.fromQueue(serverRequests), + respond: (id, result) => offerOutgoing({ jsonrpc: "2.0", id, result }), + respondError: (id, error) => offerOutgoing({ jsonrpc: "2.0", id, error }), + enqueueNotification: (notification) => + Queue.offer(notifications, notification).pipe( + Effect.flatMap((accepted) => + accepted + ? Effect.void + : transportError("write-closed", new Error("Cursor transport is closed")), + ), + ), + isTerminated: Ref.get(requestState).pipe(Effect.map((state) => state._tag === "Terminated")), + awaitTermination: Deferred.await(termination), + } satisfies CursorTransport; + }); diff --git a/packages/server/src/harness/errors.ts b/packages/server/src/harness/errors.ts index 79fc2ed4e..df69eb5cc 100644 --- a/packages/server/src/harness/errors.ts +++ b/packages/server/src/harness/errors.ts @@ -213,6 +213,29 @@ export class GrokRpcError extends Schema.TaggedError()("GrokRpcErr } } +export class CursorTransportError extends Schema.TaggedError()( + "CursorTransportError", + { + operation: Schema.String, + cause: Schema.Defect(), + }, +) { + override get message() { + return `Cursor transport operation '${this.operation}' failed: ${causeSummary(this.cause)}`; + } +} + +export class CursorRpcError extends Schema.TaggedError()("CursorRpcError", { + method: Schema.String, + code: Schema.Number, + errorMessage: Schema.String, + data: Schema.optionalKey(Schema.Unknown), +}) { + override get message() { + return `Cursor RPC '${this.method}' failed (${this.code}): ${this.errorMessage}`; + } +} + export class AgentProcessExited extends Schema.TaggedError()( "AgentProcessExited", { diff --git a/packages/server/src/rpc/runtime.ts b/packages/server/src/rpc/runtime.ts index ec7057a6e..aa2a5c384 100644 --- a/packages/server/src/rpc/runtime.ts +++ b/packages/server/src/rpc/runtime.ts @@ -23,6 +23,7 @@ import { type ClaudeCodeAgent, } from "../harness/claude-code"; import { makeCodexAdapter, makeCodexAgent, type CodexAgent } from "../harness/codex"; +import { makeCursorAdapter, makeCursorAgent, type CursorAgent } from "../harness/cursor"; import { makeGrokAdapter, makeGrokAgent, type GrokAgent } from "../harness/grok"; import { makePiAdapter, makePiAgent, type PiAgent } from "../harness/pi"; import { SessionRecoveryStoreLayer } from "../harness/session-recovery"; @@ -33,6 +34,7 @@ export class ClaudeCode extends Context.Service()(" export class Codex extends Context.Service()("Codex") {} export class Pi extends Context.Service()("Pi") {} export class Grok extends Context.Service()("Grok") {} +export class Cursor extends Context.Service()("Cursor") {} /** * The Node platform services. Every effect that touches disk, paths, or random @@ -61,7 +63,12 @@ export const GrokLayer: Layer.Layer = Layer.effect(Grok, makeGrokAgent()). Layer.provide(PlatformLayer), ); -const ProvidersLayer = Layer.mergeAll(ClaudeCodeLayer, CodexLayer, PiLayer, GrokLayer); +export const CursorLayer: Layer.Layer = Layer.effect(Cursor, makeCursorAgent()).pipe( + Layer.provide(NodeProcessLayer), + Layer.provide(PlatformLayer), +); + +const ProvidersLayer = Layer.mergeAll(ClaudeCodeLayer, CodexLayer, PiLayer, GrokLayer, CursorLayer); /** * Which CLI is installed, and whether it is new enough, is fixed for the life @@ -98,12 +105,14 @@ const RegistryLayer = Layer.effect( const codex = yield* Codex; const pi = yield* Pi; const grok = yield* Grok; + const cursor = yield* Cursor; const adapters = yield* Effect.forEach( [ makeClaudeCodeAdapter(claudeCode), makeCodexAdapter(codex), makePiAdapter(pi), makeGrokAdapter(grok), + makeCursorAdapter(cursor), ], cacheAvailability, ); diff --git a/packages/server/test/harness/adapter-capabilities.test.ts b/packages/server/test/harness/adapter-capabilities.test.ts index 15527dca3..535506006 100644 --- a/packages/server/test/harness/adapter-capabilities.test.ts +++ b/packages/server/test/harness/adapter-capabilities.test.ts @@ -2,6 +2,7 @@ import { expect, it } from "vitest"; import { makeClaudeCodeAdapter, type ClaudeCodeAgent } from "../../src/harness/claude-code"; import { makeCodexAdapter, type CodexAgent } from "../../src/harness/codex"; +import { makeCursorAdapter, type CursorAgent } from "../../src/harness/cursor"; import { makeGrokAdapter, type GrokAgent } from "../../src/harness/grok"; import { makePiAdapter, type PiAgent } from "../../src/harness/pi"; @@ -39,10 +40,18 @@ it("grok declares ask/full, defaulting to ask", () => { expect(adapter.defaultPermissionMode).toBe("ask"); }); +it("cursor declares plan/ask/full, defaulting to full", () => { + const adapter = makeCursorAdapter(stub()); + + expect(adapter.permissionModes).toEqual(["plan", "ask", "full"]); + expect(adapter.defaultPermissionMode).toBe("full"); +}); + it("only the harnesses with a model catalogue declare a probe", () => { expect(makeClaudeCodeAdapter(stub()).probeModels).toBeDefined(); expect(makeCodexAdapter(stub()).probeModels).toBeDefined(); expect(makeGrokAdapter(stub()).probeModels).toBeDefined(); + expect(makeCursorAdapter(stub()).probeModels).toBeDefined(); // Absent, not empty: pi has no model switch, so the client renders no picker. expect(makePiAdapter(stub()).probeModels).toBeUndefined(); }); @@ -53,4 +62,5 @@ it("declaring an adapter never touches its agent", () => { expect(() => makeClaudeCodeAdapter(stub())).not.toThrow(); expect(() => makeCodexAdapter(stub())).not.toThrow(); expect(() => makeGrokAdapter(stub())).not.toThrow(); + expect(() => makeCursorAdapter(stub())).not.toThrow(); }); diff --git a/packages/server/test/harness/cursor/agent.test.ts b/packages/server/test/harness/cursor/agent.test.ts new file mode 100644 index 000000000..91c2e641b --- /dev/null +++ b/packages/server/test/harness/cursor/agent.test.ts @@ -0,0 +1,209 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { layer } from "@effect/vitest"; +import { Effect, Fiber, Stream } from "effect"; + +import { makeCursorAdapter } from "../../../src/harness/cursor/adapter"; +import { makeCursorAgent } from "../../../src/harness/cursor/agent"; + +const FAKE = `#!/usr/bin/env node +const readline = require("node:readline"); +const rl = readline.createInterface({ input: process.stdin }); +const send = (frame) => process.stdout.write(JSON.stringify(frame) + "\\n"); +let pendingPrompt = null; +const completePrompt = (sessionId) => { + if (pendingPrompt === null) return; + send({ + jsonrpc: "2.0", + method: "session/update", + params: { + sessionId, + update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text: "pong" } }, + }, + }); + send({ jsonrpc: "2.0", id: pendingPrompt, result: { stopReason: "end_turn" } }); + pendingPrompt = null; +}; +rl.on("line", (line) => { + const msg = JSON.parse(line); + const reply = (result) => send({ jsonrpc: "2.0", id: msg.id, result }); + const fail = (message) => send({ jsonrpc: "2.0", id: msg.id, error: { code: -32000, message } }); + if (msg.method === "initialize") { + reply({ + protocolVersion: 1, + authMethods: [{ id: "cursor_login" }], + agentCapabilities: { loadSession: true }, + }); + return; + } + if (msg.method === "authenticate") { + if (msg.params.methodId !== "cursor_login") return fail("unknown auth method"); + reply({}); + return; + } + if (msg.method === "cursor/list_available_models") { + reply({ + models: [ + { + value: "composer-2", + name: "Composer 2", + configOptions: [ + { + id: "effort", + currentValue: "high", + options: [{ value: "low" }, { value: "high" }], + }, + ], + }, + ], + }); + return; + } + if (msg.method === "session/new") { + reply({ sessionId: "sess-new" }); + return; + } + if (msg.method === "session/load") { + if (msg.params.sessionId === "missing") return fail("session not found"); + reply({ sessionId: msg.params.sessionId }); + return; + } + if ( + msg.method === "session/set_model" || + msg.method === "session/set_mode" || + msg.method === "session/set_config_option" || + msg.method === "session/close" + ) { + reply({}); + return; + } + if (msg.method === "session/prompt") { + const text = msg.params.prompt[0].text; + if (text === "ask") { + pendingPrompt = msg.id; + send({ + jsonrpc: "2.0", + method: "session/request_permission", + id: 900, + params: { + sessionId: msg.params.sessionId, + toolCall: { toolCallId: "c1", title: "Shell", rawInput: { command: "ls" } }, + options: [ + { optionId: "allow-once", name: "Allow", kind: "allow_once" }, + { optionId: "reject-once", name: "Deny", kind: "reject_once" }, + ], + }, + }); + return; + } + send({ + jsonrpc: "2.0", + method: "session/update", + params: { + sessionId: msg.params.sessionId, + update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text: "pong" } }, + }, + }); + reply({ stopReason: "end_turn" }); + return; + } + if (msg.id === 900) completePrompt("sess-new"); +}); +`; + +function makeFake(): string { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fake-cursor-agent-")); + const file = path.join(dir, "fake-cursor-agent.js"); + fs.writeFileSync(file, FAKE); + fs.chmodSync(file, 0o755); + return file; +} + +layer(NodeServices.layer)("CursorAgent", (it) => { + it.effect("creates a session and streams a full turn", () => + Effect.gen(function* () { + const agent = yield* makeCursorAgent({ executablePath: makeFake() }); + const { sessionId } = yield* agent.session.create({ cwd: "/tmp" }); + assert.equal(sessionId, "sess-new"); + + const prompt = yield* agent.session.prompt({ sessionId, text: "ping" }); + assert.equal(prompt.started, true); + const chunks = yield* Stream.runCollect(prompt.output); + assert.deepEqual( + Array.from(chunks, (chunk) => chunk.type), + ["start", "text-start", "text-delta", "text-end", "finish"], + ); + yield* agent.session.abort(sessionId); + }), + ); + + it.effect("resume keeps the caller-provided session id", () => + Effect.gen(function* () { + const agent = yield* makeCursorAgent({ executablePath: makeFake() }); + const { sessionId } = yield* agent.session.resume({ + sessionId: "custom-id", + cwd: "/tmp", + }); + assert.equal(sessionId, "custom-id"); + yield* agent.session.abort(sessionId); + }), + ); + + it.effect("fails to resume a missing session", () => + Effect.gen(function* () { + const agent = yield* makeCursorAgent({ executablePath: makeFake() }); + const error = yield* agent.session + .resume({ sessionId: "missing", cwd: "/tmp" }) + .pipe(Effect.flip); + assert.equal(error._tag, "SessionNotResumable"); + }), + ); + + it.effect("round-trips a permission request", () => + Effect.gen(function* () { + const agent = yield* makeCursorAgent({ executablePath: makeFake() }); + const { sessionId } = yield* agent.session.create({ cwd: "/tmp" }); + const requestFiber = yield* Stream.runHead(agent.session.requestPermission(sessionId)).pipe( + Effect.forkChild, + ); + const prompt = yield* agent.session.prompt({ sessionId, text: "ask" }); + const collected = yield* Effect.forkChild(Stream.runCollect(prompt.output)); + + const request = yield* Fiber.join(requestFiber); + assert.equal(request._tag, "Some"); + if (request._tag !== "Some") return; + assert.equal(request.value.type, "tool"); + assert.equal(request.value.harnessAgentId, "cursor"); + yield* agent.session.respondPermission(sessionId, request.value.id, { + type: "tool", + behavior: "allow", + selectedActionId: "allow-once", + }); + yield* Fiber.join(collected); + yield* agent.session.abort(sessionId); + }), + ); + + it.effect("lists models without opening a session", () => + Effect.gen(function* () { + const agent = yield* makeCursorAgent({ executablePath: makeFake() }); + const models = yield* agent.listModels; + assert.equal(models.availableModels?.[0]?.modelId, "composer-2"); + assert.deepEqual(models.availableModels?.[0]?.reasoningEfforts, ["low", "high"]); + assert.equal(models.availableModels?.[0]?.defaultReasoningEffort, "high"); + }), + ); + + it.effect("adapter open yields a cursor runtime", () => + Effect.gen(function* () { + const agent = yield* makeCursorAgent({ executablePath: makeFake() }); + const session = yield* makeCursorAdapter(agent).open({ cwd: "/tmp" }); + assert.equal(session.harnessAgentId, "cursor"); + yield* session.close; + }), + ); +}); diff --git a/packages/server/test/harness/cursor/executable.test.ts b/packages/server/test/harness/cursor/executable.test.ts new file mode 100644 index 000000000..016b69fcc --- /dev/null +++ b/packages/server/test/harness/cursor/executable.test.ts @@ -0,0 +1,78 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { layer } from "@effect/vitest"; +import { Effect } from "effect"; + +import { + checkCursorAvailability, + resolveCursorExecutable, +} from "../../../src/harness/cursor/executable"; + +layer(NodeServices.layer)("Cursor executable", (it) => { + it.effect("honours VIBEST_CURSOR_EXECUTABLE when it is executable", () => + Effect.gen(function* () { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fake-cursor-bin-")); + const file = path.join(dir, "cursor-agent"); + fs.writeFileSync(file, "#!/bin/sh\n"); + fs.chmodSync(file, 0o755); + + const resolved = yield* resolveCursorExecutable({ + env: { VIBEST_CURSOR_EXECUTABLE: file, PATH: "" }, + home: dir, + }); + assert.equal(resolved, file); + + const availability = yield* checkCursorAvailability({ + env: { VIBEST_CURSOR_EXECUTABLE: file, PATH: "" }, + home: dir, + }); + assert.equal(availability.available, true); + }), + ); + + it.effect("finds cursor-agent under ~/.local/bin when PATH is empty", () => + Effect.gen(function* () { + const home = fs.mkdtempSync(path.join(os.tmpdir(), "cursor-home-")); + const bin = path.join(home, ".local", "bin"); + fs.mkdirSync(bin, { recursive: true }); + const file = path.join(bin, "cursor-agent"); + fs.writeFileSync(file, "#!/bin/sh\n"); + fs.chmodSync(file, 0o755); + + const resolved = yield* resolveCursorExecutable({ env: { PATH: "" }, home }); + assert.equal(resolved, file); + }), + ); + + it.effect("does not pick up a PATH agent binary", () => + Effect.gen(function* () { + const home = fs.mkdtempSync(path.join(os.tmpdir(), "agent-collision-")); + const pathDir = path.join(home, "bin"); + fs.mkdirSync(pathDir, { recursive: true }); + const agent = path.join(pathDir, "agent"); + fs.writeFileSync(agent, "#!/bin/sh\n"); + fs.chmodSync(agent, 0o755); + + const resolved = yield* resolveCursorExecutable({ + env: { PATH: pathDir }, + home, + }); + assert.equal(resolved, undefined); + }), + ); + + it.effect("reports missing when PATH and extra dirs are empty", () => + Effect.gen(function* () { + const home = fs.mkdtempSync(path.join(os.tmpdir(), "empty-cursor-home-")); + const availability = yield* checkCursorAvailability({ env: { PATH: "" }, home }); + assert.equal(availability.available, false); + if (availability.available === false) { + assert.match(availability.reason, /Cursor Agent was not found/); + } + }), + ); +}); diff --git a/packages/server/test/harness/cursor/transform.test.ts b/packages/server/test/harness/cursor/transform.test.ts new file mode 100644 index 000000000..787ccb5b3 --- /dev/null +++ b/packages/server/test/harness/cursor/transform.test.ts @@ -0,0 +1,113 @@ +import { expect, it } from "vitest"; + +import type { RpcNotification } from "../../../src/harness/cursor/protocol"; +import { TURN_END_METHOD } from "../../../src/harness/cursor/protocol"; +import { createCursorTransform } from "../../../src/harness/cursor/transform"; + +const update = (sessionUpdate: string, extra: Record = {}): RpcNotification => ({ + method: "session/update", + params: { sessionId: "s1", update: { sessionUpdate, ...extra } }, +}); + +it("streams text and reasoning deltas, then finishes on endTurn", () => { + const transform = createCursorTransform("s1"); + const chunks = [ + ...transform.apply(update("agent_thought_chunk", { content: { type: "text", text: "hmm" } })), + ...transform.apply(update("agent_message_chunk", { content: { type: "text", text: "hi" } })), + ...transform.endTurn(), + ]; + expect(chunks.map((chunk) => chunk.type)).toEqual([ + "start", + "reasoning-start", + "reasoning-delta", + "reasoning-end", + "text-start", + "text-delta", + "text-end", + "finish", + ]); +}); + +it("finishes when session/prompt return is injected as vibest/turn_end", () => { + const transform = createCursorTransform("s1"); + const chunks = [ + ...transform.apply(update("agent_message_chunk", { content: { type: "text", text: "hi" } })), + ...transform.apply({ method: TURN_END_METHOD, params: { sessionId: "s1" } }), + ]; + expect(chunks.map((chunk) => chunk.type)).toEqual([ + "start", + "text-start", + "text-delta", + "text-end", + "finish", + ]); +}); + +it("emits typed tool input from tool_call", () => { + const transform = createCursorTransform("s1"); + const chunks = [ + ...transform.apply( + update("tool_call", { + toolCallId: "c1", + title: "Read", + rawInput: { path: "/tmp/a.ts" }, + }), + ), + ...transform.apply( + update("tool_call_update", { + toolCallId: "c1", + status: "completed", + rawOutput: { lines: 3 }, + }), + ), + ]; + expect(chunks[0]).toMatchObject({ type: "start", messageMetadata: { sessionId: "s1" } }); + expect(chunks.slice(1)).toEqual([ + { + type: "tool-input-available", + toolCallId: "c1", + toolName: "Read", + input: { path: "/tmp/a.ts" }, + providerExecuted: true, + dynamic: false, + }, + { + type: "tool-output-available", + toolCallId: "c1", + output: { lines: 3 }, + providerExecuted: true, + dynamic: false, + }, + ]); +}); + +it("keeps unknown tools dynamic", () => { + const transform = createCursorTransform("s1"); + const chunks = [ + ...transform.apply( + update("tool_call", { + toolCallId: "c1", + title: "mystery_tool", + rawInput: {}, + }), + ), + ...transform.apply( + update("tool_call_update", { + toolCallId: "c1", + status: "completed", + rawOutput: { ok: true }, + }), + ), + ]; + expect(chunks[1]).toMatchObject({ type: "tool-input-available", dynamic: true }); + expect(chunks[2]).toMatchObject({ type: "tool-output-available", dynamic: true }); +}); + +it("skips user echoes and command list updates", () => { + const transform = createCursorTransform("s1"); + const chunks = [ + ...transform.apply(update("user_message_chunk", { content: { type: "text", text: "hello" } })), + ...transform.apply(update("available_commands_update", { availableCommands: [] })), + ]; + expect(chunks).toEqual([]); +}); diff --git a/packages/server/test/harness/cursor/transport.test.ts b/packages/server/test/harness/cursor/transport.test.ts new file mode 100644 index 000000000..98153e2d5 --- /dev/null +++ b/packages/server/test/harness/cursor/transport.test.ts @@ -0,0 +1,111 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { layer } from "@effect/vitest"; +import { Effect, Fiber, Stream } from "effect"; + +import { makeCursorTransport } from "../../../src/harness/cursor/transport"; + +const FAKE = `#!/usr/bin/env node +const readline = require("node:readline"); +const rl = readline.createInterface({ input: process.stdin }); +const send = (frame) => process.stdout.write(JSON.stringify(frame) + "\\n"); +process.stdout.write("cursor-agent startup banner (not json)\\n"); +rl.on("line", (line) => { + const msg = JSON.parse(line); + if (msg.method === "echo") { + process.stdout.write(JSON.stringify({ not: "rpc" }) + "\\n"); + send({ jsonrpc: "2.0", id: msg.id, result: msg.params }); + } + if (msg.method === "boom") send({ jsonrpc: "2.0", id: msg.id, error: { code: -1, message: "kaboom" } }); + if (msg.method === "stderrFlood") { + process.stderr.write("x".repeat(1024 * 1024), () => send({ jsonrpc: "2.0", id: msg.id, result: "drained" })); + } + if (msg.method === "notifyMe") { + send({ jsonrpc: "2.0", id: msg.id, result: null }); + send({ jsonrpc: "2.0", method: "session/update", params: { sessionId: "s1", update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text: "hi" } } } }); + } + if (msg.method === "askMe") { + send({ jsonrpc: "2.0", id: msg.id, result: null }); + send({ jsonrpc: "2.0", method: "session/request_permission", id: 999, params: { sessionId: "s1" } }); + } + if (msg.method === "exit") { + process.stderr.write("fatal cursor detail\\n", () => process.exit(7)); + } +}); +`; + +function makeFake(): string { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fake-cursor-transport-")); + const file = path.join(dir, "fake-cursor-agent.js"); + fs.writeFileSync(file, FAKE); + fs.chmodSync(file, 0o755); + return file; +} + +layer(NodeServices.layer)("CursorTransport", (it) => { + it.effect("correlates responses and exposes typed RPC errors", () => + Effect.gen(function* () { + const transport = yield* makeCursorTransport({ executablePath: makeFake() }); + + assert.deepEqual(yield* transport.request("echo", { answer: 42 }), { + answer: 42, + }); + + const error = yield* transport.request("boom").pipe(Effect.flip); + assert.equal(error._tag, "CursorRpcError"); + if (error._tag === "CursorRpcError") assert.equal(error.code, -1); + }), + ); + + it.effect("skips CLI banner lines", () => + Effect.gen(function* () { + const transport = yield* makeCursorTransport({ executablePath: makeFake() }); + assert.deepEqual(yield* transport.request("echo", { ok: true }), { ok: true }); + }), + ); + + it.effect("drains child stderr without blocking protocol responses", () => + Effect.gen(function* () { + const transport = yield* makeCursorTransport({ executablePath: makeFake() }); + assert.equal(yield* transport.request("stderrFlood"), "drained"); + }), + ); + + it.effect("routes notifications and server requests as streams", () => + Effect.gen(function* () { + const transport = yield* makeCursorTransport({ executablePath: makeFake() }); + const notificationFiber = yield* Stream.runHead(transport.notifications).pipe( + Effect.forkChild, + ); + const requestFiber = yield* Stream.runHead(transport.serverRequests).pipe(Effect.forkChild); + + yield* transport.request("notifyMe"); + yield* transport.request("askMe"); + + const notification = yield* Fiber.join(notificationFiber); + const request = yield* Fiber.join(requestFiber); + assert.equal(notification._tag, "Some"); + assert.equal(request._tag, "Some"); + if (notification._tag === "Some") { + assert.equal(notification.value.method, "session/update"); + } + if (request._tag === "Some") assert.equal(request.value.id, 999); + }), + ); + + it.effect("fails pending requests when the child exits", () => + Effect.gen(function* () { + const transport = yield* makeCursorTransport({ executablePath: makeFake() }); + const pending = yield* transport.request("exit").pipe(Effect.flip); + assert.equal(pending._tag, "AgentProcessExited"); + if (pending._tag === "AgentProcessExited") { + assert.equal(pending.harnessAgentId, "cursor"); + assert.equal(pending.code, 7); + } + }), + ); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7bef96adc..7542577ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -268,10 +268,10 @@ importers: version: 17.3.0 oxfmt: specifier: ^0.58.0 - version: 0.58.0(vite-plus@0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.12)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(yaml@2.9.0)) + version: 0.58.0(vite-plus@0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.13)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(yaml@2.9.0)) oxlint: specifier: ^1.78.0 - version: 1.78.0(oxlint-tsgolint@0.24.0)(vite-plus@0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.12)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(yaml@2.9.0)) + version: 1.78.0(oxlint-tsgolint@0.24.0)(vite-plus@0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.13)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(yaml@2.9.0)) publint: specifier: ^0.3.23 version: 0.3.23 @@ -283,10 +283,10 @@ importers: version: 19.17.2 tsdown: specifier: 'catalog:' - version: 0.22.14(oxc-resolver@11.24.2)(publint@0.3.23)(tsx@4.23.12)(typescript@7.0.2)(unrun@0.3.1) + version: 0.22.14(oxc-resolver@11.24.2)(publint@0.3.23)(tsx@4.23.13)(typescript@7.0.2)(unrun@0.3.1) tsx: specifier: latest - version: 4.23.12 + version: 4.23.13 turbo: specifier: ^2.10.10 version: 2.10.10 @@ -298,10 +298,10 @@ importers: version: 0.3.1 vite: specifier: ^8.2.1 - version: 8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0) + version: 8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) vitest: specifier: 4.1.10 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-preview@4.1.10)(jsdom@26.1.0(supports-color@10.2.2))(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-preview@4.1.10)(jsdom@26.1.0(supports-color@10.2.2))(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) apps/app: dependencies: @@ -316,7 +316,7 @@ importers: version: 1.3.5(@shikijs/themes@4.4.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@pierre/trees': specifier: 1.0.0-beta.6 - version: 1.0.0-beta.6(@pierre/theme@2.0.0)(@shikijs/themes@4.4.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(shiki@4.3.1) + version: 1.0.0-beta.6(@pierre/theme@2.0.0)(@shikijs/themes@4.4.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(shiki@4.4.3) '@tanstack/react-query': specifier: 'catalog:' version: 5.101.4(react@19.2.8) @@ -401,10 +401,10 @@ importers: devDependencies: '@tailwindcss/vite': specifier: catalog:tailwind - version: 4.3.3(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.3.3(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) '@tanstack/router-plugin': specifier: 'catalog:' - version: 1.168.32(@tanstack/react-router@1.170.29(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.57.1)(supports-color@10.2.2)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 1.168.32(@tanstack/react-router@1.170.29(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.57.1)(supports-color@10.2.2)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) '@types/react': specifier: 'catalog:' version: 19.2.18 @@ -416,19 +416,19 @@ importers: version: link:../../tools/typescript '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.5(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 6.0.5(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) code-inspector-plugin: specifier: latest - version: 2.0.7(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2) + version: 2.0.8(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2) react-doctor: specifier: ^0.9.12 - version: 0.9.12(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2))(oxlint-tsgolint@0.24.0)(supports-color@10.2.2)(vite-plus@0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.12)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(yaml@2.9.0)) + version: 0.9.12(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2))(oxlint-tsgolint@0.24.0)(supports-color@10.2.2)(vite-plus@0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.13)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(yaml@2.9.0)) react-grab: specifier: ^0.1.50 version: 0.1.50(react@19.2.8) react-scan: specifier: 'catalog:' - version: 0.5.7(patch_hash=3db48cf39ae98df6cc3c03ce20b2445004e89a6dd46e078b75ca8e795db568a9)(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(esbuild@0.28.1)(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2))(oxlint-tsgolint@0.24.0)(preact-render-to-string@6.6.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(rolldown@1.2.4)(rollup@4.57.1)(supports-color@10.2.2)(vite-plus@0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.12)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(yaml@2.9.0))(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 0.5.7(patch_hash=3db48cf39ae98df6cc3c03ce20b2445004e89a6dd46e078b75ca8e795db568a9)(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(esbuild@0.28.1)(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2))(oxlint-tsgolint@0.24.0)(preact-render-to-string@6.6.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(rolldown@1.2.4)(rollup@4.57.1)(supports-color@10.2.2)(vite-plus@0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.13)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(yaml@2.9.0))(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) apps/desktop: dependencies: @@ -477,13 +477,13 @@ importers: version: 1.62.1 '@tailwindcss/vite': specifier: catalog:tailwind - version: 4.3.3(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.3.3(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) '@tanstack/react-query': specifier: 'catalog:' version: 5.101.4(react@19.2.8) '@tanstack/router-plugin': specifier: 'catalog:' - version: 1.168.32(@tanstack/react-router@1.170.29(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(esbuild@0.28.1)(rolldown@1.2.0)(supports-color@10.2.2)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 1.168.32(@tanstack/react-router@1.170.29(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.57.1)(supports-color@10.2.2)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) '@types/react': specifier: 'catalog:' version: 19.2.18 @@ -495,10 +495,10 @@ importers: version: link:../../packages/ui '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.5(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 6.0.5(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) code-inspector-plugin: specifier: latest - version: 2.0.7(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2) + version: 2.0.8(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2) electron: specifier: ^41.10.5 version: 41.10.5(supports-color@10.2.2) @@ -507,7 +507,7 @@ importers: version: 26.15.3(electron-builder-squirrel-windows@26.15.3)(supports-color@10.2.2) electron-vite: specifier: ^6.0.0-beta.1 - version: 6.0.0-beta.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(supports-color@10.2.2)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 6.0.0-beta.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(supports-color@10.2.2)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) lucide-react: specifier: 'catalog:' version: 1.31.0(react@19.2.8) @@ -578,7 +578,7 @@ importers: version: 7.0.2 vitest: specifier: 4.1.10 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-preview@4.1.10)(jsdom@26.1.0(supports-color@10.2.2))(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-preview@4.1.10)(jsdom@26.1.0(supports-color@10.2.2))(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) packages/effect-json-store: dependencies: @@ -606,7 +606,7 @@ importers: version: 0.80.10(@modelcontextprotocol/sdk@1.29.0(supports-color@10.2.2)(zod@4.4.3))(supports-color@10.2.2)(ws@8.21.3)(zod@4.4.3) '@effect/platform-node': specifier: 4.0.0-rc.110 - version: 4.0.0-rc.110(effect@4.0.0-rc.110)(redis@6.2.1(@opentelemetry/api@1.9.0)) + version: 4.0.0-rc.110(effect@4.0.0-rc.110)(redis@6.2.1(@opentelemetry/api@1.9.1)) '@orpc/experimental-effect': specifier: catalog:orpc version: 2.0.0-beta.28(@opentelemetry/api@1.9.0)(effect@4.0.0-rc.110) @@ -1172,8 +1172,8 @@ packages: '@chevrotain/types@11.1.2': resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==} - '@code-inspector/core@2.0.7': - resolution: {integrity: sha512-9frLzGdS1TvPNSiDt5f/FFJ+aiaU8driaOWmyISOa7Cxy9LC1UOiNcJ3fmhqdIb3mHVqgdWT2NMWdr/6zWPtmA==} + '@code-inspector/core@2.0.8': + resolution: {integrity: sha512-RIGPzE5YGRThjFWvW9coAsg2H02ByJWI1yviOm/mJ6qQVFRliTG9rLwSU4gRzLsMu5V8VF+8hypMp6xSOFH5uQ==} peerDependencies: '@anthropic-ai/claude-agent-sdk': ^0.2.29 '@openai/codex-sdk': ^0.106.0 @@ -1186,20 +1186,20 @@ packages: '@opencode-ai/sdk': optional: true - '@code-inspector/esbuild@2.0.7': - resolution: {integrity: sha512-xFe6OSxWapEcLyf/KQimhdnRA34IYvYfSDrlKh8gbk8IvCEiRHUnSr2KRxRCXLCrpAlaRvSneiYIiuRJgq+NBQ==} + '@code-inspector/esbuild@2.0.8': + resolution: {integrity: sha512-xnfpBiBKrIOGN0j9IZ+k2p6aGq8TOIeHndGY+W8mxeODhO5KbM9rym0FSgVe3tEm6JcUk3kHP9h/EG61mnqDuw==} - '@code-inspector/mako@2.0.7': - resolution: {integrity: sha512-gABCx0mM7rtxXnPy0bYpu0j/2nn21DHFRmlUSMdNdSqxNQHxzP4sbEetZR/63DfsCHOzsikDGk6O2QDAsB27iA==} + '@code-inspector/mako@2.0.8': + resolution: {integrity: sha512-l243SWfGr4sUACVOCd5NbkjuFRobACnmpWXA2IyDFkedYFeGbYYpjZUwP88lxrVHt1PHZyB81HMiScE24P0ONw==} - '@code-inspector/turbopack@2.0.7': - resolution: {integrity: sha512-FD/Kp+S5a/0nSgnfxCWcnP5qUCxkKOPBWszQxwgxv7oe6x+Ca8z9imAurq4UtQAwse2XPXFmbZf2NKfIY4TDBA==} + '@code-inspector/turbopack@2.0.8': + resolution: {integrity: sha512-iLbGsojN6MwcqClTUQrqTHZHKUgbHBXSyDc4CAicmPytr5EcSYCCyICPrAY238ny+CdmzxGLaobZTA/YpScNiA==} - '@code-inspector/vite@2.0.7': - resolution: {integrity: sha512-EVJuoPvQJEjXSlub2aMkZ3k55tCOZYe6Hm/U9orVfRjd3i6vMe7HKpHqLD67fWuW8+FB0Bdu9EHPGJaJHmeOcw==} + '@code-inspector/vite@2.0.8': + resolution: {integrity: sha512-qj8lOiA6Q1cZ8iJI2zGSdEd85yok/8EFa0G2KiSrY1PaD3pLZquayRgzWMCLTC2pmLSWGM4L1UbTyDM2c8joDg==} - '@code-inspector/webpack@2.0.7': - resolution: {integrity: sha512-sA1B9RA7pNcPoZxGmegBxv+0Ac2yoUYJMcQ6aYREnINwfAVvgxKkfksjPubMuglKIEMtY1qinAp+JiQFSuImag==} + '@code-inspector/webpack@2.0.8': + resolution: {integrity: sha512-lz6LvkugtibrKbA+t/jFBkdSThveFEcLe1zutr4MUUOJh4XXWo1MYAjFHkUKnSy/+KqMC9MfBnDDtObYICBUmA==} '@csstools/color-helpers@5.1.0': resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} @@ -2187,9 +2187,6 @@ packages: '@oxc-project/types@0.138.0': resolution: {integrity: sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==} - '@oxc-project/types@0.140.0': - resolution: {integrity: sha512-h5LUOzGArYemnW1NMz/DuuQhBi96J6JL2Bk8zE4kvqxB5Sg3jxmCiH4uyOWHDkiKSt5vWlG4FIwCR/DbstcNRQ==} - '@oxc-project/types@0.143.0': resolution: {integrity: sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==} @@ -3107,73 +3104,36 @@ packages: peerDependencies: '@redis/client': ^6.2.1 - '@rolldown/binding-android-arm64@1.2.0': - resolution: {integrity: sha512-9yB1l95IrJuNGDFdOYe79vdApdz6WWBCObE+rQ2LUliYUlcyFwSYIb2xb5/Ifw7dAtMy2ZqNyd8QTSOc7duAKw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - '@rolldown/binding-android-arm64@1.2.4': resolution: {integrity: sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.2.0': - resolution: {integrity: sha512-pexNaW9ACLUOaBITOpU6qVu4VrsOFIjTv6bzgu0YUATo4eUJx0V605PxwZfndpPOn0ilqGqvGQ0M8UW0IE24jg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - '@rolldown/binding-darwin-arm64@1.2.4': resolution: {integrity: sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.2.0': - resolution: {integrity: sha512-NqKYaq0355ZmNMG4QGpxtEDxsc7tGDhjhCm4PpE0cwnBW+5Il95LJyq414niEiaKLVjnVHBEjSo1wngKxJNiFw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - '@rolldown/binding-darwin-x64@1.2.4': resolution: {integrity: sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.2.0': - resolution: {integrity: sha512-3vPoHzh6eBTz9IbB0/qZdSr0Qeks2echn+I4cHu2joV74VriPDdldswksEDzrl1mBB+oPRi+67+3Ib59paxIPQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - '@rolldown/binding-freebsd-x64@1.2.4': resolution: {integrity: sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.2.0': - resolution: {integrity: sha512-E6NNefZ1bUVmKJq2tJkf45J4Zyczj7qm9rUT7NY+Xo2474Y13qWAwc2tvBt0BAVbmtXR1llkxXg0Ou1jbDf2SQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - '@rolldown/binding-linux-arm-gnueabihf@1.2.4': resolution: {integrity: sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.2.0': - resolution: {integrity: sha512-D+TgkdgM1vu+7/Fpf8+v0ARW+RXEP9Ccazgm8zQ4JFFd9Q7SrYQ2TakU5S5ihazQDgpKyAgZDOcIFsvoHmTZ8w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - '@rolldown/binding-linux-arm64-gnu@1.2.4': resolution: {integrity: sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3181,13 +3141,6 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.2.0': - resolution: {integrity: sha512-wUqdwJBbAv0APN87GecstdMUtLjjNTs0hBALpxETD73mccFxdmt/XeizXDtN5RAlBwNKmI+Tg+blect2G+8IeQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - '@rolldown/binding-linux-arm64-musl@1.2.4': resolution: {integrity: sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3195,13 +3148,6 @@ packages: os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.2.0': - resolution: {integrity: sha512-9DtF35qR9/NrfhM4oxLplCzVVjE+KKm8Pjemi0i/sdhAWkUasjmSo8WTTubNJClhSHCfyk2yeyoXDQEDPtDAAw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - '@rolldown/binding-linux-ppc64-gnu@1.2.4': resolution: {integrity: sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3209,13 +3155,6 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.2.0': - resolution: {integrity: sha512-RzuHrBh8X8Hntd2N4VR02QGEciq/9JhcZoTpR/Cee6otRrlILGCf3cg2ygHuih+ZebUnWmMrDX6ITI85btO6rQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.2.4': resolution: {integrity: sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3223,13 +3162,6 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.2.0': - resolution: {integrity: sha512-MK7L0018jjh1jR3mh21G2j1zAVcpscJBlPo2z19pRjv2XOYGRhaV4LyiD8HO6nCDdZln9IFgCMIV5yt4E3klGQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.2.4': resolution: {integrity: sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3237,13 +3169,6 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.2.0': - resolution: {integrity: sha512-gyrxLQ9NfGb/9LoVnC4kb9miUghw1mghnkfYvNHSnVIXriabnfgGPUP4RLcJm87q3KgYz4FYUG8IDiWUT+CpSw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - '@rolldown/binding-linux-x64-musl@1.2.4': resolution: {integrity: sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3251,41 +3176,18 @@ packages: os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.2.0': - resolution: {integrity: sha512-/6VFMQGRmrhP77KXDC+StIxGzcNp5JOIyYtw0CQ8gPlzhpiIRucYfoM5FaFamHd5BJYIdH86yfP46l1p3WdrFA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - '@rolldown/binding-openharmony-arm64@1.2.4': resolution: {integrity: sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.2.0': - resolution: {integrity: sha512-rwdbUL465kisF24WEJLvP3JrEG6E5GRuIHt5wpMwHGERtHe4Wm2CIvtf5gTBgr2tGOHKh5NdKEAFS2VkOPE91g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] - - '@rolldown/binding-win32-arm64-msvc@1.2.0': - resolution: {integrity: sha512-+5suHwRiKGmhwyUaNT8a5QbrBvLFh2DbO910TEmGRH1aSxwrCezodvGQnulv4uiWEIv1Kq4ypRsJ5+O+ry1DiA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - '@rolldown/binding-win32-arm64-msvc@1.2.4': resolution: {integrity: sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.2.0': - resolution: {integrity: sha512-WfFv6/qGufotqBSBzBYwgpCkJBk8Nj7697LL9vTz/XWc67e0r3oewu8iMRwQj3AUL45GVD7wVsPjCsAAtW66Wg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - '@rolldown/binding-win32-x64-msvc@1.2.4': resolution: {integrity: sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3491,50 +3393,26 @@ packages: resolution: {integrity: sha512-8mpfsoPeRhesY3pOrzNZBL8uG6N5GVX1EHLBYbd4gzKs+c7vaEIqpTNK5VrffU33qQN4cwpP2v3u4aPPBU32sw==} engines: {node: '>=16'} - '@shikijs/core@4.3.1': - resolution: {integrity: sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA==} - engines: {node: '>=20'} - '@shikijs/core@4.4.3': resolution: {integrity: sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg==} engines: {node: '>=20'} - '@shikijs/engine-javascript@4.3.1': - resolution: {integrity: sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ==} - engines: {node: '>=20'} - '@shikijs/engine-javascript@4.4.3': resolution: {integrity: sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ==} engines: {node: '>=20'} - '@shikijs/engine-oniguruma@4.3.1': - resolution: {integrity: sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg==} - engines: {node: '>=20'} - '@shikijs/engine-oniguruma@4.4.3': resolution: {integrity: sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w==} engines: {node: '>=20'} - '@shikijs/langs@4.3.1': - resolution: {integrity: sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ==} - engines: {node: '>=20'} - '@shikijs/langs@4.4.3': resolution: {integrity: sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A==} engines: {node: '>=20'} - '@shikijs/primitive@4.3.1': - resolution: {integrity: sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A==} - engines: {node: '>=20'} - '@shikijs/primitive@4.4.3': resolution: {integrity: sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ==} engines: {node: '>=20'} - '@shikijs/themes@4.3.1': - resolution: {integrity: sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA==} - engines: {node: '>=20'} - '@shikijs/themes@4.4.3': resolution: {integrity: sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw==} engines: {node: '>=20'} @@ -3543,10 +3421,6 @@ packages: resolution: {integrity: sha512-oJSARV6NaWd+rnNJbtnpAdj3Zg0ZVyzsnMgb3vi3HA+35y8lBWUCpOnWsmyiXZIikY+x1BDqrQUgmxfzWh7Jvw==} engines: {node: '>=20'} - '@shikijs/types@4.3.1': - resolution: {integrity: sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g==} - engines: {node: '>=20'} - '@shikijs/types@4.4.3': resolution: {integrity: sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g==} engines: {node: '>=20'} @@ -3794,6 +3668,13 @@ packages: resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} engines: {node: '>=14.0.0'} cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} @@ -4148,9 +4029,6 @@ packages: '@types/react-syntax-highlighter@15.5.13': resolution: {integrity: sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==} - '@types/react@19.2.17': - resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} - '@types/react@19.2.18': resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} @@ -5191,8 +5069,8 @@ packages: resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} engines: {node: '>=0.10.0'} - code-inspector-plugin@2.0.7: - resolution: {integrity: sha512-Gaj5syVdtQt3UfFmypbHa76IPp5zSJwJCQfvjBb1SpEhJjvCk6I8uNFsNnLKvHcMPCkl/pABnCHXiiHbVXO3Vg==} + code-inspector-plugin@2.0.8: + resolution: {integrity: sha512-Ig7pHNGdlVprf6zRFLhSFI28D4PrKG72YkqNQ0uxYZHEomvKvD6FwK1DrYypbthz+N7c7g9v/uvAFM7tJn3ntw==} color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} @@ -6421,8 +6299,8 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} - launch-ide@1.4.8: - resolution: {integrity: sha512-SeY4/242PZ6M2cUtZvwEatbmIAjSK2zBm5PFUHFgJsR+1Kw51SBcqUHYzd5yofVAiyt0L047Kya7DqxoP+0Ouw==} + launch-ide@1.4.9: + resolution: {integrity: sha512-M67fqPmkMQQuiFnMtsgtMvHuB2a+8KLxEwH9ccftpy0f3CJQNoRCebZvKSn/ujxR42cMDOIBstuC/KXR/IgXeQ==} layout-base@1.0.2: resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} @@ -7178,9 +7056,6 @@ packages: package-manager-detector@0.2.11: resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} - package-manager-detector@1.6.0: - resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} - package-manager-detector@1.8.0: resolution: {integrity: sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==} @@ -7657,11 +7532,6 @@ packages: vue-tsc: optional: true - rolldown@1.2.0: - resolution: {integrity: sha512-u7tgm5l4Yw1iTqUL4EcYOAt7fFvCgQMLeidrnD4GALlC6aOznCjezYajgxeyKw27u0Q5N7fwgCzjVyPIWzwuBA==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - rolldown@1.2.4: resolution: {integrity: sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==} engines: {node: ^20.19.0 || >=22.12.0} @@ -7783,10 +7653,6 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shiki@4.3.1: - resolution: {integrity: sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw==} - engines: {node: '>=20'} - shiki@4.4.3: resolution: {integrity: sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g==} engines: {node: '>=20'} @@ -8002,10 +7868,6 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@1.2.4: - resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} - engines: {node: '>=18'} - tinyexec@1.3.0: resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} engines: {node: '>=18'} @@ -8113,8 +7975,8 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsx@4.23.12: - resolution: {integrity: sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==} + tsx@4.23.13: + resolution: {integrity: sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==} engines: {node: '>=18.0.0'} hasBin: true @@ -8504,18 +8366,6 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.21.1: - resolution: {integrity: sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - ws@8.21.3: resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} engines: {node: '>=10.0.0'} @@ -8645,8 +8495,8 @@ snapshots: '@antfu/install-pkg@1.1.0': dependencies: - package-manager-detector: 1.6.0 - tinyexec: 1.2.4 + package-manager-detector: 1.8.0 + tinyexec: 1.3.0 '@antfu/ni@30.5.0': dependencies: @@ -9311,11 +9161,11 @@ snapshots: '@chevrotain/types@11.1.2': {} - '@code-inspector/core@2.0.7(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2)': + '@code-inspector/core@2.0.8(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2)': dependencies: '@vue/compiler-dom': 3.5.27 chalk: 4.1.2 - launch-ide: 1.4.8 + launch-ide: 1.4.9 portfinder: 1.0.38(supports-color@10.2.2) ws: 8.21.3 optionalDependencies: @@ -9326,9 +9176,9 @@ snapshots: - supports-color - utf-8-validate - '@code-inspector/esbuild@2.0.7(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2)': + '@code-inspector/esbuild@2.0.8(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2)': dependencies: - '@code-inspector/core': 2.0.7(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2) + '@code-inspector/core': 2.0.8(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2) transitivePeerDependencies: - '@anthropic-ai/claude-agent-sdk' - '@openai/codex-sdk' @@ -9337,9 +9187,9 @@ snapshots: - supports-color - utf-8-validate - '@code-inspector/mako@2.0.7(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2)': + '@code-inspector/mako@2.0.8(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2)': dependencies: - '@code-inspector/core': 2.0.7(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2) + '@code-inspector/core': 2.0.8(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2) transitivePeerDependencies: - '@anthropic-ai/claude-agent-sdk' - '@openai/codex-sdk' @@ -9348,10 +9198,10 @@ snapshots: - supports-color - utf-8-validate - '@code-inspector/turbopack@2.0.7(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2)': + '@code-inspector/turbopack@2.0.8(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2)': dependencies: - '@code-inspector/core': 2.0.7(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2) - '@code-inspector/webpack': 2.0.7(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2) + '@code-inspector/core': 2.0.8(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2) + '@code-inspector/webpack': 2.0.8(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2) transitivePeerDependencies: - '@anthropic-ai/claude-agent-sdk' - '@openai/codex-sdk' @@ -9360,9 +9210,9 @@ snapshots: - supports-color - utf-8-validate - '@code-inspector/vite@2.0.7(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2)': + '@code-inspector/vite@2.0.8(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2)': dependencies: - '@code-inspector/core': 2.0.7(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2) + '@code-inspector/core': 2.0.8(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2) chalk: 4.1.1 transitivePeerDependencies: - '@anthropic-ai/claude-agent-sdk' @@ -9372,9 +9222,9 @@ snapshots: - supports-color - utf-8-validate - '@code-inspector/webpack@2.0.7(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2)': + '@code-inspector/webpack@2.0.8(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2)': dependencies: - '@code-inspector/core': 2.0.7(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2) + '@code-inspector/core': 2.0.8(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2) transitivePeerDependencies: - '@anthropic-ai/claude-agent-sdk' - '@openai/codex-sdk' @@ -9479,18 +9329,7 @@ snapshots: dependencies: '@types/ws': 8.18.1 effect: 4.0.0-rc.110 - ws: 8.21.1 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@effect/platform-node@4.0.0-rc.110(effect@4.0.0-rc.110)(redis@6.2.1(@opentelemetry/api@1.9.0))': - dependencies: - '@effect/platform-node-shared': 4.0.0-rc.110(effect@4.0.0-rc.110) - effect: 4.0.0-rc.110 - mime: 4.1.0 - redis: 6.2.1(@opentelemetry/api@1.9.0) - undici: 8.7.0 + ws: 8.21.3 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -9509,7 +9348,7 @@ snapshots: '@effect/vitest@4.0.0-rc.110(effect@4.0.0-rc.110)(vitest@4.1.10)': dependencies: effect: 4.0.0-rc.110 - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-preview@4.1.10)(jsdom@26.1.0(supports-color@10.2.2))(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-preview@4.1.10)(jsdom@26.1.0(supports-color@10.2.2))(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) '@electron-internal/extract-zip@1.0.4': {} @@ -9851,7 +9690,7 @@ snapshots: google-auth-library: 10.9.0(supports-color@10.2.2) p-retry: 4.6.2 protobufjs: 7.6.5 - ws: 8.21.1 + ws: 8.21.3 optionalDependencies: '@modelcontextprotocol/sdk': 1.29.0(supports-color@10.2.2)(zod@4.4.3) transitivePeerDependencies: @@ -10023,7 +9862,7 @@ snapshots: '@mistralai/mistralai@2.2.6(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/semantic-conventions': 1.43.0 - ws: 8.21.1 + ws: 8.21.3 zod: 4.4.3 zod-to-json-schema: 3.25.2(zod@4.4.3) optionalDependencies: @@ -10369,8 +10208,6 @@ snapshots: '@oxc-project/types@0.138.0': optional: true - '@oxc-project/types@0.140.0': {} - '@oxc-project/types@0.143.0': {} '@oxc-project/types@0.144.0': {} @@ -10767,38 +10604,38 @@ snapshots: '@pierre/diffs@1.3.5(@shikijs/themes@4.4.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@pierre/theme': 2.0.0 - '@pierre/theming': 1.0.1(@pierre/theme@2.0.0)(@shikijs/themes@4.4.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(shiki@4.3.1) + '@pierre/theming': 1.0.1(@pierre/theme@2.0.0)(@shikijs/themes@4.4.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(shiki@4.4.3) '@shikijs/transformers': 4.4.3 diff: 9.0.0 hast-util-to-html: 9.0.5 lru_map: 0.4.1 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) - shiki: 4.3.1 + shiki: 4.4.3 transitivePeerDependencies: - '@shikijs/themes' '@pierre/theme@2.0.0': {} - '@pierre/theming@1.0.0(@pierre/theme@2.0.0)(@shikijs/themes@4.4.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(shiki@4.3.1)': + '@pierre/theming@1.0.0(@pierre/theme@2.0.0)(@shikijs/themes@4.4.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(shiki@4.4.3)': optionalDependencies: '@pierre/theme': 2.0.0 '@shikijs/themes': 4.4.3 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) - shiki: 4.3.1 + shiki: 4.4.3 - '@pierre/theming@1.0.1(@pierre/theme@2.0.0)(@shikijs/themes@4.4.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(shiki@4.3.1)': + '@pierre/theming@1.0.1(@pierre/theme@2.0.0)(@shikijs/themes@4.4.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(shiki@4.4.3)': optionalDependencies: '@pierre/theme': 2.0.0 '@shikijs/themes': 4.4.3 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) - shiki: 4.3.1 + shiki: 4.4.3 - '@pierre/trees@1.0.0-beta.6(@pierre/theme@2.0.0)(@shikijs/themes@4.4.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(shiki@4.3.1)': + '@pierre/trees@1.0.0-beta.6(@pierre/theme@2.0.0)(@shikijs/themes@4.4.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(shiki@4.4.3)': dependencies: - '@pierre/theming': 1.0.0(@pierre/theme@2.0.0)(@shikijs/themes@4.4.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(shiki@4.3.1) + '@pierre/theming': 1.0.0(@pierre/theme@2.0.0)(@shikijs/themes@4.4.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(shiki@4.4.3) preact: 11.0.0-beta.0 preact-render-to-string: 6.6.5(preact@11.0.0-beta.0) react: 19.2.8 @@ -10844,7 +10681,7 @@ snapshots: '@publint/pack@0.1.6': dependencies: - tinyexec: 1.2.4 + tinyexec: 1.3.0 '@quansync/fs@1.0.0': dependencies: @@ -10856,10 +10693,10 @@ snapshots: commander: 14.0.3 ignore: 7.0.6 ora: 9.4.1 - package-manager-detector: 1.6.0 + package-manager-detector: 1.8.0 picocolors: 1.1.1 prompts: 2.4.2 - tinyexec: 1.2.4 + tinyexec: 1.3.0 '@react-grab/cli@0.2.0': dependencies: @@ -10872,138 +10709,67 @@ snapshots: prompts: 2.4.2 tinyexec: 1.3.0 - '@redis/bloom@6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.0))': - dependencies: - '@redis/client': 6.2.1(@opentelemetry/api@1.9.0) - '@redis/bloom@6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.1))': dependencies: '@redis/client': 6.2.1(@opentelemetry/api@1.9.1) - '@redis/client@6.2.1(@opentelemetry/api@1.9.0)': - dependencies: - cluster-key-slot: 1.1.2 - optionalDependencies: - '@opentelemetry/api': 1.9.0 - '@redis/client@6.2.1(@opentelemetry/api@1.9.1)': dependencies: cluster-key-slot: 1.1.2 optionalDependencies: '@opentelemetry/api': 1.9.1 - '@redis/json@6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.0))': - dependencies: - '@redis/client': 6.2.1(@opentelemetry/api@1.9.0) - '@redis/json@6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.1))': dependencies: '@redis/client': 6.2.1(@opentelemetry/api@1.9.1) - '@redis/search@6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.0))': - dependencies: - '@redis/client': 6.2.1(@opentelemetry/api@1.9.0) - '@redis/search@6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.1))': dependencies: '@redis/client': 6.2.1(@opentelemetry/api@1.9.1) - '@redis/time-series@6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.0))': - dependencies: - '@redis/client': 6.2.1(@opentelemetry/api@1.9.0) - '@redis/time-series@6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.1))': dependencies: '@redis/client': 6.2.1(@opentelemetry/api@1.9.1) - '@rolldown/binding-android-arm64@1.2.0': - optional: true - '@rolldown/binding-android-arm64@1.2.4': optional: true - '@rolldown/binding-darwin-arm64@1.2.0': - optional: true - '@rolldown/binding-darwin-arm64@1.2.4': optional: true - '@rolldown/binding-darwin-x64@1.2.0': - optional: true - '@rolldown/binding-darwin-x64@1.2.4': optional: true - '@rolldown/binding-freebsd-x64@1.2.0': - optional: true - '@rolldown/binding-freebsd-x64@1.2.4': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.2.0': - optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.2.4': optional: true - '@rolldown/binding-linux-arm64-gnu@1.2.0': - optional: true - '@rolldown/binding-linux-arm64-gnu@1.2.4': optional: true - '@rolldown/binding-linux-arm64-musl@1.2.0': - optional: true - '@rolldown/binding-linux-arm64-musl@1.2.4': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.2.0': - optional: true - '@rolldown/binding-linux-ppc64-gnu@1.2.4': optional: true - '@rolldown/binding-linux-s390x-gnu@1.2.0': - optional: true - '@rolldown/binding-linux-s390x-gnu@1.2.4': optional: true - '@rolldown/binding-linux-x64-gnu@1.2.0': - optional: true - '@rolldown/binding-linux-x64-gnu@1.2.4': optional: true - '@rolldown/binding-linux-x64-musl@1.2.0': - optional: true - '@rolldown/binding-linux-x64-musl@1.2.4': optional: true - '@rolldown/binding-openharmony-arm64@1.2.0': - optional: true - '@rolldown/binding-openharmony-arm64@1.2.4': optional: true - '@rolldown/binding-wasm32-wasi@1.2.0': - dependencies: - '@emnapi/core': 1.11.2 - '@emnapi/runtime': 1.11.2 - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2) - optional: true - - '@rolldown/binding-win32-arm64-msvc@1.2.0': - optional: true - '@rolldown/binding-win32-arm64-msvc@1.2.4': optional: true - '@rolldown/binding-win32-x64-msvc@1.2.0': - optional: true - '@rolldown/binding-win32-x64-msvc@1.2.4': optional: true @@ -11147,14 +10913,6 @@ snapshots: '@shaderfrog/glsl-parser@7.0.1': {} - '@shikijs/core@4.3.1': - dependencies: - '@shikijs/primitive': 4.3.1 - '@shikijs/types': 4.3.1 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.5 - hast-util-to-html: 9.0.5 - '@shikijs/core@4.4.3': dependencies: '@shikijs/primitive': 4.4.3 @@ -11163,52 +10921,27 @@ snapshots: '@types/hast': 3.0.5 hast-util-to-html: 9.0.5 - '@shikijs/engine-javascript@4.3.1': - dependencies: - '@shikijs/types': 4.3.1 - '@shikijs/vscode-textmate': 10.0.2 - oniguruma-to-es: 4.3.6 - '@shikijs/engine-javascript@4.4.3': dependencies: '@shikijs/types': 4.4.3 '@shikijs/vscode-textmate': 10.0.2 oniguruma-to-es: 4.3.6 - '@shikijs/engine-oniguruma@4.3.1': - dependencies: - '@shikijs/types': 4.3.1 - '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/engine-oniguruma@4.4.3': dependencies: '@shikijs/types': 4.4.3 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@4.3.1': - dependencies: - '@shikijs/types': 4.3.1 - '@shikijs/langs@4.4.3': dependencies: '@shikijs/types': 4.4.3 - '@shikijs/primitive@4.3.1': - dependencies: - '@shikijs/types': 4.3.1 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.5 - '@shikijs/primitive@4.4.3': dependencies: '@shikijs/types': 4.4.3 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 - '@shikijs/themes@4.3.1': - dependencies: - '@shikijs/types': 4.3.1 - '@shikijs/themes@4.4.3': dependencies: '@shikijs/types': 4.4.3 @@ -11218,11 +10951,6 @@ snapshots: '@shikijs/core': 4.4.3 '@shikijs/types': 4.4.3 - '@shikijs/types@4.3.1': - dependencies: - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.5 - '@shikijs/types@4.4.3': dependencies: '@shikijs/vscode-textmate': 10.0.2 @@ -11458,12 +11186,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 - '@tailwindcss/vite@4.3.3(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))': + '@tailwindcss/vite@4.3.3(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))': dependencies: '@tailwindcss/node': 4.3.3 '@tailwindcss/oxide': 4.3.3 tailwindcss: 4.3.3 - vite: 8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0) + vite: 8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) '@tanstack/history@1.162.1': {} @@ -11510,31 +11238,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.168.32(@tanstack/react-router@1.170.29(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(esbuild@0.28.1)(rolldown@1.2.0)(supports-color@10.2.2)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))': - dependencies: - '@babel/core': 7.29.7(supports-color@10.2.2) - '@babel/template': 7.29.7 - '@babel/types': 7.29.7 - '@tanstack/router-core': 1.171.24 - '@tanstack/router-generator': 1.167.30(supports-color@10.2.2) - '@tanstack/router-utils': 1.162.2(supports-color@10.2.2) - chokidar: 5.0.0 - unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.0)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) - zod: 4.4.3 - optionalDependencies: - '@tanstack/react-router': 1.170.29(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - vite: 8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0) - transitivePeerDependencies: - - '@farmfe/core' - - '@rspack/core' - - bun-types-no-globals - - esbuild - - rolldown - - rollup - - supports-color - - unloader - - '@tanstack/router-plugin@1.168.32(@tanstack/react-router@1.170.29(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.57.1)(supports-color@10.2.2)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))': + '@tanstack/router-plugin@1.168.32(@tanstack/react-router@1.170.29(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.57.1)(supports-color@10.2.2)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.7(supports-color@10.2.2) '@babel/template': 7.29.7 @@ -11543,11 +11247,11 @@ snapshots: '@tanstack/router-generator': 1.167.30(supports-color@10.2.2) '@tanstack/router-utils': 1.162.2(supports-color@10.2.2) chokidar: 5.0.0 - unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.57.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.57.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) zod: 4.4.3 optionalDependencies: '@tanstack/react-router': 1.170.29(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - vite: 8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0) + vite: 8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) transitivePeerDependencies: - '@farmfe/core' - '@rspack/core' @@ -11879,11 +11583,7 @@ snapshots: '@types/react-syntax-highlighter@15.5.13': dependencies: - '@types/react': 19.2.17 - - '@types/react@19.2.17': - dependencies: - csstype: 3.2.3 + '@types/react': 19.2.18 '@types/react@19.2.18': dependencies: @@ -11979,17 +11679,17 @@ snapshots: '@vercel/oidc@3.2.0': {} - '@vitejs/plugin-react@6.0.5(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))': + '@vitejs/plugin-react@6.0.5(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0) + vite: 8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) - '@vitest/browser-preview@4.1.10(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.10)': + '@vitest/browser-preview@4.1.10(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(vitest@4.1.10)': dependencies: '@testing-library/dom': 10.4.1 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) - '@vitest/browser': 4.1.10(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.10) - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-preview@4.1.10)(jsdom@26.1.0(supports-color@10.2.2))(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + '@vitest/browser': 4.1.10(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(vitest@4.1.10) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-preview@4.1.10)(jsdom@26.1.0(supports-color@10.2.2))(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) transitivePeerDependencies: - bufferutil - msw @@ -11997,16 +11697,16 @@ snapshots: - vite optional: true - '@vitest/browser@4.1.10(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.10)': + '@vitest/browser@4.1.10(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(vitest@4.1.10)': dependencies: '@blazediff/core': 1.9.1 - '@vitest/mocker': 4.1.10(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + '@vitest/mocker': 4.1.10(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) '@vitest/utils': 4.1.10 magic-string: 0.30.21 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.0 - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-preview@4.1.10)(jsdom@26.1.0(supports-color@10.2.2))(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-preview@4.1.10)(jsdom@26.1.0(supports-color@10.2.2))(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) ws: 8.21.3 transitivePeerDependencies: - bufferutil @@ -12024,13 +11724,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.10(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))': + '@vitest/mocker@4.1.10(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0) + vite: 8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) '@vitest/pretty-format@4.1.10': dependencies: @@ -12056,7 +11756,7 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 - '@voidzero-dev/vite-plus-core@0.2.4(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(publint@0.3.23)(tsx@4.23.12)(typescript@7.0.2)(unrun@0.3.1)(yaml@2.9.0)': + '@voidzero-dev/vite-plus-core@0.2.4(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(publint@0.3.23)(tsx@4.23.13)(typescript@7.0.2)(unrun@0.3.1)(yaml@2.9.0)': dependencies: '@oxc-project/runtime': 0.138.0 '@oxc-project/types': 0.138.0 @@ -12068,7 +11768,7 @@ snapshots: fsevents: 2.3.3 jiti: 2.7.0 publint: 0.3.23 - tsx: 4.23.12 + tsx: 4.23.13 typescript: 7.0.2 unrun: 0.3.1 yaml: 2.9.0 @@ -13126,14 +12826,14 @@ snapshots: cluster-key-slot@1.1.2: {} - code-inspector-plugin@2.0.7(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2): + code-inspector-plugin@2.0.8(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2): dependencies: - '@code-inspector/core': 2.0.7(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2) - '@code-inspector/esbuild': 2.0.7(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2) - '@code-inspector/mako': 2.0.7(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2) - '@code-inspector/turbopack': 2.0.7(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2) - '@code-inspector/vite': 2.0.7(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2) - '@code-inspector/webpack': 2.0.7(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2) + '@code-inspector/core': 2.0.8(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2) + '@code-inspector/esbuild': 2.0.8(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2) + '@code-inspector/mako': 2.0.8(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2) + '@code-inspector/turbopack': 2.0.8(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2) + '@code-inspector/vite': 2.0.8(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2) + '@code-inspector/webpack': 2.0.8(@opencode-ai/sdk@1.18.3)(supports-color@10.2.2) chalk: 4.1.1 transitivePeerDependencies: - '@anthropic-ai/claude-agent-sdk' @@ -13609,7 +13309,7 @@ snapshots: electron-to-chromium@1.5.283: {} - electron-vite@6.0.0-beta.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(supports-color@10.2.2)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)): + electron-vite@6.0.0-beta.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(supports-color@10.2.2)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)): dependencies: '@babel/core': 7.29.7(supports-color@10.2.2) '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.7(supports-color@10.2.2)) @@ -13617,7 +13317,7 @@ snapshots: esbuild: 0.25.12 magic-string: 0.30.21 picocolors: 1.1.1 - vite: 8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0) + vite: 8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) optionalDependencies: '@swc/core': 1.15.11(@swc/helpers@0.5.18) transitivePeerDependencies: @@ -14501,7 +14201,7 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - ws: 8.21.1 + ws: 8.21.3 xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil @@ -14571,7 +14271,7 @@ snapshots: kleur@3.0.3: {} - launch-ide@1.4.8: + launch-ide@1.4.9: dependencies: chalk: 4.1.2 dotenv: 16.6.1 @@ -14689,7 +14389,7 @@ snapshots: dependencies: picomatch: 4.0.5 string-argv: 0.3.2 - tinyexec: 1.2.4 + tinyexec: 1.3.0 optionalDependencies: yaml: 2.9.0 @@ -15392,7 +15092,7 @@ snapshots: '@oxc-resolver/binding-win32-arm64-msvc': 11.24.2 '@oxc-resolver/binding-win32-x64-msvc': 11.24.2 - oxfmt@0.57.0(vite-plus@0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.12)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(yaml@2.9.0)): + oxfmt@0.57.0(vite-plus@0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.13)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(yaml@2.9.0)): dependencies: tinypool: 2.1.0 optionalDependencies: @@ -15415,10 +15115,10 @@ snapshots: '@oxfmt/binding-win32-arm64-msvc': 0.57.0 '@oxfmt/binding-win32-ia32-msvc': 0.57.0 '@oxfmt/binding-win32-x64-msvc': 0.57.0 - vite-plus: 0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.12)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(yaml@2.9.0) + vite-plus: 0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.13)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(yaml@2.9.0) optional: true - oxfmt@0.58.0(vite-plus@0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.12)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(yaml@2.9.0)): + oxfmt@0.58.0(vite-plus@0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.13)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(yaml@2.9.0)): dependencies: tinypool: 2.1.0 optionalDependencies: @@ -15441,7 +15141,7 @@ snapshots: '@oxfmt/binding-win32-arm64-msvc': 0.58.0 '@oxfmt/binding-win32-ia32-msvc': 0.58.0 '@oxfmt/binding-win32-x64-msvc': 0.58.0 - vite-plus: 0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.12)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(yaml@2.9.0) + vite-plus: 0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.13)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(yaml@2.9.0) oxlint-plugin-react-doctor@0.9.12: dependencies: @@ -15462,7 +15162,7 @@ snapshots: '@oxlint-tsgolint/win32-x64': 0.24.0 optional: true - oxlint@1.72.0(oxlint-tsgolint@0.24.0)(vite-plus@0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.12)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(yaml@2.9.0)): + oxlint@1.72.0(oxlint-tsgolint@0.24.0)(vite-plus@0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.13)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(yaml@2.9.0)): optionalDependencies: '@oxlint/binding-android-arm-eabi': 1.72.0 '@oxlint/binding-android-arm64': 1.72.0 @@ -15484,10 +15184,10 @@ snapshots: '@oxlint/binding-win32-ia32-msvc': 1.72.0 '@oxlint/binding-win32-x64-msvc': 1.72.0 oxlint-tsgolint: 0.24.0 - vite-plus: 0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.12)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(yaml@2.9.0) + vite-plus: 0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.13)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(yaml@2.9.0) optional: true - oxlint@1.77.0(oxlint-tsgolint@0.24.0)(vite-plus@0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.12)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(yaml@2.9.0)): + oxlint@1.77.0(oxlint-tsgolint@0.24.0)(vite-plus@0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.13)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(yaml@2.9.0)): optionalDependencies: '@oxlint/binding-android-arm-eabi': 1.77.0 '@oxlint/binding-android-arm64': 1.77.0 @@ -15509,9 +15209,9 @@ snapshots: '@oxlint/binding-win32-ia32-msvc': 1.77.0 '@oxlint/binding-win32-x64-msvc': 1.77.0 oxlint-tsgolint: 0.24.0 - vite-plus: 0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.12)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(yaml@2.9.0) + vite-plus: 0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.13)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(yaml@2.9.0) - oxlint@1.78.0(oxlint-tsgolint@0.24.0)(vite-plus@0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.12)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(yaml@2.9.0)): + oxlint@1.78.0(oxlint-tsgolint@0.24.0)(vite-plus@0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.13)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(yaml@2.9.0)): optionalDependencies: '@oxlint/binding-android-arm-eabi': 1.78.0 '@oxlint/binding-android-arm64': 1.78.0 @@ -15533,7 +15233,7 @@ snapshots: '@oxlint/binding-win32-ia32-msvc': 1.78.0 '@oxlint/binding-win32-x64-msvc': 1.78.0 oxlint-tsgolint: 0.24.0 - vite-plus: 0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.12)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(yaml@2.9.0) + vite-plus: 0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.13)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(yaml@2.9.0) p-cancelable@2.1.1: {} @@ -15577,8 +15277,6 @@ snapshots: dependencies: quansync: 0.2.11 - package-manager-detector@1.6.0: {} - package-manager-detector@1.8.0: {} parse-entities@4.0.2: @@ -15886,7 +15584,7 @@ snapshots: date-fns-jalali: 4.1.0-0 react: 19.2.8 - react-doctor@0.9.12(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2))(oxlint-tsgolint@0.24.0)(supports-color@10.2.2)(vite-plus@0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.12)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(yaml@2.9.0)): + react-doctor@0.9.12(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2))(oxlint-tsgolint@0.24.0)(supports-color@10.2.2)(vite-plus@0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.13)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(yaml@2.9.0)): dependencies: '@astrojs/compiler': 4.0.0 '@sentry/node': 10.65.0(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(supports-color@10.2.2) @@ -15898,7 +15596,7 @@ snapshots: jiti: 2.7.0 magicast: 0.5.3 oxc-resolver: 11.24.2 - oxlint: 1.77.0(oxlint-tsgolint@0.24.0)(vite-plus@0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.12)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(yaml@2.9.0)) + oxlint: 1.77.0(oxlint-tsgolint@0.24.0)(vite-plus@0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.13)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(yaml@2.9.0)) oxlint-plugin-react-doctor: 0.9.12 prompts: 2.4.2 typescript: 5.9.3 @@ -15948,7 +15646,7 @@ snapshots: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) - react-scan@0.5.7(patch_hash=3db48cf39ae98df6cc3c03ce20b2445004e89a6dd46e078b75ca8e795db568a9)(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(esbuild@0.28.1)(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2))(oxlint-tsgolint@0.24.0)(preact-render-to-string@6.6.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(rolldown@1.2.4)(rollup@4.57.1)(supports-color@10.2.2)(vite-plus@0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.12)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(yaml@2.9.0))(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)): + react-scan@0.5.7(patch_hash=3db48cf39ae98df6cc3c03ce20b2445004e89a6dd46e078b75ca8e795db568a9)(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(esbuild@0.28.1)(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2))(oxlint-tsgolint@0.24.0)(preact-render-to-string@6.6.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(rolldown@1.2.4)(rollup@4.57.1)(supports-color@10.2.2)(vite-plus@0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.13)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(yaml@2.9.0))(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)): dependencies: '@babel/core': 7.29.7(supports-color@10.2.2) '@babel/types': 7.29.7 @@ -15960,12 +15658,12 @@ snapshots: preact: 10.29.7(preact-render-to-string@6.6.5) prompts: 2.4.2 react: 19.2.8 - react-doctor: 0.9.12(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2))(oxlint-tsgolint@0.24.0)(supports-color@10.2.2)(vite-plus@0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.12)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(yaml@2.9.0)) + react-doctor: 0.9.12(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2))(oxlint-tsgolint@0.24.0)(supports-color@10.2.2)(vite-plus@0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.13)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(yaml@2.9.0)) react-dom: 19.2.8(react@19.2.8) react-grab: 0.2.0(react@19.2.8) optionalDependencies: esbuild: 0.28.1 - unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.57.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.57.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) transitivePeerDependencies: - '@farmfe/core' - '@opentelemetry/core' @@ -16010,17 +15708,6 @@ snapshots: readdirp@5.0.0: {} - redis@6.2.1(@opentelemetry/api@1.9.0): - dependencies: - '@redis/bloom': 6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.0)) - '@redis/client': 6.2.1(@opentelemetry/api@1.9.0) - '@redis/json': 6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.0)) - '@redis/search': 6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.0)) - '@redis/time-series': 6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.0)) - transitivePeerDependencies: - - '@node-rs/xxhash' - - '@opentelemetry/api' - redis@6.2.1(@opentelemetry/api@1.9.1): dependencies: '@redis/bloom': 6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.1)) @@ -16147,12 +15834,12 @@ snapshots: robust-predicates@3.0.3: {} - rolldown-plugin-dts@0.27.14(oxc-resolver@11.24.2)(rolldown@1.2.0)(typescript@7.0.2): + rolldown-plugin-dts@0.27.14(oxc-resolver@11.24.2)(rolldown@1.2.4)(typescript@7.0.2): dependencies: dts-resolver: 3.0.0(oxc-resolver@11.24.2) get-tsconfig: 5.0.0-beta.5 obug: 2.1.4 - rolldown: 1.2.0 + rolldown: 1.2.4 yuku-ast: 0.8.7 yuku-codegen: 0.8.7 yuku-parser: 0.8.7 @@ -16161,27 +15848,6 @@ snapshots: transitivePeerDependencies: - oxc-resolver - rolldown@1.2.0: - dependencies: - '@oxc-project/types': 0.140.0 - '@rolldown/pluginutils': 1.0.1 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.2.0 - '@rolldown/binding-darwin-arm64': 1.2.0 - '@rolldown/binding-darwin-x64': 1.2.0 - '@rolldown/binding-freebsd-x64': 1.2.0 - '@rolldown/binding-linux-arm-gnueabihf': 1.2.0 - '@rolldown/binding-linux-arm64-gnu': 1.2.0 - '@rolldown/binding-linux-arm64-musl': 1.2.0 - '@rolldown/binding-linux-ppc64-gnu': 1.2.0 - '@rolldown/binding-linux-s390x-gnu': 1.2.0 - '@rolldown/binding-linux-x64-gnu': 1.2.0 - '@rolldown/binding-linux-x64-musl': 1.2.0 - '@rolldown/binding-openharmony-arm64': 1.2.0 - '@rolldown/binding-wasm32-wasi': 1.2.0 - '@rolldown/binding-win32-arm64-msvc': 1.2.0 - '@rolldown/binding-win32-x64-msvc': 1.2.0 - rolldown@1.2.4: dependencies: '@oxc-project/types': 0.144.0 @@ -16344,17 +16010,6 @@ snapshots: shebang-regex@3.0.0: {} - shiki@4.3.1: - dependencies: - '@shikijs/core': 4.3.1 - '@shikijs/engine-javascript': 4.3.1 - '@shikijs/engine-oniguruma': 4.3.1 - '@shikijs/langs': 4.3.1 - '@shikijs/themes': 4.3.1 - '@shikijs/types': 4.3.1 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.5 - shiki@4.4.3: dependencies: '@shikijs/core': 4.4.3 @@ -16609,8 +16264,6 @@ snapshots: tinybench@2.9.0: {} - tinyexec@1.2.4: {} - tinyexec@1.3.0: {} tinyglobby@0.2.17: @@ -16665,7 +16318,7 @@ snapshots: ts-dedent@2.3.0: {} - tsdown@0.22.14(oxc-resolver@11.24.2)(publint@0.3.23)(tsx@4.23.12)(typescript@7.0.2)(unrun@0.3.1): + tsdown@0.22.14(oxc-resolver@11.24.2)(publint@0.3.23)(tsx@4.23.13)(typescript@7.0.2)(unrun@0.3.1): dependencies: ansis: 4.3.1 cac: 7.0.0 @@ -16675,16 +16328,16 @@ snapshots: import-without-cache: 0.4.0 obug: 2.1.4 picomatch: 4.0.5 - rolldown: 1.2.0 - rolldown-plugin-dts: 0.27.14(oxc-resolver@11.24.2)(rolldown@1.2.0)(typescript@7.0.2) - tinyexec: 1.2.4 + rolldown: 1.2.4 + rolldown-plugin-dts: 0.27.14(oxc-resolver@11.24.2)(rolldown@1.2.4)(typescript@7.0.2) + tinyexec: 1.3.0 tinyglobby: 0.2.17 tree-kill: 1.2.2 unconfig-core: 7.5.0 verkit: 0.3.2 optionalDependencies: publint: 0.3.23 - tsx: 4.23.12 + tsx: 4.23.13 typescript: 7.0.2 unrun: 0.3.1 transitivePeerDependencies: @@ -16695,7 +16348,7 @@ snapshots: tslib@2.8.1: {} - tsx@4.23.12: + tsx@4.23.13: dependencies: esbuild: 0.28.1 optionalDependencies: @@ -16826,17 +16479,7 @@ snapshots: unpipe@1.0.0: {} - unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.0)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)): - dependencies: - '@jridgewell/remapping': 2.3.5 - picomatch: 4.0.5 - webpack-virtual-modules: 0.6.2 - optionalDependencies: - esbuild: 0.28.1 - rolldown: 1.2.0 - vite: 8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0) - - unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.57.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)): + unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.57.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)): dependencies: '@jridgewell/remapping': 2.3.5 picomatch: 4.0.5 @@ -16845,11 +16488,11 @@ snapshots: esbuild: 0.28.1 rolldown: 1.2.4 rollup: 4.57.1 - vite: 8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0) + vite: 8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) unrun@0.3.1: dependencies: - rolldown: 1.2.0 + rolldown: 1.2.4 unzipper@0.12.5: dependencies: @@ -16904,24 +16547,24 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-plus@0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.12)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(yaml@2.9.0): + vite-plus@0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.13)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(yaml@2.9.0): dependencies: '@oxc-project/types': 0.138.0 '@oxlint/plugins': 1.68.0 - '@vitest/browser': 4.1.10(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.10) - '@vitest/browser-preview': 4.1.10(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.10) + '@vitest/browser': 4.1.10(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(vitest@4.1.10) + '@vitest/browser-preview': 4.1.10(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(vitest@4.1.10) '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + '@vitest/mocker': 4.1.10(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 '@vitest/spy': 4.1.10 '@vitest/utils': 4.1.10 - '@voidzero-dev/vite-plus-core': 0.2.4(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(publint@0.3.23)(tsx@4.23.12)(typescript@7.0.2)(unrun@0.3.1)(yaml@2.9.0) - oxfmt: 0.57.0(vite-plus@0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.12)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(yaml@2.9.0)) - oxlint: 1.72.0(oxlint-tsgolint@0.24.0)(vite-plus@0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.12)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(yaml@2.9.0)) + '@voidzero-dev/vite-plus-core': 0.2.4(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(publint@0.3.23)(tsx@4.23.13)(typescript@7.0.2)(unrun@0.3.1)(yaml@2.9.0) + oxfmt: 0.57.0(vite-plus@0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.13)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(yaml@2.9.0)) + oxlint: 1.72.0(oxlint-tsgolint@0.24.0)(vite-plus@0.2.4(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@10.2.2))(publint@0.3.23)(tsx@4.23.13)(typescript@7.0.2)(unrun@0.3.1)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(yaml@2.9.0)) oxlint-tsgolint: 0.24.0 - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-preview@4.1.10)(jsdom@26.1.0(supports-color@10.2.2))(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-preview@4.1.10)(jsdom@26.1.0(supports-color@10.2.2))(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) optionalDependencies: '@voidzero-dev/vite-plus-darwin-arm64': 0.2.4 '@voidzero-dev/vite-plus-darwin-x64': 0.2.4 @@ -16963,7 +16606,7 @@ snapshots: - yaml optional: true - vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0): + vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0): dependencies: lightningcss: 1.33.0 picomatch: 4.0.5 @@ -16975,13 +16618,13 @@ snapshots: esbuild: 0.28.1 fsevents: 2.3.3 jiti: 2.7.0 - tsx: 4.23.12 + tsx: 4.23.13 yaml: 2.9.0 - vitest@4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/browser-preview@4.1.10)(jsdom@26.1.0(supports-color@10.2.2))(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)): + vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-preview@4.1.10)(jsdom@26.1.0(supports-color@10.2.2))(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + '@vitest/mocker': 4.1.10(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 @@ -16995,45 +16638,15 @@ snapshots: picomatch: 4.0.5 std-env: 4.2.0 tinybench: 2.9.0 - tinyexec: 1.2.4 - tinyglobby: 0.2.17 - tinyrainbow: 3.1.0 - vite: 8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0) - why-is-node-running: 2.3.0 - optionalDependencies: - '@opentelemetry/api': 1.9.0 - '@types/node': 24.13.3 - '@vitest/browser-preview': 4.1.10(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.10) - jsdom: 26.1.0(supports-color@10.2.2) - transitivePeerDependencies: - - msw - - vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-preview@4.1.10)(jsdom@26.1.0(supports-color@10.2.2))(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)): - dependencies: - '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) - '@vitest/pretty-format': 4.1.10 - '@vitest/runner': 4.1.10 - '@vitest/snapshot': 4.1.10 - '@vitest/spy': 4.1.10 - '@vitest/utils': 4.1.10 - es-module-lexer: 2.3.0 - expect-type: 1.3.0 - magic-string: 0.30.21 - obug: 2.1.4 - pathe: 2.0.3 - picomatch: 4.0.5 - std-env: 4.2.0 - tinybench: 2.9.0 - tinyexec: 1.2.4 + tinyexec: 1.3.0 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0) + vite: 8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.1 '@types/node': 24.13.3 - '@vitest/browser-preview': 4.1.10(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.10) + '@vitest/browser-preview': 4.1.10(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(vitest@4.1.10) jsdom: 26.1.0(supports-color@10.2.2) transitivePeerDependencies: - msw @@ -17117,8 +16730,6 @@ snapshots: wrappy@1.0.2: {} - ws@8.21.1: {} - ws@8.21.3: {} xml-name-validator@5.0.0: {}