diff --git a/Dockerfile b/Dockerfile index c8825e2b0..b8cd633e7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ FROM node:22-bookworm-slim@sha256:6c74791e557ce11fc957704f6d4fe134a7bc8d6f5ca440 WORKDIR /build/sdk/typescript -COPY sdk/typescript/package.json sdk/typescript/pnpm-lock.yaml ./ +COPY sdk/typescript/package.json sdk/typescript/pnpm-lock.yaml sdk/typescript/pnpm-workspace.yaml ./ COPY plugins/codex-security/mcp-app/package.json plugins/codex-security/mcp-app/package-lock.json /build/plugins/codex-security/mcp-app/ RUN corepack enable \ diff --git a/README.md b/README.md index 286914a70..fd829cd9a 100644 --- a/README.md +++ b/README.md @@ -50,10 +50,15 @@ Use the included Docker Compose configuration for scans of many repositories. Se The [findings service](sdk/typescript/README.md#findings-service-preview) runs from the SDK in Docker, stores findings and embeddings in SQLite, and lists -findings with pagination. It also returns potential duplicates by embedding -similarity within a repository or an explicit all-repository scope. The SDK and -`codex-security dedupe` CLI command retrieve those candidates and run independent -Codex reviews locally; `--all-repositories` opts into the broader scope. +findings with pagination. Its read-only dashboard at `/dashboard` refreshes every +five seconds and shows stored findings and duplicate groups from the service's +database. It also returns potential duplicates by embedding similarity within a +repository or an explicit all-repository scope. The +`codex-security publish scan --to custom --findings-url http://localhost:3000` +command uploads completed findings and their repository ID. The SDK and +`codex-security dedupe` command retrieve candidates, run independent Codex +reviews locally, and persist accepted duplicate groups; `--all-repositories` +opts into the broader scope. ## Other providers diff --git a/docker/fixtures/mock-reviews.mjs b/docker/fixtures/mock-reviews.mjs index 4b346a5b5..3af08e620 100644 --- a/docker/fixtures/mock-reviews.mjs +++ b/docker/fixtures/mock-reviews.mjs @@ -66,7 +66,7 @@ const server = createServer(async (request, response) => { const execute = functions.find((tool) => tool.name === "exec"); assert.match(execute.description, /### `exec_command`/); const same = findings.every( - (finding) => finding.extensions.smokeGroup === "duplicate", + (finding) => finding.extensions?.smokeGroup !== "distinct", ); const result = stage === "screen" diff --git a/plugins/codex-security/scripts/workbench_cli.py b/plugins/codex-security/scripts/workbench_cli.py index 24a9ec658..e4ceb29d8 100644 --- a/plugins/codex-security/scripts/workbench_cli.py +++ b/plugins/codex-security/scripts/workbench_cli.py @@ -339,7 +339,12 @@ def parse_args(description: str) -> argparse.Namespace: publication.add_argument("--input-file", required=True) subparsers.add_parser("database-info") + subparsers.add_parser("dashboard") + subparsers.add_parser("finding-workflow") subparsers.add_parser("store-findings") + subparsers.add_parser("store-dedupe-groups") + dedupe_groups = subparsers.add_parser("list-dedupe-groups") + dedupe_groups.add_argument("--finding-id", required=True) potential_duplicates = subparsers.add_parser("find-potential-duplicates") potential_duplicates.add_argument("--finding-id", required=True) scope = potential_duplicates.add_mutually_exclusive_group(required=True) diff --git a/plugins/codex-security/scripts/workbench_dashboard.py b/plugins/codex-security/scripts/workbench_dashboard.py new file mode 100644 index 000000000..0e7b510b6 --- /dev/null +++ b/plugins/codex-security/scripts/workbench_dashboard.py @@ -0,0 +1,115 @@ +"""Read-only dashboard projections for stored findings and duplicate groups.""" + +from __future__ import annotations + +import argparse +import json +import sqlite3 +import sys +from pathlib import Path +from typing import Any + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from workbench_findings import list_dedupe_groups + + +FINDING_RECORDS = """ +SELECT findings.id, json_extract(details_json, '$.title') AS title, + COALESCE(repositories.ids, '[]') AS repositoryIds, + json_extract(details_json, '$.severity.level') AS severity, + findings.created_at AS createdAt, findings.updated_at AS updatedAt +FROM findings LEFT JOIN ( + SELECT finding_id, json_group_array(repository_id) AS ids + FROM finding_repositories GROUP BY finding_id +) AS repositories ON repositories.finding_id = findings.id +WHERE details_json IS NOT NULL +""" + +GROUP_RECORDS = """ +SELECT groups.id, groups.id AS title, + (SELECT json_group_array(DISTINCT repository_id) + FROM finding_dedupe_group_members AS members + JOIN finding_repositories ON finding_repositories.finding_id = members.finding_id + WHERE members.group_id = groups.id) AS repositoryIds, + groups.created_at AS createdAt, groups.created_at AS updatedAt, + (SELECT COUNT(*) FROM finding_dedupe_group_members WHERE group_id = groups.id) AS memberCount +FROM finding_dedupe_groups AS groups +""" + +RECORDS = { + "findings": FINDING_RECORDS, + "groups": GROUP_RECORDS, +} + + +def item(row: sqlite3.Row) -> dict[str, Any]: + result = dict(row) + result["repositoryIds"] = json.loads(result["repositoryIds"]) + return result + + +def detail(connection: sqlite3.Connection, view: str, selected: dict[str, Any]) -> dict[str, Any]: + result: dict[str, Any] = {"item": selected} + selected_id = selected["id"] + if view == "findings": + result["finding"] = json.loads(connection.execute( + "SELECT details_json FROM findings WHERE id = ?", (selected_id,), + ).fetchone()[0]) + result["groups"] = list_dedupe_groups(connection, selected_id)["groups"] + else: + result["group"] = { + "groupId": selected_id, "createdAt": selected["createdAt"], + "findingIds": [r[0] for r in connection.execute( + "SELECT finding_id FROM finding_dedupe_group_members WHERE group_id = ? ORDER BY finding_id", + (selected_id,), + )], + } + return result + + +def dashboard(connection: sqlite3.Connection, query: dict[str, Any]) -> dict[str, Any]: + """One snapshot, no artifact reads, model calls, or writes.""" + view = query["view"] + records = RECORDS[view] + clauses: list[str] = [] + values: list[Any] = [] + if query.get("query"): + connection.create_function("casefold", 1, str.casefold, deterministic=True) + columns = ["id", "title", "repositoryIds"] + clauses.append("(" + " OR ".join(f"instr(casefold(COALESCE({c}, '')), casefold(?)) > 0" for c in columns) + ")") + values.extend([query["query"]] * len(columns)) + if query.get("repository"): + clauses.append("EXISTS (SELECT 1 FROM json_each(repositoryIds) WHERE value = ?)") + values.append(query["repository"]) + where = " WHERE " + " AND ".join(clauses) if clauses else "" + order = "createdAt DESC, id" if query["sort"] == "newest" else "updatedAt DESC, id" + connection.execute("BEGIN") + with connection: + repositories = connection.execute(""" + SELECT DISTINCT repository_id AS id, repository_id AS label + FROM finding_repositories ORDER BY repository_id + """).fetchall() + total = connection.execute(f"SELECT COUNT(*) FROM ({records}) {where}", values).fetchone()[0] + rows = connection.execute( + f"SELECT * FROM ({records}) {where} ORDER BY {order} LIMIT ? OFFSET ?", + (*values, query["limit"], query["offset"]), + ).fetchall() + selected = connection.execute( + f"SELECT * FROM ({records}) WHERE id = ?", (query["id"],), + ).fetchone() if query.get("id") else None + next_offset = query["offset"] + len(rows) + return { + "overview": { + "findings": connection.execute("SELECT COUNT(*) FROM findings WHERE details_json IS NOT NULL").fetchone()[0], + "groups": connection.execute("SELECT COUNT(*) FROM finding_dedupe_groups").fetchone()[0], + }, + "repositories": [dict(row) for row in repositories], + "items": [item(row) for row in rows], "total": total, + "limit": query["limit"], "offset": query["offset"], + "nextOffset": next_offset if next_offset < total else None, + "detail": detail(connection, view, item(selected)) if selected is not None else None, + } + + +if __name__ == "__main__": + argparse.ArgumentParser(description=__doc__).parse_args() diff --git a/plugins/codex-security/scripts/workbench_db.py b/plugins/codex-security/scripts/workbench_db.py index dd7651b44..69e58b0d5 100644 --- a/plugins/codex-security/scripts/workbench_db.py +++ b/plugins/codex-security/scripts/workbench_db.py @@ -81,8 +81,16 @@ SQLITE_RETRY_ATTEMPTS, ) from workbench_feedback import get_scan_feedback +from workbench_dashboard import dashboard from workbench_finding_index import index_findings -from workbench_findings import find_potential_duplicates, list_stored_findings, store_findings +from workbench_finding_workflows import finding_workflow, register_workflow_scan +from workbench_findings import ( + find_potential_duplicates, + list_dedupe_groups, + list_stored_findings, + store_dedupe_groups, + store_findings, +) from workbench_remediation import remediation_claim_is_active from workbench_scan_start import ( archive_scan, @@ -1652,10 +1660,12 @@ def register_cli_scan(connection: sqlite3.Connection, args: argparse.Namespace) raise SystemExit("The scan artifact directory must be empty before the scan starts.") user_context = None + workflow_id = None if args.registration_json_stdin: registration = json.load(sys.stdin) recipe_json = json.dumps(registration["recipe"], ensure_ascii=False, separators=(",", ":")) user_context = registration.get("userContext") + workflow_id = registration.get("workflowId") else: recipe_json = sys.stdin.read() if args.recipe_json_stdin else args.recipe_json recipe = parse_scan_recipe(recipe_json, repository) @@ -1751,6 +1761,8 @@ def register_cli_scan(connection: sqlite3.Connection, args: argparse.Namespace) scan_id, ), ) + if workflow_id is not None: + register_workflow_scan(connection, workflow_id, scan_id, str(scan_dir), timestamp) connection.commit() except BaseException: connection.rollback() @@ -4025,11 +4037,19 @@ def main() -> None: result = export_findings(connection, args) elif args.command == "database-info": result = {"databasePath": str(database_path())} + elif args.command == "finding-workflow": + result = finding_workflow(connection, json.load(sys.stdin), now()) + elif args.command == "dashboard": + result = dashboard(connection, json.load(sys.stdin)) elif args.command == "store-findings": payload = json.load(sys.stdin) result = store_findings(connection, payload["entries"], now(), payload.get("repositoryId")) elif args.command == "find-potential-duplicates": result = find_potential_duplicates(connection, args.finding_id, args.repository_id) + elif args.command == "store-dedupe-groups": + result = store_dedupe_groups(connection, json.load(sys.stdin)["groups"], now()) + elif args.command == "list-dedupe-groups": + result = list_dedupe_groups(connection, args.finding_id) elif args.command == "list-stored-findings": result = list_stored_findings(connection, limit=args.limit, offset=args.offset) else: diff --git a/plugins/codex-security/scripts/workbench_finding_workflows.py b/plugins/codex-security/scripts/workbench_finding_workflows.py new file mode 100644 index 000000000..dee35cbf2 --- /dev/null +++ b/plugins/codex-security/scripts/workbench_finding_workflows.py @@ -0,0 +1,190 @@ +"""Durable state for the opt-in local scan, publication, and dedupe workflow.""" + +from __future__ import annotations + +import argparse +import json +import hashlib +import sqlite3 +import sys +from pathlib import Path +from typing import Any + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from workbench_target import directory_content_digest, git_output, git_revision + +WORKFLOW_BINDINGS = { + "repositoryPath": "repository_path", + "scanRequestDigest": "scan_request_digest", + "scanId": "scan_id", + "scanDir": "scan_dir", + "artifactDigest": "artifact_digest", + "destination": "destination", +} +WORKFLOW_STAGES = ("scan", "publish", "dedupe") + + +def read_workflow(connection: sqlite3.Connection, workflow_id: str) -> dict[str, Any] | None: + row = connection.execute( + "SELECT * FROM finding_workflows WHERE id = ?", (workflow_id,) + ).fetchone() + if row is None: + return None + state = {"id": row["id"], "stages": {}} + for field, column in WORKFLOW_BINDINGS.items(): + if row[column] is not None: + state[field] = row[column] + if row["scope_repository_id"] is not None: + state["scope"] = {"repositoryId": row["scope_repository_id"]} + elif row["scope_all_repositories"] is not None: + state["scope"] = {"allRepositories": bool(row["scope_all_repositories"])} + results = json.loads(row["results_json"]) + for stage in WORKFLOW_STAGES: + current = {"status": row[f"{stage}_status"]} + if row[f"{stage}_error"] is not None: + current["error"] = row[f"{stage}_error"] + if stage in results: + current["result"] = results[stage] + state["stages"][stage] = current + if "dedupePendingWrite" in results: + state["stages"]["dedupe"]["pendingWrite"] = results["dedupePendingWrite"] + return state + + +def save_workflow(connection: sqlite3.Connection, state: dict[str, Any], timestamp: str) -> None: + values = {"id": state["id"]} + values.update({column: state.get(field) for field, column in WORKFLOW_BINDINGS.items()}) + scope = state.get("scope", {}) + values.update( + scope_repository_id=scope.get("repositoryId"), + scope_all_repositories=scope.get("allRepositories"), + ) + results = {} + for stage in WORKFLOW_STAGES: + current = state["stages"][stage] + values[f"{stage}_status"] = current["status"] + values[f"{stage}_error"] = current.get("error") + if "result" in current: + results[stage] = current["result"] + if "pendingWrite" in state["stages"]["dedupe"]: + results["dedupePendingWrite"] = state["stages"]["dedupe"]["pendingWrite"] + values.update( + results_json=json.dumps(results, allow_nan=False), created_at=timestamp, updated_at=timestamp + ) + updates = ", ".join( + f"{column} = excluded.{column}" for column in values if column not in {"id", "created_at"} + ) + connection.execute( + f"INSERT INTO finding_workflows ({', '.join(values)}) " + f"VALUES ({', '.join('?' for _ in values)}) ON CONFLICT(id) DO UPDATE SET {updates}", + tuple(values.values()), + ) + + +def bind_workflow(state: dict[str, Any], binding: dict[str, Any]) -> None: + for field, value in binding.items(): + if field not in WORKFLOW_BINDINGS and field != "scope": + raise SystemExit("Unknown workflow binding.") + if field in state and state[field] != value: + raise SystemExit( + f"Workflow {state['id']} is already bound to a different {field}. " + "Use another --workflow-id." + ) + state[field] = value + + +def register_workflow_scan( + connection: sqlite3.Connection, workflow_id: str, scan_id: str, scan_dir: str, timestamp: str +) -> None: + """Called inside the scan-registration transaction, before model execution.""" + state = read_workflow(connection, workflow_id) + if state is None: + raise SystemExit("The workflow must be started before registering its scan.") + if state["stages"]["scan"]["status"] == "completed": + raise SystemExit("The workflow scan is already complete.") + previous = state.get("scanId") + if previous is not None: + row = connection.execute("SELECT status FROM scans WHERE id = ?", (previous,)).fetchone() + if row is not None and row["status"] == "complete": + raise SystemExit("Reuse the workflow's completed scan instead of registering another.") + state["scanId"] = scan_id + state["scanDir"] = scan_dir + save_workflow(connection, state, timestamp) + + +def finding_workflow( + connection: sqlite3.Connection, payload: dict[str, Any], timestamp: str +) -> dict[str, Any]: + workflow_id = payload["id"] + if not isinstance(workflow_id, str) or not workflow_id.strip(): + raise SystemExit("workflowId must be a nonempty string.") + if payload["action"] == "get": + return {"workflow": read_workflow(connection, workflow_id)} + if payload["action"] == "source": + target = Path(payload["repository"]).resolve(strict=True) + return {"source": { + "repository": str(target), + "revision": git_revision(target), + "refsDigest": hashlib.sha256((git_output(target, "show-ref") or "").encode()).hexdigest(), + "content": directory_content_digest(target, include_ignored=True), + }} + if payload["action"] == "get-review": + row = connection.execute( + "SELECT result_json FROM finding_workflow_reviews WHERE workflow_id = ? AND review_key = ?", + (workflow_id, payload["key"]), + ).fetchone() + return {"review": json.loads(row["result_json"]) if row is not None else None} + if payload["action"] == "save-review": + binding = payload["binding"] + source = binding["source"] + scope = binding["scope"] + with connection: + connection.execute( + """INSERT INTO finding_workflow_reviews + (workflow_id, review_key, review_contract_version, codex_version, + source_repository_path, source_revision, source_refs_digest, source_content_digest, + scope_repository_id, scope_all_repositories, model, effort, settings_digest, + prompt_digest, contract_digest, result_json, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(workflow_id, review_key) DO NOTHING""", + ( + workflow_id, payload["key"], binding["version"], binding["codexVersion"], + source["repository"], source["revision"], source["refsDigest"], source["content"], + scope.get("repositoryId"), scope.get("allRepositories"), binding["model"], + binding["effort"], binding.get("settingsDigest"), binding["promptDigest"], + binding["contractDigest"], json.dumps(payload["result"], allow_nan=False), timestamp, + ), + ) + return {} + connection.execute("BEGIN IMMEDIATE") + with connection: + state = read_workflow(connection, workflow_id) + if state is None: + state = { + "id": workflow_id, + "stages": {stage: {"status": "pending"} for stage in WORKFLOW_STAGES}, + } + bind_workflow(state, payload.get("binding", {})) + action = payload["action"] + if action != "bind": + stage = payload["stage"] + if stage not in state["stages"]: + raise SystemExit("Unknown workflow stage.") + current = state["stages"][stage] + if current["status"] != "completed": + if action == "begin": + current["status"] = "running" + elif action == "complete": + state["stages"][stage] = {"status": "completed", "result": payload["result"]} + elif action == "fail": + current.update(status="failed", error=payload["error"]) + elif action == "prepare-dedupe": + current.update(result=payload["result"], pendingWrite=payload["pendingWrite"]) + else: + raise SystemExit("Unknown workflow action.") + save_workflow(connection, state, timestamp) + return {"workflow": state} + + +if __name__ == "__main__": + argparse.ArgumentParser(description=__doc__).parse_args() diff --git a/plugins/codex-security/scripts/workbench_findings.py b/plugins/codex-security/scripts/workbench_findings.py index 2492443c0..bcef8469b 100644 --- a/plugins/codex-security/scripts/workbench_findings.py +++ b/plugins/codex-security/scripts/workbench_findings.py @@ -3,6 +3,7 @@ from __future__ import annotations import argparse +import hashlib import json import math import sqlite3 @@ -145,6 +146,65 @@ def find_potential_duplicates( } +def store_dedupe_groups( + connection: sqlite3.Connection, groups: list[list[str]], timestamp: str +) -> dict[str, Any]: + """Persist reviewed sets independently, including overlapping groups, in one transaction.""" + stored: dict[str, dict[str, Any]] = {} + try: + with connection: + connection.execute("BEGIN IMMEDIATE") + for group in groups: + members = sorted(set(group)) + # Membership, not input order, identifies a group on retries. + group_id = "fdg_" + hashlib.sha256( + json.dumps(members, ensure_ascii=True, separators=(",", ":")).encode("utf-8") + ).hexdigest() + connection.execute( + "INSERT INTO finding_dedupe_groups (id, created_at) VALUES (?, ?) " + "ON CONFLICT(id) DO NOTHING", + (group_id, timestamp), + ) + connection.executemany( + "INSERT INTO finding_dedupe_group_members (group_id, finding_id) VALUES (?, ?) " + "ON CONFLICT(group_id, finding_id) DO NOTHING", + ((group_id, finding_id) for finding_id in members), + ) + created_at = connection.execute( + "SELECT created_at FROM finding_dedupe_groups WHERE id = ?", (group_id,) + ).fetchone()[0] + stored[group_id] = { + "groupId": group_id, + "findingIds": members, + "createdAt": created_at, + } + except sqlite3.IntegrityError: + return {"error": "finding_conflict"} + return {"groups": list(stored.values())} + + +def list_dedupe_groups(connection: sqlite3.Connection, finding_id: str) -> dict[str, Any]: + groups: dict[str, dict[str, Any]] = {} + rows = connection.execute( + """ + SELECT groups.id, groups.created_at, members.finding_id + FROM finding_dedupe_group_members AS matched + JOIN finding_dedupe_groups AS groups ON groups.id = matched.group_id + JOIN finding_dedupe_group_members AS members ON members.group_id = groups.id + WHERE matched.finding_id = ? + ORDER BY groups.created_at, groups.id, members.finding_id + """, + (finding_id,), + ) + for row in rows: + group = groups.setdefault( + row["id"], + {"groupId": row["id"], "findingIds": [], "createdAt": row["created_at"]}, + ) + group["findingIds"].append(row["finding_id"]) + return {"groups": list(groups.values())} + + def normalized_vector(vector: list[float]) -> list[float]: norm = math.hypot(*vector) if norm == 0 or not math.isfinite(norm): diff --git a/plugins/codex-security/scripts/workbench_schema.py b/plugins/codex-security/scripts/workbench_schema.py index 954ae5611..ff83eb326 100644 --- a/plugins/codex-security/scripts/workbench_schema.py +++ b/plugins/codex-security/scripts/workbench_schema.py @@ -1,6 +1,7 @@ """SQLite schema history for the Codex Security workbench.""" import argparse +import json import sqlite3 from collections.abc import Callable @@ -739,9 +740,144 @@ WHERE scans.target_id IS NOT NULL; """, ), + ( + 35, + "persist finding dedupe groups", + """ + CREATE TABLE finding_dedupe_groups ( + id TEXT PRIMARY KEY, + created_at TEXT NOT NULL + ); + + CREATE TABLE finding_dedupe_group_members ( + group_id TEXT NOT NULL REFERENCES finding_dedupe_groups(id) ON DELETE CASCADE, + finding_id TEXT NOT NULL REFERENCES findings(id), + PRIMARY KEY (group_id, finding_id) + ); + + CREATE INDEX finding_dedupe_groups_by_finding + ON finding_dedupe_group_members(finding_id, group_id); + """, + ), + ( + 36, + "persist local findings workflows", + """ + CREATE TABLE finding_workflows ( + id TEXT PRIMARY KEY, + state_json TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ); + """, + ), + ( + 37, + "checkpoint validated dedupe reviews", + """ + CREATE TABLE finding_workflow_reviews ( + workflow_id TEXT NOT NULL REFERENCES finding_workflows(id) ON DELETE CASCADE, + review_key TEXT NOT NULL, + binding_json TEXT NOT NULL, + result_json TEXT NOT NULL, + created_at TEXT NOT NULL, + PRIMARY KEY (workflow_id, review_key) + ); + """, + ), + ( + 38, + "store findings workflow metadata in columns", + """ + ALTER TABLE finding_workflows RENAME COLUMN state_json TO results_json; + ALTER TABLE finding_workflows ADD COLUMN repository_path TEXT; + ALTER TABLE finding_workflows ADD COLUMN scan_request_digest TEXT; + ALTER TABLE finding_workflows ADD COLUMN scan_id TEXT; + ALTER TABLE finding_workflows ADD COLUMN scan_dir TEXT; + ALTER TABLE finding_workflows ADD COLUMN artifact_digest TEXT; + ALTER TABLE finding_workflows ADD COLUMN destination TEXT; + ALTER TABLE finding_workflows ADD COLUMN scope_repository_id TEXT; + ALTER TABLE finding_workflows ADD COLUMN scope_all_repositories INTEGER; + ALTER TABLE finding_workflows ADD COLUMN scan_status TEXT NOT NULL DEFAULT 'pending'; + ALTER TABLE finding_workflows ADD COLUMN scan_error TEXT; + ALTER TABLE finding_workflows ADD COLUMN publish_status TEXT NOT NULL DEFAULT 'pending'; + ALTER TABLE finding_workflows ADD COLUMN publish_error TEXT; + ALTER TABLE finding_workflows ADD COLUMN dedupe_status TEXT NOT NULL DEFAULT 'pending'; + ALTER TABLE finding_workflows ADD COLUMN dedupe_error TEXT; + """, + ), + ( + 39, + "store dedupe checkpoint bindings in columns", + """ + ALTER TABLE finding_workflow_reviews RENAME COLUMN binding_json TO prompt_digest; + ALTER TABLE finding_workflow_reviews ADD COLUMN review_contract_version INTEGER; + ALTER TABLE finding_workflow_reviews ADD COLUMN codex_version TEXT; + ALTER TABLE finding_workflow_reviews ADD COLUMN source_repository_path TEXT; + ALTER TABLE finding_workflow_reviews ADD COLUMN source_revision TEXT; + ALTER TABLE finding_workflow_reviews ADD COLUMN source_refs_digest TEXT; + ALTER TABLE finding_workflow_reviews ADD COLUMN source_content_digest TEXT; + ALTER TABLE finding_workflow_reviews ADD COLUMN scope_repository_id TEXT; + ALTER TABLE finding_workflow_reviews ADD COLUMN scope_all_repositories INTEGER; + ALTER TABLE finding_workflow_reviews ADD COLUMN model TEXT; + ALTER TABLE finding_workflow_reviews ADD COLUMN effort TEXT; + ALTER TABLE finding_workflow_reviews ADD COLUMN settings_digest TEXT; + ALTER TABLE finding_workflow_reviews ADD COLUMN contract_digest TEXT; + """, + ), ) +def migrate_finding_workflow_review_columns(connection: sqlite3.Connection) -> None: + for row in connection.execute( + "SELECT workflow_id, review_key, prompt_digest FROM finding_workflow_reviews" + ).fetchall(): + binding = json.loads(row["prompt_digest"]) + source = binding["source"] + scope = binding["scope"] + connection.execute( + """UPDATE finding_workflow_reviews SET review_contract_version = ?, codex_version = ?, + source_repository_path = ?, source_revision = ?, source_refs_digest = ?, + source_content_digest = ?, scope_repository_id = ?, scope_all_repositories = ?, + model = ?, effort = ?, settings_digest = ?, prompt_digest = ?, contract_digest = ? + WHERE workflow_id = ? AND review_key = ?""", + ( + binding["version"], binding["codexVersion"], source["repository"], + source["revision"], source["refsDigest"], source["content"], + scope.get("repositoryId"), scope.get("allRepositories"), binding["model"], + binding["effort"], binding.get("settingsDigest"), binding["promptDigest"], + binding["contractDigest"], row["workflow_id"], row["review_key"], + ), + ) + + +def migrate_finding_workflow_columns(connection: sqlite3.Connection) -> None: + # Rename/backfill in place so existing checkpoint foreign keys and rows survive. + for row in connection.execute("SELECT id, results_json FROM finding_workflows").fetchall(): + state = json.loads(row["results_json"]) + scope = state.get("scope", {}) + stages = state["stages"] + results = {stage: value["result"] for stage, value in stages.items() if "result" in value} + if "pendingWrite" in stages["dedupe"]: + results["dedupePendingWrite"] = stages["dedupe"]["pendingWrite"] + connection.execute( + """UPDATE finding_workflows SET + repository_path = ?, scan_request_digest = ?, scan_id = ?, scan_dir = ?, + artifact_digest = ?, destination = ?, scope_repository_id = ?, scope_all_repositories = ?, + scan_status = ?, scan_error = ?, publish_status = ?, publish_error = ?, + dedupe_status = ?, dedupe_error = ?, results_json = ? WHERE id = ?""", + ( + state.get("repositoryPath"), state.get("scanRequestDigest"), + state.get("scanId"), state.get("scanDir"), state.get("artifactDigest"), + state.get("destination"), scope.get("repositoryId"), scope.get("allRepositories"), + stages["scan"]["status"], stages["scan"].get("error"), + stages["publish"]["status"], stages["publish"].get("error"), + stages["dedupe"]["status"], stages["dedupe"].get("error"), + json.dumps(results, allow_nan=False), row["id"], + ), + ) + + def apply_migrations( connection: sqlite3.Connection, migrations: tuple[tuple[int, str, str], ...], @@ -822,6 +958,10 @@ def apply_migrations( else: for statement in sql_statements(sql): connection.execute(statement) + if version == 38: + migrate_finding_workflow_columns(connection) + elif version == 39: + migrate_finding_workflow_review_columns(connection) connection.execute( "INSERT INTO schema_migrations (version, name, applied_at) VALUES (?, ?, ?)", (version, name, now()), diff --git a/plugins/codex-security/scripts/workbench_target.py b/plugins/codex-security/scripts/workbench_target.py index 3e26d5acd..1b1cb0500 100644 --- a/plugins/codex-security/scripts/workbench_target.py +++ b/plugins/codex-security/scripts/workbench_target.py @@ -414,14 +414,31 @@ def git_directory_snapshot_paths(target: Path) -> list[Path] | None: return sorted(set(paths)) -def directory_content_digest(target: Path, *, excluded: tuple[Path, ...] = ()) -> str: +def source_directory_snapshot_paths(target: Path) -> list[Path]: + paths: list[Path] = [] + pending = [target] + while pending: + for path in pending.pop().iterdir(): + if path.name == ".git": + continue + paths.append(path) + metadata = path.lstat() + # Name-surrogate reparse points include Windows directory junctions. + if stat.S_ISDIR(metadata.st_mode) and not getattr(metadata, "st_reparse_tag", 0) & 0x20000000: + pending.append(path) + return sorted(paths) + + +def directory_content_digest( + target: Path, *, excluded: tuple[Path, ...] = (), include_ignored: bool = False +) -> str: excluded_relative = [] for path in excluded: try: excluded_relative.append(path.relative_to(target)) except ValueError: continue - paths = git_directory_snapshot_paths(target) + paths = source_directory_snapshot_paths(target) if include_ignored else git_directory_snapshot_paths(target) if paths is None: paths = sorted(target.rglob("*")) digest = hashlib.sha256() @@ -440,7 +457,9 @@ def directory_content_digest(target: Path, *, excluded: tuple[Path, ...] = ()) - raw_path = os.fsencode(relative_path.as_posix()) update_digest_field(digest, b"path", raw_path) update_digest_field(digest, b"mode", str(stat.S_IMODE(metadata.st_mode)).encode()) - if stat.S_ISLNK(metadata.st_mode): + if stat.S_ISLNK(metadata.st_mode) or ( + include_ignored and getattr(metadata, "st_reparse_tag", 0) & 0x20000000 + ): update_digest_field(digest, b"kind", b"symlink") update_digest_field(digest, b"content", os.fsencode(os.readlink(path))) elif stat.S_ISDIR(metadata.st_mode): diff --git a/plugins/codex-security/tests/test_workbench_db.py b/plugins/codex-security/tests/test_workbench_db.py index 4bd9323da..136a038b8 100644 --- a/plugins/codex-security/tests/test_workbench_db.py +++ b/plugins/codex-security/tests/test_workbench_db.py @@ -58,6 +58,8 @@ "deep_scan_runs", "deep_scan_workers", "finding_decisions", + "finding_dedupe_group_members", + "finding_dedupe_groups", "finding_embeddings", "finding_locations", "finding_occurrences", @@ -65,6 +67,8 @@ "finding_remediation_attempts", "finding_repositories", "finding_triage", + "finding_workflow_reviews", + "finding_workflows", "findings", "scan_artifacts", "scan_comparison_matches", @@ -992,7 +996,7 @@ def test_workbench_persists_progress_and_indexes_completed_findings(tmp_path: Pa ) } assert tables == EXPECTED_TABLES - assert connection.execute("SELECT COUNT(*) FROM schema_migrations").fetchone() == (34,) + assert connection.execute("SELECT COUNT(*) FROM schema_migrations").fetchone() == (39,) assert connection.execute("SELECT COUNT(*) FROM findings").fetchone() == (1,) assert connection.execute("SELECT COUNT(*) FROM finding_locations").fetchone() == (1,) diff --git a/plugins/codex-security/tests/test_workbench_deep_scan.py b/plugins/codex-security/tests/test_workbench_deep_scan.py index c1346cc4f..2e39cd074 100644 --- a/plugins/codex-security/tests/test_workbench_deep_scan.py +++ b/plugins/codex-security/tests/test_workbench_deep_scan.py @@ -278,7 +278,7 @@ def claim() -> dict[str, object]: return claim_deep_scan_coordinator(state_dir, codex_home, scan_id) with sqlite3.connect(state_dir / "workbench.sqlite3") as connection: - assert connection.execute("SELECT MAX(version) FROM schema_migrations").fetchone() == (34,) + assert connection.execute("SELECT MAX(version) FROM schema_migrations").fetchone() == (39,) assert claim()["deepScan"]["coordinatorGeneration"] == 2 assert claim()["coordinatorDisposition"] == "observing" expire_deep_scan_coordinator(state_dir, scan_id) diff --git a/plugins/codex-security/tests/test_workbench_setup_and_migrations.py b/plugins/codex-security/tests/test_workbench_setup_and_migrations.py index f37be4ab5..d6d184796 100644 --- a/plugins/codex-security/tests/test_workbench_setup_and_migrations.py +++ b/plugins/codex-security/tests/test_workbench_setup_and_migrations.py @@ -405,7 +405,7 @@ def test_workbench_serializes_concurrent_first_run_migrations(tmp_path: Path) -> {"databasePath": str(state_dir / "workbench.sqlite3")}, ] with sqlite3.connect(state_dir / "workbench.sqlite3") as connection: - assert connection.execute("SELECT COUNT(*) FROM schema_migrations").fetchone() == (34,) + assert connection.execute("SELECT COUNT(*) FROM schema_migrations").fetchone() == (39,) def test_workbench_backfills_repository_targets_only_during_migration() -> None: @@ -795,6 +795,11 @@ def test_workbench_creates_single_final_schema(tmp_path: Path) -> None: (32, "separate deep scan publication failures"), (33, "store complete findings and embeddings without a scan"), (34, "associate findings with repositories"), + (35, "persist finding dedupe groups"), + (36, "persist local findings workflows"), + (37, "checkpoint validated dedupe reviews"), + (38, "store findings workflow metadata in columns"), + (39, "store dedupe checkpoint bindings in columns"), ] assert {row[1] for row in connection.execute("PRAGMA table_info(workspaces)")} >= { "diff_target_kind", @@ -897,7 +902,7 @@ def test_workbench_upgrades_preexisting_database(tmp_path: Path) -> None: connection.execute("ALTER TABLE scans DROP COLUMN handoff_claim_token") run_workbench(state_dir, "database-info") with sqlite3.connect(database) as connection: - assert connection.execute("SELECT MAX(version) FROM schema_migrations").fetchone() == (34,) + assert connection.execute("SELECT MAX(version) FROM schema_migrations").fetchone() == (39,) assert {row[1] for row in connection.execute("PRAGMA table_info(scans)")} >= { "handoff_claimed_at", "handoff_claim_token", @@ -1918,6 +1923,11 @@ def test_workbench_upgrades_released_database_schema(tmp_path: Path) -> None: (32, "separate deep scan publication failures"), (33, "store complete findings and embeddings without a scan"), (34, "associate findings with repositories"), + (35, "persist finding dedupe groups"), + (36, "persist local findings workflows"), + (37, "checkpoint validated dedupe reviews"), + (38, "store findings workflow metadata in columns"), + (39, "store dedupe checkpoint bindings in columns"), ] assert "capability_preflight_json" in { row[1] for row in connection.execute("PRAGMA table_info(workspaces)") @@ -1994,6 +2004,11 @@ def test_workbench_upgrades_pre_release_phase_progress_migration(tmp_path: Path) (32, "separate deep scan publication failures"), (33, "store complete findings and embeddings without a scan"), (34, "associate findings with repositories"), + (35, "persist finding dedupe groups"), + (36, "persist local findings workflows"), + (37, "checkpoint validated dedupe reviews"), + (38, "store findings workflow metadata in columns"), + (39, "store dedupe checkpoint bindings in columns"), ] assert "continuation_thread_id" in { row[1] for row in connection.execute("PRAGMA table_info(scans)") @@ -2078,6 +2093,11 @@ def test_workbench_upgrades_pre_release_preflight_progress_migration(tmp_path: P (32, "separate deep scan publication failures"), (33, "store complete findings and embeddings without a scan"), (34, "associate findings with repositories"), + (35, "persist finding dedupe groups"), + (36, "persist local findings workflows"), + (37, "checkpoint validated dedupe reviews"), + (38, "store findings workflow metadata in columns"), + (39, "store dedupe checkpoint bindings in columns"), ] assert "continuation_thread_id" in { row[1] for row in connection.execute("PRAGMA table_info(scans)") diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md index 4760a5d7e..681f17cc4 100644 --- a/sdk/typescript/README.md +++ b/sdk/typescript/README.md @@ -930,6 +930,47 @@ packaged `start:server` script, without invoking the CLI. Docker runs Node directly so stop signals reach the server. The existing default Docker target and bulk-scan Compose configuration are unchanged. +### Read-only dashboard + +Open `http://localhost:3000/dashboard` on the running findings service. The UI +uses the public OpenAI Apps SDK UI design system, follows the browser's light +or dark preference, and polls the service every five seconds. It never starts, +cancels, resumes, publishes, edits, or deduplicates anything. + +The dashboard opens on Findings, followed by Duplicate groups. Both views +support search, repository filtering, sorting, pagination, and record details. +Findings show stored content and links to their duplicate groups. Groups link +back to their member findings, preserving separate overlapping groups and the +original finding records. + +The dashboard reads only findings, repository associations, and duplicate groups +stored in the service's configured database. It does not display scans or +workflows: publication sends findings, not remote scan or workflow history. It +does not read report or source paths from stored records. The UI retains the last +successful data on a refresh failure and shows a connection warning until polling +succeeds. + +`GET /v1/dashboard` returns a consistent read snapshot with overview counts, +repository choices, a page of records, and optional selected-record details: + +- `view`: `findings` (default) or `groups`. +- `query`, `repository`: optional search text and exact repository ID. +- `sort`: `activity` (default; most recently updated first) or `newest`. +- `limit`, `offset`: existing pagination conventions, defaulting to 50 and 0. +- `id`: optional exact record ID to include in `detail`; unknown IDs return + `detail: null` without hiding the list. + +Overview counts are service-wide, not filtered page totals. Responses and UI +assets are served by the same Node process; no separate frontend server, CDN, +model credentials, or new CLI flags are needed to view the dashboard. Compiled +HTML, JavaScript, and CSS are included in the npm package and container. Frontend +source, build tools, and tests are not shipped as runtime dependencies. + +The existing preview access boundary is unchanged. The dashboard contains +sensitive finding content: keep the service on a trusted local endpoint or behind +an authenticated proxy. It does not add authentication or broaden the default +network binding. + ### API `POST /v1/bulk/findings` accepts `{"findings": [...]}`, using the existing SDK @@ -951,6 +992,8 @@ only available to explicit all-repository retrieval until imported with an ID. | `POST` | `/v1/bulk/findings` | HTTP 201 with an array of stored finding IDs, in request order | | `GET` | `/v1/findings?limit=50&offset=0` | HTTP 200 with a page of complete findings | | `GET` | `/v1/finding/{id}/potential-duplicates` | HTTP 200 with the stored finding and up to 50 potential duplicates, without vectors | +| `POST` | `/v1/dedupe-groups` | HTTP 201 with the persisted duplicate groups | +| `GET` | `/v1/finding/{id}/dedupe-groups` | HTTP 200 with every stored group containing this finding | Bulk insertion generates embeddings and then writes the findings and vectors in one SQLite transaction. If embedding generation fails or a finding identity @@ -999,12 +1042,49 @@ loads complete documents only for the anchor and the selected top 50 candidates, all within the same read transaction. The API does not run Codex or decide whether candidates are duplicates. +### Publishing to a custom findings service + +Publish a completed scan directly from the local CLI to the Docker service: + +```bash +codex-security publish scan --scan SCAN_ID --to custom \ + --findings-url http://localhost:3000 --json +``` + +`--findings-url` is required for `--to custom`, with no default. It is the +service base URL, including `http://` or `https://`; the client appends +`/v1/bulk/findings`, preserving any base path. A different endpoint must +implement that API and return the stored finding IDs. The command sends the +complete sealed findings and their manifest's `scan.target.targetId` as +`repositoryId`, without changing scan artifacts or forwarding model credentials. +The service creates embeddings and commits the batch before acknowledging it. + +The existing saved-scan selector, external `--scan-dir`, and interactive picker +work with custom publication. Custom publication accepts one scan, not CSV +input or Linear options. Add `--dry-run` to validate and preview the payload +without making an HTTP request. Existing Linear and Cloud destinations are +unchanged. Upload failures and incomplete receipts fail the command; uploads +are not automatically retried because a lost response may have been committed. + +```typescript +import { publishScanToCustom } from "@openai/codex-security"; + +const receipt = await publishScanToCustom("/path/to/completed-scan", { + findingsUrl: "http://localhost:3000", + // dryRun: true, + // signal: controller.signal, +}); +console.log(receipt.repositoryId, receipt.findingIds); +``` + ### Deduplication from the SDK and CLI -Import the scan's findings with their `repositoryId` through the bulk API before deduplicating. The +Publish the scan with `--to custom` (or import it through the bulk API with its +`repositoryId`) before deduplicating. The workflow reads a completed saved scan, queries candidates by finding ID, and -runs Luna and Sol in the calling SDK/CLI process. It does not upload findings, -change scan artifacts, or write grouping results to the service. +runs Luna and Sol in the calling SDK/CLI process. Once all reviews succeed, +it posts accepted groups to the service. It does not re-upload findings or +change scan artifacts. ```bash codex-security dedupe --scan SCAN_ID --findings-url http://127.0.0.1:3000 --json @@ -1048,7 +1128,109 @@ accepted duplicate groups are collapsed. A representative can be an existing stored finding outside the scan. Each `duplicateGroups` entry contains all members of an accepted group, with its canonical finding first. The canonical has the highest reported severity; ties use finding ID. Results do not delete, -merge, or change stored findings, and are not saved as durable group assignments. +merge, or change stored finding documents. Accepted groups are saved as durable +associations in the service before `deduplicationStatus` becomes `completed`. + +### Stored duplicate groups + +`POST /v1/dedupe-groups` accepts a batch of explicitly reviewed member sets: + +```json +{ + "groups": [ + ["csf_000000000000000000000001", "csf_000000000000000000000002"], + ["csf_000000000000000000000002", "csf_000000000000000000000003"] + ] +} +``` + +Each group must contain at least two distinct, existing finding IDs. The entire +batch is committed in one transaction; a missing finding returns HTTP 409 and +writes none of the batch. A response contains `groupId`, `findingIds`, and +`createdAt` for each group. Group identity depends on membership, not member +order, so submitting the same set again returns its original ID and timestamp. + +SQLite stores groups in `finding_dedupe_groups` and memberships in +`finding_dedupe_group_members`. A finding may belong to multiple groups: +`[A, B]`, `[B, C]`, and `[C, A]` are three separate reviewed sets. Overlapping +groups are not automatically united or promoted into an unreviewed larger +group. Stored members are sorted by ID; their order does not designate a +canonical. The CLI result retains its existing canonical-first ordering. + +`GET /v1/finding/{id}/dedupe-groups` returns every group containing that finding, +including each group's full membership, or `[]` if it has no groups. These +associations do not rewrite original findings, fingerprints, scan artifacts, +embeddings, or external tickets. They do not require an embedding API key or +trigger model calls. Review-generated merged findings remain review outputs; +they do not replace stored documents. + +### Resuming a local findings workflow + +Add `--workflow-id` to opt into durable state shared by `scan`, `publish scan +--to custom`, and `dedupe`. The SDK equivalents are the optional `workflowId` +fields on `ScanOptions`, `PublishScanToCustomOptions`, and `DeduplicateScanOptions`. +Without a workflow ID, existing command behavior and output shapes are unchanged. + +```bash +codex-security scan /path/to/repository --workflow-id run-001 +codex-security publish scan --workflow-id run-001 --to custom --findings-url http://localhost:3000 +codex-security dedupe --workflow-id run-001 --findings-url http://localhost:3000 --json +``` + +Repeat this sequence with the same ID after a process stops. Completed scans and +acknowledged publications are reused; unfinished stages run again. If the scan +completed before the workflow recorded its receipt, recovery verifies the saved +scan and its sealed artifacts instead of scanning again. Scan IDs and artifact +locations are recorded in the scan-registration transaction. This resumes between +completed steps; it does not resume individual model turns inside an unfinished +scan. Existing output-directory and archive safeguards still apply to scan retries. + +Publication and dedupe can use the workflow ID in place of `--scan`; an explicit +scan selector must identify that same scan. A workflow can also begin at custom +publication of a completed scan. Dedupe still requires local scan history to locate +the approved source checkout. For a workflow, dedupe first completes publication +if its receipt is missing. `--all-repositories` retains its existing default of +false. Changing a workflow's scan, destination, or bound scope is an error: choose +a different workflow ID. Use one coordinating process per workflow. + +Workflow metadata, stage statuses, errors, publication receipts, and results live +in the local workbench SQLite database under `CODEX_SECURITY_STATE_DIR`, outside +the sealed scan artifacts. Identity, scan/artifact references, destination, scope, +hashes, stage statuses, and errors use explicit columns; only receipts and result +payloads use JSON. Review source, scope, model settings, and contract/hash bindings +also use columns; review results remain JSON. Existing workflows and checkpoints +are migrated atomically in place without changing their review keys. +Successful empty results are stored as completed +results, not treated as missing work. An empty scan can complete workflow +publication with an empty receipt. Dry-run never advances a workflow stage. + +A completed `dedupe --workflow-id` returns its saved result without repeating +reviews or group writes. A publication whose acknowledgement was lost is retried +using the service's existing idempotent upsert. + +Each validated screening and pair review is checkpointed locally, +including DISTINCT decisions. Every SAME checkpoint retains its required +`canonicalFindingId` and generated `mergedFinding`. The merged record must satisfy +the Finding schema and preserve the canonical finding ID. Validation still happens +through `review_validator.submit_decisions`; invalid submissions are corrected in +the same review conversation, and invalid or unfinished reviews are not cached. + +Checkpoints bind to the exact original records and ordering, approved source path, +Git revision and current file contents (including ignored files), repository scope, +model and reasoning settings, Codex configuration and version, and prompt/contract version. Changed +inputs cause a new review rather than reusing a decision. Source changes during +review stop that attempt before group writes; restart with the same ID to review +the changed source. Original findings, never prior rationales or merged findings, +are supplied to later independent reviews. Source snapshots do not follow directory +links outside the approved checkout. + +Before posting groups, the workflow saves the exact final result and write payload. +If posting fails or its acknowledgement is lost, rerunning dedupe replays that +payload without looking up candidates or running models. Group persistence reuses +the existing membership identity, so replay does not create another group. The +dedupe stage completes only after acknowledgement. Empty results are also retained. +A completed workflow or pending write represents its already reviewed snapshot; +use another workflow ID for a fresh review rather than changing that saved result. ### Deduplication workflow @@ -1065,6 +1247,8 @@ merge, or change stored findings, and are not saved as durable group assignments pairs as transitive: if A matches B and B matches C, all three belong to one group. There is no additional whole-group review. A mistaken accepted pair can therefore join otherwise distinct findings. +5. Post all accepted groups to `/v1/dedupe-groups`. Return a completed result + only after the service accepts the write. An empty result requires no write. Each review uses a fresh, ephemeral Codex app-server thread with the complete original finding records, not earlier model rationales, vector scores, or @@ -1091,10 +1275,12 @@ Model calls run sequentially on the SDK/CLI host using its Codex sign-in or credentials are not sent to the findings API. Larger scans can take time and incur multiple model calls per finding. Empty scans and findings without eligible neighbors do not invoke review models. `completed` means this -retrieval and review process completed, not that every possible pair in the -database was compared or that model decisions are infallible. An API or review +retrieval, review, and group persistence completed, not that every possible pair in the +database was compared or that model decisions are infallible. An API, review, or write-back failure fails the command without claiming a completed result. Retry after -fixing the failure; stored findings remain unchanged. The CLI supports Ctrl-C +fixing the failure; stored findings remain unchanged. A lost write-back response +may have committed groups; retrying the same memberships does not duplicate +them. Failed or interrupted reviews write no groups. The CLI supports Ctrl-C and SIGTERM, and the SDK accepts an `AbortSignal`. ### Listing and errors @@ -1119,8 +1305,8 @@ without embedding vectors. Legacy identities without a complete document are not included. Pagination reflects current database contents, not a snapshot held between HTTP requests. -Malformed JSON, invalid finding objects, repository metadata, scopes, and pagination return HTTP -400 (`invalid_request`). Identity conflicts return 409 (`finding_conflict`), +Malformed JSON, invalid finding objects, repository metadata, groups, scopes, and pagination return HTTP +400 (`invalid_request`). Identity conflicts or missing dedupe group members return 409 (`finding_conflict`), embedding provider failures or unusable vectors return 502 (`embedding_failed`), and missing embedding credentials return 503 (`embedding_unavailable`). A potential-duplicates query without a current embedding returns 404 diff --git a/sdk/typescript/dashboard/App.tsx b/sdk/typescript/dashboard/App.tsx new file mode 100644 index 000000000..801bcbabb --- /dev/null +++ b/sdk/typescript/dashboard/App.tsx @@ -0,0 +1,573 @@ +import { useEffect, useRef, useState, type ReactNode } from "react"; +import { Badge } from "@openai/apps-sdk-ui/components/Badge"; +import { Button } from "@openai/apps-sdk-ui/components/Button"; +import { Input } from "@openai/apps-sdk-ui/components/Input"; +import type { + DashboardDetail, + DashboardItem, + DashboardSnapshot, + DashboardView, +} from "../src/server/dashboard-types.js"; +import { pollDashboard } from "./polling.js"; + +const views: { id: DashboardView; label: string; description: string }[] = [ + { + id: "findings", + label: "Findings", + description: "Findings stored in this service.", + }, + { + id: "groups", + label: "Duplicate groups", + description: + "Reviewed duplicate relationships. Overlapping groups stay separate.", + }, +]; +const count = (value: number | null | undefined) => + value == null ? "—" : value.toLocaleString(); +const timestamp = (value: string | null | undefined) => + value ? new Date(value).toLocaleString() : "—"; + +function age(value: string | null | undefined, now: number) { + if (!value) return "—"; + const seconds = Math.max(0, Math.floor((now - Date.parse(value)) / 1_000)); + if (seconds < 60) return `${seconds}s ago`; + if (seconds < 3_600) return `${Math.floor(seconds / 60)}m ago`; + if (seconds < 86_400) return `${Math.floor(seconds / 3_600)}h ago`; + return `${Math.floor(seconds / 86_400)}d ago`; +} + +function Field({ name, children }: { name: string; children: ReactNode }) { + return ( +
+
{name}
+
{children ?? "—"}
+
+ ); +} + +type Navigate = (view: DashboardView, id?: string) => void; + +function RecordLinks({ + ids, + view, + navigate, + empty = "None", +}: { + ids: string[]; + view: DashboardView; + navigate: Navigate; + empty?: string; +}) { + if (ids.length === 0) return

{empty}

; + return ( + + ); +} + +function Inspector({ + detail, + navigate, +}: { + detail: DashboardDetail; + navigate: Navigate; +}) { + const finding = detail.finding; + return ( + <> + {finding && ( + <> +
+

{finding.title}

+
+ + {finding.severity.level} + + + {finding.confidence.level} confidence + +
+

{finding.summary}

+
+ + {finding.findingId} + + + {detail.item.repositoryIds.join(", ") || "Not recorded"} + +
+
+
+

Affected locations

+ +
+ {( + [ + ["writeup", "Writeup"], + ["codeEvidence", "Code evidence"], + ["remediation", "Remediation"], + ["validation", "Validation"], + ["attackPath", "Attack path"], + ] as const + ).map(([key, title]) => + finding[key] != null ? ( +
+

{title}

+ +
+ ) : null, + )} +
+

Duplicate groups

+ group.groupId)} + view="groups" + navigate={navigate} + empty="No stored duplicate groups." + /> +
+ + )} + {detail.group && ( +
+

Group members · {detail.group.findingIds.length}

+

+ Reviewed together. Membership does not replace the original findings + or merge overlapping groups. +

+ +
+ {timestamp(detail.group.createdAt)} +
+
+ )} + + ); +} + +function FindingContent({ value }: { value: unknown }) { + if (typeof value === "string") return

{value}

; + return
{JSON.stringify(value, null, 2)}
; +} + +function Results({ + view, + items, + selected, + navigate, + now, +}: { + view: DashboardView; + items: DashboardItem[]; + selected: string; + navigate: Navigate; + now: number; +}) { + return ( +
+ + + + + + + {view === "findings" && } + {view === "groups" && } + + + + + + {items.map((item) => ( + + + + {view === "findings" && ( + + )} + {view === "groups" && ( + + )} + + + + ))} + +
+ {views.find((v) => v.id === view)!.label} +
{view === "findings" ? "Finding" : "Group"}RepositorySeverityMembersCreatedLast update
+ + {item.id !== item.title && ( + {item.id} + )} + + {item.repositoryIds.join(", ") || "—"} + + + {item.severity} + + {count(item.memberCount)} + + + +
+
+ ); +} + +export function App() { + const [view, setView] = useState("findings"); + const [query, setQuery] = useState(""); + const [repository, setRepository] = useState(""); + const [sort, setSort] = useState("activity"); + const [offset, setOffset] = useState(0); + const [selected, setSelected] = useState(""); + const inspectorHeading = useRef(null); + const resultsElement = useRef(null); + useEffect(() => { + // On narrow screens details follow the table; bring a new selection into view. + if (selected) inspectorHeading.current?.focus(); + }, [selected, view]); + const [saved, setSaved] = useState<{ + key: string; + data: DashboardSnapshot; + refreshed: number; + }>(); + const [error, setError] = useState<{ key: string; message: string }>(); + const parameters = new URLSearchParams({ + view, + query, + repository, + sort, + offset: String(offset), + }); + if (selected) parameters.set("id", selected); + const key = parameters.toString(); + useEffect( + () => + pollDashboard( + async (signal) => { + const response = await fetch(`../v1/dashboard?${key}`, { + signal, + cache: "no-store", + }); + if (!response.ok) + throw new Error( + `Dashboard request failed (HTTP ${response.status}).`, + ); + return (await response.json()) as DashboardSnapshot; + }, + (data) => { + setSaved({ key, data, refreshed: Date.now() }); + setError(undefined); + }, + (error) => + setError({ + key, + message: error instanceof Error ? error.message : String(error), + }), + ), + [key], + ); + + const data = saved?.key === key ? saved.data : undefined; + const failure = error?.key === key ? error.message : undefined; + // Keep overview/filter choices stable while a new page or detail is loading. + const overview = saved?.data.overview; + const now = Date.now(); + const current = views.find((v) => v.id === view)!; + function navigate(next: DashboardView, id?: string) { + if (next !== view) { + setView(next); + setQuery(""); + setRepository(""); + setOffset(0); + } + setSelected(id ?? ""); + } + function filter(setter: (value: string) => void, value: string) { + setter(value); + setOffset(0); + } + const counts: Record = { + findings: overview?.findings, + groups: overview?.groups, + }; + return ( +
+
+
+ + Codex Security + + Findings service +
+ Read only +
+
+ +
+
+
+

{current.label}

+

{current.description}

+
+
+
+
+
+
+ Stored findings + {count(overview?.findings)} +
+
+ Duplicate groups + {count(overview?.groups)} +
+
+
+ + + +
+ {failure && ( +
+ Unable to refresh +

+ {failure}{" "} + {data + ? "Showing the last successful data." + : "No current data to show."}{" "} + Retrying every five seconds. +

+
+ )} +
+
+ {!data ? ( +
+

+ {failure + ? "Data unavailable" + : `Loading ${current.label.toLowerCase()}…`} +

+
+ ) : data.items.length ? ( + + ) : ( +
+

+ {query || repository + ? "No matching records" + : `No ${current.label.toLowerCase()} yet`} +

+

+ {query || repository + ? "Try another search or filter." + : view === "findings" + ? "Published or imported findings will appear here." + : "Accepted duplicate groups will appear here after they are saved."} +

+
+ )} + {data && ( +
+ + {data.total === 0 + ? "0 records" + : `${data.offset + (data.items.length ? 1 : 0)}–${data.offset + data.items.length} of ${data.total}`} + +
+ + +
+
+ )} +
+ {selected && ( + + )} +
+

+ This dashboard only reads findings and duplicate groups stored in + this service. +

+
+
+
+ ); +} diff --git a/sdk/typescript/dashboard/index.html b/sdk/typescript/dashboard/index.html new file mode 100644 index 000000000..7e305b1cd --- /dev/null +++ b/sdk/typescript/dashboard/index.html @@ -0,0 +1,18 @@ + + + + + + + Findings · Codex Security + + + + +
+ + + diff --git a/sdk/typescript/dashboard/index.tsx b/sdk/typescript/dashboard/index.tsx new file mode 100644 index 000000000..65d7626e3 --- /dev/null +++ b/sdk/typescript/dashboard/index.tsx @@ -0,0 +1,11 @@ +import "./styles.css"; +import { createRoot } from "react-dom/client"; +import { App } from "./App.js"; + +const theme = matchMedia("(prefers-color-scheme: dark)"); +const applyTheme = () => { + document.documentElement.dataset["theme"] = theme.matches ? "dark" : "light"; +}; +applyTheme(); +theme.addEventListener("change", applyTheme); +createRoot(document.getElementById("root")!).render(); diff --git a/sdk/typescript/dashboard/polling.ts b/sdk/typescript/dashboard/polling.ts new file mode 100644 index 000000000..a65aec6f7 --- /dev/null +++ b/sdk/typescript/dashboard/polling.ts @@ -0,0 +1,30 @@ +import type { DashboardSnapshot } from "../src/server/dashboard-types.js"; + +/** Poll immediately and every five seconds, without overlapping requests. */ +export function pollDashboard( + read: (signal: AbortSignal) => Promise, + onData: (snapshot: DashboardSnapshot) => void, + onError: (error: unknown) => void, + clock: Pick = globalThis, +): () => void { + const controller = new AbortController(); + let pending = false; + async function refresh() { + if (pending || controller.signal.aborted) return; + pending = true; + try { + const snapshot = await read(controller.signal); + if (!controller.signal.aborted) onData(snapshot); + } catch (error) { + if (!controller.signal.aborted) onError(error); + } finally { + pending = false; + } + } + void refresh(); + const timer = clock.setInterval(() => void refresh(), 5_000); + return () => { + controller.abort(); + clock.clearInterval(timer); + }; +} diff --git a/sdk/typescript/dashboard/styles.css b/sdk/typescript/dashboard/styles.css new file mode 100644 index 000000000..c0b42fa4a --- /dev/null +++ b/sdk/typescript/dashboard/styles.css @@ -0,0 +1,433 @@ +@import "tailwindcss"; +@import "@openai/apps-sdk-ui/css"; +@source "../node_modules/@openai/apps-sdk-ui"; +@source "./**/*.{ts,tsx}"; + +body { + margin: 0; + background: var(--color-surface); + font-family: var(--font-sans); +} +.dashboard-shell { + min-height: 100vh; +} +.app-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; + padding: 18px 28px; + border-bottom: 1px solid var(--color-border); +} +.brand { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 12px; + font-size: 14px; +} +.brand-mark { + font-size: 24px; +} +.header-divider { + height: 18px; + width: 1px; + background: var(--color-border); +} +.workspace { + display: grid; + grid-template-columns: 200px minmax(0, 1fr); +} +.sidebar { + padding: 30px 16px; + border-right: 1px solid var(--color-border); + display: flex; + flex-direction: column; + gap: 16px; +} +.eyebrow { + padding: 0 12px; + font-size: 12px; + color: var(--color-text-secondary); +} +.sidebar nav { + display: grid; + gap: 4px; +} +.nav-count { + margin-left: auto; + font-variant-numeric: tabular-nums; + color: var(--color-text-secondary); +} +main { + min-width: 0; + padding: 36px 32px 24px; +} +h1 { + font-size: 28px; + font-weight: 600; + letter-spacing: -0.8px; + margin: 0 0 6px; +} +h2 { + font-size: 18px; + font-weight: 500; +} +h3 { + font-size: 14px; + font-weight: 600; + margin: 0 0 12px; +} +p { + font-size: 14px; + line-height: 1.6; +} +.page-heading { + display: flex; + align-items: flex-start; + justify-content: space-between; + flex-wrap: wrap; + gap: 20px; + margin-bottom: 28px; +} +.refresh-state { + display: flex; + align-items: flex-start; + gap: 8px; + color: var(--color-text-secondary); + font-size: 12px; + padding-top: 6px; +} +.refresh-state small { + display: block; + margin-top: 4px; + font-size: 11px; +} +.connection-dot { + display: block; + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--color-text-success); + margin-top: 5px; +} +.connection-dot.disconnected { + background: var(--color-text-danger); +} +.overview { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + border-block: 1px solid var(--color-border); + padding: 22px 0; + gap: 18px; +} +.overview > div { + display: grid; + gap: 8px; +} +.overview span { + font-size: 12px; + color: var(--color-text-secondary); +} +.overview strong { + font-size: 28px; + font-weight: 500; + line-height: 1.2; + font-variant-numeric: tabular-nums; +} +.filters { + display: flex; + flex-wrap: wrap; + align-items: end; + gap: 12px; + padding: 24px 0; +} +.filters > label { + display: grid; + gap: 6px; + min-width: 130px; + max-width: 320px; +} +.filters > label > span:not(.sr-only) { + font-size: 12px; + color: var(--color-text-secondary); +} +.filters .search-field { + min-width: 190px; + flex: 1; + max-width: none; +} +.filters select { + appearance: auto; + min-width: 0; + width: 100%; + height: 40px; + padding: 0 12px; + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + background: var(--color-surface); + color: var(--color-text); + font-size: 14px; +} +.filters select:focus-visible { + outline: 2px solid var(--color-text); + outline-offset: 2px; +} +.content-layout { + min-width: 0; +} +.content-layout.with-inspector { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(320px, 400px); + gap: 24px; + align-items: start; +} +.results { + min-width: 0; + border: 1px solid var(--color-border); + border-radius: var(--radius-xl); + overflow: hidden; +} +.table-scroll { + overflow-x: auto; +} +table { + width: 100%; + border-collapse: collapse; + text-align: left; + font-size: 13px; +} +thead { + background: var(--color-surface-secondary); +} +th, +td { + padding: 15px 16px; + vertical-align: top; + border-bottom: 1px solid var(--color-border); +} +thead th { + font-weight: 500; + font-size: 12px; + color: var(--color-text-secondary); + white-space: nowrap; +} +tbody th { + font-weight: 400; + min-width: 220px; + max-width: 360px; +} +tbody tr:last-child th, +tbody tr:last-child td { + border-bottom: 0; +} +tbody tr[data-selected] { + background: var(--color-background-primary-soft); +} +tbody tr:hover { + background: var(--color-surface-secondary); +} +.record-link { + color: var(--color-text); + text-align: left; + text-decoration: underline; + text-decoration-color: var(--color-border); + text-underline-offset: 3px; + cursor: pointer; + overflow-wrap: anywhere; +} +.record-link:hover { + text-decoration-color: currentColor; +} +.record-link:focus-visible { + outline: 2px solid var(--color-text); + outline-offset: 3px; +} +.row-title { + font-weight: 500; + text-decoration: none; +} +.row-subtitle { + display: block; + font-size: 11px; + color: var(--color-text-secondary); + margin-top: 5px; + overflow-wrap: anywhere; +} +.repository-cell { + min-width: 140px; + max-width: 240px; + overflow-wrap: anywhere; +} +.numeric { + font-variant-numeric: tabular-nums; +} +time { + font-size: 12px; +} +.pagination { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 12px; + border-top: 1px solid var(--color-border); + padding: 14px 16px; + font-size: 12px; + color: var(--color-text-secondary); +} +.inline-values { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; +} +.empty-state { + text-align: center; + padding: 64px 28px; +} +.empty-state p { + max-width: 470px; + margin: 12px auto 0; + color: var(--color-text-secondary); +} +.state-note { + font-size: 12px; + color: var(--color-text-secondary); + margin-top: 20px; +} +.inspector { + min-width: 0; + border-left: 1px solid var(--color-border); + padding-left: 24px; + overflow-wrap: anywhere; +} +.inspector-heading { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; +} +.detail-section { + padding: 20px 0; + border-bottom: 1px solid var(--color-border); +} +.detail-section:last-child { + border-bottom: 0; +} +.detail-section p { + font-size: 13px; +} +.detail-section dl { + margin-top: 12px; +} +.detail-field { + display: grid; + grid-template-columns: 112px minmax(0, 1fr); + gap: 12px; + margin: 10px 0; + font-size: 12px; +} +.detail-field dt { + color: var(--color-text-secondary); +} +.detail-field dd { + margin: 0; +} +code, +pre { + font-family: var(--font-mono); + font-size: 12px; +} +.record-links { + list-style: none; + padding: 0; + margin: 12px 0 0; + display: grid; + gap: 10px; + font-size: 12px; +} +.record-links li { + overflow-wrap: anywhere; +} +.error-message { + background: var(--color-background-danger-soft); + border-radius: var(--radius-md); + padding: 14px; + margin: 12px 0; + font-size: 13px; + overflow-wrap: anywhere; +} +.error-message strong { + color: var(--color-text-danger); +} +.error-message p { + margin-top: 4px; + white-space: pre-wrap; + font-size: 13px; +} +.finding-copy { + white-space: pre-wrap; + overflow-wrap: anywhere; + margin-top: 14px; +} +@media (max-width: 1200px) { + .content-layout.with-inspector { + grid-template-columns: minmax(0, 1fr); + } + .inspector { + border-left: 0; + padding: 0; + border-top: 1px solid var(--color-border); + } +} +@media (max-width: 800px) { + .workspace { + grid-template-columns: minmax(0, 1fr); + } + .sidebar { + padding: 12px 16px; + border-right: 0; + border-bottom: 1px solid var(--color-border); + } + .sidebar nav { + display: flex; + flex-wrap: wrap; + } + .sidebar nav > * { + width: auto; + flex: 1; + } + .eyebrow { + display: none; + } + main { + padding: 24px 16px; + } + .app-header { + padding: 16px; + } +} +@media (max-width: 480px) { + .brand { + gap: 8px; + } + .brand > span:last-child, + .header-divider { + display: none; + } + .filters > label { + flex: 1; + min-width: 125px; + max-width: 100%; + } + .filters .search-field { + flex-basis: 100%; + } + .sidebar nav > * { + flex-basis: 40%; + } + .detail-field { + grid-template-columns: 96px minmax(0, 1fr); + } +} diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json index 8846420aa..7d1ea9898 100644 --- a/sdk/typescript/package.json +++ b/sdk/typescript/package.json @@ -44,7 +44,7 @@ "scripts": { "audit:prod": "pnpm audit --prod --audit-level high", "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"", - "build": "node --run clean && tsc -p tsconfig.build.json", + "build": "node --run clean && tsc -p tsconfig.build.json && node scripts/build-dashboard.mjs", "build:plugin": "node scripts/build-plugin.mjs", "check:plugin-source": "node scripts/check-plugin-source.mjs", "check:package": "node scripts/check-package.mjs", @@ -81,16 +81,23 @@ "smol-toml": "1.6.1" }, "devDependencies": { + "@openai/apps-sdk-ui": "0.2.2", "@stryker-mutator/core": "9.6.1", + "@tailwindcss/postcss": "4.3.3", "@types/bun": "1.3.13", "@types/node": "22.19.17", "@types/papaparse": "5.3.15", "@types/react": "19.2.14", + "@types/react-dom": "19.2.3", "@types/semver": "7.8.0", + "esbuild": "0.28.2", "fast-check": "4.9.0", "ink-testing-library": "4.0.0", "json-schema-to-typescript": "15.0.4", + "postcss": "8.5.6", "prettier": "3.2.5", + "react-dom": "19.2.4", + "tailwindcss": "4.3.3", "typescript": "5.7.3" } } diff --git a/sdk/typescript/plugin-files.json b/sdk/typescript/plugin-files.json index ff98e62ee..7d77201ce 100644 --- a/sdk/typescript/plugin-files.json +++ b/sdk/typescript/plugin-files.json @@ -65,6 +65,8 @@ "scripts/workbench_db.py", "scripts/workbench_feedback.py", "scripts/workbench_finding_index.py", + "scripts/workbench_finding_workflows.py", + "scripts/workbench_dashboard.py", "scripts/workbench_findings.py", "scripts/workbench_native_indexes.py", "scripts/workbench_progress.py", diff --git a/sdk/typescript/pnpm-lock.yaml b/sdk/typescript/pnpm-lock.yaml index 44d7ab5c9..5496e12fd 100644 --- a/sdk/typescript/pnpm-lock.yaml +++ b/sdk/typescript/pnpm-lock.yaml @@ -60,9 +60,15 @@ importers: specifier: 1.6.1 version: 1.6.1 devDependencies: + '@openai/apps-sdk-ui': + specifier: 0.2.2 + version: 0.2.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.3.3) '@stryker-mutator/core': specifier: 9.6.1 version: 9.6.1(@types/node@22.19.17) + '@tailwindcss/postcss': + specifier: 4.3.3 + version: 4.3.3 '@types/bun': specifier: 1.3.13 version: 1.3.13 @@ -75,9 +81,15 @@ importers: '@types/react': specifier: 19.2.14 version: 19.2.14 + '@types/react-dom': + specifier: 19.2.3 + version: 19.2.3(@types/react@19.2.14) '@types/semver': specifier: 7.8.0 version: 7.8.0 + esbuild: + specifier: 0.28.2 + version: 0.28.2 fast-check: specifier: 4.9.0 version: 4.9.0 @@ -87,9 +99,18 @@ importers: json-schema-to-typescript: specifier: 15.0.4 version: 15.0.4 + postcss: + specifier: 8.5.6 + version: 8.5.6 prettier: specifier: 3.2.5 version: 3.2.5 + react-dom: + specifier: 19.2.4 + version: 19.2.4(react@19.2.4) + tailwindcss: + specifier: 4.3.3 + version: 4.3.3 typescript: specifier: 5.7.3 version: 5.7.3 @@ -100,6 +121,10 @@ packages: resolution: {integrity: sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw==} engines: {node: '>=18'} + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + '@apidevtools/json-schema-ref-parser@11.9.3': resolution: {integrity: sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==} engines: {node: '>= 16'} @@ -245,6 +270,10 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + '@babel/template@7.29.7': resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} @@ -260,6 +289,177 @@ packages: '@cfworker/json-schema@4.1.1': resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==} + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@floating-ui/core@1.8.0': + resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} + + '@floating-ui/dom@1.8.0': + resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} + + '@floating-ui/react-dom@2.1.9': + resolution: {integrity: sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.12': + resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} + '@graphql-typed-document-node/core@3.2.0': resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==} peerDependencies: @@ -535,6 +735,12 @@ packages: '@octokit/types@16.0.0': resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==} + '@openai/apps-sdk-ui@0.2.2': + resolution: {integrity: sha512-KaG+6qcVCKVRe51wr2te68OKoHfjRQPDVVkugxiF+SaZlgLpNbxOfyrL/FyUBmHbjh1q4p7eWDYAd75jHseZ9A==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + tailwindcss: ^4.0.10 + '@openai/codex-sdk@0.149.1': resolution: {integrity: sha512-R00Rz5327LefZggAxl28r7vFQq1vxa91OxtjZJOsQfAM/MyH8InW5qwwRu6pzUmRGp1E29XrOzm7u1TeV5Yz2A==} engines: {node: '>=18'} @@ -580,1285 +786,3574 @@ packages: cpu: [x64] os: [win32] - '@scalar/openapi-types@0.8.0': - resolution: {integrity: sha512-WmaxVSfvY5K/TwcG2B2TU1WOe1As1uc2s7myswtP6dBlcjU3hM08SApxv/jmyGaCE8t4gO5BBhmHY4pDUfmr2g==} - engines: {node: '>=22'} + '@radix-ui/number@1.1.3': + resolution: {integrity: sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==} - '@sec-ant/readable-stream@0.4.1': - resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@radix-ui/primitive@1.1.7': + resolution: {integrity: sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==} - '@sindresorhus/merge-streams@4.0.0': - resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} - engines: {node: '>=18'} + '@radix-ui/react-accessible-icon@1.1.15': + resolution: {integrity: sha512-WTQwcAvQf5sOcuUyi90lKPbhwcvQ+j55cjrSmeaN+L2vKU3DooOvlKw2MDeiJ5IkV5N905KW0/fGojKOBhD11A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@stryker-mutator/api@9.6.1': - resolution: {integrity: sha512-g8VNoFWQWbx0pdal3Vt8jVCZW+v3sc3gi94iI0GVtVgUGTqphAjJF6EAruPTx0lqvtonsaAxn5TD36hcG1d6Wg==} - engines: {node: '>=20.0.0'} + '@radix-ui/react-accordion@1.2.20': + resolution: {integrity: sha512-jDhG9FvAEnlhnjrsINbNXcUa4G+L1KqSkJSunkbKEzFRcAb52jvM0PjPxPRvhe1HNc5F5yc0yzzWeeqlH4yBIg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@stryker-mutator/core@9.6.1': - resolution: {integrity: sha512-WMgnvf+Wyh/yiruhNZwc8w8DlzmmjXhPjSn5MR8RhAXzlnWji8TQrUYgBUkHk9bEgSaIlB3KZHm37iiU5Q2cLQ==} - engines: {node: '>=20.0.0'} - hasBin: true + '@radix-ui/react-alert-dialog@1.1.23': + resolution: {integrity: sha512-VAYOiQRqj3GPpYJE0I9J+X8Ip05cyVlNdKOFeiGS2Ou1HHGfpl0BxOyZm6nmVDyU+W+NF3/XLzmjHmVGydhwgA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@stryker-mutator/instrumenter@9.6.1': - resolution: {integrity: sha512-5K8wH4Pthly25c2uKKik4Dfcoeou7sbJdFS6u3QIYHlulgFVDJwtEMWTZGkZfs7IiUEXIDNa0keRACq5jn5AvA==} - engines: {node: '>=20.0.0'} + '@radix-ui/react-arrow@1.1.15': + resolution: {integrity: sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@stryker-mutator/util@9.6.1': - resolution: {integrity: sha512-Lk/ALVctJjFv1vvwR+CFoKzDCWvsBlq7flDUnmnpuwTrGbm156EdZD1Jjq4o8KdOap0ezUZqQNE9OAI1m2+pUQ==} + '@radix-ui/react-aspect-ratio@1.1.15': + resolution: {integrity: sha512-fy+dyVR+90nelK8rqIznFlxzx7uPcGbhxH8Nfr2bHb4UfSe+e3hklOC0luK0hDwVwnRX7xTRySpsrQVeW+/oNQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@toon-format/toon@2.3.0': - resolution: {integrity: sha512-/Ew9etdRQKVMnm9fDaCG0JjyAOK/O7T0M97oum1aW4W+UR8ZhVVPBanIV7oWgHBiGlnVxV9M55PWQCHofDV07w==} + '@radix-ui/react-avatar@1.2.6': + resolution: {integrity: sha512-4ULOTJ/mqy2hT9GlWa/MFHxHSvH3nJzHnZM1waNsc5Bonv7i70aNenghXmD97S6OJ81ekXONGGt4nT1r0PfEdA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@types/bun@1.3.13': - resolution: {integrity: sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw==} + '@radix-ui/react-checkbox@1.3.11': + resolution: {integrity: sha512-Gnptr9pDDQxD3hgq2dtPbtrp/c2qH1mBwIzw3X/ivrMb2e1t0jMTi606fVEqFPaQR1ggXIVQWKj3P2WW9v7zGQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@radix-ui/react-collapsible@1.1.20': + resolution: {integrity: sha512-mcGesGplBnzN2sbvJETzpCNfSMyPnb29q1GRLU+Ib7bJrpIG2ywmRoh2V5VbA2uNvKikKUlVbAPks7JDjz4A8Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@types/lodash@4.17.24': - resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==} + '@radix-ui/react-collection@1.1.15': + resolution: {integrity: sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@types/node@22.19.17': - resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==} + '@radix-ui/react-compose-refs@1.1.5': + resolution: {integrity: sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - '@types/papaparse@5.3.15': - resolution: {integrity: sha512-JHe6vF6x/8Z85nCX4yFdDslN11d+1pr12E526X8WAfhadOeaOTx5AuIkvDKIBopfvlzpzkdMx4YyvSKCM9oqtw==} + '@radix-ui/react-context-menu@2.3.7': + resolution: {integrity: sha512-CtXP35dxaB5T3zXSd+E3uHe/QpXcpYnZmxp6OaIbfthtfW4wyb77M23BG+bwIJDtsMwEP/YssdsmNyZu7jhWew==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@types/react@19.2.14': - resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@radix-ui/react-context@1.2.2': + resolution: {integrity: sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - '@types/semver@7.8.0': - resolution: {integrity: sha512-1mAINjtQCXXeLkJ9ehXkwOcBpqtLxiVtKhpUf83DdRNdQKV0iXZpaHYqRr7nj+wvxuJzoAmAwXI+sCNMv1CzLQ==} + '@radix-ui/react-dialog@1.1.23': + resolution: {integrity: sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@types/yauzl@2.10.3': - resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + '@radix-ui/react-direction@1.1.4': + resolution: {integrity: sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - ajv@8.18.0: - resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + '@radix-ui/react-dismissable-layer@1.1.19': + resolution: {integrity: sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - ajv@8.20.0: - resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + '@radix-ui/react-dropdown-menu@2.1.24': + resolution: {integrity: sha512-geq8l2rJkxvkXsT9RMgtUE3P8pITFpTsvYpbySi1IH4fZEABD/Gp85myayFgxk0ktljGMJnCbeFkyTusvSvv7g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - angular-html-parser@10.4.0: - resolution: {integrity: sha512-++nLNyZwRfHqFh7akH5Gw/JYizoFlMRz0KRigfwfsLqV8ZqlcVRb1LkPEWdYvEKDnbktknM2J4BXaYUGrQZPww==} - engines: {node: '>= 14'} + '@radix-ui/react-focus-guards@1.1.6': + resolution: {integrity: sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - ansi-escapes@7.3.0: - resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} - engines: {node: '>=18'} + '@radix-ui/react-focus-scope@1.1.16': + resolution: {integrity: sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - ansi-regex@6.2.2: - resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} - engines: {node: '>=12'} + '@radix-ui/react-form@0.1.16': + resolution: {integrity: sha512-Q4TLEn2A7TAypxwmd6R9EwrlXDvkfYSDMrq9/887AXAGh+G1rH+kYJKSTv+Si9Y0JPKTwKYv6PviAJosysNimA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - ansi-styles@6.2.3: - resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} - engines: {node: '>=12'} + '@radix-ui/react-hover-card@1.1.23': + resolution: {integrity: sha512-H8qONfZd3ltrU3+jHCIgITbWo6e1iTKvP9DHdrvYbX48ooRM5FjEDTn16AMwdfuOGkWdZEhpl3PLL/Wk/AnHDQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + '@radix-ui/react-id@1.1.4': + resolution: {integrity: sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - auto-bind@5.0.1: - resolution: {integrity: sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - balanced-match@4.0.4: - resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} - engines: {node: 18 || 20 || >=22} + '@radix-ui/react-label@2.1.15': + resolution: {integrity: sha512-o/rdYEwZTTo5tjknnPeyQFU45kUC4i/XyeDPP+HGyi6XqpOP6Zf5Ya5vh/Yfe9Id5JiuWnnAx2XqIeD3UYZt0g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + '@radix-ui/react-menu@2.1.24': + resolution: {integrity: sha512-uW7RVuU6Lp/ZtfeY4b3kL32zccgEWvPv1+cf17ubYzHa9cL8AHokmk36cG/XEiH/smbQvumnieXX9j/e9RqJWA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - baseline-browser-mapping@2.11.13: - resolution: {integrity: sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ==} - engines: {node: '>=6.0.0'} - hasBin: true + '@radix-ui/react-menubar@1.1.24': + resolution: {integrity: sha512-eeVs0vf7cuqXaM0qLQCPcufImiJNVBXdJDLu7ZGYl2732UH23Qat/foNGrr6vYV3/DdTsBqASoggUFgH14OcZA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - before-after-hook@4.0.0: - resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} + '@radix-ui/react-navigation-menu@1.2.22': + resolution: {integrity: sha512-ou7iLEJ+yrhQndkkA4U21XIdS/CS45F4iXIkTZcb6/Ne9EMsOuDudVmCwmDnfFZZ+y1FZqXRNSIgBy+YMvZVZg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - brace-expansion@5.0.9: - resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} - engines: {node: 20 || >=22} + '@radix-ui/react-one-time-password-field@0.1.16': + resolution: {integrity: sha512-Tj9P6ntAJEw52oq/F0AGknXR4XncxEt7XU47O3xJQOiWfLzEy3d9gtgKfvjSzGxzHkfL+VzvxGu2KTFsloJqXw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - browserslist@4.28.8: - resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true + '@radix-ui/react-password-toggle-field@0.1.11': + resolution: {integrity: sha512-4gvFnmDXu3dgj21CqsufzIameRvlRd4SBqaWhcrlrNhRo0Y5i/49AmRJYe1fdAM3G2VNBbmin4b0D6cdQocwgw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - buffer-crc32@0.2.13: - resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + '@radix-ui/react-popover@1.1.23': + resolution: {integrity: sha512-mw58MrBlyHWFisTOYignD0vf/3gdcgAR+9of1s9G/38CbFiUwH1nCDkc0AUM9IrXFgN5Ue8n45j9WCgyM1sbiQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - bun-types@1.3.13: - resolution: {integrity: sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA==} + '@radix-ui/react-popper@1.3.7': + resolution: {integrity: sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} + '@radix-ui/react-portal@1.1.17': + resolution: {integrity: sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} + '@radix-ui/react-presence@1.1.10': + resolution: {integrity: sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - caniuse-lite@1.0.30001809: - resolution: {integrity: sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==} + '@radix-ui/react-primitive@2.1.10': + resolution: {integrity: sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - chalk@5.6.2: - resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + '@radix-ui/react-progress@1.1.16': + resolution: {integrity: sha512-5XnomAsoZZCY+KNTxbIghpGqPruZvKFNlvcAljVAOdDRDsH4/OZQxhtwo5wdtoDM5R6MhJBb2sPnDuRFep3lzg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - chardet@2.2.0: - resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==} + '@radix-ui/react-radio-group@1.4.7': + resolution: {integrity: sha512-cgYFEkntCxppHZgtSZ+7vh0wbZQ+IC7PPMw8DSnRG27B6kDd32/Zw0OJt7dGDigCoprMuWHjg2PvUn3PYvPFoQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - cli-boxes@3.0.0: - resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} - engines: {node: '>=10'} + '@radix-ui/react-roving-focus@1.1.19': + resolution: {integrity: sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - cli-cursor@4.0.0: - resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + '@radix-ui/react-scroll-area@1.2.18': + resolution: {integrity: sha512-Zn5Cd171wxsO3Dfg8HaW6RifTb9CYTKQJHs/G4+LN1GfmJpaQMZQyQxMprVPHpaz7QY4l9BxK2JwQuzHsXC8nA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - cli-truncate@5.2.0: - resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} - engines: {node: '>=20'} + '@radix-ui/react-select@2.3.7': + resolution: {integrity: sha512-WFGImkmbzcfxeIwq/+4HvRN0pizBwbwQUED4I13ezQsDdfl38ZntN6TmR8XaSzPBqoCToe8rF75j6NPNDSzhbg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - cli-width@4.1.0: - resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} - engines: {node: '>= 12'} + '@radix-ui/react-separator@1.1.15': + resolution: {integrity: sha512-jOLO4lssEzWpoDu7G+Ze4VjwMRUBt291pnZD0gmalREZipnTX3wadQo7Fy48GCTfe14/YRN6rw/rOJqrE85Wxw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - code-excerpt@4.0.0: - resolution: {integrity: sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + '@radix-ui/react-slider@1.4.7': + resolution: {integrity: sha512-mTSLf1GC/C0moWjTbvCM6Qn/gBjvlFt1azuWF2v7MN5C3Zq2U2J2lN3ZEYkpujuOU5Ro7A28wkviSxaKnG0BYg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - commander@14.0.3: - resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} - engines: {node: '>=20'} + '@radix-ui/react-slot@1.3.3': + resolution: {integrity: sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - content-type@2.0.0: - resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} - engines: {node: '>=18'} + '@radix-ui/react-switch@1.3.7': + resolution: {integrity: sha512-48tB/4dn2UVLBCYhTu9AuR63IHl73l/qLbLgxd86noTUor4/K4LFDAcYjK+isP5313qxaFpjPVogE7+Y0/V3Kw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + '@radix-ui/react-tabs@1.1.21': + resolution: {integrity: sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - convert-to-spaces@2.0.1: - resolution: {integrity: sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + '@radix-ui/react-toast@1.2.23': + resolution: {integrity: sha512-ofhyAsYaocRGOs/n0XWdUOSVzEAG6BfrMVM8z0c0kLEWY38w/0WuMFPTJP/HVaZPYkMvHZoKIIhNcjbTCBILPg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} + '@radix-ui/react-toggle-group@1.1.19': + resolution: {integrity: sha512-OtnwuSVjd1Ofi+AdnvhsjQdyuhCDwYs1w9RyB5BN/OavXOVQo42SYqQjwUnbPnaiPFBpQ9aX70dWeee+v2oBLA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + '@radix-ui/react-toggle@1.1.18': + resolution: {integrity: sha512-7lonPlKfSacd20GlOBx2ltuVKz9oqWYZz+oMQyOltw6t1y2nyftj2ZmwwUHYn49kqfDWcp8dNZm5NgV+5Z+mug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} + '@radix-ui/react-toolbar@1.1.19': + resolution: {integrity: sha512-Ph0IvtYw4VB12ZnZg+YtrGs8yJQsnizwo/zu0R4Y/nWugtJzA7Pg1eWeuDR9+LSqn+xjamss+UOSOJJJ4gx8jw==} peerDependencies: - supports-color: '*' + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - supports-color: + '@types/react': + optional: true + '@types/react-dom': optional: true - des.js@1.1.0: - resolution: {integrity: sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==} + '@radix-ui/react-tooltip@1.2.16': + resolution: {integrity: sha512-6EamKFRRnlpdadndbZ6LMwycfwkwPte1B42hs6QA0gYhjaOKqW4PZ4pjaW9UrlDX5eVt/OjncE7BFTPL5nmZhg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - diff-match-patch@1.0.5: - resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} + '@radix-ui/react-use-callback-ref@1.1.4': + resolution: {integrity: sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} + '@radix-ui/react-use-controllable-state@1.2.6': + resolution: {integrity: sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - electron-to-chromium@1.5.404: - resolution: {integrity: sha512-3WJtd7/lVq2Jnuz6wed1l9+1ZD2u2Tet1/1NBc4Iedkmgbu+I7YuAqdAQ8T+VZtnwysMsAf3IqSq9D1gyZjA2g==} + '@radix-ui/react-use-effect-event@0.0.5': + resolution: {integrity: sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - emoji-regex@10.6.0: - resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + '@radix-ui/react-use-escape-keydown@1.1.5': + resolution: {integrity: sha512-ge3ipobwSXTj4JyVtswQ7qZj0ZHdtbGuOno/LrgAAeSxtsJ6Vs4Gz5IkPH2bmqpjcLUFoqGhA/mueuIf63UXlA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - end-of-stream@1.4.5: - resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + '@radix-ui/react-use-is-hydrated@0.1.3': + resolution: {integrity: sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - environment@1.1.0: - resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} - engines: {node: '>=18'} + '@radix-ui/react-use-layout-effect@1.1.4': + resolution: {integrity: sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} + '@radix-ui/react-use-previous@1.1.4': + resolution: {integrity: sha512-XoSLhbRbqxFtgJoi2fNHA3C6pDlY34x508vUpUGoFZfvePfHXHbE1lC4FYFMnJWgiCRroSTw6fOsXQoVS9RwZg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} + '@radix-ui/react-use-rect@1.1.4': + resolution: {integrity: sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - es-object-atoms@1.1.2: - resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} - engines: {node: '>= 0.4'} + '@radix-ui/react-use-size@1.1.4': + resolution: {integrity: sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - es-toolkit@1.50.0: - resolution: {integrity: sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==} + '@radix-ui/react-visually-hidden@1.2.11': + resolution: {integrity: sha512-NFS86RYYZb4/exihaESBGOpMJFz8MGLAfu3mOBSGByVnVPC9JPASfYubxd/8KbkQK0sYAv8lVQDEQukDX/qXvQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} + '@radix-ui/rect@1.1.3': + resolution: {integrity: sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==} - escape-string-regexp@2.0.0: - resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} - engines: {node: '>=8'} + '@scalar/openapi-types@0.8.0': + resolution: {integrity: sha512-WmaxVSfvY5K/TwcG2B2TU1WOe1As1uc2s7myswtP6dBlcjU3hM08SApxv/jmyGaCE8t4gO5BBhmHY4pDUfmr2g==} + engines: {node: '>=22'} - execa@9.6.1: - resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} - engines: {node: ^18.19.0 || >=20.5.0} + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} - extract-zip@2.0.1: - resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} - engines: {node: '>= 10.17.0'} - hasBin: true + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} - fast-check@4.9.0: - resolution: {integrity: sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg==} - engines: {node: '>=12.17.0'} + '@stryker-mutator/api@9.6.1': + resolution: {integrity: sha512-g8VNoFWQWbx0pdal3Vt8jVCZW+v3sc3gi94iI0GVtVgUGTqphAjJF6EAruPTx0lqvtonsaAxn5TD36hcG1d6Wg==} + engines: {node: '>=20.0.0'} - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + '@stryker-mutator/core@9.6.1': + resolution: {integrity: sha512-WMgnvf+Wyh/yiruhNZwc8w8DlzmmjXhPjSn5MR8RhAXzlnWji8TQrUYgBUkHk9bEgSaIlB3KZHm37iiU5Q2cLQ==} + engines: {node: '>=20.0.0'} + hasBin: true - fast-string-truncated-width@3.0.3: - resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + '@stryker-mutator/instrumenter@9.6.1': + resolution: {integrity: sha512-5K8wH4Pthly25c2uKKik4Dfcoeou7sbJdFS6u3QIYHlulgFVDJwtEMWTZGkZfs7IiUEXIDNa0keRACq5jn5AvA==} + engines: {node: '>=20.0.0'} - fast-string-width@3.0.2: - resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + '@stryker-mutator/util@9.6.1': + resolution: {integrity: sha512-Lk/ALVctJjFv1vvwR+CFoKzDCWvsBlq7flDUnmnpuwTrGbm156EdZD1Jjq4o8KdOap0ezUZqQNE9OAI1m2+pUQ==} - fast-uri@3.1.5: - resolution: {integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==} + '@tailwindcss/node@4.3.3': + resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} - fast-wrap-ansi@0.2.2: - resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + '@tailwindcss/oxide-android-arm64@4.3.3': + resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] - fd-slicer@1.1.0: - resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + '@tailwindcss/oxide-darwin-arm64@4.3.3': + resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true + '@tailwindcss/oxide-darwin-x64@4.3.3': + resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] - fflate@0.8.2: - resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + '@tailwindcss/oxide-freebsd-x64@4.3.3': + resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] - figures@6.1.0: - resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} - engines: {node: '>=18'} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] - gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] - get-east-asian-width@1.6.0: - resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} - engines: {node: '>=18'} + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] - get-stream@5.2.0: - resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} - engines: {node: '>=8'} + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] - get-stream@9.0.1: - resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} - engines: {node: '>=18'} + '@tailwindcss/oxide@4.3.3': + resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} + engines: {node: '>= 20'} - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} + '@tailwindcss/postcss@4.3.3': + resolution: {integrity: sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==} - graphql@17.0.2: - resolution: {integrity: sha512-FRWbddMxfkjiB7z+aQDWIR+E34xo9I8c9mtK2RPv8PmMzKRvrdsreHL/Ui/TmwHJfhHChEtsFPyMHKI+xuarQQ==} - engines: {node: ^22.0.0 || ^24.0.0 || ^25.0.0 || >=26.0.0} + '@toon-format/toon@2.3.0': + resolution: {integrity: sha512-/Ew9etdRQKVMnm9fDaCG0JjyAOK/O7T0M97oum1aW4W+UR8ZhVVPBanIV7oWgHBiGlnVxV9M55PWQCHofDV07w==} - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} + '@types/bun@1.3.13': + resolution: {integrity: sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw==} - hasown@2.0.4: - resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} - engines: {node: '>= 0.4'} + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} - human-signals@8.0.1: - resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} - engines: {node: '>=18.18.0'} + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} - iconv-lite@0.7.3: - resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} - engines: {node: '>=0.10.0'} + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - incur@0.4.13: - resolution: {integrity: sha512-BeKlYFLIsRCgC8IxsUd3S2/4kPeZaNf1Rbz+Kz41jQII4jOgr7j5w4KYe00aAEQa3V0Ur57BVxE5JDTx8L2s5Q==} - engines: {node: '>=22'} - hasBin: true + '@types/hast@3.0.5': + resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} - indent-string@5.0.0: - resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} - engines: {node: '>=12'} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + '@types/katex@0.16.8': + resolution: {integrity: sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==} - ink-testing-library@4.0.0: - resolution: {integrity: sha512-yF92kj3pmBvk7oKbSq5vEALO//o7Z9Ck/OaLNlkzXNeYdwfpxMQkSowGTFUCS5MSu9bWfSZMewGpp7bFc66D7Q==} - engines: {node: '>=18'} - peerDependencies: - '@types/react': '>=18.0.0' - peerDependenciesMeta: - '@types/react': - optional: true + '@types/lodash@4.17.24': + resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==} - ink@6.8.0: - resolution: {integrity: sha512-sbl1RdLOgkO9isK42WCZlJCFN9hb++sX9dsklOvfd1YQ3bQ2AiFu12Q6tFlr0HvEUvzraJntQCCpfEoUe9DSzA==} - engines: {node: '>=20'} - peerDependencies: - '@types/react': '>=19.0.0' - react: '>=19.0.0' - react-devtools-core: '>=6.1.2' - peerDependenciesMeta: - '@types/react': - optional: true - react-devtools-core: - optional: true + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - is-fullwidth-code-point@5.1.0: - resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} - engines: {node: '>=18'} + '@types/node@22.19.17': + resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==} - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} + '@types/papaparse@5.3.15': + resolution: {integrity: sha512-JHe6vF6x/8Z85nCX4yFdDslN11d+1pr12E526X8WAfhadOeaOTx5AuIkvDKIBopfvlzpzkdMx4YyvSKCM9oqtw==} - is-in-ci@2.0.0: - resolution: {integrity: sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w==} - engines: {node: '>=20'} - hasBin: true + '@types/prismjs@1.26.6': + resolution: {integrity: sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==} - is-plain-obj@4.1.0: - resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} - engines: {node: '>=12'} + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 - is-stream@4.0.1: - resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} - engines: {node: '>=18'} + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} - is-unicode-supported@2.1.0: - resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} - engines: {node: '>=18'} + '@types/semver@7.8.0': + resolution: {integrity: sha512-1mAINjtQCXXeLkJ9ehXkwOcBpqtLxiVtKhpUf83DdRNdQKV0iXZpaHYqRr7nj+wvxuJzoAmAwXI+sCNMv1CzLQ==} - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} - js-md4@0.3.2: - resolution: {integrity: sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==} + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - js-tiktoken@1.0.21: - resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==} + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + '@ungap/structured-clone@1.3.3': + resolution: {integrity: sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==} - js-yaml@4.3.1: - resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} - hasBin: true + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} - jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} - hasBin: true + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} - json-rpc-2.0@1.7.1: - resolution: {integrity: sha512-JqZjhjAanbpkXIzFE7u8mE/iFblawwlXtONaCvRqI+pyABVz7B4M1EUNpyVW+dZjqgQ2L5HFmZCmOCgUKm00hg==} + angular-html-parser@10.4.0: + resolution: {integrity: sha512-++nLNyZwRfHqFh7akH5Gw/JYizoFlMRz0KRigfwfsLqV8ZqlcVRb1LkPEWdYvEKDnbktknM2J4BXaYUGrQZPww==} + engines: {node: '>= 14'} - json-schema-to-typescript@15.0.4: - resolution: {integrity: sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ==} - engines: {node: '>=16.0.0'} - hasBin: true + ansi-escapes@7.3.0: + resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} + engines: {node: '>=18'} - json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} - json-with-bigint@3.5.10: - resolution: {integrity: sha512-Vcx+JVNEBts/xfcoCS69sKrOhOk/3TVlvlT+XzUOefVKnnrbYSCKpDCm10pohsJFtsJVYnwa/cXRZ4eElzaM6w==} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + auto-bind@5.0.1: + resolution: {integrity: sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.11.13: + resolution: {integrity: sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ==} + engines: {node: '>=6.0.0'} hasBin: true - lodash.groupby@4.6.0: - resolution: {integrity: sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==} + before-after-hook@4.0.0: + resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} - lodash@4.18.1: - resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} - lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + browserslist@4.28.8: + resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + bun-types@1.3.13: + resolution: {integrity: sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} - mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + caniuse-lite@1.0.30001809: + resolution: {integrity: sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + + chardet@2.2.0: + resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==} + + cli-boxes@3.0.0: + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} + + cli-cursor@4.0.0: + resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + cli-truncate@5.2.0: + resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} + engines: {node: '>=20'} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} - minimalistic-assert@1.0.1: - resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + code-excerpt@4.0.0: + resolution: {integrity: sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - minimatch@10.2.6: - resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} - engines: {node: 18 || 20 || >=22} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} - minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} - mutation-server-protocol@0.4.1: - resolution: {integrity: sha512-SBGK0j8hLDne7bktgThKI8kGvGTx3rY3LAeQTmOKZ5bVnL/7TorLMvcVF7dIPJCu5RNUWhkkuF53kurygYVt3g==} + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} engines: {node: '>=18'} - mutation-testing-elements@3.7.3: - resolution: {integrity: sha512-SMeIPxngJpfjfNYctFpYQQtlBlZaVO0aoB3FKdwrI8Ee/2bkyUuCZzAOCLv1U9fnmfA37dPFq0Owduoxs2XgGQ==} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - mutation-testing-metrics@3.7.3: - resolution: {integrity: sha512-B8QrP0ZomErzTPNlhrzKWPNBln+3afwBZPHv0Q7N8wZZTYxMptzb/Gdm3ExXVmioVYrtZAtsDs7W/T/b2AixOQ==} + convert-to-spaces@2.0.1: + resolution: {integrity: sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - mutation-testing-report-schema@3.7.3: - resolution: {integrity: sha512-BHm3MYq+ckO+t5CtlG8zpqxc75rdJCkxVlE+fGuGJM3F7tNCQ/OW2N+TQVHN3BHsYa84+BFc6g3AwDYkUsw2MA==} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} - mute-stream@3.0.0: - resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} - engines: {node: ^20.17.0 || >=22.9.0} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - node-releases@2.0.53: - resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==} - engines: {node: '>=18'} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true - npm-run-path@6.0.0: - resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + des.js@1.1.0: + resolution: {integrity: sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + diff-match-patch@1.0.5: + resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.404: + resolution: {integrity: sha512-3WJtd7/lVq2Jnuz6wed1l9+1ZD2u2Tet1/1NBc4Iedkmgbu+I7YuAqdAQ8T+VZtnwysMsAf3IqSq9D1gyZjA2g==} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + enhanced-resolve@5.24.5: + resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} + engines: {node: '>=10.13.0'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} - object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} - onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} - papaparse@5.5.3: - resolution: {integrity: sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==} + es-toolkit@1.50.0: + resolution: {integrity: sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==} - parse-ms@4.0.0: - resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} engines: {node: '>=18'} + hasBin: true - patch-console@2.0.0: - resolution: {integrity: sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} - path-key@4.0.0: - resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - pdfjs-dist@6.2.108: - resolution: {integrity: sha512-YxFb+SQcodN2rnX9Tn3dHYlqfb7NjlzzfONPpJd+AKoKtUjEdevTfbC07d5TcczzOK6261auRkP/M8OBHs9vFQ==} - engines: {node: '>=22.13.0 || >=24'} + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} - pend@1.2.0: - resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + execa@9.6.1: + resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} + engines: {node: ^18.19.0 || >=20.5.0} - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + + fast-check@4.9.0: + resolution: {integrity: sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg==} + engines: {node: '>=12.17.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + + fast-uri@3.1.5: + resolution: {integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==} + + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + + fault@1.0.4: + resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==} + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + + format@0.2.2: + resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} + engines: {node: '>=0.4.x'} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphql@17.0.2: + resolution: {integrity: sha512-FRWbddMxfkjiB7z+aQDWIR+E34xo9I8c9mtK2RPv8PmMzKRvrdsreHL/Ui/TmwHJfhHChEtsFPyMHKI+xuarQQ==} + engines: {node: ^22.0.0 || ^24.0.0 || ^25.0.0 || >=26.0.0} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + hast-util-from-dom@5.0.1: + resolution: {integrity: sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==} + + hast-util-from-html-isomorphic@2.0.0: + resolution: {integrity: sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==} + + hast-util-from-html@2.0.3: + resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} + + hast-util-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} + + hast-util-is-element@3.0.0: + resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} + + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-to-text@4.0.2: + resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + + highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + + highlightjs-vue@1.0.0: + resolution: {integrity: sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==} + + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + + incur@0.4.13: + resolution: {integrity: sha512-BeKlYFLIsRCgC8IxsUd3S2/4kPeZaNf1Rbz+Kz41jQII4jOgr7j5w4KYe00aAEQa3V0Ur57BVxE5JDTx8L2s5Q==} + engines: {node: '>=22'} + hasBin: true + + indent-string@5.0.0: + resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} + engines: {node: '>=12'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ink-testing-library@4.0.0: + resolution: {integrity: sha512-yF92kj3pmBvk7oKbSq5vEALO//o7Z9Ck/OaLNlkzXNeYdwfpxMQkSowGTFUCS5MSu9bWfSZMewGpp7bFc66D7Q==} + engines: {node: '>=18'} + peerDependencies: + '@types/react': '>=18.0.0' + peerDependenciesMeta: + '@types/react': + optional: true + + ink@6.8.0: + resolution: {integrity: sha512-sbl1RdLOgkO9isK42WCZlJCFN9hb++sX9dsklOvfd1YQ3bQ2AiFu12Q6tFlr0HvEUvzraJntQCCpfEoUe9DSzA==} + engines: {node: '>=20'} + peerDependencies: + '@types/react': '>=19.0.0' + react: '>=19.0.0' + react-devtools-core: '>=6.1.2' + peerDependenciesMeta: + '@types/react': + optional: true + react-devtools-core: + optional: true + + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@5.1.0: + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} + engines: {node: '>=18'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + + is-in-ci@2.0.0: + resolution: {integrity: sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w==} + engines: {node: '>=20'} + hasBin: true + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-md4@0.3.2: + resolution: {integrity: sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==} + + js-tiktoken@1.0.21: + resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.3.1: + resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-rpc-2.0@1.7.1: + resolution: {integrity: sha512-JqZjhjAanbpkXIzFE7u8mE/iFblawwlXtONaCvRqI+pyABVz7B4M1EUNpyVW+dZjqgQ2L5HFmZCmOCgUKm00hg==} + + json-schema-to-typescript@15.0.4: + resolution: {integrity: sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ==} + engines: {node: '>=16.0.0'} + hasBin: true + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-with-bigint@3.5.10: + resolution: {integrity: sha512-Vcx+JVNEBts/xfcoCS69sKrOhOk/3TVlvlT+XzUOefVKnnrbYSCKpDCm10pohsJFtsJVYnwa/cXRZ4eElzaM6w==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + katex@0.16.47: + resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==} + hasBin: true + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + + lodash.groupby@4.6.0: + resolution: {integrity: sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + lowlight@1.20.0: + resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + luxon@3.7.1: + resolution: {integrity: sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==} + engines: {node: '>=12'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdast-util-directive@3.1.0: + resolution: {integrity: sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-math@3.0.0: + resolution: {integrity: sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-newline-to-break@2.0.0: + resolution: {integrity: sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-directive@3.0.2: + resolution: {integrity: sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-extension-math@3.1.0: + resolution: {integrity: sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mutation-server-protocol@0.4.1: + resolution: {integrity: sha512-SBGK0j8hLDne7bktgThKI8kGvGTx3rY3LAeQTmOKZ5bVnL/7TorLMvcVF7dIPJCu5RNUWhkkuF53kurygYVt3g==} + engines: {node: '>=18'} + + mutation-testing-elements@3.7.3: + resolution: {integrity: sha512-SMeIPxngJpfjfNYctFpYQQtlBlZaVO0aoB3FKdwrI8Ee/2bkyUuCZzAOCLv1U9fnmfA37dPFq0Owduoxs2XgGQ==} + + mutation-testing-metrics@3.7.3: + resolution: {integrity: sha512-B8QrP0ZomErzTPNlhrzKWPNBln+3afwBZPHv0Q7N8wZZTYxMptzb/Gdm3ExXVmioVYrtZAtsDs7W/T/b2AixOQ==} + + mutation-testing-report-schema@3.7.3: + resolution: {integrity: sha512-BHm3MYq+ckO+t5CtlG8zpqxc75rdJCkxVlE+fGuGJM3F7tNCQ/OW2N+TQVHN3BHsYa84+BFc6g3AwDYkUsw2MA==} + + mute-stream@3.0.0: + resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} + engines: {node: ^20.17.0 || >=22.9.0} + + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-releases@2.0.53: + resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==} + engines: {node: '>=18'} + + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + papaparse@5.5.3: + resolution: {integrity: sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==} + + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + patch-console@2.0.0: + resolution: {integrity: sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + pdfjs-dist@6.2.108: + resolution: {integrity: sha512-YxFb+SQcodN2rnX9Tn3dHYlqfb7NjlzzfONPpJd+AKoKtUjEdevTfbC07d5TcczzOK6261auRkP/M8OBHs9vFQ==} + engines: {node: '>=22.13.0 || >=24'} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prettier@3.2.5: + resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} + engines: {node: '>=14'} + hasBin: true + + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + engines: {node: '>=18'} + + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + + property-information@7.2.0: + resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + pure-rand@8.4.2: + resolution: {integrity: sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==} + + qs@6.15.1: + resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} + engines: {node: '>=0.6'} + + radix-ui@1.6.7: + resolution: {integrity: sha512-QBdhh1arIEUvPC0dQ5+nwWAxt7+N+oP/9jPwjJkGFoSk/sqxg32gJtSXGtFh8frAIcS6oC9cx2Q+7KYCQLOAeA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + react-dom@19.2.4: + resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} + peerDependencies: + react: ^19.2.4 + + react-markdown@9.1.0: + resolution: {integrity: sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + + react-merge-refs@2.1.1: + resolution: {integrity: sha512-jLQXJ/URln51zskhgppGJ2ub7b2WFKGq3cl3NYKtlHoTG+dN2q7EzWrn3hN3EgPsTMvpR9tpq5ijdp7YwFZkag==} + + react-reconciler@0.33.0: + resolution: {integrity: sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==} + engines: {node: '>=0.10.0'} + peerDependencies: + react: ^19.2.0 + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-syntax-highlighter@16.1.1: + resolution: {integrity: sha512-PjVawBGy80C6YbC5DDZJeUjBmC7skaoEUdvfFQediQHgCL7aKyVHe57SaJGfQsloGDac+gCpTfRdtxzWWKmCXA==} + engines: {node: '>= 16.20.2'} + peerDependencies: + react: '>= 0.14.0' + + react@19.2.4: + resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} + engines: {node: '>=0.10.0'} + + refractor@5.0.0: + resolution: {integrity: sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==} + + rehype-katex@7.0.1: + resolution: {integrity: sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==} + + remark-breaks@4.0.0: + resolution: {integrity: sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==} + + remark-directive@3.0.1: + resolution: {integrity: sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A==} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-math@6.0.0: + resolution: {integrity: sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + restore-cursor@4.0.0: + resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slice-ansi@8.0.0: + resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} + engines: {node: '>=20'} + + smol-toml@1.6.1: + resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} + engines: {node: '>= 18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string-width@8.2.2: + resolution: {integrity: sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==} + engines: {node: '>=20'} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + + tailwindcss@4.3.3: + resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + terminal-size@4.0.1: + resolution: {integrity: sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tokenx@1.3.0: + resolution: {integrity: sha512-NLdXTEZkKiO0gZuLtMoZKjCXTREXeZZt8nnnNeyoXtNZAfG/GKGSbQtLU5STspc0rMSwcA+UJfWZkbNU01iKmQ==} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tunnel@0.0.6: + resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} + engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} + + type-fest@5.8.0: + resolution: {integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==} + engines: {node: '>=20'} + + typed-inject@5.0.0: + resolution: {integrity: sha512-0Ql2ORqBORLMdAW89TQKZsb1PQkFGImFfVmncXWe7a+AA3+7dh7Se9exxZowH4kbnlvKEFkMxUYdHUpjYWFJaA==} + engines: {node: '>=18'} + + typed-rest-client@2.3.1: + resolution: {integrity: sha512-k4kX5Up6qA68D0Cby2AK+6+vM5k3qTxe+/3FqhnHRExjY5cfbOnzjQZbP/LXleF8hVoDvDqxlgk9KK83HoBZlQ==} + engines: {node: '>= 16.0.0'} + + typescript@5.7.3: + resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} + engines: {node: '>=14.17'} + hasBin: true + + underscore@1.13.8: + resolution: {integrity: sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-find-after@5.0.0: + resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-remove-position@5.0.0: + resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + + universal-user-agent@7.0.3: + resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} + + update-browserslist-db@1.3.1: + resolution: {integrity: sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + usehooks-ts@3.1.1: + resolution: {integrity: sha512-I4diPp9Cq6ieSUH2wu+fDAVQO43xwtulo+fKEidHUwZPnYImbtkTjzIJYcDcJqxgmX31GVqNFURodvcgHcW0pA==} + engines: {node: '>=16.15.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc + + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + weapon-regex@1.3.6: + resolution: {integrity: sha512-wsf1m1jmMrso5nhwVFJJHSubEBf3+pereGd7+nBKtYJ18KoB/PWJOHS3WRkwS04VrOU0iJr2bZU+l1QaTJ+9nA==} + + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + widest-line@6.0.0: + resolution: {integrity: sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA==} + engines: {node: '>=20'} + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + + yoctocolors@2.2.0: + resolution: {integrity: sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==} + engines: {node: '>=18'} + + yoga-layout@3.2.1: + resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@alcalzone/ansi-tokenize@0.2.5': + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + '@alloc/quick-lru@5.2.0': {} + + '@apidevtools/json-schema-ref-parser@11.9.3': + dependencies: + '@jsdevtools/ono': 7.1.3 + '@types/json-schema': 7.0.15 + js-yaml: 4.3.1 + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.8': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.29.7': + dependencies: + '@babel/types': 7.29.8 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.8 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-optimise-call-expression': 7.29.7 + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/traverse': 7.29.8 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-member-expression-to-functions@7.29.7': + dependencies: + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.29.7': + dependencies: + '@babel/types': 7.29.8 + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-optimise-call-expression': 7.29.7 + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + dependencies: + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + + '@babel/parser@7.29.8': + dependencies: + '@babel/types': 7.29.8 + + '@babel/plugin-proposal-decorators@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-syntax-decorators': 7.29.7(@babel/core@7.29.7) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-syntax-decorators@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-destructuring@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-explicit-resource-management@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-commonjs@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) + transitivePeerDependencies: + - supports-color + + '@babel/preset-typescript@7.28.5(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7) + transitivePeerDependencies: + - supports-color + + '@babel/runtime@7.29.7': {} + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + + '@babel/traverse@7.29.8': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@cfworker/json-schema@4.1.1': {} + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true - picomatch@4.0.5: - resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} - engines: {node: '>=12'} + '@esbuild/android-x64@0.28.2': + optional: true - prettier@3.2.5: - resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} - engines: {node: '>=14'} - hasBin: true + '@esbuild/darwin-arm64@0.28.2': + optional: true - pretty-ms@9.3.0: - resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} - engines: {node: '>=18'} + '@esbuild/darwin-x64@0.28.2': + optional: true - progress@2.0.3: - resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} - engines: {node: '>=0.4.0'} + '@esbuild/freebsd-arm64@0.28.2': + optional: true - pump@3.0.4: - resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + '@esbuild/freebsd-x64@0.28.2': + optional: true - pure-rand@8.4.2: - resolution: {integrity: sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==} + '@esbuild/linux-arm64@0.28.2': + optional: true - qs@6.15.1: - resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} - engines: {node: '>=0.6'} + '@esbuild/linux-arm@0.28.2': + optional: true - react-reconciler@0.33.0: - resolution: {integrity: sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==} - engines: {node: '>=0.10.0'} - peerDependencies: - react: ^19.2.0 + '@esbuild/linux-ia32@0.28.2': + optional: true - react@19.2.4: - resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} - engines: {node: '>=0.10.0'} + '@esbuild/linux-loong64@0.28.2': + optional: true - require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} + '@esbuild/linux-mips64el@0.28.2': + optional: true - restore-cursor@4.0.0: - resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + '@esbuild/linux-ppc64@0.28.2': + optional: true - rxjs@7.8.2: - resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + '@esbuild/linux-riscv64@0.28.2': + optional: true - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + '@esbuild/linux-s390x@0.28.2': + optional: true - scheduler@0.27.0: - resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + '@esbuild/linux-x64@0.28.2': + optional: true - semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true + '@esbuild/netbsd-arm64@0.28.2': + optional: true - semver@7.7.4: - resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} - engines: {node: '>=10'} - hasBin: true + '@esbuild/netbsd-x64@0.28.2': + optional: true - semver@7.8.5: - resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} - engines: {node: '>=10'} - hasBin: true + '@esbuild/openbsd-arm64@0.28.2': + optional: true - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} + '@esbuild/openbsd-x64@0.28.2': + optional: true - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} + '@esbuild/openharmony-arm64@0.28.2': + optional: true - side-channel-list@1.0.1: - resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} - engines: {node: '>= 0.4'} + '@esbuild/sunos-x64@0.28.2': + optional: true - side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} + '@esbuild/win32-arm64@0.28.2': + optional: true - side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} + '@esbuild/win32-ia32@0.28.2': + optional: true - side-channel@1.1.1: - resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} - engines: {node: '>= 0.4'} + '@esbuild/win32-x64@0.28.2': + optional: true - signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + '@floating-ui/core@1.8.0': + dependencies: + '@floating-ui/utils': 0.2.12 - signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} + '@floating-ui/dom@1.8.0': + dependencies: + '@floating-ui/core': 1.8.0 + '@floating-ui/utils': 0.2.12 - slice-ansi@8.0.0: - resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} - engines: {node: '>=20'} + '@floating-ui/react-dom@2.1.9(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@floating-ui/dom': 1.8.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) - smol-toml@1.6.1: - resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} - engines: {node: '>= 18'} + '@floating-ui/utils@0.2.12': {} - source-map@0.7.6: - resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} - engines: {node: '>= 12'} + '@graphql-typed-document-node/core@3.2.0(graphql@17.0.2)': + dependencies: + graphql: 17.0.2 - stack-utils@2.0.6: - resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} - engines: {node: '>=10'} + '@inquirer/ansi@2.0.7': {} - string-width@7.2.0: - resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} - engines: {node: '>=18'} + '@inquirer/checkbox@5.2.1(@types/node@22.19.17)': + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/core': 11.2.1(@types/node@22.19.17) + '@inquirer/figures': 2.0.7 + '@inquirer/type': 4.0.7(@types/node@22.19.17) + optionalDependencies: + '@types/node': 22.19.17 - string-width@8.2.2: - resolution: {integrity: sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==} - engines: {node: '>=20'} + '@inquirer/confirm@6.1.1(@types/node@22.19.17)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@22.19.17) + '@inquirer/type': 4.0.7(@types/node@22.19.17) + optionalDependencies: + '@types/node': 22.19.17 - strip-ansi@7.2.0: - resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} - engines: {node: '>=12'} + '@inquirer/core@11.2.1(@types/node@22.19.17)': + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/figures': 2.0.7 + '@inquirer/type': 4.0.7(@types/node@22.19.17) + cli-width: 4.1.0 + fast-wrap-ansi: 0.2.2 + mute-stream: 3.0.0 + signal-exit: 4.1.0 + optionalDependencies: + '@types/node': 22.19.17 - strip-final-newline@4.0.0: - resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} - engines: {node: '>=18'} + '@inquirer/editor@5.2.2(@types/node@22.19.17)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@22.19.17) + '@inquirer/external-editor': 3.0.3(@types/node@22.19.17) + '@inquirer/type': 4.0.7(@types/node@22.19.17) + optionalDependencies: + '@types/node': 22.19.17 - tagged-tag@1.0.0: - resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} - engines: {node: '>=20'} + '@inquirer/expand@5.1.1(@types/node@22.19.17)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@22.19.17) + '@inquirer/type': 4.0.7(@types/node@22.19.17) + optionalDependencies: + '@types/node': 22.19.17 - terminal-size@4.0.1: - resolution: {integrity: sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ==} - engines: {node: '>=18'} + '@inquirer/external-editor@3.0.3(@types/node@22.19.17)': + dependencies: + chardet: 2.2.0 + iconv-lite: 0.7.3 + optionalDependencies: + '@types/node': 22.19.17 - tinyglobby@0.2.17: - resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} - engines: {node: '>=12.0.0'} + '@inquirer/figures@2.0.7': {} - tokenx@1.3.0: - resolution: {integrity: sha512-NLdXTEZkKiO0gZuLtMoZKjCXTREXeZZt8nnnNeyoXtNZAfG/GKGSbQtLU5STspc0rMSwcA+UJfWZkbNU01iKmQ==} + '@inquirer/input@5.1.2(@types/node@22.19.17)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@22.19.17) + '@inquirer/type': 4.0.7(@types/node@22.19.17) + optionalDependencies: + '@types/node': 22.19.17 - tree-kill@1.2.2: - resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} - hasBin: true + '@inquirer/number@4.1.1(@types/node@22.19.17)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@22.19.17) + '@inquirer/type': 4.0.7(@types/node@22.19.17) + optionalDependencies: + '@types/node': 22.19.17 - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + '@inquirer/password@5.1.1(@types/node@22.19.17)': + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/core': 11.2.1(@types/node@22.19.17) + '@inquirer/type': 4.0.7(@types/node@22.19.17) + optionalDependencies: + '@types/node': 22.19.17 - tunnel@0.0.6: - resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} - engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} + '@inquirer/prompts@8.3.0(@types/node@22.19.17)': + dependencies: + '@inquirer/checkbox': 5.2.1(@types/node@22.19.17) + '@inquirer/confirm': 6.1.1(@types/node@22.19.17) + '@inquirer/editor': 5.2.2(@types/node@22.19.17) + '@inquirer/expand': 5.1.1(@types/node@22.19.17) + '@inquirer/input': 5.1.2(@types/node@22.19.17) + '@inquirer/number': 4.1.1(@types/node@22.19.17) + '@inquirer/password': 5.1.1(@types/node@22.19.17) + '@inquirer/rawlist': 5.3.1(@types/node@22.19.17) + '@inquirer/search': 4.2.1(@types/node@22.19.17) + '@inquirer/select': 5.2.1(@types/node@22.19.17) + optionalDependencies: + '@types/node': 22.19.17 - type-fest@5.8.0: - resolution: {integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==} - engines: {node: '>=20'} + '@inquirer/rawlist@5.3.1(@types/node@22.19.17)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@22.19.17) + '@inquirer/type': 4.0.7(@types/node@22.19.17) + optionalDependencies: + '@types/node': 22.19.17 - typed-inject@5.0.0: - resolution: {integrity: sha512-0Ql2ORqBORLMdAW89TQKZsb1PQkFGImFfVmncXWe7a+AA3+7dh7Se9exxZowH4kbnlvKEFkMxUYdHUpjYWFJaA==} - engines: {node: '>=18'} + '@inquirer/search@4.2.1(@types/node@22.19.17)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@22.19.17) + '@inquirer/figures': 2.0.7 + '@inquirer/type': 4.0.7(@types/node@22.19.17) + optionalDependencies: + '@types/node': 22.19.17 - typed-rest-client@2.3.1: - resolution: {integrity: sha512-k4kX5Up6qA68D0Cby2AK+6+vM5k3qTxe+/3FqhnHRExjY5cfbOnzjQZbP/LXleF8hVoDvDqxlgk9KK83HoBZlQ==} - engines: {node: '>= 16.0.0'} + '@inquirer/select@5.2.1(@types/node@22.19.17)': + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/core': 11.2.1(@types/node@22.19.17) + '@inquirer/figures': 2.0.7 + '@inquirer/type': 4.0.7(@types/node@22.19.17) + optionalDependencies: + '@types/node': 22.19.17 - typescript@5.7.3: - resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} - engines: {node: '>=14.17'} - hasBin: true + '@inquirer/type@4.0.7(@types/node@22.19.17)': + optionalDependencies: + '@types/node': 22.19.17 - underscore@1.13.8: - resolution: {integrity: sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==} + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 - unicorn-magic@0.3.0: - resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} - engines: {node: '>=18'} + '@jridgewell/resolve-uri@3.1.2': {} - universal-user-agent@7.0.3: - resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} + '@jridgewell/sourcemap-codec@1.5.5': {} - update-browserslist-db@1.3.1: - resolution: {integrity: sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 - weapon-regex@1.3.6: - resolution: {integrity: sha512-wsf1m1jmMrso5nhwVFJJHSubEBf3+pereGd7+nBKtYJ18KoB/PWJOHS3WRkwS04VrOU0iJr2bZU+l1QaTJ+9nA==} + '@jsdevtools/ono@7.1.3': {} - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true + '@linear/sdk@89.0.0(graphql@17.0.2)': + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@17.0.2) + transitivePeerDependencies: + - graphql - widest-line@6.0.0: - resolution: {integrity: sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA==} - engines: {node: '>=20'} + '@modelcontextprotocol/core@2.0.0-beta.4': + dependencies: + zod: 4.4.3 - wrap-ansi@9.0.2: - resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} - engines: {node: '>=18'} + '@modelcontextprotocol/server@2.0.0-beta.4': + dependencies: + '@modelcontextprotocol/core': 2.0.0-beta.4 + zod: 4.4.3 - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + '@napi-rs/canvas-android-arm64@1.0.3': + optional: true - ws@8.21.3: - resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true + '@napi-rs/canvas-darwin-arm64@1.0.3': + optional: true - yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + '@napi-rs/canvas-darwin-x64@1.0.3': + optional: true - yaml@2.9.0: - resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} - engines: {node: '>= 14.6'} - hasBin: true + '@napi-rs/canvas-linux-arm-gnueabihf@1.0.3': + optional: true - yauzl@2.10.0: - resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + '@napi-rs/canvas-linux-arm64-gnu@1.0.3': + optional: true - yoctocolors@2.2.0: - resolution: {integrity: sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==} - engines: {node: '>=18'} + '@napi-rs/canvas-linux-arm64-musl@1.0.3': + optional: true - yoga-layout@3.2.1: - resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} + '@napi-rs/canvas-linux-riscv64-gnu@1.0.3': + optional: true - zod@4.4.3: - resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + '@napi-rs/canvas-linux-x64-gnu@1.0.3': + optional: true -snapshots: + '@napi-rs/canvas-linux-x64-musl@1.0.3': + optional: true - '@alcalzone/ansi-tokenize@0.2.5': - dependencies: - ansi-styles: 6.2.3 - is-fullwidth-code-point: 5.1.0 + '@napi-rs/canvas-win32-arm64-msvc@1.0.3': + optional: true - '@apidevtools/json-schema-ref-parser@11.9.3': - dependencies: - '@jsdevtools/ono': 7.1.3 - '@types/json-schema': 7.0.15 - js-yaml: 4.3.1 + '@napi-rs/canvas-win32-x64-msvc@1.0.3': + optional: true - '@babel/code-frame@7.29.7': - dependencies: - '@babel/helper-validator-identifier': 7.29.7 - js-tokens: 4.0.0 - picocolors: 1.1.1 + '@napi-rs/canvas@1.0.3': + optionalDependencies: + '@napi-rs/canvas-android-arm64': 1.0.3 + '@napi-rs/canvas-darwin-arm64': 1.0.3 + '@napi-rs/canvas-darwin-x64': 1.0.3 + '@napi-rs/canvas-linux-arm-gnueabihf': 1.0.3 + '@napi-rs/canvas-linux-arm64-gnu': 1.0.3 + '@napi-rs/canvas-linux-arm64-musl': 1.0.3 + '@napi-rs/canvas-linux-riscv64-gnu': 1.0.3 + '@napi-rs/canvas-linux-x64-gnu': 1.0.3 + '@napi-rs/canvas-linux-x64-musl': 1.0.3 + '@napi-rs/canvas-win32-arm64-msvc': 1.0.3 + '@napi-rs/canvas-win32-x64-msvc': 1.0.3 + optional: true - '@babel/compat-data@7.29.7': {} + '@octokit/auth-token@6.0.0': {} - '@babel/core@7.29.7': + '@octokit/core@7.0.6': dependencies: - '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.8 - '@babel/helper-compilation-targets': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) - '@babel/helpers': 7.29.7 - '@babel/parser': 7.29.8 - '@babel/template': 7.29.7 - '@babel/traverse': 7.29.8 - '@babel/types': 7.29.8 - '@jridgewell/remapping': 2.3.5 - convert-source-map: 2.0.0 - debug: 4.4.3 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color + '@octokit/auth-token': 6.0.0 + '@octokit/graphql': 9.0.3 + '@octokit/request': 10.0.11 + '@octokit/request-error': 7.1.0 + '@octokit/types': 16.0.0 + before-after-hook: 4.0.0 + universal-user-agent: 7.0.3 - '@babel/generator@7.29.8': + '@octokit/endpoint@11.0.3': dependencies: - '@babel/parser': 7.29.8 - '@babel/types': 7.29.8 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 + '@octokit/types': 16.0.0 + universal-user-agent: 7.0.3 - '@babel/helper-annotate-as-pure@7.29.7': + '@octokit/graphql@9.0.3': dependencies: - '@babel/types': 7.29.8 + '@octokit/request': 10.0.11 + '@octokit/types': 16.0.0 + universal-user-agent: 7.0.3 - '@babel/helper-compilation-targets@7.29.7': - dependencies: - '@babel/compat-data': 7.29.7 - '@babel/helper-validator-option': 7.29.7 - browserslist: 4.28.8 - lru-cache: 5.1.1 - semver: 6.3.1 + '@octokit/openapi-types@27.0.0': {} - '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7)': + '@octokit/request-error@7.1.0': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-annotate-as-pure': 7.29.7 - '@babel/helper-member-expression-to-functions': 7.29.7 - '@babel/helper-optimise-call-expression': 7.29.7 - '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7) - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 - '@babel/traverse': 7.29.8 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/helper-globals@7.29.7': {} + '@octokit/types': 16.0.0 - '@babel/helper-member-expression-to-functions@7.29.7': + '@octokit/request@10.0.11': dependencies: - '@babel/traverse': 7.29.8 - '@babel/types': 7.29.8 - transitivePeerDependencies: - - supports-color + '@octokit/endpoint': 11.0.3 + '@octokit/request-error': 7.1.0 + '@octokit/types': 16.0.0 + content-type: 2.0.0 + json-with-bigint: 3.5.10 + universal-user-agent: 7.0.3 - '@babel/helper-module-imports@7.29.7': + '@octokit/types@16.0.0': dependencies: - '@babel/traverse': 7.29.8 - '@babel/types': 7.29.8 - transitivePeerDependencies: - - supports-color + '@octokit/openapi-types': 27.0.0 - '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + '@openai/apps-sdk-ui@0.2.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.3.3)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-imports': 7.29.7 - '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.8 + clsx: 2.1.1 + lodash: 4.17.21 + luxon: 3.7.1 + radix-ui: 1.6.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-markdown: 9.1.0(@types/react@19.2.14)(react@19.2.4) + react-merge-refs: 2.1.1 + react-syntax-highlighter: 16.1.1(react@19.2.4) + rehype-katex: 7.0.1 + remark-breaks: 4.0.0 + remark-directive: 3.0.1 + remark-gfm: 4.0.1 + remark-math: 6.0.0 + tailwindcss: 4.3.3 + unist-util-visit: 5.1.0 + usehooks-ts: 3.1.1(react@19.2.4) transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + - react-dom - supports-color - '@babel/helper-optimise-call-expression@7.29.7': - dependencies: - '@babel/types': 7.29.8 - - '@babel/helper-plugin-utils@7.29.7': {} - - '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7)': + '@openai/codex-sdk@0.149.1': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-member-expression-to-functions': 7.29.7 - '@babel/helper-optimise-call-expression': 7.29.7 - '@babel/traverse': 7.29.8 - transitivePeerDependencies: - - supports-color + '@openai/codex': 0.149.1 - '@babel/helper-skip-transparent-expression-wrappers@7.29.7': - dependencies: - '@babel/traverse': 7.29.8 - '@babel/types': 7.29.8 - transitivePeerDependencies: - - supports-color + '@openai/codex@0.149.1': + optionalDependencies: + '@openai/codex-darwin-arm64': '@openai/codex@0.149.1-darwin-arm64' + '@openai/codex-darwin-x64': '@openai/codex@0.149.1-darwin-x64' + '@openai/codex-linux-arm64': '@openai/codex@0.149.1-linux-arm64' + '@openai/codex-linux-x64': '@openai/codex@0.149.1-linux-x64' + '@openai/codex-win32-arm64': '@openai/codex@0.149.1-win32-arm64' + '@openai/codex-win32-x64': '@openai/codex@0.149.1-win32-x64' - '@babel/helper-string-parser@7.29.7': {} + '@openai/codex@0.149.1-darwin-arm64': + optional: true - '@babel/helper-validator-identifier@7.29.7': {} + '@openai/codex@0.149.1-darwin-x64': + optional: true - '@babel/helper-validator-option@7.29.7': {} + '@openai/codex@0.149.1-linux-arm64': + optional: true - '@babel/helpers@7.29.7': - dependencies: - '@babel/template': 7.29.7 - '@babel/types': 7.29.8 + '@openai/codex@0.149.1-linux-x64': + optional: true - '@babel/parser@7.29.8': - dependencies: - '@babel/types': 7.29.8 + '@openai/codex@0.149.1-win32-arm64': + optional: true - '@babel/plugin-proposal-decorators@7.29.7(@babel/core@7.29.7)': - dependencies: - '@babel/core': 7.29.7 - '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-decorators': 7.29.7(@babel/core@7.29.7) - transitivePeerDependencies: - - supports-color + '@openai/codex@0.149.1-win32-x64': + optional: true - '@babel/plugin-syntax-decorators@7.29.7(@babel/core@7.29.7)': - dependencies: - '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.29.7 + '@radix-ui/number@1.1.3': {} - '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': + '@radix-ui/primitive@1.1.7': {} + + '@radix-ui/react-accessible-icon@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.29.7 + '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-accordion@1.2.20(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collapsible': 1.1.20(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)': + '@radix-ui/react-alert-dialog@1.1.23(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.29.7 + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dialog': 1.1.23(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@babel/plugin-transform-destructuring@7.29.7(@babel/core@7.29.7)': + '@radix-ui/react-arrow@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.29.7 - '@babel/traverse': 7.29.8 - transitivePeerDependencies: - - supports-color + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@babel/plugin-transform-explicit-resource-management@7.29.7(@babel/core@7.29.7)': + '@radix-ui/react-aspect-ratio@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7) - transitivePeerDependencies: - - supports-color + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@babel/plugin-transform-modules-commonjs@7.29.7(@babel/core@7.29.7)': + '@radix-ui/react-avatar@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.29.7 - transitivePeerDependencies: - - supports-color + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7)': + '@radix-ui/react-checkbox@1.3.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-annotate-as-pure': 7.29.7 - '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 - '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) - transitivePeerDependencies: - - supports-color + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-collapsible@1.1.20(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@babel/preset-typescript@7.28.5(@babel/core@7.29.7)': + '@radix-ui/react-collection@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-validator-option': 7.29.7 - '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7) - transitivePeerDependencies: - - supports-color + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@babel/template@7.29.7': + '@radix-ui/react-compose-refs@1.1.5(@types/react@19.2.14)(react@19.2.4)': dependencies: - '@babel/code-frame': 7.29.7 - '@babel/parser': 7.29.8 - '@babel/types': 7.29.8 + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 - '@babel/traverse@7.29.8': + '@radix-ui/react-context-menu@2.3.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.8 - '@babel/helper-globals': 7.29.7 - '@babel/parser': 7.29.8 - '@babel/template': 7.29.7 - '@babel/types': 7.29.8 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-menu': 2.1.24(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@babel/types@7.29.8': + '@radix-ui/react-context@1.2.2(@types/react@19.2.14)(react@19.2.4)': dependencies: - '@babel/helper-string-parser': 7.29.7 - '@babel/helper-validator-identifier': 7.29.7 + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 - '@cfworker/json-schema@4.1.1': {} + '@radix-ui/react-dialog@1.1.23(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.14)(react@19.2.4) + aria-hidden: 1.2.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@graphql-typed-document-node/core@3.2.0(graphql@17.0.2)': + '@radix-ui/react-direction@1.1.4(@types/react@19.2.14)(react@19.2.4)': dependencies: - graphql: 17.0.2 - - '@inquirer/ansi@2.0.7': {} + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 - '@inquirer/checkbox@5.2.1(@types/node@22.19.17)': + '@radix-ui/react-dismissable-layer@1.1.19(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@inquirer/ansi': 2.0.7 - '@inquirer/core': 11.2.1(@types/node@22.19.17) - '@inquirer/figures': 2.0.7 - '@inquirer/type': 4.0.7(@types/node@22.19.17) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/node': 22.19.17 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@inquirer/confirm@6.1.1(@types/node@22.19.17)': + '@radix-ui/react-dropdown-menu@2.1.24(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@inquirer/core': 11.2.1(@types/node@22.19.17) - '@inquirer/type': 4.0.7(@types/node@22.19.17) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-menu': 2.1.24(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/node': 22.19.17 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@inquirer/core@11.2.1(@types/node@22.19.17)': + '@radix-ui/react-focus-guards@1.1.6(@types/react@19.2.14)(react@19.2.4)': dependencies: - '@inquirer/ansi': 2.0.7 - '@inquirer/figures': 2.0.7 - '@inquirer/type': 4.0.7(@types/node@22.19.17) - cli-width: 4.1.0 - fast-wrap-ansi: 0.2.2 - mute-stream: 3.0.0 - signal-exit: 4.1.0 + react: 19.2.4 optionalDependencies: - '@types/node': 22.19.17 + '@types/react': 19.2.14 - '@inquirer/editor@5.2.2(@types/node@22.19.17)': + '@radix-ui/react-focus-scope@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@inquirer/core': 11.2.1(@types/node@22.19.17) - '@inquirer/external-editor': 3.0.3(@types/node@22.19.17) - '@inquirer/type': 4.0.7(@types/node@22.19.17) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/node': 22.19.17 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@inquirer/expand@5.1.1(@types/node@22.19.17)': + '@radix-ui/react-form@0.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@inquirer/core': 11.2.1(@types/node@22.19.17) - '@inquirer/type': 4.0.7(@types/node@22.19.17) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-label': 2.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/node': 22.19.17 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-hover-card@1.1.23(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@inquirer/external-editor@3.0.3(@types/node@22.19.17)': + '@radix-ui/react-id@1.1.4(@types/react@19.2.14)(react@19.2.4)': dependencies: - chardet: 2.2.0 - iconv-lite: 0.7.3 + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 optionalDependencies: - '@types/node': 22.19.17 + '@types/react': 19.2.14 - '@inquirer/figures@2.0.7': {} + '@radix-ui/react-label@2.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-menu@2.1.24(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.14)(react@19.2.4) + aria-hidden: 1.2.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-menubar@1.1.24(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-menu': 2.1.24(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-navigation-menu@1.2.22(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-one-time-password-field@0.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/number': 1.1.3 + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-password-toggle-field@0.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-popover@1.1.23(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.14)(react@19.2.4) + aria-hidden: 1.2.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-popper@1.3.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@floating-ui/react-dom': 2.1.9(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-arrow': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-rect': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/rect': 1.1.3 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@inquirer/input@5.1.2(@types/node@22.19.17)': + '@radix-ui/react-portal@1.1.17(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@inquirer/core': 11.2.1(@types/node@22.19.17) - '@inquirer/type': 4.0.7(@types/node@22.19.17) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/node': 22.19.17 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@inquirer/number@4.1.1(@types/node@22.19.17)': + '@radix-ui/react-presence@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@inquirer/core': 11.2.1(@types/node@22.19.17) - '@inquirer/type': 4.0.7(@types/node@22.19.17) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/node': 22.19.17 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@inquirer/password@5.1.1(@types/node@22.19.17)': + '@radix-ui/react-primitive@2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@inquirer/ansi': 2.0.7 - '@inquirer/core': 11.2.1(@types/node@22.19.17) - '@inquirer/type': 4.0.7(@types/node@22.19.17) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/node': 22.19.17 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@inquirer/prompts@8.3.0(@types/node@22.19.17)': + '@radix-ui/react-progress@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@inquirer/checkbox': 5.2.1(@types/node@22.19.17) - '@inquirer/confirm': 6.1.1(@types/node@22.19.17) - '@inquirer/editor': 5.2.2(@types/node@22.19.17) - '@inquirer/expand': 5.1.1(@types/node@22.19.17) - '@inquirer/input': 5.1.2(@types/node@22.19.17) - '@inquirer/number': 4.1.1(@types/node@22.19.17) - '@inquirer/password': 5.1.1(@types/node@22.19.17) - '@inquirer/rawlist': 5.3.1(@types/node@22.19.17) - '@inquirer/search': 4.2.1(@types/node@22.19.17) - '@inquirer/select': 5.2.1(@types/node@22.19.17) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-radio-group@1.4.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-roving-focus@1.1.19(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-scroll-area@1.2.18(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/number': 1.1.3 + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/node': 22.19.17 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-select@2.3.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/number': 1.1.3 + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + aria-hidden: 1.2.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@inquirer/rawlist@5.3.1(@types/node@22.19.17)': + '@radix-ui/react-separator@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@inquirer/core': 11.2.1(@types/node@22.19.17) - '@inquirer/type': 4.0.7(@types/node@22.19.17) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/node': 22.19.17 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-slider@1.4.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/number': 1.1.3 + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@inquirer/search@4.2.1(@types/node@22.19.17)': + '@radix-ui/react-slot@1.3.3(@types/react@19.2.14)(react@19.2.4)': dependencies: - '@inquirer/core': 11.2.1(@types/node@22.19.17) - '@inquirer/figures': 2.0.7 - '@inquirer/type': 4.0.7(@types/node@22.19.17) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 optionalDependencies: - '@types/node': 22.19.17 + '@types/react': 19.2.14 - '@inquirer/select@5.2.1(@types/node@22.19.17)': + '@radix-ui/react-switch@1.3.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@inquirer/ansi': 2.0.7 - '@inquirer/core': 11.2.1(@types/node@22.19.17) - '@inquirer/figures': 2.0.7 - '@inquirer/type': 4.0.7(@types/node@22.19.17) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/node': 22.19.17 - - '@inquirer/type@4.0.7(@types/node@22.19.17)': + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-tabs@1.1.21(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/node': 22.19.17 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-toast@1.2.23(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@jridgewell/gen-mapping@0.3.13': + '@radix-ui/react-toggle-group@1.1.19(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toggle': 1.1.18(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@jridgewell/remapping@2.3.5': + '@radix-ui/react-toggle@1.1.18(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/sourcemap-codec@1.5.5': {} + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@jridgewell/trace-mapping@0.3.31': + '@radix-ui/react-toolbar@1.1.19(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - - '@jsdevtools/ono@7.1.3': {} + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-separator': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toggle-group': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-tooltip@1.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@linear/sdk@89.0.0(graphql@17.0.2)': + '@radix-ui/react-use-callback-ref@1.1.4(@types/react@19.2.14)(react@19.2.4)': dependencies: - '@graphql-typed-document-node/core': 3.2.0(graphql@17.0.2) - transitivePeerDependencies: - - graphql + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 - '@modelcontextprotocol/core@2.0.0-beta.4': + '@radix-ui/react-use-controllable-state@1.2.6(@types/react@19.2.14)(react@19.2.4)': dependencies: - zod: 4.4.3 + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 - '@modelcontextprotocol/server@2.0.0-beta.4': + '@radix-ui/react-use-effect-event@0.0.5(@types/react@19.2.14)(react@19.2.4)': dependencies: - '@modelcontextprotocol/core': 2.0.0-beta.4 - zod: 4.4.3 - - '@napi-rs/canvas-android-arm64@1.0.3': - optional: true - - '@napi-rs/canvas-darwin-arm64@1.0.3': - optional: true - - '@napi-rs/canvas-darwin-x64@1.0.3': - optional: true - - '@napi-rs/canvas-linux-arm-gnueabihf@1.0.3': - optional: true - - '@napi-rs/canvas-linux-arm64-gnu@1.0.3': - optional: true - - '@napi-rs/canvas-linux-arm64-musl@1.0.3': - optional: true - - '@napi-rs/canvas-linux-riscv64-gnu@1.0.3': - optional: true - - '@napi-rs/canvas-linux-x64-gnu@1.0.3': - optional: true - - '@napi-rs/canvas-linux-x64-musl@1.0.3': - optional: true - - '@napi-rs/canvas-win32-arm64-msvc@1.0.3': - optional: true - - '@napi-rs/canvas-win32-x64-msvc@1.0.3': - optional: true - - '@napi-rs/canvas@1.0.3': + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 optionalDependencies: - '@napi-rs/canvas-android-arm64': 1.0.3 - '@napi-rs/canvas-darwin-arm64': 1.0.3 - '@napi-rs/canvas-darwin-x64': 1.0.3 - '@napi-rs/canvas-linux-arm-gnueabihf': 1.0.3 - '@napi-rs/canvas-linux-arm64-gnu': 1.0.3 - '@napi-rs/canvas-linux-arm64-musl': 1.0.3 - '@napi-rs/canvas-linux-riscv64-gnu': 1.0.3 - '@napi-rs/canvas-linux-x64-gnu': 1.0.3 - '@napi-rs/canvas-linux-x64-musl': 1.0.3 - '@napi-rs/canvas-win32-arm64-msvc': 1.0.3 - '@napi-rs/canvas-win32-x64-msvc': 1.0.3 - optional: true - - '@octokit/auth-token@6.0.0': {} + '@types/react': 19.2.14 - '@octokit/core@7.0.6': + '@radix-ui/react-use-escape-keydown@1.1.5(@types/react@19.2.14)(react@19.2.4)': dependencies: - '@octokit/auth-token': 6.0.0 - '@octokit/graphql': 9.0.3 - '@octokit/request': 10.0.11 - '@octokit/request-error': 7.1.0 - '@octokit/types': 16.0.0 - before-after-hook: 4.0.0 - universal-user-agent: 7.0.3 + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 - '@octokit/endpoint@11.0.3': + '@radix-ui/react-use-is-hydrated@0.1.3(@types/react@19.2.14)(react@19.2.4)': dependencies: - '@octokit/types': 16.0.0 - universal-user-agent: 7.0.3 + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 - '@octokit/graphql@9.0.3': + '@radix-ui/react-use-layout-effect@1.1.4(@types/react@19.2.14)(react@19.2.4)': dependencies: - '@octokit/request': 10.0.11 - '@octokit/types': 16.0.0 - universal-user-agent: 7.0.3 - - '@octokit/openapi-types@27.0.0': {} + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 - '@octokit/request-error@7.1.0': + '@radix-ui/react-use-previous@1.1.4(@types/react@19.2.14)(react@19.2.4)': dependencies: - '@octokit/types': 16.0.0 + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 - '@octokit/request@10.0.11': + '@radix-ui/react-use-rect@1.1.4(@types/react@19.2.14)(react@19.2.4)': dependencies: - '@octokit/endpoint': 11.0.3 - '@octokit/request-error': 7.1.0 - '@octokit/types': 16.0.0 - content-type: 2.0.0 - json-with-bigint: 3.5.10 - universal-user-agent: 7.0.3 + '@radix-ui/rect': 1.1.3 + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 - '@octokit/types@16.0.0': + '@radix-ui/react-use-size@1.1.4(@types/react@19.2.14)(react@19.2.4)': dependencies: - '@octokit/openapi-types': 27.0.0 + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 - '@openai/codex-sdk@0.149.1': + '@radix-ui/react-visually-hidden@1.2.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@openai/codex': 0.149.1 - - '@openai/codex@0.149.1': + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@openai/codex-darwin-arm64': '@openai/codex@0.149.1-darwin-arm64' - '@openai/codex-darwin-x64': '@openai/codex@0.149.1-darwin-x64' - '@openai/codex-linux-arm64': '@openai/codex@0.149.1-linux-arm64' - '@openai/codex-linux-x64': '@openai/codex@0.149.1-linux-x64' - '@openai/codex-win32-arm64': '@openai/codex@0.149.1-win32-arm64' - '@openai/codex-win32-x64': '@openai/codex@0.149.1-win32-x64' - - '@openai/codex@0.149.1-darwin-arm64': - optional: true - - '@openai/codex@0.149.1-darwin-x64': - optional: true - - '@openai/codex@0.149.1-linux-arm64': - optional: true - - '@openai/codex@0.149.1-linux-x64': - optional: true - - '@openai/codex@0.149.1-win32-arm64': - optional: true + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@openai/codex@0.149.1-win32-x64': - optional: true + '@radix-ui/rect@1.1.3': {} '@scalar/openapi-types@0.8.0': {} @@ -1924,16 +4419,107 @@ snapshots: '@stryker-mutator/util@9.6.1': {} + '@tailwindcss/node@4.3.3': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.24.5 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.3 + + '@tailwindcss/oxide-android-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide@4.3.3': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-x64': 4.3.3 + '@tailwindcss/oxide-freebsd-x64': 4.3.3 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.3 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-x64-musl': 4.3.3 + '@tailwindcss/oxide-wasm32-wasi': 4.3.3 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 + + '@tailwindcss/postcss@4.3.3': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + postcss: 8.5.26 + tailwindcss: 4.3.3 + '@toon-format/toon@2.3.0': {} '@types/bun@1.3.13': dependencies: bun-types: 1.3.13 + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.9 + + '@types/estree@1.0.9': {} + + '@types/hast@3.0.5': + dependencies: + '@types/unist': 3.0.3 + '@types/json-schema@7.0.15': {} + '@types/katex@0.16.8': {} + '@types/lodash@4.17.24': {} + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + '@types/node@22.19.17': dependencies: undici-types: 6.21.0 @@ -1942,17 +4528,29 @@ snapshots: dependencies: '@types/node': 22.19.17 + '@types/prismjs@1.26.6': {} + + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + '@types/react@19.2.14': dependencies: csstype: 3.2.3 '@types/semver@7.8.0': {} + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + '@types/yauzl@2.10.3': dependencies: '@types/node': 22.19.17 optional: true + '@ungap/structured-clone@1.3.3': {} + ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 @@ -1979,8 +4577,14 @@ snapshots: argparse@2.0.1: {} + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + auto-bind@5.0.1: {} + bail@2.0.2: {} + balanced-match@4.0.4: {} base64-js@1.5.1: {} @@ -2019,8 +4623,18 @@ snapshots: caniuse-lite@1.0.30001809: {} + ccount@2.0.1: {} + chalk@5.6.2: {} + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + chardet@2.2.0: {} cli-boxes@3.0.0: {} @@ -2036,12 +4650,18 @@ snapshots: cli-width@4.1.0: {} + clsx@2.1.1: {} + code-excerpt@4.0.0: dependencies: convert-to-spaces: 2.0.1 + comma-separated-tokens@2.0.3: {} + commander@14.0.3: {} + commander@8.3.0: {} + content-type@2.0.0: {} convert-source-map@2.0.0: {} @@ -2060,11 +4680,25 @@ snapshots: dependencies: ms: 2.1.3 + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + + dequal@2.0.3: {} + des.js@1.1.0: dependencies: inherits: 2.0.4 minimalistic-assert: 1.0.1 + detect-libc@2.1.2: {} + + detect-node-es@1.1.0: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + diff-match-patch@1.0.5: {} dunder-proto@1.0.1: @@ -2081,6 +4715,13 @@ snapshots: dependencies: once: 1.4.0 + enhanced-resolve@5.24.5: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + entities@6.0.1: {} + environment@1.1.0: {} es-define-property@1.0.1: {} @@ -2093,10 +4734,43 @@ snapshots: es-toolkit@1.50.0: {} + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + escalade@3.2.0: {} escape-string-regexp@2.0.0: {} + escape-string-regexp@5.0.0: {} + + estree-util-is-identifier-name@3.0.0: {} + execa@9.6.1: dependencies: '@sindresorhus/merge-streams': 4.0.0 @@ -2112,6 +4786,8 @@ snapshots: strip-final-newline: 4.0.0 yoctocolors: 2.2.0 + extend@3.0.2: {} + extract-zip@2.0.1: dependencies: debug: 4.4.3 @@ -2140,6 +4816,10 @@ snapshots: dependencies: fast-string-width: 3.0.2 + fault@1.0.4: + dependencies: + format: 0.2.2 + fd-slicer@1.1.0: dependencies: pend: 1.2.0 @@ -2154,6 +4834,8 @@ snapshots: dependencies: is-unicode-supported: 2.1.0 + format@0.2.2: {} + function-bind@1.1.2: {} gensync@1.0.0-beta.2: {} @@ -2173,151 +4855,723 @@ snapshots: hasown: 2.0.4 math-intrinsics: 1.1.0 + get-nonce@1.0.1: {} + get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 es-object-atoms: 1.1.2 - get-stream@5.2.0: + get-stream@5.2.0: + dependencies: + pump: 3.0.4 + + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphql@17.0.2: {} + + has-symbols@1.1.0: {} + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + hast-util-from-dom@5.0.1: + dependencies: + '@types/hast': 3.0.5 + hastscript: 9.0.1 + web-namespaces: 2.0.1 + + hast-util-from-html-isomorphic@2.0.0: + dependencies: + '@types/hast': 3.0.5 + hast-util-from-dom: 5.0.1 + hast-util-from-html: 2.0.3 + unist-util-remove-position: 5.0.0 + + hast-util-from-html@2.0.3: + dependencies: + '@types/hast': 3.0.5 + devlop: 1.1.0 + hast-util-from-parse5: 8.0.3 + parse5: 7.3.0 + vfile: 6.0.3 + vfile-message: 4.0.3 + + hast-util-from-parse5@8.0.3: + dependencies: + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.1 + property-information: 7.2.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + + hast-util-is-element@3.0.0: + dependencies: + '@types/hast': 3.0.5 + + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.5 + + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.9 + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-to-text@4.0.2: + dependencies: + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + hast-util-is-element: 3.0.0 + unist-util-find-after: 5.0.0 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.5 + + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.5 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + + highlight.js@10.7.3: {} + + highlightjs-vue@1.0.0: {} + + html-url-attributes@3.0.1: {} + + human-signals@8.0.1: {} + + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + + incur@0.4.13: + dependencies: + '@cfworker/json-schema': 4.1.1 + '@modelcontextprotocol/server': 2.0.0-beta.4 + '@scalar/openapi-types': 0.8.0 + '@toon-format/toon': 2.3.0 + tokenx: 1.3.0 + yaml: 2.9.0 + zod: 4.4.3 + + indent-string@5.0.0: {} + + inherits@2.0.4: {} + + ink-testing-library@4.0.0(@types/react@19.2.14): + optionalDependencies: + '@types/react': 19.2.14 + + ink@6.8.0(@types/react@19.2.14)(react@19.2.4): + dependencies: + '@alcalzone/ansi-tokenize': 0.2.5 + ansi-escapes: 7.3.0 + ansi-styles: 6.2.3 + auto-bind: 5.0.1 + chalk: 5.6.2 + cli-boxes: 3.0.0 + cli-cursor: 4.0.0 + cli-truncate: 5.2.0 + code-excerpt: 4.0.0 + es-toolkit: 1.50.0 + indent-string: 5.0.0 + is-in-ci: 2.0.0 + patch-console: 2.0.0 + react: 19.2.4 + react-reconciler: 0.33.0(react@19.2.4) + scheduler: 0.27.0 + signal-exit: 3.0.7 + slice-ansi: 8.0.0 + stack-utils: 2.0.6 + string-width: 8.2.2 + terminal-size: 4.0.1 + type-fest: 5.8.0 + widest-line: 6.0.0 + wrap-ansi: 9.0.2 + ws: 8.21.3 + yoga-layout: 3.2.1 + optionalDependencies: + '@types/react': 19.2.14 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + inline-style-parser@0.2.7: {} + + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + + is-decimal@2.0.1: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@5.1.0: + dependencies: + get-east-asian-width: 1.6.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-hexadecimal@2.0.1: {} + + is-in-ci@2.0.0: {} + + is-plain-obj@4.1.0: {} + + is-stream@4.0.1: {} + + is-unicode-supported@2.1.0: {} + + isexe@2.0.0: {} + + jiti@2.7.0: {} + + js-md4@0.3.2: {} + + js-tiktoken@1.0.21: + dependencies: + base64-js: 1.5.1 + + js-tokens@4.0.0: {} + + js-yaml@4.3.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-rpc-2.0@1.7.1: {} + + json-schema-to-typescript@15.0.4: + dependencies: + '@apidevtools/json-schema-ref-parser': 11.9.3 + '@types/json-schema': 7.0.15 + '@types/lodash': 4.17.24 + is-glob: 4.0.3 + js-yaml: 4.3.1 + lodash: 4.18.1 + minimist: 1.2.8 + prettier: 3.2.5 + tinyglobby: 0.2.17 + + json-schema-traverse@1.0.0: {} + + json-with-bigint@3.5.10: {} + + json5@2.2.3: {} + + katex@0.16.47: + dependencies: + commander: 8.3.0 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + lodash.debounce@4.0.8: {} + + lodash.groupby@4.6.0: {} + + lodash@4.17.21: {} + + lodash@4.18.1: {} + + longest-streak@3.1.0: {} + + lowlight@1.20.0: + dependencies: + fault: 1.0.4 + highlight.js: 10.7.3 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + luxon@3.7.1: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + markdown-table@3.0.4: {} + + math-intrinsics@1.1.0: {} + + mdast-util-directive@3.1.0: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-visit-parents: 6.0.2 + transitivePeerDependencies: + - supports-color + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: dependencies: - pump: 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color - get-stream@9.0.1: + mdast-util-gfm@3.1.0: dependencies: - '@sec-ant/readable-stream': 0.4.1 - is-stream: 4.0.1 + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color - gopd@1.2.0: {} + mdast-util-math@3.0.0: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + longest-streak: 3.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + unist-util-remove-position: 5.0.0 + transitivePeerDependencies: + - supports-color - graphql@17.0.2: {} + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color - has-symbols@1.1.0: {} + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color - hasown@2.0.4: + mdast-util-mdxjs-esm@2.0.1: dependencies: - function-bind: 1.1.2 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color - human-signals@8.0.1: {} + mdast-util-newline-to-break@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-find-and-replace: 3.0.2 + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.3 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-directive@3.0.2: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + parse-entities: 4.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - iconv-lite@0.7.3: + micromark-extension-gfm-footnote@2.1.0: dependencies: - safer-buffer: 2.1.2 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - incur@0.4.13: + micromark-extension-gfm-strikethrough@2.1.0: dependencies: - '@cfworker/json-schema': 4.1.1 - '@modelcontextprotocol/server': 2.0.0-beta.4 - '@scalar/openapi-types': 0.8.0 - '@toon-format/toon': 2.3.0 - tokenx: 1.3.0 - yaml: 2.9.0 - zod: 4.4.3 + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - indent-string@5.0.0: {} + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - inherits@2.0.4: {} + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 - ink-testing-library@4.0.0(@types/react@19.2.14): - optionalDependencies: - '@types/react': 19.2.14 + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - ink@6.8.0(@types/react@19.2.14)(react@19.2.4): + micromark-extension-gfm@3.0.0: dependencies: - '@alcalzone/ansi-tokenize': 0.2.5 - ansi-escapes: 7.3.0 - ansi-styles: 6.2.3 - auto-bind: 5.0.1 - chalk: 5.6.2 - cli-boxes: 3.0.0 - cli-cursor: 4.0.0 - cli-truncate: 5.2.0 - code-excerpt: 4.0.0 - es-toolkit: 1.50.0 - indent-string: 5.0.0 - is-in-ci: 2.0.0 - patch-console: 2.0.0 - react: 19.2.4 - react-reconciler: 0.33.0(react@19.2.4) - scheduler: 0.27.0 - signal-exit: 3.0.7 - slice-ansi: 8.0.0 - stack-utils: 2.0.6 - string-width: 8.2.2 - terminal-size: 4.0.1 - type-fest: 5.8.0 - widest-line: 6.0.0 - wrap-ansi: 9.0.2 - ws: 8.21.3 - yoga-layout: 3.2.1 - optionalDependencies: - '@types/react': 19.2.14 - transitivePeerDependencies: - - bufferutil - - utf-8-validate + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 - is-extglob@2.1.1: {} + micromark-extension-math@3.1.0: + dependencies: + '@types/katex': 0.16.8 + devlop: 1.1.0 + katex: 0.16.47 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - is-fullwidth-code-point@5.1.0: + micromark-factory-destination@2.0.1: dependencies: - get-east-asian-width: 1.6.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - is-glob@4.0.3: + micromark-factory-label@2.0.1: dependencies: - is-extglob: 2.1.1 + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - is-in-ci@2.0.0: {} + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 - is-plain-obj@4.1.0: {} + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - is-stream@4.0.1: {} + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - is-unicode-supported@2.1.0: {} + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - isexe@2.0.0: {} + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 - js-md4@0.3.2: {} + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - js-tiktoken@1.0.21: + micromark-util-combine-extensions@2.0.1: dependencies: - base64-js: 1.5.1 + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 - js-tokens@4.0.0: {} + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 - js-yaml@4.3.1: + micromark-util-decode-string@2.0.1: dependencies: - argparse: 2.0.1 + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 - jsesc@3.1.0: {} + micromark-util-encode@2.0.1: {} - json-rpc-2.0@1.7.1: {} + micromark-util-html-tag-name@2.0.1: {} - json-schema-to-typescript@15.0.4: + micromark-util-normalize-identifier@2.0.1: dependencies: - '@apidevtools/json-schema-ref-parser': 11.9.3 - '@types/json-schema': 7.0.15 - '@types/lodash': 4.17.24 - is-glob: 4.0.3 - js-yaml: 4.3.1 - lodash: 4.18.1 - minimist: 1.2.8 - prettier: 3.2.5 - tinyglobby: 0.2.17 + micromark-util-symbol: 2.0.1 - json-schema-traverse@1.0.0: {} + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 - json-with-bigint@3.5.10: {} + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 - json5@2.2.3: {} + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - lodash.groupby@4.6.0: {} + micromark-util-symbol@2.0.1: {} - lodash@4.18.1: {} + micromark-util-types@2.0.2: {} - lru-cache@5.1.1: + micromark@4.0.2: dependencies: - yallist: 3.1.1 - - math-intrinsics@1.1.0: {} + '@types/debug': 4.1.13 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color mimic-fn@2.1.0: {} @@ -2345,6 +5599,8 @@ snapshots: mute-stream@3.0.0: {} + nanoid@3.3.18: {} + node-releases@2.0.53: {} npm-run-path@6.0.0: @@ -2364,8 +5620,22 @@ snapshots: papaparse@5.5.3: {} + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.3.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + parse-ms@4.0.0: {} + parse5@7.3.0: + dependencies: + entities: 6.0.1 + patch-console@2.0.0: {} path-key@3.1.1: {} @@ -2382,14 +5652,30 @@ snapshots: picomatch@4.0.5: {} + postcss@8.5.26: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + prettier@3.2.5: {} pretty-ms@9.3.0: dependencies: parse-ms: 4.0.0 + prismjs@1.30.0: {} + progress@2.0.3: {} + property-information@7.2.0: {} + pump@3.0.4: dependencies: end-of-stream: 1.4.5 @@ -2401,13 +5687,213 @@ snapshots: dependencies: side-channel: 1.1.1 + radix-ui@1.6.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-accessible-icon': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-accordion': 1.2.20(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-alert-dialog': 1.1.23(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-arrow': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-aspect-ratio': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-avatar': 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-checkbox': 1.3.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-collapsible': 1.1.20(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context-menu': 2.3.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-dialog': 1.1.23(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-dropdown-menu': 2.1.24(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-form': 0.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-hover-card': 1.1.23(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-label': 2.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-menu': 2.1.24(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-menubar': 1.1.24(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-navigation-menu': 1.2.22(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-one-time-password-field': 0.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-password-toggle-field': 0.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-popover': 1.1.23(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-progress': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-radio-group': 1.4.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-scroll-area': 1.2.18(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-select': 2.3.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-separator': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slider': 1.4.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-switch': 1.3.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-tabs': 1.1.21(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toast': 1.2.23(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toggle': 1.1.18(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toggle-group': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toolbar': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-tooltip': 1.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-escape-keydown': 1.1.5(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + react-dom@19.2.4(react@19.2.4): + dependencies: + react: 19.2.4 + scheduler: 0.27.0 + + react-markdown@9.1.0(@types/react@19.2.14)(react@19.2.4): + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@types/react': 19.2.14 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.1 + react: 19.2.4 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + react-merge-refs@2.1.1: {} + react-reconciler@0.33.0(react@19.2.4): dependencies: react: 19.2.4 scheduler: 0.27.0 + react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.4): + dependencies: + react: 19.2.4 + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.4) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.2.4): + dependencies: + react: 19.2.4 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.4) + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.4) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.4) + use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + + react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.4): + dependencies: + get-nonce: 1.0.1 + react: 19.2.4 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + react-syntax-highlighter@16.1.1(react@19.2.4): + dependencies: + '@babel/runtime': 7.29.7 + highlight.js: 10.7.3 + highlightjs-vue: 1.0.0 + lowlight: 1.20.0 + prismjs: 1.30.0 + react: 19.2.4 + refractor: 5.0.0 + react@19.2.4: {} + refractor@5.0.0: + dependencies: + '@types/hast': 3.0.5 + '@types/prismjs': 1.26.6 + hastscript: 9.0.1 + parse-entities: 4.0.2 + + rehype-katex@7.0.1: + dependencies: + '@types/hast': 3.0.5 + '@types/katex': 0.16.8 + hast-util-from-html-isomorphic: 2.0.0 + hast-util-to-text: 4.0.2 + katex: 0.16.47 + unist-util-visit-parents: 6.0.2 + vfile: 6.0.3 + + remark-breaks@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-newline-to-break: 2.0.0 + unified: 11.0.5 + + remark-directive@3.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-directive: 3.1.0 + micromark-extension-directive: 3.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-math@6.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-math: 3.0.0 + micromark-extension-math: 3.1.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + require-from-string@2.0.2: {} restore-cursor@4.0.0: @@ -2474,8 +5960,12 @@ snapshots: smol-toml@1.6.1: {} + source-map-js@1.2.1: {} + source-map@0.7.6: {} + space-separated-tokens@2.0.2: {} + stack-utils@2.0.6: dependencies: escape-string-regexp: 2.0.0 @@ -2491,14 +5981,31 @@ snapshots: get-east-asian-width: 1.6.0 strip-ansi: 7.2.0 + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-ansi@7.2.0: dependencies: ansi-regex: 6.2.2 strip-final-newline@4.0.0: {} + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + tagged-tag@1.0.0: {} + tailwindcss@4.3.3: {} + + tapable@2.3.3: {} + terminal-size@4.0.1: {} tinyglobby@0.2.17: @@ -2510,6 +6017,10 @@ snapshots: tree-kill@1.2.2: {} + trim-lines@3.0.1: {} + + trough@2.2.0: {} + tslib@2.8.1: {} tunnel@0.0.6: {} @@ -2536,6 +6047,49 @@ snapshots: unicorn-magic@0.3.0: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-find-after@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-remove-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-visit: 5.1.0 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + universal-user-agent@7.0.3: {} update-browserslist-db@1.3.1(browserslist@4.28.8): @@ -2544,8 +6098,45 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.4): + dependencies: + react: 19.2.4 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.4): + dependencies: + detect-node-es: 1.1.0 + react: 19.2.4 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + usehooks-ts@3.1.1(react@19.2.4): + dependencies: + lodash.debounce: 4.0.8 + react: 19.2.4 + + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + weapon-regex@1.3.6: {} + web-namespaces@2.0.1: {} + which@2.0.2: dependencies: isexe: 2.0.0 @@ -2578,3 +6169,5 @@ snapshots: yoga-layout@3.2.1: {} zod@4.4.3: {} + + zwitch@2.0.4: {} diff --git a/sdk/typescript/pnpm-workspace.yaml b/sdk/typescript/pnpm-workspace.yaml new file mode 100644 index 000000000..49c0ad742 --- /dev/null +++ b/sdk/typescript/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +allowBuilds: + esbuild: false diff --git a/sdk/typescript/scripts/build-dashboard.mjs b/sdk/typescript/scripts/build-dashboard.mjs new file mode 100644 index 000000000..17ef4d8e8 --- /dev/null +++ b/sdk/typescript/scripts/build-dashboard.mjs @@ -0,0 +1,89 @@ +import { + copyFile, + mkdir, + readFile, + readdir, + writeFile, +} from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; +import { build } from "esbuild"; +import postcss from "postcss"; +import tailwindcss from "@tailwindcss/postcss"; + +const root = fileURLToPath(new URL("../", import.meta.url)); +const destination = new URL("../dist/server/dashboard/", import.meta.url); +await mkdir(destination, { recursive: true }); +const built = await build({ + absWorkingDir: root, + entryPoints: ["dashboard/index.tsx"], + outfile: fileURLToPath(new URL("app.js", destination)), + bundle: true, + platform: "browser", + format: "esm", + target: ["es2022"], + minify: true, + metafile: true, + legalComments: "eof", + define: { "process.env.NODE_ENV": '"production"' }, + plugins: [ + { + name: "dashboard-styles", + setup(builder) { + builder.onLoad( + { filter: /dashboard[/\\]styles\.css$/ }, + async ({ path }) => { + const css = await postcss([tailwindcss({ base: root })]).process( + await readFile(path, "utf8"), + { from: path }, + ); + // Use system fonts instead of the design system's remote math fonts. + css.root.walkAtRules("font-face", (rule) => rule.remove()); + return { + contents: css.root.toString(), + loader: "css", + resolveDir: dirname(path), + }; + }, + ); + }, + }, + ], +}); +await copyFile( + new URL("../dashboard/index.html", import.meta.url), + new URL("index.html", destination), +); + +// Preserve the licenses of packages whose code or design tokens ship in the browser bundle. +const packages = new Set([ + "node_modules/@openai/apps-sdk-ui", + "node_modules/tailwindcss", +]); +for (const output of Object.values(built.metafile.outputs)) { + for (const [source, { bytesInOutput }] of Object.entries(output.inputs)) { + const directory = /^(.*node_modules\/(?:@[^/]+\/)?[^/]+)\//.exec( + source.replaceAll("\\", "/"), + )?.[1]; + if (directory && bytesInOutput > 0) packages.add(directory); + } +} +const notices = []; +for (const directory of [...packages].sort()) { + const path = join(root, directory); + const manifest = JSON.parse( + await readFile(join(path, "package.json"), "utf8"), + ); + const license = (await readdir(path)).find((name) => + /^licen[sc]e(?:\.[^.]+)?$/i.test(name), + ); + if (!license) + throw new Error(`Missing dashboard dependency license: ${manifest.name}`); + notices.push( + `${manifest.name}@${manifest.version}\n\n${await readFile(join(path, license), "utf8")}`, + ); +} +await writeFile( + new URL("THIRD_PARTY_NOTICES.txt", destination), + notices.join("\n\n---\n\n"), +); diff --git a/sdk/typescript/scripts/check-package.mjs b/sdk/typescript/scripts/check-package.mjs index 563b0c343..33df7cef2 100644 --- a/sdk/typescript/scripts/check-package.mjs +++ b/sdk/typescript/scripts/check-package.mjs @@ -175,6 +175,7 @@ const distFiles = new Set( "cost-model", "custom-validation", "custom-validation-prompt", + "custom-publish", "errors", "github", "index", @@ -197,14 +198,19 @@ const distFiles = new Set( "scan-sessions", "server/index", "deduplication/codex-review", + "deduplication/checkpointed-review", "deduplication/deduplication", "finding-retrieval", + "finding-workflow", + "findings-client", + "finding-dedupe-groups", "deduplication/deduplication-prompts", "deduplication/deduplication-reviewer", - "deduplication/findings-client", "deduplication/scan", "saved-scan", "server/embeddings", + "server/dashboard", + "server/dashboard-types", "server/errors", "server/findings-service", "server/routes", @@ -224,6 +230,15 @@ const distFiles = new Set( ), ), ); +const dashboardFiles = new Set([ + "package/dist/server/dashboard/index.html", + "package/dist/server/dashboard/app.js", + "package/dist/server/dashboard/app.css", + "package/dist/server/dashboard/THIRD_PARTY_NOTICES.txt", +]); +for (const file of dashboardFiles) { + if (!files.has(file)) throw new Error(`npm tarball is missing ${file}.`); +} for (const file of distFiles) { if (!files.has(file)) throw new Error(`npm tarball is missing ${file}.`); } @@ -235,10 +250,12 @@ for (const file of files) { normalized === "package/bin" || normalized === "package/dist" || normalized === "package/dist/server" || + normalized === "package/dist/server/dashboard" || normalized === "package/dist/deduplication" || pluginDirectories.has(normalized) : allowedRoot.has(normalized) || distFiles.has(normalized) || + dashboardFiles.has(normalized) || pluginEntries.has(normalized); if (!allowed || unsafePath.test(file) || file.includes("\\")) { throw new Error(`npm tarball contains an unexpected file: ${file}.`); diff --git a/sdk/typescript/scripts/fixtures/findings-service-sqlite.ts b/sdk/typescript/scripts/fixtures/findings-service-sqlite.ts index 6688bc700..326b2412a 100644 --- a/sdk/typescript/scripts/fixtures/findings-service-sqlite.ts +++ b/sdk/typescript/scripts/fixtures/findings-service-sqlite.ts @@ -59,6 +59,66 @@ try { ].sort(), ); + if (process.argv.includes("--expect-groups")) { + assert.equal( + db.prepare("SELECT COUNT(*) AS count FROM finding_dedupe_groups").get()![ + "count" + ], + 1, + ); + const members = db + .prepare( + "SELECT finding_id FROM finding_dedupe_group_members ORDER BY finding_id", + ) + .all(); + assert.deepEqual( + members.map((row) => row["finding_id"]), + importedIds.slice(0, 3).sort(), + ); + const workflows = db + .prepare("SELECT dedupe_status, results_json FROM finding_workflows") + .all(); + assert.equal(workflows.length, 2); + for (const row of workflows) { + const results = JSON.parse(row["results_json"] as string); + assert.equal(row["dedupe_status"], "completed"); + assert.deepEqual(results.dedupe.duplicateGroups, [ + importedIds.slice(0, 3), + ]); + assert.ok(!("dedupePendingWrite" in results)); + } + const reviews = db + .prepare( + "SELECT model, source_content_digest, prompt_digest, contract_digest, result_json FROM finding_workflow_reviews", + ) + .all(); + const models = new Set(); + const decisions = new Set(); + for (const row of reviews) { + const result = JSON.parse(row["result_json"] as string); + models.add(row["model"] as string); + assert.ok(row["source_content_digest"]); + assert.ok(row["prompt_digest"] && row["contract_digest"]); + for (const decision of "decisions" in result + ? result.decisions + : [result]) { + decisions.add(decision.decision); + if (decision.decision === "SAME") { + assert.equal(typeof decision.canonicalFindingId, "string"); + assert.equal( + decision.mergedFinding.findingId, + decision.canonicalFindingId, + ); + assert.ok( + decision.mergedFinding.extensions.mergedOriginals.length > 0, + ); + } + } + } + assert.deepEqual(models, new Set(["gpt-5.6-luna", "gpt-5.6-sol"])); + assert.deepEqual(decisions, new Set(["SAME", "DISTINCT"])); + } + if (process.argv.includes("--prepare-scan")) { const sourceDir = "/state/smoke-source"; mkdirSync(sourceDir, { recursive: true }); diff --git a/sdk/typescript/scripts/fixtures/package-consumer.ts b/sdk/typescript/scripts/fixtures/package-consumer.ts index 35c0ec868..045ce3076 100644 --- a/sdk/typescript/scripts/fixtures/package-consumer.ts +++ b/sdk/typescript/scripts/fixtures/package-consumer.ts @@ -4,9 +4,11 @@ import { deduplicateScan, estimateScanCost, planComponents, + publishScanToCustom, runComponentScans, type ComponentScanOptions, type DeduplicateScanResult, + type CustomPublicationResult, type Finding, type ScanCost, type ScanOptions, @@ -16,11 +18,23 @@ import { type ValidationResult, } from "@openai/codex-security"; +export async function publishCustom( + scanDir: string, + signal: AbortSignal, +): Promise { + return await publishScanToCustom(scanDir, { + workflowId: "example-workflow", + findingsUrl: "http://127.0.0.1:3000", + signal, + }); +} + export async function dedupe( scanId: string, signal: AbortSignal, ): Promise { return await deduplicateScan(scanId, { + workflowId: "example-workflow", findingsUrl: "http://127.0.0.1:3000", allRepositories: true, signal, @@ -28,6 +42,7 @@ export async function dedupe( } const options: ScanOptions = { + workflowId: "example-workflow", target: DiffTarget.refs({ base: "HEAD~1" }), onProgress(progress: ScanProgress) { progress.filesCompleted satisfies number; diff --git a/sdk/typescript/scripts/smoke-findings-service.ts b/sdk/typescript/scripts/smoke-findings-service.ts index df6413122..c565c217d 100644 --- a/sdk/typescript/scripts/smoke-findings-service.ts +++ b/sdk/typescript/scripts/smoke-findings-service.ts @@ -1,16 +1,20 @@ import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; -import { readFile } from "node:fs/promises"; +import { chmod, cp, mkdtemp, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; import { join } from "node:path"; import { setTimeout } from "node:timers/promises"; import { fileURLToPath } from "node:url"; import type { Finding, FindingsDocument, ScanManifest } from "../src/models.js"; import type { DeduplicateScanResult } from "../src/deduplication/scan.js"; import type { FindingsPage } from "../src/server/storage.js"; +import type { FindingDedupeGroup } from "../src/finding-dedupe-groups.js"; +import type { DashboardSnapshot } from "../src/server/dashboard-types.js"; const repositoryRoot = fileURLToPath(new URL("../../../", import.meta.url)); const container = "findings-ci"; const compose = ["compose", "-p", container, "-f", "compose.findings.yaml"]; +const localRoot = await mkdtemp(join(tmpdir(), "findings-host-publish-")); let base: string; const document: FindingsDocument = JSON.parse( await readFile( @@ -109,6 +113,81 @@ async function startService(): Promise { } } +async function checkHostPublication(): Promise { + const installed = join(localRoot, "package"); + docker([ + "cp", + `${container}:/usr/local/lib/node_modules/@openai/codex-security`, + installed, + ]); + const scanDir = join(localRoot, "completed-scan"); + await cp( + join(installed, "_bundled_plugin/examples/completed-scan"), + scanDir, + { recursive: true }, + ); + if (process.platform !== "win32") await chmod(scanDir, 0o700); + const result = spawnSync( + process.execPath, + [ + join(installed, "bin/codex-security.mjs"), + "publish", + "scan", + "--scan-dir", + scanDir, + "--to", + "custom", + "--findings-url", + base, + "--json", + ], + { + encoding: "utf8", + stdio: ["ignore", "pipe", "inherit"], + env: { ...process.env, CODEX_SECURITY_NO_UPDATE_NOTICE: "1" }, + }, + ); + if (result.error) throw result.error; + assert.equal( + result.status, + 0, + "The installed CLI must publish from the host to Docker", + ); + assert.deepEqual(JSON.parse(result.stdout), { + scanId: manifest.scan.id, + repositoryId, + findingIds: [ids[0]], + findingCount: 1, + }); + const response = await fetch( + `${base}/v1/finding/${ids[0]}/potential-duplicates?repositoryId=${repositoryId}`, + ); + assert.equal(response.status, 200); + assert.deepEqual(await response.json(), { + finding: example, + potentialDuplicates: [], + }); +} + +async function checkDashboard(): Promise { + for (const [path, type] of [ + ["/dashboard", "text/html"], + ["/dashboard/app.js", "text/javascript"], + ["/dashboard/app.css", "text/css"], + ]) { + const response = await fetch(`${base}${path}`); + assert.equal(response.status, 200); + assert.ok(response.headers.get("content-type")?.startsWith(type!)); + assert.ok((await response.text()).length > 0); + } + const response = await fetch(`${base}/v1/dashboard?view=findings`); + assert.equal(response.status, 200); + const snapshot = (await response.json()) as DashboardSnapshot; + assert.equal(snapshot.total, findings.length); + assert.equal(snapshot.overview.findings, findings.length); + assert.equal(snapshot.items.length, findings.length); +} + async function checkInsertions(): Promise { for (const [repository, batch] of [ [repositoryId, findings.slice(0, 3)], @@ -159,23 +238,24 @@ function checkCliDeduplication(): void { "--prepare-scan", ]); for (const allRepositories of [false, true]) { - const actual: unknown = JSON.parse( - docker([ - "exec", - container, - "node", - "--import", - "/test/mock-reviews.mjs", - "dist/cli.js", - "dedupe", - "--scan", - manifest.scan.id, - "--findings-url", - "http://127.0.0.1:3000", - "--json", - ...(allRepositories ? ["--all-repositories"] : []), - ]), - ); + const command = [ + "exec", + container, + "node", + "--import", + "/test/mock-reviews.mjs", + "dist/cli.js", + "dedupe", + "--scan", + manifest.scan.id, + "--workflow-id", + allRepositories ? "smoke-all" : "smoke-repository", + "--findings-url", + "http://127.0.0.1:3000", + "--json", + ...(allRepositories ? ["--all-repositories"] : []), + ]; + const actual: unknown = JSON.parse(docker(command)); const expected: DeduplicateScanResult = { scanId: manifest.scan.id, uniqueFindingIds: [ids[0]!], @@ -183,7 +263,19 @@ function checkCliDeduplication(): void { deduplicationStatus: "completed", }; assert.deepEqual(actual, expected); + const calls = docker([ + "exec", + container, + "cat", + "/state/review-calls.jsonl", + ]); + assert.deepEqual(JSON.parse(docker(command)), expected); + assert.equal( + docker(["exec", container, "cat", "/state/review-calls.jsonl"]), + calls, + ); } + findings[0] = example!; } async function checkPages(): Promise { @@ -203,7 +295,7 @@ async function checkPages(): Promise { } } -function checkStorage(): void { +function checkStorage(expectGroups = false): void { docker([ "exec", container, @@ -211,9 +303,27 @@ function checkStorage(): void { "--experimental-strip-types", "/test/findings-service-sqlite.ts", JSON.stringify(ids), + ...(expectGroups ? ["--expect-groups"] : []), ]); } +async function checkStoredGroups(): Promise { + let stored: FindingDedupeGroup[] = []; + for (const [index, id] of ids.entries()) { + const response = await fetch(`${base}/v1/finding/${id}/dedupe-groups`); + assert.equal(response.status, 200); + const groups = (await response.json()) as FindingDedupeGroup[]; + if (index === 0) { + assert.equal(groups.length, 1, "Repeated dedupe must reuse the group"); + assert.deepEqual(groups[0]!.findingIds, ids.slice(0, 3).sort()); + stored = groups; + } else { + assert.deepEqual(groups, index < 3 ? stored : []); + } + } + return stored; +} + function checkReviews(): void { const calls = docker(["exec", container, "cat", "/state/review-calls.jsonl"]) .split("\n") @@ -258,16 +368,21 @@ let passed = false; try { docker([...compose, "build"]); await startService(); + await checkHostPublication(); await checkInsertions(); + await checkDashboard(); await checkCandidates(); await checkPages(); checkStorage(); checkCliDeduplication(); + checkStorage(true); + const storedGroups = await checkStoredGroups(); checkReviews(); stopService(); docker(["rm", container]); await startService(); - checkStorage(); + checkStorage(true); + assert.deepEqual(await checkStoredGroups(), storedGroups); await checkPages(); await checkCandidates(); stopService(); @@ -277,4 +392,5 @@ try { if (!passed) docker(["logs", container], { check: false }); docker(["rm", "--force", container], { check: false }); docker([...compose, "down", "--volumes"], { check: passed }); + await rm(localRoot, { recursive: true, force: true }); } diff --git a/sdk/typescript/scripts/smoke-package.mjs b/sdk/typescript/scripts/smoke-package.mjs index bae266432..f9d50b242 100644 --- a/sdk/typescript/scripts/smoke-package.mjs +++ b/sdk/typescript/scripts/smoke-package.mjs @@ -393,7 +393,7 @@ try { [ "--input-type=module", "--eval", - `const sdk = await import(${JSON.stringify(packageManifest.name)}); for (const name of ["CodexSecurity", "publishScan", "checkScanPublication", "deduplicateScan"]) if (typeof sdk[name] !== "function") throw new Error("The installed package does not export " + name + ".");`, + `const sdk = await import(${JSON.stringify(packageManifest.name)}); for (const name of ["CodexSecurity", "publishScan", "publishScanToCustom", "checkScanPublication", "deduplicateScan"]) if (typeof sdk[name] !== "function") throw new Error("The installed package does not export " + name + ".");`, ], { cwd: consumer }, ); @@ -601,6 +601,52 @@ try { /lin_api_|security@example\.test/u, ); + const { startFindingsServer } = await import( + pathToFileURL(join(installedRoot, "dist/server/server.js")).href + ); + const dashboardServer = await startFindingsServer({ + // Package builders need only Node. Native and runtime-container tests cover SQLite. + store: { + async initialize() {}, + }, + embeddings: { + async embed() { + throw new Error("Dashboard reads must not call a model"); + }, + }, + host: "127.0.0.1", + port: 0, + }); + try { + const base = `http://127.0.0.1:${dashboardServer.address().port}`; + for (const [path, contentType] of [ + ["/dashboard", "text/html"], + ["/dashboard/app.js", "text/javascript"], + ["/dashboard/app.css", "text/css"], + ]) { + const response = await fetch(`${base}${path}`); + assert.equal(response.status, 200); + assert.ok(response.headers.get("content-type").startsWith(contentType)); + const body = await response.text(); + assert.ok(body.length > 0); + if (contentType === "text/html") { + const mounted = new URL("/service/dashboard/", base); + const assets = [...body.matchAll(/(?:href|src)="([^"]+)"/g)].map( + (match) => new URL(match[1], mounted).pathname, + ); + assert.deepEqual(assets, [ + "/service/dashboard/app.css", + "/service/dashboard/app.js", + ]); + } + } + assert.equal((await fetch(`${base}/dashboard/package.json`)).status, 404); + } finally { + await new Promise((resolve, reject) => + dashboardServer.close((error) => (error ? reject(error) : resolve())), + ); + } + run( process.execPath, [ @@ -614,7 +660,7 @@ try { await smokeNestedDeepScanWorker(installedRoot, consumer); console.log( - `Validated installed ${packageManifest.name}@${packageManifest.version}: public import, NodeNext types, CLI, credential locking, ${expectedPluginFiles.length} bundled plugin files, MCP initialization, bundled Codex version, and a nested worker without global codex.`, + `Validated installed ${packageManifest.name}@${packageManifest.version}: public import, NodeNext types, CLI, credential locking, ${expectedPluginFiles.length} bundled plugin files, MCP initialization, bundled Codex version, dashboard assets, and a nested worker without global codex.`, ); } finally { await rm(consumer, { diff --git a/sdk/typescript/src/api.ts b/sdk/typescript/src/api.ts index 426452c9b..3c248a0ab 100644 --- a/sdk/typescript/src/api.ts +++ b/sdk/typescript/src/api.ts @@ -12,7 +12,15 @@ import { } from "node:fs/promises"; import { randomUUID } from "node:crypto"; import { homedir, tmpdir } from "node:os"; -import { basename, dirname, isAbsolute, join, relative, sep } from "node:path"; +import { + basename, + dirname, + isAbsolute, + join, + relative, + resolve, + sep, +} from "node:path"; import { Codex, type CodexOptions, @@ -83,10 +91,12 @@ import { prepareKnowledgeBase, type PreparedKnowledgeBase, } from "./knowledge-base.js"; +import { FindingWorkflow, workflowDigest } from "./finding-workflow.js"; import { ScanResult, type RepositoryFinding, type TurnResultMetadata, + type ScanResultOptions, } from "./result.js"; import type { SeverityLevel } from "./models.js"; import { scanActivitiesFromEvent, type ScanActivity } from "./scan-activity.js"; @@ -106,6 +116,7 @@ import { CODEX_EXECUTABLE_VERSION, CODEX_SDK_VERSION } from "./version.js"; import { acquireCodexSecurityCredentialHomeLock, bootstrapPlugin, + bundledPluginRoot, cleanupSdkDirectory, codexSecurityCredentialAllowsAmbientImport, codexSecurityCredentialHome, @@ -207,6 +218,8 @@ export interface DeepScanOptions { } export interface ScanOptions extends DeepScanOptions { + /** Opt into a durable scan -> custom publication -> dedupe workflow. */ + workflowId?: string; auth?: ScanAuthMode; /** Stable, privacy-preserving end-user ID for this scan's model requests. */ safetyIdentifier?: string; @@ -433,8 +446,113 @@ export class CodexSecurity { options: ScanOptions = {}, ): Promise { return await this.#trackOperation(() => - this.#run(repository, { ...options }), + options.workflowId === undefined + ? this.#run(repository, { ...options }) + : this.#runWorkflow(repository, { ...options }, options.workflowId), + ); + } + + async #runWorkflow( + repository: string, + options: ScanOptions, + workflowId: string, + ): Promise { + this.#requireOpen(); + const signal = AbortSignal.any([ + this.#abortController.signal, + ...(options.signal ? [options.signal] : []), + ]); + const local = await this.#validateLocalInputs( + repository, + { ...options, outputDir: undefined, archiveExisting: false }, + signal, + ); + const workflow = new FindingWorkflow( + workflowId, + this.#dependencies.environment, + this.#dependencies.runWorkbench, + this.config.pythonPath, ); + if (options.outputDir !== undefined) + await workflow.protectArtifacts(options.outputDir); + const state = await workflow.bind({ + repositoryPath: local.repository, + scanRequestDigest: workflowDigest({ + config: this.config, + options: { + ...options, + target: options.target ?? "repository", + mode: options.mode ?? "standard", + outputDir: + options.outputDir === undefined + ? undefined + : resolve(expandHome(options.outputDir)), + workflowId: undefined, + signal: undefined, + auth: undefined, + archiveExisting: undefined, + }, + }), + }); + type ScanMetadata = Pick< + ScanResultOptions, + "threadId" | "turnResult" | "sarifPath" | "repositoryFindings" + >; + if (state.scanId && state.scanDir) { + await workflow.protectArtifacts(state.scanDir); + let metadata = state.stages.scan.result as ScanMetadata | undefined; + let completed = state.stages.scan.status === "completed"; + if (!completed) { + const scan = await workflow.registeredScan(state.scanId); + completed = + (scan["progress"] as JsonObject | undefined)?.["status"] === + "complete"; + if (completed) + metadata = { + threadId: (scan["continuationThreadId"] as string) ?? "", + turnResult: { status: "completed" }, + }; + } + if (completed) { + const contract = await loadContract(state.scanDir, { + pluginRoot: await bundledPluginRoot(), + expectedScanId: state.scanId, + signal, + }); + await workflow.bind({ artifactDigest: workflowDigest(contract) }); + metadata ??= { threadId: "", turnResult: { status: "completed" } }; + await workflow.complete("scan", metadata); + return new ScanResult({ + ...contract, + scanDir: state.scanDir, + ...metadata, + }); + } + } + await workflow.begin("scan"); + try { + const result = await this.#run(repository, options); + await workflow.protectArtifacts(result.scanDir); + await workflow.bind({ + scanId: result.manifest.scan.id, + scanDir: result.scanDir, + artifactDigest: workflowDigest({ + manifest: result.manifest, + findings: result.findings, + coverage: result.coverage, + }), + }); + await workflow.complete("scan", { + threadId: result.threadId, + turnResult: result.turnResult, + sarifPath: result.sarifPath, + repositoryFindings: result.repositoryFindings, + } satisfies ScanMetadata); + return result; + } catch (error) { + await workflow.fail("scan", error); + throw error; + } } public async validate(options: ValidationOptions): Promise { @@ -943,7 +1061,13 @@ export class CodexSecurity { ? [] : ["--parent-scan-id", options.parentScanId]), ], - JSON.stringify({ recipe, userContext: options.scanPrompt }), + JSON.stringify({ + recipe, + userContext: options.scanPrompt, + ...(options.workflowId === undefined + ? {} + : { workflowId: options.workflowId }), + }), ); const scanId = registration["scanId"]; const targetId = registration["targetId"]; diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index fe5981d4a..7848ccb07 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -61,8 +61,13 @@ import { type ScanPreflight, } from "./api.js"; import { accountStatus } from "./auth.js"; +import { publishScanToCustom } from "./custom-publish.js"; import { deduplicateScanInternal } from "./deduplication/scan.js"; -import { resolveCompletedScan, type SavedScan } from "./saved-scan.js"; +import { + resolveCompletedScan, + resolveWorkflowScan, + type SavedScan, +} from "./saved-scan.js"; import { publishFindingsCsvToCloud, publishScanToCloud, @@ -234,6 +239,7 @@ const EXPORT_DEFAULT_OUTPUTS = { sarif: "results.sarif", } as const; const VALUE_OPTIONS = new Set([ + "--workflow-id", "--auth", "--safety-identifier", "--path", @@ -284,6 +290,7 @@ const VALUE_OPTIONS = new Set([ "--scan-root", "--reason", "--to", + "--findings-url", "--linear-team", "--linear-api-key", "--project", @@ -963,6 +970,7 @@ export function resolveCliPath(directory: string, value: string): string { } interface ScanArguments extends DeepScanOptions { + workflowId?: string; auth?: ScanAuthMode; safetyIdentifier?: string; verbose?: boolean; @@ -1122,6 +1130,7 @@ interface CliDependencies { deduplicateScan?: typeof deduplicateScanInternal; publishFindingsCsvToCloud?: typeof publishFindingsCsvToCloud; publishScanToCloud?: typeof publishScanToCloud; + publishScanToCustom?: typeof publishScanToCustom; confirmPatchReview?: (question: string) => Promise; patchEditor?: ( repository: string, @@ -2073,25 +2082,41 @@ export async function main( .describe("Completed scan directory; omit to select a saved scan."), }), options: PUBLICATION_DESTINATION_OPTIONS.extend({ + workflowId: optionValue("--workflow-id") + .optional() + .describe( + "Resume the named local scan, custom publication, and dedupe workflow.", + ), scan: z .array(optionValue("--scan")) .default([]) .describe( - "Saved scan ID, unique prefix, or latest; repeat for multiple scans (Linear accepts one).", + "Saved scan ID, unique prefix, or latest; Linear and custom accept one scan.", ), scanDir: z .array(optionValue("--scan-dir")) .default([]) .describe( - "External completed scan directory; repeat for multiple scans (Linear accepts one).", + "External completed scan directory; Linear and custom accept one scan.", ), // Cloud remains an internal destination, omitted from public discovery. to: z .string() - .refine((value) => value === "linear" || value === "cloud", { - message: "Unsupported publication destination. Use --to linear.", - }) - .describe("Publication destination (linear)."), + .refine( + (value) => + value === "linear" || value === "cloud" || value === "custom", + { + message: + "Unsupported publication destination. Use --to linear or --to custom.", + }, + ) + .describe("Publication destination (linear or custom)."), + findingsUrl: optionValue("--findings-url") + .url() + .optional() + .describe( + "Findings API base URL; required with --to custom (for example http://localhost:3000).", + ), dryRun: z .boolean() .default(false) @@ -2212,7 +2237,7 @@ export async function main( ); } if ( - options.to === "cloud" && + options.to !== "linear" && (options.skipExisting || [ options.linearTeam, @@ -2223,7 +2248,22 @@ export async function main( ].some((value) => value !== undefined)) ) { throw new CodexSecurityError( - "Cloud publication cannot be combined with Linear options.", + `${options.to === "cloud" ? "Cloud" : "Custom"} publication cannot be combined with Linear options.`, + ); + } + if (options.to === "custom" && options.findingsUrl === undefined) { + throw new CodexSecurityError( + "Custom publication requires --findings-url, for example http://localhost:3000.", + ); + } + if (options.to !== "custom" && options.findingsUrl !== undefined) { + throw new CodexSecurityError( + "--findings-url is only supported with --to custom.", + ); + } + if (options.workflowId !== undefined && options.to !== "custom") { + throw new CodexSecurityError( + "--workflow-id is only supported with --to custom.", ); } const destination = @@ -2233,7 +2273,7 @@ export async function main( dependencies.environment, ) : undefined; - if (options.to === "cloud") { + if (options.to !== "linear") { dependencies.addSignalListener("SIGINT", onInterrupt); dependencies.addSignalListener("SIGTERM", onTerminate); observingSignals = true; @@ -2257,6 +2297,11 @@ export async function main( selectedScans.push(scan); } } + if (options.workflowId !== undefined && selectedScans.length === 0) { + selectedScans.push( + await resolveWorkflowScan(options.workflowId, dependencies), + ); + } let scanDir = selectedScans[0]?.scanDir; let publicationRepository = scanDir === undefined ? "scan" : basename(scanDir); @@ -2522,6 +2567,24 @@ export async function main( return { ...result }; } + if (options.to === "custom") { + const result = await ( + dependencies.publishScanToCustom ?? publishScanToCustom + )(resolveCliPath(currentDirectory, scanDir), { + findingsUrl: options.findingsUrl!, + ...(options.workflowId === undefined + ? {} + : { workflowId: options.workflowId }), + dryRun: options.dryRun, + signal: controller.signal, + ...(selectedScans[0]?.scanId === undefined + ? {} + : { expectedScanId: selectedScans[0].scanId }), + }); + controller.signal.throwIfAborted(); + return { ...result }; + } + const progress = new PublicationProgressPresenter( errorOutput, dependencies, @@ -2734,6 +2797,11 @@ export async function main( }), options: z .object({ + workflowId: optionValue("--workflow-id") + .optional() + .describe( + "Reuse completed work in the named local findings workflow.", + ), auth: z .enum(SCAN_AUTH_MODES) .default("auto") @@ -2923,6 +2991,7 @@ export async function main( const outcome = await runScan( { auth: options.auth, + workflowId: options.workflowId, safetyIdentifier: options.safetyIdentifier, verbose: options.verbose, repository: args.repository, @@ -3056,13 +3125,18 @@ export async function main( .command(publication) .command("dedupe", { description: - "Review a saved scan for duplicates using the findings API and local Codex.", + "Review a saved scan with local Codex and save duplicate groups to the findings API.", destructive: true, mcp: false, options: z.object({ - scan: optionValue("--scan").describe( - "Saved scan ID, unique prefix, or latest.", - ), + workflowId: optionValue("--workflow-id") + .optional() + .describe( + "Resume the named local findings workflow; reuses its saved scan.", + ), + scan: optionValue("--scan") + .optional() + .describe("Saved scan ID, unique prefix, or latest."), allRepositories: z .boolean() .default(false) @@ -3091,12 +3165,25 @@ export async function main( dependencies.addSignalListener("SIGINT", onInterrupt); dependencies.addSignalListener("SIGTERM", onTerminate); try { + const scanId = + options.scan ?? + (options.workflowId === undefined + ? undefined + : (await resolveWorkflowScan(options.workflowId, dependencies)) + .scanId); + if (scanId === undefined) + throw new CodexSecurityError( + "Deduplication requires --scan or --workflow-id.", + ); return await ( dependencies.deduplicateScan ?? deduplicateScanInternal )( - options.scan, + scanId, { findingsUrl: options.findingsUrl, + ...(options.workflowId === undefined + ? {} + : { workflowId: options.workflowId }), allRepositories: options.allRepositories, signal: controller.signal, }, @@ -6420,6 +6507,9 @@ async function executeScan( } security = dependencies.createSecurity(config); const options: ScanOptions = { + ...(arguments_.workflowId === undefined + ? {} + : { workflowId: arguments_.workflowId }), auth, safetyIdentifier: arguments_.safetyIdentifier, target, diff --git a/sdk/typescript/src/custom-publish.ts b/sdk/typescript/src/custom-publish.ts new file mode 100644 index 000000000..ae2cf7697 --- /dev/null +++ b/sdk/typescript/src/custom-publish.ts @@ -0,0 +1,95 @@ +import { loadContractWithScanDirectory } from "./contract.js"; +import { CodexSecurityError } from "./errors.js"; +import { FindingsClient, type FindingsRequest } from "./findings-client.js"; +import type { Finding } from "./models.js"; +import { bundledPluginRoot, type runWorkbench } from "./runtime.js"; +import { + FindingWorkflow, + workflowDestination, + workflowDigest, +} from "./finding-workflow.js"; + +export interface PublishScanToCustomOptions { + /** Resume publication within the named local findings workflow. */ + workflowId?: string; + /** Findings API base URL, such as http://localhost:3000. */ + findingsUrl: string; + /** Validate and preview the upload without making an HTTP request. */ + dryRun?: boolean; + expectedScanId?: string; + signal?: AbortSignal; +} + +export interface CustomPublicationResult { + scanId: string; + repositoryId: string; + findingIds: string[]; + findingCount: number; + dryRun?: true; + findings?: Finding[]; +} + +/** Publish complete, sealed findings to a findings API without changing scan artifacts. */ +export async function publishScanToCustom( + scanDirectory: string, + options: PublishScanToCustomOptions, +): Promise { + return await publishScanToCustomInternal(scanDirectory, options); +} + +/** @internal */ +export async function publishScanToCustomInternal( + scanDirectory: string, + options: PublishScanToCustomOptions, + dependencies: { + fetch?: FindingsRequest; + environment?: NodeJS.ProcessEnv; + runWorkbench?: typeof runWorkbench; + } = {}, +): Promise { + const { contract, scanDirectory: canonicalDirectory } = + await loadContractWithScanDirectory(scanDirectory, { + pluginRoot: await bundledPluginRoot(), + signal: options.signal, + expectedScanId: options.expectedScanId, + }); + const { manifest, findings } = contract; + if (findings.findings.length === 0 && options.workflowId === undefined) { + throw new CodexSecurityError( + "The completed scan has no findings to publish.", + ); + } + const repositoryId = manifest.scan.target.targetId; + const publish = async (): Promise => { + const findingIds = options.dryRun + ? findings.findings.map((finding) => finding.findingId) + : await new FindingsClient( + options.findingsUrl, + options.signal, + dependencies.fetch, + ).publish(findings.findings, repositoryId); + return { + scanId: manifest.scan.id, + repositoryId, + findingIds, + findingCount: findingIds.length, + ...(options.dryRun ? { dryRun: true, findings: findings.findings } : {}), + }; + }; + if (options.workflowId === undefined || options.dryRun) + return await publish(); + const workflow = new FindingWorkflow( + options.workflowId, + dependencies.environment, + dependencies.runWorkbench, + ); + await workflow.protectArtifacts(canonicalDirectory); + await workflow.bind({ + scanId: manifest.scan.id, + scanDir: canonicalDirectory, + artifactDigest: workflowDigest(contract), + destination: workflowDestination(options.findingsUrl), + }); + await workflow.complete("scan", null); + return await workflow.run("publish", publish); +} diff --git a/sdk/typescript/src/deduplication/checkpointed-review.ts b/sdk/typescript/src/deduplication/checkpointed-review.ts new file mode 100644 index 000000000..6fa28c670 --- /dev/null +++ b/sdk/typescript/src/deduplication/checkpointed-review.ts @@ -0,0 +1,93 @@ +import type { JsonObject } from "../config.js"; +import { readFile } from "node:fs/promises"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import { CodexSecurityError } from "../errors.js"; +import type { FindingSearchScope } from "../finding-retrieval.js"; +import { FindingWorkflow, workflowDigest } from "../finding-workflow.js"; +import { CODEX_EXECUTABLE_VERSION } from "../version.js"; +import { + codexSecurityCredentialHome, + expandHome, + resolveCodexCommand, +} from "../runtime.js"; +import type { CodexReview, CodexReviewRunner } from "./codex-review.js"; +import { + reviewSubmissionInstructions, + sourceReviewInstructions, +} from "./deduplication-prompts.js"; + +// Increment when validation or review execution changes without a prompt/schema change. +const REVIEW_CONTRACT_VERSION = 1; + +export async function reviewSettingsDigest( + environment: NodeJS.ProcessEnv, +): Promise { + const homes = new Set([ + expandHome( + environment["CODEX_HOME"] ?? join(homedir(), ".codex"), + environment, + ), + codexSecurityCredentialHome(environment), + ]); + const configs = await Promise.all( + [...homes].map(async (home) => { + try { + return await readFile(join(home, "config.toml"), "utf8"); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") return null; + throw error; + } + }), + ); + return workflowDigest({ + configs, + command: resolveCodexCommand(environment), + baseUrl: environment["OPENAI_BASE_URL"], + }); +} + +export class CheckpointedReviewRunner { + constructor( + private readonly workflow: FindingWorkflow, + private readonly runner: Pick, + private readonly source: JsonObject, + private readonly scope: FindingSearchScope, + private readonly settingsDigest?: string, + ) {} + + async assertSourceUnchanged(): Promise { + const current = await this.workflow.sourceSnapshot( + this.source["repository"] as string, + ); + if (workflowDigest(current) !== workflowDigest(this.source)) + throw new CodexSecurityError( + "Source changed during deduplication. Restart the workflow to review the changed inputs.", + ); + } + + async run(review: CodexReview): Promise { + const binding = { + version: REVIEW_CONTRACT_VERSION, + codexVersion: CODEX_EXECUTABLE_VERSION, + source: this.source, + scope: this.scope, + model: review.model, + effort: review.effort, + settingsDigest: this.settingsDigest, + promptDigest: workflowDigest([ + reviewSubmissionInstructions, + sourceReviewInstructions, + review.prompt, + ]), + contractDigest: workflowDigest(review.schema), + }; + const key = workflowDigest(binding); + const saved = await this.workflow.getReview(key); + if (saved !== null) return review.validate(saved); + const result = review.validate(await this.runner.run(review)); + await this.assertSourceUnchanged(); + await this.workflow.saveReview(key, binding, result); + return result; + } +} diff --git a/sdk/typescript/src/deduplication/deduplication-reviewer.ts b/sdk/typescript/src/deduplication/deduplication-reviewer.ts index a703d6652..129dc6899 100644 --- a/sdk/typescript/src/deduplication/deduplication-reviewer.ts +++ b/sdk/typescript/src/deduplication/deduplication-reviewer.ts @@ -1,4 +1,6 @@ import { z } from "incur"; +import { readFileSync } from "node:fs"; +import Ajv2020, { type ValidateFunction } from "ajv/dist/2020.js"; import type { Finding } from "../models.js"; import type { CodexReviewRunner } from "./codex-review.js"; import { pairReviewPrompt, screeningPrompt } from "./deduplication-prompts.js"; @@ -32,6 +34,33 @@ const screeningSchema = z }) .strict(); +let validateMergedFinding: ValidateFunction | undefined; + +function requireMergedFinding(result: DuplicateDecision): void { + if (result.decision !== "SAME") return; + if (validateMergedFinding === undefined) { + const schema = JSON.parse( + readFileSync( + new URL( + "../../_bundled_plugin/schemas/findings.schema.json", + import.meta.url, + ), + "utf8", + ), + ); + validateMergedFinding = new Ajv2020({ strict: false }).compile( + schema.properties.findings.items, + ); + } + if ( + !validateMergedFinding(result.mergedFinding) || + result.mergedFinding["findingId"] !== result.canonicalFindingId + ) + throw new Error( + "Every SAME decision requires a generated mergedFinding in the Finding schema with the canonical finding's identity.", + ); +} + export type ScreeningResult = z.infer; export type DuplicateDecision = z.infer; @@ -49,6 +78,7 @@ export function validateReview( findings: readonly Finding[], ): DuplicateDecision { const result = reviewSchema.parse(value); + requireMergedFinding(result); if ( result.decision === "SAME" && !findings.some((finding) => finding.findingId === result.canonicalFindingId) @@ -71,6 +101,7 @@ export function validateScreening( ); const seen = new Set(); for (const recommendation of result.decisions) { + requireMergedFinding(recommendation); const pair = recommendation.findingIds; const key = pairKey(pair); if (pair[0] === pair[1] || !required.has(key) || seen.has(key)) { diff --git a/sdk/typescript/src/deduplication/findings-client.ts b/sdk/typescript/src/deduplication/findings-client.ts deleted file mode 100644 index e5d3f7307..000000000 --- a/sdk/typescript/src/deduplication/findings-client.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { CodexSecurityError } from "../errors.js"; -import type { - FindingNeighborhood, - FindingSearchScope, -} from "../finding-retrieval.js"; - -export type FindingsRequest = ( - url: URL, - init: RequestInit, -) => Promise; - -export class FindingsClient { - constructor( - private readonly url: string, - private readonly scope: FindingSearchScope, - private readonly signal?: AbortSignal, - private readonly request: FindingsRequest = fetch, - ) {} - - async potentialDuplicates(findingId: string): Promise { - const url = new URL( - `v1/finding/${encodeURIComponent(findingId)}/potential-duplicates`, - this.url.endsWith("/") ? this.url : `${this.url}/`, - ); - if (this.scope.allRepositories === true) - url.searchParams.set("allRepositories", "true"); - else url.searchParams.set("repositoryId", this.scope.repositoryId); - const response = await this.request(url, { signal: this.signal }); - if (!response.ok) { - throw new CodexSecurityError( - `Potential-duplicates lookup for ${findingId} failed (HTTP ${response.status}).${ - response.status === 404 - ? " Import the finding with its repositoryId through POST /v1/bulk/findings before deduplicating." - : "" - }`, - ); - } - return (await response.json()) as FindingNeighborhood; - } -} diff --git a/sdk/typescript/src/deduplication/scan.ts b/sdk/typescript/src/deduplication/scan.ts index b38488183..2cb58652b 100644 --- a/sdk/typescript/src/deduplication/scan.ts +++ b/sdk/typescript/src/deduplication/scan.ts @@ -1,4 +1,4 @@ -import { loadContract } from "../contract.js"; +import { loadContractWithScanDirectory } from "../contract.js"; import { bundledPluginRoot, codexSecurityStateDirectory, @@ -18,9 +18,22 @@ import { CodexDeduplicationReviewer, type DeduplicationReviewer, } from "./deduplication-reviewer.js"; -import { FindingsClient, type FindingsRequest } from "./findings-client.js"; +import { FindingsClient, type FindingsRequest } from "../findings-client.js"; +import type { FindingSearchScope } from "../finding-retrieval.js"; +import { + FindingWorkflow, + workflowDestination, + workflowDigest, +} from "../finding-workflow.js"; +import { publishScanToCustomInternal } from "../custom-publish.js"; +import { + CheckpointedReviewRunner, + reviewSettingsDigest, +} from "./checkpointed-review.js"; export interface DeduplicateScanOptions { + /** Resume the named local findings workflow, including custom publication. */ + workflowId?: string; /** Findings API base URL. The scan's findings must already be indexed there. */ findingsUrl: string; /** Search all repositories instead of the saved scan's targetId. Defaults to false. */ @@ -32,7 +45,7 @@ export interface DeduplicateScanResult extends DeduplicationResult { scanId: string; } -/** Review a saved scan against embedding candidates, without changing findings. */ +/** Review a saved scan against embedding candidates and persist accepted duplicate groups. */ export async function deduplicateScan( scanId: string, options: DeduplicateScanOptions, @@ -47,6 +60,7 @@ export async function deduplicateScanInternal( dependencies: Partial & { environment?: NodeJS.ProcessEnv; reviewer?: DeduplicationReviewer; + reviewRunner?: Pick; fetch?: FindingsRequest; } = {}, ): Promise { @@ -76,35 +90,104 @@ export async function deduplicateScanInternal( ); }), }); - const contract = await loadContract(scan.scanDir, { - pluginRoot, - expectedScanId: scan.scanId, - signal: options.signal, - }); - const deduplicator = new FindingDeduplicator( - new FindingsClient( - options.findingsUrl, - options.allRepositories === true - ? { allRepositories: true } - : { repositoryId: contract.manifest.scan.target.targetId }, - options.signal, - dependencies.fetch, - ), - dependencies.reviewer ?? - new CodexDeduplicationReviewer( - new CodexReviewRunner( - environment, - undefined, - options.signal, - scan["targetPath"] as string, - ), - ), + const { contract, scanDirectory } = await loadContractWithScanDirectory( + scan.scanDir, + { + pluginRoot, + expectedScanId: scan.scanId, + signal: options.signal, + }, + ); + const client = new FindingsClient( + options.findingsUrl, options.signal, + dependencies.fetch, ); - return { - scanId: scan.scanId, - ...(await deduplicator.run( + const scope: FindingSearchScope = + options.allRepositories === true + ? { allRepositories: true } + : { repositoryId: contract.manifest.scan.target.targetId }; + const workflow = + options.workflowId === undefined + ? undefined + : new FindingWorkflow( + options.workflowId, + environment, + dependencies.runWorkbench === undefined + ? undefined + : (_options, args, input) => + dependencies.runWorkbench!(args, input), + ); + if (workflow) { + await workflow.protectArtifacts(scanDirectory); + await workflow.bind({ + scanId: scan.scanId, + scanDir: scanDirectory, + artifactDigest: workflowDigest(contract), + destination: workflowDestination(options.findingsUrl), + scope, + }); + await workflow.complete("scan", null); + await publishScanToCustomInternal( + scanDirectory, + { + findingsUrl: options.findingsUrl, + workflowId: options.workflowId, + expectedScanId: scan.scanId, + signal: options.signal, + }, + { + environment, + fetch: dependencies.fetch, + runWorkbench: + dependencies.runWorkbench === undefined + ? undefined + : (_options, args, input) => + dependencies.runWorkbench!(args, input), + }, + ); + } + const dedupe = async (): Promise => { + const saved = (await workflow?.get())?.stages.dedupe; + if (saved?.pendingWrite) { + await client.storeDedupeGroups(saved.pendingWrite.groups); + return saved.result as DeduplicateScanResult; + } + const runner = + dependencies.reviewRunner ?? + new CodexReviewRunner( + environment, + undefined, + options.signal, + scan["targetPath"] as string, + ); + const checkpoints = workflow + ? new CheckpointedReviewRunner( + workflow, + runner, + await workflow.sourceSnapshot(scan["targetPath"] as string), + scope, + await reviewSettingsDigest(environment), + ) + : undefined; + const deduplicator = new FindingDeduplicator( + { + potentialDuplicates: (findingId) => + client.potentialDuplicates(findingId, scope), + }, + dependencies.reviewer ?? + new CodexDeduplicationReviewer(checkpoints ?? runner), + options.signal, + ); + const reviewed = await deduplicator.run( contract.findings.findings.map((finding) => finding.findingId), - )), + ); + await checkpoints?.assertSourceUnchanged(); + options.signal?.throwIfAborted(); + const result: DeduplicateScanResult = { scanId: scan.scanId, ...reviewed }; + await workflow?.prepareDedupe(result, { groups: result.duplicateGroups }); + await client.storeDedupeGroups(result.duplicateGroups); + return result; }; + return workflow ? await workflow.run("dedupe", dedupe) : await dedupe(); } diff --git a/sdk/typescript/src/finding-dedupe-groups.ts b/sdk/typescript/src/finding-dedupe-groups.ts new file mode 100644 index 000000000..df0895b34 --- /dev/null +++ b/sdk/typescript/src/finding-dedupe-groups.ts @@ -0,0 +1,6 @@ +/** A reviewed set of duplicate findings. A finding may belong to multiple groups. */ +export interface FindingDedupeGroup { + groupId: string; + findingIds: string[]; + createdAt: string; +} diff --git a/sdk/typescript/src/finding-workflow.ts b/sdk/typescript/src/finding-workflow.ts new file mode 100644 index 000000000..07a3df6e9 --- /dev/null +++ b/sdk/typescript/src/finding-workflow.ts @@ -0,0 +1,194 @@ +import { createHash } from "node:crypto"; +import { isAbsolute, relative, sep } from "node:path"; +import type { JsonObject } from "./config.js"; +import { CodexSecurityError, safeErrorMessage } from "./errors.js"; +import type { FindingSearchScope } from "./finding-retrieval.js"; +import { + bundledPluginRoot, + canonicalizeModelSafePath, + codexSecurityStateDirectory, + resolvePluginPython, + runWorkbench, + type WorkbenchCommandOptions, +} from "./runtime.js"; + +export type WorkflowStage = "scan" | "publish" | "dedupe"; +export interface WorkflowBinding { + repositoryPath?: string; + scanRequestDigest?: string; + scanId?: string; + scanDir?: string; + artifactDigest?: string; + destination?: string; + scope?: FindingSearchScope; +} +export interface WorkflowState extends WorkflowBinding { + id: string; + stages: Record< + WorkflowStage, + { + status: "pending" | "running" | "failed" | "completed"; + result?: unknown; + error?: string; + pendingWrite?: { groups: string[][] }; + } + >; +} + +export function workflowDigest(value: unknown): string { + const json = JSON.stringify(value, (_key, item: unknown) => + item !== null && typeof item === "object" && !Array.isArray(item) + ? Object.fromEntries( + Object.entries(item).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)), + ) + : item, + ); + return createHash("sha256").update(json).digest("hex"); +} + +export function workflowDestination(url: string): string { + const route = "v1/bulk/findings"; + const destination = new URL(route, url.endsWith("/") ? url : `${url}/`); + destination.username = ""; + destination.password = ""; + return destination.href.slice(0, -route.length); +} + +/** State lives in the workbench database, never in sealed scan artifacts. */ +export class FindingWorkflow { + private options?: Promise; + + constructor( + readonly id: string, + private readonly environment: NodeJS.ProcessEnv = process.env, + private readonly workbench: typeof runWorkbench = runWorkbench, + private readonly pythonPath?: string, + ) { + if (!id.trim()) + throw new CodexSecurityError("workflowId must be a nonempty string."); + } + + async protectArtifacts(scanDir: string): Promise { + const path = relative( + await canonicalizeModelSafePath(scanDir), + await canonicalizeModelSafePath( + codexSecurityStateDirectory(this.environment), + ), + ); + if ( + path === "" || + (!isAbsolute(path) && path !== ".." && !path.startsWith(`..${sep}`)) + ) { + throw new CodexSecurityError( + "Workflow state must be outside the sealed scan artifacts.", + ); + } + } + + async get(): Promise { + return (await this.command({ action: "get" })) as WorkflowState | null; + } + + async bind(binding: WorkflowBinding): Promise { + return (await this.command({ action: "bind", binding }))!; + } + + async begin(stage: WorkflowStage): Promise { + return (await this.command({ action: "begin", stage }))!; + } + + async complete( + stage: WorkflowStage, + result: unknown, + ): Promise { + return (await this.command({ action: "complete", stage, result }))!; + } + + async fail(stage: WorkflowStage, error: unknown): Promise { + await this.command({ + action: "fail", + stage, + error: safeErrorMessage(error), + }).catch(() => undefined); + } + + async run(stage: WorkflowStage, operation: () => Promise): Promise { + const state = await this.begin(stage); + if (state.stages[stage].status === "completed") + return state.stages[stage].result as T; + try { + const result = await operation(); + await this.complete(stage, result); + return result; + } catch (error) { + await this.fail(stage, error); + throw error; + } + } + + async registeredScan(scanId: string): Promise { + const context = await this.call(["get-scan", "--scan-id", scanId]); + return context["scan"] as JsonObject; + } + + async sourceSnapshot(repository: string): Promise { + return (await this.request({ action: "source", repository }))[ + "source" + ] as JsonObject; + } + + async getReview(key: string): Promise { + return (await this.request({ action: "get-review", key }))["review"]; + } + + async saveReview( + key: string, + binding: object, + result: unknown, + ): Promise { + await this.request({ action: "save-review", key, binding, result }); + } + + async prepareDedupe( + result: unknown, + pendingWrite: { groups: string[][] }, + ): Promise { + await this.command({ + action: "prepare-dedupe", + stage: "dedupe", + result, + pendingWrite, + }); + } + + private async command(payload: object): Promise { + const result = await this.request(payload); + return result["workflow"] as unknown as WorkflowState | null; + } + + private async request(payload: object): Promise { + return await this.call( + ["finding-workflow"], + JSON.stringify({ id: this.id, ...payload }), + ); + } + + private async call( + args: readonly string[], + input?: string, + ): Promise { + this.options ??= (async () => ({ + pluginRoot: await bundledPluginRoot(), + python: await resolvePluginPython({ + environment: this.environment, + configuredPath: this.pythonPath, + }), + environment: { + ...this.environment, + CODEX_SECURITY_STATE_DIR: codexSecurityStateDirectory(this.environment), + }, + failureMessage: "Could not save or resume the findings workflow", + }))(); + return await this.workbench(await this.options, args, input); + } +} diff --git a/sdk/typescript/src/findings-client.ts b/sdk/typescript/src/findings-client.ts new file mode 100644 index 000000000..393418670 --- /dev/null +++ b/sdk/typescript/src/findings-client.ts @@ -0,0 +1,88 @@ +import { CodexSecurityError } from "./errors.js"; +import type { Finding } from "./models.js"; +import type { + FindingNeighborhood, + FindingSearchScope, +} from "./finding-retrieval.js"; + +export type FindingsRequest = ( + url: URL, + init: RequestInit, +) => Promise; + +export class FindingsClient { + constructor( + private readonly url: string, + private readonly signal?: AbortSignal, + private readonly request: FindingsRequest = fetch, + ) {} + + async potentialDuplicates( + findingId: string, + scope: FindingSearchScope, + ): Promise { + const url = this.endpoint( + `v1/finding/${encodeURIComponent(findingId)}/potential-duplicates`, + ); + if (scope.allRepositories === true) + url.searchParams.set("allRepositories", "true"); + else url.searchParams.set("repositoryId", scope.repositoryId); + const response = await this.request(url, { signal: this.signal }); + if (!response.ok) { + throw new CodexSecurityError( + `Potential-duplicates lookup for ${findingId} failed (HTTP ${response.status}).${ + response.status === 404 + ? " Import the finding with its repositoryId through POST /v1/bulk/findings before deduplicating." + : "" + }`, + ); + } + return (await response.json()) as FindingNeighborhood; + } + + async publish( + findings: readonly Finding[], + repositoryId: string, + ): Promise { + const receipt = await this.post("v1/bulk/findings", { + findings, + repositoryId, + }); + const expected = new Set(findings.map((finding) => finding.findingId)); + if ( + !Array.isArray(receipt) || + receipt.length !== findings.length || + new Set(receipt).size !== expected.size || + receipt.some((id) => !expected.has(id)) + ) { + throw new CodexSecurityError( + "The findings API did not acknowledge all published finding IDs. Check the service before retrying.", + ); + } + return receipt as string[]; + } + + async storeDedupeGroups(groups: readonly string[][]): Promise { + if (groups.length === 0) return; + await this.post("v1/dedupe-groups", { groups }); + } + + private endpoint(path: string): URL { + return new URL(path, this.url.endsWith("/") ? this.url : `${this.url}/`); + } + + private async post(path: string, body: unknown): Promise { + const response = await this.request(this.endpoint(path), { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + signal: this.signal, + }); + if (!response.ok) { + throw new CodexSecurityError( + `Findings API POST /${path} failed (HTTP ${response.status}).`, + ); + } + return await response.json(); + } +} diff --git a/sdk/typescript/src/index.ts b/sdk/typescript/src/index.ts index 52cd1bcae..eeca8727d 100644 --- a/sdk/typescript/src/index.ts +++ b/sdk/typescript/src/index.ts @@ -64,6 +64,11 @@ export { loadContract, requireScanFile } from "./contract.js"; export type { LoadedContract, ScanExpectation } from "./contract.js"; export type * from "./models.js"; export { checkScanPublication, publishScan } from "./publish.js"; +export { publishScanToCustom } from "./custom-publish.js"; +export type { + PublishScanToCustomOptions, + CustomPublicationResult, +} from "./custom-publish.js"; export { deduplicateScan } from "./deduplication/scan.js"; export type { DeduplicateScanOptions, diff --git a/sdk/typescript/src/saved-scan.ts b/sdk/typescript/src/saved-scan.ts index 8c2d04eb0..b73e50a44 100644 --- a/sdk/typescript/src/saved-scan.ts +++ b/sdk/typescript/src/saved-scan.ts @@ -8,7 +8,29 @@ export type SavedScan = JsonObject & { scanId: string; scanDir: string }; export interface SavedScanDependencies { currentDirectory(): string; - runWorkbench(args: readonly string[]): Promise; + runWorkbench(args: readonly string[], input?: string): Promise; +} + +export async function resolveWorkflowScan( + workflowId: string, + dependencies: SavedScanDependencies, +): Promise<{ scanId: string; scanDir: string }> { + const context = await dependencies.runWorkbench( + ["finding-workflow"], + JSON.stringify({ id: workflowId, action: "get" }), + ); + const workflow = context["workflow"]; + if ( + workflow === undefined || + !isJsonObject(workflow) || + typeof workflow["scanId"] !== "string" || + typeof workflow["scanDir"] !== "string" + ) { + throw new CodexSecurityError( + `Workflow ${workflowId} has no saved scan. Start it with scan --workflow-id ${workflowId}.`, + ); + } + return { scanId: workflow["scanId"], scanDir: workflow["scanDir"] }; } export async function resolveCompletedScan( diff --git a/sdk/typescript/src/server/dashboard-types.ts b/sdk/typescript/src/server/dashboard-types.ts new file mode 100644 index 000000000..4a5206046 --- /dev/null +++ b/sdk/typescript/src/server/dashboard-types.ts @@ -0,0 +1,46 @@ +import type { Finding } from "../models.js"; +import type { FindingDedupeGroup } from "../finding-dedupe-groups.js"; + +export type DashboardView = "findings" | "groups"; + +export interface DashboardQuery { + view: DashboardView; + limit: number; + offset: number; + query: string; + repository: string; + sort: "activity" | "newest"; + id?: string; +} + +/** Stored finding or duplicate group list projection. */ +export interface DashboardItem { + id: string; + title: string; + repositoryIds: string[]; + createdAt: string; + updatedAt: string; + memberCount?: number; + severity?: Finding["severity"]["level"]; +} + +export interface DashboardDetail { + item: DashboardItem; + finding?: Finding; + group?: FindingDedupeGroup; + groups?: FindingDedupeGroup[]; +} + +export interface DashboardSnapshot { + overview: { + findings: number; + groups: number; + }; + repositories: { id: string; label: string }[]; + items: DashboardItem[]; + total: number; + limit: number; + offset: number; + nextOffset: number | null; + detail: DashboardDetail | null; +} diff --git a/sdk/typescript/src/server/dashboard.ts b/sdk/typescript/src/server/dashboard.ts new file mode 100644 index 000000000..e0dc28804 --- /dev/null +++ b/sdk/typescript/src/server/dashboard.ts @@ -0,0 +1,57 @@ +import { readFile } from "node:fs/promises"; +import type { ServerResponse } from "node:http"; +import type { DashboardQuery, DashboardView } from "./dashboard-types.js"; +import { FindingsError } from "./errors.js"; +import { pagination } from "./validation.js"; + +const assets = new Map([ + ["/dashboard/", ["index.html", "text/html; charset=utf-8"]], + ["/dashboard/app.js", ["app.js", "text/javascript; charset=utf-8"]], + ["/dashboard/app.css", ["app.css", "text/css; charset=utf-8"]], +]); + +/** Serve only bundled UI assets, never a path supplied by a request. */ +export async function serveDashboard( + path: string, + response: ServerResponse, +): Promise { + if (path === "/dashboard") { + response.writeHead(308, { Location: "dashboard/" }); + response.end(); + return true; + } + const asset = assets.get(path); + if (!asset) return false; + const body = await readFile( + new URL(`./dashboard/${asset[0]}`, import.meta.url), + ); + response.writeHead(200, { + "Content-Type": asset[1]!, + "Cache-Control": "no-cache", + "X-Content-Type-Options": "nosniff", + "Content-Security-Policy": + "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; font-src 'self'; base-uri 'none'; frame-ancestors 'none'", + }); + response.end(body); + return true; +} + +export function dashboardQuery(parameters: URLSearchParams): DashboardQuery { + const view = parameters.get("view") ?? "findings"; + const sort = parameters.get("sort") ?? "activity"; + if (!["findings", "groups"].includes(view)) + throw new FindingsError("invalid_request", "Unknown dashboard view."); + if (sort !== "activity" && sort !== "newest") + throw new FindingsError( + "invalid_request", + "sort must be activity or newest.", + ); + return { + view: view as DashboardView, + ...pagination(parameters), + query: parameters.get("query") ?? "", + repository: parameters.get("repository") ?? "", + sort, + ...(parameters.has("id") ? { id: parameters.get("id")! } : {}), + }; +} diff --git a/sdk/typescript/src/server/findings-service.ts b/sdk/typescript/src/server/findings-service.ts index bb7f9372f..a271acd18 100644 --- a/sdk/typescript/src/server/findings-service.ts +++ b/sdk/typescript/src/server/findings-service.ts @@ -1,4 +1,5 @@ import type { Finding } from "../models.js"; +import type { DashboardQuery } from "./dashboard-types.js"; import type { FindingSearchScope } from "../finding-retrieval.js"; import type { FindingEmbedder } from "./embeddings.js"; import type { FindingsPage, FindingsStore } from "./storage.js"; @@ -27,7 +28,19 @@ export class FindingsService { return await this.store.findPotentialDuplicates(findingId, scope); } + async storeDedupeGroups(groups: readonly string[][]) { + return await this.store.storeDedupeGroups(groups); + } + + async listDedupeGroups(findingId: string) { + return await this.store.listDedupeGroups(findingId); + } + async list(page: { limit: number; offset: number }): Promise { return await this.store.list(page); } + + async dashboard(query: DashboardQuery) { + return await this.store.dashboard(query); + } } diff --git a/sdk/typescript/src/server/routes.ts b/sdk/typescript/src/server/routes.ts index ae3a84c90..d94e11142 100644 --- a/sdk/typescript/src/server/routes.ts +++ b/sdk/typescript/src/server/routes.ts @@ -1,10 +1,12 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import type { ValidateFunction } from "ajv"; import { FindingsError } from "./errors.js"; +import { dashboardQuery, serveDashboard } from "./dashboard.js"; import type { FindingsService } from "./findings-service.js"; import { findingSearchScope, pagination, + validateDedupeGroups, type FindingsRequest, } from "./validation.js"; @@ -17,6 +19,20 @@ export async function handleFindingsRequest( try { const url = new URL(request.url ?? "/", "http://localhost"); const route = `${request.method} ${url.pathname}`; + if ( + request.method === "GET" && + (await serveDashboard(url.pathname, response)) + ) + return; + if (route === "GET /v1/dashboard") { + response.setHeader("Cache-Control", "no-store"); + json( + response, + 200, + await service.dashboard(dashboardQuery(url.searchParams)), + ); + return; + } if (route === "GET /v1/findings") { console.log(route); json(response, 200, await service.list(pagination(url.searchParams))); @@ -37,6 +53,26 @@ export async function handleFindingsRequest( ); return; } + const dedupeGroups = /^\/v1\/finding\/([^/]+)\/dedupe-groups$/.exec( + url.pathname, + ); + if (request.method === "GET" && dedupeGroups) { + console.log("GET /v1/finding/:id/dedupe-groups"); + json(response, 200, await service.listDedupeGroups(dedupeGroups[1]!)); + return; + } + if (route === "POST /v1/dedupe-groups") { + console.log(route); + const input = await readJson(request); + if (!validateDedupeGroups(input)) { + throw new FindingsError( + "invalid_request", + "Expected {groups: [[findingId, ...], ...]} with at least two distinct finding IDs per group.", + ); + } + json(response, 201, await service.storeDedupeGroups(input.groups)); + return; + } if (route === "POST /v1/bulk/findings") { console.log(route); const input = await readJson(request); diff --git a/sdk/typescript/src/server/sqlite-store.ts b/sdk/typescript/src/server/sqlite-store.ts index 1eefb9360..4794fa928 100644 --- a/sdk/typescript/src/server/sqlite-store.ts +++ b/sdk/typescript/src/server/sqlite-store.ts @@ -6,6 +6,8 @@ import { type WorkbenchCommandOptions, } from "../runtime.js"; import { FindingsError } from "./errors.js"; +import type { DashboardQuery, DashboardSnapshot } from "./dashboard-types.js"; +import type { FindingDedupeGroup } from "../finding-dedupe-groups.js"; import type { FindingNeighborhood, FindingSearchScope, @@ -25,6 +27,13 @@ export class SqliteFindingsStore implements FindingsStore { await this.run(["database-info"]); } + async dashboard(query: DashboardQuery): Promise { + return (await this.run( + ["dashboard"], + JSON.stringify(query), + )) as unknown as DashboardSnapshot; + } + async insert( entries: readonly EmbeddedFinding[], repositoryId?: string, @@ -78,6 +87,30 @@ export class SqliteFindingsStore implements FindingsStore { return result as unknown as FindingNeighborhood; } + async storeDedupeGroups( + groups: readonly string[][], + ): Promise { + const result = await this.run( + ["store-dedupe-groups"], + JSON.stringify({ groups }), + ); + if (result["error"] === "finding_conflict") { + throw new FindingsError( + "finding_conflict", + "Every dedupe group member must already exist in the findings database.", + ); + } + return result["groups"] as unknown as FindingDedupeGroup[]; + } + + async listDedupeGroups(findingId: string): Promise { + const result = await this.run([ + "list-dedupe-groups", + `--finding-id=${findingId}`, + ]); + return result["groups"] as unknown as FindingDedupeGroup[]; + } + private async run(args: string[], input?: string) { const options = await (this.options ??= this.resolveOptions()); return await runWorkbench(options, args, input); diff --git a/sdk/typescript/src/server/storage.ts b/sdk/typescript/src/server/storage.ts index 25bb959fc..ef804176b 100644 --- a/sdk/typescript/src/server/storage.ts +++ b/sdk/typescript/src/server/storage.ts @@ -1,4 +1,6 @@ import type { Finding } from "../models.js"; +import type { DashboardQuery, DashboardSnapshot } from "./dashboard-types.js"; +import type { FindingDedupeGroup } from "../finding-dedupe-groups.js"; import type { FindingNeighborhood, FindingSearchScope, @@ -24,11 +26,14 @@ export interface FindingsPage { export interface FindingsStore { initialize(): Promise; + dashboard(query: DashboardQuery): Promise; insert( entries: readonly EmbeddedFinding[], repositoryId?: string, ): Promise; list(page: { limit: number; offset: number }): Promise; + storeDedupeGroups(groups: readonly string[][]): Promise; + listDedupeGroups(findingId: string): Promise; findPotentialDuplicates( findingId: string, scope: FindingSearchScope, diff --git a/sdk/typescript/src/server/validation.ts b/sdk/typescript/src/server/validation.ts index 8d4b57bff..a41e79c51 100644 --- a/sdk/typescript/src/server/validation.ts +++ b/sdk/typescript/src/server/validation.ts @@ -8,6 +8,24 @@ import { FindingsError } from "./errors.js"; export type FindingsRequest = { findings: Finding[]; repositoryId?: string }; +export const validateDedupeGroups = new Ajv2020().compile<{ + groups: string[][]; +}>({ + type: "object", + required: ["groups"], + properties: { + groups: { + type: "array", + items: { + type: "array", + minItems: 2, + uniqueItems: true, + items: { type: "string", minLength: 1 }, + }, + }, + }, +}); + export async function findingsRequestValidator(): Promise< ValidateFunction > { diff --git a/sdk/typescript/tests-ts/api.test.ts b/sdk/typescript/tests-ts/api.test.ts index 74836e58d..15d02a5be 100644 --- a/sdk/typescript/tests-ts/api.test.ts +++ b/sdk/typescript/tests-ts/api.test.ts @@ -67,6 +67,7 @@ import { preparedRuntime, } from "./support/api-events.js"; import { runTestInSubprocess } from "./support/test-subprocess.js"; +import { FindingWorkflow } from "../src/finding-workflow.js"; type ScanObserverName = Parameters< NonNullable @@ -78,6 +79,112 @@ const { cleanup, copyCompletedScan, temporaryDirectory } = createApiTestFixtures(); afterEach(cleanup); +test.each(["completed", "receipt-lost", "scan-interrupted"])( + "durable scan workflow resumes after %s without rerunning completed work", + async (scenario) => { + const root = await temporaryDirectory(); + const repository = join(root, "repository"); + const scanDir = join(root, "scan"); + await mkdir(repository); + await mkdir(scanDir, { mode: 0o700 }); + const environment = { + PATH: process.env["PATH"], + SystemRoot: process.env["SystemRoot"], + CODEX_SECURITY_STATE_DIR: join(root, "state"), + }; + const workflowId = "durable-scan"; + let modelCalls = 0; + let completed = false; + let loseReceipt = scenario === "receipt-lost"; + const makeClient = async (attempt: number) => { + const codexHome = join(root, `codex-home-${attempt}`); + await mkdir(codexHome); + return new TestClient( + {}, + { + environment, + prepareRuntime: async () => preparedRuntime(codexHome), + resolvePluginPython: async () => "/managed/python", + prepareOutputDir: async () => scanDir, + repositoryRevision: async () => "deadbeef", + runWorkbench: async (options, args, input) => { + if (args[0] === "finding-workflow") { + const payload = JSON.parse(input!); + if ( + loseReceipt && + payload.action === "complete" && + payload.stage === "scan" + ) { + loseReceipt = false; + throw new Error("Synthetic receipt write failure"); + } + return await runWorkbench(options, args, input); + } + if (args[0] === "get-scan") + return { + scan: { + progress: { status: completed ? "complete" : "failed" }, + continuationThreadId: "thread-1", + }, + }; + if (args[0] === "register-cli-scan") { + expect(JSON.parse(input!).workflowId).toBe(workflowId); + const registration = mockScanRegistration(args, input); + await new FindingWorkflow(workflowId, environment).bind({ + scanId: registration["scanId"] as string, + scanDir, + }); + return registration; + } + if (args[0] === "complete-scan") completed = true; + return mockWorkbench(args, input); + }, + createCodex: () => ({ + startThread: () => ({ + id: "thread-1", + async runStreamed() { + modelCalls++; + if (scenario === "scan-interrupted" && modelCalls === 1) + throw new Error("Synthetic interrupted scan"); + await copyCompletedScan(root); + return { events: completedEvents() }; + }, + }), + }), + }, + ); + }; + const first = await makeClient(1); + let original: Record | undefined; + try { + if (scenario === "completed") + original = (await first.run(repository, { workflowId })).toJSON(); + else + await expect(first.run(repository, { workflowId })).rejects.toThrow( + "Synthetic", + ); + } finally { + await first.close(); + } + const resumed = await makeClient(2); + try { + const result = await resumed.run(repository, { workflowId }); + expect(result.manifest.scan.id).toBe("scan_example_001"); + if (original) expect(result.toJSON()).toEqual(original); + expect(modelCalls).toBe(scenario === "scan-interrupted" ? 2 : 1); + expect( + (await new FindingWorkflow(workflowId, environment).get())?.stages.scan + .status, + ).toBe("completed"); + await expect( + resumed.run(repository, { workflowId, mode: "deep" }), + ).rejects.toThrow("already bound to a different"); + } finally { + await resumed.close(); + } + }, +); + const EXTERNAL_PROVIDER_CASES = [ [ "OpenRouter", diff --git a/sdk/typescript/tests-ts/cli-dedupe.test.ts b/sdk/typescript/tests-ts/cli-dedupe.test.ts index 247ee98de..896162b97 100644 --- a/sdk/typescript/tests-ts/cli-dedupe.test.ts +++ b/sdk/typescript/tests-ts/cli-dedupe.test.ts @@ -11,6 +11,56 @@ const args = [ "--json", ]; +test("dedupe resolves a workflow's pinned scan and passes the workflow ID to the SDK", async () => { + const deps = dependencies(); + deps.runWorkbench = async (args, input) => { + expect(args).toEqual(["finding-workflow"]); + expect(JSON.parse(input!)).toEqual({ + id: "workflow-example", + action: "get", + }); + return { + workflow: { + id: "workflow-example", + scanId: "exact-scan", + scanDir: "/synthetic/artifacts", + }, + }; + }; + deps.deduplicateScan = async (scanId, options) => { + expect(scanId).toBe("exact-scan"); + expect(options.workflowId).toBe("workflow-example"); + return { + scanId, + uniqueFindingIds: [], + duplicateGroups: [], + deduplicationStatus: "completed", + }; + }; + const stdout = capture(); + expect( + await main( + [ + "dedupe", + "--workflow-id", + "workflow-example", + "--findings-url", + "http://localhost:3000", + "--json", + ], + stdout.stream, + capture().stream, + deps, + ), + ).toBe(0); + expect(JSON.parse(stdout.text())).toEqual({ + scanId: "exact-scan", + uniqueFindingIds: [], + duplicateGroups: [], + deduplicationStatus: "completed", + }); +}); + test.each([false, true])( "dedupe passes the scan selector, URL, and all-repository scope %s to the SDK", async (allRepositories) => { diff --git a/sdk/typescript/tests-ts/cli-publish.test.ts b/sdk/typescript/tests-ts/cli-publish.test.ts index c1cc4fb6e..d65d6422c 100644 --- a/sdk/typescript/tests-ts/cli-publish.test.ts +++ b/sdk/typescript/tests-ts/cli-publish.test.ts @@ -74,6 +74,189 @@ function publicationResult( }; } +describe("publish scan to custom", () => { + test("uses a workflow's exact scan without changing the publication receipt", async () => { + const [scanDir] = await publicationScanDirectories(1); + const deps = dependencies({ + onWorkbench: (args) => { + expect(args).toEqual(["finding-workflow"]); + return { + workflow: { + id: "publication-workflow", + scanId: "exact-scan", + scanDir: scanDir!, + }, + }; + }, + }); + const receipt = { + scanId: "exact-scan", + repositoryId: "repository", + findingIds: ["finding"], + findingCount: 1, + }; + deps.publishScanToCustom = async (directory, options) => { + expect(directory).toBe(scanDir!); + expect(options.workflowId).toBe("publication-workflow"); + expect(options.expectedScanId).toBe("exact-scan"); + return receipt; + }; + const stdout = capture(); + expect( + await main( + [ + "publish", + "scan", + "--workflow-id", + "publication-workflow", + "--to", + "custom", + "--findings-url", + "http://localhost:3000", + "--json", + ], + stdout.stream, + capture().stream, + deps, + ), + ).toBe(0); + expect(JSON.parse(stdout.text())).toEqual(receipt); + }); + test.each([false, true])( + "publishes a selected saved scan with dry-run=%s", + async (dryRun) => { + const [scanDir] = await publicationScanDirectories(1); + const stdout = capture(); + const stderr = capture(); + const deps = dependencies({ + onWorkbench: () => ({ + scan: { + scanId: "scan-example", + scanDir: scanDir!, + progress: { status: "complete" }, + }, + }), + }); + const receipt = { + scanId: "scan-example", + repositoryId: "repository-example", + findingIds: ["finding-1"], + findingCount: 1, + }; + let calls = 0; + deps.publishScanToCustom = async (directory, options) => { + calls++; + expect(directory).toBe(scanDir!); + expect(options).toEqual({ + findingsUrl: "http://localhost:3000", + dryRun, + expectedScanId: "scan-example", + signal: expect.any(AbortSignal), + }); + return receipt; + }; + expect( + await main( + [ + "publish", + "scan", + "--scan", + "scan-example", + "--to", + "custom", + "--findings-url", + "http://localhost:3000", + "--json", + ...(dryRun ? ["--dry-run"] : []), + ], + stdout.stream, + stderr.stream, + deps, + ), + ).toBe(0); + expect(calls).toBe(1); + expect(JSON.parse(stdout.text())).toEqual(receipt); + expect(stderr.text()).toBe(""); + }, + ); + + test.each([ + [["--to", "custom"], "requires --findings-url"], + [["--to", "custom", "--findings-url", "--dry-run"], "--findings-url"], + [["--to", "custom", "--findings-url", "not-a-url"], "URL"], + [ + [ + "--to", + "custom", + "--findings-url", + "http://localhost:3000", + "--skip-existing", + ], + "cannot be combined with Linear options", + ], + [ + [...DESTINATION_OPTIONS, "--findings-url", "http://localhost:3000"], + "only supported with --to custom", + ], + ])( + "rejects incompatible custom publication inputs %j", + async (flags, message) => { + const stdout = capture(); + const stderr = capture(); + const deps = dependencies(); + deps.publishScanToCustom = async () => { + throw new Error("must not publish"); + }; + expect( + await main( + ["publish", "scan", "completed-scan", ...flags, "--json"], + stdout.stream, + stderr.stream, + deps, + ), + ).toBe(2); + expect(stderr.text()).toContain(message); + expect(stdout.text().trim()).toBe(""); + }, + ); + + test("forwards cancellation for an external scan and does not report success", async () => { + const stdout = capture(); + const stderr = capture(); + const signals = new FakeSignals(); + const deps = dependencies({ signals }); + deps.publishScanToCustom = async (directory, options) => { + expect(directory).toBe(resolve(deps.currentDirectory(), "external-scan")); + expect(options.expectedScanId).toBeUndefined(); + signals.emit("SIGINT"); + options.signal!.throwIfAborted(); + throw new Error("unreachable"); + }; + expect( + await main( + [ + "publish", + "scan", + "--scan-dir", + "external-scan", + "--to", + "custom", + "--findings-url", + "http://localhost:3000", + "--json", + ], + stdout.stream, + stderr.stream, + deps, + ), + ).toBe(130); + expect(stderr.text()).toContain("Publication canceled"); + expect(stdout.text().trim()).toBe(""); + expect(signals.listeners.get("SIGINT")?.size).toBe(0); + expect(signals.listeners.get("SIGTERM")?.size).toBe(0); + }); +}); + describe("publish check", () => { test("resolves the shared destination options without invoking publication", async () => { const stdout = capture(); diff --git a/sdk/typescript/tests-ts/cli.test.ts b/sdk/typescript/tests-ts/cli.test.ts index 8f56e82c3..398724d3d 100644 --- a/sdk/typescript/tests-ts/cli.test.ts +++ b/sdk/typescript/tests-ts/cli.test.ts @@ -1754,6 +1754,31 @@ describe("CLI", () => { }, ); + test("passes the workflow ID to scans without changing their JSON output", async () => { + const deps = dependencies(); + const result = fakeResult(); + deps.createSecurity = () => ({ + run: async (_repository, options) => { + expect(options?.workflowId).toBe("scan-workflow"); + return result; + }, + preflight: async () => fakePreflight(), + close: async () => {}, + }); + const stdout = capture(); + expect( + await main( + ["scan", ".", "--workflow-id", "scan-workflow", "--json"], + stdout.stream, + capture().stream, + deps, + ), + ).toBe(0); + expect(JSON.parse(stdout.text())).toMatchObject({ + scanDir: result.scanDir, + }); + }); + test("keeps verbose diagnostics separate from interactive progress", async () => { const stdout = capture(); const stderr = capture(true); diff --git a/sdk/typescript/tests-ts/custom-publish.test.ts b/sdk/typescript/tests-ts/custom-publish.test.ts new file mode 100644 index 000000000..38f87267d --- /dev/null +++ b/sdk/typescript/tests-ts/custom-publish.test.ts @@ -0,0 +1,147 @@ +import { chmod, cp, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, expect, test } from "bun:test"; +import { publishScanToCustomInternal as publishScanToCustom } from "../src/custom-publish.js"; +import type { FindingsDocument } from "../src/models.js"; +import { PLUGIN_ROOT } from "./plugin-root.js"; + +const directories: string[] = []; +afterEach(async () => { + await Promise.all( + directories + .splice(0) + .map((path) => rm(path, { recursive: true, force: true })), + ); +}); + +async function fixture() { + const scan = await mkdtemp(join(tmpdir(), "custom-publish-")); + directories.push(scan); + await cp(join(PLUGIN_ROOT, "examples/completed-scan"), scan, { + recursive: true, + }); + if (process.platform !== "win32") await chmod(scan, 0o700); + const source = await readFile(join(scan, "findings.json"), "utf8"); + const document = JSON.parse(source) as FindingsDocument; + return { scan, source, document }; +} + +test("publishes complete sealed findings with their repository ID to a custom base URL", async () => { + const { scan, source, document } = await fixture(); + const controller = new AbortController(); + const ids = document.findings.map((finding) => finding.findingId); + let calls = 0; + const result = await publishScanToCustom( + scan, + { + findingsUrl: "http://synthetic.test/service", + expectedScanId: document.scanId, + signal: controller.signal, + }, + { + fetch: async (url, options) => { + calls++; + expect(String(url)).toBe( + "http://synthetic.test/service/v1/bulk/findings", + ); + expect(options).toEqual({ + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + findings: document.findings, + repositoryId: "target_sha256_example", + }), + signal: controller.signal, + }); + return Response.json(ids, { status: 201 }); + }, + }, + ); + expect(result).toEqual({ + scanId: document.scanId, + repositoryId: "target_sha256_example", + findingIds: ids, + findingCount: ids.length, + }); + expect(calls).toBe(1); + expect(await readFile(join(scan, "findings.json"), "utf8")).toBe(source); +}); + +test("dry-run previews the complete upload without HTTP or credentials", async () => { + const { scan, document } = await fixture(); + const result = await publishScanToCustom( + scan, + { findingsUrl: "http://localhost:3000", dryRun: true }, + { + fetch: async () => { + throw new Error("dry-run must not send a request"); + }, + }, + ); + expect(result).toEqual({ + scanId: document.scanId, + repositoryId: "target_sha256_example", + findingIds: document.findings.map((finding) => finding.findingId), + findingCount: document.findings.length, + dryRun: true, + findings: document.findings, + }); +}); + +test("does not retry failed uploads or report incomplete receipts as successful", async () => { + const { scan } = await fixture(); + for (const [receipt, status, message] of [ + [{}, 503, "HTTP 503"], + [{}, 201, "did not acknowledge all"], + [[], 201, "did not acknowledge all"], + [["wrong-finding"], 201, "did not acknowledge all"], + ] as const) { + let calls = 0; + await expect( + publishScanToCustom( + scan, + { findingsUrl: "http://synthetic.test" }, + { + fetch: async () => { + calls++; + return Response.json(receipt, { status }); + }, + }, + ), + ).rejects.toThrow(message); + expect(calls).toBe(1); + } +}); + +test("rejects mismatched or changed sealed artifacts before publication, including dry-run", async () => { + const { scan, source } = await fixture(); + const dependencies = { + fetch: async () => { + throw new Error("must not upload invalid artifacts"); + }, + }; + await expect( + publishScanToCustom( + scan, + { + findingsUrl: "http://synthetic.test", + expectedScanId: "wrong-scan", + }, + dependencies, + ), + ).rejects.toThrow("do not match selected scan"); + await writeFile(join(scan, "findings.json"), source + "\n"); + for (const dryRun of [false, true]) { + await expect( + publishScanToCustom( + scan, + { + findingsUrl: "http://synthetic.test", + dryRun, + }, + dependencies, + ), + ).rejects.toThrow(); + } +}); diff --git a/sdk/typescript/tests-ts/finding-deduplication.test.ts b/sdk/typescript/tests-ts/finding-deduplication.test.ts index 0c6f0cc3d..78ea87488 100644 --- a/sdk/typescript/tests-ts/finding-deduplication.test.ts +++ b/sdk/typescript/tests-ts/finding-deduplication.test.ts @@ -16,7 +16,7 @@ import { type ScreeningResult, } from "../src/deduplication/deduplication-reviewer.js"; import { CodexSecurityError } from "../src/errors.js"; -import { FindingsClient } from "../src/deduplication/findings-client.js"; +import { FindingsClient } from "../src/findings-client.js"; import { deduplicateScanInternal } from "../src/deduplication/scan.js"; import { PLUGIN_ROOT } from "./plugin-root.js"; import type { JsonObject } from "../src/config.js"; @@ -361,6 +361,14 @@ test("accepts complete canonical and merged reviews and rejects invalid assignme { ...result, canonicalFindingId: null }, { ...result, mergedFinding: undefined }, { ...result, mergedFinding: null }, + { ...result, mergedFinding: {} }, + { + ...result, + mergedFinding: { + ...result.mergedFinding, + findingId: findings[1]!.findingId, + }, + }, { ...result, canonicalFindingId: "outside" }, { ...result, @@ -493,12 +501,11 @@ test("lookup failures and cancellation never produce a completed uniqueness resu for (const status of [404, 502]) { const client = new FindingsClient( "http://synthetic.test", - { allRepositories: true }, undefined, async () => new Response("", { status }), ); await expect( - client.potentialDuplicates(entry(1).findingId), + client.potentialDuplicates(entry(1).findingId, { allRepositories: true }), ).rejects.toThrow(`HTTP ${status}`); } const controller = new AbortController(); @@ -517,3 +524,83 @@ test("lookup failures and cancellation never produce a completed uniqueness resu }), ).rejects.toBe("synthetic cancellation"); }); + +test("writes accepted groups only after all reviews and fails when write-back fails", async () => { + const directory = await mkdtemp(join(tmpdir(), "dedupe-writeback-")); + try { + await cp(join(PLUGIN_ROOT, "examples/completed-scan"), directory, { + recursive: true, + }); + if (process.platform !== "win32") await chmod(directory, 0o700); + const findings = [document.findings[0]!, entry(2), entry(3)]; + const ids = findings.map((finding) => finding.findingId); + for (const status of [201, 409]) { + const phases: string[] = []; + const controller = new AbortController(); + const result = deduplicateScanInternal( + "scan_example_001", + { + findingsUrl: "http://synthetic.test/api/", + signal: controller.signal, + }, + { + runWorkbench: async () => ({ + scan: { + scanId: "scan_example_001", + scanDir: directory, + progress: { status: "complete" }, + }, + }), + fetch: async (url, options) => { + expect(options.signal).toBe(controller.signal); + if (options.method === "POST") { + phases.push("store"); + expect(String(url)).toBe( + "http://synthetic.test/api/v1/dedupe-groups", + ); + expect(JSON.parse(options.body as string)).toEqual({ + groups: [[...ids].sort()], + }); + return Response.json([], { status }); + } + phases.push("lookup"); + return Response.json({ + finding: findings[0], + potentialDuplicates: findings.slice(1), + }); + }, + reviewer: { + async screen(values) { + phases.push("screen"); + return screening( + values, + new Set( + values + .slice(1) + .map((value) => pairKey([ids[0]!, value.findingId])), + ), + ); + }, + async reviewPair(values) { + phases.push("pair"); + return same(values); + }, + }, + }, + ); + if (status === 201) { + expect((await result).duplicateGroups).toEqual([[...ids].sort()]); + } else { + await expect(result).rejects.toThrow( + "POST /v1/dedupe-groups failed (HTTP 409)", + ); + } + expect(phases).toEqual(["lookup", "screen", "pair", "pair", "store"]); + } + expect( + JSON.parse(await readFile(join(directory, "findings.json"), "utf8")), + ).toEqual(document); + } finally { + await rm(directory, { recursive: true, force: true }); + } +}); diff --git a/sdk/typescript/tests-ts/finding-workflow.test.ts b/sdk/typescript/tests-ts/finding-workflow.test.ts new file mode 100644 index 000000000..1ab5967f8 --- /dev/null +++ b/sdk/typescript/tests-ts/finding-workflow.test.ts @@ -0,0 +1,1169 @@ +import { execFileSync, spawn } from "node:child_process"; +import { createHash } from "node:crypto"; +import { once } from "node:events"; +import { + chmod, + cp, + mkdir, + mkdtemp, + readFile, + rm, + writeFile, +} from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, expect, test } from "bun:test"; +import type { JsonObject } from "../src/config.js"; +import { + FindingWorkflow, + workflowDestination, + type WorkflowState, +} from "../src/finding-workflow.js"; +import { publishScanToCustomInternal } from "../src/custom-publish.js"; +import { deduplicateScanInternal } from "../src/deduplication/scan.js"; +import { + resolvePluginPython, + runCodexCommand, + runWorkbench, +} from "../src/runtime.js"; +import type { Finding, FindingsDocument, ScanManifest } from "../src/models.js"; +import type { CodexReview } from "../src/deduplication/codex-review.js"; +import { + CheckpointedReviewRunner, + reviewSettingsDigest, +} from "../src/deduplication/checkpointed-review.js"; +import { + CodexDeduplicationReviewer, + type DuplicateDecision, +} from "../src/deduplication/deduplication-reviewer.js"; +import { PLUGIN_ROOT } from "./plugin-root.js"; + +const directories: string[] = []; +afterEach(async () => { + await Promise.all( + directories + .splice(0) + .map((directory) => rm(directory, { recursive: true, force: true })), + ); +}); + +async function fixture() { + const root = await mkdtemp(join(tmpdir(), "findings-workflow-")); + directories.push(root); + const scanDir = join(root, "scan"); + const repository = join(root, "repository"); + await mkdir(repository); + await cp(join(PLUGIN_ROOT, "examples/completed-scan"), scanDir, { + recursive: true, + }); + if (process.platform !== "win32") await chmod(scanDir, 0o700); + const environment = { + PATH: process.env["PATH"], + SystemRoot: process.env["SystemRoot"], + TEMP: process.env["TEMP"], + TMP: process.env["TMP"], + CODEX_HOME: join(root, "codex"), + CODEX_SECURITY_STATE_DIR: join(root, "state"), + }; + const document = JSON.parse( + await readFile(join(scanDir, "findings.json"), "utf8"), + ) as FindingsDocument; + const workbenchOptions = { + environment, + pluginRoot: PLUGIN_ROOT, + python: await resolvePluginPython({ environment }), + }; + const history = async (args: readonly string[], input?: string) => + args[0] === "get-scan" + ? { + scan: { + scanId: document.scanId, + scanDir, + targetPath: repository, + progress: { status: "complete" }, + }, + } + : await runWorkbench(workbenchOptions, args, input); + return { + root, + repository, + scanDir, + environment, + document, + history, + workbenchOptions, + }; +} + +async function restoreLegacyWorkflow( + environment: NodeJS.ProcessEnv, + state: object, + reviews: object[], +) { + const probe = await runCodexCommand( + { command: await resolvePluginPython({ environment }) }, + [ + "-I", + "-B", + "-c", + `import json, sqlite3, sys +sys.path.insert(0, sys.argv[1]) +from workbench_schema import MIGRATIONS, apply_migrations, sql_statements +db = sqlite3.connect(sys.argv[2]) +db.row_factory = sqlite3.Row +db.execute("PRAGMA foreign_keys = ON") +payload = json.load(sys.stdin) +state = payload["state"] +timestamp = "2026-08-01T00:00:00Z" +with db: + db.execute("DROP TABLE finding_workflow_reviews") + db.execute("DROP TABLE finding_workflows") + db.execute("DELETE FROM schema_migrations WHERE version >= 38") + for version, _, sql in MIGRATIONS: + if version in (36, 37): + for statement in sql_statements(sql): + db.execute(statement) + db.execute("INSERT INTO finding_workflows VALUES (?, ?, ?, ?)", + (state["id"], json.dumps(state), timestamp, timestamp)) + for review in payload["reviews"]: + db.execute("INSERT INTO finding_workflow_reviews VALUES (?, ?, ?, ?, ?)", + (state["id"], review["key"], json.dumps(review["binding"]), json.dumps(review["result"]), timestamp)) +before = list(db.iterdump()) +try: + apply_migrations(db, (*MIGRATIONS, (999, "synthetic failure", "INSERT INTO synthetic_missing_table VALUES (1);")), lambda: timestamp, lambda _: None) +except sqlite3.OperationalError: + pass +else: + raise AssertionError("Migration should fail") +assert not db.in_transaction +assert list(db.iterdump()) == before, "Failed migration must preserve workflow and review rows together" +db.close()`, + join(PLUGIN_ROOT, "scripts"), + join(environment["CODEX_SECURITY_STATE_DIR"]!, "workbench.sqlite3"), + ], + environment, + JSON.stringify({ state, reviews }), + ); + expect(probe.exitCode, probe.stderr).toBe(0); +} + +test("migrates workflow columns atomically without losing receipts, pending writes, or resume state", async () => { + const { environment, repository, scanDir, workbenchOptions } = + await fixture(); + const completed = { + id: "migrated-completed", + repositoryPath: repository, + scanRequestDigest: "scan-request-hash", + scanId: "synthetic-scan", + scanDir, + artifactDigest: "artifact-hash", + destination: "http://synthetic.test/", + scope: { repositoryId: "synthetic-repository" }, + stages: { + scan: { status: "completed", result: null }, + publish: { status: "completed", result: { findingIds: [] } }, + dedupe: { status: "completed", result: { duplicateGroups: [] } }, + }, + } as const; + const unfinished = { + id: "migrated-unfinished", + scope: { allRepositories: true }, + stages: { + scan: { status: "failed", error: "Synthetic scan interruption" }, + publish: { status: "running", error: "Synthetic earlier failure" }, + dedupe: { + status: "failed", + error: "Synthetic lost acknowledgement", + result: { duplicateGroups: [["a", "b"]] }, + pendingWrite: { groups: [["a", "b"]] }, + }, + }, + } satisfies WorkflowState; + const pending = { + id: "migrated-pending", + stages: { + scan: { status: "pending" }, + publish: { status: "pending" }, + dedupe: { status: "pending" }, + }, + } as const; + await mkdir(environment.CODEX_SECURITY_STATE_DIR); + const probe = await runCodexCommand( + { command: workbenchOptions.python }, + [ + "-I", + "-B", + "-c", + `import json, sqlite3, sys +sys.path.insert(0, sys.argv[1]) +from workbench_schema import MIGRATIONS, apply_migrations +db = sqlite3.connect(sys.argv[2]) +db.row_factory = sqlite3.Row +db.execute("PRAGMA foreign_keys = ON") +timestamp = "2026-08-01T00:00:00Z" +apply = lambda migrations: apply_migrations(db, migrations, lambda: timestamp, lambda _: None) +apply(tuple(m for m in MIGRATIONS if m[0] <= 36)) +states = json.load(sys.stdin) +with db: + for state in states: + db.execute("INSERT INTO finding_workflows VALUES (?, ?, ?, ?)", + (state["id"], json.dumps(state), timestamp, "2026-08-02T00:00:00Z")) + db.execute("CREATE TABLE synthetic_workflow_references (workflow_id TEXT REFERENCES finding_workflows(id) ON DELETE CASCADE)") + db.execute("INSERT INTO synthetic_workflow_references VALUES (?)", (states[0]["id"],)) +before = list(db.iterdump()) +try: + apply((*MIGRATIONS, (999, "synthetic failure", "INSERT INTO synthetic_missing_table VALUES (1);"))) +except sqlite3.OperationalError: + pass +else: + raise AssertionError("Migration should fail") +assert not db.in_transaction +assert list(db.iterdump()) == before, "Migration must roll back schema and data together" +apply(MIGRATIONS) +after = list(db.iterdump()) +apply(MIGRATIONS) +assert list(db.iterdump()) == after +assert "state_json" not in {row["name"] for row in db.execute("PRAGMA table_info(finding_workflows)")} +assert db.execute("SELECT COUNT(*) FROM synthetic_workflow_references").fetchone()[0] == 1 +assert list(db.execute("PRAGMA foreign_key_check")) == [] +assert db.execute("PRAGMA integrity_check").fetchone()[0] == "ok" +print(json.dumps([dict(row) for row in db.execute("SELECT * FROM finding_workflows ORDER BY id")]))`, + join(PLUGIN_ROOT, "scripts"), + join(environment.CODEX_SECURITY_STATE_DIR, "workbench.sqlite3"), + ], + environment, + JSON.stringify([completed, unfinished, pending]), + ); + expect(probe.exitCode, probe.stderr).toBe(0); + const rows = JSON.parse(probe.stdout); + expect(rows[0]).toMatchObject({ + id: completed.id, + repository_path: repository, + scan_request_digest: completed.scanRequestDigest, + scan_id: completed.scanId, + scan_dir: scanDir, + artifact_digest: completed.artifactDigest, + destination: completed.destination, + scope_repository_id: completed.scope.repositoryId, + scope_all_repositories: null, + scan_status: "completed", + scan_error: null, + publish_status: "completed", + publish_error: null, + dedupe_status: "completed", + dedupe_error: null, + created_at: "2026-08-01T00:00:00Z", + updated_at: "2026-08-02T00:00:00Z", + }); + expect(JSON.parse(rows[0].results_json)).toEqual({ + scan: null, + publish: completed.stages.publish.result, + dedupe: completed.stages.dedupe.result, + }); + for (const state of [completed, unfinished, pending]) { + expect(await new FindingWorkflow(state.id, environment).get()).toEqual( + state, + ); + } + const workflow = new FindingWorkflow(completed.id, environment); + for (const stage of ["scan", "publish", "dedupe"] as const) { + expect( + await workflow.run(stage, async () => { + throw new Error("Completed work must not run again after migration"); + }), + ).toEqual(completed.stages[stage].result); + } + const resumed = new FindingWorkflow(unfinished.id, environment); + await resumed.run("scan", async () => ({ scanId: "resumed-scan" })); + expect((await resumed.get())?.stages.scan).toEqual({ + status: "completed", + result: { scanId: "resumed-scan" }, + }); + expect((await resumed.get())?.stages.dedupe).toEqual( + unfinished.stages.dedupe, + ); +}); + +test("scan registration commits its workflow identity atomically and rolls back failed registration", async () => { + const { root, repository, environment, workbenchOptions } = await fixture(); + const workflow = new FindingWorkflow("registered-workflow", environment); + await workflow.bind({ repositoryPath: repository }); + await workflow.begin("scan"); + const register = async (workflowId: string, suffix: string) => { + const directory = join(root, suffix); + await mkdir(directory, { mode: 0o700 }); + return await runWorkbench( + workbenchOptions, + [ + "register-cli-scan", + "--repository", + repository, + "--scan-dir", + directory, + "--registration-json-stdin", + ], + JSON.stringify({ + workflowId, + recipe: { + repository, + mode: "standard", + target: { kind: "repository", paths: [] }, + config: {}, + }, + }), + ); + }; + const registered = await register("registered-workflow", "registered-scan"); + expect( + await new FindingWorkflow("registered-workflow", environment).get(), + ).toMatchObject({ + scanId: registered["scanId"], + scanDir: registered["scanDir"], + stages: { scan: { status: "running" } }, + }); + await expect( + register("unknown-workflow", "rolled-back-scan"), + ).rejects.toThrow("must be started"); + const history = await runWorkbench(workbenchOptions, [ + "list-scans", + "--repository", + repository, + ]); + expect(history["scans"]).toHaveLength(1); + const retried = await register("registered-workflow", "retried-scan"); + expect(retried["scanId"]).not.toBe(registered["scanId"]); + expect((await workflow.get())?.scanId).toBe(retried["scanId"] as string); +}); + +test.each(["running", "completed"])( + "survives termination with a %s stage and resumes in a fresh process", + async (status) => { + const { environment } = await fixture(); + const source = `import { FindingWorkflow } from ${JSON.stringify(new URL("../src/finding-workflow.ts", import.meta.url).href)}; +const workflow = new FindingWorkflow("interrupted", process.env); +const timer = setInterval(() => {}, 1000); +await workflow.run("publish", async () => { + if (${JSON.stringify(status)} === "running") { + process.stdout.write("ready\\n"); + await new Promise(() => {}); + } + return { findingIds: [] }; +}); +process.stdout.write("ready\\n");`; + const child = spawn(process.execPath, ["--eval", source], { + env: environment, + stdio: ["ignore", "pipe", "pipe"], + windowsHide: true, + }); + const closed = once(child, "close"); + let errors = ""; + child.stderr.on("data", (chunk) => { + errors += String(chunk); + }); + try { + const ready = await Promise.race([ + once(child.stdout, "data").then(([data]) => String(data)), + closed.then(() => { + throw new Error( + errors || "Workflow child exited before the checkpoint", + ); + }), + ]); + expect(ready).toContain("ready"); + child.kill(); + await closed; + const workflow = new FindingWorkflow("interrupted", environment); + expect((await workflow.get())?.stages.publish.status).toBe(status); + let attempts = 0; + expect( + await workflow.run("publish", async () => { + attempts++; + return { findingIds: [] }; + }), + ).toEqual({ findingIds: [] }); + expect(attempts).toBe(status === "running" ? 1 : 0); + expect( + (await new FindingWorkflow("interrupted", environment).get())?.stages + .publish, + ).toEqual({ status: "completed", result: { findingIds: [] } }); + } finally { + if (child.exitCode === null && child.signalCode === null) child.kill(); + await closed; + } + }, +); + +test("reuses publication after dedupe failure and persists a successful empty duplicate result", async () => { + const { scanDir, environment, document, history } = await fixture(); + const original = await readFile(join(scanDir, "findings.json"), "utf8"); + const workflowId = "resume-example"; + const options = { workflowId, findingsUrl: "http://synthetic.test/service" }; + let publications = 0; + let lookups = 0; + let unavailable = true; + const request = async (url: URL) => { + if (url.pathname.endsWith("/bulk/findings")) { + publications++; + return Response.json( + document.findings.map((finding) => finding.findingId), + ); + } + lookups++; + if (unavailable) throw new Error("Synthetic lookup failure"); + return Response.json({ + finding: document.findings[0], + potentialDuplicates: [], + }); + }; + const receipt = await publishScanToCustomInternal(scanDir, options, { + environment, + fetch: request, + }); + await expect( + deduplicateScanInternal(document.scanId, options, { + environment, + fetch: request, + runWorkbench: history, + }), + ).rejects.toThrow("Synthetic lookup failure"); + const workflow = new FindingWorkflow(workflowId, environment); + expect((await workflow.get())?.stages).toMatchObject({ + scan: { status: "completed" }, + publish: { status: "completed", result: receipt }, + dedupe: { status: "failed", error: "Synthetic lookup failure" }, + }); + expect(publications).toBe(1); + unavailable = false; + const result = await deduplicateScanInternal(document.scanId, options, { + environment, + fetch: request, + runWorkbench: history, + }); + expect(result).toEqual({ + scanId: document.scanId, + uniqueFindingIds: document.findings.map((finding) => finding.findingId), + duplicateGroups: [], + deduplicationStatus: "completed", + }); + expect((await workflow.get())?.stages.dedupe).toEqual({ + status: "completed", + result, + }); + expect( + await deduplicateScanInternal(document.scanId, options, { + environment, + runWorkbench: history, + fetch: async () => { + throw new Error("Completed stages must not make HTTP requests"); + }, + }), + ).toEqual(result); + expect(publications).toBe(1); + expect(lookups).toBe(2); + expect(await readFile(join(scanDir, "findings.json"), "utf8")).toBe(original); +}); + +test("an empty scan completes publication and dedupe and remains retrievable", async () => { + const { scanDir, environment, document, history } = await fixture(); + document.findings = []; + const content = JSON.stringify(document); + await writeFile(join(scanDir, "findings.json"), content); + const manifestPath = join(scanDir, "scan-manifest.json"); + const manifest = JSON.parse( + await readFile(manifestPath, "utf8"), + ) as ScanManifest; + manifest.scan.artifacts!.find( + (artifact) => artifact.path === "findings.json", + )!.sha256 = createHash("sha256").update(content).digest("hex"); + await writeFile(manifestPath, JSON.stringify(manifest)); + const options = { + workflowId: "empty-scan", + findingsUrl: "http://synthetic.test", + }; + let requests = 0; + const fetch = async (_url: URL, init: RequestInit) => { + requests++; + expect(JSON.parse(init.body as string).findings).toEqual([]); + return Response.json([]); + }; + const result = await deduplicateScanInternal(document.scanId, options, { + environment, + runWorkbench: history, + fetch, + }); + expect(result).toEqual({ + scanId: document.scanId, + uniqueFindingIds: [], + duplicateGroups: [], + deduplicationStatus: "completed", + }); + expect( + await deduplicateScanInternal(document.scanId, options, { + environment, + runWorkbench: history, + fetch, + }), + ).toEqual(result); + expect(requests).toBe(1); + expect( + (await new FindingWorkflow(options.workflowId, environment).get())?.stages + .publish, + ).toMatchObject({ + status: "completed", + result: { findingIds: [], findingCount: 0 }, + }); +}); + +test("failed publication stays unfinished, dry-run does not advance it, and retry records the receipt", async () => { + const { scanDir, environment, document } = await fixture(); + const options = { + workflowId: "publication-retry", + findingsUrl: "http://synthetic.test", + }; + await expect( + publishScanToCustomInternal(scanDir, options, { + environment, + fetch: async () => new Response("", { status: 503 }), + }), + ).rejects.toThrow("HTTP 503"); + const workflow = new FindingWorkflow(options.workflowId, environment); + const failed = await workflow.get(); + expect(failed?.stages.publish.status).toBe("failed"); + expect(failed?.stages.publish.result).toBeUndefined(); + await publishScanToCustomInternal( + scanDir, + { ...options, dryRun: true }, + { environment }, + ); + expect(await workflow.get()).toEqual(failed); + const result = await publishScanToCustomInternal(scanDir, options, { + environment, + fetch: async () => + Response.json(document.findings.map((finding) => finding.findingId)), + }); + expect((await workflow.get())?.stages.publish).toEqual({ + status: "completed", + result, + }); +}); + +test("separates workflow identities and rejects different scans, destinations, and scopes", async () => { + const { environment } = await fixture(); + const binding = { + scanId: "scan-a", + scanDir: "synthetic-artifacts", + destination: workflowDestination( + "http://synthetic:password@synthetic.test", + ), + scope: { repositoryId: "repository-a" }, + }; + const first = new FindingWorkflow("first", environment); + await first.bind(binding); + expect((await first.get())?.destination).toBe("http://synthetic.test/"); + await first.run("dedupe", async () => ({ duplicateGroups: [] })); + for (const changed of [ + { scanId: "scan-b" }, + { destination: "http://other.test/" }, + { scope: { allRepositories: true as const } }, + ]) { + await expect(first.bind({ ...binding, ...changed })).rejects.toThrow( + "already bound to a different", + ); + const separate = new FindingWorkflow( + `separate-${Object.keys(changed)[0]}`, + environment, + ); + expect( + (await separate.bind({ ...binding, ...changed })).stages.dedupe.status, + ).toBe("pending"); + } + expect((await first.get())?.stages.dedupe).toEqual({ + status: "completed", + result: { duplicateGroups: [] }, + }); +}); + +test("does not write workflow metadata into sealed artifacts", async () => { + const { scanDir, environment } = await fixture(); + await expect( + publishScanToCustomInternal( + scanDir, + { workflowId: "unsafe-location", findingsUrl: "http://synthetic.test" }, + { + environment: { + ...environment, + CODEX_SECURITY_STATE_DIR: join(scanDir, "state"), + }, + fetch: async () => { + throw new Error("Must not publish"); + }, + }, + ), + ).rejects.toThrow("outside the sealed scan artifacts"); +}); + +function merged(findings: readonly Finding[]) { + return { + decision: "SAME" as const, + rationale: "REVIEW_OUTPUT_ONLY: one correction covers the supplied paths.", + canonicalFindingId: findings[0]!.findingId, + mergedFinding: { + ...findings[0]!, + title: "MERGED_OUTPUT_ONLY", + extensions: { originals: findings }, + }, + }; +} +const distinct: DuplicateDecision = { + decision: "DISTINCT", + rationale: "REVIEW_OUTPUT_ONLY: independent corrections are required.", +}; + +test.each(["screen", "pair"])( + "resumes unfinished %s reviews using validated checkpoints and original inputs", + async (interruptAt) => { + const { scanDir, environment, document, history } = await fixture(); + const findings = [ + document.findings[0]!, + ...[1, 2, 3].map((index) => ({ + ...structuredClone(document.findings[0]!), + findingId: `csf_${"f".repeat(23)}${index}`, + title: `Synthetic original ${index}`, + })), + ]; + const options = { + workflowId: `reviews-${interruptAt}`, + findingsUrl: "http://synthetic.test", + }; + const calls: string[] = []; + let interrupted = false; + const reviewRunner = { + async run(review: CodexReview): Promise { + expect(review.prompt).not.toContain("REVIEW_OUTPUT_ONLY"); + expect(review.prompt).not.toContain("MERGED_OUTPUT_ONLY"); + const originals = JSON.parse( + review.prompt.slice(review.prompt.lastIndexOf("\n\n") + 2), + ).findings as Finding[]; + for (const finding of originals) + expect(finding).toEqual( + findings.find( + (original) => original.findingId === finding.findingId, + )!, + ); + const stage = review.model === "gpt-5.6-luna" ? "screen" : "pair"; + calls.push(stage); + if ( + !interrupted && + stage === interruptAt && + (stage !== "pair" || + calls.filter((call) => call === "pair").length === 2) + ) { + interrupted = true; + throw new Error("Synthetic interrupted review"); + } + return review.validate( + stage === "screen" + ? { + decisions: originals.slice(1).map((finding) => ({ + findingIds: [originals[0]!.findingId, finding.findingId], + ...merged([originals[0]!, finding]), + })), + } + : originals.some( + (finding) => finding.findingId === findings[3]!.findingId, + ) + ? distinct + : merged(originals), + ); + }, + }; + const fetch = async (url: URL) => + url.pathname.endsWith("/bulk/findings") + ? Response.json([findings[0]!.findingId]) + : url.pathname.endsWith("/dedupe-groups") + ? Response.json([]) + : Response.json({ + finding: findings[0], + potentialDuplicates: findings.slice(1), + }); + await publishScanToCustomInternal(scanDir, options, { environment, fetch }); + await expect( + deduplicateScanInternal(document.scanId, options, { + environment, + runWorkbench: history, + reviewRunner, + fetch, + }), + ).rejects.toThrow("Synthetic interrupted review"); + const result = await deduplicateScanInternal(document.scanId, options, { + environment, + runWorkbench: history, + reviewRunner, + fetch, + }); + expect(result.duplicateGroups).toEqual([ + findings.slice(0, 3).map((finding) => finding.findingId), + ]); + expect(calls.filter((stage) => stage === "screen")).toHaveLength( + interruptAt === "screen" ? 2 : 1, + ); + expect(calls.filter((stage) => stage === "pair")).toHaveLength( + interruptAt === "pair" ? 4 : 3, + ); + const count = calls.length; + expect( + await deduplicateScanInternal(document.scanId, options, { + environment, + runWorkbench: history, + reviewRunner, + fetch: async () => { + throw new Error("Completed workflow must use its saved result"); + }, + }), + ).toEqual(result); + expect(calls).toHaveLength(count); + }, +); + +test.each([ + "before-post", + "before-write", + "lost-ack", + "lost-completion", + "migrated-lost-ack", +])( + "replays the exact saved group payload after %s without models", + async (failure) => { + const { environment, document, history } = await fixture(); + const originals = [ + document.findings[0]!, + { ...document.findings[0]!, findingId: `csf_${"f".repeat(24)}` }, + ]; + const options = { + workflowId: `write-${failure}`, + findingsUrl: "http://synthetic.test", + }; + const bodies: string[] = []; + const checkpoints: object[] = []; + let modelCalls = 0; + let failed = false; + const reviewRunner = { + async run(review: CodexReview): Promise { + modelCalls++; + return review.validate( + review.model === "gpt-5.6-luna" + ? { + decisions: [ + { + findingIds: originals.map((finding) => finding.findingId), + ...merged(originals), + }, + ], + } + : merged(originals), + ); + }, + }; + const fetch = async (url: URL, init: RequestInit) => { + if (url.pathname.endsWith("/bulk/findings")) + return Response.json([originals[0]!.findingId]); + if (!url.pathname.endsWith("/dedupe-groups")) + return Response.json({ + finding: originals[0], + potentialDuplicates: [originals[1]], + }); + const saved = (await new FindingWorkflow( + options.workflowId, + environment, + ).get())!.stages.dedupe; + expect(saved.status).toBe("running"); + expect(saved.result).toMatchObject({ + duplicateGroups: [originals.map((finding) => finding.findingId)], + }); + expect(saved.pendingWrite).toEqual(JSON.parse(init.body as string)); + bodies.push(init.body as string); + if (!failed && failure !== "lost-completion") { + failed = true; + if (failure === "before-write") + return new Response("", { status: 503 }); + return new Response("incomplete acknowledgement", { status: 201 }); + } + return Response.json([]); + }; + const runWorkbench = async (args: readonly string[], input?: string) => { + const payload = input ? JSON.parse(input) : {}; + if ( + !failed && + failure === "before-post" && + payload.action === "prepare-dedupe" + ) { + await history(args, input); + failed = true; + throw new Error("Synthetic stop before posting"); + } + if ( + !failed && + failure === "lost-completion" && + payload.action === "complete" && + payload.stage === "dedupe" + ) { + failed = true; + throw new Error("Synthetic completion receipt failure"); + } + const result = await history(args, input); + if (payload.action === "save-review") checkpoints.push(payload); + return result; + }; + await expect( + deduplicateScanInternal(document.scanId, options, { + environment, + runWorkbench, + reviewRunner, + fetch, + }), + ).rejects.toThrow(); + if (failure === "migrated-lost-ack") { + await restoreLegacyWorkflow( + environment, + (await new FindingWorkflow(options.workflowId, environment).get())!, + checkpoints, + ); + } + expect( + (await new FindingWorkflow(options.workflowId, environment).get())?.stages + .dedupe.status, + ).toBe("failed"); + const result = await deduplicateScanInternal(document.scanId, options, { + environment, + runWorkbench: history, + reviewRunner, + fetch, + }); + expect(bodies).toHaveLength(failure === "before-post" ? 1 : 2); + expect(new Set(bodies).size).toBe(1); + expect(modelCalls).toBe(2); + expect( + (await new FindingWorkflow(options.workflowId, environment).get())?.stages + .dedupe, + ).toEqual({ status: "completed", result }); + }, +); + +test.each(["current", "legacy", "workflow-columns"])( + "persists DISTINCT and complete SAME checkpoints across %s databases", + async (version) => { + const { environment, repository, document, workbenchOptions } = + await fixture(); + const workflow = new FindingWorkflow("all-decisions", environment); + await workflow.bind({}); + if (version === "workflow-columns") { + const probe = await runCodexCommand( + { command: workbenchOptions.python }, + [ + "-I", + "-B", + "-c", + `import sqlite3, sys +with sqlite3.connect(sys.argv[1]) as db: + db.execute("DROP TABLE finding_workflow_reviews") + db.execute("DELETE FROM schema_migrations WHERE version IN (37, 39)")`, + join(environment.CODEX_SECURITY_STATE_DIR, "workbench.sqlite3"), + ], + environment, + ); + expect(probe.exitCode, probe.stderr).toBe(0); + } + const originals = [ + document.findings[0]!, + { ...document.findings[0]!, findingId: `csf_${"f".repeat(24)}` }, + ]; + let calls = 0; + const checkpoints: Array<{ + key: string; + binding: JsonObject; + result: unknown; + }> = []; + const recordCheckpoint: typeof runWorkbench = async ( + options, + args, + input, + ) => { + const result = await runWorkbench(options, args, input); + const payload = input ? JSON.parse(input) : {}; + if (payload.action === "save-review") checkpoints.push(payload); + return result; + }; + const runner = { + async run(review: CodexReview): Promise { + calls++; + return review.validate( + review.model === "gpt-5.6-luna" + ? { + decisions: [ + { + findingIds: originals.map((finding) => finding.findingId), + ...distinct, + }, + ], + } + : merged(originals), + ); + }, + }; + const makeReviewer = async () => + new CodexDeduplicationReviewer( + new CheckpointedReviewRunner( + new FindingWorkflow(workflow.id, environment, recordCheckpoint), + runner, + await workflow.sourceSnapshot(repository), + { allRepositories: true }, + "synthetic-settings-hash", + ), + ); + const first = await makeReviewer(); + const screening = await first.screen(originals); + const pair = await first.reviewPair(originals); + if (version === "legacy") + await restoreLegacyWorkflow( + environment, + (await workflow.get())!, + checkpoints, + ); + const resumed = await makeReviewer(); + expect(await resumed.screen(originals)).toEqual(screening); + expect(await resumed.reviewPair(originals)).toEqual(pair); + expect(pair).toEqual(merged(originals)); + expect(calls).toBe(2); + expect(checkpoints).toHaveLength(2); + const probe = await runCodexCommand( + { command: workbenchOptions.python }, + [ + "-I", + "-B", + "-c", + `import json, sqlite3, sys +db = sqlite3.connect(sys.argv[1]) +db.row_factory = sqlite3.Row +assert "binding_json" not in {row["name"] for row in db.execute("PRAGMA table_info(finding_workflow_reviews)")} +assert list(db.execute("PRAGMA foreign_key_check")) == [] +assert db.execute("PRAGMA integrity_check").fetchone()[0] == "ok" +print(json.dumps([dict(row) for row in db.execute("SELECT * FROM finding_workflow_reviews ORDER BY review_key")]))`, + join(environment.CODEX_SECURITY_STATE_DIR, "workbench.sqlite3"), + ], + environment, + ); + expect(probe.exitCode, probe.stderr).toBe(0); + const rows = JSON.parse(probe.stdout); + for (const { key, binding, result } of checkpoints) { + const source = binding["source"] as JsonObject; + const row = rows.find( + (row: { review_key: string }) => row.review_key === key, + ); + expect(row).toMatchObject({ + workflow_id: workflow.id, + review_contract_version: binding["version"], + codex_version: binding["codexVersion"], + source_repository_path: source["repository"], + source_revision: source["revision"], + source_refs_digest: source["refsDigest"], + source_content_digest: source["content"], + scope_repository_id: null, + scope_all_repositories: 1, + model: binding["model"], + effort: binding["effort"], + settings_digest: binding["settingsDigest"], + prompt_digest: binding["promptDigest"], + contract_digest: binding["contractDigest"], + }); + if (version === "legacy") + expect(row.created_at).toBe("2026-08-01T00:00:00Z"); + expect(JSON.parse(row.result_json)).toEqual(result); + } + }, +); + +test.each([ + "finding", + "source", + "scope", + "model", + "effort", + "prompt", + "contract", + "configuration", +])("does not reuse a checkpoint for changed %s inputs", async (changed) => { + const { environment, repository, document } = await fixture(); + const workflow = new FindingWorkflow(`changed-${changed}`, environment); + await workflow.bind({}); + let calls = 0; + const runner = { + async run(review: CodexReview): Promise { + calls++; + return review.validate(distinct); + }, + }; + const review: CodexReview = { + model: "gpt-5.6-sol", + effort: "ultra", + prompt: JSON.stringify(document.findings), + schema: { type: "object" }, + validate: () => distinct, + }; + const source = await workflow.sourceSnapshot(repository); + const scope = { repositoryId: "synthetic-repository" }; + const initial = new CheckpointedReviewRunner( + workflow, + runner, + source, + scope, + await reviewSettingsDigest(environment), + ); + await initial.run(review); + await initial.run(review); + expect(calls).toBe(1); + if (changed === "source") + await writeFile(join(repository, "source.txt"), "changed source"); + if (changed === "configuration") { + await mkdir(environment.CODEX_HOME); + await writeFile( + join(environment.CODEX_HOME, "config.toml"), + 'model_reasoning_summary = "none"\n', + ); + } + const next = { ...review }; + if (changed === "finding") + next.prompt = JSON.stringify([ + { ...document.findings[0], title: "Changed original" }, + ]); + if (changed === "model") next.model = "gpt-5.6-luna"; + if (changed === "effort") next.effort = "high"; + if (changed === "prompt") next.prompt += "Changed review contract"; + if (changed === "contract") + next.schema = { type: "object", required: ["decision"] }; + const resumed = new CheckpointedReviewRunner( + new FindingWorkflow(workflow.id, environment), + runner, + await workflow.sourceSnapshot(repository), + changed === "scope" ? { allRepositories: true } : scope, + await reviewSettingsDigest(environment), + ); + await resumed.run(next); + expect(calls).toBe(2); +}); + +test("source snapshots include revisions and ignored content without following directory links", async () => { + const { environment, repository, root } = await fixture(); + const git = (...args: string[]) => + execFileSync("git", args, { cwd: repository, stdio: "pipe" }); + git("init", "--quiet"); + await writeFile(join(repository, ".gitignore"), "ignored.txt\n"); + await writeFile(join(repository, "tracked.txt"), "original"); + await mkdir(join(repository, "tracked-directory")); + await writeFile( + join(repository, "tracked-directory", "source.txt"), + "tracked source", + ); + git("add", "."); + git( + "-c", + "user.name=Example", + "-c", + "user.email=example@example.test", + "commit", + "--quiet", + "-m", + "Synthetic source", + ); + const workflow = new FindingWorkflow("source-snapshots", environment); + const first = await workflow.sourceSnapshot(repository); + await writeFile(join(repository, "ignored.txt"), "ignored source"); + const second = await workflow.sourceSnapshot(repository); + expect(second["revision"]).toBe(first["revision"]); + expect(second["content"]).not.toBe(first["content"]); + git( + "-c", + "user.name=Example", + "-c", + "user.email=example@example.test", + "commit", + "--quiet", + "--allow-empty", + "-m", + "Next synthetic revision", + ); + expect((await workflow.sourceSnapshot(repository))["revision"]).not.toBe( + first["revision"], + ); + const beforeRef = await workflow.sourceSnapshot(repository); + git("branch", "synthetic-source-reference"); + const afterRef = await workflow.sourceSnapshot(repository); + expect(afterRef["revision"]).toBe(beforeRef["revision"]); + expect(afterRef["refsDigest"]).not.toBe(beforeRef["refsDigest"]); + const outside = join(root, "outside"); + await mkdir(outside); + await writeFile(join(outside, "private.txt"), "synthetic outside content"); + const { symlink } = await import("node:fs/promises"); + await symlink( + outside, + join(repository, "linked"), + process.platform === "win32" ? "junction" : "dir", + ); + const linked = await workflow.sourceSnapshot(repository); + await writeFile(join(outside, "private.txt"), "changed outside content"); + expect(await workflow.sourceSnapshot(repository)).toEqual(linked); + await rm(join(repository, "tracked-directory"), { recursive: true }); + await writeFile(join(outside, "source.txt"), "synthetic outside source"); + await symlink( + outside, + join(repository, "tracked-directory"), + process.platform === "win32" ? "junction" : "dir", + ); + const replaced = await workflow.sourceSnapshot(repository); + await writeFile(join(outside, "source.txt"), "changed outside source"); + expect(await workflow.sourceSnapshot(repository)).toEqual(replaced); +}); + +test("does not checkpoint a review when source changes during its execution", async () => { + const { environment, repository, document } = await fixture(); + const workflow = new FindingWorkflow("source-drift", environment); + await workflow.bind({}); + const source = await workflow.sourceSnapshot(repository); + const runner = new CheckpointedReviewRunner( + workflow, + { + async run(review: CodexReview): Promise { + await writeFile( + join(repository, "changed.txt"), + "changed during review", + ); + return review.validate(distinct); + }, + }, + source, + { allRepositories: true }, + ); + const review: CodexReview = { + model: "gpt-5.6-sol", + effort: "ultra", + prompt: JSON.stringify(document.findings), + schema: {}, + validate: () => distinct, + }; + await expect(runner.run(review)).rejects.toThrow( + "Source changed during deduplication", + ); + await rm(join(repository, "changed.txt")); + let calls = 0; + await new CheckpointedReviewRunner( + workflow, + { + async run(review: CodexReview): Promise { + calls++; + return review.validate(distinct); + }, + }, + source, + { allRepositories: true }, + ).run(review); + expect(calls).toBe(1); +}); diff --git a/sdk/typescript/tests-ts/findings-dashboard.test.ts b/sdk/typescript/tests-ts/findings-dashboard.test.ts new file mode 100644 index 000000000..d3c917ff1 --- /dev/null +++ b/sdk/typescript/tests-ts/findings-dashboard.test.ts @@ -0,0 +1,122 @@ +import { expect, test } from "bun:test"; +import { pollDashboard } from "../dashboard/polling.js"; +import type { DashboardSnapshot } from "../src/server/dashboard-types.js"; + +const snapshot: DashboardSnapshot = { + overview: { findings: 0, groups: 0 }, + repositories: [], + items: [], + total: 0, + limit: 50, + offset: 0, + nextOffset: null, + detail: null, +}; + +function clock() { + let tick = () => {}; + let interval = 0; + let cleared = false; + return { + timers: { + setInterval(callback: () => void, milliseconds: number) { + tick = callback; + interval = milliseconds; + return 1 as unknown as ReturnType; + }, + clearInterval() { + cleared = true; + }, + } as Pick, + tick: () => tick(), + interval: () => interval, + cleared: () => cleared, + }; +} + +test("dashboard polls immediately and every five seconds without overlapping requests", async () => { + const timer = clock(); + const calls: AbortSignal[] = []; + const received: DashboardSnapshot[] = []; + let resolve!: (value: DashboardSnapshot) => void; + const stop = pollDashboard( + (signal) => { + calls.push(signal); + return new Promise((done) => { + resolve = done; + }); + }, + (value) => received.push(value), + () => { + throw new Error("Unexpected poll failure"); + }, + timer.timers, + ); + try { + expect(calls).toHaveLength(1); + expect(timer.interval()).toBe(5_000); + timer.tick(); + expect(calls).toHaveLength(1); + resolve(snapshot); + await Promise.resolve(); + expect(received).toEqual([snapshot]); + timer.tick(); + expect(calls).toHaveLength(2); + } finally { + stop(); + } + expect(timer.cleared()).toBe(true); + expect(calls.every((signal) => signal.aborted)).toBe(true); + resolve(snapshot); + await Promise.resolve(); + expect(received).toHaveLength(1); +}); + +test("dashboard reports refresh errors and retries on the next polling tick", async () => { + const timer = clock(); + const received: DashboardSnapshot[] = []; + const errors: unknown[] = []; + let request = 0; + const stop = pollDashboard( + async () => { + if (++request === 2) throw new Error("Synthetic connection failure"); + return { ...snapshot, total: request }; + }, + (value) => received.push(value), + (error) => errors.push(error), + timer.timers, + ); + try { + await Promise.resolve(); + timer.tick(); + await Promise.resolve(); + expect(errors).toHaveLength(1); + expect(received.map((value) => value.total)).toEqual([1]); + timer.tick(); + await Promise.resolve(); + expect(received.map((value) => value.total)).toEqual([1, 3]); + } finally { + stop(); + } +}); + +test("disposing an old selection suppresses late errors as well as late responses", async () => { + const timer = clock(); + const errors: unknown[] = []; + let reject!: (reason: Error) => void; + const stop = pollDashboard( + () => + new Promise((_resolve, fail) => { + reject = fail; + }), + () => { + throw new Error("Unexpected data"); + }, + (error) => errors.push(error), + timer.timers, + ); + stop(); + reject(new Error("Old request failed")); + await Promise.resolve(); + expect(errors).toEqual([]); +}); diff --git a/sdk/typescript/tests-ts/findings-server.test.ts b/sdk/typescript/tests-ts/findings-server.test.ts index bee3e2e26..c53cf36b0 100644 --- a/sdk/typescript/tests-ts/findings-server.test.ts +++ b/sdk/typescript/tests-ts/findings-server.test.ts @@ -4,12 +4,14 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, expect, spyOn, test } from "bun:test"; import type { Finding, FindingsDocument } from "../src/models.js"; +import type { FindingDedupeGroup } from "../src/finding-dedupe-groups.js"; import { resolvePluginPython, runCodexCommand } from "../src/runtime.js"; import type { FindingEmbedder } from "../src/server/embeddings.js"; import { FindingsError } from "../src/server/errors.js"; import { startFindingsServer } from "../src/server/server.js"; import { SqliteFindingsStore } from "../src/server/sqlite-store.js"; import type { EmbeddedFinding, FindingsPage } from "../src/server/storage.js"; +import type { DashboardSnapshot } from "../src/server/dashboard-types.js"; import { PLUGIN_ROOT } from "./plugin-root.js"; const servers: Server[] = []; @@ -105,6 +107,190 @@ function insert( }); } +function storeGroups(base: string, groups: unknown) { + return fetch(`${base}/v1/dedupe-groups`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ groups }), + }); +} + +async function dashboard( + base: string, + parameters: Record = {}, +) { + const response = await fetch( + `${base}/v1/dashboard?${new URLSearchParams(parameters)}`, + ); + expect(response.status).toBe(200); + expect(response.headers.get("cache-control")).toBe("no-store"); + return (await response.json()) as DashboardSnapshot; +} + +test("dashboard serves only findings and groups, and never calls an embedding provider", async () => { + const { store } = await fixture(); + const base = await start(store, { + async embed() { + throw new Error("Read-only dashboard called embeddings"); + }, + }); + const redirect = await fetch(`${base}/dashboard`, { redirect: "manual" }); + expect(redirect.status).toBe(308); + for (const prefix of ["", "/service"]) { + expect( + new URL(redirect.headers.get("location")!, `${base}${prefix}/dashboard`) + .pathname, + ).toBe(`${prefix}/dashboard/`); + } + for (const view of ["findings", "groups"]) { + const result = await dashboard(base, { view }); + expect(result).toMatchObject({ + items: [], + total: 0, + nextOffset: null, + detail: null, + overview: { findings: 0, groups: 0 }, + }); + } + for (const parameters of [ + "view=unknown", + "view=scans", + "view=workflows", + "sort=unknown", + "offset=-1", + "limit=0", + ]) { + expect((await fetch(`${base}/v1/dashboard?${parameters}`)).status).toBe( + 400, + ); + } + expect( + (await fetch(`${base}/v1/dashboard`, { method: "POST", body: "{}" })) + .status, + ).toBe(404); + expect((await fetch(`${base}/dashboard/not-a-bundled-asset`)).status).toBe( + 404, + ); +}); + +test("dashboard browses imported findings and overlapping groups without local runs", async () => { + const { store, environment } = await fixture(); + const base = await start(store); + const first = finding(1), + second = finding(2), + third = finding(3); + first.title = "Évaluation synthétique"; + await store.insert( + [{ ...embedded(1), finding: first }, embedded(2)], + "repository-a", + ); + await store.insert([embedded(3)], "repository-b"); + const groups = await store.storeDedupeGroups([ + [first.findingId, second.findingId], + [second.findingId, third.findingId], + ]); + const before = await database( + environment, + "print(json.dumps(list(db.iterdump())))", + ); + expect( + await database( + environment, + `from workbench_dashboard import dashboard +allowed = {'findings', 'finding_repositories', 'finding_dedupe_groups', 'finding_dedupe_group_members'} +def authorize(action, table, column, database, source): + if action == sqlite3.SQLITE_READ and table not in allowed: + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK +db.set_authorizer(authorize) +queries = json.load(sys.stdin) +print(json.dumps([dashboard(db, query)['total'] for query in queries]))`, + [ + { + view: "findings", + limit: 50, + offset: 0, + sort: "activity", + id: first.findingId, + }, + { + view: "groups", + limit: 50, + offset: 0, + sort: "newest", + id: groups[0]!.groupId, + }, + ], + ), + ).toEqual([3, 2]); + + const page = await dashboard(base, { + limit: "1", + repository: "repository-a", + id: first.findingId, + }); + expect(page.total).toBe(2); + expect(page.repositories).toEqual([ + { id: "repository-a", label: "repository-a" }, + { id: "repository-b", label: "repository-b" }, + ]); + expect(page.nextOffset).toBe(1); + expect(page.overview).toEqual({ + findings: 3, + groups: 2, + }); + expect(page.detail).toMatchObject({ + finding: first, + groups: [groups[0]], + }); + const next = await dashboard(base, { + view: "findings", + limit: "1", + offset: "1", + repository: "repository-a", + }); + expect(next.items[0]!.id).not.toBe(page.items[0]!.id); + expect(next.nextOffset).toBeNull(); + expect( + (await dashboard(base, { view: "findings", query: first.title })).items.map( + (item) => item.id, + ), + ).toEqual([first.findingId]); + for (const query of ["évaluation", "SYNTHÉTIQUE"]) { + expect( + (await dashboard(base, { view: "findings", query })).items.map( + (item) => item.id, + ), + ).toEqual([first.findingId]); + } + expect( + ( + await dashboard(base, { view: "groups", repository: "repository-b" }) + ).items.map((item) => item.id), + ).toEqual([groups[1]!.groupId]); + const group = await dashboard(base, { + view: "groups", + id: groups[0]!.groupId, + }); + expect(group.detail!.group).toEqual(groups[0]!); + expect(group.items).toHaveLength(2); + expect( + (await dashboard(base, { view: "findings", id: "not-stored" })).detail, + ).toBeNull(); + expect( + await database(environment, "print(json.dumps(list(db.iterdump())))"), + ).toEqual(before); +}); + +async function getGroups( + base: string, + findingId: string, +): Promise { + const response = await fetch(`${base}/v1/finding/${findingId}/dedupe-groups`); + expect(response.status).toBe(200); + return (await response.json()) as FindingDedupeGroup[]; +} + async function database( environment: NodeJS.ProcessEnv, script: string, @@ -190,6 +376,116 @@ test("bulk insert preserves complete findings and embeddings without creating sc ); }); +test("persists overlapping dedupe groups idempotently without changing findings or embeddings", async () => { + const { store, environment } = await fixture(); + const base = await start(store); + const entries = [embedded(1), embedded(2), embedded(3)]; + await store.insert(entries); + const [a, b, c] = entries.map((entry) => entry.finding.findingId) as [ + string, + string, + string, + ]; + const groups = [ + [a, b], + [b, c], + [c, a], + ]; + const response = await storeGroups(base, groups); + expect(response.status).toBe(201); + const stored = (await response.json()) as FindingDedupeGroup[]; + expect(stored.map((group) => group.findingIds)).toEqual( + groups.map((group) => [...group].sort()), + ); + expect(new Set(stored.map((group) => group.groupId)).size).toBe(3); + for (const id of [a, b, c]) { + expect( + (await getGroups(base, id)).map((group) => group.groupId).sort(), + ).toEqual( + stored + .filter((group) => group.findingIds.includes(id)) + .map((group) => group.groupId) + .sort(), + ); + } + const retried = await storeGroups( + base, + groups.map((group) => [...group].reverse()), + ); + expect(await retried.json()).toEqual(stored); + const reopened = new SqliteFindingsStore(environment); + await reopened.initialize(); + expect(await reopened.listDedupeGroups(b)).toEqual(await getGroups(base, b)); + expect((await reopened.list({ limit: 50, offset: 0 })).findings).toEqual( + entries.map((entry) => entry.finding), + ); + expect( + await database( + environment, + `print(json.dumps({ + "memberships": db.execute("SELECT COUNT(*) FROM finding_dedupe_group_members").fetchone()[0], + "embeddings": [list(row) for row in db.execute("SELECT finding_id, model, vector_json FROM finding_embeddings ORDER BY finding_id")] +}))`, + ), + ).toEqual({ + memberships: 6, + embeddings: entries.map((entry) => [ + entry.finding.findingId, + "synthetic", + "[1, 0]", + ]), + }); + expect(await getGroups(base, "missing-finding")).toEqual([]); +}); + +test("rolls back the entire dedupe batch if a finding is missing and rejects invalid groups", async () => { + const { store, environment } = await fixture(); + const base = await start(store, { + embed: async () => { + throw new Error("Grouping must not embed"); + }, + }); + await store.insert([embedded(1), embedded(2), embedded(3)]); + const [a, b, c] = [1, 2, 3].map((index) => finding(index).findingId) as [ + string, + string, + string, + ]; + const original = (await ( + await storeGroups(base, [[a, b]]) + ).json()) as FindingDedupeGroup[]; + const response = await storeGroups(base, [ + [b, c], + [a, "missing-finding"], + ]); + expect(response.status).toBe(409); + expect(await response.json()).toMatchObject({ error: "finding_conflict" }); + expect(await getGroups(base, c)).toEqual([]); + expect(await getGroups(base, a)).toEqual(original); + expect( + await database( + environment, + `print(json.dumps({ + "groups": db.execute("SELECT COUNT(*) FROM finding_dedupe_groups").fetchone()[0], + "memberships": db.execute("SELECT COUNT(*) FROM finding_dedupe_group_members").fetchone()[0] +}))`, + ), + ).toEqual({ groups: 1, memberships: 2 }); + for (const groups of [ + null, + {}, + [a, b], + [[]], + [[a]], + [[a, a]], + [[a, 1]], + [[a, ""]], + ]) { + expect((await storeGroups(base, groups)).status).toBe(400); + } + expect(await (await storeGroups(base, [])).json()).toEqual([]); +}); + test("lists stable pages of 50 by default and supports limit and offset", async () => { const { store } = await fixture(); const base = await start(store); diff --git a/sdk/typescript/tests-ts/skeleton.test.ts b/sdk/typescript/tests-ts/skeleton.test.ts index 10de13787..d450d9b15 100644 --- a/sdk/typescript/tests-ts/skeleton.test.ts +++ b/sdk/typescript/tests-ts/skeleton.test.ts @@ -320,7 +320,7 @@ describe("TypeScript package skeleton", () => { ); expect(packageJson.scripts.build).toBe( - "node --run clean && tsc -p tsconfig.build.json", + "node --run clean && tsc -p tsconfig.build.json && node scripts/build-dashboard.mjs", ); expect(packageJson.scripts["build:plugin"]).toBe( "node scripts/build-plugin.mjs", diff --git a/sdk/typescript/tsconfig.json b/sdk/typescript/tsconfig.json index 6dc292729..97ce7ad63 100644 --- a/sdk/typescript/tsconfig.json +++ b/sdk/typescript/tsconfig.json @@ -2,6 +2,8 @@ "include": [ "src/**/*.ts", "src/**/*.tsx", + "dashboard/**/*.ts", + "dashboard/**/*.tsx", "tests-ts/**/*.ts", "scripts/smoke-findings-service.ts", "scripts/fixtures/findings-service-sqlite.ts" @@ -16,7 +18,7 @@ "incremental": true, "isolatedModules": true, "jsx": "react-jsx", - "lib": ["esnext"], + "lib": ["esnext", "dom", "dom.iterable"], "module": "esnext", "moduleResolution": "bundler", "noEmit": true,