diff --git a/README.md b/README.md index 659e489..19bd128 100644 --- a/README.md +++ b/README.md @@ -67,10 +67,17 @@ If you feel like the `deepsec` should look at more parts of the code, give it [t ## AI provider -When running locally, `deepsec` falls back to your existing `claude` / -`codex` subscription if you've logged in on this machine. Subscriptions -(Claude Pro/Max, ChatGPT Plus) are useful for evaluating deepsec but -generally don't have enough headroom for full repo scans. +When running locally, `deepsec` supports three built-in AI backends: + +- `claude` / `claude-agent-sdk` +- `codex` +- `cursor` + +`claude` and `codex` can route through Vercel AI Gateway, and both can +also reuse a local CLI login on your machine (`claude`, `codex`) for +evaluation workflows. Subscriptions (Claude Pro/Max, ChatGPT Plus) are +useful for trying deepsec but generally don't have enough headroom for +full repo scans. For real scans, use Vercel AI Gateway. One key covers both Claude and Codex, and the gateway's default quotas are sized for highly concurrent @@ -86,6 +93,24 @@ for the Vercel Sandbox setup. To bypass the gateway, set explicitly. Explicit values always win over the `AI_GATEWAY_API_KEY` expansion. +Cursor uses the Cursor SDK directly in local mode: + +``` +CURSOR_API_KEY=cursor_... +``` + +Its built-in default model is `composer-2.5` with Cursor's `fast` +variant disabled. For the Cursor backend, `--model` accepts raw Cursor +model ids, raw aliases, and friendly suffix slugs like +`gpt-5.4-high` or `gpt-5.4-high-1m`, which deepsec resolves against your +account's `Cursor.models.list()` catalog. Reasoning slugs do not +implicitly opt you into large context tiers; use an explicit context +option like `1m`, either alone (`gpt-5.4-1m`) or combined with other +options (`gpt-5.4-high-1m`), when you want that. Because that catalog is account-specific, +a slug that works for one user may not exist for another. Cursor support +is local-only in this release; `sandbox ... --agent cursor` is not +supported yet. + If a `process` or `revalidate` run halts because the upstream credential ran out of quota or credits, deepsec stops gracefully and tells you where to top up. Re-run the same command afterward and it picks up diff --git a/docs/configuration.md b/docs/configuration.md index e0cb6ba..91d6cf0 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -27,7 +27,7 @@ see [`samples/webapp/deepsec.config.ts`](../samples/webapp/deepsec.config.ts). | `projects` | `ProjectDeclaration[]` | The codebases deepsec knows about. | | `plugins` | `DeepsecPlugin[]` | Loaded in order; later plugins override single-slot capabilities. | | `matchers` | `{ only?: string[]; exclude?: string[] }` | Filter the matcher set used by `scan`. | -| `defaultAgent` | `string` | Default `--agent` value (`codex` or `claude`). See [models.md](models.md). | +| `defaultAgent` | `string` | Default `--agent` value (`codex`, `claude`, or `cursor`). See [models.md](models.md). | | `dataDir` | `string` | Override the `data/` directory. Defaults to `./data`. | ## ProjectDeclaration @@ -107,6 +107,7 @@ backend you're using. | `AI_GATEWAY_API_KEY` | all AI commands | Shortcut. Expands at CLI startup into `ANTHROPIC_AUTH_TOKEN` / `OPENAI_API_KEY` / `ANTHROPIC_BASE_URL` / `OPENAI_BASE_URL` — one key covers both Claude and Codex through Vercel AI Gateway. Any of those four vars set explicitly takes precedence. Falls back to `VERCEL_OIDC_TOKEN` (from `vercel env pull`) when unset, so users already authenticated for Sandbox don't need a separate gateway key. | | `ANTHROPIC_AUTH_TOKEN` | `process`, `revalidate`, `triage` (Claude backend) | API token for the Claude Agent SDK. AI Gateway-issued or Anthropic-issued. Set this if you don't use `AI_GATEWAY_API_KEY`. | | `ANTHROPIC_BASE_URL` | same | Default (when `AI_GATEWAY_API_KEY` is set): `https://ai-gateway.vercel.sh`. Set to `https://api.anthropic.com` for direct Anthropic. | +| `CURSOR_API_KEY` | `--agent cursor` | Cursor SDK API key. Required for the built-in Cursor backend. Local runs only; sandbox is not supported. | ### Optional diff --git a/docs/faq.md b/docs/faq.md index bfd88ec..61d302e 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -48,7 +48,7 @@ calibrate before committing to a full pass. `triage` is ~1¢/finding. `revalidate` is comparable to `process`. -## Should I use Claude or Codex? +## Should I use Claude, Codex, or Cursor? Both work. Different strengths: @@ -56,17 +56,20 @@ Both work. Different strengths: cross-file flows. The default. Most expensive. - **Codex (gpt-5.5):** runs in a strict sandbox (read-only, no network). Fast at grep-heavy investigations. Cheaper. +- **Cursor (`composer-2.5`):** runs through the Cursor SDK in local + read-only mode. Good if your team already uses Cursor models and wants + to pass Cursor-supported slugs directly with `--model`. Mix them. Run Claude first, then re-process unconvincing findings with -`--agent codex --reinvestigate` for a second opinion. Findings dedupe -across agents. +`--agent codex --reinvestigate` or `--agent cursor --reinvestigate` for +a second opinion. Findings dedupe across agents. -## Should I use Vercel AI Gateway or Anthropic directly? +## Should I use Vercel AI Gateway, Cursor, or a direct provider? -Either works. The gateway gives you provider failover, observability, -and zero data retention. One token covers Claude and Codex. For a quick -evaluation, use Anthropic directly. For ongoing production scanning, use -the gateway. +For `claude` and `codex`, the gateway gives you provider failover, +observability, and zero data retention. One token covers both backends. +For a quick evaluation, use Anthropic directly. For ongoing production +scanning, use the gateway. ```bash # Direct Anthropic @@ -76,11 +79,18 @@ ANTHROPIC_BASE_URL=https://api.anthropic.com # AI Gateway (recommended) ANTHROPIC_AUTH_TOKEN=vck_... ANTHROPIC_BASE_URL=https://ai-gateway.vercel.sh + +# Cursor SDK (local only) +CURSOR_API_KEY=cursor_... ``` If `claude` or `codex` is already logged in on this machine, non-sandbox runs reuse that subscription — no API key needed. +Cursor is separate from AI Gateway: it uses the Cursor SDK directly, +stays local-only for now, and does not support `deepsec sandbox ...` +yet. + See [vercel-setup.md](vercel-setup.md) for how to get a gateway key and how to wire up Vercel Sandbox auth (OIDC or access token). diff --git a/docs/models.md b/docs/models.md index f58e1d6..f3cbd1c 100644 --- a/docs/models.md +++ b/docs/models.md @@ -1,18 +1,26 @@ # Models -deepsec talks to LLMs through two interchangeable backends: +deepsec talks to LLMs through three interchangeable backends: | Backend | Default model | Used by | |-----------------------------|-----------------------|------------------------------| | `codex` (default) | `gpt-5.5` | `process`, `revalidate` | -| `claude` | `claude-opus-4-7` | `process`, `revalidate` | -| `claude` (triage) | `claude-sonnet-4-6` | `triage` (Claude-only) | +| `claude` | `claude-opus-4-8` | `process`, `revalidate` | +| `cursor` | `composer-2.5` | `process`, `revalidate` | +| `claude` (triage default) | `claude-sonnet-4-6` | `triage` | +| `cursor` (triage optional) | `composer-2.5` | `triage --agent cursor` | Both backends route through [Vercel AI Gateway](https://vercel.com/ai-gateway) by default, so a single token covers Claude **and** Codex. To use Anthropic or OpenAI directly, point `ANTHROPIC_BASE_URL` / `OPENAI_BASE_URL` at the provider. +Cursor is different: it uses the Cursor SDK directly in local mode and +authenticates with `CURSOR_API_KEY`. It does **not** route through +Vercel AI Gateway today, and sandbox execution is not supported yet. +When deepsec selects its default `composer-2.5` model, it pins Cursor's +`fast=false` variant so the default stays on the standard non-fast tier. + ## CLI selection ```bash @@ -22,14 +30,26 @@ pnpm deepsec process --project-id my-app # Claude with a specific model: pnpm deepsec process --project-id my-app --agent claude --model claude-sonnet-4-6 +# Cursor backend, default model: +pnpm deepsec process --project-id my-app --agent cursor + +# Cursor backend, raw model id from your Cursor account: +pnpm deepsec process --project-id my-app --agent cursor --model claude-4-sonnet + +# Cursor backend, friendly variant slug resolved via Cursor metadata: +pnpm deepsec process --project-id my-app --agent cursor --model gpt-5.4-high + # Codex backend, default model: pnpm deepsec process --project-id my-app --agent codex # Codex backend, specific model: pnpm deepsec process --project-id my-app --agent codex --model gpt-5.4 -# Triage uses Claude; pass a cheaper model if you want: +# Triage defaults to Claude; pass a cheaper model if you want: pnpm deepsec triage --project-id my-app --model claude-haiku-4-5 + +# Or use Cursor explicitly for triage: +pnpm deepsec triage --project-id my-app --agent cursor --model composer-2.5 ``` `--agent` and `--model` are also accepted on `revalidate`. Set the @@ -38,7 +58,7 @@ default backend project-wide via `defaultAgent` in ## Why these defaults -### `claude-opus-4-7` for `process` and `revalidate` +### `claude-opus-4-8` for `process` and `revalidate` Investigating a candidate site is a multi-step reasoning task: trace control flow, recognize an auth boundary, decide whether input is @@ -58,11 +78,44 @@ and cost for that loop. `gpt-5.5-pro` is the most careful Codex option at significantly higher cost; `gpt-5.4` and below are fine for follow-up reinvestigation passes. +### `composer-2.5` for the Cursor backend + +Cursor runs through the Cursor SDK in local read-only mode. For deepsec's +workflow, `composer-2.5` is the right default: strong reasoning, broad +availability on Cursor accounts, and a stable default path. deepsec also +disables Cursor's `fast` variant for this default, so bare +`composer-2.5` means the standard non-fast tier unless you explicitly +choose another slug. + +For Cursor specifically, deepsec accepts three `--model` forms: + +1. raw Cursor model ids, like `claude-4-sonnet` +2. raw Cursor aliases, like `gpt` +3. friendly suffix slugs, like `gpt-5.4-high` or `gpt-5.4-high-1m` + +Friendly suffix slugs are resolved against your account's live +`Cursor.models.list()` catalog. deepsec first checks exact ids and exact +aliases, then resolves known suffix slugs to a `{ id, params }` +selection. For example, `gpt-5.4-high` becomes the `gpt-5.4` model with +the matching Cursor parameters for the `high` reasoning preset, while +keeping large context windows as explicit opt-ins. If you want the large +context tier, pass a dedicated slug such as `gpt-5.4-1m`, or combine +options directly as `gpt-5.4-high-1m`. deepsec splits the suffix on `-` +and matches each option token against Cursor's discovered parameter +options, so `gpt-5.4-1m-high` also resolves correctly. + +Because deepsec does not maintain a hardcoded allowlist here, the source +of truth is your account itself. A slug that works for one user may not +exist for another if their Cursor account lacks that model or variant. +If you're unsure which ids, aliases, or suffix slugs you have, inspect +the SDK's `Cursor.models.list()` output outside of deepsec. + ### `claude-sonnet-4-6` for `triage` Triage buckets findings into P0/P1/P2/skip without re-reading the code — it just looks at the finding text. That's a cheap task; Opus is -overkill. Sonnet keeps `triage` at ~1¢/finding. +overkill. Sonnet keeps `triage` at ~1¢/finding, so it stays the default +when you don't pass `--agent`. ## Refusals @@ -109,7 +162,7 @@ Two small integration points: 1. **The model identifier** — whatever string the provider's SDK accepts. deepsec passes it through unchanged. No code change needed - to *use* a new model on either backend. + to *use* a new model on Claude, Codex, or Cursor. 2. **Pricing for the cost-per-batch readout.** The Claude Agent SDK reports cost natively, so new Claude-family models drop in with zero code changes. Codex doesn't, so add a line to diff --git a/packages/deepsec/build.mjs b/packages/deepsec/build.mjs index 577e418..8362a63 100644 --- a/packages/deepsec/build.mjs +++ b/packages/deepsec/build.mjs @@ -12,6 +12,7 @@ const repoRoot = resolve(__dirname, "../.."); // bundles its own esbuild — re-bundling it produces broken output). const external = [ "@anthropic-ai/claude-agent-sdk", + "@cursor/sdk", "@openai/codex", "@openai/codex-sdk", "@vercel/sandbox", diff --git a/packages/deepsec/package.json b/packages/deepsec/package.json index 579aadd..38614d6 100644 --- a/packages/deepsec/package.json +++ b/packages/deepsec/package.json @@ -33,6 +33,7 @@ }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.3.158", + "@cursor/sdk": "^1.0.18", "@openai/codex": "^0.125.0", "@openai/codex-sdk": "^0.125.0", "@vercel/oidc": "^3.4.0", diff --git a/packages/deepsec/src/__tests__/preflight.test.ts b/packages/deepsec/src/__tests__/preflight.test.ts index 02915f1..06457df 100644 --- a/packages/deepsec/src/__tests__/preflight.test.ts +++ b/packages/deepsec/src/__tests__/preflight.test.ts @@ -32,12 +32,14 @@ describe("assertAgentCredential", () => { beforeEach(() => { saved = { ANTHROPIC_AUTH_TOKEN: process.env.ANTHROPIC_AUTH_TOKEN, + CURSOR_API_KEY: process.env.CURSOR_API_KEY, OPENAI_API_KEY: process.env.OPENAI_API_KEY, CLAUDE_HOME: process.env.CLAUDE_HOME, CODEX_HOME: process.env.CODEX_HOME, PATH: process.env.PATH, }; delete process.env.ANTHROPIC_AUTH_TOKEN; + delete process.env.CURSOR_API_KEY; delete process.env.OPENAI_API_KEY; // Point CLAUDE_HOME / CODEX_HOME and PATH at empty tmp dirs so the // suite is hermetic — the dev running tests may have a real @@ -91,6 +93,22 @@ describe("assertAgentCredential", () => { expect(() => assertAgentCredential("codex")).not.toThrow(); }); + it("passes for cursor when CURSOR_API_KEY is set", () => { + process.env.CURSOR_API_KEY = "cursor_x"; + expect(() => assertAgentCredential("cursor")).not.toThrow(); + }); + + it("throws actionable message for cursor when CURSOR_API_KEY is missing", () => { + expect(() => assertAgentCredential("cursor")).toThrow(/--agent cursor/); + expect(() => assertAgentCredential("cursor")).toThrow(/CURSOR_API_KEY/); + expect(() => assertAgentCredential("cursor")).toThrow(/cursor\.com\/dashboard\/integrations/); + }); + + it("rejects cursor in sandbox mode even when CURSOR_API_KEY is set", () => { + process.env.CURSOR_API_KEY = "cursor_x"; + expect(() => assertAgentCredential("cursor", { inSandbox: true })).toThrow(/local runs only/); + }); + it("passes for codex when only ANTHROPIC token is set (gateway fallback)", () => { process.env.ANTHROPIC_AUTH_TOKEN = "x"; expect(() => assertAgentCredential("codex")).not.toThrow(); diff --git a/packages/deepsec/src/__tests__/resolve-agent-type.test.ts b/packages/deepsec/src/__tests__/resolve-agent-type.test.ts index 8b3c48d..e55f6ff 100644 --- a/packages/deepsec/src/__tests__/resolve-agent-type.test.ts +++ b/packages/deepsec/src/__tests__/resolve-agent-type.test.ts @@ -21,11 +21,20 @@ describe("resolveAgentType", () => { expect(resolveAgentType(undefined)).toBe("claude-agent-sdk"); }); + it("passes cursor through unchanged", () => { + expect(resolveAgentType("cursor")).toBe("cursor"); + }); + it("falls back to defaultAgent from config when not provided", () => { setLoadedConfig(defineConfig({ projects: [], defaultAgent: "codex" })); expect(resolveAgentType(undefined)).toBe("codex"); }); + it("falls back to cursor from config when set", () => { + setLoadedConfig(defineConfig({ projects: [], defaultAgent: "cursor" })); + expect(resolveAgentType(undefined)).toBe("cursor"); + }); + it("falls back to codex when neither is set", () => { setLoadedConfig(defineConfig({ projects: [] })); expect(resolveAgentType(undefined)).toBe("codex"); diff --git a/packages/deepsec/src/agent-defaults.ts b/packages/deepsec/src/agent-defaults.ts index 0a6b97a..132c0dd 100644 --- a/packages/deepsec/src/agent-defaults.ts +++ b/packages/deepsec/src/agent-defaults.ts @@ -6,6 +6,8 @@ export function defaultModelForAgent(agentType: string): string { switch (agentType) { case "codex": return "gpt-5.5"; + case "cursor": + return "composer-2.5"; default: return "claude-opus-4-8"; } diff --git a/packages/deepsec/src/cli.ts b/packages/deepsec/src/cli.ts index ddc5489..8f7cd8a 100755 --- a/packages/deepsec/src/cli.ts +++ b/packages/deepsec/src/cli.ts @@ -132,11 +132,11 @@ program .option("--run-id ", "Resume a specific processing run") .option( "--agent ", - "Agent plugin type: codex or claude (default: defaultAgent in deepsec.config.ts, else codex)", + "Agent plugin type: codex, claude, or cursor (default: defaultAgent in deepsec.config.ts, else codex)", ) .option( "--model ", - "Model to use (default: claude-opus-4-8 for claude, gpt-5.5 for codex)", + "Model to use (default: claude-opus-4-8 for claude, gpt-5.5 for codex, composer-2.5 for cursor; Cursor also accepts aliases and combined option slugs like gpt-5.4-high-1m)", ) .option("--max-turns ", "Max conversation turns per batch (default: 150)", parseInt) .option( @@ -192,11 +192,11 @@ program .option("--run-id ", "Resume a specific revalidation run") .option( "--agent ", - "Agent plugin type: codex or claude (default: defaultAgent in deepsec.config.ts, else codex)", + "Agent plugin type: codex, claude, or cursor (default: defaultAgent in deepsec.config.ts, else codex)", ) .option( "--model ", - "Model to use (default: claude-opus-4-8 for claude, gpt-5.5 for codex)", + "Model to use (default: claude-opus-4-8 for claude, gpt-5.5 for codex, composer-2.5 for cursor; Cursor also accepts aliases and combined option slugs like gpt-5.4-high-1m)", ) .option("--max-turns ", "Max conversation turns per batch (default: 150)", parseInt) .option( @@ -238,7 +238,11 @@ program "Project identifier (default: the only project in deepsec.config.ts; required if there are multiple)", ) .option("--severity ", "Severity to triage (default: MEDIUM)", "MEDIUM") - .option("--model ", "Model to use (default: claude-sonnet-4-6 — cheaper)") + .option("--agent ", "Triage backend: claude or cursor (default: claude)") + .option( + "--model ", + "Model to use (default: claude-sonnet-4-6 for claude, composer-2.5 for cursor; Cursor also accepts aliases and combined option slugs like gpt-5.4-high-1m)", + ) .option("--force", "Re-triage already-triaged findings") .option("--limit ", "Max findings to triage", parseInt) .option("--concurrency ", "Parallel triage batches (default: cores - 1)", parseInt) diff --git a/packages/deepsec/src/commands/triage.ts b/packages/deepsec/src/commands/triage.ts index 37f1d1f..9591f0f 100644 --- a/packages/deepsec/src/commands/triage.ts +++ b/packages/deepsec/src/commands/triage.ts @@ -1,13 +1,16 @@ import type { Severity } from "@deepsec/core"; import { readProjectConfig } from "@deepsec/core"; import { triage } from "@deepsec/processor"; +import { defaultModelForAgent } from "../agent-defaults.js"; import { BOLD, CYAN, DIM, GREEN, RED, RESET, YELLOW } from "../formatters.js"; import { assertAgentCredential } from "../preflight.js"; +import { resolveAgentType } from "../resolve-agent-type.js"; import { resolveProjectId } from "../resolve-project-id.js"; export async function triageCommand(opts: { projectId?: string; severity?: string; + agent?: string; force?: boolean; limit?: number; concurrency?: number; @@ -16,21 +19,27 @@ export async function triageCommand(opts: { const projectId = resolveProjectId(opts.projectId); readProjectConfig(projectId); const severity = (opts.severity ?? "MEDIUM") as Severity; - const model = opts.model ?? "claude-sonnet-4-6"; + const agentType = resolveAgentType(opts.agent ?? "claude"); + if (agentType !== "claude-agent-sdk" && agentType !== "cursor") { + throw new Error("triage currently supports only --agent claude or --agent cursor"); + } + const model = + opts.model ?? (agentType === "cursor" ? defaultModelForAgent("cursor") : "claude-sonnet-4-6"); - // Triage uses Anthropic directly — no codex path here. - assertAgentCredential("claude-agent-sdk"); + assertAgentCredential(agentType); console.log( `${BOLD}Triaging${RESET} ${severity} findings for project ${BOLD}${projectId}${RESET}`, ); - console.log(` Model: ${model} (lightweight — no code reading)`); + console.log(` Agent: ${agentType} (${model})`); + console.log(` ${DIM}Lightweight path — triage uses finding text only.${RESET}`); if (opts.force) console.log(` ${YELLOW}Force re-triaging already-triaged findings${RESET}`); console.log(); const result = await triage({ projectId, severity, + agentType, force: opts.force, limit: opts.limit, concurrency: opts.concurrency, diff --git a/packages/deepsec/src/preflight.ts b/packages/deepsec/src/preflight.ts index 30ef742..c8eea4b 100644 --- a/packages/deepsec/src/preflight.ts +++ b/packages/deepsec/src/preflight.ts @@ -81,6 +81,10 @@ function isCodex(agentType: string | undefined): boolean { return agentType === "codex"; } +function isCursor(agentType: string | undefined): boolean { + return agentType === "cursor"; +} + /** * Walk `$PATH` looking for a binary. Used as a positive signal that an * agent CLI (`claude`, `codex`) is set up on this host — if it's @@ -138,7 +142,7 @@ function hasLocalCodexAgent(): boolean { // via plugins (deepsec.config.ts → plugins: [{ agents: [...] }]) handle // their own credential resolution, so we skip the check for anything // other than these. -const KNOWN_BACKENDS = new Set(["claude-agent-sdk", "codex"]); +const KNOWN_BACKENDS = new Set(["claude-agent-sdk", "codex", "cursor"]); /** * Verify the orchestrator has an AI credential the chosen agent can use. @@ -163,6 +167,26 @@ export function assertAgentCredential( const anthropic = process.env.ANTHROPIC_AUTH_TOKEN; const openai = process.env.OPENAI_API_KEY; + const cursor = process.env.CURSOR_API_KEY; + + if (isCursor(agentType)) { + if (options.inSandbox) { + throw new Error( + `The built-in cursor provider currently supports local runs only.\n` + + `\n` + + ` Use --agent cursor with local \`process\`, \`revalidate\`, or \`triage\`.\n` + + ` Sandbox execution is not supported for Cursor yet.`, + ); + } + if (cursor) return; + throw new Error( + `Missing AI credentials for --agent cursor.\n` + + `\n` + + ` Add to .env.local: CURSOR_API_KEY=cursor_…\n` + + ` Note: Cursor runs locally only today (no sandbox support).\n` + + ` Setup: https://cursor.com/dashboard/integrations`, + ); + } if (isCodex(agentType)) { // Codex prefers OPENAI_API_KEY; AI Gateway issues a single token that diff --git a/packages/deepsec/src/quota-message.ts b/packages/deepsec/src/quota-message.ts index 4fb61b0..c1e3b34 100644 --- a/packages/deepsec/src/quota-message.ts +++ b/packages/deepsec/src/quota-message.ts @@ -39,6 +39,8 @@ function sourceLabel(source: QuotaSource): string { return "Claude Pro/Max subscription"; case "anthropic-credits": return "Anthropic API credits"; + case "cursor-quota": + return "Cursor API quota"; case "openai-quota": return "OpenAI API quota"; case "openai-subscription": @@ -96,6 +98,12 @@ export function renderQuotaMessage(args: { lines.push(""); lines.push(` Or set ${BOLD}AI_GATEWAY_API_KEY=vck_…${RESET} in .env.local.`); lines.push(` Setup: ${SETUP_DOC_URL}`); + } else if (source === "cursor-quota") { + lines.push(""); + lines.push(" Your Cursor API key hit a usage or billing limit."); + lines.push(""); + lines.push(" Check your Cursor dashboard or team billing, then rerun the command."); + lines.push(" If you want AI Gateway-backed billing, switch to the claude/codex backend."); } else { // anthropic-credits / openai-quota / unknown const accountPhrase = diff --git a/packages/deepsec/src/resolve-agent-type.ts b/packages/deepsec/src/resolve-agent-type.ts index 3932a5b..1822928 100644 --- a/packages/deepsec/src/resolve-agent-type.ts +++ b/packages/deepsec/src/resolve-agent-type.ts @@ -10,5 +10,10 @@ import { getConfig } from "@deepsec/core"; */ export function resolveAgentType(provided: string | undefined): string { const resolved = provided ?? getConfig()?.defaultAgent ?? "codex"; - return resolved === "claude" ? "claude-agent-sdk" : resolved; + switch (resolved) { + case "claude": + return "claude-agent-sdk"; + default: + return resolved; + } } diff --git a/packages/processor/package.json b/packages/processor/package.json index 45df1a1..cf2decd 100644 --- a/packages/processor/package.json +++ b/packages/processor/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.3.158", + "@cursor/sdk": "^1.0.18", "@deepsec/core": "workspace:*", "@deepsec/scanner": "workspace:*", "@openai/codex": "^0.125.0", diff --git a/packages/processor/src/__tests__/cursor-model.test.ts b/packages/processor/src/__tests__/cursor-model.test.ts new file mode 100644 index 0000000..f433450 --- /dev/null +++ b/packages/processor/src/__tests__/cursor-model.test.ts @@ -0,0 +1,292 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const { listMock } = vi.hoisted(() => ({ + listMock: vi.fn(), +})); + +vi.mock("@cursor/sdk", () => ({ + Cursor: { + models: { + list: listMock, + }, + }, +})); + +import { + __resetCursorModelCatalogCacheForTests, + resolveCursorModelSelection, +} from "../agents/cursor-model.js"; + +const cursorCatalog = [ + { + id: "composer-2.5", + displayName: "Composer 2.5", + aliases: ["composer"], + parameters: [ + { + id: "fast", + displayName: "Fast", + values: [{ value: "false" }, { value: "true", displayName: "Fast" }], + }, + ], + variants: [ + { + params: [{ id: "fast", value: "true" }], + displayName: "Composer 2.5", + isDefault: true, + }, + { + params: [{ id: "fast", value: "false" }], + displayName: "Composer 2.5", + }, + ], + }, + { + id: "gpt-5.4", + displayName: "GPT-5.4", + aliases: ["gpt"], + parameters: [ + { + id: "context", + displayName: "Context", + values: [ + { value: "272k", displayName: "272K" }, + { value: "1m", displayName: "1M" }, + ], + }, + { + id: "reasoning", + displayName: "Reasoning", + values: [ + { value: "low", displayName: "Low" }, + { value: "medium", displayName: "Medium" }, + { value: "high", displayName: "High" }, + { value: "extra-high", displayName: "Extra High" }, + ], + }, + { + id: "fast", + displayName: "Fast", + values: [{ value: "false" }, { value: "true", displayName: "Fast" }], + }, + ], + variants: [ + { + params: [ + { id: "context", value: "1m" }, + { id: "reasoning", value: "medium" }, + { id: "fast", value: "false" }, + ], + displayName: "GPT-5.4", + isDefault: true, + }, + { + params: [ + { id: "context", value: "272k" }, + { id: "reasoning", value: "high" }, + { id: "fast", value: "false" }, + ], + displayName: "GPT-5.4", + }, + { + params: [ + { id: "context", value: "272k" }, + { id: "reasoning", value: "extra-high" }, + { id: "fast", value: "false" }, + ], + displayName: "GPT-5.4", + }, + { + params: [ + { id: "context", value: "272k" }, + { id: "reasoning", value: "medium" }, + { id: "fast", value: "true" }, + ], + displayName: "GPT-5.4", + }, + ], + }, + { + id: "gpt-5.4-mini", + displayName: "GPT-5.4 Mini", + aliases: ["gpt-mini"], + parameters: [ + { + id: "reasoning", + displayName: "Reasoning", + values: [{ value: "medium", displayName: "Medium" }], + }, + ], + variants: [ + { + params: [{ id: "reasoning", value: "medium" }], + displayName: "GPT-5.4 Mini", + isDefault: true, + }, + ], + }, +]; + +describe("resolveCursorModelSelection", () => { + beforeEach(() => { + listMock.mockReset(); + __resetCursorModelCatalogCacheForTests(); + listMock.mockResolvedValue(cursorCatalog); + }); + + afterEach(() => { + __resetCursorModelCatalogCacheForTests(); + }); + + it("keeps composer-2.5 on the non-fast default", async () => { + await expect(resolveCursorModelSelection("composer-2.5")).resolves.toEqual({ + id: "composer-2.5", + params: [{ id: "fast", value: "false" }], + }); + }); + + it("passes through exact model ids", async () => { + await expect(resolveCursorModelSelection("gpt-5.4-mini")).resolves.toEqual({ + id: "gpt-5.4-mini", + }); + }); + + it("passes through exact aliases", async () => { + await expect(resolveCursorModelSelection("gpt")).resolves.toEqual({ + id: "gpt-5.4", + }); + }); + + it("resolves a suffix slug to the matching default-preserving variant", async () => { + await expect(resolveCursorModelSelection("gpt-5.4-high")).resolves.toEqual({ + id: "gpt-5.4", + params: [ + { id: "context", value: "272k" }, + { id: "reasoning", value: "high" }, + { id: "fast", value: "false" }, + ], + }); + }); + + it("accepts multiword option slugs from Cursor display names", async () => { + await expect(resolveCursorModelSelection("gpt-5.4-extra-high")).resolves.toEqual({ + id: "gpt-5.4", + params: [ + { id: "context", value: "272k" }, + { id: "reasoning", value: "extra-high" }, + { id: "fast", value: "false" }, + ], + }); + }); + + it("keeps large context as an explicit opt-in slug", async () => { + await expect(resolveCursorModelSelection("gpt-5.4-1m")).resolves.toEqual({ + id: "gpt-5.4", + params: [ + { id: "context", value: "1m" }, + { id: "reasoning", value: "medium" }, + { id: "fast", value: "false" }, + ], + }); + }); + + it("combines multiple option slugs when they target different parameters", async () => { + await expect(resolveCursorModelSelection("gpt-5.4-high-1m")).resolves.toEqual({ + id: "gpt-5.4", + params: [ + { id: "context", value: "1m" }, + { id: "reasoning", value: "high" }, + { id: "fast", value: "false" }, + ], + }); + }); + + it("accepts combined options in any order", async () => { + await expect(resolveCursorModelSelection("gpt-5.4-1m-high")).resolves.toEqual({ + id: "gpt-5.4", + params: [ + { id: "context", value: "1m" }, + { id: "reasoning", value: "high" }, + { id: "fast", value: "false" }, + ], + }); + }); + + it("lists supported suffixes for a known base model", async () => { + await expect(resolveCursorModelSelection("gpt-5.4-ultra")).rejects.toThrow( + /Supported options for `gpt-5\.4`:/, + ); + await expect(resolveCursorModelSelection("gpt-5.4-ultra")).rejects.toThrow( + /`high`/, + ); + await expect(resolveCursorModelSelection("gpt-5.4-ultra")).rejects.toThrow( + /`extra-high`/, + ); + }); + + it("surfaces ambiguous suffixes instead of guessing", async () => { + listMock.mockResolvedValue([ + { + id: "ambiguous-1.0", + displayName: "Ambiguous 1.0", + parameters: [ + { + id: "reasoning", + displayName: "Reasoning", + values: [ + { value: "low", displayName: "Low" }, + { value: "high", displayName: "High" }, + ], + }, + { + id: "effort", + displayName: "Effort", + values: [ + { value: "low", displayName: "Low" }, + { value: "high", displayName: "High" }, + ], + }, + ], + variants: [ + { + params: [ + { id: "reasoning", value: "low" }, + { id: "effort", value: "low" }, + ], + displayName: "Ambiguous 1.0", + isDefault: true, + }, + { + params: [ + { id: "reasoning", value: "high" }, + { id: "effort", value: "low" }, + ], + displayName: "Ambiguous 1.0", + }, + { + params: [ + { id: "reasoning", value: "low" }, + { id: "effort", value: "high" }, + ], + displayName: "Ambiguous 1.0", + }, + ], + }, + ]); + __resetCursorModelCatalogCacheForTests(); + + await expect(resolveCursorModelSelection("ambiguous-1.0-high")).rejects.toThrow( + /Cursor variant slug `ambiguous-1\.0-high` is ambiguous/, + ); + }); + + it("caches the catalog per api key", async () => { + await resolveCursorModelSelection("gpt-5.4-high", { apiKey: "key-1" }); + await resolveCursorModelSelection("gpt-5.4-extra-high", { apiKey: "key-1" }); + await resolveCursorModelSelection("gpt-5.4-mini", { apiKey: "key-2" }); + + expect(listMock).toHaveBeenCalledTimes(2); + expect(listMock).toHaveBeenNthCalledWith(1, { apiKey: "key-1" }); + expect(listMock).toHaveBeenNthCalledWith(2, { apiKey: "key-2" }); + }); +}); diff --git a/packages/processor/src/__tests__/cursor-sdk.test.ts b/packages/processor/src/__tests__/cursor-sdk.test.ts new file mode 100644 index 0000000..bc1b6eb --- /dev/null +++ b/packages/processor/src/__tests__/cursor-sdk.test.ts @@ -0,0 +1,334 @@ +import type { FileRecord } from "@deepsec/core"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const { createMock, listMock } = vi.hoisted(() => ({ + createMock: vi.fn(), + listMock: vi.fn(), +})); + +vi.mock("@cursor/sdk", () => { + class CursorAgentError extends Error { + readonly isRetryable: boolean; + + constructor(message: string, options?: { isRetryable?: boolean }) { + super(message); + this.name = "CursorAgentError"; + this.isRetryable = options?.isRetryable ?? false; + } + } + + return { + Agent: { + create: createMock, + }, + Cursor: { + models: { + list: listMock, + }, + }, + CursorAgentError, + }; +}); + +import { __resetCursorModelCatalogCacheForTests } from "../agents/cursor-model.js"; +import { CursorAgentSdkPlugin } from "../agents/cursor-sdk.js"; + +function makeRecord(filePath: string): FileRecord { + return { + filePath, + projectId: "proj", + candidates: [ + { + vulnSlug: "auth-bypass", + lineNumbers: [12], + snippet: "dangerous()", + matchedPattern: "dangerous", + }, + ], + lastScannedAt: new Date().toISOString(), + lastScannedRunId: "scan-1", + fileHash: "hash", + findings: [], + analysisHistory: [], + status: "pending", + }; +} + +async function collectGenerator(gen: AsyncGenerator) { + const events: TProgress[] = []; + let next = await gen.next(); + while (!next.done) { + events.push(next.value); + next = await gen.next(); + } + return { events, result: next.value }; +} + +function makeRun(resultText: string, messages: unknown[] = []) { + return { + id: "run-1", + stream: async function* () { + for (const message of messages) yield message; + }, + wait: vi.fn().mockResolvedValue({ + id: "run-1", + status: "finished", + result: resultText, + durationMs: 321, + }), + }; +} + +function makeAgent(params: { + mainResult: string; + followUpResult?: string; + messages?: unknown[]; +}) { + const asyncDispose = vi.fn().mockResolvedValue(undefined); + const send = vi + .fn() + .mockResolvedValueOnce(makeRun(params.mainResult, params.messages)) + .mockResolvedValueOnce( + makeRun(params.followUpResult ?? '{"refused": false, "skipped": []}'), + ); + return { + agentId: "cursor-agent-1", + send, + close: vi.fn(), + reload: vi.fn(), + [Symbol.asyncDispose]: asyncDispose, + }; +} + +const cursorCatalog = [ + { + id: "composer-2.5", + displayName: "Composer 2.5", + aliases: ["composer"], + parameters: [ + { + id: "fast", + displayName: "Fast", + values: [{ value: "false" }, { value: "true", displayName: "Fast" }], + }, + ], + variants: [ + { + params: [{ id: "fast", value: "true" }], + displayName: "Composer 2.5", + isDefault: true, + }, + { + params: [{ id: "fast", value: "false" }], + displayName: "Composer 2.5", + }, + ], + }, + { + id: "gpt-5.4", + displayName: "GPT-5.4", + parameters: [ + { + id: "context", + displayName: "Context", + values: [ + { value: "272k", displayName: "272K" }, + { value: "1m", displayName: "1M" }, + ], + }, + { + id: "reasoning", + displayName: "Reasoning", + values: [ + { value: "medium", displayName: "Medium" }, + { value: "high", displayName: "High" }, + ], + }, + { + id: "fast", + displayName: "Fast", + values: [{ value: "false" }, { value: "true", displayName: "Fast" }], + }, + ], + variants: [ + { + params: [ + { id: "context", value: "1m" }, + { id: "reasoning", value: "medium" }, + { id: "fast", value: "false" }, + ], + displayName: "GPT-5.4", + isDefault: true, + }, + { + params: [ + { id: "context", value: "272k" }, + { id: "reasoning", value: "high" }, + { id: "fast", value: "false" }, + ], + displayName: "GPT-5.4", + }, + ], + }, +]; + +describe("CursorAgentSdkPlugin", () => { + let prevCursorApiKey: string | undefined; + + beforeEach(() => { + prevCursorApiKey = process.env.CURSOR_API_KEY; + delete process.env.CURSOR_API_KEY; + createMock.mockReset(); + listMock.mockReset(); + listMock.mockResolvedValue(cursorCatalog); + __resetCursorModelCatalogCacheForTests(); + }); + + afterEach(() => { + if (prevCursorApiKey === undefined) delete process.env.CURSOR_API_KEY; + else process.env.CURSOR_API_KEY = prevCursorApiKey; + __resetCursorModelCatalogCacheForTests(); + }); + + it("investigate() uses composer-2.5 by default and parses findings", async () => { + const agent = makeAgent({ + mainResult: `\`\`\`json +[ + { + "filePath": "src/app.ts", + "findings": [ + { + "severity": "HIGH", + "vulnSlug": "auth-bypass", + "title": "missing auth", + "description": "route is unprotected", + "lineNumbers": [12], + "recommendation": "add auth", + "confidence": "high" + } + ] + } +] +\`\`\``, + messages: [ + { type: "thinking", agent_id: "a", run_id: "r", text: "Reading target files" }, + { + type: "tool_call", + agent_id: "a", + run_id: "r", + call_id: "c1", + name: "ReadFile", + status: "completed", + args: { path: "src/app.ts" }, + }, + ], + }); + createMock.mockResolvedValue(agent); + + const plugin = new CursorAgentSdkPlugin(); + const { events, result } = await collectGenerator( + plugin.investigate({ + batch: [makeRecord("src/app.ts")], + projectRoot: "/repo", + promptTemplate: "Investigate carefully.", + projectInfo: "", + config: {}, + }), + ); + + expect(createMock).toHaveBeenCalledWith( + expect.objectContaining({ + apiKey: undefined, + mode: "plan", + model: { id: "composer-2.5", params: [{ id: "fast", value: "false" }] }, + local: expect.objectContaining({ cwd: "/repo" }), + }), + ); + expect(agent.send).toHaveBeenNthCalledWith( + 1, + expect.stringContaining("Do not use `CreatePlan` or any planning tool."), + expect.objectContaining({ + mode: "plan", + model: { id: "composer-2.5", params: [{ id: "fast", value: "false" }] }, + }), + ); + expect(result.results).toHaveLength(1); + expect(result.results[0].filePath).toBe("src/app.ts"); + expect(result.results[0].findings[0].title).toBe("missing auth"); + expect(result.meta.agentSessionId).toBe("cursor-agent-1"); + expect(events.some((event) => (event as { type: string }).type === "tool_use")).toBe(true); + expect(agent[Symbol.asyncDispose]).toHaveBeenCalled(); + }); + + it("revalidate() resolves combined Cursor option slugs and parses verdicts", async () => { + const agent = makeAgent({ + mainResult: `\`\`\`json +[ + { + "filePath": "src/app.ts", + "title": "missing auth", + "verdict": "true-positive", + "reasoning": "request reaches the handler without auth middleware" + } +] +\`\`\``, + }); + createMock.mockResolvedValue(agent); + + const plugin = new CursorAgentSdkPlugin(); + const { result } = await collectGenerator( + plugin.revalidate({ + batch: [ + { + ...makeRecord("src/app.ts"), + status: "analyzed", + findings: [ + { + severity: "HIGH", + vulnSlug: "auth-bypass", + title: "missing auth", + description: "route is unprotected", + lineNumbers: [12], + recommendation: "add auth", + confidence: "high", + }, + ], + }, + ], + projectRoot: "/repo", + projectInfo: "API service", + config: { model: "gpt-5.4-high-1m" }, + }), + ); + + expect(createMock).toHaveBeenCalledWith( + expect.objectContaining({ + model: { + id: "gpt-5.4", + params: [ + { id: "context", value: "1m" }, + { id: "reasoning", value: "high" }, + { id: "fast", value: "false" }, + ], + }, + }), + ); + expect(agent.send).toHaveBeenNthCalledWith( + 1, + expect.any(String), + expect.objectContaining({ + model: { + id: "gpt-5.4", + params: [ + { id: "context", value: "1m" }, + { id: "reasoning", value: "high" }, + { id: "fast", value: "false" }, + ], + }, + }), + ); + expect(result.verdicts).toHaveLength(1); + expect(result.verdicts[0].verdict).toBe("true-positive"); + expect(result.meta.agentSessionId).toBe("cursor-agent-1"); + }); +}); diff --git a/packages/processor/src/__tests__/registry.test.ts b/packages/processor/src/__tests__/registry.test.ts index 744e48a..c9b40e9 100644 --- a/packages/processor/src/__tests__/registry.test.ts +++ b/packages/processor/src/__tests__/registry.test.ts @@ -56,6 +56,7 @@ describe("createDefaultAgentRegistry", () => { const registry = createDefaultAgentRegistry(); expect(registry.get("claude-agent-sdk")).toBeDefined(); expect(registry.get("codex")).toBeDefined(); - expect(registry.types().sort()).toEqual(["claude-agent-sdk", "codex"]); + expect(registry.get("cursor")).toBeDefined(); + expect(registry.types().sort()).toEqual(["claude-agent-sdk", "codex", "cursor"]); }); }); diff --git a/packages/processor/src/__tests__/triage.test.ts b/packages/processor/src/__tests__/triage.test.ts new file mode 100644 index 0000000..6cce66c --- /dev/null +++ b/packages/processor/src/__tests__/triage.test.ts @@ -0,0 +1,303 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import type { FileRecord } from "@deepsec/core"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const { queryMock, promptMock, listMock } = vi.hoisted(() => ({ + queryMock: vi.fn(), + promptMock: vi.fn(), + listMock: vi.fn(), +})); + +vi.mock("@anthropic-ai/claude-agent-sdk", () => ({ + query: queryMock, +})); + +vi.mock("@cursor/sdk", () => ({ + Agent: { + prompt: promptMock, + }, + Cursor: { + models: { + list: listMock, + }, + }, +})); + +import { __resetCursorModelCatalogCacheForTests } from "../agents/cursor-model.js"; +import { triage } from "../triage.js"; + +interface Fixture { + tmp: string; + targetRoot: string; + projectId: string; + dataRoot: string; + readRecord: (relPath: string) => FileRecord; +} + +function setupProject(): Fixture { + const projectId = "triage-proj"; + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "deepsec-triage-")); + const targetRoot = path.join(tmp, "target"); + const dataRoot = path.join(tmp, "data"); + fs.mkdirSync(path.join(dataRoot, projectId, "files", "src"), { recursive: true }); + fs.mkdirSync(path.join(targetRoot, "src"), { recursive: true }); + fs.writeFileSync(path.join(targetRoot, "src/app.ts"), "// test file\n"); + fs.writeFileSync( + path.join(dataRoot, projectId, "project.json"), + JSON.stringify({ + projectId, + rootPath: targetRoot, + createdAt: new Date().toISOString(), + }), + ); + fs.writeFileSync(path.join(dataRoot, projectId, "INFO.md"), "Auth-sensitive service."); + const record: FileRecord = { + filePath: "src/app.ts", + projectId, + candidates: [], + lastScannedAt: new Date().toISOString(), + lastScannedRunId: "scan-1", + fileHash: "hash", + findings: [ + { + severity: "MEDIUM", + vulnSlug: "auth-bypass", + title: "missing auth check", + description: "route is reachable without auth", + lineNumbers: [10], + recommendation: "add auth middleware", + confidence: "high", + }, + ], + analysisHistory: [], + status: "analyzed", + }; + fs.writeFileSync( + path.join(dataRoot, projectId, "files", "src", "app.ts.json"), + JSON.stringify(record), + ); + process.env.DEEPSEC_DATA_ROOT = dataRoot; + return { + tmp, + targetRoot, + projectId, + dataRoot, + readRecord(relPath: string) { + return JSON.parse( + fs.readFileSync(path.join(dataRoot, projectId, "files", `${relPath}.json`), "utf-8"), + ); + }, + }; +} + +const cursorCatalog = [ + { + id: "composer-2.5", + displayName: "Composer 2.5", + aliases: ["composer"], + parameters: [ + { + id: "fast", + displayName: "Fast", + values: [{ value: "false" }, { value: "true", displayName: "Fast" }], + }, + ], + variants: [ + { + params: [{ id: "fast", value: "true" }], + displayName: "Composer 2.5", + isDefault: true, + }, + { + params: [{ id: "fast", value: "false" }], + displayName: "Composer 2.5", + }, + ], + }, + { + id: "gpt-5.4", + displayName: "GPT-5.4", + parameters: [ + { + id: "context", + displayName: "Context", + values: [ + { value: "272k", displayName: "272K" }, + { value: "1m", displayName: "1M" }, + ], + }, + { + id: "reasoning", + displayName: "Reasoning", + values: [ + { value: "medium", displayName: "Medium" }, + { value: "high", displayName: "High" }, + ], + }, + { + id: "fast", + displayName: "Fast", + values: [{ value: "false" }, { value: "true", displayName: "Fast" }], + }, + ], + variants: [ + { + params: [ + { id: "context", value: "1m" }, + { id: "reasoning", value: "medium" }, + { id: "fast", value: "false" }, + ], + displayName: "GPT-5.4", + isDefault: true, + }, + { + params: [ + { id: "context", value: "272k" }, + { id: "reasoning", value: "high" }, + { id: "fast", value: "false" }, + ], + displayName: "GPT-5.4", + }, + ], + }, +]; + +describe("triage", () => { + let prevDataRoot: string | undefined; + + beforeEach(() => { + prevDataRoot = process.env.DEEPSEC_DATA_ROOT; + queryMock.mockReset(); + promptMock.mockReset(); + listMock.mockReset(); + listMock.mockResolvedValue(cursorCatalog); + __resetCursorModelCatalogCacheForTests(); + }); + + afterEach(() => { + if (prevDataRoot === undefined) delete process.env.DEEPSEC_DATA_ROOT; + else process.env.DEEPSEC_DATA_ROOT = prevDataRoot; + __resetCursorModelCatalogCacheForTests(); + }); + + it("supports the cursor backend", async () => { + const fx = setupProject(); + promptMock.mockResolvedValue({ + status: "finished", + result: `\`\`\`json +[ + { + "title": "missing auth check", + "priority": "P1", + "exploitability": "moderate", + "impact": "high", + "reasoning": "Auth is missing but exploitation still depends on route reachability." + } +] +\`\`\``, + }); + + const result = await triage({ + projectId: fx.projectId, + severity: "MEDIUM", + agentType: "cursor", + }); + + expect(promptMock).toHaveBeenCalledWith( + expect.stringContaining("Do not use `CreatePlan` or any planning tool."), + expect.objectContaining({ + mode: "plan", + model: { id: "composer-2.5", params: [{ id: "fast", value: "false" }] }, + local: expect.objectContaining({ cwd: fx.targetRoot }), + }), + ); + expect(result.triaged).toBe(1); + expect(result.p1).toBe(1); + const record = fx.readRecord("src/app.ts"); + expect(record.findings[0].triage?.priority).toBe("P1"); + expect(record.findings[0].triage?.model).toBe("composer-2.5"); + fs.rmSync(fx.tmp, { recursive: true, force: true }); + }); + + it("resolves combined Cursor option slugs for the cursor backend", async () => { + const fx = setupProject(); + promptMock.mockResolvedValue({ + status: "finished", + result: `\`\`\`json +[ + { + "title": "missing auth check", + "priority": "P1", + "exploitability": "moderate", + "impact": "high", + "reasoning": "Auth is missing but exploitation still depends on route reachability." + } +] +\`\`\``, + }); + + const result = await triage({ + projectId: fx.projectId, + severity: "MEDIUM", + agentType: "cursor", + model: "gpt-5.4-high-1m", + }); + + expect(promptMock).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + mode: "plan", + model: { + id: "gpt-5.4", + params: [ + { id: "context", value: "1m" }, + { id: "reasoning", value: "high" }, + { id: "fast", value: "false" }, + ], + }, + local: expect.objectContaining({ cwd: fx.targetRoot }), + }), + ); + expect(result.triaged).toBe(1); + const record = fx.readRecord("src/app.ts"); + expect(record.findings[0].triage?.model).toBe("gpt-5.4-high-1m"); + fs.rmSync(fx.tmp, { recursive: true, force: true }); + }); + + it("keeps the existing claude path as the default", async () => { + const fx = setupProject(); + queryMock.mockImplementation(async function* () { + yield { + type: "result", + subtype: "success", + result: `\`\`\`json +[ + { + "title": "missing auth check", + "priority": "P0", + "exploitability": "trivial", + "impact": "critical", + "reasoning": "The finding is externally reachable and immediately exploitable." + } +] +\`\`\``, + }; + }); + + const result = await triage({ + projectId: fx.projectId, + severity: "MEDIUM", + }); + + expect(queryMock).toHaveBeenCalledTimes(1); + expect(promptMock).not.toHaveBeenCalled(); + expect(result.triaged).toBe(1); + expect(result.p0).toBe(1); + const record = fx.readRecord("src/app.ts"); + expect(record.findings[0].triage?.priority).toBe("P0"); + expect(record.findings[0].triage?.model).toBe("claude-sonnet-4-6"); + fs.rmSync(fx.tmp, { recursive: true, force: true }); + }); +}); diff --git a/packages/processor/src/agents/cursor-model.ts b/packages/processor/src/agents/cursor-model.ts new file mode 100644 index 0000000..f52817b --- /dev/null +++ b/packages/processor/src/agents/cursor-model.ts @@ -0,0 +1,642 @@ +import { + Cursor, + type ModelListItem, + type ModelParameterDefinition, + type ModelParameterValue, + type ModelSelection, +} from "@cursor/sdk"; + +export const DEFAULT_CURSOR_MODEL = "composer-2.5"; + +const DEFAULT_CURSOR_MODEL_PARAMS: ModelParameterValue[] = [{ id: "fast", value: "false" }]; +const DEFAULT_CATALOG_CACHE_KEY = "__cursor-default__"; + +interface CursorModelCatalog { + modelsById: Map; + aliasesToId: Map; + ambiguousAliases: Map; + slugsToSelection: Map; + ambiguousSlugs: Map; + supportedSlugsByModelId: Map; + modelOptionsById: Map; + supportedOptionsByModelId: Map; +} + +interface CursorResolutionOptions { + apiKey?: string; +} + +interface CursorModelOptionMatch { + parameterId: string; + value: string; + canonicalToken: string; +} + +interface CursorModelOptions { + baseline: Map; + optionsByToken: Map; +} + +const cursorModelCatalogCache = new Map>(); + +function cloneParamValues(params: ModelParameterValue[] | undefined): ModelParameterValue[] | undefined { + return params?.map((param) => ({ ...param })); +} + +function cloneModelSelection(selection: ModelSelection): ModelSelection { + return { + id: selection.id, + params: cloneParamValues(selection.params), + }; +} + +function serializeModelSelection(selection: ModelSelection): string { + return JSON.stringify({ + id: selection.id, + params: [...(selection.params ?? [])] + .map((param) => ({ id: param.id, value: param.value })) + .sort((a, b) => a.id.localeCompare(b.id) || a.value.localeCompare(b.value)), + }); +} + +function normalizeCursorSlugPart(value: string | undefined): string | undefined { + if (!value) return undefined; + const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""); + return normalized || undefined; +} + +function getParameterValueDefinition( + parameter: ModelParameterDefinition, + value: string, +): { value: string; displayName?: string } | undefined { + return parameter.values.find((entry) => entry.value === value); +} + +function getCanonicalOptionToken(parameter: ModelParameterDefinition, value: string): string { + const definition = getParameterValueDefinition(parameter, value); + return ( + normalizeCursorSlugPart(definition?.displayName) ?? + normalizeCursorSlugPart(definition?.value) ?? + normalizeCursorSlugPart(value) ?? + value + ); +} + +function getOptionLookupTokens(parameter: ModelParameterDefinition, value: string): string[] { + const definition = getParameterValueDefinition(parameter, value); + const tokens = new Set(); + const canonical = getCanonicalOptionToken(parameter, value); + if (canonical) tokens.add(canonical); + const rawValue = normalizeCursorSlugPart(definition?.value ?? value); + if (rawValue) tokens.add(rawValue); + const displayName = normalizeCursorSlugPart(definition?.displayName); + if (displayName) tokens.add(displayName); + return [...tokens]; +} + +function buildExactModelSelection(input: string, resolvedId: string): ModelSelection { + if (input === DEFAULT_CURSOR_MODEL) { + return { + id: resolvedId, + params: cloneParamValues(DEFAULT_CURSOR_MODEL_PARAMS), + }; + } + + return { id: resolvedId }; +} + +function getEffectiveDefaultParams(model: ModelListItem): ModelParameterValue[] | undefined { + if (model.id === DEFAULT_CURSOR_MODEL) { + return cloneParamValues(DEFAULT_CURSOR_MODEL_PARAMS); + } + + const defaultVariant = model.variants?.find((variant) => variant.isDefault) ?? model.variants?.[0]; + return cloneParamValues(defaultVariant?.params); +} + +function parseContextWindowSize(value: string): number | undefined { + const match = value.trim().toLowerCase().match(/^(\d+(?:\.\d+)?)(k|m|g)$/); + if (!match) return undefined; + const amount = Number(match[1]); + if (!Number.isFinite(amount)) return undefined; + const multiplier = match[2] === "k" ? 1_000 : match[2] === "m" ? 1_000_000 : 1_000_000_000; + return amount * multiplier; +} + +function pickConservativeParameterValue( + parameter: ModelParameterDefinition, + defaultValue: string | undefined, +): string | undefined { + if (parameter.id === "fast" && parameter.values.some((entry) => entry.value === "false")) { + return "false"; + } + + if (parameter.id === "context") { + const sizedValues = parameter.values + .map((entry) => ({ + value: entry.value, + size: parseContextWindowSize(entry.value), + })) + .filter((entry): entry is { value: string; size: number } => entry.size !== undefined) + .sort((a, b) => a.size - b.size); + if (sizedValues.length > 0) { + return sizedValues[0].value; + } + } + + return defaultValue ?? (parameter.values.length === 1 ? parameter.values[0].value : undefined); +} + +function buildParamMap(params: ModelParameterValue[] | undefined): Map { + return new Map((params ?? []).map((param) => [param.id, param.value])); +} + +function buildOrderedParams( + model: ModelListItem, + valuesById: Map, +): ModelParameterValue[] { + const ordered: ModelParameterValue[] = []; + const seen = new Set(); + + for (const parameter of model.parameters ?? []) { + const value = valuesById.get(parameter.id); + if (value === undefined) continue; + ordered.push({ id: parameter.id, value }); + seen.add(parameter.id); + } + + for (const [id, value] of valuesById) { + if (seen.has(id)) continue; + ordered.push({ id, value }); + } + + return ordered; +} + +function buildConservativeBaselineParams(model: ModelListItem): Map { + const baseline = buildParamMap(getEffectiveDefaultParams(model)); + + for (const parameter of model.parameters ?? []) { + const preferredValue = pickConservativeParameterValue(parameter, baseline.get(parameter.id)); + if (preferredValue !== undefined) { + baseline.set(parameter.id, preferredValue); + } + } + + return baseline; +} + +function scoreVariantAgainstBaseline( + variant: { params: ModelParameterValue[] }, + baseline: Map, + requestedParamIds: Set, +): number { + const variantById = buildParamMap(variant.params); + const keys = new Set([...baseline.keys(), ...variantById.keys()]); + let score = 0; + + for (const key of keys) { + if (requestedParamIds.has(key)) continue; + if (variantById.get(key) !== baseline.get(key)) { + score++; + } + } + + return score; +} + +function buildRequestedSelection( + model: ModelListItem, + baseline: Map, + requestedParams: Map, +): ModelSelection { + const requestedBaseline = new Map(baseline); + for (const [id, value] of requestedParams) { + requestedBaseline.set(id, value); + } + const requestedParamIds = new Set(requestedParams.keys()); + + const matchingVariants = (model.variants ?? []) + .filter((variant) => + [...requestedParams].every(([requestedId, requestedValue]) => + variant.params.some((param) => param.id === requestedId && param.value === requestedValue), + ), + ) + .sort((a, b) => { + const scoreDiff = + scoreVariantAgainstBaseline(a, requestedBaseline, requestedParamIds) - + scoreVariantAgainstBaseline(b, requestedBaseline, requestedParamIds); + if (scoreDiff !== 0) return scoreDiff; + return a.params.length - b.params.length; + }); + + if (matchingVariants.length > 0) { + return { + id: model.id, + params: cloneParamValues(matchingVariants[0].params), + }; + } + + return { + id: model.id, + params: buildOrderedParams(model, requestedBaseline), + }; +} + +function buildParameterSlugSelection( + model: ModelListItem, + baseline: Map, + requestedParam: ModelParameterValue, +): ModelSelection { + return buildRequestedSelection( + model, + baseline, + new Map([[requestedParam.id, requestedParam.value]]), + ); +} + +function addSelectionCandidate( + candidates: Map>, + slug: string, + selection: ModelSelection, +): void { + let bySelectionKey = candidates.get(slug); + if (!bySelectionKey) { + bySelectionKey = new Map(); + candidates.set(slug, bySelectionKey); + } + bySelectionKey.set(serializeModelSelection(selection), cloneModelSelection(selection)); +} + +function addSlugSelection( + candidates: Map>, + supportedSlugs: Map>, + modelId: string, + optionTokens: string[], + canonicalOption: string, + selection: ModelSelection, +): void { + const displaySlug = `${modelId}-${canonicalOption}`; + let supported = supportedSlugs.get(modelId); + if (!supported) { + supported = new Set(); + supportedSlugs.set(modelId, supported); + } + supported.add(displaySlug); + + for (const token of optionTokens) { + addSelectionCandidate(candidates, `${modelId}-${token}`, selection); + } +} + +function addOptionMatch( + optionsByToken: Map, + token: string, + match: CursorModelOptionMatch, +): void { + const existing = optionsByToken.get(token); + if (existing) { + existing.push(match); + } else { + optionsByToken.set(token, [match]); + } +} + +function buildCursorModelOptions(model: ModelListItem): CursorModelOptions { + const baseline = buildConservativeBaselineParams(model); + const optionsByToken = new Map(); + + for (const parameter of model.parameters ?? []) { + for (const value of parameter.values) { + const canonicalToken = getCanonicalOptionToken(parameter, value.value); + const match: CursorModelOptionMatch = { + parameterId: parameter.id, + value: value.value, + canonicalToken, + }; + for (const token of getOptionLookupTokens(parameter, value.value)) { + addOptionMatch(optionsByToken, token, match); + } + } + } + + return { + baseline, + optionsByToken, + }; +} + +function addParameterSlugSelections( + model: ModelListItem, + candidates: Map>, + supportedSlugs: Map>, +): void { + const baseline = buildConservativeBaselineParams(model); + + for (const parameter of model.parameters ?? []) { + for (const value of parameter.values) { + const optionTokens = getOptionLookupTokens(parameter, value.value); + const canonicalOption = getCanonicalOptionToken(parameter, value.value); + + addSlugSelection( + candidates, + supportedSlugs, + model.id, + optionTokens, + canonicalOption, + buildParameterSlugSelection(model, baseline, { + id: parameter.id, + value: value.value, + }), + ); + } + } +} + +function buildCursorModelCatalog(models: ModelListItem[]): CursorModelCatalog { + const modelsById = new Map(); + const aliasCandidates = new Map>(); + const slugCandidates = new Map>(); + const supportedSlugSets = new Map>(); + const modelOptionsById = new Map(); + + for (const model of models) { + modelsById.set(model.id, model); + modelOptionsById.set(model.id, buildCursorModelOptions(model)); + } + + for (const model of models) { + for (const alias of model.aliases ?? []) { + let ids = aliasCandidates.get(alias); + if (!ids) { + ids = new Set(); + aliasCandidates.set(alias, ids); + } + ids.add(model.id); + } + + addParameterSlugSelections(model, slugCandidates, supportedSlugSets); + } + + const aliasesToId = new Map(); + const ambiguousAliases = new Map(); + for (const [alias, ids] of aliasCandidates) { + const matches = [...ids].sort(); + if (matches.length === 1) aliasesToId.set(alias, matches[0]); + else ambiguousAliases.set(alias, matches); + } + + const slugsToSelection = new Map(); + const ambiguousSlugs = new Map(); + for (const [slug, selectionsByKey] of slugCandidates) { + const matches = [...selectionsByKey.values()]; + if (matches.length === 1) { + slugsToSelection.set(slug, matches[0]); + } else { + ambiguousSlugs.set(slug, matches); + } + } + + const supportedSlugsByModelId = new Map(); + for (const [modelId, slugs] of supportedSlugSets) { + supportedSlugsByModelId.set(modelId, [...slugs].sort()); + } + + const supportedOptionsByModelId = new Map(); + for (const [modelId, options] of modelOptionsById) { + supportedOptionsByModelId.set( + modelId, + [...new Set([...options.optionsByToken.values()].flat().map((match) => match.canonicalToken))].sort(), + ); + } + + return { + modelsById, + aliasesToId, + ambiguousAliases, + slugsToSelection, + ambiguousSlugs, + supportedSlugsByModelId, + modelOptionsById, + supportedOptionsByModelId, + }; +} + +export async function loadCursorModelCatalog( + apiKey = process.env.CURSOR_API_KEY, +): Promise { + const cacheKey = apiKey ?? DEFAULT_CATALOG_CACHE_KEY; + const cached = cursorModelCatalogCache.get(cacheKey); + if (cached) return cached; + + const pending = Cursor.models + .list({ apiKey }) + .then((models) => buildCursorModelCatalog(models)) + .catch((error) => { + cursorModelCatalogCache.delete(cacheKey); + throw error; + }); + + cursorModelCatalogCache.set(cacheKey, pending); + return pending; +} + +export function splitCursorVariantSuffix( + model: string, +): { baseModel: string; option: string } | undefined { + const lastDash = model.lastIndexOf("-"); + if (lastDash <= 0 || lastDash === model.length - 1) return undefined; + return { + baseModel: model.slice(0, lastDash), + option: model.slice(lastDash + 1), + }; +} + +function splitCursorSlugSegments(value: string): string[] { + return value.split("-").filter(Boolean); +} + +function findCursorBaseModelCandidates( + input: string, + catalog: CursorModelCatalog, +): Array<{ baseInput: string; modelId: string; suffix: string }> { + const modelCandidates = [...catalog.modelsById.keys()] + .filter((id) => input.startsWith(`${id}-`)) + .sort((a, b) => b.length - a.length) + .map((id) => ({ + baseInput: id, + modelId: id, + suffix: input.slice(id.length + 1), + })); + + if (modelCandidates.length > 0) return modelCandidates; + + return [...catalog.aliasesToId.entries()] + .filter(([alias]) => input.startsWith(`${alias}-`)) + .sort((a, b) => b[0].length - a[0].length) + .map(([alias, modelId]) => ({ + baseInput: alias, + modelId, + suffix: input.slice(alias.length + 1), + })); +} + +function parseCursorOptionTokenizations( + segments: string[], + optionsByToken: Map, + offset = 0, +): CursorModelOptionMatch[][] { + if (offset >= segments.length) return [[]]; + + const results: CursorModelOptionMatch[][] = []; + + for (let end = offset + 1; end <= segments.length; end++) { + const token = segments.slice(offset, end).join("-"); + const matches = optionsByToken.get(token); + if (!matches?.length) continue; + + const tails = parseCursorOptionTokenizations(segments, optionsByToken, end); + for (const match of matches) { + for (const tail of tails) { + results.push([match, ...tail]); + } + } + } + + return results; +} + +function resolveCursorCompoundSlugCandidates( + input: string, + catalog: CursorModelCatalog, +): ModelSelection[] { + const selections = new Map(); + + for (const candidate of findCursorBaseModelCandidates(input, catalog)) { + const model = catalog.modelsById.get(candidate.modelId); + const options = catalog.modelOptionsById.get(candidate.modelId); + if (!model || !options) continue; + + const tokenizations = parseCursorOptionTokenizations( + splitCursorSlugSegments(candidate.suffix), + options.optionsByToken, + ); + + for (const tokenization of tokenizations) { + const requestedParams = new Map(); + let valid = true; + + for (const match of tokenization) { + const existing = requestedParams.get(match.parameterId); + if (existing && existing !== match.value) { + valid = false; + break; + } + requestedParams.set(match.parameterId, match.value); + } + + if (!valid || requestedParams.size === 0) continue; + + const selection = buildRequestedSelection(model, options.baseline, requestedParams); + selections.set(serializeModelSelection(selection), selection); + } + } + + return [...selections.values()]; +} + +function formatSelectionParams(selection: ModelSelection): string { + if (!selection.params?.length) return selection.id; + return `${selection.id} ${selection.params.map((param) => `${param.id}=${param.value}`).join(", ")}`; +} + +export function formatCursorModelResolutionError( + input: string, + catalog: CursorModelCatalog, +): string { + const ambiguousAlias = catalog.ambiguousAliases.get(input); + if (ambiguousAlias) { + return `Cursor model alias \`${input}\` is ambiguous for this account: ${ambiguousAlias + .map((id) => `\`${id}\``) + .join(", ")}. Use an explicit model id instead.`; + } + + const ambiguousSlug = catalog.ambiguousSlugs.get(input); + if (ambiguousSlug) { + return `Cursor variant slug \`${input}\` is ambiguous for this account: ${ambiguousSlug + .map((selection) => `\`${formatSelectionParams(selection)}\``) + .join(", ")}. Use a raw Cursor model id instead.`; + } + + const split = splitCursorVariantSuffix(input); + if (split) { + const baseCandidate = findCursorBaseModelCandidates(input, catalog)[0]; + const baseId = baseCandidate?.modelId; + if (baseId) { + const supportedOptions = catalog.supportedOptionsByModelId.get(baseId) ?? []; + if (supportedOptions.length > 0) { + return `Unknown Cursor variant slug \`${input}\`. Supported options for \`${baseId}\`: ${supportedOptions + .map((option) => `\`${option}\``) + .join(", ")}. You can combine multiple options with \`-\`, for example \`${baseId}-high-1m\`.`; + } + + return `Unknown Cursor variant slug \`${input}\`. \`${baseId}\` does not expose friendly suffix slugs for this account. Use a raw Cursor model id or alias instead.`; + } + } + + const knownIds = [...catalog.modelsById.keys()].sort(); + const preview = knownIds.slice(0, 8).map((id) => `\`${id}\``).join(", "); + const suffix = knownIds.length > 8 ? ", ..." : ""; + return `Unknown Cursor model or variant \`${input}\`. Use a Cursor model id, alias, or supported suffix slug from your account catalog (via \`Cursor.models.list()\`). Examples on this account: ${preview}${suffix}.`; +} + +export async function resolveCursorModelSelection( + model: string, + options: CursorResolutionOptions = {}, +): Promise { + const catalog = await loadCursorModelCatalog(options.apiKey); + + if (catalog.modelsById.has(model)) { + return buildExactModelSelection(model, model); + } + + const aliasId = catalog.aliasesToId.get(model); + if (aliasId) { + return { id: aliasId }; + } + + const slugSelection = catalog.slugsToSelection.get(model); + if (slugSelection) { + return cloneModelSelection(slugSelection); + } + + const compoundSelections = resolveCursorCompoundSlugCandidates(model, catalog); + if (compoundSelections.length === 1) { + return cloneModelSelection(compoundSelections[0]); + } + if (compoundSelections.length > 1) { + throw new Error( + `Cursor variant slug \`${model}\` is ambiguous for this account: ${compoundSelections + .map((selection) => `\`${formatSelectionParams(selection)}\``) + .join(", ")}. Use a raw Cursor model id instead.`, + ); + } + + throw new Error(formatCursorModelResolutionError(model, catalog)); +} + +export function __resetCursorModelCatalogCacheForTests(): void { + cursorModelCatalogCache.clear(); +} + +export function buildCursorReadOnlyPreamble(projectRoot: string): string { + return `## Environment + +You are running inside the Cursor SDK in local read-only mode. + +- **Project root**: \`${projectRoot}\` +- **Use Cursor read tools** like \`ReadFile\`, \`Glob\`, \`rg\`, and other non-mutating inspection tools to understand the code. +- **Do not edit files** or change the repository. This is a static-analysis task only. +- **Do not use \`CreatePlan\` or any planning tool.** This is not an implementation task. +- **Output**: reply with exactly one fenced \`\`\`json ... \`\`\` block matching the requested schema, with no prose before or after it.`; +} diff --git a/packages/processor/src/agents/cursor-sdk.ts b/packages/processor/src/agents/cursor-sdk.ts new file mode 100644 index 0000000..a72251d --- /dev/null +++ b/packages/processor/src/agents/cursor-sdk.ts @@ -0,0 +1,410 @@ +import type { RefusalReport } from "@deepsec/core"; +import { + Agent, + CursorAgentError, + type SDKAgent, + type SDKMessage, + type SDKToolUseMessage, +} from "@cursor/sdk"; +import { + backoff, + buildInvestigatePrompt, + buildRevalidatePrompt, + isTransientError, + MAX_ATTEMPTS, + parseInvestigateResults, + parseRefusalReport, + parseRevalidateVerdicts, + QuotaExhaustedError, + REFUSAL_FOLLOWUP_PROMPT, + writeParseFailureDebug, +} from "./shared.js"; +import { + buildCursorReadOnlyPreamble, + DEFAULT_CURSOR_MODEL, + resolveCursorModelSelection, +} from "./cursor-model.js"; +import type { + AgentPlugin, + AgentProgress, + BatchMeta, + InvestigateOutput, + InvestigateParams, + InvestigateResult, + RevalidateOutput, + RevalidateParams, + RevalidateVerdict, +} from "./types.js"; + +const DEFAULT_MODEL = DEFAULT_CURSOR_MODEL; +const READONLY_MODE = "plan"; + +function trimForProgress(text: string, max = 200): string { + const normalized = text.replace(/\s+/g, " ").trim(); + if (normalized.length <= max) return normalized; + return `${normalized.slice(0, max - 1)}...`; +} + +function extractToolTarget(args: unknown): string | undefined { + if (!args || typeof args !== "object") return undefined; + const record = args as Record; + for (const key of [ + "path", + "filePath", + "file_path", + "pattern", + "command", + "query", + "url", + "glob_pattern", + "target_notebook", + ]) { + const value = record[key]; + if (typeof value === "string" && value.trim()) return value.trim(); + } + return undefined; +} + +function shortTarget(target: string | undefined): string | undefined { + if (!target) return undefined; + if (target.includes("\n")) return trimForProgress(target, 120); + if (!target.includes("/")) return trimForProgress(target, 120); + return target.split("/").slice(-3).join("/"); +} + +function classifyCursorQuotaError(message: string): boolean { + const lower = message.toLowerCase(); + return ( + /\bquota\b/.test(lower) || + /\busage limit\b/.test(lower) || + /\bout of credits?\b/.test(lower) || + /\bbilling\b/.test(lower) + ); +} + +function buildCursorAgentOptions(projectRoot: string, model: { id: string; params?: { id: string; value: string }[] }) { + if (process.env.DEEPSEC_INSIDE_SANDBOX === "1") { + throw new Error( + "The built-in cursor provider currently supports local runs only. `deepsec sandbox ... --agent cursor` is not supported yet.", + ); + } + return { + apiKey: process.env.CURSOR_API_KEY, + model, + mode: READONLY_MODE, + local: { + cwd: projectRoot, + }, + } as const; +} + +function renderToolMessage(message: SDKToolUseMessage): string { + const target = shortTarget(extractToolTarget(message.args)); + const suffix = message.status === "running" ? "" : ` (${message.status})`; + return `${message.name}${target ? `: ${target}` : ""}${suffix}`; +} + +async function disposeAgent(agent: SDKAgent | undefined): Promise { + if (!agent) return; + try { + await agent[Symbol.asyncDispose](); + } catch { + try { + agent.close(); + } catch {} + } +} + +async function runRefusalFollowUp( + agent: SDKAgent, + model: { id: string; params?: { id: string; value: string }[] }, +): Promise { + try { + const run = await agent.send(REFUSAL_FOLLOWUP_PROMPT, { + model, + mode: READONLY_MODE, + }); + const result = await run.wait(); + if (result.status !== "finished" || !result.result) return undefined; + return parseRefusalReport(result.result); + } catch { + return undefined; + } +} + +interface CursorRunOutput { + resultText: string; + refusal?: RefusalReport; + meta: Partial; + durationMs: number; + turnCount: number; + toolUseCount: number; +} + +async function* runCursorPrompt(params: { + prompt: string; + projectRoot: string; + model: string; + retryLabel: string; +}): AsyncGenerator { + const { prompt, projectRoot, model, retryLabel } = params; + const startTime = Date.now(); + const resolvedModel = await resolveCursorModelSelection(model, { + apiKey: process.env.CURSOR_API_KEY, + }); + let resultText = ""; + let refusal: RefusalReport | undefined; + let meta: Partial = {}; + let turnCount = 0; + let toolUseCount = 0; + let lastError = ""; + let retryable = false; + + for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { + if (attempt > 1) { + yield { + type: "thinking", + message: `Retrying ${retryLabel} after transient error (attempt ${attempt}/${MAX_ATTEMPTS}): ${lastError.slice(0, 200)}`, + }; + resultText = ""; + refusal = undefined; + meta = {}; + turnCount = 0; + toolUseCount = 0; + lastError = ""; + retryable = false; + } + + let agent: SDKAgent | undefined; + + try { + agent = await Agent.create(buildCursorAgentOptions(projectRoot, resolvedModel)); + const run = await agent.send(prompt, { + model: resolvedModel, + mode: READONLY_MODE, + }); + + for await (const message of run.stream()) { + const sdkMessage = message as SDKMessage; + switch (sdkMessage.type) { + case "assistant": { + turnCount++; + const text = sdkMessage.message.content + .filter((block) => block.type === "text") + .map((block) => block.text) + .join(" "); + if (text.trim()) { + yield { + type: "thinking", + message: trimForProgress(text), + }; + } + break; + } + case "tool_call": + toolUseCount++; + yield { + type: "tool_use", + message: renderToolMessage(sdkMessage), + candidateFile: extractToolTarget(sdkMessage.args), + }; + break; + case "thinking": + yield { + type: "thinking", + message: trimForProgress(sdkMessage.text), + }; + break; + case "status": + if (sdkMessage.message) { + yield { + type: sdkMessage.status === "ERROR" ? "error" : "thinking", + message: trimForProgress(sdkMessage.message), + }; + } + break; + case "task": + if (sdkMessage.text) { + yield { + type: "thinking", + message: trimForProgress(sdkMessage.text), + }; + } + break; + } + } + + const final = await run.wait(); + if (final.status !== "finished" || !final.result) { + lastError = final.result?.trim() || `Cursor run ${final.status}`; + yield { + type: "error", + message: `Cursor run ${final.status}: ${lastError.slice(0, 300)}`, + }; + } else { + resultText = final.result; + meta = { + durationApiMs: final.durationMs, + numTurns: turnCount, + agentSessionId: agent.agentId, + }; + refusal = await runRefusalFollowUp(agent, resolvedModel); + } + } catch (err) { + lastError = err instanceof Error ? err.message : String(err); + if (classifyCursorQuotaError(lastError)) { + throw new QuotaExhaustedError("cursor-quota", lastError); + } + retryable = err instanceof CursorAgentError ? err.isRetryable : isTransientError(lastError); + yield { + type: "error", + message: `Cursor SDK error: ${lastError.slice(0, 300)}`, + }; + } finally { + await disposeAgent(agent); + } + + if (resultText) break; + if (classifyCursorQuotaError(lastError)) { + throw new QuotaExhaustedError("cursor-quota", lastError); + } + if (attempt >= MAX_ATTEMPTS || !retryable) break; + await backoff(attempt); + } + + if (!resultText) { + throw new Error( + `Cursor SDK produced no result after ${MAX_ATTEMPTS} attempt(s). Last error: ${lastError || "(none captured)"}.`, + ); + } + + return { + resultText, + refusal, + meta, + durationMs: Date.now() - startTime, + turnCount, + toolUseCount, + }; +} + +export class CursorAgentSdkPlugin implements AgentPlugin { + type = "cursor"; + + async *investigate(params: InvestigateParams): AsyncGenerator { + const { batch, projectRoot, promptTemplate, projectInfo, config, projectId } = params; + const model = (config.model as string) ?? DEFAULT_MODEL; + + yield { + type: "started", + message: `Investigating ${batch.length} file(s) with Cursor SDK (${model})`, + }; + + const prompt = `${buildCursorReadOnlyPreamble(projectRoot)}\n\n${buildInvestigatePrompt({ + promptTemplate, + projectInfo, + batch, + })}`; + const run = yield* runCursorPrompt({ + prompt, + projectRoot, + model, + retryLabel: "investigation batch", + }); + + if (run.refusal?.refused) { + yield { + type: "thinking", + message: `Refusal detected: ${run.refusal.reason ?? run.refusal.skipped?.map((s) => s.filePath ?? "?").join(", ") ?? "see raw"}`, + }; + } + + let results: InvestigateResult[]; + try { + results = parseInvestigateResults(run.resultText, batch); + } catch (err) { + writeParseFailureDebug({ + projectId, + phase: "investigate", + agentType: this.type, + resultText: run.resultText, + error: err, + batch, + }); + throw err; + } + + yield { + type: "complete", + message: `Investigation complete (${(run.durationMs / 1000).toFixed(1)}s, ${run.turnCount} turns, ${run.toolUseCount} tool calls${run.refusal?.refused ? " refusal" : ""})`, + }; + + return { + results, + meta: { + durationMs: run.durationMs, + ...run.meta, + refusal: run.refusal, + }, + }; + } + + async *revalidate(params: RevalidateParams): AsyncGenerator { + const { batch, projectRoot, projectInfo, config, force = false, projectId } = params; + const model = (config.model as string) ?? DEFAULT_MODEL; + const { prompt, totalFindings } = buildRevalidatePrompt({ + batch, + projectRoot, + projectInfo, + force, + }); + + yield { + type: "started", + message: `Revalidating ${totalFindings} finding(s) across ${batch.length} file(s) with Cursor SDK (${model})`, + }; + + const run = yield* runCursorPrompt({ + prompt: `${buildCursorReadOnlyPreamble(projectRoot)}\n\n${prompt}`, + projectRoot, + model, + retryLabel: "revalidation batch", + }); + + if (run.refusal?.refused) { + yield { + type: "thinking", + message: `Refusal detected during revalidation: ${run.refusal.reason ?? "see raw"}`, + }; + } + + let verdicts: RevalidateVerdict[]; + try { + verdicts = parseRevalidateVerdicts(run.resultText); + } catch (err) { + writeParseFailureDebug({ + projectId, + phase: "revalidate", + agentType: this.type, + resultText: run.resultText, + error: err, + batch, + }); + throw err; + } + + yield { + type: "complete", + message: `Revalidation complete (${(run.durationMs / 1000).toFixed(1)}s, ${run.turnCount} turns, ${verdicts.length} verdicts${run.refusal?.refused ? " refusal" : ""})`, + }; + + return { + verdicts, + meta: { + durationMs: run.durationMs, + ...run.meta, + refusal: run.refusal, + }, + }; + } +} diff --git a/packages/processor/src/agents/shared.ts b/packages/processor/src/agents/shared.ts index 40b1add..7c64a14 100644 --- a/packages/processor/src/agents/shared.ts +++ b/packages/processor/src/agents/shared.ts @@ -18,6 +18,7 @@ export const MAX_ATTEMPTS = 3; * Provider-specific signatures: * - `claude-subscription` — Claude Pro/Max weekly or 5-hour limit * - `anthropic-credits` — direct Anthropic API: credit balance too low + * - `cursor-quota` — Cursor API / Composer usage quota exhausted * - `openai-quota` — direct OpenAI API: insufficient_quota / 402 * - `openai-subscription` — ChatGPT Plus quota via `codex login` * - `gateway-credits` — Vercel AI Gateway: out of gateway credits @@ -26,6 +27,7 @@ export const MAX_ATTEMPTS = 3; export type QuotaSource = | "claude-subscription" | "anthropic-credits" + | "cursor-quota" | "openai-quota" | "openai-subscription" | "gateway-credits" diff --git a/packages/processor/src/index.ts b/packages/processor/src/index.ts index 50f1b83..c0f515a 100644 --- a/packages/processor/src/index.ts +++ b/packages/processor/src/index.ts @@ -21,6 +21,7 @@ import { import { noiseScore, readTechJson } from "@deepsec/scanner"; import { ClaudeAgentSdkPlugin } from "./agents/claude-agent-sdk.js"; import { CodexAgentSdkPlugin } from "./agents/codex-sdk.js"; +import { CursorAgentSdkPlugin } from "./agents/cursor-sdk.js"; import { AgentRegistry } from "./agents/registry.js"; import { QuotaExhaustedError, type QuotaSource } from "./agents/shared.js"; import type { @@ -36,6 +37,7 @@ import { languagesForBatch } from "./prompt/file-language.js"; export { ClaudeAgentSdkPlugin } from "./agents/claude-agent-sdk.js"; export { CodexAgentSdkPlugin } from "./agents/codex-sdk.js"; +export { CursorAgentSdkPlugin } from "./agents/cursor-sdk.js"; export { AgentRegistry } from "./agents/registry.js"; export { classifyQuotaError, @@ -61,6 +63,7 @@ export function createDefaultAgentRegistry(): AgentRegistry { const registry = new AgentRegistry(); registry.register(new ClaudeAgentSdkPlugin()); registry.register(new CodexAgentSdkPlugin()); + registry.register(new CursorAgentSdkPlugin()); // Plugins can contribute additional backends via `agents: []` in their // DeepsecPlugin export. The shape is validated by AgentRegistry at use. for (const a of getRegistry().agents as AgentPlugin[]) { diff --git a/packages/processor/src/triage.ts b/packages/processor/src/triage.ts index 5f07029..ee53eb7 100644 --- a/packages/processor/src/triage.ts +++ b/packages/processor/src/triage.ts @@ -1,6 +1,7 @@ import fs from "node:fs"; import path from "node:path"; import { query } from "@anthropic-ai/claude-agent-sdk"; +import { Agent } from "@cursor/sdk"; import type { FileRecord, Finding, Severity, TriagePriority } from "@deepsec/core"; import { completeRun, @@ -12,8 +13,65 @@ import { writeFileRecord, writeRunMeta, } from "@deepsec/core"; +import { + buildCursorReadOnlyPreamble, + DEFAULT_CURSOR_MODEL, + resolveCursorModelSelection, +} from "./agents/cursor-model.js"; const TRIAGE_BATCH_SIZE = 30; +const DEFAULT_CLAUDE_TRIAGE_MODEL = "claude-sonnet-4-6"; +const DEFAULT_CURSOR_TRIAGE_MODEL = DEFAULT_CURSOR_MODEL; +const READONLY_MODE = "plan"; + +function parseTriageVerdicts(resultText: string): TriageVerdict[] { + const jsonMatch = resultText.match(/```json\s*([\s\S]*?)```/); + const jsonStr = jsonMatch ? jsonMatch[1].trim() : resultText.trim(); + return JSON.parse(jsonStr) as TriageVerdict[]; +} + +async function runClaudeTriagePrompt(prompt: string, model: string): Promise { + let resultText = ""; + for await (const message of query({ + prompt, + options: { + allowedTools: [], + permissionMode: "dontAsk", + maxTurns: 1, + model, + }, + })) { + const msg = message as Record; + if (msg.type === "result" && msg.subtype === "success") { + resultText = msg.result; + } + } + return resultText; +} + +async function runCursorTriagePrompt( + prompt: string, + model: string, + projectRoot: string, +): Promise { + if (process.env.DEEPSEC_INSIDE_SANDBOX === "1") { + throw new Error("Cursor triage supports local runs only."); + } + const apiKey = process.env.CURSOR_API_KEY; + const resolvedModel = await resolveCursorModelSelection(model, { apiKey }); + const result = await Agent.prompt(`${buildCursorReadOnlyPreamble(projectRoot)}\n\n${prompt}`, { + apiKey, + model: resolvedModel, + mode: READONLY_MODE, + local: { + cwd: projectRoot, + }, + }); + if (result.status !== "finished" || !result.result) { + throw new Error(result.result?.trim() || `Cursor triage run ${result.status}`); + } + return result.result; +} interface TriageVerdict { title: string; @@ -34,10 +92,18 @@ export async function triage(params: { force?: boolean; limit?: number; concurrency?: number; + agentType?: string; model?: string; onProgress?: (progress: TriageProgress) => void; }): Promise<{ triaged: number; p0: number; p1: number; p2: number; skip: number }> { - const { projectId, severity = "MEDIUM", force = false, model = "claude-sonnet-4-6" } = params; + const agentType = params.agentType ?? "claude-agent-sdk"; + if (agentType !== "claude-agent-sdk" && agentType !== "cursor") { + throw new Error(`Unsupported triage agent type: ${agentType}`); + } + const model = + params.model ?? + (agentType === "cursor" ? DEFAULT_CURSOR_TRIAGE_MODEL : DEFAULT_CLAUDE_TRIAGE_MODEL); + const { projectId, severity = "MEDIUM", force = false } = params; const emit = (progress: TriageProgress) => { try { @@ -95,7 +161,7 @@ export async function triage(params: { projectId, rootPath: project.rootPath, type: "revalidate", - processorConfig: { agentType: "triage", model, modelConfig: {} }, + processorConfig: { agentType, model, modelConfig: {} }, }); writeRunMeta(meta); @@ -176,28 +242,13 @@ ${findingsList} \`\`\``; try { - let resultText = ""; - - for await (const message of query({ - prompt, - options: { - allowedTools: [], - permissionMode: "dontAsk", - maxTurns: 1, - model, - }, - })) { - const msg = message as Record; - if (msg.type === "result" && msg.subtype === "success") { - resultText = msg.result; - } - } - - const jsonMatch = resultText.match(/```json\s*([\s\S]*?)```/); - const jsonStr = jsonMatch ? jsonMatch[1].trim() : resultText.trim(); + const resultText = + agentType === "cursor" + ? await runCursorTriagePrompt(prompt, model, project.rootPath) + : await runClaudeTriagePrompt(prompt, model); let verdicts: TriageVerdict[] = []; try { - verdicts = JSON.parse(jsonStr); + verdicts = parseTriageVerdicts(resultText); } catch {} for (const verdict of verdicts) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d055f83..ad028f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,6 +42,9 @@ importers: '@anthropic-ai/claude-agent-sdk': specifier: ^0.3.158 version: 0.3.158(@anthropic-ai/sdk@0.96.0)(@modelcontextprotocol/sdk@1.29.0)(zod@4.4.3) + '@cursor/sdk': + specifier: ^1.0.18 + version: 1.0.18 '@openai/codex': specifier: ^0.125.0 version: 0.125.0 @@ -91,6 +94,9 @@ importers: '@anthropic-ai/claude-agent-sdk': specifier: ^0.3.158 version: 0.3.158(@anthropic-ai/sdk@0.96.0)(@modelcontextprotocol/sdk@1.29.0)(zod@4.4.3) + '@cursor/sdk': + specifier: ^1.0.18 + version: 1.0.18 '@deepsec/core': specifier: workspace:* version: link:../core @@ -310,6 +316,96 @@ packages: dev: true optional: true + /@bufbuild/protobuf@1.10.0: + resolution: {integrity: sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag==} + dev: false + + /@connectrpc/connect-node@1.7.0(@bufbuild/protobuf@1.10.0)(@connectrpc/connect@1.7.0): + resolution: {integrity: sha512-6vaPIkG/NyhxlYgytLoR9KYbPhczEboFB2OYWkA9qvUz1K7efXfeGrlRxoLtpa+r8VxyIOw73w5ktNe743nD+A==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@bufbuild/protobuf': ^1.10.0 + '@connectrpc/connect': 1.7.0 + dependencies: + '@bufbuild/protobuf': 1.10.0 + '@connectrpc/connect': 1.7.0(@bufbuild/protobuf@1.10.0) + undici: 5.29.0 + dev: false + + /@connectrpc/connect@1.7.0(@bufbuild/protobuf@1.10.0): + resolution: {integrity: sha512-iNKdJRi69YP3mq6AePRT8F/HrxWCewrhxnLMNm0vpqXAR8biwzRtO6Hjx80C6UvtKJ5sFmffQT7I4Baecz389w==} + peerDependencies: + '@bufbuild/protobuf': ^1.10.0 + dependencies: + '@bufbuild/protobuf': 1.10.0 + dev: false + + /@cursor/sdk-darwin-arm64@1.0.18: + resolution: {integrity: sha512-gXfNz+n3uf2hWCUcSdU/gBYT7TFlWwJjZAAjXFzBJ3jGu+l7ShQRa9QI2sEKvPNRtUMC/18EcQ26ks1o261Bnw==} + cpu: [arm64] + os: [darwin] + hasBin: true + requiresBuild: true + dev: false + optional: true + + /@cursor/sdk-darwin-x64@1.0.18: + resolution: {integrity: sha512-nzcPqCvPPnYsuz21olPgandVgGUzDZkHN4cUls4ukm7mddp6XqujFJv8lh9qKmVefl0/3UL2R6GFANggjwifnA==} + cpu: [x64] + os: [darwin] + hasBin: true + requiresBuild: true + dev: false + optional: true + + /@cursor/sdk-linux-arm64@1.0.18: + resolution: {integrity: sha512-MJyhVryN+0czP48SgMak/KA9xGneQ/gdsnDk1ZTJF+LemaFc+Tv7P1GT80h+3JBfu69kPyIX4epJVjroTMoFEA==} + cpu: [arm64] + os: [linux] + hasBin: true + requiresBuild: true + dev: false + optional: true + + /@cursor/sdk-linux-x64@1.0.18: + resolution: {integrity: sha512-IaJqcKtXxtQEdPxivQjDWbvynw3UX31V8RdagcZ6RgHOo+B/i/P95D8svSNC4V9nbTHwVbNGfIDQ5S64gMS2hg==} + cpu: [x64] + os: [linux] + hasBin: true + requiresBuild: true + dev: false + optional: true + + /@cursor/sdk-win32-x64@1.0.18: + resolution: {integrity: sha512-iqcGt1l/3xgnaT+PotY3237oyK3VnOZHtjUxSRP3Q7ahjaqsZVTnCwwljj+nal8ouvXidBHnxQLqGMSC2G6LTg==} + cpu: [x64] + os: [win32] + hasBin: true + requiresBuild: true + dev: false + optional: true + + /@cursor/sdk@1.0.18: + resolution: {integrity: sha512-ha5EGHliMuTNuAqnPHzKFNnND4y6erddTqtxwIt9/p8ER+QsmMoRcP38PwA2sXfZnGvIBiozveBZqa4XwUddCw==} + engines: {node: '>=18'} + dependencies: + '@bufbuild/protobuf': 1.10.0 + '@connectrpc/connect': 1.7.0(@bufbuild/protobuf@1.10.0) + '@connectrpc/connect-node': 1.7.0(@bufbuild/protobuf@1.10.0)(@connectrpc/connect@1.7.0) + '@statsig/js-client': 3.31.0 + sqlite3: 5.1.7 + zod: 3.25.76 + optionalDependencies: + '@cursor/sdk-darwin-arm64': 1.0.18 + '@cursor/sdk-darwin-x64': 1.0.18 + '@cursor/sdk-linux-arm64': 1.0.18 + '@cursor/sdk-linux-x64': 1.0.18 + '@cursor/sdk-win32-x64': 1.0.18 + transitivePeerDependencies: + - bluebird + - supports-color + dev: false + /@emnapi/core@1.10.0: resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} dependencies: @@ -801,6 +897,17 @@ packages: dev: true optional: true + /@fastify/busboy@2.1.1: + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} + engines: {node: '>=14'} + dev: false + + /@gar/promisify@1.1.3: + resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} + requiresBuild: true + dev: false + optional: true + /@hono/node-server@1.19.14(hono@4.12.19): resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} engines: {node: '>=18.14.1'} @@ -870,6 +977,26 @@ packages: dev: true optional: true + /@npmcli/fs@1.1.1: + resolution: {integrity: sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==} + requiresBuild: true + dependencies: + '@gar/promisify': 1.1.3 + semver: 7.8.4 + dev: false + optional: true + + /@npmcli/move-file@1.1.2: + resolution: {integrity: sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==} + engines: {node: '>=10'} + deprecated: This functionality has been moved to @npmcli/fs + requiresBuild: true + dependencies: + mkdirp: 1.0.4 + rimraf: 3.0.2 + dev: false + optional: true + /@openai/codex-sdk@0.125.0: resolution: {integrity: sha512-1xCIHdSbQVF880nJ2aVWdPIsWZbSpKODwuP9y/gvtChDYhYfYEW0DKp2H8ZlctkzIjlzS/WzYmP6ZZPHIvs2Dg==} engines: {node: '>=18'} @@ -1500,6 +1627,23 @@ packages: resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} dev: false + /@statsig/client-core@3.31.0: + resolution: {integrity: sha512-SuxQD6TmVszPG7FoMKwTk/uyBuVFk7XnxI3T/E0uyb7PL7GNjONtfsoh+NqBBVUJVse0CUeSFfgJPoZy1ZOslQ==} + dev: false + + /@statsig/js-client@3.31.0: + resolution: {integrity: sha512-LFa5E0LjT6sTfZv3sNGoyRLSZ1078+agdgOA+Vm1ecjG+KbSOfBLTW7hMwimrJ29slRwbYDzbtKaPJo/R37N2g==} + dependencies: + '@statsig/client-core': 3.31.0 + dev: false + + /@tootallnate/once@1.1.2: + resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} + engines: {node: '>= 6'} + requiresBuild: true + dev: false + optional: true + /@tybys/wasm-util@0.10.2: resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} requiresBuild: true @@ -1624,6 +1768,12 @@ packages: resolution: {integrity: sha512-8kkeoQKLDaKXefjV5dbhBj2aErfKp1Mc4pb6tj8144cF+Em5SPbyMbyLCHp+BVrFfFVCBluCtMx+jjvaFVZGww==} dev: false + /abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + requiresBuild: true + dev: false + optional: true + /accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -1632,6 +1782,36 @@ packages: negotiator: 1.0.0 dev: false + /agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + requiresBuild: true + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + dev: false + optional: true + + /agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + requiresBuild: true + dependencies: + humanize-ms: 1.2.1 + dev: false + optional: true + + /aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + requiresBuild: true + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + dev: false + optional: true + /ajv-formats@3.0.1(ajv@8.20.0): resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} peerDependencies: @@ -1655,7 +1835,6 @@ packages: /ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - dev: true /ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} @@ -1664,6 +1843,23 @@ packages: color-convert: 2.0.1 dev: true + /aproba@2.1.0: + resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==} + requiresBuild: true + dev: false + optional: true + + /are-we-there-yet@3.0.1: + resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + requiresBuild: true + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + dev: false + optional: true + /assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -1684,6 +1880,12 @@ packages: optional: true dev: false + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + requiresBuild: true + dev: false + optional: true + /balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} @@ -1698,6 +1900,24 @@ packages: optional: true dev: false + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: false + + /bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + dependencies: + file-uri-to-path: 1.0.0 + dev: false + + /bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: false + /body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} @@ -1715,6 +1935,15 @@ packages: - supports-color dev: false + /brace-expansion@1.1.15: + resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==} + requiresBuild: true + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: false + optional: true + /brace-expansion@5.0.5: resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} engines: {node: 18 || 20 || >=22} @@ -1722,6 +1951,13 @@ packages: balanced-match: 4.0.4 dev: false + /buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: false + /bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -1732,6 +1968,34 @@ packages: engines: {node: '>=8'} dev: true + /cacache@15.3.0: + resolution: {integrity: sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==} + engines: {node: '>= 10'} + requiresBuild: true + dependencies: + '@npmcli/fs': 1.1.1 + '@npmcli/move-file': 1.1.2 + chownr: 2.0.0 + fs-minipass: 2.1.0 + glob: 7.2.3 + infer-owner: 1.0.4 + lru-cache: 6.0.0 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-flush: 1.0.7 + minipass-pipeline: 1.2.4 + mkdirp: 1.0.4 + p-map: 4.0.0 + promise-inflight: 1.0.1 + rimraf: 3.0.2 + ssri: 8.0.1 + tar: 6.2.1 + unique-filename: 1.1.1 + transitivePeerDependencies: + - bluebird + dev: false + optional: true + /call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -1764,11 +2028,27 @@ packages: engines: {node: '>= 16'} dev: true + /chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + dev: false + + /chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + dev: false + /chownr@3.0.0: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} dev: false + /clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + requiresBuild: true + dev: false + optional: true + /cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -1789,11 +2069,30 @@ packages: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} dev: true + /color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + requiresBuild: true + dev: false + optional: true + /commander@13.1.0: resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} engines: {node: '>=18'} dev: true + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + requiresBuild: true + dev: false + optional: true + + /console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + requiresBuild: true + dev: false + optional: true + /content-disposition@1.1.0: resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} engines: {node: '>=18'} @@ -1847,16 +2146,39 @@ packages: dependencies: ms: 2.1.3 + /decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + dependencies: + mimic-response: 3.1.0 + dev: false + /deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} dev: true + /deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + dev: false + + /delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + requiresBuild: true + dev: false + optional: true + /depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} dev: false + /detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + dev: false + /dotenv@16.6.1: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} @@ -1886,13 +2208,39 @@ packages: /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - dev: true /encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} dev: false + /encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + requiresBuild: true + dependencies: + iconv-lite: 0.6.3 + dev: false + optional: true + + /end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + dependencies: + once: 1.4.0 + dev: false + + /env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + requiresBuild: true + dev: false + optional: true + + /err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + requiresBuild: true + dev: false + optional: true + /es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -2022,6 +2370,11 @@ packages: eventsource-parser: 3.0.8 dev: false + /expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + dev: false + /expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} @@ -2107,6 +2460,10 @@ packages: picomatch: 4.0.4 dev: true + /file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + dev: false + /finalhandler@2.1.1: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} @@ -2147,6 +2504,23 @@ packages: engines: {node: '>= 0.8'} dev: false + /fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + dev: false + + /fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + dependencies: + minipass: 3.3.6 + dev: false + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + requiresBuild: true + dev: false + optional: true + /fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2159,6 +2533,23 @@ packages: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} dev: false + /gauge@4.0.4: + resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + requiresBuild: true + dependencies: + aproba: 2.1.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + dev: false + optional: true + /get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -2194,6 +2585,10 @@ packages: resolve-pkg-maps: 1.0.0 dev: true + /github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + dev: false + /glob@11.1.0: resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} engines: {node: 20 || >=22} @@ -2208,16 +2603,42 @@ packages: path-scurry: 2.0.2 dev: false + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + requiresBuild: true + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: false + optional: true + /gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} dev: false + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + requiresBuild: true + dev: false + optional: true + /has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} dev: false + /has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + requiresBuild: true + dev: false + optional: true + /hasown@2.0.3: resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} @@ -2230,6 +2651,12 @@ packages: engines: {node: '>=16.9.0'} dev: false + /http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + requiresBuild: true + dev: false + optional: true + /http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -2241,18 +2668,98 @@ packages: toidentifier: 1.0.1 dev: false - /iconv-lite@0.7.2: - resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} - engines: {node: '>=0.10.0'} + /http-proxy-agent@4.0.1: + resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==} + engines: {node: '>= 6'} + requiresBuild: true dependencies: - safer-buffer: 2.1.2 + '@tootallnate/once': 1.1.2 + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color dev: false + optional: true - /inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + /https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + requiresBuild: true + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color dev: false + optional: true - /ip-address@10.2.0: + /humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + requiresBuild: true + dependencies: + ms: 2.1.3 + dev: false + optional: true + + /iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + requiresBuild: true + dependencies: + safer-buffer: 2.1.2 + dev: false + optional: true + + /iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: false + + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + requiresBuild: true + dev: false + optional: true + + /indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + requiresBuild: true + dev: false + optional: true + + /infer-owner@1.0.4: + resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} + requiresBuild: true + dev: false + optional: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + requiresBuild: true + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: false + optional: true + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: false + + /ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + dev: false + + /ip-address@10.2.0: resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} engines: {node: '>= 12'} dev: false @@ -2265,7 +2772,12 @@ packages: /is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - dev: true + + /is-lambda@1.0.1: + resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} + requiresBuild: true + dev: false + optional: true /is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} @@ -2347,12 +2859,48 @@ packages: engines: {node: 20 || >=22} dev: false + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + requiresBuild: true + dependencies: + yallist: 4.0.0 + dev: false + optional: true + /magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} dependencies: '@jridgewell/sourcemap-codec': 1.5.5 dev: true + /make-fetch-happen@9.1.0: + resolution: {integrity: sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==} + engines: {node: '>= 10'} + requiresBuild: true + dependencies: + agentkeepalive: 4.6.0 + cacache: 15.3.0 + http-cache-semantics: 4.2.0 + http-proxy-agent: 4.0.1 + https-proxy-agent: 5.0.1 + is-lambda: 1.0.1 + lru-cache: 6.0.0 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-fetch: 1.4.1 + minipass-flush: 1.0.7 + minipass-pipeline: 1.2.4 + negotiator: 0.6.4 + promise-retry: 2.0.1 + socks-proxy-agent: 6.2.1 + ssri: 8.0.1 + transitivePeerDependencies: + - bluebird + - supports-color + dev: false + optional: true + /math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -2380,6 +2928,11 @@ packages: mime-db: 1.54.0 dev: false + /mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + dev: false + /minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} @@ -2387,15 +2940,91 @@ packages: brace-expansion: 5.0.5 dev: false + /minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + requiresBuild: true + dependencies: + brace-expansion: 1.1.15 + dev: false + optional: true + /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - dev: true + + /minipass-collect@1.0.2: + resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} + engines: {node: '>= 8'} + requiresBuild: true + dependencies: + minipass: 3.3.6 + dev: false + optional: true + + /minipass-fetch@1.4.1: + resolution: {integrity: sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==} + engines: {node: '>=8'} + requiresBuild: true + dependencies: + minipass: 3.3.6 + minipass-sized: 1.0.3 + minizlib: 2.1.2 + optionalDependencies: + encoding: 0.1.13 + dev: false + optional: true + + /minipass-flush@1.0.7: + resolution: {integrity: sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==} + engines: {node: '>= 8'} + requiresBuild: true + dependencies: + minipass: 3.3.6 + dev: false + optional: true + + /minipass-pipeline@1.2.4: + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} + requiresBuild: true + dependencies: + minipass: 3.3.6 + dev: false + optional: true + + /minipass-sized@1.0.3: + resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} + engines: {node: '>=8'} + requiresBuild: true + dependencies: + minipass: 3.3.6 + dev: false + optional: true + + /minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + dependencies: + yallist: 4.0.0 + dev: false + + /minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + dev: false /minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} dev: false + /minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + dev: false + /minizlib@3.1.0: resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} engines: {node: '>= 18'} @@ -2403,6 +3032,16 @@ packages: minipass: 7.1.3 dev: false + /mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + dev: false + + /mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + dev: false + /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2412,11 +3051,78 @@ packages: hasBin: true dev: true + /napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + dev: false + + /negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + requiresBuild: true + dev: false + optional: true + /negotiator@1.0.0: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} dev: false + /node-abi@3.92.0: + resolution: {integrity: sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==} + engines: {node: '>=10'} + dependencies: + semver: 7.8.4 + dev: false + + /node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + dev: false + + /node-gyp@8.4.1: + resolution: {integrity: sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==} + engines: {node: '>= 10.12.0'} + hasBin: true + requiresBuild: true + dependencies: + env-paths: 2.2.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + make-fetch-happen: 9.1.0 + nopt: 5.0.0 + npmlog: 6.0.2 + rimraf: 3.0.2 + semver: 7.8.4 + tar: 6.2.1 + which: 2.0.2 + transitivePeerDependencies: + - bluebird + - supports-color + dev: false + optional: true + + /nopt@5.0.0: + resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} + engines: {node: '>=6'} + hasBin: true + requiresBuild: true + dependencies: + abbrev: 1.1.1 + dev: false + optional: true + + /npmlog@6.0.2: + resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + requiresBuild: true + dependencies: + are-we-there-yet: 3.0.1 + console-control-strings: 1.1.0 + gauge: 4.0.4 + set-blocking: 2.0.0 + dev: false + optional: true + /object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -2501,6 +3207,15 @@ packages: - '@emnapi/runtime' dev: true + /p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + requiresBuild: true + dependencies: + aggregate-error: 3.1.0 + dev: false + optional: true + /package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} dev: false @@ -2510,6 +3225,13 @@ packages: engines: {node: '>= 0.8'} dev: false + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + requiresBuild: true + dev: false + optional: true + /path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -2558,6 +3280,47 @@ packages: source-map-js: 1.2.1 dev: true + /prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. + hasBin: true + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.92.0 + pump: 3.0.4 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.4 + tunnel-agent: 0.6.0 + dev: false + + /promise-inflight@1.0.1: + resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} + requiresBuild: true + peerDependencies: + bluebird: '*' + peerDependenciesMeta: + bluebird: + optional: true + dev: false + optional: true + + /promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + requiresBuild: true + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + dev: false + optional: true + /proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -2566,6 +3329,13 @@ packages: ipaddr.js: 1.9.1 dev: false + /pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + dev: false + /qs@6.15.2: resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} engines: {node: '>=0.6'} @@ -2588,6 +3358,25 @@ packages: unpipe: 1.0.0 dev: false + /rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + dev: false + + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: false + /require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -2602,11 +3391,28 @@ packages: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} dev: true + /retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + requiresBuild: true + dev: false + optional: true + /retry@0.13.1: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} dev: false + /rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + requiresBuild: true + dependencies: + glob: 7.2.3 + dev: false + optional: true + /rollup@4.60.2: resolution: {integrity: sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -2655,10 +3461,20 @@ packages: - supports-color dev: false + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: false + /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} dev: false + /semver@7.8.4: + resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==} + engines: {node: '>=10'} + hasBin: true + dev: false + /send@1.2.1: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} @@ -2690,6 +3506,12 @@ packages: - supports-color dev: false + /set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + requiresBuild: true + dev: false + optional: true + /setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} dev: false @@ -2750,21 +3572,93 @@ packages: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} dev: true + /signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + requiresBuild: true + dev: false + optional: true + /signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} dev: false + /simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + dev: false + + /simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + dev: false + + /smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + requiresBuild: true + dev: false + optional: true + /smol-toml@1.6.1: resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} engines: {node: '>= 18'} dev: true + /socks-proxy-agent@6.2.1: + resolution: {integrity: sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==} + engines: {node: '>= 10'} + requiresBuild: true + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + socks: 2.8.9 + transitivePeerDependencies: + - supports-color + dev: false + optional: true + + /socks@2.8.9: + resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + requiresBuild: true + dependencies: + ip-address: 10.2.0 + smart-buffer: 4.2.0 + dev: false + optional: true + /source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} dev: true + /sqlite3@5.1.7: + resolution: {integrity: sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==} + requiresBuild: true + dependencies: + bindings: 1.5.0 + node-addon-api: 7.1.1 + prebuild-install: 7.1.3 + tar: 6.2.1 + optionalDependencies: + node-gyp: 8.4.1 + transitivePeerDependencies: + - bluebird + - supports-color + dev: false + + /ssri@8.0.1: + resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==} + engines: {node: '>= 8'} + requiresBuild: true + dependencies: + minipass: 3.3.6 + dev: false + optional: true + /stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} dev: true @@ -2803,14 +3697,24 @@ packages: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - dev: true + + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + requiresBuild: true + dependencies: + safe-buffer: 5.2.1 + dev: false /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} dependencies: ansi-regex: 5.0.1 - dev: true + + /strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + dev: false /strip-json-comments@5.0.3: resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} @@ -2823,6 +3727,26 @@ packages: js-tokens: 9.0.1 dev: true + /tar-fs@2.1.4: + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.4 + tar-stream: 2.2.0 + dev: false + + /tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: false + /tar-stream@3.1.7: resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} dependencies: @@ -2834,6 +3758,19 @@ packages: - react-native-b4a dev: false + /tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + dev: false + /tar@7.5.13: resolution: {integrity: sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==} engines: {node: '>=18'} @@ -2910,6 +3847,12 @@ packages: fsevents: 2.3.3 dev: true + /tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + dependencies: + safe-buffer: 5.2.1 + dev: false + /type-is@2.1.0: resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} engines: {node: '>= 18'} @@ -2934,16 +3877,44 @@ packages: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} dev: true + /undici@5.29.0: + resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} + engines: {node: '>=14.0'} + dependencies: + '@fastify/busboy': 2.1.1 + dev: false + /undici@7.25.0: resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} engines: {node: '>=20.18.1'} dev: false + /unique-filename@1.1.1: + resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==} + requiresBuild: true + dependencies: + unique-slug: 2.0.2 + dev: false + optional: true + + /unique-slug@2.0.2: + resolution: {integrity: sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==} + requiresBuild: true + dependencies: + imurmurhash: 0.1.4 + dev: false + optional: true + /unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} dev: false + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + requiresBuild: true + dev: false + /vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -3115,6 +4086,14 @@ packages: stackback: 0.0.2 dev: true + /wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + requiresBuild: true + dependencies: + string-width: 4.2.3 + dev: false + optional: true + /wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -3126,6 +4105,7 @@ packages: /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + requiresBuild: true dev: false /xdg-app-paths@5.1.0: @@ -3147,6 +4127,10 @@ packages: engines: {node: '>=10'} dev: true + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: false + /yallist@5.0.0: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'}