diff --git a/.agents/skills/async-state-safety/SKILL.md b/.agents/skills/async-state-safety/SKILL.md new file mode 100644 index 000000000..47eded577 --- /dev/null +++ b/.agents/skills/async-state-safety/SKILL.md @@ -0,0 +1,47 @@ +--- +name: async-state-safety +description: This skill should be used when the user asks to change "worker lifecycle", "cancellation", "retrigger behavior", "state machine", "delivery receipts", "timeouts", or "race conditions". Enforces explicit async/state invariants and targeted race-safe verification. +--- + +# Async State Safety + +## Goal + +Prevent race regressions in async and stateful paths. + +## Required Invariants + +- Define valid terminal states before coding. +- Define allowed state transitions before coding. +- Keep terminal transitions idempotent. +- Ensure duplicate events cannot double-apply terminal effects. +- Ensure retries do not corrupt state. + +## Race Checklist + +- Cancellation racing completion +- Timeout racing completion +- Retry racing ack/receipt update +- Concurrent updates to the same worker/channel record +- Missing-handle and stale-handle behavior + +## Implementation Checklist + +- Add or update transition guards. +- Keep error handling explicit and structured. +- Preserve status/event emission on all terminal branches. +- Document why each race path converges safely. + +## Verification Checklist + +- Run targeted tests for each touched race path. +- Add at least one negative-path test for terminal convergence. +- Add at least one idempotency test where applicable. +- Run broad gate checks after targeted checks pass. + +## Handoff Requirements + +- Terminal states and transition matrix +- Race windows analyzed +- Targeted commands and outcomes +- Residual risks and follow-up tests diff --git a/.agents/skills/messaging-adapter-parity/SKILL.md b/.agents/skills/messaging-adapter-parity/SKILL.md new file mode 100644 index 000000000..9893dc52f --- /dev/null +++ b/.agents/skills/messaging-adapter-parity/SKILL.md @@ -0,0 +1,40 @@ +--- +name: messaging-adapter-parity +description: This skill should be used when the user asks to change "Slack adapter", "Telegram adapter", "Discord adapter", "Webhook adapter", "status delivery", "message routing", or "delivery receipts". Enforces cross-adapter parity and explicit delivery semantics. +--- + +# Messaging Adapter Parity + +## Goal + +Prevent adapter-specific regressions by validating behavior contracts across messaging backends. + +## Contract Areas + +- User-visible reply behavior +- Status update behavior (`surfaced` vs `not surfaced`) +- Delivery receipt ack/failure semantics +- Retry behavior and bounded backoff +- Error mapping and logging clarity + +## Parity Checklist + +- For every changed adapter path, compare expected behavior with at least one other adapter. +- If an adapter intentionally does not surface a status, ensure receipt handling still converges correctly. +- Ensure unsupported features degrade gracefully and predictably. +- Ensure worker terminal notices cannot loop indefinitely. + +## Verification Checklist + +- Run targeted tests for the touched adapter. +- Run targeted tests for receipt ack/failure paths. +- Run at least one parity comparison check across adapters. +- Run broad gate checks after targeted checks pass. + +## Required Handoff + +- Adapter paths changed +- Contract decisions made +- Receipt behavior outcomes +- Verification evidence +- Residual parity gaps diff --git a/.agents/skills/pr-gates/SKILL.md b/.agents/skills/pr-gates/SKILL.md new file mode 100644 index 000000000..0bcb22ae8 --- /dev/null +++ b/.agents/skills/pr-gates/SKILL.md @@ -0,0 +1,44 @@ +--- +name: pr-gates +description: This skill should be used when the user asks to "open a PR", "prepare for review", "address review comments", "run gates", or "verify before pushing" in this repository. Enforces preflight/gate workflow, migration safety, and review-evidence closure. +--- + +# PR Gates + +## Mandatory Flow + +1. Run `just preflight` before finalizing changes. +2. Run `just gate-pr` before pushing or updating a PR. +3. If the same command fails twice in one session, stop rerunning and switch to root-cause debugging. +4. Do not push when any gate is red. + +## Review Feedback Closure + +For every P1/P2 review finding, include all three: + +- Code change reference (file path and concise rationale) +- Targeted verification command +- Pass/fail evidence from that command + +## Async And Stateful Changes + +When touching worker lifecycle, cancellation, retries, state transitions, or caches: + +- Document terminal states and allowed transitions. +- Explicitly reason about race windows and idempotency. +- Run targeted tests in addition to broad gate runs. +- Capture the exact command proving the behavior. + +## Migration Safety + +- Never edit an existing file in `migrations/`. +- Add a new timestamped migration for every schema change. +- If a gate flags migration edits, stop and create a new migration file. + +## Handoff Format + +- Summary +- Changed files +- Gate commands executed +- P1/P2 finding-to-evidence mapping +- Residual risk diff --git a/.agents/skills/pr-slicer/SKILL.md b/.agents/skills/pr-slicer/SKILL.md new file mode 100644 index 000000000..06909fc18 --- /dev/null +++ b/.agents/skills/pr-slicer/SKILL.md @@ -0,0 +1,55 @@ +--- +name: pr-slicer +description: This skill should be used when the user asks to "split this PR", "make this smaller", "create stacked PRs", "slice this change", or "reduce review churn". Helps break work into low-risk, reviewable slices with clear verification per slice. +--- + +# PR Slicer + +## Goal + +Reduce review latency and rework by shipping smaller, independent slices. + +## Default Slice Budgets + +- Target `<= 400` changed lines per slice when practical. +- Target `<= 10` changed files per slice when practical. +- Target `1-4` commits per slice. +- Keep each slice behaviorally coherent and independently verifiable. + +## Slicing Order + +1. Extract prerequisites first. +2. Land mechanical refactors next. +3. Land behavior changes after prerequisites are merged. +4. Land UI/docs/polish last. + +## Slice Packet Template + +For each slice, define: + +- `Goal` +- `Owned files` +- `Out of scope` +- `Risk level` (`low`/`medium`/`high`) +- `Verification command(s)` with expected pass condition +- `Rollback plan` + +## Hard Rules + +- Avoid mixing refactor and behavior changes in one slice unless unavoidable. +- Avoid touching unrelated subsystems in one slice. +- Avoid cross-slice hidden dependencies. +- If a slice depends on unmerged work, state it explicitly. + +## Verification Discipline + +- Run narrow checks first for touched behavior. +- Run project gate checks before handoff. +- Record exact commands and outcomes for each slice. + +## Final Handoff Format + +- Slice list with order and purpose +- Per-slice owned files +- Per-slice verification evidence +- Residual risk and follow-up slices diff --git a/.agents/skills/provider-integration-checklist/SKILL.md b/.agents/skills/provider-integration-checklist/SKILL.md new file mode 100644 index 000000000..e14d79429 --- /dev/null +++ b/.agents/skills/provider-integration-checklist/SKILL.md @@ -0,0 +1,41 @@ +--- +name: provider-integration-checklist +description: This skill should be used when the user asks to add or modify an "LLM provider", "model routing", "OAuth flow", "auth token handling", "provider config", or "fallback chain". Enforces provider integration completeness across config, routing, docs, and verification. +--- + +# Provider Integration Checklist + +## Goal + +Ship provider and routing changes without regressions in auth, config, and model selection. + +## Change Checklist + +- Add config keys and defaults in one place. +- Validate config resolution order (`env > DB > default` where applicable). +- Validate model identifier parsing and normalization. +- Validate routing defaults, task overrides, and fallback chains. +- Validate auth flow behavior for both success and failure paths. +- Validate token/secret handling and redaction behavior. + +## Compatibility Checklist + +- Keep existing providers unaffected. +- Keep unknown-provider errors actionable. +- Keep provider-specific errors distinguishable. +- Keep docs and examples aligned with actual config keys. + +## Verification Checklist + +- Narrow tests for changed provider/routing/auth paths. +- Negative-path tests for invalid config and auth failures. +- Smoke path proving model call routes to expected backend. +- Broad gate checks after targeted checks pass. + +## Required Handoff + +- Config keys changed +- Routing behavior changed +- Auth behavior changed +- Verification evidence +- Docs updated diff --git a/.agents/skills/release-bump-changelog/SKILL.md b/.agents/skills/release-bump-changelog/SKILL.md new file mode 100644 index 000000000..0c7f90e74 --- /dev/null +++ b/.agents/skills/release-bump-changelog/SKILL.md @@ -0,0 +1,56 @@ +--- +name: release-bump-changelog +description: Use this skill when preparing a release bump or updating release notes. It writes a launch-style release story from the actual change set, then runs `cargo bump` so the generated GitHub notes and the marketing copy land together in `CHANGELOG.md`. +--- + +# Release Bump + Changelog + +## Goal + +Create a version bump commit where each release section includes both: + +- a launch-style narrative (marketing copy) +- the exact GitHub-generated release notes + +## Workflow + +1. Ensure the working tree is clean (except allowed release files). +2. Draft release story markdown from real changes (PR titles, release-note bullets, and diff themes). + - Target style: similar to the `v0.2.0` narrative (clear positioning + concrete highlights). + - Keep it factual and specific to the release. + - Write to a temp file (outside repo is preferred): + - `marketing_file="$(mktemp)"` + - write markdown content to `$marketing_file` +3. Run `cargo bump ` with marketing copy input: + - `SPACEBOT_RELEASE_MARKETING_COPY_FILE="$marketing_file" cargo bump <...>` + - This invokes `scripts/release-tag.sh`. + - The script generates GitHub-native notes (`gh api .../releases/generate-notes`). + - The script upserts `CHANGELOG.md` with: + - `### Release Story` (from your marketing file) + - GitHub-generated notes body + - The script includes `CHANGELOG.md` in the release commit. +4. Verify results: + - `git show --name-only --stat` + - Confirm commit contains `Cargo.toml`, `Cargo.lock` (if present), and `CHANGELOG.md`. + - Confirm tag was created (`git tag --list "v*" --sort=-v:refname | head -n 5`). + +## Requirements + +- `gh` CLI installed and authenticated (`gh auth status`). +- `origin` remote points to GitHub, or set `SPACEBOT_RELEASE_REPO=`. +- Marketing copy is required unless explicitly bypassed with `SPACEBOT_SKIP_MARKETING_COPY=1`. + +## Release Story Format + +Use markdown only (no outer `## vX.Y.Z` heading; script adds it). Recommended structure: + +1. One strong opening paragraph (why this release matters) +2. One paragraph on major technical shifts +3. Optional short highlight bullets for standout additions/fixes + +Avoid vague hype. Tie claims to concrete shipped changes. + +## Notes + +- Do not use a standalone changelog sync script. +- `CHANGELOG.md` is seeded from historical releases and then maintained by the release bump workflow. diff --git a/.agents/skills/review-fix-loop/SKILL.md b/.agents/skills/review-fix-loop/SKILL.md new file mode 100644 index 000000000..30314e881 --- /dev/null +++ b/.agents/skills/review-fix-loop/SKILL.md @@ -0,0 +1,47 @@ +--- +name: review-fix-loop +description: This skill should be used when the user asks to "address review feedback", "fix PR comments", "close findings", "respond to reviewer notes", or "reduce review churn". Enforces a finding-to-evidence closure loop for every review round. +--- + +# Review Fix Loop + +## Goal + +Close review rounds with minimal back-and-forth by mapping every finding to a code change and proof. + +## Execution Loop + +1. Parse findings into `P1`, `P2`, `P3`. +2. Build a closure table before editing. +3. Implement the smallest coherent fix batch. +4. Run targeted verification. +5. Update closure table with outcomes. +6. Run broader gate checks. + +## Closure Table + +Use one row per finding: + +| Finding | Severity | Owned files | Planned change | Verification command | Result | +|---|---|---|---|---|---| + +## Re-Run Control + +- If the same command fails twice, stop rerunning. +- Isolate a smaller reproduction command. +- Patch the smallest likely cause. +- Re-run narrow verification before broad checks. + +## Verification Ladder + +- Start narrow: unit/module behavior checks for touched paths. +- Continue medium: compile/lint/type checks for touched surfaces. +- End broad: repository gate commands. + +## Handoff Requirements + +- Findings closed +- Commands executed +- Pass/fail evidence per finding +- Remaining open findings with rationale +- Residual risk diff --git a/.dockerignore b/.dockerignore index 0e1a7bfc4..0291aae5e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -9,10 +9,25 @@ interface/node_modules/ interface/dist/ # Not needed for the build -docs/ tests/ scripts/ + +# Exclude docs except what's needed for self_awareness.rs embeds +docs/node_modules/ +docs/.next/ +docs/out/ +docs/app/ +docs/components/ +docs/lib/ +docs/design-docs/ +docs/*.json +docs/*.ts +docs/*.tsx +docs/*.mjs +docs/*.yaml +docs/*.lock +docs/.gitignore +docs/.node-version fly.toml -AGENTS.md RUST_STYLE_GUIDE.md LICENSE diff --git a/.github/workflows/interface-ci.yml b/.github/workflows/interface-ci.yml new file mode 100644 index 000000000..a8478caed --- /dev/null +++ b/.github/workflows/interface-ci.yml @@ -0,0 +1,40 @@ +name: Interface CI + +on: + push: + branches: [main] + paths: + - "interface/**" + - ".github/workflows/interface-ci.yml" + pull_request: + branches: [main] + paths: + - "interface/**" + - ".github/workflows/interface-ci.yml" + +permissions: + contents: read + +concurrency: + group: interface-ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + quality: + name: Interface Quality + runs-on: ubuntu-24.04 + defaults: + run: + working-directory: interface + steps: + - uses: actions/checkout@v4 + + - uses: oven-sh/setup-bun@v1 + with: + bun-version: "1.3" + + - name: Install dependencies + run: bun ci + + - name: Typecheck + run: bunx tsc --noEmit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f6cf8479f..a97fd8165 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,10 +12,66 @@ on: env: REGISTRY: ghcr.io - IMAGE: ghcr.io/spacedriveapp/spacebot + IMAGE: ghcr.io/${{ github.repository_owner }}/spacebot jobs: - build: + build-binaries: + strategy: + matrix: + include: + - target: x86_64-unknown-linux-gnu + runner: ubuntu-24.04 + archive: tar.gz + - target: aarch64-unknown-linux-gnu + runner: ubuntu-24.04-arm + archive: tar.gz + runs-on: ${{ matrix.runner }} + permissions: + contents: write + outputs: + version: ${{ steps.version.outputs.version }} + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Install protoc + run: sudo apt-get update && sudo apt-get install -y protobuf-compiler + + - name: Determine version tag + id: version + run: | + if [ -n "${{ github.event.inputs.tag }}" ]; then + VERSION="${{ github.event.inputs.tag }}" + elif [[ "$GITHUB_REF" == refs/tags/v* ]]; then + VERSION="${GITHUB_REF#refs/tags/}" + else + VERSION="dev-$(echo $GITHUB_SHA | head -c 7)" + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Build binary + run: cargo build --release --target ${{ matrix.target }} + + - name: Package binary + run: | + cd target/${{ matrix.target }}/release + mkdir -p spacebot-${{ steps.version.outputs.version }}-${{ matrix.target }} + cp spacebot spacebot-${{ steps.version.outputs.version }}-${{ matrix.target }}/ + tar -czvf ../../../spacebot-${{ steps.version.outputs.version }}-${{ matrix.target }}.${{ matrix.archive }} spacebot-${{ steps.version.outputs.version }}-${{ matrix.target }} + + - name: Upload binary artifact + uses: actions/upload-artifact@v4 + with: + name: spacebot-${{ matrix.target }} + path: spacebot-${{ steps.version.outputs.version }}-${{ matrix.target }}.${{ matrix.archive }} + retention-days: 1 + + build-docker: strategy: matrix: include: @@ -59,30 +115,18 @@ jobs: id: platform run: echo "pair=$(echo ${{ matrix.platform }} | tr '/' '-')" >> "$GITHUB_OUTPUT" - - name: Build and push slim + - name: Build and push uses: docker/build-push-action@v6 with: context: . - target: slim platforms: ${{ matrix.platform }} push: true - tags: ${{ env.IMAGE }}:slim-${{ steps.platform.outputs.pair }} - cache-from: type=gha,scope=slim-${{ steps.platform.outputs.pair }} - cache-to: type=gha,mode=max,scope=slim-${{ steps.platform.outputs.pair }} + tags: ${{ env.IMAGE }}:build-${{ steps.platform.outputs.pair }} + cache-from: type=gha,scope=build-${{ steps.platform.outputs.pair }} + cache-to: type=gha,mode=max,scope=build-${{ steps.platform.outputs.pair }} - - name: Build and push full - uses: docker/build-push-action@v6 - with: - context: . - target: full - platforms: ${{ matrix.platform }} - push: true - tags: ${{ env.IMAGE }}:full-${{ steps.platform.outputs.pair }} - cache-from: type=gha,scope=full-${{ steps.platform.outputs.pair }} - cache-to: type=gha,mode=max,scope=full-${{ steps.platform.outputs.pair }} - - merge: - needs: build + merge-docker: + needs: build-docker runs-on: ubuntu-24.04 permissions: contents: write @@ -99,24 +143,16 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Create slim multi-arch manifest - run: | - docker buildx imagetools create \ - --tag ${{ env.IMAGE }}:${{ needs.build.outputs.version }}-slim \ - --tag ${{ env.IMAGE }}:slim \ - ${{ env.IMAGE }}:slim-linux-amd64 \ - ${{ env.IMAGE }}:slim-linux-arm64 - - - name: Create full multi-arch manifest + - name: Create multi-arch manifest run: | docker buildx imagetools create \ - --tag ${{ env.IMAGE }}:${{ needs.build.outputs.version }}-full \ - --tag ${{ env.IMAGE }}:full \ + --tag ${{ env.IMAGE }}:${{ needs.build-docker.outputs.version }} \ --tag ${{ env.IMAGE }}:latest \ - ${{ env.IMAGE }}:full-linux-amd64 \ - ${{ env.IMAGE }}:full-linux-arm64 + ${{ env.IMAGE }}:build-linux-amd64 \ + ${{ env.IMAGE }}:build-linux-arm64 - name: Log in to Fly registry + if: github.repository_owner == 'spacedriveapp' uses: docker/login-action@v3 with: registry: registry.fly.io @@ -124,23 +160,47 @@ jobs: password: ${{ secrets.FLY_API_TOKEN }} - name: Push to Fly registry + if: github.repository_owner == 'spacedriveapp' run: | docker buildx imagetools create \ - --tag registry.fly.io/spacebot-image:${{ needs.build.outputs.version }} \ + --tag registry.fly.io/spacebot-image:${{ needs.build-docker.outputs.version }} \ --tag registry.fly.io/spacebot-image:latest \ ${{ env.IMAGE }}:latest - name: Roll out to hosted instances - if: success() + if: github.repository_owner == 'spacedriveapp' run: | curl -sf -X POST https://api.spacebot.sh/api/admin/rollout \ -H "Content-Type: application/json" \ -H "X-Internal-Key: ${{ secrets.PLATFORM_INTERNAL_KEY }}" \ - -d '{"image_tag": "${{ needs.build.outputs.version }}"}' + -d '{"image_tag": "${{ needs.build-docker.outputs.version }}"}' + + create-release: + needs: build-binaries + if: startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-24.04 + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + + - name: Download all binary artifacts + uses: actions/download-artifact@v4 + with: + path: binaries + pattern: spacebot-* + merge-multiple: false + + - name: Flatten artifacts + run: | + mkdir -p release-assets + find binaries -name "*.tar.gz" -exec cp {} release-assets/ \; + ls -la release-assets/ - name: Create GitHub Release - if: startsWith(github.ref, 'refs/tags/v') uses: softprops/action-gh-release@v2 with: - tag_name: ${{ needs.build.outputs.version }} + tag_name: ${{ needs.build-binaries.outputs.version }} generate_release_notes: true + files: release-assets/* diff --git a/AGENTS.md b/AGENTS.md index ed0d02135..093231346 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,12 +10,39 @@ Single binary. No server dependencies. Runs on tokio. All data lives in embedded **Stack:** Rust (edition 2024), tokio, Rig (v0.30.0, agentic loop framework), SQLite (sqlx), LanceDB (embedded vector + FTS), redb (embedded key-value). +## JavaScript Tooling (Critical) + +- For UI work in `spacebot/interface/`, use `bun` for all JS/TS package management and scripts. +- **NEVER** use `npm`, `pnpm`, or `yarn` in this repo unless the user explicitly asks for one. +- Standard commands: + - `bun install` + - `bun run dev` + - `bun run build` + - `bun run test` + - `bunx ` (instead of `npx `) + ## Migration Safety - **NEVER edit an existing migration file in place** once it has been committed or applied in any environment. - Treat migration files as immutable; modifying historical migrations causes checksum mismatches and can block startup. - For schema changes, always create a new migration with a new timestamp/version. +## Delivery Gates (Mandatory) + +Run these checks in this order for code changes before pushing or updating a PR: + +1. `just preflight` — validate git/remote/auth state and avoid push-loop churn. +2. `just gate-pr` — enforce formatting, compile checks, migration safety, lib tests, and integration test compile. + +If `just` is unavailable, run the equivalent scripts directly in the same order: `./scripts/preflight.sh` then `./scripts/gate-pr.sh`. + +Additional rules: + +- If the same command fails twice in one session, stop rerunning it blindly. Capture root cause and switch strategy. +- For every external review finding marked P1/P2, add a targeted verification command in the final handoff. +- For changes in async/stateful paths (worker lifecycle, cancellation, retrigger, recall cache behavior), include explicit race/terminal-state reasoning in the PR summary and run targeted tests in addition to `just gate-pr`. +- Do not push if any gate is red. + ## Architecture Overview Five process types. Every LLM process is a Rig `Agent`. They differ in system prompt, tools, history, and hooks. @@ -40,7 +67,7 @@ Creating a branch is `let branch_history = channel_history.clone()`. The branch result is injected into the channel's history as a distinct message type. Then the branch is deleted. Multiple branches can run concurrently per channel (configurable limit). First done, first incorporated. -**Tools:** memory_recall, memory_save, channel_recall, spawn_worker +**Tools:** memory_recall, memory_save, memory_delete, channel_recall, spacebot_docs, task_create, task_list, task_update, spawn_worker **Context:** Clone of channel history at fork time **Lifecycle:** Short-lived. Returns a conclusion, then deleted. @@ -79,6 +106,7 @@ System-level observer. Primary job: generate the **memory bulletin** — a perio Also observes system-wide signals for future health monitoring and memory consolidation. **Tools (bulletin generation):** memory_recall, memory_save +**Tools (interactive cortex chat):** memory + worker tools, `spacebot_docs`, `config_inspect`, task board tools **Tools (future health monitoring):** memory_consolidate, system_monitor **Context:** Fresh per bulletin run. No compaction needed. @@ -146,12 +174,17 @@ src/ │ ├── react.rs — add emoji reaction (channel only) │ ├── memory_save.rs — write memory to store (branch + cortex + compactor) │ ├── memory_recall.rs— search + curate memories (branch only) -│ ├── channel_recall.rs— retrieve transcript from other channels (branch only) +│ ├── channel_recall.rs— retrieve transcript from any channel (branch only) │ ├── set_status.rs — update worker status (workers only) │ ├── shell.rs — execute shell commands (task workers) │ ├── file.rs — read/write/list files (task workers) │ ├── exec.rs — run subprocess (task workers) │ ├── browser.rs — web browsing (task workers) +│ ├── task_create.rs — create task-board task (branch + cortex chat) +│ ├── task_list.rs — list task-board tasks (branch + cortex chat) +│ ├── task_update.rs — update task-board task (branch + cortex chat) +│ ├── spacebot_docs.rs — read embedded Spacebot docs/changelog (branch + cortex chat) +│ ├── config_inspect.rs — inspect live runtime config (cortex chat) │ └── cron.rs — cron management (channel only) │ ├── memory.rs → memory/ @@ -258,7 +291,7 @@ let branch_history = channel_history.clone(); **ToolServer topology:** - Per-channel `ToolServer` (no memory tools, just channel action tools added per turn) -- Per-branch `ToolServer` with memory tools (memory_save, memory_recall) +- Per-branch `ToolServer` with memory tools (memory_save, memory_recall, memory_delete), channel recall, docs introspection (`spacebot_docs`), and task-board tools - Per-worker `ToolServer` with task-specific tools (shell, file, exec) - Per-cortex `ToolServer` with memory_save @@ -347,7 +380,7 @@ Phase 6 — Hardening: These are validated patterns from research (see `docs/research/pattern-analysis.md`). Implement them when building the relevant module. -**Tool nudging:** When an LLM responds with text instead of tool calls in the first 2 iterations, inject "Please proceed and use the available tools." Implement in `SpacebotHook.on_completion_response()`. Workers benefit most. +**Tool nudging / outcome gate:** Workers cannot exit with a text-only response until they signal a terminal outcome via `set_status(kind: "outcome")`. If a worker returns text without an outcome signal, the hook fires `Terminate` and retries with a nudge prompt (up to 2 retries). After retries are exhausted the worker fails with `PromptCancelled`. See `docs/design-docs/tool-nudging.md`. **Fire-and-forget DB writes:** `tokio::spawn` for conversation history saves, memory writes, worker log persistence. User gets their response immediately. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..972fd60d0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,409 @@ +# Changelog + +Seeded from GitHub releases; maintained by the release bump workflow. + +## v0.2.2 + +- Tag: `v0.2.2` +- Published: 2026-03-01T13:00:02Z +- URL: https://github.com/spacedriveapp/spacebot/releases/tag/v0.2.2 + +## What's Changed +* fix: infer default routing from configured provider by @jamiepine in https://github.com/spacedriveapp/spacebot/pull/266 +* Sandbox hardening: dynamic mode, env sanitization, leak detection by @jamiepine in https://github.com/spacedriveapp/spacebot/pull/259 +* Secret store: credential isolation, encryption at rest, output scrubbing by @jamiepine in https://github.com/spacedriveapp/spacebot/pull/260 +* feat: auto-download Chrome via fetcher, unify Docker image, fix singleton lock by @jamiepine in https://github.com/spacedriveapp/spacebot/pull/268 +* fix: preserve conversation history and improve worker retrigger reliability by @jamiepine in https://github.com/spacedriveapp/spacebot/pull/270 +* Add interface CI workflow by @marijnvdwerf in https://github.com/spacedriveapp/spacebot/pull/267 +* Split channel.rs and standardize adapter metadata keys by @jamiepine in https://github.com/spacedriveapp/spacebot/pull/271 +* fix: allow trustd mach service in macOS sandbox for TLS cert verification by @jamiepine in https://github.com/spacedriveapp/spacebot/pull/272 +* feat: add OpenRouter app attribution headers by @l33t0 in https://github.com/spacedriveapp/spacebot/pull/264 +* feat: implement link channels as task delegation (v3) by @jamiepine in https://github.com/spacedriveapp/spacebot/pull/255 + + +**Full Changelog**: https://github.com/spacedriveapp/spacebot/compare/v0.2.1...v0.2.2 + +## v0.2.1 + +- Tag: `v0.2.1` +- Published: 2026-02-27T11:54:42Z +- URL: https://github.com/spacedriveapp/spacebot/releases/tag/v0.2.1 + +## What's Changed +* Improve task UI overflow handling and docker update rollback by @fyzz-dev in https://github.com/spacedriveapp/spacebot/pull/237 +* fix Anthropic empty text blocks in retrigger flow by @jamiepine in https://github.com/spacedriveapp/spacebot/pull/243 +* Fix: Cleanup twitch_token.json when disconnecting Twitch platform by @Nebhay in https://github.com/spacedriveapp/spacebot/pull/212 +* feat: add email messaging adapter and setup docs by @jamiepine in https://github.com/spacedriveapp/spacebot/pull/244 +* fix: match installed skills by source repo, not just name by @mwmdev in https://github.com/spacedriveapp/spacebot/pull/205 +* chore: add delivery gates and repo-local pr-gates skill by @vsumner in https://github.com/spacedriveapp/spacebot/pull/238 +* feat(channel): add deterministic temporal context by @vsumner in https://github.com/spacedriveapp/spacebot/pull/239 +* feat: add IMAP email_search tool for branch read-back by @jamiepine in https://github.com/spacedriveapp/spacebot/pull/246 +* feat(cron): add strict wall-clock schedule support by @jamiepine in https://github.com/spacedriveapp/spacebot/pull/247 +* feat: named messaging adapter instances by @jamiepine in https://github.com/spacedriveapp/spacebot/pull/249 +* fix: log cross-channel messages to destination channel history by @jamiepine in https://github.com/spacedriveapp/spacebot/pull/252 +* add DeepWiki badge to README by @devabdultech in https://github.com/spacedriveapp/spacebot/pull/251 +* fix(cortex): harden startup warmup and bulletin coordination by @vsumner in https://github.com/spacedriveapp/spacebot/pull/248 +* feat: Download images as bytes for interpretation in Slack/Discord etc and fix Slack file ingestion by @egenvall in https://github.com/spacedriveapp/spacebot/pull/159 +* fix: make Ollama provider testable from settings UI by @jamiepine in https://github.com/spacedriveapp/spacebot/pull/253 + +## New Contributors +* @fyzz-dev made their first contribution in https://github.com/spacedriveapp/spacebot/pull/237 +* @devabdultech made their first contribution in https://github.com/spacedriveapp/spacebot/pull/251 + +**Full Changelog**: https://github.com/spacedriveapp/spacebot/compare/v0.2.0...v0.2.1 + +## v0.2.0 + +- Tag: `v0.2.0` +- Published: 2026-02-26T08:16:48Z +- URL: https://github.com/spacedriveapp/spacebot/releases/tag/v0.2.0 + +## v0.2.0 is the _biggest_ Spacebot release yet. + +The agent is no longer a single-channel chatbot, it's a multi-agent system with real orchestration primitives. + +Screenshot_2026-02-23_at_12 48 29_PM copy + +Agents coordinate through a spec-driven task system with a full kanban board in the UI. Tasks are structured markdown documents with requirements, constraints, and acceptance criteria. The cortex background loop picks up ready tasks, spawns workers, and handles completion or re-queuing on failure. Agents see the shared task board through the bulletin system, so delegation happens through specs, not conversation. + +Workers got a complete visibility overhaul. Full transcript persistence with gzip compression, live SSE streaming of tool calls as they happen, and a new worker_inspect tool so branches can verify what a worker actually did instead of trusting a one-line summary. + +On the security front, the old string-based command filtering (215+ lines of whack-a-mole regex) has been replaced with kernel-enforced filesystem sandboxing via bubblewrap on Linux and sandbox-exec on macOS. The LLM can't write outside the workspace because the OS won't let it. + +This release also brings OpenAI and Anthropic subscription auth support, better channel history preservation with deterministic retrigger handling, structured text payload blocking to keep raw JSON/XML out of user-facing messages, self-hosted update controls in the settings UI, new provider support (Kilo Gateway, OpenCode Go), prebuilt Linux binaries for amd64/arm64, a Nix flake, and a pile of fixes across cron scheduling, OAuth, model routing, and more. + +## What's Changed +* feat(nix): add Nix flake for building and deploying Spacebot by @skulldogged in https://github.com/spacedriveapp/spacebot/pull/47 +* Fix chatgpt oauth by @marijnvdwerf in https://github.com/spacedriveapp/spacebot/pull/187 +* Multi-agent communication graph by @jamiepine in https://github.com/spacedriveapp/spacebot/pull/150 +* Process sandbox: kernel-enforced filesystem containment for shell/exec by @jamiepine in https://github.com/spacedriveapp/spacebot/pull/188 +* Workers tab: full transcript viewer, live SSE streaming, introspection tool by @jamiepine in https://github.com/spacedriveapp/spacebot/pull/192 +* add settings update controls and harden self-hosted update flow by @jamiepine in https://github.com/spacedriveapp/spacebot/pull/207 +* feat(web): ui/ux cleanup by @skulldogged in https://github.com/spacedriveapp/spacebot/pull/143 +* feat(ci): publish binaries for linux/amd64 and linux/arm64 on release by @morgaesis in https://github.com/spacedriveapp/spacebot/pull/94 +* block structured text payloads from user replies by @jamiepine in https://github.com/spacedriveapp/spacebot/pull/209 +* Fix Z.AI Coding Plan model routing by @jamiepine in https://github.com/spacedriveapp/spacebot/pull/210 +* fix: use Bearer auth when key comes from ANTHROPIC_AUTH_TOKEN by @worldofgeese in https://github.com/spacedriveapp/spacebot/pull/196 +* fix: use Bearer auth for ANTHROPIC_AUTH_TOKEN and add ANTHROPIC_MODEL by @worldofgeese in https://github.com/spacedriveapp/spacebot/pull/197 +* Task tracking system with kanban UI and spec-driven delegation by @jamiepine in https://github.com/spacedriveapp/spacebot/pull/227 +* fix: make background result retriggers deterministic by @jamiepine in https://github.com/spacedriveapp/spacebot/pull/231 +* fix: guide users to enable device code login for ChatGPT OAuth by @mwmdev in https://github.com/spacedriveapp/spacebot/pull/214 +* fix(cron): make cron scheduler reliable under load and in containers by @mmmeff in https://github.com/spacedriveapp/spacebot/pull/186 +* Fix Z.AI coding-plan model remap for GLM-5 by @vsumner in https://github.com/spacedriveapp/spacebot/pull/223 +* fix: Default cron delivery target to current conversation by @jaaneh in https://github.com/spacedriveapp/spacebot/pull/213 +* feat(llm): add Kilo Gateway and OpenCode Go provider support by @skulldogged in https://github.com/spacedriveapp/spacebot/pull/225 + +## New Contributors +* @morgaesis made their first contribution in https://github.com/spacedriveapp/spacebot/pull/94 +* @worldofgeese made their first contribution in https://github.com/spacedriveapp/spacebot/pull/196 +* @mwmdev made their first contribution in https://github.com/spacedriveapp/spacebot/pull/214 +* @mmmeff made their first contribution in https://github.com/spacedriveapp/spacebot/pull/186 +* @jaaneh made their first contribution in https://github.com/spacedriveapp/spacebot/pull/213 + +**Full Changelog**: https://github.com/spacedriveapp/spacebot/compare/v0.1.15...v0.2.0 + +## v0.1.15 + +- Tag: `v0.1.15` +- Published: 2026-02-24T01:37:52Z +- URL: https://github.com/spacedriveapp/spacebot/releases/tag/v0.1.15 + +## What's Changed +* fix: resolve pre-existing CI failures (clippy, fmt, test) by @Marenz in https://github.com/spacedriveapp/spacebot/pull/174 +* fix: wire up ollama_base_url shorthand in config by @Marenz in https://github.com/spacedriveapp/spacebot/pull/175 +* fix: return synthetic empty text on Anthropic empty content response by @Marenz in https://github.com/spacedriveapp/spacebot/pull/171 +* fix: accept string values for timeout_seconds from LLMs by @Marenz in https://github.com/spacedriveapp/spacebot/pull/169 +* feat(telegram): use send_audio for audio MIME types by @Marenz in https://github.com/spacedriveapp/spacebot/pull/170 +* feat(skills): workers discover skills on demand via read_skill tool by @Marenz in https://github.com/spacedriveapp/spacebot/pull/172 +* fix: avoid panic on multibyte char boundary in log message truncation by @Marenz in https://github.com/spacedriveapp/spacebot/pull/176 +* ChatGPT OAuth browser flow + provider split by @marijnvdwerf in https://github.com/spacedriveapp/spacebot/pull/157 +* Fix worker completion results not reaching users by @jamiepine in https://github.com/spacedriveapp/spacebot/pull/182 +* Default MiniMax to M2.5 and enable reasoning by @hotzen in https://github.com/spacedriveapp/spacebot/pull/180 +* fix: register groq/together/xai/mistral/deepseek providers from shorthand config keys by @Marenz in https://github.com/spacedriveapp/spacebot/pull/179 +* Bugfix: Update dependencies for Slack TLS by @egenvall in https://github.com/spacedriveapp/spacebot/pull/165 +* Add warmup readiness contract and dispatch safeguards by @vsumner in https://github.com/spacedriveapp/spacebot/pull/181 +* fix(slack): Slack channel fixes, DM filtering, emoji sanitization, and restore TLS on websocket by @sra in https://github.com/spacedriveapp/spacebot/pull/148 + +## New Contributors +* @vsumner made their first contribution in https://github.com/spacedriveapp/spacebot/pull/181 +* @sra made their first contribution in https://github.com/spacedriveapp/spacebot/pull/148 + +**Full Changelog**: https://github.com/spacedriveapp/spacebot/compare/v0.1.14...v0.1.15 + +## v0.1.14 + +- Tag: `v0.1.14` +- Published: 2026-02-23T00:19:45Z +- URL: https://github.com/spacedriveapp/spacebot/releases/tag/v0.1.14 + +## What's Changed +* feat(mcp): add retry/backoff and CRUD API by @l33t0 in https://github.com/spacedriveapp/spacebot/pull/109 +* feat(ux): add drag-and-drop sorting for agents in sidebar by @MakerDZ in https://github.com/spacedriveapp/spacebot/pull/113 +* fix(channel): roll back history on PromptCancelled to prevent poisoned turns by @Marenz in https://github.com/spacedriveapp/spacebot/pull/114 +* Fix CI failures: rustfmt, clippy, and flaky test by @Marenz in https://github.com/spacedriveapp/spacebot/pull/116 +* fix(channel): prevent bot spamming from retrigger cascades by @PyRo1121 in https://github.com/spacedriveapp/spacebot/pull/115 +* feat(security): add auth middleware, SSRF protection, shell hardening, and encrypted secrets by @PyRo1121 in https://github.com/spacedriveapp/spacebot/pull/117 +* remove obsolete plan document from #58 by @hotzen in https://github.com/spacedriveapp/spacebot/pull/142 +* fix(build): restore compile after security middleware + URL validation changes by @bilawalriaz in https://github.com/spacedriveapp/spacebot/pull/125 +* fix(telegram): render markdown as Telegram HTML with safe, telegram-only fallbacks by @bilawalriaz in https://github.com/spacedriveapp/spacebot/pull/126 +* Fix Fireworks by @Nebhay in https://github.com/spacedriveapp/spacebot/pull/91 +* fix: harden 13 security vulnerabilities (phase 2) by @PyRo1121 in https://github.com/spacedriveapp/spacebot/pull/119 +* fix: replace .expect()/.unwrap() with proper error propagation in production code by @PyRo1121 in https://github.com/spacedriveapp/spacebot/pull/122 +* feat(twitch): Add Twitch token refresh by @Nebhay in https://github.com/spacedriveapp/spacebot/pull/144 +* feat: add minimax-cn provider for CN users by @shuuul in https://github.com/spacedriveapp/spacebot/pull/140 +* feat(telemetry): complete metrics instrumentation with cost tracking and per-agent context by @l33t0 in https://github.com/spacedriveapp/spacebot/pull/102 +* feat: support ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN and SPACEBOT_MODEL env vars by @adryserage in https://github.com/spacedriveapp/spacebot/pull/135 +* Fix cron timezone resolution and delete drift by @jamiepine in https://github.com/spacedriveapp/spacebot/pull/149 +* feat: update Gemini model support with latest Google models by @adryserage in https://github.com/spacedriveapp/spacebot/pull/134 +* feat(web): add favicon files and update HTML to include them by @the-snesler in https://github.com/spacedriveapp/spacebot/pull/154 +* fix: add API body size limits and memory content validation by @PyRo1121 in https://github.com/spacedriveapp/spacebot/pull/123 + +## New Contributors +* @PyRo1121 made their first contribution in https://github.com/spacedriveapp/spacebot/pull/115 +* @hotzen made their first contribution in https://github.com/spacedriveapp/spacebot/pull/142 +* @bilawalriaz made their first contribution in https://github.com/spacedriveapp/spacebot/pull/125 +* @shuuul made their first contribution in https://github.com/spacedriveapp/spacebot/pull/140 +* @adryserage made their first contribution in https://github.com/spacedriveapp/spacebot/pull/135 +* @the-snesler made their first contribution in https://github.com/spacedriveapp/spacebot/pull/154 + +**Full Changelog**: https://github.com/spacedriveapp/spacebot/compare/v0.1.13...v0.1.14 + +## v0.1.13 + +- Tag: `v0.1.13` +- Published: 2026-02-21T23:00:59Z +- URL: https://github.com/spacedriveapp/spacebot/releases/tag/v0.1.13 + +## What's Changed +* Improve channel reply flow and Discord binding behavior by @jamiepine in https://github.com/spacedriveapp/spacebot/pull/95 +* feat(messaging): unify cross-channel delivery target resolution by @jamiepine in https://github.com/spacedriveapp/spacebot/pull/97 +* feat: add dedicated voice model routing and attachment transcription by @jamiepine in https://github.com/spacedriveapp/spacebot/pull/98 +* Fix all warnings and clippy lints by @Marenz in https://github.com/spacedriveapp/spacebot/pull/87 +* Add CI workflow (check, clippy, fmt, test) by @Marenz in https://github.com/spacedriveapp/spacebot/pull/101 +* Add native poll support to telegram adapter by @Marenz in https://github.com/spacedriveapp/spacebot/pull/93 +* docs(agents): update existing documentation when adding features by @Marenz in https://github.com/spacedriveapp/spacebot/pull/106 +* feat(llm): add Google Gemini API provider support by @MakerDZ in https://github.com/spacedriveapp/spacebot/pull/111 +* prompts: add missing memory-type guidance in memory flows by @marijnvdwerf in https://github.com/spacedriveapp/spacebot/pull/112 +* add mcp client support for workers by @nexxeln in https://github.com/spacedriveapp/spacebot/pull/103 +* Avoid requiring static API key for OAuth Login by @egenvall in https://github.com/spacedriveapp/spacebot/pull/100 + +## New Contributors +* @MakerDZ made their first contribution in https://github.com/spacedriveapp/spacebot/pull/111 +* @marijnvdwerf made their first contribution in https://github.com/spacedriveapp/spacebot/pull/112 +* @nexxeln made their first contribution in https://github.com/spacedriveapp/spacebot/pull/103 + +**Full Changelog**: https://github.com/spacedriveapp/spacebot/compare/v0.1.12...v0.1.13 + +## v0.1.12 + +- Tag: `v0.1.12` +- Published: 2026-02-21T02:15:01Z +- URL: https://github.com/spacedriveapp/spacebot/releases/tag/v0.1.12 + +## Note about v0.1.11 + +v0.1.11 was removed due to a bad migration, and its tag/release were deleted. +v0.1.12 includes those intended changes plus additional fixes. + +## Highlights included from the missing v0.1.11 window + +- Added hosted agent limit functionality +- Added backup export and restore endpoints +- Added storage status endpoint and filesystem usage reporting +- Improved release tagging/version bump workflow (including Cargo.lock handling) + +## What's Changed +* fix: register NVIDIA provider and base URL by @Nebhay in https://github.com/spacedriveapp/spacebot/pull/82 +* fix: Portal Chat isolation by @jnyecode in https://github.com/spacedriveapp/spacebot/pull/80 +* Nudge previously rejected DM users when added to allow list by @Marenz in https://github.com/spacedriveapp/spacebot/pull/78 +* Telegram adapter fixes: attachments, reply-to, and retry on startup by @Marenz in https://github.com/spacedriveapp/spacebot/pull/77 +* Anthropic OAuth authentication with PKCE and auto-refresh by @Marenz in https://github.com/spacedriveapp/spacebot/pull/76 +* fix: Prevent duplicate message replies by differentiating skip and replied flags by @thesammykins in https://github.com/spacedriveapp/spacebot/pull/69 +* fix(cron): prevent timer leak and improve scheduler reliability by @michaelbship in https://github.com/spacedriveapp/spacebot/pull/81 +* feat(cron): add configurable timeout_secs per cron job by @michaelbship in https://github.com/spacedriveapp/spacebot/pull/83 +* feat: add mention-gated Discord bindings and one-time cron jobs by @jamiepine in https://github.com/spacedriveapp/spacebot/pull/88 +* docs: update README for new features since last update by @Marenz in https://github.com/spacedriveapp/spacebot/pull/92 + +## New Contributors +* @Nebhay made their first contribution in https://github.com/spacedriveapp/spacebot/pull/82 +* @michaelbship made their first contribution in https://github.com/spacedriveapp/spacebot/pull/81 + +**Full Changelog**: https://github.com/spacedriveapp/spacebot/compare/v0.1.10...v0.1.12 + +## v0.1.10 + +- Tag: `v0.1.10` +- Published: 2026-02-20T08:45:11Z +- URL: https://github.com/spacedriveapp/spacebot/releases/tag/v0.1.10 + +## What's Changed +* feat: Add Z.AI Coding Plan provider by @thesammykins in https://github.com/spacedriveapp/spacebot/pull/67 +* chore: optimize release profile to reduce binary size by @thesammykins in https://github.com/spacedriveapp/spacebot/pull/70 +* feat(slack): cache user identities and resolve channel names by @jamiepine in https://github.com/spacedriveapp/spacebot/pull/71 + +## New Contributors +* @jamiepine made their first contribution in https://github.com/spacedriveapp/spacebot/pull/71 + +**Full Changelog**: https://github.com/spacedriveapp/spacebot/compare/v0.1.9...v0.1.10 + +## v0.1.9 + +- Tag: `v0.1.9` +- Published: 2026-02-20T04:25:44Z +- URL: https://github.com/spacedriveapp/spacebot/releases/tag/v0.1.9 + +## What's Changed +* add local ollama provider by @mmattbtw in https://github.com/spacedriveapp/spacebot/pull/18 +* fix(docs): add favicon, fix theme toggle, and resolve og:image localhost issue by @andrasbacsai in https://github.com/spacedriveapp/spacebot/pull/29 +* feat(llm): add NVIDIA NIM provider support by @skulldogged in https://github.com/spacedriveapp/spacebot/pull/46 +* Update slack connector to include additional sender metadata by @ACPixel in https://github.com/spacedriveapp/spacebot/pull/43 +* feat(telemetry): add Prometheus metrics with feature-gated instrumentation by @l33t0 in https://github.com/spacedriveapp/spacebot/pull/35 +* fix(ingestion): do not delete ingest files when chunk processing fails by @sookochoff in https://github.com/spacedriveapp/spacebot/pull/57 +* feat: Improve Slack Markdown by @egenvall in https://github.com/spacedriveapp/spacebot/pull/52 +* fix: key Discord typing indicator by channel ID to prevent stuck indicator by @tomasmach in https://github.com/spacedriveapp/spacebot/pull/53 +* fix: Telegram adapter improvements by @Marenz in https://github.com/spacedriveapp/spacebot/pull/50 +* Adds pdf ingestion by @ACPixel in https://github.com/spacedriveapp/spacebot/pull/63 +* feat: add markdown preview toggle to identity editors by @tomasmach in https://github.com/spacedriveapp/spacebot/pull/59 +* Add GitHub CLI to default docker image by @ACPixel in https://github.com/spacedriveapp/spacebot/pull/61 +* feat(llm): add custom providers and dynamic API routing by @sbtobb in https://github.com/spacedriveapp/spacebot/pull/36 +* fix: prevent panic in split_message on multibyte UTF-8 char boundaries by @tomasmach in https://github.com/spacedriveapp/spacebot/pull/49 +* feat(slack): app_mention, ephemeral messages, Block Kit, scheduled messages, typing indicator by @sookochoff in https://github.com/spacedriveapp/spacebot/pull/58 +* feat(slack): slash commands (Phase 3) + Block Kit interactions (Phase 2b) by @sookochoff in https://github.com/spacedriveapp/spacebot/pull/60 +* Add Portal Chat for direct web-based agent interaction by @jnyecode in https://github.com/spacedriveapp/spacebot/pull/64 +* feat: add MiniMax as native provider by @ricorna in https://github.com/spacedriveapp/spacebot/pull/26 +* feat: add Moonshot AI (Kimi) as native provider by @ricorna in https://github.com/spacedriveapp/spacebot/pull/25 +* feat: Discord rich messages (Embeds, Buttons, Polls) by @thesammykins in https://github.com/spacedriveapp/spacebot/pull/66 + +## New Contributors +* @mmattbtw made their first contribution in https://github.com/spacedriveapp/spacebot/pull/18 +* @skulldogged made their first contribution in https://github.com/spacedriveapp/spacebot/pull/46 +* @ACPixel made their first contribution in https://github.com/spacedriveapp/spacebot/pull/43 +* @l33t0 made their first contribution in https://github.com/spacedriveapp/spacebot/pull/35 +* @sookochoff made their first contribution in https://github.com/spacedriveapp/spacebot/pull/57 +* @egenvall made their first contribution in https://github.com/spacedriveapp/spacebot/pull/52 +* @Marenz made their first contribution in https://github.com/spacedriveapp/spacebot/pull/50 +* @sbtobb made their first contribution in https://github.com/spacedriveapp/spacebot/pull/36 +* @jnyecode made their first contribution in https://github.com/spacedriveapp/spacebot/pull/64 +* @ricorna made their first contribution in https://github.com/spacedriveapp/spacebot/pull/26 + +**Full Changelog**: https://github.com/spacedriveapp/spacebot/compare/v0.1.8...v0.1.9 + +## v0.1.8 + +- Tag: `v0.1.8` +- Published: 2026-02-19T05:26:17Z +- URL: https://github.com/spacedriveapp/spacebot/releases/tag/v0.1.8 + +## What's Changed +* fix: set skip_flag in ReplyTool to prevent double reply by @tomasmach in https://github.com/spacedriveapp/spacebot/pull/39 +* fix(daemon): create instance directory before binding IPC socket by @BruceMacD in https://github.com/spacedriveapp/spacebot/pull/37 +* otel by @Brendonovich in https://github.com/spacedriveapp/spacebot/pull/30 +* fix otel by @Brendonovich in https://github.com/spacedriveapp/spacebot/pull/41 +* make otel actually work by @Brendonovich in https://github.com/spacedriveapp/spacebot/pull/42 + +## New Contributors +* @tomasmach made their first contribution in https://github.com/spacedriveapp/spacebot/pull/39 +* @BruceMacD made their first contribution in https://github.com/spacedriveapp/spacebot/pull/37 + +**Full Changelog**: https://github.com/spacedriveapp/spacebot/compare/v0.1.7...v0.1.8 + +## v0.1.7 + +- Tag: `v0.1.7` +- Published: 2026-02-18T18:09:56Z +- URL: https://github.com/spacedriveapp/spacebot/releases/tag/v0.1.7 + +## What's Changed +* fix(config): support numeric telegram chat_id binding match by @cyllas in https://github.com/spacedriveapp/spacebot/pull/34 +* Add ARM64 multi-platform Docker images by @andrasbacsai in https://github.com/spacedriveapp/spacebot/pull/27 + +## New Contributors +* @cyllas made their first contribution in https://github.com/spacedriveapp/spacebot/pull/34 +* @andrasbacsai made their first contribution in https://github.com/spacedriveapp/spacebot/pull/27 + +**Full Changelog**: https://github.com/spacedriveapp/spacebot/compare/v0.1.6...v0.1.7 + +## v0.1.6 + +- Tag: `v0.1.6` +- Published: 2026-02-18T10:14:21Z +- URL: https://github.com/spacedriveapp/spacebot/releases/tag/v0.1.6 + +**Full Changelog**: https://github.com/spacedriveapp/spacebot/compare/v0.1.5...v0.1.6 + +## v0.1.5 + +- Tag: `v0.1.5` +- Published: 2026-02-18T07:50:05Z +- URL: https://github.com/spacedriveapp/spacebot/releases/tag/v0.1.5 + +## What's Changed +* Fix broken documentation links in README by @joseph-lozano in https://github.com/spacedriveapp/spacebot/pull/11 +* fix: IPv6 socket address parsing for Docker deployments by @pablopunk in https://github.com/spacedriveapp/spacebot/pull/14 + +## New Contributors +* @joseph-lozano made their first contribution in https://github.com/spacedriveapp/spacebot/pull/11 +* @pablopunk made their first contribution in https://github.com/spacedriveapp/spacebot/pull/14 + +**Full Changelog**: https://github.com/spacedriveapp/spacebot/compare/v0.1.4...v0.1.5 + +## v0.1.4 + +- Tag: `v0.1.4` +- Published: 2026-02-17T23:23:34Z +- URL: https://github.com/spacedriveapp/spacebot/releases/tag/v0.1.4 + +## What's Changed +* Run release workflow on x86 and ARM runners by @Brendonovich in https://github.com/spacedriveapp/spacebot/pull/3 +* Fix OpenCode Zen provider icon by @Brendonovich in https://github.com/spacedriveapp/spacebot/pull/5 +* better provider list by @Brendonovich in https://github.com/spacedriveapp/spacebot/pull/6 +* Fix Z.ai provider icon by @jiunshinn in https://github.com/spacedriveapp/spacebot/pull/7 +* improve docker build by @Brendonovich in https://github.com/spacedriveapp/spacebot/pull/4 +* Fix quick start by @doanbactam in https://github.com/spacedriveapp/spacebot/pull/8 + +## New Contributors +* @doanbactam made their first contribution in https://github.com/spacedriveapp/spacebot/pull/8 + +**Full Changelog**: https://github.com/spacedriveapp/spacebot/compare/v0.1.3...v0.1.4 + +## v0.1.3 + +- Tag: `v0.1.3` +- Published: 2026-02-17T03:59:34Z +- URL: https://github.com/spacedriveapp/spacebot/releases/tag/v0.1.3 + +## What's Changed +* Add OpenCode Zen provider support by @Brendonovich in https://github.com/spacedriveapp/spacebot/pull/2 + + +**Full Changelog**: https://github.com/spacedriveapp/spacebot/compare/v0.1.2...v0.1.3 + +## v0.1.2 + +- Tag: `v0.1.2` +- Published: 2026-02-17T01:14:40Z +- URL: https://github.com/spacedriveapp/spacebot/releases/tag/v0.1.2 + +**Full Changelog**: https://github.com/spacedriveapp/spacebot/compare/v0.1.1...v0.1.2 + +## v0.1.1 + +- Tag: `v0.1.1` +- Published: 2026-02-17T00:04:10Z +- URL: https://github.com/spacedriveapp/spacebot/releases/tag/v0.1.1 + +## What's Changed +* Add native Z.ai (GLM) provider by @jiunshinn in https://github.com/spacedriveapp/spacebot/pull/1 + +## New Contributors +* @jiunshinn made their first contribution in https://github.com/spacedriveapp/spacebot/pull/1 + +**Full Changelog**: https://github.com/spacedriveapp/spacebot/commits/v0.1.1 + +## v0.1.0 + +- Tag: `v0.1.0` +- Published: 2026-02-15T22:31:48Z +- URL: https://github.com/spacedriveapp/spacebot/releases/tag/v0.1.0 + +**Full Changelog**: https://github.com/spacedriveapp/spacebot/commits/v0.1.0 diff --git a/Cargo.lock b/Cargo.lock index 8b5bf8604..227ba0403 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -241,6 +241,18 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "arraydeque" version = "0.5.1" @@ -253,6 +265,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + [[package]] name = "arrayvec" version = "0.7.6" @@ -345,7 +363,7 @@ dependencies = [ "chrono", "comfy-table", "half", - "lexical-core", + "lexical-core 1.0.6", "num-traits", "ryu", ] @@ -409,7 +427,7 @@ dependencies = [ "half", "indexmap 2.13.0", "itoa", - "lexical-core", + "lexical-core 1.0.6", "memchr", "num-traits", "ryu", @@ -636,7 +654,7 @@ dependencies = [ "aligned", "anyhow", "arg_enum_proc_macro", - "arrayvec", + "arrayvec 0.7.6", "log", "num-rational", "num-traits", @@ -654,7 +672,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" dependencies = [ "anyhow", - "arrayvec", + "arrayvec 0.7.6", "log", "nom 8.0.0", "num-rational", @@ -667,7 +685,7 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d" dependencies = [ - "arrayvec", + "arrayvec 0.7.6", ] [[package]] @@ -849,7 +867,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" dependencies = [ "arrayref", - "arrayvec", + "arrayvec 0.7.6", "cc", "cfg-if", "constant_time_eq 0.4.2", @@ -943,7 +961,7 @@ version = "3.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89ec27229c38ed0eb3c0feee3d2c1d6a4379ae44f418a29a658890e062d8f365" dependencies = [ - "darling 0.21.3", + "darling 0.23.0", "ident_case", "prettyplease", "proc-macro2", @@ -983,6 +1001,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bufstream" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8" + [[package]] name = "built" version = "0.8.0" @@ -1135,6 +1159,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "charset" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f927b07c74ba84c7e5fe4db2baeb3e996ab2688992e39ac68ce3220a677c7e" +dependencies = [ + "base64 0.22.1", + "encoding_rs", +] + [[package]] name = "chromiumoxide" version = "0.8.0" @@ -1145,6 +1179,7 @@ dependencies = [ "base64 0.22.1", "cfg-if", "chromiumoxide_cdp", + "chromiumoxide_fetcher", "chromiumoxide_types", "dunce", "fnv", @@ -1159,7 +1194,7 @@ dependencies = [ "tracing", "url", "which", - "windows-registry 0.5.3", + "windows-registry", ] [[package]] @@ -1174,6 +1209,23 @@ dependencies = [ "serde_json", ] +[[package]] +name = "chromiumoxide_fetcher" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e39b54dfcb6973284f55cf3639d44e84d23feed4e2e7d1faa4a9029a365737" +dependencies = [ + "anyhow", + "directories", + "reqwest 0.12.28", + "serde", + "thiserror 1.0.69", + "tokio", + "tracing", + "windows-version", + "zip 0.6.6", +] + [[package]] name = "chromiumoxide_pdl" version = "0.8.0" @@ -1225,6 +1277,16 @@ dependencies = [ "phf 0.12.1", ] +[[package]] +name = "chumsky" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" +dependencies = [ + "hashbrown 0.14.5", + "stacker", +] + [[package]] name = "cipher" version = "0.4.4" @@ -1523,6 +1585,17 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "cron" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8c3e73077b4b4a6ab1ea5047c37c57aee77657bc8ecd6f29b0af082d0b0c07" +dependencies = [ + "chrono", + "nom 7.1.3", + "once_cell", +] + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -2425,7 +2498,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eddada51c8576df9d6a8450c351ff63042b092c9458b8ac7d20f89cbd0ffd313" dependencies = [ - "arrayvec", + "arrayvec 0.7.6", "proc-macro2", "quote", "strsim 0.10.0", @@ -2556,6 +2629,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs" version = "6.0.0" @@ -2671,6 +2753,22 @@ dependencies = [ "serde", ] +[[package]] +name = "email-encoding" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9298e6504d9b9e780ed3f7dfd43a61be8cd0e09eb07f7706a945b0072b6670b6" +dependencies = [ + "base64 0.22.1", + "memchr", +] + +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" + [[package]] name = "emojis" version = "0.8.0" @@ -3594,6 +3692,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "hostname" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" +dependencies = [ + "cfg-if", + "libc", + "windows-link 0.2.1", +] + [[package]] name = "htmlescape" version = "0.3.1" @@ -3812,7 +3921,7 @@ dependencies = [ "tokio", "tower-service", "tracing", - "windows-registry 0.6.1", + "windows-registry", ] [[package]] @@ -4076,6 +4185,29 @@ dependencies = [ "quick-error", ] +[[package]] +name = "imap" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c617c55def8c42129e0dd503f11d7ee39d73f5c7e01eff55768b3879ff1d107d" +dependencies = [ + "base64 0.13.1", + "bufstream", + "chrono", + "imap-proto", + "lazy_static", + "native-tls", + "nom 5.1.3", + "regex", +] + +[[package]] +name = "imap-proto" +version = "0.10.2" +dependencies = [ + "nom 5.1.3", +] + [[package]] name = "imgref" version = "1.12.0" @@ -4462,7 +4594,7 @@ dependencies = [ "object_store", "permutation", "pin-project", - "prost 0.14.3", + "prost", "prost-types", "rand 0.9.2", "roaring", @@ -4536,7 +4668,7 @@ dependencies = [ "num_cpus", "object_store", "pin-project", - "prost 0.14.3", + "prost", "rand 0.9.2", "roaring", "serde_json", @@ -4575,7 +4707,7 @@ dependencies = [ "lance-geo", "log", "pin-project", - "prost 0.14.3", + "prost", "snafu", "tokio", "tracing", @@ -4628,7 +4760,7 @@ dependencies = [ "log", "lz4", "num-traits", - "prost 0.14.3", + "prost", "prost-build", "prost-types", "rand 0.9.2", @@ -4666,7 +4798,7 @@ dependencies = [ "log", "num-traits", "object_store", - "prost 0.14.3", + "prost", "prost-build", "prost-types", "snafu", @@ -4739,7 +4871,7 @@ dependencies = [ "ndarray", "num-traits", "object_store", - "prost 0.14.3", + "prost", "prost-build", "prost-types", "rand 0.9.2", @@ -4787,7 +4919,7 @@ dependencies = [ "object_store", "path_abs", "pin-project", - "prost 0.14.3", + "prost", "rand 0.9.2", "serde", "shellexpand", @@ -4892,7 +5024,7 @@ dependencies = [ "lance-io", "log", "object_store", - "prost 0.14.3", + "prost", "prost-build", "prost-types", "rand 0.9.2", @@ -5002,12 +5134,53 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" +[[package]] +name = "lettre" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e13e10e8818f8b2a60f52cb127041d388b89f3a96a62be9ceaffa22262fef7f" +dependencies = [ + "async-trait", + "base64 0.22.1", + "chumsky", + "email-encoding", + "email_address", + "fastrand", + "futures-io", + "futures-util", + "hostname", + "httpdate", + "idna", + "mime", + "native-tls", + "nom 8.0.0", + "percent-encoding", + "quoted_printable", + "socket2 0.6.2", + "tokio", + "tokio-native-tls", + "url", +] + [[package]] name = "levenshtein_automata" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25" +[[package]] +name = "lexical-core" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" +dependencies = [ + "arrayvec 0.5.2", + "bitflags 1.3.2", + "cfg-if", + "ryu", + "static_assertions", +] + [[package]] name = "lexical-core" version = "1.0.6" @@ -5278,6 +5451,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30" +[[package]] +name = "mailparse" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60819a97ddcb831a5614eb3b0174f3620e793e97e09195a395bfa948fd68ed2f" +dependencies = [ + "charset", + "data-encoding", + "quoted_printable", +] + [[package]] name = "matchers" version = "0.2.0" @@ -5576,6 +5760,17 @@ dependencies = [ "libc", ] +[[package]] +name = "nom" +version = "5.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08959a387a676302eebf4ddbcbc611da04285579f76f88ee0506c63b1a61dd4b" +dependencies = [ + "lexical-core 0.7.6", + "memchr", + "version_check", +] + [[package]] name = "nom" version = "7.1.3" @@ -5937,9 +6132,9 @@ dependencies = [ [[package]] name = "opentelemetry" -version = "0.29.1" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e87237e2775f74896f9ad219d26a2081751187eb7c9f5c58dde20a23b95d16c" +checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" dependencies = [ "futures-core", "futures-sink", @@ -5951,71 +6146,67 @@ dependencies = [ [[package]] name = "opentelemetry-http" -version = "0.29.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46d7ab32b827b5b495bd90fa95a6cb65ccc293555dcc3199ae2937d2d237c8ed" +checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" dependencies = [ "async-trait", "bytes", "http 1.4.0", "opentelemetry", "reqwest 0.12.28", - "tracing", ] [[package]] name = "opentelemetry-otlp" -version = "0.29.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d899720fe06916ccba71c01d04ecd77312734e2de3467fd30d9d580c8ce85656" +checksum = "7a2366db2dca4d2ad033cad11e6ee42844fd727007af5ad04a1730f4cb8163bf" dependencies = [ - "futures-core", "http 1.4.0", "opentelemetry", "opentelemetry-http", "opentelemetry-proto", "opentelemetry_sdk", - "prost 0.13.5", + "prost", "reqwest 0.12.28", "thiserror 2.0.18", ] [[package]] name = "opentelemetry-proto" -version = "0.29.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c40da242381435e18570d5b9d50aca2a4f4f4d8e146231adb4e7768023309b3" +checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" dependencies = [ "opentelemetry", "opentelemetry_sdk", - "prost 0.13.5", + "prost", "tonic", + "tonic-prost", ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.29.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b29a9f89f1a954936d5aa92f19b2feec3c8f3971d3e96206640db7f9706ae3" +checksum = "e62e29dfe041afb8ed2a6c9737ab57db4907285d999ef8ad3a59092a36bdc846" [[package]] name = "opentelemetry_sdk" -version = "0.29.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afdefb21d1d47394abc1ba6c57363ab141be19e27cc70d0e422b7f303e4d290b" +checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" dependencies = [ "futures-channel", "futures-executor", "futures-util", - "glob", "opentelemetry", "percent-encoding", "rand 0.9.2", - "serde_json", "thiserror 2.0.18", "tokio", "tokio-stream", - "tracing", ] [[package]] @@ -6105,6 +6296,17 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "paste" version = "1.0.15" @@ -6522,16 +6724,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "prost" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" -dependencies = [ - "bytes", - "prost-derive 0.13.5", -] - [[package]] name = "prost" version = "0.14.3" @@ -6539,7 +6731,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" dependencies = [ "bytes", - "prost-derive 0.14.3", + "prost-derive", ] [[package]] @@ -6554,26 +6746,13 @@ dependencies = [ "multimap", "petgraph", "prettyplease", - "prost 0.14.3", + "prost", "prost-types", "regex", "syn 2.0.114", "tempfile", ] -[[package]] -name = "prost-derive" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" -dependencies = [ - "anyhow", - "itertools 0.14.0", - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "prost-derive" version = "0.14.3" @@ -6593,7 +6772,7 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" dependencies = [ - "prost 0.14.3", + "prost", ] [[package]] @@ -6712,6 +6891,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "quoted_printable" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" + [[package]] name = "r-efi" version = "5.3.0" @@ -6840,7 +7025,7 @@ dependencies = [ "aligned-vec", "arbitrary", "arg_enum_proc_macro", - "arrayvec", + "arrayvec 0.7.6", "av-scenechange", "av1-grain", "bitstream-io", @@ -7070,7 +7255,6 @@ dependencies = [ "base64 0.22.1", "bytes", "encoding_rs", - "futures-channel", "futures-core", "futures-util", "h2 0.4.13", @@ -7119,8 +7303,10 @@ checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" dependencies = [ "base64 0.22.1", "bytes", + "encoding_rs", "futures-core", "futures-util", + "h2 0.4.13", "http 1.4.0", "http-body 1.0.1", "http-body-util", @@ -7129,6 +7315,8 @@ dependencies = [ "hyper-util", "js-sys", "log", + "mime", + "mime_guess", "percent-encoding", "pin-project-lite", "quinn", @@ -7137,6 +7325,7 @@ dependencies = [ "rustls-platform-verifier", "serde", "serde_json", + "serde_urlencoded", "sync_wrapper 1.0.2", "tokio", "tokio-rustls 0.26.4", @@ -7162,9 +7351,9 @@ dependencies = [ [[package]] name = "rig-core" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f7a3f0c7c00eaced15a68ee16e1bd6bb709ff598d11b9aedac8b628217dc09" +checksum = "437fa2a15825caf2505411bbe55b05c8eb122e03934938b38f9ecaa1d6ded7c8" dependencies = [ "as-any", "async-stream", @@ -7181,7 +7370,7 @@ dependencies = [ "nanoid", "ordered-float", "pin-project-lite", - "reqwest 0.12.28", + "reqwest 0.13.2", "rig-derive", "schemars 1.2.1", "serde", @@ -7224,9 +7413,9 @@ dependencies = [ [[package]] name = "rmcp" -version = "0.16.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4c9c94680f75470ee8083a0667988b5d7b5beb70b9f998a8e51de7c682ce60" +checksum = "8a0ce46f9101dc911f07e1468084c057839d15b08040d110820c5513312ef56a" dependencies = [ "async-trait", "base64 0.22.1", @@ -7251,9 +7440,9 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "0.16.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90c23c8f26cae4da838fbc3eadfaecf2d549d97c04b558e7bd90526a9c28b42a" +checksum = "abad6f5f46e220e3bda2fc90fd1ad64c1c2a2bd716d52c845eb5c9c64cda7542" dependencies = [ "darling 0.23.0", "proc-macro2", @@ -7550,7 +7739,7 @@ dependencies = [ "security-framework 3.5.1", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -7642,18 +7831,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "schemars" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" -dependencies = [ - "dyn-clone", - "schemars_derive 0.8.22", - "serde", - "serde_json", -] - [[package]] name = "schemars" version = "0.9.0" @@ -7675,23 +7852,11 @@ dependencies = [ "chrono", "dyn-clone", "ref-cast", - "schemars_derive 1.2.1", + "schemars_derive", "serde", "serde_json", ] -[[package]] -name = "schemars_derive" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.114", -] - [[package]] name = "schemars_derive" version = "1.2.1" @@ -7952,7 +8117,7 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bde37f42765dfdc34e2a039e0c84afbf79a3101c1941763b0beb816c2f17541" dependencies = [ - "arrayvec", + "arrayvec 0.7.6", "async-trait", "base64 0.22.1", "bitflags 2.10.0", @@ -8236,11 +8401,12 @@ dependencies = [ [[package]] name = "spacebot" -version = "0.1.15" +version = "0.2.2" dependencies = [ "aes-gcm", "anyhow", "arc-swap", + "argon2", "arrow-array", "arrow-schema", "async-stream", @@ -8254,38 +8420,47 @@ dependencies = [ "chrono-tz", "clap", "config", + "cron", "daemonize", "dialoguer", "dirs", "emojis", "fastembed", + "flate2", "futures", "hex", "ignore", + "imap", "indoc", "lance-index", "lancedb", + "lettre", "libc", + "mailparse", "mime_guess", "minijinja", + "moka", + "native-tls", "notify", "open", "opentelemetry", "opentelemetry-otlp", "opentelemetry-semantic-conventions", "opentelemetry_sdk", + "parking_lot", "pdf-extract", "pin-project", "prometheus", "rand 0.9.2", "redb", "regex", - "reqwest 0.12.28", + "reqwest 0.13.2", "rig-core", "rmcp", "rust-embed", "rustls 0.23.36", - "schemars 0.8.22", + "schemars 1.2.1", + "security-framework 3.5.1", "semver", "serde", "serde_json", @@ -8310,7 +8485,7 @@ dependencies = [ "twitch-irc", "urlencoding", "uuid", - "zip", + "zip 2.4.2", ] [[package]] @@ -9419,9 +9594,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "tonic" -version = "0.12.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" dependencies = [ "async-trait", "base64 0.22.1", @@ -9431,13 +9606,24 @@ dependencies = [ "http-body-util", "percent-encoding", "pin-project", - "prost 0.13.5", + "sync_wrapper 1.0.2", "tokio-stream", "tower-layer", "tower-service", "tracing", ] +[[package]] +name = "tonic-prost" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" +dependencies = [ + "bytes", + "prost", + "tonic", +] + [[package]] name = "tower" version = "0.5.3" @@ -9565,14 +9751,12 @@ dependencies = [ [[package]] name = "tracing-opentelemetry" -version = "0.30.0" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd8e764bd6f5813fd8bebc3117875190c5b0415be8f7f8059bffb6ecd979c444" +checksum = "1ac28f2d093c6c477eaa76b23525478f38de514fa9aeb1285738d4b97a9552fc" dependencies = [ "js-sys", - "once_cell", "opentelemetry", - "opentelemetry_sdk", "smallvec", "tracing", "tracing-core", @@ -10317,17 +10501,6 @@ dependencies = [ "windows-strings 0.4.2", ] -[[package]] -name = "windows-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - [[package]] name = "windows-result" version = "0.3.4" @@ -10490,6 +10663,15 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -10991,6 +11173,18 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "crc32fast", + "crossbeam-utils", + "flate2", +] + [[package]] name = "zip" version = "2.4.2" diff --git a/Cargo.toml b/Cargo.toml index d2f24f0c3..87b37d721 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "spacebot" -version = "0.1.15" +version = "0.2.2" edition = "2024" default-run = "spacebot" @@ -17,10 +17,10 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" # LLM / Rig framework -rig = { version = "0.30.0", package = "rig-core", features = ["derive"] } +rig = { version = "0.31", package = "rig-core", features = ["derive"] } # HTTP clients for LLM providers -reqwest = { version = "0.12", features = ["json", "stream"] } +reqwest = { version = "0.13", features = ["json", "stream", "form", "query", "gzip"] } # Databases sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite", "migrate", "chrono", "uuid"] } @@ -35,17 +35,20 @@ fastembed = "4" base64 = "0.22" hex = "0.4" +# Compression +flate2 = "1" + # Logging and tracing tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-appender = "0.2" # OpenTelemetry -opentelemetry = "0.29" -opentelemetry_sdk = { version = "0.29", features = ["rt-tokio", "experimental_trace_batch_span_processor_with_async_runtime"] } -opentelemetry-otlp = { version = "0.29", default-features = false, features = ["http-proto", "reqwest-client"] } -opentelemetry-semantic-conventions = "0.29" -tracing-opentelemetry = "0.30" +opentelemetry = "0.31" +opentelemetry_sdk = { version = "0.31", features = ["rt-tokio", "experimental_trace_batch_span_processor_with_async_runtime"] } +opentelemetry-otlp = { version = "0.31", default-features = false, features = ["http-proto", "reqwest-client"] } +opentelemetry-semantic-conventions = "0.31" +tracing-opentelemetry = "0.32" # Configuration dirs = "6.0" @@ -58,6 +61,7 @@ notify = "7" # Cryptography (for secrets) aes-gcm = "0.10" sha2 = "0.10" +argon2 = "0.5" rand = "0.9" # UUID generation @@ -66,6 +70,7 @@ uuid = { version = "1.15", features = ["v4", "serde"] } # Time handling chrono = { version = "0.4", features = ["serde"] } chrono-tz = "0.10" +cron = "0.12" # Regular expressions (for leak detection) regex = "1.11" @@ -75,8 +80,8 @@ futures = "0.3" pin-project = "1" # Schema validation -schemars = "0.8" -rmcp = { version = "0.16", features = ["client", "reqwest", "transport-child-process", "transport-streamable-http-client", "transport-streamable-http-client-reqwest"] } +schemars = "1.2" +rmcp = { version = "0.17", features = ["client", "reqwest", "transport-child-process", "transport-streamable-http-client", "transport-streamable-http-client-reqwest"] } # Command line (for main.rs) clap = { version = "4.5", features = ["derive"] } @@ -105,6 +110,12 @@ teloxide = { version = "0.17", default-features = false, features = ["rustls"] } # Twitch twitch-irc = { version = "5.0", default-features = false, features = ["transport-tcp-rustls-webpki-roots", "refreshing-token-rustls-webpki-roots"] } +# Email +imap = "2.4" +lettre = { version = "0.11", features = ["tokio1", "tokio1-native-tls"] } +mailparse = "0.16" +native-tls = "0.2" + # Stream utilities tokio-stream = "0.1" @@ -121,7 +132,7 @@ arrow-array = "57.3.0" arrow-schema = "57.3.0" # Browser automation -chromiumoxide = { version = "0.8", features = ["tokio-runtime"], default-features = false } +chromiumoxide = { version = "0.8", features = ["tokio-runtime", "_fetcher-rustls-tokio"], default-features = false } chromiumoxide_cdp = "0.8" # Templating for prompts @@ -142,10 +153,14 @@ prometheus = { version = "0.13", optional = true } pdf-extract = "0.10.0" open = "5.3.3" urlencoding = "2.1.3" +moka = "0.12.13" [features] metrics = ["dep:prometheus"] +[patch.crates-io] +imap-proto = { path = "vendor/imap-proto-0.10.2" } + [lints.clippy] dbg_macro = "deny" todo = "deny" @@ -153,6 +168,11 @@ unimplemented = "deny" [dev-dependencies] tokio-test = "0.4" +parking_lot = "0.12" + +# OS keystore (macOS Keychain for master key storage) +[target.'cfg(target_os = "macos")'.dependencies] +security-framework = "3" [profile.release] lto = "thin" diff --git a/Dockerfile b/Dockerfile index 438462390..c90848f92 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,7 @@ WORKDIR /build # 1. Fetch and cache Rust dependencies. # cargo fetch needs a valid target, so we create stubs that get replaced later. COPY Cargo.toml Cargo.lock ./ +COPY vendor/ vendor/ RUN mkdir src && echo "fn main() {}" > src/main.rs && touch src/lib.rs \ && cargo build --release \ && rm -rf src @@ -35,17 +36,22 @@ RUN cd interface && bun run build # build.rs runs the frontend build (already done above, node_modules present). # prompts/ is needed for include_str! in src/prompts/text.rs. # migrations/ is needed for sqlx::migrate! in src/db.rs. +# docs/ is needed for rust-embed in src/self_awareness.rs. +# AGENTS.md, README.md, CHANGELOG.md are needed for include_str! in src/self_awareness.rs. COPY build.rs ./ COPY prompts/ prompts/ COPY migrations/ migrations/ +COPY docs/ docs/ +COPY AGENTS.md README.md CHANGELOG.md ./ COPY src/ src/ RUN SPACEBOT_SKIP_FRONTEND_BUILD=1 cargo build --release \ && mv /build/target/release/spacebot /usr/local/bin/spacebot \ && cargo clean -p spacebot --release --target-dir /build/target -# ---- Slim stage ---- -# Minimal runtime with just the binary. No browser. -FROM debian:bookworm-slim AS slim +# ---- Runtime stage ---- +# Minimal runtime with Chrome runtime libraries for fetcher-downloaded Chromium. +# Chrome itself is downloaded on first browser tool use and cached on the volume. +FROM debian:bookworm-slim RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ @@ -53,28 +59,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ curl \ gh \ bubblewrap \ - && rm -rf /var/lib/apt/lists/* - -COPY --from=builder /usr/local/bin/spacebot /usr/local/bin/spacebot -COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh -RUN chmod +x /usr/local/bin/docker-entrypoint.sh - -ENV SPACEBOT_DIR=/data -ENV SPACEBOT_DEPLOYMENT=docker -EXPOSE 19898 18789 - -HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ - CMD curl -f http://localhost:19898/api/health || exit 1 - -ENTRYPOINT ["docker-entrypoint.sh"] -CMD ["spacebot", "start", "--foreground"] - -# ---- Full stage ---- -# Slim + Chromium for browser workers. -FROM slim AS full - -RUN apt-get update && apt-get install -y --no-install-recommends \ - chromium \ + openssh-server \ + # Chrome runtime dependencies — required whether Chrome is system-installed + # or downloaded by the built-in fetcher. The fetcher provides the browser + # binary; these are the shared libraries it links against. fonts-liberation \ libnss3 \ libatk-bridge2.0-0 \ @@ -91,5 +79,16 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libxtst6 \ && rm -rf /var/lib/apt/lists/* -ENV CHROME_PATH=/usr/bin/chromium -ENV CHROME_FLAGS="--no-sandbox --disable-dev-shm-usage --disable-gpu" +COPY --from=builder /usr/local/bin/spacebot /usr/local/bin/spacebot +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +ENV SPACEBOT_DIR=/data +ENV SPACEBOT_DEPLOYMENT=docker +EXPOSE 19898 18789 + +HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ + CMD curl -f http://localhost:19898/api/health || exit 1 + +ENTRYPOINT ["docker-entrypoint.sh"] +CMD ["spacebot", "start", "--foreground"] diff --git a/README.md b/README.md index 0387d04ec..eb7087898 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ + + [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/spacedriveapp/spacebot)

@@ -119,7 +121,8 @@ Every memory has a type, an importance score, and graph edges connecting it to r Cron jobs created and managed from conversation or config: - **Natural scheduling** — "check my inbox every 30 minutes" becomes a cron job with a delivery target -- **Clock-aligned intervals** — sub-daily intervals snap to UTC boundaries so jobs fire on clean marks (e.g. every 30 min fires at :00 and :30) +- **Strict wall-clock schedules** — use cron expressions for exact local-time execution (for example, `0 9 * * *` for 9:00 every day) +- **Legacy interval compatibility** — existing `interval_secs` jobs still run and remain configurable - **Configurable timeouts** — per-job `timeout_secs` to cap execution time (defaults to 120s) - **Active hours** — restrict jobs to specific time windows (supports midnight wrapping) - **Circuit breaker** — auto-disables after 3 consecutive failures @@ -184,7 +187,7 @@ coding = "ollama/qwen3" ```toml [llm.provider.my-provider] -api_type = "openai_completions" # or "anthropic" +api_type = "openai_completions" # or "openai_chat_completions", "openai_responses", "anthropic" base_url = "https://my-llm-host.example.com" api_key = "env:MY_PROVIDER_KEY" @@ -192,7 +195,7 @@ api_key = "env:MY_PROVIDER_KEY" channel = "my-provider/my-model" ``` -Additional built-in providers include **NVIDIA**, **MiniMax**, **Moonshot AI (Kimi)**, and **Z.AI Coding Plan** — configure with `nvidia_key`, `minimax_key`, `moonshot_key`, or `zai_coding_plan_key` in `[llm]`. +Additional built-in providers include **Kilo Gateway**, **OpenCode Go**, **NVIDIA**, **MiniMax**, **Moonshot AI (Kimi)**, and **Z.AI Coding Plan** — configure with `kilo_key`, `opencode_go_key`, `nvidia_key`, `minimax_key`, `moonshot_key`, or `zai_coding_plan_key` in `[llm]`. ### Skills @@ -237,6 +240,39 @@ url = "https://mcp.sentry.io" headers = { Authorization = "Bearer ${SENTRY_TOKEN}" } ``` +### Security + +Spacebot runs autonomous LLM processes that execute arbitrary shell commands and spawn subprocesses. Security isn't an add-on — it's a layered system designed so that no single failure exposes credentials or breaks containment. + +#### Credential Isolation + +Secrets are split into two categories: **system** (LLM API keys, messaging tokens — never exposed to subprocesses) and **tool** (CLI credentials like `GH_TOKEN` — injected as env vars into workers). The category is auto-assigned based on the secret name, or set explicitly. + +- **Environment sanitization** — every subprocess starts with a clean environment (`--clearenv` on Linux, `env_clear()` everywhere else). Only safe baseline vars (`PATH`, `HOME`, `LANG`), tool-category secrets, and explicit `passthrough_env` entries are present. In sandbox mode, `HOME` is set to the workspace; in passthrough mode, `HOME` uses the parent environment. System secrets never enter any subprocess +- **Secret store** — credentials live in a dedicated redb database, not in `config.toml`. Config references secrets by alias (`anthropic_key = "secret:ANTHROPIC_API_KEY"`), so the config file is safe to display, screenshot, or `cat` +- **Encryption at rest** — optional AES-256-GCM encryption with a master key derived via Argon2id. The master key lives in the OS credential store (macOS Keychain, Linux kernel keyring) — never on disk, never in an env var, never accessible to worker subprocesses +- **Keyring isolation** — on Linux, workers are spawned with a fresh empty session keyring via `pre_exec`. Even without the sandbox, workers cannot access the parent's kernel keyring where the master key lives +- **Output scrubbing** — all tool secret values are redacted from worker output before it reaches channels or LLM context. A rolling buffer handles secrets split across stream chunks. Channels see `[REDACTED]`, never raw values +- **Worker secret management** — workers can store credentials they obtain (API keys from account creation, OAuth tokens) via the `secret_set` tool. Stored secrets are immediately available to future workers + +#### Process Containment + +- **Process sandbox** — shell and exec tools run inside OS-level filesystem containment. On Linux, [bubblewrap](https://github.com/containers/bubblewrap) creates a mount namespace where the entire filesystem is read-only except the agent's workspace and configured writable paths. On macOS, `sandbox-exec` enforces equivalent restrictions via SBPL profiles. Kernel-enforced, not string-filtered +- **Dynamic sandbox mode** — sandbox settings are hot-reloadable. Toggle via the dashboard or API without restarting the agent +- **Workspace isolation** — file tools canonicalize all paths and reject anything outside the agent's workspace. Symlinks that escape are blocked +- **Leak detection** — secret-pattern checks are enforced at channel egress (`reply` and plaintext fallback output) across plaintext, URL-encoded, base64, and hex encodings. Outbound text matching a secret pattern is blocked; worker tool outputs no longer hard-fail the worker +- **Library injection blocking** — the exec tool blocks dangerous environment variables (`LD_PRELOAD`, `DYLD_INSERT_LIBRARIES`, `NODE_OPTIONS`, etc.) that could hijack child process loading +- **SSRF protection** — the browser tool blocks requests to cloud metadata endpoints, private IPs, loopback, and link-local addresses +- **Identity file protection** — writes to `SOUL.md`, `IDENTITY.md`, and `USER.md` are blocked at the application level +- **Durable binary storage** — `tools/bin` directory on PATH survives hosted rollouts. Workers are instructed to install binaries there instead of ephemeral package manager locations + +```toml +[agents.sandbox] +mode = "enabled" # "enabled" (default) or "disabled" +writable_paths = ["/home/user/projects/myapp"] # additional writable dirs beyond workspace +passthrough_env = ["CUSTOM_VAR"] # forward specific env vars to workers +``` + --- ## How It Works @@ -355,9 +391,9 @@ Memories are structured objects, not files. Every memory is a row in SQLite with Scheduled recurring tasks. Each cron job gets a fresh short-lived channel with full branching and worker capabilities. -- Multiple cron jobs run independently at different intervals +- Multiple cron jobs run independently on wall-clock schedules (or legacy intervals) - Stored in the database, created via config, conversation, or programmatically -- Clock-aligned intervals snap to UTC boundaries for predictable firing times +- Cron expressions execute against the resolved cron timezone for predictable local-time firing - Per-job `timeout_secs` to cap execution time - Circuit breaker auto-disables after 3 consecutive failures - Active hours support with midnight wrapping @@ -381,7 +417,7 @@ Read the full vision in the [roadmap](docs/content/docs/(deployment)/roadmap.mdx ### Prerequisites - **Rust** 1.85+ ([rustup](https://rustup.rs/)) -- An LLM API key from any supported provider (Anthropic, OpenAI, OpenRouter, Z.ai, Groq, Together, Fireworks, DeepSeek, xAI, Mistral, NVIDIA, MiniMax, Moonshot AI, OpenCode Zen) — or use `spacebot auth login` for Anthropic OAuth +- An LLM API key from any supported provider (Anthropic, OpenAI, OpenRouter, Kilo Gateway, Z.ai, Groq, Together, Fireworks, DeepSeek, xAI, Mistral, NVIDIA, MiniMax, Moonshot AI, OpenCode Zen, OpenCode Go) — or use `spacebot auth login` for Anthropic OAuth ### Build and Run @@ -413,6 +449,12 @@ token = "env:DISCORD_BOT_TOKEN" agent_id = "my-agent" channel = "discord" guild_id = "your-discord-guild-id" + +# Optional: route a named adapter instance +[[bindings]] +agent_id = "my-agent" +channel = "discord" +adapter = "ops" ``` ```bash @@ -448,7 +490,7 @@ OAuth tokens are stored in `anthropic_oauth.json` and auto-refresh transparently | --------------- | --------------------------------------------------------------------------------------------------------------- | | Language | **Rust** (edition 2024) | | Async runtime | **Tokio** | -| LLM framework | **[Rig](https://github.com/0xPlaygrounds/rig)** v0.30 — agentic loop, tool execution, hooks | +| LLM framework | **[Rig](https://github.com/0xPlaygrounds/rig)** v0.31 — agentic loop, tool execution, hooks | | Relational data | **SQLite** (sqlx) — conversations, memory graph, cron jobs | | Vector + FTS | **[LanceDB](https://lancedb.github.io/lancedb/)** — embeddings (HNSW), full-text (Tantivy), hybrid search (RRF) | | Key-value | **[redb](https://github.com/cberner/redb)** — settings, encrypted secrets | @@ -478,6 +520,8 @@ No server dependencies. Single binary. All data lives in embedded databases in a | [Cortex](docs/content/docs/(core)/cortex.mdx) | Memory bulletin and system observation | | [Cron Jobs](docs/content/docs/(features)/cron.mdx) | Scheduled recurring tasks | | [Routing](docs/content/docs/(core)/routing.mdx) | Model routing and fallback chains | +| [Secrets](docs/content/docs/(configuration)/secrets.mdx) | Credential storage, encryption, and output scrubbing | +| [Sandbox](docs/content/docs/(configuration)/sandbox.mdx) | Process containment and environment sanitization | | [Messaging](docs/content/docs/(messaging)/messaging.mdx) | Adapter architecture (Discord, Slack, Telegram, Twitch, Webchat, webhook) | | [Discord Setup](docs/content/docs/(messaging)/discord-setup.mdx) | Discord bot setup guide | | [Browser](docs/content/docs/(features)/browser.mdx) | Headless Chrome for workers | @@ -503,11 +547,13 @@ Contributions welcome. Read [RUST_STYLE_GUIDE.md](RUST_STYLE_GUIDE.md) before wr 1. Fork the repo 2. Create a feature branch -3. Run `./scripts/install-git-hooks.sh` once (installs pre-commit formatting hook) -4. Make your changes -5. Submit a PR +3. Install `just` (https://github.com/casey/just) if it is not already available (for example: `brew install just` or `cargo install just --locked`) +4. Run `./scripts/install-git-hooks.sh` once (installs pre-commit formatting hook) +5. Make your changes +6. Run `just preflight` and `just gate-pr` +7. Submit a PR -Formatting is still enforced in CI, but the hook catches it earlier by running `cargo fmt --all` before each commit. +Formatting is still enforced in CI, but the hook catches it earlier by running `cargo fmt --all` before each commit. `just gate-pr` mirrors the CI gate and includes migration safety, compile checks, and test verification. --- diff --git a/TODO b/TODO new file mode 100644 index 000000000..d0c959599 --- /dev/null +++ b/TODO @@ -0,0 +1,31 @@ +Things to Fix: + ☐ Add behaviour settings per channel + ☐ Force branching before response + +Cortex Loops: + ☐ Check tasks + elevate todos + ☐ Improve bulletin + ☐ Better cortex loop context + +Improvements: + ☐ Improve cron + ☐ Named adapters + ☐ Thread names should be better, match channel name style + +Live Data Sources (maybe not needed): + ☐ Repos + ☐ Documentation + +Features: + ☐ Agent creation flow / onboarding UI + ☐ Cortex chat context inspection — should be able to read the full channel context at any time + ☐ Add streaming support + +Urgent: + ☐ Send customer emails ASAP + +Settings: + ☐ Disable sandboxing / sandbox settings + +Notes: + Testing inter-agent comms: send a message to the Spacebot Tech Lead, who forwards to the Community Manager, who picks a random word relating to fruit, technology, or space exploration, sending it back up the chain. diff --git a/docs/.node-version b/docs/.node-version new file mode 100644 index 000000000..209e3ef4b --- /dev/null +++ b/docs/.node-version @@ -0,0 +1 @@ +20 diff --git a/docs/content/docs/(configuration)/config.mdx b/docs/content/docs/(configuration)/config.mdx index f098b6d73..1193fa241 100644 --- a/docs/content/docs/(configuration)/config.mdx +++ b/docs/content/docs/(configuration)/config.mdx @@ -24,6 +24,7 @@ spacebot --config /path/to.toml # CLI override anthropic_key = "env:ANTHROPIC_API_KEY" openai_key = "env:OPENAI_API_KEY" openrouter_key = "env:OPENROUTER_API_KEY" +kilo_key = "env:KILO_API_KEY" zhipu_key = "env:ZHIPU_API_KEY" groq_key = "env:GROQ_API_KEY" together_key = "env:TOGETHER_API_KEY" @@ -32,6 +33,7 @@ deepseek_key = "env:DEEPSEEK_API_KEY" xai_key = "env:XAI_API_KEY" mistral_key = "env:MISTRAL_API_KEY" opencode_zen_key = "env:OPENCODE_ZEN_API_KEY" +opencode_go_key = "env:OPENCODE_GO_API_KEY" # Custom LLM providers (alternative to legacy keys) [llm.provider.my_anthropic] @@ -60,6 +62,7 @@ context_window = 128000 # context window size in tokens history_backfill_count = 50 # messages to fetch from platform on new channel worker_log_mode = "errors_only" # "errors_only", "all_separate", or "all_combined" cron_timezone = "UTC" # optional default timezone for cron active hours +user_timezone = "UTC" # optional default timezone for channel/worker time context # Model routing per process type. [defaults.routing] @@ -87,7 +90,7 @@ emergency_threshold = 0.95 # drop oldest 50%, no LLM # Cortex (system observer) settings. [defaults.cortex] tick_interval_secs = 30 -worker_timeout_secs = 300 +worker_timeout_secs = 600 branch_timeout_secs = 60 circuit_breaker_threshold = 3 # consecutive failures before auto-disable @@ -103,8 +106,10 @@ startup_delay_secs = 5 enabled = true headless = true evaluate_enabled = false -executable_path = "/path/to/chrome" # optional, auto-detected -screenshot_dir = "/path/to/screenshots" # optional, defaults to data_dir/screenshots +persist_session = false # keep browser alive across worker lifetimes +close_policy = "close_browser" # "close_browser", "close_tabs", or "detach" +executable_path = "/path/to/chrome" # optional, auto-detected +screenshot_dir = "/path/to/screenshots" # optional, defaults to data_dir/screenshots # --- Agents --- # At least one agent is required. First agent or the one with default = true @@ -114,6 +119,7 @@ id = "main" default = true workspace = "/custom/workspace/path" # optional, defaults to ~/.spacebot/agents/{id}/workspace cron_timezone = "America/Los_Angeles" # optional per-agent cron timezone override +user_timezone = "America/Los_Angeles" # optional per-agent timezone override for channel/worker time context # Per-agent routing overrides (merges with defaults). [agents.routing] @@ -128,7 +134,7 @@ writable_paths = ["/home/user/projects/myapp"] # additional writable directories [[agents.cron]] id = "daily-check" prompt = "Check in on ongoing projects and report status." -interval_secs = 86400 +cron_expr = "0 9 * * *" delivery_target = "discord:123456789" active_start_hour = 9 active_end_hour = 17 @@ -163,17 +169,32 @@ agent_id = "main" channel = "webhook" ``` -## Environment Variable References +## Value References -Any string value in the config can reference an environment variable with the `env:` prefix: +Any string value in the config supports three resolution modes: + +| Prefix | Resolution | Example | +|--------|-----------|---------| +| `secret:` | Look up from the [secret store](/docs/secrets) | `"secret:ANTHROPIC_API_KEY"` | +| `env:` | Read from system environment variable | `"env:ANTHROPIC_API_KEY"` | +| _(none)_ | Literal value | `"sk-ant-..."` | ```toml -anthropic_key = "env:ANTHROPIC_API_KEY" +# From the secret store (recommended) +anthropic_key = "secret:ANTHROPIC_API_KEY" + +# From an environment variable +openai_key = "env:OPENAI_API_KEY" + +# Literal value (not recommended — use secret: or env: instead) +groq_key = "gsk_abc123..." ``` -This reads `ANTHROPIC_API_KEY` from the environment at startup. If the variable is unset, the value is treated as missing. +The `secret:` prefix resolves from the agent's secret store at config load time. If the secret doesn't exist, the value is treated as missing and implicit env fallbacks are tried. + +LLM keys also have implicit env fallbacks — if no key is set in the TOML, Spacebot checks `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `OPENROUTER_API_KEY`, `KILO_API_KEY`, and `OPENCODE_GO_API_KEY` automatically. -LLM keys also have implicit env fallbacks — if no key is set in the TOML, Spacebot checks `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, and `OPENROUTER_API_KEY` automatically. +Use `POST /api/secrets/migrate` to automatically move plaintext credentials from `config.toml` into the secret store and replace them with `secret:` references. See [Secret Store -- Migration](/docs/secrets#migration) for details. ## Env-Only Mode @@ -208,6 +229,7 @@ Model names include the provider as a prefix: | Anthropic | `anthropic/` | `anthropic/claude-sonnet-4-20250514` | | OpenAI | `openai/` | `openai/gpt-4o` | | OpenRouter | `openrouter//` | `openrouter/anthropic/claude-sonnet-4-20250514` | +| Kilo Gateway | `kilo//` | `kilo/anthropic/claude-sonnet-4.5` | | Custom provider | `/` | `my_openai/gpt-4o-mini` | You can mix providers across process types. See [Routing](/docs/routing) for the full routing system. @@ -236,7 +258,7 @@ Most config values are hot-reloaded when their files change. Spacebot watches `c | Setting | Why | |---------|-----| -| LLM API keys | Provider clients are initialized once | +| LLM API keys | Provider clients are initialized once (applies to `secret:`, `env:`, and literal values) | | Messaging adapters (Discord token, webhook bind/port) | Adapter connections are long-lived | | Agent topology (adding/removing `[[agents]]`) | Databases and event buses are per-agent | | Database paths | Connections are opened once at startup | @@ -290,6 +312,7 @@ System prompts (channel, branch, worker, compactor, cortex, etc.) are Jinja2 tem │ ├── lancedb/ # vector search │ ├── config.redb # key-value settings │ ├── settings.redb # runtime settings (worker_log_mode, etc.) + │ ├── secrets.redb # secret store (categories, encryption) │ └── logs/ # worker execution logs └── archives/ # compaction transcripts ``` @@ -326,17 +349,19 @@ If you define a custom provider with the same ID as a legacy key, your custom co | Key | Type | Default | Description | |-----|------|---------|-------------| -| `anthropic_key` | string | None | Anthropic API key (or `env:VAR_NAME`) | -| `openai_key` | string | None | OpenAI API key (or `env:VAR_NAME`) | -| `openrouter_key` | string | None | OpenRouter API key (or `env:VAR_NAME`) | -| `zhipu_key` | string | None | Zhipu AI (GLM) API key (or `env:VAR_NAME`) | -| `groq_key` | string | None | Groq API key (or `env:VAR_NAME`) | -| `together_key` | string | None | Together AI API key (or `env:VAR_NAME`) | -| `fireworks_key` | string | None | Fireworks AI API key (or `env:VAR_NAME`) | -| `deepseek_key` | string | None | DeepSeek API key (or `env:VAR_NAME`) | -| `xai_key` | string | None | XAI API key (or `env:VAR_NAME`) | -| `mistral_key` | string | None | Mistral API key (or `env:VAR_NAME`) | -| `opencode_zen_key` | string | None | OpenCode Zen API key (or `env:VAR_NAME`) | +| `anthropic_key` | string | None | Anthropic API key (`secret:NAME`, `env:VAR_NAME`, or literal) | +| `openai_key` | string | None | OpenAI API key (`secret:NAME`, `env:VAR_NAME`, or literal) | +| `openrouter_key` | string | None | OpenRouter API key (`secret:NAME`, `env:VAR_NAME`, or literal) | +| `kilo_key` | string | None | Kilo Gateway API key (`secret:NAME`, `env:VAR_NAME`, or literal) | +| `zhipu_key` | string | None | Zhipu AI (GLM) API key (`secret:NAME`, `env:VAR_NAME`, or literal) | +| `groq_key` | string | None | Groq API key (`secret:NAME`, `env:VAR_NAME`, or literal) | +| `together_key` | string | None | Together AI API key (`secret:NAME`, `env:VAR_NAME`, or literal) | +| `fireworks_key` | string | None | Fireworks AI API key (`secret:NAME`, `env:VAR_NAME`, or literal) | +| `deepseek_key` | string | None | DeepSeek API key (`secret:NAME`, `env:VAR_NAME`, or literal) | +| `xai_key` | string | None | XAI API key (`secret:NAME`, `env:VAR_NAME`, or literal) | +| `mistral_key` | string | None | Mistral API key (`secret:NAME`, `env:VAR_NAME`, or literal) | +| `opencode_zen_key` | string | None | OpenCode Zen API key (`secret:NAME`, `env:VAR_NAME`, or literal) | +| `opencode_go_key` | string | None | OpenCode Go API key (`secret:NAME`, `env:VAR_NAME`, or literal) | #### Custom Providers @@ -344,7 +369,7 @@ Custom providers allow configuring LLM providers with custom endpoints and API t ```toml [llm.provider.] -api_type = "anthropic" # Required - one of: anthropic, openai_completions, openai_responses +api_type = "anthropic" # Required - see supported values below base_url = "https://api..." # Required - valid URL api_key = "env:API_KEY" # Required - API key (supports env:VAR_NAME format) name = "My Provider" # Optional - friendly name for display @@ -352,16 +377,18 @@ name = "My Provider" # Optional - friendly name for display | Field | Type | Required | Description | |-------|------|----------|-------------| -| `api_type` | string | Yes | API protocol type. One of: `anthropic` (Anthropic Messages API), `openai_completions` (OpenAI Chat Completions-compatible API), or `openai_responses` (OpenAI Responses API-compatible) | +| `api_type` | string | Yes | API protocol type. One of: `anthropic`, `openai_completions`, `openai_chat_completions`, `openai_responses`, `gemini`, or `kilo_gateway` | | `base_url` | string | Yes | Base URL of the API endpoint. Must be a valid URL (including protocol) | -| `api_key` | string | Yes | API key for authentication. Supports `env:VAR_NAME` syntax to reference environment variables | +| `api_key` | string | Yes | API key for authentication. Supports `secret:NAME` and `env:VAR_NAME` syntax | | `name` | string | No | Optional friendly name for the provider (displayed in logs and UI) | > Note: -> - For `openai_completions` and `openai_responses`, configure `base_url` as the provider root URL (usually without a trailing `/v1`). +> - For `openai_completions`, `openai_chat_completions`, and `openai_responses`, configure `base_url` as the provider root URL (usually without a trailing `/v1`). > - Spacebot appends the endpoint path automatically: > - `openai_completions` -> `/v1/chat/completions` +> - `openai_chat_completions` -> `/chat/completions` > - `openai_responses` -> `/v1/responses` +> - `kilo_gateway` -> `/chat/completions` plus Kilo-required `HTTP-Referer` / `X-Title` headers > - If you include `/v1` in `base_url`, requests can end up with duplicated paths such as `/v1/v1/...`. **Provider ID Requirements:** @@ -410,6 +437,7 @@ At least one provider (legacy key or custom provider) must be configured. | `history_backfill_count` | integer | 50 | Messages to fetch from platform on new channel | | `worker_log_mode` | string | `"errors_only"` | Worker log persistence: `"errors_only"`, `"all_separate"`, or `"all_combined"` | | `cron_timezone` | string | None | Default timezone for cron active-hours evaluation (IANA name like `UTC` or `America/New_York`) | +| `user_timezone` | string | inherits `cron_timezone` | Default timezone for channel/worker temporal context (IANA name) | ### `[defaults.routing]` @@ -475,9 +503,11 @@ Thresholds are fractions of `context_window`. | Key | Type | Default | Description | |-----|------|---------|-------------| -| `tick_interval_secs` | integer | 30 | How often the cortex checks system state | -| `worker_timeout_secs` | integer | 300 | Worker timeout before cancellation | +| `tick_interval_secs` | integer | 30 | How often the cortex runtime loop runs maintenance ticks while continuously observing events | +| `worker_timeout_secs` | integer | 600 | Worker idle timeout before cancellation | | `branch_timeout_secs` | integer | 60 | Branch timeout before cancellation | +| `detached_worker_timeout_retry_limit` | integer | 2 | Retry limit before quarantining detached workers to backlog | +| `supervisor_kill_budget_per_tick` | integer | 8 | Max number of overdue processes supervisor may cancel per health tick | | `circuit_breaker_threshold` | integer | 3 | Consecutive failures before auto-disable | ### `[defaults.warmup]` @@ -489,11 +519,13 @@ Thresholds are fractions of `context_window`. | `refresh_secs` | integer | 900 | Seconds between background warmup passes | | `startup_delay_secs` | integer | 5 | Delay before first warmup pass after boot | +When warmup is enabled, it is the primary bulletin refresh path. The cortex runtime loop still performs fallback bulletin/profile refresh when warmup is disabled or when the cached bulletin is stale (`bulletin_age_secs >= max(1, warmup.refresh_secs)`). + Dispatch readiness is derived from warmup runtime state: - warmup state must be `warm` - embedding must be ready -- bulletin age must be fresh (<= `max(60s, refresh_secs * 2)`) +- bulletin age must be fresh (`<= max(60s, refresh_secs * 2)`) When branch/worker/cron dispatch happens before readiness is satisfied, Spacebot still dispatches, increments cold-dispatch metrics, and queues a forced warmup pass in the background. @@ -504,6 +536,8 @@ When branch/worker/cron dispatch happens before readiness is satisfied, Spacebot | `enabled` | bool | true | Whether workers have browser tools | | `headless` | bool | true | Run Chrome headless | | `evaluate_enabled` | bool | false | Allow JavaScript evaluation | +| `persist_session` | bool | false | Keep browser alive across worker lifetimes. Tabs, cookies, and logins survive between tasks. Requires agent restart to take effect. | +| `close_policy` | string | `"close_browser"` | What happens on close: `"close_browser"` (kill Chrome), `"close_tabs"` (close tabs, keep browser), `"detach"` (disconnect, leave everything) | | `executable_path` | string | None | Custom Chrome/Chromium path | | `screenshot_dir` | string | None | Directory for screenshots | @@ -515,6 +549,7 @@ When branch/worker/cron dispatch happens before readiness is satisfied, Spacebot | `default` | bool | false | Whether this is the default agent | | `workspace` | string | `~/.spacebot/agents/{id}/workspace` | Custom workspace path | | `cron_timezone` | string | inherits | Per-agent timezone override for cron active-hours evaluation | +| `user_timezone` | string | inherits | Per-agent timezone override for channel/worker temporal context | | `max_concurrent_branches` | integer | inherits | Override instance default | | `max_turns` | integer | inherits | Override instance default | | `context_window` | integer | inherits | Override instance default | @@ -523,12 +558,15 @@ Agent-specific routing is set via `[agents.routing]` with the same keys as `[def ### `[agents.sandbox]` -OS-level filesystem containment for shell and exec tool subprocesses. Uses bubblewrap (Linux) or sandbox-exec (macOS) to enforce read-only access to everything outside the workspace. +OS-level filesystem containment and environment sanitization for shell and exec tool subprocesses. Uses bubblewrap (Linux) or sandbox-exec (macOS) to enforce read-only access to everything outside the workspace. Environment sanitization runs in all modes -- workers never inherit the parent's environment variables. + +See [Sandbox](/docs/sandbox) for a full explanation of how containment, environment sanitization, leak detection, and durable binaries work. | Key | Type | Default | Description | |-----|------|---------|-------------| -| `mode` | string | `"enabled"` | `"enabled"` for kernel-enforced containment, `"disabled"` for full host access | +| `mode` | string | `"enabled"` | `"enabled"` for kernel-enforced containment, `"disabled"` for passthrough (full host filesystem access; env sanitization still applies) | | `writable_paths` | string[] | `[]` | Additional directories the agent can write to beyond its workspace | +| `passthrough_env` | string[] | `[]` | Environment variable names to forward from the parent process to worker subprocesses | When `mode = "enabled"`, shell and exec commands run inside a mount namespace where the entire filesystem is read-only except: @@ -539,7 +577,7 @@ When `mode = "enabled"`, shell and exec commands run inside a mount namespace wh The agent's data directory (databases, config) is explicitly re-mounted read-only even if it would otherwise be writable due to path overlap. -When `SPACEBOT_DEPLOYMENT=hosted`, sandbox mode is always `"enabled"` regardless of config. +Regardless of mode, all worker subprocesses start with a clean environment. Only `PATH` (with `tools/bin` prepended), safe variables (`HOME`, `USER`, `LANG`, `TERM`, `TMPDIR`), and any `passthrough_env` entries are injected. `HOME` is mode-dependent: workspace path when sandboxed, parent `HOME` when passthrough is enabled. Use `passthrough_env` with least privilege: only forward variables required by worker tools (for example specific credentials set in Docker Compose or systemd), and avoid forwarding broad or highly sensitive credentials. If the sandbox backend isn't available (e.g. bubblewrap not installed), processes run unsandboxed with a warning at startup. @@ -547,6 +585,7 @@ If the sandbox backend isn't available (e.g. bubblewrap not installed), processe [agents.sandbox] mode = "enabled" writable_paths = ["/home/user/projects/myapp", "/var/data/shared"] +passthrough_env = ["GH_TOKEN", "GITHUB_TOKEN"] ``` ### `[[agents.cron]]` @@ -555,6 +594,7 @@ writable_paths = ["/home/user/projects/myapp", "/var/data/shared"] |-----|------|---------|-------------| | `id` | string | **required** | Cron job identifier | | `prompt` | string | **required** | Prompt sent to a fresh channel on each tick | +| `cron_expr` | string | None | Strict wall-clock schedule (cron expression, e.g. `0 9 * * *`) | | `interval_secs` | integer | 3600 | Seconds between firings | | `delivery_target` | string | **required** | Where to send results (`adapter:target`) | | `active_start_hour` | integer | None | Start of active hours window (24h format) | @@ -570,22 +610,140 @@ Cron timezone precedence is: If a configured timezone is invalid, Spacebot logs a warning and falls back to server local time. +Channel/worker temporal context timezone precedence is: + +1. `agents.user_timezone` +2. `defaults.user_timezone` +3. `SPACEBOT_USER_TIMEZONE` +4. resolved cron timezone (from `agents.cron_timezone` / `defaults.cron_timezone` / `SPACEBOT_CRON_TIMEZONE`) +5. server local timezone + ### `[messaging.discord]` | Key | Type | Default | Description | |-----|------|---------|-------------| | `enabled` | bool | false | Enable Discord adapter | | `token` | string | None | Bot token (or `env:VAR_NAME`) | +| `instances` | table[] | [] | Optional named Discord bot instances | | `dm_allowed_users` | string[] | [] | User IDs allowed to DM the bot | +### `[[messaging.discord.instances]]` + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `name` | string | **required** | Instance selector used by bindings (`adapter = "name"`) | +| `enabled` | bool | true | Enable this named instance | +| `token` | string | **required** | Bot token (or `env:VAR_NAME`) | +| `dm_allowed_users` | string[] | [] | User IDs allowed to DM this instance | +| `allow_bot_messages` | bool | false | Whether this instance accepts bot-authored messages | + +### `[messaging.slack]` + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `enabled` | bool | false | Enable Slack adapter | +| `bot_token` | string | None | Bot token (or `env:VAR_NAME`) | +| `app_token` | string | None | App-level token (or `env:VAR_NAME`) | +| `instances` | table[] | [] | Optional named Slack app instances | +| `dm_allowed_users` | string[] | [] | Slack user IDs allowed to DM the bot | + +### `[[messaging.slack.instances]]` + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `name` | string | **required** | Instance selector used by bindings (`adapter = "name"`) | +| `enabled` | bool | true | Enable this named instance | +| `bot_token` | string | **required** | Bot token (or `env:VAR_NAME`) | +| `app_token` | string | **required** | App-level token (or `env:VAR_NAME`) | +| `dm_allowed_users` | string[] | [] | Slack user IDs allowed to DM this instance | + ### `[messaging.telegram]` | Key | Type | Default | Description | |-----|------|---------|-------------| | `enabled` | bool | false | Enable Telegram adapter | | `token` | string | None | Bot token from @BotFather (or `env:VAR_NAME`). Falls back to `TELEGRAM_BOT_TOKEN` env var | +| `instances` | table[] | [] | Optional named Telegram bot instances | | `dm_allowed_users` | string[] | [] | User IDs allowed to DM the bot. Empty = DMs from anyone accepted | +### `[[messaging.telegram.instances]]` + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `name` | string | **required** | Instance selector used by bindings (`adapter = "name"`) | +| `enabled` | bool | true | Enable this named instance | +| `token` | string | **required** | Bot token (or `env:VAR_NAME`) | +| `dm_allowed_users` | string[] | [] | User IDs allowed to DM this instance | + +### `[messaging.twitch]` + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `enabled` | bool | false | Enable Twitch adapter | +| `username` | string | None | Bot login username | +| `oauth_token` | string | None | OAuth token (`oauth:...` or plain token) | +| `instances` | table[] | [] | Optional named Twitch bot instances | +| `channels` | string[] | [] | Channels to join | +| `trigger_prefix` | string | None | Optional prefix required to trigger replies | + +### `[[messaging.twitch.instances]]` + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `name` | string | **required** | Instance selector used by bindings (`adapter = "name"`) | +| `enabled` | bool | true | Enable this named instance | +| `username` | string | **required** | Bot login username | +| `oauth_token` | string | **required** | OAuth token (`oauth:...` or plain token) | +| `channels` | string[] | [] | Channels to join for this instance | +| `trigger_prefix` | string | None | Optional prefix required to trigger replies | + +### `[messaging.email]` + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `enabled` | bool | false | Enable Email adapter | +| `imap_host` | string | None | IMAP host (or `env:VAR_NAME`) | +| `imap_port` | integer | 993 | IMAP port | +| `imap_username` | string | None | IMAP username (or `env:VAR_NAME`) | +| `imap_password` | string | None | IMAP password (or `env:VAR_NAME`) | +| `imap_use_tls` | bool | true | Use direct TLS for IMAP | +| `smtp_host` | string | None | SMTP host (or `env:VAR_NAME`) | +| `smtp_port` | integer | 587 | SMTP port | +| `smtp_username` | string | None | SMTP username (or `env:VAR_NAME`) | +| `smtp_password` | string | None | SMTP password (or `env:VAR_NAME`) | +| `smtp_use_starttls` | bool | true | Use STARTTLS for SMTP | +| `from_address` | string | None | Sender address for outgoing replies (or `env:VAR_NAME`) | +| `from_name` | string | None | Optional sender display name | +| `poll_interval_secs` | integer | 30 | How often to check for new email | +| `folders` | string[] | `["INBOX"]` | IMAP folders to poll | +| `allowed_senders` | string[] | `[]` | Optional allowlist for inbound senders (empty = all) | +| `max_body_bytes` | integer | 262144 | Max inbound body bytes before truncation | +| `max_attachment_bytes` | integer | 10485760 | Max attachment bytes to process metadata for | + +### `[[messaging.email.instances]]` + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `name` | string | **required** | Instance selector used by bindings (`adapter = "name"`) | +| `enabled` | bool | true | Enable this named instance | +| `imap_host` | string | **required** | IMAP host (or `env:VAR_NAME`) | +| `imap_port` | integer | 993 | IMAP port | +| `imap_username` | string | **required** | IMAP username (or `env:VAR_NAME`) | +| `imap_password` | string | **required** | IMAP password (or `env:VAR_NAME`) | +| `imap_use_tls` | bool | true | Use direct TLS for IMAP | +| `smtp_host` | string | **required** | SMTP host (or `env:VAR_NAME`) | +| `smtp_port` | integer | 587 | SMTP port | +| `smtp_username` | string | None | SMTP username (defaults to IMAP username) | +| `smtp_password` | string | None | SMTP password (defaults to IMAP password) | +| `smtp_use_starttls` | bool | true | Use STARTTLS for SMTP | +| `from_address` | string | None | Sender address (defaults to SMTP username) | +| `from_name` | string | None | Optional sender display name | +| `poll_interval_secs` | integer | 30 | How often to check for new email | +| `folders` | string[] | `["INBOX"]` | IMAP folders to poll | +| `allowed_senders` | string[] | `[]` | Optional allowlist (empty = all) | +| `max_body_bytes` | integer | 262144 | Max inbound body bytes | +| `max_attachment_bytes` | integer | 10485760 | Max attachment bytes | + ### `[messaging.webhook]` | Key | Type | Default | Description | @@ -601,7 +759,8 @@ Routes platform conversations to agents. Checked in order; first match wins. Unm | Key | Type | Default | Description | |-----|------|---------|-------------| | `agent_id` | string | **required** | Which agent handles matched messages | -| `channel` | string | **required** | Platform name (`discord`, `webhook`) | +| `channel` | string | **required** | Platform name (`discord`, `slack`, `telegram`, `twitch`, `email`, `webhook`) | +| `adapter` | string | None | Optional named adapter selector (e.g. `ops` => `discord:ops`) | | `guild_id` | string | None | Discord guild filter | | `chat_id` | string | None | Telegram chat filter | | `channel_ids` | string[] | [] | Discord channel ID filter (includes threads in those channels) | diff --git a/docs/content/docs/(configuration)/meta.json b/docs/content/docs/(configuration)/meta.json index 9ecb10d74..ef915e0d0 100644 --- a/docs/content/docs/(configuration)/meta.json +++ b/docs/content/docs/(configuration)/meta.json @@ -1,4 +1,4 @@ { "title": "Configuration", - "pages": ["config", "permissions"] + "pages": ["config", "secrets", "sandbox", "permissions"] } diff --git a/docs/content/docs/(configuration)/permissions.mdx b/docs/content/docs/(configuration)/permissions.mdx index 51ea4aba0..4c6113cc1 100644 --- a/docs/content/docs/(configuration)/permissions.mdx +++ b/docs/content/docs/(configuration)/permissions.mdx @@ -9,7 +9,7 @@ Per-agent permission system that controls what tools can do, enforces inter-agen ## Design Principles -**The container is the OS-level sandbox.** On spacebot.sh, each user runs in an isolated container. Self-hosters who need OS-level isolation can run Spacebot in Docker themselves. The permissions system does not attempt to replicate container-level security — it handles inter-agent boundaries and tool-level restrictions within a single Spacebot process. +**OS-level containment is handled by the [Sandbox](/docs/sandbox) when sandboxing is enabled.** The sandbox enforces filesystem boundaries and environment sanitization at the kernel level, with exact guarantees depending on mode and backend/platform. On spacebot.sh, each user also runs in an isolated container. The permissions system handles a different layer -- inter-agent boundaries and tool-level restrictions within a single Spacebot process. **Deny is an error, not invisible.** When a tool call is denied, the tool still appears in the LLM's tool list, but returns a structured error explaining the restriction. The LLM can reason about the denial and adapt. This is better than hiding tools (which causes the LLM to attempt workarounds) and better than silent failures (which cause confusion). @@ -50,7 +50,7 @@ But NOT: ``` ~/.spacebot/agents/{other_agent}/ # other agents' data -~/.spacebot/config.toml # instance config (contains API keys) +~/.spacebot/config.toml # instance config (secret references, not plaintext keys) /etc/, /home/, /Users/ # system paths ``` @@ -285,7 +285,7 @@ Default when no `[permissions]` block exists: all tools return permission denied ## What This Does NOT Do -**OS-level sandboxing.** No Docker containers, no seccomp profiles, no capability dropping. Provisioned instances (spacebot.sh) or the user's own Docker setup handles this. Spacebot's permissions system is application-level. +**OS-level sandboxing.** The permissions system is application-level -- it controls which tools the LLM can use and what paths it's allowed to access. OS-level containment is handled by the [Sandbox](/docs/sandbox), which operates independently when sandboxing is enabled; exact primitives vary by mode and backend/platform. Provisioned instances (spacebot.sh) also run in isolated containers. The permissions system and sandbox are complementary layers. **Shell command parsing.** We don't parse shell pipelines or analyze command strings for dangerous patterns. For `shell = "workspace"`, the `working_dir` is confined but the command itself runs unrestricted within that directory. Full command analysis is fragile and has diminishing returns — if you need that level of restriction, use `shell = "deny"`. diff --git a/docs/content/docs/(configuration)/sandbox.mdx b/docs/content/docs/(configuration)/sandbox.mdx new file mode 100644 index 000000000..4c801e7a0 --- /dev/null +++ b/docs/content/docs/(configuration)/sandbox.mdx @@ -0,0 +1,230 @@ +--- +title: Sandbox +description: OS-level filesystem containment and environment sanitization for worker subprocesses. +--- + +# Sandbox + +OS-level containment for builtin worker subprocesses (`shell` and `exec`). Prevents workers from modifying the host filesystem, reading inherited environment secrets, and accessing the agent's internal data directory. + +## How It Works + +When a worker runs a shell or exec command, the sandbox wraps the subprocess in an OS-level containment layer before execution. The worker's command runs normally -- it can only read a minimal runtime allowlist plus workspace paths, and can only write to explicitly allowed paths. + +``` +Worker calls shell("npm test") + → Sandbox.wrap() builds a contained command + → Subprocess runs with: + - Read access to a minimal system allowlist + workspace + - Writable access only to the workspace + configured writable paths + /tmp + - Clean environment (no inherited secrets) + - HOME set to workspace in sandbox mode, parent HOME in passthrough mode + - TMPDIR set to /tmp + - tools/bin prepended to PATH + → stdout/stderr captured and returned to worker +``` + +Two things happen regardless of whether the sandbox is enabled or disabled: + +1. **Environment sanitization** -- worker subprocesses never inherit the parent's environment variables. Secrets like `ANTHROPIC_API_KEY` are never visible to workers. +2. **PATH injection** -- the persistent `tools/bin` directory is prepended to PATH so durably installed binaries are always available. + +## Backends + +The sandbox auto-detects the best available backend at startup: + +| Platform | Backend | Mechanism | +|----------|---------|-----------| +| Linux | [bubblewrap](https://github.com/containers/bubblewrap) | Mount namespaces, PID namespaces, environment isolation | +| macOS | sandbox-exec | SBPL profile with deny-default policy | +| Other / not available | Passthrough | No filesystem containment (env sanitization still applies) | + +If the sandbox is enabled but no backend is available, processes run unsandboxed with a warning at startup. Environment sanitization still applies in all cases. + +### Linux (bubblewrap) + +The default on all hosted instances and most self-hosted Linux deployments. Bubblewrap creates a mount namespace where: + +- A minimal host runtime allowlist is mounted **read-only** (`/bin`, `/sbin`, `/usr`, `/lib`, `/lib64`, `/etc`, `/opt`, `/run`, `/nix` when present) +- The persistent tools directory is mounted **read-only** (if present) +- The workspace directory is mounted **read-write** +- `writable_paths` entries are mounted **read-write** +- `/tmp` is a private tmpfs per invocation +- `/dev` has standard device nodes +- `/proc` is a fresh procfs (when supported by the environment) +- The agent's data directory is masked with an empty tmpfs (no reads/writes) +- PID namespace isolation prevents the subprocess from seeing other processes +- `--die-with-parent` ensures the subprocess is killed if the parent exits + +Nested containers (Docker-in-Docker, Fly Machines) may not support `--proc /proc`. The sandbox probes for this at startup and falls back gracefully -- `proc_supported: false` in the startup log means `/proc` inside the sandbox shows the host's process list rather than an isolated view. + +### macOS (sandbox-exec) + +Uses Apple's sandbox-exec with a generated SBPL (Sandbox Profile Language) profile. The profile starts with `(deny default)` and explicitly allows: + +- Process execution and forking +- Reading only a backend allowlist (system runtime roots + workspace + configured writable paths + tools/bin) +- Writing only to the workspace, configured writable paths, and `/tmp` +- Network access (unrestricted) +- Standard device and IPC operations + +The agent's data directory is denied for both reads and writes even if it falls under the workspace subtree. + +Note: `sandbox-exec` is deprecated by Apple but remains functional. It's the only user-space sandbox option on macOS without requiring a full VM. + +## Filesystem Boundaries + +When the sandbox is enabled, the subprocess sees: + +| Path | Access | Notes | +|------|--------|-------| +| System runtime allowlist | Read-only | Backend-specific system roots required to execute common tools | +| Agent workspace | Read-write | Where the worker does its job | +| `writable_paths` entries | Read-write | User-configured additional paths | +| `{instance_dir}/tools/bin` | Read-only | Persistent binaries on PATH | +| `/tmp` | Read-write | Private per invocation (bubblewrap) | +| `/dev` | Read-write | Standard device nodes | +| Agent data directory | **No access** | Masked/denied to protect databases and config | + +The data directory protection is important: even if the data directory overlaps with workspace-related paths, it's explicitly blocked. Workers can't read or modify databases, config files, or identity files at the kernel level. + +## Environment Sanitization + +Worker subprocesses start with a **clean environment**. The parent process's environment variables are never inherited. This applies in all sandbox modes -- even when the sandbox is disabled, `env_clear()` strips the environment. + +A worker running `printenv` sees only: + +| Variable | Source | Value | +|----------|--------|-------| +| `PATH` | Always | `{instance_dir}/tools/bin:{system_path}` | +| `HOME` | Mode-dependent | Workspace path (sandbox enabled), parent `HOME` (sandbox disabled) | +| `TMPDIR` | Always | `/tmp` | +| `USER` | Always | From parent (if set) | +| `LANG` | Always | From parent (if set) | +| `TERM` | Always | From parent (if set) | +| `passthrough_env` entries | Config | User-configured forwarding | + +Workers never see `ANTHROPIC_API_KEY`, `DISCORD_BOT_TOKEN`, `SPACEBOT_*` internal vars, or any other environment variables from the parent process. + +### passthrough_env + +Self-hosted users who set credentials as environment variables in Docker Compose or systemd can forward specific variables to worker subprocesses: + +```toml +[agents.sandbox] +passthrough_env = ["GH_TOKEN", "GITHUB_TOKEN", "NPM_TOKEN"] +``` + +Each listed variable is read from the parent process environment at subprocess spawn time and injected into the worker's environment. Variables not in the list are stripped. + +When the [secret store](/docs/secrets) is available, `passthrough_env` is redundant -- credentials should be stored in the secret store, which injects tool secrets automatically. The field is additive and continues to work alongside the store. + +## Durable Binaries + +On hosted instances, the root filesystem is ephemeral -- machine image rollouts replace it. Binaries installed via `apt-get install` or similar disappear on the next deploy. + +The `{instance_dir}/tools/bin` directory is on the persistent volume and is prepended to `PATH` for all worker subprocesses. Binaries placed here survive restarts and rollouts. + +Workers are instructed about this in their system prompt: + +``` +Persistent binary directory: /data/tools/bin (on PATH, survives restarts and rollouts) +Binaries installed via package managers (apt, brew, etc.) land on the root filesystem +which is ephemeral on hosted instances -- they disappear on rollouts. To install a tool +durably, download or copy the binary into /data/tools/bin. +``` + +The `GET /agents/tools` API endpoint lists installed binaries for dashboard observability: + +```json +{ + "tools_bin": "/data/tools/bin", + "binaries": [ + { "name": "gh", "size": 1234567, "modified": "2026-02-20T14:15:00Z" }, + { "name": "ripgrep", "size": 3456789, "modified": "2026-02-15T10:30:00Z" } + ] +} +``` + +## Leak Detection + +Secret-pattern detection is enforced at **channel egress** (outbound user-visible text), not as a hard-stop for worker tool execution. + +Channel output is blocked when patterns are detected in: + +- `reply` tool content +- Plaintext fallback sends when a model returns raw text instead of calling `reply` + +Worker and branch tool outputs no longer terminate the process on pattern hits. This keeps long-running jobs from failing when secrets are handled internally. + +Detected patterns include: + +- OpenAI keys (`sk-...`) +- Anthropic keys (`sk-ant-...`) +- GitHub tokens (`ghp_...`) +- Google API keys (`AIza...`) +- Discord bot tokens +- Slack tokens (`xoxb-...`, `xapp-...`) +- Telegram bot tokens +- PEM private keys +- Base64-encoded, URL-encoded, and hex-encoded variants of the above + +Detection also covers encoded forms -- secrets wrapped in base64, URL encoding, or hex are decoded and checked against the same patterns. + +If a channel egress leak is detected, outbound text is blocked. The raw leaked value is never logged -- only the detection event and a truncated non-reversible fingerprint are recorded for debugging. + +### OpenCode Workers + +OpenCode workers (external coding agent processes) apply the same output scrubbing, and also scan SSE text/tool output for secret patterns for observability: + +1. **Output scrubbing** (exact-match redaction of known secret values) -- runs first +2. **Leak-pattern scan** (regex pattern matching for unknown secrets) -- runs second + +The ordering ensures that stored tool secrets are redacted before pattern scanning runs, so expected secret values in worker output don't trigger false positives. + +## Dynamic Mode Switching + +Sandbox mode can be changed at runtime via the API or dashboard without restarting the agent. The `Sandbox` struct reads the current mode from a shared `ArcSwap` on every `wrap()` call. + +``` +PUT /agents/config +{ + "sandbox": { "mode": "disabled" } +} +``` + +Backend detection runs at startup regardless of the initial mode. If the sandbox starts disabled and is later enabled via the API, bubblewrap/sandbox-exec is already detected and ready to use. + +## Configuration + +```toml +[agents.sandbox] +mode = "enabled" # "enabled" | "disabled" +writable_paths = ["/home/user/shared-data"] # additional writable directories +passthrough_env = ["GH_TOKEN"] # env vars to forward to workers +``` + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `mode` | string | `"enabled"` | `"enabled"` for OS-level containment, `"disabled"` for passthrough | +| `writable_paths` | string[] | `[]` | Additional directories workers can write to beyond the workspace | +| `passthrough_env` | string[] | `[]` | Environment variable names to forward from the parent process | + +See [Configuration](/docs/config#agentssandbox) for the full config reference. + +## Protection Layers + +The sandbox is one layer in a defense-in-depth model: + +| Layer | What It Does | Scope | +|-------|-------------|-------| +| **Sandbox (filesystem)** | Read allowlist + writable workspace/writable_paths/tmp; blocks agent data dir | Shell, exec subprocesses | +| **Env sanitization** | Clean environment, no inherited secrets | All subprocesses (including passthrough mode) | +| **File tool workspace guard** | Path validation against workspace boundary | File tool only (in-process) | +| **Exec env var blocklist** | Blocks `LD_PRELOAD`, `DYLD_INSERT_LIBRARIES`, etc. | Exec tool | +| **Leak detection** | Regex scan of outbound channel text for secret patterns | Channel egress (`reply` + plaintext fallback) | +| **Output scrubbing** | Exact-match redaction of known secret values | Worker output, status updates, OpenCode events | +| **[Secret store](/docs/secrets)** | Categorized credential storage, config resolution, tool secret injection | All agents | +| **Permissions system** | Application-level tool access control | All tools | + +The sandbox and permissions system are complementary. The [permissions system](/docs/permissions) controls which tools an agent can use and what paths the LLM is allowed to access at the application level. The sandbox enforces filesystem boundaries at the kernel level for subprocesses that are allowed to run. diff --git a/docs/content/docs/(configuration)/secrets.mdx b/docs/content/docs/(configuration)/secrets.mdx new file mode 100644 index 000000000..52342560c --- /dev/null +++ b/docs/content/docs/(configuration)/secrets.mdx @@ -0,0 +1,299 @@ +--- +title: Secret Store +description: Credential storage with categories, config resolution, encryption at rest, and output scrubbing. +--- + +# Secret Store + +Instance-level credential storage. Secrets are stored in a local database shared across all agents, resolved from `config.toml` via the `secret:` prefix, and injected into worker subprocesses based on their category. Values are scrubbed from tool output and status text, and are not included in LLM context by design. + +## Two Categories + +Every secret has a category that controls subprocess exposure: + +| Category | Subprocess Exposure | Use Case | +|----------|-------------------|----------| +| **System** | Never exposed | LLM API keys, messaging tokens, webhook secrets | +| **Tool** | Injected as env vars | `GH_TOKEN`, `NPM_TOKEN`, `AWS_ACCESS_KEY_ID` | + +All secrets are readable by the agent's internal Rust code via `SecretsStore::get()`. The category only determines whether the value is passed to worker subprocesses as an environment variable. + +### Auto-Categorization + +When you add a secret without specifying a category, the store assigns one based on the name: + +**System** (never exposed to subprocesses): +- LLM provider keys (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `OPENROUTER_API_KEY`, `GROQ_API_KEY`, `DEEPSEEK_API_KEY`, `XAI_API_KEY`, `MISTRAL_API_KEY`, `GEMINI_API_KEY`, etc.) +- Messaging adapter tokens (`DISCORD_BOT_TOKEN`, `SLACK_BOT_TOKEN`, `SLACK_APP_TOKEN`, `TELEGRAM_BOT_TOKEN`, `TWITCH_OAUTH_TOKEN`, etc.) +- Email credentials (`EMAIL_IMAP_USERNAME`, `EMAIL_IMAP_PASSWORD`, `EMAIL_SMTP_USERNAME`, `EMAIL_SMTP_PASSWORD`) +- Internal tool keys (`BRAVE_SEARCH_API_KEY`) +- Named adapter instance tokens -- any name matching `{PLATFORM}_{INSTANCE}_{FIELD}` for a known adapter field (e.g. `DISCORD_ALERTS_BOT_TOKEN`, `SLACK_SUPPORT_APP_TOKEN`, `TWITCH_GAMING_OAUTH_TOKEN`) + +**Tool** (everything else): +- Any unrecognized name defaults to Tool -- exposed to worker subprocesses as an environment variable +- Examples: `GH_TOKEN`, `NPM_TOKEN`, `AWS_ACCESS_KEY_ID`, `DOCKER_TOKEN`, `CARGO_REGISTRY_TOKEN` + +Auto-categorization is driven by the `SystemSecrets` trait. Each config section (LLM, messaging adapters, search integrations) declares its own credential fields. Adding a new adapter or provider automatically extends categorization without updating a central list. + +You can override the auto-categorization by specifying a category explicitly when adding a secret. + +## Config Resolution + +Any string value in `config.toml` supports three resolution modes: + +``` +secret:NAME → look up NAME in the secret store +env:VAR_NAME → read VAR_NAME from the system environment +anything else → literal value +``` + +The `secret:` prefix is the recommended way to reference credentials in config: + +```toml +[llm] +anthropic_key = "secret:ANTHROPIC_API_KEY" +openai_key = "secret:OPENAI_API_KEY" + +[messaging.discord] +token = "secret:DISCORD_BOT_TOKEN" +``` + +This keeps `config.toml` free of plaintext credentials. The secret store resolves references at config load time via a thread-local store reference. + +### Resolution Order + +For LLM keys specifically, the resolution chain is: + +``` +config.toml value (secret: / env: / literal) + → implicit env fallback (ANTHROPIC_API_KEY, etc.) + → missing +``` + +If `anthropic_key` is set to `"secret:ANTHROPIC_API_KEY"` and the secret store has that key, it resolves to the stored value. If the store doesn't have it, the key is treated as missing and the implicit env fallback is tried. + +## How Secrets Reach Subprocesses + +Tool-category secrets are injected into worker subprocesses as environment variables. The flow: + +``` +Worker calls shell("npm publish") + → Sandbox.wrap() builds the subprocess command + → SecretsStore.tool_env_vars() returns Tool-category secrets + → Each secret is injected via --setenv (bubblewrap) or Command::env() + → Subprocess sees NPM_TOKEN in its environment + → System-category secrets (ANTHROPIC_API_KEY, etc.) are NOT injected +``` + +This works alongside [environment sanitization](/docs/sandbox#environment-sanitization). The subprocess starts with a clean environment -- no inherited variables from the parent process. Only the safe baseline variables (`PATH`, `HOME`, `USER`, `LANG`, `TERM`, `TMPDIR`), `passthrough_env` entries, and tool secrets are present. + +### Worker-Created Secrets + +Workers have a `secret_set` tool that lets them store credentials directly into the secret store. This enables autonomous workflows where a worker creates accounts, generates API keys, or obtains tokens that should persist for future use. + +``` +Worker creates a GitHub bot account + → Worker calls secret_set(name: "GH_TOKEN", value: "ghp_abc...") + → Secret is stored with auto-categorized category (tool) + → All future workers see GH_TOKEN in their environment +``` + +The tool accepts an optional `category` parameter to override auto-categorization. If omitted, the same rules as the API apply -- known internal credentials are categorized as system, everything else as tool. + +## Encryption at Rest + +The store has two modes: + +### Unencrypted (Default) + +Secrets are stored as plaintext in a redb database. All secret store features work -- categories, env injection, output scrubbing, config resolution. Only encryption at rest is missing. + +This is the default because it requires zero setup. The store is functional immediately. + +### Encrypted (Opt-In) + +AES-256-GCM encryption with a master key derived via Argon2id. The master key lives in the OS credential store, never on disk: + +| Platform | Credential Store | Isolation | +|----------|-----------------|-----------| +| macOS | Keychain (Security framework) | Access controlled by code signature -- worker subprocesses can't retrieve the key | +| Linux | Kernel keyring (`keyctl` syscalls) | Workers are spawned with a fresh empty session keyring via `pre_exec` | +| Other | Not available | Encryption cannot be enabled | + +#### Encryption Lifecycle + +``` +Unencrypted (default) + → enable_encryption() → Unlocked (encrypted at rest, secrets readable) + → lock() → Locked (secrets unreadable, store sealed) + → unlock(password) → Unlocked + → rotate_key() → Unlocked (new key, all secrets re-encrypted) +``` + +**States:** + +| State | Reads | Writes | Description | +|-------|-------|--------|-------------| +| Unencrypted | Yes | Yes | Plaintext storage, no master key | +| Unlocked | Yes | Yes | Encrypted at rest, master key cached in memory | +| Locked | No | No | Encrypted, master key evicted -- all operations return an error | + +When encryption is enabled: + +1. A random 16-byte salt is generated and stored in the database +2. The password is derived into a 256-bit key via Argon2id (memory=64MB, iterations=3, parallelism=4) +3. All existing secrets are re-encrypted with unique 12-byte nonces +4. A sentinel value is encrypted and stored -- used to validate the key on unlock without decrypting every secret +5. The master key is stored in the OS credential store + +#### Retrieving the Master Key + +If you need to retrieve the master key after encryption (e.g., you didn't copy it during setup), you can read it directly from the OS credential store: + +**macOS:** + +```bash +security find-generic-password -s "sh.spacebot.master-key" -a "instance" -w +``` + +**Linux:** + +The key is stored in the kernel keyring under the description `sh.spacebot.master-key:instance`. Use `keyctl` to search for it: + +```bash +keyctl search @s user "sh.spacebot.master-key:instance" +# returns the key ID, then read it: +keyctl print +``` + +## API + +All endpoints operate on the instance-level secret store. No agent scoping is needed -- secrets are shared across all agents. + +### CRUD + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `GET` | `/api/secrets` | List all secrets (names, categories, timestamps -- not values) | +| `PUT` | `/api/secrets` | Add or update a secret | +| `DELETE` | `/api/secrets/{name}` | Remove a secret | +| `GET` | `/api/secrets/{name}/info` | Get metadata for a specific secret | + +**PUT body:** +```json +{ + "name": "GH_TOKEN", + "value": "ghp_abc123...", + "category": "tool" +} +``` + +If `category` is omitted, auto-categorization assigns one based on the name. + +### Store Status + +``` +GET /api/secrets/status +``` + +Returns the current store state, secret count, encryption status, and category breakdown. + +### Encryption + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `POST` | `/api/secrets/encrypt` | Enable encryption (returns master key) | +| `POST` | `/api/secrets/unlock` | Unlock with password | +| `POST` | `/api/secrets/lock` | Lock the store (evict master key) | +| `POST` | `/api/secrets/rotate` | Generate new key, re-encrypt all secrets | + +### Migration + +``` +POST /api/secrets/migrate +``` + +Scans `config.toml` for literal (plaintext) key values in known credential fields. For each one found: + +1. Stores the value in the secret store with the appropriate category +2. Replaces the literal value in `config.toml` with a `secret:NAME` reference +3. Writes the updated `config.toml` to disk + +Scanned fields are driven by `SystemSecrets` trait implementations -- the same declarations used for auto-categorization. This covers: + +- All `[llm]` provider keys +- `[defaults]` search keys +- Default messaging adapter tokens (e.g. `[messaging.discord].token`) +- Named adapter instance tokens in `[[messaging.*.instances]]` arrays (e.g. `DISCORD_ALERTS_BOT_TOKEN` for an instance named `"alerts"`) + +Values already using `env:` or `secret:` prefixes are skipped. This is a one-shot operation -- run it once after setting up the secret store to migrate existing plaintext credentials. + +### Export / Import + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `POST` | `/api/secrets/export` | Export all secrets as JSON (values included) | +| `POST` | `/api/secrets/import` | Import secrets from a JSON export | + +Export returns all secrets with their plaintext values, categories, and timestamps. The store must be unlocked (or unencrypted) to export. Import merges secrets into the store -- existing secrets with the same name are updated. + +## Output Protection + +Secret values are protected from appearing in tool output and LLM context through two layers: + +### Output Scrubbing + +The `StreamScrubber` performs exact-match redaction of all stored secret values across tool output. It handles chunk boundaries -- if a secret value spans two output chunks, it's still detected and redacted. + +All tool secrets and system secrets are registered with the scrubber. When a match is found, the value is replaced with `[REDACTED]`. + +### Leak Detection + +Regex-based pattern matching scans for secrets that may not be in the store. This catches secrets that were never added to the store but appear in output (e.g., hardcoded in source files). Detected patterns include API key formats for major providers, PEM private keys, and encoded variants (base64, URL-encoded, hex). + +See [Sandbox -- Leak Detection](/docs/sandbox#leak-detection) for the full list of detected patterns. + +The two layers run in sequence: scrubbing first (exact match), then leak detection (pattern match). This prevents stored secrets from triggering false-positive leak detection kills. + +## On-Disk Layout + +``` +~/.spacebot/data/ +└── secrets.redb # redb database with three tables: + ├── secrets # name → value (plaintext or nonce+ciphertext) + ├── secrets_metadata # name → JSON (category, timestamps) + └── secrets_config # encryption flag, argon2 salt, sentinel +``` + +The secrets database is instance-level -- a single store shared across all agents. It is separate from the main `spacebot.db` SQLite database. It uses redb for single-writer, lock-free reads. + +On first startup after upgrading from a per-agent store layout, the bootstrap process automatically migrates secrets from any legacy `~/.spacebot/agents/{id}/data/secrets.redb` files into the instance-level store. + +## Configuration + +The secret store requires no configuration in `config.toml`. It initializes automatically at instance startup, before config loading, and is shared across all agents. + +To use stored secrets in config, replace literal values or `env:` references with `secret:` references: + +```toml +[llm] +anthropic_key = "secret:ANTHROPIC_API_KEY" +openai_key = "secret:OPENAI_API_KEY" + +[messaging.discord] +token = "secret:DISCORD_BOT_TOKEN" + +[messaging.telegram] +token = "secret:TELEGRAM_BOT_TOKEN" +``` + +The `secret:` prefix works anywhere `env:` works. Both can coexist in the same config -- some keys from the store, others from environment variables. + +## Relationship to Other Systems + +| System | Relationship | +|--------|-------------| +| [Sandbox](/docs/sandbox) | Injects tool secrets into sandboxed subprocesses. Environment sanitization ensures only tool-category secrets reach workers. | +| [Configuration](/docs/config) | `secret:` prefix in config values resolves from the store at load time. Migration endpoint converts plaintext config to `secret:` references. | +| [Permissions](/docs/permissions) | Permissions control which tools agents can use. The secret store controls which credentials those tools receive. Complementary layers. | +| `passthrough_env` | Forwards env vars from the parent process. Redundant when using the secret store -- credentials should be stored in the store instead. Both work simultaneously. | diff --git a/docs/content/docs/(core)/agents.mdx b/docs/content/docs/(core)/agents.mdx index e89237cdf..30b73746c 100644 --- a/docs/content/docs/(core)/agents.mdx +++ b/docs/content/docs/(core)/agents.mdx @@ -232,7 +232,9 @@ Returns agents, humans, links, and groups for rendering. │ │ ├── data/ │ │ │ ├── spacebot.db # SQLite (memories, conversations, cron jobs) │ │ │ ├── lancedb/ # LanceDB (embeddings, FTS) -│ │ │ └── config.redb # redb (agent-level settings, secrets) +│ │ │ ├── config.redb # redb (agent-level settings) +│ │ │ ├── settings.redb # redb (runtime settings) +│ │ │ └── secrets.redb # redb (secret store) │ │ └── archives/ # compaction transcripts │ │ │ └── engineering/ diff --git a/docs/content/docs/(core)/architecture.mdx b/docs/content/docs/(core)/architecture.mdx new file mode 100644 index 000000000..89dd11c79 --- /dev/null +++ b/docs/content/docs/(core)/architecture.mdx @@ -0,0 +1,419 @@ +--- +title: Architecture +description: System-level overview of how Spacebot's processes, databases, and messaging layer fit together. +--- + +# Architecture + +Spacebot is a single binary that runs multiple concurrent AI processes, each with a dedicated role. There's no server to install, no message broker, no external database. Everything is embedded -- the LLM orchestration, the databases, the messaging adapters, the control API, and the web UI all run inside one process. + +This page is the system-level view. It explains how the pieces connect. For deep dives into individual subsystems, see the linked pages throughout. + +## The Problem + +Most AI agent systems use a single LLM session for everything -- conversation, thinking, tool execution, memory retrieval, and context management all happen in one thread. This creates fundamental bottlenecks: + +- **Blocking:** When the agent is running a tool or compacting context, the user waits. +- **Context pollution:** Tool outputs, internal reasoning, and raw search results fill the context window alongside conversation. +- **No specialization:** The same prompt and model handle tasks that have very different requirements. +- **No concurrency:** One thing happens at a time. + +Spacebot's architecture is designed around one principle: **delegation is the only way work gets done.** + +## Process Model + +Five process types, each implemented as a Rig `Agent`. They differ in system prompt, available tools, history management, and hooks. + +``` +┌─────────────────────────────────────────────────────────┐ +│ Channel │ +│ User-facing conversation. Has personality and soul. │ +│ Never blocks. Delegates everything. │ +│ │ +│ Tools: reply, branch, spawn_worker, route, cancel, │ +│ skip, react, cron, send_file, send_message │ +├────────────┬────────────────────────┬───────────────────┤ +│ │ │ │ +│ ┌──────▼──────┐ ┌──────▼──────┐ │ +│ │ Branch │ │ Worker │ │ +│ │ │ │ │ │ +│ │ Fork of │ │ Independent │ │ +│ │ channel │ │ task. No │ │ +│ │ context. │ │ channel │ │ +│ │ Thinks, │ │ context. │ │ +│ │ recalls, │ │ Executes. │ │ +│ │ returns a │ │ │ │ +│ │ conclusion. │ │ Shell, file,│ │ +│ │ │ │ exec, browse│ │ +│ │ Memory │ │ │ │ +│ │ tools only. │ │ Fire-and- │ │ +│ └─────────────┘ │ forget or │ │ +│ │ interactive.│ │ +│ └─────────────┘ │ +├─────────────────────────────────────────────────────────┤ +│ Compactor │ +│ Programmatic monitor. NOT an LLM process. │ +│ Watches context size, triggers compaction workers. │ +│ 80% → background, 85% → aggressive, 95% → emergency │ +├─────────────────────────────────────────────────────────┤ +│ Cortex │ +│ System-level observer. Sees across all channels. │ +│ Generates the memory bulletin — an LLM-curated │ +│ briefing injected into every channel's prompt. │ +└─────────────────────────────────────────────────────────┘ +``` + +### How Delegation Works + +The channel never searches memories, executes shell commands, or does heavy work. When it needs to think, it creates a **branch** -- a fork of its conversation context that goes off to reason, recall memories, and return a conclusion. When it needs work done, it spawns a **worker** -- an independent process with task tools and no conversation context. + +The channel is always responsive. Branches and workers run concurrently in `tokio::spawn`. Multiple branches can run simultaneously (configurable limit). Multiple workers can run simultaneously. The channel continues accepting messages while they work. + +``` +User message arrives + → Channel LLM turn + → Decides it needs to think → spawns Branch + → Decides it needs code written → spawns Worker + → Replies to user immediately + → Branch finishes → result injected into channel history → channel retriggered + → Worker finishes → status update injected → channel retriggered +``` + +For detailed coverage of each process type, see [Agents](/docs/agents), [Compaction](/docs/compaction), and [Cortex](/docs/cortex). + +## Inter-Process Communication + +Each agent uses two `broadcast::channel` buses: + +- `event_tx` -- control/lifecycle events shared by channel, branches, workers, compactor, and UI streams +- `memory_event_tx` -- memory-save telemetry consumed by the cortex (`MemorySaved` events only) + +This split keeps high-volume memory writes off the control bus so channel control events are less likely to lag under load. + +### Event Types + +| Event | Producer | Consumer | Purpose | +|-------|----------|----------|---------| +| `BranchStarted` | Channel | Status block | Branch is running | +| `BranchResult` | Branch | Channel | Conclusion ready, retrigger | +| `WorkerStarted` | Channel | Status block | Worker is running | +| `WorkerStatus` | Worker | Channel, Status block | Progress update via `set_status` | +| `WorkerComplete` | Worker | Channel | Task done, retrigger | +| `ToolStarted` | Hook | Channel, UI | Tool call in progress | +| `ToolCompleted` | Hook | Channel, UI | Tool call finished | +| `MemorySaved` | Branch, Cortex | Cortex | New memory telemetry for signal buffer | +| `CompactionTriggered` | Compactor | Channel | Context compacted | +| `StatusUpdate` | Various | UI (SSE) | Typing indicators, lifecycle | +| `TaskUpdated` | Branch, Worker | UI | Task board change | +| `AgentMessageSent` | Channel | Link routing | Inter-agent message | +| `AgentMessageReceived` | Link routing | Channel | Inbound inter-agent message | + +### Retriggering + +When a branch or worker completes, the channel doesn't poll for results. The completion event **retriggers** the channel -- it runs another LLM turn with the result injected into its history. This keeps the channel reactive without polling loops. + +Retrigger events are debounced. If multiple branches complete within a short window, the channel batches them into a single turn. A retrigger limit (default: 3 per turn) prevents infinite cascades where a branch result triggers a new branch that triggers another retrigger. + +### Status Block + +Every turn, the channel receives a live snapshot of all active processes: + +```markdown +## Currently Active + +### Workers +- **[code-review]** (running, 45s) — "Reviewing changes in src/memory/store.rs" +- **[test-runner]** (waiting for input, 2m) — "Tests passed. Awaiting further instructions." + +### Recently Completed +- **[search]** completed 30s ago — "Found 3 relevant files for the query." +``` + +Workers set their own status via the `set_status` tool. Short branches (< 3 seconds) are invisible in the status block to avoid noise. The status block is injected into the system prompt, giving the LLM awareness of concurrent activity. + +## Data Layer + +Three embedded databases, each purpose-built. No server processes, no network connections. Everything lives in the agent's data directory. + +``` +~/.spacebot/agents/{agent_id}/data/ +├── spacebot.db # SQLite — relational data +├── lancedb/ # LanceDB — vector embeddings, full-text search +├── config.redb # redb — key-value settings +├── settings.redb # redb — runtime settings +└── secrets.redb # redb — secret store (categories, encryption) +``` + +### SQLite (via sqlx) + +The primary database. Stores everything that benefits from relational queries: + +| Table | Purpose | +|-------|---------| +| `memories` | Memory content, types, importance scores, timestamps | +| `associations` | Graph edges between memories (weighted, typed) | +| `conversation_messages` | Persistent conversation history per channel | +| `channels` | Active channel registry with platform metadata | +| `cron_jobs` | Scheduled task definitions | +| `cron_executions` | Execution history for cron jobs | +| `worker_runs` | Worker execution history with transcripts | +| `branch_runs` | Branch execution history | +| `cortex_events` | Cortex action log (bulletin generations, maintenance) | +| `cortex_chat_messages` | Persistent admin chat with cortex | +| `tasks` | Structured task board (backlog → in_progress → done) | +| `ingestion_progress` | Chunk-level progress for file ingestion | +| `agent_profile` | Cortex-generated personality data | + +Migrations are in `migrations/` and are **immutable once committed**. Schema changes always go in new migration files. See [Memory](/docs/memory) for the memory graph schema. + +### LanceDB + +Vector storage and search. Paired with SQLite on memory ID. + +- **Embeddings** stored in Lance columnar format with HNSW indexing +- **Full-text search** via built-in Tantivy integration +- **Hybrid search** combines vector similarity and keyword matching via Reciprocal Rank Fusion (RRF) + +The embedding model runs locally via FastEmbed -- no external API calls for embeddings. See [Memory](/docs/memory) for search details. + +### redb + +Embedded key-value stores for configuration, settings, and secrets. + +- **config.redb** — key-value pairs (UI preferences, feature flags) +- **settings.redb** — runtime settings (worker_log_mode, etc.) +- **secrets.redb** — per-agent credential storage with categories (system/tool), optional AES-256-GCM encryption at rest. See [Secret Store](/docs/secrets). + +Separated from SQLite so credentials can be managed and backed up independently. + +## Messaging Layer + +Spacebot connects to multiple messaging platforms simultaneously. All adapters implement the same `Messaging` trait and feed into a unified inbound message stream. + +``` +Discord ─┐ +Slack ───┤ +Telegram ┼──→ MessagingManager ──→ InboundMessage stream ──→ main.rs event loop +Twitch ──┤ │ +Webhook ─┤ ▼ +WebChat ─┘ Channel.handle_message() + │ + ▼ + OutboundResponse + │ + ┌──────────────┼──────────────┐ + ▼ ▼ ▼ + Discord Slack Telegram +``` + +### Inbound Flow + +1. Platform adapter receives a message (Discord event, Slack webhook, Telegram update, etc.) +2. Adapter converts to `InboundMessage` — a unified type with text, media, sender info, conversation ID, and platform metadata +3. `MessagingManager` fans all adapters into a single `mpsc::channel` +4. `main.rs` event loop receives the message, resolves the target agent via message bindings, and routes to the appropriate `Channel` +5. If no `Channel` exists for this conversation ID, one is created and its event loop spawned + +### Outbound Flow + +1. Channel tools (reply, react, send_file) produce `OutboundResponse` values +2. Each channel has an outbound routing task that receives responses via `mpsc::channel` +3. The routing task determines the platform from the channel ID prefix (`discord:`, `slack:`, `telegram:`, etc.) +4. `MessagingManager::broadcast()` delivers the response to the correct platform adapter +5. Responses are also forwarded to SSE clients (WebChat, dashboard) for real-time UI updates + +### Message Bindings + +Each agent declares which messaging channels route to it: + +```toml +[[agents]] +id = "main" + +[[agents.bindings.discord]] +guild_id = "1323900500600422472" +channel_ids = ["1471388652562284626"] + +[[agents.bindings.telegram]] +chat_ids = [551234, -1001234567890] + +[[agents.bindings.webhook]] +endpoints = ["github-ci", "monitoring"] +``` + +When a message arrives, the binding resolver matches the conversation ID against all agent bindings. If no specific binding matches, the message goes to the default agent (if one is configured). See [Messaging](/docs/messaging) and the individual platform setup guides for configuration details. + +## LLM Integration + +Spacebot uses [Rig](https://github.com/0xPlaygrounds/rig) as the agentic loop framework. Every process is a Rig `Agent` with a custom `CompletionModel` implementation that routes through Spacebot's `LlmManager`. + +### Custom Model Layer + +Spacebot doesn't use Rig's built-in provider clients. Instead, `SpacebotModel` implements `CompletionModel` and delegates to `LlmManager`, which handles: + +- **Provider routing** — resolving model names to provider clients (Anthropic, OpenAI, Google, etc.) +- **Process-type defaults** — different models for channels, branches, workers, compactor, cortex +- **Task-type overrides** — specific models for coding, summarization, deep reasoning tasks +- **Fallback chains** — automatic fallback to alternative models on failure + +``` +Channel LLM call + → SpacebotModel.completion(messages, tools) + → LlmManager.resolve_model("anthropic/claude-sonnet-4-20250514") + → Anthropic client + → API call with prompt caching, custom parameters +``` + +See [Routing](/docs/routing) for the full routing configuration. + +### Agent Construction + +```rust +let agent = AgentBuilder::new(model.clone()) + .preamble(&system_prompt) + .hook(SpacebotHook::new(process_id, process_type, event_tx.clone())) + .tool_server_handle(tools.clone()) + .default_max_turns(50) + .build(); +``` + +### Hooks + +Two hook implementations control process behavior: + +**`SpacebotHook`** (channels, branches, workers) — sends `ProcessEvent`s for real-time status, tracks token usage, enforces cancellation signals, implements tool nudging (prompts the LLM to use tools if it responds with text instead of tool calls in early iterations), and runs leak detection on tool outputs. + +**`CortexHook`** (cortex only) — lighter implementation for system observation, no tool nudging. + +Hooks return `Continue`, `Terminate`, or `Skip` after each LLM turn, giving the system fine-grained control over process lifecycle. + +For profile synthesis, cortex uses Rig structured output (`prompt_typed`) so profile fields are schema-validated instead of parsed from free-form fenced JSON. + +### Max Turns + +Rig defaults to 0 (single call). Spacebot sets explicit limits per process type: + +| Process | Max Turns | Rationale | +|---------|-----------|-----------| +| Channel | 5 | Typically 1-3 turns. Prevents runaway conversations. | +| Branch | 10 | A few iterations to think, recall, and conclude. | +| Worker | 50 | Many iterations for complex tasks. Segmented into 25-turn blocks. | +| Compactor | 10 | Summarize and extract memories. Bounded. | +| Cortex | 10 | Bulletin generation. Single-pass with tool calls. | + +## Control API + +An embedded Axum HTTP server provides the control API for the dashboard and external integrations. Default port: `19898`. + +### Key Endpoint Groups + +| Group | Prefix | Purpose | +|-------|--------|---------| +| Agents | `/api/agents` | CRUD for agent definitions | +| Channels | `/api/channels` | Channel listing, history, deletion | +| Workers | `/api/workers` | Worker status, history, timeline | +| Cortex | `/api/cortex` | Bulletin, profile, cortex chat | +| Memory | `/api/memories` | Memory CRUD, graph queries | +| Config | `/api/config` | Runtime configuration read/write | +| Providers | `/api/providers` | LLM provider key management | +| Links | `/api/links` | Communication graph management | +| Tasks | `/api/tasks` | Task board CRUD | +| Cron | `/api/cron` | Scheduled task management | +| System | `/api/system` | Health, version, metrics | +| WebChat | `/api/webchat` | Embedded chat interface | +| Models | `/api/models` | Available model listing | +| Topology | `/api/topology` | Full communication graph | + +The dashboard UI is a React SPA embedded in the binary via `rust-embed` and served at the root path. It communicates with these API endpoints for all operations. + +### Real-Time Updates + +The API supports Server-Sent Events (SSE) for real-time streaming to connected clients. Status updates, tool call progress, worker lifecycle events, and memory changes are all pushed via SSE, giving the dashboard and WebChat live visibility into agent activity. + +## Startup Sequence + +``` +CLI (clap) → parse args + → Load config.toml + → Optionally daemonize (Unix socket for IPC) + → Build tokio runtime + → Initialize tracing + OpenTelemetry (optional) + → run() + → Start IPC server (stop/status commands) + → Start Axum API server + → Initialize shared resources: + LlmManager, EmbeddingModel, PromptEngine, agent links + → For each agent: + → Run SQLite migrations + → Initialize MemoryStore, LanceDB tables + → Load RuntimeConfig + identity + skills + → Best-effort startup warmup pass (bounded wait) + → Initialize MessagingManager (start all platform adapters) + → Initialize CronScheduler + → Start Cortex loops (warmup, bulletin fallback, association, ready-task) + → Register agent in active agents map + → Enter main event loop (tokio::select!) + → Inbound messages → route to Channel instances + → Agent registration/removal + → Provider setup events + → Shutdown signal → graceful shutdown +``` + +All long-running loops respect a shutdown signal via `broadcast::channel`. On shutdown, active workers are cancelled, channels are flushed, and database connections are closed cleanly. + +## Module Structure + +The crate uses the sibling file module pattern -- `src/memory.rs` is the module root for `src/memory/`, never `mod.rs`. + +``` +src/ +├── main.rs — CLI entry, config, startup, event loop +├── lib.rs — module declarations, shared types +├── config.rs — configuration loading and validation +├── error.rs — top-level Error enum +├── db.rs — database connection bundle +│ +├── agent/ — process implementations +│ ├── channel.rs — user-facing conversation +│ ├── branch.rs — forked thinking process +│ ├── worker.rs — task execution +│ ├── compactor.rs — context monitor +│ ├── cortex.rs — system observer +│ └── status.rs — live status snapshot +│ +├── tools/ — 27 tool implementations (one per file) +├── memory/ — memory graph, search, embeddings +├── llm/ — model routing, provider clients +├── messaging/ — platform adapters +├── conversation/ — history persistence, context assembly +├── prompts/ — template engine +├── hooks/ — PromptHook implementations +├── cron/ — scheduled tasks +├── api/ — 21 Axum endpoint modules +├── identity/ — identity file loading +├── secrets/ — secret store, OS keystore, output scrubbing +├── settings/ — key-value settings +├── tasks/ — task board +├── links/ — communication graph types +├── skills/ — skill management +├── opencode/ — OpenCode worker integration +├── sandbox/ — command sandboxing +├── telemetry/ — metrics (feature-gated) +└── update/ — self-update checker +``` + +## Design Principles + +**Never block the channel.** The channel never waits on branches, workers, or compaction. If something takes time, it runs concurrently and retriggers the channel when done. + +**Raw data never reaches the channel.** Memory recall goes through a branch, which curates. The channel gets clean conclusions, not raw database rows. + +**Workers have no channel context.** A worker gets a task description and tools. If something needs conversation context, it's a branch, not a worker. + +**The compactor is not an LLM.** It's a programmatic monitor that watches a number and spawns workers. The LLM work happens in the workers it spawns. + +**Prompts are files.** System prompts live in `prompts/` as Jinja2 templates, not as string constants in Rust code. Identity files (SOUL.md, IDENTITY.md, USER.md, ROLE.md) are loaded from the agent's workspace directory. + +**Three databases, three purposes.** SQLite for relational queries, LanceDB for vector search, redb for key-value config. Each doing what it's best at. + +**Graceful everything.** All loops respect shutdown signals. Errors are propagated, not silenced. The only exception is `.ok()` on channel sends where the receiver may already be dropped. diff --git a/docs/content/docs/(core)/cortex.mdx b/docs/content/docs/(core)/cortex.mdx index e349bfc6e..3355751ae 100644 --- a/docs/content/docs/(core)/cortex.mdx +++ b/docs/content/docs/(core)/cortex.mdx @@ -17,7 +17,9 @@ This is what makes Spacebot feel like it actually *knows* you. Without the bulle Bulletin generation is a two-phase process: programmatic retrieval, then LLM synthesis. -On a configurable interval (default: 60 minutes), the cortex: +When warmup is enabled (default), warmup is the primary bulletin refresher on `warmup.refresh_secs` (default: 15 minutes). The bulletin loop still runs on `bulletin_interval_secs` (default: 60 minutes) as a fallback when warmup is disabled or when the cached bulletin is stale (`bulletin_age_secs >= max(1, warmup.refresh_secs)`). + +Each bulletin generation pass does: 1. **Retrieves** memory data across eight predefined sections by querying the memory store directly (no LLM needed for retrieval): - Identity & Core Facts — typed search for Identity memories, sorted by importance @@ -34,7 +36,7 @@ On a configurable interval (default: 60 minutes), the cortex: This design avoids the problem of an LLM formulating search queries without conversation context. The retrieval phase uses `SearchMode::Typed`, `SearchMode::Recent`, and `SearchMode::Important` — metadata-based modes that query SQLite directly without needing vector embeddings or search terms. The LLM only gets involved for the part it's good at: turning structured data into readable prose. -The first bulletin is generated immediately on startup. Subsequent runs happen every `bulletin_interval_secs`. If a generation fails, the previous bulletin is preserved. If the memory graph is empty, an empty bulletin is stored without invoking the LLM. +On startup, Spacebot runs a best-effort warmup pass before adapters accept traffic (bounded wait), so the first bulletin is usually already present when the first user message arrives. If generation fails, the previous bulletin is preserved. If the memory graph is empty, an empty bulletin is stored without invoking the LLM. ### What Channels See @@ -138,6 +140,17 @@ The cortex is the only singleton in the system (per agent). There's one cortex p **The cortex** is an LLM-assisted process that sees across all channels. It doesn't manage context size (that's the compactor's job). It manages the memory bulletin, and will eventually handle memory coherence and system health. +## Interactive Cortex Chat + +Spacebot also exposes a direct admin chat session with the cortex. This is intentionally self-referential and diagnostic-focused: + +- The system prompt includes embedded architecture context (`AGENTS.md`) and changelog highlights. +- The prompt includes a live redacted runtime-config snapshot so behavior can be diagnosed against current resolved values. +- `config_inspect` returns live hot-reloaded config sections on demand. +- `spacebot_docs` reads embedded Spacebot docs/changelog/AGENTS content directly from the binary. + +This makes cortex chat a practical control-room interface for troubleshooting, validation, and operations — not a user-facing conversation mode. + ## Configuration ```toml @@ -151,8 +164,8 @@ bulletin_interval_secs = 3600 # Target word count for the memory bulletin. bulletin_max_words = 500 -# Worker is considered hanging if no status update for this long. -worker_timeout_secs = 300 +# Worker is considered hanging if no activity for this long. +worker_timeout_secs = 600 # Branch is considered stale after this duration. branch_timeout_secs = 60 @@ -223,7 +236,7 @@ Branch, worker, and cron dispatch paths consult a derived `ready_for_work` signa - warmup state is `warm` - embedding model is ready -- bulletin age is fresh (<= `max(60s, refresh_secs * 2)`) +- bulletin age is fresh (`<= max(60s, refresh_secs * 2)`) If dispatch arrives while not ready, Spacebot does **not** block the channel or scheduler: diff --git a/docs/content/docs/(core)/meta.json b/docs/content/docs/(core)/meta.json index 53cfc2a04..0e07391ec 100644 --- a/docs/content/docs/(core)/meta.json +++ b/docs/content/docs/(core)/meta.json @@ -2,6 +2,7 @@ "title": "Core Concepts", "pages": [ "philosophy", + "architecture", "agents", "memory", "routing", diff --git a/docs/content/docs/(deployment)/roadmap.mdx b/docs/content/docs/(deployment)/roadmap.mdx index 6711bdd2d..09673870f 100644 --- a/docs/content/docs/(deployment)/roadmap.mdx +++ b/docs/content/docs/(deployment)/roadmap.mdx @@ -18,7 +18,7 @@ The full message-in → LLM → response-out pipeline is wired end-to-end across - **Config** — hierarchical TOML with `Config`, `AgentConfig`, `ResolvedAgentConfig`, `Binding`, `MessagingConfig`. File watcher with event filtering and content hash debounce for hot-reload. - **Multi-agent** — per-agent database isolation, `Agent` struct bundles all dependencies - **Database connections** — SQLite + LanceDB + redb per-agent, migrations for all tables -- **LLM** — `SpacebotModel` implements Rig's `CompletionModel`, routes through `LlmManager` via HTTP with retries and fallback chains across 11 providers (Anthropic, OpenAI, OpenRouter, Z.ai, Groq, Together, Fireworks, DeepSeek, xAI, Mistral, OpenCode Zen) +- **LLM** — `SpacebotModel` implements Rig's `CompletionModel`, routes through `LlmManager` via HTTP with retries and fallback chains across 13 providers (Anthropic, OpenAI, OpenRouter, Kilo Gateway, Z.ai, Groq, Together, Fireworks, DeepSeek, xAI, Mistral, OpenCode Zen, OpenCode Go) - **Model routing** — `RoutingConfig` with process-type defaults, task overrides, fallback chains - **Memory** — full stack: types, SQLite store (CRUD + graph), LanceDB (embeddings + vector + FTS), fastembed, hybrid search (RRF fusion). `memory_type` filter wired end-to-end through SearchConfig. `total_cmp` for safe sorting. - **Memory maintenance** — decay + prune implemented @@ -36,7 +36,7 @@ The full message-in → LLM → response-out pipeline is wired end-to-end across - **Telegram adapter** — full teloxide implementation (long polling, typing indicators, attachment extraction, chat/DM filtering, 4096 char splitting) - **Slack adapter** — full slack-morphism implementation (Socket Mode, thread replies, file upload v2, reactions, streaming via edit, workspace/channel/DM filtering via hot-reloadable permissions) - **Webhook adapter** — Axum HTTP server (POST /send, GET `/poll/{id}`, GET /health) -- **Tools** — 16 tools implement Rig's `Tool` trait with real logic (reply, branch, spawn_worker, route, cancel, skip, react, memory_save, memory_recall, set_status, shell, file, exec, browser, cron, web_search) +- **Tools** — 20+ tools implement Rig's `Tool` trait with real logic (including task board tools, memory tools, `spacebot_docs`, and `config_inspect` alongside reply/branch/worker/browser/shell primitives) - **Workspace containment** — file tool validates paths stay within workspace boundary, shell/exec tools block instance directory traversal, sensitive file access, and secret env var leakage - **Conversation persistence** — `ConversationLogger` with fire-and-forget SQLite writes, compaction archiving - **Cron** — scheduler with timers, active hours, circuit breaker (3 failures → disable), creates real channels. CronTool wired into channel tool factory. @@ -61,7 +61,7 @@ The full message-in → LLM → response-out pipeline is wired end-to-end across ### Streaming -Implement `SpacebotModel.stream()` with SSE parsing. The messaging adapters already handle `StreamStart`/`StreamChunk`/`StreamEnd` response types — this is purely the LLM layer. +`SpacebotModel.stream()` is implemented with provider SSE parsing. Remaining work is wiring stream-native Rig loops (`stream_prompt`) into runtime paths that still use non-streaming `.prompt(...)` calls (for example, cortex chat and channel loops). ### Cortex Consolidation @@ -71,9 +71,9 @@ Implement `SpacebotModel.stream()` with SSE parsing. The messaging adapters alre - Wire real values into `observe()` signal extraction - Implement CortexHook observation logic (anomaly detection, consolidation triggers) -### Secrets Store +### ~~Secrets Store~~ ✓ Shipped -Encrypted credentials in redb with `DecryptedSecret` wrapper type. ChaCha20-Poly1305 AEAD with a local key file. All sensitive config values (`token`, `*_key`) encrypted at rest. +Implemented. Per-agent credential storage in `secrets.redb` with two categories (system/tool), `secret:` config resolution, auto-migration from plaintext config, and optional AES-256-GCM encryption at rest with OS keystore integration. See [Secret Store](/docs/secrets). ### Cost Tracking @@ -97,7 +97,6 @@ Per-agent token usage and cost tracking with budget enforcement. Session, daily, ### Additional Channel Adapters -- **Email** — IMAP polling for inbound, SMTP for outbound. Each email thread maps to a conversation. - **WhatsApp** — Meta Cloud API. Hosted instances receive webhooks via the platform proxy. Self-hosted users point the callback URL at their own reverse proxy or Tailscale funnel. - **Matrix** — decentralized chat protocol. Bridges to self-hosted Matrix/Element deployments. - **iMessage** — macOS-only, AppleScript bridge. Personal use on self-hosted Mac instances. diff --git a/docs/content/docs/(features)/browser.mdx b/docs/content/docs/(features)/browser.mdx index 219bdde9f..4c299022f 100644 --- a/docs/content/docs/(features)/browser.mdx +++ b/docs/content/docs/(features)/browser.mdx @@ -42,8 +42,8 @@ Single `browser` tool with an `action` discriminator. All actions share one argu | Action | Required Args | Description | |--------|--------------|-------------| -| `launch` | -- | Start Chrome. Must be called first. | -| `close` | -- | Shut down Chrome and clean up all state. | +| `launch` | -- | Start Chrome (or reconnect to a persistent browser). Must be called first. | +| `close` | -- | Shut down, close tabs, or detach depending on `close_policy`. | ### Navigation @@ -130,6 +130,8 @@ Browser config lives in `config.toml` under `[defaults.browser]` (or per-agent o enabled = true # include browser tool in worker ToolServers headless = true # run Chrome without a visible window evaluate_enabled = false # allow JavaScript evaluation via the tool +persist_session = false # keep browser alive across worker lifetimes +close_policy = "close_browser" # what happens on close executable_path = "" # custom Chrome binary path (auto-detected if empty) screenshot_dir = "" # override screenshot storage location ``` @@ -143,34 +145,68 @@ id = "web-scraper" [agents.browser] evaluate_enabled = true # this agent's workers can run JS headless = false # show the browser window for debugging +persist_session = true # keep tabs and logins between worker runs +close_policy = "detach" # workers disconnect without closing tabs ``` When `enabled = false`, the browser tool is not registered on worker ToolServers. Workers for that agent won't see it in their available tools. +### Close Policy + +Controls what happens when a worker calls `close`: + +| Policy | Behavior | +|--------|----------| +| `close_browser` | Kill Chrome and reset all state. Default. | +| `close_tabs` | Close all tracked tabs but keep Chrome running. | +| `detach` | Disconnect without touching tabs or the browser process. | + +`close_policy` is most useful with `persist_session = true`. With `detach`, workers leave the browser exactly as they found it. + +## Persistent Sessions + +By default, each worker launches its own Chrome process. When the worker finishes, the browser dies and all session state (cookies, tabs, logins) is lost. + +With `persist_session = true`, all workers for an agent share a single browser instance via a `SharedBrowserHandle` held in `RuntimeConfig`. The browser and its tabs survive across worker lifetimes. + +When a new worker calls `launch` on a persistent browser that's already running, it reconnects to the existing process, discovers all open tabs, and can continue where the previous worker left off. Combined with `close_policy = "detach"`, workers leave the browser untouched when they finish. + +This is useful for: + +- **Login persistence** -- log in once, reuse the session across multiple worker runs. +- **Watching the agent work** -- set `headless = false` to see a visible Chrome window that stays open. +- **Multi-step workflows** -- a worker can leave tabs open for a follow-up worker to pick up. + +```toml +[defaults.browser] +headless = false # visible Chrome window +persist_session = true # keep alive across workers +close_policy = "detach" # workers just disconnect +``` + +Changing `persist_session` requires an agent restart. + ## Architecture ``` -Worker (Rig Agent) - │ - ├── shell, file, exec, set_status (standard worker tools) - │ - └── browser (BrowserTool) - │ - ├── Arc> (shared across tool invocations) - │ ├── Browser (chromiumoxide handle) - │ ├── pages: HashMap (target_id → Page) - │ ├── active_target (current tab) - │ └── element_refs (snapshot ref → ElementRef) - │ - └── Config - ├── headless - ├── evaluate_enabled - └── screenshot_dir +# Default mode (persist_session = false): +Worker A → BrowserTool { own BrowserState } → own Chrome process +Worker B → BrowserTool { own BrowserState } → own Chrome process + +# Persistent mode (persist_session = true): +RuntimeConfig → SharedBrowserHandle (Arc>) +Worker A → BrowserTool { shared state } ──┐ +Worker B → BrowserTool { shared state } ──┤→ single Chrome process +Worker C → BrowserTool { shared state } ──┘ ``` -Each worker gets its own `BrowserTool` instance with its own `BrowserState`. The state is behind `Arc>` because the Rig tool trait requires `Clone`. The Chrome process (and its CDP WebSocket handler task) live for the lifetime of the worker. +In both modes, `BrowserState` holds: +- `Browser` -- chromiumoxide handle +- `pages: HashMap` -- target_id to Page +- `active_target` -- current tab +- `element_refs` -- snapshot ref to ElementRef -The CDP handler runs as a background tokio task that polls the WebSocket stream. It's spawned during `launch` and dropped when the browser closes or the worker completes. +The state is behind `Arc>` because the Rig tool trait requires `Clone`. The CDP handler runs as a background tokio task that polls the WebSocket stream, spawned during `launch`. ## Implementation @@ -192,5 +228,5 @@ Element resolution: refs map to CSS selectors built from `[role='...']` and `[ar - **No file upload/download** -- chromiumoxide doesn't expose file chooser interception. Use `shell` + `curl` for downloads. - **No network interception** -- request blocking and response modification aren't exposed through the tool, though chromiumoxide supports it via raw CDP commands. - **No cookie/storage management** -- not exposed as tool actions. Could be added if needed. -- **Single browser per worker** -- each worker gets one Chrome process. No connection pooling across workers. +- **Single browser per worker by default** -- with `persist_session = false` (default), each worker gets its own Chrome process. With `persist_session = true`, all workers for an agent share one browser and reconnect to existing tabs on `launch`. - **Selector fragility** -- the `[role][aria-label]` selector strategy works for well-structured pages but can fail on pages with missing ARIA attributes. The `content` action + `evaluate` (when enabled) serve as fallbacks. diff --git a/docs/content/docs/(features)/cron.mdx b/docs/content/docs/(features)/cron.mdx index 946d2e4e9..19307c4da 100644 --- a/docs/content/docs/(features)/cron.mdx +++ b/docs/content/docs/(features)/cron.mdx @@ -44,6 +44,7 @@ The configuration table. One row per cron job. CREATE TABLE cron_jobs ( id TEXT PRIMARY KEY, prompt TEXT NOT NULL, + cron_expr TEXT, interval_secs INTEGER NOT NULL DEFAULT 3600, delivery_target TEXT NOT NULL, active_start_hour INTEGER, @@ -57,6 +58,7 @@ CREATE TABLE cron_jobs ( |--------|-------------| | `id` | Short unique name (e.g. "check-email", "daily-summary") | | `prompt` | The instruction to execute on each run | +| `cron_expr` | Optional strict wall-clock schedule (cron expression, e.g. `0 9 * * *`) | | `interval_secs` | Seconds between runs (3600 = hourly, 86400 = daily) | | `delivery_target` | Where to send results, format `adapter:target` (e.g. `discord:123456789`) | | `active_start_hour` | Optional start of active window (0-23, 24h local time) | @@ -112,6 +114,12 @@ delivery_target = "discord:123456789012345678" active_start_hour = 9 active_end_hour = 10 +[[agents.cron]] +id = "daily-standup" +prompt = "Post a standup reminder." +cron_expr = "0 9 * * 1-5" +delivery_target = "discord:123456789012345678" + [[agents.cron]] id = "check-inbox" prompt = "Check the inbox for anything that needs attention." @@ -130,6 +138,7 @@ A user says "check my email every day at 9am" and the channel LLM calls the `cro "action": "create", "id": "check-email", "prompt": "Check the user's email inbox and summarize any important messages.", + "cron_expr": "0 9 * * *", "interval_secs": 86400, "delivery_target": "discord:123456789", "active_start_hour": 9, @@ -164,7 +173,7 @@ If active hours are not set, the cron job runs at all hours. If a configured timezone is invalid, Spacebot logs a warning and falls back to server local timezone. -Active hours don't affect the timer interval — the timer still ticks at `interval_secs`. When a tick lands outside the active window, it's skipped. The next tick happens at the normal interval, not "as soon as the window opens." +For cron-expression jobs, active hours are evaluated at fire time and can further gate delivery. For legacy interval jobs, active hours don't change tick cadence — ticks outside the window are skipped. ## Circuit Breaker @@ -279,7 +288,7 @@ pub struct CronContext { ## What's Not Implemented Yet -- **Cron expressions** — only fixed intervals for now. A cron job that should run "at 9am daily" currently uses `interval_secs: 86400` with `active_start_hour: 9, active_end_hour: 10`. Real cron scheduling would be more precise. +- **Cron expressions in config/tool/API are now supported** and are preferred for exact local-time schedules. - **Error backoff** — on failure, the next attempt happens at the normal interval. Progressive backoff (30s → 1m → 5m → 15m → 60m) would reduce cost during outages. - **Cross-run context** — each cron job starts with a blank history. A cron job that needs to know what it found last time would need to use memory recall. - **Cortex management** — the cortex should be able to observe cron job health, re-enable circuit-broken jobs, and create new cron jobs based on patterns. diff --git a/docs/content/docs/(features)/meta.json b/docs/content/docs/(features)/meta.json index 0c4bb20b8..a9903832a 100644 --- a/docs/content/docs/(features)/meta.json +++ b/docs/content/docs/(features)/meta.json @@ -1,4 +1,4 @@ { "title": "Features", - "pages": ["workers", "opencode", "tools", "mcp", "browser", "cron", "skills", "ingestion"] + "pages": ["workers", "tasks", "opencode", "tools", "mcp", "browser", "cron", "skills", "ingestion"] } diff --git a/docs/content/docs/(features)/tasks.mdx b/docs/content/docs/(features)/tasks.mdx new file mode 100644 index 000000000..cf48bb645 --- /dev/null +++ b/docs/content/docs/(features)/tasks.mdx @@ -0,0 +1,343 @@ +--- +title: Tasks +description: Kanban-style task board with structured tracking, cortex pickup, and worker execution. +--- + +# Tasks + +A kanban-style task board built into every agent. Tasks are spec-driven documents — the description is a full markdown spec that evolves through conversation, with pre-filled subtasks as an execution plan. Each task has a short title, rich description, status, priority, subtasks, and a numeric reference (`#42`). Each agent has its own independent task store backed by its own SQLite database. + +Tasks are not tickets. They're living specs written for workers who have no conversation context. A good task description includes requirements, constraints, file paths, examples, and acceptance criteria. The branch refines the spec as the user clarifies scope, and moves it to `ready` when it's complete. The cortex picks it up and spawns a worker that executes against the spec. + +## Creation Paths + +Tasks enter the system three ways: + +### 1. Conversational (via branch tools) + +The primary path. A user manages tasks through natural conversation — creating, listing, updating, approving, and closing tasks by talking to the agent. The channel delegates to a branch, and the branch uses `task_create`, `task_list`, and `task_update` tools. + +``` +User: "Create a task to refactor the auth module, high priority" + → Channel branches + → Branch calls task_create( + title: "Refactor auth module", + priority: "high", + description: "## Goal\nExtract auth logic from ...\n\n## Requirements\n- ...\n## Constraints\n- ...", + subtasks: ["Audit current auth endpoints", "Extract shared middleware", "Update tests", "Verify CI passes"] + ) + → Branch returns: "Created task #7 with 4 subtasks" + +User: "Actually, we also need to migrate the session store to Redis" + → Channel branches + → Branch calls task_update(task_number: 7, description: "") + → Branch returns: "Updated #7 — added Redis migration to the spec" + +User: "Looks good, run it" + → Channel branches + → Branch calls task_update(task_number: 7, status: "ready") + → Cortex ready-task loop picks it up → Worker executes against the spec → Done +``` + +Tasks created conversationally default to `backlog` status. The branch writes a rich markdown description and pre-fills subtasks as an execution plan. The user refines scope through conversation, and the branch updates the spec accordingly. When the spec is complete, moving to `ready` triggers automatic execution. + +### 2. Cortex promotion (from Todo memories) + +The cortex bridges the gap between quick captures and structured work. A cortex loop scans recent `Todo` memories, evaluates whether they're actionable, and promotes them to tasks in `pending_approval` status. The human reviews and approves before execution begins. + +``` +Branch saves a Todo memory during conversation + → Cortex evaluates the todo (promotion loop) + → Cortex creates a Task in "pending_approval" + → Human approves on the kanban board + → Cortex ready-task loop picks it up + → Worker executes → Done +``` + +This path is for things the agent noticed were actionable but the user didn't explicitly ask to track — the cortex catches what falls through the cracks. + +### 3. UI / API + +Tasks can be created directly from the kanban board UI or via the REST API. These default to `backlog` status with `created_by: "human"`. + +## Status (Kanban Columns) + +Five columns on the board: + +| Status | Description | +|--------|-------------| +| `pending_approval` | Created by cortex, awaiting human sign-off | +| `backlog` | Captured but not ready for work. Default for conversational and UI-created tasks | +| `ready` | Approved and waiting for the cortex to pick up | +| `in_progress` | A worker is actively executing this task | +| `done` | Completed | + +### Status Transitions + +Transitions are validated. You can't skip steps or go backwards (except to `backlog`, which is always allowed as a "re-shelve" action). + +``` +pending_approval → ready (approval) +pending_approval → backlog (shelve) +backlog → ready (manual promotion) +ready → in_progress (cortex pickup) +in_progress → done (worker success) +in_progress → ready (worker failure, re-queued) +done → backlog (reopen) +``` + +Attempting an invalid transition (e.g., `pending_approval → in_progress`, `ready → done`) returns an error. + +## Priority + +Four levels, ordered by urgency: + +``` +critical > high > medium > low +``` + +Default: `medium`. The cortex ready-task loop respects priority — a critical task is picked up before a low-priority one, regardless of creation order. + +## Subtasks + +Simple checklist items stored as a JSON array. One level deep, no nesting. + +```json +[ + {"title": "Research existing API endpoints", "completed": false}, + {"title": "Draft schema changes", "completed": true}, + {"title": "Implement migration", "completed": false} +] +``` + +Subtasks are included in the worker's prompt as an execution plan. Workers can mark subtasks complete via the `task_update` tool as they progress. + +## Metadata + +Arbitrary key-value pairs stored as a JSON object. Used for linking to external resources. + +```json +{ + "github_issue": "https://github.com/org/repo/issues/123", + "estimated_effort": "small", + "worker_type": "opencode", + "skill": "rust-dev", + "notes": "Depends on the auth refactor landing first" +} +``` + +No enforced schema. The UI renders known keys with special formatting (e.g., GitHub links become clickable) and displays unknown keys as plain key-value pairs. + +## Task Numbering + +Per-agent, monotonically increasing. The next number is `MAX(task_number) + 1` within the agent's task table. Tasks are referenced as `#1`, `#42`, etc. Numbers are never reused — deleting task `#5` doesn't free up the number. + +## Execution + +### Cortex Ready-Task Loop + +The primary execution path. A background loop runs every `cortex.tick_interval_secs` (default 30 seconds): + +1. **Claim** — Atomically finds the oldest `ready` task with the highest priority and moves it to `in_progress` +2. **Build prompt** — Renders the worker system prompt with the task title, description, and subtask checklist +3. **Spawn worker** — Creates a new worker with full tool access (shell, file, exec, browser) +4. **Bind** — Sets `worker_id` on the task, linking it to the executing worker +5. **Execute** — The worker runs its loop, using subtasks as an execution plan +6. **Complete** — On success, the task moves to `done` with `completed_at` set. On failure, the task moves back to `ready` with `worker_id` cleared, so it gets re-queued for another attempt + +Worker success/failure is determined by whether `worker.run()` returns `Ok` or `Err`. The cortex doesn't evaluate the quality of the work — a worker that completes without errors is considered successful. + +### API Execute Endpoint + +The `/api/agents/tasks/:number/execute` endpoint moves a task to `ready` (if it's in `backlog` or `pending_approval`), letting the cortex loop pick it up. Tasks already in `ready` or `in_progress` are returned as-is. + +This means execution always flows through the cortex — the API doesn't spawn workers directly. + +### Worker Scope + +Workers executing a task get a restricted version of the `task_update` tool. They can only: + +- Update subtasks (mark complete, replace the checklist) +- Update metadata + +They cannot change the task's status, priority, title, description, or worker binding. These fields are managed by the cortex and the API. This prevents a worker from marking its own task as `done` — only the cortex does that based on whether the worker succeeded or failed. + +## Bulletin Integration + +Active tasks (non-done) are included in the cortex memory bulletin under an "Active Tasks" section. Each task is listed with its number, status, priority, title, and subtask progress: + +``` +### Active Tasks + +- #3 [in_progress] (high) Implement auth refactor [2/5] +- #7 [ready] (medium) Update deployment docs +- #12 [backlog] (low) Clean up unused dependencies +``` + +This gives every channel and branch awareness of the agent's current task board without querying the task store directly. + +## LLM Tools + +Three tools available to branches and cortex chat sessions: + +### task_create + +Creates a new task. + +| Argument | Type | Required | Default | +|----------|------|----------|---------| +| `title` | string | yes | - | +| `description` | string | no | - | +| `priority` | string | no | `"medium"` | +| `subtasks` | string[] | no | `[]` | +| `metadata` | object | no | `{}` | +| `status` | string | no | `"backlog"` | + +Returns the created task number and status. + +### task_list + +Lists tasks with optional filters. + +| Argument | Type | Required | Default | +|----------|------|----------|---------| +| `status` | string | no | all | +| `priority` | string | no | all | +| `limit` | integer | no | 20 | + +### task_update + +Updates an existing task. Available to branches (unrestricted) and workers (restricted to subtasks and metadata only). + +| Argument | Type | Required | Notes | +|----------|------|----------|-------| +| `task_number` | integer | yes | The `#N` reference | +| `title` | string | no | Branch only | +| `description` | string | no | Branch only | +| `status` | string | no | Branch only | +| `priority` | string | no | Branch only | +| `subtasks` | object[] | no | Full replacement | +| `metadata` | object | no | Merged with existing | +| `complete_subtask` | integer | no | Index to mark complete | + +## API Endpoints + +All endpoints require `agent_id` as a query parameter or in the request body. + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/agents/tasks` | List tasks (filterable by status, priority) | +| `GET` | `/api/agents/tasks/:number` | Get single task by number | +| `POST` | `/api/agents/tasks` | Create task | +| `PUT` | `/api/agents/tasks/:number` | Update task | +| `DELETE` | `/api/agents/tasks/:number` | Delete task | +| `POST` | `/api/agents/tasks/:number/approve` | Approve (moves to `ready`) | +| `POST` | `/api/agents/tasks/:number/execute` | Execute (moves to `ready` for cortex pickup) | + +### SSE Events + +Task state changes emit `task_updated` SSE events to connected clients: + +```json +{ + "type": "task_updated", + "agent_id": "main", + "task_number": 42, + "status": "in_progress", + "action": "updated" +} +``` + +The `action` field is one of `"created"`, `"updated"`, or `"deleted"`. The kanban board UI uses these events for real-time updates. + +## Interface + +### Kanban Board + +The **Tasks** tab on the agent page renders a five-column kanban board. Each column corresponds to a task status. Task cards show: + +- `#N` task number and title +- Priority badge (color-coded: red for critical, amber for high, default for medium, outline for low) +- Subtask progress bar (if subtasks exist) +- Worker badge (if a worker is bound) +- Quick action buttons (Approve, Execute, Mark Done) +- Creation timestamp and author + +Clicking a card opens a detail dialog with the full description, subtask checklist, metadata, timestamps, and status action buttons. + +### Create Task + +The "Create Task" button opens a dialog with fields for title, description, priority, and initial status. Tasks created from the UI default to `backlog` status and `"human"` as the creator. + +## Storage + +One SQLite table in the agent's database. + +```sql +CREATE TABLE IF NOT EXISTS tasks ( + id TEXT PRIMARY KEY, + agent_id TEXT NOT NULL, + task_number INTEGER NOT NULL, + title TEXT NOT NULL, + description TEXT, + status TEXT NOT NULL DEFAULT 'backlog', + priority TEXT NOT NULL DEFAULT 'medium', + subtasks TEXT, -- JSON array + metadata TEXT, -- JSON object + source_memory_id TEXT, + worker_id TEXT, + created_by TEXT NOT NULL, + approved_at TIMESTAMP, + approved_by TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + completed_at TIMESTAMP, + UNIQUE(agent_id, task_number) +); +``` + +Indexes on `agent_id`, `status`, `(agent_id, task_number)`, `source_memory_id`, and `worker_id`. + +## Module Layout + +``` +src/ +├── tasks.rs → tasks/ +│ └── store.rs — TaskStore: CRUD, status transitions, claim_next_ready +│ +├── tools/ +│ ├── task_create.rs — task_create LLM tool (branches + cortex chat) +│ ├── task_list.rs — task_list LLM tool (branches + cortex chat) +│ └── task_update.rs — task_update LLM tool (branches + workers, scoped) +│ +├── api/ +│ └── tasks.rs — REST endpoints (list, get, create, update, delete, +│ approve, execute) with SSE event emission +│ +├── agent/ +│ └── cortex.rs — spawn_ready_task_loop, pickup_one_ready_task, +│ gather_active_tasks (bulletin integration) +│ +└── migrations/ + └── 20260219000001_tasks.sql +``` + +## Prompt Integration + +The channel, branch, and cortex chat prompts are all task-aware: + +- **Channel prompt** (`channel.md.j2`) — has a dedicated "Task Board" section explaining spec-driven tasks and the kanban board. The Delegation section tells the channel to branch for task management. Active tasks appear in the Memory Context via the bulletin. +- **Branch prompt** (`branch.md.j2`) — documents all three task tools (`task_create`, `task_list`, `task_update`) with spec-driven guidance. `task_create` emphasizes rich markdown descriptions and pre-filled subtasks. `task_update` is framed as iterative spec refinement. Moving to `ready` triggers cortex auto-pickup. +- **Cortex chat prompt** (`cortex_chat.md.j2`) — lists task board management as a core capability with spec-driven language. The cortex chat has all three task tools. +- **Tool descriptions** — each task tool has a description template in `prompts/en/tools/` that reinforces the spec-driven philosophy: `task_create` tells the LLM to write full markdown specs with subtask execution plans, `task_update` tells it to refine specs as scope evolves. + +The channel itself has no task tools — it always branches to manage tasks. This keeps the channel responsive and ensures task operations go through a thinking process. + +## What's Not Implemented Yet + +- **Cortex todo-promotion loop** — the cortex loop that scans `Todo` memories and promotes them to `pending_approval` tasks. The data model and execution path are ready; the promotion evaluation prompt and loop are not yet built. +- **Drag-and-drop** — the kanban board has quick action buttons but no drag-and-drop between columns yet. +- **Activity timeline** — the detail dialog doesn't show a history of status changes, approvals, and worker events. +- **Task count badge** — the Tasks tab doesn't show a badge with the pending approval count yet. +- **Task archival** — done tasks accumulate indefinitely. Options: auto-archive after N days, a separate `archived` status, or UI filtering (currently, done tasks are shown in the Done column). +- **Rejection feedback** — when a human deletes a pending task, that signal isn't fed back to the cortex. Saving it as a memory would help the cortex learn what's not actionable. diff --git a/docs/content/docs/(features)/tools.mdx b/docs/content/docs/(features)/tools.mdx index f1def1fc5..05fa01679 100644 --- a/docs/content/docs/(features)/tools.mdx +++ b/docs/content/docs/(features)/tools.mdx @@ -1,6 +1,6 @@ --- title: Tools -description: All 16 tools that give LLM processes the ability to act. +description: Tools that give LLM processes the ability to act. --- # Tools @@ -11,7 +11,7 @@ How Spacebot gives LLM processes the ability to act. Every tool implements Rig's `Tool` trait and lives in `src/tools/`. Tools are organized by function, not by consumer. Which process gets which tools is configured via ToolServer factory functions in `src/tools.rs`. -All 16 tools: +Core tools include: | Tool | Purpose | Consumers | |------|---------|-----------| @@ -25,6 +25,9 @@ All 16 tools: | `memory_save` | Write a memory to the store | Branch, Cortex, Compactor | | `memory_recall` | Search memories via hybrid search | Branch | | `channel_recall` | Retrieve transcript from another channel | Branch | +| `spacebot_docs` | Read embedded Spacebot docs/changelog/AGENTS | Branch, Cortex Chat | +| `email_search` | Search IMAP mailbox content directly | Branch | +| `config_inspect` | Inspect live resolved runtime config (redacted) | Cortex Chat | | `set_status` | Report worker progress to the channel | Worker | | `shell` | Execute shell commands | Worker | | `file` | Read, write, and list files | Worker | @@ -36,7 +39,7 @@ All 16 tools: Rig's `ToolServer` runs as a tokio task. You register tools on it, call `.run()` to get a `ToolServerHandle`, and pass that handle to agents. The handle is `Clone` — all clones point to the same server task. -Spacebot uses four ToolServer configurations: +Spacebot uses five ToolServer configurations: ### Channel ToolServer (per-channel) @@ -70,11 +73,13 @@ Each branch gets its own isolated ToolServer, created at spawn time via `create_ ├──────────────────────────────────────────────┤ │ memory_save (Arc) │ │ memory_recall (Arc) │ +│ spacebot_docs (embedded docs) │ │ channel_recall (ConversationLogger) │ +│ email_search (IMAP mailbox search) │ └──────────────────────────────────────────────┘ ``` -Branch isolation ensures `memory_recall` and `channel_recall` are never visible to the channel. All tools are registered at creation and live for the lifetime of the branch. +Branch isolation ensures `memory_recall`, `channel_recall`, `spacebot_docs`, and `email_search` are never visible to the channel. All tools are registered at creation and live for the lifetime of the branch. ### Worker ToolServer (per-worker) @@ -89,10 +94,12 @@ Each worker gets its own isolated ToolServer, created at spawn time via `create_ │ exec │ │ set_status (agent_id, worker_id, ...) │ │ browser (if browser.enabled) │ +│ web_search (if configured) │ +│ mcp_* (registered at worker startup for MCP tools connected at that time) │ └──────────────────────────────────────────┘ ``` -`shell` and `exec` hold a shared `Sandbox` reference that wraps commands in OS-level containment (bubblewrap on Linux, sandbox-exec on macOS). `file` validates paths against the workspace boundary. `set_status` is bound to a specific worker's ID so status updates route to the right place in the channel's status block. `browser` is conditionally registered based on the agent's `browser.enabled` config. +`shell` and `exec` hold a shared `Sandbox` reference that wraps commands in OS-level containment (bubblewrap on Linux, sandbox-exec on macOS). `file` validates paths against the workspace boundary. `set_status` is bound to a specific worker's ID so status updates route to the right place in the channel's status block. `browser` is conditionally registered based on the agent's `browser.enabled` config. MCP tools are fetched and registered once at worker startup for servers connected at that time. Workers don't get memory tools or channel tools. They can't talk to the user, can't recall memories, can't spawn branches. They execute their task and report status. @@ -110,6 +117,24 @@ One per agent, minimal. The cortex writes consolidated memories. It doesn't need recall (it's the consolidator, not the recaller) or any channel/worker tools. +### Cortex Chat ToolServer + +One per cortex-chat session context, full diagnostic/admin toolset. + +``` +┌──────────────────────────────────────────────┐ +│ Cortex Chat ToolServer │ +├──────────────────────────────────────────────┤ +│ memory_save / memory_recall / memory_delete│ +│ channel_recall │ +│ task_create / task_list / task_update │ +│ spacebot_docs / config_inspect │ +│ shell / file / exec │ +│ browser (if enabled) │ +│ web_search (if configured) │ +└──────────────────────────────────────────────┘ +``` + ## Factory Functions All in `src/tools.rs`: @@ -125,18 +150,21 @@ remove_channel_tools(handle) // Per branch spawn — creates an isolated ToolServer with memory + channel recall tools create_branch_tool_server(memory_search, conversation_logger) -> ToolServerHandle -// Per worker spawn — creates an isolated ToolServer (browser conditionally included) +// Per worker spawn — creates an isolated ToolServer (browser/web_search/MCP conditionally included) create_worker_tool_server(agent_id, worker_id, channel_id, event_tx, browser_config, screenshot_dir) -> ToolServerHandle // Agent startup — creates the cortex ToolServer create_cortex_tool_server(memory_search) -> ToolServerHandle + +// Cortex chat startup — creates an interactive admin ToolServer +create_cortex_chat_tool_server(...) -> ToolServerHandle ``` ## Tool Lifecycle ### Static tools (registered at creation) -`memory_save`, `memory_recall`, `channel_recall` on branch ToolServers. `shell`, `file`, `exec` on worker ToolServers. `memory_save` on cortex and compactor ToolServers. These are registered before `.run()` via the builder pattern and live for the lifetime of the ToolServer. +`memory_save`, `memory_recall`, `channel_recall`, `spacebot_docs`, `email_search` on branch ToolServers. `shell`, `file`, `exec` on worker ToolServers. `memory_save` on cortex and compactor ToolServers. These are registered before `.run()` via the builder pattern and live for the lifetime of the ToolServer. ### Dynamic tools (added/removed at runtime) @@ -153,7 +181,7 @@ create_cortex_tool_server(memory_search) -> ToolServerHandle ### Per-process tools (created and destroyed with the process) -Branch and worker ToolServers are created when the process spawns and dropped when it finishes. Each branch gets `memory_save` + `memory_recall` + `channel_recall`. Each worker gets `shell`, `file`, `exec`, `set_status` (bound to that worker's ID), and optionally `browser`. +Branch and worker ToolServers are created when the process spawns and dropped when it finishes. Each branch gets `memory_save` + `memory_recall` + `channel_recall` + `spacebot_docs` + `email_search` (plus task board tools). Each worker gets `shell`, `file`, `exec`, `set_status` (bound to that worker's ID), and optionally `browser`, `web_search`, and connected `mcp_*` tools. ## Tool Design Patterns @@ -173,19 +201,17 @@ async fn call(&self, args: Self::Args) -> Result { ### Sandbox containment -Shell and exec commands run inside an OS-level sandbox (bubblewrap on Linux, sandbox-exec on macOS). The entire filesystem is mounted read-only except the workspace, `/tmp`, and any configured `writable_paths`. The agent's data directory (databases, config files) is explicitly protected. See [Configuration](/docs/config#agentssandbox) for sandbox config options. +Shell and exec commands run inside an OS-level sandbox (bubblewrap on Linux, sandbox-exec on macOS). The entire filesystem is mounted read-only except the workspace, `/tmp`, and any configured `writable_paths`. The agent's data directory (databases, config files) is explicitly protected. + +Worker subprocesses also start with a clean environment -- they never inherit the parent's environment variables. System secrets (LLM API keys, messaging tokens) are never visible to workers regardless of sandbox mode. See [Sandbox](/docs/sandbox) for full details. The `file` tool independently validates paths against the workspace boundary and rejects writes to identity files (`SOUL.md`, `IDENTITY.md`, `USER.md`). The `exec` tool blocks dangerous environment variables (`LD_PRELOAD`, `DYLD_INSERT_LIBRARIES`, etc.) that enable library injection regardless of sandbox state. -Leak detection (via `SpacebotHook`) scans all tool output for secret patterns (API keys, tokens, PEM keys) and terminates the process if a leak is found. +Leak detection (via `SpacebotHook`) scans all tool output for secret patterns (API keys, tokens, PEM keys) and terminates the process if a leak is found. This includes base64-encoded, URL-encoded, and hex-encoded variants. ### Status reporting -Workers report progress via `set_status`. The channel sees these in its status block. Status updates use `try_send` (non-blocking) so a slow event bus never blocks tool execution. - -### Fire-and-forget sends - -`set_status` uses `try_send` instead of `.await` on the event channel. If the channel is full, the update is dropped rather than blocking the worker. +Workers report progress via `set_status`, and the channel sees those updates in its status block. `set_status` uses `try_send` (non-blocking), so if the event channel is full the update is dropped instead of blocking the worker. ## What Each Tool Does @@ -228,6 +254,10 @@ If the name doesn't match any channel, falls back to list mode so the LLM can se Channel names are resolved from the `discord_channel_name` field stored in message metadata. The tool queries `conversation_messages` in SQLite directly — it reads persisted messages, not in-memory Rig history. +### email_search + +Searches the configured email mailbox directly over IMAP with filters like sender (`from`), subject, text query, unread-only, and time window (`since_days`). Returns message metadata plus a body snippet for precise read-back in email workflows. + ### set_status Reports the worker's current progress. The status string appears in the channel's status block so the user-facing process knows what's happening without polling. diff --git a/docs/content/docs/(features)/workers.mdx b/docs/content/docs/(features)/workers.mdx index 3e0a1096a..84e9d3a5c 100644 --- a/docs/content/docs/(features)/workers.mdx +++ b/docs/content/docs/(features)/workers.mdx @@ -55,6 +55,7 @@ Conditionally added: |------|-----------| | `browser` | When `browser.enabled = true` in agent config | | `web_search` | When a Brave Search API key is configured | +| `mcp_*` | One tool per connected MCP server tool, fetched at worker start | Workers don't get memory tools, channel tools, or branch tools. They can't talk to the user, recall memories, or spawn other processes. They execute their task and report status. @@ -143,7 +144,7 @@ Worker execution logs are controlled by `worker_log_mode`: Logs include: worker ID, channel ID, timestamp, state, task, error (if any), and the full message history with tool calls and results. -## Sandbox and Protected Paths +## Sandbox and Environment Worker shell and exec commands run inside an OS-level sandbox (bubblewrap on Linux, sandbox-exec on macOS). The entire host filesystem is mounted read-only except: @@ -151,11 +152,13 @@ Worker shell and exec commands run inside an OS-level sandbox (bubblewrap on Lin - `/tmp` (private per invocation) - Any paths listed in `[agents.sandbox].writable_paths` -The agent's data directory (databases, config files) is explicitly re-mounted read-only. This is enforced at the kernel level — no amount of command creativity can bypass it. +The agent's data directory (databases, config files) is explicitly re-mounted read-only. This is enforced at the kernel level -- no amount of command creativity can bypass it. -The `file` tool additionally validates paths against the workspace boundary and blocks writes to identity files (`SOUL.md`, `IDENTITY.md`, `USER.md`). The `exec` tool blocks dangerous environment variables (`LD_PRELOAD`, `DYLD_INSERT_LIBRARIES`, etc.) that enable library injection. +Worker subprocesses also start with a **clean environment**. Workers only receive `PATH` (with `tools/bin` prepended), safe variables (`HOME`, `USER`, `LANG`, `TERM`, `TMPDIR`), tool-category secrets from the [secret store](/docs/secrets), and any explicitly configured `passthrough_env` entries. `HOME` is mode-dependent: workspace path when sandboxed, parent `HOME` in passthrough mode. System secrets like LLM API keys are hidden by default unless explicitly forwarded via `passthrough_env`. Environment sanitization applies regardless of whether the sandbox is enabled or disabled. -Sandbox mode is configurable per agent via `[agents.sandbox]`. See [Configuration](/docs/config#agentssandbox). +The `file` tool validates paths against the workspace boundary and rejects writes to identity/memory paths (for example `SOUL.md`, `IDENTITY.md`, `USER.md`) with an explicit error directing the LLM to the appropriate tool. The `exec` tool blocks dangerous environment variables (`LD_PRELOAD`, `DYLD_INSERT_LIBRARIES`, etc.) that enable library injection. + +See [Sandbox](/docs/sandbox) for full details on containment, environment sanitization, leak detection, and durable binaries. ## Configuration @@ -171,10 +174,11 @@ worker = "anthropic/claude-haiku-4.5-20250514" coding = "anthropic/claude-sonnet-4-20250514" # Sandbox is enabled by default. Disable for self-hosted/local setups -# that need full host filesystem access. +# that need full host filesystem access. Env sanitization always applies. [agents.sandbox] mode = "enabled" writable_paths = [] +passthrough_env = [] ``` ## OpenCode Workers diff --git a/docs/content/docs/(getting-started)/docker.mdx b/docs/content/docs/(getting-started)/docker.mdx index 2302f5097..ece45fbae 100644 --- a/docs/content/docs/(getting-started)/docker.mdx +++ b/docs/content/docs/(getting-started)/docker.mdx @@ -206,11 +206,23 @@ healthcheck: Spacebot checks for new releases on startup and every hour. When a new version is available, a banner appears in the web UI. +You can also open **Settings → Updates** for update status, one-click apply controls (Docker), and manual command snippets. + +`latest` is supported and continues to receive updates (it tracks the rolling `full` image). Use explicit version tags only when you want controlled rollouts. + ### Manual Update ```bash -docker pull ghcr.io/spacedriveapp/spacebot:slim -docker compose up -d +docker compose pull spacebot +docker compose up -d --force-recreate spacebot +``` + +If you're not using Compose: + +```bash +docker pull ghcr.io/spacedriveapp/spacebot:latest +docker stop spacebot && docker rm spacebot +# re-run your original docker run command with the new image tag ``` ### One-Click Update @@ -236,6 +248,18 @@ When the socket is mounted, the update banner shows an **Update now** button tha Without the socket mount, the banner still notifies you of new versions but you'll need to update manually. +One-click updates are intended for containers running Spacebot release tags. If you're running a custom/self-built image, update by rebuilding your image and recreating the container. + +### Native / Source Builds + +If Spacebot is installed from source (`cargo install --path .` or a local release build), updates are manual: + +1. Pull latest source +2. Rebuild/reinstall the binary +3. Restart Spacebot + +The web UI still shows update availability, but it cannot rebuild binaries for native installs. + ### Update API You can also check for and trigger updates programmatically: diff --git a/docs/content/docs/(getting-started)/quickstart.mdx b/docs/content/docs/(getting-started)/quickstart.mdx index 0cfb813ce..3c7c55319 100644 --- a/docs/content/docs/(getting-started)/quickstart.mdx +++ b/docs/content/docs/(getting-started)/quickstart.mdx @@ -30,13 +30,24 @@ docker run -d \ See [Docker deployment](/docs/docker) for image variants, compose files, and configuration options. +To update Docker installs, pull and recreate the container: + +```bash +docker pull ghcr.io/spacedriveapp/spacebot:latest +docker stop spacebot && docker rm spacebot +# re-run your docker run command +``` + +If you mount `/var/run/docker.sock`, the web UI can apply Docker updates directly from the update banner. +You can also manage updates from **Settings → Updates** in the web UI. + ## Build from source ### Prerequisites - **Rust 1.85+** — `rustup update stable` - **Bun** (optional, for the web UI) — `curl -fsSL https://bun.sh/install | bash` -- **An LLM API key** — Anthropic, OpenAI, or OpenRouter +- **An LLM API key** — Anthropic, OpenAI, OpenRouter, Kilo Gateway, or OpenCode Go ### Install @@ -77,6 +88,8 @@ Create `~/.spacebot/config.toml`: [llm] anthropic_key = "sk-ant-..." # or: openrouter_key = "sk-or-..." +# or: kilo_key = "sk-..." +# or: opencode_go_key = "..." # or: openai_key = "sk-..." # Keys also support env references: anthropic_key = "env:ANTHROPIC_API_KEY" diff --git a/docs/content/docs/(messaging)/email-setup.mdx b/docs/content/docs/(messaging)/email-setup.mdx new file mode 100644 index 000000000..1e3dc6769 --- /dev/null +++ b/docs/content/docs/(messaging)/email-setup.mdx @@ -0,0 +1,160 @@ +--- +title: Email Setup +description: Connect Spacebot to an inbox with IMAP + SMTP. +--- + +# Email Setup + +Connect Spacebot to any mailbox that supports IMAP and SMTP. Inbound messages are polled over IMAP, and replies are sent over SMTP. + +You need: + +- an IMAP host, username, and password +- an SMTP host, username, and password +- a sender address (`from_address`) for outbound replies + +Most providers require an app password when MFA is enabled. + +## Step 1: Prepare mailbox access + +In your mail provider settings: + +1. Enable IMAP access for the mailbox. +2. Create an app password (recommended) for IMAP/SMTP. +3. Confirm IMAP and SMTP hosts/ports. + +Typical defaults are IMAP `993` (TLS) and SMTP `587` (STARTTLS). + +## Step 2: Add Email credentials to Spacebot + + + + +1. Open **Settings** -> **Messaging Platforms**. +2. Expand the **Email** card. +3. Enter IMAP and SMTP credentials. +4. Set **From Address** (and optional **From Name**). +5. Click **Connect**. + + + + +```toml +[messaging.email] +enabled = true + +imap_host = "imap.example.com" +imap_port = 993 +imap_username = "inbox@example.com" +imap_password = "env:EMAIL_IMAP_PASSWORD" +imap_use_tls = true + +smtp_host = "smtp.example.com" +smtp_port = 587 +smtp_username = "inbox@example.com" +smtp_password = "env:EMAIL_SMTP_PASSWORD" +smtp_use_starttls = true + +from_address = "bot@example.com" +from_name = "Spacebot" + +poll_interval_secs = 30 +folders = ["INBOX"] +allowed_senders = [] +``` + +Credentials support `env:VAR_NAME` references. + + + + +## Step 3: Add a binding + +Add an Email binding to route inbound mail to an agent. + +```toml +[[bindings]] +agent_id = "main" +channel = "email" +``` + +If no Email binding exists, inbound email falls back to your default agent. + +## Email intake behavior + +Email channels are treated as intake streams, not always-on chat threads. + +- Spacebot triages inbound mail first. +- Inbound email channels are reply-disabled by default (no automatic outbound email response). +- For non-spam mail, it is encouraged to persist key memory (sender, subject, commitments, deadlines, urgency). +- For urgent mail, it can escalate to another active channel using cross-channel messaging. +- If you need to send an email intentionally, initiate it from another channel using cross-channel tooling. + +## Intentional outbound email from another channel + +When the Email adapter is configured, you can intentionally send email from a non-email channel (for example, Telegram or Discord) using cross-channel messaging. + +- explicit format: `email:alice@example.com` +- bare address also works: `alice@example.com` + +This keeps email channels inbound-only while still allowing deliberate outbound send workflows. + +## Search mailbox content (`email_search`) + +Branches can use `email_search` to query IMAP directly when you ask for exact read-back details. + +Examples: + +- "Find emails from Alice about renewal in the last 14 days" +- "Search unread emails mentioning invoice" +- "Look for subject containing Q1 roadmap" + +Use specific filters (`from`, `subject`, `query`, `since_days`) to avoid broad mailbox scans. + +## Thread behavior + +Spacebot keeps one conversation per email thread. It uses `References`, `In-Reply-To`, and `Message-ID` headers to map replies back to the correct conversation. + +Outbound replies include the correct threading headers so responses stay in the same mail thread in clients like Gmail and Outlook. + +## Filtering inbound senders + +Use `allowed_senders` to restrict who can trigger the bot. + +```toml +[messaging.email] +allowed_senders = ["@example.com", "vip@customer.com", "partner.org"] +``` + +- `"@example.com"` allows the whole domain +- `"vip@customer.com"` allows one exact sender +- `"partner.org"` is treated as a domain rule (`@partner.org`) + +## Folders and polling + +Poll multiple folders by setting `folders`: + +```toml +[messaging.email] +folders = ["INBOX", "Support", "Escalations"] +poll_interval_secs = 30 +``` + +Use a longer interval if your provider rate limits IMAP polling. + +## Verify it's working + +1. Send an email to the configured mailbox from an allowed sender. +2. Confirm a new channel appears in Spacebot with a subject-based name. +3. Reply in the same thread and confirm the message is ingested into the same conversation thread (no automated reply by default). +4. If direct email replies are explicitly enabled in your setup, verify outbound replies preserve threading headers. + +## Troubleshooting + +| Symptom | Likely cause | Fix | +|---------|--------------|-----| +| Adapter won't connect to IMAP | Wrong host/port or IMAP disabled | Recheck provider IMAP settings; verify `imap_host`, `imap_port`, TLS mode | +| SMTP send fails | Bad SMTP credentials or blocked auth | Use provider app password; verify `smtp_host`, `smtp_port`, STARTTLS | +| No inbound messages | Folder not polled or sender blocked | Add folder to `folders`; check `allowed_senders` rules | +| Bot ignores automated mails | Auto-response headers detected | Expected behavior; Spacebot skips auto-generated mail loops | +| Replies create a new thread | Missing/rewritten message headers upstream | Check provider/forwarder preserves `Message-ID`, `In-Reply-To`, `References` | diff --git a/docs/content/docs/(messaging)/messaging.mdx b/docs/content/docs/(messaging)/messaging.mdx index cd4764bca..2856c4dbe 100644 --- a/docs/content/docs/(messaging)/messaging.mdx +++ b/docs/content/docs/(messaging)/messaging.mdx @@ -1,6 +1,6 @@ --- title: Messaging -description: How Spacebot connects to Discord, Slack, Telegram, Twitch, and webhooks. +description: How Spacebot connects to Discord, Slack, Telegram, Twitch, Email, and webhooks. --- # Messaging @@ -15,8 +15,8 @@ Spacebot connects to chat platforms so your agent can talk to people where they | [Slack](/docs/slack-setup) | Supported | Bot token + app token via Socket Mode | | [Telegram](/docs/telegram-setup) | Supported | Bot token via BotFather | | [Twitch](/docs/twitch-setup) | Supported | OAuth token via Twitch IRC | +| [Email](/docs/email-setup) | Supported | IMAP polling + SMTP replies | | Webhook | Supported | HTTP endpoint for programmatic access | -| Email | Coming soon | IMAP/SMTP | | WhatsApp | Coming soon | Meta Cloud API | | Matrix | Coming soon | Decentralized chat protocol | | iMessage | Coming soon | macOS only | @@ -25,11 +25,13 @@ Spacebot connects to chat platforms so your agent can talk to people where they 1. You connect a platform by adding your tokens in the dashboard or config 2. Spacebot opens a persistent connection to that platform -3. When someone sends a message, Spacebot receives it, thinks, and replies +3. When someone sends a message, Spacebot receives it and decides whether to reply, skip, or delegate work 4. Each conversation (channel, thread, DM) gets its own isolated history You can connect multiple platforms at the same time. An agent on Discord and Slack simultaneously is just two bindings pointing at the same agent. +For Email specifically, Spacebot treats inbound mail as intake-first by default: triage, memory capture for meaningful non-spam messages, and escalation to other channels for urgent items. Inbound email channels do not auto-reply. When the Email adapter is configured, intentional outbound email can still be initiated from other channels using an explicit target such as `email:alice@example.com`. + ## Bindings Bindings route messages from a platform to a specific agent. A binding says "messages from this place go to this agent." @@ -41,7 +43,8 @@ Go to **Settings** → **Bindings** tab to create and manage bindings. Each binding specifies: - Which **agent** handles the messages -- Which **platform** (Discord, Slack, Telegram) +- Which **platform** (Discord, Slack, Telegram, Twitch, Email) +- Optionally which **adapter instance** on that platform - Optionally which **server/workspace/chat** to scope it to - Optionally which **channels** within that server @@ -55,6 +58,13 @@ agent_id = "main" channel = "discord" guild_id = "123456789" +# Route only the named Discord adapter instance `ops` +[[bindings]] +agent_id = "main" +channel = "discord" +adapter = "ops" +guild_id = "987654321" + # Route a Slack workspace to the main agent [[bindings]] agent_id = "main" @@ -73,6 +83,8 @@ chat_id = "-100123456789" Without any filtering (no guild ID, workspace ID, or chat ID), a binding accepts messages from anywhere on that platform. +Named adapters use `adapter = ""` in bindings. Omit `adapter` to target the platform default adapter (`discord`, `slack`, `telegram`, `twitch`, `email`, `webhook`). + If no binding matches an incoming message, it's routed to the default agent automatically. This means the bot responds everywhere out of the box — add bindings to restrict it to specific channels or servers. ## Multiple Agents @@ -121,6 +133,7 @@ Each chat context maps to its own Spacebot conversation with isolated history: | Slack | Each channel, each thread, each DM | | Telegram | Each chat (group, DM, or channel) | | Twitch | Each channel | +| Email | Each email thread | | Webhook | Each unique conversation ID in the request | Threads are first-class on Discord and Slack — a thread gets its own conversation, separate from the parent channel. @@ -141,4 +154,4 @@ curl -X POST http://localhost:18789/webhook \ ## Hot Reloading -Changes to bindings and permissions (channel filters, DM allowed users) take effect within a couple seconds — no restart needed. Token changes require a restart, or you can re-save from the dashboard which reconnects automatically. +Changes to bindings and permissions (channel filters, DM allowed users) take effect within a couple of seconds — no restart needed. Token and credential changes are applied by reconnecting the adapter. diff --git a/docs/content/docs/(messaging)/meta.json b/docs/content/docs/(messaging)/meta.json index 24c0c1a7b..3e5764b49 100644 --- a/docs/content/docs/(messaging)/meta.json +++ b/docs/content/docs/(messaging)/meta.json @@ -1,4 +1,4 @@ { "title": "Messaging", - "pages": ["messaging", "discord-setup", "slack-setup", "telegram-setup", "twitch-setup"] + "pages": ["messaging", "discord-setup", "slack-setup", "telegram-setup", "twitch-setup", "email-setup"] } diff --git a/docs/content/docs/(messaging)/twitch-setup.mdx b/docs/content/docs/(messaging)/twitch-setup.mdx index 6c99c3297..26812466a 100644 --- a/docs/content/docs/(messaging)/twitch-setup.mdx +++ b/docs/content/docs/(messaging)/twitch-setup.mdx @@ -5,29 +5,83 @@ description: Connect Spacebot to Twitch chat. # Twitch Setup -Connect Spacebot to Twitch chat. Takes about 5 minutes. +Connect Spacebot to Twitch chat. Takes about 10 minutes. -You need a **Twitch account** for the bot and an **OAuth token**. +You need a **Twitch account** for the bot and a **Twitch application** registered at dev.twitch.tv. ## Step 1: Create a Twitch Account Create a Twitch account for your bot (or use an existing one). The bot will send messages as this account. -## Step 2: Get an OAuth Token +## Step 2: Register a Twitch Application -The TwitchApps TMI token generator has been discontinued. +1. Go to [dev.twitch.tv/console/apps](https://dev.twitch.tv/console/apps) and log in with **any** Twitch account (doesn't need to be the bot account) +2. Click **Register Your Application** +3. Fill in the form: + - **Name:** anything (e.g. "My Spacebot") + - **OAuth Redirect URL:** `http://localhost:3000` + - **Category:** Chat Bot +4. Click **Create** +5. Click **Manage** on your new app +6. Copy the **Client ID** +7. Click **New Secret** and copy the **Client Secret** -For quick setup, use [Twitch Token Generator by swiftyspiffy](https://twitchtokengenerator.com/) and authorize with the bot account using `chat:read` and `chat:edit` scopes. + +Keep the client secret safe. If you lose it, you can generate a new one from the app management page, but the old one is immediately invalidated. + -The site returns an **ACCESS TOKEN**, **REFRESH TOKEN**, and **CLIENT ID**. For Spacebot, use the **ACCESS TOKEN** and prefix it with `oauth:`. +## Step 3: Get an OAuth Token -Example: if the generator shows `abc123def456`, set `oauth_token` to `oauth:abc123def456`. +You need to authorize the bot account with `chat:read` and `chat:edit` scopes. The easiest way is the Twitch CLI, but you can also do it manually. - -If you're building an app or service for other users, don't rely on third-party token generators. Create your own Twitch OAuth app at [dev.twitch.tv/console/apps](https://dev.twitch.tv/console/apps) and run the OAuth flow directly in your product. - + + + +Install the [Twitch CLI](https://dev.twitch.tv/docs/cli/) and configure it with your app credentials: + +```bash +twitch configure +# Enter your Client ID and Client Secret when prompted +``` -## Step 3: Add Credentials to Spacebot +Then generate a user token. This opens a browser where you log in as the **bot account**: + +```bash +twitch token -u -s 'chat:read chat:edit' +``` + +The CLI prints the **access token** and **refresh token**. Copy both. + + + + +**1. Open this URL in a browser where you're logged in as the bot account** (replace `YOUR_CLIENT_ID`): + +``` +https://id.twitch.tv/oauth2/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=http://localhost:3000&response_type=code&scope=chat:read+chat:edit +``` + +**2. Authorize the app.** You'll be redirected to `http://localhost:3000?code=AUTHORIZATION_CODE`. The page won't load — that's fine. Copy the `code` parameter from the URL. + +**3. Exchange the code for tokens:** + +```bash +curl -X POST 'https://id.twitch.tv/oauth2/token' \ + -d 'client_id=YOUR_CLIENT_ID' \ + -d 'client_secret=YOUR_CLIENT_SECRET' \ + -d 'code=AUTHORIZATION_CODE' \ + -d 'grant_type=authorization_code' \ + -d 'redirect_uri=http://localhost:3000' +``` + +The response contains `access_token` and `refresh_token`. Copy both. + + + + +## Step 4: Add Credentials to Spacebot + +Spacebot needs your **username**, **OAuth token**, **client ID**, **client secret**, and **refresh token**. With all five, Spacebot automatically refreshes expired tokens. @@ -35,7 +89,7 @@ If you're building an app or service for other users, don't rely on third-party 1. Open your Spacebot dashboard 2. Go to **Settings** → **Messaging Platforms** 3. Click **Setup** on the Twitch card -4. Enter the bot's **username** and **OAuth token** +4. Enter the bot's **username**, **OAuth token**, **client ID**, **client secret**, and **refresh token** 5. Click **Save** Spacebot connects immediately — no restart needed. @@ -48,9 +102,14 @@ Spacebot connects immediately — no restart needed. enabled = true username = "my_bot" oauth_token = "oauth:abc123def456..." +client_id = "your_client_id" +client_secret = "your_client_secret" +refresh_token = "your_refresh_token" channels = ["somechannel", "anotherchannel"] ``` +Prefix the access token with `oauth:`. For example, if the token is `abc123def456`, set `oauth_token` to `oauth:abc123def456`. + You can also reference environment variables: ```toml @@ -58,6 +117,9 @@ You can also reference environment variables: enabled = true username = "env:TWITCH_BOT_USERNAME" oauth_token = "env:TWITCH_OAUTH_TOKEN" +client_id = "env:TWITCH_CLIENT_ID" +client_secret = "env:TWITCH_CLIENT_SECRET" +refresh_token = "env:TWITCH_REFRESH_TOKEN" channels = ["somechannel"] ``` @@ -66,7 +128,7 @@ Token changes in config require a restart. -## Step 4: Join Channels +## Step 5: Join Channels Specify which Twitch channels the bot should join. Channel names are case-insensitive and should not include the `#` prefix. @@ -211,7 +273,7 @@ Each Twitch channel maps to a single conversation (`twitch:`). Unl | Symptom | Cause | Fix | |---------|-------|-----| -| `Login authentication failed` | Invalid OAuth token | Generate a fresh token using [Twitch Token Generator](https://twitchtokengenerator.com/) or your own OAuth app | +| `Login authentication failed` | Invalid or expired OAuth token | Re-run the token flow from Step 3. If you have `client_id`, `client_secret`, and `refresh_token` configured, Spacebot refreshes automatically — check that all three are set. | | Bot connects but doesn't respond | Trigger prefix set | Messages must start with the configured prefix (e.g. `!ask`) | | Bot responds to everything | No trigger prefix | Set `trigger_prefix` in the config to limit when the bot responds | | Messages getting dropped | Rate limit | Reduce response frequency or get the bot account verified | diff --git a/docs/content/docs/index.mdx b/docs/content/docs/index.mdx index ca5bba05e..ead345b0a 100644 --- a/docs/content/docs/index.mdx +++ b/docs/content/docs/index.mdx @@ -21,13 +21,13 @@ Spacebot runs as a single binary with no server dependencies. All data lives in - **SQLite** — Relational data (conversations, memory graph, cron jobs) - **LanceDB** — Vector embeddings and full-text search -- **redb** — Key-value settings and encrypted secrets +- **redb** — Key-value settings and [secret store](/docs/secrets) ## Quick Links - + diff --git a/docs/design-docs/autonomy-loop.md b/docs/design-docs/autonomy-loop.md new file mode 100644 index 000000000..e9eb0e2fe --- /dev/null +++ b/docs/design-docs/autonomy-loop.md @@ -0,0 +1,474 @@ +# Autonomy Loop + +Today Spacebot is reactive. It responds to messages, executes cron jobs from scratch, and runs four hardcoded cortex loops (warmup, bulletin, association, ready-task pickup). It has no mechanism for proactive, self-directed cognition — the kind where the agent works overnight tracking issues, processing emails, monitoring repos, and surfaces curated intelligence when you ask for it in the morning. + +The [custom cortex loops](./custom-cortex-loops.md) design proposed solving this with user-created, per-topic cortex processes. This doc supersedes that approach. The core objection: the cortex should be something the user never thinks about. It is the system's internal monologue, not a user-configurable surface. Exposing "cortex loops" as a concept — with a creation tool, dashboard tab, and per-loop settings — turns internal cognition into just a fancier cron job. + +This doc proposes two changes instead: + +1. **The autonomy loop** — a single new built-in cortex process that gives the agent general proactive behavior, driven by its identity and role rather than user-enumerated monitoring tasks. +2. **Stateful cron jobs** — adding optional memory continuity to the existing cron system so scheduled jobs can accumulate knowledge across runs. + +Together these cover the same use cases as custom cortex loops with far less conceptual overhead. The user tells the agent what it is (identity files) and what it should do on a schedule (cron jobs). The system figures out the rest. + +## The Problem + +When a user tells their agent "keep tabs on the Spacedrive issues" or "give me a morning briefing every day at 9am," there's no good mechanism for this today: + +1. **Cron jobs** spin up a fresh channel each time. No cross-run context. No accumulated knowledge. The cron fires, the agent sees the prompt cold, does its thing, delivers the result. Each run is isolated — the agent doesn't build on what it learned last time. + +2. **The existing cortex loops** are hardcoded system functions (bulletin, associations, task pickup). They don't support user-directed proactive work. + +3. **The ready-task pickup loop** provides autonomous execution, but only for tasks that were explicitly created by a human conversation. The agent never generates its own work. + +The gap: there's no way for the agent to think on its own about the things it's been told to care about. + +## The Autonomy Loop + +A new fifth built-in cortex loop, alongside warmup, bulletin, association, and ready-task pickup. One per agent. Not user-created — it's part of the system, like the bulletin. + +### What It Does + +Each cycle, the autonomy loop wakes up as a branch with memory tools and worker-spawning capability. It receives the agent's full identity context (soul, role) and the current memory bulletin. It recalls its own previous findings. Then it decides what to do — what to check on, what to investigate, what to follow up on — based on what it knows about its role, its recent history, and the tools available to it. + +The key insight: the agent's identity files already define what it should care about. A ROLE.md that says "You manage the Spacedrive project" implies the agent should be tracking Spacedrive issues, PRs, and CI. A SOUL.md that says "You are Jamie's engineering assistant" implies it should be aware of Jamie's email, calendar, and active projects. The autonomy loop operationalizes the identity — it's the system asking itself "given who I am and what I know, what should I be doing right now?" + +### What It Doesn't Do + +- It doesn't deliver results to the user. It saves memories and creates tasks. The bulletin and cron jobs handle delivery. +- It doesn't have its own personality or conversation context. It's a cortex process — internal cognition. +- It doesn't replace cron jobs. Cron jobs are "do X at time Y and tell the user." The autonomy loop is "think about what I should be aware of." +- It doesn't require user configuration beyond identity files. The agent figures out what to monitor. + +### Execution Model + +The autonomy loop runs on a configurable interval (default: 30 minutes). Each cycle: + +``` +1. CONTEXT ASSEMBLY + ├─ Load identity context (soul.md, role.md, identity.md, user.md) + ├─ Load current memory bulletin + ├─ Load active tasks from the task board + └─ Load recent cortex event log (what happened since last cycle) + +2. RECALL PHASE + ├─ Recall memories with source "cortex:autonomy" (previous cycle findings) + ├─ Recall recent high-importance memories (what's changed in the world) + └─ Build a context summary of current state + recent changes + +3. EXECUTION PHASE (branch with worker-spawning capability) + ├─ System prompt: autonomy_loop.md.j2 + ├─ Identity context + bulletin + recalled context injected + ├─ Branch decides what to investigate this cycle: + │ ├─ Check integrations relevant to its role (GitHub, email, etc.) + │ ├─ Follow up on unresolved items from previous cycles + │ ├─ Evaluate task board for stale or blocked items + │ └─ Look for patterns or emerging issues across recent activity + ├─ Spawns workers for actual work (API calls, shell commands) + ├─ Saves findings as memories (source: "cortex:autonomy") + ├─ Creates tasks for actionable items (status: pending_approval or backlog) + └─ Max turns from config (default: 15) + +4. SAVE PHASE + ├─ Force-save a cycle summary memory (source: "cortex:autonomy") + │ covering: what was checked, what was found, what was deferred + ├─ Log execution to cortex_events + └─ Update last_executed_at +``` + +### Why One Loop, Not Many + +The custom cortex loops design had separate loops for each concern ("track-spacedrive-issues", "monitor-email", "weekly-project-review"). The autonomy loop is a single process that handles all proactive concerns. The trade-offs: + +**What you lose:** +- Per-topic interval control ("check issues every 5 min, email every hour") +- Per-topic circuit breakers and failure isolation +- Clear separation of concerns in execution logs + +**What you gain:** +- The agent prioritizes dynamically. If there's a CI failure, it focuses on that instead of dutifully running all N loops regardless of urgency. +- No user-facing complexity. No "cortex loop" concept to explain, no creation tool, no dashboard tab. +- Natural cross-concern reasoning. The agent can connect "Alice emailed about the contract" with "there's a PR pending review from Alice" in a single thought process, rather than having two separate loops that don't talk to each other. +- Bounded resource consumption. One branch + N workers per cycle, not N branches each spawning their own workers. +- The agent gets better over time. As it accumulates memories about what's relevant, it naturally spends more cycles on what matters and less on what doesn't. + +**The prioritization concern is key.** With separate loops, each topic gets equal treatment regardless of urgency. With a single autonomy loop, the agent can look at everything it knows and decide "the CI failure is the most important thing right now, I'll check email later." This is closer to how human awareness actually works. + +### Source-Tagged Memory Accumulation + +The autonomy loop uses the source tag `cortex:autonomy` on all memories it creates. This requires making source tags functional in the memory system — today they're stored but never filtered on. + +**Required changes to the memory system:** + +1. Add `source: Option` to `SearchConfig` +2. Add a `WHERE source = ?` clause to `MemoryStore::get_sorted()` when source is set +3. Add source post-filtering in `MemorySearch::hybrid_search()` (alongside the existing `memory_type` filter) +4. Add a `source` parameter to `MemoryRecallArgs` in the recall tool +5. Add a SQLite index on the `source` column: `CREATE INDEX idx_memories_source ON memories(source)` +6. Expose `source` in the API query structs (`MemoriesListQuery`, `MemoriesSearchQuery`) + +The autonomy loop's recall phase uses source filtering to pull memories from previous cycles. But the memories also surface naturally through semantic recall — when a user asks "what's going on with Spacedrive issues?", autonomy loop memories about Spacedrive surface by relevance alongside memories from conversations. + +### Interaction with Existing Systems + +**Bulletin.** No changes needed. The bulletin already queries memories across all sources. Autonomy loop memories appear in the "Recent Memories," "Observations," "Events," and other bulletin sections based on type and recency. The bulletin is how the agent's autonomous findings reach every conversation. + +**Ready-task pickup.** The autonomy loop creates tasks. The ready-task loop executes them. This is the natural flow: **autonomy loop monitors and discovers → creates task → task system executes**. Tasks created by the autonomy loop should default to `pending_approval` unless the agent's config allows auto-approval for autonomy-generated tasks. + +**Cron jobs.** The autonomy loop builds awareness. Cron jobs deliver it on schedule. A morning briefing cron fires, branches to recall, and autonomy loop memories surface. No direct integration needed — the memory system bridges them. + +**Email adapter.** Email channels already handle immediate triage (save memories about important emails, skip spam). The autonomy loop synthesizes across email memories: "5 emails today, 1 urgent from Alice about a contract deadline." The channel handles triage; the autonomy loop handles synthesis. + +**Association loop.** Continues to run independently. Autonomy loop memories get the same auto-association treatment as all other memories. + +### Configuration + +Add to `CortexConfig`: + +```rust +pub struct CortexConfig { + // ... existing fields ... + + /// Whether the autonomy loop is enabled. Default: true. + pub autonomy_enabled: bool, + /// Interval between autonomy loop cycles in seconds. Default: 1800 (30 min). + /// Minimum: 300 (5 min). + pub autonomy_interval_secs: u64, + /// Max LLM turns per autonomy cycle. Default: 15. + pub autonomy_max_turns: usize, + /// Max concurrent workers the autonomy loop can spawn per cycle. Default: 3. + pub autonomy_max_workers: usize, + /// Whether autonomy-created tasks require approval. Default: true. + pub autonomy_tasks_require_approval: bool, +} +``` + +These are system settings, not user-facing configuration. They're tunable in `config.toml` or via the API but don't require a dedicated UI. + +### System Prompt + +New template: `prompts/en/autonomy_loop.md.j2` + +```jinja +You are the autonomy process for {{ agent_name }}. You are the agent's proactive awareness — the part that thinks about what's going on without being asked. + +This is not a conversation. There is no user present. You are background cognition, waking up periodically to stay informed, identify emerging issues, and prepare knowledge that will be useful when the user next interacts. + +{% if identity_context %} +{{ identity_context }} +{% endif %} + +{% if memory_bulletin %} +## Current Awareness +{{ memory_bulletin }} +{% endif %} + +## Previous Cycle Findings +{{ recalled_context }} + +## Active Tasks +{{ active_tasks }} + +## Recent Activity +{{ recent_events }} + +## Instructions + +Based on your identity, role, and current awareness: + +1. **Assess the current state.** What do you know? What has changed since your last cycle? What are the most important things happening right now? + +2. **Decide what to investigate.** You have access to workers that can execute shell commands, check APIs, read files, browse the web. What would be most valuable to check right now? Prioritize by urgency and relevance to your role. + +3. **Do the work.** Spawn workers to gather information. Review what they find. Connect it to what you already know. + +4. **Save what you learn.** Every finding, observation, status change, or emerging pattern should be saved as a memory. Use appropriate memory types: + - **event** — something that happened (CI failed, PR merged, email received) + - **observation** — a pattern or trend you notice (build times increasing, certain contributor very active) + - **fact** — a concrete piece of information (current issue count, deployment status) + - **decision** — something you decided or concluded (this issue is urgent, this can wait) + +5. **Create tasks for actionable items.** If you discover something that needs human attention or automated action, create a task on the board. Be specific about what needs to happen and why. + +6. **Decide what to defer.** You can't check everything every cycle. Note what you're deferring and why, so you can pick it up next time. + +Tag all memories with source "cortex:autonomy". Be efficient with your turn budget — spawn workers for the actual work, use your turns for reasoning and synthesis. +``` + +### Implementation + +The autonomy loop is implemented as `spawn_autonomy_loop` alongside the existing four cortex loops. It follows the same pattern: a `tokio::spawn` that runs for the lifetime of the process. + +``` +spawn_autonomy_loop(deps, logger) + └─ loop: + 1. Sleep for autonomy_interval_secs + 2. Check autonomy_enabled (hot-reload via ArcSwap) + 3. Acquire execution lock (prevent overlap) + 4. Assemble context: + a. Load identity (from RuntimeConfig) + b. Load bulletin (from RuntimeConfig) + c. Load active tasks (from TaskStore) + d. Query recent cortex events + e. Recall memories with source "cortex:autonomy" (latest N) + 5. Create a branch: + - System prompt: autonomy_loop.md.j2 with assembled context + - Tools: memory_save, memory_recall, spawn_worker, task_create, task_list + - Max turns: autonomy_max_turns + - Worker limit: autonomy_max_workers + 6. Run the branch + 7. Force-save cycle summary memory (source: "cortex:autonomy") + 8. Log to cortex_events + 9. Circuit breaker: disable after N consecutive failures +``` + +The branch creation reuses the existing `Branch` infrastructure. The only new code is the runner function, the prompt template, and the context assembly. + +## Stateful Cron Jobs + +The second half of the design. Today, cron jobs are stateless — each run spins up a fresh channel with no memory of previous runs. This is fine for simple reminders ("remind me at 9am") but insufficient for recurring analytical tasks ("give me a morning briefing of what changed overnight"). + +### The Change + +Add an optional `source_tag` field to `CronConfig`. When set, the cron job's channel automatically: + +1. **Recalls memories** tagged with this source before executing the prompt +2. **Saves relevant findings** tagged with this source after execution +3. **Accumulates knowledge** across runs — each execution builds on the previous + +```rust +pub struct CronConfig { + // ... existing fields ... + + /// Optional source tag for memory continuity across runs. + /// When set, the cron channel recalls memories with this source + /// before execution and saves findings with this source after. + /// Format: "cron:{id}" (auto-set from job ID if enabled). + pub stateful: bool, // default: false +} +``` + +When `stateful` is true, the source tag is automatically derived as `cron:{id}` (e.g., `cron:morning-briefing`). No user-specified tags — keep it simple. + +### How It Works + +When a stateful cron job fires, the existing `run_cron_job` flow is modified: + +``` +1. Create fresh channel (same as today) +2. NEW: If stateful, inject a recall preamble: + "You are running a recurring scheduled task. Here is context + from previous executions:" + + recalled memories filtered by source "cron:{id}" +3. Send the cron prompt as a synthetic message (same as today) +4. Collect responses and deliver (same as today) +5. NEW: If stateful, spawn a brief memory-persistence branch + that saves relevant findings with source "cron:{id}" +``` + +The recall happens before the prompt, injected as part of the system context. The save happens after delivery, as a fire-and-forget branch (same pattern as the existing `memory_persistence` branches that run after conversations). The cron job itself doesn't need to know about memory mechanics — it's handled by the runtime. + +### Cron Tool Update + +The `CronTool` gains a `stateful` parameter on the `create` action: + +``` +Arguments (create): + id: String (required) + prompt: String (required) + interval_secs: u64 (optional, default 3600) + delivery_target: String (required) + active_hours: String (optional) + run_once: bool (optional, default false) + stateful: bool (optional, default false) // NEW +``` + +The LLM decides whether a cron job should be stateful based on the user's request: + +- "Remind me at 9am to check email" → stateful: false (one-shot reminder) +- "Give me a morning briefing every day" → stateful: true (needs to accumulate overnight context) +- "Check the build status every hour and ping me if it fails" → stateful: true (needs to know previous state to detect changes) + +### Migration + +```sql +ALTER TABLE cron_jobs ADD COLUMN stateful INTEGER NOT NULL DEFAULT 0; +``` + +### What This Replaces + +In the custom cortex loops design, the suggested approach for a "morning briefing" was: + +1. Create cortex loop `track-spacebot-repo` (monitors repo every 30 min) +2. Create cortex loop `monitor-email` (monitors email every 15 min) +3. Create cron job `morning-briefing` (delivers at 9am, recalls loop memories) + +With the autonomy loop + stateful cron approach: + +1. The autonomy loop already monitors things relevant to the agent's role (repo, email, etc.) — no user action needed +2. Create stateful cron job `morning-briefing` (delivers at 9am, recalls previous briefing memories + autonomy loop memories surface naturally via the bulletin) + +The user creates one thing (the cron job) instead of three. The system handles the rest. + +## How It All Fits Together + +``` +Identity files (SOUL.md, ROLE.md, etc.) + │ + │ define what the agent cares about + ▼ +Autonomy Loop (runs every 30 min) + │ proactively monitors relevant integrations + │ saves findings as memories (source: "cortex:autonomy") + │ creates tasks for actionable items + │ + ├──► Memory system ◄── Email channels (triage incoming mail) + │ │ Conversations (save user context) + │ │ Compactor (extract from history) + │ │ + │ ▼ + │ Bulletin (synthesizes all memories hourly) + │ │ + │ ▼ + │ Every channel reads the bulletin + │ │ + │ ▼ + │ User conversations have ambient awareness + │ + ├──► Task board + │ │ + │ ▼ + │ Ready-task pickup loop (executes autonomy-created tasks) + │ + └──► Stateful cron jobs + │ recall previous run memories + autonomy memories surface via bulletin + │ deliver accumulated knowledge on schedule + ▼ + User gets a morning briefing with overnight findings +``` + +## Example Scenarios + +### Scenario 1: Overnight Monitoring + Morning Briefing + +**Setup:** +- ROLE.md says: "You are the engineering assistant for the Spacebot project. You monitor the spacebot repo, track CI status, and keep the team informed." +- User creates a stateful cron job: "Every morning at 9am, give me a briefing of what happened overnight" + +**Overnight:** +- Autonomy loop runs every 30 min: + - Cycle 1: Recalls no previous findings (first run). Reads role.md — should monitor spacebot repo. Spawns worker to check GitHub. Finds 2 new issues, 1 merged PR. Saves as memories. + - Cycle 2: Recalls cycle 1 findings. Spawns worker to check CI — main build passing. Checks issues again — 1 new comment on issue #423. Saves findings. + - Cycle 3: Recalls cycles 1-2. CI still passing. No new issues. Defers to next cycle. + - Cycle 4: Recalls previous cycles. New email from Alice about contract renewal (surfaced via email channel memories in bulletin). Notes this is important. Saves observation: "Alice's contract renewal needs attention by Friday." + +**9am:** +- Morning briefing cron fires (stateful) +- Recalls its own previous briefing memories (context from last briefing) +- Bulletin has been refreshed with autonomy loop findings +- Channel branches to recall — autonomy memories surface by relevance +- Delivers: "Good morning. Overnight: 2 new issues on spacebot (#423 memory leak, #424 docs update), PR #420 merged (email adapter). CI is green. Alice emailed about her contract renewal — deadline is Friday." + +### Scenario 2: Proactive Task Creation + +**Setup:** +- Agent has been told it's responsible for CI health + +**Autonomy cycle:** +- Recalls previous findings: CI was passing last cycle +- Spawns worker to check GitHub Actions — build failing for 2 hours +- Saves event memory: "CI failure on main: test_memory_search failing since commit abc123" +- Creates task: "Investigate CI failure: test_memory_search on main" (status: pending_approval, priority: high) +- Saves observation: "CI has been unstable this week — 3 failures in 5 days" + +**Next bulletin refresh:** +- Bulletin picks up the CI failure event and the instability observation +- Every channel now has ambient awareness of the CI problem + +**User opens chat:** +- Agent's channel prompt includes the bulletin with CI failure info +- User asks "what's going on?" — agent already knows, no recall needed + +### Scenario 3: Email Synthesis + +**Ongoing:** +- Email channels process incoming mail (already implemented on `feat/email-adapter`) +- Each email is triaged: save memories for important ones, skip spam +- Email memories accumulate: sender, subject, urgency, key content + +**Autonomy cycle:** +- Recalls recent email-related memories (they surface by relevance alongside cortex:autonomy memories) +- Synthesizes: "5 emails today. 1 urgent from Alice (contract renewal, Friday deadline). 2 newsletters (skipped). 1 from Bob about the API redesign meeting next Tuesday. 1 GitHub notification (PR review requested)." +- Saves consolidated observation + +**User asks "any important emails?":** +- Branch recalls autonomy observations about email +- Can also use `email_search` tool to read specific emails on demand +- Delivers curated summary without the user needing to check their inbox + +## Phases + +### Phase 1: Source Filter Infrastructure + +- Add `source: Option` to `SearchConfig` +- Add source filtering to `MemoryStore::get_sorted()` SQL queries +- Add source post-filtering in `MemorySearch::hybrid_search()` +- Add `source` parameter to `MemoryRecallArgs` and the API query structs +- Add SQLite index on `memories.source` +- This is prerequisite for both the autonomy loop and stateful cron + +### Phase 2: Autonomy Loop + +- `spawn_autonomy_loop` function in `cortex.rs` +- `autonomy_loop.md.j2` system prompt template +- Context assembly: identity + bulletin + tasks + recent events + recalled memories +- Branch creation with memory tools + worker spawning + task tools +- Forced cycle summary save (source: "cortex:autonomy") +- Execution logging to `cortex_events` +- Circuit breaker (disable after N consecutive failures) +- `CortexConfig` extensions (autonomy_enabled, autonomy_interval_secs, etc.) +- Wire into `main.rs` alongside existing cortex loops + +### Phase 3: Stateful Cron Jobs + +- Add `stateful` column to `cron_jobs` table (migration) +- Add `stateful` field to `CronConfig` and `CronJob` +- Modify `run_cron_job` to inject recalled memories when stateful +- Add post-execution memory persistence branch for stateful crons +- Update `CronTool` to accept `stateful` parameter on create +- Update cron tool description prompt to explain when to use stateful + +### Phase 4: Polish + +- Bulletin section showing autonomy loop status and recent findings +- SSE events for autonomy cycle state changes +- Smart model routing — use cheaper model for recall/save phases +- Concurrency limiting for autonomy-spawned workers +- Dashboard visibility: autonomy loop status in the cortex section (read-only, not configurable) + +### Phase Ordering + +``` +Phase 1 (source filters) — standalone, unblocks 2 and 3 +Phase 2 (autonomy loop) — depends on Phase 1 +Phase 3 (stateful cron) — depends on Phase 1, independent of Phase 2 +Phase 4 (polish) — depends on Phases 2 + 3 +``` + +Phases 2 and 3 can run in parallel after Phase 1. + +## Open Questions + +**Autonomy loop prioritization.** How does the autonomy loop decide what to check each cycle? The prompt instructs it to prioritize by urgency and role relevance, but in practice the LLM might fall into a rut (always checking the same things). Options: (a) trust the LLM and let memory accumulation guide it naturally, (b) inject a "what haven't you checked recently?" nudge using the cycle summary from the previous run, (c) rotate through a checklist derived from the role. Starting with (a) + (b) seems right — the cycle summary already captures what was deferred. + +**Autonomy loop depth vs. breadth.** Should the loop try to check everything each cycle (broad, shallow) or focus on one area per cycle (narrow, deep)? The prompt says "decide what to investigate" — leaving it to the LLM. A 30-minute interval with 15 max turns is enough for 2-3 worker spawns with reasoning around them. If the loop is too shallow, increase max_turns or decrease interval. This is a tuning problem, not an architectural one. + +**Task approval for autonomy-generated tasks.** The design defaults to `pending_approval` for tasks created by the autonomy loop. This prevents the agent from auto-executing potentially expensive or destructive work. But it means someone has to approve tasks — which defeats the purpose of autonomous operation for trusted agents. The `autonomy_tasks_require_approval` config flag allows disabling this per-agent. High-trust deployments (personal assistants) might auto-approve; team deployments might require approval. + +**Stateful cron recall budget.** How many memories should a stateful cron recall from previous runs? Too few and it loses continuity; too many and the channel's context fills up before the prompt even runs. A default of 10-15 most recent memories from `cron:{id}` is probably right, with a configurable cap. + +**Autonomy loop and multi-agent.** In a multi-agent setup, each agent has its own autonomy loop. Should they coordinate? The multi-agent communication system (send_agent_message) handles cross-agent messaging, but the autonomy loops run independently. If two agents both monitor the same repo, they'll duplicate work. This is acceptable for now — the memory system is per-agent, and each agent needs its own awareness. Cross-agent deduplication is a future concern. + +**Warmup gating.** The autonomy loop should respect the same `WorkReadiness` gates as branches and workers. It should not run until the warmup state is `Warm` and the first bulletin has been generated. This ensures it has a bulletin to reference on its first cycle. + +**Cycle budget allocation.** With 15 max turns per cycle: how many should go to reasoning vs. worker spawns? Workers run asynchronously — the branch spawns them and they report back. If the branch spawns 3 workers at turn 2 and they all complete by turn 5, it has 10 turns to reason about results and save memories. If workers are slow, the branch might hit its turn limit before results arrive. The branch-worker interaction pattern in the existing codebase handles this (branches wait for workers or move on), but the autonomy loop prompt should instruct the LLM to spawn workers early in the turn budget. diff --git a/docs/design-docs/cortex-chat.md b/docs/design-docs/cortex-chat.md index d44ae83f6..814bc7411 100644 --- a/docs/design-docs/cortex-chat.md +++ b/docs/design-docs/cortex-chat.md @@ -4,13 +4,14 @@ A global admin chat panel that provides a direct interactive line to the cortex. ## Concept -The cortex chat is not a branch of a channel — it's a persistent, agent-scoped conversation with the cortex in interactive mode. It has the full toolset (memory, shell, file, exec, browser, web search, spawn worker) and streams responses via SSE with text deltas and tool activity. +The cortex chat is not a branch of a channel — it's a persistent, agent-scoped conversation with the cortex in interactive mode. It has the full toolset (memory, shell, file, exec, browser, web search, spawn worker) and uses SSE for response delivery and tool activity events. Key properties: - **One conversation per agent** — same thread regardless of which channel page you're viewing - **Channel context is ephemeral** — injected into the system prompt, not the chat history. Switching channels changes the context for the next message - **Persistent history** — stored in SQLite, survives refresh and navigation -- **Full text streaming** — character-by-character response streaming via SSE +- **Current behavior** — SSE emits tool activity and a final `done` message with full response text +- **Target behavior** — character-by-character text delta streaming via SSE - **Full tool access** — everything except reply/react/skip (those are channel-only platform tools) ## Data Model @@ -31,7 +32,7 @@ CREATE TABLE cortex_chat_messages ( ## Phase 1: LLM Streaming Infrastructure -`SpacebotModel::stream()` is currently stubbed. Streaming is required for the cortex chat UX and is reusable across the system (channel platform streaming, future features). +`SpacebotModel::stream()` was stubbed when this design was drafted; the LLM streaming layer now exists. Remaining work is adopting stream-native Rig loops in cortex chat (`stream_prompt`) so SSE can emit text deltas instead of only final responses. ### 1a. Anthropic SSE Streaming @@ -81,8 +82,8 @@ Core struct holding: agent deps, tool server, store. 1. Load cortex chat history from DB 2. If `channel_context_id` is provided, fetch the last 50 messages from that channel 3. Render `cortex_chat.md.j2` with: identity context, memory bulletin, channel transcript (if any), worker capabilities -4. Build Rig Agent with streaming: `agent.stream_prompt(user_text).with_history(&mut history).multi_turn(50)` -5. Consume the stream, forwarding events as `CortexChatEvent` variants +4. Current implementation: `agent.prompt(user_text).with_history(&mut history)` with SSE tool events + final response +5. Target implementation: `agent.stream_prompt(user_text).with_history(...).multi_turn(50)` with text-delta forwarding 6. Save both user message and assistant response to DB ### 2d. Tool Server Factory @@ -112,10 +113,11 @@ Add to `ApiState` in `src/api/state.rs`: | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/cortex-chat/messages?agent_id=...&limit=50` | Load persisted history | -| `POST` | `/api/cortex-chat/send` | Send message, returns SSE stream | +| `POST` | `/api/cortex-chat/send` | Send message, returns SSE stream (`409` when a send is already in flight) | | `DELETE` | `/api/cortex-chat/messages?agent_id=...` | Clear history | -POST accepts `{ agent_id, message, channel_id? }` and returns `Content-Type: text/event-stream`: +POST accepts `{ agent_id, message, channel_id? }` and returns `Content-Type: text/event-stream`. +If a prior send is still running for the same agent session, the endpoint now returns `409 CONFLICT` with no stream; clients should wait briefly and retry: ``` event: text_delta diff --git a/docs/design-docs/cortex-history.md b/docs/design-docs/cortex-history.md index 381b5e448..885309eb7 100644 --- a/docs/design-docs/cortex-history.md +++ b/docs/design-docs/cortex-history.md @@ -16,13 +16,13 @@ Only things the cortex **did**, not things it passively observed. Every action f | `memory_pruned` | Low-importance orphans removed | count, importance threshold | | `association_created` | Cross-channel association made | source_id, target_id, relation_type, reason | | `contradiction_flagged` | Contradicting memories found | memory_a_id, memory_b_id, description | -| `worker_killed` | Cortex killed a hanging worker | worker_id, channel_id, reason (timeout, error loop) | -| `branch_killed` | Cortex killed a stale branch | branch_id, channel_id, reason (timeout) | -| `circuit_breaker_tripped` | Component hit failure threshold | component_key, failure_count, action_taken | +| `worker_killed` | Cortex killed a hanging worker | worker_id, channel_id (optional), timeout_secs, reason | +| `branch_killed` | Cortex killed a stale branch | branch_id, channel_id, timeout_secs, reason | +| `circuit_breaker_tripped` | Component hit failure threshold | key, failure_count, threshold, action_taken | | `observation_created` | Cortex created an observation memory | memory_id, content_preview | -| `health_check` | Periodic health summary | active_workers, active_branches, active_channels, memory_count | +| `health_check` | Periodic health summary | kill_skipped_due_to_lag, kill_budget, kill_attempts, kill_actions, worker_timeout_secs, branch_timeout_secs, pruned_dead_channels | -These map to the phases in `cortex-implementation.md`: bulletin (exists today), maintenance (Phase 3), health supervision (Phase 2), consolidation (Phase 4). The table schema supports all of them from day one even though only bulletin events will be emitted initially. The rest light up as the cortex implementation progresses. +These map to the phases in `cortex-implementation.md`: bulletin (exists today), maintenance (Phase 3), health supervision (Phase 2, implemented), consolidation (Phase 4). Health supervision currently emits `worker_killed`, `branch_killed`, `circuit_breaker_tripped`, and `health_check`; maintenance and consolidation emit their own events as those phases land. ## Data Model @@ -68,7 +68,9 @@ One method. The caller constructs the summary and details. **Maintenance** (Phase 3, future): The `MaintenanceReport` from `run_maintenance()` already contains decayed/pruned/merged counts — log a single `maintenance_run` event. -**Health supervision** (Phase 2, future): Log `worker_killed`, `branch_killed`, `circuit_breaker_tripped` when those actions are taken. +**Health supervision** (Phase 2, implemented): Log `worker_killed`, `branch_killed`, `circuit_breaker_tripped`, and `health_check` from the supervision tick. + +Phase 2 breaker state is intentionally volatile (in-memory only). After restart, counters/tripped flags reset and begin accumulating again from live events. **Consolidation** (Phase 4, future): Log `memory_merged`, `association_created`, `contradiction_flagged`, `observation_created` as the consolidation agent acts. diff --git a/docs/design-docs/cortex-implementation.md b/docs/design-docs/cortex-implementation.md index e0a0f6644..f18b6ead3 100644 --- a/docs/design-docs/cortex-implementation.md +++ b/docs/design-docs/cortex-implementation.md @@ -1,58 +1,64 @@ # Cortex Implementation Plan -The cortex is designed to be the system's self-awareness — supervising processes, maintaining memory coherence, and generating the memory bulletin. Today, only the bulletin works. Everything else is dead code or stubs. +The cortex is designed to be the system's self-awareness — supervising processes, maintaining memory coherence, and generating the memory bulletin. Phase 1 plumbing and Phase 2 health supervision are now live; maintenance and consolidation phases remain. This doc covers the path from "bulletin generator" to "full system supervisor." ## What Exists Today **Running:** -- `spawn_bulletin_loop()` — generates the memory bulletin on startup, refreshes hourly via LLM synthesis of pre-gathered memory data. Fully functional. +- `spawn_cortex_loop()` — instantiates `Cortex`, subscribes to both control and memory event buses, runs a `tokio::select!` loop for event observation and periodic ticks, and refreshes bulletin/profile on interval. +- `spawn_bulletin_loop()` — compatibility alias to `spawn_cortex_loop()`. +- `spawn_warmup_loop()` — asynchronous warmup that keeps bulletin/embedding readiness fresh. -**Defined but never instantiated:** -- `Cortex` struct — has `observe()` (converts 3 of 12 event types into signals with hardcoded dummy values) and `run_consolidation()` (logs and returns `Ok(())`). -- `Signal` enum — 6 variants, none ever constructed at runtime. +**Defined and instantiated:** +- `Cortex` struct — observes all `ProcessEvent` variants, builds a rolling signal buffer, and runs on configurable tick cadence. +- `Signal` enum — aligned to current `ProcessEvent` surface (worker/branch/tool/memory/compaction/task/link events). - `CortexHook` — all methods return `Continue` with trace logging. **Implemented but never called:** - `memory/maintenance.rs` — `apply_decay()` and `prune_memories()` work. `merge_similar_memories()` is a stub returning `Ok(0)`. -**Wired through config but never read:** -- `CortexConfig` fields: `tick_interval_secs` (30), `worker_timeout_secs` (300), `branch_timeout_secs` (60), `circuit_breaker_threshold` (3). All resolve through `env > DB > default` and support hot-reload. +**Wired through config:** +- `tick_interval_secs` and `bulletin_interval_secs` are read by the running cortex loop and hot-reload during runtime. +- Phase 2 knobs are active and hot-reloaded: `worker_timeout_secs`, `branch_timeout_secs`, `detached_worker_timeout_retry_limit`, `supervisor_kill_budget_per_tick`, and `circuit_breaker_threshold`. **Referenced in prompts but don't exist:** - `memory_consolidate` tool - `system_monitor` tool -**Event bus:** -- 12 `ProcessEvent` variants on a `broadcast::Sender` per agent. -- `MemorySaved` and `CompactionTriggered` variants are defined but never emitted by any code. +**Event buses:** +- Two per-agent `broadcast::Sender` streams: + - `event_tx` for control/lifecycle events (channel, branch, worker, compactor, task/link events) + - `memory_event_tx` for `MemorySaved` telemetry emitted by `memory_save` +- `CompactionTriggered` is emitted by the compactor on `event_tx` when thresholds are reached. -## Phase 1: The Tick Loop +## Phase 1: The Tick Loop (Implemented) Get the cortex running as a persistent process that observes the event bus and ticks on an interval. Purely programmatic — no LLM. -### Wire missing event emission +### Wire missing event emission (done) - Emit `MemorySaved` from `memory_save` tool after successful save - Emit `CompactionTriggered` from the compactor when thresholds are hit -### Instantiate the cortex +### Instantiate the cortex (done) -- Call `Cortex::new()` in `main.rs` alongside `spawn_bulletin_loop()` -- Subscribe to the event bus via `deps.event_tx.subscribe()` +- Call `Cortex::new()` from `spawn_cortex_loop()` during agent startup +- Subscribe to both buses via `deps.event_tx.subscribe()` and `deps.memory_event_tx.subscribe()` - Run a `tokio::select!` loop: - - Receive events → feed through `observe()` + - Receive control events → feed through `observe()` + - Receive memory events (`MemorySaved`) → feed through `observe()` - Tick on `cortex_config.tick_interval_secs` - Move bulletin generation into the cortex's tick loop (currently a standalone free function) -### Fix `observe()` to extract real values +### Fix `observe()` to extract real values (done) - Map all 12 `ProcessEvent` variants, not just 3 - Pull real `memory_type`, `importance`, content summaries from events - Enrich `MemorySaved` event variant with `memory_type` and `importance` fields so the cortex gets useful data without querying the store -### Rework `Signal` enum +### Rework `Signal` enum (done) - Align variants with what `ProcessEvent` actually provides - Add `WorkerStarted`, `BranchStarted`, `WorkerStatus` @@ -60,42 +66,63 @@ Get the cortex running as a persistent process that observes the event bus and t **End state:** The cortex is running, consuming all events, building a signal buffer, and ticking. It sees everything but doesn't act on anything yet. -## Phase 2: System Health +## Phase 2: System Health (Implemented) -The supervisor role. Still no LLM — state tracking and threshold checks. +The supervisor role is now active and still fully programmatic (no LLM loop for health decisions). -### Track active processes +### Control plane -Maintain state maps in the cortex: +- `ProcessControlRegistry` provides cancellation routing for: + - live channel workers/branches through weak `ChannelControlHandle`s + - detached ready-task workers through registered `DetachedWorkerControl` entries +- Channel cancel convergence now uses reason-aware methods that emit terminal synthetic events: + - worker cancel emits one `WorkerComplete` payload (`result`, `notify`, `success`) + - branch cancel removes branch status, logs terminal run, emits one synthetic `BranchResult` +### Detached worker single-winner lifecycle + +Detached ready-task workers use a lifecycle state machine: + +```text +0 active -> 1 completing -> 3 terminal +0 active -> 2 killing -> 3 terminal ``` -HashMap — start time, last status time, channel_id, error count -HashMap — start time, channel_id -``` -Populated from `WorkerStarted`, `WorkerStatus`, `WorkerComplete`, `BranchStarted`, `BranchResult` events. Cleaned up on completion events. +- Completion path must win `CAS(0 -> 1)` before terminal side-effects. +- Supervisor kill path must win `CAS(0 -> 2)` (or observe prior `2`) before timeout side-effects. +- Losing path performs no terminal writes/events. + +### Timeout retry and quarantine policy + +On detached timeout kill, a single task update writes both status and metadata: + +- `supervisor_timeout_count` increments each timeout. +- `supervisor_timeout_exhausted` flips to `true` once the retry limit is exceeded. +- If count is `<= detached_worker_timeout_retry_limit`: task returns to `ready` and clears `worker_id`. +- If count is `> detached_worker_timeout_retry_limit`: task moves to `backlog` and clears `worker_id`. -### Worker supervision (each tick) +### Lag-aware health tick -- Detect workers with no status update in `worker_timeout_secs` -- Kill hanging workers via cancellation token (needs a direct cancellation path — the current `CancelTool` works through `ChannelState`, which the cortex doesn't have) -- Clean up completed workers that haven't been acknowledged -- Log worker lifecycle stats +Per tick, cortex maintains runtime maps for workers, branches, branch latency, and breaker state. -### Branch supervision (each tick) +- If control receiver lag occurred since the prior tick, kill enforcement is skipped for that tick only. +- `health_check` logs include `kill_skipped_due_to_lag=true` when skipped. +- Lag flag is cleared at tick end so enforcement resumes next interval. -- Detect branches running longer than `branch_timeout_secs` -- Kill stale branches via cancellation token -- Track branch latency (rolling average) for system health visibility +### Kill budget and ordering -### Circuit breaker +- Overdue workers/branches are selected in deterministic oldest-first order. +- Cancellation attempts are capped by `supervisor_kill_budget_per_tick` each tick. +- Remaining overdue items roll to subsequent ticks. -- Track consecutive failures by component key (tool name, provider name, operation type) -- After `circuit_breaker_threshold` consecutive failures, flag the component and log a warning -- Reset counter on success -- How flags affect behavior is an open question (see below) +### Observe-only circuit breaker -**End state:** The cortex actively monitors process health and cleans up stuck/stale processes. +- Worker failures increment `worker_type:` counters. +- Tool failures increment only on structured JSON results with boolean `success=false` or `ok=false`. +- No text heuristics are used in Phase 2. +- Threshold crossing emits `circuit_breaker_tripped` with `action_taken="observe_only"`. +- Success resets the counter/tripped flag for that key. +- Breaker state is in-memory only in Phase 2 and resets on process restart. ## Phase 3: Memory Maintenance diff --git a/docs/design-docs/link-channels-task-delegation.md b/docs/design-docs/link-channels-task-delegation.md new file mode 100644 index 000000000..3d85230c0 --- /dev/null +++ b/docs/design-docs/link-channels-task-delegation.md @@ -0,0 +1,274 @@ +# Link Channels as Task Delegation (v3) + +Replaces the LLM-to-LLM conversational model from link-channels-v2 with deterministic task-based delegation. Agents don't talk to each other — they assign tasks. Link channels become audit logs of delegated work, not conversation threads. + +## Why + +The v2 design had agents exchange messages through mirrored link channels, with each side running its own LLM to process and respond. This was fundamentally brittle: + +- **Recursive loops**: Agents ping-pong conclusions, replies, or re-delegations endlessly. +- **Context loss**: When a link channel re-opens after concluding, the LLM has no memory of prior work and re-sends the original task. +- **Turn count gaming**: Safety caps fire, force-conclude, then the next message resets the budget. +- **Conclusion non-compliance**: Agents ignore `conclude_link` instructions and chat until the safety cap. +- **Result routing corruption**: `initiated_from` metadata gets overwritten by subsequent messages, causing results to bridge to the wrong channel. + +Every fix added more special-case logic (drop guards, peer-initiated flags, history seeding, mechanical passthroughs). The system was getting more complex with each bug, not simpler. The core problem is irreducible: two LLMs having a conversation is non-deterministic and uncontrollable. + +## The New Model + +Instead of conversations, agents delegate through **tasks**. The existing task tracking system (Phase 1 already implemented on `task-tracking` branch) provides the structured, deterministic substrate. + +``` +User asks Agent A to do something + -> Agent A decides it should be delegated to Agent B + -> Agent A calls send_agent_message (modified) + -> A task is created in Agent B's task store + -> A record is logged in the link channel between A and B + -> Agent A's turn ends (skip flag) + -> Agent B's cortex ready-task loop picks up the task + -> Worker executes the task + -> Task moves to done + -> Completion record logged in link channel + -> Agent A is notified (retrigger on originating channel) +``` + +No LLM-to-LLM conversation. No reply relay. No conclusion handshake. No turn counting. The delegation is a database write; the execution is a worker; the result is a task status change. + +## What Link Channels Become + +Link channels shift from conversation threads to **audit logs**. They record: + +1. **Task created**: "Agent A assigned task #42 to Agent B: [title]" +2. **Task completed**: "Agent B completed task #42: [summary]" +3. **Task failed**: "Agent B's worker failed on task #42: [error]" +4. **Task requeued**: "Task #42 returned to ready after worker failure" + +These are **system messages** — not LLM-generated text. The link channel is a historical record of delegation activity between two agents. When a human opens a link channel in the dashboard, they see a timeline of tasks assigned and results returned. + +Link channels are no longer processed by the LLM. There is no `handle_message()` call, no branching, no worker spawning from link channels. They are write-only logs read by humans through the UI. + +### Channel ID Convention + +Keep `link:{agent_a}:{agent_b}` as the channel ID format. The `ChannelStore` records these for the UI to discover. Messages are persisted via `ConversationLogger` with `source: "system"` so they're never fed into an LLM context window. + +## Modified `send_agent_message` Tool + +The tool's external interface stays the same — the LLM calls it with a target agent and a message. The implementation changes completely: + +``` +Before (v2): + 1. Construct InboundMessage + 2. Inject into target agent's message pipeline + 3. Target agent's link channel processes it with LLM + 4. Reply routed back through outbound handler + 5. Source agent processes reply with LLM + 6. Back and forth until conclude_link + +After (v3): + 1. Validate link exists and permits this direction + 2. Create task in target agent's task store + - title: extracted from message (first sentence or explicit title) + - description: full message content + - status: ready (skip pending_approval for agent-delegated tasks) + - priority: inferred or default medium + - created_by: "agent:{source_agent_id}" + - metadata: { delegated_by, originating_channel, link_id } + 3. Log delegation record in link channel (system message) + 4. Set skip flag (end source agent's turn) + 5. Return { success: true, task_number } +``` + +The tool needs access to the **target agent's** `TaskStore`, not just the source agent's. This means `send_agent_message` needs a way to resolve task stores across agents. + +### Cross-Agent Task Store Access + +Currently, `TaskStore` instances are per-agent and stored in `ApiState::task_stores`. The tool runs inside a specific agent's process and only has access to that agent's `AgentDeps`. + +**Decision: Per-agent task stores.** Each agent owns its own `TaskStore` backed by its own SQLite database. A superior agent instructs task creation via the link channel system and can query/read a subordinate agent's tasks (read-only cross-agent access). The task store registry (`HashMap>`) is passed to tools that need cross-agent visibility, but task *creation* on another agent goes through the link channel mechanism, not direct writes. + +This preserves agent isolation — each agent's task board is its own — while giving the hierarchy the ability to observe and manage work across the org. + +## Task Completion Notification + +When a delegated task completes, the delegating agent needs to know. Two mechanisms: + +### 1. Link Channel Record + +When a worker completes a task that has `metadata.delegated_by`, the cortex (or the task completion handler in `cortex.rs`) logs a completion message in the link channel: + +``` +[System] Task #42 completed by community-manager: "Published 3 posts to Discord announcements channel. Links: [...]" +``` + +This is a passive record — it doesn't trigger the delegating agent's LLM. + +### 2. Originating Channel Retrigger + +The task metadata includes `originating_channel` (the channel where the user originally asked for the work). When the task completes, a system message is injected into that channel: + +``` +[System] Delegated task completed by community-manager: "Published 3 posts..." +``` + +This **does** retrigger the channel's LLM, which can then relay the result to the user naturally. The message has `source: "system"` and no `formatted_author`, so it renders as a plain system notification. + +This replaces the old `bridge_to_initiator` mechanism but is much simpler — it's a single message injection on task completion, not a recursive conclusion chain. + +### 3. Task Status Polling (Optional, Future) + +The delegating agent's cortex could periodically check on delegated tasks via `task_list` filtered by `metadata.delegated_by`. This is a pull model that doesn't require any special routing — the cortex just queries its own task store for tasks it created on other agents. + +Not needed for v1 since the push notification (retrigger) handles the common case. + +## What Gets Removed + +### Files to Delete + +| File | Reason | +|------|--------| +| `src/tools/conclude_link.rs` | Conversational conclusion mechanism | +| `prompts/en/fragments/link_context.md.j2` | "You're in a conversation with agent X" prompt | +| `prompts/en/tools/conclude_link_description.md.j2` | Tool description for deleted tool | + +### Code to Remove from `src/agent/channel.rs` + +All link-conversation handling logic: + +- `link_concluded` field and all checks against it +- `link_turn_count` field and safety cap logic +- `peer_initiated_conclusion` field +- `initiated_from` field and capture logic +- `originating_channel` / `originating_source` fields +- `build_link_context()` method (~40 lines) +- `route_link_conclusion()` / `handle_link_conclusion()` / `bridge_to_initiator()` methods (~160 lines) +- History seeding for link channels (original_sent_message replay) +- Coalesce bypass for link channels +- Drop guard for concluded link channels +- `conclude_link` tool registration in `run_agent_turn()` +- `ConcludeLinkFlag` / `ConcludeLinkSummary` return values from `run_agent_turn()` +- `is_link_channel` checks throughout + +### Code to Remove from `src/main.rs` + +- Outbound reply relay for `source == "internal"` channels (~90 lines, ~lines 981-1072) +- This is the code that intercepts Agent B's reply on `link:B:A` and injects it into `link:A:B` + +### Code to Remove from `src/tools.rs` + +- `pub mod conclude_link` and re-exports +- `conclude_link` parameter in `add_channel_tools()` / `remove_channel_tools()` +- `link_counterparty_for_agent()` helper +- `has_other_delegation_targets` complexity (simplify to basic "has any link targets") + +### Prompt Changes + +- Remove `conclude_link` text entry from `src/prompts/text.rs` +- Update `org_context.md.j2` wording — replace conversation language with task delegation language +- Update `send_agent_message` tool description — "assigns a task" not "sends a message" + +## What Gets Kept + +### Link Infrastructure (Unchanged) + +- `src/links.rs` + `src/links/types.rs` — `AgentLink`, `LinkDirection`, `LinkKind`, store utilities +- `src/config.rs` — `[[links]]` TOML parsing, `LinkDef` +- `src/main.rs` — Link initialization, `ArcSwap` plumbing, `AgentDeps.links` +- `ProcessEvent::AgentMessageSent` / `AgentMessageReceived` +- `src/api/links.rs` — Link CRUD API +- Topology API + +### Link Prompt Context (Modified) + +- `org_context.md.j2` — Keep the hierarchy rendering (superiors/subordinates/peers). Update the instruction text from "send a message" to "assign a task". +- `build_org_context()` in `channel.rs` — Keep as-is. It reads link topology and renders the org hierarchy. + +### Task System (From `task-tracking` Branch) + +- `migrations/20260219000001_tasks.sql` — Schema +- `src/tasks.rs` + `src/tasks/store.rs` — `TaskStore`, CRUD, status transitions +- `src/api/tasks.rs` — REST API +- `src/tools/task_create.rs`, `task_list.rs`, `task_update.rs` — LLM tools +- `src/agent/cortex.rs` — `spawn_ready_task_loop`, `pickup_one_ready_task` + +## New Code Needed + +### 1. Cross-Agent Task Creation in `send_agent_message.rs` + +Rewrite the tool's `call()` method to create a task instead of injecting a message. Needs a `task_stores: Arc>>` field. + +### 2. Task Completion Callback in `cortex.rs` + +After `pickup_one_ready_task` marks a task as `Done`, check if `metadata.delegated_by` exists. If so: + +1. Log completion in the link channel via `ConversationLogger` +2. Inject a system message into `metadata.originating_channel` to retrigger the delegating agent + +This replaces the entire `bridge_to_initiator` mechanism with ~20 lines of straightforward code. + +### 3. Link Channel System Message Logging + +A small helper that writes system messages to link channels: + +```rust +fn log_link_event( + conversation_logger: &ConversationLogger, + link_channel_id: &str, + message: &str, +) { + // Persist as a system message (source: "system", role: "system") + // Not fed to any LLM — purely for UI display +} +``` + +Called from `send_agent_message` (task created) and from the cortex completion handler (task done/failed). + +### 4. `send_agent_message` Tool Description Update + +Rewrite `prompts/en/tools/send_agent_message_description.md.j2` to describe task delegation: + +> Assign a task to another agent. The target agent's cortex will pick it up and execute it autonomously. Use this when work falls outside your scope or belongs to a subordinate. Your turn ends after delegation — the result will be delivered when the task completes. + +## Implementation Order + +### Phase 1: Tear Out LLM Conversations + +1. Delete `conclude_link.rs`, `link_context.md.j2`, conclude_link prompt text +2. Remove all link-conversation logic from `channel.rs` (fields, methods, guards) +3. Remove outbound reply relay from `main.rs` +4. Remove conclude_link from `tools.rs` registration +5. Simplify `add_channel_tools()` — drop conclude_link param, simplify delegation target logic +6. Verify compilation, run tests + +### Phase 2: Wire Task Delegation into `send_agent_message` + +1. Add `task_stores` registry to `SendAgentMessageTool` +2. Rewrite `call()` to create a task in the target agent's store +3. Add link channel system message logging on task creation +4. Update tool description prompt +5. Update `org_context.md.j2` wording + +### Phase 3: Task Completion Notifications + +1. Add `delegated_by` / `originating_channel` metadata checks to cortex task completion handler +2. Log completion in link channel +3. Inject retrigger system message into originating channel +4. Test full delegation round-trip + +### Phase 4: UI + Polish + +1. Link channel UI shows task timeline instead of conversation +2. Task board shows delegated tasks with source agent badge +3. SSE events for delegation activity +4. Dashboard topology graph shows task flow between agents + +## Open Questions + +**Per-agent vs instance-level task store**: Should delegated tasks live in the target agent's per-agent SQLite database, or should all tasks move to `instance.db`? Per-agent is cleaner for isolation but requires cross-agent store access. Instance-level is simpler but changes the storage model. + +**Task approval for delegated tasks**: Should agent-delegated tasks skip `pending_approval` and go straight to `ready`? The design assumes yes — if Agent A trusts Agent B enough to have a link, the task should execute without human approval. But some deployments might want human-in-the-loop for all delegated work. + +**Bidirectional task results**: When Agent B completes a task delegated by Agent A, should A get the full worker output or just a summary? Full output could be large (coding task transcripts). A summary is more practical but loses detail. Could store the full result in task metadata and show a summary in the retrigger message. + +**Multi-hop delegation**: Agent A delegates to Agent B, who delegates to Agent C. The completion notification needs to bubble up through all hops. Task metadata can track `delegation_chain: [A, B]` so C's completion notifies B, which notifies A. But this adds complexity — start with single-hop and extend later. + +**Task priority inheritance**: Should delegated tasks inherit priority from the delegating agent's context? If the user marked something urgent, the delegated task should probably be `high` priority. The LLM could set this explicitly in the `send_agent_message` call, or it could be inferred. diff --git a/docs/design-docs/named-messaging-adapters.md b/docs/design-docs/named-messaging-adapters.md index 70c40e011..26799771e 100644 --- a/docs/design-docs/named-messaging-adapters.md +++ b/docs/design-docs/named-messaging-adapters.md @@ -85,7 +85,7 @@ Add: ### Messaging config structs -For each platform with token-based auth (Discord, Telegram, Slack, Twitch): +For each platform (Discord, Telegram, Slack, Twitch, Email, Webhook): - Keep existing singleton fields (`token`, `bot_token`, `app_token`, etc.) - Add optional `instances: Vec<...InstanceConfig>` with: @@ -157,10 +157,44 @@ Response payloads should include adapter instance identity so UI can render mult ## UI Changes -- Keep current quick setup flow for default token -- Add “Add another token” flow that requires a name -- Settings display becomes list of adapter instances per platform -- Binding editor adds optional adapter selector (default preselected) +Two-column layout replacing the current single-column platform accordion. + +### Layout + +``` +┌───────────────────────┬────────────────────────────────────────┐ +│ Available │ Configured Instances │ +│ │ │ +│ [Discord +] │ ┌─ discord ───────────────────────┐ │ +│ [Slack +] │ │ Discord (default) ● on │ │ +│ [Telegram +] │ │ Bindings: 2 │ │ +│ [Twitch +] │ │ [Edit] [Disable] [Remove] │ │ +│ [Email +] │ └──────────────────────────────────┘ │ +│ [Webhook +] │ ┌─ telegram ──────────────────────┐ │ +│ │ │ Telegram (default) ● on │ │ +│ Coming Soon │ │ Bindings: 1 │ │ +│ WhatsApp │ │ [Edit] [Disable] [Remove] │ │ +│ Matrix │ └──────────────────────────────────┘ │ +│ iMessage │ ┌─ telegram:support ──────────────┐ │ +│ IRC │ │ Telegram "support" ● on │ │ +│ Lark │ │ Bindings: 1 │ │ +│ DingTalk │ │ [Edit] [Disable] [Remove] │ │ +│ │ └──────────────────────────────────┘ │ +│ │ │ +│ │ (empty state when nothing configured) │ +└───────────────────────┴────────────────────────────────────────┘ +``` + +- **Left column:** Platform catalog. Each platform has a "+" button to add an instance. Coming-soon platforms listed but disabled. Always visible regardless of configured count. +- **Right column:** Configured adapter instances as expandable summary cards. Compact view shows platform icon, instance name (or "default"), enabled status, binding count. Expands to show credentials, full binding list, and controls. + +### Interaction model + +- **Add instance:** Clicking "+" on a platform creates a new card inline in the right column in editing state. Name input (required for non-default), credential fields. Save creates the instance and collapses to summary. +- **First instance:** When a platform has no instances, the first "+" creates the default instance. No name input required — same single-token paste flow as today. +- **Subsequent instances:** "+" when a platform already has the default instance creates a named instance. Name input is required. +- **Instance cards:** Expandable accordions. Click to expand, showing credentials (masked), bindings section, enable/disable toggle, disconnect/remove button. +- **Bindings:** Edited per-instance inside each expanded card. Each instance card contains its own bindings section with add/edit/remove. Binding form auto-populates the `adapter` field. The common single-token path remains one step. @@ -193,40 +227,62 @@ No migration required for existing users. ### Phase 1: Config and binding model -1. Add `adapter` to binding structs and TOML parsing -2. Add per-platform `instances` config parsing -3. Add validation rules (names, existence, duplicates) -4. Update docs for config reference +1. Add `adapter: Option` to `Binding` (`config.rs:~1153`) and `TomlBinding` (`config.rs:~2302`) +2. Add per-platform instance config structs: `DiscordInstanceConfig`, `SlackInstanceConfig`, `TelegramInstanceConfig`, `TwitchInstanceConfig`, `EmailInstanceConfig`, `WebhookInstanceConfig` — each with `name: String`, `enabled: bool`, and the same credential fields as the parent platform config +3. Add `instances: Vec` to all platform configs: `DiscordConfig`, `SlackConfig`, `TelegramConfig`, `TwitchConfig`, `EmailConfig`, `WebhookConfig` +4. Add matching TOML deser structs (`TomlXInstanceConfig`) for `[[messaging..instances]]` array-of-tables +5. Add validation: no duplicate instance names within a platform, no empty or `"default"` names, `binding.adapter` must reference an existing configured instance +6. Update `Binding::matches()` (`config.rs:~1170`) to accept adapter identity parameter ### Phase 2: Runtime adapter identity -1. Refactor `MessagingManager` keying from platform name to runtime adapter key -2. Instantiate default + named adapters per platform -3. Attach adapter identity to inbound messages -4. Route outbound operations via inbound adapter identity +1. Define runtime key format — `"telegram"` for default, `"telegram:support"` for named — as a type alias or newtype +2. Refactor `MessagingManager` `HashMap>` (`manager.rs:~17`) to key by runtime key instead of `adapter.name()`. Update `register()`, `register_and_start()`, `remove_adapter()`, `respond()`, `has_adapter()` +3. Make adapter constructors accept an optional instance name so `name()` / `runtime_key()` returns the full key. Or add `runtime_key()` to the `Messaging` trait +4. Add `adapter: String` field to `InboundMessage` carrying the runtime adapter key +5. Update startup to instantiate default adapter from root config + one adapter per `instances` entry, all registered with the manager +6. Update `respond()` (`manager.rs:~203`) to route outbound by adapter runtime key captured on inbound, not `message.source` ### Phase 3: Binding resolution and permissions -1. Extend binding match logic with adapter selector -2. Build per-adapter permission sets from filtered bindings -3. Ensure hot-reload updates per-adapter permissions correctly +1. Extend `Binding::matches()` to check `binding.adapter` against inbound adapter identity. `None` matches default adapter only. `Some("x")` matches named adapter `"x"` only +2. Build per-adapter permission sets by filtering bindings on `(platform, adapter)` when constructing each platform's permission struct +3. Update hot-reload to rebuild and `ArcSwap` permissions per adapter instance independently + +### Phase 4: API + +1. Refactor `GET /api/messaging/status` (`api/messaging.rs:~16-23`) from hardcoded per-platform struct to `Vec` with `{platform, name, configured, enabled}` +2. Add optional `adapter: Option` to `TogglePlatformRequest` and `DisconnectPlatformRequest` (`api/messaging.rs`). Omitted = default instance +3. Add adapter instance CRUD: `POST /api/messaging/instances` (create named instance with credentials), `DELETE /api/messaging/instances` (remove named instance + associated bindings) +4. Add optional `adapter` to bindings CRUD payloads (`api/bindings.rs`): `CreateBindingRequest`, `UpdateBindingRequest`, `DeleteBindingRequest`. Old payloads without `adapter` remain valid + +### Phase 5: UI + +1. Update TypeScript types (`interface/src/api/client.ts`): new `AdapterInstance` type, update `MessagingStatusResponse` to return instance list, add `adapter` to `BindingInfo` and binding request types +2. Replace `ChannelsSection` (`interface/src/routes/Settings.tsx:~785`) single-column layout with two-column: left platform catalog, right configured instances +3. Build platform catalog (left column): platform list with "+" buttons, coming-soon platforms grayed out +4. Refactor `ChannelSettingCard` (`interface/src/components/ChannelSettingCard.tsx`) into expandable instance summary cards: platform icon, instance name, status badge, binding count. Expand for credentials, bindings, controls +5. Build add-instance flow: "+" creates inline card in editing state, name input for non-default instances, credential fields, save calls instance API +6. Scope binding editor per-instance: existing `BindingsSection` logic scoped to the instance's adapter, binding form auto-populates `adapter` field +7. Default instance UX: first instance of any platform = default, no name required, same single-token paste experience as today. Only second+ instances require a name + +### Phase 6: Tests + +1. Config parsing/validation tests: valid instance arrays, empty names, duplicate names, `"default"` name rejection +2. Binding match tests: default adapter match, named adapter match, mismatch rejection, adapter-less binding matches default only +3. API backward compat tests: payloads without `adapter` field work unchanged +4. Permission filtering tests: per-adapter permission sets built correctly from filtered bindings -### Phase 4: API and UI +## Risk Notes -1. Extend bindings API payloads with optional `adapter` -2. Extend messaging status/toggle/disconnect APIs for adapter instances -3. Update dashboard settings and binding editor for named instances -4. Preserve platform-only behavior for default adapter paths +**Phase 2 is the most invasive.** Changing how `MessagingManager` keys adapters touches every adapter, startup, hot-reload, and outbound routing. Most bugs will surface here. Consider landing Phase 2 as its own PR with focused review. -### Phase 5: Test coverage and rollout docs +**`ChannelEditModal` duplication.** The current UI has duplicated adapter logic between `ChannelSettingCard` and `ChannelEditModal`. The Phase 5 refactor is a good opportunity to consolidate, but optional — updating `ChannelSettingCard` alone is sufficient if the modal is used elsewhere. -1. Add config parsing/validation tests for named instances -2. Add routing tests for adapter-specific bindings -3. Add API tests for backward compatible payloads -4. Add setup docs for multi-bot per platform scenarios +**Build order is backend-first.** Phases 1-4 are Rust. Phase 5 is React. This avoids building UI against speculative API contracts. -## Open Questions +## Resolved Questions -- Should adapter identity be surfaced as a first-class field on `InboundMessage` instead of metadata? -- For Slack, should named instances support independent app-level settings beyond tokens in this phase? -- Should proactive broadcast endpoints require explicit adapter for platforms with multiple configured instances, or keep default fallback? +- **Adapter identity on `InboundMessage`:** Yes, first-class `adapter: String` field, not metadata. Binding resolution and outbound routing both depend on it directly. +- **Slack app-level settings beyond tokens:** No, not in this phase. Named Slack instances share the same config shape (bot_token + app_token). Independent app-level settings can be added later if needed. +- **Proactive broadcast with multiple instances:** Keep default fallback. Broadcast targets the default adapter unless the caller explicitly specifies a runtime key. This matches existing behavior and avoids breaking proactive sends for users who add named instances alongside their default. diff --git a/docs/design-docs/sandbox-hardening.md b/docs/design-docs/sandbox-hardening.md new file mode 100644 index 000000000..a7defcd6f --- /dev/null +++ b/docs/design-docs/sandbox-hardening.md @@ -0,0 +1,461 @@ +# Sandbox Hardening + +Dynamic sandbox mode, hot-reload fix, capability manager, and policy enforcement for shell and OpenCode workers. + +## 1. Dynamic Sandbox Mode (Hot-Reload Fix) + +### Problem + +Disabling the sandbox on an agent via the UI doesn't work. The setting visually reverts to "enabled" and the actual sandbox enforcement doesn't change. The config file on disk is written correctly, but the in-memory state is never updated. + +### Root Cause + +Three failures in the reload path: + +1. **`reload_config()` skips sandbox.** `config.rs:5012-5014` has an explicit comment: "sandbox config is not hot-reloaded here because the Sandbox instance is constructed once at startup and shared via Arc. Changing sandbox settings requires an agent restart." Every other config field gets `.store(Arc::new(...))` in `reload_config()`, but `self.sandbox` is skipped. + +2. **API returns stale data.** `get_agent_config()` reads from `rc.sandbox.load()` (`api/config.rs:232`), which still holds the startup value. The UI receives this stale response and resets the toggle. + +3. **`Sandbox` struct stores mode as a plain field.** The `Sandbox` instance (`sandbox.rs:60-68`) captures `mode: SandboxMode` at construction. Even if the `RuntimeConfig.sandbox` ArcSwap were updated, the `Arc` in `AgentDeps` would still enforce the old mode in `wrap()`. + +### Sequence Diagram + +``` +UI: PUT /agents/config {sandbox: {mode: "disabled"}} + -> api/config.rs writes mode="disabled" to config.toml (correct) + -> api/config.rs calls rc.reload_config() (skips sandbox) + -> api/config.rs calls get_agent_config() (reads stale ArcSwap) + -> returns {sandbox: {mode: "enabled"}} (wrong) +UI: displays "enabled" (reverted) + +~2s later: + file watcher detects config.toml change + -> calls reload_config() for all agents (skips sandbox again) + -> all agents log "runtime config reloaded" (sandbox unchanged) +``` + +### Fix + +#### Change 1: Update `RuntimeConfig.sandbox` in `reload_config()` (config.rs ~line 5011) + +Add the sandbox store alongside the other fields. Remove the skip comment. + +```rust +self.warmup.store(Arc::new(resolved.warmup)); +self.sandbox.store(Arc::new(resolved.sandbox.clone())); + +mcp_manager.reconcile(&old_mcp, &new_mcp).await; +``` + +This fixes the API response so `get_agent_config()` returns the correct value after a config change. + +#### Change 2: Wrap `RuntimeConfig.sandbox` in `Arc` (config.rs:4920) + +Change the field from `ArcSwap` to `Arc>` so it can be shared with the `Sandbox` struct: + +```rust +// Before +pub sandbox: ArcSwap, + +// After +pub sandbox: Arc>, +``` + +Update `RuntimeConfig::new()` accordingly: + +```rust +// Before +sandbox: ArcSwap::from_pointee(agent_config.sandbox.clone()), + +// After +sandbox: Arc::new(ArcSwap::from_pointee(agent_config.sandbox.clone())), +``` + +All existing `.load()` and `.store()` calls work through `Arc`'s `Deref` with no changes. + +#### Change 3: Make `Sandbox` read mode dynamically (sandbox.rs) + +Replace the `mode: SandboxMode` field with `config: Arc>`. Always detect the backend at startup (even when mode is initially Disabled), so we know what's available if the user later enables it. + +```rust +pub struct Sandbox { + config: Arc>, + workspace: PathBuf, + data_dir: PathBuf, + tools_bin: PathBuf, + backend: SandboxBackend, +} +``` + +Change `Sandbox::new()` signature to accept the shared ArcSwap: + +```rust +pub async fn new( + config: Arc>, + workspace: PathBuf, + instance_dir: &Path, + data_dir: PathBuf, +) -> Self +``` + +Backend detection always runs. The initial mode only affects the startup log message. + +In `wrap()`, read the current mode dynamically: + +```rust +pub fn wrap(&self, program: &str, args: &[&str], working_dir: &Path) -> Command { + let config = self.config.load(); + // ... + if config.mode == SandboxMode::Disabled { + return self.wrap_passthrough(program, args, working_dir, &path_env); + } + match self.backend { + SandboxBackend::Bubblewrap { proc_supported } => { ... } + SandboxBackend::SandboxExec => { ... } + SandboxBackend::None => self.wrap_passthrough(program, args, working_dir, &path_env), + } +} +``` + +The `writable_paths` field is removed from the struct. Paths are read from the ArcSwap config and canonicalized in `wrap()`. This is a cheap syscall and commands aren't spawned at rates where it matters. + +#### Change 4: Pass the shared ArcSwap to `Sandbox::new()` at both construction sites + +**main.rs ~line 1358:** + +```rust +let sandbox = std::sync::Arc::new( + spacebot::sandbox::Sandbox::new( + runtime_config.sandbox.clone(), // Arc> + agent_config.workspace.clone(), + &config.instance_dir, + agent_config.data_dir.clone(), + ) + .await, +); +``` + +**api/agents.rs ~line 665:** Same change. + +### What Doesn't Change + +- `AgentDeps.sandbox` stays as `Arc` (lib.rs:214). The `Sandbox` itself now reads mode dynamically, so the `Arc` reference doesn't need to be swapped. +- `ShellTool` and `ExecTool` continue holding `Arc` and calling `.wrap()`. No changes needed. +- The bubblewrap and sandbox-exec wrapping logic is unchanged. Only the dispatch in `wrap()` reads the dynamic mode. + +### Files Changed + +| File | Change | +|------|--------| +| `src/config.rs` | `RuntimeConfig.sandbox` type to `Arc>`; `RuntimeConfig::new()` wraps in `Arc::new()`; `reload_config()` adds `self.sandbox.store()` and removes skip comment | +| `src/sandbox.rs` | `Sandbox.mode` field replaced with `config: Arc>`; `writable_paths` removed from struct, read dynamically; `Sandbox::new()` signature change; `wrap()` reads mode from ArcSwap; backend detection always runs | +| `src/main.rs` | Pass `runtime_config.sandbox.clone()` to `Sandbox::new()` | +| `src/api/agents.rs` | Same `Sandbox::new()` signature update | + +--- + +## 2. Environment Sanitization + +### Problem + +Sandbox does NOT call `env_clear()`. Bubblewrap wrapping uses `--setenv PATH` but does not use `--clearenv`. Workers inherit the full parent environment. A worker can run `printenv ANTHROPIC_API_KEY` and get the raw key. Even `remove_var` on startup doesn't help because Linux `/proc/self/environ` is an immutable kernel snapshot from exec time. + +MCP processes already do this correctly (`mcp.rs:309` calls `env_clear()`). + +### Design + +Sandbox `wrap()` must call `env_clear()` (or the bwrap equivalent `--clearenv`) and explicitly re-inject only approved variables. Three categories: + +**Always passed through:** +- `PATH` (with tools/bin prepended, as today) +- `HOME`, `USER`, `LANG`, `TERM` (basic process operation) +- `TMPDIR` (if needed) + +**Passed from secret store (tool secrets only):** +- Credentials for CLI tools workers invoke (`GH_TOKEN`, `GITHUB_TOKEN`, `NPM_TOKEN`, `AWS_*`, etc.) +- The secret store categorizes secrets as **system** (internal — LLM API keys, messaging tokens) or **tool** (external — CLI credentials). Only tool secrets are injected into worker subprocesses. System secrets stay in Rust memory and never enter any subprocess environment. +- `wrap()` reads the current tool secrets from the store and injects each via `--setenv` (bubblewrap) or `Command::env()` (passthrough/sandbox-exec). +- Skills that expect `GH_TOKEN` in the environment just work. Skills never see `ANTHROPIC_API_KEY` because it's a system secret. + +**Passed from `passthrough_env` config (fallback for self-hosted without secret store):** +- A user-configured list of env var names to forward from the parent process: `passthrough_env = ["GH_TOKEN", "GITHUB_TOKEN"]` +- This is the escape hatch for self-hosted users who set env vars in Docker/systemd but don't configure a master key. Without it, `--clearenv` would silently strip their credentials. +- When the secret store is available, `passthrough_env` is redundant (everything should be in the store). The field still works — it's additive. +- See `docs/design-docs/secret-store.md` "Env Passthrough for Self-Hosted" for details. + +**Always stripped:** +- All `SPACEBOT_*` internal vars (the master key is never in the environment — it lives in the OS credential store; see `docs/design-docs/secret-store.md`) +- All system secrets (LLM API keys, messaging tokens — see `docs/design-docs/secret-store.md`) +- Any env var not in the above three categories + +For the passthrough (no sandbox) case: same env sanitization applies in the shell/exec tools directly via `Command::env_clear()` before `Command::env()` for the allowed + secret + passthrough vars. + +This is a **hard prerequisite for the secret store** — see `docs/design-docs/secret-store.md`. The master key is protected independently by the OS credential store (Keychain / kernel keyring), but without `--clearenv`, system secrets and other sensitive env vars still leak to workers. + +### Files Changed + +| File | Change | +|------|--------| +| `src/sandbox.rs` | Add `--clearenv` to bubblewrap wrapping; add `env_clear()` to sandbox-exec and passthrough modes; re-add safe vars + tool secrets + `passthrough_env` vars | +| `src/config.rs` | Parse `passthrough_env: Vec` in `SandboxConfig` | +| `src/tools/shell.rs` | Env sanitization for passthrough (no sandbox) mode | +| `src/tools/exec.rs` | Same env sanitization | + +--- + +## 3. Durable Binary Location + +### Problem + +On hosted instances, binaries installed via `apt-get` land on the root filesystem which is ephemeral — machine image rollouts replace it. Any ad-hoc `apt-get install git` disappears on the next deploy. The agent reinstalls on demand, but this is slow and wastes turns. + +### Existing Infrastructure + +The durable path already works: + +- `{instance_dir}/tools/bin` exists — hosted boot flow creates it (`mkdir -p "$SPACEBOT_DIR/tools/bin"`). +- `Sandbox` already prepends `tools/bin` to `PATH` for worker subprocesses (`sandbox.rs:138-149`). +- `/data` survives hosted machine image rollouts. + +### Design + +No internal registry, no install manager, no package-manager guards. The agent can install binaries however it wants — `apt-get`, `curl`, compile from source. The system's only job is to tell the agent where to put them so they survive. + +#### Worker System Prompt Instruction + +Workers get a line in their system prompt: + +``` +Persistent binary directory: /data/tools/bin (on PATH, survives restarts and rollouts) +Binaries installed via package managers (apt, brew, etc.) land on the root filesystem +which is ephemeral on hosted instances — they disappear on rollouts. To install a tool +durably, download or copy the binary into /data/tools/bin. +``` + +This is an instruction, not a guard. If the agent runs `apt install gh`, it works. The binary is ephemeral. If the agent is smart it downloads to `tools/bin` instead. If it's not, the binary disappears and it reinstalls next time. Not our problem to gatekeep. + +#### Dashboard Observability (Optional) + +A lightweight API endpoint that lists the contents of `tools/bin`: + +**`GET /api/tools`** + +```json +{ + "tools_bin": "/data/tools/bin", + "binaries": [ + { "name": "git", "size": 3456789, "modified": "2026-02-15T10:30:00Z" }, + { "name": "gh", "size": 1234567, "modified": "2026-02-20T14:15:00Z" } + ] +} +``` + +Just a directory listing — no state machine, no install locks, no checksums. The dashboard can render this as a "Tools" panel showing what's installed. Purely observational. + +### Files Changed + +| File | Change | +|------|--------| +| `prompts/worker.md` | Add durable binary location instruction | +| `src/api/tools.rs` | Optional: `GET /api/tools` directory listing endpoint | + +--- + +## 4. OpenCode Auto-Allow (Independent Bug) + +### Problem + +OpenCode workers auto-allow all permission prompts (`opencode/worker.rs:429-432`). When OpenCode asks permission to run a bash command, the worker replies `PermissionReply::Once` unconditionally. OpenCode worker output is also not scanned by SpacebotHook, so leak detection doesn't apply. + +This is an independent bug regardless of the other sections — OpenCode workers operate with no policy checks at all. + +### Design + +The auto-allow behavior is intentional for now (OpenCode needs to run commands to be useful as a worker backend). The fix is to wire OpenCode worker output through **both** protection layers that cover builtin workers: + +1. **Output scrubbing (exact match)** — `StreamScrubber` from `src/secrets/scrub.rs` (see `secret-store.md`, Output Scrubbing). Replaces tool secret values with `[REDACTED:]` in SSE events before they're forwarded. Uses the rolling buffer strategy to handle secrets split across SSE chunks. This runs first — proactive redaction. + +2. **Leak detection (regex)** — shared regex patterns from `SpacebotHook` (see `src/hooks/spacebot.rs`). Scans for known API key formats (`sk-ant-*`, `ghp_*`, etc.) in the scrubbed output. If a match is found after scrubbing, it's a leak of a secret not in the store — kill the agent. This runs second — reactive safety net. + +The sequencing matters: scrubbing first means stored tool secrets are redacted before leak detection runs, so leak detection only fires on unknown/unstored secrets. Without this order, leak detection would fire on every tool secret value (which is expected in worker output) and kill the agent unnecessarily. + +Longer term, OpenCode's permission model could be integrated with the sandbox — permissions for bash commands routed through the same `wrap()` path. But that requires understanding OpenCode's permission protocol better (see Open Questions). + +### Files Changed + +| File | Change | +|------|--------| +| `src/opencode/worker.rs` | Wire SSE output through `StreamScrubber` (exact-match redaction) then leak detection (regex) before forwarding | +| `src/secrets/scrub.rs` | `StreamScrubber` — rolling buffer scrubber for chunked output (shared with other streaming paths) | +| `src/hooks/spacebot.rs` | Extract leak detection regex into a shared function callable from OpenCode worker | + +--- + +## Tool Protection Audit + +### File Tool Workspace Guard + +Workers get `ShellTool`, `FileTool`, and `ExecTool` in the same toolbox. The file tool's `resolve_path()` workspace guard (`file.rs:26-75`) is security theater when sandbox is off: + +- Worker wants to read `/data/config.toml` +- `FileTool.resolve_path()` rejects it — "outside workspace" +- Worker uses `ShellTool` with `cat /data/config.toml` — works fine, no sandbox enforcement +- Or `ExecTool` with `cat` — same thing + +The file tool's check only prevents the LLM from using that specific tool for out-of-workspace access. It doesn't prevent anything because shell and exec are right there in the same toolbox with no equivalent restriction when sandbox is off. + +When sandbox **is** on, the file tool's check is also redundant in the other direction — bwrap/sandbox-exec already makes everything outside the workspace read-only at the kernel level. The file tool runs in-process (not a subprocess), so the sandbox doesn't wrap it, but the workspace guard duplicates what the sandbox already enforces for writes. For reads, the sandbox allows reading everywhere (read-only mounts), and the file tool is actually **more restrictive** than the sandbox by blocking reads outside workspace too. + +The file tool workspace guard has exactly one scenario where it provides unique value: **sandbox is on, and you want to prevent the LLM from reading files outside the workspace via the file tool** (since the sandbox allows reads everywhere). That's a defense-in-depth argument, not a security boundary. It's worth keeping for that reason, but it should not be confused with actual containment. + +### Protection Matrix + +Current state of protection across all tool paths, with sandbox disabled: + +| Tool | Workspace Guard | Sandbox Enforcement | Env Inherited | Leak Detection | Net Protection | +|------|----------------|--------------------|----|-----|----| +| `file` (read/write/list) | Yes — `resolve_path()` blocks outside workspace | No (in-process, not a subprocess) | N/A | Yes (tool output scanned) | Workspace guard only — bypassable via shell/exec in the same toolbox | +| `shell` | Working dir validation only | Sandbox wraps subprocess — but disabled | Full parent env | Yes (args + output scanned) | Leak detection only | +| `exec` | Working dir validation only | Sandbox wraps subprocess — but disabled | Full parent env | Yes (args + output scanned) | Leak detection + dangerous env var blocklist | +| `send_file` | **None** — any absolute path | No (in-process read) | N/A | Yes (output scanned) | Leak detection only | +| `browser` | N/A | N/A | N/A | Yes (output scanned) | SSRF protection (blocks metadata endpoints, private IPs) | +| OpenCode workers | Workspace-scoped by OpenCode | Not sandboxed | Full parent env via OpenCode subprocess | No (OpenCode output not scanned by SpacebotHook) | Auto-allow on all permissions | + +### Key Observations + +- The file tool's workspace guard is the only tool-level path restriction, but it's trivially bypassed via shell/exec which are in the same toolbox. It gives a false sense of containment. +- With sandbox off, the only real protection across all tools is leak detection (reactive, pattern-based, kills the agent after the fact). +- `send_file` has no workspace validation at all — can exfiltrate any readable file as a message attachment. This is an independent bug regardless of sandbox state. +- OpenCode workers bypass both sandbox and leak detection. They inherit the full environment and auto-allow all permission prompts. +- The file tool guard's only real value is as read-containment when sandbox is on (preventing LLM from reading sensitive files outside workspace via the file tool specifically, since bwrap mounts everything read-only but still readable). + +--- + +## Phase Plan + +### Phase 1: Dynamic Sandbox Mode (Hot-Reload Fix) + +Fix the user-facing bug where toggling sandbox mode via UI doesn't take effect. Changes to `config.rs`, `sandbox.rs`, `main.rs`, `api/agents.rs`. + +1. Wrap `RuntimeConfig.sandbox` in `Arc`. +2. Add `self.sandbox.store()` to `reload_config()`. +3. Refactor `Sandbox` to read mode from `Arc>`. +4. Update both `Sandbox::new()` call sites. +5. Verify: change sandbox mode via API, confirm `GET /agents/config` returns the new value, confirm `wrap()` uses the new mode. + +### Phase 2: Environment Sanitization + +Prevent secret leakage through environment variable inheritance. Secret-store-aware — workers get tool secrets (CLI credentials) but never system secrets (LLM keys, messaging tokens) or internal vars. + +1. Add `--clearenv` to bubblewrap wrapping, re-add only safe vars + tool secrets from the secret store. +2. Add `env_clear()` to sandbox-exec and passthrough wrapping modes with same allowlist. +3. Add `env_clear()` to shell/exec tools for the no-sandbox case. +4. Verify: worker running `printenv` shows PATH/HOME/LANG + tool secrets (e.g., `GH_TOKEN`), but NOT `ANTHROPIC_API_KEY` or other system/internal vars. (The master key is never in the environment — it lives in the OS credential store.) + +### Phase 3: Durable Binary Instruction + +Add the persistent binary location instruction to worker prompts and optional dashboard observability. + +1. Add tools/bin instruction to worker system prompt. +2. Optionally add `GET /api/tools` directory listing endpoint. +3. Verify: worker prompt includes the durable path; dashboard shows installed tools. + +### Phase 4: OpenCode Output Protection + +Wire OpenCode worker output through both protection layers (output scrubbing + leak detection) that cover builtin workers. + +1. Wire SSE output through `StreamScrubber` (exact-match redaction of tool secret values, rolling buffer for split secrets). Runs first — proactive. +2. Extract leak detection regex from SpacebotHook into a shared function. +3. Scan scrubbed SSE output through leak detection. Runs second — reactive safety net for secrets not in the store. +4. Verify: a stored tool secret in OpenCode output is redacted to `[REDACTED:]`; an unknown secret pattern triggers the same kill behavior as builtin workers. + +### Phase 5: send_file Workspace Validation + +Fix the independent bug where `send_file` can exfiltrate any readable file. + +1. Add workspace validation to `send_file` matching the file tool's `resolve_path()` pattern. +2. Verify: `send_file` with a path outside workspace returns an error. + +--- + +## Open Questions + +1. **OpenCode permission protocol.** Can OpenCode's permission model be integrated with the sandbox? Would need to route bash permission requests through `wrap()`. Requires understanding the OpenCode permission protocol internals. +2. **Dynamic writable_paths.** The hot-reload fix reads `writable_paths` from the ArcSwap on every `wrap()` call, canonicalizing each time. If an agent has many writable paths and spawns commands at high frequency, this could be optimized with a change-detection cache. Likely not a concern in practice. +3. **Tool secret injection interface.** How does `wrap()` get tool secrets? Options: (a) `Sandbox` holds an `Arc` and calls `tool_env_vars()` on each `wrap()`, (b) tool secrets cached in an `Arc>>` updated when secrets change. Option (b) avoids decryption on every subprocess spawn. Tool secrets change rarely (only via dashboard), so the cache is almost always warm. +4. **Worker keyring isolation.** The `pre_exec` hook that gives workers a fresh session keyring (see `secret-store.md`) should be wired into `wrap()` alongside env sanitization. Both run regardless of sandbox state — `--clearenv` strips env vars, `keyctl(JOIN_SESSION_KEYRING)` strips keyring access. Need to verify this works correctly with bubblewrap's `--unshare-pid` (the keyring is per-session, not per-PID-namespace, so both should be independent). + +--- + +## Future: True Sandboxing (VM Isolation via stereOS) + +Everything above operates at the namespace level — bubblewrap restricts mounts, `--clearenv` strips the environment, policy guards block specific commands. These are necessary fixes but they share a fundamental limitation: the agent and the host share a kernel. A compromised or misconfigured bubblewrap invocation can be escaped. `/proc` attacks, kernel exploits, and symlink races all exist within the same kernel boundary. + +stereOS (see `docs/design-docs/stereos-integration.md`) offers a stronger primitive: **run worker processes inside a purpose-built VM with a separate kernel**. This section captures how that maps to the sandbox architecture as a future upgrade path. + +### Per-Agent VM, Not Per-Worker + +The right granularity is one VM per agent, not one per worker: + +- **Startup cost.** stereOS boots in ~2-3 seconds. Fine once at agent boot; unacceptable per fire-and-forget worker. Workers spawn constantly, agents don't. +- **Shared workspace.** All workers for an agent already share the same workspace and data directory. One VM matches the existing `Arc` isolation boundary. +- **Resource overhead.** One VM per agent (~128-256MB RAM) is manageable. One per worker would balloon memory with concurrent workers. + +The VM boots when the agent starts and stays up for the agent's lifetime. Workers spawn and die inside it. This directly parallels how the current `Sandbox` struct is constructed once at agent startup and shared via `Arc` across all workers. + +### What Changes + +Worker tool execution (shell, file, exec) currently calls `sandbox.wrap()` which prepends bubblewrap arguments to a `Command`. With VM isolation, these tools would instead dispatch commands over a vsock RPC layer to the agent's VM: + +``` +Current: ShellTool → sandbox.wrap() → bwrap ... -- sh -c "command" (same kernel) +Future: ShellTool → vm_rpc.exec() → agentd → sh -c "command" (guest kernel) +``` + +The `Sandbox` trait boundary stays the same — `wrap()` produces a `Command`. The VM backend would produce a `Command` that speaks vsock instead of forking a local subprocess. Tools don't need to know which backend they're using. + +stereOS's `agentd` daemon already handles session management inside the VM. Worker commands would be dispatched as agentd sessions, with stdout/stderr streamed back over vsock. + +### Security Model Upgrade + +stereOS adds layers that bubblewrap cannot provide: + +| Layer | bubblewrap (current) | stereOS VM | +|-------|---------------------|------------| +| **Kernel isolation** | Shared kernel (namespace-level) | Separate kernel (VM-level) | +| **PATH restriction** | Sandbox prepends `tools/bin` | Restricted shell with curated PATH, Nix tooling excluded | +| **Privilege escalation** | Relies on namespace user mapping | Explicit sudo denial (`agent ALL=(ALL:ALL) !ALL`), no wheel group | +| **Kernel hardening** | Host kernel settings apply | ptrace blocked, kernel pointers hidden, dmesg restricted, core dumps disabled | +| **Secret injection** | Env vars (cleared by `--clearenv`) | Written to tmpfs at `/run/stereos/secrets/` with root-only permissions (0700), never on disk | +| **User isolation** | UID mapping in namespace | Immutable users, no passwords, SSH keys injected ephemerally over vsock | + +The secret injection model is particularly relevant. Today, the secret store design (see `docs/design-docs/secret-store.md`) protects the master key via the OS credential store (Keychain / kernel keyring) — workers can't access it regardless of sandbox state. With stereOS, an even stronger model is possible: the master key stays on the host entirely, never entering the VM. Secrets are injected individually into the guest's tmpfs by `stereosd`. The agent process inside the VM reads from `/run/stereos/secrets/`. The OS credential store and VM isolation are complementary — the credential store protects on the host, the VM boundary protects in the guest. + +### Network Isolation + +bubblewrap's `--unshare-net` exists but breaks most useful worker tasks (git clone, API calls, web browsing). It's all-or-nothing. + +VM-level networking is controllable with more granularity. The host can configure the VM's virtual NIC to allow outbound connections to specific hosts/ports while blocking everything else. This enables scenarios like "workers can reach github.com and the OpenAI API but nothing else" — impossible with bubblewrap without a userspace proxy. + +### Blockers + +This is not ready to implement. Key gaps from the stereOS integration research: + +1. **Fly/Firecracker format mismatch.** Fly Machines use Firecracker, which expects ext4 rootfs + kernel binary. stereOS produces raw EFI, QCOW2, and kernel artifacts (bzImage + initrd). Firecracker doesn't use initrd the same way. Either stereOS needs a `formats/firecracker.nix` output, or we run QEMU/KVM on Fly (non-standard). + +2. **Architecture.** stereOS is aarch64-linux only. Fly Machines are predominantly x86_64. Cross-compilation in Nix is straightforward but untested for stereOS. + +3. **Control plane protocol.** `stereosd` speaks a custom vsock protocol. `spacebot-platform` would need a Rust client, or stereOS would need an HTTP API layer. The protocol isn't documented publicly yet. + +4. **Workspace persistence.** stereOS VMs are ephemeral by design. Spacebot needs persistent storage (SQLite, LanceDB, workspace files). Requires virtio-fs mounts to persistent volumes, which stereOS supports but the Fly integration path would need to map to Fly volumes. + +### Relationship to Current Work + +The phases above (1-5) are prerequisites, not alternatives: + +- **Phases 1-2** (hot-reload fix, env sanitization) fix correctness bugs that matter regardless of backend. Even with VM isolation, the host process still needs `--clearenv` for any non-VM code paths (MCP processes, in-process tools). +- **Phase 3** (durable binary instruction) applies inside the VM too — the VM image would include `tools/bin` on the persistent volume mount. +- **Phases 4-5** (OpenCode leak detection, send_file fix) are bug fixes that apply regardless of sandbox backend. + +bubblewrap remains the default sandbox backend for all deployments. VM isolation would be an opt-in upgrade for the hosted platform where multi-tenant security justifies the resource overhead. Self-hosted users who want maximum isolation could run a `spacebot-mixtape` directly (NixOS image, no Docker) as an alternative deployment path. + diff --git a/docs/design-docs/sandbox.md b/docs/design-docs/sandbox.md index 62333fb19..a65c34cc4 100644 --- a/docs/design-docs/sandbox.md +++ b/docs/design-docs/sandbox.md @@ -124,8 +124,6 @@ mode = "enabled" writable_paths = ["/home/user/projects/myapp"] ``` -When `SPACEBOT_DEPLOYMENT=hosted`, the platform boot script forces `mode = "enabled"` regardless of user config. - ### Types ```rust diff --git a/docs/design-docs/secret-store.md b/docs/design-docs/secret-store.md new file mode 100644 index 000000000..60766366d --- /dev/null +++ b/docs/design-docs/secret-store.md @@ -0,0 +1,853 @@ +# Secret Store + +Credential storage with two secret categories: system secrets (internal, never exposed) and tool secrets (passed to worker subprocesses as env vars). Works out of the box without encryption — the master key is an optional hardening layer that adds encryption at rest. + +**Hard dependency:** Environment sanitization (sandbox-hardening.md, Phase 2) must ship before or alongside this. Without `--clearenv` in sandbox wrapping, system secrets and other env vars leak to workers. + +## Current State + +All secrets currently live in config.toml as plaintext: + +**config.toml** — the vast majority of users (all hosted, most self-hosted) set up API keys through the dashboard's provider UI. The dashboard sends the key to `PUT /api/providers`, which writes the literal value directly into config.toml (`anthropic_key = "sk-ant-abc123..."`). The `env:` prefix (`anthropic_key = "env:ANTHROPIC_API_KEY"`) exists as a mechanism but is only used in the initial boot script template and by a small number of self-hosted users who configure env vars manually. In practice, nearly every instance has plaintext API keys in config.toml on the persistent volume. + +This file is accessible via `GET /api/config/raw` in the dashboard and via `cat /data/config.toml` through the shell tool when sandbox is off. Users have leaked keys by screensharing their config page. + +**Environment variables** on a live hosted instance are all non-sensitive infrastructure vars: + +``` +# Fly metadata (non-sensitive) +FLY_APP_NAME=sb-0759a0a6 +FLY_REGION=iad +FLY_VM_MEMORY_MB=8192 +FLY_IMAGE_REF=registry.fly.io/spacebot-image:v0.2.1 + +# Spacebot config (non-sensitive) +SPACEBOT_DEPLOYMENT=hosted +SPACEBOT_DIR=/data + +# Standard process vars +HOME=/root +PATH=/data/tools/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +PWD=/data/agents/spacedrive-discord/workspace + +# Browser config +CHROME_FLAGS=--no-sandbox --disable-dev-shm-usage --disable-gpu +``` + +API keys are NOT in the environment today — they're in config.toml. The secret store keeps it that way for system secrets and only exposes tool secrets as env vars. + +**The existing secret store** (`src/secrets/store.rs`) implements AES-256-GCM encrypted key-value storage on redb with a `DecryptedSecret` wrapper that redacts in Debug/Display. It exists, is tested, but has zero callers in production. + +## Problems + +1. **Config is toxic to display.** The dashboard shows config.toml which contains literal API keys for nearly every user. Users have leaked keys by opening their config in screenshares or screenshots. + +2. **Workers can read all secrets.** The config file is on disk at a known path (`/data/config.toml`). With sandbox off, `cat /data/config.toml` via the shell tool dumps every key. With sandbox on, the file is read-only but still readable. + +3. **Agents can't safely inspect their own config.** The file tool blocks `/data/config.toml` (outside workspace), but workers have shell/exec which bypass that trivially. If keys were not in the config, agents could freely read it — useful for self-diagnosis ("what model am I configured to use?", "which messaging adapters are enabled?"). + +4. **No separation between internal and external secrets.** LLM API keys (needed only by the Rust process internally) and tool credentials (needed by CLI tools workers invoke) are stored and handled identically. There's no reason a worker subprocess should ever see `ANTHROPIC_API_KEY`. + +5. **Prompt injection risk.** A malicious message in a Discord channel could attempt to convince the agent to read and output the config file. The leak detection hook catches known API key patterns, but if the key format isn't in the pattern list, it goes through. + +## Design + +### Two Secret Categories + +The category controls **subprocess exposure**, not internal access. All secrets in the store are readable by Rust code via `SecretsStore::get()` regardless of category. The category answers one question: **should this value be injected as an env var into worker subprocesses?** + +**System secrets** — not exposed to subprocesses. Rust code reads them from the store for internal use (LLM clients, messaging adapters, webhook integrations). Workers never see these as env vars. + +Examples: + +- `ANTHROPIC_API_KEY` +- `OPENAI_API_KEY` +- `DISCORD_BOT_TOKEN` +- `TELEGRAM_BOT_TOKEN` +- Slack tokens, webhook signing secrets + +**Tool secrets** — exposed to subprocesses as env vars. Rust code can also read them from the store if needed (e.g., a `GITHUB_TOKEN` used by both a Rust webhook integration and by `gh` CLI in workers). `wrap()` injects these via `--setenv` (bubblewrap) or `Command::env()` (passthrough). + +Examples: + +- `GH_TOKEN` / `GITHUB_TOKEN` +- `NPM_TOKEN` +- `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` +- `DOCKER_TOKEN` +- Any user-configured credential their agent needs at the shell level + +Tool is a superset of system in terms of access — tool secrets are readable by Rust code AND visible to workers. System secrets are readable by Rust code only. If a credential is needed by both Rust internals and CLI tools, make it a tool secret. There's no "both" category because tool already implies both. + +### Category Assignment + +The dashboard's secrets panel exposes the category when adding or editing a secret. Known keys are auto-categorized: + +| Pattern | Category | Rationale | +| ----------------------------------------- | -------------- | --------------------------------------------------------- | +| `*_API_KEY` matching known LLM providers | System | Only LlmManager needs these | +| `DISCORD_BOT_TOKEN`, `TELEGRAM_BOT_TOKEN` | System | Only MessagingManager needs these | +| `SLACK_*_TOKEN`, `SLACK_SIGNING_SECRET` | System | Only Slack adapter needs these | +| `GH_TOKEN`, `GITHUB_TOKEN` | Tool | `gh` CLI expects this | +| `NPM_TOKEN` | Tool | `npm` expects this | +| `AWS_*` | Tool | AWS CLI expects these | +| Everything else | **System (default)** | Unknown credentials default to the more restrictive category — not exposed to workers | + +Users can override the auto-categorization. The default for unknown secrets is **system** (not exposed to workers) because defaulting to tool would be privilege-expanding — an internal credential accidentally categorized as tool becomes visible to every worker subprocess. It's safer to require the user to explicitly opt a secret into tool category if workers need it. The dashboard shows a clear prompt: *"Should worker processes have access to this credential? (Required for CLI tools like `gh`, `npm`, `aws`.)"* + +### Two Modes: Unencrypted and Encrypted + +The secret store operates in two modes: + +**Unencrypted (default):** Secrets are stored in plaintext in `secrets.redb`. The store is always available — no master key needed, no unlock step, no setup. All secret store features work: `secret:` config references, system/tool categorization, env sanitization, output scrubbing, worker secret name injection. The only thing missing is encryption at rest — if someone gets access to the redb file, they can read the secrets. + +This is still a significant improvement over the current state (plaintext keys in config.toml) because: +- Config.toml is safe to display (only `secret:` aliases). +- System secrets never enter subprocess environments. +- Tool secret values are scrubbed from worker output. +- The dashboard secrets panel never shows plaintext values. +- The attack surface narrows from "read a config file" to "read a specific redb file and know how to parse it." + +**Encrypted (opt-in, recommended):** User enables encryption by generating a master key. Secrets are encrypted with AES-256-GCM in redb. The master key is stored in the OS credential store (macOS Keychain, Linux kernel keyring) — never as an env var or file on disk. Even if the volume is compromised, secrets are unreadable without the key. + +Hosted instances are always encrypted — the platform generates and manages the master key automatically. Self-hosted users can enable encryption at any time via the dashboard or CLI. + +### Master Key Storage (Encrypted Mode) + +When encryption is enabled, the master key is stored in the **OS credential store** — macOS Keychain or Linux kernel keyring. It never exists as an environment variable or a file readable by workers. This is the only approach that definitively protects the key from LLM-driven workers regardless of sandbox state. + +#### Why Not an Env Var or File? + +Both are readable by workers when sandbox is off: + +- **Env var:** `std::env::remove_var()` removes the variable from libc's environ list, but on Linux `/proc/self/environ` is an immutable kernel snapshot from exec time. A worker running `cat /proc//environ | strings | grep MASTER` retrieves the key even after removal. On macOS, `ps eww ` can show environment variables. +- **File on disk:** Without sandbox, the worker runs as the same user and can `cat /data/.master_key` or any other path. File permissions don't help — same user, same access. + +With sandbox on, both can be protected (bubblewrap's `--unshare-pid` hides `/proc`, and the key file can be excluded from bind mounts). But the secret store should be secure regardless of sandbox state — sandbox protects the workspace, the OS credential store protects the master key. Independent layers. + +#### macOS: Keychain + +- Store via `SecItemAdd` / retrieve via `SecItemCopyMatching` using the Security framework. +- Access is controlled by the calling binary's code signature and ACL. A `bash` subprocess spawned by ShellTool is a different binary — Keychain will not grant access without explicit user authorization. +- Even `security find-generic-password` from a worker fails because the Keychain item's access list only includes the Spacebot binary. +- The `security-framework` crate provides safe Rust bindings. + +**Keychain item:** +``` +Service: "sh.spacebot.master-key" +Account: "" (or "default" for self-hosted single-instance) +Data: <32 random bytes> +Access: Spacebot binary only (kSecAttrAccessibleAfterFirstUnlock) +``` + +#### Linux: Kernel Keyring (`keyctl`) + +- Store via `add_key("user", "spacebot_master", key_bytes, session_keyring)` — the key lives in kernel memory, not on any filesystem. +- Scoped to a **session keyring**. Spacebot creates a new session keyring on startup via `keyctl_join_session_keyring()`. Workers are spawned with a fresh empty session keyring via `pre_exec` — they cannot access the parent's keyring. +- No file to `cat`, no env var to read, no `/proc` exposure. The key is only accessible via the `keyctl` syscall with the correct keyring ID, which workers don't have. +- Works without sandbox, without root. The kernel enforces access control at the syscall level. + +**Worker isolation (pre_exec):** +```rust +// Before exec'ing the worker subprocess: +unsafe { + command.pre_exec(|| { + // Give the child a new empty session keyring. + // It cannot access the parent's session keyring. + // CRITICAL: if this fails, the child inherits the parent's keyring + // and can access the master key. Fail hard — do not spawn the worker. + let result = libc::syscall( + libc::SYS_keyctl, + 0x01, /* KEYCTL_JOIN_SESSION_KEYRING */ + std::ptr::null::(), + ); + if result < 0 { + return Err(std::io::Error::last_os_error()); + } + Ok(()) + }); +} +``` + +If `KEYCTL_JOIN_SESSION_KEYRING` fails (returns -1), `pre_exec` returns `Err`, which causes `Command::spawn()` to fail. The worker is not started. This is the correct behavior — a worker that inherits the parent's session keyring could access the master key via `keyctl read`. The failure should be logged as an error with the errno for debugging (common causes: kernel compiled without `CONFIG_KEYS`, seccomp policy blocking `keyctl`). + +This is additive to `--clearenv` — env sanitization strips env vars, the session keyring swap strips keyring access. Both run regardless of sandbox state. + +#### Hosted Deployment + +The platform generates a per-instance master key on provisioning and stores it in the platform database (tied to the user's account). On instance startup, the platform writes the key to a **tmpfs file** that Spacebot reads once, then the platform deletes it. Spacebot stores the key in the Linux kernel keyring immediately and never persists it to the volume. + +Flow: +1. Platform provisions instance → generates 32-byte random key → stores in platform DB (`instances.master_key`, encrypted at rest). +2. Platform starts Fly machine → writes key to `/run/spacebot/master_key` (tmpfs, 0600, root-only). +3. Spacebot startup → reads `/run/spacebot/master_key` → stores in kernel keyring → deletes the tmpfs file. +4. Key is now only in kernel memory. Volume compromise doesn't expose it. `/run` is tmpfs, wiped on restart. +5. On next restart, the platform re-injects the key via the same tmpfs mechanism. + +Properties: +- The key persists across restarts and rollouts — the platform always re-injects it from its database. +- The key is tied to the user's platform account, not to the volume. If the volume is compromised without platform access, the `secrets.redb` file is useless. +- The key is platform-managed. The user never sees the raw master key — the platform handles injection and rotation. The user can trigger rotation via the dashboard ("Rotate Encryption Key"), which calls the platform API to generate a new key, re-encrypt, and update its database. No key is displayed to the user. +- The platform database becomes a store of master keys, but it already stores auth tokens and Stripe credentials — same protection requirements. + +#### Self-Hosted Deployment + +The secret store is **always enabled** on self-hosted instances — it works in unencrypted mode out of the box. Users can opt into encryption through the dashboard: + +1. User navigates to Settings → Secrets. The secrets panel is fully functional (add, edit, delete secrets). A banner shows: *"Secrets are stored without encryption. Enable encryption for protection against volume compromise."* +2. User clicks "Enable Encryption." +3. Spacebot generates a 32-byte random master key, encrypts all existing secrets in place. +4. The key is stored in the OS credential store (Keychain on macOS, kernel keyring on Linux). +5. The dashboard displays the key once: *"Save this master key somewhere safe. You'll need it to unlock the secret manager after a reboot (Linux) or if the Keychain is reset (macOS)."* +6. The key is **not written to disk**. No `.master_key` file, no env var, no config entry. The only durable copies are the OS credential store and whatever the user saved externally. + +**On macOS**, the Keychain persists across restarts. The encrypted store unlocks automatically on every boot. The user's saved copy is a disaster-recovery backup only. + +**On Linux**, the kernel keyring is cleared on reboot. After a reboot, the encrypted store starts in a **locked** state. The user unlocks it via the dashboard or CLI by providing the master key (see "Dashboard & CLI Secret Management" below). This is a deliberate trade-off: + +- **No key file on disk** means no file for workers to `cat` — the master key is protected regardless of sandbox state. This was the entire motivation for the OS credential store design. +- **Unlock on reboot** is a one-time action per boot. For a headless bot that reboots rarely (planned maintenance, kernel updates), this is an acceptable cost. +- **All secrets are unavailable while locked.** `POST /api/secrets/encrypt` does an in-place encryption — plaintext values are wiped and replaced with encrypted versions. There is no parallel plaintext copy. When locked, `secret:` references fail to resolve, LLM providers and messaging adapters start without credentials (degraded mode). The bot starts and the control API is available (so the unlock command has something to talk to), but it cannot process messages or make LLM calls until unlocked. +- **Secrets configured via `env:` or literal values in config.toml still work while locked** — they don't go through the secret store. If a user keeps some credentials in `env:` config references as a fallback for critical services (e.g., the Discord bot token to stay connected and receive the unlock command), those work regardless of store state. This is a valid pattern for Linux self-hosters who want encryption but also want the bot to stay reachable after reboot. + +For users who don't want the unlock-after-reboot requirement, the unencrypted store works indefinitely. All features except encryption at rest are available. + +**Startup flow (all deployments):** + +1. Open `SecretsStore` (redb). The store always initializes — it works in unencrypted mode by default. +2. Check if the store contains encrypted secrets (encryption header in redb metadata). +3. If **unencrypted** (or no secrets yet): store is immediately available. Read all secrets. Skip to step 7. +4. If **encrypted**: check OS credential store for master key (Keychain / kernel keyring). +5. If master key found (or injected via tmpfs on hosted → load into OS credential store → delete tmpfs file): derive AES-256-GCM cipher key via Argon2id (~100ms), decrypt all secrets. Store is **unlocked**. +6. If master key not found: store enters **locked** state. Encrypted secrets are unavailable. **The bot still starts** — the control API (port 19898) and embedded dashboard come up so the user can issue the unlock command. LLM providers and messaging adapters that depend on `secret:` references start in degraded mode (no credentials). Components configured via `env:` or literal config values still work. On unlock (via dashboard or CLI): key is loaded into OS credential store → derive cipher → decrypt → re-initialize dependent components. +7. **System secrets:** passed directly to Rust components (`LlmManager`, `MessagingManager`, etc.). Never set as env vars. +8. **Tool secrets:** held in a `HashMap` for `Sandbox::wrap()` injection via `--setenv`. + +#### Key Derivation + +Argon2id rather than the current SHA-256 in `build_cipher()`. For hosted instances where the platform generates a random high-entropy key, SHA-256 would be fine — but self-hosted users may use a passphrase (future: dashboard "set your own key" option), and SHA-256 of a passphrase is trivially brutable. Argon2id handles both cases correctly (memory-hard, resistant to GPU/ASIC attacks) and the cost is a one-time ~100ms at startup. No reason to ship the weak path and upgrade later. + +#### Upgrade Path + +1. **New version ships.** The secret store is enabled by default in unencrypted mode. Auto-migration runs on first boot: literal keys in config.toml → secret store (unencrypted), config.toml rewritten with `secret:` references. All security features work immediately (env sanitization, system/tool separation, output scrubbing, safe config display). +2. **Hosted users:** The platform also generates master keys for all instances and injects them via tmpfs. Migration encrypts secrets automatically. Fully encrypted from day one, no user action required. +3. **Self-hosted users:** Migration to the unencrypted store is automatic. The dashboard shows a banner encouraging encryption: *"Secrets are stored without encryption. Enable encryption for protection against volume compromise."* Encryption is opt-in — user clicks "Enable Encryption" when ready. + +On startup, Spacebot scans the environment for variables matching known secret patterns (anything with `TOKEN`, `KEY`, `SECRET`, `PASSWORD` in the name). If found and `passthrough_env` doesn't list them, a prominent warning is logged: "Detected secrets in environment variables. Consider moving them to the secret store." The warning is informational — nothing breaks, nothing is stripped. + +### Config Resolution Prefixes + +Config values support three resolution modes via prefix: + +```toml +# Literal — plaintext value inline (current default, what migration replaces) +anthropic_key = "sk-ant-abc123..." + +# env: — read from system environment variable at resolve time +anthropic_key = "env:ANTHROPIC_API_KEY" + +# secret: — read from secret store (held in memory) +anthropic_key = "secret:ANTHROPIC_API_KEY" +``` + +The `secret:` prefix is a resolution directive: "this value lives in the `SecretsStore`, look it up by this name, return the value." The config doesn't know or care whether the secret is categorized as system or tool, or whether the store is encrypted — it just gets the resolved string. The category is metadata on the secret in the store and only matters at `wrap()` time when deciding what to inject into worker subprocesses. + +**Secret names use UPPER_SNAKE_CASE matching env var convention.** The secret name in the store IS the env var name — `GH_TOKEN`, not `gh_token`. For tool secrets, `wrap()` iterates the store and does `--setenv {name} {value}` with no translation. For system secrets, the name is just an identifier (they're never env vars), but using the same convention keeps everything consistent. Skills that say "set `GH_TOKEN`" map directly to a secret named `GH_TOKEN` in the dashboard. + +### Config Key Migration + +All provider keys and sensitive tokens move from config.toml to the secret store. Config.toml changes from: + +```toml +[llm] +anthropic_key = "sk-ant-abc123..." + +[messaging.discord] +token = "env:DISCORD_BOT_TOKEN" +``` + +To: + +```toml +[llm] +anthropic_key = "secret:ANTHROPIC_API_KEY" + +[messaging.discord] +token = "secret:DISCORD_BOT_TOKEN" +``` + +The `resolve_env_value()` function (`config.rs:2974`) is extended to handle the `secret:` prefix: + +```rust +fn resolve_secret_or_env(value: &str, secrets: &SecretsStore) -> Option { + if let Some(alias) = value.strip_prefix("secret:") { + secrets.get(alias).ok().map(|s| s.expose().to_string()) + } else if let Some(var_name) = value.strip_prefix("env:") { + std::env::var(var_name).ok() + } else { + Some(value.to_string()) // literal + } +} +``` + +The resolved values are consumed by Rust code (provider constructors, adapter init). They are never set as env vars. The `secret:` prefix is the config-level reference; the category (system vs tool) determines runtime behavior. + +**Migration path:** + +1. On first boot with the new version, if config.toml contains literal key values (not `env:` or `secret:` prefixed), auto-migrate: store each literal value in the secret store under a deterministic UPPER_SNAKE_CASE name (e.g., `anthropic_key` → `secret:ANTHROPIC_API_KEY`), with auto-detected category. Encryption is not required — migration works in unencrypted mode. +2. Rewrite config.toml in place to replace literal values with `secret:` references. +3. Log every migration step. If migration fails for any key, leave the original value in config.toml and warn. +4. For `env:` prefixed values, leave them as-is. They're already not storing the secret in the config. Users who want to migrate `env:` values to the secret store can do so explicitly via the dashboard. +5. The `env:` prefix continues to work for users who prefer env-var-based key management. +6. **Hosted:** Migration runs automatically on first boot. The platform also enables encryption (see Upgrade Path above). +7. **Self-hosted:** Migration runs automatically on first boot. The store starts in unencrypted mode. Users can enable encryption later via the dashboard. + +### Dashboard Changes + +- **Provider setup** writes `secret:` references by default. The "API Key" field in the provider UI is a password input that sends the value to the API, which stores it in the secret store (as a system secret) and writes `secret:provider_name` to config.toml. +- **Raw config view** (`GET /api/config/raw`) is safe to display since config.toml only contains aliases. +- **Secrets panel** — list all secrets with name, category (system/tool), and masked value. Add/remove/rotate. Category is editable. Never displays plaintext values (shows masked `***` with a copy button that copies from a short-lived in-memory decryption). +- **Secret store status** — indicator showing store state: `unencrypted` (working, encryption available), `unlocked` (encrypted and operational), or `locked` (encrypted, needs master key). For hosted instances, always `unlocked` (platform-managed). See "Dashboard & CLI Secret Management" for full UX details. + +### Env Sanitization Integration + +The sandbox `wrap()` function (see sandbox-hardening.md, Section 2) handles the env var injection: + +1. `--clearenv` strips everything from the subprocess. +2. Re-add safe vars: `PATH` (with tools/bin), `HOME`, `USER`, `LANG`, `TERM`. +3. Re-add **tool secrets only** — iterate the secret store's tool category, `--setenv` each into the subprocess. +4. System secrets are **never** injected. The master key is never in the process environment (it lives in the OS credential store), so there's nothing to strip — but even if it were, `--clearenv` would exclude it. + +The `Sandbox` struct needs access to the tool secrets. Options: + +- `Sandbox` holds an `Arc>>` of tool env vars, updated when secrets change. +- The `SecretsStore` exposes a `tool_env_vars() -> HashMap` method, and `Sandbox` holds an `Arc`. + +Either way, `wrap()` reads the current tool secrets on each call and injects them. This is cheap — the set changes rarely (only when the user adds/removes secrets via the dashboard). + +### Worker Secret Awareness + +Workers get the **names** of available tool secrets injected into their system prompt — never the values. This tells the worker what credentials are available without it having to run `printenv` to discover them: + +``` +Available tool secrets (set as environment variables in your shell): + GH_TOKEN, NPM_TOKEN, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY + +Commands that use these credentials will work automatically (e.g., gh commands +use GH_TOKEN). Do not echo, print, or log secret values. +``` + +This is assembled at worker construction time from the tool secret names in the store. The list updates when secrets change (workers spawned after a secret is added/removed get the updated list). + +**Why this matters:** +- Without it, the worker has to guess or run `printenv` to find out what's available. That wastes a turn and the `printenv` output contains the actual values in the worker's own context (even though the scrubber would catch them before they reach the channel). +- With it, the worker knows `GH_TOKEN` is available and can use `gh` commands immediately. No discovery step, no secret values in context. +- Skills that say "requires `GH_TOKEN`" align directly — the worker sees the name in its prompt and knows the credential is present. +- The "do not print secret values" instruction is a soft guardrail. The real guardrail is the output scrubber. But telling the LLM not to do it reduces the frequency, which means less scrubbing and less noise in logs. + +### Output Scrubbing (Tool Secret Redaction) + +Workers need tool secrets in their subprocess environment to run CLI tools. But there's no reason the secret _values_ should propagate back up to channels or branches. A worker running `gh pr create` needs `GH_TOKEN` in its env, but the channel receiving the worker's result doesn't need to see the token value if it leaks into stdout. + +**Mechanism:** Every string that flows from a worker back toward a channel or branch is checked against the current set of tool secret values (exact substring match). Any match is replaced with `[REDACTED:]`. + +``` +Worker stdout: "Authenticated as user X. Token: ghp_abc123def456..." +Scrubber: checks against all tool_env_vars() values +Match found: "ghp_abc123def456..." == tool secret "GH_TOKEN" +Redacted: "Authenticated as user X. Token: [REDACTED:GH_TOKEN]" +``` + +**Where it runs:** + +| Output path | Scrubbing point | +| ------------------------ | ------------------------------------------------------------------ | +| Worker result text | Before injection into channel/branch history | +| `set_status` tool output | Before adding to the status block (channels read this every turn) | +| OpenCode SSE events | Before forwarding to the worker event handler | +| Branch conclusions | Before injection into channel history (branches can spawn workers) | + +**Why exact match, not regex:** Leak detection (SpacebotHook) already does regex pattern matching for known key formats (`sk-ant-*`, `ghp_*`, etc.). The scrubber is complementary — it catches secrets that don't have recognizable patterns. A random 64-char string the user stored as a tool secret has no regex pattern, but the scrubber knows its exact value and catches it. The two layers work together: + +- **Leak detection (regex):** catches known formats even if the secret isn't in the store (e.g., a key the user typed inline). Reactive — kills the agent after detection. +- **Output scrubbing (exact match):** catches any stored tool secret regardless of format. Proactive — redacts before the value reaches the channel. The channel sees `[REDACTED:GH_TOKEN]` and knows the secret was used, but never sees the value. + +**Streaming safety (split-secret problem):** If a secret value is split across two adjacent SSE events or stream chunks, a per-string exact match misses it. For example, `GH_TOKEN = "ghp_abc123def456"` split as `"...ghp_abc123"` + `"def456..."` — neither chunk contains the full secret. + +Fix: the scrubber maintains a **rolling buffer** per output stream. For each stream (identified by worker ID + output path), the scrubber holds the tail of the previous chunk, sized to `max_secret_length - 1` bytes. On each new chunk, it concatenates `[tail_of_previous | new_chunk]`, scrubs the combined string, then emits everything except the new tail (which is held for the next chunk). On stream end, the held tail is flushed and scrubbed. + +```rust +struct StreamScrubber { + buffer: String, + max_secret_len: usize, // max length across all tool secret values +} + +impl StreamScrubber { + fn scrub_chunk(&mut self, chunk: &str, secrets: &HashMap) -> String { + self.buffer.push_str(chunk); + let emit_up_to = self.buffer.len().saturating_sub(self.max_secret_len - 1); + let to_emit = scrub_secrets(&self.buffer[..emit_up_to], secrets); + self.buffer = self.buffer[emit_up_to..].to_string(); + to_emit + } + + fn flush(&mut self, secrets: &HashMap) -> String { + let remaining = std::mem::take(&mut self.buffer); + scrub_secrets(&remaining, secrets) + } +} +``` + +This adds latency of `max_secret_len` bytes per chunk — typically 40-100 bytes for API keys. Negligible for worker output which is displayed progressively anyway. + +For non-streaming paths (worker result text, branch conclusions), the full string is available at once — no buffer needed, simple `scrub_secrets()` call. + +**Cost:** Comparing every worker output string against every tool secret value. With typically <20 secrets and output in the KB range, this is a substring search over a small set — negligible. The secret values are already in memory (the tool env var cache). No decryption on each check. + +**Implementation:** A `scrub_secrets(text: &str, tool_secrets: &HashMap) -> String` function that iterates the map and replaces all occurrences, plus `StreamScrubber` for chunked output paths (OpenCode SSE, streaming tool output). Both live in `src/secrets/scrub.rs`. + +### Protection Layers (Summary) + +| Layer | Requires Encryption | What It Protects Against | +| ------------------------------------------------------ | ------------------- | ------------------------------------------------------------------------------ | +| `secret:` aliases in config.toml | No | Config file exposure (screenshare, `cat`, dashboard display) | +| System/tool category separation | No | Workers seeing LLM API keys, messaging tokens, or other internal credentials | +| `DecryptedSecret` wrapper | No | Accidental logging of secret values in tracing output | +| Env sanitization (`--clearenv` + selective `--setenv`) | No | Workers only get tool secrets, never system secrets or internal vars | +| Worker secret name injection (prompt) | No | Workers know what credentials are available without running `printenv` — no secret values in LLM context | +| Output scrubbing (exact match) | No | Tool secret values propagating from worker output back to channels/branches | +| Leak detection (SpacebotHook, regex) | No | Last-resort safety net — known key format patterns in any tool output | +| Secret store encryption (AES-256-GCM) | **Yes** | Disk access to secrets.redb (stolen volume, backup leak) | +| Master key in OS credential store (Keychain / kernel keyring) | **Yes** | Worker access to the encryption key — OS enforces access control at the binary/keyring level, independent of sandbox state | +| Worker session keyring isolation (`pre_exec`) | **Yes** | Workers accessing parent's kernel keyring on Linux (additive to `--clearenv`) | + +### What This Doesn't Solve + +- **Workers can see tool secrets in their own process.** A worker running `printenv GH_TOKEN` gets the value in its subprocess. This is by design — the worker needs it to run `gh` commands. But the value is scrubbed from the worker's output before it reaches the channel. +- **Side channels within the workspace.** A worker could write a tool secret to a file, then a subsequent worker reads it. The sandbox limits where files can be written, but within the workspace it's unrestricted. Output scrubbing catches the value if it appears in any worker's output, but not if it stays in a file. +- **Encoding/obfuscation.** A worker could base64-encode a secret value before outputting it. The exact-match scrubber wouldn't catch the encoded form. Leak detection's regex patterns also wouldn't match. This is a theoretical attack by a deliberately adversarial LLM, not an accidental leak. + +The key properties: **system secrets never leave Rust memory.** Tool secrets reach worker subprocesses (necessary) but are scrubbed from all output flowing back to channels (the LLM context). A channel never sees a secret value — it sees `[REDACTED:GH_TOKEN]` at most. + +## Files Changed + +| File | Change | +| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `src/secrets/store.rs` | Add unencrypted mode; add secret category (system/tool); add `tool_env_vars()` method; add `tool_secret_names()` for prompt injection; encrypt-in-place support; Argon2id key derivation for encrypted mode | +| `src/secrets/scrub.rs` | New: `scrub_secrets()` — exact-match redaction of tool secret values in output strings | +| `src/config.rs` | Extend `resolve_env_value()` to handle `secret:` prefix; wire `SecretsStore` into config loading; migration logic for existing literal/env keys | +| `src/sandbox.rs` | `wrap()` injects tool secrets via `--setenv`; holds reference to secret store or tool env var cache | +| `src/agent/worker.rs` | Scrub worker result text through `scrub_secrets()` before injecting into channel/branch history; inject tool secret names into worker system prompt | +| `src/tools/set_status.rs` | Scrub status text through `scrub_secrets()` before updating status block | +| `src/opencode/worker.rs` | Scrub OpenCode SSE output events through `scrub_secrets()` before forwarding | +| `src/api/secrets.rs` | New: secret CRUD, status, encrypt, unlock/lock, rotate, migrate, export/import endpoints | +| `src/api/server.rs` | Add secret management routes | +| `src/secrets/keystore.rs` | New: OS credential store abstraction — `KeyStore` trait with `MacOSKeyStore` (Security framework / Keychain) and `LinuxKeyStore` (kernel keyring / keyctl) backends | +| `src/main.rs` | Initialize `SecretsStore` (unencrypted or encrypted), auto-migrate literal keys from config.toml on first boot, load master key from OS credential store if encrypted (or enter locked state), pass system secrets to provider init | +| `src/agent/worker.rs` | Add `pre_exec` hook to spawn workers with a fresh empty session keyring (Linux) | +| `spacebot-platform/api/src/fly.rs` | Generate per-instance master key on provisioning, store in platform DB, inject via tmpfs at `/run/spacebot/master_key` in `machine_config()` | +| `spacebot-platform/api/src/db.rs` | Add `master_key` column to instances table (encrypted at rest) | +| `spacebot-platform/api/src/routes.rs` | Add master key rotation endpoint for dashboard | + +## Phase Plan + +**Hard dependency on sandbox-hardening.md Phase 2 (env sanitization).** The master key is protected independently by the OS credential store, but without `--clearenv`, system secrets and other env vars still leak to worker subprocesses. + +### Phase 1: Core Secret Store (Unencrypted) + +The secret store ships and works immediately for all users without any setup. + +1. Extend `SecretsStore` to support unencrypted mode (plaintext values in redb, no cipher needed). +2. Extend `resolve_env_value()` to handle `secret:` prefix. +3. Auto-migration on first boot: detect literal keys in config.toml → store in redb → rewrite config.toml with `secret:` references. +4. System secrets: pass values directly to `LlmManager`, `MessagingManager`, etc. during init. Never set as env vars. +5. Tool secrets: expose via `tool_env_vars()` for `Sandbox::wrap()` injection. +6. Inject tool secret names (not values) into worker system prompts at construction time. +7. Secret CRUD API: `GET/PUT/DELETE /api/secrets/:name`, `GET /api/secrets/status`. +8. Dashboard secrets panel: list, add, edit, delete secrets with category assignment. + +### Phase 1.5: Encryption (Opt-In) + +Layered on top of the working unencrypted store. + +1. Implement `KeyStore` abstraction with macOS Keychain and Linux kernel keyring backends (`src/secrets/keystore.rs`). +2. `POST /api/secrets/encrypt` — generate master key, encrypt all existing secrets in place, store key in OS credential store. +3. Add `pre_exec` hook to worker spawning: children get a fresh empty session keyring (Linux). +4. Startup: detect encrypted store → load master key from OS credential store → derive cipher key via Argon2id → decrypt. If key not found → locked state. +5. Unlock/lock API: `POST /api/secrets/unlock`, `POST /api/secrets/lock`. +6. Platform: generate per-instance master key on provisioning, store in platform DB, inject via tmpfs at `/run/spacebot/master_key`. Hosted instances are always encrypted. +7. Key rotation: `POST /api/secrets/rotate`. +8. Export/import for backup and migration. + +### Phase 2: Output Scrubbing + +1. Implement `scrub_secrets()` — exact substring match against all tool secret values, replace with `[REDACTED:]`. +2. Wire into worker result path (before channel/branch history injection). +3. Wire into `set_status` (before status block update). +4. Wire into OpenCode SSE event forwarding. +5. Wire into branch conclusion path (before channel history injection). +6. Verify: worker running `echo $GH_TOKEN` produces `[REDACTED:GH_TOKEN]` in the channel's view of the result. + +### Phase 3: Hosted Encryption Rollout + +1. Platform generates master keys for all existing instances, stores in platform DB. +2. On next image rollout, keys are injected via tmpfs. Encryption is enabled automatically on first boot with the new image. +3. Verify: all hosted instances have encrypted stores; config.toml contains no plaintext keys; `GET /api/config/raw` is safe to display. + +### Phase 4: Dashboard & CLI + +1. Secrets panel: list secrets with name, category (system/tool), masked value. Add/remove/rotate. Works in both unencrypted and unlocked states. +2. Provider setup UI writes `secret:` references and stores as system secrets. +3. Secret store status indicator (`unencrypted` / `locked` / `unlocked`). +4. Encryption onboarding banner for unencrypted self-hosted stores. +5. Unlock prompt for locked stores (after Linux reboot). +6. CLI `spacebot secrets` subcommand tree. See "Dashboard & CLI Secret Management" for full specification. +7. Hosted: master key rotation via platform API. + +## Open Questions + +1. **Secret rotation and hot-reload.** When a user rotates a key (e.g., regenerates their Anthropic API key), the workflow is: update via dashboard → store encrypts new value → config references unchanged. But the LLM manager holds the old key in memory. Do we need a reload hook that re-reads system secrets from the store? Tool secrets are re-read on each `wrap()` call so they update automatically. +2. **Migration rollback.** After migrating keys from config.toml to the secret store, if the secret store becomes corrupted or the master key is lost, is there a recovery path? Should we keep a one-time encrypted backup of the pre-migration config? +3. **Platform master key storage.** The platform database will store per-instance master keys. What's the encryption/protection model for the platform database itself? Should the platform encrypt master keys at rest with its own key? +4. **Category override UX.** How prominent should the system/tool toggle be in the dashboard? Auto-categorization handles the common cases, but users need to understand the distinction to make informed overrides. +5. ~~**Self-hosted tool secrets without master key.**~~ **Resolved — see `passthrough_env` below.** +6. ~~**Linux key file exposure without sandbox.**~~ **Resolved — no key file on disk.** Linux uses the kernel keyring only. After reboot, the secret store enters a locked state and the user unlocks via dashboard or CLI. See "Dashboard & CLI Secret Management" below. +7. **Keychain ACL on unsigned dev builds.** During development, the Spacebot binary may not be code-signed. macOS Keychain ACLs based on code signature won't work for unsigned binaries — need to handle this gracefully (fall back to file-based storage in dev mode, or use a less restrictive Keychain access policy). + +### Env Passthrough for Self-Hosted + +Some self-hosted users set credentials as env vars in Docker compose or systemd rather than through the dashboard. With env sanitization (`--clearenv`), these env vars get stripped from worker subprocesses. The secret store handles this for secrets it knows about (they're injected via `--setenv`), but env vars that haven't been migrated to the store would be silently lost. That's a breaking change. + +**Fix:** A configurable passthrough list in the sandbox config: + +```toml +[agents.sandbox] +mode = "enabled" +passthrough_env = ["GH_TOKEN", "GITHUB_TOKEN", "NPM_TOKEN"] +``` + +`wrap()` builds the subprocess environment from three sources, checked in order: + +1. **Safe vars** — always passed: `PATH` (with tools/bin), `HOME`, `USER`, `LANG`, `TERM`. +2. **Tool secrets from the store** — all tool-category secrets are injected via `--setenv` (works in both unencrypted and encrypted mode). +3. **`passthrough_env` list** — for each name in the list, if the var exists in the parent process environment, pass it through to the subprocess. This is the escape hatch for self-hosted users without a master key. + +When secrets have been migrated to the store, `passthrough_env` is redundant for those vars. The config field still works (it's additive), but the dashboard can show a hint: "You have passthrough env vars configured. Consider moving these to the secret store." + +On hosted instances, `passthrough_env` is empty by default and has no effect — the platform manages all secrets via the store. + +**Why not just skip `--clearenv` when there's no master key?** Because `--clearenv` protects more than just the master key — it prevents system secrets, internal vars (`SPACEBOT_*`), and any other env vars from leaking to workers. The master key is protected by the OS credential store regardless of `--clearenv`, but env sanitization is still necessary for everything else. The passthrough list is explicit — the user declares exactly which vars they want forwarded. Everything else is stripped. + +--- + +## Dashboard & CLI Secret Management + +The secret store needs a complete management interface — onboarding, unlock/lock lifecycle, secret CRUD, and key backup/rotation. Both the embedded dashboard (SPA) and the CLI provide access to the same underlying API. + +### Secret Store States + +The store has three states, exposed via `GET /api/secrets/status`: + +| State | Meaning | Dashboard Display | +|-------|---------|-------------------| +| **`unencrypted`** | Store is active, secrets stored in plaintext in redb. No master key configured. | Full secrets panel + banner: "Enable encryption for protection against volume compromise" | +| **`locked`** | Encryption is enabled but master key is not currently in the OS credential store. Happens after Linux reboot. | Unlock prompt: "Enter your master key to unlock encrypted secrets" + limited panel (can see secret names but not add/edit/read) | +| **`unlocked`** | Encryption is enabled and master key is in the OS credential store. Secrets are decrypted and operational. | Full secrets panel | + +```json +// GET /api/secrets/status +{ + "state": "unencrypted", // "unencrypted" | "locked" | "unlocked" + "encrypted": false, // whether encryption is enabled + "secret_count": 12, // total secrets in store + "system_count": 5, // system category count + "tool_count": 7, // tool category count + "platform_managed": false // true on hosted (encryption is automatic, UI hides encryption controls) +} +``` + +How to distinguish states: the redb metadata contains an `encrypted` flag. If `encrypted == false` → `unencrypted`. If `encrypted == true` and master key is in OS credential store → `unlocked`. If `encrypted == true` and master key not found → `locked`. + +### Enabling Encryption (Self-Hosted) + +The secret store works immediately without encryption. Enabling encryption is a separate step. + +**Dashboard:** + +1. User navigates to Settings → Secrets. The secrets panel is fully functional. A banner shows: *"Secrets are stored without encryption. Enable encryption for protection against volume compromise."* +2. Clicks "Enable Encryption." +3. `POST /api/secrets/encrypt` — Spacebot generates a 32-byte random key, stores it in the OS credential store, encrypts all existing secrets in place, returns the key as a hex string. +4. Dashboard displays the key in a modal with a copy button and a warning: *"Save this key somewhere safe. On Linux, you'll need it to unlock the secret manager after a reboot. This is the only time the key will be shown."* +5. User confirms they've saved the key. Banner disappears. Store is now encrypted. + +**CLI:** + +```bash +# Enable encryption +spacebot secrets encrypt +# Output: +# Encrypting 12 secrets... +# Master key: a1b2c3d4e5f6... (64 hex chars) +# +# IMPORTANT: Save this key. You will need it to unlock the +# secret manager after a reboot. This is the only time it +# will be displayed. + +# Migration (separate from encryption — runs automatically on first boot, +# or manually if needed) +spacebot secrets migrate +# Output: +# Detected 4 plaintext keys in config.toml: +# anthropic_key → ANTHROPIC_API_KEY (system) +# openai_key → OPENAI_API_KEY (system) +# discord.token → DISCORD_BOT_TOKEN (system) +# github_token → GH_TOKEN (tool) +# Migrate? [y/N]: y +# Migrated 4 keys. config.toml updated. +``` + +### Unlock / Lock Flow (Encrypted Mode Only) + +Only applies when encryption is enabled. Unencrypted stores are always available. + +**Dashboard:** + +1. On page load, dashboard checks `GET /api/secrets/status`. +2. If `locked`: shows unlock card with a password input: *"Encrypted secrets are locked. Enter your master key to unlock."* +3. User pastes key → `POST /api/secrets/unlock` with `{ "master_key": "" }`. +4. Server validates the key (attempts to derive cipher and decrypt a known sentinel value in the store). If valid: loads into OS credential store, decrypts all secrets, re-initializes LLM providers and messaging adapters that were started without their secrets. Returns `200`. +5. If invalid: returns `401` with *"Invalid master key."* Dashboard shows error, lets user retry. +6. On success, dashboard transitions to the full secrets panel. + +**CLI:** + +```bash +# Unlock +spacebot secrets unlock +# Enter master key: ******** +# Secret manager unlocked. 12 secrets decrypted. +# Re-initialized: LlmManager (3 providers), MessagingManager (2 adapters). + +# Unlock non-interactively (for automation — key in stdin) +# NOTE: This example uses an env var for illustration. For production +# automation, pipe from a secrets manager or file, not a shell env var +# (env vars are visible in /proc and process listings — the same exposure +# the OS credential store is designed to avoid). +cat /run/secrets/spacebot_key | spacebot secrets unlock --stdin + +# Lock (clears key from OS credential store — useful for maintenance) +spacebot secrets lock +# Secret manager locked. Secrets remain encrypted on disk. +# The bot will continue running with cached credentials until restart. + +# Check status +spacebot secrets status +# State: unlocked +# Secrets: 12 (5 system, 7 tool) +``` + +**Lock behavior:** `POST /api/secrets/lock` removes the master key from the OS credential store. The derived cipher key in memory is zeroed. New secret operations fail. Already-decrypted values held by LLM providers and messaging adapters continue working until the process restarts — we don't forcefully kill active connections. On next restart, the store comes up locked. + +### Secret CRUD + +Available when the store is `unencrypted` or `unlocked`. When `locked`, read-only endpoints (`GET /api/secrets`, `GET /api/secrets/:name/info`) still work — secret names, categories, and metadata are stored as unencrypted headers in redb. Mutation endpoints (`PUT`, `DELETE`) and any operation that touches secret values return `423 Locked`. + +**List secrets:** + +``` +GET /api/secrets +``` +```json +{ + "secrets": [ + { + "name": "ANTHROPIC_API_KEY", + "category": "system", + "created_at": "2026-02-25T10:30:00Z", + "updated_at": "2026-02-25T10:30:00Z" + }, + { + "name": "GH_TOKEN", + "category": "tool", + "created_at": "2026-02-25T10:31:00Z", + "updated_at": "2026-02-27T14:00:00Z" + } + ] +} +``` + +Values are never returned in list responses. Names and categories only. + +**Add / update a secret:** + +``` +PUT /api/secrets/:name +``` +```json +{ + "value": "ghp_abc123...", + "category": "tool" +} +``` + +If the secret already exists, its value and/or category are updated. The `updated_at` timestamp is refreshed. For tool secrets, the change is immediately available to the next `wrap()` call — no restart needed. + +For system secrets, the change is stored but active components (LLM providers, adapters) continue using the old value until a config reload or restart. The response indicates this: + +```json +{ + "name": "ANTHROPIC_API_KEY", + "category": "system", + "reload_required": true, + "message": "Secret updated. Reload config or restart for the new value to take effect." +} +``` + +**Delete a secret:** + +``` +DELETE /api/secrets/:name +``` + +Removes from the store. If the secret is referenced by config.toml (`secret:NAME`), the config reference becomes a dangling pointer — `resolve_secret_or_env` returns `None` and the component logs a warning. The response warns about this: + +```json +{ + "deleted": "GH_TOKEN", + "config_references": ["agents.tools.github_token"], + "warning": "This secret is referenced in config.toml. The reference will fail to resolve." +} +``` + +**CLI equivalents:** + +```bash +# List +spacebot secrets list +# NAME CATEGORY UPDATED +# ANTHROPIC_API_KEY system 2026-02-25 10:30 +# GH_TOKEN tool 2026-02-27 14:00 + +# Add/update +spacebot secrets set GH_TOKEN --category tool +# Enter value: ******** +# Secret GH_TOKEN saved (tool). + +# Or non-interactively +echo "ghp_abc123" | spacebot secrets set GH_TOKEN --category tool --stdin + +# Delete +spacebot secrets delete GH_TOKEN +# Warning: GH_TOKEN is referenced in config.toml at agents.tools.github_token +# Delete anyway? [y/N]: y +# Deleted GH_TOKEN. + +# Show category info +spacebot secrets info GH_TOKEN +# Name: GH_TOKEN +# Category: tool +# Created: 2026-02-25 10:31 +# Updated: 2026-02-27 14:00 +# Config: agents.tools.github_token = "secret:GH_TOKEN" +``` + +### Key Rotation + +Master key rotation replaces the encryption key without changing the stored secrets: + +1. User initiates rotation via dashboard or CLI. +2. `POST /api/secrets/rotate` — Spacebot generates a new master key, re-encrypts all secrets with the new key, stores the new key in the OS credential store, returns the new key for the user to save. +3. The old key is invalidated. The user's previously saved key no longer works for unlock. + +**Dashboard:** Settings → Secrets → "Rotate Master Key" button. Confirms with a warning that the old key becomes invalid. Shows the new key in a modal. + +**CLI:** + +```bash +spacebot secrets rotate +# WARNING: This will invalidate your current master key. +# You will need to save the new key for future unlocks. +# Continue? [y/N]: y +# +# New master key: f7e8d9c0b1a2... +# Re-encrypted 12 secrets. +# +# IMPORTANT: Save this new key. Your old key no longer works. +``` + +**Hosted:** Key rotation is handled by the platform. The user clicks "Rotate Key" in the dashboard, which calls the platform API. The platform generates a new key, updates its database, re-injects on next restart. The user never sees the key. + +### Key Export / Import + +For disaster recovery and instance migration: + +```bash +# Export all secrets (encrypted with the current master key) +spacebot secrets export --output secrets-backup.enc +# Exported 12 secrets to secrets-backup.enc +# This file is encrypted with your current master key. + +# Import secrets from a backup +spacebot secrets import --input secrets-backup.enc +# Enter the master key used to create this backup: ******** +# Imported 12 secrets. 3 conflicts (existing secrets with same name): +# ANTHROPIC_API_KEY — kept existing (use --overwrite to replace) +# GH_TOKEN — kept existing +# NPM_TOKEN — kept existing + +# Import with overwrite +spacebot secrets import --input secrets-backup.enc --overwrite +``` + +**Unencrypted store warning:** If encryption is not enabled, the export file contains plaintext secrets. The CLI warns: + +``` +# WARNING: Encryption is not enabled. This export contains +# plaintext secrets. Store it securely or enable encryption +# first with: spacebot secrets encrypt +``` + +When encryption is enabled, the export file is the raw encrypted redb data plus a header with the Argon2id salt. It's useless without the master key. This covers: +- **Backup before migration** — export before upgrading, import if something goes wrong. +- **Instance migration** — export from old instance, import into new instance with the same master key. +- **Disaster recovery** — if `secrets.redb` is corrupted, import from backup. + +### API Summary + +| Endpoint | Method | Auth | Description | +|----------|--------|------|-------------| +| `/api/secrets/status` | GET | Token | Store state (`unencrypted` / `locked` / `unlocked`), secret counts | +| `/api/secrets` | GET | Token | List all secrets (name + category, no values) | +| `/api/secrets/:name` | PUT | Token | Add or update a secret | +| `/api/secrets/:name` | DELETE | Token | Delete a secret | +| `/api/secrets/:name/info` | GET | Token | Secret metadata + config references | +| `/api/secrets/migrate` | POST | Token | Auto-migrate literal keys from config.toml (runs automatically on first boot, manual trigger if needed) | +| `/api/secrets/encrypt` | POST | Token | Enable encryption: generate master key, encrypt all secrets, store key in OS credential store (only when `unencrypted`) | +| `/api/secrets/unlock` | POST | Token | Provide master key, decrypt store (only when `locked`) | +| `/api/secrets/lock` | POST | Token | Clear master key from OS credential store (only when `unlocked`) | +| `/api/secrets/rotate` | POST | Token | Rotate master key, re-encrypt all secrets (only when `unlocked`) | +| `/api/secrets/export` | POST | Token | Export backup (encrypted if encryption enabled, plaintext otherwise) | +| `/api/secrets/import` | POST | Token | Import from backup | + +Read-only endpoints (`GET /api/secrets`, `GET /api/secrets/:name/info`, `GET /api/secrets/status`) work in all states — secret names and categories are unencrypted metadata. Mutation endpoints (`PUT /api/secrets/:name`, `DELETE /api/secrets/:name`) work when `unencrypted` or `unlocked` but return `423 Locked` when locked (can't encrypt new values without the key). The encryption/unlock/lock/rotate endpoints only apply to encrypted stores. + +Authentication uses the same bearer token as the rest of the control API. On hosted instances, the dashboard proxy handles auth transparently. Self-hosted users authenticate via their configured API token. + +### CLI Subcommand Structure + +``` +spacebot secrets + status Show store state and secret counts + list List all secrets (name + category) + set Add or update a secret (interactive or --stdin) + delete Delete a secret + info Show secret metadata and config references + migrate Auto-migrate plaintext keys from config.toml + encrypt Enable encryption (generate master key, encrypt all secrets) + unlock Unlock encrypted store (interactive or --stdin) + lock Lock encrypted store (clear key from OS credential store) + rotate Rotate master key (encrypted mode only) + export Export backup + import Import from backup +``` + +All subcommands communicate with the running Spacebot instance via the control API (`localhost:19898`). They don't access the secret store directly — this ensures the same locking/unlocking semantics apply regardless of whether the user uses the dashboard or CLI. diff --git a/docs/design-docs/stereos-integration.md b/docs/design-docs/stereos-integration.md new file mode 100644 index 000000000..5512c2e87 --- /dev/null +++ b/docs/design-docs/stereos-integration.md @@ -0,0 +1,162 @@ +# stereOS Integration + +Research into [stereOS](https://github.com/papercomputeco/stereos) — a NixOS-based Linux distro purpose-built for running AI agents in hardened VMs — and how it maps to Spacebot's architecture. + +## What stereOS Is + +stereOS produces bootable machine images called **mixtapes** that bundle a minimal, security-hardened Linux system with a specific AI agent binary. It boots in under 3 seconds, runs the agent in a sandboxed user account with a restricted PATH, and is controlled by two system daemons that handle lifecycle management, secret injection, and session introspection. + +**Key properties:** + +- NixOS-based, fully declarative — the entire OS is defined in Nix modules +- Targets lightweight VMs: QEMU/KVM, Apple Virtualization.framework +- Three image formats: raw EFI, QCOW2, direct-kernel boot artifacts +- Host-guest control plane over virtio-vsock (CID 3, port 1024) with TCP fallback +- Two orchestration daemons: `stereosd` (system lifecycle) and `agentd` (agent session management) +- Currently aarch64-linux only + +**Existing mixtapes:** OpenCode, Claude Code, Gemini CLI, and a "full" variant bundling all three. + +## Security Model + +stereOS implements defense-in-depth across six layers: + +1. **Restricted shell + PATH** — agent user gets a custom login shell that sets PATH to only a curated set of approved binaries. Nix tooling is never included. All Nix-related env vars are unset. +2. **Nix daemon ACL** — only root and wheel can talk to the Nix daemon. The agent user is in `agent` group, not `wheel`. +3. **Explicit sudo denial** — `agent ALL=(ALL:ALL) !ALL` before any permissive rules. +4. **Kernel hardening** — ptrace blocked (`yama.ptrace_scope=2`), kernel pointers hidden, dmesg restricted, core dumps disabled, ICMP redirects disabled. +5. **Immutable users, no passwords** — SSH keys injected ephemerally at boot by `stereosd` over vsock. +6. **VM boundary** — the VM itself is the primary isolation mechanism. + +**Secret handling:** `stereosd` receives secrets from the host over vsock, writes them to `/run/stereos/secrets/` on tmpfs with root-only permissions (0700). Never touches disk. + +## How It Maps to Spacebot + +### 1. Hosted Platform Instance Runtime (High Value) + +`spacebot-platform` currently provisions Fly Machines for customer instances. stereOS could serve as the instance runtime: + +- Build a `spacebot-mixtape` containing the Spacebot binary +- `stereosd` handles lifecycle (start/stop/health) and secret injection (API keys, config) — replaces direct Fly Machine API calls for some operations +- Sub-3-second boot means near-instant instance provisioning +- `agentd` provides tmux session introspection — admins can attach to debug customer instances +- `mixtape.toml` manifest with SHA-256 checksums enables reproducible, verifiable deploys +- The NixOS declarative model means every instance runs an identical, auditable system + +**Gap:** Fly uses Firecracker, not QEMU. Firecracker expects its own rootfs + kernel format, not raw EFI or QCOW2. stereOS already produces kernel artifacts (bzImage + initrd + cmdline) which is close to what Firecracker needs, but the rootfs extraction would require a new image format in stereOS. This is the main blocker for hosted platform use. + +### 2. Worker Sandbox Environment (High Value, Longer Term) + +Spacebot workers execute arbitrary shell commands via `ShellTool` and `ExecTool`. Since 0.2.0, sandboxing uses bubblewrap (Linux) and `sandbox-exec` (macOS) — see `docs/design-docs/sandbox.md`. stereOS offers a stronger primitive: **run worker processes inside a purpose-built VM**. + +What this would look like: + +- Spawn one stereOS VM per agent at agent boot time +- All workers for that agent execute inside the same VM +- Worker tools (shell, file, exec) execute inside the VM via a thin RPC layer over vsock +- The VM boundary replaces bubblewrap's mount namespace — kernel-level isolation instead of namespace-level +- stereOS's restricted PATH and sudo denial apply on top of the VM boundary +- Workers can't escape because they're in a different kernel + +**Per-agent, not per-worker.** The VM boots once when the agent starts and stays up for the agent's lifetime. Workers spawn and die inside it. This is the right granularity because: + +1. **Startup latency** — 2-3 seconds per VM is fine once at agent boot, but unacceptable if every fire-and-forget worker pays that cost. Workers spawn constantly; agents don't. +2. **Shared workspace** — all workers for an agent already share the same workspace and data directory. One VM matches the existing isolation boundary. +3. **Resource overhead** — one VM per agent (~128-256MB) is manageable. One per worker would balloon memory on instances with many concurrent workers. +4. **Matches bubblewrap** — the current sandbox design creates one `Sandbox` per agent at startup and shares it via `Arc` across all workers. Same logical boundary, stronger isolation. + +**Tradeoffs vs bubblewrap:** + +| | bubblewrap (current) | stereOS VM (per-agent) | +| ---------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------- | +| **Startup latency** | ~0ms per worker | ~2-3 seconds once at agent boot, then ~0ms per worker | +| **Isolation strength** | Namespace-level (shared kernel) | VM-level (separate kernel) | +| **Resource overhead** | Minimal | ~128-256MB RAM per agent | +| **Network isolation** | Not included (bwrap --unshare-net possible but breaks most tasks) | Controllable at VM level | +| **Complexity** | Low (single bwrap call) | High (VM lifecycle, vsock RPC, image distribution) | + +**Verdict:** This is the right direction for high-security or multi-tenant scenarios (hosted platform), but overkill for self-hosted single-user instances. The bubblewrap sandbox should remain the default; VM isolation would be an opt-in upgrade for hosted deployments. + +### 3. OpenCode Worker Backend (Medium Value) + +Spacebot already supports OpenCode as a worker backend. stereOS ships an `opencode-mixtape`. There's a natural alignment — Spacebot could spawn OpenCode workers inside stereOS VMs for maximum isolation during coding sessions, especially for untrusted workloads. The plumbing already exists on both sides; it's mainly an integration question. + +### 4. Self-Hosted Deployment (Medium Value) + +For users who want to self-host Spacebot with maximum isolation, a `spacebot-mixtape` is the simplest path: + +- `nix build .#spacebot-mixtape` produces a bootable image +- User launches it in QEMU/KVM or on Apple Silicon via `run-vm.sh` +- Secrets injected at boot, workspace mounted via virtio-fs +- No Docker, no container runtime, no host dependencies beyond QEMU + +This is a clean alternative to the current Docker deployment path for security-conscious users. + +## What a `spacebot-mixtape` Would Look Like + +Adding a new mixtape to stereOS is minimal. Based on the existing patterns: + +```nix +# mixtapes/spacebot/base.nix +{ config, lib, pkgs, ... }: +{ + stereos.agent.extraPackages = [ pkgs.spacebot ]; + + # Seed a minimal config.toml — secrets come via stereosd at boot + environment.etc."skel/.config/spacebot/config.toml".text = '' + [llm] + anthropic_key = "file:/run/stereos/secrets/ANTHROPIC_API_KEY" + + [[agents]] + id = "main" + ''; +} +``` + +Then register in `flake.nix`: + +```nix +spacebot-mixtape = stereos-lib.mkMixtape { + name = "spacebot-mixtape"; + features = [ ./mixtapes/spacebot/base.nix ]; +}; +``` + +The main prerequisite is packaging the Spacebot binary as a Nix derivation. Since it's a single Rust binary with no runtime dependencies, this is straightforward — `crane` or `naersk` for the Nix build, with SQLite and OpenSSL as build inputs. + +## Blockers and Open Questions + +### Fly/Firecracker Compatibility + +stereOS produces three image formats (raw EFI, QCOW2, kernel artifacts). Firecracker needs an ext4 rootfs and a kernel binary. stereOS's kernel artifacts output (bzImage + initrd) gets partway there, but Firecracker doesn't use initrd the same way — it expects a pre-mounted rootfs, not an initramfs that pivots. This would require either: + +1. A new `formats/firecracker.nix` in stereOS that extracts the NixOS system closure into a flat ext4 image +2. Running QEMU/KVM on Fly instead of Firecracker (possible via Fly GPU machines or custom builders, but non-standard) + +### Architecture Support + +stereOS is aarch64-linux only. Fly Machines are predominantly x86_64. Cross-compilation in Nix is well-supported, so adding `x86_64-linux` is likely straightforward but untested. + +### Control Plane Protocol + +`stereosd` speaks a custom protocol over vsock. For `spacebot-platform` to manage stereOS instances, it would need a Rust client for this protocol (or stereOS would need an HTTP API). The protocol is not yet documented publicly — would need to inspect the `stereosd` source. + +### Workspace Persistence + +stereOS VMs are ephemeral by design. Spacebot instances need persistent storage (SQLite databases, LanceDB, workspace files). This would require virtio-fs mounts to a persistent volume, which stereOS supports but the Fly integration path would need to map to Fly volumes. + +## Recommendation + +**Near term (low effort, high signal):** + +- Create a `spacebot-mixtape` in stereOS for self-hosted deployment. This is 2-3 files of Nix config + a Nix derivation for the Spacebot binary. Gives us a hardened, bootable deployment option with zero Docker dependency. + +**Medium term (moderate effort):** + +- Investigate Firecracker compatibility by looking at `stereosd`'s source and the Firecracker rootfs format. If the gap is small, stereOS becomes a candidate for the hosted platform runtime. + +**Long term (high effort, high value):** + +- Per-agent VM isolation for the hosted platform. One stereOS VM per agent, all workers execute inside it. This is the most architecturally interesting use case but requires solving vsock RPC, VM lifecycle management, and workspace mounting. + +The bubblewrap sandbox (shipped in 0.2.0) remains the right default for most deployments. stereOS VM isolation is a complementary layer for high-security hosted scenarios, not a replacement. diff --git a/docs/design-docs/tool-nudging.md b/docs/design-docs/tool-nudging.md new file mode 100644 index 000000000..106d9ed86 --- /dev/null +++ b/docs/design-docs/tool-nudging.md @@ -0,0 +1,120 @@ +# Tool Nudging + +Automatic retry mechanism that prevents workers from exiting with text-only responses before signaling a terminal outcome. + +## Problem + +Workers sometimes respond with text like "I'll help you with that" or "Let me create the email now..." without actually calling any tools. This is common: +- At the start of a worker loop when the LLM is "thinking out loud" +- When the task description is vague and the LLM wants clarification +- With certain models that have a conversational tendency +- **Mid-task**, after making a few tool calls (e.g. `read_skill`, `set_status`), the model returns narration instead of continuing with tools + +Without intervention, the worker silently reaches `Done` state with no useful output. In Rig's agent loop, any text-only response (no tool calls) terminates the loop — the worker exits as if it completed successfully. + +## Solution + +Workers must explicitly signal a terminal outcome via `set_status(kind: "outcome")` before they can exit with a text-only response. Until that signal is received, any text-only response triggers a nudge that sends the worker back to work. + +### How It Works + +``` +Worker loop starts + → LLM completion + → If response includes tool calls → continue normally + → If text-only response: + → Has outcome been signaled via set_status(kind: "outcome")? → allow exit + → No outcome signal? → Terminate with "tool_nudge" reason → retry with nudge prompt + → Max 2 retries per prompt request + → If retries exhausted → worker fails (PromptCancelled) +``` + +### Outcome Signaling + +The `set_status` tool has a `kind` field: + +- `kind: "progress"` (default) — intermediate status update, does not unlock exit +- `kind: "outcome"` — terminal result signal, allows text-only exit + +Workers are instructed to call `set_status(kind: "outcome")` with a result summary before finishing. The hook marks `outcome_signaled` only after a successful `set_status` tool result is observed in `on_tool_result` (`success: true` and `kind: "outcome"`), so failed status calls do not unlock text-only exit. + +### Policy Scoping + +Tool nudging is scoped by process type: + +| Process Type | Default Policy | Reason | +|--------------|----------------|--------| +| Worker | Enabled | Workers must complete tasks before exiting | +| Branch | Disabled | Branches are for thinking, not doing | +| Channel | Disabled | Channels should be conversational | + +The policy can be overridden per-hook: + +```rust +let hook = SpacebotHook::new(...) + .with_tool_nudge_policy(ToolNudgePolicy::Disabled); +``` + +### Implementation Details + +**Outcome detection** (`src/hooks/spacebot.rs:on_tool_result`): +- After a successful `set_status` tool execution with `kind: "outcome"`, `outcome_signaled` is set to `true` +- The flag persists for the rest of the prompt request + +**Nudge decision** (`src/hooks/spacebot.rs:should_nudge_tool_usage`): +- Returns `true` when: policy enabled, nudge active, no outcome signaled, response is text-only +- Returns `false` when: outcome signaled, response has tool calls, policy disabled + +**Retry flow** (`prompt_with_tool_nudge_retry`): +1. Reset nudge state at start of prompt (clears `outcome_signaled`) +2. On text-only response without outcome: terminate with `TOOL_NUDGE_REASON` +3. Catch termination in retry loop, prune history, retry with nudge prompt +4. On success: prune the nudge prompt from history to keep context clean +5. After `TOOL_NUDGE_MAX_RETRIES` (2) exhausted: `PromptCancelled` propagates to worker → `WorkerState::Failed` + +**History hygiene**: +- Synthetic nudge prompts are removed from history on both success and retry +- Failed assistant turns are pruned but user prompts are preserved +- Prevents accumulation of nudge noise in context + +### Configuration + +There is no user-facing configuration for tool nudging. The behavior is: +- Always enabled for workers +- Always disabled for branches and channels +- Cannot be configured per-agent or per-task + +If you need to disable nudging for a specific worker scenario, override the policy when creating the hook: + +```rust +// In worker follow-up handling (already disabled) +let follow_up_hook = hook + .clone() + .with_tool_nudge_policy(ToolNudgePolicy::Disabled); +``` + +### Testing + +The nudging behavior has comprehensive test coverage: + +- **Unit tests** (`src/hooks/spacebot.rs`): + - `nudges_on_every_text_only_response_without_outcome` — nudge fires on every text-only response + - `nudges_after_tool_calls_without_outcome` — the exact bug case (read_skill + progress status + text exit) + - `outcome_signal_allows_text_only_completion` — outcome signal unlocks exit + - `progress_status_does_not_signal_outcome` — explicit progress kind doesn't unlock + - `default_status_kind_does_not_signal_outcome` — omitted kind doesn't unlock + - `does_not_nudge_when_completion_contains_tool_call` + - `process_scoped_policy_*` variants for Branch/Channel/Worker + - `tool_nudge_retry_history_hygiene_*` for history pruning + +- **Integration tests** (`tests/tool_nudge.rs`): + - Public API surface tests (constants, policy enum, hook creation) + - Event emission verification + - Process-type scoping + +### Future Considerations + +1. **Per-model tuning**: Some models may need more/fewer nudge retries +2. **Adaptive nudging**: Detect when nudging isn't working and escalate +3. **User-visible indicator**: Show in UI when a worker was nudged +4. **Metric**: Dedicated counter for nudge events diff --git a/docs/docker.md b/docs/docker.md index 88cc3a8f8..d14e4769c 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -197,6 +197,31 @@ healthcheck: - Graceful shutdown on `SIGTERM` (what `docker stop` sends). Drains active channels, closes database connections. - The PID file and Unix socket (used in daemon mode) are not created. +## Updates + +Spacebot checks for new releases on startup and every hour. When a new version is available, a banner appears in the web UI. + +The web dashboard also includes **Settings → Updates** with status details, one-click controls (Docker), and manual command snippets. + +`latest` is supported and continues to receive updates (it tracks the rolling `full` image). Use explicit version tags only when you want controlled rollouts. + +### Manual Update + +```bash +docker compose pull spacebot +docker compose up -d --force-recreate spacebot +``` + +### One-Click Update + +Mount `/var/run/docker.sock` into the Spacebot container to enable the **Update now** button in the UI. Without the socket mount, update checks still work but apply is manual. + +One-click updates are intended for containers running Spacebot release tags. If you're running a custom/self-built image, rebuild your image and recreate the container. + +### Native / Source Builds + +If Spacebot is installed from source (`cargo install --path .` or a local release build), updates are manual: pull latest source, rebuild/reinstall, then restart. + ## CI / Releases Images are built and pushed to `ghcr.io/spacedriveapp/spacebot` via GitHub Actions (`.github/workflows/release.yml`). diff --git a/fly.staging.toml b/fly.staging.toml index 97da79dce..d0af60450 100644 --- a/fly.staging.toml +++ b/fly.staging.toml @@ -3,7 +3,6 @@ primary_region = "iad" [build] dockerfile = "Dockerfile" - target = "full" [env] SPACEBOT_DIR = "/data" diff --git a/fly.toml b/fly.toml index 7e5fee5c6..fd64ec6f9 100644 --- a/fly.toml +++ b/fly.toml @@ -3,7 +3,6 @@ primary_region = "iad" [build] dockerfile = "Dockerfile" - target = "full" [env] SPACEBOT_DIR = "/data" diff --git a/interface/bun.lock b/interface/bun.lock index 0349bffde..13c16389b 100644 --- a/interface/bun.lock +++ b/interface/bun.lock @@ -34,6 +34,7 @@ "@radix-ui/react-tooltip": "^1.2.8", "@react-sigma/core": "^5.0.6", "@tanstack/react-query": "^5.62.0", + "@tanstack/react-query-devtools": "^5.91.3", "@tanstack/react-router": "^1.159.5", "@tanstack/react-virtual": "^3.13.18", "@tanstack/router-devtools": "^1.159.5", @@ -487,8 +488,12 @@ "@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, ""], + "@tanstack/query-devtools": ["@tanstack/query-devtools@5.93.0", "", {}, "sha512-+kpsx1NQnOFTZsw6HAFCW3HkKg0+2cepGtAWXjiiSOJJ1CtQpt72EE2nyZb+AjAbLRPoeRmPJ8MtQd8r8gsPdg=="], + "@tanstack/react-query": ["@tanstack/react-query@5.90.21", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, ""], + "@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.91.3", "", { "dependencies": { "@tanstack/query-devtools": "5.93.0" }, "peerDependencies": { "@tanstack/react-query": "^5.90.20", "react": "^18 || ^19" } }, "sha512-nlahjMtd/J1h7IzOOfqeyDh5LNfG0eULwlltPEonYy0QL+nqrBB+nyzJfULV+moL7sZyxc2sHdNJki+vLA9BSA=="], + "@tanstack/react-router": ["@tanstack/react-router@1.159.5", "", { "dependencies": { "@tanstack/history": "1.154.14", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.159.4", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, ""], "@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.159.5", "", { "dependencies": { "@tanstack/router-devtools-core": "1.159.4" }, "peerDependencies": { "@tanstack/react-router": "^1.159.5", "@tanstack/router-core": "^1.159.4", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, ""], diff --git a/interface/package-lock.json b/interface/package-lock.json deleted file mode 100644 index 8a6cc8497..000000000 --- a/interface/package-lock.json +++ /dev/null @@ -1,10640 +0,0 @@ -{ - "name": "spacebot-interface", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "spacebot-interface", - "version": "0.1.0", - "dependencies": { - "@codemirror/language": "^6.12.1", - "@codemirror/legacy-modes": "^6.5.2", - "@codemirror/state": "^6.5.4", - "@codemirror/theme-one-dark": "^6.1.3", - "@codemirror/view": "^6.39.14", - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^10.0.0", - "@dnd-kit/utilities": "^3.2.2", - "@fontsource/ibm-plex-sans": "^5.1.0", - "@fortawesome/fontawesome-svg-core": "^7.2.0", - "@fortawesome/free-brands-svg-icons": "^7.2.0", - "@fortawesome/free-solid-svg-icons": "^7.2.0", - "@fortawesome/react-fontawesome": "^3.0.0", - "@hookform/resolvers": "^5.2.2", - "@hugeicons/core-free-icons": "^3.1.1", - "@hugeicons/react": "^1.1.5", - "@lobehub/icons": "^4.6.0", - "@radix-ui/react-checkbox": "^1.3.3", - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-dropdown-menu": "^2.1.16", - "@radix-ui/react-popover": "^1.1.15", - "@radix-ui/react-radio-group": "^1.3.8", - "@radix-ui/react-select": "^2.2.6", - "@radix-ui/react-slider": "^1.3.6", - "@radix-ui/react-switch": "^1.2.6", - "@radix-ui/react-tabs": "^1.1.13", - "@radix-ui/react-tooltip": "^1.2.8", - "@react-sigma/core": "^5.0.6", - "@tanstack/react-query": "^5.62.0", - "@tanstack/react-router": "^1.159.5", - "@tanstack/react-virtual": "^3.13.18", - "@tanstack/router-devtools": "^1.159.5", - "@xyflow/react": "^12.10.1", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "codemirror": "^6.0.2", - "framer-motion": "^12.34.0", - "graphology": "^0.26.0", - "graphology-layout-forceatlas2": "^0.10.1", - "graphology-types": "^0.24.8", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-hook-form": "^7.71.1", - "react-markdown": "^10.1.0", - "recharts": "^3.7.0", - "remark-gfm": "^4.0.1", - "sigma": "^3.0.2", - "smol-toml": "^1.6.0", - "sonner": "^2.0.7", - "zod": "^4.3.6" - }, - "devDependencies": { - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0", - "@vitejs/plugin-react": "^4.3.4", - "autoprefixer": "^10.4.18", - "postcss": "^8.4.36", - "sass": "^1.72.0", - "tailwindcss": "^3.4.1", - "tailwindcss-animate": "^1.0.7", - "typescript": "^5.6.2", - "vite": "^6.0.0" - } - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@ant-design/colors": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-8.0.1.tgz", - "integrity": "sha512-foPVl0+SWIslGUtD/xBr1p9U4AKzPhNYEseXYRRo5QSzGACYZrQbe11AYJbYfAWnWSpGBx6JjBmSeugUsD9vqQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@ant-design/fast-color": "^3.0.0" - } - }, - "node_modules/@ant-design/cssinjs": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-2.1.0.tgz", - "integrity": "sha512-eZFrPCnrYrF3XtL7qA4L75P0qA3TtZta8H3Yggy7UYFh8gZgu5bSMNF+v4UVCzGxzYmx8ZvPdgOce0BJ6PsW9g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.11.1", - "@emotion/hash": "^0.8.0", - "@emotion/unitless": "^0.7.5", - "@rc-component/util": "^1.4.0", - "clsx": "^2.1.1", - "csstype": "^3.1.3", - "stylis": "^4.3.4" - }, - "peerDependencies": { - "react": ">=16.0.0", - "react-dom": ">=16.0.0" - } - }, - "node_modules/@ant-design/cssinjs-utils": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-2.1.1.tgz", - "integrity": "sha512-RKxkj5pGFB+FkPJ5NGhoX3DK3xsv0pMltha7Ei1AnY3tILeq38L7tuhaWDPQI/5nlPxOog44wvqpNyyGcUsNMg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@ant-design/cssinjs": "^2.1.0", - "@babel/runtime": "^7.23.2", - "@rc-component/util": "^1.4.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - } - }, - "node_modules/@ant-design/fast-color": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-3.0.1.tgz", - "integrity": "sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8.x" - } - }, - "node_modules/@ant-design/icons": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.1.0.tgz", - "integrity": "sha512-KrWMu1fIg3w/1F2zfn+JlfNDU8dDqILfA5Tg85iqs1lf8ooyGlbkA+TkwfOKKgqpUmAiRY1PTFpuOU2DAIgSUg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@ant-design/colors": "^8.0.0", - "@ant-design/icons-svg": "^4.4.0", - "@rc-component/util": "^1.3.0", - "clsx": "^2.1.1" - }, - "engines": { - "node": ">=8" - }, - "peerDependencies": { - "react": ">=16.0.0", - "react-dom": ">=16.0.0" - } - }, - "node_modules/@ant-design/icons-svg": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", - "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==", - "license": "MIT", - "peer": true - }, - "node_modules/@ant-design/react-slick": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-2.0.0.tgz", - "integrity": "sha512-HMS9sRoEmZey8LsE/Yo6+klhlzU12PisjrVcydW3So7RdklyEd2qehyU6a7Yp+OYN72mgsYs3NFCyP2lCPFVqg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.28.4", - "clsx": "^2.1.1", - "json2mq": "^0.2.0", - "throttle-debounce": "^5.0.0" - }, - "peerDependencies": { - "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/@antfu/install-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", - "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "package-manager-detector": "^1.3.0", - "tinyexec": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.1", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.0", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.28.6", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.0", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@base-ui/react": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@base-ui/react/-/react-1.0.0.tgz", - "integrity": "sha512-4USBWz++DUSLTuIYpbYkSgy1F9ZmNG9S/lXvlUN6qMK0P0RlW+6eQmDUB4DgZ7HVvtXl4pvi4z5J2fv6Z3+9hg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.28.4", - "@base-ui/utils": "0.2.3", - "@floating-ui/react-dom": "^2.1.6", - "@floating-ui/utils": "^0.2.10", - "reselect": "^5.1.1", - "tabbable": "^6.3.0", - "use-sync-external-store": "^1.6.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17 || ^18 || ^19", - "react": "^17 || ^18 || ^19", - "react-dom": "^17 || ^18 || ^19" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@base-ui/utils": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@base-ui/utils/-/utils-0.2.3.tgz", - "integrity": "sha512-/CguQ2PDaOzeVOkllQR8nocJ0FFIDqsWIcURsVmm53QGo8NhFNpePjNlyPIB41luxfOqnG7PU0xicMEw3ls7XQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.28.4", - "@floating-ui/utils": "^0.2.10", - "reselect": "^5.1.1", - "use-sync-external-store": "^1.6.0" - }, - "peerDependencies": { - "@types/react": "^17 || ^18 || ^19", - "react": "^17 || ^18 || ^19", - "react-dom": "^17 || ^18 || ^19" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@braintree/sanitize-url": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", - "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", - "license": "MIT", - "peer": true - }, - "node_modules/@chevrotain/cst-dts-gen": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.1.1.tgz", - "integrity": "sha512-fRHyv6/f542qQqiRGalrfJl/evD39mAvbJLCekPazhiextEatq1Jx1K/i9gSd5NNO0ds03ek0Cbo/4uVKmOBcw==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@chevrotain/gast": "11.1.1", - "@chevrotain/types": "11.1.1", - "lodash-es": "4.17.23" - } - }, - "node_modules/@chevrotain/gast": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.1.1.tgz", - "integrity": "sha512-Ko/5vPEYy1vn5CbCjjvnSO4U7GgxyGm+dfUZZJIWTlQFkXkyym0jFYrWEU10hyCjrA7rQtiHtBr0EaZqvHFZvg==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@chevrotain/types": "11.1.1", - "lodash-es": "4.17.23" - } - }, - "node_modules/@chevrotain/regexp-to-ast": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.1.1.tgz", - "integrity": "sha512-ctRw1OKSXkOrR8VTvOxrQ5USEc4sNrfwXHa1NuTcR7wre4YbjPcKw+82C2uylg/TEwFRgwLmbhlln4qkmDyteg==", - "license": "Apache-2.0", - "peer": true - }, - "node_modules/@chevrotain/types": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.1.tgz", - "integrity": "sha512-wb2ToxG8LkgPYnKe9FH8oGn3TMCBdnwiuNC5l5y+CtlaVRbCytU0kbVsk6CGrqTL4ZN4ksJa0TXOYbxpbthtqw==", - "license": "Apache-2.0", - "peer": true - }, - "node_modules/@chevrotain/utils": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.1.1.tgz", - "integrity": "sha512-71eTYMzYXYSFPrbg/ZwftSaSDld7UYlS8OQa3lNnn9jzNtpFbaReRRyghzqS7rI3CDaorqpPJJcXGHK+FE1TVQ==", - "license": "Apache-2.0", - "peer": true - }, - "node_modules/@codemirror/autocomplete": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz", - "integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.17.0", - "@lezer/common": "^1.0.0" - } - }, - "node_modules/@codemirror/commands": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.2.tgz", - "integrity": "sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.4.0", - "@codemirror/view": "^6.27.0", - "@lezer/common": "^1.1.0" - } - }, - "node_modules/@codemirror/language": { - "version": "6.12.1", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz", - "integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.23.0", - "@lezer/common": "^1.5.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0", - "style-mod": "^4.0.0" - } - }, - "node_modules/@codemirror/legacy-modes": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.2.tgz", - "integrity": "sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0" - } - }, - "node_modules/@codemirror/lint": { - "version": "6.9.4", - "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.4.tgz", - "integrity": "sha512-ABc9vJ8DEmvOWuH26P3i8FpMWPQkduD9Rvba5iwb6O3hxASgclm3T3krGo8NASXkHCidz6b++LWlzWIUfEPSWw==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.35.0", - "crelt": "^1.0.5" - } - }, - "node_modules/@codemirror/search": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.6.0.tgz", - "integrity": "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.37.0", - "crelt": "^1.0.5" - } - }, - "node_modules/@codemirror/state": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz", - "integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==", - "license": "MIT", - "dependencies": { - "@marijn/find-cluster-break": "^1.0.0" - } - }, - "node_modules/@codemirror/theme-one-dark": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", - "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "@lezer/highlight": "^1.0.0" - } - }, - "node_modules/@codemirror/view": { - "version": "6.39.15", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.15.tgz", - "integrity": "sha512-aCWjgweIIXLBHh7bY6cACvXuyrZ0xGafjQ2VInjp4RM4gMfscK5uESiNdrH0pE+e1lZr2B4ONGsjchl2KsKZzg==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.5.0", - "crelt": "^1.0.6", - "style-mod": "^4.1.0", - "w3c-keyname": "^2.2.4" - } - }, - "node_modules/@dnd-kit/accessibility": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", - "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/core": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", - "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", - "license": "MIT", - "dependencies": { - "@dnd-kit/accessibility": "^3.1.1", - "@dnd-kit/utilities": "^3.2.2", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/modifiers": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-9.0.0.tgz", - "integrity": "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@dnd-kit/utilities": "^3.2.2", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "@dnd-kit/core": "^6.3.0", - "react": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/sortable": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", - "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", - "license": "MIT", - "dependencies": { - "@dnd-kit/utilities": "^3.2.2", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "@dnd-kit/core": "^6.3.0", - "react": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/utilities": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", - "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@emoji-mart/data": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emoji-mart/data/-/data-1.2.1.tgz", - "integrity": "sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==", - "license": "MIT", - "peer": true - }, - "node_modules/@emoji-mart/react": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@emoji-mart/react/-/react-1.1.1.tgz", - "integrity": "sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g==", - "license": "MIT", - "peer": true, - "peerDependencies": { - "emoji-mart": "^5.2", - "react": "^16.8 || ^17 || ^18" - } - }, - "node_modules/@emotion/babel-plugin": { - "version": "11.13.5", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", - "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.16.7", - "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.2", - "@emotion/memoize": "^0.9.0", - "@emotion/serialize": "^1.3.3", - "babel-plugin-macros": "^3.1.0", - "convert-source-map": "^1.5.0", - "escape-string-regexp": "^4.0.0", - "find-root": "^1.1.0", - "source-map": "^0.5.7", - "stylis": "4.2.0" - } - }, - "node_modules/@emotion/babel-plugin/node_modules/@emotion/hash": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", - "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", - "license": "MIT" - }, - "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "license": "MIT" - }, - "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@emotion/babel-plugin/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@emotion/babel-plugin/node_modules/stylis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", - "license": "MIT" - }, - "node_modules/@emotion/cache": { - "version": "11.14.0", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", - "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", - "license": "MIT", - "dependencies": { - "@emotion/memoize": "^0.9.0", - "@emotion/sheet": "^1.4.0", - "@emotion/utils": "^1.4.2", - "@emotion/weak-memoize": "^0.4.0", - "stylis": "4.2.0" - } - }, - "node_modules/@emotion/cache/node_modules/stylis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", - "license": "MIT" - }, - "node_modules/@emotion/css": { - "version": "11.13.5", - "resolved": "https://registry.npmjs.org/@emotion/css/-/css-11.13.5.tgz", - "integrity": "sha512-wQdD0Xhkn3Qy2VNcIzbLP9MR8TafI0MJb7BEAXKp+w4+XqErksWR4OXomuDzPsN4InLdGhVe6EYcn2ZIUCpB8w==", - "license": "MIT", - "dependencies": { - "@emotion/babel-plugin": "^11.13.5", - "@emotion/cache": "^11.13.5", - "@emotion/serialize": "^1.3.3", - "@emotion/sheet": "^1.4.0", - "@emotion/utils": "^1.4.2" - } - }, - "node_modules/@emotion/hash": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", - "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", - "license": "MIT" - }, - "node_modules/@emotion/is-prop-valid": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", - "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@emotion/memoize": "^0.9.0" - } - }, - "node_modules/@emotion/memoize": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", - "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", - "license": "MIT" - }, - "node_modules/@emotion/react": { - "version": "11.14.0", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", - "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.13.5", - "@emotion/cache": "^11.14.0", - "@emotion/serialize": "^1.3.3", - "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", - "@emotion/utils": "^1.4.2", - "@emotion/weak-memoize": "^0.4.0", - "hoist-non-react-statics": "^3.3.1" - }, - "peerDependencies": { - "react": ">=16.8.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@emotion/serialize": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", - "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", - "license": "MIT", - "dependencies": { - "@emotion/hash": "^0.9.2", - "@emotion/memoize": "^0.9.0", - "@emotion/unitless": "^0.10.0", - "@emotion/utils": "^1.4.2", - "csstype": "^3.0.2" - } - }, - "node_modules/@emotion/serialize/node_modules/@emotion/hash": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", - "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", - "license": "MIT" - }, - "node_modules/@emotion/serialize/node_modules/@emotion/unitless": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", - "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", - "license": "MIT" - }, - "node_modules/@emotion/sheet": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", - "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", - "license": "MIT" - }, - "node_modules/@emotion/unitless": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", - "license": "MIT" - }, - "node_modules/@emotion/use-insertion-effect-with-fallbacks": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", - "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", - "license": "MIT", - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@emotion/utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", - "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", - "license": "MIT" - }, - "node_modules/@emotion/weak-memoize": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", - "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", - "license": "MIT" - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@floating-ui/core": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", - "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", - "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.4", - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/react": { - "version": "0.27.18", - "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.18.tgz", - "integrity": "sha512-xJWJxvmy3a05j643gQt+pRbht5XnTlGpsEsAPnMi5F5YTOEEJymA90uZKBD8OvIv5XvZ1qi4GcccSlqT3Bq44Q==", - "license": "MIT", - "peer": true, - "dependencies": { - "@floating-ui/react-dom": "^2.1.7", - "@floating-ui/utils": "^0.2.10", - "tabbable": "^6.0.0" - }, - "peerDependencies": { - "react": ">=17.0.0", - "react-dom": ">=17.0.0" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.7.tgz", - "integrity": "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.7.5" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", - "license": "MIT" - }, - "node_modules/@fontsource/ibm-plex-sans": { - "version": "5.2.8", - "license": "OFL-1.1", - "funding": { - "url": "https://github.com/sponsors/ayuhito" - } - }, - "node_modules/@fortawesome/fontawesome-common-types": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.2.0.tgz", - "integrity": "sha512-IpR0bER9FY25p+e7BmFH25MZKEwFHTfRAfhOyJubgiDnoJNsSvJ7nigLraHtp4VOG/cy8D7uiV0dLkHOne5Fhw==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/@fortawesome/fontawesome-svg-core": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.2.0.tgz", - "integrity": "sha512-6639htZMjEkwskf3J+e6/iar+4cTNM9qhoWuRfj9F3eJD6r7iCzV1SWnQr2Mdv0QT0suuqU8BoJCZUyCtP9R4Q==", - "license": "MIT", - "dependencies": { - "@fortawesome/fontawesome-common-types": "7.2.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@fortawesome/free-brands-svg-icons": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-7.2.0.tgz", - "integrity": "sha512-VNG8xqOip1JuJcC3zsVsKRQ60oXG9+oYNDCosjoU/H9pgYmLTEwWw8pE0jhPz/JWdHeUuK6+NQ3qsM4gIbdbYQ==", - "license": "(CC-BY-4.0 AND MIT)", - "dependencies": { - "@fortawesome/fontawesome-common-types": "7.2.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@fortawesome/free-solid-svg-icons": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-7.2.0.tgz", - "integrity": "sha512-YTVITFGN0/24PxzXrwqCgnyd7njDuzp5ZvaCx5nq/jg55kUYd94Nj8UTchBdBofi/L0nwRfjGOg0E41d2u9T1w==", - "license": "(CC-BY-4.0 AND MIT)", - "dependencies": { - "@fortawesome/fontawesome-common-types": "7.2.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@fortawesome/react-fontawesome": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-3.0.0.tgz", - "integrity": "sha512-x6boc1RLEjf/QPrMS20VJcabTZeGCb1hbwNybPPLjJohGPowXfjOpwQlVK6aH6MVKfCq2JXeHRIlx+tYpS18FA==", - "license": "MIT", - "dependencies": { - "semver": "^7.7.2" - }, - "peerDependencies": { - "@fortawesome/fontawesome-svg-core": "~6 || ~7", - "react": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@fortawesome/react-fontawesome/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@giscus/react": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@giscus/react/-/react-3.1.0.tgz", - "integrity": "sha512-0TCO2TvL43+oOdyVVGHDItwxD1UMKP2ZYpT6gXmhFOqfAJtZxTzJ9hkn34iAF/b6YzyJ4Um89QIt9z/ajmAEeg==", - "peer": true, - "dependencies": { - "giscus": "^1.6.0" - }, - "peerDependencies": { - "react": "^16 || ^17 || ^18 || ^19", - "react-dom": "^16 || ^17 || ^18 || ^19" - } - }, - "node_modules/@hookform/resolvers": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", - "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==", - "license": "MIT", - "dependencies": { - "@standard-schema/utils": "^0.3.0" - }, - "peerDependencies": { - "react-hook-form": "^7.55.0" - } - }, - "node_modules/@hugeicons/core-free-icons": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-3.1.1.tgz", - "integrity": "sha512-UpS2lUQFi5sKyJSWwM6rO+BnPLvVz1gsyCpPHeZyVuZqi89YH8ksliza4cwaODqKOZyeXmG8juo1ty4QtQofkg==", - "license": "MIT" - }, - "node_modules/@hugeicons/react": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@hugeicons/react/-/react-1.1.5.tgz", - "integrity": "sha512-JX/iDz3oO7hWdVqbjwFwRrAjHk8h2vI+mBkNzp4JcXG3t4idoupfjon73nLOA7cr27m0M8hrRC1Q2h6nEBGKVA==", - "license": "MIT", - "peerDependencies": { - "react": ">=16.0.0" - } - }, - "node_modules/@iconify/types": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", - "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", - "license": "MIT", - "peer": true - }, - "node_modules/@iconify/utils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.0.tgz", - "integrity": "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@antfu/install-pkg": "^1.1.0", - "@iconify/types": "^2.0.0", - "mlly": "^1.8.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@lezer/common": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz", - "integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==", - "license": "MIT" - }, - "node_modules/@lezer/highlight": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", - "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.3.0" - } - }, - "node_modules/@lezer/lr": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz", - "integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.0.0" - } - }, - "node_modules/@lit-labs/ssr-dom-shim": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.5.1.tgz", - "integrity": "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==", - "license": "BSD-3-Clause", - "peer": true - }, - "node_modules/@lit/reactive-element": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz", - "integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==", - "license": "BSD-3-Clause", - "peer": true, - "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.5.0" - } - }, - "node_modules/@lobehub/emojilib": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@lobehub/emojilib/-/emojilib-1.0.0.tgz", - "integrity": "sha512-s9KnjaPjsEefaNv150G3aifvB+J3P4eEKG+epY9zDPS2BeB6+V2jELWqAZll+nkogMaVovjEE813z3V751QwGw==", - "license": "MIT", - "peer": true - }, - "node_modules/@lobehub/fluent-emoji": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@lobehub/fluent-emoji/-/fluent-emoji-4.1.0.tgz", - "integrity": "sha512-R1MB2lfUkDvB7XAQdRzY75c1dx/tB7gEvBPaEEMarzKfCJWmXm7rheS6caVzmgwAlq5sfmTbxPL+un99sp//Yw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@lobehub/emojilib": "^1.0.0", - "antd-style": "^4.1.0", - "emoji-regex": "^10.6.0", - "es-toolkit": "^1.43.0", - "lucide-react": "^0.562.0", - "url-join": "^5.0.0" - }, - "peerDependencies": { - "react": "^19.0.0", - "react-dom": "^19.0.0" - } - }, - "node_modules/@lobehub/icons": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@lobehub/icons/-/icons-4.6.0.tgz", - "integrity": "sha512-TuU0837kalurxQfGwfyd6UODwKtPluhvuT8XrgqHo/D0B/ggbYWrLF1pwIYG+p9ccA6oz6HeaEQhmNH2eQl2sw==", - "license": "MIT", - "workspaces": [ - "packages/*" - ], - "dependencies": { - "antd-style": "^4.1.0", - "lucide-react": "^0.469.0", - "polished": "^4.3.1" - }, - "peerDependencies": { - "@lobehub/ui": "^4.3.3", - "antd": "^6.1.1", - "react": "^19.0.0", - "react-dom": "^19.0.0" - } - }, - "node_modules/@lobehub/icons/node_modules/lucide-react": { - "version": "0.469.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.469.0.tgz", - "integrity": "sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/@lobehub/ui": { - "version": "4.38.4", - "resolved": "https://registry.npmjs.org/@lobehub/ui/-/ui-4.38.4.tgz", - "integrity": "sha512-FYQeWkR0CoZCaPqEX9AUGrhaIfkYeuacW2KtV+1GS7eGVjREFNNOAgY5PLk20ZMYV/cRFsn9fNG0rqn9PxChxw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@ant-design/cssinjs": "^2.0.3", - "@base-ui/react": "1.0.0", - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/modifiers": "^9.0.0", - "@dnd-kit/sortable": "^10.0.0", - "@dnd-kit/utilities": "^3.2.2", - "@emoji-mart/data": "^1.2.1", - "@emoji-mart/react": "^1.1.1", - "@emotion/is-prop-valid": "^1.4.0", - "@floating-ui/react": "^0.27.17", - "@giscus/react": "^3.1.0", - "@mdx-js/mdx": "^3.1.1", - "@mdx-js/react": "^3.1.1", - "@pierre/diffs": "^1.0.10", - "@radix-ui/react-slot": "^1.2.4", - "@shikijs/core": "^3.22.0", - "@shikijs/transformers": "^3.22.0", - "@splinetool/runtime": "0.9.526", - "ahooks": "^3.9.6", - "antd-style": "^4.1.0", - "chroma-js": "^3.2.0", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "dayjs": "^1.11.19", - "emoji-mart": "^5.6.0", - "es-toolkit": "^1.44.0", - "fast-deep-equal": "^3.1.3", - "immer": "^11.1.3", - "katex": "^0.16.28", - "leva": "^0.10.1", - "lucide-react": "^0.563.0", - "marked": "^17.0.1", - "mermaid": "^11.12.2", - "motion": "^12.30.0", - "numeral": "^2.0.6", - "polished": "^4.3.1", - "query-string": "^9.3.1", - "rc-collapse": "^4.0.0", - "rc-footer": "^0.6.8", - "rc-image": "^7.12.0", - "rc-input-number": "^9.5.0", - "rc-menu": "^9.16.1", - "re-resizable": "^6.11.2", - "react-avatar-editor": "^14.0.0", - "react-error-boundary": "^6.1.0", - "react-hotkeys-hook": "^5.2.4", - "react-markdown": "^10.1.0", - "react-merge-refs": "^3.0.2", - "react-rnd": "^10.5.2", - "react-zoom-pan-pinch": "^3.7.0", - "rehype-github-alerts": "^4.2.0", - "rehype-katex": "^7.0.1", - "rehype-raw": "^7.0.0", - "remark-breaks": "^4.0.0", - "remark-cjk-friendly": "^1.2.3", - "remark-gfm": "^4.0.1", - "remark-github": "^12.0.0", - "remark-math": "^6.0.0", - "remend": "^1.2.0", - "shiki": "^3.22.0", - "shiki-stream": "^0.1.4", - "swr": "^2.4.0", - "ts-md5": "^2.0.1", - "unified": "^11.0.5", - "url-join": "^5.0.0", - "use-merge-value": "^1.2.0", - "uuid": "^13.0.0", - "virtua": "^0.48.5" - }, - "peerDependencies": { - "@lobehub/fluent-emoji": "^4.0.0", - "@lobehub/icons": "^4.0.0", - "antd": "^6.1.1", - "motion": "^12.0.0", - "react": "^19.0.0", - "react-dom": "^19.0.0" - } - }, - "node_modules/@lobehub/ui/node_modules/@radix-ui/react-slot": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", - "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@lobehub/ui/node_modules/immer": { - "version": "11.1.4", - "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", - "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", - "license": "MIT", - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/@lobehub/ui/node_modules/lucide-react": { - "version": "0.563.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz", - "integrity": "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==", - "license": "ISC", - "peer": true, - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/@marijn/find-cluster-break": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", - "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", - "license": "MIT" - }, - "node_modules/@mdx-js/mdx": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", - "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/estree": "^1.0.0", - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdx": "^2.0.0", - "acorn": "^8.0.0", - "collapse-white-space": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "estree-util-scope": "^1.0.0", - "estree-walker": "^3.0.0", - "hast-util-to-jsx-runtime": "^2.0.0", - "markdown-extensions": "^2.0.0", - "recma-build-jsx": "^1.0.0", - "recma-jsx": "^1.0.0", - "recma-stringify": "^1.0.0", - "rehype-recma": "^1.0.0", - "remark-mdx": "^3.0.0", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.0.0", - "source-map": "^0.7.0", - "unified": "^11.0.0", - "unist-util-position-from-estree": "^2.0.0", - "unist-util-stringify-position": "^4.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@mdx-js/react": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", - "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/mdx": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - }, - "peerDependencies": { - "@types/react": ">=16", - "react": ">=16" - } - }, - "node_modules/@mermaid-js/parser": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.0.0.tgz", - "integrity": "sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw==", - "license": "MIT", - "peer": true, - "dependencies": { - "langium": "^4.0.0" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@parcel/watcher": { - "version": "2.5.6", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.3", - "is-glob": "^4.0.3", - "node-addon-api": "^7.0.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.6", - "@parcel/watcher-darwin-arm64": "2.5.6", - "@parcel/watcher-darwin-x64": "2.5.6", - "@parcel/watcher-freebsd-x64": "2.5.6", - "@parcel/watcher-linux-arm-glibc": "2.5.6", - "@parcel/watcher-linux-arm-musl": "2.5.6", - "@parcel/watcher-linux-arm64-glibc": "2.5.6", - "@parcel/watcher-linux-arm64-musl": "2.5.6", - "@parcel/watcher-linux-x64-glibc": "2.5.6", - "@parcel/watcher-linux-x64-musl": "2.5.6", - "@parcel/watcher-win32-arm64": "2.5.6", - "@parcel/watcher-win32-ia32": "2.5.6", - "@parcel/watcher-win32-x64": "2.5.6" - } - }, - "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.6", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@pierre/diffs": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@pierre/diffs/-/diffs-1.0.11.tgz", - "integrity": "sha512-j6zIEoyImQy1HfcJqbrDwP0O5I7V2VNXAaw53FqQ+SykRfaNwABeZHs9uibXO4supaXPmTx6LEH9Lffr03e1Tw==", - "license": "apache-2.0", - "peer": true, - "dependencies": { - "@shikijs/core": "^3.0.0", - "@shikijs/engine-javascript": "^3.0.0", - "@shikijs/transformers": "^3.0.0", - "diff": "8.0.3", - "hast-util-to-html": "9.0.5", - "lru_map": "0.4.1", - "shiki": "^3.0.0" - }, - "peerDependencies": { - "react": "^18.3.1 || ^19.0.0", - "react-dom": "^18.3.1 || ^19.0.0" - } - }, - "node_modules/@primer/octicons": { - "version": "19.22.0", - "resolved": "https://registry.npmjs.org/@primer/octicons/-/octicons-19.22.0.tgz", - "integrity": "sha512-nWoh9PlE6u7xbiZF3KcUm3ktLpN2rQPt11trwp/t4EsKuYRNVWVbBp1LkCBsvZq7ScckNKUURLigIU0wS1FQdw==", - "license": "MIT", - "peer": true, - "dependencies": { - "object-assign": "^4.1.1" - } - }, - "node_modules/@radix-ui/number": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", - "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", - "license": "MIT" - }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", - "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-checkbox": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", - "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", - "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", - "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", - "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", - "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", - "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", - "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popper": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", - "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-radio-group": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", - "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", - "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", - "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slider": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", - "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", - "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tabs": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", - "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", - "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-visually-hidden": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-previous": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", - "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", - "license": "MIT", - "dependencies": { - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", - "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", - "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "license": "MIT" - }, - "node_modules/@rc-component/async-validator": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.1.0.tgz", - "integrity": "sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.24.4" - }, - "engines": { - "node": ">=14.x" - } - }, - "node_modules/@rc-component/cascader": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@rc-component/cascader/-/cascader-1.14.0.tgz", - "integrity": "sha512-Ip9356xwZUR2nbW5PRVGif4B/bDve4pLa/N+PGbvBaTnjbvmN4PFMBGQSmlDlzKP1ovxaYMvwF/dI9lXNLT4iQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/select": "~1.6.0", - "@rc-component/tree": "~1.2.0", - "@rc-component/util": "^1.4.0", - "clsx": "^2.1.1" - }, - "peerDependencies": { - "react": ">=18.0.0", - "react-dom": ">=18.0.0" - } - }, - "node_modules/@rc-component/checkbox": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@rc-component/checkbox/-/checkbox-2.0.0.tgz", - "integrity": "sha512-3CXGPpAR9gsPKeO2N78HAPOzU30UdemD6HGJoWVJOpa6WleaGB5kzZj3v6bdTZab31YuWgY/RxV3VKPctn0DwQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/util": "^1.3.0", - "clsx": "^2.1.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/collapse": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@rc-component/collapse/-/collapse-1.2.0.tgz", - "integrity": "sha512-ZRYSKSS39qsFx93p26bde7JUZJshsUBEQRlRXPuJYlAiNX0vyYlF5TsAm8JZN3LcF8XvKikdzPbgAtXSbkLUkw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.10.1", - "@rc-component/motion": "^1.1.4", - "@rc-component/util": "^1.3.0", - "clsx": "^2.1.1" - }, - "peerDependencies": { - "react": ">=18.0.0", - "react-dom": ">=18.0.0" - } - }, - "node_modules/@rc-component/color-picker": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-3.1.0.tgz", - "integrity": "sha512-o7Vavj7yyfVxFmeynXf0fCHVlC0UTE9al74c6nYuLck+gjuVdQNWSVXR8Efq/mmWFy7891SCOsfaPq6Eqe1s/g==", - "license": "MIT", - "peer": true, - "dependencies": { - "@ant-design/fast-color": "^3.0.0", - "@rc-component/util": "^1.3.0", - "clsx": "^2.1.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/context": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-2.0.1.tgz", - "integrity": "sha512-HyZbYm47s/YqtP6pKXNMjPEMaukyg7P0qVfgMLzr7YiFNMHbK2fKTAGzms9ykfGHSfyf75nBbgWw+hHkp+VImw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/util": "^1.3.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/dialog": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/@rc-component/dialog/-/dialog-1.8.4.tgz", - "integrity": "sha512-Ay6PM7phkTkquplG8fWfUGFZ2GTLx9diTl4f0d8Eqxd7W1u1KjE9AQooFQHOHnhZf0Ya3z51+5EKCWHmt/dNEw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/motion": "^1.1.3", - "@rc-component/portal": "^2.1.0", - "@rc-component/util": "^1.9.0", - "clsx": "^2.1.1" - }, - "peerDependencies": { - "react": ">=18.0.0", - "react-dom": ">=18.0.0" - } - }, - "node_modules/@rc-component/drawer": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@rc-component/drawer/-/drawer-1.4.2.tgz", - "integrity": "sha512-1ib+fZEp6FBu+YvcIktm+nCQ+Q+qIpwpoaJH6opGr4ofh2QMq+qdr5DLC4oCf5qf3pcWX9lUWPYX652k4ini8Q==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/motion": "^1.1.4", - "@rc-component/portal": "^2.1.3", - "@rc-component/util": "^1.9.0", - "clsx": "^2.1.1" - }, - "peerDependencies": { - "react": ">=18.0.0", - "react-dom": ">=18.0.0" - } - }, - "node_modules/@rc-component/dropdown": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rc-component/dropdown/-/dropdown-1.0.2.tgz", - "integrity": "sha512-6PY2ecUSYhDPhkNHHb4wfeAya04WhpmUSKzdR60G+kMNVUCX2vjT/AgTS0Lz0I/K6xrPMJ3enQbwVpeN3sHCgg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/trigger": "^3.0.0", - "@rc-component/util": "^1.2.1", - "clsx": "^2.1.1" - }, - "peerDependencies": { - "react": ">=16.11.0", - "react-dom": ">=16.11.0" - } - }, - "node_modules/@rc-component/form": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@rc-component/form/-/form-1.6.2.tgz", - "integrity": "sha512-OgIn2RAoaSBqaIgzJf/X6iflIa9LpTozci1lagLBdURDFhGA370v0+T0tXxOi8YShMjTha531sFhwtnrv+EJaQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/async-validator": "^5.1.0", - "@rc-component/util": "^1.6.2", - "clsx": "^2.1.1" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/image": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@rc-component/image/-/image-1.6.0.tgz", - "integrity": "sha512-tSfn2ZE/oP082g4QIOxeehkmgnXB7R+5AFj/lIFr4k7pEuxHBdyGIq9axoCY9qea8NN0DY6p4IB/F07tLqaT5A==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/motion": "^1.0.0", - "@rc-component/portal": "^2.1.2", - "@rc-component/util": "^1.3.0", - "clsx": "^2.1.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/input": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@rc-component/input/-/input-1.1.2.tgz", - "integrity": "sha512-Q61IMR47piUBudgixJ30CciKIy9b1H95qe7GgEKOmSJVJXvFRWJllJfQry9tif+MX2cWFXWJf/RXz4kaCeq/Fg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/util": "^1.4.0", - "clsx": "^2.1.1" - }, - "peerDependencies": { - "react": ">=16.0.0", - "react-dom": ">=16.0.0" - } - }, - "node_modules/@rc-component/input-number": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@rc-component/input-number/-/input-number-1.6.2.tgz", - "integrity": "sha512-Gjcq7meZlCOiWN1t1xCC+7/s85humHVokTBI7PJgTfoyw5OWF74y3e6P8PHX104g9+b54jsodFIzyaj6p8LI9w==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/mini-decimal": "^1.0.1", - "@rc-component/util": "^1.4.0", - "clsx": "^2.1.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/mentions": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@rc-component/mentions/-/mentions-1.6.0.tgz", - "integrity": "sha512-KIkQNP6habNuTsLhUv0UGEOwG67tlmE7KNIJoQZZNggEZl5lQJTytFDb69sl5CK3TDdISCTjKP3nGEBKgT61CQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/input": "~1.1.0", - "@rc-component/menu": "~1.2.0", - "@rc-component/textarea": "~1.1.0", - "@rc-component/trigger": "^3.0.0", - "@rc-component/util": "^1.3.0", - "clsx": "^2.1.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/menu": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@rc-component/menu/-/menu-1.2.0.tgz", - "integrity": "sha512-VWwDuhvYHSnTGj4n6bV3ISrLACcPAzdPOq3d0BzkeiM5cve8BEYfvkEhNoM0PLzv51jpcejeyrLXeMVIJ+QJlg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/motion": "^1.1.4", - "@rc-component/overflow": "^1.0.0", - "@rc-component/trigger": "^3.0.0", - "@rc-component/util": "^1.3.0", - "clsx": "^2.1.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/mini-decimal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.0.tgz", - "integrity": "sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.18.0" - }, - "engines": { - "node": ">=8.x" - } - }, - "node_modules/@rc-component/motion": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@rc-component/motion/-/motion-1.1.6.tgz", - "integrity": "sha512-aEQobs/YA0kqRvHIPjQvOytdtdRVyhf/uXAal4chBjxDu6odHckExJzjn2D+Ju1aKK6hx3pAs6BXdV9+86xkgQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/util": "^1.2.0", - "clsx": "^2.1.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/mutate-observer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-2.0.1.tgz", - "integrity": "sha512-AyarjoLU5YlxuValRi+w8JRH2Z84TBbFO2RoGWz9d8bSu0FqT8DtugH3xC3BV7mUwlmROFauyWuXFuq4IFbH+w==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/util": "^1.2.0" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/notification": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@rc-component/notification/-/notification-1.2.0.tgz", - "integrity": "sha512-OX3J+zVU7rvoJCikjrfW7qOUp7zlDeFBK2eA3SFbGSkDqo63Sl4Ss8A04kFP+fxHSxMDIS9jYVEZtU1FNCFuBA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/motion": "^1.1.4", - "@rc-component/util": "^1.2.1", - "clsx": "^2.1.1" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/overflow": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rc-component/overflow/-/overflow-1.0.0.tgz", - "integrity": "sha512-GSlBeoE0XTBi5cf3zl8Qh7Uqhn7v8RrlJ8ajeVpEkNe94HWy5l5BQ0Mwn2TVUq9gdgbfEMUmTX7tJFAg7mz0Rw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.11.1", - "@rc-component/resize-observer": "^1.0.1", - "@rc-component/util": "^1.4.0", - "clsx": "^2.1.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/pagination": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@rc-component/pagination/-/pagination-1.2.0.tgz", - "integrity": "sha512-YcpUFE8dMLfSo6OARJlK6DbHHvrxz7pMGPGmC/caZSJJz6HRKHC1RPP001PRHCvG9Z/veD039uOQmazVuLJzlw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/util": "^1.3.0", - "clsx": "^2.1.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/picker": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@rc-component/picker/-/picker-1.9.0.tgz", - "integrity": "sha512-OLisdk8AWVCG9goBU1dWzuH5QlBQk8jktmQ6p0/IyBFwdKGwyIZOSjnBYo8hooHiTdl0lU+wGf/OfMtVBw02KQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/overflow": "^1.0.0", - "@rc-component/resize-observer": "^1.0.0", - "@rc-component/trigger": "^3.6.15", - "@rc-component/util": "^1.3.0", - "clsx": "^2.1.1" - }, - "engines": { - "node": ">=12.x" - }, - "peerDependencies": { - "date-fns": ">= 2.x", - "dayjs": ">= 1.x", - "luxon": ">= 3.x", - "moment": ">= 2.x", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - }, - "peerDependenciesMeta": { - "date-fns": { - "optional": true - }, - "dayjs": { - "optional": true - }, - "luxon": { - "optional": true - }, - "moment": { - "optional": true - } - } - }, - "node_modules/@rc-component/portal": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-2.2.0.tgz", - "integrity": "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/util": "^1.2.1", - "clsx": "^2.1.1" - }, - "engines": { - "node": ">=12.x" - }, - "peerDependencies": { - "react": ">=18.0.0", - "react-dom": ">=18.0.0" - } - }, - "node_modules/@rc-component/progress": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rc-component/progress/-/progress-1.0.2.tgz", - "integrity": "sha512-WZUnH9eGxH1+xodZKqdrHke59uyGZSWgj5HBM5Kwk5BrTMuAORO7VJ2IP5Qbm9aH3n9x3IcesqHHR0NWPBC7fQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/util": "^1.2.1", - "clsx": "^2.1.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/qrcode": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.1.1.tgz", - "integrity": "sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.24.7" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/rate": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rc-component/rate/-/rate-1.0.1.tgz", - "integrity": "sha512-bkXxeBqDpl5IOC7yL7GcSYjQx9G8H+6kLYQnNZWeBYq2OYIv1MONd6mqKTjnnJYpV0cQIU2z3atdW0j1kttpTw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/util": "^1.3.0", - "clsx": "^2.1.1" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/resize-observer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@rc-component/resize-observer/-/resize-observer-1.1.1.tgz", - "integrity": "sha512-NfXXMmiR+SmUuKE1NwJESzEUYUFWIDUn2uXpxCTOLwiRUUakd62DRNFjRJArgzyFW8S5rsL4aX5XlyIXyC/vRA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/util": "^1.2.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/segmented": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@rc-component/segmented/-/segmented-1.3.0.tgz", - "integrity": "sha512-5J/bJ01mbDnoA6P/FW8SxUvKn+OgUSTZJPzCNnTBntG50tzoP7DydGhqxp7ggZXZls7me3mc2EQDXakU3iTVFg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.11.1", - "@rc-component/motion": "^1.1.4", - "@rc-component/util": "^1.3.0", - "clsx": "^2.1.1" - }, - "peerDependencies": { - "react": ">=16.0.0", - "react-dom": ">=16.0.0" - } - }, - "node_modules/@rc-component/select": { - "version": "1.6.10", - "resolved": "https://registry.npmjs.org/@rc-component/select/-/select-1.6.10.tgz", - "integrity": "sha512-y4+2LnyGZrAorIBwflk78PmFVUWcSc9pcljiH72oHj7K1YY/BFUmj224pD7P4o7J+tbIFES45Z7LIpjVmvYlNA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/overflow": "^1.0.0", - "@rc-component/trigger": "^3.0.0", - "@rc-component/util": "^1.3.0", - "@rc-component/virtual-list": "^1.0.1", - "clsx": "^2.1.1" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": "*", - "react-dom": "*" - } - }, - "node_modules/@rc-component/slider": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rc-component/slider/-/slider-1.0.1.tgz", - "integrity": "sha512-uDhEPU1z3WDfCJhaL9jfd2ha/Eqpdfxsn0Zb0Xcq1NGQAman0TWaR37OWp2vVXEOdV2y0njSILTMpTfPV1454g==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/util": "^1.3.0", - "clsx": "^2.1.1" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/steps": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@rc-component/steps/-/steps-1.2.2.tgz", - "integrity": "sha512-/yVIZ00gDYYPHSY0JP+M+s3ZvuXLu2f9rEjQqiUDs7EcYsUYrpJ/1bLj9aI9R7MBR3fu/NGh6RM9u2qGfqp+Nw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/util": "^1.2.1", - "clsx": "^2.1.1" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/switch": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rc-component/switch/-/switch-1.0.3.tgz", - "integrity": "sha512-Jgi+EbOBquje/XNdofr7xbJQZPYJP+BlPfR0h+WN4zFkdtB2EWqEfvkXJWeipflwjWip0/17rNbxEAqs8hVHfw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/util": "^1.3.0", - "clsx": "^2.1.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/table": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@rc-component/table/-/table-1.9.1.tgz", - "integrity": "sha512-FVI5ZS/GdB3BcgexfCYKi3iHhZS3Fr59EtsxORszYGrfpH1eWr33eDNSYkVfLI6tfJ7vftJDd9D5apfFWqkdJg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/context": "^2.0.1", - "@rc-component/resize-observer": "^1.0.0", - "@rc-component/util": "^1.1.0", - "@rc-component/virtual-list": "^1.0.1", - "clsx": "^2.1.1" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=18.0.0", - "react-dom": ">=18.0.0" - } - }, - "node_modules/@rc-component/tabs": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@rc-component/tabs/-/tabs-1.7.0.tgz", - "integrity": "sha512-J48cs2iBi7Ho3nptBxxIqizEliUC+ExE23faspUQKGQ550vaBlv3aGF8Epv/UB1vFWeoJDTW/dNzgIU0Qj5i/w==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/dropdown": "~1.0.0", - "@rc-component/menu": "~1.2.0", - "@rc-component/motion": "^1.1.3", - "@rc-component/resize-observer": "^1.0.0", - "@rc-component/util": "^1.3.0", - "clsx": "^2.1.1" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/textarea": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@rc-component/textarea/-/textarea-1.1.2.tgz", - "integrity": "sha512-9rMUEODWZDMovfScIEHXWlVZuPljZ2pd1LKNjslJVitn4SldEzq5vO1CL3yy3Dnib6zZal2r2DPtjy84VVpF6A==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/input": "~1.1.0", - "@rc-component/resize-observer": "^1.0.0", - "@rc-component/util": "^1.3.0", - "clsx": "^2.1.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/tooltip": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@rc-component/tooltip/-/tooltip-1.4.0.tgz", - "integrity": "sha512-8Rx5DCctIlLI4raR0I0xHjVTf1aF48+gKCNeAAo5bmF5VoR5YED+A/XEqzXv9KKqrJDRcd3Wndpxh2hyzrTtSg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/trigger": "^3.7.1", - "@rc-component/util": "^1.3.0", - "clsx": "^2.1.1" - }, - "peerDependencies": { - "react": ">=18.0.0", - "react-dom": ">=18.0.0" - } - }, - "node_modules/@rc-component/tour": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-2.3.0.tgz", - "integrity": "sha512-K04K9r32kUC+auBSQfr+Fss4SpSIS9JGe56oq/ALAX0p+i2ylYOI1MgR83yBY7v96eO6ZFXcM/igCQmubps0Ow==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/portal": "^2.2.0", - "@rc-component/trigger": "^3.0.0", - "@rc-component/util": "^1.7.0", - "clsx": "^2.1.1" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/tree": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rc-component/tree/-/tree-1.2.3.tgz", - "integrity": "sha512-mG8hF2ogQcKaEpfyxzPvMWqqkptofd7Sf+YiXOpPzuXLTLwNKfLDJtysc1/oybopbnzxNqWh2Vgwi+GYwNIb7w==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/motion": "^1.0.0", - "@rc-component/util": "^1.8.1", - "@rc-component/virtual-list": "^1.0.1", - "clsx": "^2.1.1" - }, - "engines": { - "node": ">=10.x" - }, - "peerDependencies": { - "react": "*", - "react-dom": "*" - } - }, - "node_modules/@rc-component/tree-select": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@rc-component/tree-select/-/tree-select-1.8.0.tgz", - "integrity": "sha512-iYsPq3nuLYvGqdvFAW+l+I9ASRIOVbMXyA8FGZg2lGym/GwkaWeJGzI4eJ7c9IOEhRj0oyfIN4S92Fl3J05mjQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/select": "~1.6.0", - "@rc-component/tree": "~1.2.0", - "@rc-component/util": "^1.4.0", - "clsx": "^2.1.1" - }, - "peerDependencies": { - "react": "*", - "react-dom": "*" - } - }, - "node_modules/@rc-component/trigger": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-3.9.0.tgz", - "integrity": "sha512-X8btpwfrT27AgrZVOz4swclhEHTZcqaHeQMXXBgveagOiakTa36uObXbdwerXffgV8G9dH1fAAE0DHtVQs8EHg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/motion": "^1.1.4", - "@rc-component/portal": "^2.2.0", - "@rc-component/resize-observer": "^1.1.1", - "@rc-component/util": "^1.2.1", - "clsx": "^2.1.1" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=18.0.0", - "react-dom": ">=18.0.0" - } - }, - "node_modules/@rc-component/upload": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rc-component/upload/-/upload-1.1.0.tgz", - "integrity": "sha512-LIBV90mAnUE6VK5N4QvForoxZc4XqEYZimcp7fk+lkE4XwHHyJWxpIXQQwMU8hJM+YwBbsoZkGksL1sISWHQxw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/util": "^1.3.0", - "clsx": "^2.1.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/util": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.9.0.tgz", - "integrity": "sha512-5uW6AfhIigCWeEQDthTozlxiT4Prn6xYQWeO0xokjcaa186OtwPRHBZJ2o0T0FhbjGhZ3vXdbkv0sx3gAYW7Vg==", - "license": "MIT", - "dependencies": { - "is-mobile": "^5.0.0", - "react-is": "^18.2.0" - }, - "peerDependencies": { - "react": ">=18.0.0", - "react-dom": ">=18.0.0" - } - }, - "node_modules/@rc-component/util/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "license": "MIT" - }, - "node_modules/@rc-component/virtual-list": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rc-component/virtual-list/-/virtual-list-1.0.2.tgz", - "integrity": "sha512-uvTol/mH74FYsn5loDGJxo+7kjkO4i+y4j87Re1pxJBs0FaeuMuLRzQRGaXwnMcV1CxpZLi2Z56Rerj2M00fjQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.20.0", - "@rc-component/resize-observer": "^1.0.1", - "@rc-component/util": "^1.4.0", - "clsx": "^2.1.1" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@react-sigma/core": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@react-sigma/core/-/core-5.0.6.tgz", - "integrity": "sha512-Xu2qXyvDZIhmvGC1n8d7Kcxm5Ntcz4HbPIM7CPDD2e4h3s/oxVpVPX7wtsNreJRRPj9mK+3oqB6SWXNI4mTqVg==", - "license": "MIT", - "peerDependencies": { - "graphology": "^0.26.0", - "react": "^18.0.0 || ^19.0.0", - "sigma": "^3.0.2" - } - }, - "node_modules/@reduxjs/toolkit": { - "version": "2.11.2", - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@standard-schema/utils": "^0.3.0", - "immer": "^11.0.0", - "redux": "^5.0.1", - "redux-thunk": "^3.1.0", - "reselect": "^5.1.0" - }, - "peerDependencies": { - "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", - "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-redux": { - "optional": true - } - } - }, - "node_modules/@reduxjs/toolkit/node_modules/immer": { - "version": "11.1.4", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.1", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@shikijs/core": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.22.0.tgz", - "integrity": "sha512-iAlTtSDDbJiRpvgL5ugKEATDtHdUVkqgHDm/gbD2ZS9c88mx7G1zSYjjOxp5Qa0eaW0MAQosFRmJSk354PRoQA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@shikijs/types": "3.22.0", - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4", - "hast-util-to-html": "^9.0.5" - } - }, - "node_modules/@shikijs/engine-javascript": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.22.0.tgz", - "integrity": "sha512-jdKhfgW9CRtj3Tor0L7+yPwdG3CgP7W+ZEqSsojrMzCjD1e0IxIbwUMDDpYlVBlC08TACg4puwFGkZfLS+56Tw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@shikijs/types": "3.22.0", - "@shikijs/vscode-textmate": "^10.0.2", - "oniguruma-to-es": "^4.3.4" - } - }, - "node_modules/@shikijs/engine-oniguruma": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.22.0.tgz", - "integrity": "sha512-DyXsOG0vGtNtl7ygvabHd7Mt5EY8gCNqR9Y7Lpbbd/PbJvgWrqaKzH1JW6H6qFkuUa8aCxoiYVv8/YfFljiQxA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@shikijs/types": "3.22.0", - "@shikijs/vscode-textmate": "^10.0.2" - } - }, - "node_modules/@shikijs/langs": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.22.0.tgz", - "integrity": "sha512-x/42TfhWmp6H00T6uwVrdTJGKgNdFbrEdhaDwSR5fd5zhQ1Q46bHq9EO61SCEWJR0HY7z2HNDMaBZp8JRmKiIA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@shikijs/types": "3.22.0" - } - }, - "node_modules/@shikijs/themes": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.22.0.tgz", - "integrity": "sha512-o+tlOKqsr6FE4+mYJG08tfCFDS+3CG20HbldXeVoyP+cYSUxDhrFf3GPjE60U55iOkkjbpY2uC3It/eeja35/g==", - "license": "MIT", - "peer": true, - "dependencies": { - "@shikijs/types": "3.22.0" - } - }, - "node_modules/@shikijs/transformers": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-3.22.0.tgz", - "integrity": "sha512-E7eRV7mwDBjueLF6852n2oYeJYxBq3NSsDk+uyruYAXONv4U8holGmIrT+mPRJQ1J1SNOH6L8G19KRzmBawrFw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@shikijs/core": "3.22.0", - "@shikijs/types": "3.22.0" - } - }, - "node_modules/@shikijs/types": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.22.0.tgz", - "integrity": "sha512-491iAekgKDBFE67z70Ok5a8KBMsQ2IJwOWw3us/7ffQkIBCyOQfm/aNwVMBUriP02QshIfgHCBSIYAl3u2eWjg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" - } - }, - "node_modules/@shikijs/vscode-textmate": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", - "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", - "license": "MIT", - "peer": true - }, - "node_modules/@splinetool/runtime": { - "version": "0.9.526", - "resolved": "https://registry.npmjs.org/@splinetool/runtime/-/runtime-0.9.526.tgz", - "integrity": "sha512-qznHbXA5aKwDbCgESAothCNm1IeEZcmNWG145p5aXj4w5uoqR1TZ9qkTHTKLTsUbHeitCwdhzmRqan1kxboLgQ==", - "peer": true, - "dependencies": { - "on-change": "^4.0.0", - "semver-compare": "^1.0.0" - } - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "license": "MIT" - }, - "node_modules/@standard-schema/utils": { - "version": "0.3.0", - "license": "MIT" - }, - "node_modules/@stitches/react": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@stitches/react/-/react-1.2.8.tgz", - "integrity": "sha512-9g9dWI4gsSVe8bNLlb+lMkBYsnIKCZTmvqvDG+Avnn69XfmHZKiaMrx7cgTaddq7aTPPmXiTsbFcUy0xgI4+wA==", - "license": "MIT", - "peer": true, - "peerDependencies": { - "react": ">= 16.3.0" - } - }, - "node_modules/@tanstack/history": { - "version": "1.154.14", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/query-core": { - "version": "5.90.20", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/react-query": { - "version": "5.90.21", - "license": "MIT", - "dependencies": { - "@tanstack/query-core": "5.90.20" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^18 || ^19" - } - }, - "node_modules/@tanstack/react-router": { - "version": "1.159.5", - "license": "MIT", - "dependencies": { - "@tanstack/history": "1.154.14", - "@tanstack/react-store": "^0.8.0", - "@tanstack/router-core": "1.159.4", - "isbot": "^5.1.22", - "tiny-invariant": "^1.3.3", - "tiny-warning": "^1.0.3" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": ">=18.0.0 || >=19.0.0", - "react-dom": ">=18.0.0 || >=19.0.0" - } - }, - "node_modules/@tanstack/react-router-devtools": { - "version": "1.159.5", - "license": "MIT", - "dependencies": { - "@tanstack/router-devtools-core": "1.159.4" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "@tanstack/react-router": "^1.159.5", - "@tanstack/router-core": "^1.159.4", - "react": ">=18.0.0 || >=19.0.0", - "react-dom": ">=18.0.0 || >=19.0.0" - }, - "peerDependenciesMeta": { - "@tanstack/router-core": { - "optional": true - } - } - }, - "node_modules/@tanstack/react-store": { - "version": "0.8.0", - "license": "MIT", - "dependencies": { - "@tanstack/store": "0.8.0", - "use-sync-external-store": "^1.6.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/@tanstack/react-virtual": { - "version": "3.13.18", - "license": "MIT", - "dependencies": { - "@tanstack/virtual-core": "3.13.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/@tanstack/router-core": { - "version": "1.159.4", - "license": "MIT", - "dependencies": { - "@tanstack/history": "1.154.14", - "@tanstack/store": "^0.8.0", - "cookie-es": "^2.0.0", - "seroval": "^1.4.2", - "seroval-plugins": "^1.4.2", - "tiny-invariant": "^1.3.3", - "tiny-warning": "^1.0.3" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/router-devtools": { - "version": "1.159.5", - "license": "MIT", - "dependencies": { - "@tanstack/react-router-devtools": "1.159.5", - "clsx": "^2.1.1", - "goober": "^2.1.16" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "@tanstack/react-router": "^1.159.5", - "csstype": "^3.0.10", - "react": ">=18.0.0 || >=19.0.0", - "react-dom": ">=18.0.0 || >=19.0.0" - }, - "peerDependenciesMeta": { - "csstype": { - "optional": true - } - } - }, - "node_modules/@tanstack/router-devtools-core": { - "version": "1.159.4", - "license": "MIT", - "dependencies": { - "clsx": "^2.1.1", - "goober": "^2.1.16", - "tiny-invariant": "^1.3.3" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "@tanstack/router-core": "^1.159.4", - "csstype": "^3.0.10" - }, - "peerDependenciesMeta": { - "csstype": { - "optional": true - } - } - }, - "node_modules/@tanstack/store": { - "version": "0.8.0", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/virtual-core": { - "version": "3.13.18", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/d3": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", - "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/d3-array": "*", - "@types/d3-axis": "*", - "@types/d3-brush": "*", - "@types/d3-chord": "*", - "@types/d3-color": "*", - "@types/d3-contour": "*", - "@types/d3-delaunay": "*", - "@types/d3-dispatch": "*", - "@types/d3-drag": "*", - "@types/d3-dsv": "*", - "@types/d3-ease": "*", - "@types/d3-fetch": "*", - "@types/d3-force": "*", - "@types/d3-format": "*", - "@types/d3-geo": "*", - "@types/d3-hierarchy": "*", - "@types/d3-interpolate": "*", - "@types/d3-path": "*", - "@types/d3-polygon": "*", - "@types/d3-quadtree": "*", - "@types/d3-random": "*", - "@types/d3-scale": "*", - "@types/d3-scale-chromatic": "*", - "@types/d3-selection": "*", - "@types/d3-shape": "*", - "@types/d3-time": "*", - "@types/d3-time-format": "*", - "@types/d3-timer": "*", - "@types/d3-transition": "*", - "@types/d3-zoom": "*" - } - }, - "node_modules/@types/d3-array": { - "version": "3.2.2", - "license": "MIT" - }, - "node_modules/@types/d3-axis": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", - "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-brush": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", - "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-chord": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", - "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", - "license": "MIT", - "peer": true - }, - "node_modules/@types/d3-color": { - "version": "3.1.3", - "license": "MIT" - }, - "node_modules/@types/d3-contour": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", - "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/d3-array": "*", - "@types/geojson": "*" - } - }, - "node_modules/@types/d3-delaunay": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", - "license": "MIT", - "peer": true - }, - "node_modules/@types/d3-dispatch": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", - "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", - "license": "MIT", - "peer": true - }, - "node_modules/@types/d3-drag": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", - "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-dsv": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", - "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", - "license": "MIT", - "peer": true - }, - "node_modules/@types/d3-ease": { - "version": "3.0.2", - "license": "MIT" - }, - "node_modules/@types/d3-fetch": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", - "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/d3-dsv": "*" - } - }, - "node_modules/@types/d3-force": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", - "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", - "license": "MIT", - "peer": true - }, - "node_modules/@types/d3-format": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", - "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", - "license": "MIT", - "peer": true - }, - "node_modules/@types/d3-geo": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", - "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/geojson": "*" - } - }, - "node_modules/@types/d3-hierarchy": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", - "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", - "license": "MIT", - "peer": true - }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.4", - "license": "MIT", - "dependencies": { - "@types/d3-color": "*" - } - }, - "node_modules/@types/d3-path": { - "version": "3.1.1", - "license": "MIT" - }, - "node_modules/@types/d3-polygon": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", - "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", - "license": "MIT", - "peer": true - }, - "node_modules/@types/d3-quadtree": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", - "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", - "license": "MIT", - "peer": true - }, - "node_modules/@types/d3-random": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", - "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", - "license": "MIT", - "peer": true - }, - "node_modules/@types/d3-scale": { - "version": "4.0.9", - "license": "MIT", - "dependencies": { - "@types/d3-time": "*" - } - }, - "node_modules/@types/d3-scale-chromatic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", - "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", - "license": "MIT", - "peer": true - }, - "node_modules/@types/d3-selection": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", - "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", - "license": "MIT" - }, - "node_modules/@types/d3-shape": { - "version": "3.1.8", - "license": "MIT", - "dependencies": { - "@types/d3-path": "*" - } - }, - "node_modules/@types/d3-time": { - "version": "3.0.4", - "license": "MIT" - }, - "node_modules/@types/d3-time-format": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", - "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", - "license": "MIT", - "peer": true - }, - "node_modules/@types/d3-timer": { - "version": "3.0.2", - "license": "MIT" - }, - "node_modules/@types/d3-transition": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", - "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-zoom": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", - "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", - "license": "MIT", - "dependencies": { - "@types/d3-interpolate": "*", - "@types/d3-selection": "*" - } - }, - "node_modules/@types/debug": { - "version": "4.1.12", - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "license": "MIT" - }, - "node_modules/@types/estree-jsx": { - "version": "1.0.5", - "license": "MIT", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/@types/geojson": { - "version": "7946.0.16", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", - "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", - "license": "MIT", - "peer": true - }, - "node_modules/@types/hast": { - "version": "3.0.4", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/js-cookie": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz", - "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==", - "license": "MIT", - "peer": true - }, - "node_modules/@types/katex": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz", - "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", - "license": "MIT", - "peer": true - }, - "node_modules/@types/mdast": { - "version": "4.0.4", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/mdx": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", - "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", - "license": "MIT", - "peer": true - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "license": "MIT" - }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "19.2.14", - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "devOptional": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT", - "peer": true - }, - "node_modules/@types/unist": { - "version": "3.0.3", - "license": "MIT" - }, - "node_modules/@types/use-sync-external-store": { - "version": "0.0.6", - "license": "MIT" - }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "license": "ISC" - }, - "node_modules/@use-gesture/core": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz", - "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==", - "license": "MIT", - "peer": true - }, - "node_modules/@use-gesture/react": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz", - "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==", - "license": "MIT", - "peer": true, - "dependencies": { - "@use-gesture/core": "10.3.1" - }, - "peerDependencies": { - "react": ">= 16.8.0" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/@xyflow/react": { - "version": "12.10.1", - "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.1.tgz", - "integrity": "sha512-5eSWtIK/+rkldOuFbOOz44CRgQRjtS9v5nufk77DV+XBnfCGL9HAQ8PG00o2ZYKqkEU/Ak6wrKC95Tu+2zuK3Q==", - "license": "MIT", - "dependencies": { - "@xyflow/system": "0.0.75", - "classcat": "^5.0.3", - "zustand": "^4.4.0" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, - "node_modules/@xyflow/system": { - "version": "0.0.75", - "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.75.tgz", - "integrity": "sha512-iXs+AGFLi8w/VlAoc/iSxk+CxfT6o64Uw/k0CKASOPqjqz6E0rb5jFZgJtXGZCpfQI6OQpu5EnumP5fGxQheaQ==", - "license": "MIT", - "dependencies": { - "@types/d3-drag": "^3.0.7", - "@types/d3-interpolate": "^3.0.4", - "@types/d3-selection": "^3.0.10", - "@types/d3-transition": "^3.0.8", - "@types/d3-zoom": "^3.0.8", - "d3-drag": "^3.0.0", - "d3-interpolate": "^3.0.1", - "d3-selection": "^3.0.0", - "d3-zoom": "^3.0.0" - } - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "license": "MIT", - "peer": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "license": "MIT", - "peer": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ahooks": { - "version": "3.9.6", - "resolved": "https://registry.npmjs.org/ahooks/-/ahooks-3.9.6.tgz", - "integrity": "sha512-Mr7f05swd5SmKlR9SZo5U6M0LsL4ErweLzpdgXjA1JPmnZ78Vr6wzx0jUtvoxrcqGKYnX0Yjc02iEASVxHFPjQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.21.0", - "@types/js-cookie": "^3.0.6", - "dayjs": "^1.9.1", - "intersection-observer": "^0.12.0", - "js-cookie": "^3.0.5", - "lodash": "^4.17.21", - "react-fast-compare": "^3.2.2", - "resize-observer-polyfill": "^1.5.1", - "screenfull": "^5.0.0", - "tslib": "^2.4.1" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/antd": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/antd/-/antd-6.3.0.tgz", - "integrity": "sha512-bbHJcASrRHp02wTpr940KtUHlTT6tvmaD4OAjqgOJXNmTQ/+qBDdBVWY/yeDV41p/WbWjTLlaqRGVbL3UEVpNw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@ant-design/colors": "^8.0.1", - "@ant-design/cssinjs": "^2.1.0", - "@ant-design/cssinjs-utils": "^2.1.1", - "@ant-design/fast-color": "^3.0.1", - "@ant-design/icons": "^6.1.0", - "@ant-design/react-slick": "~2.0.0", - "@babel/runtime": "^7.28.4", - "@rc-component/cascader": "~1.14.0", - "@rc-component/checkbox": "~2.0.0", - "@rc-component/collapse": "~1.2.0", - "@rc-component/color-picker": "~3.1.0", - "@rc-component/dialog": "~1.8.4", - "@rc-component/drawer": "~1.4.2", - "@rc-component/dropdown": "~1.0.2", - "@rc-component/form": "~1.6.2", - "@rc-component/image": "~1.6.0", - "@rc-component/input": "~1.1.2", - "@rc-component/input-number": "~1.6.2", - "@rc-component/mentions": "~1.6.0", - "@rc-component/menu": "~1.2.0", - "@rc-component/motion": "~1.1.6", - "@rc-component/mutate-observer": "^2.0.1", - "@rc-component/notification": "~1.2.0", - "@rc-component/pagination": "~1.2.0", - "@rc-component/picker": "~1.9.0", - "@rc-component/progress": "~1.0.2", - "@rc-component/qrcode": "~1.1.1", - "@rc-component/rate": "~1.0.1", - "@rc-component/resize-observer": "^1.1.1", - "@rc-component/segmented": "~1.3.0", - "@rc-component/select": "~1.6.5", - "@rc-component/slider": "~1.0.1", - "@rc-component/steps": "~1.2.2", - "@rc-component/switch": "~1.0.3", - "@rc-component/table": "~1.9.1", - "@rc-component/tabs": "~1.7.0", - "@rc-component/textarea": "~1.1.2", - "@rc-component/tooltip": "~1.4.0", - "@rc-component/tour": "~2.3.0", - "@rc-component/tree": "~1.2.3", - "@rc-component/tree-select": "~1.8.0", - "@rc-component/trigger": "^3.9.0", - "@rc-component/upload": "~1.1.0", - "@rc-component/util": "^1.9.0", - "clsx": "^2.1.1", - "dayjs": "^1.11.11", - "scroll-into-view-if-needed": "^3.1.0", - "throttle-debounce": "^5.0.2" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ant-design" - }, - "peerDependencies": { - "react": ">=18.0.0", - "react-dom": ">=18.0.0" - } - }, - "node_modules/antd-style": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/antd-style/-/antd-style-4.1.0.tgz", - "integrity": "sha512-vnPBGg0OVlSz90KRYZhxd89aZiOImTiesF+9MQqN8jsLGZUQTjbP04X9jTdEfsztKUuMbBWg/RmB/wHTakbtMQ==", - "license": "MIT", - "dependencies": { - "@ant-design/cssinjs": "^2.0.0", - "@babel/runtime": "^7.24.1", - "@emotion/cache": "^11.11.0", - "@emotion/css": "^11.11.2", - "@emotion/react": "^11.11.4", - "@emotion/serialize": "^1.1.3", - "@emotion/utils": "^1.2.1", - "use-merge-value": "^1.2.0" - }, - "peerDependencies": { - "antd": ">=6.0.0", - "react": ">=18" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "dev": true, - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/aria-hidden": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", - "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/astring": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", - "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", - "license": "MIT", - "peer": true, - "bin": { - "astring": "bin/astring" - } - }, - "node_modules/attr-accept": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", - "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/autoprefixer": { - "version": "10.4.24", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.28.1", - "caniuse-lite": "^1.0.30001766", - "fraction.js": "^5.3.4", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/babel-plugin-macros": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5", - "cosmiconfig": "^7.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - } - }, - "node_modules/bail": { - "version": "2.0.2", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.9.19", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.28.1", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001769", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/ccount": { - "version": "2.0.1", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities": { - "version": "2.0.2", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-html4": { - "version": "2.1.0", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-legacy": { - "version": "3.0.0", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-reference-invalid": { - "version": "2.0.1", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/chevrotain": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.1.tgz", - "integrity": "sha512-f0yv5CPKaFxfsPTBzX7vGuim4oIC1/gcS7LUGdBSwl2dU6+FON6LVUksdOo1qJjoUvXNn45urgh8C+0a24pACQ==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@chevrotain/cst-dts-gen": "11.1.1", - "@chevrotain/gast": "11.1.1", - "@chevrotain/regexp-to-ast": "11.1.1", - "@chevrotain/types": "11.1.1", - "@chevrotain/utils": "11.1.1", - "lodash-es": "4.17.23" - } - }, - "node_modules/chevrotain-allstar": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", - "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", - "license": "MIT", - "peer": true, - "dependencies": { - "lodash-es": "^4.17.21" - }, - "peerDependencies": { - "chevrotain": "^11.0.0" - } - }, - "node_modules/chokidar": { - "version": "4.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/chroma-js": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-3.2.0.tgz", - "integrity": "sha512-os/OippSlX1RlWWr+QDPcGUZs0uoqr32urfxESG9U93lhUfbnlyckte84Q8P1UQY/qth983AS1JONKmLS4T0nw==", - "license": "(BSD-3-Clause AND Apache-2.0)", - "peer": true - }, - "node_modules/class-variance-authority": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", - "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", - "license": "Apache-2.0", - "dependencies": { - "clsx": "^2.1.1" - }, - "funding": { - "url": "https://polar.sh/cva" - } - }, - "node_modules/classcat": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", - "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", - "license": "MIT" - }, - "node_modules/classnames": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", - "license": "MIT", - "peer": true - }, - "node_modules/clsx": { - "version": "2.1.1", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/codemirror": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", - "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/commands": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/lint": "^6.0.0", - "@codemirror/search": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0" - } - }, - "node_modules/collapse-white-space": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", - "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", - "license": "MIT", - "peer": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/colord": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", - "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", - "license": "MIT", - "peer": true - }, - "node_modules/comma-separated-tokens": { - "version": "2.0.3", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/commander": { - "version": "4.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/compute-scroll-into-view": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", - "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", - "license": "MIT", - "peer": true - }, - "node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "license": "MIT", - "peer": true - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie-es": { - "version": "2.0.0", - "license": "MIT" - }, - "node_modules/cose-base": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", - "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", - "license": "MIT", - "peer": true, - "dependencies": { - "layout-base": "^1.0.0" - } - }, - "node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "license": "MIT", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cosmiconfig/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/crelt": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", - "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", - "license": "MIT" - }, - "node_modules/cssesc": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "license": "MIT" - }, - "node_modules/cytoscape": { - "version": "3.33.1", - "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", - "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/cytoscape-cose-bilkent": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", - "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "cose-base": "^1.0.0" - }, - "peerDependencies": { - "cytoscape": "^3.2.0" - } - }, - "node_modules/cytoscape-fcose": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", - "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "cose-base": "^2.2.0" - }, - "peerDependencies": { - "cytoscape": "^3.2.0" - } - }, - "node_modules/cytoscape-fcose/node_modules/cose-base": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", - "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", - "license": "MIT", - "peer": true, - "dependencies": { - "layout-base": "^2.0.0" - } - }, - "node_modules/cytoscape-fcose/node_modules/layout-base": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", - "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", - "license": "MIT", - "peer": true - }, - "node_modules/d3": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", - "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", - "license": "ISC", - "peer": true, - "dependencies": { - "d3-array": "3", - "d3-axis": "3", - "d3-brush": "3", - "d3-chord": "3", - "d3-color": "3", - "d3-contour": "4", - "d3-delaunay": "6", - "d3-dispatch": "3", - "d3-drag": "3", - "d3-dsv": "3", - "d3-ease": "3", - "d3-fetch": "3", - "d3-force": "3", - "d3-format": "3", - "d3-geo": "3", - "d3-hierarchy": "3", - "d3-interpolate": "3", - "d3-path": "3", - "d3-polygon": "3", - "d3-quadtree": "3", - "d3-random": "3", - "d3-scale": "4", - "d3-scale-chromatic": "3", - "d3-selection": "3", - "d3-shape": "3", - "d3-time": "3", - "d3-time-format": "4", - "d3-timer": "3", - "d3-transition": "3", - "d3-zoom": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-array": { - "version": "3.2.4", - "license": "ISC", - "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-axis": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", - "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", - "license": "ISC", - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-brush": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", - "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", - "license": "ISC", - "peer": true, - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "3", - "d3-transition": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-chord": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", - "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", - "license": "ISC", - "peer": true, - "dependencies": { - "d3-path": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-color": { - "version": "3.1.0", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-contour": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", - "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", - "license": "ISC", - "peer": true, - "dependencies": { - "d3-array": "^3.2.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-delaunay": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", - "license": "ISC", - "peer": true, - "dependencies": { - "delaunator": "5" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dispatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", - "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-drag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", - "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-selection": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dsv": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", - "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", - "license": "ISC", - "peer": true, - "dependencies": { - "commander": "7", - "iconv-lite": "0.6", - "rw": "1" - }, - "bin": { - "csv2json": "bin/dsv2json.js", - "csv2tsv": "bin/dsv2dsv.js", - "dsv2dsv": "bin/dsv2dsv.js", - "dsv2json": "bin/dsv2json.js", - "json2csv": "bin/json2dsv.js", - "json2dsv": "bin/json2dsv.js", - "json2tsv": "bin/json2dsv.js", - "tsv2csv": "bin/dsv2dsv.js", - "tsv2json": "bin/dsv2json.js" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dsv/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/d3-ease": { - "version": "3.0.1", - "license": "BSD-3-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-fetch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", - "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", - "license": "ISC", - "peer": true, - "dependencies": { - "d3-dsv": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-force": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", - "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", - "license": "ISC", - "peer": true, - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-quadtree": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-format": { - "version": "3.1.2", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-geo": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", - "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", - "license": "ISC", - "peer": true, - "dependencies": { - "d3-array": "2.5.0 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-hierarchy": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", - "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", - "license": "ISC", - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-path": { - "version": "3.1.0", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-polygon": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", - "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", - "license": "ISC", - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-quadtree": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", - "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", - "license": "ISC", - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-random": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", - "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", - "license": "ISC", - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-sankey": { - "version": "0.12.3", - "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", - "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", - "license": "BSD-3-Clause", - "peer": true, - "dependencies": { - "d3-array": "1 - 2", - "d3-shape": "^1.2.0" - } - }, - "node_modules/d3-sankey/node_modules/d3-array": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", - "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", - "license": "BSD-3-Clause", - "peer": true, - "dependencies": { - "internmap": "^1.0.0" - } - }, - "node_modules/d3-sankey/node_modules/d3-path": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", - "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", - "license": "BSD-3-Clause", - "peer": true - }, - "node_modules/d3-sankey/node_modules/d3-shape": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", - "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", - "license": "BSD-3-Clause", - "peer": true, - "dependencies": { - "d3-path": "1" - } - }, - "node_modules/d3-sankey/node_modules/internmap": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", - "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", - "license": "ISC", - "peer": true - }, - "node_modules/d3-scale": { - "version": "4.0.2", - "license": "ISC", - "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-scale-chromatic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", - "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", - "license": "ISC", - "peer": true, - "dependencies": { - "d3-color": "1 - 3", - "d3-interpolate": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-selection": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-shape": { - "version": "3.2.0", - "license": "ISC", - "dependencies": { - "d3-path": "^3.1.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time": { - "version": "3.1.0", - "license": "ISC", - "dependencies": { - "d3-array": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "license": "ISC", - "dependencies": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-timer": { - "version": "3.0.1", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-transition": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", - "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3", - "d3-dispatch": "1 - 3", - "d3-ease": "1 - 3", - "d3-interpolate": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "d3-selection": "2 - 3" - } - }, - "node_modules/d3-zoom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", - "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "2 - 3", - "d3-transition": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/dagre-d3-es": { - "version": "7.0.13", - "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.13.tgz", - "integrity": "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==", - "license": "MIT", - "peer": true, - "dependencies": { - "d3": "^7.9.0", - "lodash-es": "^4.17.21" - } - }, - "node_modules/dayjs": { - "version": "1.11.19", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", - "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", - "license": "MIT", - "peer": true - }, - "node_modules/debug": { - "version": "4.4.3", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decimal.js-light": { - "version": "2.5.1", - "license": "MIT" - }, - "node_modules/decode-named-character-reference": { - "version": "1.3.0", - "license": "MIT", - "dependencies": { - "character-entities": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/decode-uri-component": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz", - "integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=14.16" - } - }, - "node_modules/delaunator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", - "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", - "license": "ISC", - "peer": true, - "dependencies": { - "robust-predicates": "^3.0.2" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", - "license": "MIT" - }, - "node_modules/devlop": { - "version": "1.1.0", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/diff": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", - "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", - "license": "BSD-3-Clause", - "peer": true, - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/dlv": { - "version": "1.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/dompurify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", - "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", - "license": "(MPL-2.0 OR Apache-2.0)", - "peer": true, - "optionalDependencies": { - "@types/trusted-types": "^2.0.7" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.286", - "dev": true, - "license": "ISC" - }, - "node_modules/emoji-mart": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-5.6.0.tgz", - "integrity": "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==", - "license": "MIT", - "peer": true - }, - "node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "license": "MIT", - "peer": true - }, - "node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "license": "BSD-2-Clause", - "peer": true, - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-toolkit": { - "version": "1.44.0", - "license": "MIT", - "workspaces": [ - "docs", - "benchmarks" - ] - }, - "node_modules/esast-util-from-estree": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", - "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "devlop": "^1.0.0", - "estree-util-visit": "^2.0.0", - "unist-util-position-from-estree": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/esast-util-from-js": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", - "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "acorn": "^8.0.0", - "esast-util-from-estree": "^2.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/esbuild": { - "version": "0.25.12", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "5.0.0", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/estree-util-attach-comments": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", - "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/estree": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-build-jsx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", - "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "estree-walker": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-is-identifier-name": { - "version": "3.0.0", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-scope": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", - "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-to-js": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", - "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "astring": "^1.8.0", - "source-map": "^0.7.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-visit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", - "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/eventemitter3": { - "version": "5.0.4", - "license": "MIT" - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "license": "MIT" - }, - "node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "license": "MIT", - "peer": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extend-shallow/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT", - "peer": true - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fastq": { - "version": "1.20.1", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/file-selector": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.5.0.tgz", - "integrity": "sha512-s8KNnmIDTBoD0p9uJ9uD0XY38SCeBOtj0UMXyQSLg1Ypfrfj8+dAvwsLjYQkQ2GjhVtp2HrnF5cJzMhBjfD8HA==", - "license": "MIT", - "peer": true, - "dependencies": { - "tslib": "^2.0.3" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/filter-obj": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz", - "integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "license": "MIT" - }, - "node_modules/for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fraction.js": { - "version": "5.3.4", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/framer-motion": { - "version": "12.34.3", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.34.3.tgz", - "integrity": "sha512-v81ecyZKYO/DfpTwHivqkxSUBzvceOpoI+wLfgCgoUIKxlFKEXdg0oR9imxwXumT4SFy8vRk9xzJ5l3/Du/55Q==", - "license": "MIT", - "dependencies": { - "motion-dom": "^12.34.3", - "motion-utils": "^12.29.2", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "@emotion/is-prop-valid": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/is-prop-valid": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-east-asian-width": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", - "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/giscus": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/giscus/-/giscus-1.6.0.tgz", - "integrity": "sha512-Zrsi8r4t1LVW950keaWcsURuZUQwUaMKjvJgTCY125vkW6OiEBkatE7ScJDbpqKHdZwb///7FVC21SE3iFK3PQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "lit": "^3.2.1" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/goober": { - "version": "2.1.18", - "license": "MIT", - "peerDependencies": { - "csstype": "^3.0.10" - } - }, - "node_modules/graphology": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/graphology/-/graphology-0.26.0.tgz", - "integrity": "sha512-8SSImzgUUYC89Z042s+0r/vMibY7GX/Emz4LDO5e7jYXhuoWfHISPFJYjpRLUSJGq6UQ6xlenvX1p/hJdfXuXg==", - "license": "MIT", - "dependencies": { - "events": "^3.3.0" - }, - "peerDependencies": { - "graphology-types": ">=0.24.0" - } - }, - "node_modules/graphology-layout-forceatlas2": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/graphology-layout-forceatlas2/-/graphology-layout-forceatlas2-0.10.1.tgz", - "integrity": "sha512-ogzBeF1FvWzjkikrIFwxhlZXvD2+wlY54lqhsrWprcdPjopM2J9HoMweUmIgwaTvY4bUYVimpSsOdvDv1gPRFQ==", - "license": "MIT", - "dependencies": { - "graphology-utils": "^2.1.0" - }, - "peerDependencies": { - "graphology-types": ">=0.19.0" - } - }, - "node_modules/graphology-types": { - "version": "0.24.8", - "resolved": "https://registry.npmjs.org/graphology-types/-/graphology-types-0.24.8.tgz", - "integrity": "sha512-hDRKYXa8TsoZHjgEaysSRyPdT6uB78Ci8WnjgbStlQysz7xR52PInxNsmnB7IBOM1BhikxkNyCVEFgmPKnpx3Q==", - "license": "MIT" - }, - "node_modules/graphology-utils": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/graphology-utils/-/graphology-utils-2.5.2.tgz", - "integrity": "sha512-ckHg8MXrXJkOARk56ZaSCM1g1Wihe2d6iTmz1enGOz4W/l831MBCKSayeFQfowgF8wd+PQ4rlch/56Vs/VZLDQ==", - "license": "MIT", - "peerDependencies": { - "graphology-types": ">=0.23.0" - } - }, - "node_modules/hachure-fill": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", - "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", - "license": "MIT", - "peer": true - }, - "node_modules/hasown": { - "version": "2.0.2", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hast-util-from-dom": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz", - "integrity": "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==", - "license": "ISC", - "peer": true, - "dependencies": { - "@types/hast": "^3.0.0", - "hastscript": "^9.0.0", - "web-namespaces": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-from-html": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", - "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/hast": "^3.0.0", - "devlop": "^1.1.0", - "hast-util-from-parse5": "^8.0.0", - "parse5": "^7.0.0", - "vfile": "^6.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-from-html-isomorphic": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz", - "integrity": "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-from-dom": "^5.0.0", - "hast-util-from-html": "^2.0.0", - "unist-util-remove-position": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-from-parse5": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", - "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "devlop": "^1.0.0", - "hastscript": "^9.0.0", - "property-information": "^7.0.0", - "vfile": "^6.0.0", - "vfile-location": "^5.0.0", - "web-namespaces": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-is-element": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", - "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-parse-selector": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", - "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-raw": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", - "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "@ungap/structured-clone": "^1.0.0", - "hast-util-from-parse5": "^8.0.0", - "hast-util-to-parse5": "^8.0.0", - "html-void-elements": "^3.0.0", - "mdast-util-to-hast": "^13.0.0", - "parse5": "^7.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0", - "web-namespaces": "^2.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-estree": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", - "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/estree": "^1.0.0", - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-attach-comments": "^3.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "hast-util-whitespace": "^3.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-js": "^1.0.0", - "unist-util-position": "^5.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-html": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", - "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-whitespace": "^3.0.0", - "html-void-elements": "^3.0.0", - "mdast-util-to-hast": "^13.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "stringify-entities": "^4.0.0", - "zwitch": "^2.0.4" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-jsx-runtime": { - "version": "2.3.6", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "hast-util-whitespace": "^3.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-js": "^1.0.0", - "unist-util-position": "^5.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-parse5": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", - "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "web-namespaces": "^2.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-text": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", - "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "hast-util-is-element": "^3.0.0", - "unist-util-find-after": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-whitespace": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hastscript": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", - "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-parse-selector": "^4.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "license": "BSD-3-Clause", - "dependencies": { - "react-is": "^16.7.0" - } - }, - "node_modules/hoist-non-react-statics/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, - "node_modules/html-url-attributes": { - "version": "3.0.1", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/html-void-elements": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", - "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", - "license": "MIT", - "peer": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "peer": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/immer": { - "version": "10.2.0", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/immutable": { - "version": "5.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/inline-style-parser": { - "version": "0.2.7", - "license": "MIT" - }, - "node_modules/internmap": { - "version": "2.0.3", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/intersection-observer": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.12.2.tgz", - "integrity": "sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==", - "deprecated": "The Intersection Observer polyfill is no longer needed and can safely be removed. Intersection Observer has been Baseline since 2019.", - "license": "Apache-2.0", - "peer": true - }, - "node_modules/is-alphabetical": { - "version": "2.0.1", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-alphanumerical": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "is-alphabetical": "^2.0.0", - "is-decimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT" - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-decimal": { - "version": "2.0.1", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "license": "MIT", - "peer": true, - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-hexadecimal": { - "version": "2.0.1", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-mobile": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-5.0.0.tgz", - "integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==", - "license": "MIT" - }, - "node_modules/is-number": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "license": "MIT", - "peer": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isbot": { - "version": "5.1.35", - "license": "Unlicense", - "engines": { - "node": ">=18" - } - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jiti": { - "version": "1.21.7", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/js-cookie": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", - "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "3.1.0", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT" - }, - "node_modules/json2mq": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", - "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", - "license": "MIT", - "peer": true, - "dependencies": { - "string-convert": "^0.2.0" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/katex": { - "version": "0.16.32", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.32.tgz", - "integrity": "sha512-ac0FzkRJlpw4WyH3Zu/OgU9LmPKqjHr6O2BxfSrBt8uJ1BhvH2YK3oJ4ut/K+O+6qQt2MGpdbn0MrffVEnnUDQ==", - "funding": [ - "https://opencollective.com/katex", - "https://github.com/sponsors/katex" - ], - "license": "MIT", - "peer": true, - "dependencies": { - "commander": "^8.3.0" - }, - "bin": { - "katex": "cli.js" - } - }, - "node_modules/katex/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/khroma": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", - "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==", - "peer": true - }, - "node_modules/langium": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/langium/-/langium-4.2.1.tgz", - "integrity": "sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "chevrotain": "~11.1.1", - "chevrotain-allstar": "~0.3.1", - "vscode-languageserver": "~9.0.1", - "vscode-languageserver-textdocument": "~1.0.11", - "vscode-uri": "~3.1.0" - }, - "engines": { - "node": ">=20.10.0", - "npm": ">=10.2.3" - } - }, - "node_modules/layout-base": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", - "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", - "license": "MIT", - "peer": true - }, - "node_modules/leva": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/leva/-/leva-0.10.1.tgz", - "integrity": "sha512-BcjnfUX8jpmwZUz2L7AfBtF9vn4ggTH33hmeufDULbP3YgNZ/C+ss/oO3stbrqRQyaOmRwy70y7BGTGO81S3rA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@radix-ui/react-portal": "^1.1.4", - "@radix-ui/react-tooltip": "^1.1.8", - "@stitches/react": "^1.2.8", - "@use-gesture/react": "^10.2.5", - "colord": "^2.9.2", - "dequal": "^2.0.2", - "merge-value": "^1.0.0", - "react-colorful": "^5.5.1", - "react-dropzone": "^12.0.0", - "v8n": "^1.3.3", - "zustand": "^3.6.9" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/leva/node_modules/zustand": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz", - "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12.7.0" - }, - "peerDependencies": { - "react": ">=16.8" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - } - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "license": "MIT" - }, - "node_modules/lit": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.2.tgz", - "integrity": "sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==", - "license": "BSD-3-Clause", - "peer": true, - "dependencies": { - "@lit/reactive-element": "^2.1.0", - "lit-element": "^4.2.0", - "lit-html": "^3.3.0" - } - }, - "node_modules/lit-element": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz", - "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==", - "license": "BSD-3-Clause", - "peer": true, - "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.5.0", - "@lit/reactive-element": "^2.1.0", - "lit-html": "^3.3.0" - } - }, - "node_modules/lit-html": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.2.tgz", - "integrity": "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==", - "license": "BSD-3-Clause", - "peer": true, - "dependencies": { - "@types/trusted-types": "^2.0.2" - } - }, - "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "license": "MIT", - "peer": true - }, - "node_modules/lodash-es": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", - "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", - "license": "MIT", - "peer": true - }, - "node_modules/longest-streak": { - "version": "3.1.0", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "peer": true, - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lru_map": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.4.1.tgz", - "integrity": "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg==", - "license": "MIT", - "peer": true - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lucide-react": { - "version": "0.562.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz", - "integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==", - "license": "ISC", - "peer": true, - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/markdown-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", - "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/markdown-table": { - "version": "3.0.4", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/marked": { - "version": "17.0.3", - "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.3.tgz", - "integrity": "sha512-jt1v2ObpyOKR8p4XaUJVk3YWRJ5n+i4+rjQopxvV32rSndTJXvIzuUdWWIy/1pFQMkQmvTXawzDNqOH/CUmx6A==", - "license": "MIT", - "peer": true, - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/mdast-util-find-and-replace": { - "version": "3.0.2", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "escape-string-regexp": "^5.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-from-markdown": { - "version": "2.0.2", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark": "^4.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm": { - "version": "3.1.0", - "license": "MIT", - "dependencies": { - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-gfm-autolink-literal": "^2.0.0", - "mdast-util-gfm-footnote": "^2.0.0", - "mdast-util-gfm-strikethrough": "^2.0.0", - "mdast-util-gfm-table": "^2.0.0", - "mdast-util-gfm-task-list-item": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-autolink-literal": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "ccount": "^2.0.0", - "devlop": "^1.0.0", - "mdast-util-find-and-replace": "^3.0.0", - "micromark-util-character": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-footnote": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-strikethrough": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-table": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "markdown-table": "^3.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-task-list-item": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-math": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz", - "integrity": "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "longest-streak": "^3.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.1.0", - "unist-util-remove-position": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", - "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", - "license": "MIT", - "peer": true, - "dependencies": { - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-expression": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-jsx": { - "version": "3.2.0", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "parse-entities": "^4.0.0", - "stringify-entities": "^4.0.0", - "unist-util-stringify-position": "^4.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdxjs-esm": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-newline-to-break": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-newline-to-break/-/mdast-util-newline-to-break-2.0.0.tgz", - "integrity": "sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-find-and-replace": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-phrasing": { - "version": "4.1.0", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-hast": { - "version": "13.2.1", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@ungap/structured-clone": "^1.0.0", - "devlop": "^1.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "trim-lines": "^3.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown": { - "version": "2.1.2", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^4.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "unist-util-visit": "^5.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-string": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/merge-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/merge-value/-/merge-value-1.0.0.tgz", - "integrity": "sha512-fJMmvat4NeKz63Uv9iHWcPDjCWcCkoiRoajRTEO8hlhUC6rwaHg0QCF9hBOTjZmm4JuglPckPSTtcuJL5kp0TQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "get-value": "^2.0.6", - "is-extendable": "^1.0.0", - "mixin-deep": "^1.2.0", - "set-value": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/mermaid": { - "version": "11.12.3", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.12.3.tgz", - "integrity": "sha512-wN5ZSgJQIC+CHJut9xaKWsknLxaFBwCPwPkGTSUYrTiHORWvpT8RxGk849HPnpUAQ+/9BPRqYb80jTpearrHzQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@braintree/sanitize-url": "^7.1.1", - "@iconify/utils": "^3.0.1", - "@mermaid-js/parser": "^1.0.0", - "@types/d3": "^7.4.3", - "cytoscape": "^3.29.3", - "cytoscape-cose-bilkent": "^4.1.0", - "cytoscape-fcose": "^2.2.0", - "d3": "^7.9.0", - "d3-sankey": "^0.12.3", - "dagre-d3-es": "7.0.13", - "dayjs": "^1.11.18", - "dompurify": "^3.2.5", - "katex": "^0.16.22", - "khroma": "^2.1.0", - "lodash-es": "^4.17.23", - "marked": "^16.2.1", - "roughjs": "^4.6.6", - "stylis": "^4.3.6", - "ts-dedent": "^2.2.0", - "uuid": "^11.1.0" - } - }, - "node_modules/mermaid/node_modules/marked": { - "version": "16.4.2", - "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", - "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", - "license": "MIT", - "peer": true, - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/mermaid/node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "peer": true, - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, - "node_modules/micromark": { - "version": "4.0.2", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-core-commonmark": { - "version": "2.0.3", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-destination": "^2.0.0", - "micromark-factory-label": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-factory-title": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-html-tag-name": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-cjk-friendly": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/micromark-extension-cjk-friendly/-/micromark-extension-cjk-friendly-1.2.3.tgz", - "integrity": "sha512-gRzVLUdjXBLX6zNPSnHGDoo+ZTp5zy+MZm0g3sv+3chPXY7l9gW+DnrcHcZh/jiPR6MjPKO4AEJNp4Aw6V9z5Q==", - "license": "MIT", - "peer": true, - "dependencies": { - "devlop": "^1.1.0", - "micromark-extension-cjk-friendly-util": "2.1.1", - "micromark-util-chunked": "^2.0.1", - "micromark-util-resolve-all": "^2.0.1", - "micromark-util-symbol": "^2.0.1" - }, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "micromark": "^4.0.0", - "micromark-util-types": "^2.0.0" - }, - "peerDependenciesMeta": { - "micromark-util-types": { - "optional": true - } - } - }, - "node_modules/micromark-extension-cjk-friendly-util": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-extension-cjk-friendly-util/-/micromark-extension-cjk-friendly-util-2.1.1.tgz", - "integrity": "sha512-egs6+12JU2yutskHY55FyR48ZiEcFOJFyk9rsiyIhcJ6IvWB6ABBqVrBw8IobqJTDZ/wdSr9eoXDPb5S2nW1bg==", - "license": "MIT", - "peer": true, - "dependencies": { - "get-east-asian-width": "^1.3.0", - "micromark-util-character": "^2.1.1", - "micromark-util-symbol": "^2.0.1" - }, - "engines": { - "node": ">=16" - }, - "peerDependenciesMeta": { - "micromark-util-types": { - "optional": true - } - } - }, - "node_modules/micromark-extension-gfm": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "micromark-extension-gfm-autolink-literal": "^2.0.0", - "micromark-extension-gfm-footnote": "^2.0.0", - "micromark-extension-gfm-strikethrough": "^2.0.0", - "micromark-extension-gfm-table": "^2.0.0", - "micromark-extension-gfm-tagfilter": "^2.0.0", - "micromark-extension-gfm-task-list-item": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-autolink-literal": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-footnote": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-strikethrough": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-table": { - "version": "2.1.1", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-tagfilter": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-task-list-item": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-math": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", - "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/katex": "^0.16.0", - "devlop": "^1.0.0", - "katex": "^0.16.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdx-expression": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", - "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-mdx-expression": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-events-to-acorn": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-mdx-jsx": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", - "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "micromark-factory-mdx-expression": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-events-to-acorn": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdx-md": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", - "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdxjs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", - "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "acorn": "^8.0.0", - "acorn-jsx": "^5.0.0", - "micromark-extension-mdx-expression": "^3.0.0", - "micromark-extension-mdx-jsx": "^3.0.0", - "micromark-extension-mdx-md": "^2.0.0", - "micromark-extension-mdxjs-esm": "^3.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdxjs-esm": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", - "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-events-to-acorn": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-position-from-estree": "^2.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-factory-destination": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-label": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-mdx-expression": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", - "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-events-to-acorn": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-position-from-estree": "^2.0.0", - "vfile-message": "^4.0.0" - } - }, - "node_modules/micromark-factory-space": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-title": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-whitespace": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-character": { - "version": "2.1.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-chunked": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-classify-character": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-combine-extensions": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "2.0.2", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-string": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-encode": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-events-to-acorn": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", - "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "@types/estree": "^1.0.0", - "@types/unist": "^3.0.0", - "devlop": "^1.0.0", - "estree-util-visit": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "vfile-message": "^4.0.0" - } - }, - "node_modules/micromark-util-html-tag-name": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-normalize-identifier": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-resolve-all": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-subtokenize": { - "version": "2.1.0", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-symbol": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-types": { - "version": "2.0.2", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromatch": { - "version": "4.0.8", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "license": "MIT", - "peer": true, - "dependencies": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mlly": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", - "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", - "license": "MIT", - "peer": true, - "dependencies": { - "acorn": "^8.15.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.1" - } - }, - "node_modules/motion": { - "version": "12.34.3", - "resolved": "https://registry.npmjs.org/motion/-/motion-12.34.3.tgz", - "integrity": "sha512-xZIkBGO7v/Uvm+EyaqYd+9IpXu0sZqLywVlGdCFrrMiaO9JI4Kx51mO9KlHSWwll+gZUVY5OJsWgYI5FywJ/tw==", - "license": "MIT", - "peer": true, - "dependencies": { - "framer-motion": "^12.34.3", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "@emotion/is-prop-valid": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/is-prop-valid": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, - "node_modules/motion-dom": { - "version": "12.34.3", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.34.3.tgz", - "integrity": "sha512-sYgFe+pR9aIM7o4fhs2aXtOI+oqlUd33N9Yoxcgo1Fv7M20sRkHtCmzE/VRNIcq7uNJ+qio+Xubt1FXH3pQ+eQ==", - "license": "MIT", - "dependencies": { - "motion-utils": "^12.29.2" - } - }, - "node_modules/motion-utils": { - "version": "12.29.2", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.29.2.tgz", - "integrity": "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==", - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/node-releases": { - "version": "2.0.27", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/numeral": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/numeral/-/numeral-2.0.6.tgz", - "integrity": "sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA==", - "license": "MIT", - "peer": true, - "engines": { - "node": "*" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/on-change": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/on-change/-/on-change-4.0.2.tgz", - "integrity": "sha512-cMtCyuJmTx/bg2HCpHo3ZLeF7FZnBOapLqZHr2AlLeJ5Ul0Zu2mUJJz051Fdwu/Et2YW04ZD+TtU+gVy0ACNCA==", - "license": "MIT", - "peer": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/on-change?sponsor=1" - } - }, - "node_modules/oniguruma-parser": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", - "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", - "license": "MIT", - "peer": true - }, - "node_modules/oniguruma-to-es": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.4.tgz", - "integrity": "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==", - "license": "MIT", - "peer": true, - "dependencies": { - "oniguruma-parser": "^0.12.1", - "regex": "^6.0.1", - "regex-recursion": "^6.0.2" - } - }, - "node_modules/package-manager-detector": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", - "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", - "license": "MIT", - "peer": true - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-entities": { - "version": "4.0.2", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "character-entities-legacy": "^3.0.0", - "character-reference-invalid": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "is-alphanumerical": "^2.0.0", - "is-decimal": "^2.0.0", - "is-hexadecimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/parse-entities/node_modules/@types/unist": { - "version": "2.0.11", - "license": "MIT" - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "license": "MIT", - "peer": true, - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/path-data-parser": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", - "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", - "license": "MIT", - "peer": true - }, - "node_modules/path-parse": { - "version": "1.0.7", - "license": "MIT" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "license": "MIT", - "peer": true - }, - "node_modules/picocolors": { - "version": "1.1.1", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, - "node_modules/points-on-curve": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", - "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", - "license": "MIT", - "peer": true - }, - "node_modules/points-on-path": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", - "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", - "license": "MIT", - "peer": true, - "dependencies": { - "path-data-parser": "0.1.0", - "points-on-curve": "0.2.0" - } - }, - "node_modules/polished": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/polished/-/polished-4.3.1.tgz", - "integrity": "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.17.8" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-import": { - "version": "15.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.1.0", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "6.0.1", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.1.1" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "jiti": ">=1.21.0", - "postcss": ">=8.0.9", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-nested/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "peer": true, - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT", - "peer": true - }, - "node_modules/property-information": { - "version": "7.1.0", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/query-string": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-9.3.1.tgz", - "integrity": "sha512-5fBfMOcDi5SA9qj5jZhWAcTtDfKF5WFdd2uD9nVNlbxVv1baq65aALy6qofpNEGELHvisjjasxQp7BlM9gvMzw==", - "license": "MIT", - "peer": true, - "dependencies": { - "decode-uri-component": "^0.4.1", - "filter-obj": "^5.1.0", - "split-on-first": "^3.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/rc-collapse": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-4.0.0.tgz", - "integrity": "sha512-SwoOByE39/3oIokDs/BnkqI+ltwirZbP8HZdq1/3SkPSBi7xDdvWHTp7cpNI9ullozkR6mwTWQi6/E/9huQVrA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.10.1", - "classnames": "2.x", - "rc-motion": "^2.3.4", - "rc-util": "^5.27.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-dialog": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.6.0.tgz", - "integrity": "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.10.1", - "@rc-component/portal": "^1.0.0-8", - "classnames": "^2.2.6", - "rc-motion": "^2.3.0", - "rc-util": "^5.21.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-dialog/node_modules/@rc-component/portal": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.2.tgz", - "integrity": "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.18.0", - "classnames": "^2.3.2", - "rc-util": "^5.24.4" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-footer": { - "version": "0.6.8", - "resolved": "https://registry.npmjs.org/rc-footer/-/rc-footer-0.6.8.tgz", - "integrity": "sha512-JBZ+xcb6kkex8XnBd4VHw1ZxjV6kmcwUumSHaIFdka2qzMCo7Klcy4sI6G0XtUpG/vtpislQCc+S9Bc+NLHYMg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.11.1", - "classnames": "^2.2.1" - }, - "peerDependencies": { - "react": ">=16.0.0", - "react-dom": ">=16.0.0" - } - }, - "node_modules/rc-image": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/rc-image/-/rc-image-7.12.0.tgz", - "integrity": "sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.11.2", - "@rc-component/portal": "^1.0.2", - "classnames": "^2.2.6", - "rc-dialog": "~9.6.0", - "rc-motion": "^2.6.2", - "rc-util": "^5.34.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-image/node_modules/@rc-component/portal": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.2.tgz", - "integrity": "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.18.0", - "classnames": "^2.3.2", - "rc-util": "^5.24.4" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-input": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.8.0.tgz", - "integrity": "sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.11.1", - "classnames": "^2.2.1", - "rc-util": "^5.18.1" - }, - "peerDependencies": { - "react": ">=16.0.0", - "react-dom": ">=16.0.0" - } - }, - "node_modules/rc-input-number": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.5.0.tgz", - "integrity": "sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.10.1", - "@rc-component/mini-decimal": "^1.0.1", - "classnames": "^2.2.5", - "rc-input": "~1.8.0", - "rc-util": "^5.40.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-menu": { - "version": "9.16.1", - "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.16.1.tgz", - "integrity": "sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.10.1", - "@rc-component/trigger": "^2.0.0", - "classnames": "2.x", - "rc-motion": "^2.4.3", - "rc-overflow": "^1.3.1", - "rc-util": "^5.27.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-menu/node_modules/@rc-component/portal": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.2.tgz", - "integrity": "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.18.0", - "classnames": "^2.3.2", - "rc-util": "^5.24.4" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-menu/node_modules/@rc-component/trigger": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.3.1.tgz", - "integrity": "sha512-ORENF39PeXTzM+gQEshuk460Z8N4+6DkjpxlpE7Q3gYy1iBpLrx0FOJz3h62ryrJZ/3zCAUIkT1Pb/8hHWpb3A==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.23.2", - "@rc-component/portal": "^1.1.0", - "classnames": "^2.3.2", - "rc-motion": "^2.0.0", - "rc-resize-observer": "^1.3.1", - "rc-util": "^5.44.0" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-motion": { - "version": "2.9.5", - "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.5.tgz", - "integrity": "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.11.1", - "classnames": "^2.2.1", - "rc-util": "^5.44.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-overflow": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/rc-overflow/-/rc-overflow-1.5.0.tgz", - "integrity": "sha512-Lm/v9h0LymeUYJf0x39OveU52InkdRXqnn2aYXfWmo8WdOonIKB2kfau+GF0fWq6jPgtdO9yMqveGcK6aIhJmg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.11.1", - "classnames": "^2.2.1", - "rc-resize-observer": "^1.0.0", - "rc-util": "^5.37.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-resize-observer": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz", - "integrity": "sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.20.7", - "classnames": "^2.2.1", - "rc-util": "^5.44.1", - "resize-observer-polyfill": "^1.5.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-util": { - "version": "5.44.4", - "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.4.tgz", - "integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.18.3", - "react-is": "^18.2.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-util/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "license": "MIT", - "peer": true - }, - "node_modules/re-resizable": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.11.2.tgz", - "integrity": "sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A==", - "license": "MIT", - "peer": true, - "peerDependencies": { - "react": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/react": { - "version": "19.2.4", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-avatar-editor": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/react-avatar-editor/-/react-avatar-editor-14.0.0.tgz", - "integrity": "sha512-NaQM3oo4u0a1/Njjutc2FjwKX35vQV+t6S8hovsbAlMpBN1ntIwP/g+Yr9eDIIfaNtRXL0AqboTnPmRxhD/i8A==", - "license": "MIT", - "peer": true, - "peerDependencies": { - "react": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/react-colorful": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz", - "integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==", - "license": "MIT", - "peer": true, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.4", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.4" - } - }, - "node_modules/react-draggable": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", - "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", - "license": "MIT", - "peer": true, - "dependencies": { - "clsx": "^1.1.1", - "prop-types": "^15.8.1" - }, - "peerDependencies": { - "react": ">= 16.3.0", - "react-dom": ">= 16.3.0" - } - }, - "node_modules/react-draggable/node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/react-dropzone": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-12.1.0.tgz", - "integrity": "sha512-iBYHA1rbopIvtzokEX4QubO6qk5IF/x3BtKGu74rF2JkQDXnwC4uO/lHKpaw4PJIV6iIAYOlwLv2FpiGyqHNog==", - "license": "MIT", - "peer": true, - "dependencies": { - "attr-accept": "^2.2.2", - "file-selector": "^0.5.0", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">= 10.13" - }, - "peerDependencies": { - "react": ">= 16.8" - } - }, - "node_modules/react-error-boundary": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-6.1.1.tgz", - "integrity": "sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w==", - "license": "MIT", - "peer": true, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/react-fast-compare": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", - "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", - "license": "MIT", - "peer": true - }, - "node_modules/react-hook-form": { - "version": "7.71.1", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz", - "integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/react-hook-form" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17 || ^18 || ^19" - } - }, - "node_modules/react-hotkeys-hook": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-5.2.4.tgz", - "integrity": "sha512-BgKg+A1+TawkYluh5Bo4cTmcgMN5L29uhJbDUQdHwPX+qgXRjIPYU5kIDHyxnAwCkCBiu9V5OpB2mpyeluVF2A==", - "license": "MIT", - "peer": true, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/react-is": { - "version": "19.2.4", - "license": "MIT", - "peer": true - }, - "node_modules/react-markdown": { - "version": "10.1.0", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "hast-util-to-jsx-runtime": "^2.0.0", - "html-url-attributes": "^3.0.0", - "mdast-util-to-hast": "^13.0.0", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.0.0", - "unified": "^11.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - }, - "peerDependencies": { - "@types/react": ">=18", - "react": ">=18" - } - }, - "node_modules/react-merge-refs": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/react-merge-refs/-/react-merge-refs-3.0.2.tgz", - "integrity": "sha512-MSZAfwFfdbEvwkKWP5EI5chuLYnNUxNS7vyS0i1Jp+wtd8J4Ga2ddzhaE68aMol2Z4vCnRM/oGOo1a3V75UPlw==", - "license": "MIT", - "peer": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "react": ">=16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - } - } - }, - "node_modules/react-redux": { - "version": "9.2.0", - "license": "MIT", - "dependencies": { - "@types/use-sync-external-store": "^0.0.6", - "use-sync-external-store": "^1.4.0" - }, - "peerDependencies": { - "@types/react": "^18.2.25 || ^19", - "react": "^18.0 || ^19", - "redux": "^5.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "redux": { - "optional": true - } - } - }, - "node_modules/react-refresh": { - "version": "0.17.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-remove-scroll": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", - "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.3", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", - "license": "MIT", - "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-rnd": { - "version": "10.5.2", - "resolved": "https://registry.npmjs.org/react-rnd/-/react-rnd-10.5.2.tgz", - "integrity": "sha512-0Tm4x7k7pfHf2snewJA8x7Nwgt3LV+58MVEWOVsFjk51eYruFEa6Wy7BNdxt4/lH0wIRsu7Gm3KjSXY2w7YaNw==", - "license": "MIT", - "peer": true, - "dependencies": { - "re-resizable": "6.11.2", - "react-draggable": "4.4.6", - "tslib": "2.6.2" - }, - "peerDependencies": { - "react": ">=16.3.0", - "react-dom": ">=16.3.0" - } - }, - "node_modules/react-rnd/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "license": "0BSD", - "peer": true - }, - "node_modules/react-style-singleton": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", - "license": "MIT", - "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-zoom-pan-pinch": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/react-zoom-pan-pinch/-/react-zoom-pan-pinch-3.7.0.tgz", - "integrity": "sha512-UmReVZ0TxlKzxSbYiAj+LeGRW8s8LraAFTXRAxzMYnNRgGPsxCudwZKVkjvGmjtx7SW/hZamt69NUmGf4xrkXA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8", - "npm": ">=5" - }, - "peerDependencies": { - "react": "*", - "react-dom": "*" - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readdirp": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/recharts": { - "version": "3.7.0", - "license": "MIT", - "workspaces": [ - "www" - ], - "dependencies": { - "@reduxjs/toolkit": "1.x.x || 2.x.x", - "clsx": "^2.1.1", - "decimal.js-light": "^2.5.1", - "es-toolkit": "^1.39.3", - "eventemitter3": "^5.0.1", - "immer": "^10.1.1", - "react-redux": "8.x.x || 9.x.x", - "reselect": "5.1.1", - "tiny-invariant": "^1.3.3", - "use-sync-external-store": "^1.2.2", - "victory-vendor": "^37.0.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/recma-build-jsx": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", - "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/estree": "^1.0.0", - "estree-util-build-jsx": "^3.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/recma-jsx": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", - "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", - "license": "MIT", - "peer": true, - "dependencies": { - "acorn-jsx": "^5.0.0", - "estree-util-to-js": "^2.0.0", - "recma-parse": "^1.0.0", - "recma-stringify": "^1.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - }, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/recma-parse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", - "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/estree": "^1.0.0", - "esast-util-from-js": "^2.0.0", - "unified": "^11.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/recma-stringify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", - "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/estree": "^1.0.0", - "estree-util-to-js": "^2.0.0", - "unified": "^11.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/redux": { - "version": "5.0.1", - "license": "MIT" - }, - "node_modules/redux-thunk": { - "version": "3.1.0", - "license": "MIT", - "peerDependencies": { - "redux": "^5.0.0" - } - }, - "node_modules/regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", - "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", - "license": "MIT", - "peer": true, - "dependencies": { - "regex-utilities": "^2.3.0" - } - }, - "node_modules/regex-recursion": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", - "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", - "license": "MIT", - "peer": true, - "dependencies": { - "regex-utilities": "^2.3.0" - } - }, - "node_modules/regex-utilities": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", - "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", - "license": "MIT", - "peer": true - }, - "node_modules/rehype-github-alerts": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/rehype-github-alerts/-/rehype-github-alerts-4.2.0.tgz", - "integrity": "sha512-6di6kEu9WUHKLKrkKG2xX6AOuaCMGghg0Wq7MEuM/jBYUPVIq6PJpMe00dxMfU+/YSBtDXhffpDimgDi+BObIQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@primer/octicons": "^19.20.0", - "hast-util-from-html": "^2.0.3", - "hast-util-is-element": "^3.0.0", - "unist-util-visit": "^5.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/chrisweb" - } - }, - "node_modules/rehype-katex": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.1.tgz", - "integrity": "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/hast": "^3.0.0", - "@types/katex": "^0.16.0", - "hast-util-from-html-isomorphic": "^2.0.0", - "hast-util-to-text": "^4.0.0", - "katex": "^0.16.0", - "unist-util-visit-parents": "^6.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-raw": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", - "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-raw": "^9.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-recma": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", - "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/estree": "^1.0.0", - "@types/hast": "^3.0.0", - "hast-util-to-estree": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-breaks": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-4.0.0.tgz", - "integrity": "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-newline-to-break": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-cjk-friendly": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/remark-cjk-friendly/-/remark-cjk-friendly-1.2.3.tgz", - "integrity": "sha512-UvAgxwlNk+l9Oqgl/9MWK2eWRS7zgBW/nXX9AthV7nd/3lNejF138E7Xbmk9Zs4WjTJGs721r7fAEc7tNFoH7g==", - "license": "MIT", - "peer": true, - "dependencies": { - "micromark-extension-cjk-friendly": "1.2.3" - }, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "@types/mdast": "^4.0.0", - "unified": "^11.0.0" - }, - "peerDependenciesMeta": { - "@types/mdast": { - "optional": true - } - } - }, - "node_modules/remark-gfm": { - "version": "4.0.1", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-gfm": "^3.0.0", - "micromark-extension-gfm": "^3.0.0", - "remark-parse": "^11.0.0", - "remark-stringify": "^11.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-github": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/remark-github/-/remark-github-12.0.0.tgz", - "integrity": "sha512-ByefQKFN184LeiGRCabfl7zUJsdlMYWEhiLX1gpmQ11yFg6xSuOTW7LVCv0oc1x+YvUMJW23NU36sJX2RWGgvg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-find-and-replace": "^3.0.0", - "mdast-util-to-string": "^4.0.0", - "to-vfile": "^8.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-math": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz", - "integrity": "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-math": "^3.0.0", - "micromark-extension-math": "^3.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-mdx": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", - "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", - "license": "MIT", - "peer": true, - "dependencies": { - "mdast-util-mdx": "^3.0.0", - "micromark-extension-mdxjs": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-parse": { - "version": "11.0.0", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-rehype": { - "version": "11.1.2", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "mdast-util-to-hast": "^13.0.0", - "unified": "^11.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-stringify": { - "version": "11.0.0", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-to-markdown": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remend": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/remend/-/remend-1.2.1.tgz", - "integrity": "sha512-4wC12bgXsfKAjF1ewwkNIQz5sqewz/z1xgIgjEMb3r1pEytQ37F0Cm6i+OhbTWEvguJD7lhOUJhK5fSasw9f0w==", - "license": "Apache-2.0", - "peer": true - }, - "node_modules/reselect": { - "version": "5.1.1", - "license": "MIT" - }, - "node_modules/resize-observer-polyfill": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", - "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", - "license": "MIT", - "peer": true - }, - "node_modules/resolve": { - "version": "1.22.11", - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/robust-predicates": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", - "license": "Unlicense", - "peer": true - }, - "node_modules/rollup": { - "version": "4.57.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.1", - "@rollup/rollup-android-arm64": "4.57.1", - "@rollup/rollup-darwin-arm64": "4.57.1", - "@rollup/rollup-darwin-x64": "4.57.1", - "@rollup/rollup-freebsd-arm64": "4.57.1", - "@rollup/rollup-freebsd-x64": "4.57.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", - "@rollup/rollup-linux-arm-musleabihf": "4.57.1", - "@rollup/rollup-linux-arm64-gnu": "4.57.1", - "@rollup/rollup-linux-arm64-musl": "4.57.1", - "@rollup/rollup-linux-loong64-gnu": "4.57.1", - "@rollup/rollup-linux-loong64-musl": "4.57.1", - "@rollup/rollup-linux-ppc64-gnu": "4.57.1", - "@rollup/rollup-linux-ppc64-musl": "4.57.1", - "@rollup/rollup-linux-riscv64-gnu": "4.57.1", - "@rollup/rollup-linux-riscv64-musl": "4.57.1", - "@rollup/rollup-linux-s390x-gnu": "4.57.1", - "@rollup/rollup-linux-x64-gnu": "4.57.1", - "@rollup/rollup-linux-x64-musl": "4.57.1", - "@rollup/rollup-openbsd-x64": "4.57.1", - "@rollup/rollup-openharmony-arm64": "4.57.1", - "@rollup/rollup-win32-arm64-msvc": "4.57.1", - "@rollup/rollup-win32-ia32-msvc": "4.57.1", - "@rollup/rollup-win32-x64-gnu": "4.57.1", - "@rollup/rollup-win32-x64-msvc": "4.57.1", - "fsevents": "~2.3.2" - } - }, - "node_modules/roughjs": { - "version": "4.6.6", - "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", - "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "hachure-fill": "^0.5.2", - "path-data-parser": "^0.1.0", - "points-on-curve": "^0.2.0", - "points-on-path": "^0.2.1" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/rw": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", - "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", - "license": "BSD-3-Clause", - "peer": true - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT", - "peer": true - }, - "node_modules/sass": { - "version": "1.97.3", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^4.0.0", - "immutable": "^5.0.2", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=14.0.0" - }, - "optionalDependencies": { - "@parcel/watcher": "^2.4.1" - } - }, - "node_modules/scheduler": { - "version": "0.27.0", - "license": "MIT" - }, - "node_modules/screenfull": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz", - "integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/scroll-into-view-if-needed": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", - "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "compute-scroll-into-view": "^3.0.2" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", - "license": "MIT", - "peer": true - }, - "node_modules/seroval": { - "version": "1.5.0", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/seroval-plugins": { - "version": "1.5.0", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "seroval": "^1.0" - } - }, - "node_modules/set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "license": "MIT", - "peer": true, - "dependencies": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/set-value/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/shiki": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.22.0.tgz", - "integrity": "sha512-LBnhsoYEe0Eou4e1VgJACes+O6S6QC0w71fCSp5Oya79inkwkm15gQ1UF6VtQ8j/taMDh79hAB49WUk8ALQW3g==", - "license": "MIT", - "peer": true, - "dependencies": { - "@shikijs/core": "3.22.0", - "@shikijs/engine-javascript": "3.22.0", - "@shikijs/engine-oniguruma": "3.22.0", - "@shikijs/langs": "3.22.0", - "@shikijs/themes": "3.22.0", - "@shikijs/types": "3.22.0", - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" - } - }, - "node_modules/shiki-stream": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/shiki-stream/-/shiki-stream-0.1.4.tgz", - "integrity": "sha512-4pz6JGSDmVTTkPJ/ueixHkFAXY4ySCc+unvCaDZV7hqq/sdJZirRxgIXSuNSKgiFlGTgRR97sdu2R8K55sPsrw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@shikijs/core": "^3.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "react": "^19.0.0", - "solid-js": "^1.9.0", - "vue": "^3.2.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "solid-js": { - "optional": true - }, - "vue": { - "optional": true - } - } - }, - "node_modules/sigma": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/sigma/-/sigma-3.0.2.tgz", - "integrity": "sha512-/BUbeOwPGruiBOm0YQQ6ZMcLIZ6tf/W+Jcm7dxZyAX0tK3WP9/sq7/NAWBxPIxVahdGjCJoGwej0Gdrv0DxlQQ==", - "license": "MIT", - "dependencies": { - "events": "^3.3.0", - "graphology-utils": "^2.5.2" - } - }, - "node_modules/smol-toml": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.0.tgz", - "integrity": "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==", - "license": "BSD-3-Clause", - "engines": { - "node": ">= 18" - }, - "funding": { - "url": "https://github.com/sponsors/cyyynthia" - } - }, - "node_modules/sonner": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", - "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", - "license": "MIT", - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", - "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" - } - }, - "node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "license": "BSD-3-Clause", - "peer": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/space-separated-tokens": { - "version": "2.0.2", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/split-on-first": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz", - "integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "license": "MIT", - "peer": true, - "dependencies": { - "extend-shallow": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/split-string/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "license": "MIT", - "peer": true, - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/string-convert": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", - "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==", - "license": "MIT", - "peer": true - }, - "node_modules/stringify-entities": { - "version": "4.0.4", - "license": "MIT", - "dependencies": { - "character-entities-html4": "^2.0.0", - "character-entities-legacy": "^3.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/style-mod": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", - "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", - "license": "MIT" - }, - "node_modules/style-to-js": { - "version": "1.1.21", - "license": "MIT", - "dependencies": { - "style-to-object": "1.0.14" - } - }, - "node_modules/style-to-object": { - "version": "1.0.14", - "license": "MIT", - "dependencies": { - "inline-style-parser": "0.2.7" - } - }, - "node_modules/stylis": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", - "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", - "license": "MIT" - }, - "node_modules/sucrase": { - "version": "3.35.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "tinyglobby": "^0.2.11", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/swr": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/swr/-/swr-2.4.0.tgz", - "integrity": "sha512-sUlC20T8EOt1pHmDiqueUWMmRRX03W7w5YxovWX7VR2KHEPCTMly85x05vpkP5i6Bu4h44ePSMD9Tc+G2MItFw==", - "license": "MIT", - "peer": true, - "dependencies": { - "dequal": "^2.0.3", - "use-sync-external-store": "^1.6.0" - }, - "peerDependencies": { - "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/tabbable": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", - "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", - "license": "MIT", - "peer": true - }, - "node_modules/tailwindcss": { - "version": "3.4.19", - "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.7", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tailwindcss-animate": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "peerDependencies": { - "tailwindcss": ">=3.0.0 || insiders" - } - }, - "node_modules/tailwindcss/node_modules/chokidar": { - "version": "3.6.0", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/tailwindcss/node_modules/chokidar/node_modules/readdirp": { - "version": "3.6.0", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/tailwindcss/node_modules/chokidar/node_modules/readdirp/node_modules/picomatch": { - "version": "2.3.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/tailwindcss/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/throttle-debounce": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", - "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12.22" - } - }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "license": "MIT" - }, - "node_modules/tiny-warning": { - "version": "1.0.3", - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/to-vfile": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/to-vfile/-/to-vfile-8.0.0.tgz", - "integrity": "sha512-IcmH1xB5576MJc9qcfEC/m/nQCFt3fzMHz45sSlgJyTWjRbKW1HAkJpuf3DgE57YzIlZcwcBZA5ENQbBo4aLkg==", - "license": "MIT", - "peer": true, - "dependencies": { - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/trim-lines": { - "version": "3.0.1", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/trough": { - "version": "2.2.0", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/ts-dedent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", - "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6.10" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/ts-md5": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ts-md5/-/ts-md5-2.0.1.tgz", - "integrity": "sha512-yF35FCoEOFBzOclSkMNEUbFQZuv89KEQ+5Xz03HrMSGUGB1+r+El+JiGOFwsP4p9RFNzwlrydYoTLvPOuICl9w==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "license": "0BSD" - }, - "node_modules/typescript": { - "version": "5.9.3", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/ufo": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", - "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", - "license": "MIT", - "peer": true - }, - "node_modules/unified": { - "version": "11.0.5", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "bail": "^2.0.0", - "devlop": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-find-after": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", - "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-is": { - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position-from-estree": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", - "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-remove-position": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", - "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-visit": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit": { - "version": "5.1.0", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "6.0.2", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/url-join": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz", - "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==", - "license": "MIT", - "peer": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/use-callback-ref": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-merge-value": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/use-merge-value/-/use-merge-value-1.2.0.tgz", - "integrity": "sha512-DXgG0kkgJN45TcyoXL49vJnn55LehnrmoHc7MbKi+QDBvr8dsesqws8UlyIWGHMR+JXgxc1nvY+jDGMlycsUcw==", - "license": "MIT", - "peerDependencies": { - "react": ">= 16.x" - } - }, - "node_modules/use-sidecar": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sync-external-store": { - "version": "1.6.0", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/uuid": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", - "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "peer": true, - "bin": { - "uuid": "dist-node/bin/uuid" - } - }, - "node_modules/v8n": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/v8n/-/v8n-1.5.1.tgz", - "integrity": "sha512-LdabyT4OffkyXFCe9UT+uMkxNBs5rcTVuZClvxQr08D5TUgo1OFKkoT65qYRCsiKBl/usHjpXvP4hHMzzDRj3A==", - "license": "MIT", - "peer": true - }, - "node_modules/vfile": { - "version": "6.0.3", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-location": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", - "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/unist": "^3.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message": { - "version": "4.0.3", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/victory-vendor": { - "version": "37.3.6", - "license": "MIT AND ISC", - "dependencies": { - "@types/d3-array": "^3.0.3", - "@types/d3-ease": "^3.0.0", - "@types/d3-interpolate": "^3.0.1", - "@types/d3-scale": "^4.0.2", - "@types/d3-shape": "^3.1.0", - "@types/d3-time": "^3.0.0", - "@types/d3-timer": "^3.0.0", - "d3-array": "^3.1.6", - "d3-ease": "^3.0.1", - "d3-interpolate": "^3.0.1", - "d3-scale": "^4.0.2", - "d3-shape": "^3.1.0", - "d3-time": "^3.0.0", - "d3-timer": "^3.0.1" - } - }, - "node_modules/virtua": { - "version": "0.48.6", - "resolved": "https://registry.npmjs.org/virtua/-/virtua-0.48.6.tgz", - "integrity": "sha512-Cl4uMvMV5c9RuOy9zhkFMYwx/V4YLBMYLRSWkO8J46opQZ3P7KMq0CqCVOOAKUckjl/r//D2jWTBGYWzmgtzrQ==", - "license": "MIT", - "peer": true, - "peerDependencies": { - "react": ">=16.14.0", - "react-dom": ">=16.14.0", - "solid-js": ">=1.0", - "svelte": ">=5.0", - "vue": ">=3.2" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "solid-js": { - "optional": true - }, - "svelte": { - "optional": true - }, - "vue": { - "optional": true - } - } - }, - "node_modules/vite": { - "version": "6.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vscode-jsonrpc": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", - "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/vscode-languageserver": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", - "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", - "license": "MIT", - "peer": true, - "dependencies": { - "vscode-languageserver-protocol": "3.17.5" - }, - "bin": { - "installServerIntoExtension": "bin/installServerIntoExtension" - } - }, - "node_modules/vscode-languageserver-protocol": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", - "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", - "license": "MIT", - "peer": true, - "dependencies": { - "vscode-jsonrpc": "8.2.0", - "vscode-languageserver-types": "3.17.5" - } - }, - "node_modules/vscode-languageserver-textdocument": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", - "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", - "license": "MIT", - "peer": true - }, - "node_modules/vscode-languageserver-types": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", - "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", - "license": "MIT", - "peer": true - }, - "node_modules/vscode-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", - "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", - "license": "MIT", - "peer": true - }, - "node_modules/w3c-keyname": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", - "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", - "license": "MIT" - }, - "node_modules/web-namespaces": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", - "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", - "license": "MIT", - "peer": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "dev": true, - "license": "ISC" - }, - "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zustand": { - "version": "4.5.7", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", - "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", - "license": "MIT", - "dependencies": { - "use-sync-external-store": "^1.2.2" - }, - "engines": { - "node": ">=12.7.0" - }, - "peerDependencies": { - "@types/react": ">=16.8", - "immer": ">=9.0.6", - "react": ">=16.8" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - } - } - }, - "node_modules/zwitch": { - "version": "2.0.4", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - } - } -} diff --git a/interface/package.json b/interface/package.json index 249dbad30..ea5405bea 100644 --- a/interface/package.json +++ b/interface/package.json @@ -38,6 +38,7 @@ "@radix-ui/react-tooltip": "^1.2.8", "@react-sigma/core": "^5.0.6", "@tanstack/react-query": "^5.62.0", + "@tanstack/react-query-devtools": "^5.91.3", "@tanstack/react-router": "^1.159.5", "@tanstack/react-virtual": "^3.13.18", "@tanstack/router-devtools": "^1.159.5", diff --git a/interface/src/App.tsx b/interface/src/App.tsx index 886e844c8..78e154a2f 100644 --- a/interface/src/App.tsx +++ b/interface/src/App.tsx @@ -1,5 +1,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { RouterProvider } from "@tanstack/react-router"; +import { ErrorBoundary } from "@/components/ErrorBoundary"; import { LiveContextProvider } from "@/hooks/useLiveContext"; import { router } from "@/router"; @@ -15,10 +17,15 @@ const queryClient = new QueryClient({ export function App() { return ( - - - - - + + + + + + {import.meta.env.DEV && ( + + )} + + ); } diff --git a/interface/src/api/client.ts b/interface/src/api/client.ts index e78808eea..d2c15719e 100644 --- a/interface/src/api/client.ts +++ b/interface/src/api/client.ts @@ -40,6 +40,14 @@ export interface OutboundMessageEvent { text: string; } +export interface OutboundMessageDeltaEvent { + type: "outbound_message_delta"; + agent_id: string; + channel_id: string; + text_delta: string; + aggregated_text: string; +} + export interface TypingStateEvent { type: "typing_state"; agent_id: string; @@ -53,6 +61,8 @@ export interface WorkerStartedEvent { channel_id: string | null; worker_id: string; task: string; + worker_type?: string; + interactive?: boolean; } export interface WorkerStatusEvent { @@ -63,12 +73,20 @@ export interface WorkerStatusEvent { status: string; } +export interface WorkerIdleEvent { + type: "worker_idle"; + agent_id: string; + channel_id: string | null; + worker_id: string; +} + export interface WorkerCompletedEvent { type: "worker_completed"; agent_id: string; channel_id: string | null; worker_id: string; result: string; + success?: boolean; } export interface BranchStartedEvent { @@ -94,6 +112,7 @@ export interface ToolStartedEvent { process_type: ProcessType; process_id: string; tool_name: string; + args: string; } export interface ToolCompletedEvent { @@ -103,19 +122,44 @@ export interface ToolCompletedEvent { process_type: ProcessType; process_id: string; tool_name: string; + result: string; +} + +// -- OpenCode live transcript part types -- + +export type OpenCodeToolState = + | { status: "pending" } + | { status: "running"; title?: string; input?: string } + | { status: "completed"; title?: string; input?: string; output?: string } + | { status: "error"; error?: string }; + +export type OpenCodePart = + | { type: "text"; id: string; text: string } + | { type: "tool"; id: string; tool: string } & OpenCodeToolState + | { type: "step_start"; id: string } + | { type: "step_finish"; id: string; reason?: string }; + +export interface OpenCodePartUpdatedEvent { + type: "opencode_part_updated"; + agent_id: string; + worker_id: string; + part: OpenCodePart; } export type ApiEvent = | InboundMessageEvent | OutboundMessageEvent + | OutboundMessageDeltaEvent | TypingStateEvent | WorkerStartedEvent | WorkerStatusEvent + | WorkerIdleEvent | WorkerCompletedEvent | BranchStartedEvent | BranchCompletedEvent | ToolStartedEvent - | ToolCompletedEvent; + | ToolCompletedEvent + | OpenCodePartUpdatedEvent; async function fetchJson(path: string): Promise { const response = await fetch(`${API_BASE}${path}`); @@ -168,6 +212,7 @@ export interface WorkerStatusInfo { started_at: string; notify_on_complete: boolean; tool_calls: number; + interactive: boolean; } export interface BranchStatusInfo { @@ -193,6 +238,54 @@ export interface StatusBlockSnapshot { /** channel_id -> StatusBlockSnapshot */ export type ChannelStatusResponse = Record; +// --- Workers API types --- + +export type ActionContent = + | { type: "text"; text: string } + | { type: "tool_call"; id: string; name: string; args: string }; + +export type TranscriptStep = + | { type: "action"; content: ActionContent[] } + | { type: "tool_result"; call_id: string; name: string; text: string }; + +export interface WorkerRunInfo { + id: string; + task: string; + status: string; + worker_type: string; + channel_id: string | null; + channel_name: string | null; + started_at: string; + completed_at: string | null; + has_transcript: boolean; + live_status: string | null; + tool_calls: number; + opencode_port: number | null; + interactive: boolean; +} + +export interface WorkerDetailResponse { + id: string; + task: string; + result: string | null; + status: string; + worker_type: string; + channel_id: string | null; + channel_name: string | null; + started_at: string; + completed_at: string | null; + transcript: TranscriptStep[] | null; + tool_calls: number; + opencode_session_id: string | null; + opencode_port: number | null; + interactive: boolean; +} + +export interface WorkerListResponse { + workers: WorkerRunInfo[]; + total: number; +} + export interface AgentInfo { id: string; display_name?: string; @@ -273,6 +366,8 @@ export interface UpdateStatus { release_notes: string | null; deployment: Deployment; can_apply: boolean; + cannot_apply_reason: string | null; + docker_image: string | null; checked_at: string | null; error: string | null; } @@ -515,6 +610,17 @@ export interface BrowserSection { enabled: boolean; headless: boolean; evaluate_enabled: boolean; + persist_session: boolean; + close_policy: "close_browser" | "close_tabs" | "detach"; +} + +export interface ChannelSection { + listen_only_mode: boolean; +} + +export interface SandboxSection { + mode: "enabled" | "disabled"; + writable_paths: string[]; } export interface DiscordSection { @@ -530,7 +636,9 @@ export interface AgentConfigResponse { coalesce: CoalesceSection; memory_persistence: MemoryPersistenceSection; browser: BrowserSection; + channel: ChannelSection; discord: DiscordSection; + sandbox: SandboxSection; } // Partial update types - all fields are optional @@ -591,6 +699,17 @@ export interface BrowserUpdate { enabled?: boolean; headless?: boolean; evaluate_enabled?: boolean; + persist_session?: boolean; + close_policy?: "close_browser" | "close_tabs" | "detach"; +} + +export interface ChannelUpdate { + listen_only_mode?: boolean; +} + +export interface SandboxUpdate { + mode?: "enabled" | "disabled"; + writable_paths?: string[]; } export interface DiscordUpdate { @@ -606,7 +725,9 @@ export interface AgentConfigUpdateRequest { coalesce?: CoalesceUpdate; memory_persistence?: MemoryPersistenceUpdate; browser?: BrowserUpdate; + channel?: ChannelUpdate; discord?: DiscordUpdate; + sandbox?: SandboxUpdate; } // -- Cron Types -- @@ -665,6 +786,7 @@ export interface ProviderStatus { openai: boolean; openai_chatgpt: boolean; openrouter: boolean; + kilo: boolean; zhipu: boolean; groq: boolean; together: boolean; @@ -675,6 +797,7 @@ export interface ProviderStatus { gemini: boolean; ollama: boolean; opencode_zen: boolean; + opencode_go: boolean; nvidia: boolean; minimax: boolean; minimax_cn: boolean; @@ -764,6 +887,7 @@ export interface SkillInfo { file_path: string; base_dir: string; source: "instance" | "workspace"; + source_repo?: string; } export interface SkillsListResponse { @@ -799,12 +923,14 @@ export interface RegistrySkill { skillId: string; name: string; installs: number; + description?: string; id?: string; } export interface RegistryBrowseResponse { skills: RegistrySkill[]; has_more: boolean; + total?: number; } export interface RegistrySearchResponse { @@ -813,6 +939,72 @@ export interface RegistrySearchResponse { count: number; } +// -- Task Types -- + +export type TaskStatus = "pending_approval" | "backlog" | "ready" | "in_progress" | "done"; +export type TaskPriority = "critical" | "high" | "medium" | "low"; + +export interface TaskSubtask { + title: string; + completed: boolean; +} + +export interface TaskItem { + id: string; + agent_id: string; + task_number: number; + title: string; + description?: string; + status: TaskStatus; + priority: TaskPriority; + subtasks: TaskSubtask[]; + metadata: Record; + source_memory_id?: string; + worker_id?: string; + created_by: string; + approved_at?: string; + approved_by?: string; + created_at: string; + updated_at: string; + completed_at?: string; +} + +export interface TaskListResponse { + tasks: TaskItem[]; +} + +export interface TaskResponse { + task: TaskItem; +} + +export interface TaskActionResponse { + success: boolean; + message: string; +} + +export interface CreateTaskRequest { + title: string; + description?: string; + status?: TaskStatus; + priority?: TaskPriority; + subtasks?: TaskSubtask[]; + metadata?: Record; + source_memory_id?: string; + created_by?: string; +} + +export interface UpdateTaskRequest { + title?: string; + description?: string; + status?: TaskStatus; + priority?: TaskPriority; + subtasks?: TaskSubtask[]; + metadata?: Record; + complete_subtask?: number; + worker_id?: string; + approved_by?: string; +} + // -- Messaging / Bindings Types -- export interface PlatformStatus { @@ -820,17 +1012,68 @@ export interface PlatformStatus { enabled: boolean; } +export interface AdapterInstanceStatus { + platform: string; + name: string | null; + runtime_key: string; + configured: boolean; + enabled: boolean; + binding_count: number; +} + export interface MessagingStatusResponse { discord: PlatformStatus; slack: PlatformStatus; telegram: PlatformStatus; webhook: PlatformStatus; twitch: PlatformStatus; + email: PlatformStatus; + instances: AdapterInstanceStatus[]; +} + +export interface CreateMessagingInstanceRequest { + platform: string; + name?: string; + enabled?: boolean; + credentials: { + discord_token?: string; + slack_bot_token?: string; + slack_app_token?: string; + telegram_token?: string; + twitch_username?: string; + twitch_oauth_token?: string; + twitch_client_id?: string; + twitch_client_secret?: string; + twitch_refresh_token?: string; + email_imap_host?: string; + email_imap_port?: number; + email_imap_username?: string; + email_imap_password?: string; + email_smtp_host?: string; + email_smtp_port?: number; + email_smtp_username?: string; + email_smtp_password?: string; + email_from_address?: string; + webhook_port?: number; + webhook_bind?: string; + webhook_auth_token?: string; + }; +} + +export interface DeleteMessagingInstanceRequest { + platform: string; + name?: string; +} + +export interface MessagingInstanceActionResponse { + success: boolean; + message: string; } export interface BindingInfo { agent_id: string; channel: string; + adapter: string | null; guild_id: string | null; workspace_id: string | null; chat_id: string | null; @@ -846,6 +1089,7 @@ export interface BindingsListResponse { export interface CreateBindingRequest { agent_id: string; channel: string; + adapter?: string; guild_id?: string; workspace_id?: string; chat_id?: string; @@ -856,6 +1100,17 @@ export interface CreateBindingRequest { discord_token?: string; slack_bot_token?: string; slack_app_token?: string; + telegram_token?: string; + email_imap_host?: string; + email_imap_port?: number; + email_imap_username?: string; + email_imap_password?: string; + email_smtp_host?: string; + email_smtp_port?: number; + email_smtp_username?: string; + email_smtp_password?: string; + email_from_address?: string; + email_from_name?: string; twitch_username?: string; twitch_oauth_token?: string; twitch_client_id?: string; @@ -873,11 +1128,13 @@ export interface CreateBindingResponse { export interface UpdateBindingRequest { original_agent_id: string; original_channel: string; + original_adapter?: string; original_guild_id?: string; original_workspace_id?: string; original_chat_id?: string; agent_id: string; channel: string; + adapter?: string; guild_id?: string; workspace_id?: string; chat_id?: string; @@ -894,6 +1151,7 @@ export interface UpdateBindingResponse { export interface DeleteBindingRequest { agent_id: string; channel: string; + adapter?: string; guild_id?: string; workspace_id?: string; chat_id?: string; @@ -1057,6 +1315,66 @@ export interface AgentMessageEvent { channel_id: string; } +// ── Secrets ────────────────────────────────────────────────────────────── + +export type SecretCategory = "system" | "tool"; +export type StoreState = "unencrypted" | "locked" | "unlocked"; + +export interface SecretStoreStatus { + state: StoreState; + encrypted: boolean; + secret_count: number; + system_count: number; + tool_count: number; + platform_managed: boolean; +} + +export interface SecretListItem { + name: string; + category: SecretCategory; + created_at: string; + updated_at: string; +} + +export interface SecretListResponse { + secrets: SecretListItem[]; +} + +export interface PutSecretResponse { + name: string; + category: SecretCategory; + reload_required: boolean; + message: string; +} + +export interface DeleteSecretResponse { + deleted: string; + warning?: string; +} + +export interface EncryptResponse { + master_key: string; + message: string; +} + +export interface UnlockResponse { + state: string; + secret_count: number; + message: string; +} + +export interface MigrationItem { + config_key: string; + secret_name: string; + category: SecretCategory; +} + +export interface MigrateResponse { + migrated: MigrationItem[]; + skipped: string[]; + message: string; +} + export const api = { status: () => fetchJson("/status"), overview: () => fetchJson("/overview"), @@ -1076,6 +1394,15 @@ export const api = { return fetchJson(`/channels/messages?${params}`); }, channelStatus: () => fetchJson("/channels/status"), + workersList: (agentId: string, params: { limit?: number; offset?: number; status?: string } = {}) => { + const search = new URLSearchParams({ agent_id: agentId }); + if (params.limit) search.set("limit", String(params.limit)); + if (params.offset) search.set("offset", String(params.offset)); + if (params.status) search.set("status", params.status); + return fetchJson(`/agents/workers?${search}`); + }, + workerDetail: (agentId: string, workerId: string) => + fetchJson(`/agents/workers/detail?agent_id=${encodeURIComponent(agentId)}&worker_id=${encodeURIComponent(workerId)}`), agentMemories: (agentId: string, params: MemoriesListParams = {}) => { const search = new URLSearchParams({ agent_id: agentId }); if (params.limit) search.set("limit", String(params.limit)); @@ -1411,11 +1738,13 @@ export const api = { return response.json() as Promise; }, - togglePlatform: async (platform: string, enabled: boolean) => { + togglePlatform: async (platform: string, enabled: boolean, adapter?: string) => { + const body: Record = { platform, enabled }; + if (adapter) body.adapter = adapter; const response = await fetch(`${API_BASE}/messaging/toggle`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ platform, enabled }), + body: JSON.stringify(body), }); if (!response.ok) { throw new Error(`API error: ${response.status}`); @@ -1423,11 +1752,13 @@ export const api = { return response.json() as Promise<{ success: boolean; message: string }>; }, - disconnectPlatform: async (platform: string) => { + disconnectPlatform: async (platform: string, adapter?: string) => { + const body: Record = { platform }; + if (adapter) body.adapter = adapter; const response = await fetch(`${API_BASE}/messaging/disconnect`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ platform }), + body: JSON.stringify(body), }); if (!response.ok) { throw new Error(`API error: ${response.status}`); @@ -1435,6 +1766,30 @@ export const api = { return response.json() as Promise<{ success: boolean; message: string }>; }, + createMessagingInstance: async (request: CreateMessagingInstanceRequest) => { + const response = await fetch(`${API_BASE}/messaging/instances`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(request), + }); + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + return response.json() as Promise; + }, + + deleteMessagingInstance: async (request: DeleteMessagingInstanceRequest) => { + const response = await fetch(`${API_BASE}/messaging/instances`, { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(request), + }); + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + return response.json() as Promise; + }, + // Global Settings API globalSettings: () => fetchJson("/settings"), @@ -1650,5 +2005,129 @@ export const api = { webChatHistory: (agentId: string, sessionId: string, limit = 100) => fetch(`${API_BASE}/webchat/history?agent_id=${encodeURIComponent(agentId)}&session_id=${encodeURIComponent(sessionId)}&limit=${limit}`), + // Tasks API + listTasks: (agentId: string, params?: { status?: TaskStatus; priority?: TaskPriority; limit?: number }) => { + const search = new URLSearchParams({ agent_id: agentId }); + if (params?.status) search.set("status", params.status); + if (params?.priority) search.set("priority", params.priority); + if (params?.limit) search.set("limit", String(params.limit)); + return fetchJson(`/agents/tasks?${search}`); + }, + getTask: (agentId: string, taskNumber: number) => + fetchJson(`/agents/tasks/${taskNumber}?agent_id=${encodeURIComponent(agentId)}`), + createTask: async (agentId: string, request: CreateTaskRequest): Promise => { + const response = await fetch(`${API_BASE}/agents/tasks`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ ...request, agent_id: agentId }), + }); + if (!response.ok) throw new Error(`API error: ${response.status}`); + return response.json() as Promise; + }, + updateTask: async (agentId: string, taskNumber: number, request: UpdateTaskRequest): Promise => { + const response = await fetch(`${API_BASE}/agents/tasks/${taskNumber}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ ...request, agent_id: agentId }), + }); + if (!response.ok) throw new Error(`API error: ${response.status}`); + return response.json() as Promise; + }, + deleteTask: async (agentId: string, taskNumber: number): Promise => { + const response = await fetch(`${API_BASE}/agents/tasks/${taskNumber}?agent_id=${encodeURIComponent(agentId)}`, { + method: "DELETE", + }); + if (!response.ok) throw new Error(`API error: ${response.status}`); + return response.json() as Promise; + }, + approveTask: async (agentId: string, taskNumber: number, approvedBy?: string): Promise => { + const response = await fetch(`${API_BASE}/agents/tasks/${taskNumber}/approve`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ agent_id: agentId, approved_by: approvedBy }), + }); + if (!response.ok) throw new Error(`API error: ${response.status}`); + return response.json() as Promise; + }, + executeTask: async (agentId: string, taskNumber: number): Promise => { + const response = await fetch(`${API_BASE}/agents/tasks/${taskNumber}/execute`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ agent_id: agentId }), + }); + if (!response.ok) throw new Error(`API error: ${response.status}`); + return response.json() as Promise; + }, + + // Secrets API + secretsStatus: () => fetchJson("/secrets/status"), + listSecrets: () => fetchJson("/secrets"), + putSecret: async (name: string, value: string, category?: SecretCategory): Promise => { + const response = await fetch(`${API_BASE}/secrets/${encodeURIComponent(name)}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ value, category }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + throw new Error(body.error || `API error: ${response.status}`); + } + return response.json() as Promise; + }, + deleteSecret: async (name: string): Promise => { + const response = await fetch(`${API_BASE}/secrets/${encodeURIComponent(name)}`, { + method: "DELETE", + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + throw new Error(body.error || `API error: ${response.status}`); + } + return response.json() as Promise; + }, + enableEncryption: async (): Promise => { + const response = await fetch(`${API_BASE}/secrets/encrypt`, { method: "POST" }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + throw new Error(body.error || `API error: ${response.status}`); + } + return response.json() as Promise; + }, + unlockSecrets: async (masterKey: string): Promise => { + const response = await fetch(`${API_BASE}/secrets/unlock`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ master_key: masterKey }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + throw new Error(body.error || `API error: ${response.status}`); + } + return response.json() as Promise; + }, + lockSecrets: async (): Promise<{ state: string; message: string }> => { + const response = await fetch(`${API_BASE}/secrets/lock`, { method: "POST" }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + throw new Error(body.error || `API error: ${response.status}`); + } + return response.json(); + }, + rotateKey: async (): Promise<{ master_key: string; message: string }> => { + const response = await fetch(`${API_BASE}/secrets/rotate`, { method: "POST" }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + throw new Error(body.error || `API error: ${response.status}`); + } + return response.json(); + }, + migrateSecrets: async (): Promise => { + const response = await fetch(`${API_BASE}/secrets/migrate`, { method: "POST" }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + throw new Error(body.error || `API error: ${response.status}`); + } + return response.json() as Promise; + }, + eventsUrl: `${API_BASE}/events`, }; diff --git a/interface/src/components/AgentTabs.tsx b/interface/src/components/AgentTabs.tsx index c89ddd61a..1f0aaa8b4 100644 --- a/interface/src/components/AgentTabs.tsx +++ b/interface/src/components/AgentTabs.tsx @@ -8,6 +8,7 @@ const tabs = [ { label: "Memories", to: "/agents/$agentId/memories" as const, exact: false }, { label: "Ingest", to: "/agents/$agentId/ingest" as const, exact: false }, { label: "Workers", to: "/agents/$agentId/workers" as const, exact: false }, + { label: "Tasks", to: "/agents/$agentId/tasks" as const, exact: false }, { label: "Cortex", to: "/agents/$agentId/cortex" as const, exact: false }, { label: "Skills", to: "/agents/$agentId/skills" as const, exact: false }, { label: "Cron", to: "/agents/$agentId/cron" as const, exact: false }, diff --git a/interface/src/components/ChannelCard.tsx b/interface/src/components/ChannelCard.tsx index 27e7e6084..b49be17de 100644 --- a/interface/src/components/ChannelCard.tsx +++ b/interface/src/components/ChannelCard.tsx @@ -3,19 +3,22 @@ import { AnimatePresence, motion } from "framer-motion"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { api } from "@/api/client"; import type { ChannelInfo } from "@/api/client"; -import type { ActiveBranch, ActiveWorker, ChannelLiveState } from "@/hooks/useChannelLiveState"; +import { isOpenCodeWorker, type ActiveBranch, type ActiveWorker, type ChannelLiveState } from "@/hooks/useChannelLiveState"; import { LiveDuration } from "@/components/LiveDuration"; import { formatTimeAgo, formatTimestamp, platformIcon, platformColor } from "@/lib/format"; const VISIBLE_MESSAGES = 6; function WorkerBadge({ worker }: { worker: ActiveWorker }) { + const oc = isOpenCodeWorker(worker); return ( -

-
+
+
- Worker + Worker {worker.task}
@@ -23,7 +26,7 @@ function WorkerBadge({ worker }: { worker: ActiveWorker }) { {worker.currentTool && ( <> · - {worker.currentTool} + {worker.currentTool} )} {worker.toolCalls > 0 && ( diff --git a/interface/src/components/ChannelEditModal.tsx b/interface/src/components/ChannelEditModal.tsx index e290a321b..e29a72251 100644 --- a/interface/src/components/ChannelEditModal.tsx +++ b/interface/src/components/ChannelEditModal.tsx @@ -18,7 +18,7 @@ import { import {PlatformIcon} from "@/lib/platformIcons"; import {TagInput} from "@/components/TagInput"; -type Platform = "discord" | "slack" | "telegram" | "twitch" | "webhook"; +type Platform = "discord" | "slack" | "telegram" | "twitch" | "email" | "webhook"; interface ChannelEditModalProps { platform: Platform; diff --git a/interface/src/components/ChannelSettingCard.tsx b/interface/src/components/ChannelSettingCard.tsx index d44b9801f..198babecb 100644 --- a/interface/src/components/ChannelSettingCard.tsx +++ b/interface/src/components/ChannelSettingCard.tsx @@ -1,7 +1,12 @@ import {useState} from "react"; import {AnimatePresence, motion} from "framer-motion"; import {useMutation, useQuery, useQueryClient} from "@tanstack/react-query"; -import {api, type PlatformStatus, type BindingInfo} from "@/api/client"; +import { + api, + type AdapterInstanceStatus, + type BindingInfo, + type CreateMessagingInstanceRequest, +} from "@/api/client"; import { Button, Input, @@ -20,42 +25,101 @@ import { import {PlatformIcon} from "@/lib/platformIcons"; import {TagInput} from "@/components/TagInput"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; -import {faChevronDown} from "@fortawesome/free-solid-svg-icons"; +import {faChevronDown, faPlus} from "@fortawesome/free-solid-svg-icons"; -type Platform = "discord" | "slack" | "telegram" | "twitch" | "webhook"; +type Platform = "discord" | "slack" | "telegram" | "twitch" | "email" | "webhook"; -interface ChannelSettingCardProps { - platform: Platform; - name: string; - description: string; - status?: PlatformStatus; +const PLATFORM_LABELS: Record = { + discord: "Discord", + slack: "Slack", + telegram: "Telegram", + twitch: "Twitch", + email: "Email", + webhook: "Webhook", +}; + +const DOC_LINKS: Partial> = { + discord: "https://docs.spacebot.sh/discord-setup", + slack: "https://docs.spacebot.sh/slack-setup", + telegram: "https://docs.spacebot.sh/telegram-setup", + twitch: "https://docs.spacebot.sh/twitch-setup", +}; + +// --- Platform Catalog (Left Column) --- + +interface PlatformCatalogProps { + onAddInstance: (platform: Platform) => void; +} + +export function PlatformCatalog({onAddInstance}: PlatformCatalogProps) { + const PLATFORMS: Platform[] = [ + "discord", + "slack", + "telegram", + "twitch", + "email", + "webhook", + ]; + + const COMING_SOON = [ + {platform: "whatsapp", name: "WhatsApp"}, + {platform: "matrix", name: "Matrix"}, + {platform: "imessage", name: "iMessage"}, + {platform: "irc", name: "IRC"}, + {platform: "lark", name: "Lark"}, + {platform: "dingtalk", name: "DingTalk"}, + ]; + + return ( +
+

+ Available +

+ {PLATFORMS.map((platform) => ( + + ))} + +

+ Coming Soon +

+ {COMING_SOON.map(({platform, name}) => ( +
+ + {name} +
+ ))} +
+ ); +} + +// --- Instance Card (Right Column) --- + +interface InstanceCardProps { + instance: AdapterInstanceStatus; expanded: boolean; - onToggle: () => void; + onToggleExpand: () => void; } -export function ChannelSettingCard({ - platform, - name, - description, - status, - expanded, - onToggle, -}: ChannelSettingCardProps) { +export function InstanceCard({instance, expanded, onToggleExpand}: InstanceCardProps) { const queryClient = useQueryClient(); - const configured = status?.configured ?? false; - const enabled = status?.enabled ?? false; - - const [credentialInputs, setCredentialInputs] = useState< - Record - >({}); - const [message, setMessage] = useState<{ - text: string; - type: "success" | "error"; - } | null>(null); - const [confirmDisconnect, setConfirmDisconnect] = useState(false); - const [editingBinding, setEditingBinding] = useState( - null, - ); + const [message, setMessage] = useState<{text: string; type: "success" | "error"} | null>(null); + const [confirmRemove, setConfirmRemove] = useState(false); + const [editingBinding, setEditingBinding] = useState(null); const [addingBinding, setAddingBinding] = useState(false); const [bindingForm, setBindingForm] = useState({ agent_id: "main", @@ -67,11 +131,16 @@ export function ChannelSettingCard({ dm_allowed_users: [] as string[], }); + const platform = instance.platform as Platform; + const instanceLabel = instance.name + ? `${PLATFORM_LABELS[platform]} "${instance.name}"` + : PLATFORM_LABELS[platform]; + const {data: bindingsData} = useQuery({ queryKey: ["bindings"], queryFn: () => api.bindings(), staleTime: 5_000, - enabled: expanded && configured, + enabled: expanded, }); const {data: agentsData} = useQuery({ @@ -81,35 +150,35 @@ export function ChannelSettingCard({ enabled: expanded, }); - const platformBindings = - bindingsData?.bindings?.filter((b) => b.channel === platform) ?? []; + // Filter bindings for this specific instance + const instanceBindings = (bindingsData?.bindings ?? []).filter((binding) => { + if (binding.channel !== platform) return false; + if (instance.name === null) return !binding.adapter; + return binding.adapter === instance.name; + }); const toggleEnabled = useMutation({ mutationFn: (newEnabled: boolean) => - api.togglePlatform(platform, newEnabled), + api.togglePlatform(platform, newEnabled, instance.name ?? undefined), onSuccess: () => { queryClient.invalidateQueries({queryKey: ["messaging-status"]}); }, - onError: (error) => - setMessage({text: `Failed: ${error.message}`, type: "error"}), + onError: (error) => setMessage({text: `Failed: ${error.message}`, type: "error"}), }); - // --- Mutations --- - - const saveCreds = useMutation({ - mutationFn: api.createBinding, + const deleteInstance = useMutation({ + mutationFn: () => + api.deleteMessagingInstance({platform, name: instance.name ?? undefined}), onSuccess: (result) => { if (result.success) { - setCredentialInputs({}); - setMessage({text: result.message, type: "success"}); + setConfirmRemove(false); queryClient.invalidateQueries({queryKey: ["messaging-status"]}); queryClient.invalidateQueries({queryKey: ["bindings"]}); } else { setMessage({text: result.message, type: "error"}); } }, - onError: (error) => - setMessage({text: `Failed: ${error.message}`, type: "error"}), + onError: (error) => setMessage({text: `Failed: ${error.message}`, type: "error"}), }); const addBindingMutation = useMutation({ @@ -120,12 +189,12 @@ export function ChannelSettingCard({ resetBindingForm(); setMessage({text: result.message, type: "success"}); queryClient.invalidateQueries({queryKey: ["bindings"]}); + queryClient.invalidateQueries({queryKey: ["messaging-status"]}); } else { setMessage({text: result.message, type: "error"}); } }, - onError: (error) => - setMessage({text: `Failed: ${error.message}`, type: "error"}), + onError: (error) => setMessage({text: `Failed: ${error.message}`, type: "error"}), }); const updateBindingMutation = useMutation({ @@ -136,12 +205,12 @@ export function ChannelSettingCard({ resetBindingForm(); setMessage({text: result.message, type: "success"}); queryClient.invalidateQueries({queryKey: ["bindings"]}); + queryClient.invalidateQueries({queryKey: ["messaging-status"]}); } else { setMessage({text: result.message, type: "error"}); } }, - onError: (error) => - setMessage({text: `Failed: ${error.message}`, type: "error"}), + onError: (error) => setMessage({text: `Failed: ${error.message}`, type: "error"}), }); const deleteBindingMutation = useMutation({ @@ -150,24 +219,12 @@ export function ChannelSettingCard({ if (result.success) { setMessage({text: result.message, type: "success"}); queryClient.invalidateQueries({queryKey: ["bindings"]}); + queryClient.invalidateQueries({queryKey: ["messaging-status"]}); } else { setMessage({text: result.message, type: "error"}); } }, - onError: (error) => - setMessage({text: `Failed: ${error.message}`, type: "error"}), - }); - - const disconnectMutation = useMutation({ - mutationFn: () => api.disconnectPlatform(platform), - onSuccess: () => { - setConfirmDisconnect(false); - setMessage(null); - queryClient.invalidateQueries({queryKey: ["messaging-status"]}); - queryClient.invalidateQueries({queryKey: ["bindings"]}); - }, - onError: (error) => - setMessage({text: `Failed: ${error.message}`, type: "error"}), + onError: (error) => setMessage({text: `Failed: ${error.message}`, type: "error"}), }); function resetBindingForm() { @@ -182,43 +239,15 @@ export function ChannelSettingCard({ }); } - function handleSaveCredentials() { - const request: any = {agent_id: "main", channel: platform}; - if (platform === "discord") { - if (!credentialInputs.discord_token?.trim()) return; - request.platform_credentials = { - discord_token: credentialInputs.discord_token.trim(), - }; - } else if (platform === "slack") { - if ( - !credentialInputs.slack_bot_token?.trim() || - !credentialInputs.slack_app_token?.trim() - ) - return; - request.platform_credentials = { - slack_bot_token: credentialInputs.slack_bot_token.trim(), - slack_app_token: credentialInputs.slack_app_token.trim(), - }; - } else if (platform === "telegram") { - if (!credentialInputs.telegram_token?.trim()) return; - request.platform_credentials = { - telegram_token: credentialInputs.telegram_token.trim(), - }; - } else if (platform === "twitch") { - if (!credentialInputs.twitch_username?.trim() || !credentialInputs.twitch_oauth_token?.trim()) return; - request.platform_credentials = { - twitch_username: credentialInputs.twitch_username.trim(), - twitch_oauth_token: credentialInputs.twitch_oauth_token.trim(), - twitch_client_id: credentialInputs.twitch_client_id?.trim(), - twitch_client_secret: credentialInputs.twitch_client_secret?.trim(), - twitch_refresh_token: credentialInputs.twitch_refresh_token?.trim(), - }; - } - saveCreds.mutate(request); - } - function handleAddBinding() { - const request: any = {agent_id: bindingForm.agent_id, channel: platform}; + const request: Record = { + agent_id: bindingForm.agent_id, + channel: platform, + }; + // Auto-populate adapter for named instances + if (instance.name) { + request.adapter = instance.name; + } if (platform === "discord" && bindingForm.guild_id.trim()) request.guild_id = bindingForm.guild_id.trim(); if (platform === "slack" && bindingForm.workspace_id.trim()) @@ -231,20 +260,24 @@ export function ChannelSettingCard({ request.require_mention = true; if (bindingForm.dm_allowed_users.length > 0) request.dm_allowed_users = bindingForm.dm_allowed_users; - addBindingMutation.mutate(request); + addBindingMutation.mutate(request as any); } function handleUpdateBinding() { if (!editingBinding) return; - const request: any = { + const request: Record = { original_agent_id: editingBinding.agent_id, original_channel: editingBinding.channel, + original_adapter: editingBinding.adapter || undefined, original_guild_id: editingBinding.guild_id || undefined, original_workspace_id: editingBinding.workspace_id || undefined, original_chat_id: editingBinding.chat_id || undefined, agent_id: bindingForm.agent_id, channel: platform, }; + if (instance.name) { + request.adapter = instance.name; + } if (platform === "discord" && bindingForm.guild_id.trim()) request.guild_id = bindingForm.guild_id.trim(); if (platform === "slack" && bindingForm.workspace_id.trim()) @@ -252,18 +285,21 @@ export function ChannelSettingCard({ if (platform === "telegram" && bindingForm.chat_id.trim()) request.chat_id = bindingForm.chat_id.trim(); request.channel_ids = bindingForm.channel_ids; - request.require_mention = - platform === "discord" ? bindingForm.require_mention : false; + request.require_mention = platform === "discord" ? bindingForm.require_mention : false; request.dm_allowed_users = bindingForm.dm_allowed_users; - updateBindingMutation.mutate(request); + updateBindingMutation.mutate(request as any); } function handleDeleteBinding(binding: BindingInfo) { - const request: any = {agent_id: binding.agent_id, channel: binding.channel}; + const request: Record = { + agent_id: binding.agent_id, + channel: binding.channel, + }; + if (binding.adapter) request.adapter = binding.adapter; if (binding.guild_id) request.guild_id = binding.guild_id; if (binding.workspace_id) request.workspace_id = binding.workspace_id; if (binding.chat_id) request.chat_id = binding.chat_id; - deleteBindingMutation.mutate(request); + deleteBindingMutation.mutate(request as any); } function startEditBinding(binding: BindingInfo) { @@ -284,29 +320,29 @@ export function ChannelSettingCard({ return (
- {/* Header — always visible, acts as toggle */} -
- +
- {name} - {configured && ( - - {enabled ? "● Active" : "○ Disabled"} - - )} + + {PLATFORM_LABELS[platform]} + + + {instance.name || "default"} + + + {instance.enabled ? "● Active" : "○ Disabled"} +
-

{description}

+

+ {instance.binding_count} binding{instance.binding_count !== 1 ? "s" : ""} +

-
+ {/* Expanded content */} @@ -327,67 +363,126 @@ export function ChannelSettingCard({ transition={{duration: 0.25, ease: [0.4, 0, 0.2, 1]}} className="overflow-hidden" > -
+
{/* Enable/Disable toggle */} - {configured && ( +
+
+ Enabled +

+ {instance.enabled ? "Receiving messages" : "Adapter paused"} +

+
+ toggleEnabled.mutate(checked)} + disabled={toggleEnabled.isPending} + /> +
+ + {/* Bindings section (scoped to this instance) */} +
-
- - Enabled - -

- {enabled - ? `${name} is receiving messages` - : `${name} is disconnected`} -

-
- toggleEnabled.mutate(checked)} - disabled={toggleEnabled.isPending} - /> +

Bindings

+
- )} - {/* Credentials */} - - {/* Bindings (only when connected) */} - {configured && ( - { - setAddingBinding(true); - setEditingBinding(null); - resetBindingForm(); - setMessage(null); - }} - onStartEdit={startEditBinding} - onCancelEdit={() => { - setEditingBinding(null); - setAddingBinding(false); - setMessage(null); + {instanceBindings.length > 0 ? ( +
+ {instanceBindings.map((binding, idx) => ( +
+
+ {binding.agent_id} +
+ {binding.guild_id && Guild: {binding.guild_id}} + {binding.workspace_id && Workspace: {binding.workspace_id}} + {binding.chat_id && Chat: {binding.chat_id}} + {binding.channel_ids.length > 0 && ( + + {binding.channel_ids.length} channel{binding.channel_ids.length > 1 ? "s" : ""} + + )} + {binding.dm_allowed_users.length > 0 && ( + + {binding.dm_allowed_users.length} DM user{binding.dm_allowed_users.length > 1 ? "s" : ""} + + )} + {binding.require_mention && Mention only} + {!binding.guild_id && + !binding.workspace_id && + !binding.chat_id && + binding.channel_ids.length === 0 && ( + All conversations + )} +
+
+ + +
+ ))} +
+ ) : ( +

+ No bindings. Messages will route to the default agent. +

+ )} + + {/* Add/Edit binding modal */} + { + if (!open) { + setEditingBinding(null); + setAddingBinding(false); + setMessage(null); + } }} - onAdd={handleAddBinding} - onUpdate={handleUpdateBinding} - onDelete={handleDeleteBinding} - addPending={addBindingMutation.isPending} - updatePending={updateBindingMutation.isPending} - deletePending={deleteBindingMutation.isPending} - /> - )} + > + + + + {editingBinding ? "Edit Binding" : "Add Binding"} + + + { + setEditingBinding(null); + setAddingBinding(false); + setMessage(null); + }} + saving={editingBinding ? updateBindingMutation.isPending : addBindingMutation.isPending} + /> + + +
{/* Status message */} {message && ( @@ -402,16 +497,38 @@ export function ChannelSettingCard({
)} - {/* Disconnect */} - {configured && platform !== "webhook" && ( - disconnectMutation.mutate()} - disconnecting={disconnectMutation.isPending} - /> - )} + {/* Remove instance */} +
+ {!confirmRemove ? ( + + ) : ( +
+

+ This will remove credentials and bindings for {instanceLabel}. + The adapter will stop immediately. +

+
+ + +
+
+ )} +
)} @@ -420,454 +537,430 @@ export function ChannelSettingCard({ ); } -// --- Disabled card for coming soon platforms --- +// --- Add Instance Inline Card --- -interface DisabledChannelCardProps { - platform: string; - name: string; - description: string; +interface AddInstanceCardProps { + platform: Platform; + isDefault: boolean; + onCancel: () => void; + onCreated: () => void; } -export function DisabledChannelCard({ - platform, - name, - description, -}: DisabledChannelCardProps) { - return ( -
-
- -
- {name} -

{description}

-
- -
-
- ); -} +export function AddInstanceCard({platform, isDefault, onCancel, onCreated}: AddInstanceCardProps) { + const queryClient = useQueryClient(); + const [instanceName, setInstanceName] = useState(""); + const [credentialInputs, setCredentialInputs] = useState>({}); + const [message, setMessage] = useState<{text: string; type: "success" | "error"} | null>(null); -// --- Sub-sections --- + const createInstance = useMutation({ + mutationFn: api.createMessagingInstance, + onSuccess: (result) => { + if (result.success) { + queryClient.invalidateQueries({queryKey: ["messaging-status"]}); + queryClient.invalidateQueries({queryKey: ["bindings"]}); + onCreated(); + } else { + setMessage({text: result.message, type: "error"}); + } + }, + onError: (error) => setMessage({text: `Failed: ${error.message}`, type: "error"}), + }); -function CredentialsSection({ - platform, - configured, - credentialInputs, - setCredentialInputs, - onSave, - saving, -}: { - platform: Platform; - configured: boolean; - credentialInputs: Record; - setCredentialInputs: (inputs: Record) => void; - onSave: () => void; - saving: boolean; -}) { - return ( -
-

- {configured ? "Update Credentials" : "Credentials"} -

+ function handleSave() { + const credentials: CreateMessagingInstanceRequest["credentials"] = {}; - {platform === "discord" && ( -
- - - setCredentialInputs({ - ...credentialInputs, - discord_token: e.target.value, - }) - } - placeholder={ - configured - ? "Enter new token to update" - : "MTk4NjIyNDgzNDcxOTI1MjQ4.D..." - } - onKeyDown={(e) => { - if (e.key === "Enter") onSave(); - }} - /> -

- Need help?{" "} - - Read the Discord setup docs → - -

+ if (platform === "discord") { + if (!credentialInputs.discord_token?.trim()) { + setMessage({text: "Bot token is required", type: "error"}); + return; + } + credentials.discord_token = credentialInputs.discord_token.trim(); + } else if (platform === "slack") { + if (!credentialInputs.slack_bot_token?.trim() || !credentialInputs.slack_app_token?.trim()) { + setMessage({text: "Both bot token and app token are required", type: "error"}); + return; + } + credentials.slack_bot_token = credentialInputs.slack_bot_token.trim(); + credentials.slack_app_token = credentialInputs.slack_app_token.trim(); + } else if (platform === "telegram") { + if (!credentialInputs.telegram_token?.trim()) { + setMessage({text: "Bot token is required", type: "error"}); + return; + } + credentials.telegram_token = credentialInputs.telegram_token.trim(); + } else if (platform === "twitch") { + if (!credentialInputs.twitch_username?.trim() || !credentialInputs.twitch_oauth_token?.trim()) { + setMessage({text: "Username and OAuth token are required", type: "error"}); + return; + } + credentials.twitch_username = credentialInputs.twitch_username.trim(); + credentials.twitch_oauth_token = credentialInputs.twitch_oauth_token.trim(); + if (credentialInputs.twitch_client_id?.trim()) + credentials.twitch_client_id = credentialInputs.twitch_client_id.trim(); + if (credentialInputs.twitch_client_secret?.trim()) + credentials.twitch_client_secret = credentialInputs.twitch_client_secret.trim(); + if (credentialInputs.twitch_refresh_token?.trim()) + credentials.twitch_refresh_token = credentialInputs.twitch_refresh_token.trim(); + } else if (platform === "email") { + if (!credentialInputs.email_imap_host?.trim() || !credentialInputs.email_smtp_host?.trim()) { + setMessage({text: "IMAP host and SMTP host are required", type: "error"}); + return; + } + if (!credentialInputs.email_imap_username?.trim() || !credentialInputs.email_imap_password?.trim()) { + setMessage({text: "IMAP username and password are required", type: "error"}); + return; + } + if (!credentialInputs.email_smtp_username?.trim() || !credentialInputs.email_smtp_password?.trim()) { + setMessage({text: "SMTP username and password are required", type: "error"}); + return; + } + if (!credentialInputs.email_from_address?.trim()) { + setMessage({text: "From address is required", type: "error"}); + return; + } + credentials.email_imap_host = credentialInputs.email_imap_host.trim(); + credentials.email_imap_username = credentialInputs.email_imap_username.trim(); + credentials.email_imap_password = credentialInputs.email_imap_password.trim(); + credentials.email_smtp_host = credentialInputs.email_smtp_host.trim(); + credentials.email_smtp_username = credentialInputs.email_smtp_username.trim(); + credentials.email_smtp_password = credentialInputs.email_smtp_password.trim(); + credentials.email_from_address = credentialInputs.email_from_address.trim(); + if (credentialInputs.email_imap_port?.trim()) + credentials.email_imap_port = parseInt(credentialInputs.email_imap_port.trim(), 10) || undefined; + if (credentialInputs.email_smtp_port?.trim()) + credentials.email_smtp_port = parseInt(credentialInputs.email_smtp_port.trim(), 10) || undefined; + } else if (platform === "webhook") { + if (credentialInputs.webhook_port?.trim()) + credentials.webhook_port = parseInt(credentialInputs.webhook_port.trim(), 10) || undefined; + if (credentialInputs.webhook_bind?.trim()) + credentials.webhook_bind = credentialInputs.webhook_bind.trim(); + if (credentialInputs.webhook_auth_token?.trim()) + credentials.webhook_auth_token = credentialInputs.webhook_auth_token.trim(); + } + + if (!isDefault && !instanceName.trim()) { + setMessage({text: "Instance name is required", type: "error"}); + return; + } + + createInstance.mutate({ + platform, + name: isDefault ? undefined : instanceName.trim(), + credentials, + }); + } + + const docLink = DOC_LINKS[platform]; + + return ( +
+
+
+ + + {isDefault + ? `Add ${PLATFORM_LABELS[platform]}` + : `Add ${PLATFORM_LABELS[platform]} Instance`} +
- )} - {platform === "slack" && ( - <> + {/* Instance name (only for non-default) */} + {!isDefault && (
-
+ )} + + {/* Platform-specific credential fields */} + {platform === "discord" && (
- + - setCredentialInputs({ - ...credentialInputs, - slack_app_token: e.target.value, - }) - } - placeholder={ - configured ? "Enter new token to update" : "xapp-..." - } - onKeyDown={(e) => { - if (e.key === "Enter") onSave(); - }} + value={credentialInputs.discord_token ?? ""} + onChange={(e) => setCredentialInputs({...credentialInputs, discord_token: e.target.value})} + placeholder="MTk4NjIyNDgzNDcxOTI1MjQ4.D..." + onKeyDown={(e) => { if (e.key === "Enter") handleSave(); }} />
-

- Need help?{" "} - - Read the Slack setup docs → - -

- - )} - - {platform === "telegram" && ( -
- - - setCredentialInputs({ - ...credentialInputs, - telegram_token: e.target.value, - }) - } - placeholder={ - configured - ? "Enter new token to update" - : "123456789:ABCdefGHI..." - } - onKeyDown={(e) => { - if (e.key === "Enter") onSave(); - }} - /> -

- Need help?{" "} - - Read the Telegram setup docs → - -

-
- )} + )} - {platform === "twitch" && ( - <> -
- - - setCredentialInputs({ - ...credentialInputs, - twitch_username: e.target.value, - }) - } - placeholder={ - configured ? "Enter new username to update" : "my_bot" - } - /> -
-
+ {platform === "slack" && ( + <>
- + - setCredentialInputs({ - ...credentialInputs, - twitch_client_id: e.target.value, - }) - } - placeholder={ - configured ? "Enter new client id to update" : "your-app-client-id" - } + value={credentialInputs.slack_bot_token ?? ""} + onChange={(e) => setCredentialInputs({...credentialInputs, slack_bot_token: e.target.value})} + placeholder="xoxb-..." />
- + - setCredentialInputs({ - ...credentialInputs, - twitch_client_secret: e.target.value, - }) - } - placeholder={ - configured ? "Enter new client secret to update" : "your-app-client-secret" - } + value={credentialInputs.slack_app_token ?? ""} + onChange={(e) => setCredentialInputs({...credentialInputs, slack_app_token: e.target.value})} + placeholder="xapp-..." + onKeyDown={(e) => { if (e.key === "Enter") handleSave(); }} />
+ + )} + + {platform === "telegram" && ( +
+ + setCredentialInputs({...credentialInputs, telegram_token: e.target.value})} + placeholder="123456789:ABCdefGHI..." + onKeyDown={(e) => { if (e.key === "Enter") handleSave(); }} + />
-
+ )} + + {platform === "twitch" && ( + <>
- + - setCredentialInputs({ - ...credentialInputs, - twitch_oauth_token: e.target.value, - }) - } - placeholder={ - configured ? "Enter new token to update" : "abcd1234..." - } - onKeyDown={(e) => { - if (e.key === "Enter") onSave(); - }} + value={credentialInputs.twitch_username ?? ""} + onChange={(e) => setCredentialInputs({...credentialInputs, twitch_username: e.target.value})} + placeholder="my_bot" />
+
+
+ + setCredentialInputs({...credentialInputs, twitch_client_id: e.target.value})} + placeholder="your-client-id" + /> +
+
+ + setCredentialInputs({...credentialInputs, twitch_client_secret: e.target.value})} + placeholder="your-client-secret" + /> +
+
+
+
+ + setCredentialInputs({...credentialInputs, twitch_oauth_token: e.target.value})} + placeholder="abcd1234..." + onKeyDown={(e) => { if (e.key === "Enter") handleSave(); }} + /> +
+
+ + setCredentialInputs({...credentialInputs, twitch_refresh_token: e.target.value})} + placeholder="refresh-token" + /> +
+
+ + )} + + {platform === "email" && ( + <> +
+
+ + setCredentialInputs({...credentialInputs, email_imap_host: e.target.value})} + placeholder="imap.gmail.com" + /> +
+
+ + setCredentialInputs({...credentialInputs, email_imap_port: e.target.value})} + placeholder="993" + /> +
+
+
+
+ + setCredentialInputs({...credentialInputs, email_imap_username: e.target.value})} + placeholder="user@example.com" + /> +
+
+ + setCredentialInputs({...credentialInputs, email_imap_password: e.target.value})} + placeholder="App password" + /> +
+
+
+
+ + setCredentialInputs({...credentialInputs, email_smtp_host: e.target.value})} + placeholder="smtp.gmail.com" + /> +
+
+ + setCredentialInputs({...credentialInputs, email_smtp_port: e.target.value})} + placeholder="587" + /> +
+
+
+
+ + setCredentialInputs({...credentialInputs, email_smtp_username: e.target.value})} + placeholder="user@example.com" + /> +
+
+ + setCredentialInputs({...credentialInputs, email_smtp_password: e.target.value})} + placeholder="App password" + /> +
+
- + + setCredentialInputs({...credentialInputs, email_from_address: e.target.value})} + placeholder="bot@example.com" + onKeyDown={(e) => { if (e.key === "Enter") handleSave(); }} + /> +
+ + )} + + {platform === "webhook" && ( + <> +
+
+ + setCredentialInputs({...credentialInputs, webhook_port: e.target.value})} + placeholder="18789" + /> +
+
+ + setCredentialInputs({...credentialInputs, webhook_bind: e.target.value})} + placeholder="127.0.0.1" + /> +
+
+
+ - setCredentialInputs({ - ...credentialInputs, - twitch_refresh_token: e.target.value, - }) - } - placeholder={ - configured ? "Enter new refresh token to update" : "refresh-token-from-twitch" - } + value={credentialInputs.webhook_auth_token ?? ""} + onChange={(e) => setCredentialInputs({...credentialInputs, webhook_auth_token: e.target.value})} + placeholder="Optional — leave empty for no auth" + onKeyDown={(e) => { if (e.key === "Enter") handleSave(); }} />
-
-

- Use tokens from your Twitch application with chat:read and chat:write scopes enabled. Tokens are stored in your Spacebot instance and refreshed automatically while running. -

-

+ + )} + + {docLink && ( +

Need help?{" "} - - Read the Twitch setup docs → + + Read the {PLATFORM_LABELS[platform]} setup docs →

- - )} - - {platform === "webhook" && ( -

- Webhook receiver requires no additional credentials. -

- )} - - {platform !== "webhook" && - Object.values(credentialInputs).some((v) => v?.trim()) && ( - )} -
- ); -} -function BindingsSection({ - platform, - bindings, - agents, - isEditingOrAdding, - editingBinding, - bindingForm, - setBindingForm, - onStartAdd, - onStartEdit, - onCancelEdit, - onAdd, - onUpdate, - onDelete, - addPending, - updatePending, - deletePending, -}: { - platform: Platform; - bindings: BindingInfo[]; - agents: {id: string}[]; - isEditingOrAdding: boolean; - editingBinding: BindingInfo | null; - bindingForm: { - agent_id: string; - guild_id: string; - workspace_id: string; - chat_id: string; - channel_ids: string[]; - require_mention: boolean; - dm_allowed_users: string[]; - }; - setBindingForm: (form: any) => void; - onStartAdd: () => void; - onStartEdit: (binding: BindingInfo) => void; - onCancelEdit: () => void; - onAdd: () => void; - onUpdate: () => void; - onDelete: (binding: BindingInfo) => void; - addPending: boolean; - updatePending: boolean; - deletePending: boolean; -}) { - return ( -
-
-

Bindings

- -
+ {message && ( +
+ {message.text} +
+ )} - {/* Binding list */} - {bindings.length > 0 ? ( -
- {bindings.map((binding, idx) => ( -
-
- {binding.agent_id} -
- {binding.guild_id && Guild: {binding.guild_id}} - {binding.workspace_id && ( - Workspace: {binding.workspace_id} - )} - {binding.chat_id && Chat: {binding.chat_id}} - {binding.channel_ids.length > 0 && ( - - {binding.channel_ids.length} channel - {binding.channel_ids.length > 1 ? "s" : ""} - - )} - {binding.dm_allowed_users.length > 0 && ( - - {binding.dm_allowed_users.length} DM user - {binding.dm_allowed_users.length > 1 ? "s" : ""} - - )} - {binding.require_mention && ( - Mention only - )} - {!binding.guild_id && - !binding.workspace_id && - !binding.chat_id && - binding.channel_ids.length === 0 && ( - All conversations - )} -
-
- - -
- ))} +
+ +
- ) : ( -

- No bindings. Messages will route to the default agent. -

- )} - - {/* Add/Edit binding modal */} - { - if (!open) onCancelEdit(); - }} - > - - - - {editingBinding ? "Edit Binding" : "Add Binding"} - - - - - +
); } +// --- Binding Form (shared between add/edit) --- + function BindingForm({ platform, agents, @@ -886,6 +979,7 @@ function BindingForm({ workspace_id: string; chat_id: string; channel_ids: string[]; + require_mention: boolean; dm_allowed_users: string[]; }; setBindingForm: (form: any) => void; @@ -897,21 +991,15 @@ function BindingForm({ return (
- + @@ -919,62 +1007,46 @@ function BindingForm({ {platform === "discord" && (
- + - setBindingForm({...bindingForm, guild_id: e.target.value}) - } - placeholder="Optional — leave empty for all servers" + onChange={(e) => setBindingForm({...bindingForm, guild_id: e.target.value})} + placeholder="Optional -- leave empty for all servers" />
)} {platform === "slack" && (
- + - setBindingForm({...bindingForm, workspace_id: e.target.value}) - } - placeholder="Optional — leave empty for all workspaces" + onChange={(e) => setBindingForm({...bindingForm, workspace_id: e.target.value})} + placeholder="Optional -- leave empty for all workspaces" />
)} {platform === "telegram" && (
- + - setBindingForm({...bindingForm, chat_id: e.target.value}) - } - placeholder="Optional — leave empty for all chats" + onChange={(e) => setBindingForm({...bindingForm, chat_id: e.target.value})} + placeholder="Optional -- leave empty for all chats" />
)} {(platform === "discord" || platform === "slack") && (
- + - setBindingForm({...bindingForm, channel_ids: ids}) - } + onChange={(ids) => setBindingForm({...bindingForm, channel_ids: ids})} placeholder="Add channel ID..." />
@@ -985,49 +1057,35 @@ function BindingForm({ - setBindingForm({...bindingForm, require_mention: e.target.checked}) - } + onChange={(e) => setBindingForm({...bindingForm, require_mention: e.target.checked})} className="h-4 w-4 rounded border-app-line bg-app-box" /> - +
)} {platform === "twitch" && (
- + - setBindingForm({...bindingForm, channel_ids: ids}) - } + onChange={(ids) => setBindingForm({...bindingForm, channel_ids: ids})} placeholder="Add channel name..." />
)}
- + - setBindingForm({...bindingForm, dm_allowed_users: users}) - } + onChange={(users) => setBindingForm({...bindingForm, dm_allowed_users: users})} placeholder="Add user ID..." />
- + @@ -1036,54 +1094,29 @@ function BindingForm({ ); } -function DisconnectSection({ +// --- Backward compat: keep DisabledChannelCard export for any other consumers --- + +export function DisabledChannelCard({ + platform, name, - confirmDisconnect, - setConfirmDisconnect, - onDisconnect, - disconnecting, + description, }: { + platform: string; name: string; - confirmDisconnect: boolean; - setConfirmDisconnect: (v: boolean) => void; - onDisconnect: () => void; - disconnecting: boolean; + description: string; }) { return ( -
- {!confirmDisconnect ? ( - - ) : ( -
-

- This will remove all credentials and bindings for {name}. The bot - will stop responding immediately. -

-
- - -
+
+
+ +
+ {name} +

{description}

- )} + +
); } diff --git a/interface/src/components/CortexChatPanel.tsx b/interface/src/components/CortexChatPanel.tsx index dd763848d..c9bd1c4b7 100644 --- a/interface/src/components/CortexChatPanel.tsx +++ b/interface/src/components/CortexChatPanel.tsx @@ -11,6 +11,70 @@ interface CortexChatPanelProps { onClose?: () => void; } +interface StarterPrompt { + label: string; + prompt: string; +} + +const STARTER_PROMPTS: StarterPrompt[] = [ + { + label: "Run health check", + prompt: "Give me an agent health report with active risks, stale work, and the top 3 fixes to do now.", + }, + { + label: "Audit memories", + prompt: "Audit memory quality, find stale or contradictory memories, and propose exact cleanup actions.", + }, + { + label: "Review workers", + prompt: "List recent worker runs, inspect failures, and summarize root cause plus next actions.", + }, + { + label: "Draft task spec", + prompt: "Turn this goal into a task spec with subtasks, then move it to ready when it is execution-ready: ", + }, +]; + +function EmptyCortexState({ + channelId, + onStarterPrompt, + disabled, +}: { + channelId?: string; + onStarterPrompt: (prompt: string) => void; + disabled: boolean; +}) { + const contextHint = channelId + ? "Current channel transcript is injected for this send only." + : "No channel transcript is injected. Operating at full agent scope."; + + return ( +
+
+

Cortex chat

+

+ System-level control for this agent: memory, tasks, worker inspection, and direct tool execution. +

+

{contextHint}

+ +
+ {STARTER_PROMPTS.map((item) => ( + + ))} +
+
+
+ ); +} + function ToolActivityIndicator({ activity }: { activity: ToolActivity[] }) { if (activity.length === 0) return null; @@ -128,7 +192,7 @@ function CortexChatInput({ } export function CortexChatPanel({ agentId, channelId, onClose }: CortexChatPanelProps) { - const { messages, isStreaming, error, toolActivity, sendMessage, newThread } = useCortexChat(agentId, channelId); + const { messages, threadId, isStreaming, error, toolActivity, sendMessage, newThread } = useCortexChat(agentId, channelId); const [input, setInput] = useState(""); const messagesEndRef = useRef(null); @@ -143,6 +207,11 @@ export function CortexChatPanel({ agentId, channelId, onClose }: CortexChatPanel sendMessage(trimmed); }; + const handleStarterPrompt = (prompt: string) => { + if (isStreaming || !threadId) return; + sendMessage(prompt); + }; + return (
{/* Header */} @@ -183,12 +252,6 @@ export function CortexChatPanel({ agentId, channelId, onClose }: CortexChatPanel {/* Messages */}
- {messages.length === 0 && !isStreaming && ( -
-

Ask the cortex anything

-
- )} - {messages.map((message) => (
{message.role === "user" ? ( @@ -222,6 +285,16 @@ export function CortexChatPanel({ agentId, channelId, onClose }: CortexChatPanel
+ {messages.length === 0 && !isStreaming && ( +
+ +
+ )} + {/* Input */}
{ + constructor(props: Props) { + super(props); + this.state = { hasError: false, error: null }; + } + + static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, info: ErrorInfo) { + console.error("[ErrorBoundary]", error, info.componentStack); + } + + render() { + if (!this.state.hasError) { + return this.props.children; + } + + return ( +
+
+

+ Something went wrong +

+

+ The interface crashed unexpectedly. This is usually caused by a + rendering error. +

+ {this.state.error && ( +
+							{this.state.error.message}
+						
+ )} + +
+
+ ); + } +} diff --git a/interface/src/components/Markdown.tsx b/interface/src/components/Markdown.tsx index 81bbf057e..b903aa96b 100644 --- a/interface/src/components/Markdown.tsx +++ b/interface/src/components/Markdown.tsx @@ -1,9 +1,15 @@ import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; -export function Markdown({ children }: { children: string }) { +export function Markdown({ + children, + className, +}: { + children: string; + className?: string; +}) { return ( -
+
= { anthropic: "Anthropic", openrouter: "OpenRouter", + kilo: "Kilo Gateway", openai: "OpenAI", "openai-chatgpt": "ChatGPT Plus (OAuth)", deepseek: "DeepSeek", @@ -27,6 +28,7 @@ const PROVIDER_LABELS: Record = { zhipu: "Z.ai (GLM)", ollama: "Ollama", "opencode-zen": "OpenCode Zen", + "opencode-go": "OpenCode Go", minimax: "MiniMax", "minimax-cn": "MiniMax CN", }; @@ -128,6 +130,7 @@ export function ModelSelect({ const providerOrder = [ "openrouter", + "kilo", "anthropic", "openai", "openai-chatgpt", @@ -141,12 +144,16 @@ export function ModelSelect({ "fireworks", "zhipu", "opencode-zen", + "opencode-go", "minimax", "minimax-cn", ]; + const providerRank = (provider: string) => { + const index = providerOrder.indexOf(provider); + return index === -1 ? Number.MAX_SAFE_INTEGER : index; + }; const sortedProviders = Object.keys(grouped).sort( - (a, b) => - (providerOrder.indexOf(a) ?? 99) - (providerOrder.indexOf(b) ?? 99), + (a, b) => providerRank(a) - providerRank(b), ); return ( diff --git a/interface/src/components/Sidebar.tsx b/interface/src/components/Sidebar.tsx index ef51575b8..863b9bc48 100644 --- a/interface/src/components/Sidebar.tsx +++ b/interface/src/components/Sidebar.tsx @@ -35,12 +35,13 @@ interface SidebarProps { interface SortableAgentItemProps { agentId: string; + displayName?: string; activity?: { workers: number; branches: number }; isActive: boolean; collapsed: boolean; } -function SortableAgentItem({ agentId, activity, isActive, collapsed }: SortableAgentItemProps) { +function SortableAgentItem({ agentId, displayName, activity, isActive, collapsed }: SortableAgentItemProps) { const { attributes, listeners, @@ -67,9 +68,9 @@ function SortableAgentItem({ agentId, activity, isActive, collapsed }: SortableA isActive ? "bg-sidebar-selected text-sidebar-ink" : "text-sidebar-inkDull hover:bg-sidebar-selected/50" }`} style={{ pointerEvents: isDragging ? 'none' : 'auto' }} - title={agentId} + title={displayName ?? agentId} > - {agentId.charAt(0).toUpperCase()} + {(displayName ?? agentId).charAt(0).toUpperCase()}
); @@ -87,7 +88,7 @@ function SortableAgentItem({ agentId, activity, isActive, collapsed }: SortableA }`} style={{ pointerEvents: isDragging ? 'none' : 'auto' }} > - {agentId} + {displayName ?? agentId} {activity && (activity.workers > 0 || activity.branches > 0) && (
{activity.workers > 0 && ( @@ -126,6 +127,11 @@ export function Sidebar({ liveStates, collapsed, onToggle }: SidebarProps) { const channels = channelsData?.channels ?? []; const agentIds = useMemo(() => agents.map((a) => a.id), [agents]); + const agentDisplayNames = useMemo(() => { + const map: Record = {}; + for (const a of agents) map[a.id] = a.display_name; + return map; + }, [agents]); const [agentOrder, setAgentOrder] = useAgentOrder(agentIds); const matchRoute = useMatchRoute(); @@ -225,17 +231,18 @@ export function Sidebar({ liveStates, collapsed, onToggle }: SidebarProps) { onDragEnd={handleDragEnd} > - {agentOrder.map((agentId) => { - const isActive = !!matchRoute({ to: "/agents/$agentId", params: { agentId }, fuzzy: true }); - return ( - - ); - })} + {agentOrder.map((agentId) => { + const isActive = !!matchRoute({ to: "/agents/$agentId", params: { agentId }, fuzzy: true }); + return ( + + ); + })} - )} - {!data.can_apply && data.deployment === "docker" && ( - - Mount docker.sock for one-click updates - - )} - - - - {applyError && ( -
- {applyError} -
- )} -
- ); -} diff --git a/interface/src/components/UpdatePill.tsx b/interface/src/components/UpdatePill.tsx new file mode 100644 index 000000000..7096114b4 --- /dev/null +++ b/interface/src/components/UpdatePill.tsx @@ -0,0 +1,27 @@ +import {useQuery} from "@tanstack/react-query"; +import {Link} from "@tanstack/react-router"; +import {api} from "@/api/client"; + +export function UpdatePill() { + const {data} = useQuery({ + queryKey: ["update-check"], + queryFn: api.updateCheck, + staleTime: 60_000, + refetchInterval: 300_000, + }); + + if (!data || !data.update_available || data.deployment === "hosted") { + return null; + } + + return ( + + + Update {data.latest_version ?? "available"} + + ); +} diff --git a/interface/src/components/WebChatPanel.tsx b/interface/src/components/WebChatPanel.tsx index 747e0de5a..ef38c20c3 100644 --- a/interface/src/components/WebChatPanel.tsx +++ b/interface/src/components/WebChatPanel.tsx @@ -1,37 +1,54 @@ -import {useEffect, useRef, useState} from "react"; -import { - useWebChat, - getPortalChatSessionId, - type ToolActivity, -} from "@/hooks/useWebChat"; -import type {ActiveWorker} from "@/hooks/useChannelLiveState"; -import {useLiveContext} from "@/hooks/useLiveContext"; -import {Markdown} from "@/components/Markdown"; +import { useEffect, useRef, useState } from "react"; +import { useWebChat } from "@/hooks/useWebChat"; +import { isOpenCodeWorker, type ActiveWorker } from "@/hooks/useChannelLiveState"; +import { useLiveContext } from "@/hooks/useLiveContext"; +import { Markdown } from "@/components/Markdown"; interface WebChatPanelProps { agentId: string; } -function ToolActivityIndicator({activity}: {activity: ToolActivity[]}) { - if (activity.length === 0) return null; +function ActiveWorkersPanel({ workers }: { workers: ActiveWorker[] }) { + if (workers.length === 0) return null; + + // Use neutral chrome when all workers are opencode, amber when all builtin, mixed stays amber + const allOpenCode = workers.every(isOpenCodeWorker); + const borderColor = allOpenCode ? "border-zinc-500/25 bg-zinc-500/5" : "border-amber-500/25 bg-amber-500/5"; + const headerColor = allOpenCode ? "text-zinc-200" : "text-amber-200"; + const dotColor = allOpenCode ? "bg-zinc-400" : "bg-amber-400"; return ( -
- {activity.map((tool, index) => ( - - {tool.status === "running" ? ( - - ) : ( - - )} - - {tool.tool} - +
+
+
+ + {workers.length} active worker{workers.length !== 1 ? "s" : ""} - ))} +
+
+ {workers.map((worker) => { + const oc = isOpenCodeWorker(worker); + return ( +
+ Worker + + {worker.task} + + {worker.status} + {worker.currentTool && ( + + {worker.currentTool} + + )} +
+ ); + })} +
); } @@ -46,51 +63,17 @@ function ThinkingIndicator() { ); } -function ActiveWorkersPanel({workers}: {workers: ActiveWorker[]}) { - if (workers.length === 0) return null; - - return ( -
-
-
- - {workers.length} active worker{workers.length !== 1 ? "s" : ""} - -
-
- {workers.map((worker) => ( -
- Worker - - {worker.task} - - {worker.status} - {worker.currentTool && ( - - {worker.currentTool} - - )} -
- ))} -
-
- ); -} - function FloatingChatInput({ value, onChange, onSubmit, - isStreaming, + disabled, agentId, }: { value: string; onChange: (value: string) => void; onSubmit: () => void; - isStreaming: boolean; + disabled: boolean; agentId: string; }) { const textareaRef = useRef(null); @@ -134,19 +117,19 @@ function FloatingChatInput({ onChange={(event) => onChange(event.target.value)} onKeyDown={handleKeyDown} placeholder={ - isStreaming + disabled ? "Waiting for response..." : `Message ${agentId}...` } - disabled={isStreaming} + disabled={disabled} rows={1} className="flex-1 resize-none bg-transparent px-1 py-1.5 text-sm text-ink placeholder:text-ink-faint/60 focus:outline-none disabled:opacity-40" - style={{maxHeight: "200px"}} + style={{ maxHeight: "200px" }} />
)} - {messages.length === 0 && !isStreaming && ( + {timeline.length === 0 && !isTyping && (

Start a conversation with {agentId} @@ -209,37 +195,27 @@ export function WebChatPanel({agentId}: WebChatPanelProps) {

)} - {messages.map((message) => ( -
- {message.role === "user" ? ( -
-
-

{message.content}

+ {timeline.map((item) => { + if (item.type !== "message") return null; + return ( +
+ {item.role === "user" ? ( +
+
+

{item.content}

+
-
- ) : ( -
- {message.content} -
- )} -
- ))} - - {/* Streaming state */} - {isStreaming && - messages[messages.length - 1]?.role !== "assistant" && ( -
- - {toolActivity.length === 0 && } + ) : ( +
+ {item.content} +
+ )}
- )} + ); + })} - {/* Inline tool activity during streaming assistant message */} - {isStreaming && - messages[messages.length - 1]?.role === "assistant" && - toolActivity.length > 0 && ( - - )} + {/* Typing indicator */} + {isTyping && } {error && (
@@ -255,7 +231,7 @@ export function WebChatPanel({agentId}: WebChatPanelProps) { value={input} onChange={setInput} onSubmit={handleSubmit} - isStreaming={isStreaming} + disabled={isSending || isTyping} agentId={agentId} />
diff --git a/interface/src/hooks/useChannelLiveState.ts b/interface/src/hooks/useChannelLiveState.ts index 516e61e59..40f73bc7b 100644 --- a/interface/src/hooks/useChannelLiveState.ts +++ b/interface/src/hooks/useChannelLiveState.ts @@ -1,9 +1,11 @@ import { useCallback, useEffect, useRef, useState } from "react"; +import { generateId } from "@/lib/id"; import { api, type BranchCompletedEvent, type BranchStartedEvent, type InboundMessageEvent, + type OutboundMessageDeltaEvent, type OutboundMessageEvent, type TimelineItem, type ToolCompletedEvent, @@ -12,6 +14,7 @@ import { type WorkerCompletedEvent, type WorkerStartedEvent, type WorkerStatusEvent, + type WorkerIdleEvent, type ChannelInfo, } from "../api/client"; @@ -22,6 +25,17 @@ export interface ActiveWorker { startedAt: number; toolCalls: number; currentTool: string | null; + /** Whether the worker is idle (waiting for follow-up input). */ + isIdle: boolean; + /** Whether this worker accepts follow-up input via route. */ + interactive: boolean; + /** Worker type: "builtin", "opencode", "task", etc. */ + workerType: string; +} + +/** Check whether a worker is an opencode worker (by type or task prefix). */ +export function isOpenCodeWorker(worker: { workerType?: string; task?: string }): boolean { + return worker.workerType === "opencode" || (worker.task?.startsWith("[opencode]") ?? false); } export interface ActiveBranch { @@ -38,6 +52,7 @@ export interface ChannelLiveState { timeline: TimelineItem[]; workers: Record; branches: Record; + streamingMessageId: string | null; historyLoaded: boolean; hasMore: boolean; loadingMore: boolean; @@ -46,7 +61,16 @@ export interface ChannelLiveState { const PAGE_SIZE = 50; function emptyLiveState(): ChannelLiveState { - return { isTyping: false, timeline: [], workers: {}, branches: {}, historyLoaded: false, hasMore: true, loadingMore: false }; + return { + isTyping: false, + timeline: [], + workers: {}, + branches: {}, + streamingMessageId: null, + historyLoaded: false, + hasMore: true, + loadingMore: false, + }; } /** Get a sortable timestamp from any timeline item. */ @@ -58,6 +82,26 @@ function itemTimestamp(item: TimelineItem): string { } } +function itemKey(item: TimelineItem): string { + return `${item.type}:${item.id}`; +} + +function assistantMessageItem( + id: string, + agentId: string, + content: string, +): TimelineItem { + return { + type: "message", + id, + role: "assistant", + sender_name: agentId, + sender_id: null, + content, + created_at: new Date().toISOString(), + }; +} + /** * Manages all live channel state from SSE events, message history loading, * and status snapshot fetching. Returns the state map and SSE event handlers. @@ -124,6 +168,9 @@ export function useChannelLiveState(channels: ChannelInfo[]) { startedAt: new Date(w.started_at).getTime(), toolCalls: w.tool_calls, currentTool: existingWorker?.currentTool ?? null, + isIdle: w.status === "idle", + interactive: w.interactive, + workerType: existingWorker?.workerType ?? (w.task.startsWith("[opencode]") ? "opencode" : "builtin"), }; } const branches: Record = {}; @@ -187,7 +234,7 @@ export function useChannelLiveState(channels: ChannelInfo[]) { const event = data as InboundMessageEvent; pushItem(event.channel_id, { type: "message", - id: `in-${Date.now()}-${crypto.randomUUID()}`, + id: `in-${generateId()}`, role: "user", sender_name: event.sender_name ?? event.sender_id, sender_id: event.sender_id, @@ -198,20 +245,121 @@ export function useChannelLiveState(channels: ChannelInfo[]) { const handleOutboundMessage = useCallback((data: unknown) => { const event = data as OutboundMessageEvent; - pushItem(event.channel_id, { - type: "message", - id: `out-${Date.now()}-${crypto.randomUUID()}`, - role: "assistant", - sender_name: event.agent_id, - sender_id: null, - content: event.text, - created_at: new Date().toISOString(), + setLiveStates((prev) => { + const existing = getOrCreate(prev, event.channel_id); + const streamingMessageId = existing.streamingMessageId; + if (streamingMessageId) { + const streamIndex = existing.timeline.findIndex( + (item) => item.type === "message" && item.id === streamingMessageId, + ); + + const timeline = [...existing.timeline]; + if (streamIndex >= 0) { + const streamItem = timeline[streamIndex]; + if (streamItem.type === "message") { + timeline[streamIndex] = { ...streamItem, content: event.text }; + } + } else { + timeline.push( + assistantMessageItem( + `out-${generateId()}`, + event.agent_id, + event.text, + ), + ); + } + + return { + ...prev, + [event.channel_id]: { + ...existing, + timeline, + streamingMessageId: null, + isTyping: false, + }, + }; + } + + return { + ...prev, + [event.channel_id]: { + ...existing, + timeline: [ + ...existing.timeline, + assistantMessageItem( + `out-${generateId()}`, + event.agent_id, + event.text, + ), + ], + isTyping: false, + }, + }; }); + }, []); + + const handleOutboundMessageDelta = useCallback((data: unknown) => { + const event = data as OutboundMessageDeltaEvent; setLiveStates((prev) => { const existing = getOrCreate(prev, event.channel_id); - return { ...prev, [event.channel_id]: { ...existing, isTyping: false } }; + const streamMessageId = existing.streamingMessageId; + + if (streamMessageId) { + const streamIndex = existing.timeline.findIndex( + (item) => item.type === "message" && item.id === streamMessageId, + ); + + if (streamIndex >= 0) { + const timeline = [...existing.timeline]; + const streamItem = timeline[streamIndex]; + if (streamItem.type === "message") { + timeline[streamIndex] = { + ...streamItem, + content: event.aggregated_text, + }; + } + return { + ...prev, + [event.channel_id]: { ...existing, timeline }, + }; + } + + const messageId = `stream-${generateId()}`; + return { + ...prev, + [event.channel_id]: { + ...existing, + timeline: [ + ...existing.timeline, + assistantMessageItem( + messageId, + event.agent_id, + event.aggregated_text, + ), + ], + streamingMessageId: messageId, + }, + }; + } + + const messageId = `stream-${generateId()}`; + return { + ...prev, + [event.channel_id]: { + ...existing, + timeline: [ + ...existing.timeline, + assistantMessageItem( + messageId, + event.agent_id, + event.aggregated_text, + ), + ], + streamingMessageId: messageId, + }, + }; }); - }, [pushItem]); + }, []); const handleTypingState = useCallback((data: unknown) => { const event = data as TypingStateEvent; @@ -235,14 +383,17 @@ export function useChannelLiveState(channels: ChannelInfo[]) { ...existing, workers: { ...existing.workers, - [event.worker_id]: { - id: event.worker_id, - task: event.task, - status: "starting", - startedAt: Date.now(), - toolCalls: 0, - currentTool: null, - }, + [event.worker_id]: { + id: event.worker_id, + task: event.task, + status: "starting", + startedAt: Date.now(), + toolCalls: 0, + currentTool: null, + isIdle: false, + interactive: event.interactive ?? false, + workerType: event.worker_type ?? "builtin", + }, }, }, }; @@ -274,7 +425,7 @@ export function useChannelLiveState(channels: ChannelInfo[]) { ...state, workers: { ...state.workers, - [event.worker_id]: { ...worker, status: event.status }, + [event.worker_id]: { ...worker, status: event.status, isIdle: false }, }, }, }; @@ -296,7 +447,52 @@ export function useChannelLiveState(channels: ChannelInfo[]) { ...state, workers: { ...state.workers, - [event.worker_id]: { ...worker, status: event.status }, + [event.worker_id]: { ...worker, status: event.status, isIdle: false }, + }, + }, + }; + } + } + return prev; + }); + } + }, [updateItem]); + + const handleWorkerIdle = useCallback((data: unknown) => { + const event = data as WorkerIdleEvent; + if (event.channel_id) { + setLiveStates((prev) => { + const state = prev[event.channel_id!]; + const worker = state?.workers[event.worker_id]; + if (!worker) return prev; + return { + ...prev, + [event.channel_id!]: { + ...state, + workers: { + ...state.workers, + [event.worker_id]: { ...worker, isIdle: true }, + }, + }, + }; + }); + // Update timeline item status to idle + updateItem(event.channel_id, event.worker_id, (item) => { + if (item.type !== "worker_run") return item; + return { ...item, status: "idle" }; + }); + } else { + setLiveStates((prev) => { + for (const [channelId, state] of Object.entries(prev)) { + const worker = state.workers[event.worker_id]; + if (worker) { + return { + ...prev, + [channelId]: { + ...state, + workers: { + ...state.workers, + [event.worker_id]: { ...worker, isIdle: true }, }, }, }; @@ -579,12 +775,15 @@ export function useChannelLiveState(channels: ChannelInfo[]) { setLiveStates((current) => { const existing = current[channelId]; if (!existing) return current; + const existingKeys = new Set(existing.timeline.map(itemKey)); + const olderItems = data.items.filter((item) => !existingKeys.has(itemKey(item))); + const hasMore = olderItems.length === 0 ? false : data.has_more; return { ...current, [channelId]: { ...existing, - timeline: [...data.items, ...existing.timeline], - hasMore: data.has_more, + timeline: [...olderItems, ...existing.timeline], + hasMore, loadingMore: false, }, }; @@ -606,9 +805,11 @@ export function useChannelLiveState(channels: ChannelInfo[]) { const handlers = { inbound_message: handleInboundMessage, outbound_message: handleOutboundMessage, + outbound_message_delta: handleOutboundMessageDelta, typing_state: handleTypingState, worker_started: handleWorkerStarted, worker_status: handleWorkerStatus, + worker_idle: handleWorkerIdle, worker_completed: handleWorkerCompleted, branch_started: handleBranchStarted, branch_completed: handleBranchCompleted, diff --git a/interface/src/hooks/useCortexChat.ts b/interface/src/hooks/useCortexChat.ts index dd5720ed6..578f16dec 100644 --- a/interface/src/hooks/useCortexChat.ts +++ b/interface/src/hooks/useCortexChat.ts @@ -1,5 +1,6 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { api, type CortexChatMessage } from "@/api/client"; +import { generateId } from "@/lib/id"; export interface ToolActivity { tool: string; @@ -44,7 +45,7 @@ async function consumeSSE( } function generateThreadId(): string { - return crypto.randomUUID(); + return generateId(); } export function useCortexChat(agentId: string, channelId?: string) { diff --git a/interface/src/hooks/useEventSource.ts b/interface/src/hooks/useEventSource.ts index b638f7f58..1647f8d64 100644 --- a/interface/src/hooks/useEventSource.ts +++ b/interface/src/hooks/useEventSource.ts @@ -31,8 +31,8 @@ export function useEventSource(url: string, options: UseEventSourceOptions) { const [connectionState, setConnectionState] = useState("connecting"); - const reconnectTimeout = useRef>(); - const eventSourceRef = useRef(); + const reconnectTimeout = useRef | null>(null); + const eventSourceRef = useRef(null); const retryDelayRef = useRef(INITIAL_RETRY_MS); const hadConnectionRef = useRef(false); diff --git a/interface/src/hooks/useLiveContext.tsx b/interface/src/hooks/useLiveContext.tsx index dd987dc90..0fc58d4c8 100644 --- a/interface/src/hooks/useLiveContext.tsx +++ b/interface/src/hooks/useLiveContext.tsx @@ -1,8 +1,9 @@ import { createContext, useContext, useCallback, useRef, useState, useMemo, type ReactNode } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { api, type AgentMessageEvent, type ChannelInfo } from "@/api/client"; +import { api, type AgentMessageEvent, type ChannelInfo, type ToolStartedEvent, type ToolCompletedEvent, type WorkerStatusEvent, type TranscriptStep, type OpenCodePart, type OpenCodePartUpdatedEvent } from "@/api/client"; +import { generateId } from "@/lib/id"; import { useEventSource, type ConnectionState } from "@/hooks/useEventSource"; -import { useChannelLiveState, type ChannelLiveState } from "@/hooks/useChannelLiveState"; +import { useChannelLiveState, type ChannelLiveState, type ActiveWorker } from "@/hooks/useChannelLiveState"; interface LiveContextValue { liveStates: Record; @@ -12,6 +13,16 @@ interface LiveContextValue { loadOlderMessages: (channelId: string) => void; /** Set of edge IDs ("from->to") with recent message activity */ activeLinks: Set; + /** Flat map of all active workers across all channels, keyed by worker_id. */ + activeWorkers: Record; + /** Monotonically increasing counter, bumped on every worker lifecycle SSE event. */ + workerEventVersion: number; + /** Monotonically increasing counter, bumped on every task lifecycle SSE event. */ + taskEventVersion: number; + /** Live transcript steps for running workers, keyed by worker_id. Built from SSE tool events. */ + liveTranscripts: Record; + /** Live OpenCode parts for running workers, keyed by worker_id. Parts are insertion-ordered Maps keyed by part ID. */ + liveOpenCodeParts: Record>; } const LiveContext = createContext({ @@ -21,6 +32,11 @@ const LiveContext = createContext({ hasData: false, loadOlderMessages: () => {}, activeLinks: new Set(), + activeWorkers: {}, + workerEventVersion: 0, + taskEventVersion: 0, + liveTranscripts: {}, + liveOpenCodeParts: {}, }); export function useLiveContext() { @@ -42,6 +58,39 @@ export function LiveContextProvider({ children }: { children: ReactNode }) { const channels = channelsData?.channels ?? []; const { liveStates, handlers: channelHandlers, syncStatusSnapshot, loadOlderMessages } = useChannelLiveState(channels); + // Flat active workers map + event version counter for the workers tab. + // This is a separate piece of state from channel liveStates so the workers + // tab can react to SSE events without scanning all channels. + const [workerEventVersion, setWorkerEventVersion] = useState(0); + const bumpWorkerVersion = useCallback(() => setWorkerEventVersion((v) => v + 1), []); + + const [taskEventVersion, setTaskEventVersion] = useState(0); + const bumpTaskVersion = useCallback(() => setTaskEventVersion((v) => v + 1), []); + + // Live transcript accumulator: builds TranscriptStep[] from SSE tool events + // for running workers. Cleared when worker completes. + const [liveTranscripts, setLiveTranscripts] = useState>({}); + + // Live OpenCode parts: per-worker insertion-ordered Map keyed by part ID. + // Updated via opencode_part_updated SSE events. Cleared when worker completes. + const [liveOpenCodeParts, setLiveOpenCodeParts] = useState>>({}); + + // Derive flat active workers from channel live states + const pendingToolCallIdsRef = useRef>>({}); + + const activeWorkers = useMemo(() => { + const channelAgentIds = new Map(channels.map((channel) => [channel.id, channel.agent_id])); + const map: Record = {}; + for (const [channelId, state] of Object.entries(liveStates)) { + const channelAgentId = channelAgentIds.get(channelId); + if (!channelAgentId) continue; + for (const [workerId, worker] of Object.entries(state.workers)) { + map[workerId] = { ...worker, channelId, agentId: channelAgentId }; + } + } + return map; + }, [liveStates, channels]); + // Track recently active link edges const [activeLinks, setActiveLinks] = useState>(new Set()); const timersRef = useRef>>(new Map()); @@ -85,14 +134,137 @@ export function LiveContextProvider({ children }: { children: ReactNode }) { [markEdgeActive], ); - // Merge channel handlers with agent message handlers + // Wrap channel worker handlers to also bump the worker event version + // and accumulate live transcript steps from SSE events. + const wrappedWorkerStarted = useCallback((data: unknown) => { + channelHandlers.worker_started(data); + const event = data as { worker_id: string }; + setLiveTranscripts((prev) => ({ ...prev, [event.worker_id]: [] })); + setLiveOpenCodeParts((prev) => ({ ...prev, [event.worker_id]: new Map() })); + delete pendingToolCallIdsRef.current[event.worker_id]; + bumpWorkerVersion(); + }, [channelHandlers, bumpWorkerVersion]); + + const wrappedWorkerStatus = useCallback((data: unknown) => { + channelHandlers.worker_status(data); + const event = data as WorkerStatusEvent; + // Push status text as an action step in the live transcript + if (event.status && event.status !== "starting" && event.status !== "running") { + setLiveTranscripts((prev) => { + const steps = prev[event.worker_id] ?? []; + const step: TranscriptStep = { + type: "action", + content: [{ type: "text", text: event.status }], + }; + return { ...prev, [event.worker_id]: [...steps, step] }; + }); + } + bumpWorkerVersion(); + }, [channelHandlers, bumpWorkerVersion]); + + const wrappedWorkerIdle = useCallback((data: unknown) => { + channelHandlers.worker_idle(data); + bumpWorkerVersion(); + }, [channelHandlers, bumpWorkerVersion]); + + const wrappedWorkerCompleted = useCallback((data: unknown) => { + channelHandlers.worker_completed(data); + const event = data as { worker_id: string }; + delete pendingToolCallIdsRef.current[event.worker_id]; + // Clean up live OpenCode parts — persisted transcript takes over + setLiveOpenCodeParts((prev) => { + const next = { ...prev }; + delete next[event.worker_id]; + return next; + }); + bumpWorkerVersion(); + }, [channelHandlers, bumpWorkerVersion]); + + const wrappedToolStarted = useCallback((data: unknown) => { + channelHandlers.tool_started(data); + const event = data as ToolStartedEvent; + if (event.process_type === "worker") { + const callId = generateId(); + const pendingByTool = pendingToolCallIdsRef.current[event.process_id] ?? {}; + const queue = pendingByTool[event.tool_name] ?? []; + pendingByTool[event.tool_name] = [...queue, callId]; + pendingToolCallIdsRef.current[event.process_id] = pendingByTool; + setLiveTranscripts((prev) => { + const steps = prev[event.process_id] ?? []; + const step: TranscriptStep = { + type: "action", + content: [{ + type: "tool_call", + id: callId, + name: event.tool_name, + args: event.args || "", + }], + }; + return { ...prev, [event.process_id]: [...steps, step] }; + }); + bumpWorkerVersion(); + } + }, [channelHandlers, bumpWorkerVersion]); + + const wrappedToolCompleted = useCallback((data: unknown) => { + channelHandlers.tool_completed(data); + const event = data as ToolCompletedEvent; + if (event.process_type === "worker") { + const pendingByTool = pendingToolCallIdsRef.current[event.process_id]; + const queue = pendingByTool?.[event.tool_name] ?? []; + const [callId, ...rest] = queue; + if (pendingByTool) { + if (rest.length > 0) { + pendingByTool[event.tool_name] = rest; + } else { + delete pendingByTool[event.tool_name]; + } + if (Object.keys(pendingByTool).length === 0) { + delete pendingToolCallIdsRef.current[event.process_id]; + } + } + setLiveTranscripts((prev) => { + const steps = prev[event.process_id] ?? []; + const step: TranscriptStep = { + type: "tool_result", + call_id: callId ?? `${event.process_id}:${event.tool_name}:${steps.length}`, + name: event.tool_name, + text: event.result || "", + }; + return { ...prev, [event.process_id]: [...steps, step] }; + }); + bumpWorkerVersion(); + } + }, [channelHandlers, bumpWorkerVersion]); + + // Handle OpenCode part updates — upsert parts into the per-worker ordered map + const handleOpenCodePartUpdated = useCallback((data: unknown) => { + const event = data as OpenCodePartUpdatedEvent; + setLiveOpenCodeParts((prev) => { + const existing = prev[event.worker_id] ?? new Map(); + const next = new Map(existing); + next.set(event.part.id, event.part); + return { ...prev, [event.worker_id]: next }; + }); + bumpWorkerVersion(); + }, [bumpWorkerVersion]); + + // Merge channel handlers with agent message + task handlers const handlers = useMemo( () => ({ ...channelHandlers, + worker_started: wrappedWorkerStarted, + worker_status: wrappedWorkerStatus, + worker_idle: wrappedWorkerIdle, + worker_completed: wrappedWorkerCompleted, + tool_started: wrappedToolStarted, + tool_completed: wrappedToolCompleted, + opencode_part_updated: handleOpenCodePartUpdated, agent_message_sent: handleAgentMessage, agent_message_received: handleAgentMessage, + task_updated: bumpTaskVersion, }), - [channelHandlers, handleAgentMessage], + [channelHandlers, wrappedWorkerStarted, wrappedWorkerStatus, wrappedWorkerIdle, wrappedWorkerCompleted, wrappedToolStarted, wrappedToolCompleted, handleOpenCodePartUpdated, handleAgentMessage, bumpTaskVersion], ); const onReconnect = useCallback(() => { @@ -100,7 +272,10 @@ export function LiveContextProvider({ children }: { children: ReactNode }) { queryClient.invalidateQueries({ queryKey: ["channels"] }); queryClient.invalidateQueries({ queryKey: ["status"] }); queryClient.invalidateQueries({ queryKey: ["agents"] }); - }, [syncStatusSnapshot, queryClient]); + queryClient.invalidateQueries({ queryKey: ["tasks"] }); + // Bump task version so any mounted task views refetch immediately. + bumpTaskVersion(); + }, [syncStatusSnapshot, queryClient, bumpTaskVersion]); const { connectionState } = useEventSource(api.eventsUrl, { handlers, @@ -111,7 +286,7 @@ export function LiveContextProvider({ children }: { children: ReactNode }) { const hasData = channels.length > 0 || channelsData !== undefined; return ( - + {children} ); diff --git a/interface/src/hooks/useTheme.ts b/interface/src/hooks/useTheme.ts new file mode 100644 index 000000000..797b76456 --- /dev/null +++ b/interface/src/hooks/useTheme.ts @@ -0,0 +1,87 @@ +import { useState, useEffect, useCallback } from "react"; + +export type ThemeId = "default" | "vanilla" | "midnight" | "noir"; + +export interface ThemeOption { + id: ThemeId; + name: string; + description: string; + className: string; +} + +export const THEMES: ThemeOption[] = [ + { + id: "default", + name: "Default", + description: "Dark theme with purple accent", + className: "", + }, + { + id: "vanilla", + name: "Vanilla", + description: "Light theme with blue accent", + className: "vanilla-theme", + }, + { + id: "midnight", + name: "Midnight", + description: "Deep blue dark theme", + className: "midnight-theme", + }, + { + id: "noir", + name: "Noir", + description: "Pure black and white theme", + className: "noir-theme", + }, +]; + +const STORAGE_KEY = "spacebot-theme"; + +function getInitialTheme(): ThemeId { + if (typeof window === "undefined") return "default"; + const stored = localStorage.getItem(STORAGE_KEY); + if (stored && THEMES.some((t) => t.id === stored)) { + return stored as ThemeId; + } + return "default"; +} + +function applyThemeClass(themeId: ThemeId) { + const theme = THEMES.find((t) => t.id === themeId); + const root = document.documentElement; + + // Remove all theme classes + THEMES.forEach((t) => { + if (t.className) { + root.classList.remove(t.className); + } + }); + + // Add the selected theme class + if (theme?.className) { + root.classList.add(theme.className); + } +} + +export function useTheme() { + const [theme, setThemeState] = useState(getInitialTheme); + + // Apply theme on mount and when theme changes + useEffect(() => { + applyThemeClass(theme); + }, [theme]); + + const setTheme = useCallback((newTheme: ThemeId) => { + setThemeState(newTheme); + localStorage.setItem(STORAGE_KEY, newTheme); + }, []); + + return { theme, setTheme, themes: THEMES }; +} + +// Initialize theme on page load (before React hydrates) +if (typeof window !== "undefined") { + const initialTheme = getInitialTheme(); + applyThemeClass(initialTheme); +} diff --git a/interface/src/hooks/useWebChat.ts b/interface/src/hooks/useWebChat.ts index a74bef7ad..95168480e 100644 --- a/interface/src/hooks/useWebChat.ts +++ b/interface/src/hooks/useWebChat.ts @@ -1,167 +1,41 @@ -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useState } from "react"; import { api } from "@/api/client"; -export interface ToolActivity { - tool: string; - status: "running" | "done"; -} - -export interface WebChatMessage { - id: string; - role: "user" | "assistant"; - content: string; -} - export function getPortalChatSessionId(agentId: string) { return `portal:chat:${agentId}`; } -async function consumeSSE( - response: Response, - onEvent: (eventType: string, data: string) => void, -) { - const reader = response.body?.getReader(); - if (!reader) return; - - const decoder = new TextDecoder(); - let buffer = ""; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split("\n"); - buffer = lines.pop() ?? ""; - - let currentEvent = ""; - let currentData = ""; - - for (const line of lines) { - if (line.startsWith("event: ")) { - currentEvent = line.slice(7); - } else if (line.startsWith("data: ")) { - currentData = line.slice(6); - } else if (line === "" && currentEvent) { - onEvent(currentEvent, currentData); - currentEvent = ""; - currentData = ""; - } - } - } -} - +/** + * Sends messages to the webchat endpoint. The response arrives via the global + * SSE event bus (same timeline used by regular channels) — no per-request SSE. + */ export function useWebChat(agentId: string) { const sessionId = getPortalChatSessionId(agentId); - const [messages, setMessages] = useState([]); - const [isStreaming, setIsStreaming] = useState(false); + const [isSending, setIsSending] = useState(false); const [error, setError] = useState(null); - const [toolActivity, setToolActivity] = useState([]); - const streamingTextRef = useRef(""); - - useEffect(() => { - let cancelled = false; - (async () => { - try { - const response = await api.webChatHistory(agentId, sessionId); - if (!response.ok || cancelled) return; - const history: { id: string; role: string; content: string }[] = await response.json(); - if (cancelled) return; - setMessages( - history.map((m) => ({ - id: m.id, - role: m.role as "user" | "assistant", - content: m.content, - })), - ); - } catch { /* ignore — fresh session */ } - })(); - return () => { cancelled = true; }; - }, [agentId, sessionId]); - - const sendMessage = useCallback(async (text: string) => { - if (isStreaming) return; - - setError(null); - setIsStreaming(true); - setToolActivity([]); - streamingTextRef.current = ""; - const userMessage: WebChatMessage = { - id: `user-${Date.now()}`, - role: "user", - content: text, - }; - setMessages((prev) => [...prev, userMessage]); + const sendMessage = useCallback( + async (text: string) => { + if (isSending) return; - const assistantId = `assistant-${Date.now()}`; + setError(null); + setIsSending(true); - try { - const response = await api.webChatSend(agentId, sessionId, text); - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); - } - - await consumeSSE(response, (eventType, data) => { - if (eventType === "tool_started") { - try { - const parsed = JSON.parse(data); - setToolActivity((prev) => [ - ...prev, - { tool: parsed.ToolStarted?.tool_name ?? "tool", status: "running" }, - ]); - } catch { /* ignore */ } - } else if (eventType === "tool_completed") { - try { - const parsed = JSON.parse(data); - const toolName = parsed.ToolCompleted?.tool_name ?? "tool"; - setToolActivity((prev) => - prev.map((t) => - t.tool === toolName && t.status === "running" - ? { ...t, status: "done" } - : t, - ), - ); - } catch { /* ignore */ } - } else if (eventType === "text") { - try { - const parsed = JSON.parse(data); - const content = parsed.Text ?? ""; - setMessages((prev) => { - const existing = prev.find((m) => m.id === assistantId); - if (existing) { - return prev.map((m) => - m.id === assistantId ? { ...m, content } : m, - ); - } - return [...prev, { id: assistantId, role: "assistant", content }]; - }); - } catch { /* ignore */ } - } else if (eventType === "stream_chunk") { - try { - const parsed = JSON.parse(data); - const chunk = parsed.StreamChunk ?? ""; - streamingTextRef.current += chunk; - const accumulated = streamingTextRef.current; - setMessages((prev) => { - const existing = prev.find((m) => m.id === assistantId); - if (existing) { - return prev.map((m) => - m.id === assistantId ? { ...m, content: accumulated } : m, - ); - } - return [...prev, { id: assistantId, role: "assistant", content: accumulated }]; - }); - } catch { /* ignore */ } + try { + const response = await api.webChatSend(agentId, sessionId, text); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); } - }); - } catch (error) { - setError(error instanceof Error ? error.message : "Request failed"); - } finally { - setIsStreaming(false); - setToolActivity([]); - } - }, [agentId, sessionId, isStreaming]); + } catch (error) { + setError( + error instanceof Error ? error.message : "Request failed", + ); + } finally { + setIsSending(false); + } + }, + [agentId, sessionId, isSending], + ); - return { messages, isStreaming, error, toolActivity, sendMessage }; + return { sessionId, isSending, error, sendMessage }; } diff --git a/interface/src/lib/id.ts b/interface/src/lib/id.ts new file mode 100644 index 000000000..44bca3097 --- /dev/null +++ b/interface/src/lib/id.ts @@ -0,0 +1,14 @@ +/** + * Generate a unique ID, preferring crypto.randomUUID() when available. + * + * crypto.randomUUID() requires a secure context (HTTPS or localhost). + * Over plain HTTP (e.g. Tailscale) it's unavailable, so we fall back + * to Date.now + Math.random which is sufficient for ephemeral client-side IDs. + */ +export function generateId(): string { + try { + return crypto.randomUUID(); + } catch { + return `${Date.now()}-${Math.random().toString(36).slice(2)}`; + } +} diff --git a/interface/src/lib/providerIcons.tsx b/interface/src/lib/providerIcons.tsx index 29487961a..eaf286c79 100644 --- a/interface/src/lib/providerIcons.tsx +++ b/interface/src/lib/providerIcons.tsx @@ -90,6 +90,26 @@ function OllamaIcon({ size = 24, className }: IconProps) { ); } +function KiloIcon({ size = 24, className }: IconProps) { + return ( + + ); +} + export function ProviderIcon({ provider, className = "text-ink-faint", size = 24 }: ProviderIconProps) { const iconProps: Partial = { size, @@ -101,6 +121,7 @@ export function ProviderIcon({ provider, className = "text-ink-faint", size = 24 openai: OpenAI, "openai-chatgpt": OpenAI, openrouter: OpenRouter, + kilo: KiloIcon, groq: Groq, mistral: Mistral, gemini: Google, @@ -112,6 +133,7 @@ export function ProviderIcon({ provider, className = "text-ink-faint", size = 24 "zai-coding-plan": ZAI, ollama: OllamaIcon, "opencode-zen": OpenCodeZenIcon, + "opencode-go": OpenCodeZenIcon, nvidia: NvidiaIcon, minimax: Minimax, "minimax-cn": Minimax, diff --git a/interface/src/router.tsx b/interface/src/router.tsx index 1c6950f05..d65ce23fe 100644 --- a/interface/src/router.tsx +++ b/interface/src/router.tsx @@ -8,7 +8,6 @@ import { import {BASE_PATH} from "@/api/client"; import {ConnectionBanner} from "@/components/ConnectionBanner"; import {SetupBanner} from "@/components/SetupBanner"; -import {UpdateBanner} from "@/components/UpdateBanner"; import {Sidebar} from "@/components/Sidebar"; import {Overview} from "@/routes/Overview"; import {AgentDetail} from "@/routes/AgentDetail"; @@ -20,6 +19,8 @@ import {AgentConfig} from "@/routes/AgentConfig"; import {AgentCron} from "@/routes/AgentCron"; import {AgentIngest} from "@/routes/AgentIngest"; import {AgentSkills} from "@/routes/AgentSkills"; +import {AgentWorkers} from "@/routes/AgentWorkers"; +import {AgentTasks} from "@/routes/AgentTasks"; import {AgentChat} from "@/routes/AgentChat"; import {Settings} from "@/routes/Settings"; import {useLiveContext} from "@/hooks/useLiveContext"; @@ -38,7 +39,6 @@ function RootLayout() { />
-
@@ -187,15 +187,32 @@ const agentIngestRoute = createRoute({ const agentWorkersRoute = createRoute({ getParentRoute: () => rootRoute, path: "/agents/$agentId/workers", + validateSearch: (search: Record): {worker?: string} => ({ + worker: typeof search.worker === "string" ? search.worker : undefined, + }), component: function AgentWorkersPage() { const {agentId} = agentWorkersRoute.useParams(); return (
-
-

- Workers control interface coming soon -

+
+ +
+
+ ); + }, +}); + +const agentTasksRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/agents/$agentId/tasks", + component: function AgentTasksPage() { + const {agentId} = agentTasksRoute.useParams(); + return ( +
+ +
+
); @@ -305,6 +322,7 @@ const routeTree = rootRoute.addChildren([ agentMemoriesRoute, agentIngestRoute, agentWorkersRoute, + agentTasksRoute, agentCortexRoute, agentSkillsRoute, agentCronRoute, diff --git a/interface/src/routes/AgentConfig.tsx b/interface/src/routes/AgentConfig.tsx index 8655eb339..363f915c5 100644 --- a/interface/src/routes/AgentConfig.tsx +++ b/interface/src/routes/AgentConfig.tsx @@ -1,8 +1,9 @@ import { useCallback, useEffect, useState, useRef } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { api, type AgentConfigResponse, type AgentConfigUpdateRequest } from "@/api/client"; -import { Button, SettingSidebarButton, Input, TextArea, Toggle, NumberStepper, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, cx } from "@/ui"; +import { Button, SettingSidebarButton, TextArea, Toggle, NumberStepper, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, cx } from "@/ui"; import { ModelSelect } from "@/components/ModelSelect"; +import { TagInput } from "@/components/TagInput"; import { Markdown } from "@/components/Markdown"; import { motion, AnimatePresence } from "framer-motion"; import { useSearch, useNavigate } from "@tanstack/react-router"; @@ -14,7 +15,7 @@ function supportsAdaptiveThinking(modelId: string): boolean { || id.includes("sonnet-4-6") || id.includes("sonnet-4.6"); } -type SectionId = "soul" | "identity" | "user" | "routing" | "tuning" | "compaction" | "cortex" | "coalesce" | "memory" | "browser"; +type SectionId = "soul" | "identity" | "user" | "routing" | "tuning" | "compaction" | "cortex" | "coalesce" | "memory" | "browser" | "channel" | "sandbox"; const SECTIONS: { id: SectionId; @@ -33,6 +34,8 @@ const SECTIONS: { { id: "coalesce", label: "Coalesce", group: "config", description: "Message batching", detail: "When multiple messages arrive in quick succession, coalescing batches them into a single LLM turn. This prevents the agent from responding to each message individually in fast-moving conversations." }, { id: "memory", label: "Memory Persistence", group: "config", description: "Auto-save interval", detail: "Spawns a silent background branch at regular intervals to recall existing memories and save new ones from the recent conversation. Runs without blocking the channel." }, { id: "browser", label: "Browser", group: "config", description: "Chrome automation", detail: "Controls browser automation tools available to workers. When enabled, workers can navigate web pages, take screenshots, and interact with sites. JavaScript evaluation is a separate permission." }, + { id: "channel", label: "Channel Behavior", group: "config", description: "Reply behavior", detail: "Listen-only mode suppresses unsolicited replies in busy channels. The agent still responds to slash commands, @mentions, and replies to its own messages." }, + { id: "sandbox", label: "Sandbox", group: "config", description: "Process containment", detail: "OS-level filesystem containment for shell and exec tool subprocesses. When enabled, worker processes run inside a kernel-enforced sandbox (bubblewrap on Linux, sandbox-exec on macOS) with an allowlist-only filesystem — only system paths, the workspace, and explicitly configured extra paths are accessible." }, ]; interface AgentConfigProps { @@ -62,7 +65,7 @@ export function AgentConfig({ agentId }: AgentConfigProps) { // Sync activeSection with URL search param useEffect(() => { if (search.tab) { - const validSections: SectionId[] = ["soul", "identity", "user", "routing", "tuning", "compaction", "cortex", "coalesce", "memory", "browser"]; + const validSections = SECTIONS.map((section) => section.id); if (validSections.includes(search.tab as SectionId)) { setActiveSection(search.tab as SectionId); } @@ -103,9 +106,32 @@ export function AgentConfig({ agentId }: AgentConfigProps) { const configMutation = useMutation({ mutationFn: (update: AgentConfigUpdateRequest) => api.updateAgentConfig(update), - onMutate: () => setSaving(true), + onMutate: (update) => { + setSaving(true); + // Optimistically merge the sent values into the cache so the UI + // reflects the change immediately (covers fields the backend + // doesn't yet return in its response, like sandbox). + const previous = queryClient.getQueryData(["agent-config", agentId]); + if (previous) { + const { agent_id: _, ...sections } = update; + const merged = { ...previous } as unknown as Record; + const prev = previous as unknown as Record; + for (const [key, value] of Object.entries(sections)) { + if (value !== undefined) { + merged[key] = { + ...(prev[key] as Record | undefined), + ...value, + }; + } + } + queryClient.setQueryData(["agent-config", agentId], merged as unknown as AgentConfigResponse); + } + }, onSuccess: (result) => { - queryClient.setQueryData(["agent-config", agentId], result); + // Merge server response with cache to preserve fields the backend + // doesn't yet return (e.g. sandbox). + const previous = queryClient.getQueryData(["agent-config", agentId]); + queryClient.setQueryData(["agent-config", agentId], { ...previous, ...result }); setDirty(false); setSaving(false); }, @@ -384,24 +410,33 @@ interface ConfigSectionEditorProps { onSave: (update: Partial) => void; } +const SANDBOX_DEFAULTS = { mode: "enabled" as const, writable_paths: [] as string[] }; + function ConfigSectionEditor({ sectionId, label, description, detail, config, onDirtyChange, saveHandlerRef, onSave }: ConfigSectionEditorProps) { - const [localValues, setLocalValues] = useState>(() => { + type ConfigValues = Record; + const sandbox = config.sandbox ?? SANDBOX_DEFAULTS; + const channel = config.channel ?? { listen_only_mode: false }; + const [localValues, setLocalValues] = useState(() => { // Initialize from config based on section switch (sectionId) { case "routing": - return { ...config.routing }; + return { ...config.routing } as ConfigValues; case "tuning": - return { ...config.tuning }; + return { ...config.tuning } as ConfigValues; case "compaction": - return { ...config.compaction }; + return { ...config.compaction } as ConfigValues; case "cortex": - return { ...config.cortex }; + return { ...config.cortex } as ConfigValues; case "coalesce": - return { ...config.coalesce }; + return { ...config.coalesce } as ConfigValues; case "memory": - return { ...config.memory_persistence }; + return { ...config.memory_persistence } as ConfigValues; case "browser": - return { ...config.browser }; + return { ...config.browser } as ConfigValues; + case "channel": + return { ...channel } as ConfigValues; + case "sandbox": + return { mode: sandbox.mode, writable_paths: sandbox.writable_paths } as ConfigValues; default: return {}; } @@ -438,11 +473,17 @@ function ConfigSectionEditor({ sectionId, label, description, detail, config, on case "browser": setLocalValues({ ...config.browser }); break; + case "channel": + setLocalValues({ ...channel }); + break; + case "sandbox": + setLocalValues({ mode: sandbox.mode, writable_paths: sandbox.writable_paths }); + break; } } }, [config, sectionId, localDirty]); - const handleChange = useCallback((field: string, value: string | number | boolean) => { + const handleChange = useCallback((field: string, value: string | number | boolean | string[]) => { setLocalValues((prev) => ({ ...prev, [field]: value })); setLocalDirty(true); }, []); @@ -475,6 +516,12 @@ function ConfigSectionEditor({ sectionId, label, description, detail, config, on case "browser": setLocalValues({ ...config.browser }); break; + case "channel": + setLocalValues({ ...channel }); + break; + case "sandbox": + setLocalValues({ mode: sandbox.mode, writable_paths: sandbox.writable_paths }); + break; } setLocalDirty(false); }, [config, sectionId]); @@ -783,6 +830,70 @@ function ConfigSectionEditor({ sectionId, label, description, detail, config, on value={localValues.evaluate_enabled as boolean} onChange={(v) => handleChange("evaluate_enabled", v)} /> + handleChange("persist_session", v)} + /> +
+ +

What happens when a worker calls "close" or finishes.

+ +
+
+ ); + case "sandbox": + return ( +
+
+ +

Kernel-enforced filesystem containment for shell and exec subprocesses.

+ +
+
+ +

Additional directories workers can read and write beyond the workspace. The workspace is always accessible. Press Enter to add a path.

+ handleChange("writable_paths", paths)} + placeholder="/home/user/projects/myapp" + /> +
+
+ ); + case "channel": + return ( +
+ handleChange("listen_only_mode", v)} + />
); default: @@ -815,28 +926,6 @@ function ConfigSectionEditor({ sectionId, label, description, detail, config, on // -- Form Field Components -- -interface ConfigFieldProps { - label: string; - description: string; - value: string; - onChange: (value: string) => void; -} - -function ConfigField({ label, description, value, onChange }: ConfigFieldProps) { - return ( -
- -

{description}

- onChange(e.target.value)} - className="mt-1 border-app-line/50 bg-app-darkBox/30" - /> -
- ); -} - interface ConfigToggleFieldProps { label: string; description: string; diff --git a/interface/src/routes/AgentCortex.tsx b/interface/src/routes/AgentCortex.tsx index 6765b7373..5f4a9dd3a 100644 --- a/interface/src/routes/AgentCortex.tsx +++ b/interface/src/routes/AgentCortex.tsx @@ -3,7 +3,6 @@ import { useQuery } from "@tanstack/react-query"; import { AnimatePresence, motion } from "framer-motion"; import { api, - CORTEX_EVENT_TYPES, type CortexEvent, type CortexEventType, } from "@/api/client"; diff --git a/interface/src/routes/AgentCron.tsx b/interface/src/routes/AgentCron.tsx index 469ecf297..ed6a41fef 100644 --- a/interface/src/routes/AgentCron.tsx +++ b/interface/src/routes/AgentCron.tsx @@ -2,7 +2,6 @@ import { useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { api, type CronJobWithStats, type CreateCronRequest } from "@/api/client"; import { formatDuration, formatTimeAgo } from "@/lib/format"; -import { AnimatePresence, motion } from "framer-motion"; import { Clock05Icon } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { diff --git a/interface/src/routes/AgentDetail.tsx b/interface/src/routes/AgentDetail.tsx index 4ed6f6cda..bb614da78 100644 --- a/interface/src/routes/AgentDetail.tsx +++ b/interface/src/routes/AgentDetail.tsx @@ -90,14 +90,15 @@ export function AgentDetail({ agentId, liveStates }: AgentDetailProps) { return (
- {/* Hero Section */} - setDeleteOpen(true)} + {/* Hero Section */} + setDeleteOpen(true)} /> @@ -144,35 +145,51 @@ export function AgentDetail({ agentId, liveStates }: AgentDetailProps) { {/* Memory Donut */}
-

Memory Types

- {overviewData.memory_total} +

Memory

+ + Edit +
{/* Model Routing */} {configData && ( -
+

Model Routing

Edit
- +
+ +
)} {/* Quick Stats */} -
-
+
+

Configuration

+ + Edit +
-
+
@@ -208,6 +225,7 @@ export function AgentDetail({ agentId, liveStates }: AgentDetailProps) { function HeroSection({ agentId, + displayName, channelCount, workers, branches, @@ -215,6 +233,7 @@ function HeroSection({ onDelete, }: { agentId: string; + displayName?: string; channelCount: number; workers: number; branches: number; @@ -225,7 +244,12 @@ function HeroSection({
-

{agentId}

+

+ {displayName?.trim() ? displayName : agentId} +

+ {displayName?.trim() && ( + {agentId} + )}
@@ -470,8 +494,8 @@ function ActivityHeatmap({ data }: { data: { day: number; hour: number; count: n const maxCount = Math.max(...data.map((d) => d.count), 1); const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + const hours = Array.from({ length: 24 }, (_value, index) => index); - // Create a 7x24 grid const getCell = (day: number, hour: number) => { const cell = data.find((d) => d.day === day && d.hour === hour); return cell?.count ?? 0; @@ -483,34 +507,30 @@ function ActivityHeatmap({ data }: { data: { day: number; hour: number; count: n }; return ( -
-
- {/* Hour labels */} -
-
{/* Day label spacer */} - {Array.from({ length: 24 }, (_, h) => ( -
- {h % 6 === 0 ? h : ""} +
+
+
+
+ {hours.map((hour) => ( +
+ {hour % 6 === 0 ? hour : ""}
))}
- {/* Heatmap grid */} {days.map((dayLabel, day) => ( -
-
{dayLabel}
-
- {Array.from({ length: 24 }, (_, hour) => { - const count = getCell(day, hour); - return ( -
- ); - })} -
+
+
{dayLabel}
+ {hours.map((hour) => { + const count = getCell(day, hour); + return ( +
+ ); + })}
))}
@@ -524,14 +544,15 @@ function MemoryDonut({ counts }: { counts: Record }) { value: counts[type] ?? 0, color: MEMORY_TYPE_COLORS[idx % MEMORY_TYPE_COLORS.length], })).filter((d) => d.value > 0); + const total = data.reduce((sum, item) => sum + item.value, 0); if (data.length === 0) { return
No memories
; } return ( -
-
+
+
}) { border: `1px solid ${CHART_COLORS.tooltip.border}`, borderRadius: "6px", fontSize: "12px", + padding: "4px 6px", }} - itemStyle={{ color: CHART_COLORS.tooltip.text }} + wrapperStyle={{ zIndex: 20 }} + labelStyle={{ display: "none" }} + itemStyle={{ color: CHART_COLORS.tooltip.text, margin: 0, lineHeight: 1.2 }} /> +
+ {total} + total +
{data.map((item) => ( @@ -586,7 +614,7 @@ function ModelRoutingList({ config }: { config: { routing: { channel: string; br ]; return ( -
+
{models.map(({ label, model, color }) => (
{label} @@ -628,29 +656,32 @@ function IdentitySection({ if (!hasContent) return null; const files = [ - { label: "SOUL.md", content: identity.soul }, - { label: "IDENTITY.md", content: identity.identity }, - { label: "USER.md", content: identity.user }, + { label: "SOUL.md", tab: "soul", content: identity.soul }, + { label: "IDENTITY.md", tab: "identity", content: identity.identity }, + { label: "USER.md", tab: "user", content: identity.user }, ].filter((f) => f.content && f.content.trim().length > 0 && !f.content.startsWith("