diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb85c8f..fad1441 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,9 +17,9 @@ jobs: os: [windows-latest, ubuntu-latest] python-version: ["3.11", "3.13"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} cache: pip @@ -39,9 +39,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.13" cache: pip @@ -59,3 +59,27 @@ jobs: - name: Build package run: python -m build + + - name: Verify release versions + run: python scripts/check_release_versions.py + + npm-launcher: + name: npm wrapper-only launcher + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: "24" + cache: npm + cache-dependency-path: js/package-lock.json + + - name: Install, test, and build thin launcher + working-directory: js + run: | + npm ci + npm test + npm run build + npm pack --dry-run diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml index ce54791..4912040 100644 --- a/.github/workflows/pypi-publish.yml +++ b/.github/workflows/pypi-publish.yml @@ -1,22 +1,27 @@ -name: Publish to PyPI +name: Publish packages on: workflow_dispatch: + inputs: + version: + description: "Release version or tag (for example 2.6.0 or v2.6.0)" + required: true + type: string release: types: [published] jobs: publish: - name: Build and publish package + name: Build and publish PyPI core runs-on: ubuntu-latest environment: pypi permissions: contents: read id-token: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.13" cache: pip @@ -29,8 +34,51 @@ jobs: - name: Build distributions run: python -m build + - name: Verify synchronized release version + env: + RELEASE_VERSION: ${{ github.event.release.tag_name || inputs.version }} + run: python scripts/check_release_versions.py --tag "$RELEASE_VERSION" + - name: Check distributions run: python -m twine check dist/* - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 + + publish-npm: + name: Publish npm launcher after PyPI + needs: publish + runs-on: ubuntu-latest + environment: npm + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-python@v6 + with: + python-version: "3.13" + + - uses: actions/setup-node@v6 + with: + node-version: "24" + registry-url: "https://registry.npmjs.org" + package-manager-cache: false + + - name: Verify synchronized release version + env: + RELEASE_VERSION: ${{ github.event.release.tag_name || inputs.version }} + run: python scripts/check_release_versions.py --tag "$RELEASE_VERSION" + + - name: Wait for the matching PyPI core + env: + RELEASE_VERSION: ${{ github.event.release.tag_name || inputs.version }} + run: python scripts/wait_for_pypi.py --version "$RELEASE_VERSION" + + - name: Test and publish wrapper-only npm package + working-directory: js + run: | + npm ci + npm test + npm publish diff --git a/CHANGELOG.md b/CHANGELOG.md index 2576557..bc1350b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## v2.6.0 + +### Fixed + +- Preserve quoted arguments on Windows by launching native executables without a second `cmd.exe` parsing pass. +- Add `--cwd` and `SAGE_WORKSPACE_CWD` so Electron and other desktop hosts can supply the real project directory. +- Make foreground telemetry sync non-blocking; proof snapshots remain handled by the detached sender. +- Keep the npm launcher and PyPI core compatible by upgrading an older Python core before delegation. +- Synchronize Python, npm, lockfile, exported, and MCP version metadata. + +### Packaging + +- Keep npm install passive and publish only the thin launcher/bridge files. +- Publish PyPI first and npm second from one GitHub release workflow using trusted publishing. +- Add release version checks, npm launcher CI, package dry-runs, and a PyPI-availability gate before npm publish. + ## v2.4.3 ### Fixed diff --git a/MANIFEST.in b/MANIFEST.in index 0d16ab3..f7e3a02 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,5 +3,6 @@ prune electron prune src/sage/gui prune src/sage/dashboard +prune src/sage/tui exclude src/sage/gui_server.py exclude tests/test_dashboard_render.py diff --git a/NPM_SECURITY_REVIEW.md b/NPM_SECURITY_REVIEW.md index 5afb27b..1fa3575 100644 --- a/NPM_SECURITY_REVIEW.md +++ b/NPM_SECURITY_REVIEW.md @@ -4,7 +4,7 @@ The npm package `psycgod-sage` is intended to be a safe launcher for the canonical PyPI package `psycgod-sage`. -As of npm package version `1.0.19`: +As of npm package version `2.6.0`: - `npm install -g psycgod-sage` is passive. - There is no `preinstall`, `install`, or `postinstall` lifecycle script. @@ -15,6 +15,9 @@ As of npm package version `1.0.19`: - Agent hook installation is best-effort and non-blocking for normal command execution. - Hook denials do not print the blocked command, to avoid leaking secrets from command lines into AI context or logs. - Hook installation merges with existing hook settings instead of replacing a user's existing hooks. +- The package allowlist contains only the launcher, the Python bridge, and their declarations/source maps. +- The launcher verifies that the installed PyPI core is at least the compatibility version declared by npm. +- Desktop hosts can pass the workspace through `SAGE_WORKSPACE_CWD` without changing global process state. ## Why this changed diff --git a/README.md b/README.md index 1870196..e8818b5 100644 --- a/README.md +++ b/README.md @@ -70,19 +70,19 @@ SAGE does not auto-enable MCP. MCP is optional and manual for users who want it. ## Live Proof -Latest pulled stats as of 2026-07-22: +Latest pulled stats as of 2026-08-01: | Metric | Value | |---|---:| -| SAGE telemetry command events | 23,081 | -| Tokens processed | 750.0M | -| Tokens saved | 736.1M | -| Compression rate | 98.14% | -| Estimated savings | $15,273.07 | -| Command success rate | 89.7% | -| PyPI downloads, last 7 days | 885 | -| npm downloads, last 7 days | 1,268 | -| GitHub clones, last 14 days | 1,635 | +| SAGE telemetry command events | 27,892 | +| Tokens processed | 799.5M | +| Tokens saved | 783.7M | +| Compression rate | 98.02% | +| Estimated savings | $16,261.90 | +| Command success rate | 88.3% | +| PyPI downloads, last 7 days | 632 | +| npm downloads, last 7 days | 362 | +| GitHub clones, last 14 days | 574 | Live dashboard: [sage.api.marketingstudios.in/dashboard](https://sage.api.marketingstudios.in/dashboard) Install page: [sage.api.marketingstudios.in/install](https://sage.api.marketingstudios.in/install) @@ -119,6 +119,7 @@ sage install # Activate this machine and AI-agent instruct sage doctor --activation # Verify activation npx -y psycgod-sage doctor --activation sage run -- # Wrap any command +sage run --cwd /project -- # Explicit workspace for desktop/Electron hosts sage pytest # Shortcut for: sage run -- pytest sage npm test # Shortcut for: sage run -- npm test sage git status # Shortcut for: sage run -- git status @@ -149,6 +150,7 @@ SAGE is designed to keep prompts, source code, credentials, raw command output, |---|---| | Already-open AI-agent sessions may not reload new instructions | Restart Claude/Codex/Cursor/Windsurf/OpenCode after `sage install` | | Locked-down host apps can disable shell tools | SAGE cannot enable tools the host application has blocked | +| A desktop/Electron host starts its shell in the wrong folder | Pass `sage run --cwd -- ` or set `SAGE_WORKSPACE_CWD` | | npm/PyPI installs cannot safely auto-run activation | Run `sage install` once after package install | | MCP can disconnect in some stdio agent sessions | Use normal `sage run -- ` by default; enable MCP manually only if needed | | Package installs are passive by design | Real activation starts with `sage install` | diff --git a/cloudflare/sage-api/schema/oauth_sessions.sql b/cloudflare/sage-api/schema/oauth_sessions.sql new file mode 100644 index 0000000..3d3a7e9 --- /dev/null +++ b/cloudflare/sage-api/schema/oauth_sessions.sql @@ -0,0 +1,12 @@ +-- GitHub OAuth session store for server-side callback flow. +CREATE TABLE IF NOT EXISTS oauth_sessions ( + session_id TEXT PRIMARY KEY, + state TEXT NOT NULL DEFAULT 'pending', + status TEXT NOT NULL DEFAULT 'pending', + payload_json TEXT NOT NULL DEFAULT '{}', + created_at TEXT NOT NULL, + expires_at TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_oauth_sessions_state ON oauth_sessions(state); +CREATE INDEX IF NOT EXISTS idx_oauth_sessions_expires ON oauth_sessions(expires_at); diff --git a/cloudflare/sage-api/schema/schema.sql b/cloudflare/sage-api/schema/schema.sql index 0f6d87a..aea4e04 100644 --- a/cloudflare/sage-api/schema/schema.sql +++ b/cloudflare/sage-api/schema/schema.sql @@ -74,3 +74,15 @@ CREATE INDEX IF NOT EXISTS idx_telemetry_workspace ON telemetry_events(workspace CREATE INDEX IF NOT EXISTS idx_telemetry_kind ON telemetry_events(command_kind, received_at); CREATE INDEX IF NOT EXISTS idx_aggregate_daily_key ON aggregate_daily(key_id, day); +CREATE TABLE IF NOT EXISTS oauth_sessions ( + session_id TEXT PRIMARY KEY, + state TEXT NOT NULL DEFAULT 'pending', + status TEXT NOT NULL DEFAULT 'pending', + payload_json TEXT NOT NULL DEFAULT '{}', + created_at TEXT NOT NULL, + expires_at TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_oauth_sessions_state ON oauth_sessions(state); +CREATE INDEX IF NOT EXISTS idx_oauth_sessions_expires ON oauth_sessions(expires_at); + diff --git a/cloudflare/sage-api/src/worker.js b/cloudflare/sage-api/src/worker.js index b0591a2..9e438fe 100644 --- a/cloudflare/sage-api/src/worker.js +++ b/cloudflare/sage-api/src/worker.js @@ -1,5 +1,7 @@ import { BRAND_BANNER_BASE64, BRAND_ICON_BASE64 } from "./brand-assets.js"; +const GITHUB_CLIENT_ID_OAUTH = "Ov23libLspfxbzmPhMSv"; + // Restrict browser access to the dashboard and local SAGE UI. const ALLOWED_ORIGINS = [ "http://localhost:8765", // SAGE GUI local @@ -2303,6 +2305,230 @@ async function handleMachineLogin(env, request) { }, existingKey ? 200 : 201); } +// Session store for GitHub OAuth flows (in-memory KV, backed by D1) +const OAUTH_SESSION_TTL_MS = 15 * 60 * 1000; // 15 minutes + +async function oauthSessionGet(env, sessionId) { + try { + const row = await env.DB.prepare( + "SELECT * FROM oauth_sessions WHERE session_id = ? AND expires_at > ?" + ).bind(sessionId, nowIso()).first(); + return row || null; + } catch { return null; } +} + +async function oauthSessionSet(env, sessionId, data, ttlMs = OAUTH_SESSION_TTL_MS) { + const now = nowIso(); + const expiresAt = new Date(Date.now() + ttlMs).toISOString(); + await env.DB.prepare( + `INSERT INTO oauth_sessions (session_id, state, status, payload_json, created_at, expires_at) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(session_id) DO UPDATE SET + state = excluded.state, + status = excluded.status, + payload_json = excluded.payload_json, + expires_at = excluded.expires_at` + ).bind(sessionId, data.state || "pending", data.status || "pending", JSON.stringify(data.payload || {}), now, expiresAt).run(); +} + +async function oauthSessionUpdate(env, sessionId, updates) { + const existing = await oauthSessionGet(env, sessionId); + if (!existing) return; + const payload = JSON.parse(existing.payload_json || "{}"); + Object.assign(payload, updates.payload || {}); + await env.DB.prepare( + `UPDATE oauth_sessions SET status = ?, payload_json = ? WHERE session_id = ?` + ).bind(updates.status || existing.state, JSON.stringify(payload), sessionId).run(); +} + +async function ensureOAuthSessionsTable(env) { + if (!env.DB) return; + await env.DB.prepare( + `CREATE TABLE IF NOT EXISTS oauth_sessions ( + session_id TEXT PRIMARY KEY, + state TEXT NOT NULL DEFAULT 'pending', + status TEXT NOT NULL DEFAULT 'pending', + payload_json TEXT NOT NULL DEFAULT '{}', + created_at TEXT NOT NULL, + expires_at TEXT NOT NULL + )` + ).run(); + await env.DB.prepare( + `CREATE INDEX IF NOT EXISTS idx_oauth_sessions_state ON oauth_sessions(state)` + ).run().catch(() => {}); + await env.DB.prepare( + `CREATE INDEX IF NOT EXISTS idx_oauth_sessions_expires ON oauth_sessions(expires_at)` + ).run().catch(() => {}); +} + +async function handleGithubAuthStart(env, request) { + await ensureOAuthSessionsTable(env); + const sessionId = newId("oauth"); + const state = randomHex(32); // Anti-CSRF + const baseUrl = "https://sage.api.marketingstudios.in"; + const authorizeUrl = + "https://github.com/login/oauth/authorize" + + `?client_id=${GITHUB_CLIENT_ID_OAUTH}` + + `&redirect_uri=${encodeURIComponent(baseUrl + "/auth/github/callback")}` + + `&state=${state}` + + "&scope=user:email"; + + await oauthSessionSet(env, sessionId, { state, status: "pending", payload: { session_id: sessionId, created_at: nowIso() } }); + return json({ ok: true, session_id: sessionId, authorize_url: authorizeUrl, state }); +} + +async function handleGithubAuthCallback(env, url) { + const authCode = url.searchParams.get("code") || ""; + const returnedState = url.searchParams.get("state") || ""; + + if (!authCode) { + return new Response("Missing authorization code", { status: 400 }); + } + + // Find the session by state + let session = null; + try { + session = await env.DB.prepare( + "SELECT * FROM oauth_sessions WHERE state = ? AND status = 'pending' AND expires_at > ?" + ).bind(returnedState, nowIso()).first(); + } catch {} + if (!session) { + // No session found — user started OAuth outside the CLI. Show a generic page. + return new Response(`

GitHub Connected

You can close this tab.

`, { + status: 200, + headers: { "Content-Type": "text/html; charset=utf-8" }, + }); + } + + // Exchange auth code for GitHub access token + let githubToken; + try { + const tokenResponse = await fetch("https://github.com/login/oauth/access_token", { + method: "POST", + headers: { "Content-Type": "application/json", "Accept": "application/json" }, + body: JSON.stringify({ + client_id: GITHUB_CLIENT_ID_OAUTH, + client_secret: env.GITHUB_CLIENT_SECRET, + code: authCode, + redirect_uri: "https://sage.api.marketingstudios.in/auth/github/callback", + }), + }); + const tokenData = await tokenResponse.json(); + githubToken = tokenData.access_token; + if (!githubToken) { + await oauthSessionUpdate(env, session.session_id, { status: "error", payload: { error: tokenData.error_description || "GitHub OAuth failed" } }); + return new Response("GitHub authorization failed. Please try again.", { status: 401 }); + } + } catch (exc) { + return new Response("OAuth exchange failed", { status: 500 }); + } + + // Get GitHub user info + let githubUser; + try { + const userResponse = await fetch("https://api.github.com/user", { + headers: { "Authorization": `Bearer ${githubToken}`, "Accept": "application/vnd.github.v3+json", "User-Agent": "SAGE-API/1.0" }, + }); + githubUser = await userResponse.json(); + if (!githubUser.id || !githubUser.login) { + await oauthSessionUpdate(env, session.session_id, { status: "error", payload: { error: "Failed to fetch GitHub user info" } }); + return new Response("Failed to get GitHub user info", { status: 500 }); + } + } catch (exc) { + return new Response("GitHub user info fetch failed", { status: 500 }); + } + + const githubId = String(githubUser.id); + const githubUsername = githubUser.login; + const createdAt = nowIso(); + + // Check if this GitHub user already has an active key + const existingKey = await env.DB.prepare( + "SELECT * FROM api_keys WHERE github_id = ? AND revoked_at = ''" + ).bind(githubId).first(); + if (existingKey) { + await env.DB.prepare( + "UPDATE api_keys SET revoked_at = ? WHERE key_id = ? AND revoked_at = ''" + ).bind(createdAt, existingKey.key_id).run(); + } + + // Create new API key + const keyId = newId("key"); + const secret = randomHex(32); + const token = `sage_live_${keyId}_${secret}`; + const expiryDays = 365; + const expiresAt = new Date(Date.now() + expiryDays * 24 * 60 * 60 * 1000).toISOString(); + + const installationId = textValue(JSON.parse(session.payload_json || "{}").installation_id || "", 120); + const clientVersion = textValue(JSON.parse(session.payload_json || "{}").client_version || "", 80); + const platform = textValue(JSON.parse(session.payload_json || "{}").platform || "", 80); + + const statements = [env.DB.prepare( + `INSERT INTO api_keys + (key_id, secret_hash, prefix, scope, display_name, username, github_id, github_username, + public_profile, privacy_max, expires_at, rate_limit_per_hour, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + ).bind( + keyId, await sha256(token), "sage_live", "personal", + githubUser.name || githubUsername, githubUsername, + githubId, githubUsername, + 1, 1, expiresAt, 10000, createdAt + )]; + if (installationId) { + statements.push( + env.DB.prepare( + `INSERT INTO installations (installation_id, key_id, first_seen_at, last_seen_at, client_version, platform) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(installation_id) DO UPDATE SET + key_id = excluded.key_id, last_seen_at = excluded.last_seen_at, + client_version = excluded.client_version, platform = excluded.platform` + ).bind(installationId, keyId, createdAt, createdAt, clientVersion, platform) + ); + } + await env.DB.batch(statements); + + // Store result in session + await oauthSessionUpdate(env, session.session_id, { + status: "completed", + payload: { + ok: true, + key_id: keyId, + api_key: token, + github_username: githubUsername, + github_id: parseInt(githubId, 10), + display_name: githubUser.name || githubUsername, + created_at: createdAt, + expires_at: expiresAt, + rotated_from: existingKey ? existingKey.key_id : "", + }, + }); + + return new Response(` +SAGE Connected + +
OK

SAGE Connected!

GitHub authentication successful.

You can close this window and return to SAGE.

`, { + status: 200, + headers: { "Content-Type": "text/html; charset=utf-8" }, + }); +} + +async function handleGithubAuthStatus(env, request, url) { + const sessionId = url.searchParams.get("session") || ""; + if (!sessionId) return error("Missing session parameter", 400); + + const session = await oauthSessionGet(env, sessionId); + if (!session) return error("Session not found or expired", 404); + + const payload = JSON.parse(session.payload_json || "{}"); + if (session.status === "completed") { + return json({ ok: true, status: "completed", ...payload }); + } + if (session.status === "error") { + return json({ ok: false, status: "error", error: payload.error || "OAuth failed" }); + } + return json({ ok: true, status: "pending", session_id: sessionId }); +} + async function handleCreateKey(env, request) { // Key creation is restricted to trusted clients. const masterKey = request.headers.get("X-SAGE-Master-Key"); @@ -3031,6 +3257,9 @@ async function route(request, env) { if (request.method === "GET" && url.pathname === "/v1/admin/users") return handleAdminUsers(env, request); if (request.method === "POST" && url.pathname === "/v1/admin/users/cleanup") return handleAdminUsersCleanup(env, request); if (request.method === "GET" && url.pathname === "/v1/proof") return handleProof(env); + if (request.method === "POST" && url.pathname === "/v1/github-auth/start") return handleGithubAuthStart(env, request); + if (request.method === "GET" && url.pathname === "/v1/github-auth/status") return handleGithubAuthStatus(env, request, url); + if (request.method === "GET" && url.pathname === "/auth/github/callback") return handleGithubAuthCallback(env, url); return error("Not found", 404); } diff --git a/docs/PYPI_RELEASE.md b/docs/PYPI_RELEASE.md index 09aea89..d25b186 100644 --- a/docs/PYPI_RELEASE.md +++ b/docs/PYPI_RELEASE.md @@ -53,27 +53,28 @@ python -m build python -m twine check dist/* ``` -5. Publish from GitHub using PyPI Trusted Publishing: +5. Publish from GitHub using trusted publishing for both registries: - Create a PyPI pending publisher for project `psycgod-sage`. - Use repository `PsYcGoD/sage`. - Use workflow `pypi-publish.yml`. - Use environment `pypi`. -- Publish a GitHub release. The `.github/workflows/pypi-publish.yml` workflow builds and uploads to PyPI using OIDC. +- Configure npm trusted publishing for `PsYcGoD/sage`, workflow `pypi-publish.yml`, environment `npm`, with `npm publish` allowed. +- Publish a GitHub release. The workflow uploads PyPI first, waits until that exact core version is visible, and then publishes the thin npm launcher using OIDC. No PyPI API token is needed when Trusted Publishing is configured correctly. -6. After the GitHub release workflow succeeds, install from PyPI in a clean environment and run: +6. After the GitHub release workflow succeeds, install both entry points in clean environments and run: ```bash pip install psycgod-sage sage --version -sage gui sage api status +npx -y psycgod-sage@latest --version ``` ## Not Done Yet Do not mark PyPI release complete until the package is visible on PyPI and install-tested with `pip install psycgod-sage`. -Current blocker: PyPI rejected the GitHub Trusted Publisher claim because the pending publisher used the wrong repository casing/path. The GitHub claim is `repository: PsYcGoD/sage`, so PyPI must use exactly `PsYcGoD/sage`. +Registry configuration is case-sensitive. Both trusted publishers must use `PsYcGoD/sage` and the exact workflow filename `pypi-publish.yml`; the environments are `pypi` and `npm` respectively. diff --git a/docs/assets/sage-live-dashboard.png b/docs/assets/sage-live-dashboard.png index e0fc401..f2efd9a 100644 Binary files a/docs/assets/sage-live-dashboard.png and b/docs/assets/sage-live-dashboard.png differ diff --git a/js/package-lock.json b/js/package-lock.json index 463a3c7..4ebabc3 100644 --- a/js/package-lock.json +++ b/js/package-lock.json @@ -1,134 +1,104 @@ { "name": "psycgod-sage", - "version": "2.5.6", + "version": "2.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "psycgod-sage", - "version": "2.5.6", - "hasInstallScript": true, + "version": "2.6.0", "license": "MIT", "bin": { "sage": "bin/sage.js" }, "devDependencies": { - "@types/better-sqlite3": "^7.6.11", - "@types/node": "^22.0.0", - "@types/uuid": "^10.0.0", - "typescript": "^5.5.0", - "vitest": "^2.0.0" + "@types/node": "^24.0.0", + "typescript": "^5.9.0", + "vitest": "^4.1.0" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], + "node_modules/@emnapi/core": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-alpha.3.tgz", + "integrity": "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "2.0.1", + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], + "node_modules/@emnapi/runtime": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-2.0.0-alpha.3.tgz", + "integrity": "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" + "peer": true, + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], + "node_modules/@emnapi/wasi-threads": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.0.1.tgz", + "integrity": "sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } + "license": "MIT" }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.2.tgz", + "integrity": "sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], + "node_modules/@oxc-project/types": { + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.142.0.tgz", + "integrity": "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" + "funding": { + "url": "https://github.com/sponsors/Boshen" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.1.tgz", + "integrity": "sha512-02hOeOSryYxVrOIphmLAsqnCJWxwlzFk+pEt/N/i6OgT3lShHO7xGCU5cpgchRDHboAEbSjzgGh+O/u1GswQmA==", "cpu": [ "arm64" ], @@ -136,69 +106,69 @@ "license": "MIT", "optional": true, "os": [ - "freebsd" + "android" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.1.tgz", + "integrity": "sha512-fMsTOnN0OjFm3CyppWPitKnc8UlliVARUULW6cfU6AIqjdtgmSFWSk9vecHzZduv/yMWIHDlRhM1e8Iff9uAfA==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "freebsd" + "darwin" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.1.tgz", + "integrity": "sha512-1wjKdz/XLGKHaTNHjQveQ/B23TKx4ItAqm1JbyVuvNPc4Ze0Fb48s49TAd/2zcplPl8okE/UbTgmlVfwT7eFeQ==", "cpu": [ - "arm" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.1.tgz", + "integrity": "sha512-Fa0jHR07E7YBN4vOEsbVf2briYNsuOowfLJaXULZM0ldMlaCaj2LJgLMbMe4iacRyZmvR8efFhgR9wKuGclQUg==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "freebsd" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.1.tgz", + "integrity": "sha512-pzkgu1SSHGgRRyRZ4fbmSgmajbVt+epaLP99NDjFft69v/ypfTi6swBMiVdh2EkQ0OSnHE1lZDM7DRGkyAzUpA==", "cpu": [ - "ia32" + "arm" ], "dev": true, "license": "MIT", @@ -207,15 +177,15 @@ "linux" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.1.tgz", + "integrity": "sha512-QI5SEDY8cbiYWHx0VO4vIc3UlS6a32vXHjU8Qy/17adEmZIPuByJg13UEvo9c/UCiUkdcVWY83C+b+JrwnNyUg==", "cpu": [ - "loong64" + "arm64" ], "dev": true, "license": "MIT", @@ -224,15 +194,15 @@ "linux" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.1.tgz", + "integrity": "sha512-Sm41FyCeXqmYcERoYOCbGIL5hNfd8w9LQ7Y61Bev48HkcjaJqV/iiVOaiDxjVTRMS+QKrZmD8cfPt4uMVnvM+A==", "cpu": [ - "mips64el" + "arm64" ], "dev": true, "license": "MIT", @@ -241,13 +211,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.1.tgz", + "integrity": "sha512-2x+WhXTGl9yJYPbltW/BSEPTVz9OIWQyER4N+gJEDWkkn904eRcBzELqh/Hf7K0w/ubGbKNMv0ZC+94QK/IFEg==", "cpu": [ "ppc64" ], @@ -258,30 +228,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.1.tgz", + "integrity": "sha512-eEjmQpuRQayHPWWnywaWHkFT3ToPbP3RYy42VVd/B9aBGDA+Ol25EIWHxKQST3IiWJjikCWUF7KtbfqwZrzVwQ==", "cpu": [ "s390x" ], @@ -292,13 +245,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.1.tgz", + "integrity": "sha512-/Orga1fZYkLc/56jBICcHrKchl8Z2UKdDSr3LG9ToWO1lQ6a4Livk9Xz+9WN91zsz5QR3XQz2NNoSDEvP6qadw==", "cpu": [ "x64" ], @@ -309,13 +262,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.1.tgz", + "integrity": "sha512-xxBJRL+0q0Kce7orznGWLuylHDY65vuARXZRpX+hPdv+DqK2c3NlCsVA98tlWzWNEE7yPqA/1NQ5nnCrj49Y5A==", "cpu": [ "x64" ], @@ -323,50 +276,49 @@ "license": "MIT", "optional": true, "os": [ - "netbsd" + "linux" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.1.tgz", + "integrity": "sha512-M6AdXIXw3s+/8XpKMzdGDEXGS1S7kwUsy+rcTIUIOx5Ge4nXKCtAFHFV9YKkXvGcC5WMoTjAteLzlsQROVI0Yw==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openbsd" + "openharmony" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.2.1.tgz", + "integrity": "sha512-/TX0SoRGojHzSAHpfVBbavRVSazg5U3h3Y3VXfcc0cdugq6kxdqw8LPGFiPr+/7gE/60zRcsOY2Vi9b9eT0jww==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "sunos" - ], + "dependencies": { + "@emnapi/core": "2.0.0-alpha.3", + "@emnapi/runtime": "2.0.0-alpha.3", + "@napi-rs/wasm-runtime": "^1.2.0" + }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.1.tgz", + "integrity": "sha512-EvRrivJieyHG+AO9lleZWgq+g0+S7oV2C51yuqlcyU/R9net+sI4Pj0F+lUoP2bEr6TWX3SqFaaS0SzfLxSzkw==", "cpu": [ "arm64" ], @@ -377,30 +329,13 @@ "win32" ], "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.1.tgz", + "integrity": "sha512-Z4eCmn5QJ/5+azF9knpLWKfVd9aidn0mAe9TpJgvBLId9Ax3t0+JVxBmT25Bv7NBbVW1TZyKjQjQReouMeH5UQ==", "cpu": [ "x64" ], @@ -411,676 +346,541 @@ "win32" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, "license": "MIT" }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", - "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", - "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", - "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", - "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", - "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", - "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", - "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", - "cpu": [ - "arm" - ], + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", - "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", - "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", - "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "MIT" }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", - "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", - "cpu": [ - "loong64" - ], + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "tslib": "^2.4.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", - "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", - "cpu": [ - "loong64" - ], + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", - "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", - "cpu": [ - "ppc64" - ], + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "MIT" }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", - "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", - "cpu": [ - "ppc64" - ], + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "MIT" }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", - "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", - "cpu": [ - "riscv64" - ], + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "peer": true, + "dependencies": { + "undici-types": "~7.18.0" + } }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", - "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", - "cpu": [ - "riscv64" - ], + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", - "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", - "cpu": [ - "s390x" - ], + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", - "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", - "cpu": [ - "x64" - ], + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", - "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", - "cpu": [ - "x64" - ], + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", - "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", - "cpu": [ - "x64" - ], + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", - "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", - "cpu": [ - "arm64" - ], + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", - "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", - "cpu": [ - "arm64" - ], + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", - "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", - "cpu": [ - "ia32" - ], + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", - "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", - "cpu": [ - "x64" - ], + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", - "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", - "cpu": [ - "x64" - ], + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "MIT" }, - "node_modules/@types/better-sqlite3": { - "version": "7.6.13", - "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", - "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" + "license": "Apache-2.0", + "engines": { + "node": ">=8" } }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", "dev": true, "license": "MIT" }, - "node_modules/@types/node": { - "version": "22.20.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", - "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "@types/estree": "^1.0.0" } }, - "node_modules/@types/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vitest/expect": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", - "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" } }, - "node_modules/@vitest/mocker": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", - "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", - "dependencies": { - "@vitest/spy": "2.1.9", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.12" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": ">=12.0.0" }, "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0" + "picomatch": "^3 || ^4" }, "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", - "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "picomatch": { + "optional": true + } } }, - "node_modules/@vitest/runner": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", - "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "dependencies": { - "@vitest/utils": "2.1.9", - "pathe": "^1.1.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/@vitest/snapshot": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", - "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", "dev": true, - "license": "MIT", + "license": "MPL-2.0", "dependencies": { - "@vitest/pretty-format": "2.1.9", - "magic-string": "^0.30.12", - "pathe": "^1.1.2" + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" } }, - "node_modules/@vitest/spy": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", - "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^3.0.2" + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@vitest/utils": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", - "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "2.1.9", - "loupe": "^3.1.2", - "tinyrainbow": "^1.2.0" + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=12" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=8" - } - }, - "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" + "node": ">= 12.0.0" }, - "engines": { - "node": ">=18" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 16" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.0" + "node": ">= 12.0.0" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" + "node": ">= 12.0.0" }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/expect-type": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", - "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12.0.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], "dev": true, - "hasInstallScript": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, "os": [ - "darwin" + "win32" ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -1091,17 +891,10 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/nanoid": { - "version": "3.3.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "dev": true, "funding": [ { @@ -1117,23 +910,27 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], "license": "MIT", "engines": { - "node": ">= 14.16" + "node": ">=12.20.0" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1141,10 +938,24 @@ "dev": true, "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/postcss": { - "version": "8.5.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.17.tgz", - "integrity": "sha512-J7EF+8X+CzRPaJPOv9Ck2wNWJvGnnl3PcNPAdGg6GTLjyVpyQ0yATMSXRFRV01BviT/9Gwuc3rjEyJbDJG9a4w==", + "version": "8.5.25", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", + "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", "dev": true, "funding": [ { @@ -1162,7 +973,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -1170,49 +981,38 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/rollup": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", - "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "node_modules/rolldown": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.1.tgz", + "integrity": "sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.9" + "@oxc-project/types": "=0.142.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.62.2", - "@rollup/rollup-android-arm64": "4.62.2", - "@rollup/rollup-darwin-arm64": "4.62.2", - "@rollup/rollup-darwin-x64": "4.62.2", - "@rollup/rollup-freebsd-arm64": "4.62.2", - "@rollup/rollup-freebsd-x64": "4.62.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", - "@rollup/rollup-linux-arm-musleabihf": "4.62.2", - "@rollup/rollup-linux-arm64-gnu": "4.62.2", - "@rollup/rollup-linux-arm64-musl": "4.62.2", - "@rollup/rollup-linux-loong64-gnu": "4.62.2", - "@rollup/rollup-linux-loong64-musl": "4.62.2", - "@rollup/rollup-linux-ppc64-gnu": "4.62.2", - "@rollup/rollup-linux-ppc64-musl": "4.62.2", - "@rollup/rollup-linux-riscv64-gnu": "4.62.2", - "@rollup/rollup-linux-riscv64-musl": "4.62.2", - "@rollup/rollup-linux-s390x-gnu": "4.62.2", - "@rollup/rollup-linux-x64-gnu": "4.62.2", - "@rollup/rollup-linux-x64-musl": "4.62.2", - "@rollup/rollup-openbsd-x64": "4.62.2", - "@rollup/rollup-openharmony-arm64": "4.62.2", - "@rollup/rollup-win32-arm64-msvc": "4.62.2", - "@rollup/rollup-win32-ia32-msvc": "4.62.2", - "@rollup/rollup-win32-x64-gnu": "4.62.2", - "@rollup/rollup-win32-x64-msvc": "4.62.2", - "fsevents": "~2.3.2" + "@rolldown/binding-android-arm64": "1.2.1", + "@rolldown/binding-darwin-arm64": "1.2.1", + "@rolldown/binding-darwin-x64": "1.2.1", + "@rolldown/binding-freebsd-x64": "1.2.1", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.1", + "@rolldown/binding-linux-arm64-gnu": "1.2.1", + "@rolldown/binding-linux-arm64-musl": "1.2.1", + "@rolldown/binding-linux-ppc64-gnu": "1.2.1", + "@rolldown/binding-linux-s390x-gnu": "1.2.1", + "@rolldown/binding-linux-x64-gnu": "1.2.1", + "@rolldown/binding-linux-x64-musl": "1.2.1", + "@rolldown/binding-openharmony-arm64": "1.2.1", + "@rolldown/binding-wasm32-wasi": "1.2.1", + "@rolldown/binding-win32-arm64-msvc": "1.2.1", + "@rolldown/binding-win32-x64-msvc": "1.2.1" } }, "node_modules/siginfo": { @@ -1240,9 +1040,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", "dev": true, "license": "MIT" }, @@ -1254,42 +1054,50 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", "dev": true, "license": "MIT", "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": ">=18" } }, - "node_modules/tinyrainbow": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", - "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, "engines": { - "node": ">=14.0.0" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", "dev": true, "license": "MIT", "engines": { "node": ">=14.0.0" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -1305,29 +1113,31 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "dev": true, "license": "MIT" }, "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.0.tgz", + "integrity": "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.23", + "rolldown": "~1.2.0", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -1336,23 +1146,33 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, - "less": { + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { "optional": true }, - "lightningcss": { + "less": { "optional": true }, "sass": { @@ -1369,85 +1189,89 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, - "node_modules/vite-node": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", - "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.7", - "es-module-lexer": "^1.5.4", - "pathe": "^1.1.2", - "vite": "^5.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/vitest": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", - "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "2.1.9", - "@vitest/mocker": "2.1.9", - "@vitest/pretty-format": "^2.1.9", - "@vitest/runner": "2.1.9", - "@vitest/snapshot": "2.1.9", - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "debug": "^4.3.7", - "expect-type": "^1.1.0", - "magic-string": "^0.30.12", - "pathe": "^1.1.2", - "std-env": "^3.8.0", + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", - "tinyexec": "^0.3.1", - "tinypool": "^1.0.1", - "tinyrainbow": "^1.2.0", - "vite": "^5.0.0", - "vite-node": "2.1.9", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.9", - "@vitest/ui": "2.1.9", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, + "@opentelemetry/api": { + "optional": true + }, "@types/node": { "optional": true }, - "@vitest/browser": { + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { "optional": true }, "@vitest/ui": { @@ -1458,6 +1282,9 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, diff --git a/js/package.json b/js/package.json index 8c38aa2..eebc4f1 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "psycgod-sage", - "version": "2.5.8", + "version": "2.6.0", "mcpName": "io.github.PsYcGoD/sage", "description": "npm/npx launcher for the canonical PyPI SAGE CLI wrapper for AI coding agents", "author": "PsYcGoD", @@ -12,28 +12,32 @@ "sage": "bin/sage.js" }, "files": [ - "dist", - "bin" + "bin", + "dist/index.*", + "dist/cli/index.*", + "dist/python/bridge.*" ], "scripts": { - "build": "tsc", + "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc", "dev": "tsc --watch", - "test": "vitest", + "test": "vitest --run tests/launcher.test.ts", "lint": "eslint src", - "postinstall": "node ./dist/cli/postinstall.js", - "prepublishOnly": "npm run build" + "prepack": "npm run build" }, "dependencies": {}, "devDependencies": { - "@types/better-sqlite3": "^7.6.11", - "@types/node": "^22.0.0", - "@types/uuid": "^10.0.0", - "typescript": "^5.5.0", - "vitest": "^2.0.0" + "@types/node": "^24.0.0", + "typescript": "^5.9.0", + "vitest": "^4.1.0" }, "engines": { "node": ">=18.0.0" }, + "publishConfig": { + "access": "public", + "provenance": true, + "registry": "https://registry.npmjs.org/" + }, "repository": { "type": "git", "url": "git+https://github.com/PsYcGoD/sage.git" diff --git a/js/src/index.ts b/js/src/index.ts index c962ae2..3632cc8 100644 --- a/js/src/index.ts +++ b/js/src/index.ts @@ -1,5 +1,5 @@ export { ensurePythonSage, findPython, runPythonSage, setupPythonSage, showPythonSageApiStatus } from './python/bridge.js'; -export const VERSION = '1.0.25'; +export const VERSION = '2.6.0'; export const PYPI_PACKAGE = 'psycgod-sage'; export const NPM_RUN_PREFIX = 'npx -y psycgod-sage run --'; diff --git a/js/src/python/bridge.ts b/js/src/python/bridge.ts index 972b08c..17a5861 100644 --- a/js/src/python/bridge.ts +++ b/js/src/python/bridge.ts @@ -1,4 +1,7 @@ import { spawnSync } from 'child_process'; +import { existsSync, statSync } from 'fs'; + +export const EXPECTED_SAGE_VERSION = '2.6.0'; interface PythonCommand { command: string; @@ -19,15 +22,42 @@ function runCandidate(candidate: PythonCommand, args: string[], stdio: 'pipe' | stdio, encoding: 'utf8', env: process.env, + cwd: childWorkingDirectory(), }); } +function childWorkingDirectory(): string { + const configured = process.env.SAGE_WORKSPACE_CWD; + if (configured && existsSync(configured) && statSync(configured).isDirectory()) { + return configured; + } + return process.cwd(); +} + +function parseVersion(value: string): number[] { + return value.trim().split('.').map((part) => Number.parseInt(part, 10) || 0); +} + +function versionAtLeast(actual: string, expected: string): boolean { + const left = parseVersion(actual); + const right = parseVersion(expected); + for (let index = 0; index < Math.max(left.length, right.length); index += 1) { + const difference = (left[index] || 0) - (right[index] || 0); + if (difference !== 0) return difference > 0; + } + return true; +} + export function findPython(): PythonCommand | null { if (cachedPython) return cachedPython; for (const candidate of PYTHON_CANDIDATES) { - const result = runCandidate(candidate, ['--version']); - if (result.status === 0) { + const result = runCandidate(candidate, [ + '-c', + 'import sys; print(".".join(map(str, sys.version_info[:3])))', + ]); + const version = result.status === 0 ? String(result.stdout || '').trim() : ''; + if (version && versionAtLeast(version, '3.10.0')) { cachedPython = candidate; return candidate; } @@ -36,16 +66,16 @@ export function findPython(): PythonCommand | null { return null; } -function pythonSageImportable(python: PythonCommand): boolean { - // Cheap probe: imports the package without loading the full CLI. - return runCandidate(python, ['-c', 'import sage']).status === 0; +function pythonSageVersion(python: PythonCommand): string | null { + const result = runCandidate(python, ['-c', 'import sage; print(sage.__version__)']); + return result.status === 0 ? String(result.stdout || '').trim() : null; } function installPythonSage(python: PythonCommand): boolean { console.log('Installing Python SAGE core from PyPI...'); const install = runCandidate( python, - ['-m', 'pip', 'install', '--upgrade', 'psycgod-sage'], + ['-m', 'pip', 'install', '--upgrade', `psycgod-sage>=${EXPECTED_SAGE_VERSION}`], 'inherit' ); if (install.status === 0) { @@ -56,7 +86,7 @@ function installPythonSage(python: PythonCommand): boolean { // rejects plain pip installs; retry as a user install. const userInstall = runCandidate( python, - ['-m', 'pip', 'install', '--upgrade', '--user', '--break-system-packages', 'psycgod-sage'], + ['-m', 'pip', 'install', '--upgrade', '--user', '--break-system-packages', `psycgod-sage>=${EXPECTED_SAGE_VERSION}`], 'inherit' ); if (userInstall.status !== 0) { @@ -74,36 +104,26 @@ export function ensurePythonSage(): boolean { return false; } - if (pythonSageImportable(python)) { + const installedVersion = pythonSageVersion(python); + if (installedVersion && versionAtLeast(installedVersion, EXPECTED_SAGE_VERSION)) { return true; } - return installPythonSage(python) && pythonSageImportable(python); + if (installedVersion) { + console.log( + `Updating Python SAGE core ${installedVersion} -> ${EXPECTED_SAGE_VERSION} for npm compatibility...` + ); + } + if (!installPythonSage(python)) return false; + const updatedVersion = pythonSageVersion(python); + return Boolean(updatedVersion && versionAtLeast(updatedVersion, EXPECTED_SAGE_VERSION)); } export function runPythonSage(args: string[]): number { - const python = findPython(); - if (!python) { - console.error('SAGE npm launcher needs Python 3.10+ on PATH.'); - console.error('Install Python, then retry: npm install -g psycgod-sage'); - return 1; - } - - // Run directly: the common case pays one Python start, not three. Only when - // the run fails AND the package is missing do we install and retry once. + if (!ensurePythonSage()) return 1; + const python = findPython()!; const result = runCandidate(python, ['-m', 'sage', ...args], 'inherit'); - if (result.status === 0) { - return 0; - } - if (pythonSageImportable(python)) { - return typeof result.status === 'number' ? result.status : 1; - } - - if (!installPythonSage(python)) { - return 1; - } - const retry = runCandidate(python, ['-m', 'sage', ...args], 'inherit'); - return typeof retry.status === 'number' ? retry.status : 1; + return typeof result.status === 'number' ? result.status : 1; } export function setupPythonSage(): boolean { diff --git a/js/tests/launcher.test.ts b/js/tests/launcher.test.ts new file mode 100644 index 0000000..576fbdf --- /dev/null +++ b/js/tests/launcher.test.ts @@ -0,0 +1,37 @@ +import { readFileSync } from 'fs'; +import { resolve } from 'path'; +import { describe, expect, it } from 'vitest'; + +import { VERSION } from '../src/index.js'; +import { EXPECTED_SAGE_VERSION } from '../src/python/bridge.js'; + +const packageJson = JSON.parse( + readFileSync(resolve(process.cwd(), 'package.json'), 'utf8') +); + +describe('wrapper-only npm launcher', () => { + it('keeps the npm and Python-core compatibility versions synchronized', () => { + expect(VERSION).toBe(packageJson.version); + expect(EXPECTED_SAGE_VERSION).toBe(packageJson.version); + }); + + it('has no install-time lifecycle side effects', () => { + expect(packageJson.scripts.preinstall).toBeUndefined(); + expect(packageJson.scripts.install).toBeUndefined(); + expect(packageJson.scripts.postinstall).toBeUndefined(); + }); + + it('publishes only the thin launcher and Python bridge', () => { + expect(packageJson.files).toEqual([ + 'bin', + 'dist/index.*', + 'dist/cli/index.*', + 'dist/python/bridge.*', + ]); + }); + + it('supports an explicit workspace from desktop hosts', () => { + const bridge = readFileSync(resolve(process.cwd(), 'src/python/bridge.ts'), 'utf8'); + expect(bridge).toContain('SAGE_WORKSPACE_CWD'); + }); +}); diff --git a/js/tsconfig.json b/js/tsconfig.json index 31c81c0..36c8f71 100644 --- a/js/tsconfig.json +++ b/js/tsconfig.json @@ -18,9 +18,6 @@ "include": [ "src/index.ts", "src/cli/index.ts", - "src/cli/postinstall.ts", - "src/cli/setup.ts", - "src/install/**/*", "src/python/**/*" ], "exclude": ["node_modules", "dist", "tests"] diff --git a/pyproject.toml b/pyproject.toml index 3907500..8048d3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "psycgod-sage" -version = "2.5.8" +version = "2.6.0" description = "Local-first command wrapper for AI coding agents with compressed terminal output and privacy-safe proof metrics." readme = "README.md" requires-python = ">=3.10" @@ -73,12 +73,19 @@ Repository = "https://github.com/PsYcGoD/sage" Issues = "https://github.com/PsYcGoD/sage/issues" Dashboard = "https://sage.api.marketingstudios.in/dashboard" -# The published package is the command wrapper only. GUI code (sage.gui, -# gui_server) is enterprise-only and the local dashboard UI is repo-only; -# neither may ship to PyPI/npm. +# The published package is the command wrapper only. GUI/TUI code (sage.gui, +# sage.tui, gui_server) is not public and the local dashboard UI is repo-only; +# none may ship to PyPI/npm. [tool.setuptools.packages.find] where = ["src"] -exclude = ["sage.gui", "sage.gui.*", "sage.dashboard", "sage.dashboard.*"] +exclude = [ + "sage.gui", + "sage.gui.*", + "sage.dashboard", + "sage.dashboard.*", + "sage.tui", + "sage.tui.*", +] [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/scripts/check_release_versions.py b/scripts/check_release_versions.py new file mode 100644 index 0000000..0b74de9 --- /dev/null +++ b/scripts/check_release_versions.py @@ -0,0 +1,62 @@ +"""Fail a release when public package versions drift apart.""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +import tomllib +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def _quoted_version(path: Path, name: str) -> str: + text = path.read_text(encoding="utf-8") + match = re.search(rf"{re.escape(name)}\s*=\s*['\"]([^'\"]+)['\"]", text) + if not match: + raise ValueError(f"version marker {name!r} not found in {path}") + return match.group(1) + + +def versions() -> dict[str, str]: + pyproject = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8")) + package = json.loads((ROOT / "js" / "package.json").read_text(encoding="utf-8")) + package_lock = json.loads((ROOT / "js" / "package-lock.json").read_text(encoding="utf-8")) + return { + "pyproject": str(pyproject["project"]["version"]), + "python": _quoted_version(ROOT / "src" / "sage" / "__init__.py", "__version__"), + "npm": str(package["version"]), + "npm_lock": str(package_lock["packages"][""]["version"]), + "npm_export": _quoted_version(ROOT / "js" / "src" / "index.ts", "VERSION"), + "npm_python_core": _quoted_version( + ROOT / "js" / "src" / "python" / "bridge.ts", "EXPECTED_SAGE_VERSION" + ), + } + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--tag", default="") + args = parser.parse_args() + + found = versions() + expected = found["pyproject"] + mismatches = {name: value for name, value in found.items() if value != expected} + if args.tag: + tag_version = args.tag.removeprefix("v") + if tag_version != expected: + mismatches["release_tag"] = tag_version + + if mismatches: + print(json.dumps({"expected": expected, "mismatches": mismatches}, indent=2)) + return 1 + + print(f"release versions synchronized: {expected}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/wait_for_pypi.py b/scripts/wait_for_pypi.py new file mode 100644 index 0000000..a61b7c7 --- /dev/null +++ b/scripts/wait_for_pypi.py @@ -0,0 +1,39 @@ +"""Wait until PyPI serves the Python core required by the npm launcher.""" + +from __future__ import annotations + +import argparse +import json +import sys +import time +import urllib.request + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--version", required=True) + parser.add_argument("--timeout", type=int, default=300) + parser.add_argument("--interval", type=int, default=10) + args = parser.parse_args() + + expected = args.version.removeprefix("v") + deadline = time.monotonic() + max(1, args.timeout) + url = "https://pypi.org/pypi/psycgod-sage/json" + while time.monotonic() < deadline: + try: + with urllib.request.urlopen(f"{url}?t={time.time_ns()}", timeout=15) as response: + current = str(json.load(response)["info"]["version"]) + if current == expected: + print(f"PyPI core is ready: {current}") + return 0 + print(f"PyPI currently serves {current}; waiting for {expected}") + except Exception as exc: + print(f"PyPI check failed temporarily: {exc}") + time.sleep(max(1, args.interval)) + + print(f"Timed out waiting for psycgod-sage {expected} on PyPI", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..f96cac7 --- /dev/null +++ b/setup.py @@ -0,0 +1,10 @@ +"""Legacy setuptools entry point for psycgod-sage. + +Package installation is intentionally passive. Explicit ``sage install`` or a +normal first CLI invocation performs any optional local activation. +""" + +from setuptools import setup + + +setup() diff --git a/skills/ui-ux-pro-max-skill b/skills/ui-ux-pro-max-skill deleted file mode 160000 index 12b486b..0000000 --- a/skills/ui-ux-pro-max-skill +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 12b486b22e67f5d887962ef8351c1ac863bfaeb9 diff --git a/src/sage/__init__.py b/src/sage/__init__.py index 2e6e020..e5e59e3 100644 --- a/src/sage/__init__.py +++ b/src/sage/__init__.py @@ -1 +1 @@ -__version__ = "2.5.8" +__version__ = "2.6.0" diff --git a/src/sage/_post_install.py b/src/sage/_post_install.py new file mode 100644 index 0000000..e0eb22c --- /dev/null +++ b/src/sage/_post_install.py @@ -0,0 +1,93 @@ +"""Post-install script: GitHub OAuth during pip install. + +Safely skipped in CI, Docker, headless, or non-interactive terminals. +""" + +import os +import sys +import time + + +_BANNER = """ +╔══════════════════════════════════════════════════════════════════════╗ +║ ║ +║ SAGE INSTALLED ║ +║ ║ +║ Enter the command below to activate: ║ +║ ║ +║ sage install ║ +║ ║ +║ Your AI agents will then use SAGE automatically. ║ +║ Every command runs compressed, tracked, and protected. ║ +║ ║ +╚══════════════════════════════════════════════════════════════════════╝ +""" + + +def _is_interactive() -> bool: + if "CI" in os.environ or "PYTEST_CURRENT_TEST" in os.environ: + return False + if not sys.stdout or not sys.stdout.isatty(): + return False + if not sys.stdin or not sys.stdin.isatty(): + return False + return True + + +def run_post_install() -> None: + if not _is_interactive(): + return + + try: + from .telemetry import auto_register_silent, load_config, save_config + + cfg = load_config() + + if cfg.get("api_endpoint") and cfg.get("api_key_id"): + return + + from .github_oauth import github_oauth_flow + + print() + print("SAGE needs GitHub authentication to connect this machine.") + print("A browser window will open for you to authorize.") + print() + + result = github_oauth_flow() + + api_key = str(result.get("api_key") or "") + key_id = str(result.get("key_id") or "") + github_username = str(result.get("github_username") or "") + + if not api_key or not key_id: + return + + from . import telemetry + + base = telemetry.DEFAULT_API_BASE_URL + cfg["api_base_url"] = base + cfg["api_endpoint"] = f"{base}/v1/telemetry" + cfg["api_key_id"] = key_id + telemetry._store_api_key(cfg, api_key, key_id) + cfg["api_profile"] = { + "display_name": result.get("display_name", github_username), + "username": github_username, + "github_id": int(result.get("github_id", 0)), + "public_profile": True, + "scope": "personal", + } + cfg["telemetry_level"] = 1 + save_config(cfg) + + telemetry.account_link( + github_username, + user_id=str(result.get("github_id", "")), + api_key_ref=key_id, + key_max_level=1, + ) + telemetry.account_use(github_username) + + except Exception: + pass + + print(_BANNER) diff --git a/src/sage/cli.py b/src/sage/cli.py index be82469..008a6a8 100644 --- a/src/sage/cli.py +++ b/src/sage/cli.py @@ -37,6 +37,7 @@ "firewall", "fix", "github-bot", + "github-login", "glob", "grep", "gui", @@ -123,6 +124,13 @@ def build_parser() -> argparse.ArgumentParser: action="store_true", help="Run the command with inherited terminal stdin/stdout/stderr for interactive CLIs.", ) + run.add_argument( + "--cwd", + help=( + "Run from this working directory. Desktop/Electron hosts may also " + "set SAGE_WORKSPACE_CWD." + ), + ) run.add_argument("command", nargs=argparse.REMAINDER, help="Command to run after --") predict_parser = sub.add_parser("predict", help="Predict whether a command is likely to fail.") @@ -325,6 +333,12 @@ def build_parser() -> argparse.ArgumentParser: connect_parser.add_argument("--display-name", help="Optional display name (defaults to GitHub name).") connect_parser.add_argument("--public-profile", action="store_true", help="Show your name on public proof.") connect_parser.add_argument("--expiry-days", type=int, choices=[30, 60, 90], help="API key expiration in days.") + connect_parser.add_argument("--github", action="store_true", help="Use GitHub OAuth instead of machine auth.") + + github_login_parser = sub.add_parser("github-login", help="Authenticate with GitHub OAuth and create a SAGE API key.") + github_login_parser.add_argument("--display-name", help="Optional display name (defaults to GitHub name).") + github_login_parser.add_argument("--public-profile", action="store_true", help="Show your name on public proof.") + github_login_parser.add_argument("--expiry-days", type=int, choices=[30, 60, 90], help="API key expiration in days.") login_parser = sub.add_parser("login", help="Create and store a free SAGE API key.") _add_login_args(login_parser) @@ -583,9 +597,17 @@ def setup_command(force: bool = False) -> int: cloud_connected = False print() print("Cloud connection: connecting automatically. If cloud is unreachable, SAGE stays local and retries later.") + print() try: - connect_args = argparse.Namespace(expiry_days=30, display_name=display_name, endpoint="", auto_only=True) - cloud_connected = connect_command(connect_args) == 0 + from . import telemetry + + config = telemetry.load_config() + if config.get("api_key_id"): + connect_args = argparse.Namespace(expiry_days=30, display_name=display_name, endpoint="", auto_only=True) + cloud_connected = connect_command(connect_args) == 0 + else: + connect_args = argparse.Namespace(expiry_days=30, display_name=display_name, endpoint="", auto_only=True) + cloud_connected = connect_command(connect_args) == 0 except Exception as exc: print(f"Cloud connection skipped: {exc}") print("Queued telemetry will send later after automatic setup reconnects.") @@ -676,7 +698,7 @@ def _ensure_first_run_setup(command_name: str | None) -> int: """ if os.environ.get("SAGE_SKIP_SETUP") == "1": return 0 - if command_name in {"setup", "activate", "install", "connect", "login", "logout"}: + if command_name in {"setup", "activate", "install", "connect", "login", "logout", "github-login"}: return 0 if _read_setup_state().get("completed"): return 0 @@ -747,6 +769,7 @@ def main(argv: list[str] | None = None) -> int: policy_mode=args.policy_mode, dry_run=args.dry_run, pty=args.pty, + cwd=args.cwd, ) if args.command_name == "predict": @@ -858,6 +881,9 @@ def main(argv: list[str] | None = None) -> int: if args.command_name == "connect": return connect_command(args) + if args.command_name == "github-login": + return github_login_command(args) + if args.command_name == "login": return login_command(args) @@ -1873,15 +1899,31 @@ def connect_command(args) -> int: result = None method = "" - print("Trying SAGE machine authentication...") - try: - result = telemetry.api_machine_login(expiry_days=expiry_days, display_name=display_name) - method = "hardware" - print(" Machine auth successful!") - except Exception as hw_exc: - print(f" Machine auth unavailable: {hw_exc}") - print("SAGE remains usable locally. Automatic setup will retry on repair/force setup.") - return 1 + use_github = getattr(args, "github", False) + if use_github: + print("Using GitHub OAuth...") + try: + result = telemetry.api_github_login( + display_name=display_name, + public_profile=bool(getattr(args, "public_profile", False)), + expiry_days=expiry_days, + ) + method = "github" + print(" GitHub auth successful!") + except Exception as gh_exc: + print(f" GitHub auth failed: {gh_exc}") + print("SAGE remains usable locally.") + return 1 + else: + print("Trying SAGE machine authentication...") + try: + result = telemetry.api_machine_login(expiry_days=expiry_days, display_name=display_name) + method = "hardware" + print(" Machine auth successful!") + except Exception as hw_exc: + print(f" Machine auth unavailable: {hw_exc}") + print("SAGE remains usable locally. Automatic setup will retry on repair/force setup.") + return 1 # Verify with server print("\nVerifying with server...") @@ -1937,6 +1979,65 @@ def connect_command(args) -> int: print() return 0 +def github_login_command(args) -> int: + """Authenticate using GitHub OAuth and create a SAGE API key.""" + from . import telemetry + + print("SAGE GitHub Login") + print("=" * 60) + print() + + display_name = str(getattr(args, "display_name", "") or "").strip() + if not display_name: + display_name = _machine_display_name("") + + try: + result = telemetry.api_github_login( + display_name=display_name, + public_profile=bool(getattr(args, "public_profile", False)), + expiry_days=getattr(args, "expiry_days", 365) or 365, + ) + except Exception as exc: + print(f"GitHub login failed: {exc}") + print("SAGE remains usable locally.") + return 1 + + # Verify with server + print("\nVerifying with server...") + verified = False + for _ in range(3): + try: + whoami = telemetry.api_whoami() + if whoami.get("server_verified"): + verified = True + break + except Exception: + pass + import time + time.sleep(1) + + from .install import install_sage_system_wide, is_sage_installed_system_wide + if not is_sage_installed_system_wide(): + install_sage_system_wide() + + key_storage = result.get("api_key_storage") or telemetry.load_config().get("api_key_storage", "file") + print() + print("=" * 60) + print(" SAGE CONNECTED WITH GITHUB") + print("=" * 60) + print(f" Identity: {result.get('username', 'unknown')}") + print(f" Key ID: {result.get('key_id')}") + print(f" Storage: {key_storage}") + print(f" Expires: {result.get('expires_at')}") + print(f" Server: {'VERIFIED' if verified else 'PENDING'}") + print(f" Telemetry: ON (anonymous metrics only)") + print("=" * 60) + print() + print(" Every command your AI agents run is now tracked, compressed,") + print(" and protected.") + return 0 + + def rotate_key_command(args) -> int: """Rotate API key through SAGE machine authentication.""" from . import telemetry diff --git a/src/sage/context/manager.py b/src/sage/context/manager.py index a889f27..7e56a93 100644 --- a/src/sage/context/manager.py +++ b/src/sage/context/manager.py @@ -78,7 +78,13 @@ def process_command_output( 'original_tokens': original_tokens, 'compressed_tokens': compressed_tokens, 'strategy': result.strategy, - 'verified_tokenizer': self.compressor.get_stats().get("verified_tokenizer", "unknown"), + # Do not load the full tokenizer for commands that produced no + # output. This keeps silent wrapped commands fast. + 'verified_tokenizer': ( + "n/a" + if original_tokens == 0 + else self.compressor.get_stats().get("verified_tokenizer", "unknown") + ), } def smart_file_read( diff --git a/src/sage/github_oauth.py b/src/sage/github_oauth.py new file mode 100644 index 0000000..6dbc105 --- /dev/null +++ b/src/sage/github_oauth.py @@ -0,0 +1,127 @@ +"""GitHub OAuth for SAGE API authentication — server-side callback flow. + +The client: +1. POSTs to /v1/github-auth/start → gets a session_id + authorize_url +2. Opens the browser to GitHub's authorize page +3. User authenticates on GitHub +4. GitHub redirects to api.marketingstudios.in/auth/github/callback +5. API exchanges code for token, creates SAGE API key, stores in session +6. CLI polls /v1/github-auth/status?session= until completed +""" + +from __future__ import annotations + +import json +import time +import urllib.request +import urllib.parse +import urllib.error +import webbrowser +from typing import Any + +DEFAULT_API_BASE = "https://sage.api.marketingstudios.in" + + +def github_oauth_flow( + *, + api_base: str = DEFAULT_API_BASE, + install_id: str = "", + client_version: str = "", + platform: str = "", + poll_interval: float = 2.0, + timeout: int = 300, +) -> dict[str, Any]: + """Run GitHub OAuth with server-side callback and return the SAGE API key info.""" + print("Starting GitHub authentication...") + print(f"API: {api_base}") + + # 1. Start OAuth session + session = _start_session(api_base, install_id, client_version, platform) + session_id = session["session_id"] + authorize_url = session["authorize_url"] + + print(f"Session: {session_id}") + print() + print("Opening browser for GitHub login...") + print(f"If browser does not open, visit:") + print(f" {authorize_url}") + print() + webbrowser.open(authorize_url) + + # 2. Poll for completion + print("Waiting for GitHub authorization...") + print("(Complete the authorization in your browser)") + deadline = time.time() + timeout + last_status = "" + while time.time() < deadline: + result = _poll_status(api_base, session_id) + status = result.get("status", "pending") + + if status != last_status and status == "pending": + pass + last_status = status + + if status == "completed": + print() + print("GitHub authorization successful!") + return result + + if status == "error": + error_msg = result.get("error", "Unknown error") + raise RuntimeError(f"GitHub OAuth failed: {error_msg}") + + time.sleep(poll_interval) + + raise RuntimeError("GitHub authentication timed out.") + + +def _start_session( + api_base: str, + install_id: str = "", + client_version: str = "", + platform: str = "", +) -> dict[str, Any]: + payload: dict[str, Any] = {} + if install_id: + payload["installation_id"] = install_id + if client_version: + payload["client_version"] = client_version + if platform: + payload["platform"] = platform + + data = json.dumps(payload).encode("utf-8") + request = urllib.request.Request( + f"{api_base}/v1/github-auth/start", + data=data, + headers={ + "Content-Type": "application/json", + "User-Agent": "SAGE-CLI/0.1", + }, + method="POST", + ) + try: + with urllib.request.urlopen(request, timeout=20) as response: + body = json.loads(response.read().decode("utf-8")) + except urllib.error.HTTPError as exc: + detail = exc.read().decode("utf-8", errors="replace") + raise RuntimeError(f"Failed to start OAuth session: HTTP {exc.code} {detail}") + except OSError as exc: + raise RuntimeError(f"Cannot reach SAGE API: {exc}") + + if not body.get("ok"): + raise RuntimeError(body.get("error", "Failed to start OAuth session")) + return body + + +def _poll_status(api_base: str, session_id: str) -> dict[str, Any]: + url = f"{api_base}/v1/github-auth/status?session={urllib.parse.quote(session_id)}" + try: + with urllib.request.urlopen(url, timeout=15) as response: + return json.loads(response.read().decode("utf-8")) + except urllib.error.HTTPError as exc: + if exc.code == 404: + return {"status": "error", "error": "Session expired"} + detail = exc.read().decode("utf-8", errors="replace") + return {"status": "error", "error": f"HTTP {exc.code}: {detail}"} + except OSError as exc: + return {"status": "error", "error": str(exc)} diff --git a/src/sage/mcp/client.py b/src/sage/mcp/client.py index 3872a1a..e039ce6 100644 --- a/src/sage/mcp/client.py +++ b/src/sage/mcp/client.py @@ -26,10 +26,12 @@ from dataclasses import dataclass, field from typing import Any +from .. import __version__ + log = logging.getLogger(__name__) PROTOCOL_VERSION = "2024-11-05" -CLIENT_INFO = {"name": "psycgod-sage", "version": "2.0.4"} +CLIENT_INFO = {"name": "psycgod-sage", "version": __version__} @dataclass class MCPConnectionResult: diff --git a/src/sage/mcp/server.py b/src/sage/mcp/server.py index aa7b96a..84fec86 100644 --- a/src/sage/mcp/server.py +++ b/src/sage/mcp/server.py @@ -12,6 +12,8 @@ from pathlib import Path from typing import Any, Optional +from .. import __version__ + from .tools import ( SAGE_TOOLS, @@ -38,7 +40,7 @@ log = logging.getLogger(__name__) PROTOCOL_VERSION = "2024-11-05" -SERVER_INFO = {"name": "sage", "version": "2.0.4"} +SERVER_INFO = {"name": "sage", "version": __version__} COMMAND_TOOL_NAMES = {"sage_run_command"} # 0 disables the idle watchdog. Clients like Claude Code hold one stdio server # for the whole session and only call tools intermittently; a short idle exit diff --git a/src/sage/runner.py b/src/sage/runner.py index b8333ee..a43aaa4 100644 --- a/src/sage/runner.py +++ b/src/sage/runner.py @@ -2,6 +2,7 @@ import logging import os +import shutil import subprocess import sys import queue @@ -26,7 +27,9 @@ def _build_popen_cmd(command_text: str, command_parts: list[str]) -> tuple: On non-Windows: returns (command_parts, False). On Windows + SAGE_SHELL set: uses that shell explicitly (e.g. pwsh, powershell). - On Windows default: cmd.exe via shell=True. + On Windows default: launch native executables directly and use cmd.exe only + for batch files and shell built-ins. Avoiding ``shell=True`` prevents a + second parsing pass from corrupting quoted PowerShell/Node arguments. To run PowerShell built-ins through SAGE, set SAGE_SHELL=pwsh (or powershell). """ @@ -37,7 +40,26 @@ def _build_popen_cmd(command_text: str, command_parts: list[str]) -> tuple: if shell_override: return [shell_override, "-NoProfile", "-Command", command_text], False - return command_text, True + resolved = shutil.which(command_parts[0]) if command_parts else None + if resolved and Path(resolved).suffix.lower() not in {".bat", ".cmd"}: + return command_parts, False + + comspec = os.environ.get("COMSPEC", "cmd.exe") + return [comspec, "/d", "/s", "/c", command_text], False + + +def _resolve_working_directory(cwd: str | os.PathLike[str] | None = None) -> Path: + """Resolve the project directory supplied by a CLI or desktop host.""" + candidate = ( + str(cwd).strip() + if cwd is not None + else os.environ.get("SAGE_WORKSPACE_CWD", "").strip() + or str(Path.cwd()) + ) + path = Path(candidate).expanduser().resolve() + if not path.is_dir(): + raise NotADirectoryError(f"SAGE working directory does not exist: {path}") + return path def _configure_stdio() -> None: @@ -72,12 +94,19 @@ def run_command( session_id: str = "", is_ai_session: int = 0, pty: bool = False, + cwd: str | os.PathLike[str] | None = None, ) -> int: _configure_stdio() if not command_parts: print("No command was provided. Example: sage run -- python --version") return 2 + try: + working_dir = _resolve_working_directory(cwd) + except (OSError, ValueError) as exc: + print(f"[sage] {exc}", file=sys.stderr) + return 2 + command_text = subprocess.list2cmdline(command_parts) decision = evaluate_command(command_text, mode=policy_mode, dry_run=dry_run) if decision.decision == "dry_run": @@ -116,7 +145,7 @@ def run_command( env = os.environ.copy() env.setdefault("PYTHONIOENCODING", "utf-8") env.setdefault("PYTHONUTF8", "1") - env.setdefault("SAGE_CALLER_CWD", str(Path.cwd())) + env["SAGE_CALLER_CWD"] = str(working_dir) if pty: return _run_interactive_passthrough( @@ -129,6 +158,7 @@ def run_command( kind_override=kind_override, session_id=session_id, is_ai_session=is_ai_session, + working_dir=working_dir, ) popen_args, use_shell = _build_popen_cmd(command_text, command_parts) @@ -142,6 +172,7 @@ def run_command( encoding="utf-8", errors="replace", env=env, + cwd=str(working_dir), ) output_queue: queue.Queue[tuple[str, str | None]] = queue.Queue() @@ -279,7 +310,7 @@ def enqueue_stream(stream, stream_name: str) -> None: os.environ["SAGE_SESSION_ID"] = session_id run_id = save_run( - project=str(Path.cwd()), + project=str(working_dir), command=command_text, exit_code=returncode, duration_ms=duration_ms, @@ -298,7 +329,7 @@ def enqueue_stream(stream, stream_name: str) -> None: command_kind=kind_override or command_class.kind, command_family=command_class.family, caller=caller, - workspace_hash=workspace_hash(str(Path.cwd())), + workspace_hash=workspace_hash(str(working_dir)), session_id=session_id, is_ai_session=is_ai_session, ) @@ -372,7 +403,11 @@ def _run_agents_background(): original_tokens = int(result.get("original_tokens", saved_tokens)) compressed_tokens = int(result.get("compressed_tokens", max(0, original_tokens - saved_tokens))) strategy = str(result.get("strategy", "auto")) - tokenizer = "tiktoken" if is_real_tokenizer() else "fallback" + tokenizer = ( + "n/a" + if original_tokens == 0 + else ("tiktoken" if is_real_tokenizer() else "fallback") + ) with connect() as conn: conn.execute( @@ -461,6 +496,7 @@ def _run_interactive_passthrough( kind_override: str, session_id: str, is_ai_session: int, + working_dir: Path, ) -> int: """Run an interactive command without pipes so the child owns the TTY. @@ -508,6 +544,7 @@ def _run_interactive_passthrough( popen_args, shell=use_shell, env=env, + cwd=str(working_dir), ) except KeyboardInterrupt: returncode = 130 @@ -515,7 +552,7 @@ def _run_interactive_passthrough( duration_ms = int((time.perf_counter() - started) * 1000) summary = "Interactive PTY command; live output was not captured." run_id = save_run( - project=str(Path.cwd()), + project=str(working_dir), command=command_text, exit_code=returncode, duration_ms=duration_ms, @@ -534,7 +571,7 @@ def _run_interactive_passthrough( command_kind=kind_override or command_class.kind, command_family=command_class.family, caller=caller, - workspace_hash=workspace_hash(str(Path.cwd())), + workspace_hash=workspace_hash(str(working_dir)), session_id=session_id, is_ai_session=is_ai_session, ) diff --git a/src/sage/telemetry.py b/src/sage/telemetry.py index e055682..721d9d9 100644 --- a/src/sage/telemetry.py +++ b/src/sage/telemetry.py @@ -195,6 +195,130 @@ def api_machine_login(*, expiry_days: int = 30, display_name: str = "", base_url raise RuntimeError("Could not reach SAGE API server.") +def api_github_login( + *, + display_name: str | None = None, + public_profile: bool = False, + expiry_days: int = 365, + base_url: str = "", +) -> dict[str, Any]: + """Create SAGE API key using GitHub OAuth with server-side callback flow. + + This opens the browser for GitHub OAuth. The API handles the entire + OAuth exchange server-side — no local server or client secret needed. + """ + from .github_oauth import github_oauth_flow + + config = load_config() + base = base_url or config.get("api_base_url") or DEFAULT_API_BASE_URL + + result = github_oauth_flow( + api_base=base, + install_id=config.get("installation_id", ""), + client_version=_client_version(), + platform=_platform(), + ) + + api_key = str(result.get("api_key") or "") + key_id = str(result.get("key_id") or "") + github_username = str(result.get("github_username") or "") + github_id = int(result.get("github_id") or 0) + + if not api_key or not key_id: + raise RuntimeError("SAGE API did not return an API key.") + + config["api_base_url"] = base + config["api_endpoint"] = f"{base}/v1/telemetry" + config["api_key_id"] = key_id + storage = _store_api_key(config, api_key, key_id) + config["api_profile"] = { + "display_name": result.get("display_name", github_username), + "username": github_username, + "github_id": github_id, + "public_profile": bool(public_profile), + "scope": "personal", + } + config["telemetry_level"] = 1 + save_config(config) + + account_link( + github_username, + user_id=str(github_id), + api_key_ref=key_id, + key_max_level=1, + ) + account_use(github_username) + + return { + "ok": True, + "base_url": base, + "endpoint": f"{base}/v1/telemetry", + "api_key_redacted": _redact_key(api_key), + "key_id": key_id, + "username": github_username, + "github_id": github_id, + "display_name": result.get("display_name", github_username), + "expires_at": result.get("expires_at", ""), + "public_profile": bool(public_profile), + "effective_level": 1, + "effective_level_name": "Level 1 (safe metrics only)", + "api_key_storage": storage, + } + + +def auto_register_silent() -> bool: + """Attempt machine login silently — no console output, no user prompt. + + Called automatically when telemetry has queued events but no endpoint is + configured. If the server is reachable this registers the machine, sets + telemetry level to 1, and unlocks the dashboard funnel. If it fails + (offline, server down, already registered) it's a silent no-op. + """ + try: + cfg = load_config() + if cfg.get("api_endpoint") and resolve_api_key(cfg): + return True + from urllib import request as urllib_request, error as urllib_error + import json + fp = _machine_fingerprint() + payload = { + "fingerprint": fp["fingerprint"], + "hostname": fp["hostname"], + "platform": fp["platform"], + "installation_id": cfg["installation_id"], + "client_version": _client_version(), + "expiry_days": 365, + } + data = json.dumps(payload).encode("utf-8") + req = urllib_request.Request( + f"{DEFAULT_API_BASE_URL}/v1/machine-login", + data=data, + headers={"Content-Type": "application/json", "User-Agent": "SAGE-CLI/0.1"}, + method="POST", + ) + with urllib_request.urlopen(req, timeout=10) as resp: + response = json.loads(resp.read().decode()) + api_key = str(response.get("api_key") or "") + key_id = str(response.get("key_id") or "") + if not api_key or not key_id: + return False + cfg["api_base_url"] = DEFAULT_API_BASE_URL + cfg["api_endpoint"] = f"{DEFAULT_API_BASE_URL}/v1/telemetry" + cfg["api_key_id"] = key_id + storage = _store_api_key(cfg, api_key, key_id) + cfg["api_profile"] = { + "display_name": fp["hostname"], + "username": fp["hostname"], + "public_profile": False, + "scope": "machine", + } + cfg["telemetry_level"] = max(1, int(cfg.get("telemetry_level", 0))) + save_config(cfg) + return True + except Exception: + return False + + def _agent_client(command: str, caller: str = "") -> str: first = (str(command or "").strip().split(maxsplit=1) or [""])[0] name = Path(first).name.lower() @@ -652,40 +776,30 @@ def maybe_sync_after_run(run_id: int, *, snapshot_every: int = 10) -> dict[str, "snapshot_due": False, "snapshot": None, } - try: - from .telemetry_sender import spawn_background_sender - - result["background_sender_spawned"] = bool(spawn_background_sender()) - except Exception as exc: - result["background_sender_error"] = str(exc) - snapshot_every = max(1, int(snapshot_every or 10)) state = _read_sync_state() last_snapshot_run_id = int(state.get("last_snapshot_run_id") or 0) due = run_id >= snapshot_every and run_id - last_snapshot_run_id >= snapshot_every result["snapshot_due"] = due - if not due: - return result + if due: + now = _now() + state["last_snapshot_run_id"] = int(run_id) + state["last_snapshot_scheduled_at"] = now + result["snapshot"] = {"scheduled": True} + try: + _write_sync_state(state) + except Exception: + pass - now = _now() - state["last_snapshot_attempt_run_id"] = int(run_id) - state["last_snapshot_attempt_at"] = now + # The detached sender already publishes proof snapshots before and after + # draining its batch. Never perform network I/O on the coding command's + # foreground path. try: - snapshot = send_proof_snapshot() - result["snapshot"] = snapshot - if snapshot.get("ok"): - state["last_snapshot_run_id"] = int(run_id) - state["last_snapshot_at"] = now - state["last_snapshot_error"] = "" - else: - state["last_snapshot_error"] = str(snapshot) + from .telemetry_sender import spawn_background_sender + + result["background_sender_spawned"] = bool(spawn_background_sender()) except Exception as exc: - result["snapshot"] = {"ok": False, "error": str(exc)} - state["last_snapshot_error"] = str(exc) - try: - _write_sync_state(state) - except Exception: - pass + result["background_sender_error"] = str(exc) return result diff --git a/src/sage/telemetry_sender.py b/src/sage/telemetry_sender.py index 20fde90..a3be8f2 100644 --- a/src/sage/telemetry_sender.py +++ b/src/sage/telemetry_sender.py @@ -13,15 +13,29 @@ log = logging.getLogger(__name__) +_auto_registered_this_session = False + +def _try_silent_register() -> bool: + global _auto_registered_this_session + if _auto_registered_this_session: + return True + from . import telemetry + ok = telemetry.auto_register_silent() + if ok: + _auto_registered_this_session = True + return ok + def send_batch_background(limit: int = 200) -> None: """Send a batch of queued telemetry events in background.""" try: from . import telemetry - # Quick check: if no API configured, exit immediately config = telemetry.load_config() if not config.get("api_endpoint") or not telemetry.resolve_api_key(config): - return + if _try_silent_register(): + config = telemetry.load_config() + else: + return snapshot_result = None try: diff --git a/tests/test_commands_and_telemetry.py b/tests/test_commands_and_telemetry.py index d3f9cb3..294993f 100644 --- a/tests/test_commands_and_telemetry.py +++ b/tests/test_commands_and_telemetry.py @@ -489,7 +489,9 @@ def fake_snapshot(): assert result["snapshot"] == {"ok": True} -def test_after_run_sync_publishes_snapshot_every_10_runs(isolated_telemetry, monkeypatch): +def test_after_run_sync_schedules_snapshot_without_foreground_network( + isolated_telemetry, monkeypatch +): t = isolated_telemetry calls = [] @@ -504,7 +506,7 @@ def test_after_run_sync_publishes_snapshot_every_10_runs(isolated_telemetry, mon assert t.maybe_sync_after_run(19)["snapshot_due"] is False assert t.maybe_sync_after_run(20)["snapshot_due"] is True - assert calls.count("snapshot") == 2 + assert calls.count("snapshot") == 0 assert calls.count("sender") == 4 diff --git a/tests/test_public_package_metadata.py b/tests/test_public_package_metadata.py index f70eee9..99f3ce3 100644 --- a/tests/test_public_package_metadata.py +++ b/tests/test_public_package_metadata.py @@ -1,8 +1,11 @@ from __future__ import annotations import pathlib +import runpy import tomllib +import yaml + def test_public_distribution_metadata(): data = tomllib.loads(pathlib.Path("pyproject.toml").read_text(encoding="utf-8")) @@ -11,3 +14,29 @@ def test_public_distribution_metadata(): assert data["project"]["scripts"]["sage"] == "sage.cli:main" assert "Development Status :: 4 - Beta" in data["project"]["classifiers"] assert any(dep.startswith("keyring") for dep in data["project"]["dependencies"]) + excluded = set(data["tool"]["setuptools"]["packages"]["find"]["exclude"]) + assert {"sage.gui", "sage.dashboard", "sage.tui"} <= excluded + + +def test_release_versions_are_synchronized(): + module = runpy.run_path("scripts/check_release_versions.py", run_name="release_check") + found = module["versions"]() + assert len(set(found.values())) == 1, found + + +def test_legacy_setup_has_no_install_side_effects(): + setup_text = pathlib.Path("setup.py").read_text(encoding="utf-8") + assert "cmdclass" not in setup_text + assert "PostInstallCommand" not in setup_text + + +def test_release_workflows_parse_and_publish_pypi_before_npm(): + for path in (".github/workflows/ci.yml", ".github/workflows/pypi-publish.yml"): + assert yaml.safe_load(pathlib.Path(path).read_text(encoding="utf-8")) + + workflow = pathlib.Path(".github/workflows/pypi-publish.yml").read_text( + encoding="utf-8" + ) + assert "publish-npm:" in workflow + assert "needs: publish" in workflow + assert "scripts/wait_for_pypi.py" in workflow diff --git a/tests/test_public_release_docs.py b/tests/test_public_release_docs.py index 6d0999d..13ac724 100644 --- a/tests/test_public_release_docs.py +++ b/tests/test_public_release_docs.py @@ -31,7 +31,8 @@ def test_readme_public_positioning(): assert "# SAGE" in readme assert "pip install psycgod-sage" in readme - assert "## Start Here: Install SAGE, Then Use Any AI Agent" in readme + assert "## Start Here:" in readme + assert "Then Use Any AI Agent" in readme assert "sage install" in readme assert "sage doctor --activation" in readme assert "npx -y psycgod-sage doctor --activation" in readme diff --git a/tests/test_runner_desktop.py b/tests/test_runner_desktop.py new file mode 100644 index 0000000..10dc36b --- /dev/null +++ b/tests/test_runner_desktop.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +import sys + +from sage.runner import _build_popen_cmd, run_command + + +def _quiet_runner(monkeypatch, tmp_path) -> None: + monkeypatch.setenv("LOCALAPPDATA", str(tmp_path / "local-data")) + monkeypatch.setenv("SAGE_DISABLE_PREDICT", "1") + monkeypatch.setenv("SAGE_AUTO_SEND_TELEMETRY", "0") + monkeypatch.setenv("SAGE_DISABLE_AGENTS", "1") + + +def test_explicit_cwd_supports_desktop_hosts(monkeypatch, tmp_path): + _quiet_runner(monkeypatch, tmp_path) + workspace = tmp_path / "electron workspace" + workspace.mkdir() + + exit_code = run_command( + [ + sys.executable, + "-c", + "from pathlib import Path; Path('cwd-proof.txt').write_text(str(Path.cwd()))", + ], + cwd=workspace, + ) + + assert exit_code == 0 + assert (workspace / "cwd-proof.txt").read_text(encoding="utf-8") == str( + workspace.resolve() + ) + + +def test_desktop_cwd_environment_is_supported(monkeypatch, tmp_path): + _quiet_runner(monkeypatch, tmp_path) + workspace = tmp_path / "desktop-env" + workspace.mkdir() + monkeypatch.setenv("SAGE_WORKSPACE_CWD", str(workspace)) + + exit_code = run_command( + [sys.executable, "-c", "from pathlib import Path; Path('env-proof.txt').touch()"] + ) + + assert exit_code == 0 + assert (workspace / "env-proof.txt").exists() + + +def test_invalid_explicit_cwd_fails_before_start(monkeypatch, tmp_path): + _quiet_runner(monkeypatch, tmp_path) + assert run_command([sys.executable, "-c", "pass"], cwd=tmp_path / "missing") == 2 + + +def test_windows_native_executable_avoids_shell_reparse(monkeypatch): + monkeypatch.setattr(sys, "platform", "win32") + monkeypatch.setattr("sage.runner.shutil.which", lambda _command: r"C:\\pwsh\\pwsh.exe") + parts = ["pwsh", "-Command", "Write-Output 'a|b'"] + + args, use_shell = _build_popen_cmd("unused", parts) + + assert args == parts + assert use_shell is False + + +def test_powershell_arguments_with_metacharacters_survive_on_windows( + monkeypatch, tmp_path +): + if not sys.platform.startswith("win"): + return + _quiet_runner(monkeypatch, tmp_path) + workspace = tmp_path / "quoted-command" + workspace.mkdir() + + exit_code = run_command( + [ + "powershell", + "-NoProfile", + "-Command", + "[IO.File]::WriteAllText('quoted.txt', 'a|b')", + ], + cwd=workspace, + ) + + assert exit_code == 0 + assert (workspace / "quoted.txt").read_text(encoding="utf-8") == "a|b"