diff --git a/.github/actions/install_cli_deps/action.yaml b/.github/actions/install_cli_deps/action.yaml deleted file mode 100644 index f4e55a1dfe..0000000000 --- a/.github/actions/install_cli_deps/action.yaml +++ /dev/null @@ -1,14 +0,0 @@ -runs: - using: "composite" - steps: - - if: ${{ runner.os == 'Linux' }} - shell: bash - run: | - sudo apt-get update - sudo add-apt-repository -y ppa:pipewire-debian/pipewire-upstream - sudo apt-get update - sudo apt-get install -y \ - pkg-config \ - libasound2-dev \ - libpulse-dev \ - libpipewire-0.3-dev diff --git a/.github/actions/sentry_cli/action.yaml b/.github/actions/sentry_cli/action.yaml deleted file mode 100644 index b43d24b4e2..0000000000 --- a/.github/actions/sentry_cli/action.yaml +++ /dev/null @@ -1,11 +0,0 @@ -inputs: - version: - required: false - default: "2.39.1" -runs: - using: "composite" - steps: - - run: curl -sL https://sentry.io/get-cli/ | SENTRY_CLI_VERSION="${{ inputs.version }}" sh - shell: bash - - run: sentry-cli --version - shell: bash diff --git a/.github/reports/legal-review/2026-04.md b/.github/reports/legal-review/2026-04.md deleted file mode 100644 index 917168f131..0000000000 --- a/.github/reports/legal-review/2026-04.md +++ /dev/null @@ -1,136 +0,0 @@ -# Legal Review — 2026-04 - -Scope: commits merged to `fastrepl/char` in the ~30 days leading up to 2026-04-19. -Legal docs inspected: `apps/web/content/legal/{privacy,terms,cookies,dpa}.mdx`. - -This report flags product changes that appear to introduce new data flows, sub-processors, permissions, or user surfaces that may not be reflected in the current legal documentation. All items are flagged for **human legal review** — none of the findings have been auto-remediated. When an item is ambiguous it is flagged rather than dismissed. - ---- - -## Changes Detected - -### 1. New third-party STT provider: AquaVoice -- Commit `8def7da1b` (2026-04-13) "add aquavoice" introduces a new cloud STT provider. -- Provider is user-selectable: `apps/desktop/src/settings/ai/stt/shared.tsx` exposes `aquavoice` with logo and `baseUrl: "https://api.aquavoice.com/api/v1"`. -- Wired through `crates/owhisper-client/src/providers.rs` (`Provider::AquaVoice`) and `crates/transcribe-proxy/src/env.rs` (`aquavoice_api_key`). -- Batch transcription only. - -### 2. New third-party STT provider scaffolding: SmallestAI -- Commits `7efb0c5e7` (2026-04-16) "scaffold smallestai adapter" and `7067cb4a4` "slight smallestai improvements". -- Adds full batch + live adapter at `crates/owhisper-client/src/adapter/smallestai/` (~1,000 LOC). -- Not yet wired into the settings UI as a user-selectable provider at the time of this review, but the adapter is complete and a short code path away from exposure. - -### 3. New third-party speaker-diarization proxy: Pyannote -- Commit `e4c65d02e` (2026-04-01) "add pyannote in api proxy" and `abb03da70` "update pyannote proxy route for safety". -- Adds `crates/api-pyannote/` and routes pyannote through `apps/api`. This represents raw-audio transmission to a third-party diarization service. - -### 4. New sub-processor: exe.dev via `apps/claw` + `crates/api-claw` + `crates/exedev` -- Commits `824590609`, `e2cc200f4`, `d82e644bb`, `8a39a63e8`, `ba492bfb0`, `ed515cbb3` (2026-04-17 / 2026-04-18). -- `crates/api-claw/AGENTS.md` describes a per-user VM lifecycle on top of `exe.dev`: - - One VM per user (`vm_name = "claw-" + sha256(user_id)[..12]`). - - Per-user SSH key stored in a keyring, scoped to the user's Supabase ID. - - Token minting includes `{"user_id": }` context. - - Billing hooks: "on `invoice.payment_failed` → suspend" (SSH key removal as suspension). -- `apps/claw` ships as a Dockerized HTTP app to run inside each user VM. -- This is a material new sub-processor with user-identity linkage, credential storage, and subscription-gated access control. - -### 5. New third-party agent/research integrations (scope unclear): Cursor, Exa, Devin -- `crates/cursor/` — `https://api.cursor.com`, launches agent sessions (`update crates/cursor` 0d89a0a5b, 2026-04-18). -- `crates/exa/` — `https://api.exa.ai`, web search + content retrieval (`update crates/exa` 04c5ba7b4, 2026-04-18). Used by `crates/api-research/` alongside `hypr-jina` and `hypr-mcp` and exposed via `apps/api`. -- `crates/devin/` — `https://api.devin.ai` Devin session management. -- These clients are compiled into the repo and, in the case of Exa, wired into `api-research`'s MCP server. Whether any of them are currently reachable from the desktop client in a user-facing capacity needs product confirmation; at minimum Exa is reachable via the server-side `api-research` router. - -### 6. Mobile app (Android + iOS) via Expo -- Commit `ed3ac0ad7` (2026-04-13) "expo for mobile" adds `apps/mobile` (Expo Router, bundle id `com.hyprmobile`). -- Commit `875fc585f` (2026-04-13) "bump cloudsync with mobile support" extends `crates/cloudsync` with Android (arm64-v8a, armeabi-v7a, x86_64) and iOS targets. -- The existing Privacy Policy, Terms, and DPA describe a "local-first application" on a "device" and list app/browser telemetry. They do not explicitly call out the mobile platforms or mobile-specific permissions/identifiers (e.g., iOS IDFA, Android Ad ID, push-notification tokens). - -### 7. Activity capture: storage abstraction + observation model + screenshots -- Commits `2446086f1` "activity capture experiment" (2026-04-10), `c25e92b8f` "various activity capture improvements" (2026-04-11), `d82249dbe` "various activity-capture update around the observation concept" (2026-04-13), `8f1354872` "add storage abstraction for activity-capture" (2026-04-15), plus continued work on `activity-capture-dev`. -- `crates/activity-capture/src/screenshot.rs` continues to be the capture path; `db-activity` migration `20260410000000_init.sql` persists observations. -- `crates/template-app/src/activity_capture.rs` feeds captured app-usage (app names, window titles, timestamps) into `daily-summary` LLM templates. -- A prior legal report (PR #4893, merged then removed in #4936) flagged this area. Activity capture has expanded since, and the Privacy Policy/DPA still do not explicitly describe on-device screenshotting, app/window-title logging, or daily-summary LLM processing of that data. - -### 8. New system permissions surfaced in settings -- Commit `2b51c4343` (2026-04-10) "add screen recording permission in settings" (macOS TCC screen-recording). -- Commit `5fca17a74` (2026-04-17) "update permissions for input monitoring" (macOS Input Monitoring via `plugins/permissions` + `plugins/shortcut`). -- Commit `7efc08371` (2026-04-17) "add dictation ui for macos" adds `crates/dictation-ui-macos`. -- These permissions expand the categories of data the app can read from the host OS (screen contents, keystrokes, system audio / dictation). The Privacy Policy mentions "Device Information" and "Usage Data" generically; it does not enumerate screen-recording, keystroke-level input monitoring, or dictation as data categories. - -### 9. Battery / power telemetry collection -- Commits `eb1c9a877` "add crates/power" (2026-04-11) and `f056375d6` "include percentage in crates/power". -- Collects AC/battery state, charging status, and remaining battery capacity on macOS (IOKit) and Windows. If this is piped into telemetry/analytics or `activity-capture`, it adds a new data category (power state / battery level) not explicitly enumerated in the Privacy Policy. - -### 10. File attachment support in the editor -- Commit `d73430985` (2026-04-17) "feat: file attachment support in editor (#5053)" adds arbitrary-file attachments to notes via `useFileUpload` / `plugins/file-handler`, with `attachmentId` persisted in the editor schema. -- Privacy Policy already references "Notes, documents, and other content you create or upload" as User Content, so this is likely covered, but storage location (local vs. cloud upload via Supabase/S3/R2) and any new file-size/type retention should be confirmed by legal. - -### 11. Supabase auth token refresh under feature flag -- Commit `52df539ea` (2026-04-18) "Add supabase auth token refresh under feature flag (#5085)". Introduces client-side refresh-token handling in `crates/supabase-auth` (new `refresh` feature). -- Supabase is already a disclosed sub-processor. Mostly informational; confirm that refresh-token storage location (keyring vs. disk) matches the security language in Annex I. - -### 12. Integration routes split: auth-only management endpoints -- Commit `cf1790b35` (2026-04-13) "allow disconnect integration without payment" splits `api-nango` into `session_router` (subscription-gated) and `management_router` (auth-gated). Users can now disconnect integrations without an active subscription. -- This is user-friendly; does not add new data flows. Worth noting because the DPA/Privacy language around OAuth disconnection lifecycles should remain accurate. - -### 13. Consent UX improvements (positive) -- `566c875e6` (2026-04-16) "add Microsoft Clarity script (#5066)" — Clarity added behind website tracking consent; legal docs (`privacy.mdx`, `cookies.mdx`, `dpa.mdx`) updated concurrently in the same PR. No gap. -- `fab9f4532` (2026-04-16) "adapt web consent banner by region (#5062)" — resolves consent region from request geo, switches flow for California/EEA. Aligns with CCPA/GDPR commitments in the DPA. -- `197c1714e` (2026-04-11) "hide privacy consent on auth pages" — scoped banner suppression. -- `9a2d9641e` (2026-04-13) "eliminate FOUC on cookie accept" — no legal impact. - -### 14. `Terms of Service` frontmatter date is stale -- `apps/web/content/legal/terms.mdx` is dated `2026-01-07` while `privacy.mdx`, `cookies.mdx`, and `dpa.mdx` are dated `2026-04-16`. Several disclosures that would touch the Terms (new sub-processors, mobile platform, Claw VM lifecycle, subscription suspension mechanics) were not accompanied by a Terms update. - ---- - -## Current Documentation Status - -| Area | Covered in | Status | -|------|-----------|--------| -| AquaVoice STT | DPA Annex II | **Missing.** Deepgram/AssemblyAI/Soniox are listed; AquaVoice is not. | -| SmallestAI STT | DPA Annex II | **Missing.** Needs listing before the provider is exposed in UI. | -| Pyannote speaker diarization | DPA Annex II / Privacy §6.2 | **Missing.** No current reference to pyannote. | -| exe.dev / Claw user VM | DPA Annex II, Privacy §6.2 / §5, Terms §9 | **Missing.** No disclosure of per-user VM provisioning, SSH key storage, subscription-gated suspension, or cross-border data residency of those VMs. | -| Exa / Cursor / Devin APIs | DPA Annex II | **Missing.** No mention. Needs scoping of which are user-facing. | -| Mobile app (iOS/Android) | Privacy §3.2, Terms §2, DPA §3 | **Partial.** Generic "device" language may cover it, but no explicit mobile identifiers, push-notification providers, or mobile telemetry disclosure. | -| Activity capture (screens, app/window titles, daily summaries) | Privacy §3.2, DPA §3 | **Partial / gap.** "Usage Data" language is generic. Screen capture, active-app/window-title logging, and LLM processing of that data for daily summaries are not specifically disclosed. | -| Screen-recording / input-monitoring / dictation permissions | Privacy §3.2 | **Partial.** Not enumerated as data categories; users see macOS permission prompts without a parallel privacy-policy explanation. | -| Battery / power state telemetry | Privacy §3.2 | **Missing.** Not enumerated. | -| File attachments in notes | Terms §6, Privacy §3.1 | **Likely covered** as User Content; confirm storage/retention. | -| Supabase auth token refresh | DPA Annex II, Privacy §6.2 | **Covered.** Supabase already disclosed. | -| Integration disconnect without payment | Privacy §7.5, DPA §9 | **Covered.** Improves alignment with existing deletion/disconnection rights. | -| Regional consent banner / Microsoft Clarity / GPC | Cookies §3.2 / §7, Privacy §7.4, DPA §7 | **Covered.** Updated concurrently. | -| Terms of Service date | n/a | **Stale** (`2026-01-07`) relative to other docs (`2026-04-16`). | - ---- - -## Recommended Updates (for human legal review, not auto-applied) - -1. **DPA Annex II — add / confirm sub-processors:** AquaVoice, SmallestAI (once user-reachable), Pyannote, exe.dev (Claw VMs), Exa, and any of Cursor / Devin that are user-facing. For each, include purpose, region, and safeguards consistent with existing entries. -2. **DPA §3 and Privacy §3.2 — document Claw VMs:** describe that an optional per-user compute environment is provisioned on exe.dev, with SSH credentials and a user identifier stored to manage its lifecycle, and that access is suspended on payment failure. Address data-at-rest location on that VM. -3. **Privacy §3.2 — enumerate activity-capture data:** explicitly list on-device screen captures, foreground-app names, and window titles (if retained) as a data category, describe whether any leaves the device, and disclose that this data is fed into LLM-generated daily summaries (local or cloud, whichever applies). -4. **Privacy §3.2 / Cookies — enumerate system permissions:** add explicit language for screen recording, input monitoring, microphone dictation, and accessibility permissions, mirroring the macOS TCC prompts. -5. **Privacy §3.2 — add power/battery state** to "Device Information" if that telemetry is transmitted or persisted server-side; otherwise confirm it stays local-only. -6. **Terms §2 / Privacy §3 — acknowledge mobile platforms:** update "device" language to cover iOS and Android clients, call out mobile-specific identifiers (push tokens, IDFA/Ad ID if used), and address mobile permissions. -7. **Terms §7 / §10 — payment-triggered suspension:** document that cloud-feature access and per-user compute (Claw VM) may be suspended on payment failure, and how restoration works. -8. **Terms frontmatter date:** refresh `terms.mdx`'s `date` when any of the above lands, so consumers see a consistent "last updated". -9. **DPA §5 — sub-processor notification cadence:** confirm the in-app / email notification for the new sub-processors has been (or will be) sent if they are already live for paying users. - ---- - -## Risk Assessment - -| # | Item | Severity | Rationale | -|---|------|----------|-----------| -| 4 | exe.dev / Claw user VM (new sub-processor, per-user SSH keys, identity linkage, subscription suspension) | **High** | Material new sub-processor processing user-linked credentials and potentially Customer Content. Not in Annex II. Cross-border transfer implications. | -| 7 | Activity capture (screenshots, window titles, daily-summary LLM feeds) | **High** | Expanding surface that was already flagged previously. Captures categories of data beyond what Privacy §3.2 spells out. Potential for incidental capture of third-party/sensitive content on screen. | -| 1, 2, 3 | AquaVoice, SmallestAI, Pyannote (raw-audio processors not in Annex II) | **Medium-High** | Raw audio is Personal Data; DPA requires sub-processors to be listed with safeguards. AquaVoice is already reachable from the UI. | -| 5 | Exa / Cursor / Devin client crates | **Medium** | Exa is wired into `api-research`; Cursor/Devin present but scope unclear. If user content or identifiers are sent to any of them, they need Annex II entries. | -| 6 | Mobile app + mobile cloudsync targets | **Medium** | New platforms introduce new identifiers and permissions; existing "device" language is thin. | -| 8 | Screen recording, input monitoring, dictation permissions | **Medium** | Each is a discrete user-consent surface; privacy text should map 1:1 to OS prompts. | -| 9 | Battery / power telemetry | **Low-Medium** | Only impactful if transmitted/persisted; needs confirmation. | -| 10, 11, 12 | File attachments, Supabase refresh, integration-disconnect split | **Low** | Likely covered by existing language; confirm storage/retention details. | -| 14 | Stale Terms `date` | **Low** | Cosmetic but undermines "last updated" trust; easy fix when related updates land. | - -When uncertain, items have been flagged rather than dismissed. No legal document was modified by this review; all changes above are recommendations for human counsel. diff --git a/.github/workflows/api_cd.yaml b/.github/workflows/api_cd.yaml deleted file mode 100644 index 73869b9c58..0000000000 --- a/.github/workflows/api_cd.yaml +++ /dev/null @@ -1,45 +0,0 @@ -on: - workflow_dispatch: - -jobs: - compute-version: - runs-on: ubuntu-latest - outputs: - version: ${{ steps.version.outputs.version }} - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - fetch-tags: true - - run: git fetch --tags --force - - uses: ./.github/actions/doxxer_install - - id: version - run: | - VERSION=$(doxxer --config doxxer.api.toml next patch) - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "Computed version: $VERSION" - - deploy: - needs: compute-version - runs-on: ubuntu-latest - timeout-minutes: 60 - concurrency: api-fly-deploy - steps: - - uses: actions/checkout@v4 - - uses: superfly/flyctl-actions/setup-flyctl@master - - run: flyctl deploy --config apps/api/fly.toml --dockerfile apps/api/Dockerfile --remote-only --build-arg APP_VERSION=${{ needs.compute-version.outputs.version }} - env: - FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} - - tag: - needs: [compute-version, deploy] - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v4 - - uses: mathieudutour/github-tag-action@v6.2 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - custom_tag: api_v${{ needs.compute-version.outputs.version }} - tag_prefix: "" diff --git a/.github/workflows/api_ci.yaml b/.github/workflows/api_ci.yaml deleted file mode 100644 index 4b8b0dafeb..0000000000 --- a/.github/workflows/api_ci.yaml +++ /dev/null @@ -1,23 +0,0 @@ -on: - workflow_dispatch: - push: - branches: - - main - paths: - - apps/api/** - - crates/llm-proxy/** - - crates/transcribe-proxy/** - pull_request: - paths: - - apps/api/** - - crates/llm-proxy/** - - crates/transcribe-proxy/** -jobs: - ci: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/rust_install - with: - platform: linux - - run: cargo check -p api diff --git a/.github/workflows/chrome_cd.yaml b/.github/workflows/chrome_cd.yaml deleted file mode 100644 index 3bc79f606c..0000000000 --- a/.github/workflows/chrome_cd.yaml +++ /dev/null @@ -1,17 +0,0 @@ -name: chrome_cd - -on: - workflow_dispatch: - -jobs: - zip: - runs-on: depot-ubuntu-24.04-4 - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/pnpm_install - - run: pnpm -F @hypr/chrome zip - - uses: actions/upload-artifact@v4 - with: - name: chrome-extension-zip - path: apps/chrome/.output/*.zip - retention-days: 30 diff --git a/.github/workflows/chrome_ci.yaml b/.github/workflows/chrome_ci.yaml deleted file mode 100644 index 3276929ee2..0000000000 --- a/.github/workflows/chrome_ci.yaml +++ /dev/null @@ -1,37 +0,0 @@ -name: chrome_ci - -on: - workflow_dispatch: - push: - branches: - - main - paths: - - apps/chrome/** - - pnpm-lock.yaml - - pnpm-workspace.yaml - - .github/actions/pnpm_install/** - - .github/workflows/chrome_ci.yaml - pull_request: - paths: - - apps/chrome/** - - pnpm-lock.yaml - - pnpm-workspace.yaml - - .github/actions/pnpm_install/** - - .github/workflows/chrome_ci.yaml - -jobs: - chrome_ci: - runs-on: depot-ubuntu-24.04-4 - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/pnpm_install - - run: pnpm -F @hypr/chrome typecheck - - run: pnpm -F @hypr/chrome build - - ci: - if: always() - needs: [chrome_ci] - runs-on: ubuntu-latest - steps: - - run: exit 1 - if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') diff --git a/.github/workflows/cli_desktop_ci.yml b/.github/workflows/cli_desktop_ci.yml deleted file mode 100644 index 79fc143136..0000000000 --- a/.github/workflows/cli_desktop_ci.yml +++ /dev/null @@ -1,29 +0,0 @@ -on: - pull_request: - branches: - - main - paths: - - "apps/cli/**" - - "crates/**" - - "Cargo.toml" - - "Cargo.lock" - - ".github/workflows/cli_desktop_ci.yml" - - ".github/actions/install_cli_deps/**" - -jobs: - check: - if: ${{ !startsWith(github.head_ref || '', 'blog/') }} - runs-on: macos-latest - defaults: - run: - shell: bash - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/install_cli_deps - - uses: dtolnay/rust-toolchain@stable - with: - components: clippy - - uses: Swatinem/rust-cache@v2 - - run: cargo check -p cli --features desktop-macos - - run: cargo clippy -p cli --features desktop-macos - - run: cargo test -p cli --features desktop-macos diff --git a/.github/workflows/cli_standalone_cd.yaml b/.github/workflows/cli_standalone_cd.yaml deleted file mode 100644 index 6fba86d9bb..0000000000 --- a/.github/workflows/cli_standalone_cd.yaml +++ /dev/null @@ -1,211 +0,0 @@ -on: - workflow_dispatch: - inputs: - channel: - description: "Release channel" - required: true - type: choice - options: - - nightly - - stable - -concurrency: - group: ${{ github.workflow }}-${{ inputs.channel }} - cancel-in-progress: true - -jobs: - compute-version: - runs-on: ubuntu-latest - outputs: - version: ${{ steps.version.outputs.version }} - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - fetch-tags: true - - run: git fetch --tags --force - - uses: ./.github/actions/doxxer_install - - id: version - run: | - if [[ "${{ inputs.channel }}" == "nightly" ]]; then - LATEST_TAG=$(git tag -l 'cli_v*' --sort=-creatordate | head -n1) - if [[ "$LATEST_TAG" == *"-nightly"* ]]; then - VERSION=$(doxxer --config doxxer.cli.toml next prerelease) - else - VERSION=$(doxxer --config doxxer.cli.toml next pre-patch) - fi - else - VERSION=$(doxxer --config doxxer.cli.toml next patch) - fi - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "Computed version: $VERSION" - - build-macos: - needs: compute-version - runs-on: depot-macos-15 - strategy: - matrix: - target: - - aarch64-apple-darwin - - x86_64-apple-darwin - defaults: - run: - shell: bash - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/rust_install - with: - platform: macos - - id: apple-cert - uses: ./.github/actions/apple_cert - with: - apple-certificate: ${{ secrets.APPLE_CERTIFICATE }} - apple-certificate-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - keychain-password: ${{ secrets.KEYCHAIN_PASSWORD }} - - run: echo "SDKROOT=$(xcrun --sdk macosx --show-sdk-path)" >> $GITHUB_ENV - - run: cargo build --release -p cli --features standalone-macos --target ${{ matrix.target }} - env: - POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }} - APP_VERSION: ${{ needs.compute-version.outputs.version }} - - run: | - cd apps/cli-ui - swift build -c release --arch arm64 --arch x86_64 - cp .build/apple/Products/Release/char-cli-ui ../../target/${{ matrix.target }}/release/ - - run: | - BIN="target/${{ matrix.target }}/release/char" - UI_BIN="target/${{ matrix.target }}/release/char-cli-ui" - strip -x "$BIN" - codesign -s "${{ steps.apple-cert.outputs.cert-id }}" -f --identifier com.char.cli --options runtime "$BIN" - codesign -s "${{ steps.apple-cert.outputs.cert-id }}" -f --identifier com.char.cli-ui --options runtime "$UI_BIN" - codesign --verify --verbose=4 "$BIN" - codesign --verify --verbose=4 "$UI_BIN" - ARCHIVE="char-${{ needs.compute-version.outputs.version }}-${{ matrix.target }}.tar.xz" - tar -cJf "$ARCHIVE" -C "target/${{ matrix.target }}/release" char char-cli-ui - echo "ARCHIVE=$ARCHIVE" >> $GITHUB_ENV - - uses: actions/upload-artifact@v4 - with: - name: char-${{ matrix.target }} - path: ${{ env.ARCHIVE }} - retention-days: 3 - - build-linux: - needs: compute-version - runs-on: ${{ matrix.runner }} - strategy: - matrix: - include: - - target: x86_64-unknown-linux-gnu - runner: ubuntu-latest - - target: aarch64-unknown-linux-gnu - runner: ubuntu-24.04-arm - defaults: - run: - shell: bash - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/rust_install - with: - platform: linux - - run: cargo build --release -p cli --features standalone-linux --target ${{ matrix.target }} - env: - POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }} - APP_VERSION: ${{ needs.compute-version.outputs.version }} - - run: | - BIN="target/${{ matrix.target }}/release/char" - strip "$BIN" - ARCHIVE="char-${{ needs.compute-version.outputs.version }}-${{ matrix.target }}.tar.xz" - tar -cJf "$ARCHIVE" -C "target/${{ matrix.target }}/release" char - echo "ARCHIVE=$ARCHIVE" >> $GITHUB_ENV - - uses: actions/upload-artifact@v4 - with: - name: char-${{ matrix.target }} - path: ${{ env.ARCHIVE }} - retention-days: 3 - - publish-npm: - needs: [compute-version, build-macos, build-linux] - runs-on: ubuntu-latest - permissions: - id-token: write - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: "22.14.0" - registry-url: "https://registry.npmjs.org" - - run: npm install -g npm@11.5.1 - - run: | - node -v - npm -v - - run: | - cd apps/cli/npm - VERSION="${{ needs.compute-version.outputs.version }}" - NPM_TAG="${{ inputs.channel == 'nightly' && 'nightly' || 'latest' }}" - node -e " - const fs = require('fs'); - const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); - pkg.version = '$VERSION'; - fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n'); - " - npm publish --tag "$NPM_TAG" - - create-tag: - needs: [compute-version, publish-npm] - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v4 - - uses: mathieudutour/github-tag-action@v6.2 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - custom_tag: cli_v${{ needs.compute-version.outputs.version }} - tag_prefix: "" - - release: - needs: [compute-version, build-macos, build-linux, create-tag] - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - fetch-tags: true - - run: git fetch --tags --force - - uses: actions/download-artifact@v4 - with: - path: artifacts - merge-multiple: true - - id: checksums - uses: ./.github/actions/generate_checksums - with: - files: | - artifacts/char-${{ needs.compute-version.outputs.version }}-aarch64-apple-darwin.tar.xz - artifacts/char-${{ needs.compute-version.outputs.version }}-x86_64-apple-darwin.tar.xz - artifacts/char-${{ needs.compute-version.outputs.version }}-x86_64-unknown-linux-gnu.tar.xz - artifacts/char-${{ needs.compute-version.outputs.version }}-aarch64-unknown-linux-gnu.tar.xz - - id: artifacts - run: | - LIST=$(ls artifacts/*.tar.xz artifacts/*.sha256 | paste -sd,) - echo "list=$LIST" >> $GITHUB_OUTPUT - - id: release-body - env: - VERSION: ${{ needs.compute-version.outputs.version }} - run: | - CURRENT_TAG="cli_v$VERSION" - PREV_TAG=$(git tag -l 'cli_v*' --sort=-v:refname | grep -v "^$CURRENT_TAG$" | head -n1) - - if [[ -n "$PREV_TAG" ]]; then - echo "value=https://github.com/${{ github.repository }}/compare/$PREV_TAG...$CURRENT_TAG" >> "$GITHUB_OUTPUT" - else - echo "value=https://github.com/${{ github.repository }}/commits/$CURRENT_TAG" >> "$GITHUB_OUTPUT" - fi - - uses: ncipollo/release-action@v1 - with: - tag: cli_v${{ needs.compute-version.outputs.version }} - name: cli_v${{ needs.compute-version.outputs.version }} - body: ${{ steps.release-body.outputs.value }} - prerelease: ${{ inputs.channel == 'nightly' }} - makeLatest: false - artifacts: ${{ steps.artifacts.outputs.list }} diff --git a/.github/workflows/cli_standalone_ci.yml b/.github/workflows/cli_standalone_ci.yml deleted file mode 100644 index 8f5b45361e..0000000000 --- a/.github/workflows/cli_standalone_ci.yml +++ /dev/null @@ -1,61 +0,0 @@ -on: - pull_request: - branches: - - main - paths: - - "apps/cli/**" - - "crates/**" - - "Cargo.toml" - - "Cargo.lock" - - ".github/workflows/cli_standalone_ci.yml" - - ".github/actions/install_cli_deps/**" - -jobs: - check: - if: ${{ !startsWith(github.head_ref || '', 'blog/') }} - strategy: - fail-fast: false - matrix: - include: - - os: ubuntu-latest - platform: linux - feature: standalone-linux - clippy: false - - os: macos-latest - platform: macos - feature: standalone-macos - clippy: true - runs-on: ${{ matrix.os }} - defaults: - run: - shell: bash - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/install_cli_deps - - if: matrix.clippy - uses: dtolnay/rust-toolchain@stable - with: - components: clippy - - if: ${{ !matrix.clippy }} - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - - run: cargo check -p cli --features ${{ matrix.feature }} - - if: matrix.clippy - run: cargo clippy -p cli --features ${{ matrix.feature }} - - if: runner.os == 'macOS' - run: | - rustup target add x86_64-apple-darwin - cargo check -p cli --features standalone-macos --target x86_64-apple-darwin - - e2e: - if: ${{ !startsWith(github.head_ref || '', 'blog/') }} - runs-on: macos-latest - defaults: - run: - shell: bash - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/install_cli_deps - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - - run: cargo test -p cli --features standalone-macos,e2e -- --test-threads=1 diff --git a/.github/workflows/desktop_ci.yaml b/.github/workflows/desktop_ci.yaml index b4cc3315b3..633487affb 100644 --- a/.github/workflows/desktop_ci.yaml +++ b/.github/workflows/desktop_ci.yaml @@ -55,18 +55,13 @@ jobs: --exclude desktop \ --exclude control-tauri \ --exclude ai \ - --exclude email \ --exclude lago \ - --exclude mac \ --exclude notch \ --exclude notification-macos \ - --exclude notification-macos2 \ --exclude tcc \ - --exclude apple-note \ --exclude notification-linux \ --exclude am \ --exclude aec \ - --exclude agc \ --exclude whisper \ --exclude whisper-local \ --exclude whisper-local-model \ @@ -86,7 +81,6 @@ jobs: --exclude host \ --exclude intercept \ --exclude frontmatter \ - --exclude openstatus \ --exclude audio \ --exclude audio-device \ --exclude transcribe-whisper-local \ @@ -97,7 +91,6 @@ jobs: --exclude transcribe-cactus \ --exclude llm-cactus \ --exclude local-llm-core \ - --exclude local-stt-server \ --exclude tauri-plugin-analytics\ --exclude tauri-plugin-apple-calendar \ --exclude tauri-plugin-audio-priority \ @@ -136,7 +129,6 @@ jobs: --exclude tauri-plugin-tracing \ --exclude tauri-plugin-tray \ --exclude tauri-plugin-updater2 \ - --exclude tauri-plugin-webhook \ --exclude tauri-plugin-windows \ --exclude db3 \ --exclude db-core \ diff --git a/.github/workflows/llm_e2e.yaml b/.github/workflows/llm_e2e.yaml deleted file mode 100644 index d72a6d234d..0000000000 --- a/.github/workflows/llm_e2e.yaml +++ /dev/null @@ -1,37 +0,0 @@ -on: - workflow_dispatch: - push: - branches: - - main - paths: - - crates/llm-proxy/** - pull_request: - paths: - - crates/llm-proxy/** - -jobs: - proxy-non-streaming: - runs-on: depot-ubuntu-24.04-8 - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/rust_install - with: - platform: linux - - uses: ./.github/actions/infisical_install - - run: infisical run --token="$INFISICAL_TOKEN" --env=dev --projectId="$INFISICAL_PROJECT_ID" --path="/llm" -- cargo test -p llm-proxy proxy_e2e::openrouter::non_streaming -- --ignored --nocapture - env: - INFISICAL_TOKEN: ${{ secrets.INFISICAL_TOKEN }} - INFISICAL_PROJECT_ID: ${{ secrets.INFISICAL_PROJECT_ID }} - - proxy-streaming: - runs-on: depot-ubuntu-24.04-8 - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/rust_install - with: - platform: linux - - uses: ./.github/actions/infisical_install - - run: infisical run --token="$INFISICAL_TOKEN" --env=dev --projectId="$INFISICAL_PROJECT_ID" --path="/llm" -- cargo test -p llm-proxy proxy_e2e::openrouter::streaming -- --ignored --nocapture - env: - INFISICAL_TOKEN: ${{ secrets.INFISICAL_TOKEN }} - INFISICAL_PROJECT_ID: ${{ secrets.INFISICAL_PROJECT_ID }} diff --git a/.github/workflows/openstatus.yaml b/.github/workflows/openstatus.yaml deleted file mode 100644 index ebdc1fbda9..0000000000 --- a/.github/workflows/openstatus.yaml +++ /dev/null @@ -1,18 +0,0 @@ -name: openstatus - -on: - push: - branches: - - main - paths: - - openstatus.yaml - - .github/workflows/openstatus.yaml - -jobs: - sync: - runs-on: macos-latest - steps: - - uses: actions/checkout@v4 - - uses: Homebrew/actions/setup-homebrew@master - - run: brew install openstatusHQ/cli/openstatus --cask - - run: openstatus monitors apply --access-token ${{ secrets.OPENSTATUS_API_KEY }} -y diff --git a/.github/workflows/slack_internal_cd.yaml b/.github/workflows/slack_internal_cd.yaml deleted file mode 100644 index 4a03aa0a88..0000000000 --- a/.github/workflows/slack_internal_cd.yaml +++ /dev/null @@ -1,31 +0,0 @@ -on: - workflow_dispatch: - push: - paths: - - "apps/slack-internal/**" - - "packages/agent-support/**" - - "packages/agent-designer/**" - branches: - - main - -env: - AGENT_CHANNEL_MAP: >- - { - "C0AB7TVP9B5": "designer", - "C0AA8650ERE": "internal" - } - -jobs: - deploy: - runs-on: ubuntu-latest - timeout-minutes: 60 - concurrency: slack-internal-fly-deploy - steps: - - uses: actions/checkout@v4 - - uses: superfly/flyctl-actions/setup-flyctl@master - - run: flyctl secrets set AGENT_CHANNEL_MAP='${{ env.AGENT_CHANNEL_MAP }}' --stage --app hyprnote-slack-internal - env: - FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} - - run: flyctl deploy --config apps/slack-internal/fly.toml --remote-only --build-arg GIT_SHA=${{ github.sha }} - env: - FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} diff --git a/.github/workflows/stripe_cd.yaml b/.github/workflows/stripe_cd.yaml deleted file mode 100644 index 3e2fe680d4..0000000000 --- a/.github/workflows/stripe_cd.yaml +++ /dev/null @@ -1,45 +0,0 @@ -on: - workflow_dispatch: - -jobs: - compute-version: - runs-on: ubuntu-latest - outputs: - version: ${{ steps.version.outputs.version }} - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - fetch-tags: true - - run: git fetch --tags --force - - uses: ./.github/actions/doxxer_install - - id: version - run: | - VERSION=$(doxxer --config doxxer.stripe.toml next patch) - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "Computed version: $VERSION" - - deploy: - needs: compute-version - runs-on: depot-ubuntu-24.04-8 - timeout-minutes: 60 - concurrency: stripe-fly-deploy - steps: - - uses: actions/checkout@v4 - - uses: superfly/flyctl-actions/setup-flyctl@master - - run: flyctl deploy --config apps/stripe/fly.toml --dockerfile apps/stripe/Dockerfile --remote-only - env: - FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} - - tag: - needs: [compute-version, deploy] - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v4 - - uses: mathieudutour/github-tag-action@v6.2 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - custom_tag: stripe_v${{ needs.compute-version.outputs.version }} - tag_prefix: "" diff --git a/.github/workflows/stt_e2e.yaml b/.github/workflows/stt_e2e.yaml deleted file mode 100644 index 77534e3dc5..0000000000 --- a/.github/workflows/stt_e2e.yaml +++ /dev/null @@ -1,206 +0,0 @@ -on: - workflow_dispatch: - inputs: - provider: - type: choice - options: - - deepgram - - assemblyai - - soniox - - gladia - - fireworks - - openai - - elevenlabs - push: - branches: - - main - paths: - - crates/transcribe-proxy/** - - crates/owhisper-client/src/adapter/** - pull_request: - paths: - - crates/transcribe-proxy/** - - crates/owhisper-client/src/adapter/** - -jobs: - detect-changes: - if: github.event_name != 'workflow_dispatch' - runs-on: ubuntu-latest - outputs: - providers: ${{ steps.detect.outputs.providers }} - steps: - - uses: actions/checkout@v4 - - uses: dorny/paths-filter@v3 - id: filter - with: - filters: | - transcribe_proxy: - - 'crates/transcribe-proxy/**' - deepgram: - - 'crates/owhisper-client/src/adapter/deepgram/**' - assemblyai: - - 'crates/owhisper-client/src/adapter/assemblyai/**' - soniox: - - 'crates/owhisper-client/src/adapter/soniox/**' - gladia: - - 'crates/owhisper-client/src/adapter/gladia/**' - fireworks: - - 'crates/owhisper-client/src/adapter/fireworks/**' - openai: - - 'crates/owhisper-client/src/adapter/openai/**' - elevenlabs: - - 'crates/owhisper-client/src/adapter/elevenlabs/**' - - id: detect - run: | - set -euo pipefail - - providers=() - - if [ "${{ steps.filter.outputs.deepgram }}" == "true" ]; then providers+=("deepgram"); fi - if [ "${{ steps.filter.outputs.assemblyai }}" == "true" ]; then providers+=("assemblyai"); fi - if [ "${{ steps.filter.outputs.soniox }}" == "true" ]; then providers+=("soniox"); fi - if [ "${{ steps.filter.outputs.gladia }}" == "true" ]; then providers+=("gladia"); fi - if [ "${{ steps.filter.outputs.fireworks }}" == "true" ]; then providers+=("fireworks"); fi - if [ "${{ steps.filter.outputs.openai }}" == "true" ]; then providers+=("openai"); fi - if [ "${{ steps.filter.outputs.elevenlabs }}" == "true" ]; then providers+=("elevenlabs"); fi - - if [ ${#providers[@]} -eq 0 ]; then - if [ "${{ steps.filter.outputs.transcribe_proxy }}" == "true" ]; then - providers=("deepgram" "soniox") - else - providers=("deepgram") - fi - fi - - json="$(printf '%s\n' "${providers[@]}" | jq -Rsc 'split("\n")[:-1] | unique')" - echo "providers=$json" >> "$GITHUB_OUTPUT" - - setup: - runs-on: ubuntu-latest - needs: [detect-changes] - if: always() - outputs: - providers: ${{ steps.set.outputs.providers }} - steps: - - id: set - run: | - if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then - echo 'providers=["${{ inputs.provider }}"]' >> $GITHUB_OUTPUT - else - echo "providers=${{ needs.detect-changes.outputs.providers }}" >> $GITHUB_OUTPUT - fi - - direct-provider-batch-e2e: - needs: setup - if: needs.setup.outputs.providers != '' - runs-on: depot-ubuntu-24.04-8 - strategy: - fail-fast: false - matrix: - provider: ${{ fromJson(needs.setup.outputs.providers) }} - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/rust_install - with: - platform: linux - - uses: ./.github/actions/infisical_install - - run: | - infisical run --token="$INFISICAL_TOKEN" --env=dev --projectId="$INFISICAL_PROJECT_ID" --path="/stt" -- bash -lc ' - set -euo pipefail - - provider="${PROVIDER}" - - case "$provider" in - deepgram) - : "${DEEPGRAM_API_KEY:?DEEPGRAM_API_KEY is required}" - ;; - assemblyai) - : "${ASSEMBLYAI_API_KEY:?ASSEMBLYAI_API_KEY is required}" - ;; - soniox) - : "${SONIOX_API_KEY:?SONIOX_API_KEY is required}" - ;; - gladia) - : "${GLADIA_API_KEY:?GLADIA_API_KEY is required}" - ;; - fireworks) - : "${FIREWORKS_API_KEY:?FIREWORKS_API_KEY is required}" - ;; - openai) - : "${OPENAI_API_KEY:?OPENAI_API_KEY is required}" - ;; - elevenlabs) - : "${ELEVENLABS_API_KEY:?ELEVENLABS_API_KEY is required}" - ;; - *) - echo "Unknown provider: $provider" - exit 1 - ;; - esac - - echo "Running direct provider batch e2e for $provider" - cargo test -p owhisper-client --test provider_batch_e2e "direct_batch::${provider}" -- --ignored --nocapture - ' - env: - PROVIDER: ${{ matrix.provider }} - INFISICAL_TOKEN: ${{ secrets.INFISICAL_TOKEN }} - INFISICAL_PROJECT_ID: ${{ secrets.INFISICAL_PROJECT_ID }} - - proxy-provider-e2e: - needs: setup - if: needs.setup.outputs.providers != '' - runs-on: depot-ubuntu-24.04-8 - strategy: - fail-fast: false - matrix: - provider: ${{ fromJson(needs.setup.outputs.providers) }} - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/rust_install - with: - platform: linux - - uses: ./.github/actions/infisical_install - - run: | - infisical run --token="$INFISICAL_TOKEN" --env=dev --projectId="$INFISICAL_PROJECT_ID" --path="/stt" -- bash -lc ' - set -euo pipefail - - provider="${PROVIDER}" - - case "$provider" in - deepgram) - : "${DEEPGRAM_API_KEY:?DEEPGRAM_API_KEY is required}" - ;; - assemblyai) - : "${ASSEMBLYAI_API_KEY:?ASSEMBLYAI_API_KEY is required}" - ;; - soniox) - : "${SONIOX_API_KEY:?SONIOX_API_KEY is required}" - ;; - gladia) - : "${GLADIA_API_KEY:?GLADIA_API_KEY is required}" - ;; - fireworks) - : "${FIREWORKS_API_KEY:?FIREWORKS_API_KEY is required}" - ;; - openai) - : "${OPENAI_API_KEY:?OPENAI_API_KEY is required}" - ;; - elevenlabs) - : "${ELEVENLABS_API_KEY:?ELEVENLABS_API_KEY is required}" - ;; - *) - echo "Unknown provider: $provider" - exit 1 - ;; - esac - - echo "Running transcribe-proxy provider e2e tests for $provider" - - for suite in passthrough::live passthrough::batch hyprnote::live hyprnote::batch; do - cargo test -p transcribe-proxy --test proxy_provider_e2e "${suite}::${provider}" -- --ignored --nocapture - done - ' - env: - PROVIDER: ${{ matrix.provider }} - INFISICAL_TOKEN: ${{ secrets.INFISICAL_TOKEN }} - INFISICAL_PROJECT_ID: ${{ secrets.INFISICAL_PROJECT_ID }} diff --git a/Cargo.lock b/Cargo.lock index 36abda3559..c337b82f62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,15 +8,6 @@ version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" -[[package]] -name = "aardvark-sys" -version = "0.1.0" -source = "git+https://github.com/zeroclaw-labs/zeroclaw.git?rev=30395e909f660c9a6ec3075df1f70d84077a96d0#30395e909f660c9a6ec3075df1f70d84077a96d0" -dependencies = [ - "libloading 0.8.9", - "thiserror 2.0.18", -] - [[package]] name = "activity-capture" version = "0.1.0" @@ -30,26 +21,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "activity-capture-dev" -version = "0.1.0" -dependencies = [ - "activity-capture", - "axum 0.8.9", - "chrono", - "clap", - "crossterm", - "futures-util", - "llm-cactus", - "ratatui", - "reqwest 0.13.2", - "screen-core", - "serde", - "serde_json", - "tokio", - "url", -] - [[package]] name = "activity-capture-interface" version = "0.1.0" @@ -255,16 +226,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common 0.1.6", - "generic-array", -] - [[package]] name = "aec" version = "0.1.0" @@ -302,18 +263,6 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "agc" -version = "0.1.0" -dependencies = [ - "audio-utils", - "dagc", - "data", - "hound", - "rodio", - "vad-masking", -] - [[package]] name = "agent-core" version = "0.1.0" @@ -423,22 +372,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "amp" -version = "0.1.0" -dependencies = [ - "cli-process", - "dirs 6.0.0", - "futures-util", - "serde", - "serde_json", - "tempfile", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "tokio-util", -] - [[package]] name = "analytics" version = "0.1.0" @@ -584,598 +517,173 @@ checksum = "623a80caf0a084acf22cc3f43e49b984214e1c00b79779b16683ab55d46fa764" dependencies = [ "apalis-core", "chrono", - "cron 0.16.0", + "cron", "futures-util", "serde", "ulid", ] [[package]] -name = "api" +name = "api-client" version = "0.1.0" dependencies = [ - "analytics", - "api-auth", - "api-cactus", - "api-calendar", - "api-env", - "api-mail", - "api-nango", - "api-pyannote", - "api-research", - "api-subscription", - "api-support", - "api-ticket", - "axum 0.8.9", - "dotenvy", - "envy", - "governor", - "jsonwebtoken", - "linear", - "llm-proxy", - "observability", - "opentelemetry", - "opentelemetry-otlp", - "opentelemetry_sdk", - "owhisper-client", + "chrono", + "google-calendar", + "outlook-calendar", + "progenitor-client 0.13.0", + "progenitor-utils", "reqwest 0.13.2", - "rustls 0.23.38", - "sentry", "serde", "serde_json", - "tokio", - "tower 0.5.3", - "tower-http 0.6.8", - "tracing", - "tracing-opentelemetry", - "tracing-subscriber", - "transcribe-proxy", - "url", - "utoipa", + "ticket-interface", ] [[package]] -name = "api-agent" +name = "apple-calendar" version = "0.1.0" dependencies = [ - "reqwest 0.13.2", + "backon", + "block2", + "chrono", + "itertools 0.14.0", + "json-patch 4.1.0", + "jsonschema", + "objc2", + "objc2-contacts", + "objc2-core-graphics", + "objc2-event-kit", + "objc2-foundation", + "schemars 1.2.1", "serde", "serde_json", + "specta", + "strum 0.28.0", "thiserror 2.0.18", "tokio", - "url", -] - -[[package]] -name = "api-auth" -version = "0.1.0" -dependencies = [ - "axum 0.8.9", - "supabase-auth", - "tokio", + "tracing", + "uuid", ] [[package]] -name = "api-bot" +name = "apple-todo" version = "0.1.0" dependencies = [ - "api-error", - "axum 0.8.9", - "recall", - "sentry", + "backon", + "block2", + "chrono", + "objc2", + "objc2-core-graphics", + "objc2-core-location", + "objc2-event-kit", + "objc2-foundation", + "schemars 1.2.1", "serde", "serde_json", + "specta", "thiserror 2.0.18", "tracing", - "urlencoding", - "utoipa", ] [[package]] -name = "api-cactus" -version = "0.1.0" +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" dependencies = [ - "axum 0.8.9", - "bytes", - "reqwest 0.13.2", - "serde", - "tracing", + "num-traits", ] [[package]] -name = "api-calendar" -version = "0.1.0" +name = "ar_archive_writer" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" dependencies = [ - "api-auth", - "api-error", - "api-nango", - "axum 0.8.9", - "chrono", - "google-calendar", - "nango", - "outlook-calendar", - "serde", - "thiserror 2.0.18", - "utoipa", + "object", ] [[package]] -name = "api-claw" -version = "0.1.0" +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" dependencies = [ - "base64 0.22.1", - "chrono", - "exedev", - "rand_core 0.6.4", - "serde", - "serde_json", - "sha2 0.11.0", - "ssh-key", - "thiserror 2.0.18", - "tokio", - "tracing", + "derive_arbitrary", ] [[package]] -name = "api-client" -version = "0.1.0" +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" dependencies = [ - "chrono", - "google-calendar", - "outlook-calendar", - "progenitor-client 0.13.0", - "progenitor-utils", - "reqwest 0.13.2", - "serde", - "serde_json", - "ticket-interface", + "clipboard-win", + "image", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "wl-clipboard-rs", + "x11rb", ] [[package]] -name = "api-env" -version = "0.1.0" +name = "arc-swap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" dependencies = [ - "serde", + "rustversion", ] [[package]] -name = "api-error" -version = "0.1.0" -dependencies = [ - "axum 0.8.9", - "sentry", - "serde", - "tracing", - "utoipa", -] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] -name = "api-mail" -version = "0.1.0" -dependencies = [ - "api-auth", - "api-error", - "api-nango", - "axum 0.8.9", - "google-mail", - "nango", - "serde", - "thiserror 2.0.18", - "utoipa", -] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] -name = "api-messenger" -version = "0.1.0" +name = "askama" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b8246bcbf8eb97abef10c2d92166449680d41d55c0fc6978a91dec2e3619608" dependencies = [ - "api-error", - "axum 0.8.9", - "hypr-http-utils", + "askama_macros", + "itoa", + "percent-encoding", "serde", "serde_json", - "slack-web", - "teems", - "thiserror 2.0.18", ] [[package]] -name = "api-nango" +name = "askama-utils" version = "0.1.0" dependencies = [ - "api-auth", - "api-env", - "api-error", - "axum 0.8.9", + "askama", + "askama_parser", "chrono", - "futures-util", - "hex", - "hmac 0.13.0", - "nango", - "reqwest 0.13.2", - "sentry", - "serde", - "serde_json", - "sha2 0.11.0", - "thiserror 2.0.18", - "tokio", - "tracing", - "urlencoding", - "utoipa", - "wiremock", + "insta", + "isolang", ] [[package]] -name = "api-pyannote" -version = "0.1.0" +name = "askama_derive" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f9670bc84a28bb3da91821ef74226949ab63f1265aff7c751634f1dd0e6f97c" dependencies = [ - "api-auth", - "api-env", - "api-error", - "axum 0.8.9", - "pyannote-cloud", - "reqwest 0.13.2", - "serde", - "serde_json", - "tokio", - "tower 0.5.3", - "tracing", - "utoipa", - "wiremock", -] - -[[package]] -name = "api-research" -version = "0.1.0" -dependencies = [ - "askama 0.15.6", - "axum 0.8.9", - "exa", - "jina", - "mcp", - "rmcp", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokio", - "tokio-util", -] - -[[package]] -name = "api-storage" -version = "0.1.0" -dependencies = [ - "api-error", - "api-nango", - "axum 0.8.9", - "google-drive", - "nango", - "sentry", - "serde", - "serde_json", - "thiserror 2.0.18", - "tracing", - "utoipa", -] - -[[package]] -name = "api-subscription" -version = "0.1.0" -dependencies = [ - "analytics", - "api-auth", - "api-env", - "api-error", - "async-stripe", - "async-stripe-billing", - "async-stripe-core", - "axum 0.8.9", - "backon", - "chrono", - "loops", - "observability", - "reqwest 0.13.2", - "sentry", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokio", - "tracing", - "urlencoding", - "utoipa", -] - -[[package]] -name = "api-support" -version = "0.1.0" -dependencies = [ - "api-auth", - "api-env", - "async-stream", - "async-stripe", - "async-stripe-billing", - "axum 0.8.9", - "futures-util", - "jsonwebtoken", - "llm-proxy", - "mcp", - "octocrab", - "openrouter", - "regex", - "reqwest 0.13.2", - "rmcp", - "sentry", - "serde", - "serde_json", - "sqlx", - "strum 0.28.0", - "template-support", - "thiserror 2.0.18", - "tokio", - "tokio-tungstenite 0.29.0", - "tokio-util", - "tracing", - "urlencoding", - "utoipa", -] - -[[package]] -name = "api-sync" -version = "0.1.0" -dependencies = [ - "api-error", - "axum 0.8.9", - "reqwest 0.13.2", - "sentry", - "serde", - "serde_json", - "supabase-auth", - "thiserror 2.0.18", - "tokio", - "tracing", - "utoipa", -] - -[[package]] -name = "api-ticket" -version = "0.1.0" -dependencies = [ - "api-auth", - "api-error", - "api-nango", - "axum 0.8.9", - "github-issues", - "linear", - "nango", - "serde", - "serde_json", - "thiserror 2.0.18", - "ticket-interface", - "utoipa", -] - -[[package]] -name = "apple-calendar" -version = "0.1.0" -dependencies = [ - "backon", - "block2", - "chrono", - "itertools 0.14.0", - "json-patch 4.1.0", - "jsonschema", - "objc2", - "objc2-contacts", - "objc2-core-graphics", - "objc2-event-kit", - "objc2-foundation", - "schemars 1.2.1", - "serde", - "serde_json", - "specta", - "strum 0.28.0", - "thiserror 2.0.18", - "tokio", - "tracing", - "uuid", -] - -[[package]] -name = "apple-note" -version = "0.1.0" -dependencies = [ - "flate2", - "prost 0.13.5", - "prost-build", - "serde", - "thiserror 2.0.18", -] - -[[package]] -name = "apple-todo" -version = "0.1.0" -dependencies = [ - "backon", - "block2", - "chrono", - "objc2", - "objc2-core-graphics", - "objc2-core-location", - "objc2-event-kit", - "objc2-foundation", - "schemars 1.2.1", - "serde", - "serde_json", - "specta", - "thiserror 2.0.18", - "tracing", -] - -[[package]] -name = "approx" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" -dependencies = [ - "num-traits", -] - -[[package]] -name = "ar_archive_writer" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" -dependencies = [ - "object", -] - -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -dependencies = [ - "derive_arbitrary", -] - -[[package]] -name = "arboard" -version = "3.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" -dependencies = [ - "clipboard-win", - "image", - "log", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation", - "parking_lot", - "percent-encoding", - "windows-sys 0.60.2", - "wl-clipboard-rs", - "x11rb", -] - -[[package]] -name = "arc-swap" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" -dependencies = [ - "rustversion", -] - -[[package]] -name = "arraydeque" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" - -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "askama" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4744ed2eef2645831b441d8f5459689ade2ab27c854488fbab1fbe94fce1a7" -dependencies = [ - "askama_derive 0.13.1", - "itoa", - "percent-encoding", - "serde", - "serde_json", -] - -[[package]] -name = "askama" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4" -dependencies = [ - "askama_derive 0.14.0", - "itoa", - "percent-encoding", - "serde", - "serde_json", -] - -[[package]] -name = "askama" -version = "0.15.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b8246bcbf8eb97abef10c2d92166449680d41d55c0fc6978a91dec2e3619608" -dependencies = [ - "askama_macros", - "itoa", - "percent-encoding", - "serde", - "serde_json", -] - -[[package]] -name = "askama-utils" -version = "0.1.0" -dependencies = [ - "askama 0.15.6", - "askama_parser 0.15.6", - "chrono", - "insta", - "isolang", -] - -[[package]] -name = "askama_derive" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d661e0f57be36a5c14c48f78d09011e67e0cb618f269cca9f2fd8d15b68c46ac" -dependencies = [ - "askama_parser 0.13.0", - "basic-toml", - "memchr", - "proc-macro2", - "quote", - "rustc-hash 2.1.2", - "serde", - "serde_derive", - "syn 2.0.117", -] - -[[package]] -name = "askama_derive" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f" -dependencies = [ - "askama_parser 0.14.0", - "basic-toml", - "memchr", - "proc-macro2", - "quote", - "rustc-hash 2.1.2", - "serde", - "serde_derive", - "syn 2.0.117", -] - -[[package]] -name = "askama_derive" -version = "0.15.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f9670bc84a28bb3da91821ef74226949ab63f1265aff7c751634f1dd0e6f97c" -dependencies = [ - "askama_parser 0.15.6", + "askama_parser", "basic-toml", "memchr", "proc-macro2", @@ -1192,31 +700,7 @@ version = "0.15.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0756b45480437dded0565dfc568af62ccce146fb6cfe902e808ba86e445f44f" dependencies = [ - "askama_derive 0.15.6", -] - -[[package]] -name = "askama_parser" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf315ce6524c857bb129ff794935cf6d42c82a6cff60526fe2a63593de4d0d4f" -dependencies = [ - "memchr", - "serde", - "serde_derive", - "winnow 0.7.15", -] - -[[package]] -name = "askama_parser" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358" -dependencies = [ - "memchr", - "serde", - "serde_derive", - "winnow 0.7.15", + "askama_derive", ] [[package]] @@ -1287,23 +771,12 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" dependencies = [ - "event-listener 5.4.1", + "event-listener", "event-listener-strategy", "futures-core", "pin-project-lite", ] -[[package]] -name = "async-channel" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" -dependencies = [ - "concurrent-queue", - "event-listener 2.5.3", - "futures-core", -] - [[package]] name = "async-channel" version = "2.5.0" @@ -1342,29 +815,6 @@ dependencies = [ "slab", ] -[[package]] -name = "async-imap" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a78dceaba06f029d8f4d7df20addd4b7370a30206e3926267ecda2915b0f3f66" -dependencies = [ - "async-channel 2.5.0", - "async-compression", - "base64 0.22.1", - "bytes", - "chrono", - "futures", - "imap-proto", - "log", - "nom 7.1.3", - "pin-project", - "pin-utils", - "self_cell", - "stop-token", - "thiserror 1.0.69", - "tokio", -] - [[package]] name = "async-io" version = "2.6.0" @@ -1389,7 +839,7 @@ version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" dependencies = [ - "event-listener 5.4.1", + "event-listener", "event-listener-strategy", "pin-project-lite", ] @@ -1425,14 +875,14 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" dependencies = [ - "async-channel 2.5.0", + "async-channel", "async-io", "async-lock", "async-signal", "async-task", "blocking", "cfg-if", - "event-listener 5.4.1", + "event-listener", "futures-lite", "rustix 1.1.4", ] @@ -1488,97 +938,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "async-stripe" -version = "1.0.0-rc.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ec8493f89fb9d9ac1848988955ec192df8e0ebf4d4bbd85fd93df9f681e890" -dependencies = [ - "async-stripe-client-core", - "async-stripe-shared", - "bytes", - "http-body-util", - "hyper 1.9.0", - "hyper-tls", - "hyper-util", - "miniserde", - "thiserror 2.0.18", - "tokio", - "tracing", -] - -[[package]] -name = "async-stripe-billing" -version = "1.0.0-rc.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2340b289cd5e34bb9b8b3eebed4e7a9729831ebee01388965ad4d88fd7a5a3b" -dependencies = [ - "async-stripe-client-core", - "async-stripe-shared", - "async-stripe-types", - "miniserde", - "serde", - "smol_str", - "tracing", -] - -[[package]] -name = "async-stripe-client-core" -version = "1.0.0-rc.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f095837711eb1c3ee02604b4e1d44b117014fb74da99ad4f2d70e907dbdc41" -dependencies = [ - "async-stripe-shared", - "async-stripe-types", - "bytes", - "futures-util", - "miniserde", - "serde", - "serde_json", - "serde_qs", - "thiserror 2.0.18", - "tracing", -] - -[[package]] -name = "async-stripe-core" -version = "1.0.0-rc.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee9710c3d64db48dda1ec0b4976d091b7f40cabc8ed7bcab9e93b608b70b405c" -dependencies = [ - "async-stripe-client-core", - "async-stripe-shared", - "async-stripe-types", - "miniserde", - "serde", - "smol_str", - "tracing", -] - -[[package]] -name = "async-stripe-shared" -version = "1.0.0-rc.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a352c5e36a92aa8bdd4326c8211b9a26e822e6cb4b7516a47282396a4938a231" -dependencies = [ - "async-stripe-types", - "miniserde", - "serde", - "smol_str", - "tracing", -] - -[[package]] -name = "async-stripe-types" -version = "1.0.0-rc.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1ec9960b89b1f556bf885403dd208d173a8c42d028fcd7baeca9ad2bfb13f6" -dependencies = [ - "miniserde", - "serde", - "smol_str", -] - [[package]] name = "async-task" version = "4.7.1" @@ -1628,15 +987,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "atomic" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" -dependencies = [ - "bytemuck", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -2009,11 +1359,11 @@ dependencies = [ "bytes", "fastrand", "hex", - "hmac 0.12.1", + "hmac", "http 0.2.12", "http 1.4.0", "http-body 0.4.6", - "lru 0.12.5", + "lru", "percent-encoding", "regex-lite", "sha2 0.10.9", @@ -2109,7 +1459,7 @@ dependencies = [ "crypto-bigint 0.5.5", "form_urlencoded", "hex", - "hmac 0.12.1", + "hmac", "http 0.2.12", "http 1.4.0", "p256 0.11.1", @@ -2412,7 +1762,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core 0.5.6", - "axum-macros", "base64 0.22.1", "bytes", "form_urlencoded", @@ -2503,17 +1852,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "axum-macros" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "az" version = "1.3.0" @@ -2661,7 +1999,7 @@ dependencies = [ "rustc-hash 1.1.0", "shlex", "syn 2.0.117", - "which 4.4.2", + "which", ] [[package]] @@ -2684,7 +2022,7 @@ dependencies = [ "rustc-hash 1.1.0", "shlex", "syn 2.0.117", - "which 4.4.2", + "which", ] [[package]] @@ -2708,30 +2046,15 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "bit-set" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" -dependencies = [ - "bit-vec 0.6.3", -] - [[package]] name = "bit-set" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "bit-vec 0.8.0", + "bit-vec", ] -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - [[package]] name = "bit-vec" version = "0.8.0" @@ -2804,7 +2127,7 @@ version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" dependencies = [ - "async-channel 2.5.0", + "async-channel", "async-task", "futures-io", "futures-lite", @@ -2915,16 +2238,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" -[[package]] -name = "borsh" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" -dependencies = [ - "bytes", - "cfg_aliases 0.2.1", -] - [[package]] name = "brotli" version = "8.0.2" @@ -3216,16 +2529,6 @@ dependencies = [ "serde", ] -[[package]] -name = "cargo-platform" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87a0c0e6148f11f01f32650a2ea02d532b2ad4e81d8bd41e6e565b5adc5e6082" -dependencies = [ - "serde", - "serde_core", -] - [[package]] name = "cargo_metadata" version = "0.19.2" @@ -3233,21 +2536,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" dependencies = [ "camino", - "cargo-platform 0.1.9", - "semver", - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "cargo_metadata" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9" -dependencies = [ - "camino", - "cargo-platform 0.3.2", + "cargo-platform", "semver", "serde", "serde_json", @@ -3265,21 +2554,12 @@ dependencies = [ ] [[package]] -name = "cast" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" - -[[package]] -name = "castaway" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" -dependencies = [ - "rustversion", -] - -[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] name = "cbc" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3370,17 +2650,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" -[[package]] -name = "chacha20" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures 0.2.17", -] - [[package]] name = "chacha20" version = "0.10.0" @@ -3392,19 +2661,6 @@ dependencies = [ "rand_core 0.10.1", ] -[[package]] -name = "chacha20poly1305" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" -dependencies = [ - "aead", - "chacha20 0.9.1", - "cipher", - "poly1305", - "zeroize", -] - [[package]] name = "chardetng" version = "0.1.17" @@ -3434,16 +2690,6 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58b52a9840ffff5d4d0058ae529fa066a75e794e3125546acfc61c23ad755e49" -[[package]] -name = "chrome-native-host" -version = "0.1.0" -dependencies = [ - "dirs 6.0.0", - "serde", - "serde_json", - "tempfile", -] - [[package]] name = "chrono" version = "0.4.44" @@ -3544,7 +2790,6 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common 0.1.6", "inout", - "zeroize", ] [[package]] @@ -3578,17 +2823,6 @@ dependencies = [ "clap_derive", ] -[[package]] -name = "clap-verbosity-flag" -version = "3.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d92b1fab272fe943881b77cc6e920d6543e5b1bfadbd5ed81c7c5a755742394" -dependencies = [ - "clap", - "log", - "tracing-core", -] - [[package]] name = "clap_builder" version = "4.6.0" @@ -3601,15 +2835,6 @@ dependencies = [ "strsim", ] -[[package]] -name = "clap_complete" -version = "4.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff7a1dccbdd8b078c2bdebff47e404615151534d5043da397ec50286816f9cb" -dependencies = [ - "clap", -] - [[package]] name = "clap_derive" version = "4.6.1" @@ -3644,92 +2869,6 @@ dependencies = [ "tokio-util", ] -[[package]] -name = "claw" -version = "0.1.0" -dependencies = [ - "anyhow", - "axum 0.8.9", - "serde", - "serde_json", - "supabase-auth", - "tokio", - "tower 0.5.3", - "tracing", - "tracing-subscriber", - "zeroclaw-api", - "zeroclaw-channels", - "zeroclaw-config", - "zeroclaw-gateway", - "zeroclaw-infra", - "zeroclaw-providers", - "zeroclaw-runtime", -] - -[[package]] -name = "cli" -version = "0.1.0" -dependencies = [ - "agent-core", - "analytics", - "audio", - "audio-actual", - "audio-norm", - "audio-utils", - "chrono", - "clap", - "clap-verbosity-flag", - "clap_complete", - "cli-docs", - "clio", - "codex 0.1.0", - "colored", - "comfy-table", - "crossterm", - "db-cli", - "dirs 6.0.0", - "host", - "language", - "libc", - "listener2-core", - "local-model", - "local-stt-core", - "local-stt-server", - "model-downloader", - "mp3", - "open", - "owhisper-client", - "owhisper-interface", - "portable-pty", - "ratatui", - "reqwest 0.13.2", - "rodio", - "serde", - "serde_json", - "strsim", - "strum 0.28.0", - "tempfile", - "textwrap", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "tracing", - "tracing-subscriber", - "transcript", - "url", - "uuid", - "vt100", -] - -[[package]] -name = "cli-docs" -version = "0.1.0" -dependencies = [ - "clap", - "serde", - "serde_json", -] - [[package]] name = "cli-process" version = "0.1.0" @@ -3741,21 +2880,6 @@ dependencies = [ "tokio-util", ] -[[package]] -name = "clio" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7fc6734af48458f72f5a3fa7b840903606427d98a710256e808f76a965047d9" -dependencies = [ - "cfg-if", - "clap", - "is-terminal", - "libc", - "tempfile", - "walkdir", - "windows-sys 0.42.0", -] - [[package]] name = "clipboard-win" version = "5.4.1" @@ -3793,12 +2917,6 @@ dependencies = [ "cc", ] -[[package]] -name = "cmov" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" - [[package]] name = "cobs" version = "0.3.0" @@ -3948,31 +3066,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "comfy-table" -version = "7.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958c5d6ecf1f214b4c2bbbbf6ab9523a864bd136dcf71a7e8904799acfe1ad47" -dependencies = [ - "crossterm", - "unicode-segmentation", - "unicode-width 0.2.2", -] - -[[package]] -name = "compact_str" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "rustversion", - "ryu", - "static_assertions", -] - [[package]] name = "compression-codecs" version = "0.4.37" @@ -3999,25 +3092,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "config" -version = "0.15.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e68cfe19cd7d23ffde002c24ffa5cda73931913ef394d5eaaa32037dc940c0c" -dependencies = [ - "convert_case 0.6.0", - "indexmap 2.14.0", - "json5", - "pathdiff", - "ron", - "serde-untagged", - "serde_core", - "serde_json", - "toml 1.1.2+spec-1.1.0", - "winnow 1.0.1", - "yaml-rust2", -] - [[package]] name = "console" version = "0.16.3" @@ -4026,7 +3100,6 @@ checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" dependencies = [ "encode_unicode", "libc", - "unicode-width 0.2.2", "windows-sys 0.61.2", ] @@ -4074,15 +3147,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" -[[package]] -name = "convert_case" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "convert_case" version = "0.8.0" @@ -4353,17 +3417,6 @@ dependencies = [ "itertools 0.13.0", ] -[[package]] -name = "cron" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5877d3fbf742507b66bc2a1945106bd30dd8504019d596901ddd012a4dd01740" -dependencies = [ - "chrono", - "once_cell", - "winnow 0.6.26", -] - [[package]] name = "cron" version = "0.16.0" @@ -4484,7 +3537,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", - "rand_core 0.6.4", "typenum", ] @@ -4497,16 +3549,6 @@ dependencies = [ "hybrid-array", ] -[[package]] -name = "csscolorparser" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" -dependencies = [ - "lab", - "phf 0.11.3", -] - [[package]] name = "cssparser" version = "0.29.6" @@ -4578,28 +3620,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "ctutils" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" -dependencies = [ - "cmov", -] - -[[package]] -name = "cursor" -version = "0.1.0" -dependencies = [ - "base64 0.22.1", - "reqwest 0.13.2", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokio", - "url", -] - [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -4627,12 +3647,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "dagc" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b391a0dc2723958e71bff67bc8f4c1fa14f8734e0b0e4d65f96fd25f4fdd057" - [[package]] name = "darling" version = "0.20.11" @@ -4874,23 +3888,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "db-cli" -version = "0.1.0" -dependencies = [ - "clap", - "db-app", - "db-core", - "db-migrate", - "dirs 6.0.0", - "serde", - "serde_json", - "sqlx", - "tempfile", - "thiserror 2.0.18", - "tokio", -] - [[package]] name = "db-core" version = "0.1.0" @@ -5047,12 +4044,6 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" -[[package]] -name = "deltae" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" - [[package]] name = "denoise" version = "0.1.0" @@ -5300,54 +4291,28 @@ dependencies = [ "plist", "regex", "serde", - "specta", - "sysinfo", - "thiserror 2.0.18", - "tokio", - "tracing", - "url", -] - -[[package]] -name = "deunicode" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" - -[[package]] -name = "device-monitor" -version = "0.1.0" -dependencies = [ - "audio-device", - "cidre", - "libpulse-binding", - "tracing", -] - -[[package]] -name = "devin" -version = "0.1.0" -dependencies = [ - "reqwest 0.13.2", - "serde", - "serde_json", + "specta", + "sysinfo", "thiserror 2.0.18", "tokio", + "tracing", "url", - "wiremock", ] [[package]] -name = "dialoguer" -version = "0.12.0" +name = "deunicode" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25f104b501bf2364e78d0d3974cbc774f738f5865306ed128e1e0d7499c0ad96" +checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" + +[[package]] +name = "device-monitor" +version = "0.1.0" dependencies = [ - "console", - "fuzzy-matcher", - "shell-words", - "tempfile", - "zeroize", + "audio-device", + "cidre", + "libpulse-binding", + "tracing", ] [[package]] @@ -5389,16 +4354,6 @@ dependencies = [ "block-buffer 0.12.0", "const-oid 0.10.2", "crypto-common 0.2.1", - "ctutils", -] - -[[package]] -name = "directories" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" -dependencies = [ - "dirs-sys 0.5.0", ] [[package]] @@ -5548,7 +4503,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" dependencies = [ - "bit-set 0.8.0", + "bit-set", "cssparser 0.36.0", "foldhash 0.2.0", "html5ever 0.38.0", @@ -5786,26 +4741,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "email" -version = "0.1.0" -dependencies = [ - "objc2", - "objc2-app-kit", - "objc2-foundation", - "url", -] - -[[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" @@ -5847,19 +4782,6 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" -[[package]] -name = "embedding" -version = "0.1.0" -dependencies = [ - "approx", - "data", - "knf-rs", - "onnx", - "serde", - "serde_json", - "thiserror 2.0.18", -] - [[package]] name = "encode_unicode" version = "1.0.0" @@ -5931,38 +4853,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "env_filter" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", - "log", -] - -[[package]] -name = "envy" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965" -dependencies = [ - "serde", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -6022,12 +4912,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - [[package]] name = "event-listener" version = "5.4.1" @@ -6045,7 +4929,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "event-listener 5.4.1", + "event-listener", "pin-project-lite", ] @@ -6060,37 +4944,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "exa" -version = "0.1.0" -dependencies = [ - "reqwest 0.13.2", - "schemars 1.2.1", - "serde", - "serde_json", - "specta", - "thiserror 2.0.18", - "tokio", - "url", -] - -[[package]] -name = "exedev" -version = "0.1.0" -dependencies = [ - "base64 0.22.1", - "chrono", - "rand_core 0.6.4", - "reqwest 0.13.2", - "serde", - "serde_json", - "shell-escape", - "ssh-key", - "thiserror 2.0.18", - "tokio", - "url", -] - [[package]] name = "export-core" version = "0.1.0" @@ -6139,23 +4992,13 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" -[[package]] -name = "fancy-regex" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" -dependencies = [ - "bit-set 0.5.3", - "regex", -] - [[package]] name = "fancy-regex" version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" dependencies = [ - "bit-set 0.8.0", + "bit-set", "regex-automata", "regex-syntax", ] @@ -6166,7 +5009,7 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72cf461f865c862bb7dc573f643dd6a2b6842f7c30b07882b56bd148cc2761b8" dependencies = [ - "bit-set 0.8.0", + "bit-set", "regex-automata", "regex-syntax", ] @@ -6303,17 +5146,6 @@ dependencies = [ "flate2", ] -[[package]] -name = "filedescriptor" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" -dependencies = [ - "libc", - "thiserror 1.0.69", - "winapi", -] - [[package]] name = "filetime" version = "0.2.27" @@ -6343,18 +5175,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "finl_unicode" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" - -[[package]] -name = "fixedbitset" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" - [[package]] name = "fixedbitset" version = "0.5.7" @@ -6541,15 +5361,6 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "fs-err" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" -dependencies = [ - "autocfg", -] - [[package]] name = "fs-format" version = "0.1.0" @@ -6617,7 +5428,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" dependencies = [ - "mac 0.1.1", + "mac", "new_debug_unreachable", ] @@ -6739,15 +5550,6 @@ dependencies = [ "slab", ] -[[package]] -name = "fuzzy-matcher" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" -dependencies = [ - "thread_local", -] - [[package]] name = "fxhash" version = "0.2.1" @@ -8703,55 +7505,6 @@ dependencies = [ "utoipa", ] -[[package]] -name = "google-drive" -version = "0.1.0" -dependencies = [ - "hypr-http-utils", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokio", - "urlencoding", -] - -[[package]] -name = "google-mail" -version = "0.1.0" -dependencies = [ - "hypr-http-utils", - "serde", - "serde_json", - "specta", - "thiserror 2.0.18", - "tokio", - "urlencoding", - "utoipa", -] - -[[package]] -name = "governor" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9efcab3c1958580ff1f25a2a41be1668f7603d849bb63af523b208a3cc1223b8" -dependencies = [ - "cfg-if", - "dashmap", - "futures-sink", - "futures-timer", - "futures-util", - "getrandom 0.3.4", - "hashbrown 0.16.1", - "nonzero_ext", - "parking_lot", - "portable-atomic", - "quanta", - "rand 0.9.4", - "smallvec 1.15.1", - "spinning_top", - "web-time", -] - [[package]] name = "granola" version = "0.1.0" @@ -8946,18 +7699,6 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" -[[package]] -name = "hashify" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd1246c0e5493286aeb2dde35b1f4eb9c4ce00e628641210a5e553fc001a1f26" -dependencies = [ - "indexmap 2.14.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "hashlink" version = "0.8.4" @@ -9093,85 +7834,25 @@ dependencies = [ name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hf" -version = "0.1.0" -dependencies = [ - "download-interface", - "hf-hub", - "thiserror 2.0.18", - "tokio", -] - -[[package]] -name = "hf-hub" -version = "0.4.3" -source = "git+https://github.com/huggingface/hf-hub?rev=5510260#5510260d5fd1d93238f8c43272a1a14e6848ec4b" -dependencies = [ - "dirs 6.0.0", - "futures", - "indicatif", - "libc", - "log", - "num_cpus", - "rand 0.9.4", - "reqwest 0.12.28", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokio", - "windows-sys 0.61.2", -] - -[[package]] -name = "hid-host" -version = "0.1.0" -dependencies = [ - "hid-interface", - "hidapi", - "thiserror 2.0.18", - "tracing", -] +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] -name = "hid-interface" -version = "0.1.0" -dependencies = [ - "thiserror 2.0.18", -] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "hidapi" -version = "2.6.5" +name = "hermit-abi" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1b71e1f4791fb9e93b9d7ee03d70b501ab48f6151432fbcadeabc30fe15396e" -dependencies = [ - "cc", - "cfg-if", - "libc", - "pkg-config", - "windows-sys 0.61.2", -] +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hkdf" @@ -9179,7 +7860,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac 0.12.1", + "hmac", ] [[package]] @@ -9191,15 +7872,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "hmac" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" -dependencies = [ - "digest 0.11.2", -] - [[package]] name = "home" version = "0.5.12" @@ -9280,7 +7952,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" dependencies = [ "log", - "mac 0.1.1", + "mac", "markup5ever 0.14.1", "match_token", ] @@ -9368,12 +8040,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" -[[package]] -name = "http-range-header" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" - [[package]] name = "httparse" version = "1.10.1" @@ -9507,7 +8173,6 @@ dependencies = [ "http 1.4.0", "hyper 1.9.0", "hyper-util", - "log", "rustls 0.23.38", "rustls-native-certs 0.8.3", "tokio", @@ -9528,19 +8193,6 @@ dependencies = [ "tokio-io-timeout", ] -[[package]] -name = "hyper-timeout" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" -dependencies = [ - "hyper 1.9.0", - "hyper-util", - "pin-project-lite", - "tokio", - "tower-service", -] - [[package]] name = "hyper-tls" version = "0.6.0" @@ -9970,15 +8622,6 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" -[[package]] -name = "imap-proto" -version = "0.16.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba1f9b30846c3d04371159ef3a0413ce7c1ae0a8c619cd255c60b3d902553f22" -dependencies = [ - "nom 7.1.3", -] - [[package]] name = "imara-diff" version = "0.1.8" @@ -10032,19 +8675,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "indicatif" -version = "0.18.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" -dependencies = [ - "console", - "portable-atomic", - "unicode-width 0.2.2", - "unit-prefix", - "web-time", -] - [[package]] name = "indoc" version = "2.0.7" @@ -10108,19 +8738,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "instability" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" -dependencies = [ - "darling 0.23.0", - "indoc", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "instant" version = "0.1.13" @@ -10187,17 +8804,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "is-terminal" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.61.2", -] - [[package]] name = "is-wsl" version = "0.4.0" @@ -10320,20 +8926,6 @@ dependencies = [ "jiff-tzdb", ] -[[package]] -name = "jina" -version = "0.1.0" -dependencies = [ - "reqwest 0.13.2", - "schemars 1.2.1", - "serde", - "serde_json", - "specta", - "thiserror 2.0.18", - "tokio", - "url", -] - [[package]] name = "jni" version = "0.21.1" @@ -10424,17 +9016,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "json5" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" -dependencies = [ - "pest", - "pest_derive", - "serde", -] - [[package]] name = "jsonptr" version = "0.6.3" @@ -10493,7 +9074,7 @@ dependencies = [ "base64 0.22.1", "ed25519-dalek", "getrandom 0.2.17", - "hmac 0.12.1", + "hmac", "js-sys", "p256 0.13.2", "p384", @@ -10516,17 +9097,6 @@ dependencies = [ "mutate_once", ] -[[package]] -name = "kasuari" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" -dependencies = [ - "hashbrown 0.16.1", - "portable-atomic", - "thiserror 2.0.18", -] - [[package]] name = "keyboard-types" version = "0.7.0" @@ -10683,12 +9253,6 @@ dependencies = [ "smallvec 1.15.1", ] -[[package]] -name = "lab" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" - [[package]] name = "language" version = "0.1.0" @@ -10741,29 +9305,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "lettre" -version = "0.11.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dabda5859ee7c06b995b9d1165aa52c39110e079ef609db97178d86aeb051fa7" -dependencies = [ - "base64 0.22.1", - "email-encoding", - "email_address", - "fastrand", - "httpdate", - "idna", - "mime", - "nom 8.0.0", - "percent-encoding", - "quoted_printable", - "rustls 0.23.38", - "socket2 0.6.3", - "tokio", - "url", - "webpki-roots 1.0.7", -] - [[package]] name = "levenshtein_automata" version = "0.2.1" @@ -10924,7 +9465,7 @@ dependencies = [ "tokio", "tokio-stream", "tokio-util", - "tonic 0.11.0", + "tonic", "tonic-web", "tower 0.4.13", "tower-http 0.4.4", @@ -10953,7 +9494,7 @@ checksum = "d3358538b52cfcf9af4fe7aeb57d6843aafed2e8af80807bd636fd1448e94ea7" dependencies = [ "base64 0.21.7", "bytes", - "prost 0.12.6", + "prost", "serde", ] @@ -11017,13 +9558,13 @@ dependencies = [ "libsql-rusqlite", "libsql-sys", "parking_lot", - "prost 0.12.6", + "prost", "serde", "thiserror 1.0.69", "tokio", "tokio-stream", "tokio-util", - "tonic 0.11.0", + "tonic", "tracing", "uuid", "zerocopy 0.7.35", @@ -11074,29 +9615,6 @@ dependencies = [ "wayland-protocols-wlr", ] -[[package]] -name = "line-clipping" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" -dependencies = [ - "bitflags 2.11.1", -] - -[[package]] -name = "linear" -version = "0.1.0" -dependencies = [ - "hypr-http-utils", - "serde", - "serde_json", - "specta", - "thiserror 2.0.18", - "tokio", - "tracing", - "utoipa", -] - [[package]] name = "linked-hash-map" version = "0.5.6" @@ -11238,33 +9756,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "llm-proxy" -version = "0.1.0" -dependencies = [ - "analytics", - "api-env", - "async-stream", - "axum 0.8.9", - "backon", - "bytes", - "futures-util", - "observability", - "openrouter", - "reqwest 0.13.2", - "sentry", - "serde", - "serde_json", - "strum 0.28.0", - "thiserror 2.0.18", - "tokio", - "tower 0.5.3", - "tracing", - "tracing-subscriber", - "utoipa", - "wiremock", -] - [[package]] name = "llm-types" version = "0.1.0" @@ -11319,30 +9810,6 @@ dependencies = [ "whisper-local-model", ] -[[package]] -name = "local-stt-core" -version = "0.1.0" -dependencies = [ - "local-model", - "serde", - "specta", -] - -[[package]] -name = "local-stt-server" -version = "0.1.0" -dependencies = [ - "axum 0.8.9", - "serde", - "specta", - "tauri-specta", - "tokio", - "tower-http 0.6.8", - "tracing", - "transcribe-cactus", - "transcribe-whisper-local", -] - [[package]] name = "local-waker" version = "0.1.4" @@ -11364,18 +9831,6 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -[[package]] -name = "loops" -version = "0.1.0" -dependencies = [ - "reqwest 0.13.2", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokio", - "url", -] - [[package]] name = "lru" version = "0.12.5" @@ -11385,15 +9840,6 @@ dependencies = [ "hashbrown 0.15.5", ] -[[package]] -name = "lru" -version = "0.16.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" -dependencies = [ - "hashbrown 0.16.1", -] - [[package]] name = "lru-slab" version = "0.1.2" @@ -11427,31 +9873,12 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "mac" -version = "0.1.0" -dependencies = [ - "libc", - "objc2-core-graphics", - "strum 0.28.0", -] - [[package]] name = "mac" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" -[[package]] -name = "mac_address" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" -dependencies = [ - "nix 0.29.0", - "winapi", -] - [[package]] name = "mac_address2" version = "2.0.2" @@ -11503,15 +9930,6 @@ dependencies = [ "core-foundation-sys", ] -[[package]] -name = "mail-parser" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f82a3d6522697593ba4c683e0a6ee5a40fee93bc1a525e3cc6eeb3da11fd8897" -dependencies = [ - "hashify", -] - [[package]] name = "markdown" version = "1.0.0" @@ -11617,16 +10035,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "mcp" -version = "0.1.0" -dependencies = [ - "api-auth", - "askama 0.15.6", - "axum 0.8.9", - "rmcp", -] - [[package]] name = "md-5" version = "0.10.6" @@ -11669,13 +10077,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" dependencies = [ "libc", -] - -[[package]] -name = "memmem" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" +] [[package]] name = "memo-map" @@ -11692,10 +10094,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "memory" -version = "0.1.0" - [[package]] name = "micromap" version = "0.3.0" @@ -11718,17 +10116,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "mini-internal" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8df435db5df1dd82a74f77e3c3addf6ab7665079c31e222a64f34f7475d87e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "minidump-common" version = "0.21.2" @@ -11826,17 +10213,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "miniserde" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c48e83ec09ab8a51d4e6be46608bf2a1293a79e2f7ea60246a2ce50eaef44ba" -dependencies = [ - "itoa", - "mini-internal", - "zmij", -] - [[package]] name = "minisign-verify" version = "0.2.5" @@ -11865,22 +10241,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "mobile-bridge" -version = "0.1.0" -dependencies = [ - "db-app", - "db-core", - "db-execute", - "db-migrate", - "db-reactive", - "serde_json", - "tempfile", - "thiserror 2.0.18", - "tokio", - "uniffi 0.31.1", -] - [[package]] name = "model-downloader" version = "0.1.0" @@ -11981,12 +10341,6 @@ dependencies = [ "windows-sys 0.60.2", ] -[[package]] -name = "multimap" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" - [[package]] name = "murmurhash32" version = "0.3.1" @@ -11999,31 +10353,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13d2233c9842d08cfe13f9eac96e207ca6a2ea10b80259ebe8ad0268be27d2af" -[[package]] -name = "nango" -version = "0.1.0" -dependencies = [ - "hex", - "hmac 0.13.0", - "hypr-http-utils", - "reqwest 0.13.2", - "schemars 1.2.1", - "serde", - "serde_json", - "sha2 0.11.0", - "specta", - "strum 0.28.0", - "thiserror 2.0.18", - "tokio", - "url", -] - -[[package]] -name = "nanohtml2text" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec1bc47d34ae756616f387c11fd0595f86f2cc7e6473bde9e3ded30cb902a1" - [[package]] name = "native-tls" version = "0.2.18" @@ -12105,19 +10434,6 @@ dependencies = [ "memoffset", ] -[[package]] -name = "nix" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" -dependencies = [ - "bitflags 2.11.1", - "cfg-if", - "cfg_aliases 0.2.1", - "libc", - "memoffset", -] - [[package]] name = "nix" version = "0.30.1" @@ -12155,12 +10471,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "nonzero_ext" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" - [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -12214,37 +10524,6 @@ dependencies = [ "swift-rs 1.0.7 (git+https://github.com/yujonglee/swift-rs?rev=41a1605)", ] -[[package]] -name = "notification-macos2" -version = "0.1.0" -dependencies = [ - "block2", - "env_logger", - "log", - "notification-interface", - "objc2", - "objc2-foundation", - "objc2-user-notifications", - "uuid", -] - -[[package]] -name = "notification-worker" -version = "0.1.0" -dependencies = [ - "apalis", - "apalis-cron", - "chrono", - "cron 0.16.0", - "serde", - "serde_json", - "specta", - "tauri-specta", - "thiserror 2.0.18", - "tokio", - "tracing", -] - [[package]] name = "notify" version = "8.2.0" @@ -12885,10 +11164,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" dependencies = [ - "bitflags 2.11.1", - "block2", "objc2", - "objc2-core-location", "objc2-foundation", ] @@ -12927,48 +11203,6 @@ dependencies = [ "tracing-opentelemetry", ] -[[package]] -name = "octocrab" -version = "0.49.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63f6687a23731011d0117f9f4c3cdabaa7b5e42ca671f42b5cc0657c492540e3" -dependencies = [ - "arc-swap", - "async-trait", - "base64 0.22.1", - "bytes", - "cargo_metadata 0.23.1", - "cfg-if", - "chrono", - "either", - "futures", - "futures-util", - "getrandom 0.2.17", - "http 1.4.0", - "http-body 1.0.1", - "http-body-util", - "hyper 1.9.0", - "hyper-rustls 0.27.9", - "hyper-timeout 0.5.2", - "hyper-util", - "jsonwebtoken", - "once_cell", - "percent-encoding", - "pin-project", - "secrecy", - "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "snafu", - "tokio", - "tower 0.5.3", - "tower-http 0.6.8", - "tracing", - "url", - "web-time", -] - [[package]] name = "ogg_next_sys" version = "0.1.4" @@ -13011,12 +11245,6 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - [[package]] name = "open" version = "5.3.3" @@ -13142,18 +11370,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "openstatus" -version = "0.1.0" -dependencies = [ - "reqwest 0.13.2", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokio", - "url", -] - [[package]] name = "opentelemetry" version = "0.30.0" @@ -13168,50 +11384,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "opentelemetry-http" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f6639e842a97dbea8886e3439710ae463120091e2e064518ba8e716e6ac36d" -dependencies = [ - "async-trait", - "bytes", - "http 1.4.0", - "opentelemetry", - "reqwest 0.12.28", -] - -[[package]] -name = "opentelemetry-otlp" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbee664a43e07615731afc539ca60c6d9f1a9425e25ca09c57bc36c87c55852b" -dependencies = [ - "http 1.4.0", - "opentelemetry", - "opentelemetry-http", - "opentelemetry-proto", - "opentelemetry_sdk", - "prost 0.13.5", - "reqwest 0.12.28", - "thiserror 2.0.18", - "tokio", - "tonic 0.13.1", - "tracing", -] - -[[package]] -name = "opentelemetry-proto" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e046fd7660710fe5a05e8748e70d9058dc15c94ba914e7c4faa7c728f0e8ddc" -dependencies = [ - "opentelemetry", - "opentelemetry_sdk", - "prost 0.13.5", - "tonic 0.13.1", -] - [[package]] name = "opentelemetry_sdk" version = "0.30.0" @@ -13234,15 +11406,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" -[[package]] -name = "ordered-float" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" -dependencies = [ - "num-traits", -] - [[package]] name = "ordered-multimap" version = "0.7.3" @@ -13383,17 +11546,6 @@ dependencies = [ "ws-client", ] -[[package]] -name = "owhisper-config" -version = "0.0.1" -dependencies = [ - "config", - "dirs 6.0.0", - "schemars 1.2.1", - "serde", - "thiserror 2.0.18", -] - [[package]] name = "owhisper-interface" version = "0.1.0" @@ -13608,7 +11760,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ "digest 0.10.7", - "hmac 0.12.1", + "hmac", ] [[package]] @@ -13706,23 +11858,13 @@ dependencies = [ "sha2 0.10.9", ] -[[package]] -name = "petgraph" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" -dependencies = [ - "fixedbitset 0.5.7", - "indexmap 2.14.0", -] - [[package]] name = "petgraph" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ - "fixedbitset 0.5.7", + "fixedbitset", "hashbrown 0.15.5", "indexmap 2.14.0", ] @@ -14134,29 +12276,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "poly1305" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" -dependencies = [ - "cpufeatures 0.2.17", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "porkbun" -version = "0.1.0" -dependencies = [ - "reqwest 0.13.2", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokio", - "url", -] - [[package]] name = "port-killer" version = "0.1.0" @@ -14184,27 +12303,6 @@ dependencies = [ "portable-atomic", ] -[[package]] -name = "portable-pty" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4a596a2b3d2752d94f51fac2d4a96737b8705dddd311a32b9af47211f08671e" -dependencies = [ - "anyhow", - "bitflags 1.3.2", - "downcast-rs 1.2.1", - "filedescriptor", - "lazy_static", - "libc", - "log", - "nix 0.28.0", - "serial2", - "shared_library", - "shell-words", - "winapi", - "winreg 0.10.1", -] - [[package]] name = "postcard" version = "1.1.3" @@ -14238,20 +12336,11 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec 0.11.6", -] - -[[package]] -name = "power" -version = "0.1.0" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ - "cidre", - "thiserror 2.0.18", - "windows 0.62.2", + "zerovec 0.11.6", ] [[package]] @@ -14524,20 +12613,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "prometheus" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ca5326d8d0b950a9acd87e6a3f94745394f62e4dae1b1ee22b2bc0c394af43a" -dependencies = [ - "cfg-if", - "fnv", - "lazy_static", - "memchr", - "parking_lot", - "thiserror 2.0.18", -] - [[package]] name = "prost" version = "0.12.6" @@ -14545,37 +12620,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" dependencies = [ "bytes", - "prost-derive 0.12.6", -] - -[[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-build" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" -dependencies = [ - "heck 0.5.0", - "itertools 0.14.0", - "log", - "multimap", - "once_cell", - "petgraph 0.7.1", - "prettyplease", - "prost 0.13.5", - "prost-types", - "regex", - "syn 2.0.117", - "tempfile", + "prost-derive", ] [[package]] @@ -14591,28 +12636,6 @@ dependencies = [ "syn 2.0.117", ] -[[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.117", -] - -[[package]] -name = "prost-types" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" -dependencies = [ - "prost 0.13.5", -] - [[package]] name = "psl-types" version = "2.0.11" @@ -14656,22 +12679,6 @@ version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" -[[package]] -name = "pyannote-cloud" -version = "0.1.0" -dependencies = [ - "chrono", - "progenitor-client 0.13.0", - "progenitor-utils", - "regress", - "reqwest 0.13.2", - "serde", - "serde_json", - "tokio", - "utoipa", - "wiremock", -] - [[package]] name = "pyannote-local" version = "0.1.0" @@ -14694,21 +12701,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edecfcd5d755a5e5d98e24cf43113e7cdaec5a070edd0f6b250c03a573da30fa" -[[package]] -name = "quanta" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" -dependencies = [ - "crossbeam-utils", - "libc", - "once_cell", - "raw-cpuid", - "wasi 0.11.1+wasi-snapshot-preview1", - "web-sys", - "winapi", -] - [[package]] name = "quick-error" version = "2.0.1" @@ -14743,28 +12735,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "quickcheck" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95c589f335db0f6aaa168a7cd27b1fc6920f5e1470c804f814d9cd6e62a0f70b" -dependencies = [ - "env_logger", - "log", - "rand 0.10.1", -] - -[[package]] -name = "quickcheck_macros" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a28b8493dd664c8b171dd944da82d933f7d456b829bfb236738e1fe06c5ba4" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "quinn" version = "0.11.9" @@ -14830,12 +12800,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "quoted_printable" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478e0585659a122aa407eb7e3c0e1fa51b1d8a870038bd29f0cf4a8551eea972" - [[package]] name = "r-efi" version = "5.3.0" @@ -14910,7 +12874,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ - "chacha20 0.10.0", + "chacha20", "getrandom 0.4.2", "rand_core 0.10.1", ] @@ -15025,100 +12989,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "ratatui" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc" -dependencies = [ - "instability", - "ratatui-core", - "ratatui-crossterm", - "ratatui-macros", - "ratatui-termwiz", - "ratatui-widgets", -] - -[[package]] -name = "ratatui-core" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" -dependencies = [ - "bitflags 2.11.1", - "compact_str", - "hashbrown 0.16.1", - "indoc", - "itertools 0.14.0", - "kasuari", - "lru 0.16.4", - "strum 0.27.2", - "thiserror 2.0.18", - "unicode-segmentation", - "unicode-truncate", - "unicode-width 0.2.2", -] - -[[package]] -name = "ratatui-crossterm" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" -dependencies = [ - "cfg-if", - "crossterm", - "instability", - "ratatui-core", -] - -[[package]] -name = "ratatui-macros" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7f1342a13e83e4bb9d0b793d0ea762be633f9582048c892ae9041ef39c936f4" -dependencies = [ - "ratatui-core", - "ratatui-widgets", -] - -[[package]] -name = "ratatui-termwiz" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f76fe0bd0ed4295f0321b1676732e2454024c15a35d01904ddb315afd3d545c" -dependencies = [ - "ratatui-core", - "termwiz", -] - -[[package]] -name = "ratatui-widgets" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" -dependencies = [ - "bitflags 2.11.1", - "hashbrown 0.16.1", - "indoc", - "instability", - "itertools 0.14.0", - "line-clipping", - "ratatui-core", - "strum 0.27.2", - "time", - "unicode-segmentation", - "unicode-width 0.2.2", -] - -[[package]] -name = "raw-cpuid" -version = "11.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" -dependencies = [ - "bitflags 2.11.1", -] - [[package]] name = "raw-window-handle" version = "0.6.2" @@ -15170,17 +13040,6 @@ dependencies = [ "rustfft", ] -[[package]] -name = "recall" -version = "0.1.0" -dependencies = [ - "reqwest 0.13.2", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokio", -] - [[package]] name = "redox_syscall" version = "0.3.5" @@ -15506,7 +13365,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" dependencies = [ "crypto-bigint 0.4.9", - "hmac 0.12.1", + "hmac", "zeroize", ] @@ -15516,7 +13375,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "hmac 0.12.1", + "hmac", "subtle", ] @@ -15578,51 +13437,6 @@ dependencies = [ "portable-atomic-util", ] -[[package]] -name = "rmcp" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67d69668de0b0ccd9cc435f700f3b39a7861863cf37a15e1f304ea78688a4826" -dependencies = [ - "async-trait", - "base64 0.22.1", - "bytes", - "chrono", - "futures", - "http 1.4.0", - "http-body 1.0.1", - "http-body-util", - "pastey", - "pin-project-lite", - "rand 0.10.1", - "rmcp-macros", - "schemars 1.2.1", - "serde", - "serde_json", - "sse-stream", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "tokio-util", - "tower-service", - "tracing", - "url", - "uuid", -] - -[[package]] -name = "rmcp-macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48fdc01c81097b0aed18633e676e269fefa3a78ec1df56b4fe597c1241b92025" -dependencies = [ - "darling 0.23.0", - "proc-macro2", - "quote", - "serde_json", - "syn 2.0.117", -] - [[package]] name = "rodio" version = "0.22.2" @@ -15645,21 +13459,6 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c85cd47a33a4510b1424fe796498e174c6a9cf94e606460ef022a19f3e4ff85e" -[[package]] -name = "ron" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4147b952f3f819eca0e99527022f7d6a8d05f111aeb0a62960c74eb283bec8fc" -dependencies = [ - "bitflags 2.11.1", - "indexmap 2.14.0", - "once_cell", - "serde", - "serde_derive", - "typeid", - "unicode-ident", -] - [[package]] name = "roxmltree" version = "0.20.0" @@ -15791,42 +13590,7 @@ dependencies = [ "num-traits", "realfft", "visibility", - "windowfunctions", -] - -[[package]] -name = "rumqttc" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0feff8d882bff0b2fddaf99355a10336d43dd3ed44204f85ece28cf9626ab519" -dependencies = [ - "bytes", - "fixedbitset 0.5.7", - "flume", - "futures-util", - "log", - "rustls-native-certs 0.8.3", - "rustls-pemfile", - "rustls-webpki 0.102.8", - "thiserror 2.0.18", - "tokio", - "tokio-rustls 0.26.4", - "tokio-stream", - "tokio-util", -] - -[[package]] -name = "rusqlite" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" -dependencies = [ - "bitflags 2.11.1", - "fallible-iterator 0.3.0", - "fallible-streaming-iterator", - "hashlink 0.10.0", - "libsqlite3-sys", - "smallvec 1.15.1", + "windowfunctions", ] [[package]] @@ -15959,7 +13723,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" dependencies = [ "aws-lc-rs", - "log", "once_cell", "ring", "rustls-pki-types", @@ -16347,20 +14110,6 @@ dependencies = [ "libc", ] -[[package]] -name = "segmentation" -version = "0.1.0" -dependencies = [ - "approx", - "criterion", - "data", - "hound", - "onnx", - "serde", - "serde_json", - "thiserror 2.0.18", -] - [[package]] name = "selectors" version = "0.24.0" @@ -16398,12 +14147,6 @@ dependencies = [ "smallvec 1.15.1", ] -[[package]] -name = "self_cell" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" - [[package]] name = "semver" version = "1.0.28" @@ -16429,7 +14172,6 @@ dependencies = [ "sentry-core", "sentry-debug-images", "sentry-panic", - "sentry-tower", "sentry-tracing", "tokio", "ureq", @@ -16517,21 +14259,6 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "sentry-tower" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a303d0127d95ae928a937dcc0886931d28b4186e7338eea7d5786827b69b002" -dependencies = [ - "axum 0.8.9", - "http 1.4.0", - "pin-project", - "sentry-core", - "tower-layer", - "tower-service", - "url", -] - [[package]] name = "sentry-tracing" version = "0.42.0" @@ -16678,7 +14405,6 @@ version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ - "indexmap 2.14.0", "itoa", "memchr", "serde", @@ -16806,17 +14532,6 @@ dependencies = [ "unsafe-libyaml", ] -[[package]] -name = "serial2" -version = "0.2.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcdbc46aa3882ec3d48ec2b5abcb4f0d863a13d7599265f3faa6d851f23c12f3" -dependencies = [ - "cfg-if", - "libc", - "winapi", -] - [[package]] name = "serial_test" version = "3.4.0" @@ -16966,22 +14681,6 @@ dependencies = [ "windows-sys 0.60.2", ] -[[package]] -name = "shared_library" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" -dependencies = [ - "lazy_static", - "libc", -] - -[[package]] -name = "shell-escape" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f" - [[package]] name = "shell-words" version = "1.1.1" @@ -17164,16 +14863,6 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" -[[package]] -name = "slack-web" -version = "0.1.0" -dependencies = [ - "hypr-http-utils", - "serde", - "serde_json", - "thiserror 2.0.18", -] - [[package]] name = "slotmap" version = "1.1.1" @@ -17230,43 +14919,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "smawk" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" - -[[package]] -name = "smol_str" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9676b89cd56310a87b93dec47b11af744f34d5fc9f367b829474eec0a891350d" -dependencies = [ - "borsh", - "serde", -] - -[[package]] -name = "snafu" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e84b3f4eacbf3a1ce05eac6763b4d629d60cbc94d632e4092c54ade71f1e1a2" -dependencies = [ - "snafu-derive", -] - -[[package]] -name = "snafu-derive" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "socket2" version = "0.5.10" @@ -17412,15 +15064,6 @@ dependencies = [ "lock_api", ] -[[package]] -name = "spinning_top" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" -dependencies = [ - "lock_api", -] - [[package]] name = "spki" version = "0.6.0" @@ -17466,7 +15109,7 @@ dependencies = [ "crc", "crossbeam-queue", "either", - "event-listener 5.4.1", + "event-listener", "futures-core", "futures-intrusive", "futures-io", @@ -17477,7 +15120,6 @@ dependencies = [ "log", "memchr", "percent-encoding", - "rustls 0.23.38", "serde", "serde_json", "sha2 0.10.9", @@ -17487,7 +15129,6 @@ dependencies = [ "tokio-stream", "tracing", "url", - "webpki-roots 0.26.11", ] [[package]] @@ -17520,8 +15161,6 @@ dependencies = [ "serde_json", "sha2 0.10.9", "sqlx-core", - "sqlx-mysql", - "sqlx-postgres", "sqlx-sqlite", "syn 2.0.117", "thiserror 2.0.18", @@ -17551,7 +15190,7 @@ dependencies = [ "generic-array", "hex", "hkdf", - "hmac 0.12.1", + "hmac", "itoa", "log", "md-5", @@ -17559,7 +15198,6 @@ dependencies = [ "percent-encoding", "rand 0.8.6", "rsa", - "serde", "sha1", "sha2 0.10.9", "smallvec 1.15.1", @@ -17588,7 +15226,7 @@ dependencies = [ "futures-util", "hex", "hkdf", - "hmac 0.12.1", + "hmac", "home", "itoa", "log", @@ -17630,56 +15268,6 @@ dependencies = [ "url", ] -[[package]] -name = "sse-stream" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c5e6deb40826033bd7b11c7ef25ef71193fabd71f680f40dd16538a2704d2f4" -dependencies = [ - "bytes", - "futures-util", - "http-body 1.0.1", - "http-body-util", - "pin-project-lite", -] - -[[package]] -name = "ssh-cipher" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f" -dependencies = [ - "cipher", - "ssh-encoding", -] - -[[package]] -name = "ssh-encoding" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15" -dependencies = [ - "base64ct", - "pem-rfc7468 0.7.0", - "sha2 0.10.9", -] - -[[package]] -name = "ssh-key" -version = "0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b86f5297f0f04d08cabaa0f6bff7cb6aec4d9c3b49d87990d63da9d9156a8c3" -dependencies = [ - "ed25519-dalek", - "rand_core 0.6.4", - "sha2 0.10.9", - "signature 2.2.0", - "ssh-cipher", - "ssh-encoding", - "subtle", - "zeroize", -] - [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -17705,18 +15293,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "stop-token" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af91f480ee899ab2d9f8435bfdfc14d08a5754bd9d3fef1f1a1c23336aad6c8b" -dependencies = [ - "async-channel 1.9.0", - "cfg-if", - "futures-core", - "pin-project-lite", -] - [[package]] name = "storage" version = "0.1.0" @@ -17958,18 +15534,6 @@ dependencies = [ "wiremock", ] -[[package]] -name = "supabase-storage" -version = "0.1.0" -dependencies = [ - "observability", - "reqwest 0.13.2", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokio", -] - [[package]] name = "supervisor" version = "0.1.0" @@ -18484,7 +16048,7 @@ dependencies = [ "itertools 0.14.0", "levenshtein_automata", "log", - "lru 0.12.5", + "lru", "lz4_flex", "measure_time", "memmap2", @@ -19048,7 +16612,7 @@ dependencies = [ name = "tauri-plugin-deeplink2" version = "0.1.0" dependencies = [ - "askama 0.15.6", + "askama", "axum 0.8.9", "docs", "open", @@ -20032,23 +17596,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "tauri-plugin-webhook" -version = "0.1.0" -dependencies = [ - "serde", - "serde_json", - "specta", - "specta-typescript", - "strum 0.28.0", - "tauri", - "tauri-plugin", - "tauri-specta", - "thiserror 2.0.18", - "utoipa", - "uuid", -] - [[package]] name = "tauri-plugin-window-state" version = "2.4.1" @@ -20186,7 +17733,7 @@ checksum = "219a1f983a2af3653f75b5747f76733b0da7ff03069c7a41901a5eb3ace4557d" dependencies = [ "anyhow", "brotli", - "cargo_metadata 0.19.2", + "cargo_metadata", "ctor", "dunce", "glob", @@ -20234,16 +17781,6 @@ dependencies = [ "swift-rs 1.0.7 (git+https://github.com/yujonglee/swift-rs?rev=41a1605)", ] -[[package]] -name = "teems" -version = "0.1.0" -dependencies = [ - "hypr-http-utils", - "serde", - "serde_json", - "thiserror 2.0.18", -] - [[package]] name = "tempfile" version = "3.27.0" @@ -20261,7 +17798,7 @@ dependencies = [ name = "template-app" version = "0.1.0" dependencies = [ - "askama 0.15.6", + "askama", "askama-utils", "insta", "serde", @@ -20290,18 +17827,11 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "template-cli" -version = "0.1.0" -dependencies = [ - "askama 0.15.6", -] - [[package]] name = "template-eval" version = "0.1.0" dependencies = [ - "askama 0.15.6", + "askama", "askama-utils", "insta", "libtest-mimic", @@ -20318,7 +17848,7 @@ dependencies = [ name = "template-support" version = "0.1.0" dependencies = [ - "askama 0.15.6", + "askama", "serde", "specta", "utoipa", @@ -20331,7 +17861,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" dependencies = [ "futf", - "mac 0.1.1", + "mac", "utf-8", ] @@ -20367,85 +17897,12 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "terminal_size" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" -dependencies = [ - "rustix 1.1.4", - "windows-sys 0.61.2", -] - -[[package]] -name = "terminfo" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" -dependencies = [ - "fnv", - "nom 7.1.3", - "phf 0.11.3", - "phf_codegen 0.11.3", -] - -[[package]] -name = "termios" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" -dependencies = [ - "libc", -] - [[package]] name = "termtree" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" -[[package]] -name = "termwiz" -version = "0.23.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" -dependencies = [ - "anyhow", - "base64 0.22.1", - "bitflags 2.11.1", - "fancy-regex 0.11.0", - "filedescriptor", - "finl_unicode", - "fixedbitset 0.4.2", - "hex", - "lazy_static", - "libc", - "log", - "memmem", - "nix 0.29.0", - "num-derive", - "num-traits", - "ordered-float", - "pest", - "pest_derive", - "phf 0.11.3", - "sha2 0.10.9", - "signal-hook 0.3.18", - "siphasher 1.0.2", - "terminfo", - "termios", - "thiserror 1.0.69", - "ucd-trie", - "unicode-segmentation", - "vtparse", - "wezterm-bidi", - "wezterm-blob-leases", - "wezterm-color-types", - "wezterm-dynamic", - "wezterm-input-types", - "winapi", -] - [[package]] name = "testcontainers" version = "0.24.0" @@ -20484,18 +17941,6 @@ dependencies = [ "testcontainers", ] -[[package]] -name = "textwrap" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" -dependencies = [ - "smawk", - "terminal_size", - "unicode-linebreak", - "unicode-width 0.2.2", -] - [[package]] name = "thin-vec" version = "0.2.16" @@ -20776,24 +18221,12 @@ dependencies = [ ] [[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls 0.23.38", - "tokio", -] - -[[package]] -name = "tokio-socks" -version = "0.5.2" +name = "tokio-rustls" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "either", - "futures-util", - "thiserror 1.0.69", + "rustls 0.23.38", "tokio", ] @@ -20806,7 +18239,6 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", - "tokio-util", ] [[package]] @@ -20849,13 +18281,9 @@ dependencies = [ "futures-util", "log", "native-tls", - "rustls 0.23.38", - "rustls-pki-types", "tokio", "tokio-native-tls", - "tokio-rustls 0.26.4", "tungstenite 0.29.0", - "webpki-roots 0.26.11", ] [[package]] @@ -20895,15 +18323,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - [[package]] name = "toml" version = "0.8.2" @@ -21006,7 +18425,6 @@ dependencies = [ "indexmap 2.14.0", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "toml_writer", "winnow 1.0.1", ] @@ -21040,10 +18458,10 @@ dependencies = [ "http 0.2.12", "http-body 0.4.6", "hyper 0.14.32", - "hyper-timeout 0.4.1", + "hyper-timeout", "percent-encoding", "pin-project", - "prost 0.12.6", + "prost", "tokio", "tokio-stream", "tower 0.4.13", @@ -21052,32 +18470,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "tonic" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e581ba15a835f4d9ea06c55ab1bd4dce26fc53752c69a04aac00703bfb49ba9" -dependencies = [ - "async-trait", - "base64 0.22.1", - "bytes", - "http 1.4.0", - "http-body 1.0.1", - "http-body-util", - "hyper 1.9.0", - "hyper-timeout 0.5.2", - "hyper-util", - "percent-encoding", - "pin-project", - "prost 0.13.5", - "tokio", - "tokio-stream", - "tower 0.5.3", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tonic-web" version = "0.11.0" @@ -21091,7 +18483,7 @@ dependencies = [ "hyper 0.14.32", "pin-project", "tokio-stream", - "tonic 0.11.0", + "tonic", "tower-http 0.4.4", "tower-layer", "tower-service", @@ -21126,9 +18518,7 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", - "indexmap 2.14.0", "pin-project-lite", - "slab", "sync_wrapper 1.0.2", "tokio", "tokio-util", @@ -21149,7 +18539,7 @@ dependencies = [ "futures-util", "http 0.2.12", "http-body 0.4.6", - "http-range-header 0.3.1", + "http-range-header", "pin-project-lite", "tower 0.4.13", "tower-layer", @@ -21171,12 +18561,7 @@ dependencies = [ "http 1.4.0", "http-body 1.0.1", "http-body-util", - "http-range-header 0.4.2", - "httpdate", "iri-string", - "mime", - "mime_guess", - "percent-encoding", "pin-project-lite", "tokio", "tokio-util", @@ -21184,7 +18569,6 @@ dependencies = [ "tower-layer", "tower-service", "tracing", - "uuid", ] [[package]] @@ -21284,16 +18668,6 @@ dependencies = [ "web-time", ] -[[package]] -name = "tracing-serde" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" -dependencies = [ - "serde", - "tracing-core", -] - [[package]] name = "tracing-subscriber" version = "0.3.23" @@ -21305,15 +18679,12 @@ dependencies = [ "nu-ansi-term", "once_cell", "regex-automata", - "serde", - "serde_json", "sharded-slab", "smallvec 1.15.1", "thread_local", "tracing", "tracing-core", "tracing-log", - "tracing-serde", ] [[package]] @@ -21372,52 +18743,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "transcribe-proxy" -version = "0.1.0" -dependencies = [ - "analytics", - "api-auth", - "api-env", - "audio-mime", - "audio-utils", - "axum 0.8.9", - "backon", - "base64 0.22.1", - "bytes", - "codes-iso-639", - "data", - "futures-util", - "insta", - "itertools 0.14.0", - "language", - "observability", - "owhisper-client", - "owhisper-interface", - "quickcheck", - "quickcheck_macros", - "reqwest 0.13.2", - "reqwest-middleware", - "rodio", - "sentry", - "serde", - "serde_html_form 0.4.0", - "serde_json", - "supabase-storage", - "tempfile", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "tokio-tungstenite 0.29.0", - "tower 0.5.3", - "tracing", - "tracing-subscriber", - "url", - "urlencoding", - "utoipa", - "uuid", -] - [[package]] name = "transcribe-whisper-local" version = "0.1.0" @@ -21513,7 +18838,7 @@ checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6" dependencies = [ "memchr", "nom 8.0.0", - "petgraph 0.8.3", + "petgraph", ] [[package]] @@ -21593,8 +18918,6 @@ dependencies = [ "log", "native-tls", "rand 0.9.4", - "rustls 0.23.38", - "rustls-pki-types", "sha1", "thiserror 2.0.18", ] @@ -21616,12 +18939,6 @@ version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" -[[package]] -name = "typed-path" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" - [[package]] name = "typeid" version = "1.0.3" @@ -22135,375 +19452,95 @@ checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" [[package]] name = "unicode-bom" version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" - -[[package]] -name = "unicode-ccc" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" - -[[package]] -name = "unicode-general-category" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" - -[[package]] -name = "unicode-id" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ba288e709927c043cbe476718d37be306be53fb1fafecd0dbe36d072be2580" - -[[package]] -name = "unicode-id-start" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81b79ad29b5e19de4260020f8919b443b2ef0277d242ce532ec7b7a2cc8b6007" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-linebreak" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" - -[[package]] -name = "unicode-math-class" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d246cf599d5fae3c8d56e04b20eb519adb89a8af8d0b0fbcded369aa3647d65" - -[[package]] -name = "unicode-normalization" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-properties" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" - -[[package]] -name = "unicode-script" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" - -[[package]] -name = "unicode-segmentation" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" - -[[package]] -name = "unicode-truncate" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" -dependencies = [ - "itertools 0.14.0", - "unicode-segmentation", - "unicode-width 0.2.2", -] - -[[package]] -name = "unicode-vo" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" - -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "uniffi" -version = "0.29.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3291800a6b06569f7d3e15bdb6dc235e0f0c8bd3eb07177f430057feb076415f" -dependencies = [ - "anyhow", - "camino", - "cargo_metadata 0.19.2", - "clap", - "uniffi_bindgen 0.29.5", - "uniffi_core 0.29.5", - "uniffi_macros 0.29.5", - "uniffi_pipeline 0.29.5", -] - -[[package]] -name = "uniffi" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc5f2297ee5b893405bed1a6929faec4713a061df158ecf5198089f23910d470" -dependencies = [ - "anyhow", - "cargo_metadata 0.19.2", - "uniffi_bindgen 0.31.1", - "uniffi_core 0.31.1", - "uniffi_macros 0.31.1", - "uniffi_pipeline 0.31.1", -] - -[[package]] -name = "uniffi-bindgen" -version = "0.1.0" -dependencies = [ - "uniffi 0.29.5", -] - -[[package]] -name = "uniffi_bindgen" -version = "0.29.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a04b99fa7796eaaa7b87976a0dbdd1178dc1ee702ea00aca2642003aef9b669e" -dependencies = [ - "anyhow", - "askama 0.13.1", - "camino", - "cargo_metadata 0.19.2", - "fs-err", - "glob", - "goblin", - "heck 0.5.0", - "indexmap 2.14.0", - "once_cell", - "serde", - "tempfile", - "textwrap", - "toml 0.5.11", - "uniffi_internal_macros 0.29.5", - "uniffi_meta 0.29.5", - "uniffi_pipeline 0.29.5", - "uniffi_udl 0.29.5", -] - -[[package]] -name = "uniffi_bindgen" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bc0c60a9607e7ab77a2ad47ec5530178015014839db25af7512447d2238016c" -dependencies = [ - "anyhow", - "askama 0.14.0", - "camino", - "cargo_metadata 0.19.2", - "fs-err", - "glob", - "goblin", - "heck 0.5.0", - "indexmap 2.14.0", - "once_cell", - "serde", - "tempfile", - "textwrap", - "toml 0.9.12+spec-1.1.0", - "uniffi_internal_macros 0.31.1", - "uniffi_meta 0.31.1", - "uniffi_pipeline 0.31.1", - "uniffi_udl 0.31.1", -] +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" [[package]] -name = "uniffi_core" -version = "0.29.5" +name = "unicode-ccc" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38a9a27529ccff732f8efddb831b65b1e07f7dea3fd4cacd4a35a8c4b253b98" -dependencies = [ - "anyhow", - "bytes", - "once_cell", - "static_assertions", -] +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" [[package]] -name = "uniffi_core" -version = "0.31.1" +name = "unicode-general-category" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77baf5d539fe2e1ad6805e942dbc5dbdeb2b83eb5f2b3a6535d422ca4b02a12f" -dependencies = [ - "anyhow", - "bytes", - "once_cell", - "static_assertions", -] +checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" [[package]] -name = "uniffi_internal_macros" -version = "0.29.5" +name = "unicode-id" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09acd2ce09c777dd65ee97c251d33c8a972afc04873f1e3b21eb3492ade16933" -dependencies = [ - "anyhow", - "indexmap 2.14.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] +checksum = "70ba288e709927c043cbe476718d37be306be53fb1fafecd0dbe36d072be2580" [[package]] -name = "uniffi_internal_macros" -version = "0.31.1" +name = "unicode-id-start" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4b42137524f4be6400fcaca9d02c1d4ecb6ad917e4013c0b93235526d8396e5" -dependencies = [ - "anyhow", - "indexmap 2.14.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] +checksum = "81b79ad29b5e19de4260020f8919b443b2ef0277d242ce532ec7b7a2cc8b6007" [[package]] -name = "uniffi_macros" -version = "0.29.5" +name = "unicode-ident" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5596f178c4f7aafa1a501c4e0b96236a96bc2ef92bdb453d83e609dad0040152" -dependencies = [ - "camino", - "fs-err", - "once_cell", - "proc-macro2", - "quote", - "serde", - "syn 2.0.117", - "toml 0.5.11", - "uniffi_meta 0.29.5", -] +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] -name = "uniffi_macros" -version = "0.31.1" +name = "unicode-math-class" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9273ec45330d8fe9a3701b7b983cea7a4e218503359831967cb95d26b873561" -dependencies = [ - "camino", - "fs-err", - "once_cell", - "proc-macro2", - "quote", - "serde", - "syn 2.0.117", - "toml 0.9.12+spec-1.1.0", - "uniffi_meta 0.31.1", -] +checksum = "7d246cf599d5fae3c8d56e04b20eb519adb89a8af8d0b0fbcded369aa3647d65" [[package]] -name = "uniffi_meta" -version = "0.29.5" +name = "unicode-normalization" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beadc1f460eb2e209263c49c4f5b19e9a02e00a3b2b393f78ad10d766346ecff" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ - "anyhow", - "siphasher 0.3.11", - "uniffi_internal_macros 0.29.5", - "uniffi_pipeline 0.29.5", + "tinyvec", ] [[package]] -name = "uniffi_meta" -version = "0.31.1" +name = "unicode-properties" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "431d2f443e7828a6c29d188de98b6771a6491ee98bba2d4372643bf93f988a18" -dependencies = [ - "anyhow", - "siphasher 1.0.2", - "uniffi_internal_macros 0.31.1", - "uniffi_pipeline 0.31.1", -] +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] -name = "uniffi_pipeline" -version = "0.29.5" +name = "unicode-script" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd76b3ac8a2d964ca9fce7df21c755afb4c77b054a85ad7a029ad179cc5abb8a" -dependencies = [ - "anyhow", - "heck 0.5.0", - "indexmap 2.14.0", - "tempfile", - "uniffi_internal_macros 0.29.5", -] +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" [[package]] -name = "uniffi_pipeline" -version = "0.31.1" +name = "unicode-segmentation" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "761ef74f6175e15603d0424cc5f98854c5baccfe7bf4ccb08e5816f9ab8af689" -dependencies = [ - "anyhow", - "heck 0.5.0", - "indexmap 2.14.0", - "tempfile", - "uniffi_internal_macros 0.31.1", -] +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] -name = "uniffi_udl" -version = "0.29.5" +name = "unicode-vo" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4319cf905911d70d5b97ce0f46f101619a22e9a189c8c46d797a9955e9233716" -dependencies = [ - "anyhow", - "textwrap", - "uniffi_meta 0.29.5", - "weedle2", -] +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" [[package]] -name = "uniffi_udl" -version = "0.31.1" +name = "unicode-width" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68773ec0e1c067b6505a73bbf6a5782f31a7f9209333a0df97b87565c46bf370" -dependencies = [ - "anyhow", - "textwrap", - "uniffi_meta 0.31.1", - "weedle2", -] +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] -name = "unit-prefix" -version = "0.5.2" +name = "unicode-width" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] -name = "universal-hash" -version = "0.5.1" +name = "unicode-xid" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" -dependencies = [ - "crypto-common 0.1.6", - "subtle", -] +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "unsafe-libyaml" @@ -22523,24 +19560,6 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" -[[package]] -name = "updater-core" -version = "0.1.0" -dependencies = [ - "base64 0.22.1", - "futures-util", - "http 1.4.0", - "minisign-verify", - "percent-encoding", - "reqwest 0.13.2", - "semver", - "serde", - "serde_json", - "thiserror 2.0.18", - "time", - "url", -] - [[package]] name = "ureq" version = "3.3.0" @@ -22689,7 +19708,6 @@ version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ - "atomic", "getrandom 0.4.2", "js-sys", "serde_core", @@ -22786,13 +19804,6 @@ dependencies = [ "rustversion", ] -[[package]] -name = "version" -version = "0.1.0" -dependencies = [ - "semver", -] - [[package]] name = "version-compare" version = "0.2.1" @@ -22856,48 +19867,6 @@ dependencies = [ "libc", ] -[[package]] -name = "vt100" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84cd863bf0db7e392ba3bd04994be3473491b31e66340672af5d11943c6274de" -dependencies = [ - "itoa", - "log", - "unicode-width 0.1.14", - "vte", -] - -[[package]] -name = "vte" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" -dependencies = [ - "arrayvec", - "utf8parse", - "vte_generate_state_changes", -] - -[[package]] -name = "vte_generate_state_changes" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e369bee1b05d510a7b4ed645f5faa90619e05437111783ea5848f28d97d3c2e" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "vtparse" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" -dependencies = [ - "utf8parse", -] - [[package]] name = "walkdir" version = "2.5.0" @@ -23232,7 +20201,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", - "serde", "wasm-bindgen", ] @@ -23355,93 +20323,12 @@ dependencies = [ "windows-core 0.61.2", ] -[[package]] -name = "weedle2" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e" -dependencies = [ - "nom 7.1.3", -] - [[package]] name = "weezl" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" -[[package]] -name = "wezterm-bidi" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" -dependencies = [ - "log", - "wezterm-dynamic", -] - -[[package]] -name = "wezterm-blob-leases" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" -dependencies = [ - "getrandom 0.3.4", - "mac_address", - "sha2 0.10.9", - "thiserror 1.0.69", - "uuid", -] - -[[package]] -name = "wezterm-color-types" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" -dependencies = [ - "csscolorparser", - "deltae", - "lazy_static", - "wezterm-dynamic", -] - -[[package]] -name = "wezterm-dynamic" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" -dependencies = [ - "log", - "ordered-float", - "strsim", - "thiserror 1.0.69", - "wezterm-dynamic-derive", -] - -[[package]] -name = "wezterm-dynamic-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "wezterm-input-types" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" -dependencies = [ - "bitflags 1.3.2", - "euclid", - "lazy_static", - "serde", - "wezterm-dynamic", -] - [[package]] name = "which" version = "4.4.2" @@ -23454,15 +20341,6 @@ dependencies = [ "rustix 0.38.44", ] -[[package]] -name = "which" -version = "8.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81995fafaaaf6ae47a7d0cc83c67caf92aeb7e5331650ae6ff856f7c0c60c459" -dependencies = [ - "libc", -] - [[package]] name = "whichlang" version = "0.1.1" @@ -23803,23 +20681,8 @@ name = "windows-strings" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-sys" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", +dependencies = [ + "windows-link 0.2.1", ] [[package]] @@ -24089,15 +20952,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winnow" -version = "0.6.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28" -dependencies = [ - "memchr", -] - [[package]] name = "winnow" version = "0.7.15" @@ -24477,558 +21331,148 @@ dependencies = [ "markup5ever 0.38.0", ] -[[package]] -name = "xmlparser" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" - -[[package]] -name = "xmlwriter" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" - -[[package]] -name = "xmp-writer" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce9e2f4a404d9ebffc0a9832cf4f50907220ba3d7fffa9099261a5cab52f2dd7" - -[[package]] -name = "xshell" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e7290c623014758632efe00737145b6867b66292c42167f2ec381eb566a373d" -dependencies = [ - "xshell-macros", -] - -[[package]] -name = "xshell-macros" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32ac00cd3f8ec9c1d33fb3e7958a82df6989c42d747bd326c822b1d625283547" - -[[package]] -name = "xtask" -version = "0.1.0" -dependencies = [ - "anyhow", - "pathdiff", - "tempfile", - "toml 0.8.2", - "xshell", -] - -[[package]] -name = "xz2" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" -dependencies = [ - "lzma-sys", -] - -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] - -[[package]] -name = "yaml-rust2" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9" -dependencies = [ - "arraydeque", - "encoding_rs", - "hashlink 0.10.0", -] - -[[package]] -name = "yoke" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive 0.7.5", - "zerofrom", -] - -[[package]] -name = "yoke" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" -dependencies = [ - "stable_deref_trait", - "yoke-derive 0.8.2", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "zbus" -version = "5.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" -dependencies = [ - "async-broadcast", - "async-executor", - "async-io", - "async-lock", - "async-process", - "async-recursion", - "async-task", - "async-trait", - "blocking", - "enumflags2", - "event-listener 5.4.1", - "futures-core", - "futures-lite", - "hex", - "libc", - "ordered-stream", - "rustix 1.1.4", - "serde", - "serde_repr", - "tracing", - "uds_windows", - "uuid", - "windows-sys 0.61.2", - "winnow 0.7.15", - "zbus_macros", - "zbus_names", - "zvariant", -] - -[[package]] -name = "zbus_macros" -version = "5.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" -dependencies = [ - "proc-macro-crate 3.5.0", - "proc-macro2", - "quote", - "syn 2.0.117", - "zbus_names", - "zvariant", - "zvariant_utils", -] - -[[package]] -name = "zbus_names" -version = "4.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" -dependencies = [ - "serde", - "winnow 0.7.15", - "zvariant", -] - -[[package]] -name = "zeroclaw-api" -version = "0.7.0" -source = "git+https://github.com/zeroclaw-labs/zeroclaw.git?rev=30395e909f660c9a6ec3075df1f70d84077a96d0#30395e909f660c9a6ec3075df1f70d84077a96d0" -dependencies = [ - "anyhow", - "async-trait", - "futures-util", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "zeroclaw-channels" -version = "0.7.0" -source = "git+https://github.com/zeroclaw-labs/zeroclaw.git?rev=30395e909f660c9a6ec3075df1f70d84077a96d0#30395e909f660c9a6ec3075df1f70d84077a96d0" -dependencies = [ - "anyhow", - "async-imap", - "async-trait", - "axum 0.8.9", - "base64 0.22.1", - "chrono", - "directories", - "futures-util", - "hex", - "hmac 0.12.1", - "image", - "lettre", - "lru 0.16.4", - "mail-parser", - "nanohtml2text", - "parking_lot", - "portable-atomic", - "rand 0.10.1", - "regex", - "reqwest 0.12.28", - "rumqttc", - "rusqlite", - "rustls 0.23.38", - "rustls-pki-types", - "serde", - "serde_json", - "sha2 0.10.9", - "shellexpand", - "tokio", - "tokio-rustls 0.26.4", - "tokio-socks", - "tokio-tungstenite 0.29.0", - "tokio-util", - "toml 1.1.2+spec-1.1.0", - "tracing", - "urlencoding", - "uuid", - "webpki-roots 1.0.7", - "zeroclaw-api", - "zeroclaw-config", - "zeroclaw-infra", - "zeroclaw-memory", - "zeroclaw-providers", - "zeroclaw-runtime", - "zeroclaw-tools", -] - -[[package]] -name = "zeroclaw-config" -version = "0.7.0" -source = "git+https://github.com/zeroclaw-labs/zeroclaw.git?rev=30395e909f660c9a6ec3075df1f70d84077a96d0#30395e909f660c9a6ec3075df1f70d84077a96d0" -dependencies = [ - "anyhow", - "chacha20poly1305", - "chrono", - "directories", - "hex", - "hostname", - "parking_lot", - "rand 0.10.1", - "regex", - "reqwest 0.12.28", - "rustls 0.23.38", - "rustls-pki-types", - "schemars 1.2.1", - "serde", - "serde_json", - "sha2 0.10.9", - "shellexpand", - "thiserror 2.0.18", - "tokio", - "tokio-rustls 0.26.4", - "tokio-socks", - "tokio-stream", - "tokio-tungstenite 0.29.0", - "toml 1.1.2+spec-1.1.0", - "toml_edit 0.25.11+spec-1.1.0", - "tracing", - "url", - "uuid", - "webpki-roots 1.0.7", - "zeroclaw-api", - "zeroclaw-macros", -] +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] -name = "zeroclaw-gateway" -version = "0.7.0" -source = "git+https://github.com/zeroclaw-labs/zeroclaw.git?rev=30395e909f660c9a6ec3075df1f70d84077a96d0#30395e909f660c9a6ec3075df1f70d84077a96d0" +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + +[[package]] +name = "xmp-writer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce9e2f4a404d9ebffc0a9832cf4f50907220ba3d7fffa9099261a5cab52f2dd7" + +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" dependencies = [ - "anyhow", - "async-trait", - "axum 0.8.9", - "chrono", - "directories", - "futures-util", - "hex", - "hmac 0.12.1", - "http-body-util", - "hyper 1.9.0", - "hyper-util", - "mime_guess", - "parking_lot", - "rusqlite", - "rustls 0.23.38", - "rustls-pemfile", - "serde", - "serde_json", - "sha2 0.10.9", - "tokio", - "tokio-rustls 0.26.4", - "tokio-stream", - "toml 1.1.2+spec-1.1.0", - "tower 0.5.3", - "tower-http 0.6.8", - "tracing", - "uuid", - "zeroclaw-api", - "zeroclaw-channels", - "zeroclaw-config", - "zeroclaw-hardware", - "zeroclaw-infra", - "zeroclaw-memory", - "zeroclaw-providers", - "zeroclaw-runtime", - "zeroclaw-tools", + "lzma-sys", ] [[package]] -name = "zeroclaw-hardware" -version = "0.7.0" -source = "git+https://github.com/zeroclaw-labs/zeroclaw.git?rev=30395e909f660c9a6ec3075df1f70d84077a96d0#30395e909f660c9a6ec3075df1f70d84077a96d0" +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" dependencies = [ - "aardvark-sys", - "anyhow", - "async-trait", - "directories", - "glob", - "portable-atomic", - "reqwest 0.12.28", - "serde", - "serde_json", - "tempfile", - "thiserror 2.0.18", - "tokio", - "toml 1.1.2+spec-1.1.0", - "tracing", - "uuid", - "zeroclaw-api", - "zeroclaw-config", - "zeroclaw-tools", + "linked-hash-map", ] [[package]] -name = "zeroclaw-infra" -version = "0.7.0" -source = "git+https://github.com/zeroclaw-labs/zeroclaw.git?rev=30395e909f660c9a6ec3075df1f70d84077a96d0#30395e909f660c9a6ec3075df1f70d84077a96d0" +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ - "anyhow", - "chrono", - "parking_lot", - "portable-atomic", - "rusqlite", "serde", - "serde_json", - "tokio", - "tracing", - "zeroclaw-api", + "stable_deref_trait", + "yoke-derive 0.7.5", + "zerofrom", ] [[package]] -name = "zeroclaw-macros" -version = "0.7.0" -source = "git+https://github.com/zeroclaw-labs/zeroclaw.git?rev=30395e909f660c9a6ec3075df1f70d84077a96d0#30395e909f660c9a6ec3075df1f70d84077a96d0" +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "stable_deref_trait", + "yoke-derive 0.8.2", + "zerofrom", ] [[package]] -name = "zeroclaw-memory" -version = "0.7.0" -source = "git+https://github.com/zeroclaw-labs/zeroclaw.git?rev=30395e909f660c9a6ec3075df1f70d84077a96d0#30395e909f660c9a6ec3075df1f70d84077a96d0" +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ - "anyhow", - "async-trait", - "chrono", - "parking_lot", - "regex", - "reqwest 0.12.28", - "rusqlite", - "serde", - "serde_json", - "sha2 0.10.9", - "tokio", - "tracing", - "uuid", - "zeroclaw-api", - "zeroclaw-config", + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", ] [[package]] -name = "zeroclaw-providers" -version = "0.7.0" -source = "git+https://github.com/zeroclaw-labs/zeroclaw.git?rev=30395e909f660c9a6ec3075df1f70d84077a96d0#30395e909f660c9a6ec3075df1f70d84077a96d0" +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ - "anyhow", - "async-trait", - "base64 0.22.1", - "chacha20poly1305", - "chrono", - "directories", - "futures-util", - "hex", - "hmac 0.12.1", - "parking_lot", - "rand 0.10.1", - "regex", - "reqwest 0.12.28", - "ring", - "serde", - "serde_json", - "sha2 0.10.9", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "tokio-util", - "tracing", - "uuid", - "zeroclaw-api", - "zeroclaw-config", + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", ] [[package]] -name = "zeroclaw-runtime" -version = "0.7.0" -source = "git+https://github.com/zeroclaw-labs/zeroclaw.git?rev=30395e909f660c9a6ec3075df1f70d84077a96d0#30395e909f660c9a6ec3075df1f70d84077a96d0" +name = "zbus" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" dependencies = [ - "aardvark-sys", - "anyhow", + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", "async-trait", - "base64 0.22.1", - "chacha20poly1305", - "chrono", - "chrono-tz 0.10.4", - "console", - "cron 0.15.0", - "dialoguer", - "directories", - "flate2", - "futures-util", - "glob", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", "hex", - "hmac 0.12.1", - "hostname", - "image", - "indicatif", "libc", - "lru 0.16.4", - "nanohtml2text", - "parking_lot", - "portable-atomic", - "prometheus", - "rand 0.10.1", - "regex", - "reqwest 0.12.28", - "ring", - "rumqttc", - "rusqlite", - "rustls 0.23.38", - "rustls-pemfile", - "rustls-pki-types", - "schemars 1.2.1", + "ordered-stream", + "rustix 1.1.4", "serde", - "serde_json", - "sha2 0.10.9", - "shellexpand", - "tar", - "tempfile", - "thiserror 2.0.18", - "tokio", - "tokio-rustls 0.26.4", - "tokio-stream", - "tokio-tungstenite 0.29.0", - "tokio-util", - "toml 1.1.2+spec-1.1.0", + "serde_repr", "tracing", - "urlencoding", + "uds_windows", "uuid", - "webpki-roots 1.0.7", - "which 8.0.2", - "zeroclaw-api", - "zeroclaw-config", - "zeroclaw-infra", - "zeroclaw-macros", - "zeroclaw-memory", - "zeroclaw-providers", - "zeroclaw-tool-call-parser", - "zeroclaw-tools", - "zip 8.5.1", + "windows-sys 0.61.2", + "winnow 0.7.15", + "zbus_macros", + "zbus_names", + "zvariant", ] [[package]] -name = "zeroclaw-tool-call-parser" -version = "0.7.0" -source = "git+https://github.com/zeroclaw-labs/zeroclaw.git?rev=30395e909f660c9a6ec3075df1f70d84077a96d0#30395e909f660c9a6ec3075df1f70d84077a96d0" +name = "zbus_macros" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" dependencies = [ - "regex", - "serde", - "serde_json", - "tracing", + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "zbus_names", + "zvariant", + "zvariant_utils", ] [[package]] -name = "zeroclaw-tools" -version = "0.7.0" -source = "git+https://github.com/zeroclaw-labs/zeroclaw.git?rev=30395e909f660c9a6ec3075df1f70d84077a96d0#30395e909f660c9a6ec3075df1f70d84077a96d0" +name = "zbus_names" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" dependencies = [ - "anyhow", - "async-trait", - "base64 0.22.1", - "chrono", - "directories", - "futures-util", - "glob", - "hex", - "nanohtml2text", - "parking_lot", - "regex", - "reqwest 0.12.28", "serde", - "serde_json", - "sha2 0.10.9", - "tempfile", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "tokio-tungstenite 0.29.0", - "tokio-util", - "toml 1.1.2+spec-1.1.0", - "tracing", - "urlencoding", - "uuid", - "which 8.0.2", - "zeroclaw-api", - "zeroclaw-config", - "zeroclaw-infra", - "zeroclaw-memory", - "zeroclaw-providers", + "winnow 0.7.15", + "zvariant", ] [[package]] @@ -25198,7 +21642,7 @@ dependencies = [ "displaydoc", "flate2", "getrandom 0.3.4", - "hmac 0.12.1", + "hmac", "indexmap 2.14.0", "lzma-rs", "memchr", @@ -25224,19 +21668,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "zip" -version = "8.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcab981e19633ebcf0b001ddd37dd802996098bc1864f90b7c5d970ce76c1d59" -dependencies = [ - "crc32fast", - "flate2", - "indexmap 2.14.0", - "memchr", - "typed-path", -] - [[package]] name = "zlib-rs" version = "0.5.5" diff --git a/Cargo.toml b/Cargo.toml index 843c2eb191..42b726659d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,6 @@ debug = false [workspace] resolver = "2" members = [ - "apps/api", - "apps/cli", "apps/desktop/src-tauri", "crates/*", "legacy/*", @@ -31,32 +29,11 @@ hypr-activity-capture-macos = { path = "crates/activity-capture-macos", package hypr-activity-capture-windows = { path = "crates/activity-capture-windows", package = "activity-capture-windows" } hypr-aec = { path = "crates/aec", package = "aec" } hypr-afconvert = { path = "crates/afconvert", package = "afconvert" } -hypr-agc = { path = "crates/agc", package = "agc" } hypr-agent-core = { path = "crates/agent-core", package = "agent-core" } hypr-am = { path = "crates/am", package = "am" } -hypr-amp = { path = "crates/amp", package = "amp" } hypr-analytics = { path = "crates/analytics", package = "analytics" } -hypr-api-agent = { path = "crates/api-agent", package = "api-agent" } -hypr-api-auth = { path = "crates/api-auth", package = "api-auth" } -hypr-api-bot = { path = "crates/api-bot", package = "api-bot" } -hypr-api-cactus = { path = "crates/api-cactus", package = "api-cactus" } -hypr-api-calendar = { path = "crates/api-calendar", package = "api-calendar" } -hypr-api-claw = { path = "crates/api-claw", package = "api-claw" } hypr-api-client = { path = "crates/api-client", package = "api-client" } -hypr-api-env = { path = "crates/api-env", package = "api-env" } -hypr-api-error = { path = "crates/api-error", package = "api-error" } -hypr-api-mail = { path = "crates/api-mail", package = "api-mail" } -hypr-api-messenger = { path = "crates/api-messenger", package = "api-messenger" } -hypr-api-nango = { path = "crates/api-nango", package = "api-nango" } -hypr-api-pyannote = { path = "crates/api-pyannote", package = "api-pyannote" } -hypr-api-research = { path = "crates/api-research", package = "api-research" } -hypr-api-storage = { path = "crates/api-storage", package = "api-storage" } -hypr-api-subscription = { path = "crates/api-subscription", package = "api-subscription" } -hypr-api-support = { path = "crates/api-support", package = "api-support" } -hypr-api-sync = { path = "crates/api-sync", package = "api-sync" } -hypr-api-ticket = { path = "crates/api-ticket", package = "api-ticket" } hypr-apple-calendar = { path = "crates/apple-calendar", package = "apple-calendar" } -hypr-apple-note = { path = "crates/apple-note", package = "apple-note" } hypr-apple-todo = { path = "crates/apple-todo", package = "apple-todo" } hypr-askama-utils = { path = "crates/askama-utils", package = "askama-utils" } hypr-audacity = { path = "crates/audacity", package = "audacity" } @@ -81,11 +58,9 @@ hypr-claude = { path = "crates/claude", package = "claude" } hypr-cli-process = { path = "crates/cli-process", package = "cli-process" } hypr-cloudsync = { path = "crates/cloudsync", package = "cloudsync" } hypr-codex = { path = "crates/codex", package = "codex" } -hypr-cursor = { path = "crates/cursor", package = "cursor" } hypr-data = { path = "crates/data", package = "data" } hypr-db-app = { path = "crates/db-app", package = "db-app" } hypr-db-change = { path = "crates/db-change", package = "db-change" } -hypr-db-cli = { path = "crates/db-cli", package = "db-cli" } hypr-db-core = { path = "crates/db-core", package = "db-core" } hypr-db-execute = { path = "crates/db-execute", package = "db-execute" } hypr-db-migrate = { path = "crates/db-migrate", package = "db-migrate" } @@ -93,13 +68,9 @@ hypr-db-reactive = { path = "crates/db-reactive", package = "db-reactive" } hypr-denoise = { path = "crates/denoise", package = "denoise" } hypr-detect = { path = "crates/detect", package = "detect" } hypr-device-monitor = { path = "crates/device-monitor", package = "device-monitor" } -hypr-devin = { path = "crates/devin", package = "devin" } hypr-dictation-ui-macos = { path = "crates/dictation-ui-macos", package = "dictation-ui-macos" } hypr-docs = { path = "crates/docs", package = "docs" } hypr-download-interface = { path = "crates/download-interface", package = "download-interface" } -hypr-embedding = { path = "crates/embedding", package = "embedding" } -hypr-exa = { path = "crates/exa", package = "exa" } -hypr-exedev = { path = "crates/exedev", package = "exedev" } hypr-export-core = { path = "crates/export-core", package = "export-core" } hypr-file = { path = "crates/file", package = "file" } hypr-frontmatter = { path = "crates/frontmatter", package = "frontmatter" } @@ -109,83 +80,54 @@ hypr-gbnf = { path = "crates/gbnf", package = "gbnf" } hypr-gguf = { path = "crates/gguf", package = "gguf" } hypr-github-issues = { path = "crates/github-issues", package = "github-issues" } hypr-google-calendar = { path = "crates/google-calendar", package = "google-calendar" } -hypr-google-drive = { path = "crates/google-drive", package = "google-drive" } -hypr-google-mail = { path = "crates/google-mail", package = "google-mail" } hypr-granola = { path = "crates/granola", package = "granola" } -hypr-hf = { path = "crates/hf", package = "hf" } -hypr-hid-host = { path = "crates/hid-host", package = "hid-host" } -hypr-hid-interface = { path = "crates/hid-interface", package = "hid-interface" } hypr-hooks = { path = "crates/hooks", package = "hooks" } hypr-host = { path = "crates/host", package = "host" } hypr-http = { path = "crates/http", package = "hypr-http-utils" } hypr-importer-core = { path = "crates/importer-core", package = "importer-core" } hypr-intercept = { path = "crates/intercept", package = "intercept" } -hypr-jina = { path = "crates/jina", package = "jina" } hypr-language = { path = "crates/language", package = "language" } -hypr-linear = { path = "crates/linear", package = "linear" } hypr-listener-core = { path = "crates/listener-core", package = "listener-core" } hypr-listener2-core = { path = "crates/listener2-core", package = "listener2-core" } hypr-llm-cactus = { path = "crates/llm-cactus", package = "llm-cactus" } -hypr-llm-proxy = { path = "crates/llm-proxy", package = "llm-proxy" } hypr-llm-types = { path = "crates/llm-types", package = "llm-types" } hypr-lmstudio = { path = "crates/lmstudio", package = "lmstudio" } hypr-local-llm-core = { path = "crates/local-llm-core", package = "local-llm-core" } hypr-local-model = { path = "crates/local-model", package = "local-model" } -hypr-local-stt-core = { path = "crates/local-stt-core", package = "local-stt-core" } -hypr-local-stt-server = { path = "crates/local-stt-server", package = "local-stt-server" } -hypr-loops = { path = "crates/loops", package = "loops" } -hypr-mac = { path = "crates/mac", package = "mac" } -hypr-mcp = { path = "crates/mcp", package = "mcp" } -hypr-mobile-bridge = { path = "crates/mobile-bridge", package = "mobile-bridge" } hypr-model-downloader = { path = "crates/model-downloader", package = "model-downloader" } hypr-model-manager = { path = "crates/model-manager", package = "model-manager" } hypr-mp3 = { path = "crates/mp3", package = "mp3" } -hypr-nango = { path = "crates/nango", package = "nango" } hypr-notification = { path = "crates/notification", package = "notification" } hypr-notification-interface = { path = "crates/notification-interface", package = "notification-interface" } -hypr-notification-macos2 = { path = "crates/notification-macos2", package = "notification-macos2" } hypr-notion = { path = "crates/notion", package = "notion" } hypr-observability = { path = "crates/observability", package = "observability" } hypr-onnx = { path = "crates/onnx", package = "onnx" } hypr-opencode = { path = "crates/opencode", package = "opencode" } hypr-openrouter = { path = "crates/openrouter", package = "openrouter" } -hypr-openstatus = { path = "crates/openstatus", package = "openstatus" } hypr-outlook-calendar = { path = "crates/outlook-calendar", package = "outlook-calendar" } -hypr-porkbun = { path = "crates/porkbun", package = "porkbun" } -hypr-power = { path = "crates/power", package = "power" } -hypr-pyannote-cloud = { path = "crates/pyannote-cloud", package = "pyannote-cloud" } hypr-pyannote-local = { path = "crates/pyannote-local", package = "pyannote-local" } -hypr-recall = { path = "crates/recall", package = "recall" } hypr-resampler = { path = "crates/resampler", package = "resampler" } hypr-s3 = { path = "crates/s3", package = "s3" } hypr-screen-core = { path = "crates/screen-core", package = "screen-core" } -hypr-segmentation = { path = "crates/segmentation", package = "segmentation" } hypr-shortcut-macos = { path = "crates/shortcut-macos", package = "shortcut-macos" } -hypr-slack-web = { path = "crates/slack-web", package = "slack-web" } hypr-storage = { path = "crates/storage", package = "storage" } hypr-supabase-auth = { path = "crates/supabase-auth", package = "supabase-auth" } -hypr-supabase-storage = { path = "crates/supabase-storage", package = "supabase-storage" } hypr-tantivy-core = { path = "crates/tantivy", package = "tantivy-core" } hypr-tauri-utils = { path = "crates/tauri-utils", package = "tauri-utils" } hypr-tcc = { path = "crates/tcc", package = "tcc" } -hypr-teems = { path = "crates/teems", package = "teems" } hypr-template-app = { path = "crates/template-app", package = "template-app" } hypr-template-app-legacy = { path = "crates/template-app-legacy", package = "template-app-legacy" } -hypr-template-cli = { path = "crates/template-cli", package = "template-cli" } hypr-template-eval = { path = "crates/template-eval", package = "template-eval" } hypr-template-support = { path = "crates/template-support", package = "template-support" } hypr-ticket-interface = { path = "crates/ticket-interface", package = "ticket-interface" } hypr-tiptap = { path = "crates/tiptap", package = "tiptap" } hypr-transcribe-cactus = { path = "crates/transcribe-cactus", package = "transcribe-cactus" } hypr-transcribe-core = { path = "crates/transcribe-core", package = "transcribe-core" } -hypr-transcribe-proxy = { path = "crates/transcribe-proxy", package = "transcribe-proxy" } hypr-transcribe-whisper-local = { path = "crates/transcribe-whisper-local", package = "transcribe-whisper-local" } hypr-transcript = { path = "crates/transcript", package = "transcript" } hypr-transcription-core = { path = "crates/transcription-core", package = "transcription-core" } -hypr-updater-core = { path = "crates/updater-core", package = "updater-core" } hypr-vad = { path = "crates/vad", package = "vad" } hypr-vad-masking = { path = "crates/vad-masking", package = "vad-masking" } -hypr-version = { path = "crates/version", package = "version" } hypr-whisper = { path = "crates/whisper", package = "whisper" } hypr-whisper-local = { path = "crates/whisper-local", package = "whisper-local" } hypr-whisper-local-model = { path = "crates/whisper-local-model", package = "whisper-local-model" } @@ -199,7 +141,6 @@ progenitor-client = "0.13" openai-transcription = { path = "crates/openai-transcription", package = "openai-transcription" } owhisper-client = { path = "crates/owhisper-client", package = "owhisper-client" } -owhisper-config = { path = "crates/owhisper-config", package = "owhisper-config" } owhisper-interface = { path = "crates/owhisper-interface", package = "owhisper-interface" } soniox = { path = "crates/soniox", package = "soniox" } @@ -269,7 +210,6 @@ tauri-plugin-tracing = { path = "plugins/tracing" } tauri-plugin-transcription = { path = "plugins/transcription" } tauri-plugin-tray = { path = "plugins/tray" } tauri-plugin-updater2 = { path = "plugins/updater2" } -tauri-plugin-webhook = { path = "plugins/webhook" } tauri-plugin-windows = { path = "plugins/windows" } async-stream = "0.3.6" diff --git a/OBSERVABILITY.md b/OBSERVABILITY.md deleted file mode 100644 index fc1d2e4671..0000000000 --- a/OBSERVABILITY.md +++ /dev/null @@ -1,542 +0,0 @@ -# Observability - -This document is the living spec for observability in this repo. - -It defines: - -- how we use OpenTelemetry -- how data is expected to appear in Honeycomb -- what `x-request-id` means -- how we use Sentry tags, contexts, and user identity -- which attribute names are allowed - -If a change introduces new tracing fields, propagation behavior, or Sentry tagging conventions, update this file in the same change. - -## Scope - -This repo has multiple binaries and runtime surfaces, but the same conventions apply everywhere: - -- `apps/api` is one OTEL service -- `apps/desktop` is one Sentry/desktop service -- internal route groups or modules are not separate OTEL services -- internal logical breakdowns use `hyprnote.subsystem` - -Current canonical subsystem values include: - -- `edge` -- `llm` -- `stt` -- `subscription` - -## Observability Stack - -We use three separate concepts: - -1. OpenTelemetry - - canonical tracing model - - canonical attribute naming model - - canonical propagation model -2. Honeycomb - - primary trace analysis backend - - expects OTEL resources, spans, and high-cardinality fields -3. Sentry - - error reporting and local debugging context - - should mirror OTEL naming where practical - -`x-request-id` is not trace propagation. It is a separate request-correlation mechanism. - -## Resource And Service Model - -### Canonical OTEL resource attributes - -Every process should set: - -- `service.namespace = "hyprnote"` -- `service.name = ` -- `service.version` -- `deployment.environment` - -Current canonical service names: - -- API: `api` -- Desktop: `desktop` - -### What counts as a service - -Use one `service.name` per deployable/runtime process. - -Do not create separate `service.name` values for: - -- axum route groups -- internal modules -- handler categories -- provider adapters - -For example, `edge`, `llm`, `stt`, and `subscription` inside `apps/api` are not separate services. They are subsystems within the `api` service. - -### Subsystems - -Use: - -- `hyprnote.subsystem` - -Examples: - -- API ingress span: `hyprnote.subsystem = "edge"` -- LLM handler span: `hyprnote.subsystem = "llm"` -- STT websocket/session spans: `hyprnote.subsystem = "stt"` - -Do not use a bare `service` span field for this. - -## Propagation - -### Canonical propagation format - -For distributed tracing, use W3C Trace Context: - -- `traceparent` -- `baggage` only when intentionally needed - -Sentry headers may also exist: - -- `sentry-trace` -- `baggage` - -But OTEL trace stitching must work through W3C propagation. - -### Rules - -Inbound requests: - -- extract remote W3C trace context -- set the server span parent from the extracted context - -Outbound requests: - -- inject current W3C trace context - -Do not use custom trace propagation headers when W3C exists. - -### Current implementation - -Rust shared helpers live in: - -- [`crates/observability/src/lib.rs`](/Users/yujonglee/dev/char/crates/observability/src/lib.rs) - -API ingress extraction and root HTTP span setup live in: - -- [`apps/api/src/main.rs`](/Users/yujonglee/dev/char/apps/api/src/main.rs) - -Desktop request header creation lives in: - -- [`apps/desktop/src/shared/utils.ts`](/Users/yujonglee/dev/char/apps/desktop/src/shared/utils.ts) -- [`apps/desktop/src/ai/traced-fetch.ts`](/Users/yujonglee/dev/char/apps/desktop/src/ai/traced-fetch.ts) -- [`apps/desktop/src/auth/context.tsx`](/Users/yujonglee/dev/char/apps/desktop/src/auth/context.tsx) - -### Baggage policy - -Do not put user identity or device identifiers into baggage by default. - -In particular, do not propagate: - -- `enduser.id` -- `enduser.pseudo.id` -- device fingerprints - -as baggage unless there is an explicit need and a privacy review. - -## Request ID - -### Meaning - -`x-request-id` is a correlation ID for support, logs, and local debugging. - -It is not: - -- a trace ID -- a span ID -- a substitute for `traceparent` - -### Rules - -- generate it once at ingress if missing -- forward it unchanged when useful -- record it as `hyprnote.request.id` -- keep it semantically separate from OTEL trace context - -Never do this: - -- `x-request-id = trace_id` -- reconstruct trace relationships from `x-request-id` - -### Current implementation - -API ingress uses request-id middleware and records the value on the root span: - -- [`apps/api/src/main.rs`](/Users/yujonglee/dev/char/apps/api/src/main.rs) - -Desktop client requests add `x-request-id` separately from `traceparent`: - -- [`apps/desktop/src/ai/traced-fetch.ts`](/Users/yujonglee/dev/char/apps/desktop/src/ai/traced-fetch.ts) -- [`apps/desktop/src/auth/context.tsx`](/Users/yujonglee/dev/char/apps/desktop/src/auth/context.tsx) - -## Naming Rules - -### Rule 1: Prefer OTEL semantic conventions - -If OTEL defines a field for the concept, use the OTEL field. - -Examples: - -- `service.namespace` -- `service.name` -- `http.request.method` -- `http.route` -- `http.response.status_code` -- `url.path` -- `enduser.id` -- `enduser.pseudo.id` -- `error.type` -- `error.message` -- `error.code` -- `service.peer.name` -- `gen_ai.operation.name` -- `gen_ai.provider.name` -- `gen_ai.request.model` -- `gen_ai.response.model` -- `gen_ai.response.id` -- `gen_ai.usage.input_tokens` -- `gen_ai.usage.output_tokens` - -### Rule 2: Custom fields must use `hyprnote.*` - -If OTEL does not define a field, use: - -- `hyprnote.*` - -Do not use: - -- `app.*` -- bare ad hoc names like `service`, `provider`, `status`, `session_id`, `user_id` - -We avoid `app.*` because OpenTelemetry owns that namespace. - -### Rule 3: One concept, one name - -If a concept already has an approved key, reuse it everywhere: - -- OTEL spans -- tracing logs/events -- Sentry tags -- Sentry contexts - -Do not rename the same concept differently per backend. - -## Canonical Field Families - -### Identity - -- `enduser.id` -- `enduser.pseudo.id` - -Use: - -- `enduser.id` for authenticated user ID -- `enduser.pseudo.id` for device fingerprint or other stable pseudonymous device identity - -### Request and duration - -- `hyprnote.request.id` -- `hyprnote.duration_ms` -- `hyprnote.retry.delay_ms` -- `hyprnote.timeout_s` -- `hyprnote.timeout.elapsed` - -### HTTP and routing - -- `http.request.method` -- `http.route` -- `http.response.status_code` -- `url.path` -- `url.full` when needed -- `otel.kind` -- `otel.name` - -Ingress HTTP spans should be `otel.kind = "server"`. - -### LLM - -Use OTEL GenAI fields where available: - -- `gen_ai.operation.name` -- `gen_ai.provider.name` -- `gen_ai.request.model` -- `gen_ai.response.model` -- `gen_ai.response.id` -- `gen_ai.usage.input_tokens` -- `gen_ai.usage.output_tokens` - -Use `hyprnote.*` for Hyprnote-specific request metadata: - -- `hyprnote.gen_ai.request.streaming` -- `hyprnote.gen_ai.request.message_count` -- `hyprnote.gen_ai.request.model_candidate_count` -- `hyprnote.gen_ai.request.tool_calling` -- `hyprnote.task.name` - -### STT and audio - -Use: - -- `hyprnote.stt.provider.name` -- `hyprnote.stt.routing_strategy` -- `hyprnote.stt.model` -- `hyprnote.stt.language_codes` -- `hyprnote.stt.language_code` -- `hyprnote.stt.session.id` -- `hyprnote.stt.job.id` -- `hyprnote.stt.provider_session.id` -- `hyprnote.stt.provider_session.duration_s` -- `hyprnote.stt.provider_session.expires_at` -- `hyprnote.stt.provider.error_code` -- `hyprnote.audio.sample_rate_hz` -- `hyprnote.audio.channel_count` -- `hyprnote.audio.channel_index` -- `hyprnote.audio.size_bytes` -- `hyprnote.audio.duration_s` -- `hyprnote.audio.device` - -### Vendor-specific fields - -Keep vendor-specific fields namespaced: - -- `hyprnote.supabase.*` -- `hyprnote.stripe.*` -- `hyprnote.connection.*` -- `hyprnote.integration.*` -- `hyprnote.bot.*` - -Always prefer `service.peer.name` for the downstream system name. - -### Payload and debug-only fields - -If raw payload capture is necessary for debug logs, use: - -- `hyprnote.payload.raw` -- `hyprnote.http.response.body` -- `hyprnote.http.body_preview` - -Do not put large raw payloads on high-volume spans by default. - -## Honeycomb Conventions - -### Service breakdown - -Honeycomb service views come from OTEL resource attributes, especially: - -- `service.name` - -Because of that: - -- `apps/api` must stay one Honeycomb service: `api` -- internal analysis should use `hyprnote.subsystem` - -### High cardinality - -Honeycomb handles high-cardinality fields well. IDs are allowed when they help debugging. - -Good high-cardinality examples: - -- `hyprnote.request.id` -- `enduser.id` -- `enduser.pseudo.id` -- `gen_ai.response.id` -- `hyprnote.stt.job.id` -- provider session IDs - -Do not avoid useful IDs just because they are high cardinality. - -### Root span quality - -Server entry spans should: - -- have a remote parent if the request carries one -- set `otel.kind = "server"` -- set `otel.name` -- record HTTP route and status - -### Span field declaration rule - -When using `tracing`, declare fields up front if you plan to `record` them later. - -This matters for: - -- `#[tracing::instrument(fields(...))]` -- `tracing::info_span!(...)` - -If a field is not declared on span creation, later `span.record(...)` calls will not create a new OTEL attribute. - -## Sentry Conventions - -### Purpose - -Sentry is for: - -- errors -- crash reports -- request-local debugging context - -It is not the canonical trace schema. OTEL is. - -### Tag naming - -Reuse OTEL names when possible. - -Canonical Sentry tags include: - -- `service.namespace` -- `service.name` -- `enduser.id` -- `enduser.pseudo.id` -- `http.response.status_code` -- `error.type` -- `gen_ai.provider.name` -- `gen_ai.request.model` -- `hyprnote.gen_ai.request.streaming` -- `hyprnote.stt.provider.name` -- `hyprnote.stt.routing_strategy` -- `hyprnote.stt.model` -- `hyprnote.stt.language_codes` - -### Context naming - -Use contexts for structured objects that are too rich for tags. - -Canonical context names include: - -- `gen_ai.request` -- `gen_ai.response` -- `hyprnote.stt.request` -- `hyprnote.enduser.claims` -- `hyprnote.session` - -### Sentry user - -Set `scope.set_user(...)` when identity is available. - -API: - -- authenticated requests use the auth subject as the Sentry user ID - -Desktop: - -- use a pseudonymous device identity when no authenticated user exists yet - -### Alignment rule - -Do not invent Sentry-only field names for concepts that already exist in OTEL unless Sentry forces it. - -Good: - -- `enduser.id` -- `service.name` -- `error.type` - -Bad: - -- `user_id` -- `service` -- `upstream.status` -- `llm.model` when `gen_ai.request.model` already exists - -## Error Conventions - -Use: - -- `error.type` for machine-readable classification -- `error.message` for the display/debug message -- `error.code` when an external or protocol code exists - -Examples: - -- provider returned a structured error code -- timeout class -- invalid payload class - -Avoid ad hoc variants such as: - -- `message` -- `error` -- `error_type` -- `error_code` - -## Header Conventions - -Canonical headers used in this repo: - -- `traceparent` -- `baggage` -- `sentry-trace` -- `x-request-id` -- `x-device-fingerprint` - -Meaning: - -- `traceparent`: canonical trace propagation -- `baggage`: optional propagation metadata, usually originating from Sentry on desktop HTTP requests -- `sentry-trace`: Sentry tracing integration -- `x-request-id`: request correlation only -- `x-device-fingerprint`: local pseudonymous device identifier - -## What To Do When Adding Instrumentation - -1. Decide whether the concept already has an OTEL semantic convention. -2. If yes, use the OTEL field name. -3. If no, add a `hyprnote.*` field. -4. If the field will be recorded later on a span, declare it at span creation. -5. If the code crosses a network boundary, extract or inject W3C trace context. -6. If request correlation is needed, keep `x-request-id` separate from trace propagation. -7. Mirror the most important fields into Sentry tags or contexts using the same names. -8. Update this file if you introduce a new field family or a new rule. - -## Anti-Patterns - -Do not do any of the following: - -- per-span `service = "llm"` style fields -- `x-request-id = trace_id` -- custom propagation instead of W3C trace context -- `app.*` custom fields -- different names for the same concept across OTEL and Sentry -- stuffing user identity into baggage by default -- creating new span attributes with `span.record` without declaring them first -- using route groups as separate Honeycomb services - -## Current Reference Points - -The current implementation that this spec describes is centered in: - -- [`apps/api/src/observability.rs`](/Users/yujonglee/dev/char/apps/api/src/observability.rs) -- [`apps/api/src/main.rs`](/Users/yujonglee/dev/char/apps/api/src/main.rs) -- [`apps/api/src/auth.rs`](/Users/yujonglee/dev/char/apps/api/src/auth.rs) -- [`crates/observability/src/lib.rs`](/Users/yujonglee/dev/char/crates/observability/src/lib.rs) -- [`crates/llm-proxy/src/handler/mod.rs`](/Users/yujonglee/dev/char/crates/llm-proxy/src/handler/mod.rs) -- [`crates/llm-proxy/src/handler/non_streaming.rs`](/Users/yujonglee/dev/char/crates/llm-proxy/src/handler/non_streaming.rs) -- [`crates/llm-proxy/src/handler/streaming.rs`](/Users/yujonglee/dev/char/crates/llm-proxy/src/handler/streaming.rs) -- [`crates/transcribe-proxy/src/routes/streaming/mod.rs`](/Users/yujonglee/dev/char/crates/transcribe-proxy/src/routes/streaming/mod.rs) -- [`crates/transcribe-proxy/src/routes/streaming/session.rs`](/Users/yujonglee/dev/char/crates/transcribe-proxy/src/routes/streaming/session.rs) -- [`apps/desktop/src/shared/utils.ts`](/Users/yujonglee/dev/char/apps/desktop/src/shared/utils.ts) -- [`apps/desktop/src/ai/traced-fetch.ts`](/Users/yujonglee/dev/char/apps/desktop/src/ai/traced-fetch.ts) -- [`apps/desktop/src/auth/context.tsx`](/Users/yujonglee/dev/char/apps/desktop/src/auth/context.tsx) -- [`apps/desktop/src-tauri/src/lib.rs`](/Users/yujonglee/dev/char/apps/desktop/src-tauri/src/lib.rs) - -## Change Policy - -Treat this document as normative. - -If code and this file disagree: - -- update the code to match this spec, or -- update this spec in the same change with a deliberate rationale - -Do not let drift accumulate. diff --git a/Taskfile.yaml b/Taskfile.yaml index 57b85c518e..51d5f81a60 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -14,17 +14,6 @@ tasks: - stripe listen --skip-verify --forward-to http://localhost:8788/webhook/stripe bacon: bacon {{.CLI_ARGS}} - cli:ui: - platforms: [darwin] - cmds: - - cd apps/cli-ui && swift build -c release --arch arm64 --arch x86_64 - - cli:dev: - platforms: [darwin] - cmds: - - task: cli:ui - - cp apps/cli-ui/.build/apple/Products/Release/char-cli-ui target/debug/ - - cargo run -p cli --features standalone-macos -- {{.CLI_ARGS}} stat: btop -f hyprnote -u 500 --preset 1 clean-plugins: diff --git a/apps/api/AGENTS.md b/apps/api/AGENTS.md deleted file mode 100644 index 6adee0f3d8..0000000000 --- a/apps/api/AGENTS.md +++ /dev/null @@ -1,9 +0,0 @@ -```bash -infisical export \ - --env=dev \ - --secret-overriding=false \ - --format=dotenv \ - --output-file="apps/api/.env" \ - --projectId=87dad7b5-72a6-4791-9228-b3b86b169db1 \ - --path="/ai" -``` diff --git a/apps/api/Cargo.toml b/apps/api/Cargo.toml deleted file mode 100644 index f88733611e..0000000000 --- a/apps/api/Cargo.toml +++ /dev/null @@ -1,47 +0,0 @@ -[package] -name = "api" -version = "0.1.0" -edition = "2024" - -[dependencies] -hypr-analytics = { workspace = true } -hypr-api-auth = { workspace = true } -hypr-api-cactus = { workspace = true } -hypr-api-calendar = { workspace = true } -hypr-api-env = { workspace = true } -hypr-api-mail = { workspace = true } -hypr-api-nango = { workspace = true } -hypr-api-pyannote = { workspace = true } -hypr-api-research = { workspace = true } -hypr-api-subscription = { workspace = true } -hypr-api-support = { workspace = true } -hypr-api-ticket = { workspace = true } -hypr-linear = { workspace = true } -hypr-llm-proxy = { workspace = true } -hypr-observability = { workspace = true } -hypr-transcribe-proxy = { workspace = true } -owhisper-client = { workspace = true } - -axum = { workspace = true, features = ["ws"] } -opentelemetry = { workspace = true } -opentelemetry-otlp = { workspace = true } -opentelemetry_sdk = { workspace = true } -rustls = { workspace = true } -tokio = { workspace = true, features = ["rt-multi-thread", "macros", "signal"] } -tower-http = { workspace = true, features = ["trace", "cors", "request-id"] } -tracing = { workspace = true } -tracing-opentelemetry = { workspace = true } -tracing-subscriber = { workspace = true, features = ["env-filter"] } - -reqwest = { workspace = true, features = ["json"] } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -url = { workspace = true } - -dotenvy = { workspace = true } -envy = { workspace = true } -governor = { workspace = true } -jsonwebtoken = { workspace = true } -sentry = { workspace = true, features = ["tower", "tower-axum-matched-path", "tracing"] } -tower = { workspace = true } -utoipa = { workspace = true } diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile deleted file mode 100644 index 571073476b..0000000000 --- a/apps/api/Dockerfile +++ /dev/null @@ -1,45 +0,0 @@ -# syntax=docker/dockerfile:1 - -ARG RUST_VERSION=1.93.0 - -FROM rust:${RUST_VERSION}-bookworm AS planner -RUN apt-get update && apt-get install -y --no-install-recommends pkg-config libssl-dev ca-certificates && rm -rf /var/lib/apt/lists/* -RUN cargo install cargo-chef --locked -WORKDIR /app -COPY Cargo.toml Cargo.lock ./ -RUN sed -i '/^members = \[/,/^\]/c\members = ["apps/api", "crates/*"]' Cargo.toml -COPY crates crates -COPY apps/api/Cargo.toml apps/api/Cargo.toml -RUN mkdir -p apps/api/src && echo "fn main() {}" > apps/api/src/main.rs -RUN cargo chef prepare --recipe-path recipe.json - -FROM rust:${RUST_VERSION}-bookworm AS build -ARG APP_VERSION -RUN apt-get update && apt-get install -y --no-install-recommends pkg-config libssl-dev ca-certificates && rm -rf /var/lib/apt/lists/* -RUN cargo install cargo-chef --locked -ENV APP_VERSION=${APP_VERSION} -WORKDIR /app -COPY --from=planner /app/recipe.json recipe.json -RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=locked \ - --mount=type=cache,target=/usr/local/cargo/git,sharing=locked \ - cargo chef cook --release --recipe-path recipe.json -p api -COPY Cargo.toml Cargo.lock ./ -RUN sed -i '/^members = \[/,/^\]/c\members = ["apps/api", "crates/*"]' Cargo.toml -COPY crates crates -COPY apps/api apps/api -RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=locked \ - --mount=type=cache,target=/usr/local/cargo/git,sharing=locked \ - cargo clean -p progenitor-utils -RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=locked \ - --mount=type=cache,target=/usr/local/cargo/git,sharing=locked \ - cargo build --release -p api - -FROM debian:bookworm-slim AS runtime -RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates libssl3 && rm -rf /var/lib/apt/lists/* -RUN groupadd -g 1001 appgroup && \ - useradd -u 1001 -g appgroup -d /nonexistent -s /usr/sbin/nologin -M appuser -COPY --from=build --chown=appuser:appgroup /app/target/release/api /usr/local/bin/api -USER appuser -WORKDIR /app -EXPOSE 3001 -ENTRYPOINT ["/usr/local/bin/api"] diff --git a/apps/api/fly.toml b/apps/api/fly.toml deleted file mode 100644 index ab4530fc40..0000000000 --- a/apps/api/fly.toml +++ /dev/null @@ -1,38 +0,0 @@ -app = 'hyprnote-ai' -primary_region = 'sjc' -kill_signal = 'SIGTERM' -kill_timeout = 30 -swap_size_mb = 512 - -[deploy] -strategy = "bluegreen" - -[env] -PORT = "3001" - -[http_service] -processes = ['app'] -internal_port = 3001 -force_https = true -auto_stop_machines = 'stop' -auto_start_machines = true -min_machines_running = 1 - -[http_service.concurrency] -type = "connections" -hard_limit = 200 -soft_limit = 150 - -[[http_service.checks]] -grace_period = "20s" -interval = "15s" -method = "GET" -path = "/health" -protocol = "http" -timeout = "4s" - -[[vm]] -processes = ['app'] -memory = '1gb' -cpu_kind = 'shared' -cpus = 1 diff --git a/apps/api/src/auth.rs b/apps/api/src/auth.rs deleted file mode 100644 index fc39cdb664..0000000000 --- a/apps/api/src/auth.rs +++ /dev/null @@ -1,63 +0,0 @@ -use std::collections::BTreeMap; - -use axum::{extract::Request, middleware::Next, response::Response}; - -use hypr_api_auth::AuthContext; -pub use hypr_api_auth::{AuthState, optional_auth, require_auth}; - -const DEVICE_FINGERPRINT_HEADER: &str = "x-device-fingerprint"; - -pub async fn sentry_and_analytics(mut request: Request, next: Next) -> Response { - let span = tracing::Span::current(); - let device_fingerprint = request - .headers() - .get(DEVICE_FINGERPRINT_HEADER) - .and_then(|h| h.to_str().ok()) - .map(String::from); - - if let Some(auth) = request.extensions().get::() { - sentry::configure_scope(|scope| { - scope.set_user(Some(sentry::User { - id: Some(auth.claims.sub.clone()), - email: auth.claims.email.clone(), - username: Some(auth.claims.sub.clone()), - ..Default::default() - })); - scope.set_tag("enduser.id", &auth.claims.sub); - if let Some(fingerprint) = device_fingerprint.as_deref() { - scope.set_tag("enduser.pseudo.id", fingerprint); - } - - let mut ctx = BTreeMap::new(); - ctx.insert( - "hyprnote.enduser.entitlements".into(), - sentry::protocol::Value::Array( - auth.claims - .entitlements - .iter() - .map(|e| sentry::protocol::Value::String(e.clone())) - .collect(), - ), - ); - scope.set_context( - "hyprnote.enduser.claims", - sentry::protocol::Context::Other(ctx), - ); - }); - - let user_id = auth.claims.sub.clone(); - span.record("enduser.id", user_id.as_str()); - request - .extensions_mut() - .insert(hypr_analytics::AuthenticatedUserId(user_id)); - } - - if let Some(fingerprint) = device_fingerprint { - span.record("enduser.pseudo.id", fingerprint.as_str()); - request - .extensions_mut() - .insert(hypr_analytics::DeviceFingerprint(fingerprint)); - } - - next.run(request).await -} diff --git a/apps/api/src/env.rs b/apps/api/src/env.rs deleted file mode 100644 index 52d259cc06..0000000000 --- a/apps/api/src/env.rs +++ /dev/null @@ -1,80 +0,0 @@ -use std::path::Path; -use std::sync::OnceLock; - -use envy::Error as EnvyError; -use serde::Deserialize; - -fn default_port() -> u16 { - 3001 -} - -#[derive(Deserialize)] -pub struct Env { - #[serde(default = "default_port")] - pub port: u16, - #[serde(default, deserialize_with = "hypr_api_env::filter_empty")] - pub sentry_dsn: Option, - #[serde(default, deserialize_with = "hypr_api_env::filter_empty")] - pub posthog_api_key: Option, - - #[serde(flatten)] - pub observability: crate::observability::Env, - - #[serde(flatten)] - pub supabase: hypr_api_env::SupabaseEnv, - #[serde(flatten)] - pub nango: hypr_api_env::NangoEnv, - #[serde(flatten)] - pub stripe: hypr_api_env::StripeEnv, - #[serde(flatten)] - pub pyannote: hypr_api_env::PyannoteEnv, - #[serde(flatten)] - pub github_app: hypr_api_support::GitHubAppEnv, - #[serde(flatten)] - pub support_database: hypr_api_support::SupportDatabaseEnv, - - pub cactus_api_key: String, - pub exa_api_key: String, - pub jina_api_key: String, - - #[serde(flatten)] - pub loops: hypr_api_env::LoopsEnv, - - #[serde(flatten)] - pub llm: hypr_llm_proxy::Env, - #[serde(flatten)] - pub stt: hypr_transcribe_proxy::Env, -} - -static ENV: OnceLock = OnceLock::new(); - -pub fn env() -> &'static Env { - ENV.get_or_init(|| { - let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); - let repo_root = manifest_dir - .parent() - .and_then(|p| p.parent()) - .unwrap_or(manifest_dir); - - let _ = dotenvy::from_path(repo_root.join(".env.supabase")); - let _ = dotenvy::from_path(manifest_dir.join(".env")); - envy::from_env().unwrap_or_else(|error| panic!("{}", format_env_error(error))) - }) -} - -fn format_env_error(error: EnvyError) -> String { - match error { - EnvyError::MissingValue(field) => { - let env_var = field_name_to_env_var(&field); - format!("Failed to load environment: missing {env_var} (field: {field})") - } - other => format!("Failed to load environment: {other}"), - } -} - -fn field_name_to_env_var(field: &str) -> String { - field - .chars() - .flat_map(|ch| ch.to_uppercase()) - .collect::() -} diff --git a/apps/api/src/main.rs b/apps/api/src/main.rs deleted file mode 100644 index 775f55423b..0000000000 --- a/apps/api/src/main.rs +++ /dev/null @@ -1,556 +0,0 @@ -mod auth; -mod env; -mod observability; -mod openapi; -mod rate_limit; - -use std::net::SocketAddr; -use std::num::NonZeroU32; -use std::sync::Arc; -use std::time::Duration; -use std::time::SystemTime; - -use axum::{Router, body::Body, extract::MatchedPath, http::HeaderMap, http::Request, middleware}; -use sentry::integrations::tower::{NewSentryLayer, SentryHttpLayer}; -use sentry::protocol::{Context, Value}; -use tower::ServiceBuilder; -use tower_http::{ - classify::ServerErrorsFailureClass, - cors::{self, CorsLayer}, - request_id::{MakeRequestUuid, PropagateRequestIdLayer, SetRequestIdLayer}, - trace::TraceLayer, -}; - -use auth::AuthState; -use env::env; - -use crate::env::Env; - -pub const DEVICE_FINGERPRINT_HEADER: &str = "x-device-fingerprint"; -pub const REQUEST_ID_HEADER: &str = "x-request-id"; - -fn forwarded_header_value(headers: &HeaderMap, name: &str) -> Option { - headers - .get(name) - .and_then(|value| value.to_str().ok()) - .and_then(|value| value.split(',').next()) - .map(str::trim) - .filter(|value| !value.is_empty()) - .map(ToString::to_string) -} - -fn request_scheme(request: &Request) -> String { - forwarded_header_value(request.headers(), "x-forwarded-proto") - .or_else(|| request.uri().scheme_str().map(ToString::to_string)) - .unwrap_or_else(|| "http".to_string()) -} - -fn request_server_endpoint(request: &Request, scheme: &str) -> (Option, Option) { - let authority = forwarded_header_value(request.headers(), "x-forwarded-host") - .or_else(|| { - request - .headers() - .get("host") - .and_then(|value| value.to_str().ok()) - .map(ToString::to_string) - }) - .or_else(|| request.uri().host().map(ToString::to_string)); - let Some(authority) = authority else { - return (None, None); - }; - let authority = authority.trim(); - if authority.is_empty() { - return (None, None); - } - let Ok(url) = reqwest::Url::parse(&format!("{scheme}://{authority}")) else { - return (Some(authority.to_string()), None); - }; - let host = url.host_str().map(ToString::to_string); - let port = url.port_or_known_default(); - (host, port) -} - -fn request_client_address(request: &Request) -> Option { - forwarded_header_value(request.headers(), "x-forwarded-for") -} - -async fn app() -> Router { - let env = env(); - - let analytics = build_analytics_client(env); - - let llm_config = - hypr_llm_proxy::LlmProxyConfig::new(&env.llm).with_analytics(analytics.clone()); - let stt_config = hypr_transcribe_proxy::SttProxyConfig::new(&env.stt, &env.supabase) - .with_hyprnote_routing(hypr_transcribe_proxy::HyprnoteRoutingConfig::default()) - .with_analytics(analytics.clone()); - - let stt_rate_limit = rate_limit::RateLimitState::builder() - .pro( - governor::Quota::with_period(Duration::from_mins(5)) - .unwrap() - .allow_burst(NonZeroU32::new(20).unwrap()), - ) - .free( - governor::Quota::with_period(Duration::from_hours(24)) - .unwrap() - .allow_burst(NonZeroU32::new(3).unwrap()), - ) - .build(); - let llm_rate_limit = rate_limit::RateLimitState::builder() - .pro( - governor::Quota::with_period(Duration::from_secs(1)) - .unwrap() - .allow_burst(NonZeroU32::new(30).unwrap()), - ) - .free( - governor::Quota::with_period(Duration::from_hours(12)) - .unwrap() - .allow_burst(NonZeroU32::new(5).unwrap()), - ) - .build(); - - let auth_state_pro = AuthState::new(&env.supabase.supabase_url) - .with_required_entitlements(vec!["hyprnote_pro".into(), "hyprnote_lite".into()]); - let auth_state_basic = AuthState::new(&env.supabase.supabase_url); - let auth_state_support = AuthState::new(&env.supabase.supabase_url); - - let nango_config = hypr_api_nango::NangoConfig::new( - &env.nango, - &env.supabase, - Some(env.supabase.supabase_service_role_key.clone()), - ); - let nango_connection_state = hypr_api_nango::NangoConnectionState::from_config(&nango_config); - let subscription_config = - hypr_api_subscription::SubscriptionConfig::new(&env.supabase, &env.stripe, &env.loops) - .with_analytics(analytics.clone()); - let support_config = hypr_api_support::SupportConfig::new( - &env.github_app, - &env.llm, - &env.support_database, - &env.stripe, - &env.supabase, - auth_state_support.clone(), - ); - let cactus_config = hypr_api_cactus::CactusProxyConfig { - api_key: env.cactus_api_key.clone(), - upstream_base: None, - }; - let research_config = hypr_api_research::ResearchConfig { - exa_api_key: env.exa_api_key.clone(), - jina_api_key: env.jina_api_key.clone(), - }; - let pyannote_config = hypr_api_pyannote::PyannoteConfig::new(&env.pyannote); - - use hypr_api_nango::NangoIntegrationId; - - let mut forward_handlers = hypr_api_nango::ForwardHandlerRegistry::new(); - forward_handlers.insert( - hypr_api_nango::Linear::ID.to_string(), - hypr_api_nango::forward_handler(hypr_linear::webhook::handle), - ); - - let webhook_routes = Router::new() - .nest( - "/nango", - hypr_api_nango::webhook_router(nango_config.clone(), forward_handlers), - ) - .nest( - "/stt", - hypr_transcribe_proxy::callback_router(stt_config.clone()), - ); - - let auth_state_integration = - AuthState::new(&env.supabase.supabase_url).with_required_entitlement("hyprnote_pro"); - - let pro_routes = Router::new() - .merge(hypr_api_research::router(research_config)) - .nest("/pyannote", hypr_api_pyannote::router(pyannote_config)) - .route_layer(middleware::from_fn(auth::sentry_and_analytics)) - .route_layer(middleware::from_fn_with_state( - auth_state_pro, - auth::require_auth, - )); - - let integration_routes = Router::new() - .nest("/calendar", hypr_api_calendar::router()) - .nest("/mail", hypr_api_mail::router()) - .nest("/ticket", hypr_api_ticket::router()) - .nest( - "/nango", - hypr_api_nango::session_router(nango_config.clone()), - ) - .layer(axum::Extension(nango_connection_state)) - .route_layer(middleware::from_fn(auth::sentry_and_analytics)) - .route_layer(middleware::from_fn_with_state( - auth_state_integration, - auth::require_auth, - )); - - let integration_management_routes = Router::new() - .nest( - "/nango", - hypr_api_nango::management_router(nango_config.clone()), - ) - .route_layer(middleware::from_fn(auth::sentry_and_analytics)) - .route_layer(middleware::from_fn_with_state( - auth_state_basic.clone(), - auth::require_auth, - )); - - let stt_routes = Router::new() - .merge(hypr_transcribe_proxy::listen_router(stt_config.clone())) - .nest("/stt", hypr_transcribe_proxy::router(stt_config)) - .route_layer(middleware::from_fn_with_state( - stt_rate_limit, - rate_limit::rate_limit, - )); - - let llm_routes = Router::new() - .merge(hypr_llm_proxy::chat_completions_router(llm_config.clone())) - .nest("/llm", hypr_llm_proxy::router(llm_config)) - .route_layer(middleware::from_fn_with_state( - llm_rate_limit, - rate_limit::rate_limit, - )); - - let subscription_router = hypr_api_subscription::router(subscription_config); - let auth_routes = Router::new() - .merge(stt_routes) - .merge(llm_routes) - .nest("/subscription", subscription_router.clone()) - .nest("/rpc", subscription_router.clone()) - .nest("/billing", subscription_router) - .route_layer(middleware::from_fn(auth::sentry_and_analytics)) - .route_layer(middleware::from_fn_with_state( - auth_state_basic, - auth::require_auth, - )); - - let support_routes = Router::new() - .merge(hypr_api_support::router(support_config).await) - .layer(middleware::from_fn_with_state( - auth_state_support.clone(), - auth::optional_auth, - )); - - Router::new() - .route("/health", axum::routing::get(version)) - .route("/openapi.json", axum::routing::get(openapi_json)) - .nest("/cactus", hypr_api_cactus::router(cactus_config)) - .merge(support_routes) - .merge(webhook_routes) - .merge(pro_routes) - .merge(integration_routes) - .merge(integration_management_routes) - .merge(auth_routes) - .layer( - CorsLayer::new() - .allow_origin(cors::Any) - .allow_methods(cors::Any) - .allow_headers(cors::Any) - .expose_headers([axum::http::header::HeaderName::from_static( - REQUEST_ID_HEADER, - )]), - ) - .layer( - ServiceBuilder::new() - .layer(SetRequestIdLayer::x_request_id(MakeRequestUuid)) - .layer(PropagateRequestIdLayer::x_request_id()) - .layer(NewSentryLayer::>::new_from_top()) - .layer(SentryHttpLayer::new().enable_transaction()) - .layer( - TraceLayer::new_for_http() - .make_span_with(|request: &Request| { - let path = request.uri().path(); - - if path == "/health" { - return tracing::Span::none(); - } - - let method = request.method(); - let matched_path = request - .extensions() - .get::() - .map(MatchedPath::as_str) - .unwrap_or(path); - let scheme = request_scheme(request); - let (server_address, server_port) = - request_server_endpoint(request, &scheme); - let client_address = request_client_address(request); - let span_op = match path { - p if p.starts_with("/llm") - || p.starts_with("/chat/completions") => - { - "http.server.llm" - } - p if p.starts_with("/stt") || p.starts_with("/listen") => { - "http.server.stt" - } - _ => "http.server", - }; - - let span = tracing::info_span!( - "http_request", - http.request.method = %method, - http.route = %matched_path, - url.path = %path, - url.scheme = %scheme, - http.response.status_code = tracing::field::Empty, - server.address = tracing::field::Empty, - server.port = tracing::field::Empty, - client.address = tracing::field::Empty, - hyprnote.subsystem = "edge", - enduser.id = tracing::field::Empty, - enduser.pseudo.id = tracing::field::Empty, - hyprnote.stt.provider.name = tracing::field::Empty, - hyprnote.stt.routing_strategy = tracing::field::Empty, - hyprnote.stt.model = tracing::field::Empty, - hyprnote.stt.language_codes = tracing::field::Empty, - hyprnote.audio.sample_rate_hz = tracing::field::Empty, - hyprnote.audio.channel_count = tracing::field::Empty, - gen_ai.provider.name = tracing::field::Empty, - hyprnote.gen_ai.request.streaming = tracing::field::Empty, - hyprnote.gen_ai.request.message_count = tracing::field::Empty, - hyprnote.request.id = tracing::field::Empty, - error.type = tracing::field::Empty, - otel.status_code = tracing::field::Empty, - otel.kind = "server", - otel.name = %format!("{} {}", method, matched_path), - span.op = %span_op, - ); - if let Some(server_address) = server_address.as_deref() { - span.record("server.address", server_address); - } - if let Some(server_port) = server_port { - span.record("server.port", server_port as i64); - } - if let Some(client_address) = client_address.as_deref() { - span.record("client.address", client_address); - } - hypr_observability::set_remote_parent(&span, request.headers()); - span - }) - .on_request(|request: &Request, span: &tracing::Span| { - // Skip logging for health checks - if request.uri().path() == "/health" { - return; - } - if let Some(request_id) = request - .headers() - .get(REQUEST_ID_HEADER) - .and_then(|v| v.to_str().ok()) - { - span.record("hyprnote.request.id", request_id); - } - configure_sentry_trace_scope(span, env, SystemTime::now()); - tracing::info!( - parent: span, - http.request.method = %request.method(), - url.path = %request.uri().path(), - "http_request_started" - ); - }) - .on_response( - |response: &axum::http::Response, - latency: std::time::Duration, - span: &tracing::Span| { - if span.is_disabled() { - return; - } - span.record( - "http.response.status_code", - response.status().as_u16() as i64, - ); - if response.status().is_server_error() { - hypr_observability::mark_span_as_error( - span, - &response.status().as_u16().to_string(), - ); - } - tracing::info!( - parent: span, - http.response.status_code = %response.status().as_u16(), - hyprnote.duration_ms = %latency.as_millis(), - "http_request_finished" - ); - }, - ) - .on_failure( - |failure_class: ServerErrorsFailureClass, - latency: std::time::Duration, - span: &tracing::Span| { - if span.is_disabled() { - return; - } - let error_type = match &failure_class { - ServerErrorsFailureClass::StatusCode(status) => { - status.as_u16().to_string() - } - ServerErrorsFailureClass::Error(_) => { - "http_server_failure".to_string() - } - }; - hypr_observability::mark_span_as_error(span, error_type.as_str()); - tracing::error!( - parent: span, - error.type = %error_type, - error = %failure_class, - hyprnote.duration_ms = %latency.as_millis(), - "http_request_failed" - ); - }, - ), - ), - ) -} - -fn build_analytics_client(env: &Env) -> Arc { - let mut builder = hypr_analytics::AnalyticsClientBuilder::default(); - if cfg!(debug_assertions) { - tracing::info!("analytics: dev mode, printing events as tracing"); - } else { - let key = env - .posthog_api_key - .as_ref() - .expect("POSTHOG_API_KEY is required in production"); - builder = builder.with_posthog(key); - } - Arc::new(builder.build()) -} - -fn main() -> std::io::Result<()> { - rustls::crypto::aws_lc_rs::default_provider() - .install_default() - .expect("failed to install rustls crypto provider"); - - let _ = openapi::write_openapi_json(); - - let env = env(); - - let _guard = sentry::init(sentry::ClientOptions { - dsn: env.sentry_dsn.as_ref().and_then(|s| s.parse().ok()), - release: option_env!("APP_VERSION").map(|v| format!("hyprnote-api@{}", v).into()), - environment: Some( - if cfg!(debug_assertions) { - "development" - } else { - "production" - } - .into(), - ), - traces_sample_rate: 1.0, - sample_rate: 1.0, - send_default_pii: true, - auto_session_tracking: true, - session_mode: sentry::SessionMode::Request, - attach_stacktrace: true, - max_breadcrumbs: 100, - ..Default::default() - }); - - sentry::configure_scope(|scope| { - scope.set_tag("service.namespace", "hyprnote"); - scope.set_tag("service.name", "api"); - }); - - let observability = observability::init("api", &env.observability); - - hypr_transcribe_proxy::ApiKeys::from(&env.stt.stt).log_configured_providers(); - - tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build()? - .block_on(async { - let addr = SocketAddr::from(([0, 0, 0, 0], env.port)); - tracing::info!(addr = %addr, "server_listening"); - - let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); - axum::serve(listener, app().await) - .with_graceful_shutdown(shutdown_signal()) - .await - .unwrap(); - }); - - if let Some(client) = sentry::Hub::current().client() { - client.close(Some(Duration::from_secs(2))); - } - observability.shutdown(); - - Ok(()) -} - -async fn shutdown_signal() { - tokio::signal::ctrl_c() - .await - .expect("failed to install CTRL+C signal handler"); - tracing::info!("shutdown_signal_received"); -} - -async fn openapi_json() -> axum::Json { - axum::Json(openapi::openapi()) -} - -async fn version() -> &'static str { - option_env!("APP_VERSION").unwrap_or("unknown") -} - -fn configure_sentry_trace_scope(span: &tracing::Span, env: &Env, request_started_at: SystemTime) { - let Some(trace_identifiers) = hypr_observability::span_identifiers(span) else { - return; - }; - - let trace_url = build_honeycomb_trace_url(env, &trace_identifiers, request_started_at); - sentry::configure_scope(|scope| { - scope.set_tag( - "hyprnote.honeycomb.trace_id", - trace_identifiers.trace_id.as_str(), - ); - scope.set_tag( - "hyprnote.honeycomb.span_id", - trace_identifiers.span_id.as_str(), - ); - if let Some(trace_url) = trace_url.as_deref() { - scope.set_tag("hyprnote.honeycomb.trace_url", trace_url); - } - - let mut context = std::collections::BTreeMap::new(); - context.insert("trace_id".into(), Value::String(trace_identifiers.trace_id)); - context.insert("span_id".into(), Value::String(trace_identifiers.span_id)); - if let Some(trace_url) = trace_url { - context.insert("trace_url".into(), Value::String(trace_url)); - } - scope.set_context("hyprnote.honeycomb", Context::Other(context)); - }); -} - -fn build_honeycomb_trace_url( - env: &Env, - trace_identifiers: &hypr_observability::TraceIdentifiers, - request_started_at: SystemTime, -) -> Option { - let team = env.observability.honeycomb_ui_team.as_deref()?; - let environment = env.observability.honeycomb_ui_environment.as_deref()?; - let base_url = env - .observability - .honeycomb_ui_base_url - .as_deref() - .unwrap_or("https://ui.honeycomb.io") - .trim_end_matches('/'); - let trace_start_ts = request_started_at - .duration_since(SystemTime::UNIX_EPOCH) - .ok()? - .as_secs() - .to_string(); - - let mut url = url::Url::parse(&format!( - "{base_url}/{team}/environments/{environment}/trace" - )) - .ok()?; - url.query_pairs_mut() - .append_pair("trace_id", trace_identifiers.trace_id.as_str()) - .append_pair("span", trace_identifiers.span_id.as_str()) - .append_pair("trace_start_ts", trace_start_ts.as_str()); - - Some(url.into()) -} diff --git a/apps/api/src/observability.rs b/apps/api/src/observability.rs deleted file mode 100644 index 55ada0aad5..0000000000 --- a/apps/api/src/observability.rs +++ /dev/null @@ -1,179 +0,0 @@ -use std::collections::HashMap; - -use opentelemetry::KeyValue; -use opentelemetry::global; -use opentelemetry::trace::TracerProvider as _; -use opentelemetry_otlp::WithExportConfig; -use opentelemetry_otlp::WithHttpConfig; -use opentelemetry_sdk::Resource; -use opentelemetry_sdk::trace::SdkTracerProvider; -use serde::Deserialize; -use tracing_subscriber::prelude::*; - -#[derive(Deserialize)] -pub struct Env { - #[serde(default, deserialize_with = "hypr_api_env::filter_empty")] - pub otel_service_name: Option, - #[serde(flatten)] - direct: DirectHoneycombEnv, - #[serde(flatten)] - collector: OtelCollectorEnv, - #[serde(default, deserialize_with = "hypr_api_env::filter_empty")] - pub honeycomb_ui_base_url: Option, - #[serde(default, deserialize_with = "hypr_api_env::filter_empty")] - pub honeycomb_ui_team: Option, - #[serde(default, deserialize_with = "hypr_api_env::filter_empty")] - pub honeycomb_ui_environment: Option, -} - -#[derive(Deserialize)] -struct DirectHoneycombEnv { - #[serde(default, deserialize_with = "hypr_api_env::filter_empty")] - honeycomb_api_key: Option, - #[serde(default, deserialize_with = "hypr_api_env::filter_empty")] - honeycomb_api_endpoint: Option, - #[serde(default, deserialize_with = "hypr_api_env::filter_empty")] - honeycomb_dataset: Option, -} - -#[derive(Deserialize)] -struct OtelCollectorEnv { - #[serde(default, deserialize_with = "hypr_api_env::filter_empty")] - otel_exporter_otlp_endpoint: Option, -} - -pub struct ObservabilityGuard { - otel_provider: Option, -} - -pub fn init(service_name: &str, env: &Env) -> ObservabilityGuard { - hypr_observability::install_trace_context_propagator(); - let otel_provider = init_otel_tracer_provider(service_name, env); - let env_filter = tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| "info,tower_http=debug".into()); - - if let Some(provider) = otel_provider.as_ref() { - let tracer = provider.tracer(service_name.to_string()); - tracing_subscriber::registry() - .with(env_filter) - .with(tracing_subscriber::fmt::layer()) - .with(tracing_opentelemetry::layer().with_tracer(tracer)) - .with(sentry::integrations::tracing::layer()) - .init(); - } else { - tracing_subscriber::registry() - .with(env_filter) - .with(tracing_subscriber::fmt::layer()) - .with(sentry::integrations::tracing::layer()) - .init(); - } - - ObservabilityGuard { otel_provider } -} - -impl ObservabilityGuard { - pub fn shutdown(self) { - if let Some(provider) = self.otel_provider - && let Err(e) = provider.shutdown() - { - tracing::warn!(error = %e, "otel_tracer_shutdown_failed"); - } - } -} - -fn init_otel_tracer_provider(service_name: &str, env: &Env) -> Option { - let export_config = trace_export_config(env)?; - - let exporter_builder = opentelemetry_otlp::SpanExporter::builder() - .with_http() - .with_endpoint(trace_export_endpoint(&export_config.endpoint)) - .with_headers(export_config.headers); - let exporter = exporter_builder.build().ok()?; - - let configured_service_name = env - .otel_service_name - .clone() - .unwrap_or_else(|| service_name.to_string()); - let environment = if cfg!(debug_assertions) { - "development" - } else { - "production" - }; - let version = option_env!("APP_VERSION").unwrap_or("unknown"); - - let resource = Resource::builder_empty() - .with_attributes([ - KeyValue::new("service.namespace", "hyprnote"), - KeyValue::new("service.name", configured_service_name), - KeyValue::new("service.version", version.to_string()), - KeyValue::new("deployment.environment", environment), - ]) - .build(); - - let provider = SdkTracerProvider::builder() - .with_batch_exporter(exporter) - .with_resource(resource) - .build(); - - global::set_tracer_provider(provider.clone()); - Some(provider) -} - -struct TraceExportConfig { - endpoint: String, - headers: HashMap, -} - -fn trace_export_config(env: &Env) -> Option { - if let Some(config) = env.direct.trace_export_config() { - return Some(config); - } - - env.collector.trace_export_config() -} - -impl DirectHoneycombEnv { - fn trace_export_config(&self) -> Option { - let api_key = self.honeycomb_api_key.clone()?; - let mut headers = HashMap::from([("x-honeycomb-team".to_string(), api_key)]); - if let Some(dataset) = self.honeycomb_dataset.clone() { - headers.insert("x-honeycomb-dataset".to_string(), dataset); - } - - Some(TraceExportConfig { - endpoint: normalize_endpoint( - self.honeycomb_api_endpoint - .as_deref() - .unwrap_or("https://api.honeycomb.io"), - "https", - ), - headers, - }) - } -} - -impl OtelCollectorEnv { - fn trace_export_config(&self) -> Option { - Some(TraceExportConfig { - endpoint: normalize_endpoint(self.otel_exporter_otlp_endpoint.as_deref()?, "http"), - headers: HashMap::new(), - }) - } -} - -fn normalize_endpoint(endpoint: &str, default_scheme: &str) -> String { - if endpoint.contains("://") { - endpoint.to_string() - } else { - format!("{default_scheme}://{endpoint}") - } -} - -fn trace_export_endpoint(base_url: &str) -> String { - let base_url = base_url.trim_end_matches('/'); - if base_url.ends_with("/v1/traces") { - return base_url.to_string(); - } - - format!("{base_url}/v1/traces") -} diff --git a/apps/api/src/openapi.rs b/apps/api/src/openapi.rs deleted file mode 100644 index da565574de..0000000000 --- a/apps/api/src/openapi.rs +++ /dev/null @@ -1,208 +0,0 @@ -use std::collections::BTreeMap; - -use utoipa::openapi::path::{Operation, PathItem}; -use utoipa::openapi::security::{Http, HttpAuthScheme, SecurityRequirement, SecurityScheme}; -use utoipa::{Modify, OpenApi}; - -#[derive(OpenApi)] -#[openapi( - info( - title = "Char AI API", - version = "1.0.0", - description = "AI services API for speech-to-text transcription, LLM chat completions, and subscription management" - ), - tags( - (name = "stt", description = "Speech-to-text transcription endpoints"), - (name = "llm", description = "LLM chat completions endpoints"), - (name = "pyannote", description = "Pyannote speaker diarization and voice processing"), - (name = "calendar", description = "Calendar management"), - (name = "mail", description = "Mail management"), - (name = "ticket", description = "Ticket management"), - (name = "nango", description = "Integration management via Nango"), - (name = "subscription", description = "Subscription and trial management") - ), - modifiers(&SecurityAddon) -)] -pub struct ApiDoc; - -pub fn openapi() -> utoipa::openapi::OpenApi { - let mut doc = ApiDoc::openapi(); - - let stt_doc = hypr_transcribe_proxy::openapi(); - let llm_doc = hypr_llm_proxy::openapi(); - let pyannote_doc = with_path_prefix(hypr_api_pyannote::openapi(), "/pyannote"); - let calendar_doc = with_path_prefix(hypr_api_calendar::openapi(), "/calendar"); - let mail_doc = with_path_prefix(hypr_api_mail::openapi(), "/mail"); - let ticket_doc = with_path_prefix(hypr_api_ticket::openapi(), "/ticket"); - let nango_doc = with_path_prefix(hypr_api_nango::openapi(), "/nango"); - let subscription_doc = with_path_prefix(hypr_api_subscription::openapi(), "/subscription"); - let support_doc = hypr_api_support::openapi(); - - doc.merge(stt_doc); - doc.merge(llm_doc); - doc.merge(pyannote_doc); - doc.merge(calendar_doc); - doc.merge(mail_doc); - doc.merge(ticket_doc); - doc.merge(nango_doc); - doc.merge(subscription_doc); - doc.merge(support_doc); - - apply_bearer_auth_to_protected_paths(&mut doc); - - doc -} - -pub fn write_openapi_json() -> std::io::Result { - let doc = openapi(); - let json = serde_json::to_string_pretty(&doc) - .map_err(|e| std::io::Error::other(format!("serialize openapi: {e}")))?; - - let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("openapi.gen.json"); - std::fs::write(&path, json)?; - Ok(path) -} - -struct SecurityAddon; - -impl Modify for SecurityAddon { - fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { - if let Some(components) = openapi.components.as_mut() { - components.add_security_scheme( - "bearer_auth", - SecurityScheme::Http( - Http::builder() - .scheme(HttpAuthScheme::Bearer) - .bearer_format("JWT") - .description(Some("Supabase JWT token")) - .build(), - ), - ); - } - } -} - -fn with_path_prefix(mut doc: utoipa::openapi::OpenApi, prefix: &str) -> utoipa::openapi::OpenApi { - let prefix = prefix.trim_end_matches('/'); - if prefix.is_empty() { - return doc; - } - - let paths = std::mem::take(&mut doc.paths.paths); - - let prefixed: BTreeMap = paths - .into_iter() - .map(|(path, item)| (format!("{prefix}{path}"), item)) - .collect(); - - doc.paths.paths = prefixed; - doc -} - -fn apply_bearer_auth_to_protected_paths(doc: &mut utoipa::openapi::OpenApi) { - let paths = &mut doc.paths.paths; - - for (path, item) in paths.iter_mut() { - if path == "/nango/webhook" { - clear_operation_security(item); - continue; - } - - if path.starts_with("/calendar") - || path.starts_with("/mail") - || path.starts_with("/ticket") - || path.starts_with("/subscription") - || path.starts_with("/nango") - || path.starts_with("/pyannote") - { - set_operation_security(item); - } - } -} - -fn set_operation_security(item: &mut PathItem) { - let security = Some(vec![SecurityRequirement::new( - "bearer_auth", - Vec::::new(), - )]); - - with_each_operation(item, |op| { - op.security = security.clone(); - }); -} - -fn clear_operation_security(item: &mut PathItem) { - with_each_operation(item, |op| { - op.security = None; - }); -} - -fn with_each_operation(item: &mut PathItem, mut f: impl FnMut(&mut Operation)) { - if let Some(op) = item.get.as_mut() { - f(op); - } - if let Some(op) = item.put.as_mut() { - f(op); - } - if let Some(op) = item.post.as_mut() { - f(op); - } - if let Some(op) = item.delete.as_mut() { - f(op); - } - if let Some(op) = item.options.as_mut() { - f(op); - } - if let Some(op) = item.head.as_mut() { - f(op); - } - if let Some(op) = item.patch.as_mut() { - f(op); - } - if let Some(op) = item.trace.as_mut() { - f(op); - } -} - -#[cfg(test)] -mod tests { - fn assert_bearer(path: &utoipa::openapi::path::PathItem, method: &str) { - let operation = match method { - "get" => path.get.as_ref().unwrap(), - "post" => path.post.as_ref().unwrap(), - _ => unreachable!("unsupported method"), - }; - let security = operation.security.as_ref().unwrap(); - - assert!(security.iter().any(|item| { - serde_json::to_value(item) - .unwrap() - .get("bearer_auth") - .is_some() - })); - } - - #[test] - fn pyannote_paths_are_prefixed_and_protected() { - let doc = super::openapi(); - assert_bearer(doc.paths.paths.get("/pyannote/v1/diarize").unwrap(), "post"); - assert_bearer( - doc.paths.paths.get("/pyannote/v1/identify").unwrap(), - "post", - ); - assert_bearer( - doc.paths.paths.get("/pyannote/v1/voiceprint").unwrap(), - "post", - ); - assert!(!doc.paths.paths.contains_key("/pyannote/v1/jobs")); - assert!(!doc.paths.paths.contains_key("/pyannote/v1/jobs/{jobId}")); - assert!(!doc.paths.paths.contains_key("/pyannote/v1/media/input")); - assert!(!doc.paths.paths.contains_key("/pyannote/v1/media/output")); - assert!(!doc.paths.paths.contains_key("/pyannote/v1/test")); - } - - #[test] - fn gen_openapi_json() { - super::write_openapi_json().unwrap(); - } -} diff --git a/apps/api/src/rate_limit.rs b/apps/api/src/rate_limit.rs deleted file mode 100644 index 543ca6f457..0000000000 --- a/apps/api/src/rate_limit.rs +++ /dev/null @@ -1,87 +0,0 @@ -use std::sync::Arc; - -use axum::{ - extract::Request, - http::StatusCode, - middleware::Next, - response::{IntoResponse, Response}, -}; -use governor::{ - Quota, RateLimiter, - clock::{Clock, DefaultClock}, - state::keyed::DefaultKeyedStateStore, -}; -use hypr_api_auth::AuthContext; - -type KeyedLimiter = RateLimiter, DefaultClock>; - -#[derive(Clone)] -pub struct RateLimitState { - limiter_pro: Arc, - limiter_free: Arc, -} - -impl RateLimitState { - pub fn builder() -> RateLimitStateBuilder { - RateLimitStateBuilder { - pro: None, - free: None, - } - } -} - -pub struct RateLimitStateBuilder { - pro: Option, - free: Option, -} - -impl RateLimitStateBuilder { - pub fn pro(mut self, quota: Quota) -> Self { - self.pro = Some(quota); - self - } - - pub fn free(mut self, quota: Quota) -> Self { - self.free = Some(quota); - self - } - - pub fn build(self) -> RateLimitState { - RateLimitState { - limiter_pro: Arc::new(RateLimiter::keyed(self.pro.expect("pro quota is required"))), - limiter_free: Arc::new(RateLimiter::keyed( - self.free.expect("free quota is required"), - )), - } - } -} - -pub async fn rate_limit( - axum::extract::State(state): axum::extract::State, - request: Request, - next: Next, -) -> Result { - if cfg!(debug_assertions) { - return Ok(next.run(request).await); - } - - if let Some(auth) = request.extensions().get::() { - let limiter = if auth.claims.is_paid() { - &state.limiter_pro - } else { - &state.limiter_free - }; - if let Err(not_until) = limiter.check_key(&auth.claims.sub) { - let wait = not_until.wait_time_from(DefaultClock::default().now()); - let retry_after = wait.as_secs().max(1).to_string(); - return Err(( - StatusCode::TOO_MANY_REQUESTS, - [("retry-after", retry_after)], - "rate limit exceeded", - ) - .into_response()); - } - } - - Ok(next.run(request).await) -} diff --git a/apps/bot/.gitignore b/apps/bot/.gitignore deleted file mode 100644 index 43370fa915..0000000000 --- a/apps/bot/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.tsbuildinfo diff --git a/apps/chrome/.gitignore b/apps/chrome/.gitignore deleted file mode 100644 index 8b6f140f03..0000000000 --- a/apps/chrome/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -.output/ -.wxt/ -node_modules/ diff --git a/apps/chrome/README.md b/apps/chrome/README.md deleted file mode 100644 index e423706abf..0000000000 --- a/apps/chrome/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# Char Chrome Extension (WXT) - -Google Meet POC: DOM parsing + native messaging to `com.char.native_host`. - -## Dev - -- `pnpm -F @hypr/chrome dev` -- WXT launches a browser profile and hot-reloads extension changes. -- `pnpm -F @hypr/chrome dev:nobrowser` (builds + watches, but does not auto-launch Chrome) -- Popup UI uses React via `@wxt-dev/module-react`. -- Styling uses Tailwind CSS v4. -- Meet badge UI is injected via WXT Shadow Root UI to isolate styles from the page. - -## No-browser dev workflow - -1. Run `pnpm -F @hypr/chrome dev:nobrowser`. -2. Keep that process running for rebuilds/HMR. -3. In Chrome, open `chrome://extensions` and enable Developer mode. -4. Click Load unpacked and select `apps/chrome/.output/chrome-mv3-dev`. -5. Test popup via the extension icon or open `chrome-extension:///popup.html`. - -Notes: - -- `http://localhost:3000` is WXT's internal dev server endpoint for extension tooling, not a normal app page. -- If you run `pnpm -F @hypr/chrome build`, load `apps/chrome/.output/chrome-mv3` instead. - -## Testing - -- `pnpm -F @hypr/chrome test:unit` -- `pnpm -F @hypr/chrome test:e2e` - -## Build / Publish - -- `pnpm -F @hypr/chrome build` (output: `apps/chrome/.output/chrome-mv3`) -- `pnpm -F @hypr/chrome zip` (output zip for Chrome Web Store upload) - -## Native host - -Requires a registered native messaging host `com.char.native_host` pointing to `char-chrome-native-host`. diff --git a/apps/chrome/assets/tailwind.css b/apps/chrome/assets/tailwind.css deleted file mode 100644 index f1d8c73cdc..0000000000 --- a/apps/chrome/assets/tailwind.css +++ /dev/null @@ -1 +0,0 @@ -@import "tailwindcss"; diff --git a/apps/chrome/entrypoints/background.ts b/apps/chrome/entrypoints/background.ts deleted file mode 100644 index 28ad7550b0..0000000000 --- a/apps/chrome/entrypoints/background.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { Browser } from "wxt/browser"; - -import { type HostMessage, parseIncomingMessage } from "./shared/host-message"; - -const NATIVE_HOST_NAME = "com.char.native_host"; - -let nativePort: Browser.runtime.Port | null = null; - -function connectNativeHost() { - if (nativePort) { - return nativePort; - } - - nativePort = browser.runtime.connectNative(NATIVE_HOST_NAME); - - nativePort.onDisconnect.addListener(() => { - const error = browser.runtime.lastError; - if (error) { - console.warn("[char] native host disconnected:", error.message); - } - nativePort = null; - }); - - return nativePort; -} - -function postToNativeHost(payload: HostMessage) { - try { - const port = connectNativeHost(); - port.postMessage(payload); - return true; - } catch (error) { - console.warn("[char] failed to post to native host:", error); - nativePort = null; - return false; - } -} - -export default defineBackground(() => { - browser.runtime.onMessage.addListener((message) => { - const payload = parseIncomingMessage(message); - if (!payload) { - return { ok: false, error: "invalid_message" }; - } - - const ok = postToNativeHost(payload); - return { ok }; - }); -}); diff --git a/apps/chrome/entrypoints/meet.content/dom.ts b/apps/chrome/entrypoints/meet.content/dom.ts deleted file mode 100644 index 82dd49f671..0000000000 --- a/apps/chrome/entrypoints/meet.content/dom.ts +++ /dev/null @@ -1,144 +0,0 @@ -const MAX_PARTICIPANTS = 30; -const MAX_PARTICIPANT_NAME_LENGTH = 80; - -export type Participant = { - name: string; - is_self: boolean; -}; - -function byTextPattern(value: string | null | undefined, patterns: string[]) { - const text = (value || "").toLowerCase(); - return patterns.some((pattern) => text.includes(pattern)); -} - -export function normalizeParticipantName( - value: string | null | undefined, -): string | null { - const name = (value || "").trim().replace(/\s+/g, " "); - if (!name || name.length > MAX_PARTICIPANT_NAME_LENGTH) { - return null; - } - - if (byTextPattern(name, ["microphone", "camera", "pin", "present"])) { - return null; - } - - return name; -} - -export function isSelfParticipant(name: string) { - const normalized = name.toLowerCase(); - return ( - normalized === "you" || - normalized.endsWith("(you)") || - normalized.includes(" (you)") - ); -} - -export function getMuteState(root: ParentNode) { - const muteSelectors = [ - "button[aria-label*='microphone' i]", - "button[aria-label*='mic' i]", - "button[data-is-muted]", - ]; - - for (const selector of muteSelectors) { - const button = root.querySelector(selector); - if (!button) { - continue; - } - - const dataMuted = button.getAttribute("data-is-muted"); - if (dataMuted === "true") { - return true; - } - if (dataMuted === "false") { - return false; - } - - const ariaPressed = button.getAttribute("aria-pressed"); - if (ariaPressed === "true") { - return true; - } - if (ariaPressed === "false") { - return false; - } - - const label = button.getAttribute("aria-label") || ""; - if (byTextPattern(label, ["turn on microphone", "unmute"])) { - return true; - } - if (byTextPattern(label, ["turn off microphone", "mute"])) { - return false; - } - } - - return false; -} - -export function getParticipantNames(root: ParentNode): Participant[] { - const names = new Map(); - - const participantElements = root.querySelectorAll( - "[data-participant-id]", - ); - - for (const participant of participantElements) { - const participantId = participant - .getAttribute("data-participant-id") - ?.trim(); - - const selfNameElement = - participant.querySelector("[data-self-name]"); - const labelElement = participant.querySelector( - "[aria-label], span[dir='auto'], span", - ); - - const rawName = - selfNameElement?.getAttribute("data-self-name") || - labelElement?.getAttribute("aria-label") || - labelElement?.textContent || - participant.getAttribute("aria-label") || - ""; - - const name = normalizeParticipantName(rawName); - if (!name) { - continue; - } - - const isSelf = Boolean(selfNameElement) || isSelfParticipant(name); - const key = participantId || name.toLowerCase(); - const existing = names.get(key); - - if (existing) { - existing.is_self = existing.is_self || isSelf; - continue; - } - - names.set(key, { name, is_self: isSelf }); - - if (names.size >= MAX_PARTICIPANTS) { - break; - } - } - - return Array.from(names.values()); -} - -export function isMeetPageActive( - location: { hostname: string; pathname: string }, - root: ParentNode, -) { - if (location.hostname !== "meet.google.com") { - return false; - } - - const codePattern = /^\/[a-z]{3}-[a-z]{4}-[a-z]{3}/; - if (codePattern.test(location.pathname)) { - return true; - } - - return Boolean( - root.querySelector("[role='toolbar'], [aria-label*='Call controls' i]"), - ); -} diff --git a/apps/chrome/entrypoints/meet.content/index.ts b/apps/chrome/entrypoints/meet.content/index.ts deleted file mode 100644 index dac1d0fe88..0000000000 --- a/apps/chrome/entrypoints/meet.content/index.ts +++ /dev/null @@ -1,173 +0,0 @@ -import "~/assets/tailwind.css"; - -import { - getMuteState, - getParticipantNames, - isMeetPageActive, - type Participant, -} from "./dom"; - -const SEND_INTERVAL_MS = 2500; -const MUTATION_DEBOUNCE_MS = 250; -const BADGE_ANCHOR_SELECTOR = - "[role='toolbar'], [aria-label*='Call controls' i], footer"; - -type MeetingStateMessage = { - type: "meeting_state"; - url: string; - is_active: true; - muted: boolean; - participants: Participant[]; -}; - -type MeetingEndedMessage = { - type: "meeting_ended"; - url: string; - is_active: false; -}; - -type HostMessage = MeetingStateMessage | MeetingEndedMessage; - -let badgeElement: HTMLDivElement | null = null; - -function buildMeetingStatePayload(): MeetingStateMessage { - return { - type: "meeting_state", - url: window.location.href, - is_active: true, - muted: getMuteState(document), - participants: getParticipantNames(document), - }; -} - -function buildMeetingEndedPayload(): MeetingEndedMessage { - return { - type: "meeting_ended", - url: window.location.href, - is_active: false, - }; -} - -function getBadgeClass(isConnected: boolean) { - const stateClass = isConnected - ? "border-[#2c7a5e] bg-[#0f2f26] text-[#8af8c5]" - : "border-[#9a6a34] bg-[#302012] text-[#f9c786]"; - - return [ - "whitespace-nowrap rounded-full border px-3 py-2 text-xs leading-none tracking-[0.01em]", - "pointer-events-none mr-[10px] self-center font-[IBM_Plex_Sans] transition-colors duration-150", - stateClass, - ].join(" "); -} - -function updateBadge(isConnected: boolean) { - if (!badgeElement) { - return; - } - - badgeElement.className = getBadgeClass(isConnected); - badgeElement.textContent = isConnected - ? "Char: connected" - : "Char: disconnected"; -} - -async function sendPayload(payload: HostMessage) { - try { - const response = (await browser.runtime.sendMessage(payload)) as - | { ok?: boolean } - | undefined; - updateBadge(Boolean(response?.ok)); - } catch (error) { - console.warn("[char] failed to send message to background:", error); - updateBadge(false); - } -} - -export default defineContentScript({ - matches: ["https://meet.google.com/*"], - cssInjectionMode: "ui", - runAt: "document_idle", - async main(ctx) { - const ui = await createShadowRootUi(ctx, { - name: "char-meet-badge", - position: "inline", - anchor: BADGE_ANCHOR_SELECTOR, - append: "first", - onMount: (container) => { - badgeElement = document.createElement("div"); - container.append(badgeElement); - updateBadge(false); - return badgeElement; - }, - onRemove: () => { - badgeElement = null; - }, - }); - - ui.autoMount(); - - let lastPayloadKey = ""; - let lastSentAt = 0; - let mutationTimeout: number | null = null; - - const maybeSendState = (force = false) => { - if (!isMeetPageActive(window.location, document)) { - if (force) { - void sendPayload(buildMeetingEndedPayload()); - } - return; - } - - const payload = buildMeetingStatePayload(); - const payloadKey = JSON.stringify(payload); - const now = Date.now(); - const shouldSend = - force || - payloadKey !== lastPayloadKey || - now - lastSentAt >= SEND_INTERVAL_MS; - - if (!shouldSend) { - return; - } - - lastPayloadKey = payloadKey; - lastSentAt = now; - void sendPayload(payload); - }; - - const observer = new MutationObserver(() => { - if (mutationTimeout !== null) { - return; - } - - mutationTimeout = window.setTimeout(() => { - mutationTimeout = null; - maybeSendState(); - }, MUTATION_DEBOUNCE_MS); - }); - - observer.observe(document.documentElement, { - childList: true, - subtree: true, - attributes: true, - attributeFilter: ["aria-label", "data-is-muted", "aria-pressed"], - }); - - maybeSendState(true); - - ctx.setInterval(() => { - maybeSendState(); - }, SEND_INTERVAL_MS); - - ctx.addEventListener(window, "beforeunload", () => { - if (mutationTimeout !== null) { - window.clearTimeout(mutationTimeout); - } - void sendPayload(buildMeetingEndedPayload()); - }); - - ctx.addEventListener(window, "wxt:locationchange", () => { - maybeSendState(true); - }); - }, -}); diff --git a/apps/chrome/entrypoints/popup/App.tsx b/apps/chrome/entrypoints/popup/App.tsx deleted file mode 100644 index 9a90f93b1c..0000000000 --- a/apps/chrome/entrypoints/popup/App.tsx +++ /dev/null @@ -1,12 +0,0 @@ -export function App() { - return ( -
-

- Char for Google Meet -

-

- Native host bridge is enabled for meeting state sync. -

-
- ); -} diff --git a/apps/chrome/entrypoints/popup/index.html b/apps/chrome/entrypoints/popup/index.html deleted file mode 100644 index e7eba9ac89..0000000000 --- a/apps/chrome/entrypoints/popup/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - Char - - - -
- - - diff --git a/apps/chrome/entrypoints/popup/main.tsx b/apps/chrome/entrypoints/popup/main.tsx deleted file mode 100644 index 09371d4434..0000000000 --- a/apps/chrome/entrypoints/popup/main.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { createRoot } from "react-dom/client"; - -import { App } from "./App"; - -const container = document.getElementById("root"); - -if (container) { - const root = createRoot(container); - root.render(); -} diff --git a/apps/chrome/entrypoints/shared/host-message.ts b/apps/chrome/entrypoints/shared/host-message.ts deleted file mode 100644 index 74152c5a4e..0000000000 --- a/apps/chrome/entrypoints/shared/host-message.ts +++ /dev/null @@ -1,121 +0,0 @@ -const MAX_URL_LENGTH = 2048; -const MAX_PARTICIPANTS = 30; -const MAX_PARTICIPANT_NAME_LENGTH = 80; - -type Participant = { - name: string; - is_self: boolean; -}; - -type MeetingStateMessage = { - type: "meeting_state"; - url: string; - is_active: true; - muted: boolean; - participants: Participant[]; -}; - -type MeetingEndedMessage = { - type: "meeting_ended"; - url: string; - is_active: false; -}; - -export type HostMessage = MeetingStateMessage | MeetingEndedMessage; - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null; -} - -function isMeetUrl(value: unknown): value is string { - if (typeof value !== "string") { - return false; - } - - const url = value.trim(); - if (!url || url.length > MAX_URL_LENGTH) { - return false; - } - - try { - const parsed = new URL(url); - return ( - parsed.protocol === "https:" && parsed.hostname === "meet.google.com" - ); - } catch { - return false; - } -} - -function parseParticipant(value: unknown): Participant | null { - if (!isRecord(value)) { - return null; - } - - const name = typeof value.name === "string" ? value.name.trim() : ""; - if (!name || name.length > MAX_PARTICIPANT_NAME_LENGTH) { - return null; - } - - if (typeof value.is_self !== "boolean") { - return null; - } - - return { - name, - is_self: value.is_self, - }; -} - -export function parseIncomingMessage(message: unknown): HostMessage | null { - if (!isRecord(message) || typeof message.type !== "string") { - return null; - } - - if (message.type === "meeting_state") { - if (message.is_active !== true || typeof message.muted !== "boolean") { - return null; - } - - const { url, participants: rawParticipants } = message; - if (!isMeetUrl(url) || !Array.isArray(rawParticipants)) { - return null; - } - - const participants: Participant[] = []; - for (const rawParticipant of rawParticipants) { - const participant = parseParticipant(rawParticipant); - if (!participant) { - continue; - } - - participants.push(participant); - if (participants.length >= MAX_PARTICIPANTS) { - break; - } - } - - return { - type: "meeting_state", - url, - is_active: true, - muted: message.muted, - participants, - }; - } - - if (message.type === "meeting_ended") { - const { url } = message; - if (message.is_active !== false || !isMeetUrl(url)) { - return null; - } - - return { - type: "meeting_ended", - url, - is_active: false, - }; - } - - return null; -} diff --git a/apps/chrome/package.json b/apps/chrome/package.json deleted file mode 100644 index c1667a6a06..0000000000 --- a/apps/chrome/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "@hypr/chrome", - "private": true, - "version": "0.0.1", - "type": "module", - "scripts": { - "dev": "wxt", - "dev:nobrowser": "WXT_NO_BROWSER=1 wxt", - "dev:firefox": "wxt -b firefox", - "build": "wxt build", - "zip": "wxt zip", - "typecheck": "tsc --noEmit", - "test:unit": "vitest run", - "test:e2e": "wxt build && playwright test", - "postinstall": "wxt prepare" - }, - "dependencies": { - "react": "^19.2.5", - "react-dom": "^19.2.5" - }, - "devDependencies": { - "@playwright/test": "^1.59.1", - "@tailwindcss/vite": "^4.2.2", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", - "@wxt-dev/module-react": "^1.2.2", - "happy-dom": "^20.8.9", - "tailwindcss": "^4.2.2", - "typescript": "^5.9.3", - "vitest": "^4.1.4", - "wxt": "^0.20.20" - } -} diff --git a/apps/chrome/playwright.config.ts b/apps/chrome/playwright.config.ts deleted file mode 100644 index 3fbe02e325..0000000000 --- a/apps/chrome/playwright.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { defineConfig } from "@playwright/test"; - -export default defineConfig({ - testDir: "tests/e2e", - fullyParallel: false, - workers: 1, - timeout: 60_000, -}); diff --git a/apps/chrome/tests/e2e/popup.spec.ts b/apps/chrome/tests/e2e/popup.spec.ts deleted file mode 100644 index 58666f6f6c..0000000000 --- a/apps/chrome/tests/e2e/popup.spec.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { chromium, expect, test } from "@playwright/test"; -import os from "node:os"; -import path from "node:path"; - -test("popup renders basic React UI", async () => { - const extensionPath = path.resolve( - import.meta.dirname, - "../../.output/chrome-mv3", - ); - - const context = await chromium.launchPersistentContext( - path.join(os.tmpdir(), "char-wxt-e2e"), - { - channel: "chromium", - headless: false, - args: [ - `--disable-extensions-except=${extensionPath}`, - `--load-extension=${extensionPath}`, - ], - }, - ); - - try { - let [background] = context.serviceWorkers(); - if (!background) { - background = await context.waitForEvent("serviceworker"); - } - - const extensionId = background.url().split("/")[2]; - const page = await context.newPage(); - - await page.goto(`chrome-extension://${extensionId}/popup.html`); - - await expect( - page.getByRole("heading", { name: "Char for Google Meet" }), - ).toBeVisible(); - await expect( - page.getByText("Native host bridge is enabled for meeting state sync."), - ).toBeVisible(); - } finally { - await context.close(); - } -}); diff --git a/apps/chrome/tests/unit/dom.test.ts b/apps/chrome/tests/unit/dom.test.ts deleted file mode 100644 index 2989c35e0f..0000000000 --- a/apps/chrome/tests/unit/dom.test.ts +++ /dev/null @@ -1,148 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; -import { describe, expect, it } from "vitest"; - -import { - getMuteState, - getParticipantNames, - isMeetPageActive, - isSelfParticipant, - normalizeParticipantName, -} from "../../entrypoints/meet.content/dom"; - -function loadFixture(name: string) { - const filePath = path.resolve(import.meta.dirname, "fixtures", name); - return fs.readFileSync(filePath, "utf8"); -} - -function renderFixture(name: string) { - document.body.innerHTML = loadFixture(name); -} - -describe("getMuteState", () => { - it("returns false when mic is unmuted", () => { - renderFixture("meet-active-unmuted.html"); - expect(getMuteState(document)).toBe(false); - }); - - it("returns true when data-is-muted is true", () => { - renderFixture("meet-active-muted.html"); - expect(getMuteState(document)).toBe(true); - }); - - it("returns true when aria-pressed is true", () => { - renderFixture("meet-self-and-filtered.html"); - expect(getMuteState(document)).toBe(true); - }); - - it("defaults to false when no mute control exists", () => { - renderFixture("meet-lobby.html"); - expect(getMuteState(document)).toBe(false); - }); -}); - -describe("getParticipantNames", () => { - it("extracts participants from real-looking DOM", () => { - renderFixture("meet-active-unmuted.html"); - - expect(getParticipantNames(document)).toEqual([ - { name: "Alice Kim", is_self: false }, - { name: "Bob Stone", is_self: false }, - ]); - }); - - it("detects self participants and filters invalid names", () => { - renderFixture("meet-self-and-filtered.html"); - - expect(getParticipantNames(document)).toEqual([ - { name: "Casey Rivera", is_self: true }, - { name: "Taylor (You)", is_self: true }, - ]); - }); - - it("caps participants at the configured maximum", () => { - renderFixture("meet-many-participants.html"); - - const participants = getParticipantNames(document); - expect(participants).toHaveLength(30); - expect(participants[0]).toEqual({ name: "Participant 01", is_self: false }); - expect(participants[29]).toEqual({ - name: "Participant 30", - is_self: false, - }); - }); - - it("deduplicates repeated participant ids", () => { - document.body.innerHTML = ` -
Alice Kim
-
Alice Kim
- `; - - expect(getParticipantNames(document)).toEqual([ - { name: "Alice Kim", is_self: false }, - ]); - }); -}); - -describe("isMeetPageActive", () => { - it("returns true for a meet code pathname", () => { - renderFixture("meet-lobby.html"); - expect( - isMeetPageActive( - { hostname: "meet.google.com", pathname: "/abc-defg-hij" }, - document, - ), - ).toBe(true); - }); - - it("returns true when call controls exist", () => { - renderFixture("meet-active-unmuted.html"); - expect( - isMeetPageActive( - { hostname: "meet.google.com", pathname: "/landing" }, - document, - ), - ).toBe(true); - }); - - it("returns false for non-meet domains", () => { - renderFixture("meet-active-unmuted.html"); - expect( - isMeetPageActive( - { hostname: "example.com", pathname: "/abc-defg-hij" }, - document, - ), - ).toBe(false); - }); - - it("returns false for meet lobby without controls", () => { - renderFixture("meet-lobby.html"); - expect( - isMeetPageActive( - { hostname: "meet.google.com", pathname: "/landing" }, - document, - ), - ).toBe(false); - }); -}); - -describe("normalizeParticipantName", () => { - it("normalizes whitespace", () => { - expect(normalizeParticipantName(" Alice Kim ")).toBe("Alice Kim"); - }); - - it("rejects control-like values", () => { - expect(normalizeParticipantName("Turn on microphone")).toBeNull(); - }); -}); - -describe("isSelfParticipant", () => { - it("detects you markers", () => { - expect(isSelfParticipant("You")).toBe(true); - expect(isSelfParticipant("Alex (You)")).toBe(true); - }); - - it("does not flag regular names", () => { - expect(isSelfParticipant("Yousef")).toBe(false); - }); -}); diff --git a/apps/chrome/tests/unit/fixtures/meet-active-muted.html b/apps/chrome/tests/unit/fixtures/meet-active-muted.html deleted file mode 100644 index b4b32133a2..0000000000 --- a/apps/chrome/tests/unit/fixtures/meet-active-muted.html +++ /dev/null @@ -1,11 +0,0 @@ -
-
- -
- -
-
- Alice Kim -
-
-
diff --git a/apps/chrome/tests/unit/fixtures/meet-active-unmuted.html b/apps/chrome/tests/unit/fixtures/meet-active-unmuted.html deleted file mode 100644 index d959b9e612..0000000000 --- a/apps/chrome/tests/unit/fixtures/meet-active-unmuted.html +++ /dev/null @@ -1,14 +0,0 @@ -
-
- -
- -
-
- Alice Kim -
-
- -
-
-
diff --git a/apps/chrome/tests/unit/fixtures/meet-lobby.html b/apps/chrome/tests/unit/fixtures/meet-lobby.html deleted file mode 100644 index 63213c51d7..0000000000 --- a/apps/chrome/tests/unit/fixtures/meet-lobby.html +++ /dev/null @@ -1,5 +0,0 @@ -
-
- -
-
diff --git a/apps/chrome/tests/unit/fixtures/meet-many-participants.html b/apps/chrome/tests/unit/fixtures/meet-many-participants.html deleted file mode 100644 index 25f532d344..0000000000 --- a/apps/chrome/tests/unit/fixtures/meet-many-participants.html +++ /dev/null @@ -1,37 +0,0 @@ -
-
-
-
Participant 01
-
Participant 02
-
Participant 03
-
Participant 04
-
Participant 05
-
Participant 06
-
Participant 07
-
Participant 08
-
Participant 09
-
Participant 10
-
Participant 11
-
Participant 12
-
Participant 13
-
Participant 14
-
Participant 15
-
Participant 16
-
Participant 17
-
Participant 18
-
Participant 19
-
Participant 20
-
Participant 21
-
Participant 22
-
Participant 23
-
Participant 24
-
Participant 25
-
Participant 26
-
Participant 27
-
Participant 28
-
Participant 29
-
Participant 30
-
Participant 31
-
Participant 32
-
-
diff --git a/apps/chrome/tests/unit/fixtures/meet-self-and-filtered.html b/apps/chrome/tests/unit/fixtures/meet-self-and-filtered.html deleted file mode 100644 index dbcf62885c..0000000000 --- a/apps/chrome/tests/unit/fixtures/meet-self-and-filtered.html +++ /dev/null @@ -1,21 +0,0 @@ -
-
- -
- -
-
-
- Ignored Name -
-
- Taylor (You) -
-
- Turn on microphone -
-
- -
-
-
diff --git a/apps/chrome/tests/unit/host-message.test.ts b/apps/chrome/tests/unit/host-message.test.ts deleted file mode 100644 index 28d8a9c4ee..0000000000 --- a/apps/chrome/tests/unit/host-message.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { parseIncomingMessage } from "../../entrypoints/shared/host-message"; - -describe("parseIncomingMessage", () => { - it("parses valid meeting_state payload", () => { - const message = parseIncomingMessage({ - type: "meeting_state", - url: "https://meet.google.com/abc-defg-hij", - is_active: true, - muted: false, - participants: [{ name: "Alice", is_self: true }], - }); - - expect(message).toEqual({ - type: "meeting_state", - url: "https://meet.google.com/abc-defg-hij", - is_active: true, - muted: false, - participants: [{ name: "Alice", is_self: true }], - }); - }); - - it("rejects invalid origin", () => { - const message = parseIncomingMessage({ - type: "meeting_state", - url: "https://example.com/abc", - is_active: true, - muted: false, - participants: [], - }); - - expect(message).toBeNull(); - }); - - it("parses meeting_ended payload", () => { - const message = parseIncomingMessage({ - type: "meeting_ended", - url: "https://meet.google.com/abc-defg-hij", - is_active: false, - }); - - expect(message).toEqual({ - type: "meeting_ended", - url: "https://meet.google.com/abc-defg-hij", - is_active: false, - }); - }); -}); diff --git a/apps/chrome/tsconfig.json b/apps/chrome/tsconfig.json deleted file mode 100644 index 6ad10a0017..0000000000 --- a/apps/chrome/tsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "./.wxt/tsconfig.json", - "compilerOptions": { - "jsx": "react-jsx" - } -} diff --git a/apps/chrome/turbo.json b/apps/chrome/turbo.json deleted file mode 100644 index c222a2210d..0000000000 --- a/apps/chrome/turbo.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "$schema": "https://turbo.build/schema.json", - "extends": ["//"], - "tasks": { - "build": { - "outputs": [".output/**"] - } - } -} diff --git a/apps/chrome/vitest.config.ts b/apps/chrome/vitest.config.ts deleted file mode 100644 index 34ae4dffdc..0000000000 --- a/apps/chrome/vitest.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defineConfig } from "vitest/config"; -import { WxtVitest } from "wxt/testing/vitest-plugin"; - -export default defineConfig({ - plugins: [WxtVitest()], - test: { - environment: "happy-dom", - include: ["tests/unit/**/*.test.ts"], - }, -}); diff --git a/apps/chrome/wxt.config.ts b/apps/chrome/wxt.config.ts deleted file mode 100644 index 8eac6c45ec..0000000000 --- a/apps/chrome/wxt.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import tailwindcss from "@tailwindcss/vite"; -import { defineConfig } from "wxt"; - -const NO_BROWSER = process.env.WXT_NO_BROWSER === "1"; - -export default defineConfig({ - modules: ["@wxt-dev/module-react"], - vite: () => ({ - plugins: [tailwindcss() as any], - }), - webExt: NO_BROWSER ? { disabled: true } : undefined, - manifest: { - name: "Char for Google Meet", - description: - "POC extension: Google Meet DOM parsing and native messaging bridge to Char desktop.", - permissions: ["nativeMessaging"], - }, -}); diff --git a/apps/cli-ui/.gitignore b/apps/cli-ui/.gitignore deleted file mode 100644 index 24e5b0a1ae..0000000000 --- a/apps/cli-ui/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.build diff --git a/apps/cli-ui/Package.swift b/apps/cli-ui/Package.swift deleted file mode 100644 index b25a19af10..0000000000 --- a/apps/cli-ui/Package.swift +++ /dev/null @@ -1,14 +0,0 @@ -// swift-tools-version: 5.9 - -import PackageDescription - -let package = Package( - name: "CliUI", - platforms: [.macOS(.v14)], - targets: [ - .executableTarget( - name: "char-cli-ui", - path: "Sources/CliUI" - ) - ] -) diff --git a/apps/cli-ui/Sources/CliUI/ContentView.swift b/apps/cli-ui/Sources/CliUI/ContentView.swift deleted file mode 100644 index 8ce08b47a6..0000000000 --- a/apps/cli-ui/Sources/CliUI/ContentView.swift +++ /dev/null @@ -1,58 +0,0 @@ -import SwiftUI - -struct ContentView: View { - @ObservedObject var state: CliUIState - - var body: some View { - HStack(spacing: 10) { - // Cancel button - Button(action: { sendAction("cancel") }) { - Image(systemName: "xmark") - .font(.system(size: 10, weight: .medium)) - .foregroundColor(.white.opacity(0.6)) - .frame(width: 24, height: 24) - .background(Color.white.opacity(0.1)) - .clipShape(Circle()) - } - .buttonStyle(.plain) - - // Recording indicator + waveform - HStack(spacing: 6) { - Circle() - .fill(state.isRecording ? Color.blue : Color.gray) - .frame(width: 8, height: 8) - - WaveformView(level: state.audioLevel, tick: state.levelTick) - } - - // Stop button - Button(action: { sendAction("stop") }) { - RoundedRectangle(cornerRadius: 2) - .fill(Color.red) - .frame(width: 10, height: 10) - .frame(width: 24, height: 24) - .background(Color.white.opacity(0.1)) - .clipShape(Circle()) - } - .buttonStyle(.plain) - } - .padding(.horizontal, 12) - .padding(.vertical, 8) - .background( - RoundedRectangle(cornerRadius: 26) - .fill(.ultraThinMaterial) - .environment(\.colorScheme, .dark) - ) - .overlay( - RoundedRectangle(cornerRadius: 26) - .stroke(Color.white.opacity(0.1), lineWidth: 0.5) - ) - } -} - -final class CliUIState: ObservableObject { - @Published var isRecording = true - @Published var audioLevel: Float = 0 - @Published var levelTick: UInt64 = 0 - @Published var status: String? -} diff --git a/apps/cli-ui/Sources/CliUI/OverlayPanel.swift b/apps/cli-ui/Sources/CliUI/OverlayPanel.swift deleted file mode 100644 index d864e76aeb..0000000000 --- a/apps/cli-ui/Sources/CliUI/OverlayPanel.swift +++ /dev/null @@ -1,37 +0,0 @@ -import AppKit -import SwiftUI - -final class OverlayPanel: NSPanel { - init(contentView: NSView) { - super.init( - contentRect: .zero, - styleMask: [.borderless, .nonactivatingPanel], - backing: .buffered, - defer: false - ) - - self.level = .floating - self.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] - self.isOpaque = false - self.backgroundColor = .clear - self.hasShadow = true - self.hidesOnDeactivate = false - self.contentView = contentView - - positionAtBottomRight() - } - - private func positionAtBottomRight() { - guard let screen = NSScreen.main else { return } - - let panelWidth: CGFloat = 220 - let panelHeight: CGFloat = 44 - let rightMargin: CGFloat = 20 - let bottomMargin: CGFloat = 20 - - let x = screen.frame.maxX - panelWidth - rightMargin - let y = screen.frame.origin.y + bottomMargin - - setFrame(NSRect(x: x, y: y, width: panelWidth, height: panelHeight), display: true) - } -} diff --git a/apps/cli-ui/Sources/CliUI/Protocol.swift b/apps/cli-ui/Sources/CliUI/Protocol.swift deleted file mode 100644 index 0e9cdab2b8..0000000000 --- a/apps/cli-ui/Sources/CliUI/Protocol.swift +++ /dev/null @@ -1,66 +0,0 @@ -import Foundation - -// MARK: - Daemon → UI (stdin) - -enum InboundMessage { - case state(StateMessage) - case levels(LevelsMessage) - case dismiss -} - -struct StateMessage: Decodable { - let recording: Bool? - let status: String? -} - -struct LevelsMessage: Decodable { - let left: Float - let right: Float -} - -extension InboundMessage { - static func parse(_ line: String) -> InboundMessage? { - guard let data = line.data(using: .utf8) else { return nil } - - struct Envelope: Decodable { - let type: String - } - - guard let envelope = try? JSONDecoder().decode(Envelope.self, from: data) else { - return nil - } - - switch envelope.type { - case "state": - guard let msg = try? JSONDecoder().decode(StateMessage.self, from: data) else { - return nil - } - return .state(msg) - case "levels": - guard let msg = try? JSONDecoder().decode(LevelsMessage.self, from: data) else { - return nil - } - return .levels(msg) - case "dismiss": - return .dismiss - default: - return nil - } - } -} - -// MARK: - UI → Daemon (stdout) - -struct OutboundAction: Encodable { - let type = "action" - let action: String -} - -func sendAction(_ action: String) { - let msg = OutboundAction(action: action) - guard let data = try? JSONEncoder().encode(msg), - let json = String(data: data, encoding: .utf8) - else { return } - print(json) - fflush(stdout) -} diff --git a/apps/cli-ui/Sources/CliUI/Waveform.swift b/apps/cli-ui/Sources/CliUI/Waveform.swift deleted file mode 100644 index eee3655ae8..0000000000 --- a/apps/cli-ui/Sources/CliUI/Waveform.swift +++ /dev/null @@ -1,30 +0,0 @@ -import SwiftUI - -struct WaveformView: View { - let level: Float - let tick: UInt64 - - private let barCount = 9 - @State private var animatedBars: [CGFloat] = Array(repeating: 0.15, count: 9) - - var body: some View { - HStack(spacing: 2) { - ForEach(0.. { - console.error(err); - process.exit(1); -}); - -child.on("exit", (code, signal) => { - if (signal) { - process.kill(process.pid, signal); - } else { - process.exit(code ?? 1); - } -}); diff --git a/apps/cli/npm/install.js b/apps/cli/npm/install.js deleted file mode 100644 index 726a7a2729..0000000000 --- a/apps/cli/npm/install.js +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env node - -const { execSync } = require("node:child_process"); -const { existsSync, mkdirSync } = require("node:fs"); -const { join } = require("node:path"); - -const REPO = "fastrepl/char"; - -const TARGETS = { - "darwin-arm64": "aarch64-apple-darwin", - "darwin-x64": "x86_64-apple-darwin", - "linux-x64": "x86_64-unknown-linux-gnu", - "linux-arm64": "aarch64-unknown-linux-gnu", -}; - -const target = TARGETS[`${process.platform}-${process.arch}`]; -if (!target) { - console.error(`Unsupported platform: ${process.platform}-${process.arch}`); - process.exit(1); -} - -const version = require("./package.json").version; -const tag = `cli_v${version}`; -const archive = `char-${version}-${target}.tar.xz`; -const url = `https://github.com/${REPO}/releases/download/${tag}/${archive}`; - -const binDir = join(__dirname, "bin"); -if (!existsSync(binDir)) { - mkdirSync(binDir, { recursive: true }); -} - -const dest = join(binDir, "char"); -if (existsSync(dest)) { - process.exit(0); -} - -console.error(`Downloading char from ${url}`); -const tmp = join(require("node:os").tmpdir(), archive); -execSync(`curl -fsSL -o "${tmp}" "${url}"`, { stdio: "inherit" }); -execSync(`tar -xf "${tmp}" -C "${binDir}"`, { stdio: "inherit" }); -execSync(`rm -f "${tmp}"`); -execSync(`chmod +x "${dest}"`); - -const uiDest = join(binDir, "char-cli-ui"); -if (existsSync(uiDest)) { - execSync(`chmod +x "${uiDest}"`); -} diff --git a/apps/cli/npm/package.json b/apps/cli/npm/package.json deleted file mode 100644 index 3fb57e3a7e..0000000000 --- a/apps/cli/npm/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "char", - "version": "0.0.0", - "description": "Live transcription and audio tools", - "license": "GPL-3.0", - "repository": { - "type": "git", - "url": "git+https://github.com/fastrepl/char.git", - "directory": "apps/cli/npm" - }, - "homepage": "https://char.com", - "bugs": { - "url": "https://github.com/fastrepl/char/issues" - }, - "keywords": [ - "char", - "cli", - "transcription", - "notes" - ], - "bin": { - "char": "bin/run.js" - }, - "scripts": { - "postinstall": "node install.js" - }, - "files": [ - "bin/", - "install.js" - ], - "engines": { - "node": ">=18" - } -} diff --git a/apps/cli/src/app.rs b/apps/cli/src/app.rs deleted file mode 100644 index bd6846e74b..0000000000 --- a/apps/cli/src/app.rs +++ /dev/null @@ -1,97 +0,0 @@ -use crate::config::paths::{self, AppPaths}; -#[cfg(feature = "_cli-audio")] -use crate::stt; - -pub struct AppContext { - analytics: hypr_analytics::AnalyticsClient, - paths: AppPaths, - #[cfg(feature = "_cli-audio")] - quiet: bool, - trace_buffer: crate::OptTraceBuffer, -} - -impl AppContext { - pub fn new( - base: Option<&std::path::Path>, - quiet: bool, - trace_buffer: crate::OptTraceBuffer, - ) -> Self { - #[cfg(not(feature = "_cli-audio"))] - let _ = quiet; - - let paths = paths::resolve_paths(base); - - Self { - analytics: analytics_client(), - paths, - #[cfg(feature = "_cli-audio")] - quiet, - trace_buffer, - } - } - - pub fn track_command(&self, subcommand: &'static str) { - let client = self.analytics.clone(); - tokio::spawn(async move { - let machine_id = hypr_host::fingerprint(); - let payload = hypr_analytics::AnalyticsPayload::builder("cli_command_invoked") - .with("subcommand", subcommand) - .with("app_identifier", "com.char.cli") - .with( - "app_version", - option_env!("APP_VERSION").unwrap_or(env!("CARGO_PKG_VERSION")), - ) - .build(); - let _ = client.event(machine_id, payload).await; - }); - } - - #[cfg(feature = "_cli-audio")] - pub fn quiet(&self) -> bool { - self.quiet - } - - #[cfg(feature = "_cli-audio")] - pub fn paths(&self) -> &AppPaths { - &self.paths - } - - #[cfg(feature = "_cli-audio")] - pub fn stt_overrides( - &self, - provider: Option, - base_url: Option, - api_key: Option, - model: Option, - language: String, - ) -> stt::SttOverrides { - stt::SttOverrides { - provider, - base_url, - api_key, - model, - language, - models_base: self.paths.models_base.clone(), - } - } - - #[cfg(feature = "_cli-tui")] - pub fn trace_buffer(&self) -> crate::OptTraceBuffer { - self.trace_buffer.clone() - } - - #[cfg(not(feature = "_cli-tui"))] - pub fn trace_buffer(&self) -> crate::OptTraceBuffer { - self.trace_buffer - } -} - -fn analytics_client() -> hypr_analytics::AnalyticsClient { - let mut builder = hypr_analytics::AnalyticsClientBuilder::default(); - if std::env::var_os("DO_NOT_TRACK").is_none() - && let Some(key) = option_env!("POSTHOG_API_KEY") - { - builder = builder.with_posthog(key); - } - builder.build() -} diff --git a/apps/cli/src/cli.rs b/apps/cli/src/cli.rs deleted file mode 100644 index eb02ae3e54..0000000000 --- a/apps/cli/src/cli.rs +++ /dev/null @@ -1,357 +0,0 @@ -use clap::{CommandFactory, Parser, Subcommand, ValueEnum}; -use clap_verbosity_flag::{InfoLevel, Verbosity}; - -/// Live transcription and audio tools -#[derive(Parser)] -#[command( - name = "char", - version = option_env!("APP_VERSION").unwrap_or(env!("CARGO_PKG_VERSION")), - propagate_version = true, - after_help = "Docs: https://cli.char.com\nDiscussions: https://github.com/fastrepl/char/discussions/4788\nBugs: https://github.com/fastrepl/char/issues" -)] -pub struct Cli { - #[command(subcommand)] - pub command: Option, - - #[arg(long)] - pub no_color: bool, - - #[command(flatten)] - pub verbose: Verbosity, -} - -pub(crate) fn parse_base_url(value: &str) -> Result { - let parsed = url::Url::parse(value).map_err(|e| format!("invalid URL '{value}': {e}"))?; - if parsed.scheme() != "http" && parsed.scheme() != "https" { - return Err(format!( - "invalid URL '{value}': scheme must be http or https" - )); - } - Ok(value.to_string()) -} - -#[derive(Clone, Copy, Debug, ValueEnum)] -pub enum OutputFormat { - Pretty, - Json, -} - -#[derive(Subcommand, strum::IntoStaticStr)] -#[strum(serialize_all = "snake_case")] -#[allow(clippy::large_enum_variant)] -pub enum Commands { - #[cfg(feature = "_cli-audio")] - /// Transcribe an audio file - Transcribe { - #[command(flatten)] - args: crate::commands::transcribe::Args, - }, - #[cfg(feature = "_cli-audio")] - /// Manage local models - Models { - #[command(flatten)] - args: crate::commands::model::Args, - }, - /// Generate shell completions - Completions { - #[arg(value_enum)] - shell: clap_complete::Shell, - }, - #[cfg(feature = "_cli-audio")] - /// Play an audio file - Play { - #[command(flatten)] - args: crate::commands::play::Args, - }, - #[cfg(feature = "_cli-audio")] - /// Record audio to an MP3 file - Record { - #[command(flatten)] - args: crate::commands::record::Args, - }, - #[cfg(feature = "_cli-skill-update")] - /// Install char skill for AI coding agents - Skill { - #[command(subcommand)] - command: crate::commands::skill::Commands, - }, - #[cfg(feature = "_cli-desktop")] - /// Open the desktop app or download page - Desktop, - #[cfg(feature = "_cli-db")] - /// Manage desktop SQLite data - Db { - #[command(flatten)] - args: hypr_db_cli::Args, - }, - #[cfg(feature = "_cli-desktop")] - /// Report a bug on GitHub - #[command(hide = true)] - Bug, - #[cfg(feature = "_cli-desktop")] - /// Open char.com - #[command(hide = true)] - Hello, - #[cfg(feature = "_cli-skill-update")] - /// Update char to the latest version - Update, - - #[cfg(feature = "todo")] - /// Manage todos and automations - Todo { - #[command(subcommand)] - command: Option, - }, -} - -impl Commands { - pub fn base_override(&self) -> Option<&std::path::Path> { - match self { - #[cfg(feature = "_cli-audio")] - Commands::Transcribe { args } => args.base.as_deref(), - #[cfg(feature = "_cli-audio")] - Commands::Models { args } => args.base.as_deref(), - #[cfg(feature = "_cli-audio")] - Commands::Play { args } => args.base.as_deref(), - #[cfg(feature = "_cli-audio")] - Commands::Record { args } => args.base.as_deref(), - #[cfg(feature = "_cli-db")] - Commands::Db { args } => args.base.as_deref(), - _ => None, - } - } -} - -pub fn generate_completions(shell: clap_complete::Shell) { - let mut cmd = Cli::command(); - clap_complete::generate(shell, &mut cmd, "char", &mut std::io::stdout()); -} - -#[cfg(test)] -mod tests { - use super::*; - - fn render_help(command: &mut clap::Command) -> String { - let mut bytes = Vec::new(); - command.write_long_help(&mut bytes).unwrap(); - String::from_utf8(bytes).unwrap() - } - - #[test] - fn verify_cli() { - Cli::command().debug_assert(); - } - - #[test] - #[cfg(feature = "_cli-audio")] - fn root_help_only_shows_truly_global_options() { - let mut command = Cli::command(); - let help = render_help(&mut command); - - assert!(help.contains("--no-color")); - assert!(!help.contains("--base-url")); - assert!(!help.contains("--api-key")); - assert!(!help.contains("--model ")); - assert!(!help.contains("--language ")); - assert!(!help.contains("--base ")); - } - - #[test] - #[cfg(all( - feature = "_cli-desktop", - not(feature = "_cli-audio"), - not(feature = "_cli-skill-update"), - not(feature = "todo") - ))] - fn desktop_help_stays_lightweight() { - let mut command = Cli::command(); - let help = render_help(&mut command); - - assert!(help.contains("desktop")); - assert!(help.contains("completions")); - assert!(!help.contains("transcribe")); - assert!(!help.contains("models")); - assert!(!help.contains("record")); - assert!(!help.contains("play")); - assert!(!help.contains("update")); - assert!(!help.contains("meetings")); - assert!(!help.contains("export")); - } - - #[test] - #[cfg(feature = "_cli-db")] - fn desktop_db_help_shows_db_command() { - let mut command = Cli::command(); - let help = render_help(&mut command); - - assert!(help.contains("db")); - } - - #[test] - #[cfg(feature = "_cli-db")] - fn db_help_renders() { - let mut command = Cli::command(); - let mut db = command.find_subcommand_mut("db").unwrap().clone(); - let help = render_help(&mut db); - - assert!(help.contains("templates")); - assert!(help.contains("--base ")); - } - - #[test] - #[cfg(feature = "_cli-db")] - fn db_templates_help_renders() { - let mut command = Cli::command(); - let mut db = command.find_subcommand_mut("db").unwrap().clone(); - let mut templates = db.find_subcommand_mut("templates").unwrap().clone(); - let help = render_help(&mut templates); - - assert!(help.contains("list")); - assert!(help.contains("get")); - assert!(help.contains("upsert")); - assert!(help.contains("delete")); - } - - #[test] - #[cfg(feature = "_cli-audio")] - fn transcribe_help_keeps_stt_config() { - let mut command = Cli::command(); - let mut transcribe = command.find_subcommand_mut("transcribe").unwrap().clone(); - let help = render_help(&mut transcribe); - - assert!(help.contains("--base-url")); - assert!(help.contains("--api-key")); - assert!(help.contains("--model ")); - assert!(help.contains("--language ")); - assert!(help.contains("--base ")); - } - - #[test] - #[cfg(all(feature = "_cli-audio", target_os = "macos"))] - fn transcribe_accepts_whispercpp_provider() { - let input = tempfile::NamedTempFile::new().unwrap(); - Cli::try_parse_from([ - "char", - "transcribe", - "--input", - input.path().to_str().unwrap(), - "--provider", - "whispercpp", - ]) - .unwrap(); - } - - #[test] - #[cfg(all( - feature = "_cli-audio", - target_os = "macos", - any(target_arch = "arm", target_arch = "aarch64") - ))] - fn transcribe_accepts_cactus_provider_on_apple_silicon() { - let input = tempfile::NamedTempFile::new().unwrap(); - Cli::try_parse_from([ - "char", - "transcribe", - "--input", - input.path().to_str().unwrap(), - "--provider", - "cactus", - ]) - .unwrap(); - } - - #[test] - #[cfg(feature = "_cli-audio")] - fn models_help_only_shows_base_override() { - let mut command = Cli::command(); - let mut models = command.find_subcommand_mut("models").unwrap().clone(); - let help = render_help(&mut models); - - assert!(help.contains("--base ")); - assert!(help.contains("list")); - assert!(help.contains("download")); - assert!(help.contains("delete")); - assert!(!help.contains("paths")); - assert!(!help.contains("cactus")); - assert!(!help.contains("--supported")); - assert!(!help.contains("--base-url")); - assert!(!help.contains("--api-key")); - assert!(!help.contains("--model ")); - assert!(!help.contains("--language ")); - } - - #[test] - #[cfg(feature = "_cli-audio")] - fn record_rejects_stt_only_flags() { - match Cli::try_parse_from(["char", "record", "--api-key", "secret"]) { - Ok(_) => panic!("record unexpectedly accepted --api-key"), - Err(error) => assert_eq!(error.kind(), clap::error::ErrorKind::UnknownArgument), - } - } - - #[test] - #[cfg(feature = "_cli-audio")] - fn model_delete_uses_long_only_force() { - Cli::try_parse_from(["char", "models", "delete", "tiny", "--force"]).unwrap(); - - match Cli::try_parse_from(["char", "models", "delete", "tiny", "-f"]) { - Ok(_) => panic!("models delete unexpectedly accepted -f"), - Err(error) => assert_eq!(error.kind(), clap::error::ErrorKind::UnknownArgument), - } - } - - #[test] - #[cfg(feature = "_cli-audio")] - fn models_list_accepts_json_format() { - let mut command = Cli::command(); - let mut list = command - .find_subcommand_mut("models") - .unwrap() - .find_subcommand_mut("list") - .unwrap() - .clone(); - let help = render_help(&mut list); - - assert!(help.contains("json")); - } - - #[test] - #[cfg(all( - feature = "_cli-audio", - feature = "_cli-skill-update", - feature = "_cli-db" - ))] - fn generate_docs_standalone() { - let cmd = Cli::command(); - let json = cli_docs::generate_json(&cmd); - - let json_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../cli-web/src/data/cli.gen.json"); - std::fs::create_dir_all(json_path.parent().unwrap()).unwrap(); - std::fs::write(&json_path, json).unwrap(); - } - - #[test] - #[cfg(all( - feature = "_cli-desktop", - not(feature = "_cli-audio"), - not(feature = "_cli-skill-update"), - not(feature = "todo") - ))] - fn generate_docs_desktop() { - let cmd = Cli::command(); - let md = cli_docs::generate(&cmd); - - let frontmatter = "\ ---- -title: \"CLI Reference\" -section: \"CLI\" -description: \"Command-line reference for the char CLI\" ----\n\n"; - - let mdx_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../web/content/docs/cli/index.mdx"); - std::fs::create_dir_all(mdx_path.parent().unwrap()).unwrap(); - std::fs::write(&mdx_path, format!("{frontmatter}{md}")).unwrap(); - } -} diff --git a/apps/cli/src/commands/bug.rs b/apps/cli/src/commands/bug.rs deleted file mode 100644 index 031bac247f..0000000000 --- a/apps/cli/src/commands/bug.rs +++ /dev/null @@ -1,14 +0,0 @@ -use crate::error::{CliError, CliResult}; - -const BUG_URL: &str = "https://github.com/fastrepl/char/issues/new?labels=cli"; - -pub fn run() -> CliResult<()> { - if let Err(e) = open::that(BUG_URL) { - return Err(CliError::operation_failed( - "open bug report page", - format!("{e}\nPlease visit: {BUG_URL}"), - )); - } - - Ok(()) -} diff --git a/apps/cli/src/commands/desktop.rs b/apps/cli/src/commands/desktop.rs deleted file mode 100644 index 798da0ce11..0000000000 --- a/apps/cli/src/commands/desktop.rs +++ /dev/null @@ -1,55 +0,0 @@ -use crate::error::{CliError, CliResult}; - -const DOWNLOAD_URL: &str = "https://char.com/download"; -const DESKTOP_DEEPLINKS: &[&str] = &[ - "hyprnote://focus", - "hyprnote-nightly://focus", - "hyprnote-staging://focus", - "hypr://focus", -]; -const DESKTOP_DATA_FOLDERS: &[&str] = &[ - "hyprnote", - "com.hyprnote.dev", - "com.hyprnote.staging", - "com.hyprnote.nightly", - "com.hyprnote.stable", -]; - -pub enum DesktopAction { - OpenedApp, - OpenedDownloadPage, -} - -pub fn run() -> CliResult { - if !desktop_app_exists() { - if let Err(e) = open::that(DOWNLOAD_URL) { - return Err(CliError::operation_failed( - "open desktop app or download page", - format!("{e}\nPlease visit: {DOWNLOAD_URL}"), - )); - } - - return Ok(DesktopAction::OpenedDownloadPage); - } - - for deeplink in DESKTOP_DEEPLINKS { - if open::that(deeplink).is_ok() { - return Ok(DesktopAction::OpenedApp); - } - } - - Err(CliError::operation_failed( - "open desktop app", - "Desktop app appears to be installed, but no registered deeplink responded".to_string(), - )) -} - -fn desktop_app_exists() -> bool { - let Some(data_dir) = dirs::data_dir() else { - return false; - }; - - DESKTOP_DATA_FOLDERS - .iter() - .any(|folder| data_dir.join(folder).exists()) -} diff --git a/apps/cli/src/commands/hello.rs b/apps/cli/src/commands/hello.rs deleted file mode 100644 index ca2d277ec1..0000000000 --- a/apps/cli/src/commands/hello.rs +++ /dev/null @@ -1,14 +0,0 @@ -use crate::error::{CliError, CliResult}; - -const HELLO_URL: &str = "https://char.com"; - -pub fn run() -> CliResult<()> { - if let Err(e) = open::that(HELLO_URL) { - return Err(CliError::operation_failed( - "open char.com", - format!("{e}\nPlease visit: {HELLO_URL}"), - )); - } - - Ok(()) -} diff --git a/apps/cli/src/commands/integration/claude.rs b/apps/cli/src/commands/integration/claude.rs deleted file mode 100644 index cbdefd04a0..0000000000 --- a/apps/cli/src/commands/integration/claude.rs +++ /dev/null @@ -1,48 +0,0 @@ -use clap::Subcommand; - -use crate::error::{CliError, CliResult}; - -#[derive(Subcommand)] -pub enum Commands { - /// Receive a hook event from Claude Code (reads JSON from stdin) - Notify, - /// Install char as a Claude Code hook handler - Install, - /// Remove char from Claude Code hooks - Uninstall, -} - -pub async fn run(command: Commands) -> CliResult<()> { - match command { - Commands::Notify => notify(), - Commands::Install => install(), - Commands::Uninstall => uninstall(), - } -} - -fn notify() -> CliResult<()> { - let event = super::read_stdin_json()?; - - // TODO: write to app DB - super::print_pretty_json(&event) -} - -fn install() -> CliResult<()> { - let response = hypr_agent_core::install_cli(hypr_agent_core::InstallCliRequest { - provider: hypr_agent_core::ProviderKind::Claude, - }) - .map_err(|e| CliError::operation_failed("install claude integration", e))?; - - eprintln!("{}", response.message); - Ok(()) -} - -fn uninstall() -> CliResult<()> { - let response = hypr_agent_core::uninstall_cli(hypr_agent_core::UninstallCliRequest { - provider: hypr_agent_core::ProviderKind::Claude, - }) - .map_err(|e| CliError::operation_failed("uninstall claude integration", e))?; - - eprintln!("{}", response.message); - Ok(()) -} diff --git a/apps/cli/src/commands/integration/codex.rs b/apps/cli/src/commands/integration/codex.rs deleted file mode 100644 index 3c2ce4a324..0000000000 --- a/apps/cli/src/commands/integration/codex.rs +++ /dev/null @@ -1,52 +0,0 @@ -use clap::Subcommand; - -use crate::error::{CliError, CliResult}; - -#[derive(Subcommand)] -pub enum Commands { - /// Receive a notification from Codex - Notify { - /// JSON payload from Codex - payload: String, - }, - /// Install char as the Codex notify handler - Install, - /// Remove char from the Codex notify handler - Uninstall, -} - -pub async fn run(command: Commands) -> CliResult<()> { - match command { - Commands::Notify { payload } => notify(&payload), - Commands::Install => install(), - Commands::Uninstall => uninstall(), - } -} - -fn notify(payload: &str) -> CliResult<()> { - let event: hypr_codex::NotifyEvent = serde_json::from_str(payload) - .map_err(|e| CliError::invalid_argument("payload", payload.to_string(), e.to_string()))?; - - // TODO: write to app DB - super::print_pretty_json(&event) -} - -fn install() -> CliResult<()> { - let response = hypr_agent_core::install_cli(hypr_agent_core::InstallCliRequest { - provider: hypr_agent_core::ProviderKind::Codex, - }) - .map_err(|e| CliError::operation_failed("install codex integration", e))?; - - eprintln!("{}", response.message); - Ok(()) -} - -fn uninstall() -> CliResult<()> { - let response = hypr_agent_core::uninstall_cli(hypr_agent_core::UninstallCliRequest { - provider: hypr_agent_core::ProviderKind::Codex, - }) - .map_err(|e| CliError::operation_failed("uninstall codex integration", e))?; - - eprintln!("{}", response.message); - Ok(()) -} diff --git a/apps/cli/src/commands/integration/mod.rs b/apps/cli/src/commands/integration/mod.rs deleted file mode 100644 index 4d24253039..0000000000 --- a/apps/cli/src/commands/integration/mod.rs +++ /dev/null @@ -1,28 +0,0 @@ -pub mod claude; -pub mod codex; -pub mod opencode; - -use serde::Serialize; - -use crate::error::{CliError, CliResult}; - -pub(super) fn read_stdin() -> CliResult { - std::io::read_to_string(std::io::stdin()) - .map_err(|e| CliError::operation_failed("read stdin", e.to_string())) -} - -pub(super) fn read_stdin_json() -> CliResult { - let input = read_stdin()?; - - serde_json::from_str(&input) - .map_err(|e| CliError::invalid_argument("stdin", input, e.to_string())) -} - -pub(super) fn print_pretty_json(event: &impl Serialize) -> CliResult<()> { - println!( - "{}", - serde_json::to_string_pretty(event) - .map_err(|e| CliError::operation_failed("serialize", e.to_string()))? - ); - Ok(()) -} diff --git a/apps/cli/src/commands/integration/opencode.rs b/apps/cli/src/commands/integration/opencode.rs deleted file mode 100644 index deab6cd00b..0000000000 --- a/apps/cli/src/commands/integration/opencode.rs +++ /dev/null @@ -1,52 +0,0 @@ -use clap::Subcommand; - -use crate::error::{CliError, CliResult}; - -#[derive(Subcommand)] -pub enum Commands { - /// Receive a hook event from OpenCode - Notify { - /// JSON payload from OpenCode - payload: String, - }, - /// Install char as an OpenCode plugin - Install, - /// Remove char from OpenCode plugins - Uninstall, -} - -pub async fn run(command: Commands) -> CliResult<()> { - match command { - Commands::Notify { payload } => notify(&payload), - Commands::Install => install(), - Commands::Uninstall => uninstall(), - } -} - -fn notify(payload: &str) -> CliResult<()> { - let event: serde_json::Value = serde_json::from_str(payload) - .map_err(|e| CliError::invalid_argument("payload", payload.to_string(), e.to_string()))?; - - // TODO: write to app DB - super::print_pretty_json(&event) -} - -fn install() -> CliResult<()> { - let response = hypr_agent_core::install_cli(hypr_agent_core::InstallCliRequest { - provider: hypr_agent_core::ProviderKind::Opencode, - }) - .map_err(|e| CliError::operation_failed("install opencode integration", e))?; - - eprintln!("{}", response.message); - Ok(()) -} - -fn uninstall() -> CliResult<()> { - let response = hypr_agent_core::uninstall_cli(hypr_agent_core::UninstallCliRequest { - provider: hypr_agent_core::ProviderKind::Opencode, - }) - .map_err(|e| CliError::operation_failed("uninstall opencode integration", e))?; - - eprintln!("{}", response.message); - Ok(()) -} diff --git a/apps/cli/src/commands/mod.rs b/apps/cli/src/commands/mod.rs deleted file mode 100644 index 62207bbe72..0000000000 --- a/apps/cli/src/commands/mod.rs +++ /dev/null @@ -1,119 +0,0 @@ -#[cfg(feature = "_cli-audio")] -pub mod transcribe; -#[cfg(feature = "_cli-audio")] -pub(crate) mod update_check; - -#[cfg(feature = "todo")] -pub mod integration; -#[cfg(feature = "todo")] -pub mod todo; - -#[cfg(feature = "_cli-desktop")] -pub mod bug; -#[cfg(feature = "_cli-desktop")] -pub mod desktop; -#[cfg(feature = "_cli-desktop")] -pub mod hello; -#[cfg(feature = "_cli-audio")] -pub mod model; -#[cfg(feature = "_cli-audio")] -pub mod play; -#[cfg(feature = "_cli-audio")] -pub mod record; -#[cfg(feature = "_cli-skill-update")] -pub mod skill; -#[cfg(feature = "_cli-skill-update")] -pub mod update; - -use std::path::{Path, PathBuf}; - -use crate::app::AppContext; -use crate::cli::{Cli, Commands as CliCommand}; -use crate::error::{CliError, CliResult}; - -pub(crate) fn resolve_session_dir(base: Option<&Path>, timestamp: &str) -> CliResult { - let base = base.map(Path::to_path_buf).unwrap_or_else(|| { - dirs::data_dir() - .unwrap_or_else(std::env::temp_dir) - .join("char") - }); - - let mut dir = base.join(timestamp); - if !dir.exists() { - std::fs::create_dir_all(&dir) - .map_err(|e| CliError::operation_failed("create session directory", e.to_string()))?; - return Ok(dir); - } - - for i in 1.. { - dir = base.join(format!("{timestamp}-{i}")); - if !dir.exists() { - std::fs::create_dir_all(&dir).map_err(|e| { - CliError::operation_failed("create session directory", e.to_string()) - })?; - return Ok(dir); - } - } - - unreachable!() -} - -pub async fn run(ctx: &AppContext, command: Option) -> CliResult<()> { - #[cfg(not(any(feature = "_cli-audio", feature = "_cli-skill-update")))] - let _ = ctx; - - match command { - #[cfg(feature = "_cli-audio")] - Some(CliCommand::Transcribe { args }) => transcribe::run(ctx, args).await, - #[cfg(feature = "_cli-audio")] - Some(CliCommand::Models { args }) => model::run(ctx, args).await, - #[cfg(feature = "_cli-audio")] - Some(CliCommand::Play { args }) => play::run(ctx, args).await, - #[cfg(feature = "_cli-audio")] - Some(CliCommand::Record { args }) => record::run(ctx, args).await, - #[cfg(feature = "_cli-skill-update")] - Some(CliCommand::Skill { command }) => skill::run(ctx, command).await, - Some(CliCommand::Completions { shell }) => { - crate::cli::generate_completions(shell); - Ok(()) - } - #[cfg(feature = "_cli-desktop")] - Some(CliCommand::Desktop) => { - use desktop::DesktopAction; - match desktop::run()? { - DesktopAction::OpenedApp => eprintln!("Opened desktop app"), - DesktopAction::OpenedDownloadPage => { - eprintln!("Desktop app not found — opened download page") - } - } - Ok(()) - } - #[cfg(feature = "_cli-db")] - Some(CliCommand::Db { args }) => hypr_db_cli::run(args) - .await - .map_err(|e| e.to_string().into()), - #[cfg(feature = "_cli-desktop")] - Some(CliCommand::Bug) => { - bug::run()?; - eprintln!("Opened bug report page in browser"); - Ok(()) - } - #[cfg(feature = "_cli-desktop")] - Some(CliCommand::Hello) => { - hello::run()?; - eprintln!("Opened char.com in browser"); - Ok(()) - } - #[cfg(feature = "_cli-skill-update")] - Some(CliCommand::Update) => update::run(), - #[cfg(feature = "todo")] - Some(CliCommand::Todo { command }) => todo::run(command).await, - None => { - use clap::CommandFactory; - - Cli::command().print_help().ok(); - println!(); - Ok(()) - } - } -} diff --git a/apps/cli/src/commands/model/delete.rs b/apps/cli/src/commands/model/delete.rs deleted file mode 100644 index 9ce75d21bf..0000000000 --- a/apps/cli/src/commands/model/delete.rs +++ /dev/null @@ -1,35 +0,0 @@ -use std::io::{IsTerminal, Write}; -use std::path::Path; - -use hypr_local_model::LocalModel; - -use crate::error::{CliError, CliResult}; - -pub(super) async fn delete(model: LocalModel, models_base: &Path, force: bool) -> CliResult<()> { - if !force && std::io::stderr().is_terminal() { - eprint!("Delete {}? [y/N] ", model.display_name()); - std::io::stderr() - .flush() - .map_err(|e| CliError::operation_failed("prompt", e.to_string()))?; - let mut input = String::new(); - std::io::stdin() - .read_line(&mut input) - .map_err(|e| CliError::operation_failed("read confirmation", e.to_string()))?; - if !input.trim().eq_ignore_ascii_case("y") { - eprintln!("Cancelled"); - return Ok(()); - } - } - - let manager = super::make_manager(models_base, None); - - if let Err(e) = manager.delete(&model).await { - return Err(CliError::operation_failed( - "delete model", - format!("{}: {e}", model.cli_name()), - )); - } - - eprintln!("Deleted {}", model.display_name()); - Ok(()) -} diff --git a/apps/cli/src/commands/model/download.rs b/apps/cli/src/commands/model/download.rs deleted file mode 100644 index d37d043ae7..0000000000 --- a/apps/cli/src/commands/model/download.rs +++ /dev/null @@ -1,118 +0,0 @@ -use std::path::Path; -use std::time::Duration; - -use hypr_local_model::LocalModel; -use tokio::sync::mpsc; - -use super::runtime; -use crate::error::{CliError, CliResult}; -use crate::tui::{InlineViewport, SPINNER}; - -pub(super) async fn download( - model: LocalModel, - models_base: &Path, - trace_buffer: crate::OptTraceBuffer, -) -> CliResult<()> { - let (progress_tx, mut progress_rx) = mpsc::unbounded_channel(); - - let manager = super::make_manager(models_base, Some(progress_tx)); - - if manager.is_downloaded(&model).await.unwrap_or(false) { - eprintln!( - "Model already downloaded: {} ({})", - model.display_name(), - model.install_path(models_base).display() - ); - return Ok(()); - } - - let mut viewport = if trace_buffer.is_some() { - InlineViewport::stderr(5, trace_buffer).ok() - } else { - None - }; - - if let Err(e) = manager.download(&model).await { - drop(viewport); - return Err(CliError::operation_failed( - "start model download", - format!("{}: {e}", model.cli_name()), - )); - } - - let mut pct: u8 = 0; - let mut spinner_idx: usize = 0; - let mut tick = tokio::time::interval(Duration::from_millis(80)); - - loop { - let done = tokio::select! { - event = progress_rx.recv() => { - match event { - Some(runtime::DownloadEvent::Progress(p)) => { pct = p; false } - Some(runtime::DownloadEvent::Completed | runtime::DownloadEvent::Failed) | None => true, - } - } - _ = tick.tick() => { - spinner_idx = (spinner_idx + 1) % SPINNER.len(); - false - } - }; - - draw_download(&mut viewport, &model, spinner_idx, pct); - - if done { - break; - } - } - - while manager.is_downloading(&model).await { - tokio::time::sleep(Duration::from_millis(120)).await; - } - - if let Some(ref mut vp) = viewport { - vp.clear() - .map_err(|e| CliError::operation_failed("clear viewport", e.to_string()))?; - } - drop(viewport); - - if manager.is_downloaded(&model).await.unwrap_or(false) { - eprintln!( - "Downloaded {} -> {}", - model.display_name(), - model.install_path(models_base).display() - ); - Ok(()) - } else { - Err(CliError::operation_failed( - "download model", - model.cli_name().to_string(), - )) - } -} - -fn draw_download( - viewport: &mut Option, - model: &LocalModel, - spinner_idx: usize, - pct: u8, -) { - if let Some(vp) = viewport { - vp.poll_input(); - let name = model.display_name(); - let pct_str = format!("{}%", pct); - vp.draw_strings(&[ - format!( - "{} Downloading {}... {}", - SPINNER[spinner_idx], name, pct_str - ), - format_gauge(pct), - ]); - } -} - -fn format_gauge(pct: u8) -> String { - let width = 40; - let filled = (pct as usize * width) / 100; - let empty = width - filled; - format!(" [{}{}]", "█".repeat(filled), "░".repeat(empty),) -} diff --git a/apps/cli/src/commands/model/list.rs b/apps/cli/src/commands/model/list.rs deleted file mode 100644 index 877847d4a9..0000000000 --- a/apps/cli/src/commands/model/list.rs +++ /dev/null @@ -1,149 +0,0 @@ -use std::path::Path; - -use comfy_table::{ContentArrangement, Table, presets::UTF8_FULL_CONDENSED}; -use hypr_local_model::LocalModel; -use hypr_model_downloader::{DownloadableModel, ModelDownloadManager}; - -use crate::cli::OutputFormat; -use crate::error::CliResult; - -#[derive(Clone, Debug, serde::Serialize)] -pub(crate) struct ModelRow { - pub(crate) name: String, - pub(crate) kind: String, - pub(crate) status: String, - pub(crate) downloaded: bool, - pub(crate) downloadable: bool, - pub(crate) available_on_current_platform: bool, - pub(crate) display_name: String, - pub(crate) description: String, - pub(crate) install_path: String, -} - -pub(crate) async fn collect_model_rows( - models: &[LocalModel], - models_base: &Path, - manager: &ModelDownloadManager, -) -> Vec { - let mut rows = Vec::new(); - for model in models { - let downloadable = model.download_url().is_some(); - let available_on_current_platform = model.is_available_on_current_platform(); - - let status = match manager.is_downloaded(model).await { - Ok(true) => "downloaded", - Ok(false) if downloadable && available_on_current_platform => "available", - Ok(false) if downloadable => "unsupported", - Ok(false) => continue, - Err(_) => "error", - }; - - rows.push(ModelRow { - name: model.cli_name().to_string(), - kind: model.kind().to_string(), - status: status.to_string(), - downloaded: status == "downloaded", - downloadable, - available_on_current_platform, - display_name: model.display_name().to_string(), - description: model.description().to_string(), - install_path: model.install_path(models_base).display().to_string(), - }); - } - rows.sort_by(|a, b| { - status_rank(&a.status) - .cmp(&status_rank(&b.status)) - .then_with(|| a.name.cmp(&b.name)) - }); - rows -} - -pub(super) async fn write_model_output( - rows: &[ModelRow], - _models_base: &Path, - format: OutputFormat, -) -> CliResult<()> { - match format { - OutputFormat::Json => { - crate::output::write_json(None, &rows).await?; - } - OutputFormat::Pretty => { - if rows.is_empty() { - eprintln!("No models found."); - return Ok(()); - } - - let home = dirs::home_dir(); - - let mut table = Table::new(); - table - .load_preset(UTF8_FULL_CONDENSED) - .set_content_arrangement(ContentArrangement::Dynamic); - - table.set_header(vec!["Name", "Status", "Path"]); - - for row in rows { - let path = match &home { - Some(h) => row - .install_path - .strip_prefix(&h.display().to_string()) - .map(|rest| format!("~{rest}")) - .unwrap_or_else(|| row.install_path.clone()), - None => row.install_path.clone(), - }; - table.add_row(vec![row.name.clone(), row.status.clone(), path]); - } - - println!("{table}"); - } - } - Ok(()) -} - -fn status_rank(status: &str) -> usize { - match status { - "downloaded" => 0, - "available" => 1, - "unsupported" => 2, - _ => 3, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn row(name: &str, kind: &str, status: &str) -> ModelRow { - ModelRow { - name: name.to_string(), - kind: kind.to_string(), - status: status.to_string(), - downloaded: status == "downloaded", - downloadable: status != "unavailable", - available_on_current_platform: status != "unsupported", - display_name: name.to_string(), - description: String::new(), - install_path: format!("/tmp/{name}"), - } - } - - #[test] - fn sorts_downloaded_before_available_and_unsupported() { - let mut rows = vec![ - row("model-b", "llm", "unsupported"), - row("model-c", "llm", "available"), - row("model-a", "stt-whisper", "downloaded"), - ]; - - rows.sort_by(|a, b| { - status_rank(&a.status) - .cmp(&status_rank(&b.status)) - .then_with(|| a.name.cmp(&b.name)) - }); - - assert_eq!( - rows.iter().map(|row| row.name.as_str()).collect::>(), - vec!["model-a", "model-c", "model-b"] - ); - } -} diff --git a/apps/cli/src/commands/model/mod.rs b/apps/cli/src/commands/model/mod.rs deleted file mode 100644 index 4ca9d7c072..0000000000 --- a/apps/cli/src/commands/model/mod.rs +++ /dev/null @@ -1,135 +0,0 @@ -mod delete; -mod download; -pub(crate) mod list; -pub(crate) mod runtime; - -use std::sync::Arc; - -use hypr_local_model::{LocalModel, LocalModelKind}; -use hypr_model_downloader::ModelDownloadManager; -use tokio::sync::mpsc; - -use clap::Subcommand; - -use crate::app::AppContext; -use crate::cli::OutputFormat; -use crate::error::{CliError, CliResult, did_you_mean}; -use runtime::CliModelRuntime; - -#[derive(clap::Args, Debug)] -pub struct Args { - #[arg(long, env = "CHAR_BASE", hide_env_values = true, value_name = "DIR")] - pub base: Option, - - #[command(subcommand)] - pub command: Commands, -} - -#[derive(Subcommand, Debug)] -pub enum Commands { - /// List local models and their status - List { - #[arg(short = 'f', long, value_enum, default_value = "pretty")] - format: OutputFormat, - }, - /// Download a model by name - Download { name: String }, - /// Delete a downloaded model - Delete { - name: String, - #[arg(long)] - force: bool, - }, -} - -struct ModelScope { - models: Vec, - label: &'static str, - list_cmd: &'static str, -} - -impl ModelScope { - fn all() -> Self { - Self { - models: LocalModel::all() - .into_iter() - .filter(|m| m.model_kind() == LocalModelKind::Stt) - .filter(model_is_enabled) - .collect(), - label: "model", - list_cmd: "char models list", - } - } - - fn resolve(&self, name: &str) -> CliResult { - self.models - .iter() - .find(|m| m.cli_name() == name) - .or_else(|| { - if name.starts_with("cactus-") { - None - } else { - let cactus_name = format!("cactus-{name}"); - self.models.iter().find(|m| m.cli_name() == cactus_name) - } - }) - .cloned() - .ok_or_else(|| { - let names: Vec<&str> = self.models.iter().map(|m| m.cli_name()).collect(); - let mut hint = String::new(); - if let Some(suggestion) = did_you_mean(name, &names) { - hint.push_str(&format!("Did you mean '{suggestion}'?\n\n")); - } - hint.push_str(&format!("Run `{}` to see available models.", self.list_cmd)); - CliError::not_found(format!("{} '{name}'", self.label), Some(hint)) - }) - } -} - -pub async fn run(ctx: &AppContext, args: Args) -> CliResult<()> { - let resolved = ctx.paths(); - let models_base = resolved.models_base.clone(); - - match args.command { - Commands::List { format } => list_models(&ModelScope::all(), &models_base, format).await, - Commands::Download { name } => { - let model = ModelScope::all().resolve(&name)?; - download::download(model, &models_base, ctx.trace_buffer()).await - } - Commands::Delete { name, force } => { - let model = ModelScope::all().resolve(&name)?; - delete::delete(model, &models_base, force).await - } - } -} - -async fn list_models( - scope: &ModelScope, - models_base: &std::path::Path, - format: OutputFormat, -) -> CliResult<()> { - let manager = make_manager(models_base, None); - let rows = list::collect_model_rows(&scope.models, models_base, &manager).await; - list::write_model_output(&rows, models_base, format).await -} - -fn make_manager( - models_base: &std::path::Path, - progress_tx: Option>, -) -> ModelDownloadManager { - let runtime = Arc::new(CliModelRuntime { - models_base: models_base.to_path_buf(), - progress_tx, - }); - ModelDownloadManager::new(runtime) -} - -pub(crate) fn model_is_enabled(model: &LocalModel) -> bool { - if matches!(model, LocalModel::Am(_)) { - return false; - } - cfg!(all( - target_os = "macos", - any(target_arch = "arm", target_arch = "aarch64") - )) || !matches!(model, LocalModel::Cactus(_) | LocalModel::CactusLlm(_)) -} diff --git a/apps/cli/src/commands/model/paths.rs b/apps/cli/src/commands/model/paths.rs deleted file mode 100644 index 1d0c0871eb..0000000000 --- a/apps/cli/src/commands/model/paths.rs +++ /dev/null @@ -1,83 +0,0 @@ -use std::path::Path; - -use serde::Serialize; - -use crate::cli::OutputFormat; -use crate::error::CliResult; - -#[derive(Debug, PartialEq, Eq, Serialize)] -struct PathsOutput { - base: String, - db_path: String, - models_base: String, -} - -fn build_output(base: &Path, db_path: &Path, models_base: &Path) -> PathsOutput { - PathsOutput { - base: base.display().to_string(), - db_path: db_path.display().to_string(), - models_base: models_base.display().to_string(), - } -} - -fn format_pretty(output: &PathsOutput) -> String { - format!( - "base={}\ndb_path={}\nmodels_base={}", - output.base, output.db_path, output.models_base - ) -} - -pub(super) async fn paths( - base: &Path, - db_path: &Path, - models_base: &Path, - format: OutputFormat, -) -> CliResult<()> { - let output = build_output(base, db_path, models_base); - - match format { - OutputFormat::Pretty => println!("{}", format_pretty(&output)), - OutputFormat::Json => crate::output::write_json(None, &output).await?, - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn format_pretty_matches_existing_text_shape() { - let output = build_output( - Path::new("/tmp/char"), - Path::new("/tmp/char/app.db"), - Path::new("/tmp/char/models"), - ); - - assert_eq!( - format_pretty(&output), - "base=/tmp/char\ndb_path=/tmp/char/app.db\nmodels_base=/tmp/char/models" - ); - } - - #[test] - fn json_output_uses_three_stable_keys() { - let output = build_output( - Path::new("/tmp/char"), - Path::new("/tmp/char/app.db"), - Path::new("/tmp/char/models"), - ); - - let json = serde_json::to_value(output).unwrap(); - - assert_eq!( - json, - serde_json::json!({ - "base": "/tmp/char", - "db_path": "/tmp/char/app.db", - "models_base": "/tmp/char/models", - }) - ); - } -} diff --git a/apps/cli/src/commands/model/runtime.rs b/apps/cli/src/commands/model/runtime.rs deleted file mode 100644 index 2135b78c59..0000000000 --- a/apps/cli/src/commands/model/runtime.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::path::PathBuf; - -use hypr_local_model::LocalModel; -use hypr_model_downloader::ModelDownloaderRuntime; -use tokio::sync::mpsc; - -pub(crate) enum DownloadEvent { - Progress(u8), - Completed, - Failed, -} - -pub(crate) struct CliModelRuntime { - pub(crate) models_base: PathBuf, - pub(crate) progress_tx: Option>, -} - -impl ModelDownloaderRuntime for CliModelRuntime { - fn models_base(&self) -> Result { - Ok(self.models_base.clone()) - } - - fn emit_progress(&self, _model: &LocalModel, status: hypr_model_downloader::DownloadStatus) { - let Some(tx) = &self.progress_tx else { - return; - }; - - use hypr_model_downloader::DownloadStatus; - match status { - DownloadStatus::Downloading(p) => { - let _ = tx.send(DownloadEvent::Progress(p)); - } - DownloadStatus::Completed => { - let _ = tx.send(DownloadEvent::Completed); - } - DownloadStatus::Failed(_) => { - let _ = tx.send(DownloadEvent::Failed); - } - } - } -} diff --git a/apps/cli/src/commands/play.rs b/apps/cli/src/commands/play.rs deleted file mode 100644 index 346b6aa969..0000000000 --- a/apps/cli/src/commands/play.rs +++ /dev/null @@ -1,243 +0,0 @@ -use std::io::IsTerminal; -use std::path::PathBuf; -use std::time::Duration; - -use ratatui::text::Line; -use rodio::source::Source; - -use crate::app::AppContext; -use crate::error::{CliError, CliResult}; -use crate::tui::waveform::{ - MIC_COLOR, PlaybackWaveform, SYS_COLOR, compute_peaks, deinterleave_stereo, -}; -use crate::tui::{InlineViewport, InputAction}; - -const WAVEFORM_WIDTH: usize = 46; -const SEEK_STEP: Duration = Duration::from_secs(5); - -#[derive(clap::Args)] -pub struct Args { - /// Timestamp (e.g. 20260327_143022) or path to an audio file - pub target: String, - - /// Base directory for session lookup - #[arg(long, env = "CHAR_BASE", hide_env_values = true, value_name = "DIR")] - pub base: Option, -} - -fn resolve_audio_path(target: &str, base: Option<&std::path::Path>) -> CliResult { - let as_path = PathBuf::from(target); - if as_path.is_file() { - return Ok(as_path); - } - - let base = base.map(PathBuf::from).unwrap_or_else(|| { - dirs::data_dir() - .unwrap_or_else(std::env::temp_dir) - .join("char") - }); - - let session_audio = base.join(target).join("audio.mp3"); - if session_audio.is_file() { - return Ok(session_audio); - } - - Err(CliError::not_found( - format!("audio file for '{target}'"), - Some(format!( - "Pass a file path or a session timestamp.\nLooked in: {}", - session_audio.display() - )), - )) -} - -struct PlayState { - file_name: String, - total: Duration, - left_peaks: Vec, - right_peaks: Option>, - pos: Duration, - paused: bool, -} - -impl PlayState { - fn fraction(&self) -> f64 { - if self.total.as_secs_f64() > 0.0 { - (self.pos.as_secs_f64() / self.total.as_secs_f64()).clamp(0.0, 1.0) - } else { - 0.0 - } - } - - fn waveform_lines(&self, fraction: f64) -> Vec> { - match &self.right_peaks { - Some(right) => PlaybackWaveform::lines_dual( - &self.left_peaks, - right, - fraction, - MIC_COLOR, - SYS_COLOR, - WAVEFORM_WIDTH, - 2, - ), - None => { - PlaybackWaveform::lines(&self.left_peaks, fraction, MIC_COLOR, WAVEFORM_WIDTH, 2) - } - } - } - - fn lines(&self) -> Vec> { - let mut lines = self.waveform_lines(self.fraction()); - - let status = if self.paused { "paused " } else { "playing" }; - lines.push(Line::from(format!( - "{} {} / {} {}", - status, - format_duration(self.pos), - format_duration(self.total), - self.file_name, - ))); - - lines - } - - fn completion_lines(&self) -> Vec> { - let mut lines = self.waveform_lines(1.0); - lines.push(Line::from(format!( - "played {} {}", - format_duration(self.total), - self.file_name, - ))); - lines - } -} - -fn format_duration(d: Duration) -> String { - let secs = d.as_secs(); - format!("{:02}:{:02}", secs / 60, secs % 60) -} - -pub async fn run(ctx: &AppContext, args: Args) -> CliResult<()> { - use rodio::{Decoder, Player, stream::DeviceSinkBuilder}; - - let path = resolve_audio_path(&args.target, args.base.as_deref())?; - - let bytes = std::fs::read(&path) - .map_err(|e| CliError::operation_failed("read audio file", e.to_string()))?; - - // Decode once to analyze peaks and compute duration. - let analyze = Decoder::try_from(std::io::Cursor::new(bytes.clone())) - .map_err(|e| CliError::operation_failed("decode audio file", e.to_string()))?; - let sample_rate = analyze.sample_rate().get() as f64; - let num_channels = analyze.channels().get() as usize; - let samples: Vec = analyze.collect(); - let duration = - Duration::from_secs_f64(samples.len() as f64 / (sample_rate * num_channels as f64)); - - let (left_peaks, right_peaks) = if num_channels >= 2 { - let (left, right) = deinterleave_stereo(&samples); - ( - compute_peaks(&left, WAVEFORM_WIDTH), - Some(compute_peaks(&right, WAVEFORM_WIDTH)), - ) - } else { - (compute_peaks(&samples, WAVEFORM_WIDTH), None) - }; - - // Decode again for playback. - let source = Decoder::try_from(std::io::Cursor::new(bytes)) - .map_err(|e| CliError::operation_failed("decode audio file", e.to_string()))?; - - let mut stream = DeviceSinkBuilder::open_default_sink() - .map_err(|e| CliError::operation_failed("open audio device", e.to_string()))?; - stream.log_on_drop(false); - - let file_name = path - .file_name() - .map(|n| n.to_string_lossy().into_owned()) - .unwrap_or_else(|| path.display().to_string()); - - let mut state = PlayState { - file_name, - total: duration, - left_peaks, - right_peaks, - pos: Duration::ZERO, - paused: false, - }; - - let quiet = ctx.quiet(); - let stderr_is_tty = std::io::stderr().is_terminal(); - let mut viewport = if !quiet && stderr_is_tty { - Some( - InlineViewport::stderr_interactive(5, ctx.trace_buffer(), true) - .map_err(|e| CliError::operation_failed("init play viewport", e.to_string()))?, - ) - } else { - None - }; - - if let Some(view) = viewport.as_mut() { - view.draw(&state.lines()); - } else if !quiet { - eprintln!("Playing {}", path.display()); - } - - let player = Player::connect_new(stream.mixer()); - player.append(source); - - let tick = Duration::from_millis(100); - loop { - let done = player.get_pos() >= duration || player.empty(); - - if done { - break; - } - - tokio::select! { - _ = tokio::time::sleep(tick) => { - state.pos = player.get_pos(); - state.paused = player.is_paused(); - - if let Some(view) = viewport.as_mut() { - for action in view.poll_input() { - match action { - InputAction::TogglePause => { - if player.is_paused() { - player.play(); - } else { - player.pause(); - } - } - InputAction::SeekForward => { - let target = - (player.get_pos() + SEEK_STEP).min(state.total); - let _ = player.try_seek(target); - } - InputAction::SeekBackward => { - let target = player.get_pos().saturating_sub(SEEK_STEP); - let _ = player.try_seek(target); - } - _ => {} - } - } - state.pos = player.get_pos(); - state.paused = player.is_paused(); - view.draw(&state.lines()); - } - } - _ = tokio::signal::ctrl_c() => { - break; - } - } - } - - if let Some(view) = viewport.as_mut() { - state.pos = player.get_pos(); - view.draw(&state.completion_lines()); - view.finish() - .map_err(|e| CliError::operation_failed("finish play viewport", e.to_string()))?; - } - - Ok(()) -} diff --git a/apps/cli/src/commands/record/app.rs b/apps/cli/src/commands/record/app.rs deleted file mode 100644 index 1426cc087d..0000000000 --- a/apps/cli/src/commands/record/app.rs +++ /dev/null @@ -1,205 +0,0 @@ -use std::path::PathBuf; -use std::time::Duration; - -use ratatui::text::{Line, Span}; - -use super::{AudioMode, ProgressUpdate}; -use crate::tui::waveform::{LiveWaveform, LiveWaveformState, WaveformMode}; - -const WAVEFORM_WIDTH: usize = 16; - -pub(crate) struct App { - pub(crate) output: PathBuf, - pub(crate) sample_rate: u32, - pub(crate) channels: u16, - audio: AudioMode, - elapsed: Duration, - audio_secs: f64, - waveform: LiveWaveformState, -} - -impl App { - pub(crate) fn new(audio: AudioMode, output: PathBuf, sample_rate: u32, channels: u16) -> Self { - Self { - output, - sample_rate, - channels, - audio, - elapsed: Duration::ZERO, - audio_secs: 0.0, - waveform: LiveWaveformState::new(WAVEFORM_WIDTH), - } - } - - pub(crate) fn audio_label(&self) -> &'static str { - match self.audio { - AudioMode::Input => "mic", - AudioMode::Output => "system", - AudioMode::Dual => "dual", - } - } - - fn waveform_mode(&self) -> WaveformMode { - match self.audio { - AudioMode::Dual => WaveformMode::Dual, - AudioMode::Input | AudioMode::Output => WaveformMode::Mono, - } - } - - pub(crate) fn update(&mut self, progress: &ProgressUpdate) { - self.elapsed = progress.elapsed; - self.audio_secs = progress.audio_secs; - self.waveform - .push(progress.left_level, progress.right_level); - } - - pub(crate) fn finish(&mut self, elapsed: Duration, audio_secs: f64) { - self.elapsed = elapsed; - self.audio_secs = audio_secs; - } - - pub(crate) fn lines(&self) -> Vec> { - let file_name = self - .output - .file_name() - .map(|n| n.to_string_lossy().into_owned()) - .unwrap_or_else(|| self.output.display().to_string()); - - let line0 = Line::from(format!( - "recording {} {}", - self.audio_label(), - format_elapsed(self.elapsed) - )); - let line1 = Line::from(format!("{} Hz {} ch", self.sample_rate, self.channels)); - - let mut spans = vec![Span::raw(format!("{} ", file_name))]; - spans.extend(LiveWaveform::spans( - &self.waveform, - self.waveform_mode(), - WAVEFORM_WIDTH, - )); - let line2 = Line::from(spans); - - vec![line0, line1, line2] - } - - pub(crate) fn completion_lines(&self) -> Vec> { - let short = short_output_path(&self.output); - let session_dir = session_dir_name(&self.output); - - vec![ - Line::from(format!("saved {:.1}s {}", self.audio_secs, short)), - Line::from(format!("char play {session_dir}")), - Line::from(format!("char transcribe {session_dir}")), - ] - } - - pub(crate) fn summary_line(&self) -> String { - let session_dir = session_dir_name(&self.output); - - format!( - "{:.1}s {}\n\nchar play {session_dir}\nchar transcribe {session_dir}", - self.audio_secs, - self.output.display(), - ) - } -} - -fn short_output_path(path: &std::path::Path) -> String { - if let Some(home) = dirs::home_dir() { - if let Ok(rel) = path.strip_prefix(&home) { - return format!("~/{}", rel.display()); - } - } - path.display().to_string() -} - -fn session_dir_name(output: &std::path::Path) -> String { - output - .parent() - .and_then(|p| p.file_name()) - .map(|n| n.to_string_lossy().into_owned()) - .unwrap_or_default() -} - -fn format_elapsed(duration: Duration) -> String { - let secs = duration.as_secs(); - format!("{:02}:{:02}", secs / 60, secs % 60) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::tui::waveform::{MIC_COLOR, SYS_COLOR}; - - const BLOCKS: [char; 9] = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█']; - - #[test] - fn dual_mode_overlaid_waveform() { - let mut app = App::new(AudioMode::Dual, PathBuf::from("out.wav"), 16_000, 2); - for _ in 0..3 { - app.update(&ProgressUpdate { - elapsed: Duration::from_secs(5), - sample_count: 80_000, - audio_secs: 5.0, - left_level: 0.75, - right_level: 0.25, - render_ui: true, - emit_event: true, - }); - } - - let lines = app.lines(); - let text: String = lines[2].spans.iter().map(|s| s.content.as_ref()).collect(); - assert!(text.contains("mic")); - assert!(text.contains("sys")); - - let has_block = lines[2] - .spans - .iter() - .any(|s| s.content.chars().any(|c| BLOCKS[1..].contains(&c))); - assert!(has_block); - } - - #[test] - fn mic_dominant_gets_mic_color() { - let mut app = App::new(AudioMode::Dual, PathBuf::from("out.wav"), 16_000, 2); - app.update(&ProgressUpdate { - elapsed: Duration::from_secs(1), - sample_count: 16_000, - audio_secs: 1.0, - left_level: 0.5, - right_level: 0.01, - render_ui: true, - emit_event: true, - }); - let lines = app.lines(); - let block_span = lines[2] - .spans - .iter() - .find(|s| s.content.chars().any(|c| BLOCKS[1..].contains(&c))); - assert!(block_span.is_some()); - assert_eq!(block_span.unwrap().style.fg, Some(MIC_COLOR)); - } - - #[test] - fn sys_dominant_gets_sys_color() { - let mut app = App::new(AudioMode::Dual, PathBuf::from("out.wav"), 16_000, 2); - app.update(&ProgressUpdate { - elapsed: Duration::from_secs(1), - sample_count: 16_000, - audio_secs: 1.0, - left_level: 0.01, - right_level: 0.5, - render_ui: true, - emit_event: true, - }); - let lines = app.lines(); - let block_span = lines[2] - .spans - .iter() - .find(|s| s.content.chars().any(|c| BLOCKS[1..].contains(&c))); - assert!(block_span.is_some()); - assert_eq!(block_span.unwrap().style.fg, Some(SYS_COLOR)); - } -} diff --git a/apps/cli/src/commands/record/mod.rs b/apps/cli/src/commands/record/mod.rs deleted file mode 100644 index ef38a6b458..0000000000 --- a/apps/cli/src/commands/record/mod.rs +++ /dev/null @@ -1,263 +0,0 @@ -mod app; -mod runtime; - -use std::fs; -use std::io::{BufWriter, IsTerminal, Write}; -use std::path::PathBuf; - -use hypr_audio::AudioProvider; -use hypr_audio_actual::ActualAudio; -use hypr_audio_utils::chunk_size_for_stt; -use serde::Serialize; - -use crate::app::AppContext; -use crate::cli::OutputFormat; -use crate::error::{CliError, CliResult}; -use crate::output::EventWriter; - -pub(crate) use app::App; -pub(crate) use runtime::{CaptureResult, ProgressUpdate}; - -use crate::tui::InlineViewport; - -#[derive(Clone, Copy, Debug, clap::ValueEnum)] -pub enum AudioMode { - Input, - Output, - Dual, -} - -#[derive(clap::Args)] -pub struct Args { - #[arg(long, value_enum, default_value = "dual")] - pub audio: AudioMode, - #[arg(short = 'o', long, value_name = "FILE")] - pub output: Option, - #[arg(short = 'f', long, value_enum, default_value = "pretty")] - pub format: OutputFormat, - #[arg(long, env = "CHAR_BASE", hide_env_values = true, value_name = "DIR")] - pub base: Option, -} - -#[derive(Debug, Serialize)] -#[serde(tag = "type", rename_all = "snake_case")] -enum RecordEvent { - Started { - audio: String, - sample_rate: u32, - channels: u16, - output: String, - started_at: String, - }, - Progress { - elapsed_ms: u64, - audio_secs: f64, - sample_count: u64, - level_left: f32, - level_right: f32, - }, - Stopped { - reason: String, - elapsed_ms: u64, - audio_secs: f64, - output: String, - }, - Failed { - stage: String, - message: String, - }, -} - -pub async fn run(ctx: &AppContext, args: Args) -> CliResult<()> { - run_with_audio(args, ctx.quiet(), ctx.trace_buffer(), &ActualAudio).await -} - -async fn run_with_audio( - args: Args, - quiet: bool, - trace_buffer: Option, - audio: &A, -) -> CliResult<()> { - let update_check = super::update_check::UpdateHandle::spawn(); - let sample_rate = 16_000u32; - let format = args.format; - - let output_path = match args.output { - Some(output) => output, - None => { - let ts = chrono::Local::now().format("%Y%m%d_%H%M%S").to_string(); - let dir = super::resolve_session_dir(args.base.as_deref(), &ts)?; - dir.join("audio.mp3") - } - }; - - let channels: u16 = match args.audio { - AudioMode::Input | AudioMode::Output => 1, - AudioMode::Dual => 2, - }; - let chunk_size = chunk_size_for_stt(sample_rate); - - let mut app = App::new(args.audio, output_path.clone(), sample_rate, channels); - - let mut event_writer = match format { - OutputFormat::Json => Some(EventWriter::new(BufWriter::new(std::io::stdout()))), - OutputFormat::Pretty => None, - }; - if let Some(writer) = event_writer.as_mut() { - writer.emit(&RecordEvent::Started { - audio: app.audio_label().to_string(), - sample_rate, - channels, - output: output_path.display().to_string(), - started_at: chrono::Utc::now().to_rfc3339(), - })?; - } - - let stderr_is_tty = std::io::stderr().is_terminal(); - let mut viewport = match format { - OutputFormat::Pretty if !quiet && stderr_is_tty => Some( - InlineViewport::stderr(5, trace_buffer) - .map_err(|e| CliError::operation_failed("init record viewport", e.to_string()))?, - ), - _ => None, - }; - if let Some(view) = viewport.as_mut() { - view.draw(&app.lines()); - } else if !quiet && matches!(format, OutputFormat::Pretty) { - eprintln!( - "recording {} -> {}", - app.audio_label(), - output_path.display() - ); - } - - let capture = runtime::capture(audio, args.audio, sample_rate, chunk_size, |progress| { - app.update(&progress); - if progress.emit_event - && let Some(writer) = event_writer.as_mut() - { - writer.emit(&RecordEvent::Progress { - elapsed_ms: progress.elapsed.as_millis() as u64, - audio_secs: progress.audio_secs, - sample_count: progress.sample_count, - level_left: progress.left_level, - level_right: progress.right_level, - })?; - } - if progress.render_ui - && let Some(view) = viewport.as_mut() - { - view.poll_input(); - view.draw(&app.lines()); - } - Ok(()) - }) - .await; - - match capture { - Ok(result) => { - finish_success( - result, - &mut app, - viewport.as_mut(), - event_writer.as_mut(), - quiet, - update_check, - ) - .await - } - Err(error) => { - if let Some(view) = viewport.as_mut() { - view.clear().ok(); - } - if let Some(writer) = event_writer.as_mut() { - writer.emit(&RecordEvent::Failed { - stage: "capture".to_string(), - message: error.to_string(), - })?; - } - Err(error) - } - } -} - -async fn finish_success( - result: CaptureResult, - app: &mut App, - viewport: Option<&mut InlineViewport>, - event_writer: Option<&mut EventWriter>, - quiet: bool, - update_check: super::update_check::UpdateHandle, -) -> CliResult<()> { - ensure_parent_dirs(&app.output)?; - - let mut mp3_buf = Vec::new(); - match app.channels { - 1 => { - let mut encoder = hypr_mp3::MonoStreamEncoder::new(app.sample_rate) - .map_err(|e| CliError::operation_failed("create mp3 encoder", e.to_string()))?; - encoder - .encode_i16(&result.samples, &mut mp3_buf) - .map_err(|e| CliError::operation_failed("encode mp3", e.to_string()))?; - encoder - .flush(&mut mp3_buf) - .map_err(|e| CliError::operation_failed("flush mp3", e.to_string()))?; - } - 2 => { - let mut encoder = hypr_mp3::StereoStreamEncoder::new(app.sample_rate) - .map_err(|e| CliError::operation_failed("create mp3 encoder", e.to_string()))?; - let (left, right): (Vec, Vec) = result - .samples - .chunks(2) - .map(|pair| (pair[0], pair.get(1).copied().unwrap_or(0))) - .unzip(); - encoder - .encode_i16(&left, &right, &mut mp3_buf) - .map_err(|e| CliError::operation_failed("encode mp3", e.to_string()))?; - encoder - .flush(&mut mp3_buf) - .map_err(|e| CliError::operation_failed("flush mp3", e.to_string()))?; - } - _ => { - return Err(CliError::operation_failed( - "encode mp3", - format!("unsupported channel count: {}", app.channels), - )); - } - } - std::fs::write(&app.output, &mp3_buf) - .map_err(|e| CliError::operation_failed("write mp3 file", e.to_string()))?; - - app.finish(result.elapsed, result.audio_secs); - - if let Some(writer) = event_writer { - writer.emit(&RecordEvent::Stopped { - reason: result.stop_reason.as_str().to_string(), - elapsed_ms: result.elapsed.as_millis() as u64, - audio_secs: result.audio_secs, - output: app.output.display().to_string(), - })?; - } - - if let Some(view) = viewport { - view.draw(&app.completion_lines()); - view.finish() - .map_err(|e| CliError::operation_failed("finish record viewport", e.to_string()))?; - } else if !quiet { - eprintln!("{}", app.summary_line()); - } - - if let Some(version) = update_check.result().await { - eprintln!(" update available ({version}): char update"); - } - - Ok(()) -} - -fn ensure_parent_dirs(path: &std::path::Path) -> CliResult<()> { - if let Some(parent) = path.parent() { - fs::create_dir_all(parent) - .map_err(|e| CliError::operation_failed("create output directory", e.to_string()))?; - } - Ok(()) -} diff --git a/apps/cli/src/commands/record/runtime.rs b/apps/cli/src/commands/record/runtime.rs deleted file mode 100644 index b8ca2fc04d..0000000000 --- a/apps/cli/src/commands/record/runtime.rs +++ /dev/null @@ -1,276 +0,0 @@ -use std::pin::pin; -use std::time::{Duration, Instant}; - -use hypr_audio::{AudioProvider, CaptureConfig}; -use tokio::signal; -use tokio_stream::StreamExt; - -use super::AudioMode; -use crate::error::{CliError, CliResult}; - -const UI_TICK: Duration = Duration::from_millis(250); -const EVENT_TICK: Duration = Duration::from_secs(1); - -pub(crate) struct ProgressUpdate { - pub(crate) elapsed: Duration, - pub(crate) sample_count: u64, - pub(crate) audio_secs: f64, - pub(crate) left_level: f32, - pub(crate) right_level: f32, - pub(crate) render_ui: bool, - pub(crate) emit_event: bool, -} - -pub(crate) struct CaptureResult { - pub(crate) samples: Vec, - pub(crate) elapsed: Duration, - pub(crate) audio_secs: f64, - pub(crate) stop_reason: StopReason, -} - -#[derive(Clone, Copy)] -pub(crate) enum StopReason { - CtrlC, - Eof, -} - -impl StopReason { - pub(crate) fn as_str(self) -> &'static str { - match self { - Self::CtrlC => "ctrl_c", - Self::Eof => "eof", - } - } -} - -pub(crate) async fn capture( - audio: &A, - mode: AudioMode, - sample_rate: u32, - chunk_size: usize, - mut on_progress: impl FnMut(ProgressUpdate) -> CliResult<()>, -) -> CliResult { - let stream = match mode { - AudioMode::Input => audio - .open_mic_capture(None, sample_rate, chunk_size) - .map_err(|e| CliError::operation_failed("open mic capture", e.to_string()))?, - AudioMode::Output => audio - .open_speaker_capture(sample_rate, chunk_size) - .map_err(|e| CliError::operation_failed("open speaker capture", e.to_string()))?, - AudioMode::Dual => audio - .open_capture(CaptureConfig { - sample_rate, - chunk_size, - mic_device: None, - enable_aec: false, - }) - .map_err(|e| CliError::operation_failed("open dual capture", e.to_string()))?, - }; - let mut stream = pin!(stream); - let mut samples = Vec::new(); - let started = Instant::now(); - let mut last_ui = Instant::now() - UI_TICK; - let mut last_event = Instant::now() - EVENT_TICK; - let mut stop_reason = StopReason::Eof; - - loop { - tokio::select! { - frame = stream.next() => { - let Some(result) = frame else { break }; - let frame = result - .map_err(|e| CliError::operation_failed("audio capture", e.to_string()))?; - - let (left, right) = match mode { - AudioMode::Input => { - let raw = frame.preferred_mic(); - samples.extend(raw.iter().map(|&s| to_i16(s))); - (peak_level(&raw), 0.0) - } - AudioMode::Output => { - let raw = &frame.raw_speaker; - samples.extend(raw.iter().map(|&s| to_i16(s))); - (peak_level(raw), 0.0) - } - AudioMode::Dual => { - let (mic, speaker) = frame.raw_dual(); - for (&m, &s) in mic.iter().zip(speaker.iter()) { - samples.push(to_i16(m)); - samples.push(to_i16(s)); - } - (peak_level(&mic), peak_level(&speaker)) - } - }; - - let elapsed = started.elapsed(); - let audio_secs = samples_to_audio_secs(samples.len() as u64, sample_rate, mode); - let render_ui = last_ui.elapsed() >= UI_TICK; - let emit_event = last_event.elapsed() >= EVENT_TICK; - if render_ui || emit_event { - on_progress(ProgressUpdate { - elapsed, - sample_count: audio_frame_count(samples.len() as u64, mode), - audio_secs, - left_level: left, - right_level: right, - render_ui, - emit_event, - })?; - let now = Instant::now(); - if render_ui { - last_ui = now; - } - if emit_event { - last_event = now; - } - } - } - _ = signal::ctrl_c() => { - stop_reason = StopReason::CtrlC; - break; - } - } - } - - let elapsed = started.elapsed(); - let audio_secs = samples_to_audio_secs(samples.len() as u64, sample_rate, mode); - on_progress(ProgressUpdate { - elapsed, - sample_count: audio_frame_count(samples.len() as u64, mode), - audio_secs, - left_level: 0.0, - right_level: 0.0, - render_ui: true, - emit_event: true, - })?; - - Ok(CaptureResult { - samples, - elapsed, - audio_secs, - stop_reason, - }) -} - -fn to_i16(sample: f32) -> i16 { - (sample * 32767.0) as i16 -} - -fn peak_level(samples: &[f32]) -> f32 { - samples - .iter() - .map(|sample| sample.abs()) - .fold(0.0_f32, f32::max) - .clamp(0.0, 1.0) -} - -fn samples_to_audio_secs(samples_len: u64, sample_rate: u32, mode: AudioMode) -> f64 { - let divisor = match mode { - AudioMode::Dual => sample_rate as f64 * 2.0, - AudioMode::Input | AudioMode::Output => sample_rate as f64, - }; - samples_len as f64 / divisor -} - -fn audio_frame_count(samples_len: u64, mode: AudioMode) -> u64 { - match mode { - AudioMode::Dual => samples_len / 2, - AudioMode::Input | AudioMode::Output => samples_len, - } -} - -#[cfg(test)] -mod tests { - use std::sync::Arc; - - use super::*; - use hypr_audio::{CaptureFrame, CaptureStream, Error}; - use tokio_stream::iter; - - #[test] - fn peak_level_clamps_to_unit_interval() { - assert_eq!(peak_level(&[0.1, -0.4, 0.2]), 0.4); - assert_eq!(peak_level(&[2.0]), 1.0); - } - - #[test] - fn dual_audio_frame_count_uses_stereo_pairs() { - assert_eq!(audio_frame_count(10, AudioMode::Dual), 5); - assert_eq!(audio_frame_count(10, AudioMode::Input), 10); - } - - struct TestAudio { - frames: Vec>, - } - - impl AudioProvider for TestAudio { - fn open_capture(&self, _config: CaptureConfig) -> Result { - Ok(CaptureStream::new(iter(self.frames.clone()))) - } - - fn open_speaker_capture( - &self, - _sample_rate: u32, - _chunk_size: usize, - ) -> Result { - Ok(CaptureStream::new(iter(self.frames.clone()))) - } - - fn open_mic_capture( - &self, - _device: Option, - _sample_rate: u32, - _chunk_size: usize, - ) -> Result { - Ok(CaptureStream::new(iter(self.frames.clone()))) - } - - fn default_device_name(&self) -> String { - "test".to_string() - } - - fn list_mic_devices(&self) -> Vec { - vec!["test".to_string()] - } - - fn play_silence(&self) -> std::sync::mpsc::Sender<()> { - let (tx, _rx) = std::sync::mpsc::channel(); - tx - } - - fn play_bytes(&self, _bytes: &'static [u8]) -> std::sync::mpsc::Sender<()> { - let (tx, _rx) = std::sync::mpsc::channel(); - tx - } - - fn probe_mic(&self, _device: Option) -> Result<(), Error> { - Ok(()) - } - - fn probe_speaker(&self) -> Result<(), Error> { - Ok(()) - } - } - - #[tokio::test] - async fn capture_returns_samples_and_eof_reason() { - let audio = TestAudio { - frames: vec![Ok(CaptureFrame { - raw_mic: Arc::from([0.25_f32, -0.25, 0.5, -0.5]), - raw_speaker: Arc::from([]), - aec_mic: None, - })], - }; - let mut updates = Vec::new(); - - let result = capture(&audio, AudioMode::Input, 16_000, 4, |progress| { - updates.push(progress.sample_count); - Ok(()) - }) - .await - .unwrap(); - - assert_eq!(result.stop_reason.as_str(), "eof"); - assert_eq!(result.samples.len(), 4); - assert!(!updates.is_empty()); - } -} diff --git a/apps/cli/src/commands/skill/install.rs b/apps/cli/src/commands/skill/install.rs deleted file mode 100644 index 7a2b1bec22..0000000000 --- a/apps/cli/src/commands/skill/install.rs +++ /dev/null @@ -1,168 +0,0 @@ -use std::io::IsTerminal; -use std::path::PathBuf; -use std::time::Duration; - -use ratatui::style::{Color, Style}; -use ratatui::text::{Line, Span}; - -use crate::error::{CliError, CliResult}; -use crate::tui::InlineViewport; -use crate::tui::InputAction; - -const SKILL_CONTENT: &str = include_str!("../../../../../skills/cli/SKILL.md"); - -#[derive(Clone, Copy)] -enum SkillFormat { - ClaudeCode, - Codex, - GitHubCopilot, -} - -impl SkillFormat { - const ALL: [SkillFormat; 3] = [ - SkillFormat::ClaudeCode, - SkillFormat::Codex, - SkillFormat::GitHubCopilot, - ]; - - fn label(self) -> &'static str { - match self { - SkillFormat::ClaudeCode => "Claude Code", - SkillFormat::Codex => "Codex", - SkillFormat::GitHubCopilot => "GitHub Copilot", - } - } - - fn dir(self) -> &'static str { - match self { - SkillFormat::ClaudeCode => ".claude/skills/char", - SkillFormat::Codex => ".codex/skills/char", - SkillFormat::GitHubCopilot => ".github/skills/char", - } - } - - fn cli_value(self) -> &'static str { - match self { - SkillFormat::ClaudeCode => "claude", - SkillFormat::Codex => "codex", - SkillFormat::GitHubCopilot => "github-copilot", - } - } - - fn from_str(s: &str) -> Option { - match s { - "claude" => Some(SkillFormat::ClaudeCode), - "codex" => Some(SkillFormat::Codex), - "github-copilot" => Some(SkillFormat::GitHubCopilot), - _ => None, - } - } -} - -const HIGHLIGHT: Color = Color::Rgb(0xFD, 0xE6, 0xAE); - -fn selector_lines(selected: usize) -> Vec> { - let mut lines = vec![]; - - for (i, format) in SkillFormat::ALL.iter().enumerate() { - let marker = if i == selected { "> " } else { " " }; - let style = if i == selected { - Style::default().fg(HIGHLIGHT) - } else { - Style::default().fg(Color::DarkGray) - }; - - lines.push(Line::from(vec![ - Span::styled(marker, style), - Span::styled(format.label(), style), - Span::raw(" "), - Span::styled( - format!("./{}/", format.dir()), - Style::default().fg(Color::DarkGray), - ), - ])); - } - - lines -} - -fn write_skill(format: SkillFormat) -> CliResult { - let dir = PathBuf::from(format.dir()); - std::fs::create_dir_all(&dir) - .map_err(|e| CliError::operation_failed("create skill directory", e.to_string()))?; - - let path = dir.join("SKILL.md"); - std::fs::write(&path, SKILL_CONTENT) - .map_err(|e| CliError::operation_failed("write SKILL.md", e.to_string()))?; - - Ok(path) -} - -pub fn run(format_arg: Option<&str>) -> CliResult<()> { - if let Some(name) = format_arg { - let format = SkillFormat::from_str(name).ok_or_else(|| { - CliError::invalid_argument( - "--format", - name, - format!( - "expected one of: {}", - SkillFormat::ALL - .iter() - .map(|f| f.cli_value()) - .collect::>() - .join(", ") - ), - ) - })?; - let path = write_skill(format)?; - eprintln!("Installed char skill at {}", path.display()); - return Ok(()); - } - - if !std::io::stderr().is_terminal() { - return Err(CliError::operation_failed( - "skill install", - "interactive selection requires a terminal; use --format to specify".to_string(), - )); - } - - let mut selected: usize = 0; - let count = SkillFormat::ALL.len(); - - let mut viewport = InlineViewport::stderr_interactive(3, None, true) - .map_err(|e| CliError::operation_failed("create viewport", e.to_string()))?; - - viewport.draw(&selector_lines(selected)); - - loop { - std::thread::sleep(Duration::from_millis(30)); - - for action in viewport.poll_input() { - match action { - InputAction::Up => { - selected = if selected == 0 { - count - 1 - } else { - selected - 1 - }; - } - InputAction::Down => { - selected = (selected + 1) % count; - } - InputAction::Confirm => { - viewport - .clear() - .map_err(|e| CliError::operation_failed("clear viewport", e.to_string()))?; - - let format = SkillFormat::ALL[selected]; - let path = write_skill(format)?; - eprintln!("Installed char skill at {}", path.display()); - return Ok(()); - } - _ => {} - } - } - - viewport.draw(&selector_lines(selected)); - } -} diff --git a/apps/cli/src/commands/skill/mod.rs b/apps/cli/src/commands/skill/mod.rs deleted file mode 100644 index 8c5692826f..0000000000 --- a/apps/cli/src/commands/skill/mod.rs +++ /dev/null @@ -1,22 +0,0 @@ -mod install; - -use clap::Subcommand; - -use crate::app::AppContext; -use crate::error::CliResult; - -#[derive(Subcommand)] -pub enum Commands { - /// Install char skill for AI coding agents - Install { - /// Skip interactive selection: claude, codex, github-copilot - #[arg(short, long)] - format: Option, - }, -} - -pub async fn run(_ctx: &AppContext, command: Commands) -> CliResult<()> { - match command { - Commands::Install { format } => install::run(format.as_deref()), - } -} diff --git a/apps/cli/src/commands/todo/mod.rs b/apps/cli/src/commands/todo/mod.rs deleted file mode 100644 index bc6d8d54aa..0000000000 --- a/apps/cli/src/commands/todo/mod.rs +++ /dev/null @@ -1,40 +0,0 @@ -use clap::Subcommand; - -use crate::error::CliResult; - -#[derive(Subcommand)] -pub enum Commands { - /// Claude Code integration - Claude { - #[command(subcommand)] - command: crate::commands::integration::claude::Commands, - }, - /// Codex integration - Codex { - #[command(subcommand)] - command: crate::commands::integration::codex::Commands, - }, - /// OpenCode integration - Opencode { - #[command(subcommand)] - command: crate::commands::integration::opencode::Commands, - }, -} - -pub async fn run(command: Option) -> CliResult<()> { - match command { - Some(Commands::Claude { command }) => { - crate::commands::integration::claude::run(command).await - } - Some(Commands::Codex { command }) => { - crate::commands::integration::codex::run(command).await - } - Some(Commands::Opencode { command }) => { - crate::commands::integration::opencode::run(command).await - } - None => { - eprintln!("Todo is not ready yet."); - Ok(()) - } - } -} diff --git a/apps/cli/src/commands/transcribe/mod.rs b/apps/cli/src/commands/transcribe/mod.rs deleted file mode 100644 index c7dc610019..0000000000 --- a/apps/cli/src/commands/transcribe/mod.rs +++ /dev/null @@ -1,804 +0,0 @@ -mod output; - -use std::io::BufWriter; -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use std::time::Duration; - -use serde::Serialize; -use tokio::sync::mpsc; - -use hypr_listener2_core::{BatchErrorCode, BatchEvent}; -use owhisper_interface::batch_stream::BatchStreamEvent; - -use crate::OptTraceBuffer; -use crate::app::AppContext; -use crate::cli::OutputFormat; -use crate::error::{CliError, CliResult}; -use crate::stt::{ChannelBatchRuntime, SttOverrides, resolve_config}; - -#[derive(clap::Args)] -pub struct Args { - /// Timestamp (e.g. 20260327_143022) or path to an audio file - #[arg(value_name = "INPUT")] - pub target: Option, - #[arg(short = 'i', long, value_name = "FILE", visible_alias = "file")] - pub input: Option, - #[arg(short = 'p', long, value_enum)] - pub provider: crate::stt::SttProvider, - #[arg(long = "keyword", short = 'k', value_name = "KEYWORD")] - pub keywords: Vec, - #[arg(short = 'o', long, value_name = "FILE")] - pub output: Option, - #[arg(short = 'f', long, value_enum, default_value = "pretty")] - pub format: OutputFormat, - #[arg(long, env = "CHAR_BASE", hide_env_values = true, value_name = "DIR")] - pub base: Option, - #[arg(long, env = "CHAR_BASE_URL", hide_env_values = true, value_parser = crate::cli::parse_base_url)] - pub base_url: Option, - #[arg(long, env = "CHAR_API_KEY", hide_env_values = true)] - pub api_key: Option, - #[arg(short = 'm', long, env = "CHAR_MODEL", hide_env_values = true)] - pub model: Option, - #[arg( - short = 'l', - long, - env = "CHAR_LANGUAGE", - hide_env_values = true, - default_value = "en" - )] - pub language: String, -} - -// -- JSONL event types (for --format json) -- - -#[derive(Debug, Serialize)] -#[serde(tag = "type", rename_all = "snake_case")] -enum TranscribeEvent { - Started { - input: String, - provider: String, - }, - Progress { - percentage: f64, - transcript: String, - }, - Completed { - elapsed_ms: u64, - audio_duration_secs: Option, - response: owhisper_interface::batch::Response, - }, - Failed { - code: String, - message: String, - }, -} - -use crate::output::EventWriter; - -// -- Batch handle -- - -struct BatchHandle { - rx: mpsc::UnboundedReceiver, - task: tokio::task::JoinHandle< - Result, - >, - started: std::time::Instant, - _normalized_input_dir: Option, - _server: crate::stt::ServerGuard, -} - -async fn start_batch( - input: &Path, - keywords: Vec, - stt: SttOverrides, - audio_dest: Option<&Path>, - on_normalize_progress: Option<&mut dyn FnMut(f64)>, -) -> CliResult { - let resolved = resolve_config(stt).await?; - let (temp_dir, normalized_input_path) = - normalize_input_file(input, audio_dest, on_normalize_progress)?; - let params = build_batch_params(&resolved, &normalized_input_path, keywords)?; - - let (batch_tx, batch_rx) = mpsc::unbounded_channel::(); - let runtime = Arc::new(ChannelBatchRuntime { tx: batch_tx }); - - let started = std::time::Instant::now(); - let task = tokio::spawn(async move { hypr_listener2_core::run_batch(runtime, params).await }); - - Ok(BatchHandle { - rx: batch_rx, - task, - started, - _normalized_input_dir: temp_dir, - _server: resolved.server, - }) -} - -fn normalize_input_file( - input: &Path, - dest: Option<&Path>, - on_progress: Option<&mut dyn FnMut(f64)>, -) -> CliResult<(Option, PathBuf)> { - let (temp_dir, tmp_path, target_path) = if let Some(dest) = dest { - let tmp_path = dest.with_extension("mp3.tmp"); - (None, tmp_path, dest.to_path_buf()) - } else { - let td = tempfile::Builder::new() - .prefix("char-transcribe-") - .tempdir() - .map_err(|e| { - CliError::operation_failed("create normalization tempdir", e.to_string()) - })?; - let tmp_path = td.path().join("input.mp3.tmp"); - let target_path = td.path().join("input.mp3"); - (Some(td), tmp_path, target_path) - }; - - hypr_audio_norm::normalize_file(input, &tmp_path, &target_path, None, on_progress) - .map_err(|e| CliError::operation_failed("normalize audio input", e.to_string()))?; - - Ok((temp_dir, target_path)) -} - -fn build_batch_params( - resolved: &crate::stt::ResolvedSttConfig, - input: &std::path::Path, - keywords: Vec, -) -> CliResult { - let file_path = input.to_str().ok_or_else(|| { - CliError::invalid_argument( - "--input", - input.display().to_string(), - "path must be valid utf-8", - ) - })?; - - Ok(resolved.to_batch_params( - uuid::Uuid::new_v4().to_string(), - file_path.to_string(), - keywords, - )) -} - -fn input_timestamp(path: &Path) -> String { - let system_time = std::fs::metadata(path) - .and_then(|m| m.modified()) - .unwrap_or_else(|_| std::time::SystemTime::now()); - format_timestamp(system_time) -} - -fn format_timestamp(t: std::time::SystemTime) -> String { - let secs = t - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_secs(); - let days = secs / 86400; - let time_of_day = secs % 86400; - let hour = time_of_day / 3600; - let min = (time_of_day % 3600) / 60; - let sec = time_of_day % 60; - - // days since epoch to y/m/d (civil calendar) - let (y, m, d) = days_to_ymd(days as i64); - format!("{y:04}{m:02}{d:02}_{hour:02}{min:02}{sec:02}") -} - -fn days_to_ymd(days: i64) -> (i64, u32, u32) { - // Algorithm from http://howardhinnant.github.io/date_algorithms.html - let z = days + 719468; - let era = z.div_euclid(146097); - let doe = z.rem_euclid(146097) as u64; - let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; - let y = yoe as i64 + era * 400; - let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); - let mp = (5 * doy + 2) / 153; - let d = (doy - (153 * mp + 2) / 5 + 1) as u32; - let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32; - let y = if m <= 2 { y + 1 } else { y }; - (y, m, d) -} - -fn extract_stream_transcript(event: &BatchStreamEvent) -> Option<&str> { - event.text() -} - -struct CollectedBatch { - response: owhisper_interface::batch::Response, - elapsed: Duration, -} - -struct OutputTarget { - output_path: PathBuf, - audio_dest: Option, -} - -fn finish_batch( - task_result: Result< - Result, - tokio::task::JoinError, - >, - failure: Option<(BatchErrorCode, String)>, - started: std::time::Instant, -) -> CliResult { - let result = task_result - .map_err(|e| CliError::operation_failed("batch transcription", e.to_string()))?; - let output = if let Ok(output) = result { - output - } else { - let error = result.err().unwrap(); - let message = if let Some((code, message)) = failure { - format!("{code:?}: {message}") - } else { - error.to_string() - }; - return Err(CliError::operation_failed("batch transcription", message)); - }; - - Ok(CollectedBatch { - response: output.response, - elapsed: started.elapsed(), - }) -} - -fn resolve_input( - target: Option<&str>, - input: Option<&str>, - base: Option<&Path>, -) -> CliResult { - let raw = match (target, input) { - (Some(t), _) => t, - (None, Some(i)) => i, - (None, None) => { - return Err(CliError::invalid_argument( - "INPUT", - "(missing)", - "provide an audio file or session timestamp", - )); - } - }; - - let as_path = PathBuf::from(raw); - if as_path.is_file() { - return Ok(as_path); - } - - let base_dir = base.map(Path::to_path_buf).unwrap_or_else(|| { - dirs::data_dir() - .unwrap_or_else(std::env::temp_dir) - .join("char") - }); - - let session_audio = base_dir.join(raw).join("audio.mp3"); - if session_audio.is_file() { - return Ok(session_audio); - } - - Err(CliError::not_found( - format!("audio file for '{raw}'"), - Some(format!( - "Pass a file path or a session timestamp.\nLooked in: {}", - session_audio.display() - )), - )) -} - -fn resolve_output_target( - input_path: &Path, - output: Option, - base: Option<&Path>, -) -> CliResult { - if let Some(output_path) = output { - return Ok(OutputTarget { - output_path, - audio_dest: None, - }); - } - - let ts = input_timestamp(input_path); - let dir = super::resolve_session_dir(base, &ts)?; - - Ok(OutputTarget { - output_path: dir.join("transcript.json"), - audio_dest: Some(dir.join("audio.mp3")), - }) -} - -fn saves_json(output_path: &Path) -> bool { - output_path.file_name() == Some(std::ffi::OsStr::new("transcript.json")) -} - -async fn save_pretty_output( - output_path: Option<&Path>, - pretty: &str, - response: &owhisper_interface::batch::Response, -) -> CliResult<()> { - let Some(output_path) = output_path else { - return Ok(()); - }; - - if saves_json(output_path) { - crate::output::write_json(Some(output_path), response).await - } else { - crate::output::write_text(Some(output_path), pretty.to_string()).await - } -} - -// -- Entry point -- - -#[allow(clippy::unit_arg)] -pub async fn run(ctx: &AppContext, args: Args) -> CliResult<()> { - let input_path = resolve_input( - args.target.as_deref(), - args.input.as_deref(), - args.base.as_deref(), - )?; - let format = args.format; - let output_target = - resolve_output_target(&input_path, args.output.clone(), args.base.as_deref())?; - let output_path = Some(output_target.output_path.clone()); - let input_display = input_path.display().to_string(); - let provider_display = format!("{:?}", args.provider).to_lowercase(); - let stt = ctx.stt_overrides( - Some(args.provider), - args.base_url.clone(), - args.api_key.clone(), - args.model.clone(), - args.language.clone(), - ); - - match format { - OutputFormat::Json => { - run_json( - &input_path, - args, - stt, - output_path, - output_target.audio_dest, - input_display, - provider_display, - ) - .await - } - OutputFormat::Pretty => { - run_pretty( - &input_path, - args, - stt, - output_path, - output_target.audio_dest, - ctx.trace_buffer(), - ) - .await - } - } -} - -// -- JSON mode -- - -async fn run_json( - input_path: &Path, - args: Args, - stt: SttOverrides, - output_path: Option, - audio_dest: Option, - input_display: String, - provider_display: String, -) -> CliResult<()> { - let mut writer = EventWriter::new(BufWriter::new(std::io::stdout())); - - writer.emit(&TranscribeEvent::Started { - input: input_display, - provider: provider_display, - })?; - - let mut handle = match start_batch( - input_path, - args.keywords.clone(), - stt, - audio_dest.as_deref(), - None, - ) - .await - { - Ok(h) => h, - Err(e) => { - let _ = writer.emit(&TranscribeEvent::Failed { - code: "error".to_string(), - message: e.to_string(), - }); - return Err(e); - } - }; - - let mut failure: Option<(BatchErrorCode, String)> = None; - - while let Some(event) = handle.rx.recv().await { - match event { - BatchEvent::BatchStarted { .. } | BatchEvent::BatchCompleted { .. } => {} - BatchEvent::BatchResponseStreamed { - event: streamed, .. - } => { - let transcript = extract_stream_transcript(&streamed) - .unwrap_or("") - .to_string(); - writer.emit(&TranscribeEvent::Progress { - percentage: streamed.percentage(), - transcript, - })?; - } - BatchEvent::BatchResponse { .. } => {} - BatchEvent::BatchFailed { code, error, .. } => { - failure = Some((code, error)); - } - } - } - - let result = match finish_batch(handle.task.await, failure, handle.started) { - Ok(r) => r, - Err(e) => { - let _ = writer.emit(&TranscribeEvent::Failed { - code: "error".to_string(), - message: e.to_string(), - }); - return Err(e); - } - }; - - let audio_duration_secs = result - .response - .metadata - .get("duration") - .and_then(|v| v.as_f64()); - - writer.emit(&TranscribeEvent::Completed { - elapsed_ms: result.elapsed.as_millis() as u64, - audio_duration_secs, - response: result.response.clone(), - })?; - - if let Some(path) = &output_path { - crate::output::write_json(Some(path.as_path()), &result.response).await?; - } - - Ok(()) -} - -// -- Pretty mode -- - -async fn run_pretty( - input_path: &Path, - args: Args, - stt: SttOverrides, - output_path: Option, - audio_dest: Option, - _trace_buffer: OptTraceBuffer, -) -> CliResult<()> { - let update_check = super::update_check::UpdateHandle::spawn(); - - #[cfg(feature = "_cli-audio")] - let file_name = input_path - .file_name() - .map(|n| n.to_string_lossy().into_owned()) - .unwrap_or_else(|| input_path.display().to_string()); - - #[cfg(feature = "_cli-audio")] - let mut viewport = if _trace_buffer.is_some() { - crate::tui::InlineViewport::stderr(5, _trace_buffer).ok() - } else { - None - }; - - #[cfg(feature = "_cli-audio")] - let mut normalize_spinner_idx = 0usize; - #[cfg(feature = "_cli-audio")] - let mut normalize_progress = |percentage: f64| { - normalize_spinner_idx = (normalize_spinner_idx + 1) % crate::tui::SPINNER.len(); - draw_normalization( - &mut viewport, - &file_name, - normalize_spinner_idx, - percentage.clamp(0.0, 1.0), - ); - }; - - #[cfg(feature = "_cli-audio")] - let mut handle = match start_batch( - input_path, - args.keywords.clone(), - stt, - audio_dest.as_deref(), - Some(&mut normalize_progress), - ) - .await - { - Ok(handle) => handle, - Err(error) => { - if let Some(ref mut vp) = viewport { - let _ = vp.clear(); - } - return Err(error); - } - }; - - #[cfg(not(feature = "_cli-audio"))] - let mut handle = start_batch( - input_path, - args.keywords.clone(), - stt, - audio_dest.as_deref(), - None, - ) - .await?; - - let mut failure: Option<(BatchErrorCode, String)> = None; - - #[cfg(feature = "_cli-audio")] - let (mut spinner_idx, mut last_pct, mut last_transcript, mut tick) = ( - 0usize, - 0.0f64, - String::new(), - tokio::time::interval(Duration::from_millis(80)), - ); - - loop { - #[cfg(feature = "_cli-audio")] - let event = if viewport.is_some() { - tokio::select! { - ev = handle.rx.recv() => ev, - _ = tick.tick() => { - spinner_idx = (spinner_idx + 1) % crate::tui::SPINNER.len(); - if let Some(ref mut vp) = viewport { - vp.poll_input(); - let pct_str = format!("{:.0}%", last_pct * 100.0); - vp.draw_strings(&[ - format!("{} Transcribing {}... {}", crate::tui::SPINNER[spinner_idx], file_name, pct_str), - format!(" {}", crate::tui::truncate_line(&last_transcript, 76)), - ]); - } - continue; - } - } - } else { - handle.rx.recv().await - }; - - #[cfg(not(feature = "_cli-audio"))] - let event = handle.rx.recv().await; - - let Some(event) = event else { break }; - - match event { - BatchEvent::BatchStarted { .. } | BatchEvent::BatchCompleted { .. } => {} - BatchEvent::BatchResponseStreamed { - event: streamed, .. - } => { - #[cfg(feature = "_cli-audio")] - { - last_pct = streamed.percentage(); - if let Some(t) = extract_stream_transcript(&streamed) - && !t.is_empty() - { - last_transcript = t.to_string(); - } - } - #[cfg(not(feature = "_cli-audio"))] - { - let _ = streamed; - } - } - BatchEvent::BatchResponse { .. } => {} - BatchEvent::BatchFailed { code, error, .. } => { - failure = Some((code, error)); - } - } - } - - let result = finish_batch(handle.task.await, failure, handle.started)?; - let response = &result.response; - let pretty = output::format_pretty(response); - - crate::output::write_text(None, pretty.clone()).await?; - save_pretty_output(output_path.as_deref(), &pretty, response).await?; - - let audio_duration = response - .metadata - .get("duration") - .and_then(|v| v.as_f64()) - .unwrap_or(0.0); - - let mut parts = Vec::new(); - if audio_duration > 0.0 { - parts.push(format!("{:.1}s audio", audio_duration)); - } - parts.push(format!("in {:.1}s", result.elapsed.as_secs_f64())); - - #[cfg(feature = "_cli-audio")] - if let Some(ref mut vp) = viewport { - let mut lines = vec![format!("saved {}", parts.join(" "))]; - if let Some(path) = &output_path { - lines.push(format!("{}", path.display())); - } - vp.draw_strings(&lines); - vp.finish() - .map_err(|e| CliError::operation_failed("finish viewport", e.to_string()))?; - } else { - if let Some(path) = &output_path { - parts.push(format!("-> {}", path.display())); - } - use colored::Colorize; - eprintln!("{}", parts.join(", ").dimmed()); - } - - #[cfg(not(feature = "_cli-audio"))] - { - if let Some(path) = &output_path { - parts.push(format!("-> {}", path.display())); - } - use colored::Colorize; - eprintln!("{}", parts.join(", ").dimmed()); - } - - if let Some(version) = update_check.result().await { - eprintln!(" update available ({version}): char update"); - } - - Ok(()) -} - -#[cfg(feature = "_cli-audio")] -fn draw_normalization( - viewport: &mut Option, - file_name: &str, - spinner_idx: usize, - percentage: f64, -) { - if let Some(vp) = viewport { - vp.poll_input(); - let pct = (percentage * 100.0).round().clamp(0.0, 100.0) as u8; - vp.draw_strings(&[ - format!( - "{} Normalizing {}... {}%", - crate::tui::SPINNER[spinner_idx], - file_name, - pct - ), - format_gauge(pct), - ]); - } -} - -#[cfg(feature = "_cli-audio")] -fn format_gauge(pct: u8) -> String { - let width = 40; - let filled = (pct as usize * width) / 100; - let empty = width - filled; - format!(" [{}{}]", "█".repeat(filled), "░".repeat(empty)) -} - -#[cfg(test)] -mod tests { - use std::io::Cursor; - use std::path::Path; - - use owhisper_interface::batch; - - use super::*; - - #[test] - fn event_writer_serializes_jsonl_lines() { - let mut bytes = Cursor::new(Vec::new()); - let mut writer = EventWriter::new(&mut bytes); - - writer - .emit(&TranscribeEvent::Started { - input: "test.wav".to_string(), - provider: "deepgram".to_string(), - }) - .unwrap(); - - let output = String::from_utf8(bytes.into_inner()).unwrap(); - assert!(output.ends_with('\n')); - assert!(output.contains("\"type\":\"started\"")); - assert!(output.contains("\"input\":\"test.wav\"")); - } - - #[test] - fn build_batch_params_preserves_keywords() { - let resolved = crate::stt::ResolvedSttConfig { - provider: hypr_listener2_core::BatchProvider::Deepgram, - base_url: "https://example.com".to_string(), - api_key: "secret".to_string(), - model: "nova".to_string(), - language: "en".parse().unwrap(), - server: crate::stt::ServerGuard::default(), - }; - - let params = build_batch_params( - &resolved, - Path::new("/tmp/example.mp3"), - vec!["roadmap".to_string(), "planning".to_string()], - ) - .unwrap(); - - assert_eq!(params.keywords, vec!["roadmap", "planning"]); - } - - #[test] - fn resolve_output_target_uses_json_session_artifact_by_default() { - let dir = tempfile::tempdir().unwrap(); - let input = dir.path().join("input.mp3"); - std::fs::write(&input, b"audio").unwrap(); - - let target = resolve_output_target(&input, None, Some(dir.path())).unwrap(); - - assert_eq!(target.output_path.file_name().unwrap(), "transcript.json"); - assert_eq!( - target.audio_dest.as_ref().and_then(|path| path.file_name()), - Some(std::ffi::OsStr::new("audio.mp3")) - ); - } - - #[test] - fn saves_json_only_for_transcript_json() { - assert!(saves_json(Path::new("/tmp/transcript.json"))); - assert!(!saves_json(Path::new("/tmp/output.txt"))); - } - - #[tokio::test] - async fn save_pretty_output_persists_json_response_for_transcript_json() { - let dir = tempfile::tempdir().unwrap(); - let out = dir.path().join("transcript.json"); - let response = batch::Response { - metadata: serde_json::json!({ "duration": 1.2 }), - results: batch::Results { - channels: vec![batch::Channel { - alternatives: vec![batch::Alternatives { - transcript: "hello world".to_string(), - confidence: 1.0, - words: vec![batch::Word { - word: "hello".to_string(), - start: 0.0, - end: 0.5, - confidence: 1.0, - channel: 0, - speaker: None, - punctuated_word: Some("hello".to_string()), - }], - }], - }], - }, - }; - - save_pretty_output(Some(&out), "pretty text", &response) - .await - .unwrap(); - - let saved = std::fs::read_to_string(out).unwrap(); - let parsed: batch::Response = serde_json::from_str(&saved).unwrap(); - - assert_eq!( - parsed.results.channels[0].alternatives[0].transcript, - "hello world" - ); - assert!(saved.starts_with('{')); - } - - #[tokio::test] - async fn save_pretty_output_writes_pretty_text_for_non_json_path() { - let dir = tempfile::tempdir().unwrap(); - let out = dir.path().join("transcript.txt"); - let response = batch::Response { - metadata: serde_json::json!({}), - results: batch::Results { - channels: Vec::new(), - }, - }; - - save_pretty_output(Some(&out), "pretty text", &response) - .await - .unwrap(); - - assert_eq!(std::fs::read_to_string(out).unwrap(), "pretty text\n"); - } -} diff --git a/apps/cli/src/commands/transcribe/output.rs b/apps/cli/src/commands/transcribe/output.rs deleted file mode 100644 index 3ac5509c27..0000000000 --- a/apps/cli/src/commands/transcribe/output.rs +++ /dev/null @@ -1,237 +0,0 @@ -use colored::Colorize; - -use crate::output::format_timestamp_secs; - -const PAUSE_THRESHOLD_SECS: f64 = 0.5; - -const SPEAKER_COLORS: &[colored::Color] = &[ - colored::Color::Cyan, - colored::Color::Green, - colored::Color::Yellow, - colored::Color::Magenta, - colored::Color::Blue, - colored::Color::Red, -]; - -fn speaker_color(speaker: usize) -> colored::Color { - SPEAKER_COLORS[speaker % SPEAKER_COLORS.len()] -} - -struct Segment<'a> { - start: f64, - end: f64, - words: Vec<&'a str>, - identity: usize, -} - -pub(super) fn format_pretty(response: &owhisper_interface::batch::Response) -> String { - let mut segments: Vec = Vec::new(); - let num_channels = response.results.channels.len(); - - struct TaggedWord<'a> { - text: &'a str, - start: f64, - end: f64, - identity: usize, - } - - let mut all_words: Vec = Vec::new(); - for (channel_idx, channel) in response.results.channels.iter().enumerate() { - let Some(alt) = channel.alternatives.first() else { - continue; - }; - for word in &alt.words { - all_words.push(TaggedWord { - text: word - .punctuated_word - .as_deref() - .unwrap_or(word.word.as_str()), - start: word.start, - end: word.end, - identity: word_identity(word, channel_idx, num_channels), - }); - } - } - all_words.sort_by(|a, b| { - a.start - .partial_cmp(&b.start) - .unwrap_or(std::cmp::Ordering::Equal) - }); - - for word in &all_words { - let should_split = segments - .last() - .map(|seg| word.start - seg.end > PAUSE_THRESHOLD_SECS || word.identity != seg.identity) - .unwrap_or(true); - - if should_split { - segments.push(Segment { - start: word.start, - end: word.end, - words: vec![word.text], - identity: word.identity, - }); - } else if let Some(seg) = segments.last_mut() { - seg.end = word.end; - seg.words.push(word.text); - } - } - - if segments.is_empty() { - return extract_transcript(response); - } - - let term_width = textwrap::termwidth(); - let show_speaker = - num_channels > 1 || segments.iter().any(|s| s.identity != segments[0].identity); - - segments - .iter() - .map(|seg| { - let timestamp = format!( - "[{} \u{2192} {}]", - format_timestamp_secs(seg.start), - format_timestamp_secs(seg.end) - ) - .dimmed() - .to_string(); - - let label = format!("{} ", timestamp); - let text = seg.words.join(" "); - let text = if show_speaker { - text.color(speaker_color(seg.identity)).to_string() - } else { - text - }; - - let visible_prefix_len = 22; - let wrap_width = term_width.saturating_sub(visible_prefix_len); - - if wrap_width == 0 || text.len() <= wrap_width { - format!("{}{}", label, text) - } else { - let indent = " ".repeat(visible_prefix_len); - let wrapped = textwrap::fill( - &text, - textwrap::Options::new(wrap_width).subsequent_indent(""), - ); - let mut lines = wrapped.lines(); - let first = lines.next().unwrap_or(""); - let rest: Vec<&str> = lines.collect(); - if rest.is_empty() { - format!("{}{}", label, first) - } else { - format!( - "{}{}\n{}", - label, - first, - rest.iter() - .map(|l| format!("{}{}", indent, l)) - .collect::>() - .join("\n") - ) - } - } - }) - .collect::>() - .join("\n\n") -} - -fn word_identity( - word: &owhisper_interface::batch::Word, - channel_idx: usize, - total_channels: usize, -) -> usize { - if total_channels > 1 { - channel_idx - } else { - word.speaker.unwrap_or(word.channel.max(0) as usize) - } -} - -pub(super) fn extract_transcript(response: &owhisper_interface::batch::Response) -> String { - response - .results - .channels - .iter() - .filter_map(|c| c.alternatives.first()) - .map(|alt| alt.transcript.trim()) - .filter(|t| !t.is_empty()) - .collect::>() - .join("\n") -} - -#[cfg(test)] -mod tests { - use owhisper_interface::batch; - - use super::*; - - fn response_with_channels(channel_words: Vec>) -> batch::Response { - batch::Response { - metadata: serde_json::json!({}), - results: batch::Results { - channels: channel_words - .into_iter() - .map(|words| batch::Channel { - alternatives: vec![batch::Alternatives { - transcript: words - .iter() - .map(|word| word.word.as_str()) - .collect::>() - .join(" "), - confidence: 1.0, - words, - }], - }) - .collect(), - }, - } - } - - fn word(text: &str, start: f64, end: f64, channel: i32, speaker: Option) -> batch::Word { - batch::Word { - word: text.to_string(), - start, - end, - confidence: 1.0, - channel, - speaker, - punctuated_word: Some(text.to_string()), - } - } - - #[test] - fn pretty_output_splits_multichannel_words_by_channel() { - colored::control::set_override(false); - - let response = response_with_channels(vec![ - vec![word("left", 0.0, 0.4, 0, None)], - vec![word("right", 0.1, 0.5, 0, None)], - ]); - - let pretty = format_pretty(&response); - let blocks = pretty.split("\n\n").collect::>(); - - assert_eq!(blocks.len(), 2); - assert!(blocks[0].contains("left")); - assert!(blocks[1].contains("right")); - } - - #[test] - fn pretty_output_splits_single_channel_words_by_speaker() { - colored::control::set_override(false); - - let response = response_with_channels(vec![vec![ - word("hello", 0.0, 0.4, 0, Some(0)), - word("again", 0.45, 0.8, 0, Some(1)), - ]]); - - let pretty = format_pretty(&response); - let blocks = pretty.split("\n\n").collect::>(); - - assert_eq!(blocks.len(), 2); - assert!(blocks[0].contains("hello")); - assert!(blocks[1].contains("again")); - } -} diff --git a/apps/cli/src/commands/update.rs b/apps/cli/src/commands/update.rs deleted file mode 100644 index 38af4dbac3..0000000000 --- a/apps/cli/src/commands/update.rs +++ /dev/null @@ -1,19 +0,0 @@ -use std::process::Command; - -use crate::error::{CliError, CliResult}; - -pub fn run() -> CliResult<()> { - let status = Command::new("npm") - .args(["i", "-g", "char@latest"]) - .status() - .map_err(|e| CliError::operation_failed("run npm", e.to_string()))?; - - if !status.success() { - return Err(CliError::operation_failed( - "update char", - "npm install failed", - )); - } - - Ok(()) -} diff --git a/apps/cli/src/commands/update_check.rs b/apps/cli/src/commands/update_check.rs deleted file mode 100644 index 5eafac37e0..0000000000 --- a/apps/cli/src/commands/update_check.rs +++ /dev/null @@ -1,158 +0,0 @@ -const RELEASES_PER_PAGE: u32 = 100; - -#[derive(serde::Deserialize)] -struct Release { - tag_name: String, - prerelease: bool, -} - -pub struct UpdateHandle(tokio::task::JoinHandle>); - -impl UpdateHandle { - pub fn spawn() -> Self { - Self(tokio::spawn(async { - let current = option_env!("APP_VERSION").unwrap_or(env!("CARGO_PKG_VERSION")); - let client = reqwest::Client::builder() - .user_agent("char-cli") - .timeout(std::time::Duration::from_secs(3)) - .build() - .ok()?; - let latest = fetch_latest_cli_release(&client).await?; - is_newer(&latest, current).then_some(latest) - })) - } - - pub async fn result(self) -> Option { - self.0.await.ok().flatten() - } -} - -async fn fetch_latest_cli_release(client: &reqwest::Client) -> Option { - let mut best: Option<((u64, u64, u64), String)> = None; - let mut page = 1; - - loop { - let releases = fetch_release_page(client, page).await?; - let is_last = releases.len() < RELEASES_PER_PAGE as usize; - - for release in &releases { - if let Some((triple, version)) = parse_cli_release(release) - && best.as_ref().is_none_or(|(b, _)| triple > *b) - { - best = Some((triple, version.to_string())); - } - } - - if is_last { - break; - } - page += 1; - } - - best.map(|(_, version)| version) -} - -async fn fetch_release_page(client: &reqwest::Client, page: u32) -> Option> { - client - .get("https://api.github.com/repos/fastrepl/char/releases") - .query(&[("per_page", RELEASES_PER_PAGE), ("page", page)]) - .send() - .await - .ok()? - .json() - .await - .ok() -} - -#[cfg(test)] -fn find_latest_cli_release_on_page(releases: &[Release]) -> Option { - releases - .iter() - .filter_map(parse_cli_release) - .max_by_key(|(triple, _)| *triple) - .map(|(_, version)| version.to_string()) -} - -fn parse_cli_release(release: &Release) -> Option<((u64, u64, u64), &str)> { - if release.prerelease { - return None; - } - - let version = release.tag_name.strip_prefix("cli_v")?; - Some((parse_triple(version)?, version)) -} - -fn parse_triple(s: &str) -> Option<(u64, u64, u64)> { - let s = s.split('-').next().unwrap_or(s); - let mut parts = s.split('.'); - let major = parts.next()?.parse().ok()?; - let minor = parts.next()?.parse().ok()?; - let patch = parts.next()?.parse().ok()?; - Some((major, minor, patch)) -} - -fn is_newer(latest: &str, current: &str) -> bool { - match (parse_triple(latest), parse_triple(current)) { - (Some(l), Some(c)) => l > c, - _ => false, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn release(tag_name: &str, prerelease: bool) -> Release { - Release { - tag_name: tag_name.to_string(), - prerelease, - } - } - - #[test] - fn newer_version_detected() { - assert!(is_newer("1.2.0", "1.1.0")); - assert!(is_newer("2.0.0", "1.9.9")); - assert!(!is_newer("1.0.0", "1.0.0")); - assert!(!is_newer("0.9.0", "1.0.0")); - } - - #[test] - fn handles_prerelease_suffix() { - assert!(is_newer("1.1.0-beta", "1.0.0")); - } - - #[test] - fn selects_highest_stable_cli_release_on_page() { - assert_eq!( - find_latest_cli_release_on_page(&[ - release("desktop_v9.9.9", false), - release("cli_v1.1.0-beta", true), - release("cli_v1.0.3", false), - release("cli_v1.0.2", false), - ]), - Some("1.0.3".to_string()) - ); - } - - #[test] - fn picks_highest_version_regardless_of_order() { - assert_eq!( - find_latest_cli_release_on_page(&[ - release("cli_v1.0.9", false), - release("cli_v1.0.8", false), - release("cli_v1.0.11", false), - release("cli_v1.0.10", false), - ]), - Some("1.0.11".to_string()) - ); - } - - #[test] - fn returns_none_when_page_has_no_stable_cli_release() { - assert_eq!( - find_latest_cli_release_on_page(&[release("desktop_v9.9.9", false)]), - None - ); - } -} diff --git a/apps/cli/src/config/mod.rs b/apps/cli/src/config/mod.rs deleted file mode 100644 index 8118b2968e..0000000000 --- a/apps/cli/src/config/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod paths; diff --git a/apps/cli/src/config/paths.rs b/apps/cli/src/config/paths.rs deleted file mode 100644 index b632daf6c2..0000000000 --- a/apps/cli/src/config/paths.rs +++ /dev/null @@ -1,40 +0,0 @@ -use std::path::{Path, PathBuf}; - -#[derive(Clone, Debug)] -pub struct AppPaths { - #[allow(dead_code)] - pub base: PathBuf, - pub models_base: PathBuf, -} - -pub fn resolve_paths(base_override: Option<&Path>) -> AppPaths { - let data_dir = dirs::data_dir().unwrap_or_else(std::env::temp_dir); - let base = base_override - .map(Path::to_path_buf) - .unwrap_or_else(|| data_dir.join("char")); - let models_base = base.join("models"); - - AppPaths { base, models_base } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn resolve_paths_uses_override() { - let base = Path::new("/tmp/char-cli-tests"); - let paths = resolve_paths(Some(base)); - - assert_eq!(paths.base, base); - assert_eq!(paths.models_base, base.join("models")); - } - - #[test] - fn resolve_paths_defaults_models_under_base() { - let paths = resolve_paths(None); - - assert!(paths.base.ends_with("char")); - assert_eq!(paths.models_base, paths.base.join("models")); - } -} diff --git a/apps/cli/src/config/settings.rs b/apps/cli/src/config/settings.rs deleted file mode 100644 index 98e98a4842..0000000000 --- a/apps/cli/src/config/settings.rs +++ /dev/null @@ -1,104 +0,0 @@ -use std::collections::HashMap; -use std::path::Path; - -use sqlx::SqlitePool; - -#[derive(Clone, Debug)] -pub struct ProviderConfig { - pub base_url: Option, - pub api_key: Option, -} - -#[derive(Clone, Debug)] -pub struct Settings { - pub current_stt_provider: Option, - pub current_stt_model: Option, - pub stt_providers: HashMap, -} - -pub async fn migrate_json_settings_to_db(pool: &SqlitePool, base_path: &Path) { - let has_settings = hypr_db_app::load_all_settings(pool) - .await - .map(|v| !v.is_empty()) - .unwrap_or(false); - let has_connections = hypr_db_app::list_connections(pool, "stt") - .await - .map(|v| !v.is_empty()) - .unwrap_or(false); - - if has_settings || has_connections { - return; - } - - let json_path = base_path.join("settings.json"); - let Some(settings) = load_settings_from_json(&json_path) else { - return; - }; - - if let Some(ref v) = settings.current_stt_provider { - let _ = hypr_db_app::set_setting(pool, "current_stt_provider", v).await; - } - if let Some(ref v) = settings.current_stt_model { - let _ = hypr_db_app::set_setting(pool, "current_stt_model", v).await; - } - - for (provider_id, config) in &settings.stt_providers { - let _ = hypr_db_app::upsert_connection( - pool, - "stt", - provider_id, - config.base_url.as_deref().unwrap_or(""), - config.api_key.as_deref().unwrap_or(""), - ) - .await; - } -} - -fn load_settings_from_json(path: &Path) -> Option { - let content = std::fs::read_to_string(path).ok()?; - let json: serde_json::Value = serde_json::from_str(&content).ok()?; - let ai = json.get("ai")?.as_object()?; - - let current_stt_provider = get_string(ai.get("current_stt_provider")); - let current_stt_model = get_string(ai.get("current_stt_model")); - let stt_providers = parse_provider_map(ai.get("stt")); - - Some(Settings { - current_stt_provider, - current_stt_model, - stt_providers, - }) -} - -fn get_string(value: Option<&serde_json::Value>) -> Option { - value?.as_str().map(ToString::to_string) -} - -fn parse_provider_map(value: Option<&serde_json::Value>) -> HashMap { - let mut out = HashMap::new(); - let Some(obj) = value.and_then(serde_json::Value::as_object) else { - return out; - }; - - for (provider_id, config) in obj { - let Some(config_obj) = config.as_object() else { - continue; - }; - - let base_url = config_obj - .get("base_url") - .and_then(serde_json::Value::as_str) - .map(str::trim) - .filter(|v| !v.is_empty()) - .map(ToString::to_string); - let api_key = config_obj - .get("api_key") - .and_then(serde_json::Value::as_str) - .map(str::trim) - .filter(|v| !v.is_empty()) - .map(ToString::to_string); - out.insert(provider_id.clone(), ProviderConfig { base_url, api_key }); - } - - out -} diff --git a/apps/cli/src/error.rs b/apps/cli/src/error.rs deleted file mode 100644 index f398eb6426..0000000000 --- a/apps/cli/src/error.rs +++ /dev/null @@ -1,152 +0,0 @@ -use thiserror::Error; - -pub type CliResult = Result; - -#[derive(Debug, Error)] -pub enum CliError { - #[error("{0}")] - Message(String), - - #[error("{name} is required{}", hint_suffix(.hint))] - RequiredArgument { name: String, hint: Option }, - - #[error("invalid {name} '{value}': {reason}{}", hint_suffix(.hint))] - InvalidArgument { - name: &'static str, - value: String, - reason: String, - hint: Option, - }, - - #[error("{action} failed: {reason}")] - OperationFailed { - action: &'static str, - reason: String, - }, - - #[error("{what} not found{}", hint_suffix(.hint))] - NotFound { what: String, hint: Option }, -} - -fn hint_suffix(hint: &Option) -> String { - match hint { - Some(h) => format!("\n hint: {h}"), - None => String::new(), - } -} - -impl CliError { - pub fn msg(message: impl Into) -> Self { - Self::Message(message.into()) - } - - pub fn required_argument(name: impl Into) -> Self { - Self::RequiredArgument { - name: name.into(), - hint: None, - } - } - - pub fn required_argument_with_hint(name: impl Into, hint: impl Into) -> Self { - Self::RequiredArgument { - name: name.into(), - hint: Some(hint.into()), - } - } - - pub fn invalid_argument( - name: &'static str, - value: impl Into, - reason: impl Into, - ) -> Self { - Self::InvalidArgument { - name, - value: value.into(), - reason: reason.into(), - hint: None, - } - } - - pub fn operation_failed(action: &'static str, reason: impl Into) -> Self { - Self::OperationFailed { - action, - reason: reason.into(), - } - } - - pub fn not_found(what: impl Into, hint: Option) -> Self { - Self::NotFound { - what: what.into(), - hint, - } - } -} - -/// Returns the closest match from `candidates` if one exceeds a 0.7 Jaro-Winkler threshold. -pub fn did_you_mean<'a>(input: &str, candidates: &[&'a str]) -> Option<&'a str> { - candidates - .iter() - .filter_map(|c| { - let sim = strsim::jaro_winkler(input, c); - if sim > 0.7 { Some((*c, sim)) } else { None } - }) - .max_by(|a, b| a.1.total_cmp(&b.1)) - .map(|(c, _)| c) -} - -impl From for CliError { - fn from(message: String) -> Self { - Self::Message(message) - } -} - -impl From<&str> for CliError { - fn from(message: &str) -> Self { - Self::msg(message) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn invalid_argument_has_structured_fields() { - let error = CliError::invalid_argument("--language", "xx", "unknown code"); - - match error { - CliError::InvalidArgument { - name, - value, - reason, - .. - } => { - assert_eq!(name, "--language"); - assert_eq!(value, "xx"); - assert_eq!(reason, "unknown code"); - } - _ => panic!("expected invalid argument variant"), - } - } - - #[test] - fn did_you_mean_finds_close_match() { - let candidates = &["deepgram", "soniox", "whispercpp", "cactus"]; - assert_eq!(did_you_mean("deepgran", candidates), Some("deepgram")); - assert_eq!(did_you_mean("sonix", candidates), Some("soniox")); - assert_eq!(did_you_mean("whispr", candidates), Some("whispercpp")); - assert_eq!(did_you_mean("completely-wrong", candidates), None); - } - - #[test] - fn not_found_includes_hint_in_display() { - let error = CliError::not_found( - "model 'foo'", - Some("Run `char model list` to see available models.".to_string()), - ); - - let rendered = error.to_string(); - assert!(rendered.contains("model 'foo' not found")); - assert!(rendered.contains("hint:")); - } -} diff --git a/apps/cli/src/main.rs b/apps/cli/src/main.rs deleted file mode 100644 index 88b4109b51..0000000000 --- a/apps/cli/src/main.rs +++ /dev/null @@ -1,159 +0,0 @@ -mod app; -mod cli; -mod commands; -mod config; -mod error; -mod output; -#[cfg(feature = "_cli-audio")] -mod stt; -#[cfg(feature = "_cli-tui")] -pub(crate) mod tui; - -use crate::cli::{Cli, Commands}; -use crate::error::CliResult; -use clap::Parser; - -#[allow(clippy::let_unit_value)] -fn main() { - let cli = Cli::parse(); - - if cli.no_color || std::env::var_os("NO_COLOR").is_some() { - colored::control::set_override(false); - } - - let trace_buffer = init_tracing(&cli); - - let runtime = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .expect("failed to build tokio runtime"); - let result = runtime.block_on(run(cli, trace_buffer)); - - if let Err(error) = result { - eprintln!("error: {error}"); - std::process::exit(1); - } -} - -#[cfg(feature = "_cli-tui")] -type OptTraceBuffer = Option; -#[cfg(not(feature = "_cli-tui"))] -type OptTraceBuffer = (); - -fn init_tracing(cli: &Cli) -> OptTraceBuffer { - let level = cli.verbose.tracing_level_filter(); - - #[cfg(feature = "_cli-audio")] - let wants_json = matches!( - cli.command, - Some(Commands::Transcribe { - args: commands::transcribe::Args { - format: cli::OutputFormat::Json, - .. - }, - }) - ); - #[cfg(not(feature = "_cli-audio"))] - let wants_json = false; - - #[cfg(feature = "_cli-audio")] - let wants_json = wants_json - || matches!( - cli.command, - Some(Commands::Record { - args: commands::record::Args { - format: cli::OutputFormat::Json, - .. - }, - }) - ); - - #[cfg(feature = "_cli-audio")] - let wants_capture = !wants_json - && std::io::IsTerminal::is_terminal(&std::io::stderr()) - && matches!( - cli.command, - Some( - Commands::Transcribe { .. } - | Commands::Models { .. } - | Commands::Record { .. } - | Commands::Play { .. }, - ) - ); - - #[cfg(feature = "_cli-audio")] - if wants_capture { - let buf = tui::new_trace_buffer(); - init_tracing_capture(level, buf.clone()); - return Some(buf); - } - - if wants_json { - init_tracing_json(level); - } else { - init_tracing_stderr(level); - } - - #[cfg(feature = "_cli-tui")] - return None; - #[cfg(not(feature = "_cli-tui"))] - return; -} - -fn init_tracing_stderr(level: tracing_subscriber::filter::LevelFilter) { - use tracing_subscriber::EnvFilter; - let filter = EnvFilter::builder() - .with_default_directive(level.into()) - .from_env_lossy(); - tracing_subscriber::fmt() - .with_env_filter(filter) - .with_writer(std::io::stderr) - .init(); -} - -fn init_tracing_json(level: tracing_subscriber::filter::LevelFilter) { - use tracing_subscriber::EnvFilter; - let filter = EnvFilter::builder() - .with_default_directive(level.into()) - .from_env_lossy(); - tracing_subscriber::fmt() - .json() - .with_env_filter(filter) - .with_writer(std::io::stderr) - .init(); -} - -#[cfg(feature = "_cli-audio")] -fn init_tracing_capture(level: tracing_subscriber::filter::LevelFilter, buffer: tui::TraceBuffer) { - use tracing_subscriber::EnvFilter; - use tracing_subscriber::layer::SubscriberExt; - use tracing_subscriber::util::SubscriberInitExt; - - let filter = EnvFilter::builder() - .with_default_directive(level.into()) - .from_env_lossy(); - let capture = tui::CaptureLayer::new(buffer); - tracing_subscriber::registry() - .with(filter) - .with(capture) - .init(); -} - -async fn run(cli: Cli, trace_buffer: OptTraceBuffer) -> CliResult<()> { - let base = cli - .command - .as_ref() - .and_then(Commands::base_override) - .map(std::path::Path::to_path_buf); - let tracked = cli.command.as_ref().map(Into::into); - let Cli { - command, verbose, .. - } = cli; - let ctx = app::AppContext::new(base.as_deref(), verbose.is_silent(), trace_buffer); - - if let Some(subcommand) = tracked { - ctx.track_command(subcommand); - } - - commands::run(&ctx, command).await -} diff --git a/apps/cli/src/output.rs b/apps/cli/src/output.rs deleted file mode 100644 index 1bdd9904bd..0000000000 --- a/apps/cli/src/output.rs +++ /dev/null @@ -1,100 +0,0 @@ -use crate::error::{CliError, CliResult}; -use std::io::{IsTerminal, Write}; -use std::path::Path; - -#[allow(dead_code)] -pub fn format_timestamp_ms(ms: i64) -> String { - let total_secs = (ms / 1000).max(0); - let mins = total_secs / 60; - let secs = total_secs % 60; - if mins >= 60 { - let hours = mins / 60; - let mins = mins % 60; - format!("{hours:02}:{mins:02}:{secs:02}") - } else { - format!("{mins:02}:{secs:02}") - } -} - -pub fn format_timestamp_secs(secs: f64) -> String { - let total_tenths = (secs.max(0.0) * 10.0).round() as u64; - let mins = total_tenths / 600; - let s = (total_tenths % 600) / 10; - let frac = total_tenths % 10; - format!("{mins:02}:{s:02}.{frac}") -} - -async fn ensure_parent_dirs(path: &Path) -> CliResult<()> { - if let Some(parent) = path.parent() { - tokio::fs::create_dir_all(parent) - .await - .map_err(|e| CliError::operation_failed("create output directory", e.to_string()))?; - } - Ok(()) -} - -async fn write_bytes_to(output: Option<&Path>, bytes: Vec) -> CliResult<()> { - if let Some(path) = output { - ensure_parent_dirs(path).await?; - tokio::fs::write(path, bytes) - .await - .map_err(|e| CliError::operation_failed("write output", e.to_string()))?; - return Ok(()); - } - - std::io::stdout() - .write_all(&bytes) - .map_err(|e| CliError::operation_failed("write output", e.to_string()))?; - Ok(()) -} - -pub async fn write_text(output: Option<&Path>, text: String) -> CliResult<()> { - write_bytes_to(output, (text + "\n").into_bytes()).await -} - -pub async fn write_json(output: Option<&Path>, value: &impl serde::Serialize) -> CliResult<()> { - let pretty = output.is_some() || std::io::stdout().is_terminal(); - let mut bytes: Vec = if pretty { - serde_json::to_vec_pretty(value) - } else { - serde_json::to_vec(value) - } - .map_err(|e| CliError::operation_failed("serialize response", e.to_string()))?; - bytes.push(b'\n'); - - write_bytes_to(output, bytes).await -} - -pub struct EventWriter { - writer: W, -} - -impl EventWriter { - pub fn new(writer: W) -> Self { - Self { writer } - } - - pub fn emit(&mut self, event: &impl serde::Serialize) -> CliResult<()> { - serde_json::to_writer(&mut self.writer, event) - .map_err(|e| CliError::operation_failed("serialize event", e.to_string()))?; - self.writer - .write_all(b"\n") - .map_err(|e| CliError::operation_failed("write event", e.to_string()))?; - self.writer - .flush() - .map_err(|e| CliError::operation_failed("flush event", e.to_string())) - } -} - -#[cfg(test)] -mod tests { - use super::format_timestamp_secs; - - #[test] - fn format_timestamp_secs_handles_rounding_boundaries() { - assert_eq!(format_timestamp_secs(5.94), "00:05.9"); - assert_eq!(format_timestamp_secs(5.95), "00:06.0"); - assert_eq!(format_timestamp_secs(59.95), "01:00.0"); - assert_eq!(format_timestamp_secs(0.0), "00:00.0"); - } -} diff --git a/apps/cli/src/stt/config.rs b/apps/cli/src/stt/config.rs deleted file mode 100644 index e8c02f458c..0000000000 --- a/apps/cli/src/stt/config.rs +++ /dev/null @@ -1,486 +0,0 @@ -use std::path::{Path, PathBuf}; - -use hypr_listener2_core::{BatchEvent, BatchParams, BatchProvider, BatchRuntime}; -use hypr_local_model::{CactusSttModel, LocalModel, WhisperModel}; -#[cfg(target_os = "macos")] -use hypr_local_stt_server::LocalSttServer; -use tokio::sync::mpsc; - -use crate::error::{CliError, CliResult, did_you_mean}; - -use super::SttProvider; - -#[cfg(target_os = "macos")] -pub type ServerGuard = Option; - -#[cfg(not(target_os = "macos"))] -pub type ServerGuard = (); - -pub struct SttOverrides { - pub provider: Option, - pub base_url: Option, - pub api_key: Option, - pub model: Option, - pub language: String, - pub models_base: PathBuf, -} - -pub struct ChannelBatchRuntime { - pub tx: mpsc::UnboundedSender, -} - -impl BatchRuntime for ChannelBatchRuntime { - fn emit(&self, event: BatchEvent) { - let _ = self.tx.send(event); - } -} - -#[cfg(target_os = "macos")] -pub struct LocalServerInfo { - pub server: LocalSttServer, - pub base_url: String, - pub model_name: String, -} - -pub struct ResolvedSttConfig { - pub provider: BatchProvider, - pub base_url: String, - pub api_key: String, - pub model: String, - pub language: hypr_language::Language, - pub server: ServerGuard, -} - -impl ResolvedSttConfig { - pub fn model_option(&self) -> Option { - if self.model.is_empty() { - None - } else { - Some(self.model.clone()) - } - } - - pub fn to_batch_params( - &self, - session_id: String, - file_path: String, - keywords: Vec, - ) -> BatchParams { - BatchParams { - session_id, - provider: self.provider.clone(), - file_path, - model: self.model_option(), - base_url: self.base_url.clone(), - api_key: self.api_key.clone(), - languages: vec![self.language.clone()], - keywords, - num_speakers: None, - min_speakers: None, - max_speakers: None, - } - } -} - -pub async fn resolve_config(overrides: SttOverrides) -> CliResult { - let language_code = overrides.language; - let language = language_code - .parse::() - .map_err(|e| CliError::invalid_argument("--language", language_code, e.to_string()))?; - - let provider = overrides.provider.ok_or_else(|| { - CliError::required_argument_with_hint("--provider", "Pass --provider explicitly") - })?; - let base_url = overrides.base_url; - let api_key = overrides.api_key; - let model = overrides.model; - - #[cfg(target_os = "macos")] - if provider.is_local() { - let info = match provider { - SttProvider::Whispercpp => { - resolve_and_spawn_whisper(&overrides.models_base, model.as_deref()).await? - } - #[cfg(all(target_os = "macos", any(target_arch = "arm", target_arch = "aarch64")))] - SttProvider::Cactus => { - resolve_and_spawn_cactus(&overrides.models_base, model.as_deref()).await? - } - _ => unreachable!("cloud providers are handled below"), - }; - - return Ok(ResolvedSttConfig { - provider: provider.to_batch_provider(), - base_url: info.base_url, - api_key: api_key.unwrap_or_default(), - model: info.model_name, - language, - server: Some(info.server), - }); - } - - if let Some(cloud) = provider.cloud_provider() { - let base_url = base_url.unwrap_or_else(|| cloud.default_api_base().to_string()); - let api_key = api_key - .or_else(|| std::env::var(cloud.env_key_name()).ok()) - .ok_or_else(|| { - CliError::required_argument_with_hint( - "STT API key", - format!("Pass --api-key or set {} env var", cloud.env_key_name()), - ) - })?; - return Ok(ResolvedSttConfig { - provider: provider.to_batch_provider(), - base_url, - api_key, - model: model.unwrap_or_default(), - language, - server: ServerGuard::default(), - }); - } - - let base_url = - base_url.ok_or_else(|| CliError::required_argument("--base-url (or CHAR_BASE_URL)"))?; - let api_key = - api_key.ok_or_else(|| CliError::required_argument("--api-key (or CHAR_API_KEY)"))?; - - Ok(ResolvedSttConfig { - provider: provider.to_batch_provider(), - base_url, - api_key, - model: model.unwrap_or_default(), - language, - server: ServerGuard::default(), - }) -} - -#[cfg(target_os = "macos")] -pub async fn resolve_and_spawn_whisper( - models_base: &Path, - model_name: Option<&str>, -) -> CliResult { - let (model, model_path) = resolve_whisper_model(models_base, model_name)?; - - let server = LocalSttServer::start_whisper(model_path) - .await - .map_err(|e| CliError::operation_failed("start local whisper server", e.to_string()))?; - - Ok(LocalServerInfo { - base_url: server.base_url().to_string(), - model_name: LocalModel::Whisper(model.clone()).cli_name().to_string(), - server, - }) -} - -#[cfg(all(target_os = "macos", any(target_arch = "arm", target_arch = "aarch64")))] -pub async fn resolve_and_spawn_cactus( - models_base: &Path, - model_name: Option<&str>, -) -> CliResult { - let (model, model_path) = resolve_cactus_model(models_base, model_name)?; - - let server = LocalSttServer::start_cactus(model_path) - .await - .map_err(|e| CliError::operation_failed("start local cactus server", e.to_string()))?; - - Ok(LocalServerInfo { - base_url: server.base_url().to_string(), - model_name: model.to_string(), - server, - }) -} - -fn whisper_enabled() -> bool { - cfg!(target_os = "macos") -} - -fn unsupported_whisper_error() -> CliError { - CliError::msg("whisper local models are only available on macOS") -} - -fn missing_whisper_model_error() -> CliError { - CliError::required_argument_with_hint( - "--model", - format!( - "Pass --model explicitly for --provider whispercpp. Valid models: {}", - whisper_model_names().join(", ") - ), - ) -} - -fn resolve_whisper_model( - models_base: &Path, - name: Option<&str>, -) -> CliResult<(WhisperModel, PathBuf)> { - if !whisper_enabled() { - return Err(unsupported_whisper_error()); - } - - let name = name.ok_or_else(missing_whisper_model_error)?; - let model = LocalModel::all() - .into_iter() - .find_map(|model| match model { - LocalModel::Whisper(whisper) if model.cli_name() == name => Some(whisper), - _ => None, - }) - .ok_or_else(|| not_found_whisper_model(models_base, name, false))?; - - let model_path = LocalModel::Whisper(model.clone()).install_path(models_base); - if !model_path.exists() { - return Err(CliError::not_found( - format!("whisper model file at '{}'", model_path.display()), - Some(format!( - "Download it first: char models download {}", - LocalModel::Whisper(model.clone()).cli_name() - )), - )); - } - - Ok((model, model_path)) -} - -fn not_found_whisper_model( - models_base: &Path, - name: &str, - include_downloaded_hint: bool, -) -> CliError { - if !whisper_enabled() { - return unsupported_whisper_error(); - } - - let names = whisper_model_names(); - let mut hint = String::new(); - if let Some(suggestion) = did_you_mean(name, &names) { - hint.push_str(&format!("Did you mean '{suggestion}'?\n\n")); - } - if include_downloaded_hint { - hint.push_str(&suggest_whisper_models(models_base)); - } else { - hint.push_str("Run `char models list` to see available models."); - } - - CliError::not_found(format!("whisper model '{name}'"), Some(hint)) -} - -fn whisper_model_names() -> Vec<&'static str> { - LocalModel::all() - .iter() - .filter_map(|model| match model { - LocalModel::Whisper(_) => Some(model.cli_name()), - _ => None, - }) - .collect() -} - -fn suggest_whisper_models(models_base: &Path) -> String { - if !whisper_enabled() { - return "Whisper local models are only available on macOS.".to_string(); - } - - let mut downloaded = Vec::new(); - let mut available = Vec::new(); - - for model in LocalModel::all() { - let LocalModel::Whisper(_) = &model else { - continue; - }; - - if model.install_path(models_base).exists() { - downloaded.push(model.cli_name()); - } else { - available.push(model.cli_name()); - } - } - - let mut hint = String::new(); - if !downloaded.is_empty() { - hint.push_str("Downloaded models:\n"); - for name in &downloaded { - hint.push_str(&format!(" {name}\n")); - } - } - if !available.is_empty() { - if !downloaded.is_empty() { - hint.push_str("Other models (not downloaded):\n"); - } else { - hint.push_str("No models downloaded. Available models:\n"); - } - for name in &available { - hint.push_str(&format!(" {name}\n")); - } - hint.push_str("Download with: char models download "); - } - if hint.is_empty() { - hint.push_str("No whisper models found. Run `char models list` to check."); - } - hint -} - -fn cactus_enabled() -> bool { - cfg!(target_os = "macos") && cfg!(any(target_arch = "arm", target_arch = "aarch64")) -} - -fn unsupported_cactus_error() -> CliError { - CliError::msg("cactus local models are only available on Apple Silicon Macs") -} - -fn canonical_cactus_name(name: &str) -> String { - if name.starts_with("cactus-") { - name.to_string() - } else { - format!("cactus-{name}") - } -} - -fn missing_cactus_model_error() -> CliError { - CliError::required_argument_with_hint( - "--model", - format!( - "Pass --model explicitly for --provider cactus. Valid models: {}", - cactus_model_names().join(", ") - ), - ) -} - -fn cactus_model_names() -> Vec<&'static str> { - LocalModel::all() - .iter() - .filter_map(|model| match model { - LocalModel::Cactus(_) => Some(model.cli_name()), - _ => None, - }) - .collect() -} - -fn resolve_cactus_model( - models_base: &Path, - name: Option<&str>, -) -> CliResult<(CactusSttModel, PathBuf)> { - if !cactus_enabled() { - return Err(unsupported_cactus_error()); - } - - let name = name.ok_or_else(missing_cactus_model_error)?; - let canonical = canonical_cactus_name(name); - let model = LocalModel::all() - .into_iter() - .find_map(|model| match model { - LocalModel::Cactus(cactus) - if model.cli_name() == name || model.cli_name() == canonical => - { - Some(cactus) - } - _ => None, - }) - .ok_or_else(|| not_found_cactus_model(models_base, name, false))?; - - let model_path = LocalModel::Cactus(model.clone()).install_path(models_base); - if !model_path.exists() { - return Err(CliError::not_found( - format!("cactus model files at '{}'", model_path.display()), - Some(format!( - "Download it first: char models download {}", - LocalModel::Cactus(model.clone()).cli_name() - )), - )); - } - - Ok((model, model_path)) -} - -fn not_found_cactus_model( - models_base: &Path, - name: &str, - include_downloaded_hint: bool, -) -> CliError { - if !cactus_enabled() { - return unsupported_cactus_error(); - } - - let names: Vec<&str> = LocalModel::all() - .iter() - .filter_map(|model| { - if matches!(model, LocalModel::Cactus(_)) { - Some(model.cli_name()) - } else { - None - } - }) - .collect(); - - let mut hint = String::new(); - if let Some(suggestion) = did_you_mean(name, &names) { - hint.push_str(&format!("Did you mean '{suggestion}'?\n\n")); - } - if include_downloaded_hint { - hint.push_str(&suggest_cactus_models(models_base)); - } else { - hint.push_str("Run `char models list` to see available models."); - } - - CliError::not_found(format!("cactus model '{name}'"), Some(hint)) -} - -fn suggest_cactus_models(models_base: &Path) -> String { - if !cactus_enabled() { - return "Cactus local models are only available on Apple Silicon Macs.".to_string(); - } - let mut downloaded = Vec::new(); - let mut available = Vec::new(); - - for model in LocalModel::all() { - let LocalModel::Cactus(_) = &model else { - continue; - }; - - if model.install_path(models_base).exists() { - downloaded.push(model.cli_name()); - } else { - available.push(model.cli_name()); - } - } - - let mut hint = String::new(); - if !downloaded.is_empty() { - hint.push_str("Downloaded models:\n"); - for name in &downloaded { - hint.push_str(&format!(" {name}\n")); - } - } - if !available.is_empty() { - if !downloaded.is_empty() { - hint.push_str("Other models (not downloaded):\n"); - } else { - hint.push_str("No models downloaded. Available models:\n"); - } - for name in &available { - hint.push_str(&format!(" {name}\n")); - } - hint.push_str("Download with: char models download "); - } - if hint.is_empty() { - hint.push_str("No cactus models found. Run `char models list` to check."); - } - hint -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn missing_whisper_model_error_mentions_model_flag() { - let error = missing_whisper_model_error(); - let rendered = error.to_string(); - - assert!(rendered.contains("--model")); - assert!(rendered.contains("whisper-small")); - } - - #[test] - fn whisper_not_found_suggests_close_match() { - let error = not_found_whisper_model(Path::new("/tmp"), "whisper-smal", false); - let rendered = error.to_string(); - - assert!(rendered.contains("Did you mean 'whisper-small'?")); - } -} diff --git a/apps/cli/src/stt/mod.rs b/apps/cli/src/stt/mod.rs deleted file mode 100644 index 248aa06af5..0000000000 --- a/apps/cli/src/stt/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -mod config; -mod provider; - -#[cfg(all(target_os = "macos", any(target_arch = "arm", target_arch = "aarch64")))] -#[allow(unused_imports)] -pub use config::resolve_and_spawn_cactus; -pub use config::{ - ChannelBatchRuntime, ResolvedSttConfig, ServerGuard, SttOverrides, resolve_config, -}; -pub use provider::SttProvider; diff --git a/apps/cli/src/stt/provider.rs b/apps/cli/src/stt/provider.rs deleted file mode 100644 index fa51c1cbfd..0000000000 --- a/apps/cli/src/stt/provider.rs +++ /dev/null @@ -1,160 +0,0 @@ -use clap::ValueEnum; - -use hypr_listener2_core::BatchProvider; - -#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)] -pub enum SttProvider { - Deepgram, - Soniox, - Assemblyai, - Fireworks, - Openai, - Gladia, - Elevenlabs, - Mistral, - Pyannote, - #[cfg(target_os = "macos")] - Whispercpp, - #[cfg(all(target_os = "macos", any(target_arch = "arm", target_arch = "aarch64")))] - Cactus, -} - -impl SttProvider { - fn meta(self) -> ProviderMeta { - match self { - Self::Deepgram => ProviderMeta::cloud( - "deepgram", - owhisper_client::Provider::Deepgram, - BatchProvider::Deepgram, - ), - Self::Soniox => ProviderMeta::cloud( - "soniox", - owhisper_client::Provider::Soniox, - BatchProvider::Soniox, - ), - Self::Assemblyai => ProviderMeta::cloud( - "assemblyai", - owhisper_client::Provider::AssemblyAI, - BatchProvider::AssemblyAI, - ), - Self::Fireworks => ProviderMeta::cloud( - "fireworks", - owhisper_client::Provider::Fireworks, - BatchProvider::Fireworks, - ), - Self::Openai => ProviderMeta::cloud( - "openai", - owhisper_client::Provider::OpenAI, - BatchProvider::OpenAI, - ), - Self::Gladia => ProviderMeta::cloud( - "gladia", - owhisper_client::Provider::Gladia, - BatchProvider::Gladia, - ), - Self::Elevenlabs => ProviderMeta::cloud( - "elevenlabs", - owhisper_client::Provider::ElevenLabs, - BatchProvider::ElevenLabs, - ), - Self::Mistral => ProviderMeta::cloud( - "mistral", - owhisper_client::Provider::Mistral, - BatchProvider::Mistral, - ), - Self::Pyannote => ProviderMeta::cloud( - "pyannote", - owhisper_client::Provider::Pyannote, - BatchProvider::Pyannote, - ), - #[cfg(target_os = "macos")] - Self::Whispercpp => ProviderMeta::local("whispercpp", BatchProvider::WhisperLocal), - #[cfg(all(target_os = "macos", any(target_arch = "arm", target_arch = "aarch64")))] - Self::Cactus => ProviderMeta::local("cactus", BatchProvider::Cactus), - } - } - - pub fn id(self) -> &'static str { - self.meta().id - } - - pub fn from_id(id: &str) -> Option { - providers() - .iter() - .find(|provider| provider.id() == id) - .copied() - } - - pub fn is_local(&self) -> bool { - self.meta().is_local - } - - pub(crate) fn cloud_provider(&self) -> Option { - self.meta().cloud_provider - } - - pub(crate) fn to_batch_provider(self) -> BatchProvider { - self.meta().batch_provider - } -} - -struct ProviderMeta { - id: &'static str, - cloud_provider: Option, - batch_provider: BatchProvider, - is_local: bool, -} - -impl ProviderMeta { - fn cloud( - id: &'static str, - cloud_provider: owhisper_client::Provider, - batch_provider: BatchProvider, - ) -> Self { - Self { - id, - cloud_provider: Some(cloud_provider), - batch_provider, - is_local: false, - } - } - - fn local(id: &'static str, batch_provider: BatchProvider) -> Self { - Self { - id, - cloud_provider: None, - batch_provider, - is_local: true, - } - } -} - -fn providers() -> &'static [SttProvider] { - &[ - SttProvider::Deepgram, - SttProvider::Soniox, - SttProvider::Assemblyai, - SttProvider::Fireworks, - SttProvider::Openai, - SttProvider::Gladia, - SttProvider::Elevenlabs, - SttProvider::Mistral, - SttProvider::Pyannote, - #[cfg(target_os = "macos")] - SttProvider::Whispercpp, - #[cfg(all(target_os = "macos", any(target_arch = "arm", target_arch = "aarch64")))] - SttProvider::Cactus, - ] -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn provider_ids_round_trip() { - for provider in providers() { - assert_eq!(SttProvider::from_id(provider.id()), Some(*provider)); - } - } -} diff --git a/apps/cli/src/tui/capture.rs b/apps/cli/src/tui/capture.rs deleted file mode 100644 index 8981ffa4cd..0000000000 --- a/apps/cli/src/tui/capture.rs +++ /dev/null @@ -1,73 +0,0 @@ -use std::collections::VecDeque; -use std::fmt::Write; -use std::sync::{Arc, Mutex}; - -use tracing_subscriber::Layer; - -const TRACE_CAPACITY: usize = 64; - -pub type TraceBuffer = Arc>>; - -pub fn new_trace_buffer() -> TraceBuffer { - Arc::new(Mutex::new(VecDeque::with_capacity(TRACE_CAPACITY))) -} - -pub struct CaptureLayer { - buffer: TraceBuffer, -} - -impl CaptureLayer { - pub fn new(buffer: TraceBuffer) -> Self { - Self { buffer } - } -} - -impl Layer for CaptureLayer { - fn on_event( - &self, - event: &tracing::Event<'_>, - _ctx: tracing_subscriber::layer::Context<'_, S>, - ) { - let meta = event.metadata(); - let level = meta.level(); - let target = meta.target(); - - let mut message = String::new(); - let mut visitor = MessageVisitor(&mut message); - event.record(&mut visitor); - - let line = format!("{level:>5} {target}: {message}"); - if let Ok(mut buf) = self.buffer.lock() { - if buf.len() >= TRACE_CAPACITY { - buf.pop_front(); - } - buf.push_back(line); - } - } -} - -struct MessageVisitor<'a>(&'a mut String); - -impl tracing::field::Visit for MessageVisitor<'_> { - fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) { - if field.name() == "message" { - let _ = write!(self.0, "{:?}", value); - } else { - if !self.0.is_empty() { - self.0.push(' '); - } - let _ = write!(self.0, "{}={:?}", field.name(), value); - } - } - - fn record_str(&mut self, field: &tracing::field::Field, value: &str) { - if field.name() == "message" { - let _ = write!(self.0, "{}", value); - } else { - if !self.0.is_empty() { - self.0.push(' '); - } - let _ = write!(self.0, "{}={}", field.name(), value); - } - } -} diff --git a/apps/cli/src/tui/input.rs b/apps/cli/src/tui/input.rs deleted file mode 100644 index 6db22e18d1..0000000000 --- a/apps/cli/src/tui/input.rs +++ /dev/null @@ -1,171 +0,0 @@ -use std::sync::Arc; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::mpsc; -use std::thread::JoinHandle; -use std::time::Duration; - -use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers}; - -const INPUT_POLL_INTERVAL: Duration = Duration::from_millis(50); - -#[derive(Clone, Copy, PartialEq, Eq)] -pub(crate) enum View { - Progress, - Traces, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) enum InputAction { - ToggleView, - Interrupt, - SeekForward, - SeekBackward, - TogglePause, - Up, - Down, - Confirm, -} - -pub(crate) struct BackgroundInput { - rx: mpsc::Receiver, - running: Arc, - handle: Option>, -} - -impl BackgroundInput { - pub fn spawn() -> Self { - let (tx, rx) = mpsc::channel(); - let running = Arc::new(AtomicBool::new(true)); - let thread_running = Arc::clone(&running); - let handle = std::thread::spawn(move || { - while thread_running.load(Ordering::Relaxed) { - if !event::poll(INPUT_POLL_INTERVAL).unwrap_or(false) { - continue; - } - - let Ok(Event::Key(key)) = event::read() else { - continue; - }; - - match action_for_key(key) { - Some(InputAction::Interrupt) => { - #[cfg(unix)] - unsafe { - libc::kill(libc::getpid(), libc::SIGINT); - } - #[cfg(not(unix))] - std::process::exit(130); - } - Some(action) => { - let _ = tx.send(action); - } - None => {} - } - } - }); - - Self { - rx, - running, - handle: Some(handle), - } - } - - pub fn try_recv(&self) -> Result { - self.rx.try_recv() - } - - pub fn shutdown(&mut self) { - self.running.store(false, Ordering::Relaxed); - if let Some(handle) = self.handle.take() { - let _ = handle.join(); - } - } -} - -fn action_for_key(key: KeyEvent) -> Option { - match key { - KeyEvent { - code: KeyCode::Char('d'), - modifiers: KeyModifiers::NONE, - .. - } => Some(InputAction::ToggleView), - KeyEvent { - code: KeyCode::Char('c'), - modifiers: KeyModifiers::CONTROL, - .. - } => Some(InputAction::Interrupt), - KeyEvent { - code: KeyCode::Right, - modifiers: KeyModifiers::NONE, - .. - } => Some(InputAction::SeekForward), - KeyEvent { - code: KeyCode::Left, - modifiers: KeyModifiers::NONE, - .. - } => Some(InputAction::SeekBackward), - KeyEvent { - code: KeyCode::Char(' '), - modifiers: KeyModifiers::NONE, - .. - } => Some(InputAction::TogglePause), - KeyEvent { - code: KeyCode::Up, - modifiers: KeyModifiers::NONE, - .. - } - | KeyEvent { - code: KeyCode::Char('k'), - modifiers: KeyModifiers::NONE, - .. - } => Some(InputAction::Up), - KeyEvent { - code: KeyCode::Down, - modifiers: KeyModifiers::NONE, - .. - } - | KeyEvent { - code: KeyCode::Char('j'), - modifiers: KeyModifiers::NONE, - .. - } => Some(InputAction::Down), - KeyEvent { - code: KeyCode::Enter, - modifiers: KeyModifiers::NONE, - .. - } => Some(InputAction::Confirm), - _ => None, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn ctrl_c_maps_to_interrupt_action() { - let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL); - assert_eq!(action_for_key(key), Some(InputAction::Interrupt)); - } - - #[test] - fn plain_d_maps_to_toggle_action() { - let key = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE); - assert_eq!(action_for_key(key), Some(InputAction::ToggleView)); - } - - #[test] - fn arrow_keys_map_to_seek() { - let right = KeyEvent::new(KeyCode::Right, KeyModifiers::NONE); - assert_eq!(action_for_key(right), Some(InputAction::SeekForward)); - let left = KeyEvent::new(KeyCode::Left, KeyModifiers::NONE); - assert_eq!(action_for_key(left), Some(InputAction::SeekBackward)); - } - - #[test] - fn space_maps_to_toggle_pause() { - let key = KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE); - assert_eq!(action_for_key(key), Some(InputAction::TogglePause)); - } -} diff --git a/apps/cli/src/tui/logo.rs b/apps/cli/src/tui/logo.rs deleted file mode 100644 index 3844396a12..0000000000 --- a/apps/cli/src/tui/logo.rs +++ /dev/null @@ -1,12 +0,0 @@ -use ratatui::style::{Color, Style}; -use ratatui::text::{Line, Span}; - -pub const LOGO_COLOR: Color = Color::Rgb(0xFD, 0xE6, 0xAE); - -const LOGO: [&str; 3] = [" █▌ ▐█ ", "▐▌ ▐▌", " █▌ ▐█ "]; - -pub fn logo_lines<'a>() -> Vec> { - LOGO.iter() - .map(|line| Line::from(Span::styled(*line, Style::default().fg(LOGO_COLOR)))) - .collect() -} diff --git a/apps/cli/src/tui/mod.rs b/apps/cli/src/tui/mod.rs deleted file mode 100644 index 08bd2127c7..0000000000 --- a/apps/cli/src/tui/mod.rs +++ /dev/null @@ -1,23 +0,0 @@ -mod capture; -mod input; -mod logo; -mod viewport; -pub mod waveform; - -pub use capture::{CaptureLayer, TraceBuffer, new_trace_buffer}; -pub(crate) use input::InputAction; -pub use viewport::InlineViewport; - -pub const SPINNER: &[char] = &['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; - -pub fn truncate_line(s: &str, max: usize) -> &str { - let char_count = s.chars().count(); - if char_count <= max { - return s; - } - let skip = char_count - max; - match s.char_indices().nth(skip) { - Some((byte_idx, _)) => &s[byte_idx..], - None => s, - } -} diff --git a/apps/cli/src/tui/viewport.rs b/apps/cli/src/tui/viewport.rs deleted file mode 100644 index d172e58812..0000000000 --- a/apps/cli/src/tui/viewport.rs +++ /dev/null @@ -1,313 +0,0 @@ -use std::io::{self, Stderr}; - -use ratatui::backend::CrosstermBackend; -use ratatui::layout::{Constraint, Flex, Layout}; -use ratatui::style::{Color, Style}; -use ratatui::text::{Line, Span}; -use ratatui::widgets::{Block, BorderType, Paragraph}; -use ratatui::{Terminal, TerminalOptions, Viewport}; - -use super::capture::TraceBuffer; -use super::input::{BackgroundInput, InputAction, View}; -use super::logo::logo_lines; - -const MAX_BOX_WIDTH: u16 = 60; - -pub struct InlineViewport { - terminal: Terminal>, - traces: Option, - view: View, - raw_mode: bool, - input: Option, -} - -impl InlineViewport { - pub fn stderr(height: u16, traces: Option) -> io::Result { - Self::stderr_interactive(height, traces, false) - } - - pub fn stderr_interactive( - height: u16, - traces: Option, - interactive: bool, - ) -> io::Result { - let raw_mode = interactive || traces.is_some(); - let mut input = None; - if raw_mode { - crossterm::terminal::enable_raw_mode()?; - input = Some(BackgroundInput::spawn()); - } - let backend = CrosstermBackend::new(io::stderr()); - let terminal = match Terminal::with_options( - backend, - TerminalOptions { - viewport: Viewport::Inline(height), - }, - ) { - Ok(terminal) => terminal, - Err(error) => { - if let Some(mut input) = input { - input.shutdown(); - } - if raw_mode { - let _ = crossterm::terminal::disable_raw_mode(); - } - return Err(error); - } - }; - Ok(Self { - terminal, - traces, - view: View::Progress, - raw_mode, - input, - }) - } - - /// Poll keyboard input. Handles view-toggle internally and returns - /// any actions the caller should handle (seek, pause, etc.). - pub fn poll_input(&mut self) -> Vec { - if !self.raw_mode { - return Vec::new(); - } - let Some(input) = self.input.as_ref() else { - return Vec::new(); - }; - let mut passthrough = Vec::new(); - while let Ok(action) = input.try_recv() { - if action == InputAction::ToggleView { - self.view = match self.view { - View::Progress => View::Traces, - View::Traces => View::Progress, - }; - } else { - passthrough.push(action); - } - } - passthrough - } - - fn shutdown_input(&mut self) { - if let Some(mut input) = self.input.take() { - input.shutdown(); - } - } - - fn teardown_raw_mode(&mut self) -> io::Result<()> { - if !self.raw_mode { - return Ok(()); - } - self.shutdown_input(); - crossterm::terminal::disable_raw_mode()?; - self.raw_mode = false; - Ok(()) - } - - pub fn draw(&mut self, lines: &[Line]) { - match self.view { - View::Progress => self.draw_lines(lines), - View::Traces => self.draw_traces(), - } - } - - pub fn draw_strings(&mut self, lines: &[String]) { - let styled: Vec = lines.iter().map(|s| Line::from(s.clone())).collect(); - self.draw(&styled); - } - - fn draw_lines(&mut self, lines: &[Line]) { - let logo = logo_lines(); - let logo_width = 10; - - let row_count = lines.len().max(logo.len()); - let mut content: Vec = Vec::with_capacity(row_count + 1); - for i in 0..row_count { - let mut spans: Vec = Vec::new(); - if i < logo.len() { - spans.extend(logo[i].spans.iter().cloned()); - spans.push(Span::raw(" ")); - } else { - spans.push(Span::raw(" ".repeat(logo_width + 2))); - } - if i < lines.len() { - spans.extend(lines[i].spans.iter().cloned()); - } - content.push(Line::from(spans)); - } - - if self.traces.is_some() { - content.push(Line::from(vec![ - Span::raw(" ".repeat(logo_width + 2)), - Span::styled( - "press 'd' to toggle traces", - Style::default().fg(Color::DarkGray), - ), - ])); - } - - let _ = self.terminal.draw(|frame| { - let full = frame.area(); - let capped_width = full.width.min(MAX_BOX_WIDTH); - let area = Layout::horizontal([Constraint::Length(capped_width)]) - .flex(Flex::Start) - .split(full)[0]; - let block = Block::bordered() - .border_type(BorderType::Rounded) - .border_style(Style::default().fg(Color::DarkGray)); - let inner = block.inner(area); - frame.render_widget(block, area); - - let chunks = - Layout::vertical(vec![Constraint::Length(1); inner.height as usize]).split(inner); - for (i, line) in content.iter().enumerate() { - if i < chunks.len() { - frame.render_widget(Paragraph::new(line.clone()), chunks[i]); - } - } - }); - } - - fn draw_traces(&mut self) { - let traces = match self.traces { - Some(ref buf) => buf, - None => return, - }; - - let _ = self.terminal.draw(|frame| { - let full = frame.area(); - let capped_width = full.width.min(MAX_BOX_WIDTH); - let area = Layout::horizontal([Constraint::Length(capped_width)]) - .flex(Flex::Start) - .split(full)[0]; - let block = Block::bordered() - .border_type(BorderType::Rounded) - .border_style(Style::default().fg(Color::DarkGray)); - let inner = block.inner(area); - frame.render_widget(block, area); - - let inner_height = inner.height as usize; - let trace_lines: Vec = if let Ok(buf) = traces.lock() { - buf.iter() - .rev() - .take(inner_height.saturating_sub(1)) - .cloned() - .collect::>() - .into_iter() - .rev() - .collect() - } else { - vec![] - }; - - let chunks = Layout::vertical(vec![Constraint::Length(1); inner_height]).split(inner); - - let header = Paragraph::new(Line::from("[traces] press 'd' to toggle")) - .style(Style::default().fg(Color::DarkGray)); - if !chunks.is_empty() { - frame.render_widget(header, chunks[0]); - } - - for (i, line) in trace_lines.iter().enumerate() { - if i + 1 < chunks.len() { - let p = Paragraph::new(Line::from(line.as_str())) - .style(Style::default().fg(Color::DarkGray)); - frame.render_widget(p, chunks[i + 1]); - } - } - }); - } - - /// Tear down raw mode but leave the last-drawn content visible. - /// The caller must have called `draw()` with the final content - /// immediately before calling this. - pub fn finish(&mut self) -> io::Result<()> { - self.teardown_raw_mode()?; - // After an inline draw, ratatui leaves the cursor on the last - // viewport row (the bottom border). We need to move past it - // so the shell prompt doesn't overwrite the border. - let mut stderr = io::stderr(); - crossterm::execute!(stderr, crossterm::cursor::MoveDown(1))?; - eprintln!(); - Ok(()) - } - - pub fn clear(&mut self) -> io::Result<()> { - use crossterm::{cursor, execute, terminal}; - - let area = self.terminal.get_frame().area(); - let height = area.height; - - self.terminal.draw(|frame| { - let area = frame.area(); - frame.render_widget(ratatui::widgets::Clear, area); - frame.render_widget(Paragraph::new(""), area); - })?; - - let mut stderr = io::stderr(); - execute!( - stderr, - cursor::MoveUp(height), - terminal::Clear(terminal::ClearType::FromCursorDown) - )?; - - self.teardown_raw_mode()?; - Ok(()) - } -} - -impl Drop for InlineViewport { - fn drop(&mut self) { - let _ = self.teardown_raw_mode(); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use ratatui::backend::TestBackend; - - fn draw_card(terminal: &mut Terminal, lines: &[String]) { - let content: Vec = lines.iter().map(|s| Line::from(s.as_str())).collect(); - terminal - .draw(|frame| { - let area = frame.area(); - let block = Block::bordered().border_type(BorderType::Rounded); - let inner = block.inner(area); - frame.render_widget(block, area); - - let chunks = Layout::vertical(vec![Constraint::Length(1); inner.height as usize]) - .split(inner); - for (i, line) in content.iter().enumerate() { - if i < chunks.len() { - frame.render_widget(Paragraph::new(line.clone()), chunks[i]); - } - } - }) - .unwrap(); - } - - #[test] - fn render_draws_card_with_three_lines() { - let backend = TestBackend::new(40, 5); - let mut terminal = Terminal::with_options( - backend, - TerminalOptions { - viewport: Viewport::Inline(5), - }, - ) - .unwrap(); - - let lines = [ - "recording mic 00:05".to_string(), - "16000 Hz 1 ch 5.0s audio".to_string(), - "out.wav lvl ||||....".to_string(), - ]; - draw_card(&mut terminal, &lines); - - let buf = terminal.backend().buffer().clone(); - let content_line: String = (0..buf.area.width) - .map(|x| buf[(x, 1)].symbol().chars().next().unwrap_or(' ')) - .collect(); - assert!(content_line.contains("recording mic")); - } -} diff --git a/apps/cli/src/tui/waveform.rs b/apps/cli/src/tui/waveform.rs deleted file mode 100644 index 5334b00958..0000000000 --- a/apps/cli/src/tui/waveform.rs +++ /dev/null @@ -1,407 +0,0 @@ -use std::collections::VecDeque; - -use ratatui::buffer::Buffer; -use ratatui::layout::Rect; -use ratatui::style::{Color, Style}; -use ratatui::text::{Line, Span}; -use ratatui::widgets::{StatefulWidget, Widget}; - -const BLOCKS: [char; 9] = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█']; -pub const MIC_COLOR: Color = Color::Rgb(0xFD, 0xE6, 0xAE); -pub const SYS_COLOR: Color = Color::Rgb(0xE0, 0xC0, 0x80); - -fn to_perceptual(level: f32) -> f32 { - if level <= 0.0 { - 0.0 - } else { - let db = 20.0 * level.log10(); - ((db + 48.0) / 48.0).clamp(0.0, 1.0) - } -} - -fn block_char(perceptual: f32) -> char { - let idx = (perceptual * 8.0).round() as usize; - BLOCKS[idx.min(8)] -} - -#[derive(Clone, Copy)] -pub enum WaveformMode { - Mono, - Dual, -} - -pub struct LiveWaveformState { - left: VecDeque, - right: VecDeque, - width: usize, -} - -impl LiveWaveformState { - pub fn new(width: usize) -> Self { - Self { - left: VecDeque::with_capacity(width + 1), - right: VecDeque::with_capacity(width + 1), - width, - } - } - - pub fn push(&mut self, left: f32, right: f32) { - push_level(&mut self.left, left, self.width); - push_level(&mut self.right, right, self.width); - } -} - -pub struct LiveWaveform { - pub mode: WaveformMode, -} - -impl StatefulWidget for LiveWaveform { - type State = LiveWaveformState; - - fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { - let width = area.width as usize; - let spans = match self.mode { - WaveformMode::Mono => mono_spans(&state.left, MIC_COLOR, width), - WaveformMode::Dual => overlaid_spans(&state.left, &state.right, width), - }; - let line = Line::from(spans); - buf.set_line(area.x, area.y, &line, area.width); - } -} - -impl LiveWaveform { - pub fn spans( - state: &LiveWaveformState, - mode: WaveformMode, - width: usize, - ) -> Vec> { - match mode { - WaveformMode::Mono => mono_spans(&state.left, MIC_COLOR, width), - WaveformMode::Dual => overlaid_spans(&state.left, &state.right, width), - } - } -} - -pub struct PlaybackWaveform<'a> { - pub peaks: &'a [f32], - pub fraction: f64, - pub color: Color, -} - -impl Widget for PlaybackWaveform<'_> { - fn render(self, area: Rect, buf: &mut Buffer) { - let width = area.width as usize; - let spans = playback_spans(self.peaks, self.fraction, self.color, width); - let line = Line::from(spans); - buf.set_line(area.x, area.y, &line, area.width); - } -} - -impl<'a> PlaybackWaveform<'a> { - pub fn lines( - peaks: &[f32], - fraction: f64, - color: Color, - width: usize, - rows: usize, - ) -> Vec> { - playback_multirow(peaks, fraction, color, width, rows) - } - - pub fn lines_dual( - left_peaks: &[f32], - right_peaks: &[f32], - fraction: f64, - left_color: Color, - right_color: Color, - width: usize, - rows: usize, - ) -> Vec> { - playback_multirow_dual( - left_peaks, - right_peaks, - fraction, - left_color, - right_color, - width, - rows, - ) - } -} - -/// Compute peak amplitudes for `width` segments from f32 samples (range -1.0..1.0). -pub fn compute_peaks(samples: &[f32], width: usize) -> Vec { - if samples.is_empty() || width == 0 { - return vec![0.0; width]; - } - let chunk_size = (samples.len() + width - 1) / width; - let mut peaks = Vec::with_capacity(width); - for chunk in samples.chunks(chunk_size) { - let peak = chunk - .iter() - .map(|s| s.abs()) - .fold(0.0_f32, f32::max) - .clamp(0.0, 1.0); - peaks.push(peak); - } - peaks.truncate(width); - while peaks.len() < width { - peaks.push(0.0); - } - peaks -} - -// --------------------------------------------------------------------------- -// Internal helpers -// --------------------------------------------------------------------------- - -fn push_level(history: &mut VecDeque, level: f32, width: usize) { - if history.len() >= width { - history.pop_front(); - } - history.push_back(level); -} - -fn mono_spans(history: &VecDeque, color: Color, width: usize) -> Vec> { - let mut spans = Vec::with_capacity(width); - let start = width.saturating_sub(history.len()); - for i in 0..width { - if i < start { - spans.push(Span::raw(" ")); - } else { - let level = to_perceptual(history[i - start]); - let ch = block_char(level); - spans.push(Span::styled(String::from(ch), Style::default().fg(color))); - } - } - spans -} - -fn overlaid_spans(mic: &VecDeque, sys: &VecDeque, width: usize) -> Vec> { - let mut spans = Vec::with_capacity(width + 4); - spans.push(Span::styled("mic", Style::default().fg(MIC_COLOR))); - spans.push(Span::raw("/")); - spans.push(Span::styled("sys", Style::default().fg(SYS_COLOR))); - spans.push(Span::raw(" ")); - - let mic_start = width.saturating_sub(mic.len()); - let sys_start = width.saturating_sub(sys.len()); - - for i in 0..width { - let m = if i >= mic_start { - to_perceptual(mic[i - mic_start]) - } else { - 0.0 - }; - let s = if i >= sys_start { - to_perceptual(sys[i - sys_start]) - } else { - 0.0 - }; - - let level = m.max(s); - if level <= 0.0 { - spans.push(Span::raw(" ")); - } else { - let color = if m >= s { MIC_COLOR } else { SYS_COLOR }; - spans.push(Span::styled( - String::from(block_char(level)), - Style::default().fg(color), - )); - } - } - spans -} - -const MIN_BAR: f32 = 0.125; - -fn normalize_peaks(peaks: &[f32], width: usize) -> (Vec, usize) { - let resampled = resample_peaks(peaks, width); - let max_peak = resampled.iter().cloned().fold(0.0_f32, f32::max); - let norm = if max_peak > 0.0 { 1.0 / max_peak } else { 1.0 }; - let normalized: Vec = resampled - .iter() - .map(|&p| { - let n = (p * norm).clamp(0.0, 1.0); - if n > 0.0 { n.max(MIN_BAR) } else { n } - }) - .collect(); - (normalized, width) -} - -fn playback_spans(peaks: &[f32], fraction: f64, color: Color, width: usize) -> Vec> { - let (normalized, width) = normalize_peaks(peaks, width); - let played_cols = (fraction * width as f64).round() as usize; - let mut spans = Vec::with_capacity(width); - for (i, &n) in normalized.iter().enumerate() { - let ch = block_char(n); - let fg = if i < played_cols { - color - } else { - Color::DarkGray - }; - spans.push(Span::styled(String::from(ch), Style::default().fg(fg))); - } - spans -} - -fn playback_multirow( - peaks: &[f32], - fraction: f64, - color: Color, - width: usize, - rows: usize, -) -> Vec> { - let (normalized, width) = normalize_peaks(peaks, width); - let played_cols = (fraction * width as f64).round() as usize; - let total_levels = rows as f32 * 8.0; - - let mut result = Vec::with_capacity(rows); - for row in 0..rows { - let row_base = (rows - 1 - row) as f32 * 8.0; - let mut spans = Vec::with_capacity(width); - for (i, &n) in normalized.iter().enumerate() { - let filled = (n * total_levels - row_base).clamp(0.0, 8.0); - let ch = block_char(filled / 8.0); - let fg = if i < played_cols { - color - } else { - Color::DarkGray - }; - spans.push(Span::styled(String::from(ch), Style::default().fg(fg))); - } - result.push(Line::from(spans)); - } - result -} - -fn playback_multirow_dual( - left_peaks: &[f32], - right_peaks: &[f32], - fraction: f64, - left_color: Color, - right_color: Color, - width: usize, - rows: usize, -) -> Vec> { - let (left_norm, width) = normalize_peaks(left_peaks, width); - let (right_norm, _) = normalize_peaks(right_peaks, width); - let played_cols = (fraction * width as f64).round() as usize; - let total_levels = rows as f32 * 8.0; - - let mut result = Vec::with_capacity(rows); - for row in 0..rows { - let row_base = (rows - 1 - row) as f32 * 8.0; - let mut spans = Vec::with_capacity(width); - for i in 0..width { - let l = *left_norm.get(i).unwrap_or(&0.0); - let r = *right_norm.get(i).unwrap_or(&0.0); - let level = l.max(r); - let filled = (level * total_levels - row_base).clamp(0.0, 8.0); - let ch = block_char(filled / 8.0); - let fg = if i < played_cols { - if l >= r { left_color } else { right_color } - } else { - Color::DarkGray - }; - spans.push(Span::styled(String::from(ch), Style::default().fg(fg))); - } - result.push(Line::from(spans)); - } - result -} - -/// Deinterleave stereo samples into separate left and right channel vectors. -pub fn deinterleave_stereo(samples: &[f32]) -> (Vec, Vec) { - let len = samples.len() / 2; - let mut left = Vec::with_capacity(len); - let mut right = Vec::with_capacity(len); - for pair in samples.chunks_exact(2) { - left.push(pair[0]); - right.push(pair[1]); - } - (left, right) -} - -fn resample_peaks(peaks: &[f32], width: usize) -> Vec { - if peaks.is_empty() || width == 0 { - return vec![0.0; width]; - } - if peaks.len() == width { - return peaks.to_vec(); - } - let mut out = Vec::with_capacity(width); - for i in 0..width { - let pos = i as f64 * (peaks.len() as f64 - 1.0) / (width as f64 - 1.0).max(1.0); - let lo = pos.floor() as usize; - let hi = (lo + 1).min(peaks.len() - 1); - let t = pos - lo as f64; - out.push(peaks[lo] * (1.0 - t as f32) + peaks[hi] * t as f32); - } - out -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn compute_peaks_basic() { - let samples: Vec = vec![0.03, -0.06, 0.09, -0.12, 0.15, -0.18, 0.21, -0.24]; - let peaks = compute_peaks(&samples, 4); - assert_eq!(peaks.len(), 4); - assert!(peaks[3] > peaks[0]); - } - - #[test] - fn compute_peaks_empty() { - assert_eq!(compute_peaks(&[], 4), vec![0.0; 4]); - } - - #[test] - fn live_waveform_state_push() { - let mut state = LiveWaveformState::new(4); - for i in 0..6 { - state.push(i as f32 * 0.1, 0.0); - } - assert_eq!(state.left.len(), 4); - assert_eq!(state.right.len(), 4); - } - - #[test] - fn playback_spans_split_color() { - let peaks = vec![0.5; 10]; - let spans = playback_spans(&peaks, 0.5, MIC_COLOR, 10); - assert_eq!(spans.len(), 10); - assert_eq!(spans[0].style.fg, Some(MIC_COLOR)); - assert_eq!(spans[9].style.fg, Some(Color::DarkGray)); - } - - #[test] - fn overlaid_mic_dominant() { - let mut mic = VecDeque::new(); - let mut sys = VecDeque::new(); - mic.push_back(0.5); - sys.push_back(0.01); - let spans = overlaid_spans(&mic, &sys, 1); - let block_span = spans - .iter() - .find(|s| s.content.chars().any(|c| BLOCKS[1..].contains(&c))); - assert!(block_span.is_some()); - assert_eq!(block_span.unwrap().style.fg, Some(MIC_COLOR)); - } - - #[test] - fn overlaid_sys_dominant() { - let mut mic = VecDeque::new(); - let mut sys = VecDeque::new(); - mic.push_back(0.01); - sys.push_back(0.5); - let spans = overlaid_spans(&mic, &sys, 1); - let block_span = spans - .iter() - .find(|s| s.content.chars().any(|c| BLOCKS[1..].contains(&c))); - assert!(block_span.is_some()); - assert_eq!(block_span.unwrap().style.fg, Some(SYS_COLOR)); - } -} diff --git a/apps/cli/tests/pty.rs b/apps/cli/tests/pty.rs deleted file mode 100644 index 60e29430f0..0000000000 --- a/apps/cli/tests/pty.rs +++ /dev/null @@ -1,167 +0,0 @@ -use std::io::{Read, Write}; -use std::path::PathBuf; -use std::sync::{Arc, Mutex}; -use std::time::{Duration, Instant}; - -use portable_pty::{CommandBuilder, NativePtySystem, PtySize, PtySystem}; -use vt100::Parser; - -const PTY_ROWS: u16 = 24; -const PTY_COLS: u16 = 80; -const POLL_INTERVAL: Duration = Duration::from_millis(30); - -struct PtyApp { - child: Box, - writer: Box, - output: Arc>>, - screen: Arc>, - reader_handle: Option>, - _temp_dir: PathBuf, -} - -impl PtyApp { - #[allow(deprecated)] - fn spawn(args: &[&str]) -> Self { - let temp_dir = tempfile::tempdir().expect("failed to create temp dir"); - let temp_path = temp_dir.into_path(); - - let pty_system = NativePtySystem::default(); - let pair = pty_system - .openpty(PtySize { - rows: PTY_ROWS, - cols: PTY_COLS, - pixel_width: 0, - pixel_height: 0, - }) - .expect("failed to open pty"); - - let mut cmd = CommandBuilder::new(env!("CARGO_BIN_EXE_char")); - for arg in args { - cmd.arg(arg); - } - - cmd.env("TERM", "xterm-256color"); - cmd.env("NO_COLOR", "1"); - cmd.env("XDG_CONFIG_HOME", temp_path.join("config")); - cmd.env("XDG_DATA_HOME", temp_path.join("data")); - cmd.env("XDG_CACHE_HOME", temp_path.join("cache")); - cmd.env("HYPR_ANALYTICS_DISABLED", "1"); - cmd.env("HYPR_ENV", "test"); - - let child = pair.slave.spawn_command(cmd).expect("failed to spawn"); - drop(pair.slave); - - let writer = pair.master.take_writer().expect("failed to take writer"); - let mut reader = pair - .master - .try_clone_reader() - .expect("failed to clone reader"); - - let output: Arc>> = Arc::new(Mutex::new(Vec::new())); - let output_clone = Arc::clone(&output); - - let screen = Arc::new(Mutex::new(Parser::new(PTY_ROWS, PTY_COLS, 0))); - let screen_clone = Arc::clone(&screen); - - let reader_handle = std::thread::spawn(move || { - let mut buf = [0u8; 4096]; - loop { - match reader.read(&mut buf) { - Ok(0) => break, - Ok(n) => { - { - let mut out = output_clone.lock().unwrap(); - out.extend_from_slice(&buf[..n]); - } - { - let mut scr = screen_clone.lock().unwrap(); - scr.process(&buf[..n]); - } - } - Err(_) => break, - } - } - }); - - PtyApp { - child, - writer, - output, - screen, - reader_handle: Some(reader_handle), - _temp_dir: temp_path, - } - } - - fn screen_text(&self) -> String { - let scr = self.screen.lock().unwrap(); - scr.screen().contents() - } - - fn raw_output(&self) -> String { - let out = self.output.lock().unwrap(); - String::from_utf8_lossy(&out).into_owned() - } - - fn wait_for_screen(&self, needle: &str, timeout: Duration) -> bool { - let start = Instant::now(); - while start.elapsed() < timeout { - let text = self.screen_text(); - if !text.is_empty() && text.contains(needle) { - return true; - } - std::thread::sleep(POLL_INTERVAL); - } - false - } -} - -impl Drop for PtyApp { - fn drop(&mut self) { - let _ = self.writer.write_all(b"\x03"); - let _ = self.writer.flush(); - - let start = Instant::now(); - while start.elapsed() < Duration::from_millis(500) { - if let Ok(Some(_)) = self.child.try_wait() { - break; - } - std::thread::sleep(POLL_INTERVAL); - } - - let _ = self.child.kill(); - let _ = self.child.wait(); - - if let Some(handle) = self.reader_handle.take() { - let _ = handle.join(); - } - - let _ = std::fs::remove_dir_all(&self._temp_dir); - - std::thread::sleep(Duration::from_millis(100)); - } -} - -#[cfg_attr(not(feature = "e2e"), ignore)] -#[test] -fn root_help_renders() { - let app = PtyApp::spawn(&[]); - assert!( - app.wait_for_screen("Docs:", Duration::from_secs(5)), - "root help should render.\nScreen:\n{}\nRaw:\n{}", - app.screen_text(), - app.raw_output() - ); -} - -#[cfg_attr(not(feature = "e2e"), ignore)] -#[test] -fn transcribe_help_renders() { - let app = PtyApp::spawn(&["transcribe", "--help"]); - assert!( - app.wait_for_screen("Usage: char transcribe", Duration::from_secs(5)), - "transcribe help should render.\nScreen:\n{}\nRaw:\n{}", - app.screen_text(), - app.raw_output() - ); -} diff --git a/apps/slack-internal/.gitignore b/apps/slack-internal/.gitignore deleted file mode 100644 index 5a23824c2e..0000000000 --- a/apps/slack-internal/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ - -# LangGraph API -.langgraph_api diff --git a/apps/slack-internal/Dockerfile b/apps/slack-internal/Dockerfile deleted file mode 100644 index 9e83a4a2cb..0000000000 --- a/apps/slack-internal/Dockerfile +++ /dev/null @@ -1,31 +0,0 @@ -FROM node:22-slim AS node-base -WORKDIR /repo - -ENV PNPM_HOME="/pnpm" -ENV PATH="$PNPM_HOME:$PATH" -ENV CI=true -RUN corepack enable - -FROM node-base AS deps -COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ -COPY apps/slack-internal/package.json apps/slack-internal/package.json -COPY packages/agent-core/package.json packages/agent-core/package.json -COPY packages/agent-support/package.json packages/agent-support/package.json -COPY packages/agent-designer/package.json packages/agent-designer/package.json -RUN pnpm fetch --filter @hypr/slack-internal... - -FROM deps AS build -COPY apps/slack-internal apps/slack-internal -COPY packages/agent-core packages/agent-core -COPY packages/agent-support packages/agent-support -COPY packages/agent-designer packages/agent-designer -RUN pnpm install --filter @hypr/slack-internal... --frozen-lockfile -RUN pnpm deploy --filter @hypr/slack-internal --prod --legacy /runtime - -FROM oven/bun:1 AS runtime -WORKDIR /app -ARG GIT_SHA -ENV NODE_ENV="production" -ENV GIT_SHA=${GIT_SHA} -COPY --from=build /runtime ./ -CMD ["bun", "src/index.ts"] diff --git a/apps/slack-internal/fly.toml b/apps/slack-internal/fly.toml deleted file mode 100644 index 75f8aa5e6c..0000000000 --- a/apps/slack-internal/fly.toml +++ /dev/null @@ -1,25 +0,0 @@ -app = 'hyprnote-slack-internal' -primary_region = 'sjc' - -[build] -dockerfile = "Dockerfile" - -[deploy] -strategy = 'bluegreen' - -[env] -BUN_ENV = "production" - -[[services]] -internal_port = 8080 -protocol = "tcp" - -[[services.tcp_checks]] -interval = "10s" -timeout = "2s" -grace_period = "5s" - -[[vm]] -memory = '512mb' -cpu_kind = 'shared' -cpus = 1 diff --git a/apps/slack-internal/manifest.json b/apps/slack-internal/manifest.json deleted file mode 100644 index f43187a93a..0000000000 --- a/apps/slack-internal/manifest.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "_metadata": { - "major_version": 1, - "minor_version": 1 - }, - "display_information": { - "name": "Hyprnote Internal", - "description": "Hyprnote Slack bot for internal usage", - "background_color": "#1a1a2e" - }, - "features": { - "bot_user": { - "display_name": "Hyprnote", - "always_online": true - } - }, - "oauth_config": { - "scopes": { - "bot": [ - "app_mentions:read", - "canvases:read", - "channels:history", - "chat:write", - "files:read", - "groups:history", - "im:history", - "mpim:history" - ] - } - }, - "settings": { - "event_subscriptions": { - "bot_events": [ - "app_mention", - "message.channels", - "message.groups", - "message.im", - "message.mpim" - ] - }, - "interactivity": { - "is_enabled": true - }, - "org_deploy_enabled": false, - "socket_mode_enabled": true, - "token_rotation_enabled": false - } -} diff --git a/apps/slack-internal/package.json b/apps/slack-internal/package.json deleted file mode 100644 index 89791f12b5..0000000000 --- a/apps/slack-internal/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "@hypr/slack-internal", - "private": true, - "type": "module", - "scripts": { - "start": "bun src/index.ts", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "@hypr/agent-designer": "workspace:^", - "@hypr/agent-support": "workspace:^", - "@langchain/core": "^1.1.39", - "@langchain/langgraph": "^1.2.8", - "@sentry/bun": "^10.48.0", - "@slack/bolt": "^4.7.0", - "@slack/types": "^2.20.1", - "@slack/web-api": "^7.15.0", - "@t3-oss/env-core": "^0.13.11", - "jsx-slack": "^6.1.2", - "marked": "^17.0.6", - "zod": "^4.3.6" - }, - "devDependencies": { - "@types/bun": "^1.3.12", - "@types/marked": "^6.0.0", - "typescript": "^5.9.3" - } -} diff --git a/apps/slack-internal/src/app.ts b/apps/slack-internal/src/app.ts deleted file mode 100644 index 9dd1358c4d..0000000000 --- a/apps/slack-internal/src/app.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { App, LogLevel } from "@slack/bolt"; - -import { env } from "./env"; - -export const app = new App({ - token: env.SLACK_BOT_TOKEN, - socketMode: true, - appToken: env.SLACK_APP_TOKEN, - logLevel: - process.env.NODE_ENV === "development" ? LogLevel.DEBUG : LogLevel.INFO, -}); diff --git a/apps/slack-internal/src/config/agents.ts b/apps/slack-internal/src/config/agents.ts deleted file mode 100644 index 6707043369..0000000000 --- a/apps/slack-internal/src/config/agents.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { agent as designerAgent } from "@hypr/agent-designer"; -import type { CompiledAgentGraph } from "@hypr/agent-support"; -import { agent as internalAgent } from "@hypr/agent-support"; - -import { env } from "../env"; - -export interface AgentConfig { - agent: CompiledAgentGraph; - name: string; - description: string; -} - -export const agents = { - internal: { - agent: internalAgent, - name: "Internal Ops Agent", - description: - "Handles internal operations, Stripe, Loops, Supabase, PostHog", - }, - designer: { - agent: designerAgent, - name: "Designer Agent", - description: "Handles UI/UX design tasks using Magic Patterns", - }, -} as const satisfies Record; - -export type AgentType = keyof typeof agents; - -function parseChannelAgentMap(): Record { - try { - const parsed = JSON.parse(env.AGENT_CHANNEL_MAP) as Record; - const validMap: Record = {}; - - for (const [channelId, agentType] of Object.entries(parsed)) { - if (agentType in agents) { - validMap[channelId] = agentType as AgentType; - } else { - console.warn( - `Invalid agent type "${agentType}" for channel "${channelId}". Valid types: ${Object.keys(agents).join(", ")}`, - ); - } - } - - if (Object.keys(validMap).length > 0) { - console.log(`Loaded channel-agent mappings: ${JSON.stringify(validMap)}`); - } - - return validMap; - } catch (error) { - console.error("Failed to parse AGENT_CHANNEL_MAP:", error); - return {}; - } -} - -const channelAgentMap: Record = parseChannelAgentMap(); - -const defaultAgent: AgentType = "internal"; - -export function setChannelAgent(channelId: string, agentType: AgentType): void { - channelAgentMap[channelId] = agentType; -} - -export function getAgentForChannel(channelId: string): AgentConfig { - const agentType = channelAgentMap[channelId] ?? defaultAgent; - return agents[agentType]; -} - -export function getAgentTypeForChannel(channelId: string): AgentType { - return channelAgentMap[channelId] ?? defaultAgent; -} diff --git a/apps/slack-internal/src/env.ts b/apps/slack-internal/src/env.ts deleted file mode 100644 index 1a9817ca4c..0000000000 --- a/apps/slack-internal/src/env.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { createEnv } from "@t3-oss/env-core"; -import { z } from "zod"; - -export const env = createEnv({ - server: { - SLACK_BOT_TOKEN: z.string(), - SLACK_APP_TOKEN: z.string(), - AGENT_CHANNEL_MAP: z.string().optional().default("{}"), - }, - runtimeEnv: process.env, - emptyStringAsUndefined: true, -}); diff --git a/apps/slack-internal/src/index.ts b/apps/slack-internal/src/index.ts deleted file mode 100644 index f6a93a0d90..0000000000 --- a/apps/slack-internal/src/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -import "./instrument"; - -import { registerTool, setupCheckpointer } from "@hypr/agent-support"; - -import { app } from "./app"; -import { registerListeners } from "./listeners"; -import { chartTool } from "./tools/chart"; -import { readSlackMessageTool } from "./tools/read-slack-message"; - -registerTool(chartTool); -registerTool(readSlackMessageTool); - -registerListeners(app); - -const healthServer = Bun.serve({ - port: 8080, - fetch(req) { - if (new URL(req.url).pathname === "/health") { - return new Response("ok"); - } - return new Response("not found", { status: 404 }); - }, -}); - -(async () => { - try { - await setupCheckpointer(); - await app.start(); - console.log("Slack Internal app is running!"); - console.log(`Health server listening on port ${healthServer.port}`); - } catch (error) { - console.error("Failed to start app:", error); - process.exit(1); - } -})(); diff --git a/apps/slack-internal/src/instrument.ts b/apps/slack-internal/src/instrument.ts deleted file mode 100644 index fdf2bce2b6..0000000000 --- a/apps/slack-internal/src/instrument.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as Sentry from "@sentry/bun"; - -const isProduction = Bun.env.BUN_ENV === "production"; - -Sentry.init({ - dsn: Bun.env.SENTRY_DSN, - environment: isProduction ? "production" : "development", - release: Bun.env.GIT_SHA - ? `slack-internal@${Bun.env.GIT_SHA.slice(0, 7)}` - : "slack-internal@local", - sampleRate: 1.0, - enabled: isProduction, - initialScope: { - tags: { - service: "slack-internal", - }, - }, -}); diff --git a/apps/slack-internal/src/listeners/actions/agent-approval.tsx b/apps/slack-internal/src/listeners/actions/agent-approval.tsx deleted file mode 100644 index 2d3fcaa56a..0000000000 --- a/apps/slack-internal/src/listeners/actions/agent-approval.tsx +++ /dev/null @@ -1,134 +0,0 @@ -/** @jsxImportSource jsx-slack */ -import { Command } from "@langchain/langgraph"; -import * as Sentry from "@sentry/bun"; -import type { App } from "@slack/bolt"; -import type { KnownBlock } from "@slack/types"; -import { Blocks, Section } from "jsx-slack"; - -import type { AgentStateType, HumanResponse } from "@hypr/agent-support"; - -import { getAgentForChannel } from "../../config/agents"; - -export function registerAgentApprovalAction(app: App) { - app.action( - "agent_approve", - async ({ action, ack, respond, body, client, logger }) => { - await ack(); - - if (action.type !== "button" || !action.value) { - return; - } - - const threadId = action.value; - const channel = body.channel?.id; - - if (!channel) { - logger.error("No channel found in action body"); - return; - } - - await respond({ - replace_original: true, - blocks: ( - -
:white_check_mark: Approved. Executing...
-
- ) as unknown as KnownBlock[], - }); - - try { - const { agent } = getAgentForChannel(channel); - const resumeValue: HumanResponse = { type: "accept", args: null }; - const result = (await agent.invoke( - new Command({ resume: resumeValue }), - { configurable: { thread_id: threadId } }, - )) as AgentStateType; - - await client.chat.postMessage({ - channel, - thread_ts: threadId, - blocks: ( - -
{result?.output || "Execution completed."}
-
- ) as unknown as KnownBlock[], - }); - } catch (error) { - logger.error("Agent approval error:", error); - Sentry.withScope((scope) => { - scope.setTag("slack_channel", channel); - scope.setTag("slack_thread_id", threadId); - scope.setTag("action_type", "agent_approve"); - scope.setContext("agent_approval", { channel, threadId }); - Sentry.captureException(error); - }); - await client.chat.postMessage({ - channel, - thread_ts: threadId, - text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`, - }); - } - }, - ); - - app.action( - "agent_reject", - async ({ action, ack, respond, body, client, logger }) => { - await ack(); - - if (action.type !== "button" || !action.value) { - return; - } - - const threadId = action.value; - const channel = body.channel?.id; - - if (!channel) { - logger.error("No channel found in action body"); - return; - } - - await respond({ - replace_original: true, - blocks: ( - -
:x: Rejected by user.
-
- ) as unknown as KnownBlock[], - }); - - try { - const { agent } = getAgentForChannel(channel); - const resumeValue: HumanResponse = { type: "ignore", args: null }; - const result = (await agent.invoke( - new Command({ resume: resumeValue }), - { configurable: { thread_id: threadId } }, - )) as AgentStateType; - - await client.chat.postMessage({ - channel, - thread_ts: threadId, - blocks: ( - -
{result?.output || "Action was rejected."}
-
- ) as unknown as KnownBlock[], - }); - } catch (error) { - logger.error("Agent rejection error:", error); - Sentry.withScope((scope) => { - scope.setTag("slack_channel", channel); - scope.setTag("slack_thread_id", threadId); - scope.setTag("action_type", "agent_reject"); - scope.setContext("agent_rejection", { channel, threadId }); - Sentry.captureException(error); - }); - await client.chat.postMessage({ - channel, - thread_ts: threadId, - text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`, - }); - } - }, - ); -} diff --git a/apps/slack-internal/src/listeners/index.ts b/apps/slack-internal/src/listeners/index.ts deleted file mode 100644 index 8d31fb35c6..0000000000 --- a/apps/slack-internal/src/listeners/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { App } from "@slack/bolt"; - -import { registerAgentApprovalAction } from "./actions/agent-approval"; -import { registerAgentMessage } from "./messages/agent"; - -export function registerListeners(app: App) { - registerAgentApprovalAction(app); - registerAgentMessage(app); -} diff --git a/apps/slack-internal/src/listeners/messages/agent.tsx b/apps/slack-internal/src/listeners/messages/agent.tsx deleted file mode 100644 index 64e6eb81f9..0000000000 --- a/apps/slack-internal/src/listeners/messages/agent.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import type { App } from "@slack/bolt"; - -import { handleAgentMessage, shouldIgnoreMessage } from "./handler"; - -export function registerAgentMessage(app: App) { - app.event("app_mention", async ({ event, say, client, logger }) => { - const text = event.text.replace(/<@[A-Z0-9]+>/g, "").trim(); - - if (!text || shouldIgnoreMessage(text)) { - return; - } - - const threadTs = event.thread_ts ?? event.ts; - const isFirstMessage = !event.thread_ts; - - await handleAgentMessage({ - text, - rawText: event.text, - threadTs, - eventTs: event.ts, - channel: event.channel, - userId: event.user, - isFirstMessage, - useSimpleExitBlock: true, - client, - say, - logger, - }); - }); - - app.message(async ({ message, say, client, logger, context }) => { - if ("bot_id" in message) return; - if (!("text" in message) || !message.text) return; - - const text = message.text.trim(); - if (!text) return; - if (shouldIgnoreMessage(text)) return; - - const isDM = message.channel_type === "im"; - const isThreadReply = "thread_ts" in message && message.thread_ts; - - if (!isDM && !isThreadReply) return; - - if (isThreadReply && !isDM) { - const parentResult = await client.conversations.history({ - channel: message.channel, - latest: message.thread_ts as string, - inclusive: true, - limit: 1, - }); - - const parentMessage = parentResult.messages?.[0]; - const botUserId = context.botUserId; - - if (!parentMessage?.text?.includes(`<@${botUserId}>`)) { - return; - } - } - - const threadTs = - "thread_ts" in message ? (message.thread_ts ?? message.ts) : message.ts; - const isFirstMessage = !isThreadReply; - const userId = "user" in message ? message.user : undefined; - - await handleAgentMessage({ - text, - rawText: message.text, - threadTs, - eventTs: threadTs, - channel: message.channel, - userId, - isFirstMessage, - useSimpleExitBlock: false, - client, - say, - logger, - }); - }); -} diff --git a/apps/slack-internal/src/listeners/messages/blocks.tsx b/apps/slack-internal/src/listeners/messages/blocks.tsx deleted file mode 100644 index 51f94968e1..0000000000 --- a/apps/slack-internal/src/listeners/messages/blocks.tsx +++ /dev/null @@ -1,110 +0,0 @@ -/** @jsxImportSource jsx-slack */ -import type { KnownBlock } from "@slack/types"; -import { Actions, Blocks, Button, Section } from "jsx-slack"; - -import { markdownToBlocks } from "../../utils/markdown-to-blocks"; - -export function ExitBlock(): KnownBlock[] { - return ( - -
:wave: Session ended. Conversation history cleared.
-
- ) as unknown as KnownBlock[]; -} - -export function ExitBlockSimple(): KnownBlock[] { - return ( - -
:wave:
-
- ) as unknown as KnownBlock[]; -} - -export function WelcomeBlock({ - langsmithUrl, -}: { - langsmithUrl: string | null; -}): KnownBlock[] { - return ( - -
Continue conversation in this thread
- {langsmithUrl ? ( - - - - ) : null} -
- :bulb: Tip: Start your message with !aside to have the - agent ignore it -
-
- ) as unknown as KnownBlock[]; -} - -export function InterruptBlock({ - toolName, - toolArgs, - threadTs, -}: { - toolName: string; - toolArgs: Record; - threadTs: string; -}): KnownBlock[] { - return ( - -
- :warning: Tool {toolName} wants to execute: -
{JSON.stringify(toolArgs, null, 2)}
-
- - - - -
- ) as unknown as KnownBlock[]; -} - -export function ResponseBlock(text: string): KnownBlock[] { - return markdownToBlocks(text || "No response generated."); -} - -export function TerminateBlock({ - langsmithUrl, -}: { - langsmithUrl: string | null; -}): KnownBlock[] { - return ( - -
:octagonal_sign: Agent terminated.
- {langsmithUrl ? ( - - - - ) : null} -
- ) as unknown as KnownBlock[]; -} - -export function ProgressBlock({ - name, - task, -}: { - name: string; - task: string; -}): KnownBlock[] { - return ( - -
- :mag: {name}: {task} -
-
- ) as unknown as KnownBlock[]; -} diff --git a/apps/slack-internal/src/listeners/messages/handler.ts b/apps/slack-internal/src/listeners/messages/handler.ts deleted file mode 100644 index 605b99138b..0000000000 --- a/apps/slack-internal/src/listeners/messages/handler.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { HumanMessage } from "@langchain/core/messages"; -import * as Sentry from "@sentry/bun"; -import type { SayFn } from "@slack/bolt"; -import type { WebClient } from "@slack/web-api"; - -import { - clearThread, - extractOutput, - generateRunId, - getImages, - getInterruptToolArgs, - getInterruptToolName, - getLangSmithUrl, - isInterrupted, -} from "@hypr/agent-support"; -import type { AgentStateType, AgentStreamState } from "@hypr/agent-support"; - -import { getAgentForChannel } from "../../config/agents"; -import { env } from "../../env"; -import { - fetchReferencedSlackMessages, - type ReferencedContent, -} from "../../utils/slack-message-reader"; -import { - ExitBlock, - ExitBlockSimple, - InterruptBlock, - ProgressBlock, - ResponseBlock, - TerminateBlock, - WelcomeBlock, -} from "./blocks"; - -export function shouldIgnoreMessage(text: string): boolean { - const trimmed = text.trim(); - if (trimmed.startsWith("!aside")) return true; - if (/^\(aside\)/i.test(trimmed)) return true; - if (/^\(aside\s*\)/i.test(trimmed)) return true; - return false; -} - -function isExitCommand(text: string): boolean { - const command = text.trim().toUpperCase(); - return command === "EXIT" || command === "TERMINATE"; -} - -function isTerminateResponse(output: string | undefined): boolean { - return output?.trim() === "TERMINATE"; -} - -function buildAgentState( - text: string, - referencedContent: ReferencedContent[], -): Partial { - const rawImages = referencedContent.flatMap((c) => - c.images.map((img) => ({ base64: img.base64, mimeType: img.mimeType })), - ); - - let request: string; - if (referencedContent.length === 0) { - request = text; - } else { - const textParts = referencedContent.map((c) => c.text); - request = `${text}\n\n--- Referenced Slack Content ---\n${textParts.join("\n\n---\n\n")}`; - } - - return { - messages: [new HumanMessage(request)], - images: getImages({ request: "", images: rawImages }), - output: "", - }; -} - -async function fetchReferencedContent( - client: WebClient, - text: string, -): Promise { - return fetchReferencedSlackMessages(client, text, env.SLACK_BOT_TOKEN); -} - -interface Logger { - error: (message: string, ...args: unknown[]) => void; -} - -export interface AgentMessageContext { - text: string; - rawText: string; - threadTs: string; - eventTs: string; - channel: string; - userId?: string; - isFirstMessage: boolean; - useSimpleExitBlock?: boolean; - client: WebClient; - say: SayFn; - logger: Logger; -} - -export async function handleAgentMessage( - ctx: AgentMessageContext, -): Promise { - const { - text, - rawText, - threadTs, - eventTs, - channel, - userId, - isFirstMessage, - useSimpleExitBlock, - client, - say, - logger, - } = ctx; - - try { - if (isExitCommand(text)) { - await clearThread(threadTs); - await say({ - thread_ts: eventTs, - blocks: useSimpleExitBlock ? ExitBlockSimple() : ExitBlock(), - }); - return; - } - - const runId = generateRunId(); - - if (isFirstMessage) { - const langsmithUrl = getLangSmithUrl(threadTs); - await say({ - thread_ts: eventTs, - blocks: WelcomeBlock({ langsmithUrl }), - }); - } - - const referencedMessages = await fetchReferencedContent(client, rawText); - const initialState = buildAgentState(text, referencedMessages); - - const { agent } = getAgentForChannel(channel); - - let finalState: AgentStreamState = {}; - const progressItems: Array<{ name: string; task: string }> = []; - let progressMessageTs: string | undefined; - - for await (const chunk of await agent.stream(initialState, { - runId, - configurable: { thread_id: threadTs }, - metadata: { - slack_channel: channel, - slack_user: userId, - }, - tags: ["slack-internal"], - streamMode: ["values", "custom"], - })) { - const [mode, data] = chunk as unknown as [string, unknown]; - - if (mode === "custom") { - const customData = data as { type: string; name: string; task: string }; - if (customData.type === "subgraph") { - progressItems.push({ name: customData.name, task: customData.task }); - - const blocks = progressItems - .map((item) => ProgressBlock({ name: item.name, task: item.task })) - .flat(); - - if (progressMessageTs) { - await client.chat.update({ - channel, - ts: progressMessageTs, - blocks, - }); - } else { - const result = await say({ - thread_ts: eventTs, - blocks, - }); - progressMessageTs = result.ts; - } - } - } else if (mode === "values") { - finalState = data as AgentStreamState; - } - } - - if (isInterrupted(finalState)) { - const interruptData = finalState.__interrupt__[0].value; - await say({ - thread_ts: eventTs, - blocks: InterruptBlock({ - toolName: getInterruptToolName(interruptData), - toolArgs: getInterruptToolArgs(interruptData), - threadTs, - }), - }); - return; - } - - const output = extractOutput(finalState); - - if (isTerminateResponse(output)) { - await clearThread(threadTs); - const langsmithUrl = getLangSmithUrl(threadTs); - await say({ - thread_ts: eventTs, - blocks: TerminateBlock({ langsmithUrl }), - }); - return; - } - - await say({ - thread_ts: eventTs, - blocks: ResponseBlock(output ?? "No response generated."), - }); - } catch (error) { - logger.error("Agent error:", error); - Sentry.withScope((scope) => { - scope.setTag("slack_channel", channel); - scope.setTag("slack_thread_ts", threadTs); - if (userId) { - scope.setTag("slack_user", userId); - } - scope.setContext("agent_message", { - channel, - threadTs, - userId, - isFirstMessage, - }); - Sentry.captureException(error); - }); - await say({ - thread_ts: eventTs, - text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`, - }); - } -} diff --git a/apps/slack-internal/src/tools/chart.ts b/apps/slack-internal/src/tools/chart.ts deleted file mode 100644 index 74e7f512ad..0000000000 --- a/apps/slack-internal/src/tools/chart.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { tool } from "@langchain/core/tools"; -import { z } from "zod"; - -const QUICKCHART_BASE_URL = "https://quickchart.io/chart"; - -export const chartTool = tool( - async ({ - config, - width, - height, - backgroundColor, - }: { - config: string; - width?: number; - height?: number; - backgroundColor?: string; - }) => { - const params = new URLSearchParams(); - params.set("c", config); - - if (width) { - params.set("w", width.toString()); - } - if (height) { - params.set("h", height.toString()); - } - if (backgroundColor) { - params.set("bkg", backgroundColor); - } - - const chartUrl = `${QUICKCHART_BASE_URL}?${params.toString()}`; - - return `![Chart](${chartUrl})`; - }, - { - name: "chart", - description: - "Generate a chart image using QuickChart.io. Returns a markdown image that will be rendered in Slack. The config parameter should be a Chart.js configuration object as a JSON string. Supports bar, line, pie, doughnut, radar, scatter, and other Chart.js chart types.", - schema: z.object({ - config: z - .string() - .describe( - 'Chart.js configuration object as a JSON string. Example: {"type":"bar","data":{"labels":["Q1","Q2","Q3","Q4"],"datasets":[{"label":"Sales","data":[10,20,30,40]}]}}', - ), - width: z - .number() - .optional() - .describe("Chart width in pixels (default: 500)"), - height: z - .number() - .optional() - .describe("Chart height in pixels (default: 300)"), - backgroundColor: z - .string() - .optional() - .describe( - "Background color (default: transparent). Use 'white' for better visibility in Slack.", - ), - }), - }, -); diff --git a/apps/slack-internal/src/tools/read-slack-message.ts b/apps/slack-internal/src/tools/read-slack-message.ts deleted file mode 100644 index 8360188128..0000000000 --- a/apps/slack-internal/src/tools/read-slack-message.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { tool } from "@langchain/core/tools"; -import { WebClient } from "@slack/web-api"; -import { z } from "zod"; - -import { env } from "../env"; -import { - fetchSlackThread, - formatThreadContent, - parseSlackMessageLinks, -} from "../utils/slack-message-reader"; - -const client = new WebClient(env.SLACK_BOT_TOKEN); - -export const readSlackMessageTool = tool( - async ({ url }: { url: string }) => { - const links = parseSlackMessageLinks(url); - - if (links.length === 0) { - return "Invalid Slack message URL. Expected format: https://workspace.slack.com/archives/CHANNEL_ID/pTIMESTAMP"; - } - - const link = links[0]; - const threadTs = link.threadTs || link.ts; - - const thread = await fetchSlackThread( - client, - link.channel, - threadTs, - env.SLACK_BOT_TOKEN, - ); - - if (!thread) { - return "Could not fetch the Slack message. The bot may not have access to this channel."; - } - - return formatThreadContent(thread); - }, - { - name: "readSlackMessage", - description: - "Read a Slack message and its entire thread by URL. Returns the parent message and all replies, including any files, canvases, and URLs found in the messages. Use this when you encounter a Slack message URL and need to read its content.", - schema: z.object({ - url: z - .string() - .describe( - "The Slack message URL (e.g., https://workspace.slack.com/archives/C123ABC456/p1234567890123456)", - ), - }), - }, -); diff --git a/apps/slack-internal/src/utils/markdown-to-blocks.ts b/apps/slack-internal/src/utils/markdown-to-blocks.ts deleted file mode 100644 index 3cd9d10494..0000000000 --- a/apps/slack-internal/src/utils/markdown-to-blocks.ts +++ /dev/null @@ -1,313 +0,0 @@ -import type { - DividerBlock, - ImageBlock, - KnownBlock, - RichTextBlock, - RichTextElement, - RichTextSection, -} from "@slack/types"; -import { lexer, type Token, type Tokens } from "marked"; - -const TABLE_MAX_ROWS = 100; -const TABLE_MAX_COLS = 20; - -type Style = NonNullable; - -function textEl(value: string, style?: Style): RichTextElement { - return style - ? { type: "text", text: value, style } - : { type: "text", text: value }; -} - -function parseTextWithEmojis(value: string, style?: Style): RichTextElement[] { - const elements: RichTextElement[] = []; - const regex = /:([a-zA-Z0-9_+-]+):/g; - let lastIndex = 0; - let match; - - while ((match = regex.exec(value)) !== null) { - if (match[1].length === 0) continue; - if (match.index > lastIndex) { - elements.push(textEl(value.slice(lastIndex, match.index), style)); - } - elements.push({ type: "emoji", name: match[1] }); - lastIndex = match.index + match[0].length; - } - - if (lastIndex < value.length) { - elements.push(textEl(value.slice(lastIndex), style)); - } - - return elements.length > 0 - ? elements - : value.length > 0 - ? [textEl(value, style)] - : []; -} - -function inlineTokensToElements( - tokens: Token[], - style?: Style, -): RichTextElement[] { - const elements: RichTextElement[] = []; - - for (const token of tokens) { - switch (token.type) { - case "text": - elements.push( - ...parseTextWithEmojis((token as Tokens.Text).text, style), - ); - break; - case "strong": - elements.push( - ...inlineTokensToElements((token as Tokens.Strong).tokens, { - ...style, - bold: true, - }), - ); - break; - case "em": - elements.push( - ...inlineTokensToElements((token as Tokens.Em).tokens, { - ...style, - italic: true, - }), - ); - break; - case "del": - elements.push( - ...inlineTokensToElements((token as Tokens.Del).tokens, { - ...style, - strike: true, - }), - ); - break; - case "codespan": - elements.push( - textEl((token as Tokens.Codespan).text, { ...style, code: true }), - ); - break; - case "link": { - const t = token as Tokens.Link; - elements.push({ type: "link", url: t.href, text: t.text }); - break; - } - case "escape": - elements.push(textEl((token as Tokens.Escape).text, style)); - break; - case "br": - elements.push({ type: "text", text: "\n" }); - break; - default: - if ("text" in token && typeof token.text === "string") { - elements.push(textEl(token.text, style)); - } - } - } - - return elements; -} - -function toSection(tokens: Token[]): RichTextSection { - const elements = inlineTokensToElements(tokens); - return { - type: "rich_text_section", - elements: elements.length > 0 ? elements : [{ type: "text", text: "" }], - }; -} - -function listItemToSection(item: Tokens.ListItem): RichTextSection { - const firstToken = item.tokens?.[0] as - | (Token & { tokens?: Token[] }) - | undefined; - const inlineTokens = firstToken?.tokens ?? (firstToken ? [firstToken] : []); - return inlineTokens.length > 0 - ? toSection(inlineTokens) - : { - type: "rich_text_section", - elements: [{ type: "text", text: item.text }], - }; -} - -// Slack only supports ONE native table block per message. -function tableToNativeBlock(t: Tokens.Table): KnownBlock | null { - const headerRow = t.header - .slice(0, TABLE_MAX_COLS) - .map((c) => ({ type: "raw_text" as const, text: c.text || " " })); - - if (headerRow.length === 0) return null; - - const dataRows = t.rows - .slice(0, TABLE_MAX_ROWS - 1) - .map((row) => - row - .slice(0, TABLE_MAX_COLS) - .map((c) => ({ type: "raw_text" as const, text: c.text || " " })), - ) - .filter((row) => row.length > 0); - - const rows = [headerRow, ...dataRows]; - return { type: "table", rows } as KnownBlock; -} - -// Fallback for additional tables: render as preformatted text. -function tableToPreformatted(t: Tokens.Table): RichTextBlock { - const header = t.header.map((c) => c.text); - const separator = header.map((h) => "-".repeat(h.length)); - const rows = t.rows.map((row) => row.map((c) => c.text).join(" | ")); - const content = [header.join(" | "), separator.join(" | "), ...rows].join( - "\n", - ); - - return { - type: "rich_text", - elements: [ - { - type: "rich_text_preformatted", - elements: [{ type: "text", text: content }], - }, - ], - }; -} - -function tokenToBlocks(token: Token): KnownBlock[] { - switch (token.type) { - case "heading": - case "paragraph": { - const t = token as Tokens.Paragraph | Tokens.Heading; - if (!t.tokens?.length) return []; - - // If the paragraph contains only an image, convert it to an ImageBlock - if (t.tokens.length === 1 && t.tokens[0].type === "image") { - const img = t.tokens[0] as Tokens.Image; - return [ - { - type: "image", - image_url: img.href, - alt_text: img.text || "image", - } as ImageBlock, - ]; - } - - return [ - { type: "rich_text", elements: [toSection(t.tokens)] } as RichTextBlock, - ]; - } - - case "list": { - const t = token as Tokens.List; - const sections = t.items.map(listItemToSection); - if (sections.length === 0) return []; - return [ - { - type: "rich_text", - elements: [ - { - type: "rich_text_list", - style: t.ordered ? "ordered" : "bullet", - elements: sections, - }, - ], - } as RichTextBlock, - ]; - } - - case "code": { - const t = token as Tokens.Code; - return [ - { - type: "rich_text", - elements: [ - { - type: "rich_text_preformatted", - elements: [{ type: "text", text: t.text }], - }, - ], - } as RichTextBlock, - ]; - } - - case "blockquote": { - const t = token as Tokens.Blockquote; - const quoteElements: RichTextElement[] = []; - for (const inner of t.tokens) { - if (inner.type === "paragraph") { - const para = inner as Tokens.Paragraph; - if (para.tokens) - quoteElements.push(...inlineTokensToElements(para.tokens)); - } - } - if (quoteElements.length === 0) return []; - return [ - { - type: "rich_text", - elements: [{ type: "rich_text_quote", elements: quoteElements }], - } as RichTextBlock, - ]; - } - - case "hr": - return [{ type: "divider" } as DividerBlock]; - - case "image": { - const t = token as Tokens.Image; - return [ - { - type: "image", - image_url: t.href, - alt_text: t.text || "image", - } as ImageBlock, - ]; - } - - default: - return []; - } -} - -export function markdownToBlocks(markdown: string): KnownBlock[] { - const tokens = lexer(markdown); - const blocks: KnownBlock[] = []; - let nativeTableBlock: KnownBlock | null = null; - - for (const token of tokens) { - if (token.type === "space") continue; - - if (token.type === "table") { - const t = token as Tokens.Table; - // Slack only supports one native table per message; subsequent tables become preformatted. - if (!nativeTableBlock) { - const table = tableToNativeBlock(t); - if (table) { - nativeTableBlock = table; - } else { - blocks.push(tableToPreformatted(t)); - } - } else { - blocks.push(tableToPreformatted(t)); - } - } else { - blocks.push(...tokenToBlocks(token)); - } - } - - if (nativeTableBlock) blocks.unshift(nativeTableBlock); - - if (blocks.length === 0) { - return [ - { - type: "rich_text", - elements: [ - { - type: "rich_text_section", - elements: [ - { type: "text", text: markdown || "No response generated." }, - ], - }, - ], - } as RichTextBlock, - ]; - } - - return blocks; -} diff --git a/apps/slack-internal/src/utils/slack-message-reader.ts b/apps/slack-internal/src/utils/slack-message-reader.ts deleted file mode 100644 index 3a9c403037..0000000000 --- a/apps/slack-internal/src/utils/slack-message-reader.ts +++ /dev/null @@ -1,367 +0,0 @@ -import type { WebClient } from "@slack/web-api"; - -export interface SlackMessageLink { - channel: string; - ts: string; - threadTs?: string; -} - -export interface FileInfo { - id: string; - name: string; - mimetype: string; - size: number; - content?: string; - base64?: string; -} - -export interface CanvasInfo { - id: string; - content: string; -} - -export interface ProcessedMessage { - text: string; - user?: string; - ts: string; - files: FileInfo[]; - canvases: CanvasInfo[]; - urls: string[]; -} - -export interface ThreadContent { - parentMessage: ProcessedMessage; - replies: ProcessedMessage[]; -} - -export function parseSlackMessageLinks(text: string): SlackMessageLink[] { - const regex = - /https:\/\/[^\/]+\.slack\.com\/archives\/([A-Z0-9]+)\/p(\d{10})(\d{6})(?:\?thread_ts=(\d+\.\d+))?/g; - const matches: SlackMessageLink[] = []; - - let match; - while ((match = regex.exec(text)) !== null) { - const ts = `${match[2]}.${match[3]}`; - matches.push({ - channel: match[1], - ts, - threadTs: match[4], - }); - } - - return matches; -} - -export function extractUrlsFromText(text: string): string[] { - const urlRegex = /<(https?:\/\/[^|>]+)(?:\|[^>]*)?>/g; - const urls: string[] = []; - - let match; - while ((match = urlRegex.exec(text)) !== null) { - const url = match[1]; - if (!url.includes(".slack.com/archives/")) { - urls.push(url); - } - } - - return urls; -} - -function extractCanvasIds(text: string): string[] { - const canvasRegex = /]*>/g; - const ids: string[] = []; - - let match; - while ((match = canvasRegex.exec(text)) !== null) { - ids.push(match[1]); - } - - return ids; -} - -async function fetchFileContent( - client: WebClient, - fileId: string, - token: string, -): Promise { - try { - const result = await client.files.info({ file: fileId }); - const file = result.file; - if (!file) return null; - - const fileInfo: FileInfo = { - id: file.id!, - name: file.name || "unknown", - mimetype: file.mimetype || "unknown", - size: file.size || 0, - }; - - const textMimetypes = [ - "text/plain", - "text/markdown", - "text/html", - "text/csv", - "application/json", - "application/xml", - "text/javascript", - "text/typescript", - ]; - - if ( - file.mimetype && - textMimetypes.some((t) => file.mimetype!.includes(t)) - ) { - if (file.url_private) { - try { - const response = await fetch(file.url_private, { - headers: { Authorization: `Bearer ${token}` }, - }); - if (response.ok) { - fileInfo.content = await response.text(); - } - } catch { - // Failed to fetch file content - } - } - } else if (file.mimetype?.startsWith("image/")) { - if (file.url_private) { - try { - const response = await fetch(file.url_private, { - headers: { Authorization: `Bearer ${token}` }, - }); - if (response.ok) { - const arrayBuffer = await response.arrayBuffer(); - fileInfo.base64 = Buffer.from(arrayBuffer).toString("base64"); - } - } catch { - // Failed to fetch image content - } - } - } - - return fileInfo; - } catch { - return null; - } -} - -async function fetchCanvasContent( - client: WebClient, - canvasId: string, -): Promise { - try { - const result = await client.canvases.sections.lookup({ - canvas_id: canvasId, - criteria: { section_types: ["any_header"] }, - }); - - if (!result.sections || result.sections.length === 0) { - return null; - } - - const content = ( - result.sections as Array<{ document_content?: { markdown?: string } }> - ) - .map((section) => section.document_content?.markdown || "") - .filter(Boolean) - .join("\n\n"); - - return { id: canvasId, content }; - } catch { - return null; - } -} - -interface SlackMessage { - text?: string; - user?: string; - ts?: string; - files?: Array<{ id?: string }>; - thread_ts?: string; -} - -async function processMessage( - client: WebClient, - message: SlackMessage, - token: string, -): Promise { - const text = message.text || ""; - const urls = extractUrlsFromText(text); - const canvasIds = extractCanvasIds(text); - - const files: FileInfo[] = []; - if (message.files) { - const fileResults = await Promise.all( - message.files.map((f) => - f.id ? fetchFileContent(client, f.id, token) : null, - ), - ); - files.push(...fileResults.filter((f): f is FileInfo => f !== null)); - } - - const canvases: CanvasInfo[] = []; - if (canvasIds.length > 0) { - const canvasResults = await Promise.all( - canvasIds.map((id) => fetchCanvasContent(client, id)), - ); - canvases.push(...canvasResults.filter((c): c is CanvasInfo => c !== null)); - } - - return { - text, - user: message.user, - ts: message.ts || "", - files, - canvases, - urls, - }; -} - -export async function fetchSlackThread( - client: WebClient, - channel: string, - ts: string, - token: string, -): Promise { - try { - const result = await client.conversations.replies({ - channel, - ts, - inclusive: true, - limit: 100, - }); - - if (!result.messages || result.messages.length === 0) { - return null; - } - - const [parentRaw, ...repliesRaw] = result.messages; - const parentMessage = await processMessage( - client, - parentRaw as SlackMessage, - token, - ); - const replies = await Promise.all( - repliesRaw.map((m) => processMessage(client, m as SlackMessage, token)), - ); - - return { parentMessage, replies }; - } catch { - return null; - } -} - -function formatFileInfo(file: FileInfo): string { - if (file.content) { - return `[File: ${file.name} (${file.mimetype})]\n${file.content}`; - } - if (file.base64) { - return `[Image: ${file.name} - included as image attachment]`; - } - return `[File: ${file.name} (${file.mimetype}, ${file.size} bytes)]`; -} - -function formatProcessedMessage(msg: ProcessedMessage, indent = ""): string { - const parts: string[] = []; - - if (msg.text) { - parts.push(`${indent}${msg.text}`); - } - - if (msg.files.length > 0) { - parts.push(`${indent}Files:`); - for (const file of msg.files) { - parts.push(`${indent} ${formatFileInfo(file)}`); - } - } - - if (msg.canvases.length > 0) { - for (const canvas of msg.canvases) { - parts.push(`${indent}[Canvas]\n${canvas.content}`); - } - } - - if (msg.urls.length > 0) { - parts.push(`${indent}URLs: ${msg.urls.join(", ")}`); - } - - return parts.join("\n"); -} - -export function formatThreadContent(thread: ThreadContent): string { - const parts: string[] = []; - - parts.push("=== Parent Message ==="); - parts.push(formatProcessedMessage(thread.parentMessage)); - - if (thread.replies.length > 0) { - parts.push("\n=== Thread Replies ==="); - for (let i = 0; i < thread.replies.length; i++) { - parts.push(`\n--- Reply ${i + 1} ---`); - parts.push(formatProcessedMessage(thread.replies[i])); - } - } - - return parts.join("\n"); -} - -export interface ImageAttachment { - base64: string; - mimeType: string; - name: string; -} - -export interface ReferencedContent { - text: string; - images: ImageAttachment[]; -} - -function extractImagesFromThread(thread: ThreadContent): ImageAttachment[] { - const images: ImageAttachment[] = []; - - const processMessage = (msg: ProcessedMessage) => { - for (const file of msg.files) { - if (file.base64 && file.mimetype.startsWith("image/")) { - images.push({ - base64: file.base64, - mimeType: file.mimetype, - name: file.name, - }); - } - } - }; - - processMessage(thread.parentMessage); - for (const reply of thread.replies) { - processMessage(reply); - } - - return images; -} - -export async function fetchReferencedSlackMessages( - client: WebClient, - text: string, - token: string, -): Promise { - const links = parseSlackMessageLinks(text); - const results: ReferencedContent[] = []; - - for (const link of links) { - const threadTs = link.threadTs || link.ts; - const thread = await fetchSlackThread( - client, - link.channel, - threadTs, - token, - ); - if (thread) { - results.push({ - text: formatThreadContent(thread), - images: extractImagesFromThread(thread), - }); - } - } - - return results; -} diff --git a/apps/slack-internal/tsconfig.json b/apps/slack-internal/tsconfig.json deleted file mode 100644 index fa6102b0a3..0000000000 --- a/apps/slack-internal/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "include": ["src/**/*.ts", "src/**/*.tsx"], - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "bundler", - "allowImportingTsExtensions": false, - "strict": true, - "skipLibCheck": true, - "noUncheckedSideEffectImports": true, - "types": ["@types/bun"], - "jsx": "react-jsx", - "jsxImportSource": "jsx-slack" - } -} diff --git a/apps/stripe/Dockerfile b/apps/stripe/Dockerfile deleted file mode 100644 index 9a4d8a07b3..0000000000 --- a/apps/stripe/Dockerfile +++ /dev/null @@ -1,28 +0,0 @@ -# https://pnpm.io/docker#example-3-build-on-cicd -FROM node:22-slim AS node-base -WORKDIR /repo - -ENV PNPM_HOME="/pnpm" -ENV PATH="$PNPM_HOME:$PATH" -ENV CI=true -RUN corepack enable - -FROM node-base AS deps -COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ -COPY apps/stripe/package.json apps/stripe/package.json -COPY packages/supabase/package.json packages/supabase/package.json -RUN pnpm fetch --prod --filter @hypr/stripe - -FROM deps AS build -COPY apps/stripe apps/stripe -COPY packages/supabase packages/supabase -RUN pnpm install --filter @hypr/stripe --frozen-lockfile -RUN pnpm deploy --filter @hypr/stripe --prod --legacy /runtime - -FROM oven/bun:1 AS runtime -WORKDIR /app - -COPY --from=build /runtime ./ - -EXPOSE 8788 -CMD ["bun", "src/index.ts"] diff --git a/apps/stripe/fly.toml b/apps/stripe/fly.toml deleted file mode 100644 index c5251d3b5b..0000000000 --- a/apps/stripe/fly.toml +++ /dev/null @@ -1,49 +0,0 @@ -# fly.toml app configuration file generated for hyprnote-stripe on 2026-02-05T16:52:13+09:00 -# -# See https://fly.io/docs/reference/configuration/ for information about how to use this file. -# - -app = 'hyprnote-stripe' -primary_region = 'sjc' -kill_signal = 'SIGTERM' -kill_timeout = '30s' -swap_size_mb = 512 - -[build] -dockerfile = 'Dockerfile' - -[deploy] -strategy = 'bluegreen' - -[env] -BUN_ENV = 'production' -PORT = '8080' - -[processes] -web = 'bun src/index.ts' - -[http_service] -internal_port = 8080 -force_https = true -auto_stop_machines = 'stop' -auto_start_machines = true -min_machines_running = 1 -processes = ['web'] - -[http_service.concurrency] -type = 'requests' -hard_limit = 200 -soft_limit = 150 - -[[http_service.checks]] -interval = '15s' -timeout = '4s' -grace_period = '20s' -method = 'GET' -path = '/health' -protocol = 'http' - -[[vm]] -memory = '512mb' -cpus = 1 -cpu_kind = 'shared' diff --git a/apps/stripe/package.json b/apps/stripe/package.json deleted file mode 100644 index 5483d405e2..0000000000 --- a/apps/stripe/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "@hypr/stripe", - "private": true, - "type": "module", - "scripts": { - "dev": "dotenvx run --ignore MISSING_ENV_FILE -f ../../.env.supabase -f ../../.env.stripe -f .env -- bun --hot src/index.ts", - "typecheck": "tsc --noEmit", - "test": "sh -c 'if find src -type f \\( -name \"*.test.ts\" -o -name \"*.test.tsx\" -o -name \"*.test.js\" -o -name \"*.test.jsx\" -o -name \"*.spec.ts\" -o -name \"*.spec.tsx\" -o -name \"*.spec.js\" -o -name \"*.spec.jsx\" \\) | grep -q .; then command -v bun >/dev/null 2>&1 || { echo \"bun is required to run @hypr/stripe tests\" >&2; exit 1; }; bun test --pass-with-no-tests; else node --test; fi'" - }, - "dependencies": { - "@hypr/supabase": "workspace:*", - "@sentry/bun": "^10.48.0", - "@supabase/stripe-sync-engine": "^0.45.0", - "@supabase/supabase-js": "^2.103.0", - "@t3-oss/env-core": "^0.13.11", - "effect": "^3.21.0", - "hono": "^4.12.12", - "pg": "^8.20.0", - "stripe": "^20.4.1", - "zod": "^4.3.6" - }, - "devDependencies": { - "@dotenvx/dotenvx": "^1.61.0", - "@types/bun": "^1.3.12", - "@types/pg": "^8.20.0", - "typescript": "^5.9.3" - } -} diff --git a/apps/stripe/src/billing-bridge.ts b/apps/stripe/src/billing-bridge.ts deleted file mode 100644 index c7fc541494..0000000000 --- a/apps/stripe/src/billing-bridge.ts +++ /dev/null @@ -1,97 +0,0 @@ -import Stripe from "stripe"; - -import { stripe } from "./integration/stripe"; -import { supabaseAdmin } from "./integration/supabase"; - -const CUSTOMER_EVENTS: Stripe.Event.Type[] = [ - "checkout.session.completed", - "customer.created", - "customer.updated", - "customer.subscription.created", - "customer.subscription.updated", -]; - -export async function syncBillingBridge(event: Stripe.Event) { - if (!isCustomerEvent(event.type)) { - return; - } - - const customerId = getCustomerId(event.data.object); - - if (!customerId) { - return; - } - - const customer = await getStripeCustomer(customerId); - - if (!customer) { - return; - } - - const userId = getUserIdFromCustomer(customer); - - if (!userId) { - return; - } - - const { error } = await supabaseAdmin.from("profiles").upsert( - { - id: userId, - stripe_customer_id: customerId, - }, - { onConflict: "id" }, - ); - - if (error) { - throw error; - } -} - -const isCustomerEvent = (eventType: string) => - CUSTOMER_EVENTS.includes(eventType as Stripe.Event.Type); - -const getCustomerId = ( - eventObject: Stripe.Event.Data.Object, -): string | null => { - const obj = eventObject as { - customer?: string | { id: string }; - id?: string; - }; - - if (typeof obj.customer === "string") { - return obj.customer; - } - - if (obj.customer && typeof obj.customer === "object") { - return obj.customer.id; - } - - if (obj.id?.startsWith("cus_")) { - return obj.id; - } - - return null; -}; - -const getStripeCustomer = async (customerId: string) => { - const customer = await stripe.customers.retrieve(customerId); - - if (isDeletedCustomer(customer)) { - return null; - } - - return customer; -}; - -const isDeletedCustomer = ( - customer: Stripe.Customer | Stripe.DeletedCustomer, -): customer is Stripe.DeletedCustomer => - "deleted" in customer && customer.deleted === true; - -const getUserIdFromCustomer = (customer: Stripe.Customer): string | null => { - const metadata = customer.metadata ?? {}; - - return ( - metadata["userId"] || metadata["user_id"] || metadata["userID"] || null - ); -}; diff --git a/apps/stripe/src/env.ts b/apps/stripe/src/env.ts deleted file mode 100644 index 82b448d2a4..0000000000 --- a/apps/stripe/src/env.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { createEnv } from "@t3-oss/env-core"; -import { z } from "zod"; - -export const env = createEnv({ - server: { - PORT: z.coerce.number().default(8788), - NODE_ENV: z - .enum(["development", "test", "production"]) - .default("development"), - DATABASE_URL: z.string().min(1), - SUPABASE_URL: z.url(), - SUPABASE_SERVICE_ROLE_KEY: z.string().min(1), - STRIPE_SECRET_KEY: z.string().min(1), - STRIPE_WEBHOOK_SECRET: z.string().min(1), - }, - runtimeEnv: Bun.env, - emptyStringAsUndefined: true, - skipValidation: Bun.env.CI === "true", -}); diff --git a/apps/stripe/src/hono-bindings.ts b/apps/stripe/src/hono-bindings.ts deleted file mode 100644 index 2b1aacc68d..0000000000 --- a/apps/stripe/src/hono-bindings.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type Stripe from "stripe"; - -export type AppBindings = { - Variables: { - stripeEvent: Stripe.Event; - stripeRawBody: string; - stripeSignature: string; - }; -}; diff --git a/apps/stripe/src/index.ts b/apps/stripe/src/index.ts deleted file mode 100644 index 433cedbfe2..0000000000 --- a/apps/stripe/src/index.ts +++ /dev/null @@ -1,47 +0,0 @@ -import * as Sentry from "@sentry/bun"; -import { Hono } from "hono"; -import { bodyLimit } from "hono/body-limit"; -import { cors } from "hono/cors"; -import { logger } from "hono/logger"; - -import { env } from "./env"; -import type { AppBindings } from "./hono-bindings"; -import { verifyStripeWebhook } from "./middleware"; -import { routes } from "./routes"; - -Sentry.init({ - dsn: Bun.env.SENTRY_DSN, - environment: env.NODE_ENV, - enabled: env.NODE_ENV === "production", -}); - -const app = new Hono(); - -app.use(logger()); -app.use(bodyLimit({ maxSize: 1024 * 1024 * 5 })); - -const corsMiddleware = cors({ - origin: "*", - allowHeaders: ["content-type", "stripe-signature"], - allowMethods: ["GET", "POST", "OPTIONS"], -}); - -app.use("*", corsMiddleware); - -app.use("/webhook/stripe", verifyStripeWebhook); - -app.route("/", routes); - -app.onError((err, c) => { - Sentry.captureException(err, { - extra: { path: c.req.path, method: c.req.method }, - }); - return c.json({ error: "internal_server_error" }, 500); -}); - -app.notFound((c) => c.text("not_found", 404)); - -export default { - port: env.PORT, - fetch: app.fetch, -}; diff --git a/apps/stripe/src/integration/index.ts b/apps/stripe/src/integration/index.ts deleted file mode 100644 index a624a6a5de..0000000000 --- a/apps/stripe/src/integration/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./stripe"; -export * from "./stripe-sync"; -export * from "./supabase"; diff --git a/apps/stripe/src/integration/stripe-sync.ts b/apps/stripe/src/integration/stripe-sync.ts deleted file mode 100644 index f48817a1ab..0000000000 --- a/apps/stripe/src/integration/stripe-sync.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { StripeSync } from "@supabase/stripe-sync-engine"; - -import { env } from "../env"; -import { STRIPE_API_VERSION } from "./stripe"; - -export const stripeSync = new StripeSync({ - schema: "stripe", - poolConfig: { connectionString: env.DATABASE_URL }, - stripeSecretKey: env.STRIPE_SECRET_KEY, - stripeWebhookSecret: env.STRIPE_WEBHOOK_SECRET, - stripeApiVersion: STRIPE_API_VERSION, - backfillRelatedEntities: true, -}); diff --git a/apps/stripe/src/integration/stripe.ts b/apps/stripe/src/integration/stripe.ts deleted file mode 100644 index 43afed1d97..0000000000 --- a/apps/stripe/src/integration/stripe.ts +++ /dev/null @@ -1,9 +0,0 @@ -import Stripe from "stripe"; - -import { env } from "../env"; - -export const STRIPE_API_VERSION = "2026-02-25.clover"; - -export const stripe = new Stripe(env.STRIPE_SECRET_KEY, { - apiVersion: STRIPE_API_VERSION, -}); diff --git a/apps/stripe/src/integration/supabase.ts b/apps/stripe/src/integration/supabase.ts deleted file mode 100644 index 7370a99538..0000000000 --- a/apps/stripe/src/integration/supabase.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createClient } from "@hypr/supabase"; - -import { env } from "../env"; - -export const supabaseAdmin = createClient( - env.SUPABASE_URL, - env.SUPABASE_SERVICE_ROLE_KEY, -); diff --git a/apps/stripe/src/middleware/index.ts b/apps/stripe/src/middleware/index.ts deleted file mode 100644 index c3c598ddcc..0000000000 --- a/apps/stripe/src/middleware/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./stripe"; diff --git a/apps/stripe/src/middleware/stripe.ts b/apps/stripe/src/middleware/stripe.ts deleted file mode 100644 index b495b4ba30..0000000000 --- a/apps/stripe/src/middleware/stripe.ts +++ /dev/null @@ -1,44 +0,0 @@ -import * as Sentry from "@sentry/bun"; -import { createMiddleware } from "hono/factory"; -import Stripe from "stripe"; - -import { env } from "../env"; -import { stripe } from "../integration/stripe"; - -const cryptoProvider = Stripe.createSubtleCryptoProvider(); - -export const verifyStripeWebhook = createMiddleware<{ - Variables: { - stripeEvent: Stripe.Event; - stripeRawBody: string; - stripeSignature: string; - }; -}>(async (c, next) => { - const signature = c.req.header("Stripe-Signature"); - - if (!signature) { - return c.text("missing_stripe_signature", 400); - } - - const body = await c.req.text(); - try { - const event = await stripe.webhooks.constructEventAsync( - body, - signature, - env.STRIPE_WEBHOOK_SECRET, - undefined, - cryptoProvider, - ); - - c.set("stripeEvent", event); - c.set("stripeRawBody", body); - c.set("stripeSignature", signature); - await next(); - } catch (err) { - Sentry.captureException(err, { - tags: { webhook: "stripe", step: "signature_verification" }, - }); - const message = err instanceof Error ? err.message : "unknown_error"; - return c.text(message, 400); - } -}); diff --git a/apps/stripe/src/routes/health.ts b/apps/stripe/src/routes/health.ts deleted file mode 100644 index 2cf0d9c63f..0000000000 --- a/apps/stripe/src/routes/health.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Hono } from "hono"; - -import type { AppBindings } from "../hono-bindings"; - -export const health = new Hono(); - -health.get("/", (c) => { - return c.json({ status: "ok" }, 200); -}); diff --git a/apps/stripe/src/routes/index.ts b/apps/stripe/src/routes/index.ts deleted file mode 100644 index 473478a7d5..0000000000 --- a/apps/stripe/src/routes/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Hono } from "hono"; - -import type { AppBindings } from "../hono-bindings"; -import { health } from "./health"; -import { webhook } from "./webhook"; - -export const routes = new Hono(); - -routes.route("/health", health); -routes.route("/webhook", webhook); diff --git a/apps/stripe/src/routes/webhook.ts b/apps/stripe/src/routes/webhook.ts deleted file mode 100644 index aa4419526c..0000000000 --- a/apps/stripe/src/routes/webhook.ts +++ /dev/null @@ -1,64 +0,0 @@ -import * as Sentry from "@sentry/bun"; -import { Hono } from "hono"; - -import { syncBillingBridge } from "../billing-bridge"; -import { env } from "../env"; -import type { AppBindings } from "../hono-bindings"; -import { stripeSync } from "../integration/stripe-sync"; - -export const webhook = new Hono(); - -webhook.post("/stripe", async (c) => { - const stripeEvent = c.get("stripeEvent"); - const rawBody = c.get("stripeRawBody"); - const signature = c.get("stripeSignature"); - - try { - await stripeSync.processWebhook(rawBody, signature); - } catch (error) { - if (env.NODE_ENV !== "production") { - console.error(error); - } else { - if ( - error instanceof Error && - error.message === "Unhandled webhook event" - ) { - Sentry.captureMessage( - `Unhandled Stripe webhook event: ${stripeEvent.type}`, - { - level: "warning", - tags: { - webhook: "stripe", - event_type: stripeEvent.type, - }, - extra: { - api_version: stripeEvent.api_version, - }, - }, - ); - } else { - Sentry.captureException(error, { - tags: { - webhook: "stripe", - event_type: stripeEvent.type, - }, - extra: { - api_version: stripeEvent.api_version, - }, - }); - return c.json({ error: "stripe_sync_failed" }, 500); - } - } - } - - try { - await syncBillingBridge(stripeEvent); - } catch (error) { - Sentry.captureException(error, { - tags: { webhook: "stripe", event_type: stripeEvent.type }, - }); - return c.json({ error: "billing_bridge_sync_failed" }, 500); - } - - return c.json({ ok: true }, 200); -}); diff --git a/apps/stripe/src/scripts/stripe-backfill-entitlements.ts b/apps/stripe/src/scripts/stripe-backfill-entitlements.ts deleted file mode 100644 index a09d6e075f..0000000000 --- a/apps/stripe/src/scripts/stripe-backfill-entitlements.ts +++ /dev/null @@ -1,205 +0,0 @@ -// Backfill entitlements for customers who subscribed before entitlements were properly set up. -// Unlike stripe-sync-entitlements.ts which relies on Stripe's entitlements API, -// this script manually calculates entitlements based on subscription state. -// -// Stripe's entitlements API only updates at subscription creation or billing cycle, -// so past customers need this backfill. -import { Effect, Schedule } from "effect"; -import pg from "pg"; -import { parseArgs } from "util"; - -const { values } = parseArgs({ - args: Bun.argv.slice(2), - options: { - "dry-run": { - type: "boolean", - default: false, - }, - }, - strict: true, - allowPositionals: false, -}); - -const dryRun = values["dry-run"] ?? false; - -const { DATABASE_URL } = Bun.env; - -if (!DATABASE_URL) { - throw new Error("Missing required DATABASE_URL environment variable"); -} - -const pool = new pg.Pool({ connectionString: DATABASE_URL }); - -const PRODUCT_ID = "prod_SHWUtH1i2DPvSD"; -const ENTITLEMENT_LOOKUP_KEY = "hyprnote_pro"; - -class DbError { - readonly _tag = "DbError"; - constructor(readonly message: string) {} -} - -const retryPolicy = Schedule.exponential("100 millis").pipe( - Schedule.jittered, - Schedule.intersect(Schedule.recurs(3)), -); - -const fetchCustomersWithActiveSubscriptions = Effect.tryPromise({ - try: () => - pool.query<{ customer: string }>( - `SELECT DISTINCT s.customer - FROM stripe.subscriptions s - JOIN stripe.subscription_items si ON si.subscription = s.id - JOIN stripe.prices p ON si.price = p.id - WHERE s.status IN ('active', 'trialing', 'past_due') - AND p.product = $1 - AND s.customer IS NOT NULL`, - [PRODUCT_ID], - ), - catch: (e) => - new DbError( - `Failed to fetch subscriptions: ${e instanceof Error ? e.message : String(e)}`, - ), -}).pipe(Effect.map((result) => new Set(result.rows.map((r) => r.customer)))); - -const fetchCustomersWithEntitlements = Effect.tryPromise({ - try: () => - pool.query<{ customer: string }>( - `SELECT DISTINCT customer FROM stripe.active_entitlements WHERE lookup_key = $1`, - [ENTITLEMENT_LOOKUP_KEY], - ), - catch: (e) => - new DbError( - `Failed to fetch entitlements: ${e instanceof Error ? e.message : String(e)}`, - ), -}).pipe(Effect.map((result) => new Set(result.rows.map((r) => r.customer)))); - -const createEntitlement = (customerId: string) => - Effect.tryPromise({ - try: () => - pool.query( - `INSERT INTO stripe.active_entitlements (id, object, livemode, feature, customer, lookup_key, last_synced_at) - VALUES ($1, $2, $3, $4, $5, $6, $7) - ON CONFLICT (customer, lookup_key) DO UPDATE SET - last_synced_at = EXCLUDED.last_synced_at`, - [ - `backfill_${customerId}_${ENTITLEMENT_LOOKUP_KEY}`, - "entitlements.active_entitlement", - true, - null, - customerId, - ENTITLEMENT_LOOKUP_KEY, - new Date().toISOString(), - ], - ), - catch: (e) => new DbError(e instanceof Error ? e.message : String(e)), - }).pipe( - Effect.retry(retryPolicy), - Effect.map(() => true), - Effect.catchAll((e) => - Effect.gen(function* () { - yield* Effect.logError( - `Failed to create entitlement for ${customerId}: ${e.message}`, - ); - return false; - }), - ), - ); - -const deleteEntitlement = (customerId: string) => - Effect.tryPromise({ - try: () => - pool.query( - `DELETE FROM stripe.active_entitlements WHERE customer = $1 AND lookup_key = $2`, - [customerId, ENTITLEMENT_LOOKUP_KEY], - ), - catch: (e) => new DbError(e instanceof Error ? e.message : String(e)), - }).pipe( - Effect.retry(retryPolicy), - Effect.map(() => true), - Effect.catchAll((e) => - Effect.gen(function* () { - yield* Effect.logError( - `Failed to delete entitlement for ${customerId}: ${e.message}`, - ); - return false; - }), - ), - ); - -const program = Effect.gen(function* () { - yield* Effect.log( - `Starting entitlement backfill${dryRun ? " (DRY RUN)" : ""}...`, - ); - yield* Effect.log(`Product ID: ${PRODUCT_ID}`); - yield* Effect.log(`Entitlement lookup_key: ${ENTITLEMENT_LOOKUP_KEY}`); - - const [activeCustomers, entitledCustomers] = yield* Effect.all([ - fetchCustomersWithActiveSubscriptions, - fetchCustomersWithEntitlements, - ]); - - yield* Effect.log( - `Found ${activeCustomers.size} customers with active subscriptions`, - ); - yield* Effect.log( - `Found ${entitledCustomers.size} customers with existing entitlements`, - ); - - const toAdd = [...activeCustomers].filter((c) => !entitledCustomers.has(c)); - const toRemove = [...entitledCustomers].filter( - (c) => !activeCustomers.has(c), - ); - - yield* Effect.log(`Customers needing entitlements added: ${toAdd.length}`); - yield* Effect.log( - `Customers needing entitlements removed: ${toRemove.length}`, - ); - - if (dryRun) { - if (toAdd.length > 0) { - yield* Effect.log(`Would add entitlements for: ${toAdd.join(", ")}`); - } - if (toRemove.length > 0) { - yield* Effect.log( - `Would remove entitlements for: ${toRemove.join(", ")}`, - ); - } - yield* Effect.log("Dry run complete - no changes made"); - return; - } - - let added = 0; - let removed = 0; - let errors = 0; - - for (const customerId of toAdd) { - const success = yield* createEntitlement(customerId); - if (success) { - added++; - } else { - errors++; - } - } - - for (const customerId of toRemove) { - const success = yield* deleteEntitlement(customerId); - if (success) { - removed++; - } else { - errors++; - } - } - - yield* Effect.log( - `Backfill complete: added=${added}, removed=${removed}, errors=${errors}`, - ); -}); - -Effect.runPromise(program) - .catch((error) => { - console.error("Fatal error:", error); - process.exit(1); - }) - .finally(() => { - pool.end(); - }); diff --git a/apps/stripe/src/scripts/stripe-backfill-features.ts b/apps/stripe/src/scripts/stripe-backfill-features.ts deleted file mode 100644 index fa004a8836..0000000000 --- a/apps/stripe/src/scripts/stripe-backfill-features.ts +++ /dev/null @@ -1,148 +0,0 @@ -// Sync features from Stripe's entitlements API to the local database. -// Features define what entitlements can be granted to customers. -// https://docs.stripe.com/api/entitlements/feature/list -import { Effect, Schedule } from "effect"; -import pg from "pg"; -import Stripe from "stripe"; -import { parseArgs } from "util"; - -import { STRIPE_API_VERSION } from "../integration/stripe"; - -const { values } = parseArgs({ - args: Bun.argv.slice(2), - options: { - "dry-run": { - type: "boolean", - default: false, - }, - }, - strict: true, - allowPositionals: false, -}); - -const dryRun = values["dry-run"] ?? false; - -const { STRIPE_SECRET_KEY, DATABASE_URL } = Bun.env; - -if (!STRIPE_SECRET_KEY || !DATABASE_URL) { - throw new Error( - "Missing required STRIPE_SECRET_KEY or DATABASE_URL environment variables", - ); -} - -const pool = new pg.Pool({ connectionString: DATABASE_URL }); -const stripe = new Stripe(STRIPE_SECRET_KEY, { - apiVersion: STRIPE_API_VERSION, -}); - -class DbError { - readonly _tag = "DbError"; - constructor(readonly message: string) {} -} - -const isRateLimitError = (error: unknown): boolean => - error instanceof Stripe.errors.StripeError && error.code === "rate_limit"; - -const retryPolicy = Schedule.exponential("500 millis").pipe( - Schedule.jittered, - Schedule.whileInput(isRateLimitError), - Schedule.intersect(Schedule.recurs(5)), -); - -const fetchFeaturesFromStripe = Effect.tryPromise({ - try: async () => { - const features: Stripe.Entitlements.Feature[] = []; - for await (const feature of stripe.entitlements.features.list()) { - features.push(feature); - } - return features; - }, - catch: (error) => error, -}).pipe(Effect.retry(retryPolicy)); - -const upsertFeature = (feature: Stripe.Entitlements.Feature) => - Effect.tryPromise({ - try: () => - pool.query( - `INSERT INTO stripe.features (id, object, livemode, name, lookup_key, active, metadata, last_synced_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) - ON CONFLICT (id) DO UPDATE SET - object = EXCLUDED.object, - livemode = EXCLUDED.livemode, - name = EXCLUDED.name, - lookup_key = EXCLUDED.lookup_key, - active = EXCLUDED.active, - metadata = EXCLUDED.metadata, - last_synced_at = EXCLUDED.last_synced_at`, - [ - feature.id, - feature.object, - feature.livemode, - feature.name, - feature.lookup_key, - feature.active, - JSON.stringify(feature.metadata || {}), - new Date().toISOString(), - ], - ), - catch: (e) => new DbError(e instanceof Error ? e.message : String(e)), - }).pipe( - Effect.map(() => true), - Effect.catchAll((e) => - Effect.gen(function* () { - yield* Effect.logError( - `Failed to upsert feature ${feature.id}: ${e.message}`, - ); - return false; - }), - ), - ); - -const program = Effect.gen(function* () { - yield* Effect.log( - `Starting Stripe features sync${dryRun ? " (DRY RUN)" : ""}...`, - ); - - const features = yield* fetchFeaturesFromStripe; - - yield* Effect.log(`Found ${features.length} features in Stripe`); - - if (features.length === 0) { - yield* Effect.log("No features found in Stripe. Nothing to sync."); - return; - } - - for (const feature of features) { - yield* Effect.log( - ` - ${feature.lookup_key}: ${feature.name} (active: ${feature.active})`, - ); - } - - if (dryRun) { - yield* Effect.log("Dry run complete - no changes made"); - return; - } - - let synced = 0; - let errors = 0; - - for (const feature of features) { - const success = yield* upsertFeature(feature); - if (success) { - synced++; - } else { - errors++; - } - } - - yield* Effect.log(`Sync complete: synced=${synced}, errors=${errors}`); -}); - -Effect.runPromise(program) - .catch((error) => { - console.error("Fatal error:", error); - process.exit(1); - }) - .finally(() => { - pool.end(); - }); diff --git a/apps/stripe/src/scripts/stripe-backfill.ts b/apps/stripe/src/scripts/stripe-backfill.ts deleted file mode 100644 index cd81872d43..0000000000 --- a/apps/stripe/src/scripts/stripe-backfill.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { StripeSync } from "@supabase/stripe-sync-engine"; -import { parseArgs } from "util"; - -import { STRIPE_API_VERSION } from "../integration/stripe"; - -const { DATABASE_URL, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET } = Bun.env; - -if (!DATABASE_URL || !STRIPE_SECRET_KEY || !STRIPE_WEBHOOK_SECRET) { - throw new Error("Missing required environment variables"); -} - -const { values } = parseArgs({ - args: Bun.argv.slice(2), - options: { - "created-gte": { - type: "string", - }, - "created-lte": { - type: "string", - }, - help: { - type: "boolean", - short: "h", - }, - }, - strict: true, - allowPositionals: false, -}); - -if (values.help) { - console.log(` -Usage: bun stripe-backfill.ts [options] - -Options: - --created-gte Only sync objects created at or after this unix timestamp - --created-lte Only sync objects created at or before this unix timestamp - -h, --help Show this help message - -Examples: - bun stripe-backfill.ts - bun stripe-backfill.ts --created-gte 1704067200 - bun stripe-backfill.ts --created-gte 1704067200 --created-lte 1706745600 -`); - process.exit(0); -} - -const createdGte = values["created-gte"] - ? parseInt(values["created-gte"], 10) - : undefined; -const createdLte = values["created-lte"] - ? parseInt(values["created-lte"], 10) - : undefined; - -if (values["created-gte"] && isNaN(createdGte!)) { - throw new Error( - "Invalid --created-gte value: must be a valid unix timestamp", - ); -} - -if (values["created-lte"] && isNaN(createdLte!)) { - throw new Error( - "Invalid --created-lte value: must be a valid unix timestamp", - ); -} - -const created: { gte?: number; lte?: number } | undefined = - createdGte !== undefined || createdLte !== undefined - ? { - ...(createdGte !== undefined && { gte: createdGte }), - ...(createdLte !== undefined && { lte: createdLte }), - } - : undefined; - -const sync = new StripeSync({ - poolConfig: { - connectionString: DATABASE_URL, - max: 10, - }, - schema: "stripe", - stripeSecretKey: STRIPE_SECRET_KEY, - stripeWebhookSecret: STRIPE_WEBHOOK_SECRET, - autoExpandLists: true, - stripeApiVersion: STRIPE_API_VERSION, - backfillRelatedEntities: true, -}); - -if (created) { - console.log( - `Starting Stripe backfill with date filter: ${JSON.stringify(created)}`, - ); -} else { - console.log("Starting Stripe backfill (all objects)..."); -} - -await sync.syncBackfill({ object: "all", created }); -console.log("Backfill complete."); diff --git a/apps/stripe/src/scripts/stripe-migrate-legacy-pro-prices.ts b/apps/stripe/src/scripts/stripe-migrate-legacy-pro-prices.ts deleted file mode 100644 index ac83a36a41..0000000000 --- a/apps/stripe/src/scripts/stripe-migrate-legacy-pro-prices.ts +++ /dev/null @@ -1,567 +0,0 @@ -import Stripe from "stripe"; -import { parseArgs } from "util"; - -const HARDCODED_OLD_MONTHLY_PRICE_ID = "price_1RsWbzEABq1oJeLy4hfpEFJT"; -const HARDCODED_OLD_YEARLY_PRICE_ID = "price_1RsuVFEABq1oJeLy6mPncvSp"; -const HARDCODED_NEW_MONTHLY_PRICE_ID = "price_1T2Z8ZEABq1oJeLyqbCPC7cl"; -const HARDCODED_NEW_YEARLY_PRICE_ID = "price_1T2Z8IEABq1oJeLyNN5InKs4"; -const STRIPE_API_VERSION = "2026-02-25.clover"; - -const { values } = parseArgs({ - args: Bun.argv.slice(2), - options: { - apply: { - type: "boolean", - }, - "allow-multi-item-subscriptions": { - type: "boolean", - }, - "allow-pending-updates": { - type: "boolean", - }, - "allow-scheduled-subscriptions": { - type: "boolean", - }, - help: { - type: "boolean", - short: "h", - }, - "include-cancel-at-period-end": { - type: "boolean", - }, - limit: { - type: "string", - }, - "new-monthly-price": { - type: "string", - }, - "new-yearly-price": { - type: "string", - }, - "old-monthly-price": { - type: "string", - }, - "old-yearly-price": { - type: "string", - }, - statuses: { - type: "string", - default: "active,trialing,past_due", - }, - subscription: { - type: "string", - }, - }, - strict: true, - allowPositionals: false, -}); - -if (values.help) { - console.log(` -Usage: bun stripe-migrate-legacy-pro-prices.ts [options] - -Migrates the legacy Char Pro prices: - - $8/month -> $25/month - - $59/year -> $250/year - -The script updates matching subscription items with proration_behavior=none, so -same-interval subscriptions keep their current renewal date and pick up the new -price on the next renewal. - -Options: - --old-monthly-price Override the baked-in legacy $8/month price ID - --old-yearly-price Override the baked-in legacy $59/year price ID - --new-monthly-price Override the baked-in $25/month price ID - --new-yearly-price Override the baked-in $250/year price ID - --statuses Subscription statuses to include - Default: active,trialing,past_due - --limit Only process the first n matches - --subscription Restrict to a single subscription - --include-cancel-at-period-end Include subscriptions already set to cancel - --allow-scheduled-subscriptions Include subscriptions with schedules attached - --allow-pending-updates Include subscriptions with pending updates - --allow-multi-item-subscriptions Include subscriptions with more than one item - --apply Apply updates. Without this, dry-run only - -h, --help Show this help message - -Examples: - bun stripe-migrate-legacy-pro-prices.ts - - bun stripe-migrate-legacy-pro-prices.ts \\ - --subscription sub_123 \\ - --apply -`); - process.exit(0); -} - -const STRIPE_SECRET_KEY = Bun.env.STRIPE_SECRET_KEY; - -if (!STRIPE_SECRET_KEY) { - throw new Error("Missing required environment variable STRIPE_SECRET_KEY"); -} - -const oldMonthlyPriceId = - values["old-monthly-price"] ?? HARDCODED_OLD_MONTHLY_PRICE_ID; -const oldYearlyPriceId = - values["old-yearly-price"] ?? HARDCODED_OLD_YEARLY_PRICE_ID; -const newMonthlyPriceId = - values["new-monthly-price"] ?? HARDCODED_NEW_MONTHLY_PRICE_ID; -const newYearlyPriceId = - values["new-yearly-price"] ?? HARDCODED_NEW_YEARLY_PRICE_ID; - -const limit = parsePositiveInteger(values.limit, "--limit"); -const subscriptionFilter = values.subscription ?? null; -const shouldApply = values.apply ?? false; -const includeCancelAtPeriodEnd = - values["include-cancel-at-period-end"] ?? false; -const allowScheduledSubscriptions = - values["allow-scheduled-subscriptions"] ?? false; -const allowPendingUpdates = values["allow-pending-updates"] ?? false; -const allowMultiItemSubscriptions = - values["allow-multi-item-subscriptions"] ?? false; - -const allowedStatuses = parseStatuses(values.statuses); - -const stripe = new Stripe(STRIPE_SECRET_KEY, { - apiVersion: STRIPE_API_VERSION, - maxNetworkRetries: 2, -}); - -const PRICE_RULES = [ - { - key: "monthly", - interval: "month", - oldAmount: 800, - newAmount: 2500, - oldPriceId: oldMonthlyPriceId, - newPriceId: newMonthlyPriceId, - }, - { - key: "yearly", - interval: "year", - oldAmount: 5900, - newAmount: 25000, - oldPriceId: oldYearlyPriceId, - newPriceId: newYearlyPriceId, - }, -] as const; - -type Rule = (typeof PRICE_RULES)[number]; -type SkipReason = - | "status" - | "cancel_at_period_end" - | "scheduled" - | "pending_update" - | "multi_item_subscription" - | "matching_item_count" - | "subscription_filter"; - -type Candidate = { - ruleKey: Rule["key"]; - subscriptionId: string; - subscriptionItemId: string; - customerId: string | null; - status: Stripe.Subscription.Status; - quantity: number; - currentPeriodEnd: number; - cancelAtPeriodEnd: boolean; - targetPriceId: string; -}; - -type RuleStats = { - scanned: number; - matched: number; - skipped: Record; -}; - -const livemode = await validateConfiguredPrices(PRICE_RULES); - -console.log( - `${shouldApply ? "Applying" : "Dry run"} legacy price migration in ${ - livemode ? "live" : "test" - } mode`, -); -console.log( - `Statuses: ${Array.from(allowedStatuses).join(", ")} | include_cancel_at_period_end=${includeCancelAtPeriodEnd} | allow_scheduled=${allowScheduledSubscriptions} | allow_pending_updates=${allowPendingUpdates} | allow_multi_item=${allowMultiItemSubscriptions}`, -); -if (subscriptionFilter) { - console.log(`Subscription filter: ${subscriptionFilter}`); -} -if (limit !== null) { - console.log(`Limit: ${limit}`); -} - -const allCandidates: Candidate[] = []; -const statsByRule = new Map(); - -let remainingLimit = limit; -for (const rule of PRICE_RULES) { - const { candidates, stats } = await collectCandidatesForRule( - rule, - remainingLimit, - ); - allCandidates.push(...candidates); - statsByRule.set(rule.key, stats); - - if (remainingLimit !== null) { - remainingLimit -= candidates.length; - if (remainingLimit <= 0) { - break; - } - } -} - -allCandidates.sort((a, b) => { - if (a.currentPeriodEnd !== b.currentPeriodEnd) { - return a.currentPeriodEnd - b.currentPeriodEnd; - } - - return a.subscriptionId.localeCompare(b.subscriptionId); -}); - -for (const rule of PRICE_RULES) { - const stats = statsByRule.get(rule.key) ?? emptyStats(); - console.log( - `[${rule.key}] scanned=${stats.scanned} matched=${stats.matched} skipped=${formatSkipped(stats.skipped)}`, - ); -} - -console.log(`Total candidates: ${allCandidates.length}`); - -if (allCandidates.length > 0) { - console.table( - allCandidates.slice(0, 20).map((candidate) => ({ - rule: candidate.ruleKey, - subscription: candidate.subscriptionId, - item: candidate.subscriptionItemId, - customer: candidate.customerId ?? "n/a", - status: candidate.status, - quantity: candidate.quantity, - renews_at: unixToIso(candidate.currentPeriodEnd), - cancel_at_period_end: candidate.cancelAtPeriodEnd, - })), - ); -} - -if (allCandidates.length > 20) { - console.log( - `... ${allCandidates.length - 20} additional candidates not shown`, - ); -} - -if (!shouldApply) { - console.log("Dry run complete. Re-run with --apply to perform updates."); - process.exit(0); -} - -let successCount = 0; -const failures: Array<{ - subscriptionId: string; - itemId: string; - error: string; -}> = []; - -for (const [index, candidate] of allCandidates.entries()) { - console.log( - `[${index + 1}/${allCandidates.length}] ${candidate.ruleKey} ${candidate.subscriptionId} -> ${candidate.targetPriceId}`, - ); - - try { - const updated = await stripe.subscriptionItems.update( - candidate.subscriptionItemId, - { - price: candidate.targetPriceId, - proration_behavior: "none", - quantity: candidate.quantity, - }, - { - idempotencyKey: `legacy-price-migration:${candidate.subscriptionItemId}:${candidate.targetPriceId}`, - }, - ); - - if (updated.price.id !== candidate.targetPriceId) { - throw new Error( - `Updated item ${updated.id} but Stripe returned price ${updated.price.id}`, - ); - } - - successCount++; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - failures.push({ - subscriptionId: candidate.subscriptionId, - itemId: candidate.subscriptionItemId, - error: message, - }); - console.error( - `Failed to update ${candidate.subscriptionId}/${candidate.subscriptionItemId}: ${message}`, - ); - } -} - -console.log( - `Migration complete: updated=${successCount} failed=${failures.length} total=${allCandidates.length}`, -); - -if (failures.length > 0) { - console.table(failures); - process.exit(1); -} - -function parsePositiveInteger( - value: string | undefined, - label: string, -): number | null { - if (!value) { - return null; - } - - const parsed = Number.parseInt(value, 10); - if (!Number.isInteger(parsed) || parsed <= 0) { - throw new Error(`${label} must be a positive integer`); - } - - return parsed; -} - -function parseStatuses(value: string): Set { - const knownStatuses = new Set([ - "incomplete", - "incomplete_expired", - "trialing", - "active", - "past_due", - "canceled", - "unpaid", - "paused", - ]); - - const statuses = value - .split(",") - .map((status) => status.trim()) - .filter(Boolean); - - if (statuses.length === 0) { - throw new Error("--statuses must include at least one subscription status"); - } - - for (const status of statuses) { - if (!knownStatuses.has(status)) { - throw new Error(`Unknown subscription status in --statuses: ${status}`); - } - } - - return new Set(statuses as Stripe.Subscription.Status[]); -} - -async function validateConfiguredPrices( - rules: readonly Rule[], -): Promise { - const prices = await Promise.all( - rules.flatMap((rule) => [ - stripe.prices.retrieve(rule.oldPriceId), - stripe.prices.retrieve(rule.newPriceId), - ]), - ); - - const livemodeSet = new Set(prices.map((price) => price.livemode)); - if (livemodeSet.size !== 1) { - throw new Error("Configured prices mix live and test mode Stripe objects"); - } - - const productIds = new Set(prices.map((price) => price.product)); - if (productIds.size !== 1) { - throw new Error( - "Configured prices do not all belong to the same Stripe product", - ); - } - - for (const rule of rules) { - const oldPrice = prices.find((price) => price.id === rule.oldPriceId); - const newPrice = prices.find((price) => price.id === rule.newPriceId); - - if (!oldPrice || !newPrice) { - throw new Error(`Failed to load prices for ${rule.key}`); - } - - assertPrice(oldPrice, { - amount: rule.oldAmount, - interval: rule.interval, - label: `${rule.key} old price`, - requireActive: false, - }); - assertPrice(newPrice, { - amount: rule.newAmount, - interval: rule.interval, - label: `${rule.key} new price`, - requireActive: true, - }); - - console.log( - `[${rule.key}] ${formatPrice(oldPrice)} -> ${formatPrice(newPrice)}`, - ); - } - - return prices[0]?.livemode ?? false; -} - -function assertPrice( - price: Stripe.Price, - expected: { - amount: number; - interval: "month" | "year"; - label: string; - requireActive: boolean; - }, -) { - if (!price.recurring) { - throw new Error(`${expected.label} must be a recurring Stripe price`); - } - - if (price.currency !== "usd") { - throw new Error(`${expected.label} must be USD`); - } - - if (price.unit_amount !== expected.amount) { - throw new Error( - `${expected.label} must be ${formatUnitAmount(expected.amount)}, got ${formatUnitAmount(price.unit_amount)}`, - ); - } - - if (price.recurring.interval !== expected.interval) { - throw new Error( - `${expected.label} must recur every ${expected.interval}, got ${price.recurring.interval}`, - ); - } - - if (price.recurring.interval_count !== 1) { - throw new Error(`${expected.label} must have interval_count=1`); - } - - if (price.recurring.usage_type !== "licensed") { - throw new Error(`${expected.label} must use licensed billing`); - } - - if (expected.requireActive && !price.active) { - throw new Error(`${expected.label} must be active`); - } -} - -async function collectCandidatesForRule( - rule: Rule, - remainingLimit: number | null, -): Promise<{ candidates: Candidate[]; stats: RuleStats }> { - const candidates: Candidate[] = []; - const stats = emptyStats(); - - for await (const subscription of stripe.subscriptions.list({ - limit: 100, - price: rule.oldPriceId, - status: "all", - })) { - stats.scanned++; - - if (subscriptionFilter && subscription.id !== subscriptionFilter) { - stats.skipped.subscription_filter++; - continue; - } - - if (!allowedStatuses.has(subscription.status)) { - stats.skipped.status++; - continue; - } - - if (!includeCancelAtPeriodEnd && subscription.cancel_at_period_end) { - stats.skipped.cancel_at_period_end++; - continue; - } - - if (!allowScheduledSubscriptions && subscription.schedule) { - stats.skipped.scheduled++; - continue; - } - - if (!allowPendingUpdates && subscription.pending_update) { - stats.skipped.pending_update++; - continue; - } - - const matchingItems = subscription.items.data.filter( - (item) => item.price.id === rule.oldPriceId, - ); - - if (matchingItems.length !== 1) { - stats.skipped.matching_item_count++; - continue; - } - - if (!allowMultiItemSubscriptions && subscription.items.data.length !== 1) { - stats.skipped.multi_item_subscription++; - continue; - } - - const item = matchingItems[0]; - candidates.push({ - ruleKey: rule.key, - subscriptionId: subscription.id, - subscriptionItemId: item.id, - customerId: - typeof subscription.customer === "string" - ? subscription.customer - : (subscription.customer?.id ?? null), - status: subscription.status, - quantity: item.quantity ?? 1, - currentPeriodEnd: item.current_period_end, - cancelAtPeriodEnd: subscription.cancel_at_period_end, - targetPriceId: rule.newPriceId, - }); - stats.matched++; - - if (remainingLimit !== null && candidates.length >= remainingLimit) { - break; - } - } - - return { candidates, stats }; -} - -function emptyStats(): RuleStats { - return { - scanned: 0, - matched: 0, - skipped: { - status: 0, - cancel_at_period_end: 0, - scheduled: 0, - pending_update: 0, - multi_item_subscription: 0, - matching_item_count: 0, - subscription_filter: 0, - }, - }; -} - -function formatSkipped(skipped: RuleStats["skipped"]): string { - return ( - Object.entries(skipped) - .filter(([, count]) => count > 0) - .map(([reason, count]) => `${reason}=${count}`) - .join(", ") || "none" - ); -} - -function formatPrice(price: Stripe.Price): string { - return `${price.id} (${formatUnitAmount(price.unit_amount)} / ${price.recurring?.interval ?? "n/a"}, active=${price.active}, livemode=${price.livemode})`; -} - -function formatUnitAmount(amount: number | null): string { - if (amount === null) { - return "unknown"; - } - - return `$${(amount / 100).toFixed(2)}`; -} - -function unixToIso(timestamp: number): string { - return new Date(timestamp * 1000).toISOString(); -} diff --git a/apps/stripe/src/scripts/stripe-sync-entitlements.ts b/apps/stripe/src/scripts/stripe-sync-entitlements.ts deleted file mode 100644 index 5eb88c106e..0000000000 --- a/apps/stripe/src/scripts/stripe-sync-entitlements.ts +++ /dev/null @@ -1,317 +0,0 @@ -// https://github.com/supabase/stripe-sync-engine/blob/main/packages/sync-engine/README.md#syncing-a-single-entity -// Entitlements can not be synced with "stripe-sync-engine". So we need this script. -// -// Syncs entitlements for customers that are "worth looking at": -// 1. Customers with active/trialing/past_due subscriptions (should have entitlements) -// 2. Customers with existing entitlements (might need updates or cleanup) -// -// This handles both backfill (pre-webhook customers) and daily verification. -import { Effect, Schedule } from "effect"; -import pg from "pg"; -import Stripe from "stripe"; -import { parseArgs } from "util"; - -import { STRIPE_API_VERSION } from "../integration/stripe"; - -const { values } = parseArgs({ - args: Bun.argv.slice(2), - options: { - "skip-recent-hours": { - type: "string", - default: "6", - }, - }, - strict: true, - allowPositionals: false, -}); - -const skipRecentHours = parseInt(values["skip-recent-hours"] ?? "6", 10); - -const { STRIPE_SECRET_KEY, DATABASE_URL } = Bun.env; - -if (!STRIPE_SECRET_KEY || !DATABASE_URL) { - throw new Error("Missing required environment variables"); -} - -const pool = new pg.Pool({ connectionString: DATABASE_URL }); -const stripe = new Stripe(STRIPE_SECRET_KEY, { - apiVersion: STRIPE_API_VERSION, -}); - -const isRateLimitError = (error: unknown): boolean => - error instanceof Stripe.errors.StripeError && error.code === "rate_limit"; - -const retryPolicy = Schedule.exponential("500 millis").pipe( - Schedule.jittered, - Schedule.whileInput(isRateLimitError), - Schedule.intersect(Schedule.recurs(5)), -); - -class DbError { - readonly _tag = "DbError"; - constructor(readonly message: string) {} -} - -const fetchRecentlySyncedCustomers = (hours: number) => - Effect.gen(function* () { - if (hours <= 0) return new Set(); - - const cutoff = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString(); - - const result = yield* Effect.tryPromise({ - try: () => - pool.query<{ id: string }>( - `SELECT id FROM stripe.customers WHERE last_synced_at >= $1`, - [cutoff], - ), - catch: (e) => new DbError(e instanceof Error ? e.message : String(e)), - }).pipe( - Effect.catchAll((e) => - Effect.gen(function* () { - yield* Effect.logWarning( - `Failed to fetch recently synced customers: ${e.message}`, - ); - return { rows: [] as { id: string }[] }; - }), - ), - ); - - return new Set(result.rows.map((c) => c.id)); - }); - -const fetchCustomersToSync = Effect.gen(function* () { - const [subscriptionsResult, entitlementsResult, recentlySynced] = - yield* Effect.all([ - Effect.tryPromise({ - try: () => - pool.query<{ customer: string }>( - `SELECT customer FROM stripe.subscriptions WHERE status IN ('active', 'trialing', 'past_due')`, - ), - catch: (e) => - new DbError( - `Failed to fetch subscriptions: ${e instanceof Error ? e.message : String(e)}`, - ), - }), - Effect.tryPromise({ - try: () => - pool.query<{ customer: string }>( - `SELECT customer FROM stripe.active_entitlements`, - ), - catch: (e) => - new DbError( - `Failed to fetch existing entitlements: ${e instanceof Error ? e.message : String(e)}`, - ), - }), - fetchRecentlySyncedCustomers(skipRecentHours), - ]); - - const uniqueIds = new Set([ - ...subscriptionsResult.rows.map((s) => s.customer).filter(Boolean), - ...entitlementsResult.rows.map((e) => e.customer).filter(Boolean), - ]); - - const filtered = Array.from(uniqueIds).filter( - (id) => !recentlySynced.has(id), - ); - const skipped = uniqueIds.size - filtered.length; - - if (skipped > 0) { - yield* Effect.log( - `Skipping ${skipped} customers synced within the last ${skipRecentHours} hours`, - ); - } - - return filtered; -}); - -const fetchCustomerEntitlements = (customerId: string) => - Effect.tryPromise({ - try: async () => { - const entitlements: Stripe.Entitlements.ActiveEntitlement[] = []; - for await (const entitlement of stripe.entitlements.activeEntitlements.list( - { - customer: customerId, - }, - )) { - entitlements.push(entitlement); - } - return entitlements; - }, - catch: (error) => error, - }).pipe(Effect.retry(retryPolicy)); - -const deleteAllEntitlements = (customerId: string) => - Effect.tryPromise({ - try: () => - pool.query(`DELETE FROM stripe.active_entitlements WHERE customer = $1`, [ - customerId, - ]), - catch: (e) => new DbError(e instanceof Error ? e.message : String(e)), - }).pipe( - Effect.map((result) => ({ - updated: 0, - deleted: result.rowCount ?? 0, - hasError: false, - })), - Effect.catchAll((e) => - Effect.gen(function* () { - yield* Effect.logError( - `Failed to delete entitlements for ${customerId}: ${e.message}`, - ); - return { updated: 0, deleted: 0, hasError: true }; - }), - ), - ); - -const syncEntitlements = ( - customerId: string, - entitlements: Stripe.Entitlements.ActiveEntitlement[], -) => - Effect.gen(function* () { - const activeLookupKeys = entitlements.map((e) => e.lookup_key); - - const deleteResult = yield* Effect.tryPromise({ - try: () => - pool.query( - `DELETE FROM stripe.active_entitlements WHERE customer = $1 AND lookup_key != ALL($2)`, - [customerId, activeLookupKeys], - ), - catch: (e) => new DbError(e instanceof Error ? e.message : String(e)), - }).pipe( - Effect.catchAll((e) => - Effect.gen(function* () { - yield* Effect.logError( - `Failed to delete stale entitlements for ${customerId}: ${e.message}`, - ); - return null; - }), - ), - ); - - if (deleteResult === null) { - return { updated: 0, deleted: 0, hasError: true }; - } - - const deleteCount = deleteResult.rowCount ?? 0; - - for (const entitlement of entitlements) { - const upsertResult = yield* Effect.tryPromise({ - try: () => - pool.query( - `INSERT INTO stripe.active_entitlements (id, object, livemode, feature, customer, lookup_key, last_synced_at) - VALUES ($1, $2, $3, $4, $5, $6, $7) - ON CONFLICT (customer, lookup_key) DO UPDATE SET - id = EXCLUDED.id, - object = EXCLUDED.object, - livemode = EXCLUDED.livemode, - feature = EXCLUDED.feature, - last_synced_at = EXCLUDED.last_synced_at`, - [ - entitlement.id, - entitlement.object, - entitlement.livemode, - entitlement.feature, - customerId, - entitlement.lookup_key, - new Date().toISOString(), - ], - ), - catch: (e) => new DbError(e instanceof Error ? e.message : String(e)), - }).pipe( - Effect.catchAll((e) => - Effect.gen(function* () { - yield* Effect.logError( - `Failed to upsert entitlement for ${customerId}: ${e.message}`, - ); - return null; - }), - ), - ); - - if (upsertResult === null) { - return { updated: 0, deleted: deleteCount, hasError: true }; - } - } - - return { - updated: entitlements.length, - deleted: deleteCount, - hasError: false, - }; - }); - -const updateCustomerLastSyncedAt = (customerId: string) => - Effect.tryPromise({ - try: () => - pool.query( - `UPDATE stripe.customers SET last_synced_at = $1 WHERE id = $2`, - [new Date().toISOString(), customerId], - ), - catch: (e) => new DbError(e instanceof Error ? e.message : String(e)), - }).pipe(Effect.catchAll(() => Effect.void)); - -const processCustomer = (customerId: string) => - Effect.gen(function* () { - const entitlements = yield* fetchCustomerEntitlements(customerId); - - const result = - entitlements.length === 0 - ? yield* deleteAllEntitlements(customerId) - : yield* syncEntitlements(customerId, entitlements); - - if (!result.hasError) { - yield* updateCustomerLastSyncedAt(customerId); - } - - return result; - }).pipe( - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.logError( - `Failed to process customer ${customerId}: ${error}`, - ); - return { updated: 0, deleted: 0, hasError: true }; - }), - ), - ); - -const program = Effect.gen(function* () { - yield* Effect.log("Starting Stripe entitlements sync..."); - yield* Effect.log( - "Fetching customers with active subscriptions or existing entitlements...", - ); - - const customerIds = yield* fetchCustomersToSync; - - yield* Effect.log(`Found ${customerIds.length} customers to process`); - - let processed = 0; - let totalUpdated = 0; - let totalDeleted = 0; - let totalErrors = 0; - - for (const customerId of customerIds) { - const result = yield* processCustomer(customerId); - processed++; - totalUpdated += result.updated ?? 0; - totalDeleted += result.deleted; - if (result.hasError) totalErrors++; - - if (processed % 100 === 0) { - yield* Effect.log(`Progress: ${processed}/${customerIds.length}`); - } - } - - yield* Effect.log( - `Sync complete: processed=${processed}, updated=${totalUpdated}, deleted=${totalDeleted}, errors=${totalErrors}`, - ); -}); - -Effect.runPromise(program) - .catch((error) => { - console.error("Fatal error:", error); - process.exit(1); - }) - .finally(() => { - pool.end(); - }); diff --git a/apps/stripe/tsconfig.json b/apps/stripe/tsconfig.json deleted file mode 100644 index 44907d8c5f..0000000000 --- a/apps/stripe/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "include": ["src/**/*.ts"], - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "bundler", - "allowImportingTsExtensions": false, - "strict": true, - "skipLibCheck": true, - "noUncheckedSideEffectImports": true, - "types": ["@types/bun"] - } -} diff --git a/apps/unsigned-web/.gitignore b/apps/unsigned-web/.gitignore deleted file mode 100644 index 7378532443..0000000000 --- a/apps/unsigned-web/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -.astro -dist diff --git a/apps/unsigned-web/astro.config.mjs b/apps/unsigned-web/astro.config.mjs deleted file mode 100644 index f359475759..0000000000 --- a/apps/unsigned-web/astro.config.mjs +++ /dev/null @@ -1,11 +0,0 @@ -import mdx from "@astrojs/mdx"; -import tailwindcss from "@tailwindcss/vite"; -import { defineConfig } from "astro/config"; - -export default defineConfig({ - site: "https://unsigned.char.com", - integrations: [mdx()], - vite: { - plugins: [tailwindcss()], - }, -}); diff --git a/apps/unsigned-web/netlify.toml b/apps/unsigned-web/netlify.toml deleted file mode 100644 index e391f2c896..0000000000 --- a/apps/unsigned-web/netlify.toml +++ /dev/null @@ -1,12 +0,0 @@ -# Set base directory to "apps/unsigned-web" in Netlify UI -[build] -command = "pnpm -F @hypr/unsigned-web build" -publish = "apps/unsigned-web/dist" - -[build.environment] -NODE_VERSION = "22" - -[[redirects]] -from = "/*" -to = "/" -status = 200 diff --git a/apps/unsigned-web/package.json b/apps/unsigned-web/package.json deleted file mode 100644 index cb850f65e8..0000000000 --- a/apps/unsigned-web/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "@hypr/unsigned-web", - "version": "0.0.0", - "private": true, - "type": "module", - "scripts": { - "dev": "astro dev", - "build": "astro build", - "preview": "astro preview", - "typecheck": "astro check" - }, - "dependencies": { - "@astrojs/mdx": "^5.0.2", - "astro": "^6.0.8", - "astro-seo": "^1.1.0" - }, - "devDependencies": { - "@astrojs/check": "^0.9.8", - "@tailwindcss/vite": "^4.1.0", - "tailwindcss": "^4.1.0", - "typescript": "~5.6.3" - } -} diff --git a/apps/unsigned-web/public/favicon.png b/apps/unsigned-web/public/favicon.png deleted file mode 100644 index 6f79203c81..0000000000 Binary files a/apps/unsigned-web/public/favicon.png and /dev/null differ diff --git a/apps/unsigned-web/public/icon.svg b/apps/unsigned-web/public/icon.svg deleted file mode 100644 index a79d98f2b0..0000000000 --- a/apps/unsigned-web/public/icon.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/apps/unsigned-web/public/logo.svg b/apps/unsigned-web/public/logo.svg deleted file mode 100644 index abfa22159a..0000000000 --- a/apps/unsigned-web/public/logo.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/apps/unsigned-web/src/layouts/Base.astro b/apps/unsigned-web/src/layouts/Base.astro deleted file mode 100644 index 2547b858d1..0000000000 --- a/apps/unsigned-web/src/layouts/Base.astro +++ /dev/null @@ -1,44 +0,0 @@ ---- -import "../styles.css"; -import { SEO } from "astro-seo"; - -const title = "Unsigned Char"; -const description = - "Unsigned Char is a bot-free meeting note taker for macOS that runs fully on device. It's open source and free to use forever."; -const url = "https://unsigned.char.com"; ---- - - - - - - - - - - - - - - - - - - diff --git a/apps/unsigned-web/src/pages/index.mdx b/apps/unsigned-web/src/pages/index.mdx deleted file mode 100644 index a0949b184f..0000000000 --- a/apps/unsigned-web/src/pages/index.mdx +++ /dev/null @@ -1,39 +0,0 @@ ---- -layout: "../layouts/Base.astro" ---- - -
- - Unsigned Char - - - -
diff --git a/apps/unsigned-web/src/styles.css b/apps/unsigned-web/src/styles.css deleted file mode 100644 index 50b9d2eda2..0000000000 --- a/apps/unsigned-web/src/styles.css +++ /dev/null @@ -1,56 +0,0 @@ -@import "tailwindcss"; - -@theme { - --font-mono: "IBM Plex Mono", ui-monospace, "SFMono-Regular", monospace; - --font-serif: "Instrument Serif", Georgia, serif; -} - -@keyframes fade-up { - from { - opacity: 0; - transform: translateY(14px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.stagger-1 { - animation: fade-up 0.6s cubic-bezier(0.16, 1, 0.3, 1) 0s both; -} - -.stagger-2 { - animation: fade-up 0.6s cubic-bezier(0.16, 1, 0.3, 1) 0.15s both; -} - -.stagger-3 { - animation: fade-up 0.6s cubic-bezier(0.16, 1, 0.3, 1) 0.3s both; -} - -.stagger-4 { - animation: fade-up 0.6s cubic-bezier(0.16, 1, 0.3, 1) 0.5s both; -} - -::selection { - background: rgba(23, 23, 23, 0.14); - color: #171717; -} - -::-webkit-scrollbar { - width: 5px; - height: 5px; -} - -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: #2a2a2a; - border-radius: 3px; -} - -::-webkit-scrollbar-thumb:hover { - background: #404040; -} diff --git a/apps/unsigned-web/tsconfig.json b/apps/unsigned-web/tsconfig.json deleted file mode 100644 index ddbee4f2ac..0000000000 --- a/apps/unsigned-web/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "astro/tsconfigs/strict", - "compilerOptions": { - "resolveJsonModule": true, - "allowSyntheticDefaultImports": true - } -} diff --git a/bacon.toml b/bacon.toml deleted file mode 100644 index 92054a9b55..0000000000 --- a/bacon.toml +++ /dev/null @@ -1,16 +0,0 @@ -default_job = "api" - -[jobs.api] -command = ["cargo", "run", "-p", "api"] -need_stdout = true -on_change_strategy = "kill_then_restart" -allow_failures = true -default_watch = false -watch = [ - "apps/api/src", - "crates/analytics/src", - "crates/llm-proxy/src", - "crates/supabase-auth/src", - "crates/transcribe-proxy/src", - "crates/owhisper-client/src", -] diff --git a/bitrise.yml b/bitrise.yml deleted file mode 100644 index 879b462477..0000000000 --- a/bitrise.yml +++ /dev/null @@ -1,136 +0,0 @@ ---- -format_version: "23" -default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git -project_type: ios - -workflows: - archive_and_export_app: - summary: Create an IPA for the iOS app. - steps: - - activate-ssh-key@4: {} - - git-clone@8: {} - - restore-spm-cache@2: {} - - script@1: - title: Install Rust toolchain for iOS - inputs: - - content: |- - #!/usr/bin/env bash - set -euxo pipefail - - export PATH="$HOME/.cargo/bin:$PATH" - - if ! command -v rustup >/dev/null 2>&1; then - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal - fi - - channel="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml)" - - rustup toolchain install "$channel" \ - --profile minimal \ - --component rust-analyzer \ - --component rustfmt \ - --component clippy - - rustup target add --toolchain "$channel" \ - aarch64-apple-ios \ - aarch64-apple-ios-sim \ - x86_64-apple-ios - - script@1: - title: Generate iOS mobile bridge - inputs: - - content: |- - #!/usr/bin/env bash - set -euxo pipefail - - export PATH="$HOME/.cargo/bin:$PATH" - - channel="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml)" - - cargo +"$channel" check -p mobile-bridge -p uniffi-bindgen -p xtask - cargo +"$channel" xtask mobile-bridge - - xcode-archive@5: - inputs: - - project_path: $BITRISE_PROJECT_PATH - - scheme: $BITRISE_SCHEME - - distribution_method: $BITRISE_DISTRIBUTION_METHOD - - automatic_code_signing: api-key - - cache_level: none - - save-spm-cache@1: {} - - deploy-to-bitrise-io@2: {} - - build: - summary: Build the iOS app for testing. - steps: - - activate-ssh-key@4: {} - - git-clone@8: {} - - restore-spm-cache@2: {} - - script@1: - title: Install Rust toolchain for iOS - inputs: - - content: |- - #!/usr/bin/env bash - set -euxo pipefail - - export PATH="$HOME/.cargo/bin:$PATH" - - if ! command -v rustup >/dev/null 2>&1; then - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal - fi - - channel="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml)" - - rustup toolchain install "$channel" \ - --profile minimal \ - --component rust-analyzer \ - --component rustfmt \ - --component clippy - - rustup target add --toolchain "$channel" \ - aarch64-apple-ios \ - aarch64-apple-ios-sim \ - x86_64-apple-ios - - script@1: - title: Generate iOS mobile bridge - inputs: - - content: |- - #!/usr/bin/env bash - set -euxo pipefail - - export PATH="$HOME/.cargo/bin:$PATH" - - channel="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml)" - - cargo +"$channel" check -p mobile-bridge -p uniffi-bindgen -p xtask - cargo +"$channel" xtask mobile-bridge - - xcode-build-for-test@3: - inputs: - - project_path: $BITRISE_PROJECT_PATH - - scheme: $BITRISE_SCHEME - - destination: generic/platform=iOS Simulator - - cache_level: none - - save-spm-cache@1: {} - - deploy-to-bitrise-io@2: {} - triggers: - push: - - branch: main - changed_files: "apps/mobile/.*|crates/mobile-bridge/.*|crates/uniffi-bindgen/.*|crates/xtask/.*|Cargo.toml|Cargo.lock|\\.cargo/config.toml|bitrise\\.yml" - pull_request: - - source_branch: "*" - changed_files: "apps/mobile/.*|crates/mobile-bridge/.*|crates/uniffi-bindgen/.*|crates/xtask/.*|Cargo.toml|Cargo.lock|\\.cargo/config.toml|bitrise\\.yml" - -meta: - bitrise.io: - stack: osx-xcode-26.2.x - machine_type_id: g2.mac.large - -app: - envs: - - BITRISE_PROJECT_PATH: apps/mobile/ios/HyprMobile/HyprMobile.xcodeproj - opts: - is_expand: false - - BITRISE_SCHEME: HyprMobile - opts: - is_expand: false - - BITRISE_DISTRIBUTION_METHOD: ad-hoc - opts: - is_expand: false diff --git a/crates/activity-capture-dev/Cargo.toml b/crates/activity-capture-dev/Cargo.toml deleted file mode 100644 index 7adee63fdf..0000000000 --- a/crates/activity-capture-dev/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "activity-capture-dev" -version = "0.1.0" -edition = "2024" -publish = false - -[[bin]] -name = "activity-capture" -path = "src/main.rs" - -[dependencies] -hypr-activity-capture = { workspace = true } -hypr-llm-cactus = { workspace = true } -hypr-screen-core = { workspace = true } - -axum = { workspace = true } -chrono = { workspace = true } -clap = { workspace = true, features = ["derive"] } -crossterm = { workspace = true } -futures-util = { workspace = true } -ratatui = { workspace = true } -reqwest = { workspace = true, features = ["json"] } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -tokio = { workspace = true, features = ["macros", "net", "rt", "sync", "time"] } -url = { workspace = true } diff --git a/crates/activity-capture-dev/README.md b/crates/activity-capture-dev/README.md deleted file mode 100644 index cdd916d438..0000000000 --- a/crates/activity-capture-dev/README.md +++ /dev/null @@ -1,3 +0,0 @@ -```bash -cargo run -p activity-capture-dev -``` diff --git a/crates/activity-capture-dev/out/.gitignore b/crates/activity-capture-dev/out/.gitignore deleted file mode 100644 index bf27f3114d..0000000000 --- a/crates/activity-capture-dev/out/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -* -!.gitignore -!.gitkeep diff --git a/crates/activity-capture-dev/out/.gitkeep b/crates/activity-capture-dev/out/.gitkeep deleted file mode 100644 index 8b13789179..0000000000 --- a/crates/activity-capture-dev/out/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/activity-capture-dev/src/app.rs b/crates/activity-capture-dev/src/app.rs deleted file mode 100644 index c4252fb836..0000000000 --- a/crates/activity-capture-dev/src/app.rs +++ /dev/null @@ -1,939 +0,0 @@ -use std::{ - collections::BTreeSet, - io, - ops::RangeInclusive, - path::Path, - sync::mpsc::{self, Receiver, TryRecvError}, - thread, - time::{Duration, Instant, SystemTime, UNIX_EPOCH}, -}; - -use crossterm::{ - event::{ - self, Event as CrosstermEvent, KeyCode, KeyEvent, KeyEventKind, MouseEvent, MouseEventKind, - }, - execute, -}; -use futures_util::StreamExt; -use hypr_activity_capture::{ - ActivityCapture, ActivityScreenshotCapture, Capabilities, CaptureError, CaptureStream, - EventCoalescer, PendingCapture, PlatformCapture, ScreenshotConfig, ScreenshotDecision, - ScreenshotPolicy, Transition, WatchOptions, capture_screenshot, -}; -use ratatui::{DefaultTerminal, layout::Rect, widgets::ListState}; -use tokio::sync::oneshot; - -use crate::{ - event_row::{EventRow, RowStatus}, - export::{ExportScope, RawRecord, copy_records, save_records, save_screenshot_image}, - options::{CaptureRuntimeMode, Options}, - theme::Theme, - ui::{self, ScreenData}, - vlm::VlmRuntime, -}; - -const UI_IDLE_POLL: Duration = Duration::from_millis(250); - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum View { - List, - Details, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum DetailTab { - Details, - Raw, -} - -#[derive(Debug, Clone, Copy)] -pub(crate) struct SessionStats { - pub(crate) event_count: usize, - pub(crate) distinct_apps: usize, - pub(crate) focus_count: usize, - pub(crate) update_count: usize, - pub(crate) idle_count: usize, - pub(crate) screenshot_count: usize, - pub(crate) vlm_count: usize, -} - -pub(crate) fn run(options: Options, color_enabled: bool) -> io::Result<()> { - let _mouse_capture = MouseCaptureGuard::enable()?; - ratatui::run(|terminal| { - let app = ActivityApp::new(options, Theme::new(color_enabled))?; - app.run(terminal) - }) -} - -struct MouseCaptureGuard; - -impl MouseCaptureGuard { - fn enable() -> io::Result { - execute!(std::io::stdout(), crossterm::event::EnableMouseCapture)?; - Ok(Self) - } -} - -impl Drop for MouseCaptureGuard { - fn drop(&mut self) { - let _ = execute!(std::io::stdout(), crossterm::event::DisableMouseCapture); - } -} - -enum RuntimeEvent { - Transition(Transition), - Error(CaptureError), - Ended, -} - -enum CaptureDriver { - Once, - Polling { - next_capture_at: Option, - }, - Watch { - receiver: Receiver, - stop_tx: Option>, - handle: Option>, - active: bool, - }, - Stopped, -} - -impl CaptureDriver { - fn polling(next_capture_at: Option) -> Self { - Self::Polling { next_capture_at } - } - - fn watch(stream: CaptureStream) -> io::Result { - let (event_tx, receiver) = mpsc::channel(); - let (stop_tx, stop_rx) = oneshot::channel(); - - let handle = thread::Builder::new() - .name("activity-capture-dev-watch".to_string()) - .spawn(move || { - let runtime = match tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - { - Ok(runtime) => runtime, - Err(error) => { - let _ = event_tx.send(RuntimeEvent::Error(CaptureError::platform( - error.to_string(), - ))); - return; - } - }; - - runtime.block_on(async move { - let mut stop_rx = stop_rx; - let mut stream = stream; - let mut coalescer = EventCoalescer::default(); - - loop { - tokio::select! { - _ = &mut stop_rx => break, - item = stream.next() => { - match item { - Some(Ok(sample)) => { - if let Some(transition) = coalescer.push(sample.snapshot) { - if event_tx.send(RuntimeEvent::Transition(transition)).is_err() { - break; - } - } - } - Some(Err(error)) => { - let _ = event_tx.send(RuntimeEvent::Error(error)); - break; - } - None => break, - } - } - } - } - }); - }) - .map_err(io::Error::other)?; - - Ok(Self::Watch { - receiver, - stop_tx: Some(stop_tx), - handle: Some(handle), - active: true, - }) - } - - fn poll_timeout(&self) -> Duration { - match self { - Self::Polling { - next_capture_at: Some(deadline), - } => deadline.saturating_duration_since(Instant::now()), - Self::Polling { - next_capture_at: None, - } - | Self::Watch { .. } - | Self::Once - | Self::Stopped => UI_IDLE_POLL, - } - } - - fn tick(&mut self) -> bool { - match self { - Self::Polling { next_capture_at } => { - next_capture_at.is_some_and(|deadline| Instant::now() >= deadline) - } - _ => false, - } - } - - fn schedule_next_poll(&mut self, interval: Duration) { - if let Self::Polling { next_capture_at } = self { - *next_capture_at = Some(Instant::now() + interval); - } - } - - fn runtime_events(&mut self) -> Vec { - let mut ready = Vec::new(); - let Self::Watch { - receiver, active, .. - } = self - else { - return ready; - }; - - loop { - match receiver.try_recv() { - Ok(event) => ready.push(event), - Err(TryRecvError::Empty) => break, - Err(TryRecvError::Disconnected) => { - if *active { - *active = false; - ready.push(RuntimeEvent::Ended); - } - break; - } - } - } - - ready - } - - fn is_watch_live(&self) -> bool { - matches!(self, Self::Watch { active: true, .. }) - } - - fn stop(&mut self) { - let mut driver = CaptureDriver::Stopped; - std::mem::swap(self, &mut driver); - - if let CaptureDriver::Watch { - stop_tx, - handle, - active: _, - receiver: _, - } = driver - { - if let Some(stop_tx) = stop_tx { - let _ = stop_tx.send(()); - } - if let Some(handle) = handle { - let _ = handle.join(); - } - } - } -} - -struct ActivityApp { - capture: PlatformCapture, - options: Options, - capabilities: Capabilities, - theme: Theme, - coalescer: EventCoalescer, - capture_driver: CaptureDriver, - runtime_label: String, - events: Vec, - raw_records: Vec, - list_state: ListState, - selection_anchor: Option, - status_message: Option, - view: View, - detail_tab: DetailTab, - should_exit: bool, - list_inner_area: Rect, - screenshot_policy: ScreenshotPolicy, - latest_capture: Option, - pending_screenshot: Option, - vlm: Option, -} - -impl Drop for ActivityApp { - fn drop(&mut self) { - self.capture_driver.stop(); - } -} - -impl ActivityApp { - fn new(options: Options, theme: Theme) -> io::Result { - let capture = PlatformCapture::with_policy(options.policy()); - let capabilities = capture.capabilities(); - let resolved_runtime = if options.once { - CaptureRuntimeMode::Poll - } else { - options - .resolve_runtime_mode(capabilities) - .map_err(io::Error::other)? - }; - let runtime_label = if options.once { - "once".to_string() - } else { - options.runtime_label(resolved_runtime) - }; - let screenshot_dwell_ms = options.screenshot_dwell_ms; - let vlm = options.vlm_settings().map(VlmRuntime::spawn).transpose()?; - let mut app = Self { - capture, - options, - capabilities, - theme, - coalescer: EventCoalescer::default(), - capture_driver: CaptureDriver::Once, - runtime_label, - events: Vec::new(), - raw_records: Vec::new(), - list_state: ListState::default(), - selection_anchor: None, - status_message: None, - view: View::List, - detail_tab: DetailTab::Details, - should_exit: false, - list_inner_area: Rect::default(), - screenshot_policy: ScreenshotPolicy::new(ScreenshotConfig { - dwell_ms: screenshot_dwell_ms, - ..ScreenshotConfig::default() - }), - latest_capture: None, - pending_screenshot: None, - vlm, - }; - - if let Some(vlm) = app.vlm.as_ref() { - app.status_message = Some(format!("vlm ready: {}", vlm.model_name())); - } - - if app.options.once { - app.capture_once()?; - return Ok(app); - } - - match resolved_runtime { - CaptureRuntimeMode::Watch => { - app.capture - .snapshot() - .map_err(|error| io::Error::other(error.to_string()))?; - let stream = app - .capture - .watch(WatchOptions { - poll_interval: app.options.poll_interval(), - emit_initial: !app.options.no_emit_initial, - }) - .map_err(|error| io::Error::other(error.to_string()))?; - app.capture_driver = CaptureDriver::watch(stream)?; - } - CaptureRuntimeMode::Poll => { - app.capture_once()?; - app.capture_driver = - CaptureDriver::polling(Some(Instant::now() + app.options.poll_interval())); - } - } - - Ok(app) - } - - fn run(mut self, terminal: &mut DefaultTerminal) -> io::Result<()> { - while !self.should_exit { - self.drain_runtime_events(); - self.check_pending_screenshot(); - self.drain_vlm_results(); - ui::render(terminal, self.screen_data())?; - - let timeout = self.screenshot_poll_timeout(); - if event::poll(timeout)? { - self.handle_terminal_event(event::read()?); - } else if self.capture_driver.tick() { - self.capture_once()?; - self.capture_driver - .schedule_next_poll(self.options.poll_interval()); - } - } - - Ok(()) - } - - fn drain_runtime_events(&mut self) { - for event in self.capture_driver.runtime_events() { - match event { - RuntimeEvent::Transition(transition) => self.push_transition(transition), - RuntimeEvent::Error(error) => { - self.status_message = Some(format!( - "capture error ({:?}): {}", - error.kind, error.message - )); - self.capture_driver.stop(); - } - RuntimeEvent::Ended => { - self.status_message = Some("watch stream ended".to_string()); - } - } - } - } - - fn capture_once(&mut self) -> io::Result<()> { - let snapshot = self - .capture - .snapshot() - .map_err(|error| io::Error::other(error.to_string()))?; - - match snapshot { - Some(snapshot) => { - if let Some(transition) = self.coalescer.push(Some(snapshot)) { - self.push_transition(transition); - } - } - None if self.events.is_empty() => { - let row = EventRow::idle(SystemTime::now(), None); - let record = RawRecord::placeholder(&row, "capture returned no snapshot"); - self.push_row(row, record); - } - None => { - if let Some(transition) = self.coalescer.push(None) { - self.push_transition(transition); - } - } - } - - Ok(()) - } - - fn handle_terminal_event(&mut self, event: CrosstermEvent) { - match event { - CrosstermEvent::Key(key) if key.kind == KeyEventKind::Press => self.handle_key(key), - CrosstermEvent::Mouse(mouse) => self.handle_mouse(mouse), - CrosstermEvent::Resize(_, _) => {} - _ => {} - } - } - - fn handle_key(&mut self, key: KeyEvent) { - match self.view { - View::List => self.handle_list_key(key), - View::Details => self.handle_details_key(key), - } - } - - fn handle_list_key(&mut self, key: KeyEvent) { - match key.code { - KeyCode::Char('q') | KeyCode::Esc => self.should_exit = true, - KeyCode::Char('j') | KeyCode::Down => self.select_next(), - KeyCode::Char('k') | KeyCode::Up => self.select_previous(), - KeyCode::Char('g') | KeyCode::Home => self.select_first(), - KeyCode::Char('G') | KeyCode::End => self.select_last(), - KeyCode::Char('v') => self.toggle_selection_anchor(), - KeyCode::Char('y') => self.copy_selection(), - KeyCode::Char('s') => self.save_selection(), - KeyCode::Char('S') => self.save_session(), - KeyCode::Char('r') => { - self.detail_tab = DetailTab::Raw; - self.view = View::Details; - } - KeyCode::Char('l') | KeyCode::Right | KeyCode::Enter => { - self.detail_tab = DetailTab::Details; - self.view = View::Details; - } - _ => {} - } - } - - fn handle_details_key(&mut self, key: KeyEvent) { - match key.code { - KeyCode::Char('q') => self.should_exit = true, - KeyCode::Esc | KeyCode::Backspace | KeyCode::Left | KeyCode::Char('h') => { - self.view = View::List; - } - KeyCode::Char('j') | KeyCode::Down => self.select_next(), - KeyCode::Char('k') | KeyCode::Up => self.select_previous(), - KeyCode::Char('g') | KeyCode::Home => self.select_first(), - KeyCode::Char('G') | KeyCode::End => self.select_last(), - KeyCode::Char('d') => self.detail_tab = DetailTab::Details, - KeyCode::Char('r') | KeyCode::Tab => self.toggle_detail_tab(), - KeyCode::Char('y') => self.copy_selection(), - KeyCode::Char('s') => self.save_selection(), - KeyCode::Char('S') => self.save_session(), - _ => {} - } - } - - fn toggle_detail_tab(&mut self) { - self.detail_tab = match self.detail_tab { - DetailTab::Details => DetailTab::Raw, - DetailTab::Raw => DetailTab::Details, - }; - } - - fn handle_mouse(&mut self, mouse: MouseEvent) { - if self.view != View::List { - return; - } - - match mouse.kind { - MouseEventKind::Down(_) => { - if let Some(index) = self.row_at(mouse.column, mouse.row) { - self.select_index(index); - self.detail_tab = DetailTab::Details; - self.view = View::Details; - } - } - MouseEventKind::ScrollDown => { - if self.row_hit_area(mouse.column, mouse.row) { - self.select_next(); - } - } - MouseEventKind::ScrollUp => { - if self.row_hit_area(mouse.column, mouse.row) { - self.select_previous(); - } - } - _ => {} - } - } - - fn row_hit_area(&self, column: u16, row: u16) -> bool { - let area = self.list_inner_area; - column >= area.x - && column < area.x.saturating_add(area.width) - && row >= area.y - && row < area.y.saturating_add(area.height) - } - - fn row_at(&self, column: u16, row: u16) -> Option { - if !self.row_hit_area(column, row) { - return None; - } - - let relative_row = usize::from(row.saturating_sub(self.list_inner_area.y)); - let index = self.list_state.offset() + relative_row; - (index < self.events.len()).then_some(index) - } - - fn push_transition(&mut self, transition: Transition) { - let decision = self - .screenshot_policy - .on_transition(&transition, unix_ms_now()); - self.apply_screenshot_decision(decision); - - if let Some(row) = EventRow::from_transition(&transition) { - let record = RawRecord::from_transition(&row, transition); - self.push_row(row, record); - } - } - - fn apply_screenshot_decision(&mut self, decision: ScreenshotDecision) { - match decision { - ScreenshotDecision::None => {} - ScreenshotDecision::CancelPending => { - self.pending_screenshot = None; - } - ScreenshotDecision::Schedule(pending) => { - self.pending_screenshot = Some(pending); - } - ScreenshotDecision::CancelAndSchedule(pending) => { - self.pending_screenshot = Some(pending); - } - } - } - - fn check_pending_screenshot(&mut self) { - let Some(pending) = self.pending_screenshot.as_ref() else { - return; - }; - let now = unix_ms_now(); - if now < pending.due_at_ms { - return; - } - let pending_id = pending.id; - let Some(fired) = self.screenshot_policy.fire(pending_id, now) else { - self.pending_screenshot = None; - return; - }; - self.pending_screenshot = None; - - match capture_screenshot(&fired.target) { - Ok(image) => { - let snapshot = - self.coalescer - .current() - .map(|event| event.snapshot.clone()) - .unwrap_or_else(|| { - self.capture.snapshot().ok().flatten().unwrap_or_else(|| { - panic!("expected a snapshot when capturing a screenshot in the dev tool") - }) - }); - let capture = ActivityScreenshotCapture { - request_id: fired.id, - observation_id: "legacy-dev".to_string(), - observation_key: fired.cooldown_scope.clone(), - fingerprint: fired.fingerprint, - reason: fired.reason.as_str().to_string(), - kind: hypr_activity_capture::ObservationScreenshotKind::Entry, - scheduled_at_ms: fired.scheduled_at_ms, - captured_at_ms: now, - target: fired.target, - snapshot, - image, - }; - let saved_path = match save_screenshot_image(&capture) { - Ok(path) => { - self.status_message = - Some(format!("screenshot saved to {}", file_label(&path))); - Some(path) - } - Err(error) => { - self.status_message = Some(format!("screenshot save failed: {error}")); - None - } - }; - if let (Some(vlm), Some(path)) = (self.vlm.as_ref(), saved_path.as_ref()) { - if let Err(error) = vlm.enqueue(capture.clone(), path.clone()) { - self.status_message = Some(format!("vlm queue failed: {error}")); - } else { - self.status_message = Some(format!( - "screenshot saved to {} and queued vlm", - file_label(path) - )); - } - } - let row = EventRow::screenshot(&capture, saved_path.as_deref()); - let record = RawRecord::screenshot(&row, &capture, saved_path.as_deref()); - self.push_row(row, record); - self.latest_capture = Some(capture); - } - Err(error) => { - self.status_message = Some(format!("screenshot capture failed: {error}")); - } - } - } - - fn drain_vlm_results(&mut self) { - let Some(vlm) = self.vlm.as_mut() else { - return; - }; - - for result in vlm.drain_results() { - self.status_message = Some(match &result.response { - Ok(_) => format!("vlm completed for {}", file_label(&result.screenshot_path)), - Err(error) => format!( - "vlm failed for {}: {error}", - file_label(&result.screenshot_path) - ), - }); - let row = EventRow::vlm(&result); - let record = RawRecord::vlm(&row, &result); - self.push_row(row, record); - } - } - - fn screenshot_poll_timeout(&self) -> Duration { - let base = self.capture_driver.poll_timeout(); - let Some(pending) = self.pending_screenshot.as_ref() else { - return base; - }; - let now = unix_ms_now(); - if now >= pending.due_at_ms { - return Duration::ZERO; - } - let remaining = Duration::from_millis((pending.due_at_ms - now) as u64); - base.min(remaining) - } - - fn push_row(&mut self, row: EventRow, raw_record: RawRecord) { - let should_follow_tail = matches!(self.view, View::List) - && self - .selected_index() - .is_none_or(|index| index + 1 >= self.events.len()); - - self.events.push(row); - self.raw_records.push(raw_record); - - if should_follow_tail || self.events.len() == 1 { - self.select_last(); - } - } - - fn selected_index(&self) -> Option { - self.list_state - .selected() - .filter(|index| *index < self.events.len()) - } - - fn select_index(&mut self, index: usize) { - if self.events.is_empty() { - self.list_state.select(None); - } else { - self.list_state - .select(Some(index.min(self.events.len() - 1))); - } - } - - fn select_first(&mut self) { - if !self.events.is_empty() { - self.list_state.select(Some(0)); - } - } - - fn select_last(&mut self) { - if !self.events.is_empty() { - self.list_state.select(Some(self.events.len() - 1)); - } - } - - fn select_next(&mut self) { - if self.events.is_empty() { - return; - } - - let next = self - .selected_index() - .map_or(0, |index| (index + 1).min(self.events.len() - 1)); - self.select_index(next); - } - - fn select_previous(&mut self) { - if self.events.is_empty() { - return; - } - - let previous = self - .selected_index() - .map_or(0, |index| index.saturating_sub(1)); - self.select_index(previous); - } - - fn toggle_selection_anchor(&mut self) { - match (self.selection_anchor, self.selected_index()) { - (Some(_), _) => { - self.selection_anchor = None; - self.status_message = Some("selection cleared".to_string()); - } - (None, Some(index)) => { - self.selection_anchor = Some(index); - self.status_message = Some(format!("selection started at row {}", index + 1)); - } - (None, None) => { - self.status_message = Some("nothing selected".to_string()); - } - } - } - - fn copy_selection(&mut self) { - let Some(range) = self.export_range() else { - self.status_message = Some("nothing to copy".to_string()); - return; - }; - - let count = range_len(&range); - match copy_records(&self.raw_records, range, ExportScope::Selection) { - Ok(_) => { - self.status_message = Some(format!("copied {count} raw record(s) as JSON")); - } - Err(error) => { - self.status_message = Some(format!("copy failed: {error}")); - } - } - } - - fn save_selection(&mut self) { - let Some(range) = self.export_range() else { - self.status_message = Some("nothing to save".to_string()); - return; - }; - - let count = range_len(&range); - match save_records(&self.raw_records, range, ExportScope::Selection) { - Ok(path) => { - self.status_message = Some(format!( - "saved {count} raw record(s) to {}", - file_label(&path) - )); - } - Err(error) => { - self.status_message = Some(format!("save failed: {error}")); - } - } - } - - fn save_session(&mut self) { - if self.raw_records.is_empty() { - self.status_message = Some("nothing to save".to_string()); - return; - } - - let range = 0..=self.raw_records.len() - 1; - match save_records(&self.raw_records, range.clone(), ExportScope::Session) { - Ok(path) => { - self.status_message = Some(format!( - "saved {} raw record(s) to {}", - range_len(&range), - file_label(&path) - )); - } - Err(error) => { - self.status_message = Some(format!("save failed: {error}")); - } - } - } - - fn export_range(&self) -> Option> { - let current = self.selected_index()?; - let anchor = self.selection_anchor.unwrap_or(current); - Some(anchor.min(current)..=anchor.max(current)) - } - - fn selection_summary(&self) -> Option { - let range = self.export_range()?; - self.selection_anchor.map(|_| { - format!( - "range={}..{} ({} rows)", - range.start() + 1, - range.end() + 1, - range_len(&range) - ) - }) - } - - fn runtime_summary(&self) -> String { - if self.options.once { - let mut summary = self.runtime_label.clone(); - if let Some(vlm) = self.vlm.as_ref() { - summary.push_str(&format!(" vlm={}", vlm.model_name())); - } - return summary; - } - - let live = if self.capture_driver.is_watch_live() { - "live" - } else if matches!(self.capture_driver, CaptureDriver::Watch { .. }) { - "stopped" - } else { - "active" - }; - - let mut summary = format!("{} {live}", self.runtime_label); - if let Some(vlm) = self.vlm.as_ref() { - summary.push_str(&format!(" vlm={}", vlm.model_name())); - } - summary - } - - fn session_stats(&self) -> SessionStats { - let mut apps = BTreeSet::new(); - let mut focus_count = 0; - let mut update_count = 0; - let mut idle_count = 0; - let mut screenshot_count = 0; - let mut vlm_count = 0; - - for row in &self.events { - if row.app_name != "-" { - apps.insert(row.app_name.as_str()); - } - - match row.status { - RowStatus::Focus => focus_count += 1, - RowStatus::Update => update_count += 1, - RowStatus::Idle => idle_count += 1, - RowStatus::Screenshot => screenshot_count += 1, - RowStatus::Vlm => vlm_count += 1, - } - } - - SessionStats { - event_count: self.events.len(), - distinct_apps: apps.len(), - focus_count, - update_count, - idle_count, - screenshot_count, - vlm_count, - } - } - - fn selected_raw_json(&self) -> Option { - self.selected_index() - .and_then(|index| self.raw_records.get(index)) - .map(RawRecord::pretty_json) - } - - fn screen_data(&mut self) -> ScreenData<'_> { - let selected_index = self.selected_index(); - let selected_raw_json = self.selected_raw_json(); - let selection_summary = self.selection_summary(); - let policy_label = self.options.policy_label(); - let browser_policy_label = self.options.browser_policy_label(); - let capture_summary = self.capture_summary(); - let runtime_summary = self.runtime_summary(); - let session_stats = self.session_stats(); - - ScreenData { - options: &self.options, - capabilities: self.capabilities, - theme: self.theme, - view: self.view, - detail_tab: self.detail_tab, - runtime_summary, - policy_label, - browser_policy_label, - capture_summary, - session_stats, - events: &self.events, - selected_index, - selected_range: self.export_range(), - selection_summary, - status_message: self.status_message.clone(), - selected_raw_json, - list_state: &mut self.list_state, - list_inner_area: &mut self.list_inner_area, - } - } - - fn capture_summary(&self) -> String { - let mut summary = format!("dwell={}ms", self.options.screenshot_dwell_ms); - if let Some(pending) = self.pending_screenshot.as_ref() { - let now = unix_ms_now(); - let remaining_ms = pending.due_at_ms.saturating_sub(now).max(0); - summary.push_str(&format!( - " pending={}ms target={}", - remaining_ms, pending.target.app_name - )); - } else { - summary.push_str(" pending=none"); - } - summary - } -} - -fn range_len(range: &RangeInclusive) -> usize { - range.end() - range.start() + 1 -} - -fn file_label(path: &Path) -> String { - path.file_name() - .and_then(|value| value.to_str()) - .map(str::to_owned) - .unwrap_or_else(|| path.to_string_lossy().into_owned()) -} - -fn unix_ms_now() -> i64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_millis().min(i64::MAX as u128) as i64) - .unwrap_or(0) -} diff --git a/crates/activity-capture-dev/src/event_row.rs b/crates/activity-capture-dev/src/event_row.rs deleted file mode 100644 index 27c54de8d9..0000000000 --- a/crates/activity-capture-dev/src/event_row.rs +++ /dev/null @@ -1,550 +0,0 @@ -use std::{ - path::Path, - time::{Duration, SystemTime, UNIX_EPOCH}, -}; - -use hypr_activity_capture::{ - ActivityKind, ActivityScreenshotCapture, ContentLevel, Event, Snapshot, SnapshotSource, - TextAnchorConfidence, TextAnchorKind, Transition, -}; -use hypr_screen_core::CaptureSubject; - -use crate::{ - formatting::{compact, compact_url, format_duration, format_timestamp}, - ui::{ - APP_PREVIEW_LIMIT, DIFF_PREVIEW_LIMIT, TEXT_PREVIEW_LIMIT, TITLE_PREVIEW_LIMIT, - URL_PREVIEW_LIMIT, - }, - vlm::InferenceResult, -}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum RowStatus { - Focus, - Update, - Idle, - Screenshot, - Vlm, -} - -impl RowStatus { - pub(crate) const fn label(self) -> &'static str { - match self { - Self::Focus => "focus", - Self::Update => "update", - Self::Idle => "idle", - Self::Screenshot => "snap", - Self::Vlm => "vlm", - } - } -} - -#[derive(Debug, Clone)] -pub(crate) struct DetailField { - pub(crate) label: String, - pub(crate) value: String, -} - -#[derive(Debug, Clone)] -pub(crate) struct EventRow { - pub(crate) captured_at: SystemTime, - pub(crate) app_name: String, - pub(crate) status: RowStatus, - pub(crate) context: String, - pub(crate) summary: String, - pub(crate) details: Vec, -} - -impl EventRow { - pub(crate) fn from_transition(transition: &Transition) -> Option { - match (transition.previous.as_ref(), transition.current.as_ref()) { - (None, Some(current)) => Some(Self::focus(current, None)), - (Some(previous), Some(current)) if same_app(previous, current) => { - Some(Self::update(previous, current)) - } - (Some(previous), Some(current)) => Some(Self::focus(current, Some(previous))), - (Some(previous), None) => Some(Self::idle_after(previous)), - (None, None) => None, - } - } - - pub(crate) fn idle(captured_at: SystemTime, previous_app: Option<&str>) -> Self { - let mut details = vec![ - detail("Event", "idle"), - detail("Captured", format_timestamp(captured_at)), - ]; - - if let Some(previous_app) = previous_app { - details.push(detail("From", detail_value(previous_app))); - } - - Self { - captured_at, - app_name: "-".to_string(), - status: RowStatus::Idle, - context: "idle".to_string(), - summary: previous_app - .map(|value| format!("from={}", compact(value, APP_PREVIEW_LIMIT))) - .unwrap_or_else(|| "-".to_string()), - details, - } - } - - fn idle_after(previous: &Event) -> Self { - let mut row = Self::idle(previous.ended_at, Some(previous.snapshot.app_name.as_str())); - row.details - .push(detail("Last span", event_span_label(previous))); - row.details - .push(detail("Last fingerprint", previous.fingerprint.clone())); - row - } - - fn focus(current: &Event, previous: Option<&Event>) -> Self { - let snapshot = ¤t.snapshot; - let previous_app = previous.map(|value| value.snapshot.app_name.as_str()); - let mut details = vec![ - detail("Event", "focus"), - detail("Captured", format_timestamp(current.ended_at)), - detail("App", detail_value(&snapshot.app_name)), - ]; - - if let Some(previous_app) = previous_app { - details.push(detail("From", detail_value(previous_app))); - } - - push_event_details(&mut details, current); - push_snapshot_details(&mut details, snapshot); - - Self { - captured_at: current.ended_at, - app_name: snapshot.app_name.clone(), - status: RowStatus::Focus, - context: context_label(snapshot), - summary: focus_summary(snapshot, previous_app), - details, - } - } - - pub(crate) fn screenshot( - capture: &ActivityScreenshotCapture, - saved_path: Option<&Path>, - ) -> Self { - let captured_at = UNIX_EPOCH + Duration::from_millis(capture.captured_at_ms as u64); - let scheduled_at = UNIX_EPOCH + Duration::from_millis(capture.scheduled_at_ms as u64); - let dwell = Duration::from_millis( - capture - .captured_at_ms - .saturating_sub(capture.scheduled_at_ms) as u64, - ); - let image = &capture.image; - - let mut details = vec![ - detail("Event", "screenshot"), - detail("Captured", format_timestamp(captured_at)), - detail("Scheduled", format_timestamp(scheduled_at)), - detail("Dwell", format_duration(dwell)), - detail("App", detail_value(&capture.target.app_name)), - ]; - if let Some(title) = capture.target.title.as_deref() { - details.push(detail("Window", detail_value(title))); - } - details.push(detail("Reason", format!("{:?}", capture.reason))); - details.push(detail("Fingerprint", capture.fingerprint.clone())); - details.push(detail("PID", capture.target.pid.to_string())); - details.push(detail( - "Image", - format!("{}x{} {}", image.width, image.height, image.mime_type), - )); - match &image.subject { - CaptureSubject::Window(window) => details.push(detail( - "Source", - format!("window {} ({})", window.title, window.app_name), - )), - CaptureSubject::Display(display) => details.push(detail( - "Source", - format!( - "display {}{}", - display.name, - if display.is_primary { " (primary)" } else { "" } - ), - )), - } - details.push(detail( - "Size", - format!("{:.1} KB", image.image_bytes.len() as f64 / 1024.0), - )); - if let Some(path) = saved_path { - details.push(detail("Saved", path.display().to_string())); - } - - let title_preview = capture - .target - .title - .as_deref() - .map(|t| format!(" title={}", compact(t, TITLE_PREVIEW_LIMIT))) - .unwrap_or_default(); - - Self { - captured_at, - app_name: capture.target.app_name.clone(), - status: RowStatus::Screenshot, - context: format!("{}x{}", image.width, image.height), - summary: format!("reason={:?}{title_preview}", capture.reason,), - details, - } - } - - pub(crate) fn vlm(result: &InferenceResult) -> Self { - let mut details = vec![ - detail("Event", "vlm"), - detail("Captured", format_timestamp(result.finished_at)), - detail("App", detail_value(&result.screenshot.target.app_name)), - detail("Model", detail_value(&result.model_name)), - detail("Latency", format_duration(result.latency())), - detail("Prompt", detail_value(&result.prompt)), - detail("Screenshot", result.screenshot_path.display().to_string()), - detail("Fingerprint", result.screenshot.fingerprint.clone()), - ]; - - if let Some(title) = result.screenshot.target.title.as_deref() { - details.push(detail("Window", detail_value(title))); - } - - let (context, summary) = match &result.response { - Ok(response) => { - details.push(detail("Response", detail_value(response))); - ( - result.model_name.clone(), - format!("ok={}", compact(response, TEXT_PREVIEW_LIMIT * 2)), - ) - } - Err(error) => { - details.push(detail("Error", detail_value(error))); - ( - result.model_name.clone(), - format!("error={}", compact(error, TEXT_PREVIEW_LIMIT)), - ) - } - }; - - Self { - captured_at: result.finished_at, - app_name: result.screenshot.target.app_name.clone(), - status: RowStatus::Vlm, - context, - summary, - details, - } - } - - fn update(previous: &Event, current: &Event) -> Self { - let mut details = vec![ - detail("Event", "update"), - detail("Captured", format_timestamp(current.ended_at)), - detail("App", detail_value(¤t.snapshot.app_name)), - detail("Previous span", event_span_label(previous)), - ]; - - push_event_details(&mut details, current); - push_change_details( - &mut details, - "Title", - previous.snapshot.window_title.as_deref(), - current.snapshot.window_title.as_deref(), - ); - push_change_details( - &mut details, - "URL", - previous.snapshot.url.as_deref(), - current.snapshot.url.as_deref(), - ); - push_change_details( - &mut details, - "Text", - primary_text(&previous.snapshot), - primary_text(¤t.snapshot), - ); - - if details.len() == 5 { - details.push(detail("Change", "metadata changed")); - } - - push_snapshot_details(&mut details, ¤t.snapshot); - - Self { - captured_at: current.ended_at, - app_name: current.snapshot.app_name.clone(), - status: RowStatus::Update, - context: context_label(¤t.snapshot), - summary: update_summary(&previous.snapshot, ¤t.snapshot), - details, - } - } -} - -fn same_app(previous: &Event, current: &Event) -> bool { - previous.snapshot.bundle_id == current.snapshot.bundle_id - || previous.snapshot.app_name == current.snapshot.app_name -} - -fn focus_summary(snapshot: &Snapshot, previous_app: Option<&str>) -> String { - let mut parts = Vec::new(); - - if let Some(previous_app) = previous_app { - parts.push(format!("from={}", compact(previous_app, APP_PREVIEW_LIMIT))); - } - if let Some(title) = snapshot.window_title.as_deref() { - parts.push(format!("title={}", display_value("title", title))); - } - if let Some(url) = snapshot.url.as_deref() { - parts.push(format!("url={}", display_value("url", url))); - } else if let Some(text) = primary_text(snapshot) { - parts.push(format!("text={}", display_value("text", text))); - } - - parts.join(" ") -} - -fn update_summary(previous: &Snapshot, current: &Snapshot) -> String { - let mut changes = Vec::new(); - - push_change( - &mut changes, - "title", - previous.window_title.as_deref(), - current.window_title.as_deref(), - ); - push_change( - &mut changes, - "url", - previous.url.as_deref(), - current.url.as_deref(), - ); - push_change( - &mut changes, - "text", - primary_text(previous), - primary_text(current), - ); - - if changes.is_empty() { - changes.push("metadata changed".to_string()); - } - - changes.join(" ") -} - -fn primary_text(snapshot: &Snapshot) -> Option<&str> { - snapshot - .text_anchor_text - .as_deref() - .or(snapshot.text_anchor_selected_text.as_deref()) - .or(snapshot.visible_text.as_deref()) -} - -fn push_change( - changes: &mut Vec, - label: &'static str, - before: Option<&str>, - after: Option<&str>, -) { - let before_display = before.map(|value| diff_value(label, value)); - let after_display = after.map(|value| diff_value(label, value)); - - if before_display == after_display { - return; - } - - match (before_display, after_display) { - (Some(before), Some(after)) => changes.push(format!("{label}:{before} -> {after}")), - (None, Some(after)) => changes.push(format!("{label}:+{after}")), - (Some(before), None) => changes.push(format!("{label}:-{before}")), - (None, None) => {} - } -} - -fn display_value(label: &str, value: &str) -> String { - match label { - "url" => compact_url(value, URL_PREVIEW_LIMIT), - "text" => compact(value, TEXT_PREVIEW_LIMIT), - _ => compact(value, TITLE_PREVIEW_LIMIT), - } -} - -fn diff_value(label: &str, value: &str) -> String { - match label { - "url" => compact_url(value, DIFF_PREVIEW_LIMIT * 2), - _ => compact(value, DIFF_PREVIEW_LIMIT), - } -} - -fn push_event_details(details: &mut Vec, event: &Event) { - details.push(detail("Started", format_timestamp(event.started_at))); - details.push(detail("Ended", format_timestamp(event.ended_at))); - details.push(detail("Span", event_span_label(event))); - details.push(detail("Fingerprint", event.fingerprint.clone())); -} - -fn push_snapshot_details(details: &mut Vec, snapshot: &Snapshot) { - details.push(detail("PID", snapshot.pid.to_string())); - details.push(detail( - "Activity", - activity_kind_label(snapshot.activity_kind), - )); - if let Some(bundle_id) = snapshot.bundle_id.as_deref() { - details.push(detail("Bundle", detail_value(bundle_id))); - } - if let Some(title) = snapshot.window_title.as_deref() { - details.push(detail("Window", detail_value(title))); - } - if let Some(url) = snapshot.url.as_deref() { - details.push(detail("URL", detail_value(url))); - } - if let Some(text) = primary_text(snapshot) { - details.push(detail("Text", detail_value(text))); - } - if let Some(anchor_kind) = snapshot.text_anchor_kind { - details.push(detail("Anchor", anchor_kind_label(anchor_kind))); - } - if let Some(anchor_identity) = snapshot.text_anchor_identity.as_deref() { - details.push(detail("Anchor ID", detail_value(anchor_identity))); - } - if let Some(anchor_text) = snapshot.text_anchor_text.as_deref() { - details.push(detail("Anchor text", detail_value(anchor_text))); - } - if let Some(selected_text) = snapshot.text_anchor_selected_text.as_deref() { - details.push(detail("Selected", detail_value(selected_text))); - } - if let Some(prefix) = snapshot.text_anchor_prefix.as_deref() { - details.push(detail("Prefix", detail_value(prefix))); - } - if let Some(suffix) = snapshot.text_anchor_suffix.as_deref() { - details.push(detail("Suffix", detail_value(suffix))); - } - if let Some(confidence) = snapshot.text_anchor_confidence { - details.push(detail("Confidence", anchor_confidence_label(confidence))); - } - details.push(detail( - "Content", - content_level_label(snapshot.content_level), - )); - details.push(detail("Source", source_label(snapshot.source))); -} - -fn push_change_details( - details: &mut Vec, - label: &str, - before: Option<&str>, - after: Option<&str>, -) { - if before == after { - return; - } - - if let Some(before) = before { - details.push(detail(format!("{label} before"), detail_value(before))); - } - if let Some(after) = after { - details.push(detail(format!("{label} after"), detail_value(after))); - } - if before.is_none() && after.is_none() { - details.push(detail(label, "-")); - } -} - -fn detail(label: impl Into, value: impl Into) -> DetailField { - DetailField { - label: label.into(), - value: value.into(), - } -} - -fn detail_value(value: &str) -> String { - let collapsed = value.split_whitespace().collect::>().join(" "); - if collapsed.is_empty() { - "-".to_string() - } else { - collapsed - } -} - -fn event_span_label(event: &Event) -> String { - let span = event - .ended_at - .duration_since(event.started_at) - .unwrap_or(Duration::ZERO); - format_duration(span) -} - -fn context_label(snapshot: &Snapshot) -> String { - format!( - "{}/{}/{}", - activity_kind_tag(snapshot.activity_kind), - content_level_tag(snapshot.content_level), - source_tag(snapshot.source), - ) -} - -fn activity_kind_tag(kind: ActivityKind) -> &'static str { - match kind { - ActivityKind::ForegroundWindow => "win", - ActivityKind::Browser => "browser", - ActivityKind::AudioSession => "audio", - } -} - -fn activity_kind_label(kind: ActivityKind) -> &'static str { - match kind { - ActivityKind::ForegroundWindow => "foreground_window", - ActivityKind::Browser => "browser", - ActivityKind::AudioSession => "audio_session", - } -} - -fn content_level_tag(level: ContentLevel) -> &'static str { - match level { - ContentLevel::Metadata => "meta", - ContentLevel::Url => "url", - ContentLevel::Full => "full", - } -} - -fn content_level_label(level: ContentLevel) -> &'static str { - match level { - ContentLevel::Metadata => "metadata", - ContentLevel::Url => "url", - ContentLevel::Full => "full", - } -} - -fn source_tag(source: SnapshotSource) -> &'static str { - match source { - SnapshotSource::Accessibility => "ax", - SnapshotSource::Workspace => "ws", - } -} - -fn source_label(source: SnapshotSource) -> &'static str { - match source { - SnapshotSource::Accessibility => "accessibility", - SnapshotSource::Workspace => "workspace", - } -} - -fn anchor_kind_label(kind: TextAnchorKind) -> &'static str { - match kind { - TextAnchorKind::FocusedEdit => "focused_edit", - TextAnchorKind::SelectedText => "selected_text", - TextAnchorKind::FocusedElement => "focused_element", - TextAnchorKind::Document => "document", - TextAnchorKind::None => "none", - } -} - -fn anchor_confidence_label(confidence: TextAnchorConfidence) -> &'static str { - match confidence { - TextAnchorConfidence::High => "high", - TextAnchorConfidence::Medium => "medium", - TextAnchorConfidence::Low => "low", - } -} diff --git a/crates/activity-capture-dev/src/export.rs b/crates/activity-capture-dev/src/export.rs deleted file mode 100644 index 6f02ac8197..0000000000 --- a/crates/activity-capture-dev/src/export.rs +++ /dev/null @@ -1,468 +0,0 @@ -use std::{ - fs, - io::{self, Write}, - ops::RangeInclusive, - path::{Path, PathBuf}, - process::{Command, Stdio}, - time::SystemTime, -}; - -use chrono::Local; -use hypr_activity_capture::{ActivityScreenshotCapture, Event, Transition}; -use hypr_screen_core::CaptureSubject; -use serde::Serialize; - -use crate::{ - event_row::{DetailField, EventRow}, - vlm::InferenceResult, -}; - -#[derive(Debug, Clone, Copy)] -pub(crate) enum ExportScope { - Selection, - Session, -} - -impl ExportScope { - fn label(self) -> &'static str { - match self { - Self::Selection => "selection", - Self::Session => "session", - } - } -} - -#[derive(Debug, Clone, Serialize)] -pub(crate) struct RawRecord { - captured_at: SystemTime, - status: String, - app_name: String, - summary: String, - details: Vec, - raw: RawPayload, -} - -impl RawRecord { - pub(crate) fn from_transition(row: &EventRow, transition: Transition) -> Self { - Self { - captured_at: row.captured_at, - status: row.status.label().to_string(), - app_name: row.app_name.clone(), - summary: row.summary.clone(), - details: row.details.iter().map(RawDetailField::from).collect(), - raw: RawPayload::Transition { - previous: transition.previous, - current: transition.current, - }, - } - } - - pub(crate) fn screenshot( - row: &EventRow, - capture: &ActivityScreenshotCapture, - saved_path: Option<&Path>, - ) -> Self { - Self { - captured_at: row.captured_at, - status: row.status.label().to_string(), - app_name: row.app_name.clone(), - summary: row.summary.clone(), - details: row.details.iter().map(RawDetailField::from).collect(), - raw: RawPayload::Screenshot { - fingerprint: capture.fingerprint.clone(), - reason: format!("{:?}", capture.reason), - scheduled_at_ms: capture.scheduled_at_ms, - captured_at_ms: capture.captured_at_ms, - pid: capture.target.pid, - app_name: capture.target.app_name.clone(), - title: capture.target.title.clone(), - image_width: capture.image.width, - image_height: capture.image.height, - image_bytes_len: capture.image.image_bytes.len(), - subject: screenshot_subject(&capture.image.subject), - saved_path: saved_path.map(|path| path.display().to_string()), - }, - } - } - - pub(crate) fn vlm(row: &EventRow, result: &InferenceResult) -> Self { - let (response_text, error) = match &result.response { - Ok(response) => (Some(response.clone()), None), - Err(error) => (None, Some(error.clone())), - }; - - Self { - captured_at: row.captured_at, - status: row.status.label().to_string(), - app_name: row.app_name.clone(), - summary: row.summary.clone(), - details: row.details.iter().map(RawDetailField::from).collect(), - raw: RawPayload::VlmInference { - finished_at_ms: unix_ms(result.finished_at), - latency_ms: result.latency().as_millis().min(u64::MAX as u128) as u64, - fingerprint: result.screenshot.fingerprint.clone(), - pid: result.screenshot.target.pid, - app_name: result.screenshot.target.app_name.clone(), - title: result.screenshot.target.title.clone(), - screenshot_path: result.screenshot_path.display().to_string(), - model_name: result.model_name.clone(), - prompt: result.prompt.clone(), - response_text, - error, - }, - } - } - - pub(crate) fn placeholder(row: &EventRow, reason: impl Into) -> Self { - Self { - captured_at: row.captured_at, - status: row.status.label().to_string(), - app_name: row.app_name.clone(), - summary: row.summary.clone(), - details: row.details.iter().map(RawDetailField::from).collect(), - raw: RawPayload::Placeholder { - reason: reason.into(), - }, - } - } - - pub(crate) fn pretty_json(&self) -> String { - serde_json::to_string_pretty(self).unwrap_or_else(|error| { - format!( - "{{\"error\":\"failed to serialize selected record\",\"detail\":\"{}\"}}", - error - ) - }) - } -} - -#[derive(Debug, Clone, Serialize)] -struct RawDetailField { - label: String, - value: String, -} - -impl From<&DetailField> for RawDetailField { - fn from(detail: &DetailField) -> Self { - Self { - label: detail.label.clone(), - value: detail.value.clone(), - } - } -} - -#[derive(Debug, Clone, Serialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -enum RawPayload { - Transition { - previous: Option, - current: Option, - }, - Placeholder { - reason: String, - }, - Screenshot { - fingerprint: String, - reason: String, - scheduled_at_ms: i64, - captured_at_ms: i64, - pid: u32, - app_name: String, - title: Option, - image_width: u32, - image_height: u32, - image_bytes_len: usize, - subject: ScreenshotSubjectPayload, - saved_path: Option, - }, - VlmInference { - finished_at_ms: u64, - latency_ms: u64, - fingerprint: String, - pid: u32, - app_name: String, - title: Option, - screenshot_path: String, - model_name: String, - prompt: String, - response_text: Option, - error: Option, - }, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -enum ScreenshotSubjectPayload { - Window { - id: u32, - pid: u32, - app_name: String, - title: String, - rect: ScreenshotRectPayload, - }, - Display { - id: u32, - name: String, - rect: ScreenshotRectPayload, - is_primary: bool, - }, -} - -#[derive(Debug, Clone, Serialize)] -struct ScreenshotRectPayload { - x: i32, - y: i32, - width: u32, - height: u32, -} - -#[derive(Debug, Serialize)] -struct ExportBundle { - exported_at: SystemTime, - scope: &'static str, - range: ExportRange, - records: Vec, -} - -#[derive(Debug, Serialize)] -struct ExportRange { - start_index: usize, - end_index: usize, - count: usize, -} - -#[derive(Debug, Serialize)] -struct ExportedRecord { - index: usize, - #[serde(flatten)] - record: RawRecord, -} - -pub(crate) fn copy_records( - records: &[RawRecord], - range: RangeInclusive, - scope: ExportScope, -) -> io::Result { - let (json, count) = serialize_records(records, range, scope)?; - copy_to_clipboard(&json)?; - Ok(count) -} - -pub(crate) fn save_records( - records: &[RawRecord], - range: RangeInclusive, - scope: ExportScope, -) -> io::Result { - let (json, _) = serialize_records(records, range, scope)?; - let file_name = format!( - "activity-capture-{}-{}.json", - scope.label(), - Local::now().format("%Y%m%d-%H%M%S") - ); - - let directory = export_directory()?; - fs::create_dir_all(&directory)?; - let path = unique_path(&directory, &file_name); - fs::write(&path, json)?; - Ok(path) -} - -pub(crate) fn save_screenshot_image(capture: &ActivityScreenshotCapture) -> io::Result { - let app_slug: String = capture - .target - .app_name - .chars() - .map(|c| { - if c.is_alphanumeric() { - c.to_ascii_lowercase() - } else { - '-' - } - }) - .collect(); - let extension = screenshot_extension(&capture.image.mime_type); - let file_name = format!( - "screenshot-{}-{}.{}", - Local::now().format("%H%M%S"), - app_slug, - extension, - ); - - let directory = export_directory()?; - fs::create_dir_all(&directory)?; - let path = unique_path(&directory, &file_name); - fs::write(&path, &capture.image.image_bytes)?; - Ok(path) -} - -fn serialize_records( - records: &[RawRecord], - range: RangeInclusive, - scope: ExportScope, -) -> io::Result<(String, usize)> { - let start = *range.start(); - let end = *range.end(); - let slice = records - .get(start..=end) - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid export range"))?; - - let bundle = ExportBundle { - exported_at: SystemTime::now(), - scope: scope.label(), - range: ExportRange { - start_index: start, - end_index: end, - count: slice.len(), - }, - records: slice - .iter() - .enumerate() - .map(|(offset, record)| ExportedRecord { - index: start + offset, - record: record.clone(), - }) - .collect(), - }; - - serde_json::to_string_pretty(&bundle) - .map(|json| (json, slice.len())) - .map_err(io::Error::other) -} - -fn unique_path(directory: &Path, file_name: &str) -> PathBuf { - let candidate = directory.join(file_name); - if !candidate.exists() { - return candidate; - } - - let stem = Path::new(file_name) - .file_stem() - .and_then(|value| value.to_str()) - .unwrap_or("activity-capture-export"); - let extension = Path::new(file_name) - .extension() - .and_then(|value| value.to_str()) - .unwrap_or("json"); - - for suffix in 2.. { - let candidate = directory.join(format!("{stem}-{suffix}.{extension}")); - if !candidate.exists() { - return candidate; - } - } - - unreachable!("infinite suffix range should always find a free path") -} - -fn screenshot_subject(subject: &CaptureSubject) -> ScreenshotSubjectPayload { - match subject { - CaptureSubject::Window(window) => ScreenshotSubjectPayload::Window { - id: window.id, - pid: window.pid, - app_name: window.app_name.clone(), - title: window.title.clone(), - rect: screenshot_rect(window.rect), - }, - CaptureSubject::Display(display) => ScreenshotSubjectPayload::Display { - id: display.id, - name: display.name.clone(), - rect: screenshot_rect(display.rect), - is_primary: display.is_primary, - }, - } -} - -fn screenshot_rect(rect: hypr_screen_core::CaptureRect) -> ScreenshotRectPayload { - ScreenshotRectPayload { - x: rect.x, - y: rect.y, - width: rect.width, - height: rect.height, - } -} - -fn screenshot_extension(mime_type: &str) -> &'static str { - match mime_type { - "image/png" => "png", - "image/webp" => "webp", - "image/jpeg" => "jpg", - _ => "img", - } -} - -fn unix_ms(value: SystemTime) -> u64 { - value - .duration_since(SystemTime::UNIX_EPOCH) - .map(|duration| duration.as_millis().min(u64::MAX as u128) as u64) - .unwrap_or(0) -} - -fn export_directory() -> io::Result { - Ok(Path::new(env!("CARGO_MANIFEST_DIR")).join("out")) -} - -fn copy_to_clipboard(contents: &str) -> io::Result<()> { - #[cfg(target_os = "macos")] - { - return copy_with_command("pbcopy", &[], contents); - } - - #[cfg(target_os = "windows")] - { - return copy_with_command("cmd", &["/C", "clip"], contents); - } - - #[cfg(all(unix, not(target_os = "macos")))] - { - let candidates = [ - ("wl-copy", Vec::<&str>::new()), - ("xclip", vec!["-selection", "clipboard"]), - ("xsel", vec!["--clipboard", "--input"]), - ]; - - let mut last_error = None; - for (command, args) in candidates { - match copy_with_command(command, &args, contents) { - Ok(()) => return Ok(()), - Err(error) if error.kind() == io::ErrorKind::NotFound => { - last_error = Some(error); - } - Err(error) => return Err(error), - } - } - - return Err(last_error.unwrap_or_else(|| { - io::Error::new( - io::ErrorKind::NotFound, - "no supported clipboard command found (tried wl-copy, xclip, xsel)", - ) - })); - } - - #[allow(unreachable_code)] - Err(io::Error::new( - io::ErrorKind::Unsupported, - "clipboard export is not supported on this platform", - )) -} - -fn copy_with_command(command: &str, args: &[&str], contents: &str) -> io::Result<()> { - let mut child = Command::new(command) - .args(args) - .stdin(Stdio::piped()) - .spawn()?; - let Some(mut stdin) = child.stdin.take() else { - return Err(io::Error::other("clipboard stdin is unavailable")); - }; - stdin.write_all(contents.as_bytes())?; - drop(stdin); - - let status = child.wait()?; - if status.success() { - Ok(()) - } else { - Err(io::Error::other(format!( - "{command} failed with status {status}" - ))) - } -} diff --git a/crates/activity-capture-dev/src/formatting.rs b/crates/activity-capture-dev/src/formatting.rs deleted file mode 100644 index 79606a2198..0000000000 --- a/crates/activity-capture-dev/src/formatting.rs +++ /dev/null @@ -1,80 +0,0 @@ -use std::time::{Duration, SystemTime}; - -use chrono::{DateTime, Local}; -use url::Url; - -pub fn compact(value: &str, limit: usize) -> String { - let mut collapsed = value.split_whitespace().collect::>().join(" "); - if collapsed.is_empty() { - return "-".to_string(); - } - if collapsed.chars().count() > limit { - collapsed = collapsed.chars().take(limit).collect::(); - collapsed.push_str("..."); - } - collapsed -} - -pub fn compact_url(value: &str, limit: usize) -> String { - let compacted = match Url::parse(value) { - Ok(url) => { - let mut compacted = match url.host_str() { - Some(host) => format!("{}://{host}", url.scheme()), - None => format!("{}:", url.scheme()), - }; - - let mut suffix = url.path().to_string(); - if let Some(query) = url.query() { - suffix.push('?'); - suffix.push_str(query); - } - if !suffix.is_empty() && suffix != "/" { - compacted.push_str(&suffix); - } - - compacted - } - Err(_) => value.to_string(), - }; - - compact(&compacted, limit) -} - -pub fn format_timestamp(time: SystemTime) -> String { - let local: DateTime = time.into(); - local.format("%H:%M:%S").to_string() -} - -pub fn format_duration(duration: Duration) -> String { - if duration.as_millis() < 1_000 { - return format!("{}ms", duration.as_millis()); - } - - if duration.as_secs() < 60 { - return format!("{:.1}s", duration.as_secs_f64()); - } - - let minutes = duration.as_secs() / 60; - let seconds = duration.as_secs() % 60; - format!("{minutes}m{seconds:02}s") -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn formats_short_duration_in_millis() { - assert_eq!(format_duration(Duration::from_millis(275)), "275ms"); - } - - #[test] - fn formats_second_duration_with_fraction() { - assert_eq!(format_duration(Duration::from_millis(1_500)), "1.5s"); - } - - #[test] - fn formats_minute_duration() { - assert_eq!(format_duration(Duration::from_secs(125)), "2m05s"); - } -} diff --git a/crates/activity-capture-dev/src/main.rs b/crates/activity-capture-dev/src/main.rs deleted file mode 100644 index 6cdff5b98b..0000000000 --- a/crates/activity-capture-dev/src/main.rs +++ /dev/null @@ -1,24 +0,0 @@ -mod app; -mod event_row; -mod export; -mod formatting; -mod options; -mod theme; -mod ui; -mod vlm; - -use std::{io::IsTerminal, process}; - -use clap::Parser; - -use crate::{app::run, options::Options}; - -fn main() { - let options = Options::parse(); - let color_enabled = !options.no_color && std::io::stdout().is_terminal(); - - if let Err(error) = run(options, color_enabled) { - eprintln!("snapshot error: {error}"); - process::exit(1); - } -} diff --git a/crates/activity-capture-dev/src/options.rs b/crates/activity-capture-dev/src/options.rs deleted file mode 100644 index db2fdfa40f..0000000000 --- a/crates/activity-capture-dev/src/options.rs +++ /dev/null @@ -1,367 +0,0 @@ -use std::{path::PathBuf, time::Duration}; - -use clap::{Parser, ValueEnum}; -use hypr_activity_capture::{ - BrowserPolicy, BundleRule, Capabilities, CaptureAccess, CapturePolicy, DomainRule, PolicyMode, -}; - -use crate::vlm::{ - DEFAULT_VLM_MODEL_NAME, DEFAULT_VLM_PROMPT, DEFAULT_VLM_TIMEOUT_SECS, VlmSettings, -}; - -const DEFAULT_POLL_INTERVAL_MS: u64 = 750; -const DEFAULT_SCREENSHOT_DWELL_MS: u64 = 2_000; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] -pub(crate) enum RuntimePreference { - Auto, - Watch, - Poll, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum CaptureRuntimeMode { - Watch, - Poll, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] -enum PolicyModeArg { - OptIn, - OptOut, -} - -impl From for PolicyMode { - fn from(value: PolicyModeArg) -> Self { - match value { - PolicyModeArg::OptIn => Self::OptIn, - PolicyModeArg::OptOut => Self::OptOut, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] -enum AccessArg { - None, - Metadata, - Url, - Full, -} - -impl From for CaptureAccess { - fn from(value: AccessArg) -> Self { - match value { - AccessArg::None => Self::None, - AccessArg::Metadata => Self::Metadata, - AccessArg::Url => Self::Url, - AccessArg::Full => Self::Full, - } - } -} - -#[derive(Debug, Clone, Parser)] -#[command( - about = "Inspect activity capture transitions, policy, and raw events in a TUI", - long_about = None -)] -pub struct Options { - #[arg( - long = "poll-ms", - default_value_t = DEFAULT_POLL_INTERVAL_MS, - value_parser = clap::value_parser!(u64).range(1..) - )] - pub poll_ms: u64, - - #[arg(long, value_enum, default_value_t = RuntimePreference::Auto)] - pub mode: RuntimePreference, - - #[arg(long, value_enum, conflicts_with = "metadata_only")] - policy_mode: Option, - - #[arg(long, conflicts_with = "policy_mode")] - pub metadata_only: bool, - - #[arg(long = "app-rule", value_name = "APP=ACCESS", value_parser = parse_app_rule)] - pub app_rules: Vec, - - #[arg( - long = "domain-rule", - value_name = "[*.]DOMAIN=ACCESS", - value_parser = parse_domain_rule - )] - pub domain_rules: Vec, - - #[arg(long)] - pub allow_private_browsing: bool, - - #[arg(long)] - pub keep_query: bool, - - #[arg(long)] - pub keep_fragment: bool, - - #[arg(long)] - pub allow_text_without_url: bool, - - #[arg(long)] - pub no_emit_initial: bool, - - #[arg(long)] - pub no_color: bool, - - #[arg(long)] - pub once: bool, - - #[arg( - long = "screenshot-dwell-ms", - default_value_t = DEFAULT_SCREENSHOT_DWELL_MS, - value_parser = clap::value_parser!(u64).range(0..) - )] - pub screenshot_dwell_ms: u64, - - #[arg(long = "vlm-model", value_name = "PATH")] - pub vlm_model: Option, - - #[arg(long = "vlm-model-name", default_value = DEFAULT_VLM_MODEL_NAME)] - pub vlm_model_name: String, - - #[arg(long = "vlm-prompt", default_value = DEFAULT_VLM_PROMPT)] - pub vlm_prompt: String, - - #[arg( - long = "vlm-timeout-secs", - default_value_t = DEFAULT_VLM_TIMEOUT_SECS, - value_parser = clap::value_parser!(u64).range(1..) - )] - pub vlm_timeout_secs: u64, -} - -impl Options { - pub fn poll_interval(&self) -> Duration { - Duration::from_millis(self.poll_ms) - } - - pub fn policy(&self) -> CapturePolicy { - CapturePolicy { - mode: self.effective_policy_mode(), - app_rules: self.app_rules.clone(), - browser: BrowserPolicy { - rules: self.domain_rules.clone(), - require_url_for_text_access: !self.allow_text_without_url, - block_private_browsing: !self.allow_private_browsing, - strip_query: !self.keep_query, - strip_fragment: !self.keep_fragment, - }, - } - } - - pub fn resolve_runtime_mode( - &self, - capabilities: Capabilities, - ) -> Result { - match self.mode { - RuntimePreference::Auto if capabilities.can_watch => Ok(CaptureRuntimeMode::Watch), - RuntimePreference::Auto => Ok(CaptureRuntimeMode::Poll), - RuntimePreference::Watch if capabilities.can_watch => Ok(CaptureRuntimeMode::Watch), - RuntimePreference::Watch => { - Err("watch mode was requested, but this platform capture does not support watch streams".to_string()) - } - RuntimePreference::Poll => Ok(CaptureRuntimeMode::Poll), - } - } - - pub fn runtime_label(&self, resolved: CaptureRuntimeMode) -> String { - match (self.mode, resolved) { - (RuntimePreference::Auto, CaptureRuntimeMode::Watch) => "watch(auto)".to_string(), - (RuntimePreference::Auto, CaptureRuntimeMode::Poll) => { - "poll(auto-fallback)".to_string() - } - (RuntimePreference::Watch, CaptureRuntimeMode::Watch) => "watch".to_string(), - (RuntimePreference::Poll, CaptureRuntimeMode::Poll) => "poll".to_string(), - (RuntimePreference::Watch, CaptureRuntimeMode::Poll) => "poll".to_string(), - (RuntimePreference::Poll, CaptureRuntimeMode::Watch) => "watch".to_string(), - } - } - - pub fn policy_label(&self) -> String { - format!( - "mode={} apps={} domains={}", - policy_mode_label(self.effective_policy_mode()), - self.app_rules.len(), - self.domain_rules.len(), - ) - } - - pub fn browser_policy_label(&self) -> String { - format!( - "private={} query={} fragment={} text-needs-url={}", - if self.allow_private_browsing { - "allow" - } else { - "block" - }, - if self.keep_query { "keep" } else { "strip" }, - if self.keep_fragment { "keep" } else { "strip" }, - yes_no(!self.allow_text_without_url), - ) - } - - pub fn vlm_settings(&self) -> Option { - self.vlm_model.as_ref().map(|model_path| VlmSettings { - model_path: model_path.clone(), - model_name: self.vlm_model_name.clone(), - prompt: self.vlm_prompt.clone(), - timeout: Duration::from_secs(self.vlm_timeout_secs), - }) - } - - fn effective_policy_mode(&self) -> PolicyMode { - if self.metadata_only { - PolicyMode::OptIn - } else { - self.policy_mode.unwrap_or(PolicyModeArg::OptOut).into() - } - } -} - -fn parse_app_rule(value: &str) -> Result { - let (bundle_id, access) = split_rule(value)?; - Ok(BundleRule { - bundle_id: bundle_id.to_string(), - access, - }) -} - -fn parse_domain_rule(value: &str) -> Result { - let (domain, access) = split_rule(value)?; - let include_subdomains = domain.starts_with("*."); - let domain = if include_subdomains { - &domain[2..] - } else { - domain - }; - - let domain = domain.trim(); - if domain.is_empty() { - return Err("domain rule is missing a domain".to_string()); - } - - Ok(DomainRule { - domain: domain.to_string(), - include_subdomains, - access, - }) -} - -fn split_rule(value: &str) -> Result<(&str, CaptureAccess), String> { - let Some((key, access)) = value.rsplit_once('=') else { - return Err("expected VALUE=ACCESS".to_string()); - }; - - let key = key.trim(); - if key.is_empty() { - return Err("rule key is empty".to_string()); - } - - let access = AccessArg::from_str(access.trim(), true) - .map(CaptureAccess::from) - .map_err(|_| "access must be one of: none, metadata, url, full".to_string())?; - - Ok((key, access)) -} - -fn policy_mode_label(mode: PolicyMode) -> &'static str { - match mode { - PolicyMode::OptIn => "opt-in", - PolicyMode::OptOut => "opt-out", - } -} - -fn yes_no(value: bool) -> &'static str { - if value { "yes" } else { "no" } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn options() -> Options { - Options { - poll_ms: DEFAULT_POLL_INTERVAL_MS, - mode: RuntimePreference::Auto, - policy_mode: None, - metadata_only: false, - app_rules: Vec::new(), - domain_rules: Vec::new(), - allow_private_browsing: false, - keep_query: false, - keep_fragment: false, - allow_text_without_url: false, - no_emit_initial: false, - no_color: false, - once: false, - screenshot_dwell_ms: DEFAULT_SCREENSHOT_DWELL_MS, - vlm_model: None, - vlm_model_name: DEFAULT_VLM_MODEL_NAME.to_string(), - vlm_prompt: DEFAULT_VLM_PROMPT.to_string(), - vlm_timeout_secs: DEFAULT_VLM_TIMEOUT_SECS, - } - } - - #[test] - fn parses_domain_rule_with_subdomains() { - let rule = parse_domain_rule("*.example.com=full").unwrap(); - - assert_eq!(rule.domain, "example.com"); - assert!(rule.include_subdomains); - assert_eq!(rule.access, CaptureAccess::Full); - } - - #[test] - fn policy_uses_browser_flags_and_rules() { - let mut options = options(); - options.app_rules = vec![BundleRule { - bundle_id: "com.example.app".to_string(), - access: CaptureAccess::Url, - }]; - options.domain_rules = vec![DomainRule { - domain: "example.com".to_string(), - include_subdomains: true, - access: CaptureAccess::Full, - }]; - options.allow_private_browsing = true; - options.keep_query = true; - options.keep_fragment = true; - options.allow_text_without_url = true; - - let policy = options.policy(); - - assert_eq!(policy.mode, PolicyMode::OptOut); - assert_eq!(policy.app_rules.len(), 1); - assert_eq!(policy.browser.rules.len(), 1); - assert!(!policy.browser.block_private_browsing); - assert!(!policy.browser.require_url_for_text_access); - assert!(!policy.browser.strip_fragment); - assert!(!policy.browser.strip_query); - } - - #[test] - fn explicit_watch_mode_errors_when_capability_is_missing() { - let mut options = options(); - options.mode = RuntimePreference::Watch; - - let error = options - .resolve_runtime_mode(Capabilities::default()) - .unwrap_err(); - - assert!(error.contains("does not support watch streams")); - } - - #[test] - fn metadata_only_forces_opt_in_policy() { - let mut options = options(); - options.metadata_only = true; - - assert_eq!(options.policy().mode, PolicyMode::OptIn); - } -} diff --git a/crates/activity-capture-dev/src/theme.rs b/crates/activity-capture-dev/src/theme.rs deleted file mode 100644 index 052b31322b..0000000000 --- a/crates/activity-capture-dev/src/theme.rs +++ /dev/null @@ -1,88 +0,0 @@ -use ratatui::style::{Color, Modifier, Style}; - -use crate::event_row::RowStatus; - -#[derive(Debug, Clone, Copy)] -pub(crate) struct Theme { - colors: bool, -} - -impl Theme { - pub(crate) const fn new(colors: bool) -> Self { - Self { colors } - } - - pub(crate) fn title(self) -> Style { - self.style(Color::Cyan, true) - } - - pub(crate) fn label(self) -> Style { - self.style(Color::Gray, true) - } - - pub(crate) fn timestamp(self) -> Style { - self.style(Color::DarkGray, false) - } - - pub(crate) fn selected_row(self) -> Style { - if self.colors { - Style::default() - .bg(Color::Rgb(32, 45, 66)) - .add_modifier(Modifier::BOLD) - } else { - Style::default().add_modifier(Modifier::REVERSED | Modifier::BOLD) - } - } - - pub(crate) fn range_row(self) -> Style { - if self.colors { - Style::default().bg(Color::Rgb(19, 27, 38)) - } else { - Style::default().add_modifier(Modifier::UNDERLINED) - } - } - - pub(crate) fn status(self, status: RowStatus) -> Style { - match status { - RowStatus::Focus => self.style(Color::Cyan, true), - RowStatus::Update => self.style(Color::Yellow, true), - RowStatus::Idle => self.style(Color::DarkGray, true), - RowStatus::Screenshot => self.style(Color::Green, true), - RowStatus::Vlm => self.style(Color::Magenta, true), - } - } - - pub(crate) fn app(self, app_name: &str) -> Style { - if !self.colors { - return Style::default().add_modifier(Modifier::BOLD); - } - - let palette = [ - Color::Blue, - Color::Magenta, - Color::Cyan, - Color::Green, - Color::Yellow, - Color::Red, - ]; - let index = app_name.bytes().fold(0usize, |acc, byte| { - acc.wrapping_mul(31).wrapping_add(byte as usize) - }) % palette.len(); - - Style::default() - .fg(palette[index]) - .add_modifier(Modifier::BOLD) - } - - fn style(self, color: Color, bold: bool) -> Style { - let mut style = if self.colors { - Style::default().fg(color) - } else { - Style::default() - }; - if bold { - style = style.add_modifier(Modifier::BOLD); - } - style - } -} diff --git a/crates/activity-capture-dev/src/ui.rs b/crates/activity-capture-dev/src/ui.rs deleted file mode 100644 index 4013c51198..0000000000 --- a/crates/activity-capture-dev/src/ui.rs +++ /dev/null @@ -1,304 +0,0 @@ -use std::ops::RangeInclusive; - -use hypr_activity_capture::Capabilities; -use ratatui::{ - DefaultTerminal, Frame, - layout::{Constraint, Layout, Rect}, - text::{Line, Span, Text}, - widgets::{ - Block, HighlightSpacing, List, ListItem, ListState, Paragraph, StatefulWidget, Widget, Wrap, - }, -}; - -use crate::{ - app::{DetailTab, SessionStats, View}, - event_row::EventRow, - formatting::{compact, format_timestamp}, - options::Options, - theme::Theme, -}; - -pub(crate) const APP_PREVIEW_LIMIT: usize = 18; -pub(crate) const APP_COLUMN_MIN_WIDTH: usize = 14; -pub(crate) const APP_COLUMN_MAX_WIDTH: usize = 22; -pub(crate) const TITLE_PREVIEW_LIMIT: usize = 56; -pub(crate) const URL_PREVIEW_LIMIT: usize = 72; -pub(crate) const TEXT_PREVIEW_LIMIT: usize = 40; -pub(crate) const DIFF_PREVIEW_LIMIT: usize = 28; -pub(crate) const STATUS_COLUMN_WIDTH: usize = 6; - -pub(crate) struct ScreenData<'a> { - pub(crate) options: &'a Options, - pub(crate) capabilities: Capabilities, - pub(crate) theme: Theme, - pub(crate) view: View, - pub(crate) detail_tab: DetailTab, - pub(crate) runtime_summary: String, - pub(crate) policy_label: String, - pub(crate) browser_policy_label: String, - pub(crate) capture_summary: String, - pub(crate) session_stats: SessionStats, - pub(crate) events: &'a [EventRow], - pub(crate) selected_index: Option, - pub(crate) selected_range: Option>, - pub(crate) selection_summary: Option, - pub(crate) status_message: Option, - pub(crate) selected_raw_json: Option, - pub(crate) list_state: &'a mut ListState, - pub(crate) list_inner_area: &'a mut Rect, -} - -pub(crate) fn render( - terminal: &mut DefaultTerminal, - mut screen: ScreenData<'_>, -) -> std::io::Result<()> { - terminal.draw(|frame| render_frame(frame, &mut screen))?; - Ok(()) -} - -fn render_frame(frame: &mut Frame, screen: &mut ScreenData<'_>) { - let layout = Layout::vertical([ - Constraint::Length(8), - Constraint::Min(1), - Constraint::Length(1), - ]); - let [header_area, body_area, footer_area] = frame.area().layout(&layout); - - render_header(screen, header_area, frame); - - match screen.view { - View::List => render_list(screen, body_area, frame), - View::Details => render_details(screen, body_area, frame), - } - - Paragraph::new(footer_text(screen.view, screen.detail_tab)) - .render(footer_area, frame.buffer_mut()); -} - -fn render_header(screen: &ScreenData<'_>, area: Rect, frame: &mut Frame) { - let selection = screen.selection_summary.as_deref().unwrap_or("single row"); - let status_message = screen.status_message.as_deref().unwrap_or("No export yet"); - let layout = Layout::vertical([Constraint::Length(3), Constraint::Min(5)]); - let [summary_area, detail_area] = area.layout(&layout); - - Paragraph::new(Line::from(vec![ - Span::styled("activity-capture", screen.theme.title()), - Span::raw(format!(" poll={}ms", screen.options.poll_ms)), - Span::raw(format!(" runtime={}", screen.runtime_summary)), - Span::raw(match screen.view { - View::List => " view=list", - View::Details => " view=details", - }), - ])) - .block(Block::bordered().title("Session")) - .render(summary_area, frame.buffer_mut()); - - let columns = Layout::horizontal([ - Constraint::Fill(3), - Constraint::Fill(4), - Constraint::Fill(4), - ]); - let [stats_area, policy_area, export_area] = detail_area.layout(&columns); - - Paragraph::new(Text::from(vec![ - Line::from(vec![ - Span::styled("events", screen.theme.label()), - Span::raw(format!( - " {} apps {}", - screen.session_stats.event_count, screen.session_stats.distinct_apps - )), - ]), - Line::from(vec![ - Span::styled("focus", screen.theme.label()), - Span::raw(format!( - " {} update {} idle {} snap {} vlm {}", - screen.session_stats.focus_count, - screen.session_stats.update_count, - screen.session_stats.idle_count, - screen.session_stats.screenshot_count, - screen.session_stats.vlm_count, - )), - ]), - Line::from(vec![ - Span::styled("selected", screen.theme.label()), - Span::raw(format!(" {selection}")), - ]), - ])) - .block(Block::bordered().title("Stats")) - .render(stats_area, frame.buffer_mut()); - - Paragraph::new(Text::from(vec![ - Line::from(vec![ - Span::styled("policy", screen.theme.label()), - Span::raw(format!(" {}", screen.policy_label)), - ]), - Line::from(vec![ - Span::styled("browser", screen.theme.label()), - Span::raw(format!(" {}", screen.browser_policy_label)), - ]), - Line::from(vec![ - Span::styled("capture", screen.theme.label()), - Span::raw(format!(" {}", screen.capture_summary)), - ]), - Line::from(vec![ - Span::styled("caps", screen.theme.label()), - Span::raw(format!( - " watch={} text={} url={} ax={}", - yes_no(screen.capabilities.can_watch), - yes_no(screen.capabilities.can_capture_visible_text), - yes_no(screen.capabilities.can_capture_browser_url), - yes_no(screen.capabilities.requires_accessibility_permission), - )), - ]), - ])) - .block(Block::bordered().title("Policy")) - .render(policy_area, frame.buffer_mut()); - - Paragraph::new(Text::from(vec![ - Line::from(vec![ - Span::styled("status", screen.theme.label()), - Span::raw(format!(" {status_message}")), - ]), - Line::from("y copy JSON s save current/range"), - Line::from("S save session r raw JSON v range"), - ])) - .block(Block::bordered().title("Export")) - .wrap(Wrap { trim: false }) - .render(export_area, frame.buffer_mut()); -} - -fn render_list(screen: &mut ScreenData<'_>, area: Rect, frame: &mut Frame) { - let app_width = app_width(screen.events); - let block = Block::bordered().title("Events"); - *screen.list_inner_area = block.inner(area); - - let items = screen - .events - .iter() - .enumerate() - .map(|(index, row)| { - let item = ListItem::new(row_line(row, app_width, screen.theme)); - if screen - .selected_range - .as_ref() - .is_some_and(|range| index >= *range.start() && index <= *range.end()) - { - item.style(screen.theme.range_row()) - } else { - item - } - }) - .collect::>(); - - StatefulWidget::render( - List::new(items) - .block(block) - .highlight_symbol("› ") - .highlight_spacing(HighlightSpacing::Always) - .highlight_style(screen.theme.selected_row()), - area, - frame.buffer_mut(), - screen.list_state, - ); -} - -fn render_details(screen: &mut ScreenData<'_>, area: Rect, frame: &mut Frame) { - *screen.list_inner_area = Rect::default(); - - let Some(row) = selected_row(screen.events, screen.selected_index) else { - Paragraph::new("No event selected") - .block(Block::bordered().title("Details")) - .render(area, frame.buffer_mut()); - return; - }; - - let layout = Layout::vertical([Constraint::Length(3), Constraint::Min(1)]); - let [summary_area, details_area] = area.layout(&layout); - let app_width = app_width(screen.events); - - Paragraph::new(row_line(row, app_width, screen.theme)) - .block(Block::bordered().title("Selected Event")) - .render(summary_area, frame.buffer_mut()); - - match screen.detail_tab { - DetailTab::Details => { - let detail_lines = row - .details - .iter() - .map(|detail| { - Line::from(vec![ - Span::styled(format!("{:>14}: ", detail.label), screen.theme.label()), - Span::raw(detail.value.clone()), - ]) - }) - .collect::>(); - - Paragraph::new(Text::from(detail_lines)) - .block(Block::bordered().title("Details")) - .wrap(Wrap { trim: false }) - .render(details_area, frame.buffer_mut()); - } - DetailTab::Raw => { - let raw = screen - .selected_raw_json - .as_deref() - .unwrap_or("{\"error\":\"no record selected\"}"); - Paragraph::new(raw) - .block(Block::bordered().title("Raw JSON")) - .wrap(Wrap { trim: false }) - .render(details_area, frame.buffer_mut()); - } - } -} - -fn footer_text(view: View, detail_tab: DetailTab) -> &'static str { - match view { - View::List => { - "q/Esc quit • ↑↓ or j/k move • g/G home/end • Enter details • r raw JSON • v toggle range" - } - View::Details => match detail_tab { - DetailTab::Details => { - "q quit • Esc/←/h back • ↑↓ or j/k move • Tab/r raw JSON • s save current/range" - } - DetailTab::Raw => { - "q quit • Esc/←/h back • ↑↓ or j/k move • Tab/d details • s save current/range" - } - }, - } -} - -fn app_width(events: &[EventRow]) -> usize { - events - .iter() - .map(|row| compact(&row.app_name, APP_PREVIEW_LIMIT).chars().count()) - .fold(APP_COLUMN_MIN_WIDTH, usize::max) - .min(APP_COLUMN_MAX_WIDTH) -} - -fn row_line(row: &EventRow, app_width: usize, theme: Theme) -> Line<'static> { - let app_name = compact(&row.app_name, APP_PREVIEW_LIMIT); - let status = row.status.label(); - - Line::from(vec![ - Span::styled(format_timestamp(row.captured_at), theme.timestamp()), - Span::raw(" "), - Span::styled(format!("{app_name:app_width$}"), theme.app(&row.app_name)), - Span::raw(" "), - Span::styled( - format!("{status:STATUS_COLUMN_WIDTH$}"), - theme.status(row.status), - ), - Span::raw(" "), - Span::styled(format!("[{}]", row.context), theme.label()), - Span::raw(" "), - Span::raw(row.summary.clone()), - ]) -} - -fn selected_row(events: &[EventRow], selected_index: Option) -> Option<&EventRow> { - selected_index.and_then(|index| events.get(index)) -} - -fn yes_no(value: bool) -> &'static str { - if value { "yes" } else { "no" } -} diff --git a/crates/activity-capture-dev/src/vlm.rs b/crates/activity-capture-dev/src/vlm.rs deleted file mode 100644 index 7cfa097192..0000000000 --- a/crates/activity-capture-dev/src/vlm.rs +++ /dev/null @@ -1,333 +0,0 @@ -use std::{ - io, - net::{Ipv4Addr, SocketAddr}, - path::{Path, PathBuf}, - sync::mpsc::{self, Receiver, TryRecvError}, - thread, - time::{Duration, SystemTime}, -}; - -use axum::http::StatusCode; -use hypr_activity_capture::ActivityScreenshotCapture; -use hypr_llm_cactus::{CompleteService, ModelManagerBuilder}; -use reqwest::Client; -use serde_json::json; -use tokio::sync::{mpsc as tokio_mpsc, oneshot}; -use url::Url; - -pub(crate) const DEFAULT_VLM_MODEL_NAME: &str = "cactus"; -pub(crate) const DEFAULT_VLM_PROMPT: &str = - "Describe what is visible in this screenshot and infer the active user task in 2-4 sentences."; -pub(crate) const DEFAULT_VLM_TIMEOUT_SECS: u64 = 120; - -#[derive(Debug, Clone)] -pub(crate) struct VlmSettings { - pub(crate) model_path: PathBuf, - pub(crate) model_name: String, - pub(crate) prompt: String, - pub(crate) timeout: Duration, -} - -#[derive(Debug, Clone)] -pub(crate) struct InferenceResult { - pub(crate) screenshot: ActivityScreenshotCapture, - pub(crate) screenshot_path: PathBuf, - pub(crate) model_name: String, - pub(crate) prompt: String, - pub(crate) response: Result, - pub(crate) started_at: SystemTime, - pub(crate) finished_at: SystemTime, -} - -impl InferenceResult { - pub(crate) fn latency(&self) -> Duration { - self.finished_at - .duration_since(self.started_at) - .unwrap_or_default() - } -} - -enum Command { - Infer { - screenshot: ActivityScreenshotCapture, - screenshot_path: PathBuf, - }, - Shutdown, -} - -pub(crate) struct VlmRuntime { - model_name: String, - command_tx: tokio_mpsc::UnboundedSender, - result_rx: Receiver, - handle: Option>, -} - -impl VlmRuntime { - pub(crate) fn spawn(settings: VlmSettings) -> io::Result { - let (command_tx, command_rx) = tokio_mpsc::unbounded_channel(); - let (result_tx, result_rx) = mpsc::channel(); - let (ready_tx, ready_rx) = mpsc::channel(); - let model_name = settings.model_name.clone(); - - let handle = thread::Builder::new() - .name("activity-capture-dev-vlm".to_string()) - .spawn(move || run_worker(settings, command_rx, result_tx, ready_tx)) - .map_err(io::Error::other)?; - - match ready_rx.recv() { - Ok(Ok(())) => Ok(Self { - model_name, - command_tx, - result_rx, - handle: Some(handle), - }), - Ok(Err(error)) => { - let _ = handle.join(); - Err(error) - } - Err(error) => { - let _ = handle.join(); - Err(io::Error::other(error)) - } - } - } - - pub(crate) fn model_name(&self) -> &str { - &self.model_name - } - - pub(crate) fn enqueue( - &self, - screenshot: ActivityScreenshotCapture, - screenshot_path: PathBuf, - ) -> io::Result<()> { - self.command_tx - .send(Command::Infer { - screenshot, - screenshot_path, - }) - .map_err(|error| io::Error::new(io::ErrorKind::BrokenPipe, error.to_string())) - } - - pub(crate) fn drain_results(&mut self) -> Vec { - let mut results = Vec::new(); - loop { - match self.result_rx.try_recv() { - Ok(result) => results.push(result), - Err(TryRecvError::Empty) => break, - Err(TryRecvError::Disconnected) => break, - } - } - results - } -} - -impl Drop for VlmRuntime { - fn drop(&mut self) { - let _ = self.command_tx.send(Command::Shutdown); - if let Some(handle) = self.handle.take() { - let _ = handle.join(); - } - } -} - -fn run_worker( - settings: VlmSettings, - mut command_rx: tokio_mpsc::UnboundedReceiver, - result_tx: mpsc::Sender, - ready_tx: mpsc::Sender>, -) { - let runtime = match tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - { - Ok(runtime) => runtime, - Err(error) => { - let _ = ready_tx.send(Err(io::Error::other(error))); - return; - } - }; - - runtime.block_on(async move { - let server = - match LocalServer::spawn(&settings.model_path, settings.model_name.clone()).await { - Ok(server) => server, - Err(error) => { - let _ = ready_tx.send(Err(error)); - return; - } - }; - - let client = match Client::builder().timeout(settings.timeout).build() { - Ok(client) => client, - Err(error) => { - let _ = ready_tx.send(Err(io::Error::other(error))); - server.stop().await; - return; - } - }; - - let _ = ready_tx.send(Ok(())); - - while let Some(command) = command_rx.recv().await { - match command { - Command::Infer { - screenshot, - screenshot_path, - } => { - let started_at = SystemTime::now(); - let response = run_inference( - &client, - server.completions_url(), - &settings, - &screenshot, - &screenshot_path, - ) - .await; - let finished_at = SystemTime::now(); - - let _ = result_tx.send(InferenceResult { - screenshot, - screenshot_path, - model_name: settings.model_name.clone(), - prompt: settings.prompt.clone(), - response, - started_at, - finished_at, - }); - } - Command::Shutdown => break, - } - } - - server.stop().await; - }); -} - -struct LocalServer { - addr: SocketAddr, - shutdown_tx: Option>, - handle: tokio::task::JoinHandle<()>, -} - -impl LocalServer { - async fn spawn(model_path: &Path, model_name: String) -> io::Result { - let _metadata = std::fs::metadata(model_path).map_err(|error| { - io::Error::new( - io::ErrorKind::NotFound, - format!("model path is not accessible: {error}"), - ) - })?; - - let manager = ModelManagerBuilder::default() - .register(model_name.clone(), model_path.to_path_buf()) - .default_model(model_name) - .build(); - - let router = CompleteService::new(manager).into_router(|error| async move { - (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()) - }); - - let listener = tokio::net::TcpListener::bind((Ipv4Addr::LOCALHOST, 0u16)) - .await - .map_err(io::Error::other)?; - let addr = listener.local_addr().map_err(io::Error::other)?; - let (shutdown_tx, shutdown_rx) = oneshot::channel(); - - let handle = tokio::spawn(async move { - let _ = axum::serve(listener, router) - .with_graceful_shutdown(async { - let _ = shutdown_rx.await; - }) - .await; - }); - - Ok(Self { - addr, - shutdown_tx: Some(shutdown_tx), - handle, - }) - } - - fn completions_url(&self) -> String { - format!("http://{}/v1/chat/completions", self.addr) - } - - async fn stop(mut self) { - if let Some(shutdown_tx) = self.shutdown_tx.take() { - let _ = shutdown_tx.send(()); - } - let _ = self.handle.await; - } -} - -async fn run_inference( - client: &Client, - url: String, - settings: &VlmSettings, - screenshot: &ActivityScreenshotCapture, - screenshot_path: &Path, -) -> Result { - let image_url = Url::from_file_path(screenshot_path) - .map_err(|_| { - format!( - "failed to convert screenshot path to file URL: {}", - screenshot_path.display() - ) - })? - .to_string(); - let response = client - .post(url) - .json(&json!({ - "model": settings.model_name, - "stream": false, - "messages": [ - { - "role": "user", - "content": [ - { - "type": "text", - "text": build_prompt(&settings.prompt, screenshot), - }, - { - "type": "image_url", - "image_url": { - "url": image_url, - } - } - ] - } - ] - })) - .send() - .await - .map_err(|error| error.to_string())?; - - let status = response.status(); - let body = response.text().await.map_err(|error| error.to_string())?; - if !status.is_success() { - return Err(format!("HTTP {status}: {body}")); - } - - parse_response_text(&body) -} - -fn build_prompt(prompt: &str, screenshot: &ActivityScreenshotCapture) -> String { - let title = screenshot.target.title.as_deref().unwrap_or("-"); - - format!( - "{prompt}\n\nContext:\n- app: {}\n- window: {}\n- reason: {:?}\n- fingerprint: {}", - screenshot.target.app_name, title, screenshot.reason, screenshot.fingerprint - ) -} - -fn parse_response_text(body: &str) -> Result { - let value: serde_json::Value = - serde_json::from_str(body).map_err(|error| format!("invalid JSON response: {error}"))?; - value["choices"][0]["message"]["content"] - .as_str() - .map(str::trim) - .filter(|content| !content.is_empty()) - .map(ToOwned::to_owned) - .ok_or_else(|| "response did not include assistant content".to_string()) -} diff --git a/crates/activity-capture-dev/src/widgets/header.rs b/crates/activity-capture-dev/src/widgets/header.rs deleted file mode 100644 index 1dc6f1bd84..0000000000 --- a/crates/activity-capture-dev/src/widgets/header.rs +++ /dev/null @@ -1,117 +0,0 @@ -use hypr_activity_capture::Capabilities; -use ratatui::{ - buffer::Buffer, - layout::Rect, - text::{Line, Span, Text}, - widgets::{Block, Paragraph, Widget}, -}; - -use crate::{ - app::{SessionStats, View}, - options::Options, - theme::Theme, -}; - -pub(super) struct SessionHeader<'a> { - options: &'a Options, - capabilities: Capabilities, - theme: Theme, - view: View, - runtime_summary: &'a str, - policy_label: &'a str, - browser_policy_label: &'a str, - session_stats: SessionStats, - selection_summary: Option<&'a str>, - status_message: Option<&'a str>, -} - -impl<'a> SessionHeader<'a> { - pub(super) fn new( - options: &'a Options, - capabilities: Capabilities, - theme: Theme, - view: View, - runtime_summary: &'a str, - policy_label: &'a str, - browser_policy_label: &'a str, - session_stats: SessionStats, - selection_summary: Option<&'a str>, - status_message: Option<&'a str>, - ) -> Self { - Self { - options, - capabilities, - theme, - view, - runtime_summary, - policy_label, - browser_policy_label, - session_stats, - selection_summary, - status_message, - } - } -} - -impl Widget for SessionHeader<'_> { - fn render(self, area: Rect, buf: &mut Buffer) { - let selection = self.selection_summary.unwrap_or("single row"); - let export = self.status_message.unwrap_or( - "y copy JSON s save selection/current S save full session r raw JSON v range", - ); - - let lines = vec![ - Line::from(vec![ - Span::styled("activity-capture", self.theme.title()), - Span::raw(format!(" poll={}ms", self.options.poll_ms)), - Span::raw(format!(" runtime={}", self.runtime_summary)), - Span::raw(match self.view { - View::List => " view=list", - View::Details => " view=details", - }), - ]), - Line::from(vec![ - Span::styled("session", self.theme.label()), - Span::raw(format!( - " events={} apps={} focus={} update={} idle={} snap={} selected={selection}", - self.session_stats.event_count, - self.session_stats.distinct_apps, - self.session_stats.focus_count, - self.session_stats.update_count, - self.session_stats.idle_count, - self.session_stats.screenshot_count, - )), - ]), - Line::from(vec![ - Span::styled("policy", self.theme.label()), - Span::raw(format!(" {}", self.policy_label)), - ]), - Line::from(vec![ - Span::styled("browser", self.theme.label()), - Span::raw(format!(" {}", self.browser_policy_label)), - ]), - Line::from(vec![ - Span::styled("capabilities", self.theme.label()), - Span::raw(format!( - " watch={} text={} url={} ax={}", - yes_no(self.capabilities.can_watch), - yes_no(self.capabilities.can_capture_visible_text), - yes_no(self.capabilities.can_capture_browser_url), - yes_no(self.capabilities.requires_accessibility_permission), - )), - ]), - Line::from(vec![ - Span::styled("export", self.theme.label()), - Span::raw(format!(" {export}")), - ]), - ]; - - Paragraph::new(Text::from(lines)) - .block(Block::bordered().title("Session")) - .render(area, buf); - } -} - -fn yes_no(value: bool) -> &'static str { - if value { "yes" } else { "no" } -} diff --git a/crates/agc/Cargo.toml b/crates/agc/Cargo.toml deleted file mode 100644 index 2ad19b1a55..0000000000 --- a/crates/agc/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "agc" -version = "0.1.0" -edition = "2024" - -[dev-dependencies] -hound = { workspace = true } -hypr-data = { workspace = true } -rodio = { workspace = true } - -[dependencies] -hypr-audio-utils = { workspace = true } -hypr-vad-masking = { workspace = true } - -dagc = "0.1.1" diff --git a/crates/agc/src/lib.rs b/crates/agc/src/lib.rs deleted file mode 100644 index 6c712115fb..0000000000 --- a/crates/agc/src/lib.rs +++ /dev/null @@ -1,131 +0,0 @@ -use dagc::MonoAgc; -use hypr_vad_masking::{StreamingVad, VadConfig}; - -pub struct VadAgc { - agc: MonoAgc, - vad: Option, - vad_cfg: VadConfig, - mask_non_speech: bool, -} - -impl VadAgc { - pub fn new(desired_output_rms: f32, distortion_factor: f32) -> Self { - Self { - agc: MonoAgc::new(desired_output_rms, distortion_factor).expect("failed_to_create_agc"), - vad: None, - vad_cfg: VadConfig::default(), - mask_non_speech: false, - } - } - - pub fn with_masking(mut self, mask_non_speech: bool) -> Self { - self.mask_non_speech = mask_non_speech; - self - } - - pub fn with_vad_config(mut self, cfg: VadConfig) -> Self { - self.vad_cfg = cfg; - self - } - - pub fn process(&mut self, samples: &mut [f32]) { - if samples.is_empty() { - return; - } - - let vad = self - .vad - .get_or_insert_with(|| StreamingVad::with_config(samples.len(), self.vad_cfg.clone())); - - let agc = &mut self.agc; - let mask_non_speech = self.mask_non_speech; - - vad.process_in_place(samples, |frame, is_speech| { - agc.freeze_gain(!is_speech); - if !is_speech && mask_non_speech { - frame.fill(0.0); - } - agc.process(frame); - }); - } - - pub fn gain(&self) -> f32 { - self.agc.gain() - } -} - -impl Default for VadAgc { - fn default() -> Self { - Self::new(0.03, 0.0001) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_agc() { - let input_audio = rodio::Decoder::try_from( - std::fs::File::open(hypr_data::english_1::AUDIO_PATH).unwrap(), - ) - .unwrap(); - let original_samples: Vec = input_audio.collect(); - - let mut agc = VadAgc::default(); - - let mut processed_samples = Vec::new(); - let chunks = original_samples.chunks(512); - - for chunk in chunks { - let mut target = chunk.to_vec(); - agc.process(&mut target); - - for &sample in &target { - processed_samples.push(sample); - } - } - - let wav = hound::WavSpec { - channels: 1, - sample_rate: 16000, - bits_per_sample: 32, - sample_format: hound::SampleFormat::Float, - }; - let mut writer = hound::WavWriter::create("./test.wav", wav).unwrap(); - for sample in processed_samples { - writer.write_sample(sample).unwrap(); - } - } - - #[test] - fn test_cross_call_framing() { - let input_audio = rodio::Decoder::try_from( - std::fs::File::open(hypr_data::english_1::AUDIO_PATH).unwrap(), - ) - .unwrap(); - let original_samples: Vec = input_audio.collect(); - - let mut agc = VadAgc::default(); - let mut processed: Vec = Vec::new(); - for chunk in original_samples.chunks(200) { - let mut target = chunk.to_vec(); - agc.process(&mut target); - processed.extend_from_slice(&target); - } - - assert_eq!(processed.len(), original_samples.len()); - - for &sample in &processed { - assert!(sample.is_finite(), "Sample is not finite"); - } - - let rms: f32 = processed.iter().map(|&s| s * s).sum::() / processed.len() as f32; - let rms = rms.sqrt(); - assert!( - rms > 0.0 && rms < 1.0, - "RMS {} is out of expected range", - rms - ); - } -} diff --git a/crates/amp/Cargo.toml b/crates/amp/Cargo.toml deleted file mode 100644 index 7e18c59570..0000000000 --- a/crates/amp/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "amp" -version = "0.1.0" -edition = "2024" - -[dependencies] -dirs = { workspace = true } -futures-util = { workspace = true } -hypr-cli-process = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -tempfile = { workspace = true } -thiserror = { workspace = true } -tokio = { workspace = true, features = ["io-util", "macros", "process", "rt", "sync"] } -tokio-stream = { workspace = true } -tokio-util = { workspace = true } diff --git a/crates/amp/src/config.rs b/crates/amp/src/config.rs deleted file mode 100644 index ee5c1b28cc..0000000000 --- a/crates/amp/src/config.rs +++ /dev/null @@ -1,60 +0,0 @@ -use std::path::{Path, PathBuf}; - -pub fn settings_path() -> PathBuf { - dirs::config_dir() - .expect("could not determine config directory") - .join("amp") - .join("settings.json") -} - -pub fn read_settings(path: &Path) -> Result { - match std::fs::read_to_string(path) { - Ok(contents) => serde_json::from_str(&contents) - .map_err(|e| format!("failed to parse {}: {e}", path.display())), - Err(e) if e.kind() == std::io::ErrorKind::NotFound => { - Ok(serde_json::Value::Object(serde_json::Map::new())) - } - Err(e) => Err(format!("failed to read {}: {e}", path.display())), - } -} - -pub fn write_settings(path: &Path, value: &serde_json::Value) -> Result<(), String> { - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent) - .map_err(|e| format!("failed to create {}: {e}", parent.display()))?; - } - let contents = serde_json::to_string_pretty(value) - .map_err(|e| format!("failed to serialize settings: {e}"))?; - std::fs::write(path, contents).map_err(|e| format!("failed to write {}: {e}", path.display())) -} - -#[cfg(test)] -mod tests { - use serde_json::json; - - use super::{read_settings, write_settings}; - - #[test] - fn read_settings_returns_empty_object_when_missing() { - let temp_dir = tempfile::tempdir().expect("tempdir"); - let path = temp_dir.path().join("settings.json"); - - assert_eq!( - read_settings(&path).expect("settings"), - serde_json::Value::Object(serde_json::Map::new()) - ); - } - - #[test] - fn write_settings_round_trips() { - let temp_dir = tempfile::tempdir().expect("tempdir"); - let path = temp_dir.path().join("nested").join("settings.json"); - let settings = json!({ - "theme": "dark", - "permissions": ["allow"] - }); - - write_settings(&path, &settings).expect("write"); - assert_eq!(read_settings(&path).expect("read"), settings); - } -} diff --git a/crates/amp/src/error.rs b/crates/amp/src/error.rs deleted file mode 100644 index 4ad47ce744..0000000000 --- a/crates/amp/src/error.rs +++ /dev/null @@ -1,46 +0,0 @@ -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("failed to spawn amp: {0}")] - Spawn(#[source] std::io::Error), - #[error("failed to kill amp process: {0}")] - Kill(#[source] std::io::Error), - #[error("amp process missing stdin")] - MissingStdin, - #[error("amp process missing stdout")] - MissingStdout, - #[error("failed to write prompt to amp stdin: {0}")] - StdinWrite(#[source] std::io::Error), - #[error("failed to read amp stdout: {0}")] - StdoutRead(#[source] std::io::Error), - #[error("failed to wait for amp process: {0}")] - Wait(#[source] std::io::Error), - #[error("failed to parse event JSON: {0}")] - ParseEvent(#[from] serde_json::Error), - #[error("failed to create settings file: {0}")] - SettingsIo(#[source] std::io::Error), - #[error("amp exec exited unsuccessfully: {detail}")] - ProcessFailed { detail: String }, - #[error("turn cancelled")] - Cancelled, - #[error("turn failed: {0}")] - TurnFailed(String), - #[error("mutex poisoned")] - Poisoned, -} - -impl From for Error { - fn from(value: hypr_cli_process::ProcessError) -> Self { - match value { - hypr_cli_process::ProcessError::MissingStdin => Self::MissingStdin, - hypr_cli_process::ProcessError::MissingStdout => Self::MissingStdout, - hypr_cli_process::ProcessError::StdinWrite(error) => Self::StdinWrite(error), - hypr_cli_process::ProcessError::StdoutRead(error) => Self::StdoutRead(error), - hypr_cli_process::ProcessError::Wait(error) => Self::Wait(error), - hypr_cli_process::ProcessError::Kill(error) => Self::Kill(error), - hypr_cli_process::ProcessError::ProcessFailed { detail } => { - Self::ProcessFailed { detail } - } - hypr_cli_process::ProcessError::Cancelled => Self::Cancelled, - } - } -} diff --git a/crates/amp/src/events.rs b/crates/amp/src/events.rs deleted file mode 100644 index 91cd1684b7..0000000000 --- a/crates/amp/src/events.rs +++ /dev/null @@ -1,390 +0,0 @@ -use std::path::PathBuf; -use std::pin::Pin; - -use futures_util::Stream; -use serde::{Deserialize, Serialize}; - -use crate::error::Error; - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct Usage { - pub input_tokens: u64, - #[serde(skip_serializing_if = "Option::is_none")] - pub cache_creation_input_tokens: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub cache_read_input_tokens: Option, - pub output_tokens: u64, - #[serde(skip_serializing_if = "Option::is_none")] - pub service_tier: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct TextContent { - #[serde(rename = "type")] - pub content_type: String, - pub text: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct ToolUseContent { - #[serde(rename = "type")] - pub content_type: String, - pub id: String, - pub name: String, - pub input: serde_json::Map, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct ToolResultContent { - #[serde(rename = "type")] - pub content_type: String, - pub tool_use_id: String, - pub content: String, - pub is_error: bool, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(tag = "type")] -pub enum ContentBlock { - #[serde(rename = "text")] - Text { text: String }, - #[serde(rename = "tool_use")] - ToolUse { - id: String, - name: String, - input: serde_json::Map, - }, - #[serde(rename = "tool_result")] - ToolResult { - tool_use_id: String, - content: String, - is_error: bool, - }, - #[serde(rename = "image")] - Image { - source_path: String, - #[serde(skip_serializing_if = "Option::is_none")] - source: Option, - }, - #[serde(other)] - Unknown, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct MessageEnvelope { - pub role: String, - pub content: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub id: Option, - #[serde(rename = "type", skip_serializing_if = "Option::is_none")] - pub message_type: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub model: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub stop_reason: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub stop_sequence: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub usage: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct McpServer { - pub name: String, - pub status: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct SystemInitMessage { - pub subtype: String, - pub session_id: String, - pub cwd: String, - pub tools: Vec, - pub mcp_servers: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct AssistantMessage { - pub session_id: String, - pub message: MessageEnvelope, - pub parent_tool_use_id: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct UserMessage { - pub session_id: String, - pub message: MessageEnvelope, - pub parent_tool_use_id: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct ResultBase { - pub session_id: String, - pub duration_ms: u64, - pub num_turns: u64, - #[serde(skip_serializing_if = "Option::is_none")] - pub usage: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub permission_denials: Option>, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct ResultMessage { - #[serde(flatten)] - pub base: ResultBase, - pub result: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct ErrorResult { - #[serde(flatten)] - pub base: ResultBase, - pub error: String, - pub subtype: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub enum ThreadEvent { - SystemInit(SystemInitMessage), - Assistant(AssistantMessage), - User(UserMessage), - Result(ResultMessage), - ErrorResult(ErrorResult), - Unknown(serde_json::Value), -} - -impl ThreadEvent { - pub fn session_id(&self) -> Option<&str> { - match self { - Self::SystemInit(message) => Some(&message.session_id), - Self::Assistant(message) => Some(&message.session_id), - Self::User(message) => Some(&message.session_id), - Self::Result(message) => Some(&message.base.session_id), - Self::ErrorResult(message) => Some(&message.base.session_id), - Self::Unknown(_) => None, - } - } -} - -impl TryFrom for ThreadEvent { - type Error = Error; - - fn try_from(value: serde_json::Value) -> Result { - let Some(event_type) = value.get("type").and_then(serde_json::Value::as_str) else { - return Ok(Self::Unknown(value)); - }; - - match event_type { - "system" => { - if value.get("subtype").and_then(serde_json::Value::as_str) == Some("init") { - Ok(Self::SystemInit(serde_json::from_value(value)?)) - } else { - Ok(Self::Unknown(value)) - } - } - "assistant" => Ok(Self::Assistant(serde_json::from_value(value)?)), - "user" => Ok(Self::User(serde_json::from_value(value)?)), - "result" => { - if value.get("is_error").and_then(serde_json::Value::as_bool) == Some(true) { - Ok(Self::ErrorResult(serde_json::from_value(value)?)) - } else { - Ok(Self::Result(serde_json::from_value(value)?)) - } - } - _ => Ok(Self::Unknown(value)), - } - } -} - -#[derive(Debug, Clone)] -pub enum UserInput { - Text(String), - LocalImage(PathBuf), - Message(UserInputMessage), -} - -impl From<&str> for UserInput { - fn from(value: &str) -> Self { - Self::Text(value.to_string()) - } -} - -impl From for UserInput { - fn from(value: String) -> Self { - Self::Text(value) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct UserInputMessage { - #[serde(rename = "type")] - pub message_type: String, - pub message: MessageEnvelope, -} - -impl UserInputMessage { - pub fn text(text: impl Into) -> Self { - Self { - message_type: "user".to_string(), - message: MessageEnvelope { - role: "user".to_string(), - content: vec![ContentBlock::Text { text: text.into() }], - id: None, - message_type: None, - model: None, - stop_reason: None, - stop_sequence: None, - usage: None, - }, - } - } -} - -#[derive(Debug, Clone)] -pub enum Input { - Text(String), - Items(Vec), -} - -impl Input { - pub(crate) fn normalize(self) -> Result { - match self { - Self::Text(text) => Ok(NormalizedInput::Text(text)), - Self::Items(items) => { - let mut lines = Vec::new(); - let mut content = Vec::new(); - - for item in items { - match item { - UserInput::Text(text) => content.push(ContentBlock::Text { text }), - UserInput::LocalImage(path) => content.push(ContentBlock::Image { - source_path: format!("file://{}", path.display()), - source: None, - }), - UserInput::Message(message) => { - lines.push(serde_json::to_string(&message)?); - } - } - } - - if !content.is_empty() { - lines.push(serde_json::to_string(&UserInputMessage { - message_type: "user".to_string(), - message: MessageEnvelope { - role: "user".to_string(), - content, - id: None, - message_type: None, - model: None, - stop_reason: None, - stop_sequence: None, - usage: None, - }, - })?); - } - - Ok(NormalizedInput::StreamJson(lines.join("\n"))) - } - } - } -} - -impl From for Input { - fn from(value: String) -> Self { - Self::Text(value) - } -} - -impl From<&str> for Input { - fn from(value: &str) -> Self { - Self::Text(value.to_string()) - } -} - -impl From> for Input { - fn from(value: Vec) -> Self { - Self::Items(value) - } -} - -pub(crate) enum NormalizedInput { - Text(String), - StreamJson(String), -} - -#[derive(Debug, Clone)] -pub struct Turn { - pub events: Vec, - pub final_response: String, - pub usage: Option, -} - -pub type EventStream = Pin> + Send>>; - -pub struct RunStreamedResult { - pub events: EventStream, -} - -pub type StreamMessage = ThreadEvent; - -#[cfg(test)] -mod tests { - use std::path::PathBuf; - - use serde_json::json; - - use super::{ContentBlock, Input, ThreadEvent, UserInput, UserInputMessage}; - - #[test] - fn serializes_text_only_input_message() { - let message = UserInputMessage::text("Analyze this code"); - let value = serde_json::to_value(message).expect("serialize"); - assert_eq!(value["type"], "user"); - assert_eq!(value["message"]["content"][0]["type"], "text"); - assert_eq!(value["message"]["content"][0]["text"], "Analyze this code"); - } - - #[test] - fn normalizes_text_and_image_input_to_stream_json() { - let input = Input::from(vec![ - UserInput::Text("what do you see?".to_string()), - UserInput::LocalImage(PathBuf::from("/tmp/example.jpg")), - ]); - - let normalized = input.normalize().expect("normalize"); - let crate::events::NormalizedInput::StreamJson(line) = normalized else { - panic!("expected stream json"); - }; - - let value: serde_json::Value = serde_json::from_str(&line).expect("json"); - assert_eq!(value["message"]["content"][0]["type"], "text"); - assert_eq!(value["message"]["content"][1]["type"], "image"); - assert_eq!( - value["message"]["content"][1]["source_path"], - "file:///tmp/example.jpg" - ); - } - - #[test] - fn preserves_unknown_events() { - let event = ThreadEvent::try_from(json!({ - "type": "system", - "subtype": "mystery", - "session_id": "T-1" - })) - .expect("event"); - - assert!(matches!(event, ThreadEvent::Unknown(_))); - } - - #[test] - fn content_block_text_serialization_matches_schema() { - let value = serde_json::to_value(ContentBlock::Text { - text: "hello".to_string(), - }) - .expect("serialize"); - - assert_eq!(value["type"], "text"); - assert_eq!(value["text"], "hello"); - } -} diff --git a/crates/amp/src/exec.rs b/crates/amp/src/exec.rs deleted file mode 100644 index ecdf019cdd..0000000000 --- a/crates/amp/src/exec.rs +++ /dev/null @@ -1,202 +0,0 @@ -use std::collections::BTreeMap; -use std::path::PathBuf; -use std::process::Stdio; - -use hypr_cli_process::{spawn_streaming_lines, spawn_with_retry}; -use tokio::process::Command; -use tokio_util::sync::CancellationToken; - -use crate::error::Error; -use crate::events::{EventStream, NormalizedInput, ThreadEvent}; -use crate::options::AmpMode; -#[derive(Debug, Clone)] -pub(crate) struct AmpExec { - executable_path: PathBuf, - env_override: Option>, -} - -pub(crate) struct AmpExecRun { - pub events: EventStream, - pub shutdown: CancellationToken, -} - -pub(crate) struct AmpExecArgs { - pub input: NormalizedInput, - pub amp_api_key: Option, - pub thread_id: Option, - pub mode: Option, - pub working_directory: Option, - pub include_thinking_stream: bool, - pub settings_file: Option, - pub cancellation_token: Option, -} - -impl AmpExec { - pub(crate) fn new( - executable_path: Option, - env_override: Option>, - ) -> Self { - Self { - executable_path: executable_path.unwrap_or_else(|| PathBuf::from("amp")), - env_override, - } - } - - pub(crate) fn run(&self, args: AmpExecArgs) -> Result { - if args - .cancellation_token - .as_ref() - .is_some_and(CancellationToken::is_cancelled) - { - return Err(Error::Cancelled); - } - - let command_args = self.command_args(&args); - let mut command = Command::new(&self.executable_path); - command.args(command_args); - command.stdin(Stdio::piped()); - command.stdout(Stdio::piped()); - command.stderr(Stdio::piped()); - command.kill_on_drop(true); - - if let Some(env) = &self.env_override { - command.env_clear(); - for (key, value) in env { - command.env(key, value); - } - } - - if let Some(api_key) = &args.amp_api_key { - command.env("AMP_API_KEY", api_key); - } - - if let Some(working_directory) = &args.working_directory { - command.current_dir(working_directory); - } - - let child = spawn_with_retry(&mut command).map_err(Error::Spawn)?; - let prompt = match args.input { - NormalizedInput::Text(text) => text, - NormalizedInput::StreamJson(text) => text, - }; - let stream = spawn_streaming_lines(child, Some(prompt), args.cancellation_token, |line| { - let value = serde_json::from_str::(&line)?; - Ok(ThreadEvent::try_from(value)?) - })?; - - Ok(AmpExecRun { - events: stream.events, - shutdown: stream.shutdown, - }) - } - - pub(crate) fn command_args(&self, args: &AmpExecArgs) -> Vec { - let mut command_args = Vec::new(); - - if let Some(thread_id) = &args.thread_id { - command_args.push("threads".to_string()); - command_args.push("continue".to_string()); - command_args.push(thread_id.clone()); - } - - command_args.push("--execute".to_string()); - - if let Some(mode) = args.mode { - command_args.push("--mode".to_string()); - command_args.push(serde_variant(&mode)); - } - - command_args.push("--stream-json".to_string()); - - if args.include_thinking_stream { - command_args.push("--stream-json-thinking".to_string()); - } - - if matches!(args.input, NormalizedInput::StreamJson(_)) { - command_args.push("--stream-json-input".to_string()); - } - - if let Some(settings_file) = &args.settings_file { - command_args.push("--settings-file".to_string()); - command_args.push(settings_file.display().to_string()); - } - - command_args - } -} - -fn serde_variant(value: &AmpMode) -> String { - serde_json::to_value(value) - .ok() - .and_then(|value| value.as_str().map(ToOwned::to_owned)) - .unwrap_or_else(|| "smart".to_string()) -} - -#[cfg(test)] -mod tests { - use std::collections::BTreeMap; - use std::path::PathBuf; - - use tokio_util::sync::CancellationToken; - - use super::{AmpExec, AmpExecArgs}; - use crate::events::NormalizedInput; - use crate::options::AmpMode; - - #[test] - fn preserves_command_arg_shape() { - let exec = AmpExec::new(Some(PathBuf::from("/bin/amp")), Some(BTreeMap::new())); - let args = AmpExecArgs { - input: NormalizedInput::Text("hello".to_string()), - amp_api_key: Some("key".to_string()), - thread_id: Some("T-1".to_string()), - mode: Some(AmpMode::Deep), - working_directory: Some(PathBuf::from("/tmp/workspace")), - include_thinking_stream: true, - settings_file: Some(PathBuf::from("/tmp/settings.json")), - cancellation_token: Some(CancellationToken::new()), - }; - - assert_eq!( - exec.command_args(&args), - vec![ - "threads".to_string(), - "continue".to_string(), - "T-1".to_string(), - "--execute".to_string(), - "--mode".to_string(), - "deep".to_string(), - "--stream-json".to_string(), - "--stream-json-thinking".to_string(), - "--settings-file".to_string(), - "/tmp/settings.json".to_string(), - ] - ); - } - - #[test] - fn adds_stream_json_input_flag_for_structured_input() { - let exec = AmpExec::new(Some(PathBuf::from("/bin/amp")), None); - let args = AmpExecArgs { - input: NormalizedInput::StreamJson("{}".to_string()), - amp_api_key: None, - thread_id: None, - mode: Some(AmpMode::Smart), - working_directory: None, - include_thinking_stream: false, - settings_file: None, - cancellation_token: None, - }; - - assert_eq!( - exec.command_args(&args), - vec![ - "--execute".to_string(), - "--mode".to_string(), - "smart".to_string(), - "--stream-json".to_string(), - "--stream-json-input".to_string(), - ] - ); - } -} diff --git a/crates/amp/src/health.rs b/crates/amp/src/health.rs deleted file mode 100644 index b86b6eb310..0000000000 --- a/crates/amp/src/health.rs +++ /dev/null @@ -1,239 +0,0 @@ -use std::path::PathBuf; -use std::process::{Command, Output}; - -use serde::{Deserialize, Serialize}; - -use crate::options::AmpOptions; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum HealthStatus { - Ready, - Warning, - Error, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum AmpAuthStatus { - Authenticated, - Unauthenticated, - Unknown, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct HealthCheck { - pub binary_path: PathBuf, - pub installed: bool, - pub version: Option, - pub status: HealthStatus, - pub auth_status: AmpAuthStatus, - pub message: Option, -} - -pub fn health_check() -> HealthCheck { - health_check_with_options(&AmpOptions::default()) -} - -pub fn health_check_with_options(options: &AmpOptions) -> HealthCheck { - let binary_path = options - .amp_path_override - .clone() - .unwrap_or_else(|| PathBuf::from("amp")); - - let version_output = match run_command(&binary_path, options, &["--version"]) { - Ok(output) => output, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => { - return HealthCheck { - binary_path, - installed: false, - version: None, - status: HealthStatus::Error, - auth_status: AmpAuthStatus::Unknown, - message: Some("Amp CLI (`amp`) is not installed or not on PATH.".to_string()), - }; - } - Err(error) => { - return HealthCheck { - binary_path, - installed: false, - version: None, - status: HealthStatus::Error, - auth_status: AmpAuthStatus::Unknown, - message: Some(format!("Failed to execute Amp CLI health check: {error}.")), - }; - } - }; - - let version_text = combined_output(&version_output); - let version = parse_version(&version_text); - - if !version_output.status.success() { - return HealthCheck { - binary_path, - installed: true, - version, - status: HealthStatus::Error, - auth_status: AmpAuthStatus::Unknown, - message: Some(format!( - "Amp CLI is installed but failed to run. {}", - detail_from_output(&version_output) - )), - }; - } - - HealthCheck { - binary_path, - installed: true, - version, - status: HealthStatus::Ready, - auth_status: AmpAuthStatus::Unknown, - message: Some( - "Amp authentication status could not be verified non-interactively.".to_string(), - ), - } -} - -fn run_command( - binary_path: &PathBuf, - options: &AmpOptions, - args: &[&str], -) -> Result { - let mut command = Command::new(binary_path); - command.args(args); - - if let Some(env) = &options.env { - command.env_clear(); - command.envs(env); - } - - if let Some(api_key) = &options.api_key { - command.env("AMP_API_KEY", api_key); - } - - command.output() -} - -fn combined_output(output: &Output) -> String { - let stdout = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); - if stderr.is_empty() { - stdout.into_owned() - } else if stdout.is_empty() { - stderr.into_owned() - } else { - format!("{stdout}\n{stderr}") - } -} - -fn detail_from_output(output: &Output) -> String { - let detail = combined_output(output).trim().to_string(); - if detail.is_empty() { - match output.status.code() { - Some(code) => format!("Command exited with code {code}."), - None => "Command exited unsuccessfully.".to_string(), - } - } else { - detail - } -} - -fn parse_version(output: &str) -> Option { - output - .split(|c: char| c.is_whitespace()) - .find_map(normalize_semver_token) -} - -fn normalize_semver_token(token: &str) -> Option { - let trimmed = - token.trim_matches(|c: char| !(c.is_ascii_alphanumeric() || c == '.' || c == '-')); - let trimmed = trimmed.trim_start_matches('v'); - let mut parts = trimmed.split('.'); - let major = parts.next()?; - let minor = parts.next()?; - let patch = parts.next()?; - - if !(major.chars().all(|c| c.is_ascii_digit()) - && minor.chars().all(|c| c.is_ascii_digit()) - && patch - .chars() - .take_while(|c| *c != '-') - .all(|c| c.is_ascii_digit())) - { - return None; - } - - Some(trimmed.to_string()) -} - -#[cfg(test)] -mod tests { - use std::collections::BTreeMap; - use std::fs; - use std::path::PathBuf; - - use tempfile::TempDir; - - use super::{AmpAuthStatus, HealthStatus, health_check_with_options, normalize_semver_token}; - use crate::options::AmpOptions; - - #[test] - fn parses_semver_tokens() { - assert_eq!(normalize_semver_token("amp-cli-0.23.1"), None); - assert_eq!( - normalize_semver_token("v0.23.1"), - Some("0.23.1".to_string()) - ); - assert_eq!(normalize_semver_token("0.23.1"), Some("0.23.1".to_string())); - } - - #[test] - #[cfg(unix)] - fn health_check_respects_env_override_without_leaking_parent_env() { - let temp_dir = tempfile::tempdir().expect("tempdir"); - let script = write_fake_amp_script( - &temp_dir, - r#" -[ -z "${PARENT_ONLY:-}" ] || exit 11 -[ "${CUSTOM_ENV:-}" = "custom" ] || exit 12 -printf 'amp 0.23.1\n' -"#, - ); - - unsafe { - std::env::set_var("PARENT_ONLY", "leak"); - } - - let health = health_check_with_options(&AmpOptions { - amp_path_override: Some(script), - api_key: None, - settings_overrides: None, - env: Some(BTreeMap::from([( - "CUSTOM_ENV".to_string(), - "custom".to_string(), - )])), - }); - - unsafe { - std::env::remove_var("PARENT_ONLY"); - } - - assert_eq!(health.status, HealthStatus::Ready); - assert_eq!(health.auth_status, AmpAuthStatus::Unknown); - assert_eq!(health.version.as_deref(), Some("0.23.1")); - } - - #[cfg(unix)] - fn write_fake_amp_script(temp_dir: &TempDir, body: &str) -> PathBuf { - use std::os::unix::fs::PermissionsExt; - - let path = temp_dir.path().join("amp"); - let script = format!("#!/bin/sh\nset -eu\n{body}"); - fs::write(&path, script).expect("write fake amp"); - let mut permissions = fs::metadata(&path).expect("metadata").permissions(); - permissions.set_mode(0o755); - fs::set_permissions(&path, permissions).expect("chmod"); - path - } -} diff --git a/crates/amp/src/lib.rs b/crates/amp/src/lib.rs deleted file mode 100644 index 19717c1711..0000000000 --- a/crates/amp/src/lib.rs +++ /dev/null @@ -1,22 +0,0 @@ -mod config; -mod error; -mod events; -mod exec; -mod health; -mod options; -mod settings; -mod thread; - -pub use config::{read_settings, settings_path, write_settings}; -pub use error::Error; -pub use events::{ - AssistantMessage, ContentBlock, ErrorResult, EventStream, Input, MessageEnvelope, ResultBase, - ResultMessage, RunStreamedResult, StreamMessage, SystemInitMessage, TextContent, ThreadEvent, - ToolResultContent, ToolUseContent, Turn, Usage, UserInput, UserInputMessage, UserMessage, -}; -pub use health::{ - AmpAuthStatus as HealthAuthStatus, HealthCheck, HealthStatus, health_check, - health_check_with_options, -}; -pub use options::{AmpMode, AmpOptions, ThreadOptions, TurnOptions}; -pub use thread::{Amp, Thread}; diff --git a/crates/amp/src/options.rs b/crates/amp/src/options.rs deleted file mode 100644 index 923773c2ec..0000000000 --- a/crates/amp/src/options.rs +++ /dev/null @@ -1,33 +0,0 @@ -use std::collections::BTreeMap; -use std::path::PathBuf; - -use serde::{Deserialize, Serialize}; -use tokio_util::sync::CancellationToken; - -#[derive(Debug, Clone, Default)] -pub struct AmpOptions { - pub amp_path_override: Option, - pub api_key: Option, - pub settings_overrides: Option, - pub env: Option>, -} - -#[derive(Debug, Clone, Default)] -pub struct ThreadOptions { - pub mode: Option, - pub working_directory: Option, -} - -#[derive(Debug, Clone, Default)] -pub struct TurnOptions { - pub cancellation_token: Option, - pub include_thinking_stream: bool, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum AmpMode { - Smart, - Rush, - Deep, -} diff --git a/crates/amp/src/settings.rs b/crates/amp/src/settings.rs deleted file mode 100644 index 1a70987221..0000000000 --- a/crates/amp/src/settings.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::path::{Path, PathBuf}; - -use tempfile::TempDir; - -use crate::error::Error; - -#[derive(Debug)] -pub(crate) struct SettingsFile { - _temp_dir: TempDir, - path: PathBuf, -} - -impl SettingsFile { - pub(crate) fn path(&self) -> &Path { - &self.path - } -} - -pub(crate) fn create_settings_file( - settings: Option<&serde_json::Value>, -) -> Result, Error> { - let Some(settings) = settings else { - return Ok(None); - }; - - let temp_dir = tempfile::tempdir().map_err(Error::SettingsIo)?; - let path = temp_dir.path().join("settings.json"); - let contents = serde_json::to_vec(settings)?; - std::fs::write(&path, contents).map_err(Error::SettingsIo)?; - - Ok(Some(SettingsFile { - _temp_dir: temp_dir, - path, - })) -} - -#[cfg(test)] -mod tests { - use serde_json::json; - - use super::create_settings_file; - - #[test] - fn creates_and_removes_temp_settings_file() { - let settings_file = create_settings_file(Some(&json!({ - "theme": "dark" - }))) - .expect("settings file should be created") - .expect("settings file should exist"); - - let settings_path = settings_file.path().to_path_buf(); - let settings_dir = settings_path - .parent() - .expect("settings file should have a parent directory") - .to_path_buf(); - - assert!(settings_path.is_file()); - drop(settings_file); - assert!(!settings_dir.exists()); - } -} diff --git a/crates/amp/src/thread.rs b/crates/amp/src/thread.rs deleted file mode 100644 index bd2377599f..0000000000 --- a/crates/amp/src/thread.rs +++ /dev/null @@ -1,368 +0,0 @@ -use std::sync::{Arc, Mutex}; -use std::task::{Context, Poll}; - -use futures_util::Stream; -use futures_util::StreamExt; -use tokio_util::sync::CancellationToken; - -use crate::error::Error; -use crate::events::{Input, RunStreamedResult, ThreadEvent, Turn}; -use crate::exec::{AmpExec, AmpExecArgs}; -use crate::options::{AmpOptions, ThreadOptions, TurnOptions}; -use crate::settings::{SettingsFile, create_settings_file}; - -#[derive(Debug, Clone)] -pub struct Amp { - exec: Arc, - options: AmpOptions, -} - -impl Amp { - pub fn new(options: AmpOptions) -> Self { - let exec = AmpExec::new(options.amp_path_override.clone(), options.env.clone()); - Self { - exec: Arc::new(exec), - options, - } - } - - pub fn start_thread(&self, options: ThreadOptions) -> Thread { - Thread::new(self.exec.clone(), self.options.clone(), options, None) - } - - pub fn resume_thread(&self, id: impl Into, options: ThreadOptions) -> Thread { - Thread::new( - self.exec.clone(), - self.options.clone(), - options, - Some(id.into()), - ) - } -} - -#[derive(Debug, Clone)] -pub struct Thread { - exec: Arc, - options: AmpOptions, - thread_options: ThreadOptions, - id: Arc>>, -} - -impl Thread { - pub(crate) fn new( - exec: Arc, - options: AmpOptions, - thread_options: ThreadOptions, - id: Option, - ) -> Self { - Self { - exec, - options, - thread_options, - id: Arc::new(Mutex::new(id)), - } - } - - pub fn id(&self) -> Result, Error> { - self.id - .lock() - .map(|guard| guard.clone()) - .map_err(|_| Error::Poisoned) - } - - pub async fn run_streamed( - &self, - input: I, - turn_options: TurnOptions, - ) -> Result - where - I: Into, - { - let input = input.into().normalize()?; - let thread_id = self.id()?; - let settings_file = create_settings_file(self.options.settings_overrides.as_ref())?; - let settings_path = settings_file - .as_ref() - .map(|settings_file| settings_file.path().to_path_buf()); - - let stream = self.exec.run(AmpExecArgs { - input, - amp_api_key: self.options.api_key.clone(), - thread_id, - mode: self.thread_options.mode, - working_directory: self.thread_options.working_directory.clone(), - include_thinking_stream: turn_options.include_thinking_stream, - settings_file: settings_path, - cancellation_token: turn_options.cancellation_token, - })?; - - Ok(RunStreamedResult { - events: Box::pin(ManagedEventStream { - inner: stream.events, - _settings_file: settings_file, - thread_id: self.id.clone(), - shutdown: stream.shutdown, - }), - }) - } - - pub async fn run(&self, input: I, turn_options: TurnOptions) -> Result - where - I: Into, - { - let streamed = self.run_streamed(input, turn_options).await?; - let mut events = streamed.events; - let mut items = Vec::new(); - let mut final_response = String::new(); - let mut usage = None; - - while let Some(event) = events.next().await { - let event = event?; - match &event { - ThreadEvent::Assistant(message) => { - for content in &message.message.content { - if let crate::events::ContentBlock::Text { text } = content { - final_response = text.clone(); - } - } - if let Some(message_usage) = &message.message.usage { - usage = Some(message_usage.clone()); - } - } - ThreadEvent::Result(message) => { - final_response = message.result.clone(); - if let Some(message_usage) = &message.base.usage { - usage = Some(message_usage.clone()); - } - } - ThreadEvent::ErrorResult(message) => { - return Err(Error::TurnFailed(message.error.clone())); - } - ThreadEvent::SystemInit(_) | ThreadEvent::User(_) | ThreadEvent::Unknown(_) => {} - } - items.push(event); - } - - Ok(Turn { - events: items, - final_response, - usage, - }) - } -} - -struct ManagedEventStream { - inner: crate::events::EventStream, - _settings_file: Option, - thread_id: Arc>>, - shutdown: CancellationToken, -} - -impl Stream for ManagedEventStream { - type Item = Result; - - fn poll_next( - mut self: std::pin::Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - match self.inner.as_mut().poll_next(cx) { - Poll::Ready(Some(Ok(event))) => { - if let Some(thread_id) = event.session_id().map(ToOwned::to_owned) { - if let Ok(mut guard) = self.thread_id.lock() { - *guard = Some(thread_id); - } - } - Poll::Ready(Some(Ok(event))) - } - other => other, - } - } -} - -impl Drop for ManagedEventStream { - fn drop(&mut self) { - self.shutdown.cancel(); - } -} - -#[cfg(test)] -mod tests { - use std::collections::BTreeMap; - use std::fs; - use std::path::PathBuf; - use std::sync::{Arc, Mutex}; - use std::time::Duration; - - use futures_util::StreamExt; - use tokio::runtime::Builder; - use tokio_util::sync::CancellationToken; - - use super::{Amp, ManagedEventStream}; - use crate::error::Error; - use crate::options::{AmpMode, AmpOptions, ThreadOptions, TurnOptions}; - - #[test] - fn run_without_turn_options_preserves_existing_behavior() { - let runtime = runtime(); - let temp_dir = tempfile::tempdir().expect("tempdir"); - let args_file = temp_dir.path().join("args.txt"); - let stdin_file = temp_dir.path().join("stdin.txt"); - let script = write_fake_amp_script( - &temp_dir, - r#" -: > "$ARGS_FILE" -for arg in "$@"; do - printf '%s\n' "$arg" >> "$ARGS_FILE" -done -cat > "$STDIN_FILE" -printf '%s\n' '{"type":"system","subtype":"init","cwd":"/tmp","session_id":"T-1","tools":[],"mcp_servers":[]}' -printf '%s\n' '{"type":"assistant","session_id":"T-1","message":{"type":"message","role":"assistant","content":[{"type":"text","text":"done"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":1,"output_tokens":1}},"parent_tool_use_id":null}' -printf '%s\n' '{"type":"result","subtype":"success","session_id":"T-1","is_error":false,"result":"done","duration_ms":1,"num_turns":1,"usage":{"input_tokens":1,"output_tokens":1}}' -"#, - ); - - let turn = runtime - .block_on( - test_amp( - script, - env_map([ - ("ARGS_FILE", args_file.clone()), - ("STDIN_FILE", stdin_file.clone()), - ]), - ) - .start_thread(ThreadOptions::default()) - .run("hello", TurnOptions::default()), - ) - .expect("turn should succeed"); - - assert_eq!(turn.final_response, "done"); - assert_eq!(turn.usage.expect("usage").output_tokens, 1); - let args = fs::read_to_string(args_file).expect("args file"); - assert!(args.contains("--execute")); - assert!(args.contains("--stream-json")); - assert_eq!(fs::read_to_string(stdin_file).expect("stdin file"), "hello"); - } - - #[test] - fn run_streamed_sets_thread_id_from_init_event() { - let runtime = runtime(); - let temp_dir = tempfile::tempdir().expect("tempdir"); - let script = write_fake_amp_script( - &temp_dir, - r#" -printf '%s\n' '{"type":"system","subtype":"init","cwd":"/tmp","session_id":"T-99","tools":[],"mcp_servers":[]}' -printf '%s\n' '{"type":"result","subtype":"success","session_id":"T-99","is_error":false,"result":"ok","duration_ms":1,"num_turns":1}' -"#, - ); - - let thread = test_amp(script, BTreeMap::new()).start_thread(ThreadOptions::default()); - let streamed = runtime - .block_on(thread.run_streamed("hello", TurnOptions::default())) - .expect("stream"); - let events = runtime.block_on(async { streamed.events.collect::>().await }); - - assert_eq!(events.len(), 2); - assert_eq!(thread.id().expect("id"), Some("T-99".to_string())); - } - - #[test] - fn dropping_managed_event_stream_cancels_shutdown_token() { - let shutdown = CancellationToken::new(); - let stream = ManagedEventStream { - inner: Box::pin(futures_util::stream::empty()), - _settings_file: None, - thread_id: Arc::new(Mutex::new(None)), - shutdown: shutdown.clone(), - }; - - drop(stream); - assert!(shutdown.is_cancelled()); - } - - #[test] - fn cancellation_kills_child_process() { - let runtime = runtime(); - let temp_dir = tempfile::tempdir().expect("tempdir"); - let pid_file = temp_dir.path().join("pid.txt"); - let script = write_fake_amp_script( - &temp_dir, - r#" -printf '%s' "$$" > "$PID_FILE" -printf '%s\n' '{"type":"system","subtype":"init","cwd":"/tmp","session_id":"T-cancel","tools":[],"mcp_servers":[]}' -while :; do sleep 1; done -"#, - ); - - let cancellation_token = CancellationToken::new(); - let thread = test_amp(script, env_map([("PID_FILE", pid_file.clone())])).start_thread( - ThreadOptions { - mode: Some(AmpMode::Rush), - working_directory: None, - }, - ); - - let streamed = runtime - .block_on(thread.run_streamed( - "hello", - TurnOptions { - cancellation_token: Some(cancellation_token.clone()), - include_thinking_stream: false, - }, - )) - .expect("stream"); - let mut events = streamed.events; - let first = runtime.block_on(async { events.next().await.expect("first event") }); - assert!(first.is_ok()); - cancellation_token.cancel(); - let result = runtime.block_on(async { events.next().await.expect("event") }); - std::thread::sleep(Duration::from_millis(200)); - - assert!(matches!(result, Err(Error::Cancelled))); - let pid = fs::read_to_string(pid_file).expect("pid file"); - assert!(!process_exists(pid.trim())); - } - - fn runtime() -> tokio::runtime::Runtime { - Builder::new_current_thread() - .enable_all() - .build() - .expect("rt") - } - - fn test_amp(path: PathBuf, env: BTreeMap) -> Amp { - Amp::new(AmpOptions { - amp_path_override: Some(path), - api_key: None, - settings_overrides: None, - env: Some(env), - }) - } - - fn env_map(entries: [(&str, PathBuf); N]) -> BTreeMap { - entries - .into_iter() - .map(|(key, value)| (key.to_string(), value.display().to_string())) - .collect() - } - - fn process_exists(pid: &str) -> bool { - std::process::Command::new("kill") - .args(["-0", pid]) - .status() - .map(|status| status.success()) - .unwrap_or(false) - } - - #[cfg(unix)] - fn write_fake_amp_script(temp_dir: &tempfile::TempDir, body: &str) -> PathBuf { - use std::os::unix::fs::PermissionsExt; - - let path = temp_dir.path().join("amp"); - let script = format!("#!/bin/sh\nset -eu\n{body}"); - fs::write(&path, script).expect("write fake amp"); - let mut permissions = fs::metadata(&path).expect("metadata").permissions(); - permissions.set_mode(0o755); - fs::set_permissions(&path, permissions).expect("chmod"); - path - } -} diff --git a/crates/api-agent/Cargo.toml b/crates/api-agent/Cargo.toml deleted file mode 100644 index 9c0232b875..0000000000 --- a/crates/api-agent/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "api-agent" -version = "0.1.0" -edition = "2024" - -[dependencies] -reqwest = { workspace = true, features = ["json"] } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -thiserror = { workspace = true } -url = { workspace = true } - -[dev-dependencies] -tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/crates/api-agent/src/client.rs b/crates/api-agent/src/client.rs deleted file mode 100644 index a42f1dd9cc..0000000000 --- a/crates/api-agent/src/client.rs +++ /dev/null @@ -1,245 +0,0 @@ -use reqwest::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderValue}; - -use crate::{ - Agent, AgentMessage, CreateAgentRequest, CreateMessageRequest, Error, IdResponse, - ListAgentsRequest, ListAgentsResponse, ListMessagesRequest, ListMessagesResponse, - UpdateAgentRequest, -}; - -#[derive(Default)] -pub struct ApiAgentClientBuilder { - api_key: Option, - api_base: Option, - client: Option, -} - -impl ApiAgentClientBuilder { - pub fn api_key(mut self, api_key: impl Into) -> Self { - self.api_key = Some(api_key.into()); - self - } - - pub fn api_base(mut self, api_base: impl Into) -> Self { - self.api_base = Some(api_base.into()); - self - } - - pub fn http_client(mut self, client: reqwest::Client) -> Self { - self.client = Some(client); - self - } - - pub fn build(self) -> Result { - let api_key = self.api_key.ok_or(Error::MissingApiKey)?; - let mut headers = HeaderMap::new(); - - let mut auth_value = HeaderValue::from_str(&format!("Bearer {api_key}")) - .map_err(|_| Error::InvalidApiKey)?; - auth_value.set_sensitive(true); - headers.insert(AUTHORIZATION, auth_value); - headers.insert(ACCEPT, HeaderValue::from_static("application/json")); - headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); - - let client = match self.client { - Some(client) => client, - None => reqwest::Client::builder() - .default_headers(headers) - .build()?, - }; - - let api_base = self - .api_base - .unwrap_or_else(|| "https://api.char.com".to_string()) - .parse()?; - - Ok(ApiAgentClient { client, api_base }) - } -} - -#[derive(Clone)] -pub struct ApiAgentClient { - client: reqwest::Client, - api_base: url::Url, -} - -impl ApiAgentClient { - pub fn builder() -> ApiAgentClientBuilder { - ApiAgentClientBuilder::default() - } - - pub fn api_base(&self) -> &url::Url { - &self.api_base - } - - pub async fn list_codex_agents( - &self, - req: ListAgentsRequest, - ) -> Result { - let mut url = self.endpoint("/v1/codex/agents")?; - { - let mut pairs = url.query_pairs_mut(); - if let Some(limit) = req.limit { - pairs.append_pair("limit", &limit.to_string()); - } - if let Some(cursor) = req.cursor.as_deref() { - pairs.append_pair("cursor", cursor); - } - if let Some(status) = req.status { - let value = serde_json::to_string(&status) - .expect("serializing AgentStatus should not fail"); - pairs.append_pair("status", value.trim_matches('"')); - } - } - - self.send(self.client.get(url)).await - } - - pub async fn get_codex_agent(&self, id: &str) -> Result { - let url = self.endpoint(&format!("/v1/codex/agents/{id}"))?; - self.send(self.client.get(url)).await - } - - pub async fn create_codex_agent(&self, req: &CreateAgentRequest) -> Result { - let url = self.endpoint("/v1/codex/agents")?; - self.send(self.client.post(url).json(req)).await - } - - pub async fn update_codex_agent( - &self, - id: &str, - req: &UpdateAgentRequest, - ) -> Result { - let url = self.endpoint(&format!("/v1/codex/agents/{id}"))?; - self.send(self.client.patch(url).json(req)).await - } - - pub async fn delete_codex_agent(&self, id: &str) -> Result { - let url = self.endpoint(&format!("/v1/codex/agents/{id}"))?; - self.send(self.client.delete(url)).await - } - - pub async fn list_codex_messages( - &self, - id: &str, - req: ListMessagesRequest, - ) -> Result { - let mut url = self.endpoint(&format!("/v1/codex/agents/{id}/messages"))?; - { - let mut pairs = url.query_pairs_mut(); - if let Some(limit) = req.limit { - pairs.append_pair("limit", &limit.to_string()); - } - if let Some(cursor) = req.cursor.as_deref() { - pairs.append_pair("cursor", cursor); - } - } - - self.send(self.client.get(url)).await - } - - pub async fn create_codex_message( - &self, - id: &str, - req: &CreateMessageRequest, - ) -> Result { - let url = self.endpoint(&format!("/v1/codex/agents/{id}/messages"))?; - self.send(self.client.post(url).json(req)).await - } - - pub async fn list_devin_agents( - &self, - req: ListAgentsRequest, - ) -> Result { - let mut url = self.endpoint("/v1/devin/agents")?; - { - let mut pairs = url.query_pairs_mut(); - if let Some(limit) = req.limit { - pairs.append_pair("limit", &limit.to_string()); - } - if let Some(cursor) = req.cursor.as_deref() { - pairs.append_pair("cursor", cursor); - } - if let Some(status) = req.status { - let value = serde_json::to_string(&status) - .expect("serializing AgentStatus should not fail"); - pairs.append_pair("status", value.trim_matches('"')); - } - } - - self.send(self.client.get(url)).await - } - - pub async fn get_devin_agent(&self, id: &str) -> Result { - let url = self.endpoint(&format!("/v1/devin/agents/{id}"))?; - self.send(self.client.get(url)).await - } - - pub async fn create_devin_agent(&self, req: &CreateAgentRequest) -> Result { - let url = self.endpoint("/v1/devin/agents")?; - self.send(self.client.post(url).json(req)).await - } - - pub async fn update_devin_agent( - &self, - id: &str, - req: &UpdateAgentRequest, - ) -> Result { - let url = self.endpoint(&format!("/v1/devin/agents/{id}"))?; - self.send(self.client.patch(url).json(req)).await - } - - pub async fn delete_devin_agent(&self, id: &str) -> Result { - let url = self.endpoint(&format!("/v1/devin/agents/{id}"))?; - self.send(self.client.delete(url)).await - } - - pub async fn list_devin_messages( - &self, - id: &str, - req: ListMessagesRequest, - ) -> Result { - let mut url = self.endpoint(&format!("/v1/devin/agents/{id}/messages"))?; - { - let mut pairs = url.query_pairs_mut(); - if let Some(limit) = req.limit { - pairs.append_pair("limit", &limit.to_string()); - } - if let Some(cursor) = req.cursor.as_deref() { - pairs.append_pair("cursor", cursor); - } - } - - self.send(self.client.get(url)).await - } - - pub async fn create_devin_message( - &self, - id: &str, - req: &CreateMessageRequest, - ) -> Result { - let url = self.endpoint(&format!("/v1/devin/agents/{id}/messages"))?; - self.send(self.client.post(url).json(req)).await - } - - fn endpoint(&self, path: &str) -> Result { - self.api_base.join(path).map_err(Error::from) - } - - async fn send(&self, request: reqwest::RequestBuilder) -> Result - where - T: serde::de::DeserializeOwned, - { - let response = request.send().await?; - let status = response.status(); - - if status.is_success() { - return Ok(response.json().await?); - } - - let message = response.text().await.unwrap_or_default(); - Err(Error::Api { - status: status.as_u16(), - message, - }) - } -} diff --git a/crates/api-agent/src/error.rs b/crates/api-agent/src/error.rs deleted file mode 100644 index 7136c38df5..0000000000 --- a/crates/api-agent/src/error.rs +++ /dev/null @@ -1,13 +0,0 @@ -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("request error: {0}")] - Request(#[from] reqwest::Error), - #[error("url parse error: {0}")] - Url(#[from] url::ParseError), - #[error("missing api key")] - MissingApiKey, - #[error("invalid api key")] - InvalidApiKey, - #[error("api error (status {status}): {message}")] - Api { status: u16, message: String }, -} diff --git a/crates/api-agent/src/lib.rs b/crates/api-agent/src/lib.rs deleted file mode 100644 index 9a9ada61fd..0000000000 --- a/crates/api-agent/src/lib.rs +++ /dev/null @@ -1,53 +0,0 @@ -mod client; -mod error; -mod types; - -pub use client::{ApiAgentClient, ApiAgentClientBuilder}; -pub use error::Error; -pub use types::*; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn agent_status_serialization() { - let status = AgentStatus::Succeeded; - let json = serde_json::to_string(&status).unwrap(); - assert_eq!(json, "\"succeeded\""); - - let parsed: AgentStatus = serde_json::from_str("\"running\"").unwrap(); - assert_eq!(parsed, AgentStatus::Running); - } - - #[test] - fn message_role_serialization() { - let role = MessageRole::Assistant; - let json = serde_json::to_string(&role).unwrap(); - assert_eq!(json, "\"assistant\""); - - let parsed: MessageRole = serde_json::from_str("\"tool\"").unwrap(); - assert_eq!(parsed, MessageRole::Tool); - } - - #[test] - fn create_agent_request_serialization_omits_empty_fields() { - let value = serde_json::to_value(CreateAgentRequest { - prompt: "Summarize the latest failures".to_string(), - model: Some("gpt-5.4".to_string()), - title: None, - messages: Some(vec![Message { - role: MessageRole::User, - content: "Look at CI".to_string(), - }]), - metadata: None, - }) - .unwrap(); - - assert_eq!(value["prompt"], "Summarize the latest failures"); - assert_eq!(value["model"], "gpt-5.4"); - assert_eq!(value["messages"][0]["role"], "user"); - assert!(value.get("title").is_none()); - assert!(value.get("metadata").is_none()); - } -} diff --git a/crates/api-agent/src/types.rs b/crates/api-agent/src/types.rs deleted file mode 100644 index fdffcc3d4d..0000000000 --- a/crates/api-agent/src/types.rs +++ /dev/null @@ -1,126 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum AgentStatus { - Queued, - Running, - Waiting, - Succeeded, - Failed, - Cancelled, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum MessageRole { - System, - User, - Assistant, - Tool, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Message { - pub role: MessageRole, - pub content: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Agent { - pub id: String, - pub status: AgentStatus, - #[serde(default)] - pub title: Option, - #[serde(default)] - pub model: Option, - #[serde(default)] - pub created_at: Option, - #[serde(default)] - pub updated_at: Option, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ListAgentsRequest { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub limit: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub cursor: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub status: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ListAgentsResponse { - pub agents: Vec, - #[serde(default)] - pub next_cursor: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CreateAgentRequest { - pub prompt: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub model: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub title: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub messages: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub metadata: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct UpdateAgentRequest { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub status: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub title: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub metadata: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentMessage { - pub id: String, - pub role: MessageRole, - pub content: String, - #[serde(default)] - pub created_at: Option, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ListMessagesRequest { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub limit: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub cursor: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ListMessagesResponse { - pub messages: Vec, - #[serde(default)] - pub next_cursor: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CreateMessageRequest { - pub role: MessageRole, - pub content: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct IdResponse { - pub id: String, -} diff --git a/crates/api-auth/Cargo.toml b/crates/api-auth/Cargo.toml deleted file mode 100644 index 6dda24221a..0000000000 --- a/crates/api-auth/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "api-auth" -version = "0.1.0" -edition = "2024" - -[dependencies] -hypr-supabase-auth = { workspace = true, features = ["server"] } - -axum = { workspace = true } - -[dev-dependencies] -tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/crates/api-auth/src/lib.rs b/crates/api-auth/src/lib.rs deleted file mode 100644 index 14f8458192..0000000000 --- a/crates/api-auth/src/lib.rs +++ /dev/null @@ -1,199 +0,0 @@ -use axum::{ - extract::{Request, State}, - http::StatusCode, - middleware::Next, - response::{IntoResponse, Response}, -}; -use hypr_supabase_auth::server::{Error as SupabaseAuthError, SupabaseAuth}; - -pub use hypr_supabase_auth::Claims; - -#[derive(Clone)] -pub struct AuthContext { - pub token: String, - pub claims: Claims, -} - -#[derive(Clone)] -pub struct AuthState { - inner: SupabaseAuth, - required_entitlements: Option>, -} - -impl AuthState { - pub fn new(supabase_url: &str) -> Self { - Self { - inner: SupabaseAuth::new(supabase_url), - required_entitlements: None, - } - } - - pub fn with_required_entitlement(mut self, entitlement: impl Into) -> Self { - self.required_entitlements = Some(vec![entitlement.into()]); - self - } - - pub fn with_required_entitlements(mut self, entitlements: Vec) -> Self { - self.required_entitlements = Some(entitlements); - self - } - - pub fn extract_token(auth_header: &str) -> Option<&str> { - SupabaseAuth::extract_token(auth_header) - } - - pub async fn verify_token(&self, token: &str) -> Result { - self.inner.verify_token(token).await.map_err(AuthError) - } -} - -pub struct AuthError(SupabaseAuthError); - -impl From for AuthError { - fn from(err: SupabaseAuthError) -> Self { - Self(err) - } -} - -impl IntoResponse for AuthError { - fn into_response(self) -> Response { - let (status, message) = match self.0 { - SupabaseAuthError::MissingAuthHeader => { - (StatusCode::UNAUTHORIZED, "missing_authorization_header") - } - SupabaseAuthError::InvalidAuthHeader => { - (StatusCode::UNAUTHORIZED, "invalid_authorization_header") - } - SupabaseAuthError::JwksFetchFailed => { - (StatusCode::INTERNAL_SERVER_ERROR, "jwks_fetch_failed") - } - SupabaseAuthError::InvalidToken => (StatusCode::UNAUTHORIZED, "invalid_token"), - SupabaseAuthError::MissingEntitlement(_) => { - (StatusCode::FORBIDDEN, "subscription_required") - } - }; - (status, message).into_response() - } -} - -pub async fn require_auth( - State(state): State, - mut request: Request, - next: Next, -) -> Result { - let auth_header = request - .headers() - .get("Authorization") - .and_then(|h| h.to_str().ok()) - .ok_or(SupabaseAuthError::MissingAuthHeader)?; - - let token = SupabaseAuth::extract_token(auth_header) - .ok_or(SupabaseAuthError::InvalidAuthHeader)? - .to_owned(); - - let claims = match &state.required_entitlements { - Some(entitlements) => { - let refs: Vec<&str> = entitlements.iter().map(|s| s.as_str()).collect(); - state.inner.require_any_entitlement(&token, &refs).await? - } - None => state.inner.verify_token(&token).await?, - }; - - request - .extensions_mut() - .insert(AuthContext { token, claims }); - - Ok(next.run(request).await) -} - -pub async fn optional_auth( - State(state): State, - mut request: Request, - next: Next, -) -> Response { - if let Some(auth_header) = request - .headers() - .get("Authorization") - .and_then(|h| h.to_str().ok()) - && let Some(token) = SupabaseAuth::extract_token(auth_header) - { - let token = token.to_owned(); - if let Ok(claims) = state.inner.verify_token(&token).await { - request - .extensions_mut() - .insert(AuthContext { token, claims }); - } - } - next.run(request).await -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_auth_error_missing_header() { - let err = AuthError(SupabaseAuthError::MissingAuthHeader); - let response = err.into_response(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - } - - #[test] - fn test_auth_error_invalid_header() { - let err = AuthError(SupabaseAuthError::InvalidAuthHeader); - let response = err.into_response(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - } - - #[test] - fn test_auth_error_jwks_fetch_failed() { - let err = AuthError(SupabaseAuthError::JwksFetchFailed); - let response = err.into_response(); - assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); - } - - #[test] - fn test_auth_error_invalid_token() { - let err = AuthError(SupabaseAuthError::InvalidToken); - let response = err.into_response(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - } - - #[test] - fn test_auth_error_missing_entitlement() { - let err = AuthError(SupabaseAuthError::MissingEntitlement("pro".to_string())); - let response = err.into_response(); - assert_eq!(response.status(), StatusCode::FORBIDDEN); - } - - #[test] - fn test_auth_state_new() { - let state = AuthState::new("https://example.supabase.co"); - assert_eq!(state.required_entitlements, None); - } - - #[test] - fn test_auth_state_with_required_entitlement() { - let state = - AuthState::new("https://example.supabase.co").with_required_entitlement("hyprnote_pro"); - assert_eq!( - state.required_entitlements, - Some(vec!["hyprnote_pro".to_string()]) - ); - } - - #[test] - fn test_auth_state_with_required_entitlements() { - let state = AuthState::new("https://example.supabase.co").with_required_entitlements(vec![ - "hyprnote_pro".to_string(), - "hyprnote_lite".to_string(), - ]); - assert_eq!( - state.required_entitlements, - Some(vec![ - "hyprnote_pro".to_string(), - "hyprnote_lite".to_string() - ]) - ); - } -} diff --git a/crates/api-bot/Cargo.toml b/crates/api-bot/Cargo.toml deleted file mode 100644 index 80c1b31ff7..0000000000 --- a/crates/api-bot/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "api-bot" -version = "0.1.0" -edition = "2024" - -[dependencies] -hypr-api-error = { workspace = true } -hypr-recall = { workspace = true } - -utoipa = { workspace = true } - -axum = { workspace = true } -sentry = { workspace = true } -tracing = { workspace = true } - -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -thiserror = { workspace = true } -urlencoding = { workspace = true } diff --git a/crates/api-bot/src/config.rs b/crates/api-bot/src/config.rs deleted file mode 100644 index 4ca70f2f8d..0000000000 --- a/crates/api-bot/src/config.rs +++ /dev/null @@ -1,11 +0,0 @@ -use serde::Deserialize; - -#[derive(Clone, Deserialize)] -pub struct BotConfig { - pub recall_api_key: String, - /// Publicly reachable base URL for this API server, used to build webhook URLs. - /// e.g. "https://api.hyprnote.com" - pub public_url: String, - /// Publicly accessible MP4 URL played by the onboarding bot. - pub demo_video_url: String, -} diff --git a/crates/api-bot/src/error.rs b/crates/api-bot/src/error.rs deleted file mode 100644 index 053379041e..0000000000 --- a/crates/api-bot/src/error.rs +++ /dev/null @@ -1,36 +0,0 @@ -use axum::{ - http::StatusCode, - response::{IntoResponse, Response}, -}; -use thiserror::Error; - -pub type Result = std::result::Result; - -#[derive(Debug, Error)] -pub enum BotError { - #[error("Invalid request: {0}")] - #[allow(dead_code)] - BadRequest(String), - - #[error("Recall API error: {0}")] - Recall(#[from] hypr_recall::Error), - - #[error("Internal error: {0}")] - Internal(String), -} - -impl IntoResponse for BotError { - fn into_response(self) -> Response { - let (status, code, message) = match self { - Self::BadRequest(msg) => (StatusCode::BAD_REQUEST, "bad_request", msg), - Self::Recall(err) => (StatusCode::BAD_GATEWAY, "recall_error", err.to_string()), - Self::Internal(msg) => ( - StatusCode::INTERNAL_SERVER_ERROR, - "internal_server_error", - msg, - ), - }; - - hypr_api_error::error_response(status, code, &message) - } -} diff --git a/crates/api-bot/src/lib.rs b/crates/api-bot/src/lib.rs deleted file mode 100644 index 8c985f8e97..0000000000 --- a/crates/api-bot/src/lib.rs +++ /dev/null @@ -1,8 +0,0 @@ -mod config; -mod error; -mod openapi; -mod routes; - -pub use config::BotConfig; -pub use openapi::openapi; -pub use routes::router; diff --git a/crates/api-bot/src/openapi.rs b/crates/api-bot/src/openapi.rs deleted file mode 100644 index d11859e958..0000000000 --- a/crates/api-bot/src/openapi.rs +++ /dev/null @@ -1,28 +0,0 @@ -use utoipa::OpenApi; - -#[derive(OpenApi)] -#[openapi( - paths( - crate::routes::bot::send_bot, - crate::routes::bot::remove_bot, - crate::routes::bot::start_demo, - crate::routes::bot::demo_status, - ), - components( - schemas( - crate::routes::bot::SendBotRequest, - crate::routes::bot::SendBotResponse, - crate::routes::bot::StartDemoRequest, - crate::routes::bot::StartDemoResponse, - crate::routes::bot::DemoStatusResponse, - ) - ), - tags( - (name = "bot", description = "Meeting bot management") - ) -)] -struct ApiDoc; - -pub fn openapi() -> utoipa::openapi::OpenApi { - ApiDoc::openapi() -} diff --git a/crates/api-bot/src/routes/bot.rs b/crates/api-bot/src/routes/bot.rs deleted file mode 100644 index 644abdc62d..0000000000 --- a/crates/api-bot/src/routes/bot.rs +++ /dev/null @@ -1,241 +0,0 @@ -use std::sync::Arc; - -use axum::{Extension, Json, extract::Path, response::Html}; -use hypr_recall::{ - AutomaticLeaveConfig, BotStatusCode, BotVariant, CreateBotRequest, EveryoneLeftConfig, - OutputMedia, OutputMediaConfig, OutputMediaKind, OutputMediaWebpageConfig, - RealTimeTranscriptionConfig, RecallClient, RecordingConfig, SilenceDetectionConfig, - StartRecordingOn, TranscriptionOptions, TranscriptionProvider, VariantKind, -}; -use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; - -use crate::config::BotConfig; -use crate::error::Result; - -#[derive(Debug, Deserialize, ToSchema)] -pub struct SendBotRequest { - pub meeting_url: String, - #[serde(default)] - pub bot_name: Option, -} - -#[derive(Debug, Serialize, ToSchema)] -pub struct SendBotResponse { - pub bot_id: String, -} - -#[utoipa::path( - post, - path = "/bot", - request_body = SendBotRequest, - responses( - (status = 200, description = "Bot sent to meeting", body = SendBotResponse), - (status = 400, description = "Bad request"), - (status = 502, description = "Recall API error"), - ) -)] -pub async fn send_bot( - Extension(config): Extension>, - Extension(client): Extension>, - Json(req): Json, -) -> Result> { - let transcript_url = format!("{}/bot/webhook/transcript", config.public_url); - - let bot = client - .create_bot(CreateBotRequest { - meeting_url: req.meeting_url, - bot_name: req.bot_name.unwrap_or_else(|| "Hyprnote".into()), - transcription_options: Some(TranscriptionOptions { - provider: TranscriptionProvider::MeetingCaptions, - }), - real_time_transcription: Some(RealTimeTranscriptionConfig { - destination_url: transcript_url, - partial_results: false, - }), - output_media: None, - metadata: None, - automatic_leave: None, - recording_config: None, - variant: None, - }) - .await?; - - Ok(Json(SendBotResponse { bot_id: bot.id })) -} - -#[derive(Debug, Deserialize, ToSchema)] -pub struct StartDemoRequest { - pub meeting_url: String, -} - -#[derive(Debug, Serialize, ToSchema)] -pub struct StartDemoResponse { - pub bot_id: String, - pub meeting_url: String, -} - -#[derive(Debug, Serialize, ToSchema)] -pub struct DemoStatusResponse { - pub status: String, - pub ready: bool, -} - -#[utoipa::path( - post, - path = "/onboarding/demo", - request_body = StartDemoRequest, - responses( - (status = 200, description = "Onboarding bot dispatched", body = StartDemoResponse), - (status = 502, description = "Recall API error"), - ) -)] -pub async fn start_demo( - Extension(config): Extension>, - Extension(client): Extension>, - Json(req): Json, -) -> Result> { - let player_url = format!("{}/onboarding/player", config.public_url); - - let bot = client - .create_bot(CreateBotRequest { - meeting_url: req.meeting_url.clone(), - bot_name: "Hyprnote Demo".into(), - transcription_options: None, - real_time_transcription: None, - output_media: Some(OutputMedia { - camera: Some(OutputMediaConfig { - kind: OutputMediaKind::Webpage, - config: OutputMediaWebpageConfig { url: player_url }, - }), - screenshare: None, - }), - metadata: None, - // Disable automatic recording — this bot only plays video, recording is useless and costly. - recording_config: Some(RecordingConfig { - start_recording_on: Some(StartRecordingOn::Manual), - }), - // Use web_4_core for smooth video playback (9x the default CPU). - variant: Some(BotVariant { - zoom: Some(VariantKind::Web4Core), - google_meet: Some(VariantKind::Web4Core), - microsoft_teams: Some(VariantKind::Web4Core), - webex: None, - }), - automatic_leave: Some(AutomaticLeaveConfig { - // Leave promptly after video ends — the page goes silent once playback finishes. - silence_detection: Some(SilenceDetectionConfig { - timeout: Some(10), - activate_after: Some(1), - }), - noone_joined_timeout: Some(120), - everyone_left_timeout: Some(EveryoneLeftConfig { - timeout: Some(5), - activate_after: None, - }), - ..Default::default() - }), - }) - .await?; - - Ok(Json(StartDemoResponse { - bot_id: bot.id, - meeting_url: req.meeting_url, - })) -} - -#[utoipa::path( - get, - path = "/onboarding/demo/{bot_id}", - params(("bot_id" = String, Path, description = "Recall bot ID")), - responses( - (status = 200, description = "Bot status", body = DemoStatusResponse), - (status = 502, description = "Recall API error"), - ) -)] -pub async fn demo_status( - Extension(client): Extension>, - Path(bot_id): Path, -) -> Result> { - let bot = client.get_bot(&bot_id).await?; - let ready = matches!( - bot.status.code, - BotStatusCode::InCallNotRecording | BotStatusCode::InCallRecording - ); - - Ok(Json(DemoStatusResponse { - status: format!("{:?}", bot.status.code), - ready, - })) -} - -#[utoipa::path( - delete, - path = "/bot/{bot_id}", - params(("bot_id" = String, Path, description = "Recall bot ID")), - responses( - (status = 200, description = "Bot removed from call"), - (status = 502, description = "Recall API error"), - ) -)] -pub async fn remove_bot( - Extension(client): Extension>, - Path(bot_id): Path, -) -> Result<()> { - client.remove_bot(&bot_id).await?; - - Ok(()) -} - -pub async fn player(Extension(config): Extension>) -> Html { - let url = html_escape(&config.demo_video_url); - Html(format!( - r#" - - - - - - - -
Thanks for watching!
- - -"# - )) -} - -fn html_escape(s: &str) -> String { - s.replace('&', "&") - .replace('"', """) - .replace('<', "<") - .replace('>', ">") -} diff --git a/crates/api-bot/src/routes/mod.rs b/crates/api-bot/src/routes/mod.rs deleted file mode 100644 index 3cfb9b749e..0000000000 --- a/crates/api-bot/src/routes/mod.rs +++ /dev/null @@ -1,32 +0,0 @@ -pub(crate) mod bot; -pub(crate) mod webhook; - -use std::sync::Arc; - -use axum::{ - Router, - routing::{delete, get, post}, -}; - -use hypr_recall::RecallClient; - -use crate::config::BotConfig; -use crate::error::BotError; - -pub fn router(config: BotConfig) -> Result { - let client = - RecallClient::new(&config.recall_api_key).map_err(|e| BotError::Internal(e.to_string()))?; - - let router = Router::new() - .route("/bot", post(bot::send_bot)) - .route("/bot/:bot_id", delete(bot::remove_bot)) - .route("/onboarding/demo", post(bot::start_demo)) - .route("/onboarding/demo/:bot_id", get(bot::demo_status)) - .route("/onboarding/player", get(bot::player)) - .route("/webhook", post(webhook::status_change)) - .route("/webhook/transcript", post(webhook::transcript)) - .layer(axum::Extension(Arc::new(client))) - .layer(axum::Extension(Arc::new(config))); - - Ok(router) -} diff --git a/crates/api-bot/src/routes/webhook.rs b/crates/api-bot/src/routes/webhook.rs deleted file mode 100644 index 5b6340a5d6..0000000000 --- a/crates/api-bot/src/routes/webhook.rs +++ /dev/null @@ -1,64 +0,0 @@ -use std::sync::Arc; - -use axum::{Extension, Json}; -use hypr_recall::{BotStatusCode, BotStatusWebhook, RecallClient, TranscriptWebhook}; - -use crate::error::Result; - -pub async fn status_change( - Extension(client): Extension>, - Json(event): Json, -) -> Result<()> { - let bot_id = &event.data.bot_id; - let code = &event.data.status.code; - - tracing::info!( - hyprnote.bot.id = %bot_id, - hyprnote.bot.status_code = ?code, - "bot_status_change" - ); - - match code { - BotStatusCode::CallEnded => { - tracing::info!(hyprnote.bot.id = %bot_id, "bot_call_ended"); - } - BotStatusCode::Fatal => { - let message = event.data.status.message.as_deref().unwrap_or("unknown"); - tracing::error!( - hyprnote.bot.id = %bot_id, - error = %message, - "bot_fatal" - ); - sentry::capture_message( - &format!("Recall bot {bot_id} fatal: {message}"), - sentry::Level::Error, - ); - // Best-effort removal — the bot may already be gone, but this ensures cleanup - // if it somehow got stuck in a recoverable state. - let _ = client.remove_bot(bot_id).await; - } - _ => {} - } - - Ok(()) -} - -pub async fn transcript(Json(payload): Json) -> Result<()> { - let text = payload - .transcript - .words - .iter() - .map(|w| w.text.as_str()) - .collect::>() - .join(" "); - - tracing::info!( - hyprnote.bot.id = %payload.bot_id, - hyprnote.transcript.speaker = %payload.transcript.speaker, - hyprnote.transcript.is_final = payload.transcript.is_final, - hyprnote.transcript.char_count = text.chars().count() as u64, - "transcript_received" - ); - - Ok(()) -} diff --git a/crates/api-cactus/Cargo.toml b/crates/api-cactus/Cargo.toml deleted file mode 100644 index e67e56e966..0000000000 --- a/crates/api-cactus/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "api-cactus" -version = "0.1.0" -edition = "2024" - -[dependencies] -axum = { workspace = true } -bytes = { workspace = true } -reqwest = { workspace = true, features = ["json", "stream"] } -serde = { workspace = true, features = ["derive"] } -tracing = { workspace = true } diff --git a/crates/api-cactus/src/lib.rs b/crates/api-cactus/src/lib.rs deleted file mode 100644 index 36730e11c9..0000000000 --- a/crates/api-cactus/src/lib.rs +++ /dev/null @@ -1,117 +0,0 @@ -use axum::{ - Router, - body::Body, - extract::State, - http::{HeaderMap, StatusCode}, - response::{IntoResponse, Response}, - routing::post, -}; -use reqwest::Client; - -const DEFAULT_UPSTREAM_BASE: &str = "https://104.198.76.3"; -const DEVICE_FINGERPRINT_HEADER: &str = "x-device-fingerprint"; - -#[derive(Clone)] -pub struct CactusProxyConfig { - pub api_key: String, - pub upstream_base: Option, -} - -#[derive(Clone)] -struct AppState { - config: CactusProxyConfig, - client: Client, -} - -pub fn router(config: CactusProxyConfig) -> Router { - let state = AppState { - config, - client: Client::builder() - .danger_accept_invalid_certs(true) - .build() - .expect("failed to build reqwest client"), - }; - - Router::new() - .route("/text", post(proxy_text)) - .route("/vlm", post(proxy_vlm)) - .route("/transcribe", post(proxy_transcribe)) - .with_state(state) -} - -async fn proxy( - state: &AppState, - path: &str, - headers: &mut HeaderMap, - body: bytes::Bytes, -) -> Response { - let fingerprint = headers - .remove(DEVICE_FINGERPRINT_HEADER) - .and_then(|v| v.to_str().ok().map(String::from)); - - if let Some(fp) = fingerprint { - tracing::info!(enduser.pseudo.id = %fp, "cactus_proxy_{}", path); - } - - let base = state - .config - .upstream_base - .as_deref() - .unwrap_or(DEFAULT_UPSTREAM_BASE); - let url = format!("{base}/api/v1/{path}"); - - let result = state - .client - .post(&url) - .header("Content-Type", "application/json") - .header("X-API-Key", &state.config.api_key) - .body(body) - .send() - .await; - - match result { - Ok(resp) => { - let status = - StatusCode::from_u16(resp.status().as_u16()).unwrap_or(StatusCode::BAD_GATEWAY); - match resp.bytes().await { - Ok(bytes) => Response::builder() - .status(status) - .header("Content-Type", "application/json") - .body(Body::from(bytes)) - .unwrap(), - Err(e) => { - tracing::error!(error = %e, "cactus_proxy_body_read_failed"); - StatusCode::BAD_GATEWAY.into_response() - } - } - } - Err(e) => { - tracing::error!(error = %e, upstream_url = %url, "cactus_proxy_request_failed"); - StatusCode::BAD_GATEWAY.into_response() - } - } -} - -async fn proxy_text( - State(state): State, - mut headers: HeaderMap, - body: bytes::Bytes, -) -> Response { - proxy(&state, "text", &mut headers, body).await -} - -async fn proxy_vlm( - State(state): State, - mut headers: HeaderMap, - body: bytes::Bytes, -) -> Response { - proxy(&state, "vlm", &mut headers, body).await -} - -async fn proxy_transcribe( - State(state): State, - mut headers: HeaderMap, - body: bytes::Bytes, -) -> Response { - proxy(&state, "transcribe", &mut headers, body).await -} diff --git a/crates/api-calendar/Cargo.toml b/crates/api-calendar/Cargo.toml deleted file mode 100644 index 616f82aa52..0000000000 --- a/crates/api-calendar/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "api-calendar" -version = "0.1.0" -edition = "2024" - -[dependencies] -hypr-api-auth = { workspace = true } -hypr-api-error = { workspace = true } -hypr-api-nango = { workspace = true } -hypr-google-calendar = { workspace = true, features = ["utoipa"] } -hypr-nango = { workspace = true } -hypr-outlook-calendar = { workspace = true, features = ["utoipa"] } - -chrono = { workspace = true, features = ["serde"] } - -axum = { workspace = true } -utoipa = { workspace = true } - -serde = { workspace = true, features = ["derive"] } -thiserror = { workspace = true } diff --git a/crates/api-calendar/src/error.rs b/crates/api-calendar/src/error.rs deleted file mode 100644 index 25fdb41fbe..0000000000 --- a/crates/api-calendar/src/error.rs +++ /dev/null @@ -1,40 +0,0 @@ -use axum::{ - http::StatusCode, - response::{IntoResponse, Response}, -}; -use thiserror::Error; - -pub type Result = std::result::Result; - -#[derive(Debug, Error)] -pub enum CalendarError { - #[error("Authentication error: {0}")] - #[allow(dead_code)] - Auth(String), - - #[error("Invalid request: {0}")] - BadRequest(String), - - #[error("Internal error: {0}")] - Internal(String), - - #[error(transparent)] - NangoConnection(#[from] hypr_api_nango::NangoConnectionError), -} - -impl IntoResponse for CalendarError { - fn into_response(self) -> Response { - let (status, code, message) = match self { - Self::Auth(message) => (StatusCode::UNAUTHORIZED, "unauthorized", message), - Self::BadRequest(message) => (StatusCode::BAD_REQUEST, "bad_request", message), - Self::Internal(message) => ( - StatusCode::INTERNAL_SERVER_ERROR, - "internal_server_error", - message, - ), - Self::NangoConnection(err) => return err.into_response(), - }; - - hypr_api_error::error_response(status, code, &message) - } -} diff --git a/crates/api-calendar/src/google/mod.rs b/crates/api-calendar/src/google/mod.rs deleted file mode 100644 index 06292ea930..0000000000 --- a/crates/api-calendar/src/google/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub(crate) mod routes; - -use axum::{Router, routing::post}; - -pub fn router() -> Router { - Router::new() - .route("/list-calendars", post(routes::list_calendars)) - .route("/list-events", post(routes::list_events)) -} diff --git a/crates/api-calendar/src/google/routes.rs b/crates/api-calendar/src/google/routes.rs deleted file mode 100644 index 5f48dd2531..0000000000 --- a/crates/api-calendar/src/google/routes.rs +++ /dev/null @@ -1,149 +0,0 @@ -use axum::{Extension, Json}; -use hypr_api_auth::AuthContext; -use hypr_api_nango::{GoogleCalendar, NangoConnectionState, NangoIntegrationId}; -use hypr_google_calendar::{ - EventOrderBy, EventType, GoogleCalendarClient, ListCalendarsResponse, ListEventsResponse, -}; -use serde::Deserialize; -use utoipa::ToSchema; - -use crate::error::{CalendarError, Result}; - -#[derive(Debug, Deserialize, ToSchema)] -pub struct GoogleListCalendarsRequest { - pub connection_id: String, -} - -#[derive(Debug, Deserialize, ToSchema)] -pub struct GoogleListEventsRequest { - pub connection_id: String, - pub calendar_id: String, - #[serde(default)] - pub time_min: Option, - #[serde(default)] - pub time_max: Option, - #[serde(default)] - pub max_results: Option, - #[serde(default)] - pub page_token: Option, - #[serde(default)] - pub single_events: Option, - #[serde(default)] - pub order_by: Option, -} - -#[utoipa::path( - post, - path = "/google/list-calendars", - operation_id = "google_list_calendars", - request_body = GoogleListCalendarsRequest, - responses( - (status = 200, description = "Google calendars fetched", body = ListCalendarsResponse), - (status = 401, description = "Unauthorized"), - (status = 500, description = "Internal server error"), - ), - tag = "calendar", -)] -pub async fn list_calendars( - Extension(auth): Extension, - Extension(nango_state): Extension, - Json(req): Json, -) -> Result> { - let http = nango_state - .build_http_client( - &auth.token, - &auth.claims.sub, - GoogleCalendar::ID, - &req.connection_id, - ) - .await?; - - let client = GoogleCalendarClient::new(http); - - let response = client - .list_calendars() - .await - .map_err(|e| CalendarError::Internal(e.to_string()))?; - - Ok(Json(response)) -} - -#[utoipa::path( - post, - path = "/google/list-events", - operation_id = "google_list_events", - request_body = GoogleListEventsRequest, - responses( - (status = 200, description = "Google events fetched", body = ListEventsResponse), - (status = 401, description = "Unauthorized"), - (status = 500, description = "Internal server error"), - ), - tag = "calendar", -)] -pub async fn list_events( - Extension(auth): Extension, - Extension(nango_state): Extension, - Json(req): Json, -) -> Result> { - let http = nango_state - .build_http_client( - &auth.token, - &auth.claims.sub, - GoogleCalendar::ID, - &req.connection_id, - ) - .await?; - - let client = GoogleCalendarClient::new(http); - - let time_min = req - .time_min - .as_deref() - .map(|s| { - chrono::DateTime::parse_from_rfc3339(s) - .map(|dt| dt.with_timezone(&chrono::Utc)) - .map_err(|e| CalendarError::BadRequest(format!("Invalid time_min: {e}"))) - }) - .transpose()?; - - let time_max = req - .time_max - .as_deref() - .map(|s| { - chrono::DateTime::parse_from_rfc3339(s) - .map(|dt| dt.with_timezone(&chrono::Utc)) - .map_err(|e| CalendarError::BadRequest(format!("Invalid time_max: {e}"))) - }) - .transpose()?; - - let order_by = req - .order_by - .as_deref() - .map(|s| match s { - "startTime" => Ok(EventOrderBy::StartTime), - "updated" => Ok(EventOrderBy::Updated), - other => Err(CalendarError::BadRequest(format!( - "Invalid order_by: {other}" - ))), - }) - .transpose()?; - - let google_req = hypr_google_calendar::ListEventsRequest { - calendar_id: req.calendar_id, - time_min, - time_max, - max_results: req.max_results, - page_token: req.page_token, - single_events: req.single_events, - order_by, - event_types: Some(vec![EventType::Default]), - ..Default::default() - }; - - let response = client - .list_events(google_req) - .await - .map_err(|e| CalendarError::Internal(e.to_string()))?; - - Ok(Json(response)) -} diff --git a/crates/api-calendar/src/lib.rs b/crates/api-calendar/src/lib.rs deleted file mode 100644 index c668cf7196..0000000000 --- a/crates/api-calendar/src/lib.rs +++ /dev/null @@ -1,14 +0,0 @@ -mod error; -mod google; -mod openapi; -mod outlook; - -use axum::Router; - -pub use openapi::openapi; - -pub fn router() -> Router { - Router::new() - .nest("/google", google::router()) - .nest("/outlook", outlook::router()) -} diff --git a/crates/api-calendar/src/openapi.rs b/crates/api-calendar/src/openapi.rs deleted file mode 100644 index 3ec66a9b44..0000000000 --- a/crates/api-calendar/src/openapi.rs +++ /dev/null @@ -1,28 +0,0 @@ -use utoipa::OpenApi; - -#[derive(OpenApi)] -#[openapi( - paths( - crate::google::routes::list_calendars, - crate::google::routes::list_events, - crate::outlook::routes::list_calendars, - crate::outlook::routes::list_events, - ), - components(schemas( - crate::google::routes::GoogleListCalendarsRequest, - crate::google::routes::GoogleListEventsRequest, - crate::outlook::routes::OutlookListCalendarsRequest, - crate::outlook::routes::OutlookListEventsRequest, - )), - tags( - (name = "calendar", description = "Calendar management") - ) -)] -struct ApiDoc; - -pub fn openapi() -> utoipa::openapi::OpenApi { - let mut doc = ApiDoc::openapi(); - doc.merge(hypr_google_calendar::openapi::openapi()); - doc.merge(hypr_outlook_calendar::openapi::openapi()); - doc -} diff --git a/crates/api-calendar/src/outlook/mod.rs b/crates/api-calendar/src/outlook/mod.rs deleted file mode 100644 index 06292ea930..0000000000 --- a/crates/api-calendar/src/outlook/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub(crate) mod routes; - -use axum::{Router, routing::post}; - -pub fn router() -> Router { - Router::new() - .route("/list-calendars", post(routes::list_calendars)) - .route("/list-events", post(routes::list_events)) -} diff --git a/crates/api-calendar/src/outlook/routes.rs b/crates/api-calendar/src/outlook/routes.rs deleted file mode 100644 index 7774cd692e..0000000000 --- a/crates/api-calendar/src/outlook/routes.rs +++ /dev/null @@ -1,136 +0,0 @@ -use axum::{Extension, Json}; -use hypr_api_auth::AuthContext; -use hypr_api_nango::{NangoConnectionState, NangoIntegrationId, Outlook}; -use hypr_outlook_calendar::{ListCalendarsResponse, ListEventsResponse, OutlookCalendarClient}; -use serde::Deserialize; -use utoipa::ToSchema; - -use crate::error::{CalendarError, Result}; - -#[derive(Debug, Deserialize, ToSchema)] -pub struct OutlookListCalendarsRequest { - pub connection_id: String, -} - -#[derive(Debug, Deserialize, ToSchema)] -pub struct OutlookListEventsRequest { - pub connection_id: String, - pub calendar_id: String, - #[serde(default)] - pub time_min: Option, - #[serde(default)] - pub time_max: Option, - #[serde(default)] - pub max_results: Option, - #[serde(default)] - pub order_by: Option, -} - -#[utoipa::path( - post, - path = "/outlook/list-calendars", - operation_id = "outlook_list_calendars", - request_body = OutlookListCalendarsRequest, - responses( - (status = 200, description = "Outlook calendars fetched", body = ListCalendarsResponse), - (status = 401, description = "Unauthorized"), - (status = 500, description = "Internal server error"), - ), - tag = "calendar", -)] -pub async fn list_calendars( - Extension(auth): Extension, - Extension(nango_state): Extension, - Json(req): Json, -) -> Result> { - let http = nango_state - .build_http_client( - &auth.token, - &auth.claims.sub, - Outlook::ID, - &req.connection_id, - ) - .await? - .with_base_url_override("https://graph.microsoft.com/v1.0"); - - let client = OutlookCalendarClient::new(http); - - let response = client - .list_calendars() - .await - .map_err(|e| CalendarError::Internal(e.to_string()))?; - - Ok(Json(response)) -} - -#[utoipa::path( - post, - path = "/outlook/list-events", - operation_id = "outlook_list_events", - request_body = OutlookListEventsRequest, - responses( - (status = 200, description = "Outlook events fetched", body = ListEventsResponse), - (status = 401, description = "Unauthorized"), - (status = 500, description = "Internal server error"), - ), - tag = "calendar", -)] -pub async fn list_events( - Extension(auth): Extension, - Extension(nango_state): Extension, - Json(req): Json, -) -> Result> { - let http = nango_state - .build_http_client( - &auth.token, - &auth.claims.sub, - Outlook::ID, - &req.connection_id, - ) - .await? - .with_base_url_override("https://graph.microsoft.com/v1.0"); - - let client = OutlookCalendarClient::new(http); - - let start_date_time = req - .time_min - .as_deref() - .map(|s| { - chrono::DateTime::parse_from_rfc3339(s) - .map(|dt| dt.with_timezone(&chrono::Utc)) - .map_err(|e| CalendarError::BadRequest(format!("Invalid time_min: {e}"))) - }) - .transpose()?; - - let end_date_time = req - .time_max - .as_deref() - .map(|s| { - chrono::DateTime::parse_from_rfc3339(s) - .map(|dt| dt.with_timezone(&chrono::Utc)) - .map_err(|e| CalendarError::BadRequest(format!("Invalid time_max: {e}"))) - }) - .transpose()?; - - let order_by = req.order_by.as_deref().map(|s| match s { - "startTime" => "start/dateTime".to_string(), - "updated" => "lastModifiedDateTime".to_string(), - other => other.to_string(), - }); - - let outlook_req = hypr_outlook_calendar::ListEventsRequest { - calendar_id: req.calendar_id, - start_date_time, - end_date_time, - top: req.max_results, - order_by, - ..Default::default() - }; - - let response = client - .list_events(outlook_req) - .await - .map_err(|e| CalendarError::Internal(e.to_string()))?; - - Ok(Json(response)) -} diff --git a/crates/api-claw/AGENTS.md b/crates/api-claw/AGENTS.md deleted file mode 100644 index 563bf5f57a..0000000000 --- a/crates/api-claw/AGENTS.md +++ /dev/null @@ -1,70 +0,0 @@ -# api-claw - -User-scoped VM lifecycle on top of `crates/exedev`. Treat every user as -getting exactly one VM, running `apps/claw`. - -## Layering - -1. `crates/exedev` — typed SDK for `POST https://exe.dev/exec` and the VM - auth proxy. No knowledge of users, no policy. -2. `crates/api-claw` (this crate) — deterministic naming, per-user keys, - provision/suspend/resume/deprovision mechanics, VM-scoped token minting. - **Does not know about billing.** -3. `crates/api-subscription` (or equivalent) — calls into `ClawManager` - from webhook handlers. This is where policy lives (e.g. "on - `invoice.payment_failed` → suspend"). - -Keep these layers one-way: never let billing leak into `exedev`, and never -let low-level exe.dev details leak out of `exedev::commands`. - -## Conventions (encoded in code) - -- **VM name**: `vm_name(UserId) = "claw-" + first 12 hex of sha256(user_id)`. - DNS-safe for `.exe.xyz`. Do not construct VM names any other way. - There is no tag-based lookup because `ls` does not return tags. -- **One SSH key per user**, generated on first `provision` and added via - `ssh-key add`. This gives us per-user revocation: removing the key - invalidates every token derived from it without touching other users. -- **Token ctx** always includes `{"user_id": }`. Extra fields go under - the same object via `ClawCallOptions::with_ctx`. Claw reads this verbatim - from the `X-ExeDev-Token-Ctx` header. -- **Token lifetime** is whatever the caller asks for (`ClawCallOptions::exp`). - Prefer minutes, not days: tokens are re-minted on every `call`. -- **Suspension** = remove the user's SSH key (`ssh_key_remove`). **Resume** - = re-add the same stored key. The VM keeps running in both states; only - HTTPS access is gated. - -## Adding new VM commands - -If you need to drive a command that `exedev` doesn't wrap yet: - -1. Add the typed method to `crates/exedev/src/commands.rs`. Cover response - parsing with a fixture in `commands::tests` (copy a real response from - a gated smoke test). -2. If the command participates in user lifecycle, expose it via - `ClawManager`. Otherwise keep it on the raw client. - -Never add a new `exec_raw` caller outside `exedev`; all command-string -construction and quoting must stay in one place. - -## Testing - -- `cargo test -p exedev --lib` — quoting, naming, token namespace, response - deserialization. All offline. -- `EXEDEV_TOKEN= cargo test -p exedev --test live_smoke` — - hits the real API to confirm the typed models still match. Runs no-ops - only (`whoami`, `ls`, `ssh-key list`). -- `cargo test -p api-claw --lib` — deterministic naming, keyring round-trips, - token ctx shape. - -Do not add tests that `provision` real VMs in CI; cost and state leak are -too high. Add a gated live test only if the flow being exercised genuinely -can't be unit-tested. - -## What this crate does NOT do - -- Bill or observe billing state. -- Store SSH keys durably. `InMemoryKeyring` is for tests; wire in a - Supabase-backed `UserKeyring` impl in the caller. -- Talk to claw's application layer. See `apps/claw/src/http.rs` for the - receiving side of `ClawManager::call`. diff --git a/crates/api-claw/Cargo.toml b/crates/api-claw/Cargo.toml deleted file mode 100644 index 84afd7f256..0000000000 --- a/crates/api-claw/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "api-claw" -version = "0.1.0" -edition = "2024" -publish = false - -[dependencies] -hypr-exedev = { workspace = true } - -base64 = { workspace = true } -chrono = { workspace = true } -rand_core = { version = "0.6.4", features = ["getrandom"] } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -sha2 = { workspace = true } -ssh-key = { version = "0.6", default-features = false, features = ["alloc", "ed25519"] } -thiserror = { workspace = true } -tokio = { workspace = true, features = ["sync"] } -tracing = { workspace = true } - -[dev-dependencies] -tokio = { workspace = true, features = ["rt", "macros"] } diff --git a/crates/api-claw/src/keyring.rs b/crates/api-claw/src/keyring.rs deleted file mode 100644 index d1510e137b..0000000000 --- a/crates/api-claw/src/keyring.rs +++ /dev/null @@ -1,131 +0,0 @@ -use std::collections::HashMap; -use std::sync::Arc; - -use rand_core::OsRng; -use ssh_key::{Algorithm, LineEnding, PrivateKey}; -use tokio::sync::Mutex; - -use crate::Error; -use crate::naming::UserId; - -/// Per-user SSH keypair as stored by the caller. -/// -/// The `private_pem` is in OpenSSH-ASCII format (`-----BEGIN OPENSSH PRIVATE KEY-----`). -/// `public_openssh` is the single-line authorized-keys format that exe.dev's -/// `ssh-key add` expects (e.g. `ssh-ed25519 AAAA... label`). -#[derive(Debug, Clone)] -pub struct UserSshKey { - pub private_pem: String, - pub public_openssh: String, - pub fingerprint: String, -} - -/// Abstraction over per-user SSH key storage. -/// -/// Implementations MUST: -/// - return the *same* keypair for the same `UserId` on repeat calls, or -/// callers will lose the ability to mint tokens the server recognizes; -/// - be safe to call concurrently for the same user (the manager calls this -/// from provision/resume paths that may race). -pub trait UserKeyring: Send + Sync + 'static { - fn get_or_create( - &self, - user: &UserId, - ) -> impl std::future::Future> + Send; - - fn remove(&self, user: &UserId) -> impl std::future::Future> + Send; -} - -/// Generate a fresh Ed25519 keypair. Exposed so storage backends can implement -/// `get_or_create` by calling into this helper when a miss occurs. -pub fn generate_ssh_key(label: &str) -> Result { - let key = PrivateKey::random(&mut OsRng, Algorithm::Ed25519) - .map_err(|e| Error::KeyGen(e.to_string()))?; - let private_pem = key - .to_openssh(LineEnding::LF) - .map_err(|e| Error::KeyGen(e.to_string()))? - .to_string(); - let mut public_openssh = key - .public_key() - .to_openssh() - .map_err(|e| Error::KeyGen(e.to_string()))?; - if !label.is_empty() { - public_openssh.push(' '); - public_openssh.push_str(label); - } - let fingerprint = key - .public_key() - .fingerprint(ssh_key::HashAlg::Sha256) - .to_string(); - Ok(UserSshKey { - private_pem, - public_openssh, - fingerprint, - }) -} - -/// In-memory keyring for tests and local dev. Not durable. -#[derive(Default)] -pub struct InMemoryKeyring { - inner: Arc>>, -} - -impl InMemoryKeyring { - pub fn new() -> Self { - Self::default() - } -} - -impl UserKeyring for InMemoryKeyring { - async fn get_or_create(&self, user: &UserId) -> Result { - let mut guard = self.inner.lock().await; - if let Some(existing) = guard.get(user) { - return Ok(existing.clone()); - } - let label = format!("api-claw:{user}"); - let key = generate_ssh_key(&label)?; - guard.insert(user.clone(), key.clone()); - Ok(key) - } - - async fn remove(&self, user: &UserId) -> Result<(), Error> { - self.inner.lock().await.remove(user); - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn keyring_is_deterministic_per_user() { - let kr = InMemoryKeyring::new(); - let u = UserId::new("u1"); - let a = kr.get_or_create(&u).await.unwrap(); - let b = kr.get_or_create(&u).await.unwrap(); - assert_eq!(a.public_openssh, b.public_openssh); - assert_eq!(a.fingerprint, b.fingerprint); - } - - #[tokio::test] - async fn different_users_get_different_keys() { - let kr = InMemoryKeyring::new(); - let a = kr.get_or_create(&UserId::new("a")).await.unwrap(); - let b = kr.get_or_create(&UserId::new("b")).await.unwrap(); - assert_ne!(a.public_openssh, b.public_openssh); - } - - #[test] - fn generate_emits_openssh_pem_and_pubkey() { - let k = generate_ssh_key("label").unwrap(); - assert!( - k.private_pem.contains("BEGIN OPENSSH PRIVATE KEY"), - "bad pem: {}", - k.private_pem - ); - assert!(k.public_openssh.starts_with("ssh-ed25519 ")); - assert!(k.public_openssh.ends_with(" label")); - assert!(k.fingerprint.starts_with("SHA256:")); - } -} diff --git a/crates/api-claw/src/lib.rs b/crates/api-claw/src/lib.rs deleted file mode 100644 index c6074fc7fc..0000000000 --- a/crates/api-claw/src/lib.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! User-scoped VM lifecycle on top of [`exedev`](../exedev/index.html). -//! -//! The `exedev` crate is a thin typed SDK over the exe.dev HTTPS API. This -//! crate adds the conventions we need to treat VMs as "one per user": -//! -//! - Deterministic naming: [`UserId`] → [`vm_name`] (`claw-`), so any -//! caller can look up a user's VM with a single `ls ` query and the -//! name is a DNS-safe subdomain (`claw-.exe.xyz`). -//! - [`UserKeyring`] abstracts where per-user SSH keys live (supabase, kms, -//! in-memory for tests). The manager never touches storage directly. -//! - [`ClawManager`] orchestrates `provision / get / suspend / resume / -//! deprovision`, mints VM-scoped tokens with a `ctx` that carries the user -//! id through to claw (via `X-ExeDev-Token-Ctx`), and exposes a typed -//! `call::` helper for HTTPS control-plane requests. -//! -//! Billing is intentionally out of scope: callers (e.g. the Stripe webhook in -//! `apps/api`) decide *when* to provision/suspend/deprovision and delegate -//! the mechanics here. - -mod keyring; -mod manager; -mod naming; - -pub use hypr_exedev as exedev; - -pub use keyring::{InMemoryKeyring, UserKeyring}; -pub use manager::{ - ClawCallOptions, ClawManager, ClawProvisionSpec, ClawTokenCtx, ProvisionOutcome, -}; -pub use naming::{UserId, VM_NAME_PREFIX, vm_name}; - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error(transparent)] - Exedev(#[from] hypr_exedev::Error), - #[error("keyring backend: {0}")] - Keyring(String), - #[error("ssh key pem generation: {0}")] - KeyGen(String), - #[error(transparent)] - Json(#[from] serde_json::Error), -} diff --git a/crates/api-claw/src/manager.rs b/crates/api-claw/src/manager.rs deleted file mode 100644 index 627655afd6..0000000000 --- a/crates/api-claw/src/manager.rs +++ /dev/null @@ -1,390 +0,0 @@ -use std::collections::BTreeMap; - -use chrono::{DateTime, Utc}; -use hypr_exedev::{ - Exe0Token, ExedevClient, Permissions, ShareVisibility, Vm, VmAuth, VmHttpClient, VmNewArgs, -}; -use serde::{Serialize, de::DeserializeOwned}; - -use crate::Error; -use crate::keyring::{UserKeyring, UserSshKey}; -use crate::naming::{UserId, vm_name}; - -/// Static description of what "a claw VM" looks like for this deployment. -/// -/// Everything here is environment-level: image, disk, the port claw binds -/// inside the VM, and a *template* of env vars. Per-user env vars (tokens, -/// user id) are injected by [`ClawManager::provision`]. -#[derive(Debug, Clone)] -pub struct ClawProvisionSpec { - pub image: String, - pub disk: String, - /// Internal port the claw HTTP listener binds to. Published via `share port`. - pub port: u16, - /// Deployment-wide env applied to every VM (e.g. OPENAI_BASE_URL). Per-user - /// secrets should be layered in via `per_user_env` on [`ClawManager`]. - pub env: BTreeMap, -} - -impl Default for ClawProvisionSpec { - fn default() -> Self { - Self { - image: "ghcr.io/boldsoftware/exeuntu:latest".into(), - disk: "20GB".into(), - port: 8080, - env: BTreeMap::new(), - } - } -} - -/// Shape of the `ctx` JSON embedded in VM-scoped tokens. Signed by exe.dev -/// and forwarded to claw verbatim via `X-ExeDev-Token-Ctx`. -#[derive(Debug, Clone, Serialize, serde::Deserialize)] -pub struct ClawTokenCtx { - pub user_id: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub tenant: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub scopes: Vec, -} - -/// What [`ClawManager::provision`] produced. `Created` means we issued a new -/// VM in this call; `Existed` means it was already present and we short-circuited. -#[derive(Debug, Clone)] -pub enum ProvisionOutcome { - Created(Vm), - Existed(Vm), -} - -impl ProvisionOutcome { - pub fn vm(&self) -> &Vm { - match self { - ProvisionOutcome::Created(vm) | ProvisionOutcome::Existed(vm) => vm, - } - } - - pub fn into_vm(self) -> Vm { - match self { - ProvisionOutcome::Created(vm) | ProvisionOutcome::Existed(vm) => vm, - } - } -} - -/// Per-user env closure. Given a user id, returns the env vars claw needs in -/// that user's VM. Kept separate from the static [`ClawProvisionSpec`] so that -/// secrets can be fetched per-call from a vault without widening the spec type. -pub type PerUserEnv = Box BTreeMap + Send + Sync + 'static>; - -pub struct ClawManager { - client: ExedevClient, - spec: ClawProvisionSpec, - keyring: K, - per_user_env: PerUserEnv, -} - -impl ClawManager { - pub fn new(client: ExedevClient, spec: ClawProvisionSpec, keyring: K) -> Self { - Self { - client, - spec, - keyring, - per_user_env: Box::new(|_| BTreeMap::new()), - } - } - - /// Install the per-user env closure. Called for every provision and for - /// every token mint (via `ctx.user_id`), so avoid network calls here if - /// you can help it. - pub fn with_per_user_env(mut self, f: PerUserEnv) -> Self { - self.per_user_env = f; - self - } - - pub fn vm_name(&self, user: &UserId) -> String { - vm_name(user) - } - - pub fn spec(&self) -> &ClawProvisionSpec { - &self.spec - } - - /// Look up the VM for a user. `None` if not yet provisioned. - pub async fn get(&self, user: &UserId) -> Result, Error> { - let name = vm_name(user); - Ok(self.client.vm_get(&name).await?) - } - - /// Idempotent provision. - /// - /// Does: - /// 1. keyring `get_or_create` — either returns an existing keypair or - /// generates a fresh one; - /// 2. ensures the public key is registered on the exe.dev account - /// (unique per user — `remove` on deprovision revokes every token - /// ever minted for this user without touching other users); - /// 3. if `ls ` already has a running/starting VM, returns `Existed`; - /// 4. otherwise `new --name= ...` with the spec + per-user env; - /// 5. sets the proxy port and forces it private (bearer-token access). - pub async fn provision(&self, user: &UserId) -> Result { - let name = vm_name(user); - let key = self.keyring.get_or_create(user).await?; - self.ensure_ssh_key_registered(&key, user).await?; - - if let Some(existing) = self.client.vm_get(&name).await? { - self.configure_proxy(&name).await?; - return Ok(ProvisionOutcome::Existed(existing)); - } - - let args = self.build_new_args(user, &name); - self.client.vm_new(args).await?; - - // Poll once via vm_get; `new` returns once the VM is booked but we - // still want a strongly-typed record. - let vm = self.client.vm_get(&name).await?.ok_or_else(|| { - hypr_exedev::Error::NotFound(format!("vm {name} not found after new")) - })?; - - self.configure_proxy(&name).await?; - Ok(ProvisionOutcome::Created(vm)) - } - - /// Revoke all tokens minted for a user without destroying the VM. - /// - /// Implementation: remove the user's SSH key from the exe.dev account. - /// Every exe0/exe1 token derived from it stops validating immediately. - /// Call [`ClawManager::resume`] to reinstate. - pub async fn suspend(&self, user: &UserId) -> Result<(), Error> { - let key = self.keyring.get_or_create(user).await?; - // `ssh-key remove` accepts name | fingerprint | public-key. - // We use the fingerprint since the key may have been renamed upstream. - match self.client.ssh_key_remove(&key.fingerprint).await { - Ok(()) => Ok(()), - // Ignore "not found" — already suspended is fine. - Err(hypr_exedev::Error::NotFound(_)) => Ok(()), - Err(e) => Err(e.into()), - } - } - - /// Reverse of [`ClawManager::suspend`]: re-registers the user's key. - pub async fn resume(&self, user: &UserId) -> Result<(), Error> { - let key = self.keyring.get_or_create(user).await?; - self.ensure_ssh_key_registered(&key, user).await - } - - /// Delete the VM and purge the user's keypair. - pub async fn deprovision(&self, user: &UserId) -> Result<(), Error> { - let name = vm_name(user); - if self.client.vm_get(&name).await?.is_some() { - self.client.vm_remove(&[&name]).await?; - } - let key = self.keyring.get_or_create(user).await?; - let _ = self.client.ssh_key_remove(&key.fingerprint).await; - self.keyring.remove(user).await?; - Ok(()) - } - - /// Mint a VM-scoped exe0 token for this user. Signed with the user's key, - /// so revoking the key revokes the token. - pub fn mint_vm_token( - &self, - user: &UserId, - key: &UserSshKey, - exp: DateTime, - extra_ctx: Option, - ) -> Result { - let name = vm_name(user); - let ctx = ClawTokenCtx { - user_id: user.to_string(), - tenant: None, - scopes: vec![], - }; - let mut ctx_json = serde_json::to_value(&ctx)?; - if let Some(extra) = extra_ctx - && let (Some(ctx_map), Some(extra_map)) = (ctx_json.as_object_mut(), extra.as_object()) - { - for (k, v) in extra_map { - ctx_map.insert(k.clone(), v.clone()); - } - } - let perms = Permissions::default() - .with_exp(exp.timestamp()) - .with_ctx(&ctx_json)? - .to_json()?; - Ok(Exe0Token::mint_for_vm(&name, &perms, &key.private_pem)?) - } - - /// Build an HTTPS client pointed at a user's VM, authed with a short-lived - /// VM-scoped exe0 token. For repeated calls, reuse the returned client. - pub async fn http_client( - &self, - user: &UserId, - opts: ClawCallOptions, - ) -> Result { - let key = self.keyring.get_or_create(user).await?; - let token = self.mint_vm_token(user, &key, opts.exp, opts.extra_ctx)?; - Ok(VmHttpClient::for_vm( - &vm_name(user), - VmAuth::Bearer(token.into_string()), - )?) - } - - /// One-shot typed HTTP call against a user's VM. - pub async fn call( - &self, - user: &UserId, - path: &str, - body: &Req, - opts: ClawCallOptions, - ) -> Result { - let http = self.http_client(user, opts).await?; - Ok(http.post_json_for(path, body).await?) - } - - // --- internals --- - - fn build_new_args(&self, user: &UserId, name: &str) -> VmNewArgs { - let mut args = VmNewArgs::new() - .name(name) - .image(&self.spec.image) - .disk(&self.spec.disk) - .no_email(true); - for (k, v) in &self.spec.env { - args = args.env(k, v); - } - for (k, v) in (self.per_user_env)(user) { - args = args.env(k, v); - } - args - } - - async fn ensure_ssh_key_registered( - &self, - key: &UserSshKey, - _user: &UserId, - ) -> Result<(), Error> { - let existing = self.client.ssh_key_list().await?; - if existing - .iter() - .any(|k| k.fingerprint == key.fingerprint || k.public_key == key.public_openssh) - { - return Ok(()); - } - self.client.ssh_key_add(&key.public_openssh).await?; - Ok(()) - } - - async fn configure_proxy(&self, name: &str) -> Result<(), Error> { - self.client.share_port(name, self.spec.port).await?; - self.client - .share_visibility(name, ShareVisibility::Private) - .await?; - Ok(()) - } -} - -/// Options for a single VM call. `exp` is an absolute time, not a duration, -/// so callers are explicit about token lifetime. -#[derive(Debug, Clone)] -pub struct ClawCallOptions { - pub exp: DateTime, - pub extra_ctx: Option, -} - -impl ClawCallOptions { - /// Token valid for `seconds` from now. Use short values: the token is - /// re-minted every time `call` runs, so minutes are usually plenty. - pub fn ttl_seconds(seconds: i64) -> Self { - Self { - exp: Utc::now() + chrono::Duration::seconds(seconds), - extra_ctx: None, - } - } - - pub fn with_ctx(mut self, ctx: serde_json::Value) -> Self { - self.extra_ctx = Some(ctx); - self - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::keyring::InMemoryKeyring; - - fn noop_manager() -> ClawManager { - let client = ExedevClient::builder().token("exe1.dummy").build().unwrap(); - ClawManager::new(client, ClawProvisionSpec::default(), InMemoryKeyring::new()) - } - - #[test] - fn vm_name_matches_naming_module() { - let m = noop_manager(); - let u = UserId::new("u1"); - assert_eq!(m.vm_name(&u), vm_name(&u)); - } - - #[tokio::test] - async fn mint_vm_token_includes_user_in_ctx() { - let m = noop_manager(); - let user = UserId::new("user-42"); - let key = m.keyring.get_or_create(&user).await.unwrap(); - let exp = Utc::now() + chrono::Duration::minutes(5); - let token = m.mint_vm_token(&user, &key, exp, None).unwrap(); - - let parts: Vec<&str> = token.as_str().split('.').collect(); - assert_eq!(parts.len(), 3); - assert_eq!(parts[0], "exe0"); - - use base64::Engine; - let payload = base64::engine::general_purpose::URL_SAFE_NO_PAD - .decode(parts[1]) - .unwrap(); - let v: serde_json::Value = serde_json::from_slice(&payload).unwrap(); - assert_eq!(v["ctx"]["user_id"], "user-42"); - } - - #[tokio::test] - async fn mint_vm_token_merges_extra_ctx() { - let m = noop_manager(); - let user = UserId::new("u"); - let key = m.keyring.get_or_create(&user).await.unwrap(); - let token = m - .mint_vm_token( - &user, - &key, - Utc::now() + chrono::Duration::minutes(5), - Some(serde_json::json!({ "session": "abc" })), - ) - .unwrap(); - - use base64::Engine; - let payload = base64::engine::general_purpose::URL_SAFE_NO_PAD - .decode(token.as_str().split('.').nth(1).unwrap()) - .unwrap(); - let v: serde_json::Value = serde_json::from_slice(&payload).unwrap(); - assert_eq!(v["ctx"]["user_id"], "u"); - assert_eq!(v["ctx"]["session"], "abc"); - } - - #[tokio::test] - async fn build_new_args_injects_spec_and_per_user_env() { - let client = ExedevClient::builder().token("t").build().unwrap(); - let spec = ClawProvisionSpec { - image: "img".into(), - disk: "20GB".into(), - port: 8080, - env: BTreeMap::from([("GLOBAL".into(), "g".into())]), - }; - let m = ClawManager::new(client, spec, InMemoryKeyring::new()).with_per_user_env(Box::new( - |u: &UserId| BTreeMap::from([("USER_ID".into(), u.to_string())]), - )); - let user = UserId::new("bob"); - let args = m.build_new_args(&user, "claw-abc"); - assert_eq!(args.name.as_deref(), Some("claw-abc")); - assert_eq!(args.image.as_deref(), Some("img")); - assert_eq!(args.disk.as_deref(), Some("20GB")); - assert!(args.env.contains(&("GLOBAL".into(), "g".into()))); - assert!(args.env.contains(&("USER_ID".into(), "bob".into()))); - assert!(args.no_email); - } -} diff --git a/crates/api-claw/src/naming.rs b/crates/api-claw/src/naming.rs deleted file mode 100644 index ca09269c9f..0000000000 --- a/crates/api-claw/src/naming.rs +++ /dev/null @@ -1,91 +0,0 @@ -use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; - -pub const VM_NAME_PREFIX: &str = "claw"; - -/// Opaque user identifier. We accept any string so callers can plug in their -/// own id source (supabase uuid, stripe customer id, etc.). The VM name is -/// derived from a hash so the raw id never leaves our systems via the exe.dev -/// control plane. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct UserId(String); - -impl UserId { - pub fn new(id: impl Into) -> Self { - Self(id.into()) - } - - pub fn as_str(&self) -> &str { - &self.0 - } -} - -impl std::fmt::Display for UserId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.0) - } -} - -impl From for UserId { - fn from(s: String) -> Self { - Self(s) - } -} - -impl From<&str> for UserId { - fn from(s: &str) -> Self { - Self(s.to_string()) - } -} - -/// Deterministic, DNS-safe VM name for a user: `claw-`. -/// -/// Chosen so: -/// - it fits the `[a-z0-9-]` subdomain label rules for `.exe.xyz` -/// - short enough to comfortably fit in DNS labels and logs -/// - collision probability is astronomically low for our user counts -/// - one-way: the user id can't be recovered from the name -pub fn vm_name(user: &UserId) -> String { - let mut hasher = Sha256::new(); - hasher.update(user.as_str().as_bytes()); - let digest = hasher.finalize(); - let hex: String = digest.iter().take(6).map(|b| format!("{b:02x}")).collect(); - format!("{VM_NAME_PREFIX}-{hex}") -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn deterministic_across_calls() { - let u = UserId::new("user-123"); - assert_eq!(vm_name(&u), vm_name(&u)); - } - - #[test] - fn different_users_different_names() { - let a = vm_name(&UserId::new("a")); - let b = vm_name(&UserId::new("b")); - assert_ne!(a, b); - } - - #[test] - fn dns_safe() { - let name = vm_name(&UserId::new("User With Spaces & Weird!@#")); - assert!(name.starts_with("claw-")); - assert!( - name.chars() - .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-'), - "non-DNS chars in: {name}" - ); - assert!(!name.starts_with('-') && !name.ends_with('-')); - assert!(name.len() <= 63, "too long for DNS label: {name}"); - } - - #[test] - fn prefix_matches_const() { - let name = vm_name(&UserId::new("x")); - assert!(name.starts_with(&format!("{VM_NAME_PREFIX}-"))); - } -} diff --git a/crates/api-client/build.rs b/crates/api-client/build.rs index c0894a5556..3147aff4dc 100644 --- a/crates/api-client/build.rs +++ b/crates/api-client/build.rs @@ -33,7 +33,7 @@ const TYPE_REPLACEMENTS: &[(&str, &str)] = &[ fn main() { let src = concat!( env!("CARGO_MANIFEST_DIR"), - "/../../apps/api/openapi.gen.json" + "/../../packages/api-client/openapi.gen.json" ); println!("cargo:rerun-if-changed={src}"); diff --git a/crates/api-env/Cargo.toml b/crates/api-env/Cargo.toml deleted file mode 100644 index 8f7d17e88b..0000000000 --- a/crates/api-env/Cargo.toml +++ /dev/null @@ -1,7 +0,0 @@ -[package] -name = "api-env" -version = "0.1.0" -edition = "2024" - -[dependencies] -serde = { workspace = true, features = ["derive"] } diff --git a/crates/api-env/src/lib.rs b/crates/api-env/src/lib.rs deleted file mode 100644 index 441e52d43c..0000000000 --- a/crates/api-env/src/lib.rs +++ /dev/null @@ -1,59 +0,0 @@ -use serde::Deserialize; - -pub fn filter_empty<'de, D>(deserializer: D) -> Result, D::Error> -where - D: serde::Deserializer<'de>, -{ - let s: Option = Option::deserialize(deserializer)?; - Ok(s.filter(|s| !s.is_empty())) -} - -pub fn string_to_u64<'de, D>(deserializer: D) -> Result -where - D: serde::Deserializer<'de>, -{ - let s = String::deserialize(deserializer)?; - s.parse().map_err(serde::de::Error::custom) -} - -#[derive(Clone, Deserialize)] -pub struct SupabaseEnv { - pub supabase_url: String, - pub supabase_anon_key: String, - pub supabase_service_role_key: String, -} - -#[derive(Clone, Deserialize)] -pub struct NangoEnv { - #[serde(default)] - pub nango_api_base: Option, - pub nango_secret_key: String, -} - -#[derive(Clone, Deserialize)] -pub struct OpenRouterEnv { - pub openrouter_api_key: String, -} - -#[derive(Clone, Deserialize)] -pub struct StripeEnv { - pub stripe_secret_key: String, - pub stripe_monthly_price_id: String, - pub stripe_yearly_price_id: String, -} - -#[derive(Clone, Deserialize)] -pub struct LoopsEnv { - pub loops_key: String, -} - -#[derive(Clone, Deserialize)] -pub struct PyannoteEnv { - pub pyannote_api_key: String, - #[serde(default = "default_pyannote_api_base")] - pub pyannote_api_base: String, -} - -fn default_pyannote_api_base() -> String { - "https://api.pyannote.ai".to_string() -} diff --git a/crates/api-error/Cargo.toml b/crates/api-error/Cargo.toml deleted file mode 100644 index df8dca801e..0000000000 --- a/crates/api-error/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "api-error" -version = "0.1.0" -edition = "2024" - -[dependencies] -axum = { workspace = true } -sentry = { workspace = true } -serde = { workspace = true, features = ["derive"] } -tracing = { workspace = true } -utoipa = { workspace = true } diff --git a/crates/api-error/src/lib.rs b/crates/api-error/src/lib.rs deleted file mode 100644 index 41600fe722..0000000000 --- a/crates/api-error/src/lib.rs +++ /dev/null @@ -1,34 +0,0 @@ -use axum::{ - Json, - http::StatusCode, - response::{IntoResponse, Response}, -}; -use serde::Serialize; -use utoipa::ToSchema; - -#[derive(Debug, Serialize, ToSchema)] -pub struct ErrorDetails { - pub code: String, - pub message: String, -} - -#[derive(Debug, Serialize, ToSchema)] -pub struct ErrorResponse { - pub error: ErrorDetails, -} - -pub fn error_response(status: StatusCode, code: &str, message: &str) -> Response { - let (code, message) = if status.is_server_error() { - tracing::error!(error.type = %code, error = %message); - sentry::capture_message(message, sentry::Level::Error); - (code.to_string(), "Internal server error".to_string()) - } else { - (code.to_string(), message.to_string()) - }; - - let body = Json(ErrorResponse { - error: ErrorDetails { code, message }, - }); - - (status, body).into_response() -} diff --git a/crates/api-mail/Cargo.toml b/crates/api-mail/Cargo.toml deleted file mode 100644 index 5598af9c5a..0000000000 --- a/crates/api-mail/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "api-mail" -version = "0.1.0" -edition = "2024" - -[dependencies] -hypr-api-auth = { workspace = true } -hypr-api-error = { workspace = true } -hypr-api-nango = { workspace = true } -hypr-google-mail = { workspace = true, features = ["utoipa"] } -hypr-nango = { workspace = true } - -axum = { workspace = true } -utoipa = { workspace = true } - -serde = { workspace = true, features = ["derive"] } -thiserror = { workspace = true } diff --git a/crates/api-mail/src/error.rs b/crates/api-mail/src/error.rs deleted file mode 100644 index 03ec30be03..0000000000 --- a/crates/api-mail/src/error.rs +++ /dev/null @@ -1,41 +0,0 @@ -use axum::{ - http::StatusCode, - response::{IntoResponse, Response}, -}; -use thiserror::Error; - -pub type Result = std::result::Result; - -#[derive(Debug, Error)] -pub enum MailError { - #[error("Authentication error: {0}")] - #[allow(dead_code)] - Auth(String), - - #[error("Invalid request: {0}")] - #[allow(dead_code)] - BadRequest(String), - - #[error("Internal error: {0}")] - Internal(String), - - #[error(transparent)] - NangoConnection(#[from] hypr_api_nango::NangoConnectionError), -} - -impl IntoResponse for MailError { - fn into_response(self) -> Response { - let (status, code, message) = match self { - Self::Auth(message) => (StatusCode::UNAUTHORIZED, "unauthorized", message), - Self::BadRequest(message) => (StatusCode::BAD_REQUEST, "bad_request", message), - Self::Internal(message) => ( - StatusCode::INTERNAL_SERVER_ERROR, - "internal_server_error", - message, - ), - Self::NangoConnection(err) => return err.into_response(), - }; - - hypr_api_error::error_response(status, code, &message) - } -} diff --git a/crates/api-mail/src/google/mod.rs b/crates/api-mail/src/google/mod.rs deleted file mode 100644 index 56f9e1af32..0000000000 --- a/crates/api-mail/src/google/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -pub(crate) mod routes; - -use axum::{Router, routing::post}; - -pub fn router() -> Router { - Router::new() - .route("/list-labels", post(routes::list_labels)) - .route("/list-messages", post(routes::list_messages)) - .route("/get-message", post(routes::get_message)) - .route("/get-attachment", post(routes::get_attachment)) - .route("/get-profile", post(routes::get_profile)) - .route("/list-threads", post(routes::list_threads)) - .route("/get-thread", post(routes::get_thread)) - .route("/list-history", post(routes::list_history)) -} diff --git a/crates/api-mail/src/google/routes.rs b/crates/api-mail/src/google/routes.rs deleted file mode 100644 index 7575f36f24..0000000000 --- a/crates/api-mail/src/google/routes.rs +++ /dev/null @@ -1,458 +0,0 @@ -use axum::{Extension, Json}; -use hypr_api_auth::AuthContext; -use hypr_api_nango::{GoogleMail, NangoConnectionState, NangoIntegrationId}; -use hypr_google_mail::{ - Attachment, GoogleMailClient, ListHistoryResponse, ListLabelsResponse, ListMessagesResponse, - ListThreadsResponse, Message, Profile, Thread, -}; -use serde::Deserialize; -use utoipa::ToSchema; - -use crate::error::{MailError, Result}; - -#[derive(Debug, Deserialize, ToSchema)] -pub struct GoogleListLabelsRequest { - pub connection_id: String, -} - -#[derive(Debug, Deserialize, ToSchema)] -pub struct GoogleListMessagesRequest { - pub connection_id: String, - #[serde(default)] - pub q: Option, - #[serde(default)] - pub max_results: Option, - #[serde(default)] - pub page_token: Option, - #[serde(default)] - pub label_ids: Option>, - #[serde(default)] - pub include_spam_trash: Option, -} - -#[derive(Debug, Deserialize, ToSchema)] -pub struct GoogleGetMessageRequest { - pub connection_id: String, - pub id: String, - #[serde(default)] - pub format: Option, - #[serde(default)] - pub metadata_headers: Option>, -} - -#[derive(Debug, Deserialize, ToSchema)] -pub struct GoogleGetAttachmentRequest { - pub connection_id: String, - pub message_id: String, - pub attachment_id: String, -} - -#[derive(Debug, Deserialize, ToSchema)] -pub struct GoogleGetProfileRequest { - pub connection_id: String, -} - -#[derive(Debug, Deserialize, ToSchema)] -pub struct GoogleListThreadsRequest { - pub connection_id: String, - #[serde(default)] - pub q: Option, - #[serde(default)] - pub max_results: Option, - #[serde(default)] - pub page_token: Option, - #[serde(default)] - pub label_ids: Option>, - #[serde(default)] - pub include_spam_trash: Option, -} - -#[derive(Debug, Deserialize, ToSchema)] -pub struct GoogleGetThreadRequest { - pub connection_id: String, - pub id: String, - #[serde(default)] - pub format: Option, - #[serde(default)] - pub metadata_headers: Option>, -} - -#[derive(Debug, Deserialize, ToSchema)] -pub struct GoogleListHistoryRequest { - pub connection_id: String, - pub start_history_id: String, - #[serde(default)] - pub max_results: Option, - #[serde(default)] - pub page_token: Option, - #[serde(default)] - pub label_id: Option, - #[serde(default)] - pub history_types: Option>, -} - -#[utoipa::path( - post, - path = "/google/list-labels", - operation_id = "google_list_labels", - request_body = GoogleListLabelsRequest, - responses( - (status = 200, description = "Google mail labels fetched", body = ListLabelsResponse), - (status = 401, description = "Unauthorized"), - (status = 500, description = "Internal server error"), - ), - tag = "mail", -)] -pub async fn list_labels( - Extension(auth): Extension, - Extension(nango_state): Extension, - Json(req): Json, -) -> Result> { - let http = nango_state - .build_http_client( - &auth.token, - &auth.claims.sub, - GoogleMail::ID, - &req.connection_id, - ) - .await?; - - let client = GoogleMailClient::new(http); - - let response = client - .list_labels() - .await - .map_err(|e| MailError::Internal(e.to_string()))?; - - Ok(Json(response)) -} - -#[utoipa::path( - post, - path = "/google/list-messages", - operation_id = "google_list_messages", - request_body = GoogleListMessagesRequest, - responses( - (status = 200, description = "Google mail messages fetched", body = ListMessagesResponse), - (status = 401, description = "Unauthorized"), - (status = 500, description = "Internal server error"), - ), - tag = "mail", -)] -pub async fn list_messages( - Extension(auth): Extension, - Extension(nango_state): Extension, - Json(req): Json, -) -> Result> { - let http = nango_state - .build_http_client( - &auth.token, - &auth.claims.sub, - GoogleMail::ID, - &req.connection_id, - ) - .await?; - - let client = GoogleMailClient::new(http); - - let google_req = hypr_google_mail::ListMessagesRequest { - q: req.q, - max_results: req.max_results, - page_token: req.page_token, - label_ids: req.label_ids, - include_spam_trash: req.include_spam_trash, - }; - - let response = client - .list_messages(google_req) - .await - .map_err(|e| MailError::Internal(e.to_string()))?; - - Ok(Json(response)) -} - -#[utoipa::path( - post, - path = "/google/get-message", - operation_id = "google_get_message", - request_body = GoogleGetMessageRequest, - responses( - (status = 200, description = "Google mail message fetched", body = Message), - (status = 401, description = "Unauthorized"), - (status = 500, description = "Internal server error"), - ), - tag = "mail", -)] -pub async fn get_message( - Extension(auth): Extension, - Extension(nango_state): Extension, - Json(req): Json, -) -> Result> { - let http = nango_state - .build_http_client( - &auth.token, - &auth.claims.sub, - GoogleMail::ID, - &req.connection_id, - ) - .await?; - - let client = GoogleMailClient::new(http); - - let format = req - .format - .as_deref() - .map(|s| match s { - "full" => Ok(hypr_google_mail::MessageFormat::Full), - "metadata" => Ok(hypr_google_mail::MessageFormat::Metadata), - "minimal" => Ok(hypr_google_mail::MessageFormat::Minimal), - "raw" => Ok(hypr_google_mail::MessageFormat::Raw), - other => Err(MailError::BadRequest(format!("Invalid format: {other}"))), - }) - .transpose()?; - - let google_req = hypr_google_mail::GetMessageRequest { - id: req.id, - format, - metadata_headers: req.metadata_headers, - }; - - let response = client - .get_message(google_req) - .await - .map_err(|e| MailError::Internal(e.to_string()))?; - - Ok(Json(response)) -} - -#[utoipa::path( - post, - path = "/google/get-attachment", - operation_id = "google_get_attachment", - request_body = GoogleGetAttachmentRequest, - responses( - (status = 200, description = "Google mail attachment fetched", body = Attachment), - (status = 401, description = "Unauthorized"), - (status = 500, description = "Internal server error"), - ), - tag = "mail", -)] -pub async fn get_attachment( - Extension(auth): Extension, - Extension(nango_state): Extension, - Json(req): Json, -) -> Result> { - let http = nango_state - .build_http_client( - &auth.token, - &auth.claims.sub, - GoogleMail::ID, - &req.connection_id, - ) - .await?; - - let client = GoogleMailClient::new(http); - - let response = client - .get_attachment(&req.message_id, &req.attachment_id) - .await - .map_err(|e| MailError::Internal(e.to_string()))?; - - Ok(Json(response)) -} - -#[utoipa::path( - post, - path = "/google/get-profile", - operation_id = "google_get_profile", - request_body = GoogleGetProfileRequest, - responses( - (status = 200, description = "Google mail profile fetched", body = Profile), - (status = 401, description = "Unauthorized"), - (status = 500, description = "Internal server error"), - ), - tag = "mail", -)] -pub async fn get_profile( - Extension(auth): Extension, - Extension(nango_state): Extension, - Json(req): Json, -) -> Result> { - let http = nango_state - .build_http_client( - &auth.token, - &auth.claims.sub, - GoogleMail::ID, - &req.connection_id, - ) - .await?; - - let client = GoogleMailClient::new(http); - - let response = client - .get_profile() - .await - .map_err(|e| MailError::Internal(e.to_string()))?; - - Ok(Json(response)) -} - -#[utoipa::path( - post, - path = "/google/list-threads", - operation_id = "google_list_threads", - request_body = GoogleListThreadsRequest, - responses( - (status = 200, description = "Google mail threads fetched", body = ListThreadsResponse), - (status = 401, description = "Unauthorized"), - (status = 500, description = "Internal server error"), - ), - tag = "mail", -)] -pub async fn list_threads( - Extension(auth): Extension, - Extension(nango_state): Extension, - Json(req): Json, -) -> Result> { - let http = nango_state - .build_http_client( - &auth.token, - &auth.claims.sub, - GoogleMail::ID, - &req.connection_id, - ) - .await?; - - let client = GoogleMailClient::new(http); - - let google_req = hypr_google_mail::ListThreadsRequest { - q: req.q, - max_results: req.max_results, - page_token: req.page_token, - label_ids: req.label_ids, - include_spam_trash: req.include_spam_trash, - }; - - let response = client - .list_threads(google_req) - .await - .map_err(|e| MailError::Internal(e.to_string()))?; - - Ok(Json(response)) -} - -#[utoipa::path( - post, - path = "/google/get-thread", - operation_id = "google_get_thread", - request_body = GoogleGetThreadRequest, - responses( - (status = 200, description = "Google mail thread fetched", body = Thread), - (status = 401, description = "Unauthorized"), - (status = 500, description = "Internal server error"), - ), - tag = "mail", -)] -pub async fn get_thread( - Extension(auth): Extension, - Extension(nango_state): Extension, - Json(req): Json, -) -> Result> { - let http = nango_state - .build_http_client( - &auth.token, - &auth.claims.sub, - GoogleMail::ID, - &req.connection_id, - ) - .await?; - - let client = GoogleMailClient::new(http); - - let format = req - .format - .as_deref() - .map(|s| match s { - "full" => Ok(hypr_google_mail::MessageFormat::Full), - "metadata" => Ok(hypr_google_mail::MessageFormat::Metadata), - "minimal" => Ok(hypr_google_mail::MessageFormat::Minimal), - "raw" => Ok(hypr_google_mail::MessageFormat::Raw), - other => Err(MailError::BadRequest(format!("Invalid format: {other}"))), - }) - .transpose()?; - - let google_req = hypr_google_mail::GetThreadRequest { - id: req.id, - format, - metadata_headers: req.metadata_headers, - }; - - let response = client - .get_thread(google_req) - .await - .map_err(|e| MailError::Internal(e.to_string()))?; - - Ok(Json(response)) -} - -#[utoipa::path( - post, - path = "/google/list-history", - operation_id = "google_list_history", - request_body = GoogleListHistoryRequest, - responses( - (status = 200, description = "Google mail history fetched", body = ListHistoryResponse), - (status = 401, description = "Unauthorized"), - (status = 500, description = "Internal server error"), - ), - tag = "mail", -)] -pub async fn list_history( - Extension(auth): Extension, - Extension(nango_state): Extension, - Json(req): Json, -) -> Result> { - let http = nango_state - .build_http_client( - &auth.token, - &auth.claims.sub, - GoogleMail::ID, - &req.connection_id, - ) - .await?; - - let client = GoogleMailClient::new(http); - - let history_types = req - .history_types - .map(|types| { - types - .iter() - .map(|s| match s.as_str() { - "messageAdded" => Ok(hypr_google_mail::HistoryType::MessageAdded), - "messageDeleted" => Ok(hypr_google_mail::HistoryType::MessageDeleted), - "labelAdded" => Ok(hypr_google_mail::HistoryType::LabelAdded), - "labelRemoved" => Ok(hypr_google_mail::HistoryType::LabelRemoved), - other => Err(MailError::BadRequest(format!( - "Invalid history type: {other}" - ))), - }) - .collect::, _>>() - }) - .transpose()?; - - let google_req = hypr_google_mail::ListHistoryRequest { - start_history_id: req.start_history_id, - max_results: req.max_results, - page_token: req.page_token, - label_id: req.label_id, - history_types, - }; - - let response = client - .list_history(google_req) - .await - .map_err(|e| MailError::Internal(e.to_string()))?; - - Ok(Json(response)) -} diff --git a/crates/api-mail/src/lib.rs b/crates/api-mail/src/lib.rs deleted file mode 100644 index 479b52ea29..0000000000 --- a/crates/api-mail/src/lib.rs +++ /dev/null @@ -1,11 +0,0 @@ -mod error; -mod google; -mod openapi; - -use axum::Router; - -pub use openapi::openapi; - -pub fn router() -> Router { - Router::new().nest("/google", google::router()) -} diff --git a/crates/api-mail/src/openapi.rs b/crates/api-mail/src/openapi.rs deleted file mode 100644 index daf6809ca5..0000000000 --- a/crates/api-mail/src/openapi.rs +++ /dev/null @@ -1,35 +0,0 @@ -use utoipa::OpenApi; - -#[derive(OpenApi)] -#[openapi( - paths( - crate::google::routes::list_labels, - crate::google::routes::list_messages, - crate::google::routes::get_message, - crate::google::routes::get_attachment, - crate::google::routes::get_profile, - crate::google::routes::list_threads, - crate::google::routes::get_thread, - crate::google::routes::list_history, - ), - components(schemas( - crate::google::routes::GoogleListLabelsRequest, - crate::google::routes::GoogleListMessagesRequest, - crate::google::routes::GoogleGetMessageRequest, - crate::google::routes::GoogleGetAttachmentRequest, - crate::google::routes::GoogleGetProfileRequest, - crate::google::routes::GoogleListThreadsRequest, - crate::google::routes::GoogleGetThreadRequest, - crate::google::routes::GoogleListHistoryRequest, - )), - tags( - (name = "mail", description = "Mail management") - ) -)] -struct ApiDoc; - -pub fn openapi() -> utoipa::openapi::OpenApi { - let mut doc = ApiDoc::openapi(); - doc.merge(hypr_google_mail::openapi::openapi()); - doc -} diff --git a/crates/api-messenger/Cargo.toml b/crates/api-messenger/Cargo.toml deleted file mode 100644 index 979f25d673..0000000000 --- a/crates/api-messenger/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "api-messenger" -version = "0.1.0" -edition = "2024" - -[dependencies] -hypr-api-error = { workspace = true } -hypr-http = { workspace = true } -hypr-slack-web = { workspace = true } -hypr-teems = { workspace = true } - -axum = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -thiserror = { workspace = true } diff --git a/crates/api-messenger/src/error.rs b/crates/api-messenger/src/error.rs deleted file mode 100644 index eaec340303..0000000000 --- a/crates/api-messenger/src/error.rs +++ /dev/null @@ -1,47 +0,0 @@ -use axum::{ - http::StatusCode, - response::{IntoResponse, Response}, -}; -use thiserror::Error; - -pub type Result = std::result::Result; - -#[derive(Debug, Error)] -pub enum MessengerError { - #[error("Slack error: {0}")] - Slack(#[from] hypr_slack_web::Error), - - #[error("Teams error: {0}")] - Teams(#[from] hypr_teems::Error), - - #[error("Bad request: {0}")] - BadRequest(String), - - #[error("Internal error: {0}")] - Internal(String), -} - -impl IntoResponse for MessengerError { - fn into_response(self) -> Response { - let (status, code, message) = match self { - Self::BadRequest(msg) => (StatusCode::BAD_REQUEST, "bad_request", msg), - Self::Slack(err) => ( - StatusCode::INTERNAL_SERVER_ERROR, - "slack_error", - err.to_string(), - ), - Self::Teams(err) => ( - StatusCode::INTERNAL_SERVER_ERROR, - "teams_error", - err.to_string(), - ), - Self::Internal(msg) => ( - StatusCode::INTERNAL_SERVER_ERROR, - "internal_server_error", - msg, - ), - }; - - hypr_api_error::error_response(status, code, &message) - } -} diff --git a/crates/api-messenger/src/lib.rs b/crates/api-messenger/src/lib.rs deleted file mode 100644 index 8909bf4c78..0000000000 --- a/crates/api-messenger/src/lib.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod error; -mod routes; - -pub use error::{MessengerError, Result}; -pub use routes::router; diff --git a/crates/api-messenger/src/routes/messenger.rs b/crates/api-messenger/src/routes/messenger.rs deleted file mode 100644 index e62ec44750..0000000000 --- a/crates/api-messenger/src/routes/messenger.rs +++ /dev/null @@ -1,89 +0,0 @@ -use axum::Json; -use serde::{Deserialize, Serialize}; - -use crate::error::{MessengerError, Result}; - -#[derive(Debug, Deserialize)] -#[serde(tag = "platform", rename_all = "lowercase")] -pub enum SendMessageRequest { - Slack(SlackSendRequest), - Teams(TeamsSendRequest), -} - -#[derive(Debug, Deserialize)] -pub struct SlackSendRequest { - pub channel: String, - #[serde(default)] - pub text: Option, - #[serde(default)] - pub blocks: Option, - #[serde(default)] - pub thread_ts: Option, -} - -#[derive(Debug, Deserialize)] -#[allow(dead_code)] -pub struct TeamsSendRequest { - pub team_id: String, - pub channel_id: String, - pub content: String, - #[serde(default)] - pub content_type: Option, -} - -#[derive(Debug, Serialize)] -pub struct SendMessageResponse { - pub platform: String, - pub message_id: String, - pub channel: String, -} - -pub async fn send_message( - Json(payload): Json, -) -> Result> { - match payload { - SendMessageRequest::Slack(req) => { - if req.text.is_none() && req.blocks.is_none() { - return Err(MessengerError::BadRequest( - "either text or blocks must be provided".into(), - )); - } - - let slack_req = hypr_slack_web::PostMessageRequest { - channel: req.channel.clone(), - text: req.text, - blocks: req.blocks, - attachments: None, - thread_ts: req.thread_ts, - reply_broadcast: None, - mrkdwn: None, - unfurl_links: None, - unfurl_media: None, - metadata: None, - username: None, - icon_url: None, - icon_emoji: None, - }; - - let _ = slack_req; - - Err(MessengerError::Internal( - "slack client not configured in app state".into(), - )) - } - SendMessageRequest::Teams(req) => { - let teams_req = hypr_teems::SendMessageRequest { - body: hypr_teems::MessageBody { - content: req.content, - content_type: req.content_type, - }, - }; - - let _ = teams_req; - - Err(MessengerError::Internal( - "teams client not configured in app state".into(), - )) - } - } -} diff --git a/crates/api-messenger/src/routes/mod.rs b/crates/api-messenger/src/routes/mod.rs deleted file mode 100644 index 64d20c1d10..0000000000 --- a/crates/api-messenger/src/routes/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub(crate) mod messenger; - -use axum::{Router, routing::post}; - -pub fn router() -> Router { - Router::new().route("/send", post(messenger::send_message)) -} diff --git a/crates/api-nango/Cargo.toml b/crates/api-nango/Cargo.toml deleted file mode 100644 index ae8f0ae8d3..0000000000 --- a/crates/api-nango/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "api-nango" -version = "0.1.0" -edition = "2024" - -[dependencies] -hypr-api-auth = { workspace = true } -hypr-api-env = { workspace = true } -hypr-api-error = { workspace = true } -hypr-nango = { workspace = true } - -chrono = { workspace = true } -urlencoding = { workspace = true } -utoipa = { workspace = true } - -axum = { workspace = true } -futures-util = { workspace = true } -reqwest = { workspace = true, features = ["json"] } -sentry = { workspace = true } -tokio = { workspace = true } -tracing = { workspace = true } - -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -thiserror = { workspace = true } - -[dev-dependencies] -hex = "0.4" -hmac = { workspace = true } -sha2 = { workspace = true } -tokio = { workspace = true } -wiremock = { workspace = true } diff --git a/crates/api-nango/src/config.rs b/crates/api-nango/src/config.rs deleted file mode 100644 index da3e1f0d84..0000000000 --- a/crates/api-nango/src/config.rs +++ /dev/null @@ -1,47 +0,0 @@ -use hypr_api_env::{NangoEnv, SupabaseEnv}; -use hypr_nango::NangoClient; - -#[derive(Clone)] -pub struct NangoConfig { - pub nango: NangoEnv, - pub supabase_url: String, - pub supabase_anon_key: String, - pub supabase_service_role_key: Option, -} - -impl NangoConfig { - pub fn new( - nango: &NangoEnv, - supabase: &SupabaseEnv, - supabase_service_role_key: Option, - ) -> Self { - Self { - nango: nango.clone(), - supabase_url: supabase.supabase_url.clone(), - supabase_anon_key: supabase.supabase_anon_key.clone(), - supabase_service_role_key, - } - } - - #[cfg(test)] - pub(crate) fn for_test(nango_base: &str, supabase_base: &str) -> Self { - Self { - nango: NangoEnv { - nango_api_base: Some(nango_base.to_string()), - nango_secret_key: "test-secret".to_string(), - }, - supabase_url: supabase_base.to_string(), - supabase_anon_key: "test-anon-key".to_string(), - supabase_service_role_key: Some("test-service-role-key".to_string()), - } - } -} - -pub(crate) fn build_nango_client(config: &NangoConfig) -> Result { - let mut builder = hypr_nango::NangoClient::builder().api_key(&config.nango.nango_secret_key); - if let Some(api_base) = &config.nango.nango_api_base { - builder = builder.api_base(api_base); - } - - builder.build() -} diff --git a/crates/api-nango/src/error.rs b/crates/api-nango/src/error.rs deleted file mode 100644 index e34566495c..0000000000 --- a/crates/api-nango/src/error.rs +++ /dev/null @@ -1,50 +0,0 @@ -use axum::{ - http::StatusCode, - response::{IntoResponse, Response}, -}; -use thiserror::Error; - -pub type Result = std::result::Result; - -#[derive(Debug, Error)] -pub enum NangoError { - #[error("Authentication error: {0}")] - Auth(String), - - #[error("Forbidden: {0}")] - Forbidden(String), - - #[error("Nango error: {0}")] - Nango(String), - - #[error("Invalid request: {0}")] - BadRequest(String), - - #[error("Internal error: {0}")] - #[allow(dead_code)] - Internal(String), -} - -impl From for NangoError { - fn from(err: hypr_nango::Error) -> Self { - Self::Nango(err.to_string()) - } -} - -impl IntoResponse for NangoError { - fn into_response(self) -> Response { - let (status, code, message) = match self { - Self::Auth(message) => (StatusCode::UNAUTHORIZED, "unauthorized", message), - Self::Forbidden(message) => (StatusCode::FORBIDDEN, "forbidden", message), - Self::BadRequest(message) => (StatusCode::BAD_REQUEST, "bad_request", message), - Self::Nango(message) => (StatusCode::INTERNAL_SERVER_ERROR, "nango_error", message), - Self::Internal(message) => ( - StatusCode::INTERNAL_SERVER_ERROR, - "internal_server_error", - message, - ), - }; - - hypr_api_error::error_response(status, code, &message) - } -} diff --git a/crates/api-nango/src/extractor.rs b/crates/api-nango/src/extractor.rs deleted file mode 100644 index 1d3a5f31cd..0000000000 --- a/crates/api-nango/src/extractor.rs +++ /dev/null @@ -1,259 +0,0 @@ -use std::marker::PhantomData; - -use axum::{ - extract::FromRequestParts, - http::{StatusCode, request::Parts}, - response::{IntoResponse, Response}, -}; -use hypr_api_auth::AuthContext; -use hypr_nango::{NangoClient, OwnedNangoHttpClient, OwnedNangoProxy}; - -use crate::integrations::NangoIntegrationId; - -#[derive(Clone)] -pub struct NangoConnectionState { - nango: NangoClient, - http_client: reqwest::Client, - supabase_url: String, - supabase_anon_key: String, -} - -impl NangoConnectionState { - pub fn new( - nango: NangoClient, - supabase_url: impl Into, - supabase_anon_key: impl Into, - ) -> Self { - Self { - nango, - http_client: reqwest::Client::new(), - supabase_url: supabase_url.into().trim_end_matches('/').to_string(), - supabase_anon_key: supabase_anon_key.into(), - } - } - - pub fn from_config(config: &crate::config::NangoConfig) -> Self { - let nango = crate::config::build_nango_client(config).expect("failed to build NangoClient"); - - Self::new(nango, &config.supabase_url, &config.supabase_anon_key) - } - - pub async fn build_http_client( - &self, - auth_token: &str, - user_id: &str, - integration_id: &str, - connection_id: &str, - ) -> Result { - let encoded_user_id = urlencoding::encode(user_id); - let encoded_connection_id = urlencoding::encode(connection_id); - let encoded_integration_id = urlencoding::encode(integration_id); - let url = format!( - "{}/rest/v1/nango_connections?select=connection_id,status&user_id=eq.{}&connection_id=eq.{}&integration_id=eq.{}", - self.supabase_url, encoded_user_id, encoded_connection_id, encoded_integration_id, - ); - - let response = self - .http_client - .get(&url) - .header("Authorization", format!("Bearer {}", auth_token)) - .header("apikey", &self.supabase_anon_key) - .send() - .await - .map_err(|e| NangoConnectionError::Database(e.to_string()))?; - - if !response.status().is_success() { - let status = response.status(); - let body = response.text().await.unwrap_or_default(); - return Err(NangoConnectionError::Database(format!( - "query failed: {} - {}", - status, body - ))); - } - - #[derive(serde::Deserialize)] - struct Row { - #[serde(default)] - status: String, - } - - let rows: Vec = response - .json() - .await - .map_err(|e| NangoConnectionError::Database(e.to_string()))?; - - match rows.into_iter().next() { - Some(row) if row.status == "reconnect_required" => { - return Err(NangoConnectionError::ReconnectRequired( - integration_id.to_string(), - )); - } - Some(_) => {} - None => { - return Err(NangoConnectionError::NotConnected( - integration_id.to_string(), - )); - } - } - - let proxy = OwnedNangoProxy::new( - &self.nango, - integration_id.to_string(), - connection_id.to_string(), - ); - Ok(OwnedNangoHttpClient::new(proxy)) - } - - async fn get_connection_id( - &self, - auth_token: &str, - user_id: &str, - integration_id: &str, - ) -> Result { - #[cfg(debug_assertions)] - if let Ok(connection_id) = std::env::var("DEV_NANGO_CONNECTION_ID") - && !connection_id.is_empty() - { - return Ok(connection_id); - } - - let encoded_user_id = urlencoding::encode(user_id); - let encoded_integration_id = urlencoding::encode(integration_id); - let url = format!( - "{}/rest/v1/nango_connections?select=connection_id,status&user_id=eq.{}&integration_id=eq.{}", - self.supabase_url, encoded_user_id, encoded_integration_id, - ); - - let response = self - .http_client - .get(&url) - .header("Authorization", format!("Bearer {}", auth_token)) - .header("apikey", &self.supabase_anon_key) - .send() - .await - .map_err(|e| NangoConnectionError::Database(e.to_string()))?; - - if !response.status().is_success() { - let status = response.status(); - let body = response.text().await.unwrap_or_default(); - return Err(NangoConnectionError::Database(format!( - "query failed: {} - {}", - status, body - ))); - } - - let rows: Vec = response - .json() - .await - .map_err(|e| NangoConnectionError::Database(e.to_string()))?; - - match rows.into_iter().next() { - Some(row) if row.status == "reconnect_required" => Err( - NangoConnectionError::ReconnectRequired(integration_id.to_string()), - ), - Some(row) => Ok(row.connection_id), - None => Err(NangoConnectionError::NotConnected( - integration_id.to_string(), - )), - } - } -} - -pub struct NangoConnection { - http: OwnedNangoHttpClient, - _marker: PhantomData, -} - -impl NangoConnection { - pub fn into_http(self) -> OwnedNangoHttpClient { - self.http - } -} - -#[derive(Debug)] -pub enum NangoConnectionError { - NotAuthenticated, - NotConnected(String), - ReconnectRequired(String), - MissingState, - Database(String), -} - -impl IntoResponse for NangoConnectionError { - fn into_response(self) -> Response { - let (status, code, message) = match &self { - Self::NotAuthenticated => ( - StatusCode::UNAUTHORIZED, - "unauthorized", - "not authenticated".to_string(), - ), - Self::NotConnected(integration_id) => ( - StatusCode::BAD_REQUEST, - "not_connected", - format!("no connection found for integration: {}", integration_id), - ), - Self::ReconnectRequired(integration_id) => ( - StatusCode::FAILED_DEPENDENCY, - "reconnect_required", - format!( - "connection requires reconnect for integration: {}", - integration_id - ), - ), - Self::MissingState => ( - StatusCode::INTERNAL_SERVER_ERROR, - "internal_server_error", - "NangoConnectionState not found in request extensions".to_string(), - ), - Self::Database(msg) => ( - StatusCode::INTERNAL_SERVER_ERROR, - "internal_server_error", - msg.clone(), - ), - }; - - hypr_api_error::error_response(status, code, &message) - } -} - -impl std::fmt::Display for NangoConnectionError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::NotAuthenticated => write!(f, "not authenticated"), - Self::NotConnected(id) => write!(f, "not connected: {}", id), - Self::ReconnectRequired(id) => write!(f, "reconnect required: {}", id), - Self::MissingState => write!(f, "missing NangoConnectionState"), - Self::Database(msg) => write!(f, "database error: {}", msg), - } - } -} - -impl std::error::Error for NangoConnectionError {} - -impl FromRequestParts for NangoConnection { - type Rejection = NangoConnectionError; - - async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { - let auth = parts - .extensions - .get::() - .ok_or(NangoConnectionError::NotAuthenticated)?; - - let nango_state = parts - .extensions - .get::() - .ok_or(NangoConnectionError::MissingState)?; - - let connection_id = nango_state - .get_connection_id(&auth.token, &auth.claims.sub, I::ID) - .await?; - - let proxy = OwnedNangoProxy::new(&nango_state.nango, I::ID.to_string(), connection_id); - let http = OwnedNangoHttpClient::new(proxy); - - Ok(NangoConnection { - http, - _marker: PhantomData, - }) - } -} diff --git a/crates/api-nango/src/integrations.rs b/crates/api-nango/src/integrations.rs deleted file mode 100644 index 31b5faf940..0000000000 --- a/crates/api-nango/src/integrations.rs +++ /dev/null @@ -1,51 +0,0 @@ -pub trait NangoIntegrationId: Send + Sync + 'static { - const ID: &'static str; -} - -pub struct GoogleCalendar; - -impl NangoIntegrationId for GoogleCalendar { - const ID: &'static str = "google-calendar"; -} - -pub struct GoogleDrive; - -impl NangoIntegrationId for GoogleDrive { - const ID: &'static str = "google-drive"; -} - -pub struct GoogleMail; - -impl NangoIntegrationId for GoogleMail { - const ID: &'static str = "google-mail"; -} - -pub struct Outlook; - -impl NangoIntegrationId for Outlook { - const ID: &'static str = "outlook"; -} - -pub struct GitHub; - -impl NangoIntegrationId for GitHub { - const ID: &'static str = "github"; -} - -pub struct Linear; - -impl NangoIntegrationId for Linear { - const ID: &'static str = "linear"; -} - -pub struct Slack; - -impl NangoIntegrationId for Slack { - const ID: &'static str = "slack"; -} - -pub struct Discord; - -impl NangoIntegrationId for Discord { - const ID: &'static str = "discord"; -} diff --git a/crates/api-nango/src/lib.rs b/crates/api-nango/src/lib.rs deleted file mode 100644 index 9b84968c55..0000000000 --- a/crates/api-nango/src/lib.rs +++ /dev/null @@ -1,20 +0,0 @@ -mod config; -mod error; -pub mod extractor; -pub mod integrations; -mod openapi; -mod routes; -mod state; -mod supabase; - -pub use config::NangoConfig; -pub use extractor::{NangoConnection, NangoConnectionError, NangoConnectionState}; -pub use integrations::{ - Discord, GitHub, GoogleCalendar, GoogleDrive, GoogleMail, Linear, NangoIntegrationId, Outlook, - Slack, -}; -pub use openapi::openapi; -pub use routes::{ - ForwardHandler, ForwardHandlerRegistry, forward_handler, management_router, router, - session_router, webhook_router, -}; diff --git a/crates/api-nango/src/openapi.rs b/crates/api-nango/src/openapi.rs deleted file mode 100644 index a4f16cba92..0000000000 --- a/crates/api-nango/src/openapi.rs +++ /dev/null @@ -1,40 +0,0 @@ -use utoipa::OpenApi; - -use crate::routes::{ - ConnectionItem, CreateSessionRequest, DeleteConnectionRequest, DeleteConnectionResponse, - ListConnectionsResponse, SessionMode, SessionResponse, WebhookResponse, WhoAmIItem, - WhoAmIResponse, -}; - -#[derive(OpenApi)] -#[openapi( - paths( - crate::routes::connect::create_session, - crate::routes::disconnect::delete_connection, - crate::routes::status::list_connections, - crate::routes::webhook::nango_webhook, - crate::routes::whoami::whoami, - ), - components( - schemas( - CreateSessionRequest, - SessionMode, - DeleteConnectionRequest, - DeleteConnectionResponse, - SessionResponse, - ConnectionItem, - ListConnectionsResponse, - WebhookResponse, - WhoAmIItem, - WhoAmIResponse, - ) - ), - tags( - (name = "nango", description = "Integration management via Nango") - ) -)] -struct ApiDoc; - -pub fn openapi() -> utoipa::openapi::OpenApi { - ApiDoc::openapi() -} diff --git a/crates/api-nango/src/routes/connect.rs b/crates/api-nango/src/routes/connect.rs deleted file mode 100644 index fd98ed4752..0000000000 --- a/crates/api-nango/src/routes/connect.rs +++ /dev/null @@ -1,168 +0,0 @@ -use axum::{Extension, Json, extract::State}; -use hypr_api_auth::AuthContext; -use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; - -use crate::error::Result; -use crate::state::AppState; - -#[derive(Debug, Deserialize, ToSchema)] -pub struct CreateSessionRequest { - pub integration_id: String, - #[serde(default = "default_session_mode")] - pub mode: SessionMode, - #[serde(default)] - pub connection_id: Option, -} - -fn default_session_mode() -> SessionMode { - SessionMode::Auto -} - -#[derive(Debug, Deserialize, ToSchema)] -#[serde(rename_all = "snake_case")] -pub enum SessionMode { - Auto, - Connect, - Reconnect, -} - -#[derive(Debug, Serialize, ToSchema)] -pub struct SessionResponse { - pub token: String, - pub expires_at: String, - pub mode: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub connection_id: Option, -} - -#[utoipa::path( - post, - path = "/session", - request_body(content = CreateSessionRequest, content_type = "application/json"), - responses( - (status = 200, description = "Session created", body = SessionResponse), - (status = 401, description = "Unauthorized"), - (status = 500, description = "Internal server error"), - ), - tag = "nango", -)] -pub async fn create_session( - State(state): State, - Extension(auth): Extension, - Json(body): Json, -) -> Result> { - let user_id = auth.claims.sub; - - match body.mode { - SessionMode::Connect => { - // Always create a new connection - } - SessionMode::Reconnect => { - let connection_id = body.connection_id.as_deref().ok_or_else(|| { - crate::error::NangoError::BadRequest( - "connection_id is required for reconnect mode".to_string(), - ) - })?; - - let owns = state - .supabase - .verify_connection_ownership( - &auth.token, - &user_id, - connection_id, - &body.integration_id, - ) - .await?; - - if !owns { - return Err(crate::error::NangoError::Forbidden( - "connection not found or not owned by user".to_string(), - )); - } - - let reconnect_req = hypr_nango::ReconnectSessionRequest { - connection_id: connection_id.to_string(), - integration_id: body.integration_id.clone(), - }; - - let session = state.nango.reconnect_session(reconnect_req).await?; - - return Ok(Json(SessionResponse { - token: session.token, - expires_at: session.expires_at, - mode: "reconnect".to_string(), - connection_id: Some(connection_id.to_string()), - })); - } - SessionMode::Auto => { - if let Some(existing) = state - .supabase - .lookup_connection(&auth.token, &user_id, &body.integration_id) - .await? - { - let reconnect_req = hypr_nango::ReconnectSessionRequest { - connection_id: existing.connection_id.clone(), - integration_id: body.integration_id.clone(), - }; - - match state.nango.reconnect_session(reconnect_req).await { - Ok(session) => { - return Ok(Json(SessionResponse { - token: session.token, - expires_at: session.expires_at, - mode: "reconnect".to_string(), - connection_id: Some(existing.connection_id), - })); - } - Err(hypr_nango::Error::Api(404, response_body)) => { - tracing::warn!( - enduser.id = %user_id, - hyprnote.integration.id = %body.integration_id, - hyprnote.connection.id = %existing.connection_id, - hyprnote.connection.status = %existing.status, - hyprnote.http.response.body = %response_body, - "reconnect session failed with not found, cleaning stale local row" - ); - state - .supabase - .delete_connection(&user_id, &body.integration_id) - .await?; - } - Err(err) => { - return Err(err.into()); - } - } - } - } - } - - let email = auth.claims.email; - - let mut tags = std::collections::HashMap::new(); - tags.insert("end_user_id".to_string(), user_id.clone()); - if let Some(ref e) = email { - tags.insert("end_user_email".to_string(), e.clone()); - } - - let req = hypr_nango::CreateConnectSessionRequest { - end_user: hypr_nango::EndUser { - id: user_id, - display_name: None, - email, - tags: Some(tags), - }, - organization: None, - allowed_integrations: Some(vec![body.integration_id]), - integrations_config_defaults: None, - }; - - let session = state.nango.create_connect_session(req).await?; - - Ok(Json(SessionResponse { - token: session.token, - expires_at: session.expires_at, - mode: "connect".to_string(), - connection_id: None, - })) -} diff --git a/crates/api-nango/src/routes/disconnect.rs b/crates/api-nango/src/routes/disconnect.rs deleted file mode 100644 index ce5eb94341..0000000000 --- a/crates/api-nango/src/routes/disconnect.rs +++ /dev/null @@ -1,85 +0,0 @@ -use axum::{Extension, Json, extract::State}; -use hypr_api_auth::AuthContext; -use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; - -use crate::error::Result; -use crate::state::AppState; - -#[derive(Debug, Deserialize, ToSchema)] -pub struct DeleteConnectionRequest { - pub connection_id: String, - pub integration_id: String, -} - -#[derive(Debug, Serialize, ToSchema)] -pub struct DeleteConnectionResponse { - pub status: String, -} - -#[utoipa::path( - delete, - path = "/connections", - request_body(content = DeleteConnectionRequest, content_type = "application/json"), - responses( - (status = 200, description = "Connection disconnected", body = DeleteConnectionResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 500, description = "Internal server error"), - ), - tag = "nango", -)] -pub async fn delete_connection( - State(state): State, - Extension(auth): Extension, - Json(body): Json, -) -> Result> { - let owns = state - .supabase - .verify_connection_ownership( - &auth.token, - &auth.claims.sub, - &body.connection_id, - &body.integration_id, - ) - .await?; - - if !owns { - tracing::warn!( - enduser.id = %auth.claims.sub, - hyprnote.connection.id = %body.connection_id, - hyprnote.integration.id = %body.integration_id, - "disconnect denied: connection not owned by user" - ); - return Err(crate::error::NangoError::Forbidden( - "connection not found or not owned by user".to_string(), - )); - } - - match state - .nango - .delete_connection(&body.connection_id, &body.integration_id) - .await - { - Ok(()) => {} - Err(hypr_nango::Error::Api(404, response_body)) => { - tracing::warn!( - enduser.id = %auth.claims.sub, - hyprnote.connection.id = %body.connection_id, - hyprnote.integration.id = %body.integration_id, - hyprnote.http.response.body = %response_body, - "nango connection already deleted, cleaning local row" - ); - } - Err(err) => return Err(err.into()), - } - - state - .supabase - .delete_connection_by_connection(&body.integration_id, &body.connection_id) - .await?; - - Ok(Json(DeleteConnectionResponse { - status: "ok".to_string(), - })) -} diff --git a/crates/api-nango/src/routes/identity.rs b/crates/api-nango/src/routes/identity.rs deleted file mode 100644 index ecb1ae9b21..0000000000 --- a/crates/api-nango/src/routes/identity.rs +++ /dev/null @@ -1,58 +0,0 @@ -use hypr_nango::OwnedNangoProxy; - -#[derive(serde::Deserialize)] -struct GoogleUserInfo { - email: Option, - name: Option, -} - -#[derive(serde::Deserialize)] -struct OutlookMe { - mail: Option, - #[serde(rename = "userPrincipalName")] - user_principal_name: Option, - #[serde(rename = "displayName")] - display_name: Option, -} - -pub(crate) async fn fetch_identity( - nango: &hypr_nango::NangoClient, - integration_id: &str, - connection_id: &str, -) -> std::result::Result<(Option, Option), String> { - let proxy = OwnedNangoProxy::new(nango, integration_id.to_string(), connection_id.to_string()); - - match integration_id { - // https://docs.cloud.google.com/identity-platform/docs/reference/rest/v1/UserInfo - "google-calendar" | "google-drive" => { - let resp = proxy - .get("/oauth2/v1/userinfo?alt=json") - .map_err(|e| e.to_string())? - .send() - .await - .map_err(|e| e.to_string())? - .error_for_status() - .map_err(|e| e.to_string())?; - - let me: GoogleUserInfo = resp.json().await.map_err(|e| e.to_string())?; - Ok((me.email, me.name)) - } - - // https://learn.microsoft.com/en-us/graph/api/user-get - "outlook" => { - let resp = proxy - .get("/v1.0/me?$select=mail,userPrincipalName,displayName") - .map_err(|e| e.to_string())? - .send() - .await - .map_err(|e| e.to_string())? - .error_for_status() - .map_err(|e| e.to_string())?; - - let me: OutlookMe = resp.json().await.map_err(|e| e.to_string())?; - Ok((me.mail.or(me.user_principal_name), me.display_name)) - } - - other => Err(format!("unsupported integration: {other}")), - } -} diff --git a/crates/api-nango/src/routes/mod.rs b/crates/api-nango/src/routes/mod.rs deleted file mode 100644 index e792f0c735..0000000000 --- a/crates/api-nango/src/routes/mod.rs +++ /dev/null @@ -1,52 +0,0 @@ -pub(crate) mod connect; -pub(crate) mod disconnect; -pub(crate) mod identity; -pub(crate) mod status; -pub(crate) mod webhook; -pub(crate) mod whoami; - -use axum::{ - Router, - routing::{get, post}, -}; - -use crate::config::NangoConfig; -use crate::state::AppState; - -pub use connect::{CreateSessionRequest, SessionMode, SessionResponse}; -pub use disconnect::{DeleteConnectionRequest, DeleteConnectionResponse}; -pub use status::{ConnectionItem, ListConnectionsResponse}; -pub use webhook::{ForwardHandler, ForwardHandlerRegistry, WebhookResponse, forward_handler}; -pub use whoami::{WhoAmIItem, WhoAmIResponse}; - -pub fn router(config: NangoConfig) -> Router { - session_router(config.clone()).merge(management_router(config)) -} - -pub fn session_router(config: NangoConfig) -> Router { - let state = AppState::new(config); - - Router::new() - .route("/session", post(connect::create_session)) - .with_state(state) -} - -pub fn management_router(config: NangoConfig) -> Router { - let state = AppState::new(config); - - Router::new() - .route( - "/connections", - get(status::list_connections).delete(disconnect::delete_connection), - ) - .route("/whoami", get(whoami::whoami)) - .with_state(state) -} - -pub fn webhook_router(config: NangoConfig, forward_handlers: ForwardHandlerRegistry) -> Router { - let state = AppState::with_forward_handlers(config, forward_handlers); - - Router::new() - .route("/webhook", post(webhook::nango_webhook)) - .with_state(state) -} diff --git a/crates/api-nango/src/routes/status.rs b/crates/api-nango/src/routes/status.rs deleted file mode 100644 index a10176061f..0000000000 --- a/crates/api-nango/src/routes/status.rs +++ /dev/null @@ -1,63 +0,0 @@ -use axum::{Extension, Json, extract::State}; -use hypr_api_auth::AuthContext; -use serde::Serialize; -use utoipa::ToSchema; - -use crate::error::Result; -use crate::state::AppState; - -#[derive(Debug, Serialize, ToSchema)] -pub struct ConnectionItem { - pub integration_id: String, - pub connection_id: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub status: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub last_error_type: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub last_error_description: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub last_error_at: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub updated_at: Option, -} - -#[derive(Debug, Serialize, ToSchema)] -pub struct ListConnectionsResponse { - pub connections: Vec, -} - -#[utoipa::path( - get, - path = "/connections", - responses( - (status = 200, description = "List of active connections", body = ListConnectionsResponse), - (status = 401, description = "Unauthorized"), - (status = 500, description = "Internal server error"), - ), - tag = "nango", -)] -pub async fn list_connections( - State(state): State, - Extension(auth): Extension, -) -> Result> { - let rows = state - .supabase - .list_user_connections(&auth.token, &auth.claims.sub) - .await?; - - let connections = rows - .into_iter() - .map(|row| ConnectionItem { - integration_id: row.integration_id, - connection_id: row.connection_id, - status: Some(row.status), - last_error_type: row.last_error_type, - last_error_description: row.last_error_description, - last_error_at: row.last_error_at, - updated_at: row.updated_at, - }) - .collect(); - - Ok(Json(ListConnectionsResponse { connections })) -} diff --git a/crates/api-nango/src/routes/webhook.rs b/crates/api-nango/src/routes/webhook.rs deleted file mode 100644 index 319b0ada05..0000000000 --- a/crates/api-nango/src/routes/webhook.rs +++ /dev/null @@ -1,521 +0,0 @@ -use std::collections::HashMap; -use std::future::Future; -use std::pin::Pin; -use std::sync::Arc; - -use axum::{Json, extract::State, http::HeaderMap}; -use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; - -use hypr_nango::{AuthOperation, NangoAuthWebhook, WebhookType}; - -use crate::error::{NangoError, Result}; -use crate::state::AppState; - -pub type ForwardHandler = - Arc Pin + Send>> + Send + Sync>; - -pub type ForwardHandlerRegistry = HashMap; - -pub fn forward_handler(f: F) -> ForwardHandler -where - F: Fn(serde_json::Value) -> Fut + Send + Sync + 'static, - Fut: Future + Send + 'static, -{ - Arc::new(move |payload| Box::pin(f(payload))) -} - -#[derive(Debug, Serialize, ToSchema)] -pub struct WebhookResponse { - pub status: String, -} - -#[derive(Debug, Deserialize)] -struct WebhookTypeEnvelope { - #[serde(rename = "type")] - webhook_type: WebhookType, -} - -#[utoipa::path( - post, - path = "/webhook", - responses( - (status = 200, description = "Webhook processed", body = WebhookResponse), - (status = 401, description = "Invalid signature"), - (status = 400, description = "Bad request"), - ), - tag = "nango", -)] -pub async fn nango_webhook( - State(state): State, - headers: HeaderMap, - body: String, -) -> Result> { - let signature = headers - .get("x-nango-hmac-sha256") - .and_then(|h| h.to_str().ok()) - .ok_or_else(|| NangoError::Auth("Missing X-Nango-Hmac-Sha256 header".to_string()))?; - - let valid = hypr_nango::verify_webhook_signature( - &state.config.nango.nango_secret_key, - body.as_bytes(), - signature, - ); - if !valid { - return Err(NangoError::Auth("Invalid webhook signature".to_string())); - } - - let envelope: WebhookTypeEnvelope = - serde_json::from_str(&body).map_err(|e| NangoError::BadRequest(e.to_string()))?; - - match envelope.webhook_type { - WebhookType::Forward => handle_forward_webhook(&state, &body)?, - WebhookType::Auth => { - let payload: NangoAuthWebhook = - serde_json::from_str(&body).map_err(|e| NangoError::BadRequest(e.to_string()))?; - handle_auth_webhook(&state, payload).await?; - } - other => { - tracing::info!(webhook_type = ?other, "nango webhook received (ignored)"); - } - } - - Ok(Json(WebhookResponse { - status: "ok".to_string(), - })) -} - -fn handle_forward_webhook(state: &AppState, body: &str) -> Result<()> { - let forward: hypr_nango::NangoForwardWebhook = - serde_json::from_str(body).map_err(|e| NangoError::BadRequest(e.to_string()))?; - - tracing::info!( - provider = %forward.provider, - connection_id = %forward.connection_id, - "nango forward webhook received" - ); - - if let Some(handler) = state - .forward_handlers - .get(forward.provider_config_key.as_str()) - { - let handler = handler.clone(); - tokio::spawn(async move { - handler(forward.payload).await; - }); - } else { - tracing::info!( - provider_config_key = %forward.provider_config_key, - "unhandled forward webhook provider" - ); - } - - Ok(()) -} - -pub(crate) async fn handle_auth_webhook(state: &AppState, payload: NangoAuthWebhook) -> Result<()> { - tracing::info!( - webhook_type = ?payload.r#type, - operation = ?payload.operation, - connection_id = %payload.connection_id, - end_user_id = payload.end_user_id().unwrap_or("unknown"), - "nango webhook received" - ); - - if !state.supabase.is_configured() { - tracing::warn!("supabase_service_role_key not configured, skipping connection persistence"); - return Ok(()); - } - - if payload.operation == AuthOperation::Refresh && !payload.success { - let error_type = payload.error.as_ref().map(|e| e.r#type.as_str()); - let error_description = payload.error.as_ref().map(|e| e.description.as_str()); - - state - .supabase - .mark_connection_refresh_failed( - &payload.provider_config_key, - &payload.connection_id, - error_type, - error_description, - ) - .await - .map_err(|e| { - tracing::error!(error = %e, "failed_to_persist_nango_refresh_failure_state"); - NangoError::Internal(e.to_string()) - })?; - - tracing::warn!( - hyprnote.connection.id = %payload.connection_id, - hyprnote.integration.id = %payload.provider_config_key, - error.type = error_type, - error = error_description, - "nango token refresh failed" - ); - - return Ok(()); - } - - if payload.success && payload.operation == AuthOperation::Deletion { - state - .supabase - .delete_connection_by_connection(&payload.provider_config_key, &payload.connection_id) - .await - .map_err(|e| { - tracing::error!(error = %e, "failed_to_delete_nango_connection"); - NangoError::Internal(e.to_string()) - })?; - - tracing::info!( - hyprnote.integration.id = %payload.provider_config_key, - hyprnote.connection.id = %payload.connection_id, - "nango connection deleted locally from webhook" - ); - - return Ok(()); - } - - if payload.success && payload.operation != AuthOperation::Deletion { - let Some(end_user_id) = payload.end_user_id() else { - tracing::warn!( - hyprnote.connection.id = %payload.connection_id, - hyprnote.integration.id = %payload.provider_config_key, - "nango auth webhook missing end user id, skipping persistence" - ); - return Ok(()); - }; - - state - .supabase - .upsert_connection( - end_user_id, - &payload.provider_config_key, - &payload.connection_id, - &payload.provider, - ) - .await - .map_err(|e| { - tracing::error!(error = %e, "failed_to_upsert_nango_connection"); - NangoError::Internal(e.to_string()) - })?; - - tracing::info!( - enduser.id = end_user_id, - hyprnote.integration.id = %payload.provider_config_key, - hyprnote.connection.id = %payload.connection_id, - hyprnote.auth.operation = ?payload.operation, - "nango connection upserted" - ); - - if matches!( - payload.operation, - AuthOperation::Creation | AuthOperation::Override - ) { - spawn_identity_task( - state.nango.clone(), - payload.provider_config_key.clone(), - payload.connection_id.clone(), - ); - } - } - - Ok(()) -} - -fn spawn_identity_task( - nango: hypr_nango::NangoClient, - integration_id: String, - connection_id: String, -) { - tokio::spawn(async move { - match super::identity::fetch_identity(&nango, &integration_id, &connection_id).await { - Ok((email, _display_name)) => { - let Some(identity) = email else { - return; - }; - - let mut tags = match nango.get_connection(&connection_id, &integration_id).await { - Ok(connection) => connection.tags.unwrap_or_default(), - Err(e) => { - tracing::warn!( - hyprnote.connection.id = %connection_id, - hyprnote.integration.id = %integration_id, - error = %e, - "failed to fetch connection before patching account_identity tag" - ); - return; - } - }; - tags.insert("account_identity".to_string(), identity.clone()); - - let req = hypr_nango::PatchConnectionRequest { - end_user: None, - tags: Some(tags), - }; - - match nango - .patch_connection(&connection_id, &integration_id, req) - .await - { - Ok(()) => { - tracing::info!( - hyprnote.connection.id = %connection_id, - hyprnote.integration.id = %integration_id, - account_identity = %identity, - "account_identity tag set" - ); - } - Err(e) => { - tracing::warn!( - hyprnote.connection.id = %connection_id, - hyprnote.integration.id = %integration_id, - error = %e, - "failed to patch account_identity tag" - ); - } - } - } - Err(e) => { - tracing::warn!( - hyprnote.connection.id = %connection_id, - hyprnote.integration.id = %integration_id, - error = %e, - "failed to fetch identity for account_identity tag" - ); - } - } - }); -} - -#[cfg(test)] -mod tests { - use wiremock::matchers::{body_json, method, path, path_regex, query_param}; - use wiremock::{Mock, MockServer, ResponseTemplate}; - - use hypr_nango::{ - AuthOperation, NangoAuthWebhook, NangoWebhookEndUser, NangoWebhookError, WebhookType, - }; - - use crate::config::NangoConfig; - use crate::error::NangoError; - use crate::state::AppState; - - use super::{handle_auth_webhook, handle_forward_webhook}; - - const SECRET: &str = "test-secret"; - - fn sign_body(body: &str) -> String { - use hmac::{Hmac, KeyInit, Mac}; - use sha2::Sha256; - let mut mac = Hmac::::new_from_slice(SECRET.as_bytes()).unwrap(); - mac.update(body.as_bytes()); - hex::encode(mac.finalize().into_bytes()) - } - - async fn make_fixture() -> (MockServer, MockServer, AppState) { - let nango_mock = MockServer::start().await; - let supabase_mock = MockServer::start().await; - let config = NangoConfig::for_test(&nango_mock.uri(), &supabase_mock.uri()); - let state = AppState::new(config); - (nango_mock, supabase_mock, state) - } - - fn auth_payload(operation: AuthOperation, success: bool) -> NangoAuthWebhook { - NangoAuthWebhook { - r#type: WebhookType::Auth, - operation, - connection_id: "conn-123".to_string(), - auth_mode: "OAUTH2".to_string(), - provider_config_key: "google-calendar".to_string(), - provider: "google-calendar".to_string(), - environment: "DEV".to_string(), - success, - tags: None, - end_user: Some(NangoWebhookEndUser { - end_user_id: "user-abc".to_string(), - end_user_email: None, - tags: None, - }), - error: None, - } - } - - // --- Signature validation --- - - #[test] - fn sign_body_produces_valid_signature() { - let body = r#"{"type":"auth"}"#; - let sig = sign_body(body); - assert!(hypr_nango::verify_webhook_signature( - SECRET, - body.as_bytes(), - &sig - )); - } - - #[test] - fn wrong_signature_is_invalid() { - let body = r#"{"type":"auth"}"#; - assert!(!hypr_nango::verify_webhook_signature( - SECRET, - body.as_bytes(), - "bad-sig" - )); - } - - // --- handle_auth_webhook --- - - #[tokio::test] - async fn creation_upserts_connection_and_patches_identity() { - let (nango_mock, supabase_mock, state) = make_fixture().await; - - Mock::given(method("POST")) - .and(path("/rest/v1/nango_connections")) - .respond_with(ResponseTemplate::new(201)) - .expect(1) - .mount(&supabase_mock) - .await; - - Mock::given(method("GET")) - .and(path_regex("/proxy/.*userinfo.*")) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({"email": "user@example.com", "name": "Test User"}), - )) - .mount(&nango_mock) - .await; - - Mock::given(method("GET")) - .and(path("/connections/conn-123")) - .and(query_param("provider_config_key", "google-calendar")) - .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "id": 1, - "connection_id": "conn-123", - "provider_config_key": "google-calendar", - "provider": "google-calendar", - "errors": [], - "end_user": null, - "tags": { - "end_user_id": "user-abc", - "end_user_email": "user@old.example.com" - }, - "metadata": {}, - "connection_config": {}, - "created_at": "2026-01-01T00:00:00Z", - "updated_at": "2026-01-01T00:00:00Z", - "last_fetched_at": "2026-01-01T00:00:00Z", - "credentials": {} - }))) - .expect(1) - .mount(&nango_mock) - .await; - - Mock::given(method("PATCH")) - .and(path_regex("/connections/conn-123")) - .and(query_param("provider_config_key", "google-calendar")) - .and(body_json(serde_json::json!({ - "tags": { - "end_user_id": "user-abc", - "end_user_email": "user@old.example.com", - "account_identity": "user@example.com" - } - }))) - .respond_with(ResponseTemplate::new(200)) - .expect(1) - .mount(&nango_mock) - .await; - - handle_auth_webhook(&state, auth_payload(AuthOperation::Creation, true)) - .await - .unwrap(); - - tokio::time::sleep(std::time::Duration::from_millis(200)).await; - - supabase_mock.verify().await; - nango_mock.verify().await; - } - - #[tokio::test] - async fn refresh_failure_marks_reconnect_required() { - let (_nango_mock, supabase_mock, state) = make_fixture().await; - - Mock::given(method("PATCH")) - .and(path_regex("/rest/v1/nango_connections")) - .respond_with(ResponseTemplate::new(200)) - .expect(1) - .mount(&supabase_mock) - .await; - - let mut payload = auth_payload(AuthOperation::Refresh, false); - payload.error = Some(NangoWebhookError { - r#type: "refresh_token_error".to_string(), - description: "Token expired".to_string(), - }); - - handle_auth_webhook(&state, payload).await.unwrap(); - - supabase_mock.verify().await; - } - - #[tokio::test] - async fn deletion_deletes_connection() { - let (_nango_mock, supabase_mock, state) = make_fixture().await; - - Mock::given(method("DELETE")) - .and(path_regex("/rest/v1/nango_connections")) - .and(query_param("integration_id", "eq.google-calendar")) - .and(query_param("connection_id", "eq.conn-123")) - .respond_with(ResponseTemplate::new(200)) - .expect(1) - .mount(&supabase_mock) - .await; - - handle_auth_webhook(&state, auth_payload(AuthOperation::Deletion, true)) - .await - .unwrap(); - - supabase_mock.verify().await; - } - - #[tokio::test] - async fn missing_end_user_id_skips_persistence() { - let (_nango_mock, supabase_mock, state) = make_fixture().await; - - // No mocks mounted — any HTTP call would return connection refused - Mock::given(method("POST")) - .respond_with(ResponseTemplate::new(500)) - .expect(0) - .mount(&supabase_mock) - .await; - - let mut payload = auth_payload(AuthOperation::Creation, true); - payload.end_user = None; - - let result = handle_auth_webhook(&state, payload).await; - - assert!(result.is_ok()); - supabase_mock.verify().await; - } - - #[tokio::test] - async fn supabase_not_configured_skips_all_persistence() { - let nango_mock = MockServer::start().await; - let supabase_mock = MockServer::start().await; - - let mut config = NangoConfig::for_test(&nango_mock.uri(), &supabase_mock.uri()); - config.supabase_service_role_key = None; - let state = AppState::new(config); - - // No mocks — any HTTP would panic - let result = handle_auth_webhook(&state, auth_payload(AuthOperation::Creation, true)).await; - assert!(result.is_ok()); - } - - #[tokio::test] - async fn malformed_forward_webhook_returns_bad_request() { - let (_nango_mock, _supabase_mock, state) = make_fixture().await; - - let result = handle_forward_webhook(&state, r#"{"type":"forward"}"#); - - assert!(matches!(result, Err(NangoError::BadRequest(_)))); - } -} diff --git a/crates/api-nango/src/routes/whoami.rs b/crates/api-nango/src/routes/whoami.rs deleted file mode 100644 index 2dc95a2bf7..0000000000 --- a/crates/api-nango/src/routes/whoami.rs +++ /dev/null @@ -1,91 +0,0 @@ -use axum::{Extension, Json, extract::State}; -use hypr_api_auth::AuthContext; -use hypr_nango::ListConnectionsParams; -use serde::Serialize; -use utoipa::ToSchema; - -use crate::error::Result; -use crate::state::AppState; - -#[derive(Debug, Serialize, ToSchema)] -pub struct WhoAmIItem { - pub integration_id: String, - pub connection_id: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub email: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub display_name: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub error: Option, -} - -#[derive(Debug, Serialize, ToSchema)] -pub struct WhoAmIResponse { - pub accounts: Vec, -} - -#[utoipa::path( - get, - path = "/whoami", - responses( - (status = 200, description = "User info for all connections", body = WhoAmIResponse), - (status = 401, description = "Unauthorized"), - (status = 500, description = "Internal server error"), - ), - tag = "nango", -)] -pub async fn whoami( - State(state): State, - Extension(auth): Extension, -) -> Result> { - let rows = state - .supabase - .list_user_connections(&auth.token, &auth.claims.sub) - .await?; - - let nango_connections = state - .nango - .list_connections(ListConnectionsParams { - end_user_id: Some(auth.claims.sub.clone()), - ..Default::default() - }) - .await - .unwrap_or_default(); - - let nango_map: std::collections::HashMap<(&str, &str), _> = nango_connections - .iter() - .map(|c| { - ( - (c.provider_config_key.as_str(), c.connection_id.as_str()), - c, - ) - }) - .collect(); - - let accounts = rows - .into_iter() - .map(|row| { - let is_reconnect_required = row.status == "reconnect_required"; - - let email = nango_map - .get(&(row.integration_id.as_str(), row.connection_id.as_str())) - .and_then(|c| c.tags.as_ref()) - .and_then(|tags| tags.get("account_identity")) - .cloned(); - - WhoAmIItem { - integration_id: row.integration_id, - connection_id: row.connection_id, - email, - display_name: None, - error: if is_reconnect_required { - Some("reconnect_required".to_string()) - } else { - None - }, - } - }) - .collect(); - - Ok(Json(WhoAmIResponse { accounts })) -} diff --git a/crates/api-nango/src/state.rs b/crates/api-nango/src/state.rs deleted file mode 100644 index 5c4c3b7c17..0000000000 --- a/crates/api-nango/src/state.rs +++ /dev/null @@ -1,39 +0,0 @@ -use hypr_nango::NangoClient; - -use crate::config::{NangoConfig, build_nango_client}; -use crate::routes::webhook::ForwardHandlerRegistry; -use crate::supabase::SupabaseClient; - -#[derive(Clone)] -pub(crate) struct AppState { - pub(crate) config: NangoConfig, - pub(crate) nango: NangoClient, - pub(crate) supabase: SupabaseClient, - pub(crate) forward_handlers: ForwardHandlerRegistry, -} - -impl AppState { - pub(crate) fn new(config: NangoConfig) -> Self { - Self::with_forward_handlers(config, ForwardHandlerRegistry::new()) - } - - pub(crate) fn with_forward_handlers( - config: NangoConfig, - forward_handlers: ForwardHandlerRegistry, - ) -> Self { - let nango = build_nango_client(&config).expect("failed to build NangoClient"); - - let supabase = SupabaseClient::new( - &config.supabase_url, - &config.supabase_anon_key, - config.supabase_service_role_key.clone(), - ); - - Self { - config, - nango, - supabase, - forward_handlers, - } - } -} diff --git a/crates/api-nango/src/supabase.rs b/crates/api-nango/src/supabase.rs deleted file mode 100644 index 9edde5df65..0000000000 --- a/crates/api-nango/src/supabase.rs +++ /dev/null @@ -1,333 +0,0 @@ -#[derive(serde::Deserialize)] -pub(crate) struct NangoConnectionRow { - #[serde(default)] - pub integration_id: String, - pub connection_id: String, - #[serde(default)] - pub status: String, - #[serde(default)] - pub last_error_type: Option, - #[serde(default)] - pub last_error_description: Option, - #[serde(default)] - pub last_error_at: Option, - #[serde(default)] - pub updated_at: Option, -} - -#[derive(serde::Deserialize)] -pub(crate) struct LookupConnectionRow { - pub connection_id: String, - #[serde(default)] - pub status: String, -} - -#[derive(Clone)] -pub(crate) struct SupabaseClient { - supabase_url: String, - supabase_anon_key: String, - supabase_service_role_key: Option, - http_client: reqwest::Client, -} - -impl SupabaseClient { - pub(crate) fn new( - supabase_url: impl Into, - supabase_anon_key: impl Into, - supabase_service_role_key: Option, - ) -> Self { - Self { - supabase_url: supabase_url.into().trim_end_matches('/').to_string(), - supabase_anon_key: supabase_anon_key.into(), - supabase_service_role_key, - http_client: reqwest::Client::new(), - } - } - - pub(crate) fn is_configured(&self) -> bool { - self.supabase_service_role_key.is_some() - } - - pub(crate) async fn anon_query( - &self, - url: &str, - auth_token: &str, - ) -> Result { - self.http_client - .get(url) - .header("Authorization", format!("Bearer {}", auth_token)) - .header("apikey", &self.supabase_anon_key) - .send() - .await - .map_err(|e| crate::error::NangoError::Internal(e.to_string())) - } - - fn service_role_key(&self) -> Result<&str, crate::error::NangoError> { - self.supabase_service_role_key.as_deref().ok_or_else(|| { - crate::error::NangoError::Internal( - "supabase_service_role_key not configured".to_string(), - ) - }) - } - - pub(crate) async fn verify_connection_ownership( - &self, - auth_token: &str, - user_id: &str, - connection_id: &str, - integration_id: &str, - ) -> Result { - let encoded_user_id = urlencoding::encode(user_id); - let encoded_connection_id = urlencoding::encode(connection_id); - let encoded_integration_id = urlencoding::encode(integration_id); - let url = format!( - "{}/rest/v1/nango_connections?select=connection_id&user_id=eq.{}&connection_id=eq.{}&integration_id=eq.{}", - self.supabase_url, encoded_user_id, encoded_connection_id, encoded_integration_id, - ); - - let response = self.anon_query(&url, auth_token).await?; - - if !response.status().is_success() { - let status = response.status(); - let body = response.text().await.unwrap_or_default(); - return Err(crate::error::NangoError::Internal(format!( - "ownership check failed: {} - {}", - status, body - ))); - } - - let rows: Vec = response - .json() - .await - .map_err(|e| crate::error::NangoError::Internal(e.to_string()))?; - - Ok(!rows.is_empty()) - } - - pub(crate) async fn lookup_connection( - &self, - auth_token: &str, - user_id: &str, - integration_id: &str, - ) -> Result, crate::error::NangoError> { - let encoded_user_id = urlencoding::encode(user_id); - let encoded_integration_id = urlencoding::encode(integration_id); - let url = format!( - "{}/rest/v1/nango_connections?select=connection_id,status&user_id=eq.{}&integration_id=eq.{}", - self.supabase_url, encoded_user_id, encoded_integration_id, - ); - - let response = self.anon_query(&url, auth_token).await?; - - if !response.status().is_success() { - let status = response.status(); - let body = response.text().await.unwrap_or_default(); - return Err(crate::error::NangoError::Internal(format!( - "lookup query failed: {} - {}", - status, body - ))); - } - - let rows: Vec = response - .json() - .await - .map_err(|e| crate::error::NangoError::Internal(e.to_string()))?; - - Ok(rows.into_iter().next()) - } - - pub(crate) async fn list_user_connections( - &self, - auth_token: &str, - user_id: &str, - ) -> Result, crate::error::NangoError> { - let encoded_user_id = urlencoding::encode(user_id); - let url = format!( - "{}/rest/v1/nango_connections?select=integration_id,connection_id,status,last_error_type,last_error_description,last_error_at,updated_at&user_id=eq.{}", - self.supabase_url, encoded_user_id, - ); - - let response = self.anon_query(&url, auth_token).await?; - - if !response.status().is_success() { - let status = response.status(); - let body = response.text().await.unwrap_or_default(); - return Err(crate::error::NangoError::Internal(format!( - "query failed: {} - {}", - status, body - ))); - } - - response - .json() - .await - .map_err(|e| crate::error::NangoError::Internal(e.to_string())) - } - - pub(crate) async fn upsert_connection( - &self, - user_id: &str, - integration_id: &str, - connection_id: &str, - provider: &str, - ) -> Result<(), crate::error::NangoError> { - let service_role_key = self.service_role_key()?; - - let url = format!( - "{}/rest/v1/nango_connections?on_conflict=integration_id,connection_id", - self.supabase_url, - ); - - let body = serde_json::json!({ - "user_id": user_id, - "integration_id": integration_id, - "connection_id": connection_id, - "provider": provider, - "status": "connected", - "last_error_type": serde_json::Value::Null, - "last_error_description": serde_json::Value::Null, - "last_error_at": serde_json::Value::Null, - "updated_at": chrono::Utc::now().to_rfc3339(), - }); - - let response = self - .http_client - .post(&url) - .header("Authorization", format!("Bearer {}", service_role_key)) - .header("apikey", service_role_key) - .header("Content-Type", "application/json") - .header("Prefer", "resolution=merge-duplicates") - .json(&body) - .send() - .await - .map_err(|e| crate::error::NangoError::Internal(e.to_string()))?; - - if !response.status().is_success() { - let status = response.status(); - let body = response.text().await.unwrap_or_default(); - return Err(crate::error::NangoError::Internal(format!( - "upsert failed: {} - {}", - status, body - ))); - } - - Ok(()) - } - - pub(crate) async fn delete_connection( - &self, - user_id: &str, - integration_id: &str, - ) -> Result<(), crate::error::NangoError> { - let service_role_key = self.service_role_key()?; - - let encoded_user_id = urlencoding::encode(user_id); - let encoded_integration_id = urlencoding::encode(integration_id); - let url = format!( - "{}/rest/v1/nango_connections?user_id=eq.{}&integration_id=eq.{}", - self.supabase_url, encoded_user_id, encoded_integration_id, - ); - - let response = self - .http_client - .delete(&url) - .header("Authorization", format!("Bearer {}", service_role_key)) - .header("apikey", service_role_key) - .send() - .await - .map_err(|e| crate::error::NangoError::Internal(e.to_string()))?; - - if !response.status().is_success() { - let status = response.status(); - let body = response.text().await.unwrap_or_default(); - return Err(crate::error::NangoError::Internal(format!( - "delete failed: {} - {}", - status, body - ))); - } - - Ok(()) - } - - pub(crate) async fn delete_connection_by_connection( - &self, - integration_id: &str, - connection_id: &str, - ) -> Result<(), crate::error::NangoError> { - let service_role_key = self.service_role_key()?; - - let encoded_integration_id = urlencoding::encode(integration_id); - let encoded_connection_id = urlencoding::encode(connection_id); - let url = format!( - "{}/rest/v1/nango_connections?integration_id=eq.{}&connection_id=eq.{}", - self.supabase_url, encoded_integration_id, encoded_connection_id, - ); - - let response = self - .http_client - .delete(&url) - .header("Authorization", format!("Bearer {}", service_role_key)) - .header("apikey", service_role_key) - .send() - .await - .map_err(|e| crate::error::NangoError::Internal(e.to_string()))?; - - if !response.status().is_success() { - let status = response.status(); - let body = response.text().await.unwrap_or_default(); - return Err(crate::error::NangoError::Internal(format!( - "delete by connection failed: {} - {}", - status, body - ))); - } - - Ok(()) - } - - pub(crate) async fn mark_connection_refresh_failed( - &self, - integration_id: &str, - connection_id: &str, - error_type: Option<&str>, - error_description: Option<&str>, - ) -> Result<(), crate::error::NangoError> { - let service_role_key = self.service_role_key()?; - - let encoded_integration_id = urlencoding::encode(integration_id); - let encoded_connection_id = urlencoding::encode(connection_id); - let url = format!( - "{}/rest/v1/nango_connections?integration_id=eq.{}&connection_id=eq.{}", - self.supabase_url, encoded_integration_id, encoded_connection_id, - ); - - let body = serde_json::json!({ - "status": "reconnect_required", - "last_error_type": error_type, - "last_error_description": error_description, - "last_error_at": chrono::Utc::now().to_rfc3339(), - "updated_at": chrono::Utc::now().to_rfc3339(), - }); - - let response = self - .http_client - .patch(&url) - .header("Authorization", format!("Bearer {}", service_role_key)) - .header("apikey", service_role_key) - .header("Content-Type", "application/json") - .json(&body) - .send() - .await - .map_err(|e| crate::error::NangoError::Internal(e.to_string()))?; - - if !response.status().is_success() { - let status = response.status(); - let body = response.text().await.unwrap_or_default(); - return Err(crate::error::NangoError::Internal(format!( - "mark refresh failed failed: {} - {}", - status, body - ))); - } - - Ok(()) - } -} diff --git a/crates/api-pyannote/Cargo.toml b/crates/api-pyannote/Cargo.toml deleted file mode 100644 index 46e625305a..0000000000 --- a/crates/api-pyannote/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "api-pyannote" -version = "0.1.0" -edition = "2024" - -[dependencies] -hypr-api-auth = { workspace = true } -hypr-api-env = { workspace = true } -hypr-api-error = { workspace = true } -hypr-pyannote-cloud = { workspace = true } - -axum = { workspace = true } -reqwest = { workspace = true, features = ["json"] } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -tracing = { workspace = true } -utoipa = { workspace = true } - -[dev-dependencies] -tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } -tower = { workspace = true } -wiremock = { workspace = true } diff --git a/crates/api-pyannote/src/config.rs b/crates/api-pyannote/src/config.rs deleted file mode 100644 index 165673cbb0..0000000000 --- a/crates/api-pyannote/src/config.rs +++ /dev/null @@ -1,24 +0,0 @@ -use hypr_api_env::PyannoteEnv; - -#[derive(Clone)] -pub struct PyannoteConfig { - pub api_key: String, - pub api_base: String, -} - -impl PyannoteConfig { - pub fn new(env: &PyannoteEnv) -> Self { - Self { - api_key: env.pyannote_api_key.clone(), - api_base: env.pyannote_api_base.clone(), - } - } - - pub fn client( - &self, - ) -> Result> { - hypr_pyannote_cloud::Client::builder(&self.api_key) - .base_url(&self.api_base) - .build() - } -} diff --git a/crates/api-pyannote/src/error.rs b/crates/api-pyannote/src/error.rs deleted file mode 100644 index 5ef2604342..0000000000 --- a/crates/api-pyannote/src/error.rs +++ /dev/null @@ -1,54 +0,0 @@ -use axum::{ - http::StatusCode, - response::{IntoResponse, Response}, -}; - -pub type Result = std::result::Result; - -#[derive(Debug)] -pub struct PyannoteError { - status: StatusCode, - code: &'static str, - message: String, -} - -impl PyannoteError { - pub fn bad_request(message: impl Into) -> Self { - Self { - status: StatusCode::BAD_REQUEST, - code: "bad_request", - message: message.into(), - } - } - - pub fn bad_gateway(message: impl Into) -> Self { - Self { - status: StatusCode::BAD_GATEWAY, - code: "upstream_error", - message: message.into(), - } - } - - pub fn upstream(status: StatusCode, message: impl Into) -> Self { - let code = match status { - StatusCode::BAD_REQUEST => "bad_request", - StatusCode::PAYMENT_REQUIRED => "subscription_required", - StatusCode::TOO_MANY_REQUESTS => "rate_limited", - StatusCode::NOT_FOUND => "not_found", - _ if status.is_server_error() => "upstream_error", - _ => "request_failed", - }; - - Self { - status, - code, - message: message.into(), - } - } -} - -impl IntoResponse for PyannoteError { - fn into_response(self) -> Response { - hypr_api_error::error_response(self.status, self.code, &self.message) - } -} diff --git a/crates/api-pyannote/src/lib.rs b/crates/api-pyannote/src/lib.rs deleted file mode 100644 index af30734748..0000000000 --- a/crates/api-pyannote/src/lib.rs +++ /dev/null @@ -1,9 +0,0 @@ -mod config; -mod error; -mod openapi; -mod request; -mod routes; - -pub use config::PyannoteConfig; -pub use openapi::openapi; -pub use routes::router; diff --git a/crates/api-pyannote/src/openapi.rs b/crates/api-pyannote/src/openapi.rs deleted file mode 100644 index 0c364e3e86..0000000000 --- a/crates/api-pyannote/src/openapi.rs +++ /dev/null @@ -1,130 +0,0 @@ -use utoipa::OpenApi; -use utoipa::openapi::{ - OpenApi as OpenApiDoc, - path::{Operation, PathItem}, -}; - -#[derive(OpenApi)] -#[openapi(components(schemas( - crate::request::DiarizeRequest, - crate::request::DiarizeRequestModel, - crate::request::IdentifyRequest, - crate::request::IdentifyRequestModel, - crate::request::MatchingOptions, - crate::request::TranscriptionConfiguration, - crate::request::TranscriptionConfigurationModel, - crate::request::Voiceprint, - crate::request::VoiceprintRequest, - crate::request::VoiceprintRequestModel, -)))] -struct ApiDoc; - -pub fn openapi() -> OpenApiDoc { - let mut doc = hypr_pyannote_cloud::openapi(); - let custom = ApiDoc::openapi(); - - doc.paths.paths.retain(|path, _| { - matches!( - path.as_str(), - "/v1/diarize" | "/v1/identify" | "/v1/voiceprint" - ) - }); - - doc.servers = None; - doc.security = None; - - if let Some(components) = doc.components.as_mut() { - components.security_schemes.clear(); - - if let Some(custom_components) = custom.components { - components.schemas.extend(custom_components.schemas); - } - } - - // Account-wide jobs, shared media helpers, and `/v1/test` validate or expose - // capabilities of the shared server-side Pyannote account and are not public. - for item in doc.paths.paths.values_mut() { - with_each_operation(item, |operation| { - operation.security = None; - operation.tags = Some(vec!["pyannote".to_string()]); - }); - } - - doc -} - -fn with_each_operation(item: &mut PathItem, mut f: impl FnMut(&mut Operation)) { - if let Some(op) = item.get.as_mut() { - f(op); - } - if let Some(op) = item.put.as_mut() { - f(op); - } - if let Some(op) = item.post.as_mut() { - f(op); - } - if let Some(op) = item.delete.as_mut() { - f(op); - } - if let Some(op) = item.options.as_mut() { - f(op); - } - if let Some(op) = item.head.as_mut() { - f(op); - } - if let Some(op) = item.patch.as_mut() { - f(op); - } - if let Some(op) = item.trace.as_mut() { - f(op); - } -} - -#[cfg(test)] -mod tests { - #[test] - fn normalizes_upstream_pyannote_doc() { - let doc = super::openapi(); - let diarize = doc.paths.paths.get("/v1/diarize").unwrap(); - let post = diarize.post.as_ref().unwrap(); - - assert!(doc.servers.is_none()); - assert!(post.security.is_none()); - assert_eq!(post.tags.as_ref().unwrap(), &vec!["pyannote".to_string()]); - } - - #[test] - fn includes_only_public_write_routes() { - let doc = super::openapi(); - - assert!(doc.paths.paths.contains_key("/v1/diarize")); - assert!(doc.paths.paths.contains_key("/v1/identify")); - assert!(doc.paths.paths.contains_key("/v1/voiceprint")); - assert!(!doc.paths.paths.contains_key("/v1/jobs")); - assert!(!doc.paths.paths.contains_key("/v1/jobs/{jobId}")); - assert!(!doc.paths.paths.contains_key("/v1/media/input")); - assert!(!doc.paths.paths.contains_key("/v1/media/output")); - assert!(!doc.paths.paths.contains_key("/v1/test")); - } - - #[test] - fn replaces_public_request_schemas() { - let doc = super::openapi(); - let schemas = &doc.components.as_ref().unwrap().schemas; - - let diarize = serde_json::to_value(schemas.get("DiarizeRequest").unwrap()).unwrap(); - let identify = serde_json::to_value(schemas.get("IdentifyRequest").unwrap()).unwrap(); - let voiceprint = serde_json::to_value(schemas.get("VoiceprintRequest").unwrap()).unwrap(); - - assert!(diarize["properties"].get("webhook").is_none()); - assert!(diarize["properties"].get("webhookStatusOnly").is_none()); - assert!(identify["properties"].get("webhook").is_none()); - assert!(identify["properties"].get("webhookStatusOnly").is_none()); - assert!(voiceprint["properties"].get("webhook").is_none()); - assert!(voiceprint["properties"].get("webhookStatusOnly").is_none()); - assert_eq!( - identify["required"], - serde_json::json!(["url", "voiceprints"]) - ); - } -} diff --git a/crates/api-pyannote/src/request.rs b/crates/api-pyannote/src/request.rs deleted file mode 100644 index 636e71a915..0000000000 --- a/crates/api-pyannote/src/request.rs +++ /dev/null @@ -1,252 +0,0 @@ -use hypr_pyannote_cloud::types; -use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; - -#[derive(Debug, Deserialize, Serialize, ToSchema)] -#[serde(deny_unknown_fields)] -pub struct DiarizeRequest { - #[serde(default)] - pub confidence: bool, - #[serde(default)] - pub exclusive: bool, - #[serde( - rename = "maxSpeakers", - default, - skip_serializing_if = "Option::is_none" - )] - pub max_speakers: Option, - #[serde( - rename = "minSpeakers", - default, - skip_serializing_if = "Option::is_none" - )] - pub min_speakers: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub model: Option, - #[serde( - rename = "numSpeakers", - default, - skip_serializing_if = "Option::is_none" - )] - pub num_speakers: Option, - #[serde(default)] - pub transcription: bool, - #[serde( - rename = "transcriptionConfig", - default, - skip_serializing_if = "Option::is_none" - )] - pub transcription_config: Option, - #[serde( - rename = "turnLevelConfidence", - default, - skip_serializing_if = "Option::is_none" - )] - pub turn_level_confidence: Option, - pub url: String, -} - -impl From for types::DiarizeRequest { - fn from(value: DiarizeRequest) -> Self { - Self { - confidence: value.confidence, - exclusive: value.exclusive, - max_speakers: value.max_speakers, - min_speakers: value.min_speakers, - model: value.model.map(Into::into), - num_speakers: value.num_speakers, - transcription: value.transcription, - transcription_config: value.transcription_config.map(Into::into), - turn_level_confidence: value.turn_level_confidence, - url: value.url, - webhook: None, - webhook_status_only: false, - } - } -} - -#[derive(Debug, Deserialize, Serialize, ToSchema)] -pub enum DiarizeRequestModel { - #[serde(rename = "precision-2")] - Precision2, - #[serde(rename = "community-1")] - Community1, -} - -impl From for types::DiarizeRequestModel { - fn from(value: DiarizeRequestModel) -> Self { - match value { - DiarizeRequestModel::Precision2 => Self::Precision2, - DiarizeRequestModel::Community1 => Self::Community1, - } - } -} - -#[derive(Debug, Deserialize, Serialize, ToSchema)] -#[serde(deny_unknown_fields)] -pub struct IdentifyRequest { - #[serde(default)] - pub confidence: bool, - #[serde(default)] - pub exclusive: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub matching: Option, - #[serde( - rename = "maxSpeakers", - default, - skip_serializing_if = "Option::is_none" - )] - pub max_speakers: Option, - #[serde( - rename = "minSpeakers", - default, - skip_serializing_if = "Option::is_none" - )] - pub min_speakers: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub model: Option, - #[serde( - rename = "numSpeakers", - default, - skip_serializing_if = "Option::is_none" - )] - pub num_speakers: Option, - #[serde( - rename = "turnLevelConfidence", - default, - skip_serializing_if = "Option::is_none" - )] - pub turn_level_confidence: Option, - pub url: String, - pub voiceprints: Vec, -} - -impl From for types::IdentifyRequest { - fn from(value: IdentifyRequest) -> Self { - Self { - confidence: value.confidence, - exclusive: value.exclusive, - matching: value.matching.map(Into::into), - max_speakers: value.max_speakers, - min_speakers: value.min_speakers, - model: value.model.map(Into::into), - num_speakers: value.num_speakers, - turn_level_confidence: value.turn_level_confidence, - url: Some(value.url), - voiceprints: value.voiceprints.into_iter().map(Into::into).collect(), - webhook: None, - webhook_status_only: false, - } - } -} - -#[derive(Debug, Deserialize, Serialize, ToSchema)] -pub enum IdentifyRequestModel { - #[serde(rename = "precision-2")] - Precision2, -} - -impl From for types::IdentifyRequestModel { - fn from(value: IdentifyRequestModel) -> Self { - match value { - IdentifyRequestModel::Precision2 => Self::Precision2, - } - } -} - -#[derive(Debug, Deserialize, Serialize, ToSchema)] -pub struct MatchingOptions { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub exclusive: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub threshold: Option, -} - -impl From for types::MatchingOptions { - fn from(value: MatchingOptions) -> Self { - Self { - exclusive: value.exclusive, - threshold: value.threshold, - } - } -} - -#[derive(Debug, Deserialize, Serialize, ToSchema)] -pub struct TranscriptionConfiguration { - pub model: TranscriptionConfigurationModel, -} - -impl From for types::TranscriptionConfiguration { - fn from(value: TranscriptionConfiguration) -> Self { - Self { - model: value.model.into(), - } - } -} - -#[derive(Debug, Deserialize, Serialize, ToSchema)] -pub enum TranscriptionConfigurationModel { - #[serde(rename = "parakeet-tdt-0.6b-v3")] - ParakeetTdt06bV3, - #[serde(rename = "faster-whisper-large-v3-turbo")] - FasterWhisperLargeV3Turbo, -} - -impl From for types::TranscriptionConfigurationModel { - fn from(value: TranscriptionConfigurationModel) -> Self { - match value { - TranscriptionConfigurationModel::ParakeetTdt06bV3 => Self::ParakeetTdt06bV3, - TranscriptionConfigurationModel::FasterWhisperLargeV3Turbo => { - Self::FasterWhisperLargeV3Turbo - } - } - } -} - -#[derive(Debug, Deserialize, Serialize, ToSchema)] -pub struct Voiceprint { - pub label: String, - pub voiceprint: String, -} - -impl From for types::Voiceprint { - fn from(value: Voiceprint) -> Self { - Self { - label: value.label.try_into().expect("validated voiceprint label"), - voiceprint: value.voiceprint, - } - } -} - -#[derive(Debug, Deserialize, Serialize, ToSchema)] -#[serde(deny_unknown_fields)] -pub struct VoiceprintRequest { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub model: Option, - pub url: String, -} - -impl From for types::VoiceprintRequest { - fn from(value: VoiceprintRequest) -> Self { - Self { - model: value.model.map(Into::into), - url: value.url, - webhook: None, - webhook_status_only: false, - } - } -} - -#[derive(Debug, Deserialize, Serialize, ToSchema)] -pub enum VoiceprintRequestModel { - #[serde(rename = "precision-2")] - Precision2, -} - -impl From for types::VoiceprintRequestModel { - fn from(value: VoiceprintRequestModel) -> Self { - match value { - VoiceprintRequestModel::Precision2 => Self::Precision2, - } - } -} diff --git a/crates/api-pyannote/src/routes.rs b/crates/api-pyannote/src/routes.rs deleted file mode 100644 index 5f204e8564..0000000000 --- a/crates/api-pyannote/src/routes.rs +++ /dev/null @@ -1,699 +0,0 @@ -use axum::{ - Extension, Json, Router, - body::Body, - extract::State, - http::StatusCode, - response::{IntoResponse, Response}, - routing::post, -}; -use hypr_api_auth::AuthContext; -use hypr_pyannote_cloud::ClientInfo; -use serde::{Deserialize, Serialize}; - -use crate::{ - config::PyannoteConfig, - error::{PyannoteError, Result}, - request::{DiarizeRequest, IdentifyRequest, VoiceprintRequest}, -}; - -#[derive(Clone)] -struct AppState { - client: hypr_pyannote_cloud::Client, -} - -#[derive(Debug, Deserialize)] -struct UpstreamApiError { - message: String, -} - -#[derive(Debug, Deserialize)] -struct UpstreamValidationError { - message: String, -} - -pub fn router(config: PyannoteConfig) -> Router { - let state = AppState { - client: config.client().expect("failed to build pyannote client"), - }; - - Router::new() - .route("/v1/diarize", post(diarize)) - .route("/v1/identify", post(identify)) - .route("/v1/voiceprint", post(voiceprint)) - .with_state(state) -} - -async fn diarize( - Extension(auth): Extension, - State(state): State, - Json(body): Json, -) -> Result { - let body = sanitize_diarize_request(auth.claims.sub.as_str(), body)?; - let payload = upstream_payload(body)?; - - forward_request( - state - .client - .client() - .post(format!("{}/v1/diarize", state.client.baseurl())) - .json(&payload), - ) - .await -} - -async fn identify( - Extension(auth): Extension, - State(state): State, - Json(body): Json, -) -> Result { - let body = sanitize_identify_request(auth.claims.sub.as_str(), body)?; - let payload = upstream_payload(body)?; - - forward_request( - state - .client - .client() - .post(format!("{}/v1/identify", state.client.baseurl())) - .json(&payload), - ) - .await -} - -async fn voiceprint( - Extension(auth): Extension, - State(state): State, - Json(body): Json, -) -> Result { - let body = sanitize_voiceprint_request(auth.claims.sub.as_str(), body)?; - let payload = upstream_payload(body)?; - - forward_request( - state - .client - .client() - .post(format!("{}/v1/voiceprint", state.client.baseurl())) - .json(&payload), - ) - .await -} - -async fn forward_request(request: reqwest::RequestBuilder) -> Result { - let response = request - .send() - .await - .map_err(|err| PyannoteError::bad_gateway(err.to_string()))?; - let status = status_code(response.status()); - let bytes = response - .bytes() - .await - .map_err(|err| PyannoteError::bad_gateway(err.to_string()))?; - - if status.is_success() { - return Ok(( - status, - [("content-type", "application/json")], - Body::from(bytes), - ) - .into_response()); - } - - let body = String::from_utf8_lossy(&bytes).to_string(); - let message = extract_upstream_error_message(&body).unwrap_or_else(|| default_message(status)); - Err(PyannoteError::upstream(status, message)) -} - -fn sanitize_diarize_request( - user_id: &str, - mut body: DiarizeRequest, -) -> Result { - body.url = validate_media_url(user_id, &body.url)?; - Ok(body.into()) -} - -fn sanitize_identify_request( - user_id: &str, - mut body: IdentifyRequest, -) -> Result { - body.url = validate_media_url(user_id, &body.url)?; - Ok(body.into()) -} - -fn sanitize_voiceprint_request( - user_id: &str, - mut body: VoiceprintRequest, -) -> Result { - body.url = validate_media_url(user_id, &body.url)?; - Ok(body.into()) -} - -fn validate_media_url(user_id: &str, url: &str) -> Result { - let prefix = format!("media://users/{user_id}/"); - if url.starts_with(&prefix) { - Ok(url.to_string()) - } else { - Err(PyannoteError::bad_request( - "Invalid media URL: expected caller-owned managed media", - )) - } -} - -fn upstream_payload(body: impl Serialize) -> Result { - let mut payload = - serde_json::to_value(body).map_err(|err| PyannoteError::bad_gateway(err.to_string()))?; - - if let Some(object) = payload.as_object_mut() { - object.remove("webhookStatusOnly"); - } - - Ok(payload) -} - -fn extract_upstream_error_message(body: &str) -> Option { - if body.trim().is_empty() { - return None; - } - - serde_json::from_str::(body) - .map(|error| error.message) - .ok() - .or_else(|| { - serde_json::from_str::(body) - .map(|error| error.message) - .ok() - }) - .or_else(|| error_message_from_json_body(body)) -} - -fn status_code(status: reqwest::StatusCode) -> StatusCode { - StatusCode::from_u16(status.as_u16()).unwrap_or(StatusCode::BAD_GATEWAY) -} - -fn error_message_from_json_body(body: &str) -> Option { - if body.trim().is_empty() { - return None; - } - - serde_json::from_str::(body) - .ok() - .and_then(|value| { - value - .get("message") - .and_then(serde_json::Value::as_str) - .map(ToString::to_string) - .or_else(|| { - value - .get("error") - .and_then(|error| error.get("message")) - .and_then(serde_json::Value::as_str) - .map(ToString::to_string) - }) - }) -} - -fn default_message(status: StatusCode) -> String { - match status { - StatusCode::BAD_REQUEST => "Invalid request".to_string(), - StatusCode::PAYMENT_REQUIRED => "Subscription is required".to_string(), - StatusCode::TOO_MANY_REQUESTS => "Too many requests".to_string(), - StatusCode::NOT_FOUND => "Resource not found".to_string(), - _ => status - .canonical_reason() - .unwrap_or("Upstream request failed") - .to_string(), - } -} - -#[cfg(test)] -mod tests { - use axum::{Extension, Router, body::Body, body::to_bytes, http::Request, http::StatusCode}; - use hypr_api_auth::{AuthContext, Claims}; - use serde_json::{Value, json}; - use tower::ServiceExt; - use wiremock::{ - Mock, MockServer, ResponseTemplate, - matchers::{header, method, path}, - }; - - use crate::config::PyannoteConfig; - - fn router(server: &MockServer) -> Router { - super::router(PyannoteConfig { - api_key: "pyannote-key".to_string(), - api_base: server.uri(), - }) - .layer(Extension(AuthContext { - token: "token".to_string(), - claims: Claims { - sub: "user-123".to_string(), - email: None, - entitlements: vec![], - subscription_status: None, - trial_end: None, - }, - })) - } - - async fn response_json(response: axum::response::Response) -> Value { - let bytes = to_bytes(response.into_body(), usize::MAX).await.unwrap(); - serde_json::from_slice(&bytes).unwrap() - } - - #[tokio::test] - async fn diarize_forwards_owned_media_url_and_auth_header() { - let server = MockServer::start().await; - - Mock::given(method("POST")) - .and(path("/v1/diarize")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "jobId": "job-123", - "status": "created" - }))) - .mount(&server) - .await; - - let response = router(&server) - .oneshot( - Request::post("/v1/diarize") - .header("content-type", "application/json") - .body(Body::from(r#"{"url":"media://users/user-123/audio.wav"}"#)) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - assert_eq!( - response_json(response).await, - json!({"jobId": "job-123", "status": "created"}) - ); - - let requests = server.received_requests().await.unwrap(); - let request = &requests[0]; - assert_eq!(request.method.as_str(), "POST"); - assert_eq!(request.url.path(), "/v1/diarize"); - assert_eq!( - request - .headers - .get("authorization") - .unwrap() - .to_str() - .unwrap(), - "Bearer pyannote-key" - ); - - let body = request.body_json::().unwrap(); - assert_eq!(body["url"], json!("media://users/user-123/audio.wav")); - assert!(body.get("webhook").is_none()); - assert!(body.get("webhookStatusOnly").is_none()); - } - - #[tokio::test] - async fn identify_forwards_owned_media_url_without_webhook_fields() { - let server = MockServer::start().await; - - Mock::given(method("POST")) - .and(path("/v1/identify")) - .and(header("authorization", "Bearer pyannote-key")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "jobId": "job-123", - "status": "created" - }))) - .mount(&server) - .await; - - let response = router(&server) - .oneshot( - Request::post("/v1/identify") - .header("content-type", "application/json") - .body(Body::from( - r#"{"url":"media://users/user-123/audio.wav","voiceprints":[{"label":"speaker-a","voiceprint":"abc"}]}"#, - )) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - - let requests = server.received_requests().await.unwrap(); - let body = requests[0].body_json::().unwrap(); - assert_eq!(body["url"], json!("media://users/user-123/audio.wav")); - assert!(body.get("webhook").is_none()); - assert!(body.get("webhookStatusOnly").is_none()); - } - - #[tokio::test] - async fn voiceprint_forwards_owned_media_url_without_webhook_fields() { - let server = MockServer::start().await; - - Mock::given(method("POST")) - .and(path("/v1/voiceprint")) - .respond_with( - ResponseTemplate::new(200) - .set_body_json(json!({"jobId": "job-123", "status": "created"})), - ) - .mount(&server) - .await; - - let response = router(&server) - .oneshot( - Request::post("/v1/voiceprint") - .header("content-type", "application/json") - .body(Body::from(r#"{"url":"media://users/user-123/audio.wav"}"#)) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - - let requests = server.received_requests().await.unwrap(); - let body = requests[0].body_json::().unwrap(); - assert_eq!(body["url"], json!("media://users/user-123/audio.wav")); - assert!(body.get("webhook").is_none()); - assert!(body.get("webhookStatusOnly").is_none()); - } - - #[tokio::test] - async fn test_route_is_not_exposed() { - let server = MockServer::start().await; - - let response = router(&server) - .oneshot(Request::get("/v1/test").body(Body::empty()).unwrap()) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::NOT_FOUND); - } - - #[tokio::test] - async fn jobs_route_is_not_exposed() { - let server = MockServer::start().await; - - let response = router(&server) - .oneshot(Request::get("/v1/jobs").body(Body::empty()).unwrap()) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::NOT_FOUND); - } - - #[tokio::test] - async fn job_detail_route_is_not_exposed() { - let server = MockServer::start().await; - - let response = router(&server) - .oneshot( - Request::get("/v1/jobs/job-123") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::NOT_FOUND); - } - - #[tokio::test] - async fn media_input_route_is_not_exposed() { - let server = MockServer::start().await; - - let response = router(&server) - .oneshot( - Request::post("/v1/media/input") - .header("content-type", "application/json") - .body(Body::from(r#"{"url":"media://users/user-123/audio.wav"}"#)) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::NOT_FOUND); - } - - #[tokio::test] - async fn media_output_route_is_not_exposed() { - let server = MockServer::start().await; - - let response = router(&server) - .oneshot( - Request::post("/v1/media/output") - .header("content-type", "application/json") - .body(Body::from(r#"{"url":"media://users/user-123/audio.wav"}"#)) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::NOT_FOUND); - } - - #[tokio::test] - async fn diarize_rejects_external_url() { - let server = MockServer::start().await; - - let response = router(&server) - .oneshot( - Request::post("/v1/diarize") - .header("content-type", "application/json") - .body(Body::from(r#"{"url":"https://example.com/audio.wav"}"#)) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - assert_eq!( - response_json(response).await, - json!({"error": {"code": "bad_request", "message": "Invalid media URL: expected caller-owned managed media"}}) - ); - } - - #[tokio::test] - async fn identify_rejects_media_owned_by_another_user() { - let server = MockServer::start().await; - - let response = router(&server) - .oneshot( - Request::post("/v1/identify") - .header("content-type", "application/json") - .body(Body::from( - r#"{"url":"media://users/user-999/audio.wav","voiceprints":[{"label":"speaker-a","voiceprint":"abc"}]}"#, - )) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - assert_eq!( - response_json(response).await, - json!({"error": {"code": "bad_request", "message": "Invalid media URL: expected caller-owned managed media"}}) - ); - } - - #[tokio::test] - async fn voiceprint_requires_url() { - let server = MockServer::start().await; - - let response = router(&server) - .oneshot( - Request::post("/v1/voiceprint") - .header("content-type", "application/json") - .body(Body::from(r#"{}"#)) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY); - } - - #[tokio::test] - async fn diarize_rejects_unknown_webhook_fields() { - let server = MockServer::start().await; - - let response = router(&server) - .oneshot( - Request::post("/v1/diarize") - .header("content-type", "application/json") - .body(Body::from( - r#"{"url":"media://users/user-123/audio.wav","webhook":"https://example.com/webhook"}"#, - )) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY); - } - - #[tokio::test] - async fn upstream_bad_request_maps_to_char_error_shape() { - let server = MockServer::start().await; - - Mock::given(method("POST")) - .and(path("/v1/voiceprint")) - .respond_with( - ResponseTemplate::new(400).set_body_json(json!({"message": "Invalid key"})), - ) - .mount(&server) - .await; - - let response = router(&server) - .oneshot( - Request::post("/v1/voiceprint") - .header("content-type", "application/json") - .body(Body::from(r#"{"url":"media://users/user-123/audio.wav"}"#)) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - assert_eq!( - response_json(response).await, - json!({"error": {"code": "bad_request", "message": "Invalid key"}}) - ); - } - - #[tokio::test] - async fn upstream_rate_limit_maps_to_char_error_shape() { - let server = MockServer::start().await; - - Mock::given(method("POST")) - .and(path("/v1/voiceprint")) - .respond_with(ResponseTemplate::new(429).set_body_json(json!({"message": "Slow down"}))) - .mount(&server) - .await; - - let response = router(&server) - .oneshot( - Request::post("/v1/voiceprint") - .header("content-type", "application/json") - .body(Body::from(r#"{"url":"media://users/user-123/audio.wav"}"#)) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS); - assert_eq!( - response_json(response).await, - json!({"error": {"code": "rate_limited", "message": "Slow down"}}) - ); - } - - #[tokio::test] - async fn upstream_validation_error_preserves_message() { - let server = MockServer::start().await; - - Mock::given(method("POST")) - .and(path("/v1/voiceprint")) - .respond_with(ResponseTemplate::new(400).set_body_json(json!({ - "message": "Invalid request", - "errors": [{"field": "url", "message": "Invalid URL"}] - }))) - .mount(&server) - .await; - - let response = router(&server) - .oneshot( - Request::post("/v1/voiceprint") - .header("content-type", "application/json") - .body(Body::from(r#"{"url":"media://users/user-123/audio.wav"}"#)) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - assert_eq!( - response_json(response).await, - json!({"error": {"code": "bad_request", "message": "Invalid request"}}) - ); - } - - #[tokio::test] - async fn malformed_upstream_body_falls_back_to_default_message() { - let server = MockServer::start().await; - - Mock::given(method("POST")) - .and(path("/v1/voiceprint")) - .respond_with(ResponseTemplate::new(429).set_body_string("<<>>")) - .mount(&server) - .await; - - let response = router(&server) - .oneshot( - Request::post("/v1/voiceprint") - .header("content-type", "application/json") - .body(Body::from(r#"{"url":"media://users/user-123/audio.wav"}"#)) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS); - assert_eq!( - response_json(response).await, - json!({"error": {"code": "rate_limited", "message": "Too many requests"}}) - ); - } - - #[tokio::test] - async fn empty_upstream_body_falls_back_to_default_message() { - let server = MockServer::start().await; - - Mock::given(method("POST")) - .and(path("/v1/voiceprint")) - .respond_with(ResponseTemplate::new(429)) - .mount(&server) - .await; - - let response = router(&server) - .oneshot( - Request::post("/v1/voiceprint") - .header("content-type", "application/json") - .body(Body::from(r#"{"url":"media://users/user-123/audio.wav"}"#)) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS); - assert_eq!( - response_json(response).await, - json!({"error": {"code": "rate_limited", "message": "Too many requests"}}) - ); - } - - #[tokio::test] - async fn upstream_server_error_still_redacts_message() { - let server = MockServer::start().await; - - Mock::given(method("POST")) - .and(path("/v1/voiceprint")) - .respond_with( - ResponseTemplate::new(500).set_body_json(json!({"message": "Upstream exploded"})), - ) - .mount(&server) - .await; - - let response = router(&server) - .oneshot( - Request::post("/v1/voiceprint") - .header("content-type", "application/json") - .body(Body::from(r#"{"url":"media://users/user-123/audio.wav"}"#)) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); - assert_eq!( - response_json(response).await, - json!({"error": {"code": "upstream_error", "message": "Internal server error"}}) - ); - } -} diff --git a/crates/api-research/Cargo.toml b/crates/api-research/Cargo.toml deleted file mode 100644 index ddc46e2352..0000000000 --- a/crates/api-research/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "api-research" -version = "0.1.0" -edition = "2024" - -[dependencies] -hypr-exa = { workspace = true } -hypr-jina = { workspace = true } -hypr-mcp = { workspace = true } - -axum = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -thiserror = { workspace = true } - -askama = { workspace = true } - -rmcp = { workspace = true, features = ["server"] } -tokio-util = { workspace = true } - -[dev-dependencies] -tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/crates/api-research/askama.toml b/crates/api-research/askama.toml deleted file mode 100644 index 291eba2963..0000000000 --- a/crates/api-research/askama.toml +++ /dev/null @@ -1,2 +0,0 @@ -[general] -dirs = ["assets"] diff --git a/crates/api-research/assets/research_chat.md.jinja b/crates/api-research/assets/research_chat.md.jinja deleted file mode 100644 index c9b92dd572..0000000000 --- a/crates/api-research/assets/research_chat.md.jinja +++ /dev/null @@ -1,13 +0,0 @@ -You are a helpful research assistant for Hyprnote, a privacy-first AI-powered note-taking application. - -Your role is to help users with: - -- Searching the web for relevant information -- Retrieving and summarizing content from web pages -- Gathering research materials to support their notes - -When a user asks you to find information, use the `search` tool to search the web. -When a user wants to read the contents of specific URLs, use the `get_contents` tool or the `read_url` tool to retrieve the text content. -Use `read_url` when you need a single URL converted to clean markdown text. - -Keep your responses concise and informative. Provide sources and links when sharing research findings. diff --git a/crates/api-research/src/config.rs b/crates/api-research/src/config.rs deleted file mode 100644 index ca646a7c6f..0000000000 --- a/crates/api-research/src/config.rs +++ /dev/null @@ -1,5 +0,0 @@ -#[derive(Clone)] -pub struct ResearchConfig { - pub exa_api_key: String, - pub jina_api_key: String, -} diff --git a/crates/api-research/src/lib.rs b/crates/api-research/src/lib.rs deleted file mode 100644 index f076c6d2b7..0000000000 --- a/crates/api-research/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod config; -mod mcp; -mod routes; -mod state; - -pub use config::ResearchConfig; -pub use routes::router; diff --git a/crates/api-research/src/mcp/mod.rs b/crates/api-research/src/mcp/mod.rs deleted file mode 100644 index 1ef6df80a8..0000000000 --- a/crates/api-research/src/mcp/mod.rs +++ /dev/null @@ -1,16 +0,0 @@ -mod prompts; -mod server; -mod tools; - -use crate::state::AppState; - -use server::ResearchMcpServer; - -pub(crate) fn mcp_service( - state: AppState, -) -> rmcp::transport::streamable_http_server::StreamableHttpService< - ResearchMcpServer, - rmcp::transport::streamable_http_server::session::local::LocalSessionManager, -> { - hypr_mcp::create_service(move || Ok(ResearchMcpServer::new(state.clone()))) -} diff --git a/crates/api-research/src/mcp/prompts/mod.rs b/crates/api-research/src/mcp/prompts/mod.rs deleted file mode 100644 index 2432ae19b9..0000000000 --- a/crates/api-research/src/mcp/prompts/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod research_chat; - -pub(crate) use research_chat::research_chat; diff --git a/crates/api-research/src/mcp/prompts/research_chat.rs b/crates/api-research/src/mcp/prompts/research_chat.rs deleted file mode 100644 index 072af1a439..0000000000 --- a/crates/api-research/src/mcp/prompts/research_chat.rs +++ /dev/null @@ -1,10 +0,0 @@ -use askama::Template; -use rmcp::{ErrorData as McpError, model::*}; - -#[derive(Template, Default)] -#[template(path = "research_chat.md.jinja")] -struct ResearchChatPrompt; - -pub(crate) fn research_chat() -> Result { - hypr_mcp::render_prompt::("System prompt for the Char research chat") -} diff --git a/crates/api-research/src/mcp/server.rs b/crates/api-research/src/mcp/server.rs deleted file mode 100644 index 73597d831c..0000000000 --- a/crates/api-research/src/mcp/server.rs +++ /dev/null @@ -1,123 +0,0 @@ -use rmcp::{ - ErrorData as McpError, RoleServer, ServerHandler, handler::server::tool::ToolRouter, - handler::server::wrapper::Parameters, model::*, service::RequestContext, tool, tool_handler, - tool_router, -}; - -use crate::state::AppState; - -use super::prompts; -use super::tools; - -#[derive(Clone)] -pub struct ResearchMcpServer { - state: AppState, - tool_router: ToolRouter, -} - -impl ResearchMcpServer { - pub(super) fn new(state: AppState) -> Self { - Self { - state, - tool_router: Self::tool_router(), - } - } -} - -#[tool_router] -impl ResearchMcpServer { - #[tool( - description = "Search the web using Exa. Returns relevant results for a given query.", - annotations( - read_only_hint = true, - destructive_hint = false, - open_world_hint = true - ) - )] - async fn search( - &self, - Parameters(params): Parameters, - ) -> Result { - tools::search(&self.state, params).await - } - - #[tool( - description = "Get the contents of web pages by URL. Returns the text content of the given URLs.", - annotations( - read_only_hint = true, - destructive_hint = false, - open_world_hint = true - ) - )] - async fn get_contents( - &self, - Parameters(params): Parameters, - ) -> Result { - tools::get_contents(&self.state, params).await - } - - #[tool( - description = "Read a URL and convert it to clean, LLM-friendly markdown text. Powered by Jina Reader.", - annotations( - read_only_hint = true, - destructive_hint = false, - open_world_hint = true - ) - )] - async fn read_url( - &self, - Parameters(params): Parameters, - ) -> Result { - tools::read_url(&self.state, params).await - } -} - -#[tool_handler] -impl ServerHandler for ResearchMcpServer { - fn get_info(&self) -> ServerInfo { - ServerInfo::new( - ServerCapabilities::builder() - .enable_tools() - .enable_prompts() - .build(), - ) - .with_protocol_version(ProtocolVersion::V_2024_11_05) - .with_server_info(Implementation::new( - "hyprnote-research", - env!("CARGO_PKG_VERSION"), - )) - .with_instructions( - "Char research server. Provides tools for web search and content retrieval powered by Exa.", - ) - } - - async fn list_prompts( - &self, - _params: Option, - _context: RequestContext, - ) -> Result { - Ok(ListPromptsResult { - prompts: vec![Prompt::new( - "research_chat", - Some("System prompt for the Char research chat"), - None::>, - )], - next_cursor: None, - meta: None, - }) - } - - async fn get_prompt( - &self, - params: GetPromptRequestParams, - _context: RequestContext, - ) -> Result { - match params.name.as_str() { - "research_chat" => prompts::research_chat(), - _ => Err(McpError::invalid_params( - format!("Unknown prompt: {}", params.name), - None, - )), - } - } -} diff --git a/crates/api-research/src/mcp/tools/get_contents.rs b/crates/api-research/src/mcp/tools/get_contents.rs deleted file mode 100644 index 7a23df2fc3..0000000000 --- a/crates/api-research/src/mcp/tools/get_contents.rs +++ /dev/null @@ -1,19 +0,0 @@ -use rmcp::{ErrorData as McpError, model::*}; - -use crate::state::AppState; - -pub(crate) async fn get_contents( - state: &AppState, - params: hypr_exa::GetContentsRequest, -) -> Result { - let response = state - .exa - .get_contents(params) - .await - .map_err(|e: hypr_exa::Error| McpError::internal_error(e.to_string(), None))?; - - Ok(CallToolResult::success(vec![Content::text( - serde_json::to_string(&response) - .map_err(|e| McpError::internal_error(e.to_string(), None))?, - )])) -} diff --git a/crates/api-research/src/mcp/tools/mod.rs b/crates/api-research/src/mcp/tools/mod.rs deleted file mode 100644 index cc99d0b794..0000000000 --- a/crates/api-research/src/mcp/tools/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod get_contents; -mod read_url; -mod search; - -pub(crate) use get_contents::get_contents; -pub(crate) use read_url::read_url; -pub(crate) use search::search; diff --git a/crates/api-research/src/mcp/tools/read_url.rs b/crates/api-research/src/mcp/tools/read_url.rs deleted file mode 100644 index 777b2d85fc..0000000000 --- a/crates/api-research/src/mcp/tools/read_url.rs +++ /dev/null @@ -1,16 +0,0 @@ -use rmcp::{ErrorData as McpError, model::*}; - -use crate::state::AppState; - -pub(crate) async fn read_url( - state: &AppState, - params: hypr_jina::ReadUrlRequest, -) -> Result { - let text = state - .jina - .read_url(params) - .await - .map_err(|e: hypr_jina::Error| McpError::internal_error(e.to_string(), None))?; - - Ok(CallToolResult::success(vec![Content::text(text)])) -} diff --git a/crates/api-research/src/mcp/tools/search.rs b/crates/api-research/src/mcp/tools/search.rs deleted file mode 100644 index d640255eb4..0000000000 --- a/crates/api-research/src/mcp/tools/search.rs +++ /dev/null @@ -1,19 +0,0 @@ -use rmcp::{ErrorData as McpError, model::*}; - -use crate::state::AppState; - -pub(crate) async fn search( - state: &AppState, - params: hypr_exa::SearchRequest, -) -> Result { - let response = state - .exa - .search(params) - .await - .map_err(|e: hypr_exa::Error| McpError::internal_error(e.to_string(), None))?; - - Ok(CallToolResult::success(vec![Content::text( - serde_json::to_string(&response) - .map_err(|e| McpError::internal_error(e.to_string(), None))?, - )])) -} diff --git a/crates/api-research/src/routes.rs b/crates/api-research/src/routes.rs deleted file mode 100644 index 0dbe03396e..0000000000 --- a/crates/api-research/src/routes.rs +++ /dev/null @@ -1,12 +0,0 @@ -use axum::Router; - -use crate::config::ResearchConfig; -use crate::mcp::mcp_service; -use crate::state::AppState; - -pub fn router(config: ResearchConfig) -> Router { - let state = AppState::new(config); - let mcp = mcp_service(state); - - Router::new().nest("/research", Router::new().nest_service("/mcp", mcp)) -} diff --git a/crates/api-research/src/state.rs b/crates/api-research/src/state.rs deleted file mode 100644 index 0594dd9e4e..0000000000 --- a/crates/api-research/src/state.rs +++ /dev/null @@ -1,26 +0,0 @@ -use hypr_exa::ExaClient; -use hypr_jina::JinaClient; - -use crate::config::ResearchConfig; - -#[derive(Clone)] -pub(crate) struct AppState { - pub(crate) exa: ExaClient, - pub(crate) jina: JinaClient, -} - -impl AppState { - pub(crate) fn new(config: ResearchConfig) -> Self { - let exa = ExaClient::builder() - .api_key(config.exa_api_key) - .build() - .expect("failed to build Exa client"); - - let jina = JinaClient::builder() - .api_key(config.jina_api_key) - .build() - .expect("failed to build Jina client"); - - Self { exa, jina } - } -} diff --git a/crates/api-storage/Cargo.toml b/crates/api-storage/Cargo.toml deleted file mode 100644 index 0ca67d1869..0000000000 --- a/crates/api-storage/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "api-storage" -version = "0.1.0" -edition = "2024" - -[dependencies] -hypr-api-error = { workspace = true } -hypr-api-nango = { workspace = true } -hypr-google-drive = { workspace = true } -hypr-nango = { workspace = true } - -utoipa = { workspace = true } - -axum = { workspace = true } -sentry = { workspace = true } -tracing = { workspace = true } - -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -thiserror = { workspace = true } diff --git a/crates/api-storage/src/error.rs b/crates/api-storage/src/error.rs deleted file mode 100644 index c074bcfea5..0000000000 --- a/crates/api-storage/src/error.rs +++ /dev/null @@ -1,41 +0,0 @@ -use axum::{ - http::StatusCode, - response::{IntoResponse, Response}, -}; -use thiserror::Error; - -pub type Result = std::result::Result; - -#[derive(Debug, Error)] -pub enum StorageError { - #[error("Authentication error: {0}")] - #[allow(dead_code)] - Auth(String), - - #[error("Invalid request: {0}")] - #[allow(dead_code)] - BadRequest(String), - - #[error("Internal error: {0}")] - Internal(String), - - #[error(transparent)] - NangoConnection(#[from] hypr_api_nango::NangoConnectionError), -} - -impl IntoResponse for StorageError { - fn into_response(self) -> Response { - let (status, code, message) = match self { - Self::Auth(message) => (StatusCode::UNAUTHORIZED, "unauthorized", message), - Self::BadRequest(message) => (StatusCode::BAD_REQUEST, "bad_request", message), - Self::Internal(message) => ( - StatusCode::INTERNAL_SERVER_ERROR, - "internal_server_error", - message, - ), - Self::NangoConnection(err) => return err.into_response(), - }; - - hypr_api_error::error_response(status, code, &message) - } -} diff --git a/crates/api-storage/src/lib.rs b/crates/api-storage/src/lib.rs deleted file mode 100644 index c9b1108350..0000000000 --- a/crates/api-storage/src/lib.rs +++ /dev/null @@ -1,6 +0,0 @@ -mod error; -mod openapi; -mod routes; - -pub use openapi::openapi; -pub use routes::router; diff --git a/crates/api-storage/src/openapi.rs b/crates/api-storage/src/openapi.rs deleted file mode 100644 index df71540b4f..0000000000 --- a/crates/api-storage/src/openapi.rs +++ /dev/null @@ -1,39 +0,0 @@ -use utoipa::OpenApi; - -#[derive(OpenApi)] -#[openapi( - paths( - crate::routes::storage::list_files, - crate::routes::storage::get_file, - crate::routes::storage::download_file, - crate::routes::storage::create_folder, - crate::routes::storage::delete_file, - crate::routes::storage::upload_file, - crate::routes::storage::update_metadata, - ), - components( - schemas( - crate::routes::storage::ListFilesRequest, - crate::routes::storage::ListFilesResponse, - crate::routes::storage::GetFileRequest, - crate::routes::storage::GetFileResponse, - crate::routes::storage::DownloadFileRequest, - crate::routes::storage::DownloadFileResponse, - crate::routes::storage::CreateFolderRequest, - crate::routes::storage::CreateFolderResponse, - crate::routes::storage::DeleteFileRequest, - crate::routes::storage::UploadFileRequest, - crate::routes::storage::UploadFileResponse, - crate::routes::storage::UpdateMetadataRequest, - crate::routes::storage::UpdateMetadataResponse, - ) - ), - tags( - (name = "storage", description = "Storage management (Google Drive)") - ) -)] -struct ApiDoc; - -pub fn openapi() -> utoipa::openapi::OpenApi { - ApiDoc::openapi() -} diff --git a/crates/api-storage/src/routes/mod.rs b/crates/api-storage/src/routes/mod.rs deleted file mode 100644 index c5ea7ea82d..0000000000 --- a/crates/api-storage/src/routes/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -pub(crate) mod storage; - -use axum::{Router, routing::post}; - -pub fn router() -> Router { - Router::new() - .route("/files", post(storage::list_files)) - .route("/files/get", post(storage::get_file)) - .route("/files/download", post(storage::download_file)) - .route("/files/create-folder", post(storage::create_folder)) - .route("/files/delete", post(storage::delete_file)) - .route("/files/upload", post(storage::upload_file)) - .route("/files/update", post(storage::update_metadata)) -} diff --git a/crates/api-storage/src/routes/storage.rs b/crates/api-storage/src/routes/storage.rs deleted file mode 100644 index 425d77b8f0..0000000000 --- a/crates/api-storage/src/routes/storage.rs +++ /dev/null @@ -1,318 +0,0 @@ -use axum::Json; -use hypr_api_nango::{GoogleDrive, NangoConnection}; -use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; - -use crate::error::{Result, StorageError}; - -#[derive(Debug, Deserialize, ToSchema)] -pub struct ListFilesRequest { - #[serde(default)] - pub q: Option, - #[serde(default)] - pub page_size: Option, - #[serde(default)] - pub page_token: Option, - #[serde(default)] - pub order_by: Option, -} - -#[derive(Debug, Serialize, ToSchema)] -pub struct ListFilesResponse { - pub files: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub next_page_token: Option, -} - -#[derive(Debug, Deserialize, ToSchema)] -pub struct GetFileRequest { - pub file_id: String, -} - -#[derive(Debug, Serialize, ToSchema)] -pub struct GetFileResponse { - pub file: serde_json::Value, -} - -#[derive(Debug, Deserialize, ToSchema)] -pub struct DownloadFileRequest { - pub file_id: String, -} - -#[derive(Debug, Serialize, ToSchema)] -pub struct DownloadFileResponse { - pub data: Vec, -} - -#[derive(Debug, Deserialize, ToSchema)] -pub struct CreateFolderRequest { - pub name: String, - #[serde(default)] - pub parent_id: Option, -} - -#[derive(Debug, Serialize, ToSchema)] -pub struct CreateFolderResponse { - pub file: serde_json::Value, -} - -#[derive(Debug, Deserialize, ToSchema)] -pub struct DeleteFileRequest { - pub file_id: String, -} - -#[derive(Debug, Deserialize, ToSchema)] -pub struct UploadFileRequest { - pub name: String, - #[serde(default)] - pub parent_id: Option, - pub mime_type: String, - pub data: Vec, -} - -#[derive(Debug, Serialize, ToSchema)] -pub struct UploadFileResponse { - pub file: serde_json::Value, -} - -#[derive(Debug, Deserialize, ToSchema)] -pub struct UpdateMetadataRequest { - pub file_id: String, - #[serde(default)] - pub name: Option, - #[serde(default)] - pub description: Option, - #[serde(default)] - pub starred: Option, - #[serde(default)] - pub trashed: Option, -} - -#[derive(Debug, Serialize, ToSchema)] -pub struct UpdateMetadataResponse { - pub file: serde_json::Value, -} - -#[utoipa::path( - post, - path = "/files", - request_body = ListFilesRequest, - responses( - (status = 200, description = "Files listed", body = ListFilesResponse), - (status = 401, description = "Unauthorized"), - (status = 500, description = "Internal server error"), - ), - tag = "storage", -)] -pub async fn list_files( - nango: NangoConnection, - Json(payload): Json, -) -> Result> { - let client = hypr_google_drive::GoogleDriveClient::new(nango.into_http()); - - let req = hypr_google_drive::ListFilesRequest { - q: payload.q, - page_size: payload.page_size, - page_token: payload.page_token, - order_by: payload.order_by, - fields: None, - }; - - let response = client - .list_files(req) - .await - .map_err(|e| StorageError::Internal(e.to_string()))?; - - let files: Vec = response - .files - .iter() - .map(|f| serde_json::to_value(f).unwrap_or_default()) - .collect(); - - Ok(Json(ListFilesResponse { - files, - next_page_token: response.next_page_token, - })) -} - -#[utoipa::path( - post, - path = "/files/get", - request_body = GetFileRequest, - responses( - (status = 200, description = "File metadata fetched", body = GetFileResponse), - (status = 401, description = "Unauthorized"), - (status = 500, description = "Internal server error"), - ), - tag = "storage", -)] -pub async fn get_file( - nango: NangoConnection, - Json(payload): Json, -) -> Result> { - let client = hypr_google_drive::GoogleDriveClient::new(nango.into_http()); - - let req = hypr_google_drive::GetFileRequest { - file_id: payload.file_id, - fields: None, - }; - - let file = client - .get_file(req) - .await - .map_err(|e| StorageError::Internal(e.to_string()))?; - - let file = serde_json::to_value(file).unwrap_or_default(); - - Ok(Json(GetFileResponse { file })) -} - -#[utoipa::path( - post, - path = "/files/download", - request_body = DownloadFileRequest, - responses( - (status = 200, description = "File content downloaded", body = DownloadFileResponse), - (status = 401, description = "Unauthorized"), - (status = 500, description = "Internal server error"), - ), - tag = "storage", -)] -pub async fn download_file( - nango: NangoConnection, - Json(payload): Json, -) -> Result> { - let client = hypr_google_drive::GoogleDriveClient::new(nango.into_http()); - - let data = client - .download_file(&payload.file_id) - .await - .map_err(|e| StorageError::Internal(e.to_string()))?; - - Ok(Json(DownloadFileResponse { data })) -} - -#[utoipa::path( - post, - path = "/files/create-folder", - request_body = CreateFolderRequest, - responses( - (status = 200, description = "Folder created", body = CreateFolderResponse), - (status = 401, description = "Unauthorized"), - (status = 500, description = "Internal server error"), - ), - tag = "storage", -)] -pub async fn create_folder( - nango: NangoConnection, - Json(payload): Json, -) -> Result> { - let client = hypr_google_drive::GoogleDriveClient::new(nango.into_http()); - - let req = hypr_google_drive::CreateFolderRequest { - name: payload.name, - parent_id: payload.parent_id, - }; - - let file = client - .create_folder(req) - .await - .map_err(|e| StorageError::Internal(e.to_string()))?; - - let file = serde_json::to_value(file).unwrap_or_default(); - - Ok(Json(CreateFolderResponse { file })) -} - -#[utoipa::path( - post, - path = "/files/delete", - request_body = DeleteFileRequest, - responses( - (status = 200, description = "File deleted"), - (status = 401, description = "Unauthorized"), - (status = 500, description = "Internal server error"), - ), - tag = "storage", -)] -pub async fn delete_file( - nango: NangoConnection, - Json(payload): Json, -) -> Result<()> { - let client = hypr_google_drive::GoogleDriveClient::new(nango.into_http()); - - client - .delete_file(&payload.file_id) - .await - .map_err(|e| StorageError::Internal(e.to_string()))?; - - Ok(()) -} - -#[utoipa::path( - post, - path = "/files/upload", - request_body = UploadFileRequest, - responses( - (status = 200, description = "File uploaded", body = UploadFileResponse), - (status = 401, description = "Unauthorized"), - (status = 500, description = "Internal server error"), - ), - tag = "storage", -)] -pub async fn upload_file( - nango: NangoConnection, - Json(payload): Json, -) -> Result> { - let client = hypr_google_drive::GoogleDriveClient::new(nango.into_http()); - - let req = hypr_google_drive::UploadFileRequest { - name: payload.name, - parent_id: payload.parent_id, - mime_type: payload.mime_type, - data: payload.data, - }; - - let file = client - .upload_file(req) - .await - .map_err(|e| StorageError::Internal(e.to_string()))?; - - let file = serde_json::to_value(file).unwrap_or_default(); - - Ok(Json(UploadFileResponse { file })) -} - -#[utoipa::path( - post, - path = "/files/update", - request_body = UpdateMetadataRequest, - responses( - (status = 200, description = "File metadata updated", body = UpdateMetadataResponse), - (status = 401, description = "Unauthorized"), - (status = 500, description = "Internal server error"), - ), - tag = "storage", -)] -pub async fn update_metadata( - nango: NangoConnection, - Json(payload): Json, -) -> Result> { - let client = hypr_google_drive::GoogleDriveClient::new(nango.into_http()); - - let req = hypr_google_drive::UpdateMetadataRequest { - name: payload.name, - description: payload.description, - starred: payload.starred, - trashed: payload.trashed, - }; - - let file = client - .update_metadata(&payload.file_id, req) - .await - .map_err(|e| StorageError::Internal(e.to_string()))?; - - let file = serde_json::to_value(file).unwrap_or_default(); - - Ok(Json(UpdateMetadataResponse { file })) -} diff --git a/crates/api-subscription/Cargo.toml b/crates/api-subscription/Cargo.toml deleted file mode 100644 index 47ec0f84dd..0000000000 --- a/crates/api-subscription/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "api-subscription" -version = "0.1.0" -edition = "2024" - -[dependencies] -hypr-analytics = { workspace = true } -hypr-api-auth = { workspace = true } -hypr-api-env = { workspace = true } -hypr-api-error = { workspace = true } -hypr-loops = { workspace = true } -hypr-observability = { workspace = true } - -utoipa = { workspace = true } - -axum = { workspace = true } -reqwest = { workspace = true, features = ["json"] } -sentry = { workspace = true } -tokio = { workspace = true } -tracing = { workspace = true } - -chrono = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -thiserror = { workspace = true } -urlencoding = { workspace = true } - -backon = { workspace = true } - -async-stripe = { workspace = true } -async-stripe-billing = { workspace = true, features = ["subscription"] } -async-stripe-core = { workspace = true, features = ["customer"] } diff --git a/crates/api-subscription/src/config.rs b/crates/api-subscription/src/config.rs deleted file mode 100644 index 0d6e3f162b..0000000000 --- a/crates/api-subscription/src/config.rs +++ /dev/null @@ -1,30 +0,0 @@ -use std::sync::Arc; - -use hypr_analytics::AnalyticsClient; - -use crate::StripeEnv; -use hypr_api_env::{LoopsEnv, SupabaseEnv}; - -#[derive(Clone)] -pub struct SubscriptionConfig { - pub supabase: SupabaseEnv, - pub stripe: StripeEnv, - pub loops: LoopsEnv, - pub analytics: Option>, -} - -impl SubscriptionConfig { - pub fn new(supabase: &SupabaseEnv, stripe: &StripeEnv, loops: &LoopsEnv) -> Self { - Self { - supabase: supabase.clone(), - stripe: stripe.clone(), - loops: loops.clone(), - analytics: None, - } - } - - pub fn with_analytics(mut self, analytics: Arc) -> Self { - self.analytics = Some(analytics); - self - } -} diff --git a/crates/api-subscription/src/env.rs b/crates/api-subscription/src/env.rs deleted file mode 100644 index f93015876c..0000000000 --- a/crates/api-subscription/src/env.rs +++ /dev/null @@ -1 +0,0 @@ -pub use hypr_api_env::StripeEnv; diff --git a/crates/api-subscription/src/error.rs b/crates/api-subscription/src/error.rs deleted file mode 100644 index 01d8afe70a..0000000000 --- a/crates/api-subscription/src/error.rs +++ /dev/null @@ -1,43 +0,0 @@ -use axum::{ - http::StatusCode, - response::{IntoResponse, Response}, -}; -use thiserror::Error; - -pub type Result = std::result::Result; - -#[derive(Debug, Error)] -pub enum SubscriptionError { - #[error("Supabase request failed: {0}")] - SupabaseRequest(String), - - #[error("Stripe error: {0}")] - Stripe(String), - - #[error("Internal error: {0}")] - Internal(String), -} - -impl From for SubscriptionError { - fn from(err: stripe::StripeError) -> Self { - Self::Stripe(err.to_string()) - } -} - -impl IntoResponse for SubscriptionError { - fn into_response(self) -> Response { - let (status, code, message) = match self { - Self::SupabaseRequest(msg) => { - (StatusCode::INTERNAL_SERVER_ERROR, "supabase_error", msg) - } - Self::Stripe(msg) => (StatusCode::INTERNAL_SERVER_ERROR, "stripe_error", msg), - Self::Internal(msg) => ( - StatusCode::INTERNAL_SERVER_ERROR, - "internal_server_error", - msg, - ), - }; - - hypr_api_error::error_response(status, code, &message) - } -} diff --git a/crates/api-subscription/src/lib.rs b/crates/api-subscription/src/lib.rs deleted file mode 100644 index 8f07f8377e..0000000000 --- a/crates/api-subscription/src/lib.rs +++ /dev/null @@ -1,14 +0,0 @@ -mod config; -mod env; -mod error; -mod openapi; -mod routes; -mod state; -mod stripe; -mod supabase; -mod trial; - -pub use config::SubscriptionConfig; -pub use env::StripeEnv; -pub use openapi::openapi; -pub use routes::router; diff --git a/crates/api-subscription/src/openapi.rs b/crates/api-subscription/src/openapi.rs deleted file mode 100644 index 773bbe208d..0000000000 --- a/crates/api-subscription/src/openapi.rs +++ /dev/null @@ -1,33 +0,0 @@ -use utoipa::OpenApi; - -use crate::routes::{ - CanStartTrialReason, CanStartTrialResponse, DeleteAccountResponse, Interval, StartTrialReason, - StartTrialResponse, -}; - -#[derive(OpenApi)] -#[openapi( - paths( - crate::routes::rpc::can_start_trial, - crate::routes::billing::start_trial, - crate::routes::account::delete_account, - ), - components( - schemas( - CanStartTrialResponse, - CanStartTrialReason, - StartTrialResponse, - StartTrialReason, - Interval, - DeleteAccountResponse, - ) - ), - tags( - (name = "subscription", description = "Subscription and trial management") - ) -)] -struct ApiDoc; - -pub fn openapi() -> utoipa::openapi::OpenApi { - ApiDoc::openapi() -} diff --git a/crates/api-subscription/src/routes/account.rs b/crates/api-subscription/src/routes/account.rs deleted file mode 100644 index 98aa82d115..0000000000 --- a/crates/api-subscription/src/routes/account.rs +++ /dev/null @@ -1,193 +0,0 @@ -use std::time::Duration; - -use axum::{ - Extension, Json, - extract::State, - http::StatusCode, - response::{IntoResponse, Response}, -}; -use backon::{ExponentialBuilder, Retryable}; -use hypr_api_auth::AuthContext; -use serde::Serialize; -use stripe_core::customer::DeleteCustomer; -use utoipa::ToSchema; - -use crate::state::AppState; - -#[derive(Debug, Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct DeleteAccountResponse { - pub deleted: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub error: Option, -} - -fn retry_policy() -> ExponentialBuilder { - ExponentialBuilder::default() - .with_min_delay(Duration::from_millis(100)) - .with_max_times(3) -} - -#[utoipa::path( - delete, - path = "/delete-account", - responses( - (status = 200, description = "Account deleted successfully", body = DeleteAccountResponse), - (status = 401, description = "Unauthorized"), - (status = 500, description = "Internal server error"), - ), - tag = "subscription", -)] -pub async fn delete_account( - State(state): State, - Extension(auth): Extension, -) -> Response { - let user_id = &auth.claims.sub; - - if let Err(response) = try_delete_stripe_customer(&state, user_id).await { - return response; - } - - let _ = (|| { - state - .supabase - .admin_delete_storage_objects("audio-files", user_id) - }) - .retry(retry_policy()) - .sleep(tokio::time::sleep) - .await - .inspect_err(|e| { - tracing::warn!( - enduser.id = %user_id, - error = %e, - "storage_cleanup_failed" - ) - }); - - try_delete_loops_contact(&state, &auth.token, user_id).await; - - match (|| state.supabase.admin_delete_user(user_id)) - .retry(retry_policy()) - .sleep(tokio::time::sleep) - .await - { - Ok(()) => { - tracing::info!(enduser.id = %user_id, "account_deleted"); - ( - StatusCode::OK, - Json(DeleteAccountResponse { - deleted: true, - error: None, - }), - ) - .into_response() - } - Err(e) => { - tracing::error!( - enduser.id = %user_id, - error = %e, - "account_deletion_failed" - ); - sentry::capture_message(&e.to_string(), sentry::Level::Error); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(DeleteAccountResponse { - deleted: false, - error: Some("account_deletion_failed".to_string()), - }), - ) - .into_response() - } - } -} - -async fn try_delete_stripe_customer(state: &AppState, user_id: &str) -> Result<(), Response> { - let customer_id = match (|| state.supabase.admin_get_stripe_customer_id(user_id)) - .retry(retry_policy()) - .sleep(tokio::time::sleep) - .await - { - Ok(Some(id)) => id, - Ok(None) => { - tracing::info!(enduser.id = %user_id, "no_stripe_customer_to_delete"); - return Ok(()); - } - Err(e) => { - tracing::warn!( - enduser.id = %user_id, - error = %e, - "failed_to_lookup_stripe_customer" - ); - return Ok(()); - } - }; - - match (|| async { DeleteCustomer::new(&*customer_id).send(&state.stripe).await }) - .retry(retry_policy()) - .sleep(tokio::time::sleep) - .await - { - Ok(_) => { - tracing::info!( - enduser.id = %user_id, - hyprnote.billing.customer.id = %customer_id, - "stripe_customer_deleted" - ); - Ok(()) - } - Err(e) => { - tracing::error!( - enduser.id = %user_id, - hyprnote.billing.customer.id = %customer_id, - error = %e, - "stripe_customer_deletion_failed" - ); - sentry::capture_message( - &format!("stripe customer deletion failed for {}: {}", user_id, e), - sentry::Level::Error, - ); - Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(DeleteAccountResponse { - deleted: false, - error: Some("stripe_customer_deletion_failed".to_string()), - }), - ) - .into_response()) - } - } -} - -async fn try_delete_loops_contact(state: &AppState, token: &str, user_id: &str) { - let email = match (|| state.supabase.get_user_email(token)) - .retry(retry_policy()) - .sleep(tokio::time::sleep) - .await - { - Ok(Some(email)) => email, - Ok(None) => { - tracing::warn!(enduser.id = %user_id, "no_email_for_loops_deletion"); - return; - } - Err(e) => { - tracing::warn!( - enduser.id = %user_id, - error = %e, - "failed_to_get_email_for_loops" - ); - return; - } - }; - - let _ = (|| state.loops.delete_contact_by_email(&email)) - .retry(retry_policy()) - .sleep(tokio::time::sleep) - .await - .inspect_err(|e| { - tracing::warn!( - enduser.id = %user_id, - error = %e, - "loops_contact_deletion_failed" - ) - }); -} diff --git a/crates/api-subscription/src/routes/billing.rs b/crates/api-subscription/src/routes/billing.rs deleted file mode 100644 index ad6bc52848..0000000000 --- a/crates/api-subscription/src/routes/billing.rs +++ /dev/null @@ -1,143 +0,0 @@ -use axum::{ - Extension, - extract::{Query, State}, - response::{IntoResponse, Response}, -}; -use hypr_analytics::{AnalyticsClient, DeviceFingerprint, ToAnalyticsPayload}; -use hypr_api_auth::AuthContext; - -use crate::state::AppState; -use crate::stripe::{create_trial_subscription, get_or_create_customer}; -use crate::trial::{Interval, StartTrialQuery, StartTrialResponse, TrialOutcome}; - -#[utoipa::path( - post, - path = "/start-trial", - params(StartTrialQuery), - responses( - (status = 200, description = "Trial started successfully", body = StartTrialResponse), - (status = 401, description = "Unauthorized"), - (status = 500, description = "Internal server error"), - ), - tag = "subscription", -)] -#[tracing::instrument( - name = "subscription.start_trial", - skip(state, query, auth, device_fingerprint), - fields(hyprnote.subsystem = "subscription") -)] -pub async fn start_trial( - State(state): State, - Query(query): Query, - Extension(auth): Extension, - device_fingerprint: Option>, -) -> Response { - let user_id = &auth.claims.sub; - let device_fingerprint = - device_fingerprint.map(|Extension(DeviceFingerprint(fingerprint))| fingerprint); - - let source = if device_fingerprint.is_some() { - "desktop" - } else { - "web" - }; - let distinct_id = device_fingerprint.as_deref().unwrap_or(user_id); - - let can_start: bool = match state - .supabase - .rpc("can_start_trial", &auth.token, None) - .await - { - Ok(v) => v, - Err(e) => { - return emit_and_respond( - state.config.analytics.as_deref(), - distinct_id, - user_id, - source, - TrialOutcome::RpcError(e.to_string()), - ) - .await; - } - }; - - let outcome = - if !can_start { - TrialOutcome::NotEligible - } else { - let customer_id = - match get_or_create_customer(&state.supabase, &state.stripe, &auth.token, user_id) - .await - { - Ok(Some(id)) => id, - Ok(None) => { - return emit_and_respond( - state.config.analytics.as_deref(), - distinct_id, - user_id, - source, - TrialOutcome::CustomerError("customer not found".to_string()), - ) - .await; - } - Err(e) => { - return emit_and_respond( - state.config.analytics.as_deref(), - distinct_id, - user_id, - source, - TrialOutcome::CustomerError(e.to_string()), - ) - .await; - } - }; - - let price_id = match query.interval { - Interval::Monthly => &state.config.stripe.stripe_monthly_price_id, - Interval::Yearly => &state.config.stripe.stripe_yearly_price_id, - }; - - match create_trial_subscription(&state.stripe, &customer_id, price_id, user_id).await { - Ok(()) => TrialOutcome::Started(query.interval), - Err(e) => TrialOutcome::StripeError(e.to_string()), - } - }; - - emit_and_respond( - state.config.analytics.as_deref(), - distinct_id, - user_id, - source, - outcome, - ) - .await -} - -async fn emit_and_respond( - analytics: Option<&AnalyticsClient>, - distinct_id: &str, - user_id: &str, - source: &str, - outcome: O, -) -> Response -where - O: IntoResponse + ToAnalyticsPayload, -{ - if let Some(analytics) = analytics { - let mut payload = outcome.to_analytics_payload(); - payload.props.insert("source".to_string(), source.into()); - if distinct_id != user_id { - payload.props.insert("user_id".to_string(), user_id.into()); - } - - if let Err(e) = analytics.event(distinct_id, payload).await { - tracing::warn!("analytics event error: {e}"); - } - if let Some(props) = outcome.to_analytics_properties() - && let Err(e) = analytics.set_properties(user_id, props).await - { - tracing::warn!("analytics set_properties error: {e}"); - } - } - outcome.into_response() -} diff --git a/crates/api-subscription/src/routes/mod.rs b/crates/api-subscription/src/routes/mod.rs deleted file mode 100644 index d407cc38cc..0000000000 --- a/crates/api-subscription/src/routes/mod.rs +++ /dev/null @@ -1,25 +0,0 @@ -pub(crate) mod account; -pub(crate) mod billing; -pub(crate) mod rpc; - -use axum::{ - Router, - routing::{delete, get, post}, -}; - -use crate::config::SubscriptionConfig; -use crate::state::AppState; - -pub use crate::trial::{Interval, StartTrialReason, StartTrialResponse}; -pub use account::DeleteAccountResponse; -pub use rpc::{CanStartTrialReason, CanStartTrialResponse}; - -pub fn router(config: SubscriptionConfig) -> Router { - let state = AppState::new(config); - - Router::new() - .route("/can-start-trial", get(rpc::can_start_trial)) - .route("/start-trial", post(billing::start_trial)) - .route("/delete-account", delete(account::delete_account)) - .with_state(state) -} diff --git a/crates/api-subscription/src/routes/rpc.rs b/crates/api-subscription/src/routes/rpc.rs deleted file mode 100644 index 086ba77646..0000000000 --- a/crates/api-subscription/src/routes/rpc.rs +++ /dev/null @@ -1,62 +0,0 @@ -use axum::{Extension, Json, extract::State}; -use serde::Serialize; -use utoipa::ToSchema; - -use crate::state::AppState; - -use hypr_api_auth::AuthContext; - -#[derive(Debug, Serialize, ToSchema)] -#[serde(rename_all = "snake_case")] -pub enum CanStartTrialReason { - Eligible, - NotEligible, - Error, -} - -#[derive(Debug, Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct CanStartTrialResponse { - #[schema(example = true)] - pub can_start_trial: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub reason: Option, -} - -#[utoipa::path( - get, - path = "/can-start-trial", - responses( - (status = 200, description = "Check successful", body = CanStartTrialResponse), - (status = 401, description = "Unauthorized"), - (status = 500, description = "Internal server error"), - ), - tag = "subscription", -)] -pub async fn can_start_trial( - State(state): State, - Extension(auth): Extension, -) -> Json { - let result: std::result::Result = state - .supabase - .rpc("can_start_trial", &auth.token, None) - .await; - - match result { - Ok(true) => Json(CanStartTrialResponse { - can_start_trial: true, - reason: Some(CanStartTrialReason::Eligible), - }), - Ok(false) => Json(CanStartTrialResponse { - can_start_trial: false, - reason: Some(CanStartTrialReason::NotEligible), - }), - Err(e) => { - tracing::error!(error = %e, "can_start_trial_rpc_failed"); - Json(CanStartTrialResponse { - can_start_trial: false, - reason: Some(CanStartTrialReason::Error), - }) - } - } -} diff --git a/crates/api-subscription/src/state.rs b/crates/api-subscription/src/state.rs deleted file mode 100644 index 0ee6518d89..0000000000 --- a/crates/api-subscription/src/state.rs +++ /dev/null @@ -1,36 +0,0 @@ -use hypr_loops::LoopClient; -use stripe::Client as StripeClient; - -use crate::config::SubscriptionConfig; -use crate::supabase::SupabaseClient; - -#[derive(Clone)] -pub(crate) struct AppState { - pub(crate) config: SubscriptionConfig, - pub(crate) supabase: SupabaseClient, - pub(crate) stripe: StripeClient, - pub(crate) loops: LoopClient, -} - -impl AppState { - pub(crate) fn new(config: SubscriptionConfig) -> Self { - let supabase = SupabaseClient::new( - config.supabase.supabase_url.clone(), - config.supabase.supabase_anon_key.clone(), - config.supabase.supabase_service_role_key.clone(), - ); - - let stripe = StripeClient::new(&config.stripe.stripe_secret_key); - - let loops = LoopClient::builder() - .api_key(&config.loops.loops_key) - .build(); - - Self { - config, - supabase, - stripe, - loops, - } - } -} diff --git a/crates/api-subscription/src/stripe.rs b/crates/api-subscription/src/stripe.rs deleted file mode 100644 index a0eeb9a9d9..0000000000 --- a/crates/api-subscription/src/stripe.rs +++ /dev/null @@ -1,141 +0,0 @@ -use chrono::Utc; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::time::Instant; -use stripe::StripeRequest; -use stripe_billing::subscription::{ - CreateSubscription, CreateSubscriptionItems, CreateSubscriptionTrialSettings, - CreateSubscriptionTrialSettingsEndBehavior, - CreateSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, -}; -use stripe_core::customer::CreateCustomer; - -use crate::error::{Result, SubscriptionError}; -use crate::supabase::SupabaseClient; - -#[derive(Debug, Deserialize)] -struct Profile { - stripe_customer_id: Option, -} - -pub(crate) async fn get_or_create_customer( - supabase: &SupabaseClient, - stripe: &stripe::Client, - auth_token: &str, - user_id: &str, -) -> Result> { - let profiles: Vec = supabase - .select( - "profiles", - auth_token, - "stripe_customer_id", - &[("id", &format!("eq.{}", user_id))], - ) - .await?; - - if let Some(profile) = profiles.first() - && let Some(customer_id) = &profile.stripe_customer_id - { - return Ok(Some(customer_id.clone())); - } - - let email = supabase.get_user_email(auth_token).await?; - - let metadata: HashMap = [("userId".to_string(), user_id.to_string())].into(); - - let mut create_customer = CreateCustomer::new().metadata(metadata); - - if let Some(ref email_str) = email { - create_customer = create_customer.email(email_str); - } - - let idempotency_key: stripe::IdempotencyKey = format!("create-customer-{}", user_id) - .try_into() - .map_err(|e: stripe::IdempotentKeyError| SubscriptionError::Internal(e.to_string()))?; - let start = Instant::now(); - let customer = create_customer - .customize() - .request_strategy(stripe::RequestStrategy::Idempotent(idempotency_key)) - .send(stripe) - .await - .map_err(|e: stripe::StripeError| SubscriptionError::Stripe(e.to_string()))?; - tracing::info!( - service.peer.name = "stripe", - hyprnote.stripe.operation = "create_customer", - hyprnote.duration_ms = start.elapsed().as_millis() as u64, - "stripe_request_finished" - ); - - #[derive(Serialize)] - struct UpdateData { - stripe_customer_id: String, - } - - supabase - .update( - "profiles", - auth_token, - &[ - ("id", &format!("eq.{}", user_id)), - ("stripe_customer_id", "is.null"), - ], - &UpdateData { - stripe_customer_id: customer.id.to_string(), - }, - ) - .await?; - - let updated_profiles: Vec = supabase - .select( - "profiles", - auth_token, - "stripe_customer_id", - &[("id", &format!("eq.{}", user_id))], - ) - .await?; - - Ok(updated_profiles - .first() - .and_then(|p| p.stripe_customer_id.clone())) -} - -pub(crate) async fn create_trial_subscription( - stripe: &stripe::Client, - customer_id: &str, - price_id: &str, - user_id: &str, -) -> Result<()> { - let mut item = CreateSubscriptionItems::new(); - item.price = Some(price_id.to_string()); - - let create_sub = CreateSubscription::new() - .customer(customer_id) - .items(vec![item]) - .trial_period_days(14u32) - .trial_settings(CreateSubscriptionTrialSettings::new( - CreateSubscriptionTrialSettingsEndBehavior::new( - CreateSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Cancel, - ), - )); - - let date = Utc::now().format("%Y-%m-%d").to_string(); - let idempotency_key: stripe::IdempotencyKey = format!("trial-{}-{}", user_id, date) - .try_into() - .map_err(|e: stripe::IdempotentKeyError| SubscriptionError::Internal(e.to_string()))?; - - let start = Instant::now(); - create_sub - .customize() - .request_strategy(stripe::RequestStrategy::Idempotent(idempotency_key)) - .send(stripe) - .await - .map_err(|e: stripe::StripeError| SubscriptionError::Stripe(e.to_string()))?; - tracing::info!( - service.peer.name = "stripe", - hyprnote.stripe.operation = "create_trial_subscription", - hyprnote.duration_ms = start.elapsed().as_millis() as u64, - "stripe_request_finished" - ); - - Ok(()) -} diff --git a/crates/api-subscription/src/supabase.rs b/crates/api-subscription/src/supabase.rs deleted file mode 100644 index 7cc0daca4d..0000000000 --- a/crates/api-subscription/src/supabase.rs +++ /dev/null @@ -1,403 +0,0 @@ -use reqwest::Client; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use std::time::Instant; - -use crate::error::{Result, SubscriptionError}; - -fn url_encode(s: &str) -> String { - urlencoding::encode(s).into_owned() -} - -#[derive(Clone)] -pub struct SupabaseClient { - base_url: String, - anon_key: String, - service_role_key: String, - http_client: Client, -} - -impl SupabaseClient { - pub fn new( - supabase_url: impl Into, - anon_key: impl Into, - service_role_key: impl Into, - ) -> Self { - Self { - base_url: supabase_url.into().trim_end_matches('/').to_string(), - anon_key: anon_key.into(), - service_role_key: service_role_key.into(), - http_client: Client::new(), - } - } - - fn with_trace_context(&self, builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder { - hypr_observability::with_current_trace_context(builder) - } - - pub async fn rpc Deserialize<'de>>( - &self, - function_name: &str, - auth_token: &str, - body: Option, - ) -> Result { - let url = format!("{}/rest/v1/rpc/{}", self.base_url, function_name); - - let start = Instant::now(); - let response = self - .with_trace_context( - self.http_client - .post(&url) - .header("Authorization", format!("Bearer {}", auth_token)) - .header("apikey", &self.anon_key) - .header("Content-Type", "application/json") - .json(&body.unwrap_or(Value::Object(Default::default()))), - ) - .send() - .await - .map_err(|e| SubscriptionError::SupabaseRequest(e.to_string()))?; - tracing::info!( - service.peer.name = "supabase", - hyprnote.supabase.operation = "rpc", - hyprnote.supabase.function = %function_name, - http.response.status_code = response.status().as_u16(), - hyprnote.duration_ms = start.elapsed().as_millis() as u64, - "supabase_request_finished" - ); - - if !response.status().is_success() { - let status = response.status(); - let body = response - .text() - .await - .unwrap_or_else(|_| "unknown".to_string()); - return Err(SubscriptionError::SupabaseRequest(format!( - "RPC {} failed: {} - {}", - function_name, status, body - ))); - } - - response - .json() - .await - .map_err(|e| SubscriptionError::SupabaseRequest(e.to_string())) - } - - pub async fn select Deserialize<'de>>( - &self, - table: &str, - auth_token: &str, - select: &str, - filters: &[(&str, &str)], - ) -> Result> { - let mut url = format!( - "{}/rest/v1/{}?select={}", - self.base_url, - url_encode(table), - url_encode(select) - ); - for (key, value) in filters { - url.push_str(&format!("&{}={}", url_encode(key), url_encode(value))); - } - - let start = Instant::now(); - let response = self - .with_trace_context( - self.http_client - .get(&url) - .header("Authorization", format!("Bearer {}", auth_token)) - .header("apikey", &self.anon_key), - ) - .send() - .await - .map_err(|e| SubscriptionError::SupabaseRequest(e.to_string()))?; - tracing::info!( - service.peer.name = "supabase", - hyprnote.supabase.operation = "select", - hyprnote.supabase.table = %table, - http.response.status_code = response.status().as_u16(), - hyprnote.duration_ms = start.elapsed().as_millis() as u64, - "supabase_request_finished" - ); - - if !response.status().is_success() { - let status = response.status(); - let body = response - .text() - .await - .unwrap_or_else(|_| "unknown".to_string()); - return Err(SubscriptionError::SupabaseRequest(format!( - "SELECT from {} failed: {} - {}", - table, status, body - ))); - } - - response - .json() - .await - .map_err(|e| SubscriptionError::SupabaseRequest(e.to_string())) - } - - pub async fn update( - &self, - table: &str, - auth_token: &str, - filters: &[(&str, &str)], - data: &T, - ) -> Result<()> { - let mut url = format!("{}/rest/v1/{}", self.base_url, url_encode(table)); - if !filters.is_empty() { - url.push('?'); - for (i, (key, value)) in filters.iter().enumerate() { - if i > 0 { - url.push('&'); - } - url.push_str(&format!("{}={}", url_encode(key), url_encode(value))); - } - } - - let start = Instant::now(); - let response = self - .with_trace_context( - self.http_client - .patch(&url) - .header("Authorization", format!("Bearer {}", auth_token)) - .header("apikey", &self.anon_key) - .header("Content-Type", "application/json") - .json(data), - ) - .send() - .await - .map_err(|e| SubscriptionError::SupabaseRequest(e.to_string()))?; - tracing::info!( - service.peer.name = "supabase", - hyprnote.supabase.operation = "update", - hyprnote.supabase.table = %table, - http.response.status_code = response.status().as_u16(), - hyprnote.duration_ms = start.elapsed().as_millis() as u64, - "supabase_request_finished" - ); - - if !response.status().is_success() { - let status = response.status(); - let body = response - .text() - .await - .unwrap_or_else(|_| "unknown".to_string()); - return Err(SubscriptionError::SupabaseRequest(format!( - "UPDATE {} failed: {} - {}", - table, status, body - ))); - } - - Ok(()) - } - - pub async fn admin_get_stripe_customer_id(&self, user_id: &str) -> Result> { - let url = format!( - "{}/rest/v1/profiles?select=stripe_customer_id&id=eq.{}", - self.base_url, - url_encode(user_id) - ); - - let start = Instant::now(); - let response = self - .with_trace_context( - self.http_client - .get(&url) - .header("Authorization", format!("Bearer {}", self.service_role_key)) - .header("apikey", &self.service_role_key), - ) - .send() - .await - .map_err(|e| SubscriptionError::SupabaseRequest(e.to_string()))?; - tracing::info!( - service.peer.name = "supabase", - hyprnote.supabase.operation = "admin_get_stripe_customer_id", - http.response.status_code = response.status().as_u16(), - hyprnote.duration_ms = start.elapsed().as_millis() as u64, - "supabase_request_finished" - ); - - if !response.status().is_success() { - let status = response.status(); - let body = response - .text() - .await - .unwrap_or_else(|_| "unknown".to_string()); - return Err(SubscriptionError::SupabaseRequest(format!( - "GET stripe_customer_id for {} failed: {} - {}", - user_id, status, body - ))); - } - - #[derive(Deserialize)] - struct Row { - stripe_customer_id: Option, - } - - let rows: Vec = response - .json() - .await - .map_err(|e| SubscriptionError::SupabaseRequest(e.to_string()))?; - - Ok(rows.into_iter().next().and_then(|r| r.stripe_customer_id)) - } - - pub async fn admin_delete_storage_objects(&self, bucket: &str, user_id: &str) -> Result<()> { - // List objects in the user's folder - let list_url = format!("{}/storage/v1/object/list/{}", self.base_url, bucket); - - let start = Instant::now(); - let response = self - .with_trace_context( - self.http_client - .post(&list_url) - .header("Authorization", format!("Bearer {}", self.service_role_key)) - .header("apikey", &self.service_role_key) - .json(&serde_json::json!({ - "prefix": format!("{}/", user_id), - "limit": 1000 - })), - ) - .send() - .await - .map_err(|e| SubscriptionError::SupabaseRequest(e.to_string()))?; - tracing::info!( - service.peer.name = "supabase", - hyprnote.supabase.operation = "admin_delete_storage_objects.list", - http.response.status_code = response.status().as_u16(), - hyprnote.duration_ms = start.elapsed().as_millis() as u64, - "supabase_request_finished" - ); - - if !response.status().is_success() { - // Not critical — storage may be empty - tracing::warn!( - enduser.id = %user_id, - "failed to list storage objects, skipping cleanup" - ); - return Ok(()); - } - - #[derive(Deserialize)] - struct StorageObject { - name: String, - } - - let objects: Vec = response.json().await.unwrap_or_default(); - - if objects.is_empty() { - return Ok(()); - } - - // Delete all objects in the user's folder - let delete_url = format!("{}/storage/v1/object/{}", self.base_url, bucket); - let prefixes: Vec = objects - .into_iter() - .map(|o| format!("{}/{}", user_id, o.name)) - .collect(); - - let start = Instant::now(); - let response = self - .with_trace_context( - self.http_client - .delete(&delete_url) - .header("Authorization", format!("Bearer {}", self.service_role_key)) - .header("apikey", &self.service_role_key) - .json(&serde_json::json!({ "prefixes": prefixes })), - ) - .send() - .await - .map_err(|e| SubscriptionError::SupabaseRequest(e.to_string()))?; - tracing::info!( - service.peer.name = "supabase", - hyprnote.supabase.operation = "admin_delete_storage_objects.delete", - http.response.status_code = response.status().as_u16(), - hyprnote.duration_ms = start.elapsed().as_millis() as u64, - "supabase_request_finished" - ); - - Ok(()) - } - - pub async fn admin_delete_user(&self, user_id: &str) -> Result<()> { - let url = format!( - "{}/auth/v1/admin/users/{}", - self.base_url, - url_encode(user_id) - ); - - let start = Instant::now(); - let response = self - .with_trace_context( - self.http_client - .delete(&url) - .header("Authorization", format!("Bearer {}", self.service_role_key)) - .header("apikey", &self.service_role_key), - ) - .send() - .await - .map_err(|e| SubscriptionError::SupabaseRequest(e.to_string()))?; - tracing::info!( - service.peer.name = "supabase", - hyprnote.supabase.operation = "admin_delete_user", - http.response.status_code = response.status().as_u16(), - hyprnote.duration_ms = start.elapsed().as_millis() as u64, - "supabase_request_finished" - ); - - if !response.status().is_success() { - let status = response.status(); - let body = response - .text() - .await - .unwrap_or_else(|_| "unknown".to_string()); - return Err(SubscriptionError::SupabaseRequest(format!( - "DELETE user {} failed: {} - {}", - user_id, status, body - ))); - } - - Ok(()) - } - - pub async fn get_user_email(&self, auth_token: &str) -> Result> { - let url = format!("{}/auth/v1/user", self.base_url); - - let response = self - .with_trace_context( - self.http_client - .get(&url) - .header("Authorization", format!("Bearer {}", auth_token)) - .header("apikey", &self.anon_key), - ) - .send() - .await - .map_err(|e| SubscriptionError::SupabaseRequest(e.to_string()))?; - - if !response.status().is_success() { - let status = response.status(); - let body = response - .text() - .await - .unwrap_or_else(|_| "unknown".to_string()); - return Err(SubscriptionError::SupabaseRequest(format!( - "GET user failed: {} - {}", - status, body - ))); - } - - #[derive(Deserialize)] - struct UserResponse { - email: Option, - } - - let user: UserResponse = response - .json() - .await - .map_err(|e| SubscriptionError::SupabaseRequest(e.to_string()))?; - - Ok(user.email) - } -} diff --git a/crates/api-subscription/src/trial.rs b/crates/api-subscription/src/trial.rs deleted file mode 100644 index e4f5f80567..0000000000 --- a/crates/api-subscription/src/trial.rs +++ /dev/null @@ -1,125 +0,0 @@ -use axum::{ - Json, - http::StatusCode, - response::{IntoResponse, Response}, -}; -use chrono::Utc; -use hypr_analytics::{AnalyticsPayload, PropertiesPayload, ToAnalyticsPayload}; -use serde::{Deserialize, Serialize}; -use utoipa::{IntoParams, ToSchema}; - -use hypr_api_error::error_response; - -#[derive(Debug, Deserialize, IntoParams)] -pub struct StartTrialQuery { - #[serde(default = "default_interval")] - #[param(example = "monthly")] - pub interval: Interval, -} - -fn default_interval() -> Interval { - Interval::Monthly -} - -#[derive(Debug, Deserialize, Clone, Copy, ToSchema)] -#[serde(rename_all = "lowercase")] -pub enum Interval { - Monthly, - Yearly, -} - -#[derive(Debug, Serialize, ToSchema)] -#[serde(rename_all = "snake_case")] -pub enum StartTrialReason { - Started, - NotEligible, -} - -#[derive(Debug, Serialize, ToSchema)] -pub struct StartTrialResponse { - #[schema(example = true)] - pub started: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub reason: Option, -} - -pub(crate) enum TrialOutcome { - NotEligible, - StripeError(String), - CustomerError(String), - RpcError(String), - Started(Interval), -} - -impl ToAnalyticsPayload for TrialOutcome { - fn to_analytics_payload(&self) -> AnalyticsPayload { - match self { - Self::NotEligible => AnalyticsPayload::builder("trial_skipped") - .with("reason", "not_eligible") - .build(), - Self::StripeError(_) => AnalyticsPayload::builder("trial_failed") - .with("reason", "stripe_error") - .build(), - Self::CustomerError(_) => AnalyticsPayload::builder("trial_failed") - .with("reason", "customer_error") - .build(), - Self::RpcError(_) => AnalyticsPayload::builder("trial_failed") - .with("reason", "rpc_error") - .build(), - Self::Started(interval) => { - let plan = match interval { - Interval::Monthly => "pro_monthly", - Interval::Yearly => "pro_yearly", - }; - AnalyticsPayload::builder("trial_started") - .with("plan", plan) - .build() - } - } - } - - fn to_analytics_properties(&self) -> Option { - match self { - Self::Started(_) => { - let trial_end_date = (Utc::now() + chrono::Duration::days(14)).to_rfc3339(); - Some( - PropertiesPayload::builder() - .set("plan", "trial") - .set("trial_end_date", trial_end_date) - .build(), - ) - } - _ => None, - } - } -} - -impl IntoResponse for TrialOutcome { - fn into_response(self) -> Response { - match self { - Self::NotEligible => Json(StartTrialResponse { - started: false, - reason: Some(StartTrialReason::NotEligible), - }) - .into_response(), - Self::StripeError(msg) => error_response( - StatusCode::INTERNAL_SERVER_ERROR, - "failed_to_create_subscription", - &msg, - ), - Self::CustomerError(msg) => error_response( - StatusCode::INTERNAL_SERVER_ERROR, - "failed_to_create_customer", - &msg, - ), - Self::RpcError(msg) => { - error_response(StatusCode::INTERNAL_SERVER_ERROR, "rpc_error", &msg) - } - Self::Started(_) => Json(StartTrialResponse { - started: true, - reason: Some(StartTrialReason::Started), - }) - .into_response(), - } - } -} diff --git a/crates/api-support/Cargo.toml b/crates/api-support/Cargo.toml deleted file mode 100644 index 49b4642e7f..0000000000 --- a/crates/api-support/Cargo.toml +++ /dev/null @@ -1,43 +0,0 @@ -[package] -name = "api-support" -version = "0.1.0" -edition = "2024" - -[dependencies] -hypr-api-auth = { workspace = true } -hypr-api-env = { workspace = true } -hypr-llm-proxy = { workspace = true } -hypr-mcp = { workspace = true } -hypr-openrouter = { workspace = true } -hypr-template-support = { workspace = true } -regex = { workspace = true } -reqwest = { workspace = true } -urlencoding = { workspace = true } - -utoipa = { workspace = true } - -axum = { workspace = true } -sentry = { workspace = true } -tracing = { workspace = true } - -jsonwebtoken = { workspace = true } -octocrab = "0.49" -sqlx = { workspace = true, features = ["runtime-tokio", "tls-rustls", "postgres", "json"] } - -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -strum = { workspace = true, features = ["derive"] } -thiserror = { workspace = true } - -async-stream = { workspace = true } -futures-util = { workspace = true } -rmcp = { workspace = true, features = ["server", "elicitation", "schemars"] } -tokio = { workspace = true, features = ["rt", "time", "sync"] } -tokio-tungstenite = { workspace = true } -tokio-util = { workspace = true } - -async-stripe = { workspace = true } -async-stripe-billing = { workspace = true, features = ["billing_portal_session", "subscription"] } - -[dev-dependencies] -tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/crates/api-support/src/config.rs b/crates/api-support/src/config.rs deleted file mode 100644 index 16b97c33eb..0000000000 --- a/crates/api-support/src/config.rs +++ /dev/null @@ -1,33 +0,0 @@ -use hypr_api_auth::AuthState; - -use crate::env::{GitHubAppEnv, OpenRouterEnv, StripeEnv, SupabaseEnv, SupportDatabaseEnv}; - -#[derive(Clone)] -pub struct SupportConfig { - pub github: GitHubAppEnv, - pub openrouter: OpenRouterEnv, - pub support_database: SupportDatabaseEnv, - pub stripe: StripeEnv, - pub supabase: SupabaseEnv, - pub auth: AuthState, -} - -impl SupportConfig { - pub fn new( - github: &GitHubAppEnv, - openrouter: &OpenRouterEnv, - support_database: &SupportDatabaseEnv, - stripe: &StripeEnv, - supabase: &SupabaseEnv, - auth: AuthState, - ) -> Self { - Self { - github: github.clone(), - openrouter: openrouter.clone(), - support_database: support_database.clone(), - stripe: stripe.clone(), - supabase: supabase.clone(), - auth, - } - } -} diff --git a/crates/api-support/src/env.rs b/crates/api-support/src/env.rs deleted file mode 100644 index 0a16751dd6..0000000000 --- a/crates/api-support/src/env.rs +++ /dev/null @@ -1,21 +0,0 @@ -use serde::Deserialize; - -#[derive(Clone, Deserialize)] -pub struct GitHubAppEnv { - #[serde(deserialize_with = "hypr_api_env::string_to_u64")] - pub github_bot_app_id: u64, - pub github_bot_private_key: String, - #[serde(deserialize_with = "hypr_api_env::string_to_u64")] - pub github_bot_installation_id: u64, - pub github_repo_id: String, - pub github_discussion_category_id: String, -} - -#[derive(Clone, Deserialize)] -pub struct SupportDatabaseEnv { - pub support_database_url: String, -} - -pub use hypr_api_env::OpenRouterEnv; -pub use hypr_api_env::StripeEnv; -pub use hypr_api_env::SupabaseEnv; diff --git a/crates/api-support/src/error.rs b/crates/api-support/src/error.rs deleted file mode 100644 index d2ecd87124..0000000000 --- a/crates/api-support/src/error.rs +++ /dev/null @@ -1,70 +0,0 @@ -use axum::{ - Json, - http::StatusCode, - response::{IntoResponse, Response}, -}; -use thiserror::Error; - -pub type Result = std::result::Result; - -#[derive(Debug, Error)] -pub enum SupportError { - #[error("Invalid request: {0}")] - InvalidRequest(String), - - #[error("GitHub API error: {0}")] - GitHub(String), - - #[error("Internal error: {0}")] - Internal(String), -} - -impl From for SupportError { - fn from(err: octocrab::Error) -> Self { - Self::GitHub(err.to_string()) - } -} - -impl IntoResponse for SupportError { - fn into_response(self) -> Response { - let internal_message = "Internal server error".to_string(); - - match self { - Self::InvalidRequest(message) => ( - StatusCode::BAD_REQUEST, - Json(crate::routes::FeedbackResponse { - success: false, - issue_url: None, - error: Some(message), - }), - ) - .into_response(), - Self::GitHub(message) => { - tracing::error!(error = %message, "github_error"); - sentry::capture_message(&message, sentry::Level::Error); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(crate::routes::FeedbackResponse { - success: false, - issue_url: None, - error: Some(internal_message), - }), - ) - .into_response() - } - Self::Internal(message) => { - tracing::error!(error = %message, "internal_error"); - sentry::capture_message(&message, sentry::Level::Error); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(crate::routes::FeedbackResponse { - success: false, - issue_url: None, - error: Some(internal_message), - }), - ) - .into_response() - } - } - } -} diff --git a/crates/api-support/src/github.rs b/crates/api-support/src/github.rs deleted file mode 100644 index f765b47ff1..0000000000 --- a/crates/api-support/src/github.rs +++ /dev/null @@ -1,522 +0,0 @@ -use crate::error::{Result, SupportError}; -use crate::logs; -use crate::state::AppState; -use serde::Deserialize; -use std::collections::HashMap; - -const GITHUB_OWNER: &str = "fastrepl"; -const GITHUB_REPO: &str = "char"; -const DEFAULT_ISSUE_LABELS: &[&str] = &[ - "area/backend", - "area/ui", - "Engineering", - "os/linux", - "os/macos", - "os/windows", - "product/cli", - "product/desktop", - "product/owhisper", - "product/slack-bot", - "product/web", -]; - -#[derive(Debug, Deserialize)] -struct GitHubLabel { - name: String, -} - -pub(crate) struct BugReportInput<'a> { - pub description: &'a str, - pub platform: &'a str, - pub arch: &'a str, - pub os_version: &'a str, - pub app_version: &'a str, - pub source: &'a str, - pub logs: Option<&'a str>, -} - -pub(crate) struct FeatureRequestInput<'a> { - pub description: &'a str, - pub platform: &'a str, - pub arch: &'a str, - pub os_version: &'a str, - pub app_version: &'a str, - pub source: &'a str, -} - -pub(crate) async fn submit_bug_report( - state: &AppState, - input: BugReportInput<'_>, -) -> Result { - let (description, title) = make_title(input.description, "Bug Report"); - - let body = hypr_template_support::render(hypr_template_support::SupportTemplate::BugReport( - hypr_template_support::BugReport { - description, - platform: input.platform.to_string(), - arch: input.arch.to_string(), - os_version: input.os_version.to_string(), - app_version: input.app_version.to_string(), - source: input.source.to_string(), - }, - )) - .map_err(|e| SupportError::Internal(e.to_string()))?; - - let labels = resolve_issue_labels( - state, - &title, - &body, - &[ - "product/desktop".to_string(), - platform_to_label(input.platform).to_string(), - ], - ) - .await?; - let (url, number) = create_issue(state, &title, &body, &labels, Some("Bug")).await?; - - if let Some(logs) = input.logs { - attach_log_analysis(state, number, logs).await; - } - - Ok(url) -} - -pub(crate) async fn submit_feature_request( - state: &AppState, - input: FeatureRequestInput<'_>, -) -> Result { - let (description, title) = make_title(input.description, "Feature Request"); - - let body = - hypr_template_support::render(hypr_template_support::SupportTemplate::FeatureRequest( - hypr_template_support::FeatureRequest { - description, - platform: input.platform.to_string(), - arch: input.arch.to_string(), - os_version: input.os_version.to_string(), - app_version: input.app_version.to_string(), - source: input.source.to_string(), - }, - )) - .map_err(|e| SupportError::Internal(e.to_string()))?; - - let category_id = &state.config.github.github_discussion_category_id; - if category_id.is_empty() { - return Err(SupportError::Internal( - "GitHub discussion category not configured".to_string(), - )); - } - - create_discussion(state, &title, &body, category_id).await -} - -fn make_title(description: &str, fallback: &str) -> (String, String) { - let description = description.trim().to_string(); - let first_line = description - .lines() - .next() - .unwrap_or("") - .chars() - .take(100) - .collect::(); - let title = if first_line.is_empty() { - fallback.to_string() - } else { - first_line - }; - (description, title) -} - -pub(crate) async fn get_repo_labels(state: &AppState) -> Result> { - let client = state.installation_client().await?; - let labels: Vec = client - .get( - format!("/repos/{GITHUB_OWNER}/{GITHUB_REPO}/labels?per_page=100"), - None::<&()>, - ) - .await?; - Ok(labels.into_iter().map(|label| label.name).collect()) -} - -pub(crate) async fn resolve_issue_labels( - state: &AppState, - title: &str, - body: &str, - requested_labels: &[String], -) -> Result> { - let repo_labels = match get_repo_labels(state).await { - Ok(labels) => labels, - Err(error) => { - tracing::warn!(error = %error, "failed_to_fetch_repo_labels"); - fallback_repo_labels() - } - }; - - Ok(select_issue_labels( - &repo_labels, - title, - body, - requested_labels, - )) -} - -fn fallback_repo_labels() -> Vec { - DEFAULT_ISSUE_LABELS - .iter() - .map(|label| (*label).to_string()) - .collect() -} - -fn platform_to_label(platform: &str) -> &'static str { - let platform = platform.trim().to_ascii_lowercase(); - if platform.contains("mac") { - "os/macos" - } else if platform.contains("win") { - "os/windows" - } else if platform.contains("linux") { - "os/linux" - } else { - "" - } -} - -fn select_issue_labels( - repo_labels: &[String], - title: &str, - body: &str, - requested_labels: &[String], -) -> Vec { - let repo_labels_by_key: HashMap = repo_labels - .iter() - .map(|label| (normalize_label(label), label.as_str())) - .collect(); - let mut selected = Vec::new(); - - let mut push_label = |label: &str| { - let normalized = normalize_label(label); - if let Some(existing) = repo_labels_by_key.get(&normalized) { - let existing = existing.to_string(); - if !selected.contains(&existing) { - selected.push(existing); - } - } - }; - - for label in requested_labels { - push_label(label); - } - - let text = normalize_search_text(&format!("{title}\n{body}")); - - for (needle, label) in [ - ("desktop", "product/desktop"), - ("cli", "product/cli"), - ("command line", "product/cli"), - ("terminal", "product/cli"), - ("web", "product/web"), - ("browser", "product/web"), - ("slack", "product/slack-bot"), - ("owhisper", "product/owhisper"), - ("macos", "os/macos"), - ("mac os", "os/macos"), - ("darwin", "os/macos"), - ("windows", "os/windows"), - ("linux", "os/linux"), - ] { - if contains_term(&text, needle) { - push_label(label); - } - } - - if contains_any( - &text, - &[ - "ui", "ux", "button", "dialog", "modal", "screen", "layout", "render", "sidebar", - "editor", - ], - ) { - push_label("area/ui"); - } - - if contains_any( - &text, - &[ - "api", "backend", "server", "database", "db", "auth", "sync", "webhook", "mcp", - ], - ) { - push_label("area/backend"); - } - - push_label("Engineering"); - - selected -} - -fn normalize_label(label: &str) -> String { - label.trim().to_ascii_lowercase() -} - -fn normalize_search_text(text: &str) -> String { - let mut normalized = String::with_capacity(text.len() + 2); - normalized.push(' '); - - let mut last_was_space = true; - for ch in text.chars() { - if ch.is_ascii_alphanumeric() { - normalized.push(ch.to_ascii_lowercase()); - last_was_space = false; - } else if !last_was_space { - normalized.push(' '); - last_was_space = true; - } - } - - if !last_was_space { - normalized.push(' '); - } - - normalized -} - -fn contains_term(text: &str, needle: &str) -> bool { - let needle = normalize_search_text(needle); - text.contains(&needle) -} - -fn contains_any(text: &str, needles: &[&str]) -> bool { - needles.iter().any(|needle| contains_term(text, needle)) -} - -async fn attach_log_analysis(state: &AppState, issue_number: u64, log_text: &str) { - let clean_logs = logs::strip_ansi_escapes(log_text); - - let log_summary = - logs::analyze_logs(&state.config.openrouter.openrouter_api_key, &clean_logs).await; - - let summary_section = match log_summary.as_deref() { - Some(s) if !s.trim().is_empty() => format!("### Summary\n```\n{s}\n```"), - _ => "_No errors or warnings found._".to_string(), - }; - - let tail = logs::safe_tail(&clean_logs, 10000); - let log_comment = hypr_template_support::render( - hypr_template_support::SupportTemplate::LogAnalysis(hypr_template_support::LogAnalysis { - summary_section, - tail: tail.to_string(), - }), - ) - .unwrap_or_default(); - - let _ = add_issue_comment(state, issue_number, &log_comment).await; -} - -pub(crate) async fn create_issue( - state: &AppState, - title: &str, - body: &str, - labels: &[String], - issue_type: Option<&str>, -) -> Result<(String, u64)> { - let client = state.installation_client().await?; - - let mut payload = serde_json::json!({ - "title": title, - "body": body, - "labels": labels, - }); - - if let Some(t) = issue_type { - payload["type"] = serde_json::Value::String(t.to_string()); - } - - let issue: octocrab::models::issues::Issue = client - .post( - format!("/repos/{GITHUB_OWNER}/{GITHUB_REPO}/issues"), - Some(&payload), - ) - .await?; - - Ok((issue.html_url.to_string(), issue.number)) -} - -pub(crate) async fn add_issue_comment( - state: &AppState, - issue_number: u64, - comment: &str, -) -> Result { - let client = state.installation_client().await?; - let comment = client - .issues(GITHUB_OWNER, GITHUB_REPO) - .create_comment(issue_number, comment) - .await?; - Ok(comment.html_url.to_string()) -} - -pub(crate) async fn search_issues( - state: &AppState, - query: &str, - state_filter: Option<&str>, - limit: u8, -) -> Result> { - let client = state.installation_client().await?; - - let mut q = format!("repo:{GITHUB_OWNER}/{GITHUB_REPO} is:issue {query}"); - if let Some(s) = state_filter { - match s { - "open" | "closed" => q.push_str(&format!(" is:{s}")), - _ => { - return Err(SupportError::Internal( - "Invalid state filter: must be 'open' or 'closed'".to_string(), - )); - } - } - } - - let result = client - .search() - .issues_and_pull_requests(&q) - .per_page(limit) - .send() - .await?; - - let items: Vec = result - .items - .into_iter() - .map(|issue| { - serde_json::json!({ - "number": issue.number, - "title": issue.title, - "state": format!("{:?}", issue.state).to_lowercase(), - "url": issue.html_url.to_string(), - "created_at": issue.created_at.to_rfc3339(), - "labels": issue.labels.iter().map(|l| &l.name).collect::>(), - }) - }) - .collect(); - - Ok(items) -} - -async fn create_discussion( - state: &AppState, - title: &str, - body: &str, - category_id: &str, -) -> Result { - let client = state.installation_client().await?; - - let query = serde_json::json!({ - "query": r#" - mutation($repositoryId: ID!, $categoryId: ID!, $title: String!, $body: String!) { - createDiscussion(input: { - repositoryId: $repositoryId - categoryId: $categoryId - title: $title - body: $body - }) { - discussion { - url - } - } - } - "#, - "variables": { - "repositoryId": state.config.github.github_repo_id, - "categoryId": category_id, - "title": title, - "body": body, - }, - }); - - let data: serde_json::Value = client.graphql(&query).await?; - - data["data"]["createDiscussion"]["discussion"]["url"] - .as_str() - .map(|s| s.to_string()) - .ok_or_else(|| { - SupportError::GitHub(format!( - "unexpected GraphQL response: {}", - serde_json::to_string(&data).unwrap_or_default() - )) - }) -} - -#[cfg(test)] -mod tests { - use super::{fallback_repo_labels, select_issue_labels}; - - fn repo_labels() -> Vec { - vec![ - "area/backend".to_string(), - "area/ui".to_string(), - "Engineering".to_string(), - "os/linux".to_string(), - "os/macos".to_string(), - "os/windows".to_string(), - "product/cli".to_string(), - "product/desktop".to_string(), - "product/owhisper".to_string(), - "product/slack-bot".to_string(), - "product/web".to_string(), - ] - } - - #[test] - fn selects_requested_and_inferred_labels() { - let labels = select_issue_labels( - &repo_labels(), - "Desktop UI crash on macOS", - "The desktop app crashes when opening the sidebar on macOS.", - &["product/desktop".to_string()], - ); - - assert_eq!( - labels, - vec![ - "product/desktop".to_string(), - "os/macos".to_string(), - "area/ui".to_string(), - "Engineering".to_string(), - ] - ); - } - - #[test] - fn ignores_unknown_labels_and_keeps_known_matches() { - let labels = select_issue_labels( - &repo_labels(), - "API sync fails on Linux", - "The backend webhook handler returns 500 on Linux.", - &["unknown".to_string(), "OS/Linux".to_string()], - ); - - assert_eq!( - labels, - vec![ - "os/linux".to_string(), - "area/backend".to_string(), - "Engineering".to_string(), - ] - ); - } - - #[test] - fn fallback_labels_preserve_safe_defaults() { - let labels = select_issue_labels( - &fallback_repo_labels(), - "Desktop UI crash on macOS", - "The desktop app crashes when opening the sidebar on macOS.", - &["product/desktop".to_string()], - ); - - assert_eq!( - labels, - vec![ - "product/desktop".to_string(), - "os/macos".to_string(), - "area/ui".to_string(), - "Engineering".to_string(), - ] - ); - } -} diff --git a/crates/api-support/src/lib.rs b/crates/api-support/src/lib.rs deleted file mode 100644 index ba50562871..0000000000 --- a/crates/api-support/src/lib.rs +++ /dev/null @@ -1,15 +0,0 @@ -mod config; -mod env; -mod error; -mod github; -mod logs; -mod mcp; -mod openapi; -mod redact; -mod routes; -mod state; - -pub use config::SupportConfig; -pub use env::{GitHubAppEnv, OpenRouterEnv, StripeEnv, SupabaseEnv, SupportDatabaseEnv}; -pub use openapi::openapi; -pub use routes::router; diff --git a/crates/api-support/src/logs.rs b/crates/api-support/src/logs.rs deleted file mode 100644 index 7c33c76bf9..0000000000 --- a/crates/api-support/src/logs.rs +++ /dev/null @@ -1,68 +0,0 @@ -fn strip_code_fences(s: &str) -> String { - let trimmed = s.trim(); - let stripped = trimmed - .strip_prefix("```") - .and_then(|s| { - let s = match s.find('\n') { - Some(pos) => &s[pos + 1..], - None => return None, - }; - s.strip_suffix("```") - }) - .map(|s| s.trim()) - .unwrap_or(trimmed); - stripped.to_string() -} - -pub(crate) fn strip_ansi_escapes(s: &str) -> String { - let mut result = String::with_capacity(s.len()); - let mut chars = s.chars().peekable(); - while let Some(c) = chars.next() { - if c == '\x1b' { - if chars.peek() == Some(&'[') { - chars.next(); - while let Some(&next) = chars.peek() { - chars.next(); - if next.is_ascii_alphabetic() { - break; - } - } - } - } else { - result.push(c); - } - } - result -} - -pub(crate) fn safe_tail(s: &str, max_bytes: usize) -> &str { - let start = s.len().saturating_sub(max_bytes); - let start = s - .char_indices() - .map(|(i, _)| i) - .find(|&i| i >= start) - .unwrap_or(s.len()); - &s[start..] -} - -pub(crate) async fn analyze_logs(api_key: &str, logs: &str) -> Option { - let client = hypr_openrouter::Client::new(api_key); - let tail = safe_tail(logs, 10000); - - let req = hypr_openrouter::ChatCompletionRequest { - model: Some("google/gemini-2.0-flash-001".to_string()), - max_tokens: Some(300), - messages: vec![hypr_openrouter::ChatMessage::new( - hypr_openrouter::Role::User, - format!( - "Extract only ERROR and WARNING entries from these logs. Output max 800 chars, no explanation:\n\n{tail}" - ), - )], - ..Default::default() - }; - - let resp = client.chat_completion(&req).await.ok()?; - let content = resp.choices.first()?.message.content.as_ref()?; - let text = content.as_text()?; - Some(strip_code_fences(text).chars().take(800).collect()) -} diff --git a/crates/api-support/src/mcp/mod.rs b/crates/api-support/src/mcp/mod.rs deleted file mode 100644 index cbaeb6a97f..0000000000 --- a/crates/api-support/src/mcp/mod.rs +++ /dev/null @@ -1,16 +0,0 @@ -mod prompts; -mod server; -mod tools; - -use crate::state::AppState; - -use server::SupportMcpServer; - -pub(crate) fn mcp_service( - state: AppState, -) -> rmcp::transport::streamable_http_server::StreamableHttpService< - SupportMcpServer, - rmcp::transport::streamable_http_server::session::local::LocalSessionManager, -> { - hypr_mcp::create_service(move || Ok(SupportMcpServer::new(state.clone()))) -} diff --git a/crates/api-support/src/mcp/prompts.rs b/crates/api-support/src/mcp/prompts.rs deleted file mode 100644 index da7ecd955c..0000000000 --- a/crates/api-support/src/mcp/prompts.rs +++ /dev/null @@ -1,13 +0,0 @@ -use rmcp::{ErrorData as McpError, model::*}; - -pub(crate) fn support_chat() -> Result { - hypr_template_support::render_support_chat() - .map_err(|e| McpError::internal_error(e.to_string(), None)) - .map(|content| { - GetPromptResult::new(vec![PromptMessage::new_text( - PromptMessageRole::Assistant, - content, - )]) - .with_description("System prompt for the Char support chat") - }) -} diff --git a/crates/api-support/src/mcp/server.rs b/crates/api-support/src/mcp/server.rs deleted file mode 100644 index e51934d133..0000000000 --- a/crates/api-support/src/mcp/server.rs +++ /dev/null @@ -1,208 +0,0 @@ -use rmcp::schemars::{self, JsonSchema}; -use rmcp::{ - ErrorData as McpError, RoleServer, ServerHandler, elicit_safe, - handler::server::tool::ToolRouter, handler::server::wrapper::Parameters, model::*, - service::RequestContext, tool, tool_handler, tool_router, -}; -use serde::Serialize; - -#[derive(Debug, Serialize, serde::Deserialize, JsonSchema)] -struct Confirmation { - #[schemars(description = "Set to true to confirm this action")] - confirmed: bool, -} -elicit_safe!(Confirmation); - -use crate::state::AppState; - -use hypr_mcp::McpAuth; - -use super::prompts; -use super::tools::{ - self, AddCommentParams, CreateBillingPortalSessionParams, CreateIssueParams, - ListSubscriptionsParams, SearchIssuesParams, -}; - -#[derive(Clone)] -pub(crate) struct SupportMcpServer { - state: AppState, - tool_router: ToolRouter, -} - -impl SupportMcpServer { - pub(super) fn new(state: AppState) -> Self { - Self { - state, - tool_router: Self::tool_router(), - } - } -} - -async fn require_confirmation( - context: &RequestContext, - message: impl Into, -) -> Option { - match context.peer.elicit::(message.into()).await { - Ok(Some(c)) if c.confirmed => None, - _ => Some(CallToolResult::success(vec![Content::text( - "Action cancelled by user.", - )])), - } -} - -#[tool_router] -impl SupportMcpServer { - #[tool( - description = "Create a new GitHub issue for a bug report or feature request. Always search_issues first to avoid duplicates. Include reproduction steps for bugs and a clear rationale for feature requests.", - annotations( - read_only_hint = false, - destructive_hint = true, - idempotent_hint = false, - open_world_hint = true - ) - )] - async fn create_issue( - &self, - context: RequestContext, - Parameters(params): Parameters, - ) -> Result { - if let Some(cancelled) = require_confirmation( - &context, - format!("Create GitHub issue: \"{}\"?", params.title), - ) - .await - { - return Ok(cancelled); - } - tools::create_issue(&self.state, params).await - } - - #[tool( - description = "Add a comment to an existing GitHub issue. Use this to append user-reported details, reproduction steps, or environment info to an already-tracked issue.", - annotations( - read_only_hint = false, - destructive_hint = true, - idempotent_hint = false, - open_world_hint = true - ) - )] - async fn add_comment( - &self, - context: RequestContext, - Parameters(params): Parameters, - ) -> Result { - if let Some(cancelled) = require_confirmation( - &context, - format!("Add comment to issue #{}?", params.issue_number), - ) - .await - { - return Ok(cancelled); - } - tools::add_comment(&self.state, params).await - } - - #[tool( - description = "Search GitHub issues by keywords or error messages. Use this before creating a new issue to check for duplicates. Try broad terms first, then narrow down.", - annotations( - read_only_hint = true, - destructive_hint = false, - open_world_hint = true - ) - )] - async fn search_issues( - &self, - Parameters(params): Parameters, - ) -> Result { - tools::search_issues(&self.state, params).await - } - - #[tool( - description = "Create a Stripe billing portal session URL. The user can visit this URL to manage their subscription, update payment methods, or cancel. Only use when the user explicitly wants to manage billing.", - annotations( - read_only_hint = false, - destructive_hint = false, - idempotent_hint = false, - open_world_hint = true - ) - )] - async fn create_billing_portal_session( - &self, - McpAuth(auth): McpAuth, - Parameters(params): Parameters, - ) -> Result { - let auth = auth.ok_or_else(|| { - McpError::invalid_request("Sign in to manage your subscription", None) - })?; - tools::create_billing_portal_session(&self.state, &auth, params).await - } - - #[tool( - description = "List the user's Stripe subscriptions with plan details and status. Use this to answer billing questions like current plan, trial status, or cancellation state before suggesting further actions.", - annotations( - read_only_hint = true, - destructive_hint = false, - open_world_hint = true - ) - )] - async fn list_subscriptions( - &self, - McpAuth(auth): McpAuth, - Parameters(params): Parameters, - ) -> Result { - let auth = auth.ok_or_else(|| { - McpError::invalid_request("Sign in to manage your subscription", None) - })?; - tools::list_subscriptions(&self.state, &auth, params).await - } -} - -#[tool_handler] -impl ServerHandler for SupportMcpServer { - fn get_info(&self) -> ServerInfo { - ServerInfo::new( - ServerCapabilities::builder() - .enable_tools() - .enable_prompts() - .build(), - ) - .with_protocol_version(ProtocolVersion::LATEST) - .with_server_info(Implementation::new( - "char-support", - env!("CARGO_PKG_VERSION"), - )) - .with_instructions( - "Char support server. Provides tools for GitHub issue management (search, create, comment) and Stripe billing operations (list subscriptions, billing portal). Always search before creating issues to avoid duplicates.", - ) - } - - async fn list_prompts( - &self, - _params: Option, - _context: RequestContext, - ) -> Result { - Ok(ListPromptsResult { - prompts: vec![Prompt::new( - "support_chat", - Some("System prompt for the Char support chat"), - None::>, - )], - next_cursor: None, - meta: None, - }) - } - - async fn get_prompt( - &self, - params: GetPromptRequestParams, - _context: RequestContext, - ) -> Result { - match params.name.as_str() { - "support_chat" => prompts::support_chat(), - _ => Err(McpError::invalid_params( - format!("Unknown prompt: {}", params.name), - None, - )), - } - } -} diff --git a/crates/api-support/src/mcp/tools/add_comment.rs b/crates/api-support/src/mcp/tools/add_comment.rs deleted file mode 100644 index 6dd9fe6a68..0000000000 --- a/crates/api-support/src/mcp/tools/add_comment.rs +++ /dev/null @@ -1,39 +0,0 @@ -use rmcp::{ - ErrorData as McpError, - model::*, - schemars::{self, JsonSchema}, -}; -use serde::Deserialize; - -use crate::github; -use crate::redact; -use crate::state::AppState; - -#[derive(Debug, Deserialize, JsonSchema)] -pub(crate) struct AddCommentParams { - #[schemars(description = "The GitHub issue number (from search_issues results)")] - pub issue_number: u64, - #[schemars( - description = "Comment body in markdown. Include any new details: reproduction steps, environment info, or user-reported symptoms that add context to the existing issue." - )] - pub body: String, -} - -pub(crate) async fn add_comment( - state: &AppState, - params: AddCommentParams, -) -> Result { - let body = redact::redact_pii(¶ms.body); - - let url = github::add_issue_comment(state, params.issue_number, &body) - .await - .map_err(|e| McpError::internal_error(e.to_string(), None))?; - - Ok(CallToolResult::success(vec![Content::text( - serde_json::json!({ - "success": true, - "comment_url": url, - }) - .to_string(), - )])) -} diff --git a/crates/api-support/src/mcp/tools/create_billing_portal_session.rs b/crates/api-support/src/mcp/tools/create_billing_portal_session.rs deleted file mode 100644 index 002817539a..0000000000 --- a/crates/api-support/src/mcp/tools/create_billing_portal_session.rs +++ /dev/null @@ -1,45 +0,0 @@ -use hypr_api_auth::AuthContext; -use rmcp::{ - ErrorData as McpError, - model::*, - schemars::{self, JsonSchema}, -}; -use serde::Deserialize; -use stripe_billing::billing_portal_session::CreateBillingPortalSession; - -use crate::state::AppState; - -#[derive(Debug, Deserialize, JsonSchema)] -pub(crate) struct CreateBillingPortalSessionParams { - #[schemars( - description = "URL to redirect the user back to after they finish in the billing portal. Can be omitted." - )] - pub return_url: Option, -} - -pub(crate) async fn create_billing_portal_session( - state: &AppState, - auth: &AuthContext, - params: CreateBillingPortalSessionParams, -) -> Result { - let customer_id = state - .get_stripe_customer_id(auth) - .await - .map_err(|e| McpError::internal_error(e, None))? - .ok_or_else(|| { - McpError::invalid_request("No Stripe customer found for this account", None) - })?; - - let mut req = CreateBillingPortalSession::new().customer(&customer_id); - if let Some(url) = params.return_url { - req = req.return_url(url); - } - let session = req - .send(&state.stripe) - .await - .map_err(|e| McpError::internal_error(e.to_string(), None))?; - - Ok(CallToolResult::success(vec![Content::text( - serde_json::json!({ "url": session.url }).to_string(), - )])) -} diff --git a/crates/api-support/src/mcp/tools/create_issue.rs b/crates/api-support/src/mcp/tools/create_issue.rs deleted file mode 100644 index d9f94937cb..0000000000 --- a/crates/api-support/src/mcp/tools/create_issue.rs +++ /dev/null @@ -1,60 +0,0 @@ -use rmcp::{ - ErrorData as McpError, - model::*, - schemars::{self, JsonSchema}, -}; -use serde::Deserialize; - -use crate::github; -use crate::redact; -use crate::state::AppState; - -#[derive(Debug, Deserialize, JsonSchema)] -pub(crate) struct CreateIssueParams { - #[schemars(description = "Concise issue title, e.g. 'Transcription stops after 30 minutes'")] - pub title: String, - #[schemars( - description = "Issue body in markdown. For bugs: include steps to reproduce, expected vs actual behavior, and environment info. For features: describe the use case and desired behavior." - )] - pub body: String, - #[schemars( - description = "The issue type. Use 'Bug' for bugs, 'Feature' for feature requests." - )] - pub issue_type: Option, - #[schemars( - description = "Optional GitHub labels to apply. Use existing repository labels when known; the server will validate them against the live repo labels and auto-select additional matching labels." - )] - pub labels: Option>, -} - -pub(crate) async fn create_issue( - state: &AppState, - params: CreateIssueParams, -) -> Result { - let labels = github::resolve_issue_labels( - state, - ¶ms.title, - ¶ms.body, - params.labels.as_deref().unwrap_or(&[]), - ) - .await - .map_err(|e| McpError::internal_error(e.to_string(), None))?; - - let title = redact::redact_pii(¶ms.title); - let body = redact::redact_pii(¶ms.body); - - let (url, number) = - github::create_issue(state, &title, &body, &labels, params.issue_type.as_deref()) - .await - .map_err(|e| McpError::internal_error(e.to_string(), None))?; - - Ok(CallToolResult::success(vec![Content::text( - serde_json::json!({ - "success": true, - "issue_url": url, - "issue_number": number, - "labels": labels, - }) - .to_string(), - )])) -} diff --git a/crates/api-support/src/mcp/tools/list_subscriptions.rs b/crates/api-support/src/mcp/tools/list_subscriptions.rs deleted file mode 100644 index 9ad664fdc8..0000000000 --- a/crates/api-support/src/mcp/tools/list_subscriptions.rs +++ /dev/null @@ -1,67 +0,0 @@ -use hypr_api_auth::AuthContext; -use rmcp::{ - ErrorData as McpError, - model::*, - schemars::{self, JsonSchema}, -}; -use serde::Deserialize; -use stripe_billing::subscription::{ListSubscription, ListSubscriptionStatus}; - -use crate::state::AppState; - -#[derive(Debug, Deserialize, JsonSchema)] -pub(crate) struct ListSubscriptionsParams { - #[schemars( - description = "Filter by subscription status. Values: 'active', 'canceled', 'ended', 'past_due', 'trialing', 'unpaid', or 'all'. Omit to return only active subscriptions." - )] - pub status: Option, -} - -pub(crate) async fn list_subscriptions( - state: &AppState, - auth: &AuthContext, - params: ListSubscriptionsParams, -) -> Result { - let customer_id = state - .get_stripe_customer_id(auth) - .await - .map_err(|e| McpError::internal_error(e, None))? - .ok_or_else(|| { - McpError::invalid_request("No Stripe customer found for this account", None) - })?; - - let mut req = ListSubscription::new().customer(&customer_id); - if let Some(status) = params.status { - let parsed: ListSubscriptionStatus = status - .parse() - .map_err(|_| McpError::invalid_params(format!("Invalid status: {status}"), None))?; - req = req.status(parsed); - } - - let list = req - .send(&state.stripe) - .await - .map_err(|e| McpError::internal_error(e.to_string(), None))?; - - let subscriptions: Vec = list - .data - .iter() - .map(|sub| { - serde_json::json!({ - "id": sub.id.as_str(), - "status": format!("{:?}", sub.status), - "start_date": sub.start_date, - "cancel_at_period_end": sub.cancel_at_period_end, - "cancel_at": sub.cancel_at, - "canceled_at": sub.canceled_at, - "trial_start": sub.trial_start, - "trial_end": sub.trial_end, - }) - }) - .collect(); - - Ok(CallToolResult::success(vec![Content::text( - serde_json::to_string(&subscriptions) - .map_err(|e| McpError::internal_error(e.to_string(), None))?, - )])) -} diff --git a/crates/api-support/src/mcp/tools/mod.rs b/crates/api-support/src/mcp/tools/mod.rs deleted file mode 100644 index ef49abe0c4..0000000000 --- a/crates/api-support/src/mcp/tools/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -mod add_comment; -mod create_billing_portal_session; -mod create_issue; -mod list_subscriptions; -mod search_issues; - -pub(crate) use add_comment::{AddCommentParams, add_comment}; -pub(crate) use create_billing_portal_session::{ - CreateBillingPortalSessionParams, create_billing_portal_session, -}; -pub(crate) use create_issue::{CreateIssueParams, create_issue}; -pub(crate) use list_subscriptions::{ListSubscriptionsParams, list_subscriptions}; -pub(crate) use search_issues::{SearchIssuesParams, search_issues}; diff --git a/crates/api-support/src/mcp/tools/search_issues.rs b/crates/api-support/src/mcp/tools/search_issues.rs deleted file mode 100644 index bad6694f10..0000000000 --- a/crates/api-support/src/mcp/tools/search_issues.rs +++ /dev/null @@ -1,44 +0,0 @@ -use rmcp::{ - ErrorData as McpError, - model::*, - schemars::{self, JsonSchema}, -}; -use serde::Deserialize; - -use crate::github; -use crate::state::AppState; - -#[derive(Debug, Deserialize, JsonSchema)] -pub(crate) struct SearchIssuesParams { - #[schemars( - description = "Search query: use keywords from the user's problem description, error messages, or feature area (e.g. 'transcription audio', 'calendar sync')" - )] - pub query: String, - #[schemars( - description = "Filter by state: 'open' or 'closed'. Defaults to 'open' if omitted." - )] - pub state: Option, - #[schemars( - description = "Maximum number of results (default: 20, max: 100). Use a small limit like 5-10 for quick checks." - )] - pub limit: Option, -} - -pub(crate) async fn search_issues( - state: &AppState, - params: SearchIssuesParams, -) -> Result { - let limit = params.limit.unwrap_or(20).min(100); - - let items = github::search_issues(state, ¶ms.query, params.state.as_deref(), limit) - .await - .map_err(|e| McpError::internal_error(e.to_string(), None))?; - - Ok(CallToolResult::success(vec![Content::text( - serde_json::json!({ - "total_results": items.len(), - "issues": items, - }) - .to_string(), - )])) -} diff --git a/crates/api-support/src/openapi.rs b/crates/api-support/src/openapi.rs deleted file mode 100644 index b737995728..0000000000 --- a/crates/api-support/src/openapi.rs +++ /dev/null @@ -1,26 +0,0 @@ -use utoipa::OpenApi; - -use crate::routes::{FeedbackRequest, FeedbackResponse}; - -#[derive(OpenApi)] -#[openapi( - paths( - crate::routes::feedback::submit, - ), - components( - schemas( - FeedbackRequest, - FeedbackResponse, - crate::routes::feedback::FeedbackType, - crate::routes::feedback::DeviceInfo, - ) - ), - tags( - (name = "support", description = "User feedback and support") - ) -)] -struct ApiDoc; - -pub fn openapi() -> utoipa::openapi::OpenApi { - ApiDoc::openapi() -} diff --git a/crates/api-support/src/redact.rs b/crates/api-support/src/redact.rs deleted file mode 100644 index a39490ad7f..0000000000 --- a/crates/api-support/src/redact.rs +++ /dev/null @@ -1,62 +0,0 @@ -use regex::Regex; -use std::sync::LazyLock; - -static EMAIL_RE: LazyLock = - LazyLock::new(|| Regex::new(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}").unwrap()); - -static STRIPE_CUSTOMER_RE: LazyLock = - LazyLock::new(|| Regex::new(r"cus_[a-zA-Z0-9]{14,}").unwrap()); - -static UUID_RE: LazyLock = LazyLock::new(|| { - Regex::new(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}") - .unwrap() -}); - -pub(crate) fn redact_pii(text: &str) -> String { - let text = EMAIL_RE.replace_all(text, "[email redacted]"); - let text = STRIPE_CUSTOMER_RE.replace_all(&text, "[stripe-id redacted]"); - let text = UUID_RE.replace_all(&text, "[id redacted]"); - text.into_owned() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn redacts_email() { - assert_eq!( - redact_pii("Contact user@example.com for details"), - "Contact [email redacted] for details" - ); - } - - #[test] - fn redacts_stripe_customer_id() { - assert_eq!( - redact_pii("Customer cus_OaBC1234567890 reported"), - "Customer [stripe-id redacted] reported" - ); - } - - #[test] - fn redacts_uuid() { - assert_eq!( - redact_pii("User 550e8400-e29b-41d4-a716-446655440000 said"), - "User [id redacted] said" - ); - } - - #[test] - fn redacts_multiple_pii_types() { - let input = "User 550e8400-e29b-41d4-a716-446655440000 (john@test.com, cus_OaBC1234567890) reported a bug"; - let expected = "User [id redacted] ([email redacted], [stripe-id redacted]) reported a bug"; - assert_eq!(redact_pii(input), expected); - } - - #[test] - fn leaves_non_pii_unchanged() { - let input = "App crashes on macOS 15.1 when clicking record button"; - assert_eq!(redact_pii(input), input); - } -} diff --git a/crates/api-support/src/routes/feedback.rs b/crates/api-support/src/routes/feedback.rs deleted file mode 100644 index fb3fd90698..0000000000 --- a/crates/api-support/src/routes/feedback.rs +++ /dev/null @@ -1,99 +0,0 @@ -use axum::{Json, extract::State}; -use serde::{Deserialize, Serialize}; - -pub use hypr_template_support::DeviceInfo; - -use crate::error::SupportError; -use crate::github::{self, BugReportInput, FeatureRequestInput}; -use crate::state::AppState; - -#[derive(Debug, Default, Deserialize, utoipa::ToSchema)] -#[serde(rename_all = "lowercase")] -pub enum FeedbackType { - #[default] - Bug, - Feature, -} - -#[derive(Debug, Deserialize, utoipa::ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct FeedbackRequest { - #[serde(default)] - pub r#type: FeedbackType, - pub description: String, - pub logs: Option, - pub device_info: DeviceInfo, -} - -#[derive(Debug, Serialize, utoipa::ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct FeedbackResponse { - pub success: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub issue_url: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub error: Option, -} - -const SOURCE: &str = "from the Char Desktop app"; - -#[utoipa::path( - post, - path = "/feedback/submit", - request_body = FeedbackRequest, - responses( - (status = 200, description = "Feedback submitted successfully", body = FeedbackResponse), - (status = 400, description = "Invalid request", body = FeedbackResponse), - (status = 500, description = "Server error", body = FeedbackResponse), - ), - tag = "feedback", -)] -pub async fn submit( - State(state): State, - Json(payload): Json, -) -> std::result::Result, SupportError> { - if payload.description.trim().len() < 10 { - return Err(SupportError::InvalidRequest( - "description must be at least 10 characters".into(), - )); - } - let di = &payload.device_info; - - let url = match payload.r#type { - FeedbackType::Bug => { - github::submit_bug_report( - &state, - BugReportInput { - description: &payload.description, - platform: &di.platform, - arch: &di.arch, - os_version: &di.os_version, - app_version: &di.app_version, - source: SOURCE, - logs: payload.logs.as_deref(), - }, - ) - .await? - } - FeedbackType::Feature => { - github::submit_feature_request( - &state, - FeatureRequestInput { - description: &payload.description, - platform: &di.platform, - arch: &di.arch, - os_version: &di.os_version, - app_version: &di.app_version, - source: SOURCE, - }, - ) - .await? - } - }; - - Ok(Json(FeedbackResponse { - success: true, - issue_url: Some(url), - error: None, - })) -} diff --git a/crates/api-support/src/routes/mod.rs b/crates/api-support/src/routes/mod.rs deleted file mode 100644 index 381e9b5048..0000000000 --- a/crates/api-support/src/routes/mod.rs +++ /dev/null @@ -1,42 +0,0 @@ -pub(crate) mod feedback; - -use axum::{Router, routing::post}; - -use crate::config::SupportConfig; -use crate::mcp::mcp_service; -use crate::state::AppState; - -pub use feedback::{FeedbackRequest, FeedbackResponse}; - -pub async fn router(config: SupportConfig) -> Router { - let resolver = hypr_llm_proxy::StaticModelResolver::default() - .with_models( - hypr_llm_proxy::MODEL_KEY_DEFAULT, - vec![ - "openai/gpt-oss-120b".into(), - "moonshotai/kimi-k2-0905".into(), - ], - ) - .with_models( - hypr_llm_proxy::MODEL_KEY_TOOL_CALLING, - vec![ - "anthropic/claude-haiku-4.5".into(), - "moonshotai/kimi-k2-0905:exacto".into(), - ], - ); - let llm_config = hypr_llm_proxy::LlmProxyConfig::new(&config.openrouter) - .with_model_resolver(std::sync::Arc::new(resolver)); - let llm_router = hypr_llm_proxy::router(llm_config); - - let state = AppState::new(config).await; - let mcp = mcp_service(state.clone()); - - Router::new() - .nest( - "/feedback", - Router::new().route("/submit", post(feedback::submit)), - ) - .nest("/support", Router::new().nest_service("/mcp", mcp)) - .nest_service("/support/llm", llm_router) - .with_state(state) -} diff --git a/crates/api-support/src/state.rs b/crates/api-support/src/state.rs deleted file mode 100644 index d9fd47f435..0000000000 --- a/crates/api-support/src/state.rs +++ /dev/null @@ -1,96 +0,0 @@ -use hypr_api_auth::{AuthContext, AuthState}; -use octocrab::Octocrab; -use reqwest::Client as HttpClient; -use serde::Deserialize; -use sqlx::PgPool; -use stripe::Client as StripeClient; - -use crate::config::SupportConfig; - -#[derive(Clone)] -pub(crate) struct AppState { - pub(crate) config: SupportConfig, - pub(crate) octocrab: Octocrab, - pub(crate) _db_pool: PgPool, - pub(crate) stripe: StripeClient, - pub(crate) _auth: AuthState, - pub(crate) http_client: HttpClient, -} - -impl AppState { - pub(crate) async fn new(config: SupportConfig) -> Self { - let key = { - let pem = config.github.github_bot_private_key.replace("\\n", "\n"); - let pem = if pem.starts_with("-----") { - pem - } else { - format!("-----BEGIN RSA PRIVATE KEY-----\n{pem}\n-----END RSA PRIVATE KEY-----") - }; - jsonwebtoken::EncodingKey::from_rsa_pem(pem.as_bytes()) - .expect("invalid GitHub App private key") - }; - - let octocrab = Octocrab::builder() - .app(config.github.github_bot_app_id.into(), key) - .build() - .expect("failed to build octocrab client"); - - let db_pool = PgPool::connect(&config.support_database.support_database_url) - .await - .expect("failed to connect to Supabase Postgres"); - - let stripe = StripeClient::new(&config.stripe.stripe_secret_key); - - let auth = config.auth.clone(); - - Self { - config, - octocrab, - _db_pool: db_pool, - stripe, - _auth: auth, - http_client: HttpClient::new(), - } - } - - pub(crate) async fn installation_client(&self) -> Result { - self.octocrab - .installation(self.config.github.github_bot_installation_id.into()) - } - - pub(crate) async fn get_stripe_customer_id( - &self, - auth: &AuthContext, - ) -> Result, String> { - #[derive(Deserialize)] - struct Profile { - stripe_customer_id: Option, - } - - let url = format!( - "{}/rest/v1/profiles?select={}&id=eq.{}", - self.config.supabase.supabase_url, - "stripe_customer_id", - urlencoding::encode(&auth.claims.sub), - ); - - let response = self - .http_client - .get(&url) - .header("Authorization", format!("Bearer {}", auth.token)) - .header("apikey", &self.config.supabase.supabase_anon_key) - .send() - .await - .map_err(|e| e.to_string())?; - - if !response.status().is_success() { - let status = response.status(); - let body = response.text().await.unwrap_or_default(); - return Err(format!("profiles query failed: {} - {}", status, body)); - } - - let profiles: Vec = response.json().await.map_err(|e| e.to_string())?; - - Ok(profiles.first().and_then(|p| p.stripe_customer_id.clone())) - } -} diff --git a/crates/api-sync/Cargo.toml b/crates/api-sync/Cargo.toml deleted file mode 100644 index b6ce7bacd5..0000000000 --- a/crates/api-sync/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "api-sync" -version = "0.1.0" -edition = "2024" - -[dependencies] -hypr-api-error = { workspace = true } -hypr-supabase-auth = { workspace = true, features = ["server"] } - -utoipa = { workspace = true } - -axum = { workspace = true } -reqwest = { workspace = true, features = ["json"] } -sentry = { workspace = true } -tokio = { workspace = true } -tracing = { workspace = true } - -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -thiserror = { workspace = true } diff --git a/crates/api-sync/src/config.rs b/crates/api-sync/src/config.rs deleted file mode 100644 index 562d9f3490..0000000000 --- a/crates/api-sync/src/config.rs +++ /dev/null @@ -1,23 +0,0 @@ -use std::sync::Arc; - -#[derive(Clone)] -pub struct SyncConfig { - pub supabase_url: String, - pub supabase_anon_key: String, - pub auth: Option>, -} - -impl SyncConfig { - pub fn new(supabase_url: impl Into, supabase_anon_key: impl Into) -> Self { - Self { - supabase_url: supabase_url.into(), - supabase_anon_key: supabase_anon_key.into(), - auth: None, - } - } - - pub fn with_auth(mut self, auth: Arc) -> Self { - self.auth = Some(auth); - self - } -} diff --git a/crates/api-sync/src/error.rs b/crates/api-sync/src/error.rs deleted file mode 100644 index 20d948f06a..0000000000 --- a/crates/api-sync/src/error.rs +++ /dev/null @@ -1,41 +0,0 @@ -use axum::{ - http::StatusCode, - response::{IntoResponse, Response}, -}; -use thiserror::Error; - -pub type Result = std::result::Result; - -#[derive(Debug, Error)] -pub enum SyncError { - #[error("Authentication error: {0}")] - Auth(String), - - #[error("Invalid request: {0}")] - BadRequest(String), - - #[error("Internal error: {0}")] - Internal(String), -} - -impl From for SyncError { - fn from(err: hypr_supabase_auth::server::Error) -> Self { - Self::Auth(err.to_string()) - } -} - -impl IntoResponse for SyncError { - fn into_response(self) -> Response { - let (status, code, message) = match self { - Self::Auth(message) => (StatusCode::UNAUTHORIZED, "unauthorized", message), - Self::BadRequest(message) => (StatusCode::BAD_REQUEST, "bad_request", message), - Self::Internal(message) => ( - StatusCode::INTERNAL_SERVER_ERROR, - "internal_server_error", - message, - ), - }; - - hypr_api_error::error_response(status, code, &message) - } -} diff --git a/crates/api-sync/src/lib.rs b/crates/api-sync/src/lib.rs deleted file mode 100644 index bfe4393fcc..0000000000 --- a/crates/api-sync/src/lib.rs +++ /dev/null @@ -1,9 +0,0 @@ -mod config; -mod error; -mod routes; -mod state; - -pub use config::SyncConfig; -pub use error::{Result, SyncError}; -pub use routes::{openapi, router}; -pub use state::AppState; diff --git a/crates/api-sync/src/routes/mod.rs b/crates/api-sync/src/routes/mod.rs deleted file mode 100644 index 26447a54f2..0000000000 --- a/crates/api-sync/src/routes/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -use axum::Router; -use utoipa::OpenApi; - -use crate::state::AppState; - -#[derive(OpenApi)] -#[openapi( - tags( - (name = "sync", description = "Sync management") - ) -)] -pub struct ApiDoc; - -pub fn openapi() -> utoipa::openapi::OpenApi { - ApiDoc::openapi() -} - -pub fn router(state: AppState) -> Router { - Router::new().with_state(state) -} diff --git a/crates/api-sync/src/state.rs b/crates/api-sync/src/state.rs deleted file mode 100644 index 8c8768ac0f..0000000000 --- a/crates/api-sync/src/state.rs +++ /dev/null @@ -1,12 +0,0 @@ -use crate::config::SyncConfig; - -#[derive(Clone)] -pub struct AppState { - pub config: SyncConfig, -} - -impl AppState { - pub fn new(config: SyncConfig) -> Self { - Self { config } - } -} diff --git a/crates/api-ticket/Cargo.toml b/crates/api-ticket/Cargo.toml deleted file mode 100644 index ef65e2eec4..0000000000 --- a/crates/api-ticket/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "api-ticket" -version = "0.1.0" -edition = "2024" - -[dependencies] -hypr-api-auth = { workspace = true } -hypr-api-error = { workspace = true } -hypr-api-nango = { workspace = true } -hypr-github-issues = { workspace = true, features = ["utoipa"] } -hypr-linear = { workspace = true } -hypr-nango = { workspace = true } -hypr-ticket-interface = { workspace = true, features = ["utoipa"] } - -axum = { workspace = true } -utoipa = { workspace = true } - -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -thiserror = { workspace = true } diff --git a/crates/api-ticket/src/error.rs b/crates/api-ticket/src/error.rs deleted file mode 100644 index f925c2ae01..0000000000 --- a/crates/api-ticket/src/error.rs +++ /dev/null @@ -1,41 +0,0 @@ -use axum::{ - http::StatusCode, - response::{IntoResponse, Response}, -}; -use thiserror::Error; - -pub type Result = std::result::Result; - -#[derive(Debug, Error)] -pub enum TicketError { - #[error("Authentication error: {0}")] - #[allow(dead_code)] - Auth(String), - - #[error("Invalid request: {0}")] - #[allow(dead_code)] - BadRequest(String), - - #[error("Internal error: {0}")] - Internal(String), - - #[error(transparent)] - NangoConnection(#[from] hypr_api_nango::NangoConnectionError), -} - -impl IntoResponse for TicketError { - fn into_response(self) -> Response { - let (status, code, message) = match self { - Self::Auth(message) => (StatusCode::UNAUTHORIZED, "unauthorized", message), - Self::BadRequest(message) => (StatusCode::BAD_REQUEST, "bad_request", message), - Self::Internal(message) => ( - StatusCode::INTERNAL_SERVER_ERROR, - "internal_server_error", - message, - ), - Self::NangoConnection(err) => return err.into_response(), - }; - - hypr_api_error::error_response(status, code, &message) - } -} diff --git a/crates/api-ticket/src/github/AGENTS.md b/crates/api-ticket/src/github/AGENTS.md deleted file mode 100644 index 933a1754ef..0000000000 --- a/crates/api-ticket/src/github/AGENTS.md +++ /dev/null @@ -1,3 +0,0 @@ -- (O) https://nango.dev/docs/api-integrations/github.md -- (X) https://nango.dev/docs/api-integrations/github-app.md -- (X) https://nango.dev/docs/api-integrations/github-app-oauth.md diff --git a/crates/api-ticket/src/github/mod.rs b/crates/api-ticket/src/github/mod.rs deleted file mode 100644 index 6b6046a85d..0000000000 --- a/crates/api-ticket/src/github/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub(crate) mod routes; - -use axum::{Router, routing::post}; - -pub fn router() -> Router { - Router::new() - .route("/list-repos", post(routes::list_repos)) - .route("/list-tickets", post(routes::list_tickets)) -} diff --git a/crates/api-ticket/src/github/routes.rs b/crates/api-ticket/src/github/routes.rs deleted file mode 100644 index f39569254b..0000000000 --- a/crates/api-ticket/src/github/routes.rs +++ /dev/null @@ -1,173 +0,0 @@ -use axum::{Extension, Json}; -use hypr_api_auth::AuthContext; -use hypr_api_nango::{GitHub, NangoConnectionState, NangoIntegrationId}; -use hypr_github_issues::{GitHubIssuesClient, IssueStateFilter}; -use hypr_ticket_interface::{CollectionPage, CollectionRef, TicketPage}; -use serde::Deserialize; -use utoipa::ToSchema; - -use crate::error::{Result, TicketError}; -use crate::normalize::github_issue_to_ticket; - -#[derive(Debug, Deserialize, ToSchema)] -pub struct GitHubListReposRequest { - pub connection_id: String, - #[serde(default)] - pub limit: Option, - #[serde(default)] - pub cursor: Option, -} - -#[derive(Debug, Deserialize, ToSchema)] -pub struct GitHubListTicketsRequest { - pub connection_id: String, - pub owner: String, - pub repo: String, - #[serde(default)] - pub state: Option, - #[serde(default)] - pub labels: Option>, - #[serde(default)] - pub limit: Option, - #[serde(default)] - pub cursor: Option, -} - -#[utoipa::path( - post, - path = "/github/list-repos", - operation_id = "github_list_repos", - request_body = GitHubListReposRequest, - responses( - (status = 200, description = "GitHub repositories fetched", body = CollectionPage), - (status = 401, description = "Unauthorized"), - (status = 500, description = "Internal server error"), - ), - tag = "ticket", -)] -pub async fn list_repos( - Extension(auth): Extension, - Extension(nango_state): Extension, - Json(req): Json, -) -> Result> { - let http = nango_state - .build_http_client( - &auth.token, - &auth.claims.sub, - GitHub::ID, - &req.connection_id, - ) - .await?; - - let client = GitHubIssuesClient::new(http); - - let page: u32 = req - .cursor - .as_deref() - .and_then(|c| c.parse().ok()) - .unwrap_or(1); - let per_page = req.limit.unwrap_or(30); - - let repos = client - .list_repos(hypr_github_issues::ListReposRequest { - per_page: Some(per_page), - page: Some(page), - sort: Some("updated".to_string()), - }) - .await - .map_err(|e| TicketError::Internal(e.to_string()))?; - - let next_cursor = if repos.len() as u32 >= per_page { - Some((page + 1).to_string()) - } else { - None - }; - - let items = repos - .into_iter() - .map(|r| CollectionRef { - id: r.id.to_string(), - name: r.full_name.clone(), - key: Some(r.full_name), - url: Some(r.html_url), - }) - .collect(); - - Ok(Json(CollectionPage { items, next_cursor })) -} - -#[utoipa::path( - post, - path = "/github/list-tickets", - operation_id = "github_list_tickets", - request_body = GitHubListTicketsRequest, - responses( - (status = 200, description = "GitHub tickets fetched", body = TicketPage), - (status = 401, description = "Unauthorized"), - (status = 500, description = "Internal server error"), - ), - tag = "ticket", -)] -pub async fn list_tickets( - Extension(auth): Extension, - Extension(nango_state): Extension, - Json(req): Json, -) -> Result> { - let http = nango_state - .build_http_client( - &auth.token, - &auth.claims.sub, - GitHub::ID, - &req.connection_id, - ) - .await?; - - let client = GitHubIssuesClient::new(http); - - let page: u32 = req - .cursor - .as_deref() - .and_then(|c| c.parse().ok()) - .unwrap_or(1); - let per_page = req.limit.unwrap_or(30); - - let state_filter = match req.state.as_deref() { - Some("open") => Some(IssueStateFilter::Open), - Some("closed") => Some(IssueStateFilter::Closed), - Some("all") => Some(IssueStateFilter::All), - _ => Some(IssueStateFilter::All), - }; - - let issues = client - .list_issues(hypr_github_issues::ListIssuesRequest { - owner: req.owner.clone(), - repo: req.repo.clone(), - state: state_filter, - labels: req.labels, - per_page: Some(per_page), - page: Some(page), - ..Default::default() - }) - .await - .map_err(|e| TicketError::Internal(e.to_string()))?; - - let next_cursor = if issues.len() as u32 >= per_page { - Some((page + 1).to_string()) - } else { - None - }; - - let collection = CollectionRef { - id: format!("{}/{}", req.owner, req.repo), - name: format!("{}/{}", req.owner, req.repo), - key: Some(format!("{}/{}", req.owner, req.repo)), - url: Some(format!("https://github.com/{}/{}", req.owner, req.repo)), - }; - - let items = issues - .iter() - .map(|issue| github_issue_to_ticket(issue, &collection)) - .collect(); - - Ok(Json(TicketPage { items, next_cursor })) -} diff --git a/crates/api-ticket/src/lib.rs b/crates/api-ticket/src/lib.rs deleted file mode 100644 index 4fed949ab4..0000000000 --- a/crates/api-ticket/src/lib.rs +++ /dev/null @@ -1,15 +0,0 @@ -mod error; -mod github; -mod linear; -mod normalize; -mod openapi; - -use axum::Router; - -pub use openapi::openapi; - -pub fn router() -> Router { - Router::new() - .nest("/github", github::router()) - .nest("/linear", linear::router()) -} diff --git a/crates/api-ticket/src/linear/mod.rs b/crates/api-ticket/src/linear/mod.rs deleted file mode 100644 index 4414cb5983..0000000000 --- a/crates/api-ticket/src/linear/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub(crate) mod routes; - -use axum::{Router, routing::post}; - -pub fn router() -> Router { - Router::new() - .route("/list-teams", post(routes::list_teams)) - .route("/list-tickets", post(routes::list_tickets)) -} diff --git a/crates/api-ticket/src/linear/routes.rs b/crates/api-ticket/src/linear/routes.rs deleted file mode 100644 index 0e876580fe..0000000000 --- a/crates/api-ticket/src/linear/routes.rs +++ /dev/null @@ -1,157 +0,0 @@ -use axum::{Extension, Json}; -use hypr_api_auth::AuthContext; -use hypr_api_nango::{Linear, NangoConnectionState, NangoIntegrationId}; -use hypr_linear::LinearClient; -use hypr_ticket_interface::{CollectionPage, CollectionRef, TicketPage}; -use serde::Deserialize; -use utoipa::ToSchema; - -use crate::error::{Result, TicketError}; -use crate::normalize::linear_issue_to_ticket; - -#[derive(Debug, Deserialize, ToSchema)] -pub struct LinearListTeamsRequest { - pub connection_id: String, - #[serde(default)] - pub limit: Option, - #[serde(default)] - pub cursor: Option, -} - -#[derive(Debug, Deserialize, ToSchema)] -pub struct LinearListTicketsRequest { - pub connection_id: String, - pub team_id: String, - #[serde(default)] - pub query: Option, - #[serde(default)] - pub limit: Option, - #[serde(default)] - pub cursor: Option, -} - -#[utoipa::path( - post, - path = "/linear/list-teams", - operation_id = "linear_list_teams", - request_body = LinearListTeamsRequest, - responses( - (status = 200, description = "Linear teams fetched", body = CollectionPage), - (status = 401, description = "Unauthorized"), - (status = 500, description = "Internal server error"), - ), - tag = "ticket", -)] -pub async fn list_teams( - Extension(auth): Extension, - Extension(nango_state): Extension, - Json(req): Json, -) -> Result> { - let http = nango_state - .build_http_client( - &auth.token, - &auth.claims.sub, - Linear::ID, - &req.connection_id, - ) - .await?; - - let client = LinearClient::new(http); - - let teams = client - .list_teams(hypr_linear::ListTeamsRequest { - first: req.limit, - after: req.cursor, - }) - .await - .map_err(|e| TicketError::Internal(e.to_string()))?; - - let next_cursor = if teams.page_info.has_next_page { - teams.page_info.end_cursor - } else { - None - }; - - let items = teams - .nodes - .into_iter() - .map(|t| CollectionRef { - id: t.id, - name: t.name, - key: Some(t.key), - url: None, - }) - .collect(); - - Ok(Json(CollectionPage { items, next_cursor })) -} - -#[utoipa::path( - post, - path = "/linear/list-tickets", - operation_id = "linear_list_tickets", - request_body = LinearListTicketsRequest, - responses( - (status = 200, description = "Linear tickets fetched", body = TicketPage), - (status = 401, description = "Unauthorized"), - (status = 500, description = "Internal server error"), - ), - tag = "ticket", -)] -pub async fn list_tickets( - Extension(auth): Extension, - Extension(nango_state): Extension, - Json(req): Json, -) -> Result> { - let http = nango_state - .build_http_client( - &auth.token, - &auth.claims.sub, - Linear::ID, - &req.connection_id, - ) - .await?; - - let client = LinearClient::new(http); - - let issues = client - .list_issues(hypr_linear::ListIssuesRequest { - team_id: Some(req.team_id.clone()), - first: req.limit, - after: req.cursor, - query: req.query, - }) - .await - .map_err(|e| TicketError::Internal(e.to_string()))?; - - let next_cursor = if issues.page_info.has_next_page { - issues.page_info.end_cursor - } else { - None - }; - - // Build collection ref from the first issue's team, or from the request team_id. - let collection = issues - .nodes - .first() - .map(|i| CollectionRef { - id: i.team.id.clone(), - name: i.team.name.clone(), - key: Some(i.team.key.clone()), - url: None, - }) - .unwrap_or_else(|| CollectionRef { - id: req.team_id, - name: String::new(), - key: None, - url: None, - }); - - let items = issues - .nodes - .iter() - .map(|issue| linear_issue_to_ticket(issue, &collection)) - .collect(); - - Ok(Json(TicketPage { items, next_cursor })) -} diff --git a/crates/api-ticket/src/normalize.rs b/crates/api-ticket/src/normalize.rs deleted file mode 100644 index f39437ba7d..0000000000 --- a/crates/api-ticket/src/normalize.rs +++ /dev/null @@ -1,341 +0,0 @@ -use hypr_ticket_interface::{ - CollectionRef, LabelRef, PersonRef, PullRequestDetail, TicketKind, TicketPriority, - TicketProviderType, TicketState, TicketSummary, -}; - -pub fn github_issue_to_ticket( - issue: &hypr_github_issues::Issue, - collection: &CollectionRef, -) -> TicketSummary { - let is_pr = issue.is_pull_request(); - let kind = if is_pr { - TicketKind::PullRequest - } else { - TicketKind::Issue - }; - - let (state, state_detail) = github_state(issue); - - let pull_request = if is_pr { - let pr = issue.pull_request.as_ref().unwrap(); - Some(PullRequestDetail { - is_draft: issue.draft.unwrap_or(false), - is_merged: pr.merged_at.is_some(), - source_branch: None, - target_branch: None, - merged_at: pr.merged_at.clone(), - merged_by: None, - }) - } else { - None - }; - - let author = issue.user.as_ref().map(github_user_to_person); - let assignees = issue - .assignees - .as_ref() - .map(|a| a.iter().map(github_user_to_person).collect()) - .unwrap_or_default(); - let labels = issue - .labels - .as_ref() - .map(|l| l.iter().map(github_label_to_ref).collect()) - .unwrap_or_default(); - - let raw = serde_json::to_string(issue).unwrap_or_default(); - - TicketSummary { - provider: TicketProviderType::GitHub, - kind, - id: issue.number.to_string(), - number: Some(issue.number), - collection: collection.clone(), - title: issue.title.clone(), - state, - state_detail, - priority: None, - author, - assignees, - labels, - url: issue.html_url.clone(), - created_at: issue.created_at.clone(), - updated_at: issue.updated_at.clone(), - closed_at: issue.closed_at.clone(), - pull_request, - raw, - } -} - -fn github_state(issue: &hypr_github_issues::Issue) -> (TicketState, Option) { - match issue.state.as_str() { - "open" => (TicketState::Open, Some("open".to_string())), - "closed" => { - if let Some(ref pr) = issue.pull_request - && pr.merged_at.is_some() - { - return (TicketState::Done, Some("merged".to_string())); - } - match issue.state_reason.as_deref() { - Some("completed") => (TicketState::Done, Some("completed".to_string())), - Some("not_planned") => (TicketState::Closed, Some("not_planned".to_string())), - _ => (TicketState::Closed, Some("closed".to_string())), - } - } - other => (TicketState::Open, Some(other.to_string())), - } -} - -fn github_user_to_person(user: &hypr_github_issues::User) -> PersonRef { - PersonRef { - id: Some(user.id.to_string()), - name: Some(user.login.clone()), - email: None, - avatar_url: user.avatar_url.clone(), - } -} - -fn github_label_to_ref(label: &hypr_github_issues::Label) -> LabelRef { - LabelRef { - id: label.id.to_string(), - name: label.name.clone(), - color: label.color.clone(), - } -} - -pub fn linear_issue_to_ticket( - issue: &hypr_linear::Issue, - collection: &CollectionRef, -) -> TicketSummary { - let (state, state_detail) = linear_state(issue); - let priority = linear_priority(issue); - - let author = issue.creator.as_ref().map(linear_user_to_person); - let assignees = issue - .assignee - .as_ref() - .map(|a| vec![linear_user_to_person(a)]) - .unwrap_or_default(); - let labels = issue - .labels - .as_ref() - .map(|l| l.nodes.iter().map(linear_label_to_ref).collect()) - .unwrap_or_default(); - - let raw = serde_json::to_string(issue).unwrap_or_default(); - - TicketSummary { - provider: TicketProviderType::Linear, - kind: TicketKind::Issue, - id: issue.identifier.clone(), - number: Some(issue.number as u64), - collection: collection.clone(), - title: issue.title.clone(), - state, - state_detail, - priority, - author, - assignees, - labels, - url: issue.url.clone(), - created_at: issue.created_at.clone(), - updated_at: issue.updated_at.clone(), - closed_at: issue - .completed_at - .clone() - .or_else(|| issue.cancelled_at.clone()), - pull_request: None, - raw, - } -} - -fn linear_state(issue: &hypr_linear::Issue) -> (TicketState, Option) { - let state_name = issue.state.name.clone(); - let normalized = match issue.state.state_type.as_str() { - "backlog" => TicketState::Backlog, - "unstarted" => TicketState::Open, - "started" => TicketState::InProgress, - "completed" => TicketState::Done, - "cancelled" | "canceled" => TicketState::Closed, - _ => TicketState::Open, - }; - (normalized, Some(state_name)) -} - -fn linear_priority(issue: &hypr_linear::Issue) -> Option { - issue.priority.map(|p| match p as u32 { - 0 => TicketPriority::None, - 1 => TicketPriority::Urgent, - 2 => TicketPriority::High, - 3 => TicketPriority::Medium, - 4 => TicketPriority::Low, - _ => TicketPriority::None, - }) -} - -fn linear_user_to_person(user: &hypr_linear::LinearUser) -> PersonRef { - PersonRef { - id: Some(user.id.clone()), - name: Some(user.name.clone()), - email: user.email.clone(), - avatar_url: user.avatar_url.clone(), - } -} - -fn linear_label_to_ref(label: &hypr_linear::LinearLabel) -> LabelRef { - LabelRef { - id: label.id.clone(), - name: label.name.clone(), - color: label.color.clone(), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn make_collection() -> CollectionRef { - CollectionRef { - id: "test".to_string(), - name: "owner/repo".to_string(), - key: Some("owner/repo".to_string()), - url: Some("https://github.com/owner/repo".to_string()), - } - } - - #[test] - fn github_open_issue_maps_to_open() { - let issue = hypr_github_issues::Issue { - id: 1, - number: 42, - title: "Test issue".to_string(), - body: None, - state: "open".to_string(), - state_reason: None, - html_url: "https://github.com/owner/repo/issues/42".to_string(), - url: "https://api.github.com/repos/owner/repo/issues/42".to_string(), - user: None, - assignees: None, - labels: None, - milestone: None, - pull_request: None, - locked: None, - comments: None, - created_at: "2024-01-01T00:00:00Z".to_string(), - updated_at: "2024-01-01T00:00:00Z".to_string(), - closed_at: None, - closed_by: None, - draft: None, - }; - let ticket = github_issue_to_ticket(&issue, &make_collection()); - assert_eq!(ticket.kind, TicketKind::Issue); - assert_eq!(ticket.state, TicketState::Open); - assert!(ticket.pull_request.is_none()); - } - - #[test] - fn github_merged_pr_maps_to_done() { - let issue = hypr_github_issues::Issue { - id: 2, - number: 43, - title: "Test PR".to_string(), - body: None, - state: "closed".to_string(), - state_reason: None, - html_url: "https://github.com/owner/repo/pull/43".to_string(), - url: "https://api.github.com/repos/owner/repo/issues/43".to_string(), - user: None, - assignees: None, - labels: None, - milestone: None, - pull_request: Some(hypr_github_issues::IssuePullRequest { - url: None, - html_url: None, - diff_url: None, - patch_url: None, - merged_at: Some("2024-01-02T00:00:00Z".to_string()), - }), - locked: None, - comments: None, - created_at: "2024-01-01T00:00:00Z".to_string(), - updated_at: "2024-01-02T00:00:00Z".to_string(), - closed_at: Some("2024-01-02T00:00:00Z".to_string()), - closed_by: None, - draft: None, - }; - let ticket = github_issue_to_ticket(&issue, &make_collection()); - assert_eq!(ticket.kind, TicketKind::PullRequest); - assert_eq!(ticket.state, TicketState::Done); - let pr = ticket.pull_request.unwrap(); - assert!(pr.is_merged); - } - - #[test] - fn github_closed_not_planned_maps_to_closed() { - let issue = hypr_github_issues::Issue { - id: 3, - number: 44, - title: "Won't fix".to_string(), - body: None, - state: "closed".to_string(), - state_reason: Some("not_planned".to_string()), - html_url: "https://github.com/owner/repo/issues/44".to_string(), - url: "https://api.github.com/repos/owner/repo/issues/44".to_string(), - user: None, - assignees: None, - labels: None, - milestone: None, - pull_request: None, - locked: None, - comments: None, - created_at: "2024-01-01T00:00:00Z".to_string(), - updated_at: "2024-01-01T00:00:00Z".to_string(), - closed_at: Some("2024-01-01T00:00:00Z".to_string()), - closed_by: None, - draft: None, - }; - let ticket = github_issue_to_ticket(&issue, &make_collection()); - assert_eq!(ticket.state, TicketState::Closed); - } - - #[test] - fn linear_started_maps_to_in_progress() { - let issue = hypr_linear::Issue { - id: "abc".to_string(), - identifier: "ENG-123".to_string(), - number: 123.0, - title: "Linear issue".to_string(), - description: None, - url: "https://linear.app/team/issue/ENG-123".to_string(), - priority: Some(2.0), - priority_label: Some("High".to_string()), - state: hypr_linear::WorkflowState { - id: "state-1".to_string(), - name: "In Progress".to_string(), - state_type: "started".to_string(), - }, - assignee: None, - creator: None, - team: hypr_linear::TeamRef { - id: "team-1".to_string(), - name: "Engineering".to_string(), - key: "ENG".to_string(), - }, - labels: None, - created_at: "2024-01-01T00:00:00Z".to_string(), - updated_at: "2024-01-01T00:00:00Z".to_string(), - completed_at: None, - cancelled_at: None, - }; - let collection = CollectionRef { - id: "team-1".to_string(), - name: "Engineering".to_string(), - key: Some("ENG".to_string()), - url: None, - }; - let ticket = linear_issue_to_ticket(&issue, &collection); - assert_eq!(ticket.kind, TicketKind::Issue); - assert_eq!(ticket.state, TicketState::InProgress); - assert_eq!(ticket.priority, Some(TicketPriority::High)); - assert!(ticket.pull_request.is_none()); - } -} diff --git a/crates/api-ticket/src/openapi.rs b/crates/api-ticket/src/openapi.rs deleted file mode 100644 index e5b6887130..0000000000 --- a/crates/api-ticket/src/openapi.rs +++ /dev/null @@ -1,25 +0,0 @@ -use utoipa::OpenApi; - -#[derive(OpenApi)] -#[openapi( - paths( - crate::github::routes::list_repos, - crate::github::routes::list_tickets, - crate::linear::routes::list_teams, - crate::linear::routes::list_tickets, - ), - components(schemas( - crate::github::routes::GitHubListReposRequest, - crate::github::routes::GitHubListTicketsRequest, - crate::linear::routes::LinearListTeamsRequest, - crate::linear::routes::LinearListTicketsRequest, - )), - tags( - (name = "ticket", description = "Ticket management") - ) -)] -struct ApiDoc; - -pub fn openapi() -> utoipa::openapi::OpenApi { - ApiDoc::openapi() -} diff --git a/crates/apple-note/Cargo.toml b/crates/apple-note/Cargo.toml deleted file mode 100644 index f3716ba633..0000000000 --- a/crates/apple-note/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "apple-note" -version = "0.1.0" -edition = "2024" - -[dependencies] -flate2 = "1" -prost = "0.13" -serde = { workspace = true, features = ["derive"] } -thiserror = { workspace = true } - -[build-dependencies] -prost-build = "0.13" diff --git a/crates/apple-note/README.md b/crates/apple-note/README.md deleted file mode 100644 index 99ad9a1599..0000000000 --- a/crates/apple-note/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# apple-note - -A Rust library for parsing Apple Notes protobuf data, extracting text content, and handling embedded objects like tables. - -## Credits - -This crate is a Rust port of [apple_cloud_notes_parser](https://github.com/threeplanetssoftware/apple_cloud_notes_parser) by [Three Planets Software](https://github.com/threeplanetssoftware). The original Ruby implementation provides comprehensive parsing of Apple Notes data from iOS and macOS backups. - -The test suite in this crate follows the structure and test cases from the original Ruby implementation's spec directory to ensure compatibility and correctness. - -## Features - -- Parse Apple Notes protobuf data (gzipped and uncompressed) -- Extract plaintext and formatted text spans from notes -- Handle various text formatting (bold, italic, underline, strikethrough, links, colors) -- Parse embedded tables with support for RTL languages -- Detect and classify embedded object types (images, drawings, PDFs, audio, video, etc.) -- Convert notes to Markdown format - -## Usage - -```rust -use apple_note::{parse_note_store_proto, note_to_markdown, extract_plaintext}; - -// Parse a note from gzipped protobuf data -let data = std::fs::read("note_data.bin")?; -let proto = parse_note_store_proto(&data)?; - -// Extract plaintext -let text = extract_plaintext(&proto.document.note); - -// Convert to markdown -let markdown = note_to_markdown(&proto.document.note); -``` - -## Test Data - -The test data files in `tests/data/` are derived from the test fixtures in the original apple_cloud_notes_parser repository. - -## License - -See the repository root for license information. diff --git a/crates/apple-note/src/convert.rs b/crates/apple-note/src/convert.rs deleted file mode 100644 index e5012a3477..0000000000 --- a/crates/apple-note/src/convert.rs +++ /dev/null @@ -1,167 +0,0 @@ -use crate::{ - STYLE_TYPE_CHECKBOX, STYLE_TYPE_DASHED_LIST, STYLE_TYPE_DOTTED_LIST, STYLE_TYPE_HEADING, - STYLE_TYPE_MONOSPACED, STYLE_TYPE_NUMBERED_LIST, STYLE_TYPE_SUBHEADING, STYLE_TYPE_TITLE, - extract::extract_text_spans, proto::Note, -}; - -pub fn note_to_markdown(note: &Note) -> String { - let spans = extract_text_spans(note); - let mut markdown = String::new(); - let mut in_code_block = false; - let mut list_counters: Vec = Vec::new(); - - for span in spans { - let lines: Vec<&str> = span.text.split('\n').collect(); - - for (line_idx, line) in lines.iter().enumerate() { - if line_idx > 0 && !in_code_block { - markdown.push('\n'); - } - - let is_block_quote = span - .paragraph_style - .as_ref() - .and_then(|ps| ps.block_quote) - .map(|bq| bq == 1) - .unwrap_or(false); - - let indent_amount = span - .paragraph_style - .as_ref() - .and_then(|ps| ps.indent_amount) - .unwrap_or(0) as usize; - - let mut prefix = String::new(); - - if is_block_quote { - prefix.push_str("> "); - } - - for _ in 0..indent_amount { - prefix.push_str(" "); - } - - if let Some(style_type) = span.style_type { - match style_type { - STYLE_TYPE_TITLE => { - if !in_code_block { - markdown.push_str(&prefix); - markdown.push_str("# "); - } - } - STYLE_TYPE_HEADING => { - if !in_code_block { - markdown.push_str(&prefix); - markdown.push_str("## "); - } - } - STYLE_TYPE_SUBHEADING => { - if !in_code_block { - markdown.push_str(&prefix); - markdown.push_str("### "); - } - } - STYLE_TYPE_MONOSPACED => { - if !in_code_block && line_idx == 0 { - markdown.push_str("```\n"); - in_code_block = true; - } - } - STYLE_TYPE_DOTTED_LIST => { - markdown.push_str(&prefix); - markdown.push_str("- "); - } - STYLE_TYPE_DASHED_LIST => { - markdown.push_str(&prefix); - markdown.push_str("- "); - } - STYLE_TYPE_NUMBERED_LIST => { - while list_counters.len() <= indent_amount { - list_counters.push(1); - } - markdown.push_str(&prefix); - markdown.push_str(&format!("{}. ", list_counters[indent_amount])); - list_counters[indent_amount] += 1; - } - STYLE_TYPE_CHECKBOX => { - let is_checked = span - .paragraph_style - .as_ref() - .and_then(|ps| ps.checklist.as_ref()) - .map(|cl| cl.done == 1) - .unwrap_or(false); - markdown.push_str(&prefix); - if is_checked { - markdown.push_str("- [x] "); - } else { - markdown.push_str("- [ ] "); - } - } - _ => { - if !in_code_block { - markdown.push_str(&prefix); - } - } - } - } else if !in_code_block { - markdown.push_str(&prefix); - } - - let mut formatted_text = String::new(); - - if in_code_block { - formatted_text.push_str(line); - } else { - if span.bold && span.italic { - formatted_text.push_str("***"); - } else if span.bold { - formatted_text.push_str("**"); - } else if span.italic { - formatted_text.push('*'); - } - - if span.strikethrough { - formatted_text.push_str("~~"); - } - - if let Some(ref link) = span.link { - formatted_text.push('['); - formatted_text.push_str(line); - formatted_text.push_str("]("); - formatted_text.push_str(link); - formatted_text.push(')'); - } else { - formatted_text.push_str(line); - } - - if span.strikethrough { - formatted_text.push_str("~~"); - } - - if span.bold && span.italic { - formatted_text.push_str("***"); - } else if span.bold { - formatted_text.push_str("**"); - } else if span.italic { - formatted_text.push('*'); - } - } - - markdown.push_str(&formatted_text); - - if in_code_block && line_idx == lines.len() - 1 { - let next_is_code = false; - if !next_is_code { - markdown.push_str("\n```"); - in_code_block = false; - } - } - } - } - - if in_code_block { - markdown.push_str("\n```"); - } - - markdown -} diff --git a/crates/apple-note/src/embedded.rs b/crates/apple-note/src/embedded.rs deleted file mode 100644 index fb63404bf2..0000000000 --- a/crates/apple-note/src/embedded.rs +++ /dev/null @@ -1,79 +0,0 @@ -use crate::proto::Note; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum EmbeddedObjectType { - Table, - Image, - PDF, - Drawing, - URL, - Audio, - Video, - Document, - Gallery, - Hashtag, - Mention, - Link, - Unknown, -} - -impl EmbeddedObjectType { - pub fn from_uti(uti: &str) -> Self { - match uti { - "com.apple.notes.table" => Self::Table, - "com.apple.notes.ICTable" => Self::Table, - "public.image" => Self::Image, - "com.apple.drawing.2" => Self::Drawing, - "public.url" => Self::URL, - "public.audio" => Self::Audio, - "public.movie" => Self::Video, - "com.apple.paper" => Self::Document, - "com.apple.notes.gallery" => Self::Gallery, - "com.adobe.pdf" => Self::PDF, - _ if uti.contains("image") => Self::Image, - _ if uti.contains("video") => Self::Video, - _ if uti.contains("audio") => Self::Audio, - _ if uti.contains("pdf") => Self::PDF, - _ => Self::Unknown, - } - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct EmbeddedObject { - pub object_type: EmbeddedObjectType, - pub uuid: String, - pub type_uti: String, -} - -impl EmbeddedObject { - pub fn new(object_type: EmbeddedObjectType, uuid: String, type_uti: String) -> Self { - Self { - object_type, - uuid, - type_uti, - } - } -} - -pub fn extract_embedded_objects(note: &Note) -> Vec { - let mut objects = Vec::new(); - - for attr_run in ¬e.attribute_run { - if let Some(ref attachment_info) = attr_run.attachment_info { - let uuid = attachment_info - .attachment_identifier - .clone() - .unwrap_or_default(); - - let type_uti = attachment_info.type_uti.clone().unwrap_or_default(); - - let object_type = EmbeddedObjectType::from_uti(&type_uti); - - objects.push(EmbeddedObject::new(object_type, uuid, type_uti)); - } - } - - objects -} diff --git a/crates/apple-note/src/extract.rs b/crates/apple-note/src/extract.rs deleted file mode 100644 index c232d9ecf6..0000000000 --- a/crates/apple-note/src/extract.rs +++ /dev/null @@ -1,93 +0,0 @@ -use crate::proto::{Note, ParagraphStyle}; - -#[derive(Debug, Clone, PartialEq)] -pub struct TextSpan { - pub text: String, - pub bold: bool, - pub italic: bool, - pub underline: bool, - pub strikethrough: bool, - pub link: Option, - pub style_type: Option, - pub paragraph_style: Option, -} - -impl TextSpan { - pub fn new(text: String) -> Self { - Self { - text, - bold: false, - italic: false, - underline: false, - strikethrough: false, - link: None, - style_type: None, - paragraph_style: None, - } - } -} - -pub fn extract_text_spans(note: &Note) -> Vec { - let mut spans = Vec::new(); - let mut current_char_index = 0; - - let chars: Vec = note.note_text.chars().collect(); - - for attr_run in ¬e.attribute_run { - if attr_run.attachment_info.is_some() { - current_char_index += attr_run.length as usize; - continue; - } - - let length = attr_run.length as usize; - let end_char_index = current_char_index + length; - - if end_char_index > chars.len() { - break; - } - - let text_slice: String = chars[current_char_index..end_char_index].iter().collect(); - - let mut span = TextSpan::new(text_slice.to_string()); - - if let Some(font_weight) = attr_run.font_weight { - match font_weight { - 1 => span.bold = true, - 2 => span.italic = true, - 3 => { - span.bold = true; - span.italic = true; - } - _ => {} - } - } - - if let Some(underlined) = attr_run.underlined { - span.underline = underlined == 1; - } - - if let Some(strikethrough) = attr_run.strikethrough { - span.strikethrough = strikethrough == 1; - } - - if let Some(ref link) = attr_run.link - && !link.is_empty() - { - span.link = Some(link.clone()); - } - - if let Some(ref para_style) = attr_run.paragraph_style { - span.style_type = para_style.style_type; - span.paragraph_style = Some(para_style.clone()); - } - - spans.push(span); - current_char_index = end_char_index; - } - - spans -} - -pub fn extract_plaintext(note: &Note) -> String { - note.note_text.clone() -} diff --git a/crates/apple-note/src/lib.rs b/crates/apple-note/src/lib.rs deleted file mode 100644 index b6bff8635c..0000000000 --- a/crates/apple-note/src/lib.rs +++ /dev/null @@ -1,48 +0,0 @@ -pub mod convert; -pub mod embedded; -pub mod extract; -pub mod parser; -pub mod proto; -pub mod table; -pub mod utils; - -pub use convert::*; -pub use embedded::*; -pub use extract::*; -pub use parser::*; -pub use proto::*; -pub use table::*; -pub use utils::*; - -pub const STYLE_TYPE_DEFAULT: i32 = -1; -pub const STYLE_TYPE_TITLE: i32 = 0; -pub const STYLE_TYPE_HEADING: i32 = 1; -pub const STYLE_TYPE_SUBHEADING: i32 = 2; -pub const STYLE_TYPE_MONOSPACED: i32 = 4; -pub const STYLE_TYPE_DOTTED_LIST: i32 = 100; -pub const STYLE_TYPE_DASHED_LIST: i32 = 101; -pub const STYLE_TYPE_NUMBERED_LIST: i32 = 102; -pub const STYLE_TYPE_CHECKBOX: i32 = 103; - -pub const STYLE_TYPE_BLOCK_QUOTE: i32 = 1; - -pub const STYLE_ALIGNMENT_LEFT: i32 = 0; -pub const STYLE_ALIGNMENT_CENTER: i32 = 1; -pub const STYLE_ALIGNMENT_RIGHT: i32 = 2; -pub const STYLE_ALIGNMENT_JUSTIFY: i32 = 3; - -pub const FONT_TYPE_DEFAULT: i32 = 0; -pub const FONT_TYPE_BOLD: i32 = 1; -pub const FONT_TYPE_ITALIC: i32 = 2; -pub const FONT_TYPE_BOLD_ITALIC: i32 = 3; - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("Failed to decode protobuf: {0}")] - Decode(#[from] prost::DecodeError), - - #[error("Failed to decompress data: {0}")] - Decompression(#[from] std::io::Error), -} - -pub type Result = std::result::Result; diff --git a/crates/apple-note/src/parser.rs b/crates/apple-note/src/parser.rs deleted file mode 100644 index 26500efc5c..0000000000 --- a/crates/apple-note/src/parser.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::{Result, proto::MergableDataProto, proto::NoteStoreProto, utils}; -use prost::Message; - -pub fn parse_note_store_proto(data: &[u8]) -> Result { - let decompressed = if utils::is_gzip(data) { - utils::decompress_gzip(data)? - } else { - data.to_vec() - }; - - let proto = NoteStoreProto::decode(&decompressed[..])?; - Ok(proto) -} - -pub fn parse_mergable_data_proto(data: &[u8]) -> Result { - let decompressed = if utils::is_gzip(data) { - utils::decompress_gzip(data)? - } else { - data.to_vec() - }; - - let proto = MergableDataProto::decode(&decompressed[..])?; - Ok(proto) -} diff --git a/crates/apple-note/src/proto.rs b/crates/apple-note/src/proto.rs deleted file mode 100644 index 457086b57e..0000000000 --- a/crates/apple-note/src/proto.rs +++ /dev/null @@ -1,309 +0,0 @@ -use prost::Message; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, PartialEq, Message, Serialize, Deserialize)] -pub struct Color { - #[prost(float, required, tag = "1")] - pub red: f32, - #[prost(float, required, tag = "2")] - pub green: f32, - #[prost(float, required, tag = "3")] - pub blue: f32, - #[prost(float, required, tag = "4")] - pub alpha: f32, -} - -impl Color { - pub fn red_hex_string(&self) -> String { - format!("{:02X}", (self.red * 255.0) as u8) - } - - pub fn green_hex_string(&self) -> String { - format!("{:02X}", (self.green * 255.0) as u8) - } - - pub fn blue_hex_string(&self) -> String { - format!("{:02X}", (self.blue * 255.0) as u8) - } - - pub fn full_hex_string(&self) -> String { - format!( - "#{}{}{}", - self.red_hex_string(), - self.green_hex_string(), - self.blue_hex_string() - ) - } -} - -#[derive(Clone, PartialEq, Message, Serialize, Deserialize)] -pub struct AttachmentInfo { - #[prost(string, optional, tag = "1")] - pub attachment_identifier: Option, - #[prost(string, optional, tag = "2")] - pub type_uti: Option, -} - -#[derive(Clone, PartialEq, Message, Serialize, Deserialize)] -pub struct Font { - #[prost(string, optional, tag = "1")] - pub font_name: Option, - #[prost(float, optional, tag = "2")] - pub point_size: Option, - #[prost(int32, optional, tag = "3")] - pub font_hints: Option, -} - -#[derive(Clone, PartialEq, Message, Serialize, Deserialize)] -pub struct Checklist { - #[prost(bytes, required, tag = "1")] - pub uuid: Vec, - #[prost(int32, required, tag = "2")] - pub done: i32, -} - -#[derive(Clone, PartialEq, Message, Serialize, Deserialize)] -pub struct ParagraphStyle { - #[prost(int32, optional, tag = "1", default = "-1")] - pub style_type: Option, - #[prost(int32, optional, tag = "2")] - pub alignment: Option, - #[prost(int32, optional, tag = "4")] - pub indent_amount: Option, - #[prost(message, optional, tag = "5")] - pub checklist: Option, - #[prost(int32, optional, tag = "8")] - pub block_quote: Option, -} - -#[derive(Clone, PartialEq, Message, Serialize, Deserialize)] -pub struct AttributeRun { - #[prost(int32, required, tag = "1")] - pub length: i32, - #[prost(message, optional, tag = "2")] - pub paragraph_style: Option, - #[prost(message, optional, tag = "3")] - pub font: Option, - #[prost(int32, optional, tag = "5")] - pub font_weight: Option, - #[prost(int32, optional, tag = "6")] - pub underlined: Option, - #[prost(int32, optional, tag = "7")] - pub strikethrough: Option, - #[prost(int32, optional, tag = "8")] - pub superscript: Option, - #[prost(string, optional, tag = "9")] - pub link: Option, - #[prost(message, optional, tag = "10")] - pub color: Option, - #[prost(message, optional, tag = "12")] - pub attachment_info: Option, - #[prost(int32, optional, tag = "13")] - pub unknown_identifier: Option, - #[prost(int32, optional, tag = "14")] - pub emphasis_style: Option, -} - -impl AttributeRun { - /// Check if two AttributeRuns have the same style - pub fn same_style(&self, other: &AttributeRun) -> bool { - self.font_weight == other.font_weight - && self.underlined == other.underlined - && self.strikethrough == other.strikethrough - && self.superscript == other.superscript - && self.link == other.link - && self.paragraph_style == other.paragraph_style - && self.font == other.font - && self.color == other.color - } -} - -#[derive(Clone, PartialEq, Message, Serialize, Deserialize)] -pub struct Note { - #[prost(string, required, tag = "2")] - pub note_text: String, - #[prost(message, repeated, tag = "5")] - pub attribute_run: Vec, -} - -#[derive(Clone, PartialEq, Message, Serialize, Deserialize)] -pub struct Document { - #[prost(int32, required, tag = "2")] - pub version: i32, - #[prost(message, required, tag = "3")] - pub note: Note, -} - -#[derive(Clone, PartialEq, Message, Serialize, Deserialize)] -pub struct NoteStoreProto { - #[prost(message, required, tag = "2")] - pub document: Document, -} - -#[derive(Clone, PartialEq, Message, Serialize, Deserialize)] -pub struct ObjectId { - #[prost(uint64, required, tag = "2")] - pub unsigned_integer_value: u64, - #[prost(string, required, tag = "4")] - pub string_value: String, - #[prost(int32, required, tag = "6")] - pub object_index: i32, -} - -#[derive(Clone, PartialEq, Message, Serialize, Deserialize)] -pub struct DictionaryElement { - #[prost(message, required, tag = "1")] - pub key: ObjectId, - #[prost(message, required, tag = "2")] - pub value: ObjectId, -} - -#[derive(Clone, PartialEq, Message, Serialize, Deserialize)] -pub struct Dictionary { - #[prost(message, repeated, tag = "1")] - pub element: Vec, -} - -#[derive(Clone, PartialEq, Message, Serialize, Deserialize)] -pub struct RegisterLatest { - #[prost(message, required, tag = "2")] - pub contents: ObjectId, -} - -#[derive(Clone, PartialEq, Message, Serialize, Deserialize)] -pub struct MapEntry { - #[prost(int32, required, tag = "1")] - pub key: i32, - #[prost(message, required, tag = "2")] - pub value: ObjectId, -} - -#[derive(Clone, PartialEq, Message, Serialize, Deserialize)] -pub struct MergeableDataObjectMap { - #[prost(int32, required, tag = "1")] - pub r#type: i32, - #[prost(message, repeated, tag = "3")] - pub map_entry: Vec, -} - -#[derive(Clone, PartialEq, Message, Serialize, Deserialize)] -pub struct OrderedSetOrderingArrayAttachment { - #[prost(int32, required, tag = "1")] - pub index: i32, - #[prost(bytes, required, tag = "2")] - pub uuid: Vec, -} - -#[derive(Clone, PartialEq, Message, Serialize, Deserialize)] -pub struct OrderedSetOrderingArray { - #[prost(message, required, tag = "1")] - pub contents: Note, - #[prost(message, repeated, tag = "2")] - pub attachment: Vec, -} - -#[derive(Clone, PartialEq, Message, Serialize, Deserialize)] -pub struct OrderedSetOrdering { - #[prost(message, required, tag = "1")] - pub array: OrderedSetOrderingArray, - #[prost(message, required, tag = "2")] - pub contents: Dictionary, -} - -#[derive(Clone, PartialEq, Message, Serialize, Deserialize)] -pub struct OrderedSet { - #[prost(message, required, tag = "1")] - pub ordering: OrderedSetOrdering, - #[prost(message, required, tag = "2")] - pub elements: Dictionary, -} - -#[derive(Clone, PartialEq, Message, Serialize, Deserialize)] -pub struct ListEntryDetailsKey { - #[prost(int32, required, tag = "1")] - pub list_entry_details_type_index: i32, - #[prost(int32, required, tag = "2")] - pub list_entry_details_key: i32, -} - -#[derive(Clone, PartialEq, Message, Serialize, Deserialize)] -pub struct ListEntryDetails { - #[prost(message, optional, tag = "1")] - pub list_entry_details_key: Option, - #[prost(message, optional, tag = "2")] - pub id: Option, -} - -#[derive(Clone, PartialEq, Message, Serialize, Deserialize)] -pub struct ListEntry { - #[prost(message, required, tag = "2")] - pub id: ObjectId, - #[prost(message, optional, tag = "3")] - pub details: Option, - #[prost(message, required, tag = "4")] - pub additional_details: ListEntryDetails, -} - -#[derive(Clone, PartialEq, Message, Serialize, Deserialize)] -pub struct List { - #[prost(message, repeated, tag = "1")] - pub list_entry: Vec, -} - -#[derive(Clone, PartialEq, Message, Serialize, Deserialize)] -pub struct UnknownMergeableDataObjectEntryMessageEntry { - #[prost(int32, optional, tag = "1")] - pub unknown_int1: Option, - #[prost(int64, optional, tag = "2")] - pub unknown_int2: Option, -} - -#[derive(Clone, PartialEq, Message, Serialize, Deserialize)] -pub struct UnknownMergeableDataObjectEntryMessage { - #[prost(message, optional, tag = "1")] - pub unknown_entry: Option, -} - -#[derive(Clone, PartialEq, Message, Serialize, Deserialize)] -pub struct MergeableDataObjectEntry { - #[prost(message, required, tag = "1")] - pub register_latest: RegisterLatest, - #[prost(message, optional, tag = "5")] - pub list: Option, - #[prost(message, optional, tag = "6")] - pub dictionary: Option, - #[prost(message, optional, tag = "9")] - pub unknown_message: Option, - #[prost(message, optional, tag = "10")] - pub note: Option, - #[prost(message, optional, tag = "13")] - pub custom_map: Option, - #[prost(message, optional, tag = "16")] - pub ordered_set: Option, -} - -#[derive(Clone, PartialEq, Message, Serialize, Deserialize)] -pub struct MergeableDataObjectData { - #[prost(message, repeated, tag = "3")] - pub mergeable_data_object_entry: Vec, - #[prost(string, repeated, tag = "4")] - pub mergeable_data_object_key_item: Vec, - #[prost(string, repeated, tag = "5")] - pub mergeable_data_object_type_item: Vec, - #[prost(bytes = "vec", repeated, tag = "6")] - pub mergeable_data_object_uuid_item: Vec>, -} - -#[derive(Clone, PartialEq, Message, Serialize, Deserialize)] -pub struct MergeableDataObject { - #[prost(int32, required, tag = "2")] - pub version: i32, - #[prost(message, required, tag = "3")] - pub mergeable_data_object_data: MergeableDataObjectData, -} - -#[derive(Clone, PartialEq, Message, Serialize, Deserialize)] -pub struct MergableDataProto { - #[prost(message, required, tag = "2")] - pub mergable_data_object: MergeableDataObject, -} diff --git a/crates/apple-note/src/table.rs b/crates/apple-note/src/table.rs deleted file mode 100644 index 1329835b12..0000000000 --- a/crates/apple-note/src/table.rs +++ /dev/null @@ -1,310 +0,0 @@ -use crate::proto::{MergableDataProto, MergeableDataObjectEntry}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -const LEFT_TO_RIGHT_DIRECTION: &str = "CRTableColumnDirectionLeftToRight"; -const RIGHT_TO_LEFT_DIRECTION: &str = "CRTableColumnDirectionRightToLeft"; - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct Table { - pub rows: Vec>, - pub direction: String, -} - -impl Table { - pub fn new() -> Self { - Self { - rows: Vec::new(), - direction: LEFT_TO_RIGHT_DIRECTION.to_string(), - } - } - - pub fn row_count(&self) -> usize { - self.rows.len() - } - - pub fn column_count(&self) -> usize { - self.rows.first().map(|r| r.len()).unwrap_or(0) - } -} - -impl Default for Table { - fn default() -> Self { - Self::new() - } -} - -fn get_target_uuid_from_object_entry(object_entry: &MergeableDataObjectEntry) -> Option { - object_entry - .custom_map - .as_ref()? - .map_entry - .first() - .map(|entry| entry.value.unsigned_integer_value) -} - -pub fn parse_table(proto: &MergableDataProto) -> Option { - let data = &proto.mergable_data_object.mergeable_data_object_data; - - let key_items = &data.mergeable_data_object_key_item; - let type_items = &data.mergeable_data_object_type_item; - let uuid_items = &data.mergeable_data_object_uuid_item; - let table_objects = &data.mergeable_data_object_entry; - - let mut table_direction = LEFT_TO_RIGHT_DIRECTION.to_string(); - - for entry in table_objects { - if let Some(ref custom_map) = entry.custom_map - && let Some(first_entry) = custom_map.map_entry.first() - && (first_entry.key as usize) == key_items.len() - && let Some(dir) = &first_entry.value.string_value.as_str().into() - { - table_direction = (*dir).to_string(); - } - } - - for entry in table_objects { - if let Some(ref custom_map) = entry.custom_map { - let type_index = custom_map.r#type as usize; - if type_index < type_items.len() && type_items[type_index] == "com.apple.notes.ICTable" - { - return parse_table_entry( - entry, - key_items, - uuid_items, - table_objects, - &table_direction, - ); - } - } - } - - None -} - -fn parse_table_entry( - table_entry: &MergeableDataObjectEntry, - key_items: &[String], - uuid_items: &[Vec], - table_objects: &[MergeableDataObjectEntry], - table_direction: &str, -) -> Option
{ - let custom_map = table_entry.custom_map.as_ref()?; - - let mut row_indices: HashMap = HashMap::new(); - let mut column_indices: HashMap = HashMap::new(); - let mut total_rows = 0; - let mut total_columns = 0; - let mut cell_columns_entry: Option<&MergeableDataObjectEntry> = None; - - for map_entry in &custom_map.map_entry { - let key_index = map_entry.key as usize; - if key_index == 0 || key_index > key_items.len() { - continue; - } - - let key_name = &key_items[key_index - 1]; - let object_index = map_entry.value.object_index as usize; - - if object_index >= table_objects.len() { - continue; - } - - let target_object = &table_objects[object_index]; - - match key_name.as_str() { - "crRows" => { - total_rows = parse_rows(target_object, uuid_items, table_objects, &mut row_indices); - } - "crColumns" => { - total_columns = parse_columns( - target_object, - uuid_items, - table_objects, - &mut column_indices, - ); - } - "cellColumns" => { - cell_columns_entry = Some(target_object); - } - _ => {} - } - } - - if total_rows == 0 || total_columns == 0 { - return None; - } - - let mut reconstructed_table = vec![vec![String::new(); total_columns]; total_rows]; - - if let Some(cell_columns) = cell_columns_entry { - parse_cell_columns( - cell_columns, - table_objects, - &row_indices, - &column_indices, - &mut reconstructed_table, - ); - } - - if table_direction == RIGHT_TO_LEFT_DIRECTION { - for row in &mut reconstructed_table { - row.reverse(); - } - } - - Some(Table { - rows: reconstructed_table, - direction: table_direction.to_string(), - }) -} - -fn parse_rows( - object_entry: &MergeableDataObjectEntry, - uuid_items: &[Vec], - table_objects: &[MergeableDataObjectEntry], - row_indices: &mut HashMap, -) -> usize { - let ordered_set = match &object_entry.ordered_set { - Some(set) => set, - None => return 0, - }; - - let mut total_rows = 0; - - for attachment in &ordered_set.ordering.array.attachment { - if let Some(uuid_index) = uuid_items.iter().position(|u| u == &attachment.uuid) { - row_indices.insert(uuid_index as u64, total_rows); - total_rows += 1; - } - } - - for element in &ordered_set.ordering.contents.element { - let key_index = element.key.object_index as usize; - let value_index = element.value.object_index as usize; - - if key_index >= table_objects.len() || value_index >= table_objects.len() { - continue; - } - - if let (Some(key_uuid), Some(value_uuid)) = ( - get_target_uuid_from_object_entry(&table_objects[key_index]), - get_target_uuid_from_object_entry(&table_objects[value_index]), - ) && let Some(&key_row_index) = row_indices.get(&key_uuid) - { - row_indices.insert(value_uuid, key_row_index); - } - } - - total_rows -} - -fn parse_columns( - object_entry: &MergeableDataObjectEntry, - uuid_items: &[Vec], - table_objects: &[MergeableDataObjectEntry], - column_indices: &mut HashMap, -) -> usize { - let ordered_set = match &object_entry.ordered_set { - Some(set) => set, - None => return 0, - }; - - let mut total_columns = 0; - - for attachment in &ordered_set.ordering.array.attachment { - if let Some(uuid_index) = uuid_items.iter().position(|u| u == &attachment.uuid) { - column_indices.insert(uuid_index as u64, total_columns); - total_columns += 1; - } - } - - for element in &ordered_set.ordering.contents.element { - let key_index = element.key.object_index as usize; - let value_index = element.value.object_index as usize; - - if key_index >= table_objects.len() || value_index >= table_objects.len() { - continue; - } - - if let (Some(key_uuid), Some(value_uuid)) = ( - get_target_uuid_from_object_entry(&table_objects[key_index]), - get_target_uuid_from_object_entry(&table_objects[value_index]), - ) && let Some(&key_col_index) = column_indices.get(&key_uuid) - { - column_indices.insert(value_uuid, key_col_index); - } - } - - total_columns -} - -fn parse_cell_columns( - cell_columns_entry: &MergeableDataObjectEntry, - table_objects: &[MergeableDataObjectEntry], - row_indices: &HashMap, - column_indices: &HashMap, - reconstructed_table: &mut [Vec], -) { - let dictionary = match &cell_columns_entry.dictionary { - Some(dict) => dict, - None => return, - }; - - for column in &dictionary.element { - let column_index = column.key.object_index as usize; - if column_index >= table_objects.len() { - continue; - } - - let current_column = match get_target_uuid_from_object_entry(&table_objects[column_index]) { - Some(uuid) => uuid, - None => continue, - }; - - let target_dict_index = column.value.object_index as usize; - if target_dict_index >= table_objects.len() { - continue; - } - - let target_dictionary_object = &table_objects[target_dict_index]; - let target_dict = match &target_dictionary_object.dictionary { - Some(dict) => dict, - None => continue, - }; - - for row in &target_dict.element { - let row_index = row.key.object_index as usize; - if row_index >= table_objects.len() { - continue; - } - - let current_row = match get_target_uuid_from_object_entry(&table_objects[row_index]) { - Some(uuid) => uuid, - None => continue, - }; - - let target_cell_index = row.value.object_index as usize; - if target_cell_index >= table_objects.len() { - continue; - } - - let target_cell = &table_objects[target_cell_index]; - - let cell_text = if let Some(ref note) = target_cell.note { - note.note_text.clone() - } else { - String::new() - }; - - if let (Some(&row_idx), Some(&col_idx)) = ( - row_indices.get(¤t_row), - column_indices.get(¤t_column), - ) && row_idx < reconstructed_table.len() - && col_idx < reconstructed_table[row_idx].len() - { - reconstructed_table[row_idx][col_idx] = cell_text; - } - } - } -} diff --git a/crates/apple-note/src/utils.rs b/crates/apple-note/src/utils.rs deleted file mode 100644 index 30002895b0..0000000000 --- a/crates/apple-note/src/utils.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::Result; -use flate2::read::GzDecoder; -use std::io::Read; - -const IOS_CORE_TIME_OFFSET: i64 = 978307200; - -pub fn core_time_to_unix(core_time: i64) -> i64 { - core_time + IOS_CORE_TIME_OFFSET -} - -pub fn is_gzip(data: &[u8]) -> bool { - data.len() > 2 && data[0] == 0x1f && data[1] == 0x8b -} - -pub fn decompress_gzip(data: &[u8]) -> Result> { - let mut decoder = GzDecoder::new(data); - let mut decompressed = Vec::new(); - decoder.read_to_end(&mut decompressed)?; - Ok(decompressed) -} diff --git a/crates/apple-note/tests/attribute_run_tests.rs b/crates/apple-note/tests/attribute_run_tests.rs deleted file mode 100644 index 188c048a5f..0000000000 --- a/crates/apple-note/tests/attribute_run_tests.rs +++ /dev/null @@ -1,661 +0,0 @@ -//! AttributeRun tests -//! -//! These tests correspond to the AttributeRun tests in the apple_cloud_notes_parser Ruby implementation: -//! https://github.com/threeplanetssoftware/apple_cloud_notes_parser/blob/master/spec/base_classes/proto_patches.rb -//! -//! Tests cover style comparison between attribute runs, including font weight, underlined, -//! strikethrough, superscript, link, paragraph style, font, and color comparisons. - -use apple_note::{AttributeRun, Color, Font, ParagraphStyle}; - -#[test] -fn test_same_style_no_styles() { - let run1 = AttributeRun { - length: 1, - paragraph_style: None, - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - let run2 = AttributeRun { - length: 1, - paragraph_style: None, - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - assert!(run1.same_style(&run2)); -} - -#[test] -fn test_different_font_weights() { - let run1 = AttributeRun { - length: 1, - font_weight: Some(2), - paragraph_style: None, - font: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - let run2 = AttributeRun { - length: 1, - font_weight: Some(4), - paragraph_style: None, - font: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - assert!(!run1.same_style(&run2)); -} - -#[test] -fn test_different_underlined_values() { - let run1 = AttributeRun { - length: 1, - underlined: Some(2), - font_weight: None, - paragraph_style: None, - font: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - let run2 = AttributeRun { - length: 1, - underlined: Some(3), - font_weight: None, - paragraph_style: None, - font: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - assert!(!run1.same_style(&run2)); -} - -#[test] -fn test_different_strikethrough_values() { - let run1 = AttributeRun { - length: 1, - strikethrough: Some(2), - font_weight: None, - paragraph_style: None, - font: None, - underlined: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - let run2 = AttributeRun { - length: 1, - strikethrough: Some(3), - font_weight: None, - paragraph_style: None, - font: None, - underlined: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - assert!(!run1.same_style(&run2)); -} - -#[test] -fn test_different_superscript_values() { - let run1 = AttributeRun { - length: 1, - superscript: Some(2), - font_weight: None, - paragraph_style: None, - font: None, - underlined: None, - strikethrough: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - let run2 = AttributeRun { - length: 1, - superscript: Some(3), - font_weight: None, - paragraph_style: None, - font: None, - underlined: None, - strikethrough: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - assert!(!run1.same_style(&run2)); -} - -#[test] -fn test_different_link_values() { - let run1 = AttributeRun { - length: 1, - link: Some("https://google.com".to_string()), - font_weight: None, - paragraph_style: None, - font: None, - underlined: None, - strikethrough: None, - superscript: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - let run2 = AttributeRun { - length: 1, - link: Some("http://yahoo.com".to_string()), - font_weight: None, - paragraph_style: None, - font: None, - underlined: None, - strikethrough: None, - superscript: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - assert!(!run1.same_style(&run2)); -} - -#[test] -fn test_same_paragraph_styles() { - let style1 = ParagraphStyle { - style_type: None, - alignment: None, - indent_amount: Some(1), - checklist: None, - block_quote: None, - }; - let style2 = ParagraphStyle { - style_type: None, - alignment: None, - indent_amount: Some(1), - checklist: None, - block_quote: None, - }; - let run1 = AttributeRun { - length: 1, - paragraph_style: Some(style1), - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - let run2 = AttributeRun { - length: 1, - paragraph_style: Some(style2), - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - assert!(run1.same_style(&run2)); -} - -#[test] -fn test_different_paragraph_styles() { - let style1 = ParagraphStyle { - style_type: None, - alignment: None, - indent_amount: Some(1), - checklist: None, - block_quote: None, - }; - let style2 = ParagraphStyle { - style_type: None, - alignment: None, - indent_amount: Some(2), - checklist: None, - block_quote: None, - }; - let run1 = AttributeRun { - length: 1, - paragraph_style: Some(style1), - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - let run2 = AttributeRun { - length: 1, - paragraph_style: Some(style2), - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - assert!(!run1.same_style(&run2)); -} - -#[test] -fn test_same_fonts() { - let font1 = Font { - font_name: Some("Consolas".to_string()), - point_size: None, - font_hints: None, - }; - let font2 = Font { - font_name: Some("Consolas".to_string()), - point_size: None, - font_hints: None, - }; - let run1 = AttributeRun { - length: 1, - font: Some(font1), - paragraph_style: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - let run2 = AttributeRun { - length: 1, - font: Some(font2), - paragraph_style: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - assert!(run1.same_style(&run2)); -} - -#[test] -fn test_different_fonts() { - let font1 = Font { - font_name: Some("Consolas".to_string()), - point_size: None, - font_hints: None, - }; - let font2 = Font { - font_name: Some("Times New Roman".to_string()), - point_size: None, - font_hints: None, - }; - let run1 = AttributeRun { - length: 1, - font: Some(font1), - paragraph_style: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - let run2 = AttributeRun { - length: 1, - font: Some(font2), - paragraph_style: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - assert!(!run1.same_style(&run2)); -} - -#[test] -fn test_same_colors() { - let color1 = Color { - red: 1.0, - green: 1.0, - blue: 1.0, - alpha: 1.0, - }; - let color2 = Color { - red: 1.0, - green: 1.0, - blue: 1.0, - alpha: 1.0, - }; - let run1 = AttributeRun { - length: 1, - color: Some(color1), - paragraph_style: None, - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - let run2 = AttributeRun { - length: 1, - color: Some(color2), - paragraph_style: None, - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - assert!(run1.same_style(&run2)); -} - -#[test] -fn test_different_colors() { - let color1 = Color { - red: 1.0, - green: 1.0, - blue: 1.0, - alpha: 1.0, - }; - let color2 = Color { - red: 0.0, - green: 0.0, - blue: 0.0, - alpha: 0.0, - }; - let run1 = AttributeRun { - length: 1, - color: Some(color1), - paragraph_style: None, - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - let run2 = AttributeRun { - length: 1, - color: Some(color2), - paragraph_style: None, - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - assert!(!run1.same_style(&run2)); -} - -#[test] -fn test_mixed_attributes_same_style() { - let color = Color { - red: 1.0, - green: 0.0, - blue: 0.0, - alpha: 1.0, - }; - let font = Font { - font_name: Some("Arial".to_string()), - point_size: Some(12.0), - font_hints: None, - }; - let run1 = AttributeRun { - length: 5, - font_weight: Some(1), - underlined: Some(1), - color: Some(color.clone()), - font: Some(font.clone()), - paragraph_style: None, - strikethrough: None, - superscript: None, - link: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - let run2 = AttributeRun { - length: 5, - font_weight: Some(1), - underlined: Some(1), - color: Some(color), - font: Some(font), - paragraph_style: None, - strikethrough: None, - superscript: None, - link: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - assert!(run1.same_style(&run2)); -} - -#[test] -fn test_mixed_attributes_different_style() { - let color1 = Color { - red: 1.0, - green: 0.0, - blue: 0.0, - alpha: 1.0, - }; - let color2 = Color { - red: 0.0, - green: 1.0, - blue: 0.0, - alpha: 1.0, - }; - let font = Font { - font_name: Some("Arial".to_string()), - point_size: Some(12.0), - font_hints: None, - }; - let run1 = AttributeRun { - length: 5, - font_weight: Some(1), - underlined: Some(1), - color: Some(color1), - font: Some(font.clone()), - paragraph_style: None, - strikethrough: None, - superscript: None, - link: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - let run2 = AttributeRun { - length: 5, - font_weight: Some(1), - underlined: Some(1), - color: Some(color2), - font: Some(font), - paragraph_style: None, - strikethrough: None, - superscript: None, - link: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - assert!(!run1.same_style(&run2)); -} - -#[test] -fn test_none_vs_some_link() { - let run1 = AttributeRun { - length: 1, - link: None, - font_weight: None, - paragraph_style: None, - font: None, - underlined: None, - strikethrough: None, - superscript: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - let run2 = AttributeRun { - length: 1, - link: Some("https://example.com".to_string()), - font_weight: None, - paragraph_style: None, - font: None, - underlined: None, - strikethrough: None, - superscript: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - assert!(!run1.same_style(&run2)); -} - -#[test] -fn test_same_link() { - let run1 = AttributeRun { - length: 1, - link: Some("https://example.com".to_string()), - font_weight: None, - paragraph_style: None, - font: None, - underlined: None, - strikethrough: None, - superscript: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - let run2 = AttributeRun { - length: 1, - link: Some("https://example.com".to_string()), - font_weight: None, - paragraph_style: None, - font: None, - underlined: None, - strikethrough: None, - superscript: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - assert!(run1.same_style(&run2)); -} - -#[test] -fn test_clone_attribute_run() { - let run = AttributeRun { - length: 10, - font_weight: Some(1), - underlined: Some(1), - paragraph_style: None, - font: None, - strikethrough: None, - superscript: None, - link: Some("https://test.com".to_string()), - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - let cloned = run.clone(); - assert_eq!(run.length, cloned.length); - assert_eq!(run.font_weight, cloned.font_weight); - assert_eq!(run.underlined, cloned.underlined); - assert_eq!(run.link, cloned.link); - assert!(run.same_style(&cloned)); -} diff --git a/crates/apple-note/tests/color_tests.rs b/crates/apple-note/tests/color_tests.rs deleted file mode 100644 index f179769427..0000000000 --- a/crates/apple-note/tests/color_tests.rs +++ /dev/null @@ -1,122 +0,0 @@ -//! Color tests -//! -//! These tests correspond to the Color tests in the apple_cloud_notes_parser Ruby implementation: -//! https://github.com/threeplanetssoftware/apple_cloud_notes_parser/blob/master/spec/base_classes/proto_patches.rb -//! -//! Tests cover hex string generation for RGB color values. - -use apple_note::Color; - -#[test] -fn test_red_hex_string_100_percent() { - let color = Color { - red: 1.0, - green: 0.0, - blue: 0.0, - alpha: 0.0, - }; - assert_eq!(color.red_hex_string(), "FF"); - assert_eq!(color.green_hex_string(), "00"); - assert_eq!(color.blue_hex_string(), "00"); - assert_eq!(color.full_hex_string(), "#FF0000"); -} - -#[test] -fn test_hex_string_0_percent_with_padding() { - let color = Color { - red: 0.0, - green: 0.0, - blue: 0.0, - alpha: 0.0, - }; - assert_eq!(color.red_hex_string(), "00"); - assert_eq!(color.green_hex_string(), "00"); - assert_eq!(color.blue_hex_string(), "00"); - assert_eq!(color.full_hex_string(), "#000000"); -} - -#[test] -fn test_blue_hex_string_100_percent() { - let color = Color { - red: 0.0, - green: 0.0, - blue: 1.0, - alpha: 0.0, - }; - assert_eq!(color.red_hex_string(), "00"); - assert_eq!(color.green_hex_string(), "00"); - assert_eq!(color.blue_hex_string(), "FF"); - assert_eq!(color.full_hex_string(), "#0000FF"); -} - -#[test] -fn test_green_hex_string_100_percent() { - let color = Color { - red: 0.0, - green: 1.0, - blue: 0.0, - alpha: 0.0, - }; - assert_eq!(color.red_hex_string(), "00"); - assert_eq!(color.green_hex_string(), "FF"); - assert_eq!(color.blue_hex_string(), "00"); - assert_eq!(color.full_hex_string(), "#00FF00"); -} - -#[test] -fn test_white_color() { - let color = Color { - red: 1.0, - green: 1.0, - blue: 1.0, - alpha: 1.0, - }; - assert_eq!(color.full_hex_string(), "#FFFFFF"); -} - -#[test] -fn test_partial_color_values() { - let color = Color { - red: 0.5, - green: 0.25, - blue: 0.75, - alpha: 0.0, - }; - assert_eq!(color.red_hex_string(), "7F"); - assert_eq!(color.green_hex_string(), "3F"); - assert_eq!(color.blue_hex_string(), "BF"); -} - -#[test] -fn test_color_equality() { - let color1 = Color { - red: 1.0, - green: 0.0, - blue: 0.0, - alpha: 0.0, - }; - let color2 = Color { - red: 1.0, - green: 0.0, - blue: 0.0, - alpha: 0.0, - }; - assert_eq!(color1, color2); -} - -#[test] -fn test_color_inequality() { - let color1 = Color { - red: 1.0, - green: 0.0, - blue: 0.0, - alpha: 0.0, - }; - let color2 = Color { - red: 0.0, - green: 1.0, - blue: 0.0, - alpha: 0.0, - }; - assert_ne!(color1, color2); -} diff --git a/crates/apple-note/tests/data/ZSERVERRECORDDATA.bin b/crates/apple-note/tests/data/ZSERVERRECORDDATA.bin deleted file mode 100644 index 47d861356d..0000000000 Binary files a/crates/apple-note/tests/data/ZSERVERRECORDDATA.bin and /dev/null differ diff --git a/crates/apple-note/tests/data/ZSERVERSHAREDATA.bin b/crates/apple-note/tests/data/ZSERVERSHAREDATA.bin deleted file mode 100644 index b5ca3a61b8..0000000000 Binary files a/crates/apple-note/tests/data/ZSERVERSHAREDATA.bin and /dev/null differ diff --git a/crates/apple-note/tests/data/block_quotes_gzipped.bin b/crates/apple-note/tests/data/block_quotes_gzipped.bin deleted file mode 100644 index 46e9ab0fae..0000000000 Binary files a/crates/apple-note/tests/data/block_quotes_gzipped.bin and /dev/null differ diff --git a/crates/apple-note/tests/data/color_formatting_gzipped.bin b/crates/apple-note/tests/data/color_formatting_gzipped.bin deleted file mode 100644 index 7ad97904d1..0000000000 Binary files a/crates/apple-note/tests/data/color_formatting_gzipped.bin and /dev/null differ diff --git a/crates/apple-note/tests/data/emoji_formatting_1_gzipped.bin b/crates/apple-note/tests/data/emoji_formatting_1_gzipped.bin deleted file mode 100644 index bf6fbb12ad..0000000000 Binary files a/crates/apple-note/tests/data/emoji_formatting_1_gzipped.bin and /dev/null differ diff --git a/crates/apple-note/tests/data/emoji_formatting_2_gzipped.bin b/crates/apple-note/tests/data/emoji_formatting_2_gzipped.bin deleted file mode 100644 index 2b82bcc655..0000000000 Binary files a/crates/apple-note/tests/data/emoji_formatting_2_gzipped.bin and /dev/null differ diff --git a/crates/apple-note/tests/data/emoji_formatting_3_gzipped.bin b/crates/apple-note/tests/data/emoji_formatting_3_gzipped.bin deleted file mode 100644 index 1690812af4..0000000000 Binary files a/crates/apple-note/tests/data/emoji_formatting_3_gzipped.bin and /dev/null differ diff --git a/crates/apple-note/tests/data/html_gzipped.bin b/crates/apple-note/tests/data/html_gzipped.bin deleted file mode 100644 index 56819eaacf..0000000000 Binary files a/crates/apple-note/tests/data/html_gzipped.bin and /dev/null differ diff --git a/crates/apple-note/tests/data/list_indents_gzipped.bin b/crates/apple-note/tests/data/list_indents_gzipped.bin deleted file mode 100644 index 9525512611..0000000000 Binary files a/crates/apple-note/tests/data/list_indents_gzipped.bin and /dev/null differ diff --git a/crates/apple-note/tests/data/right_to_left_table_gzipped.bin b/crates/apple-note/tests/data/right_to_left_table_gzipped.bin deleted file mode 100644 index 6b06b670d4..0000000000 Binary files a/crates/apple-note/tests/data/right_to_left_table_gzipped.bin and /dev/null differ diff --git a/crates/apple-note/tests/data/simple_note_protobuf.bin b/crates/apple-note/tests/data/simple_note_protobuf.bin deleted file mode 100644 index 9be70990e2..0000000000 Binary files a/crates/apple-note/tests/data/simple_note_protobuf.bin and /dev/null differ diff --git a/crates/apple-note/tests/data/simple_note_protobuf_gzipped.bin b/crates/apple-note/tests/data/simple_note_protobuf_gzipped.bin deleted file mode 100644 index a62f5dd639..0000000000 Binary files a/crates/apple-note/tests/data/simple_note_protobuf_gzipped.bin and /dev/null differ diff --git a/crates/apple-note/tests/data/table_formats_gzipped.bin b/crates/apple-note/tests/data/table_formats_gzipped.bin deleted file mode 100644 index b174cc1ebd..0000000000 Binary files a/crates/apple-note/tests/data/table_formats_gzipped.bin and /dev/null differ diff --git a/crates/apple-note/tests/data/table_gzipped.bin b/crates/apple-note/tests/data/table_gzipped.bin deleted file mode 100644 index ed86ad751f..0000000000 Binary files a/crates/apple-note/tests/data/table_gzipped.bin and /dev/null differ diff --git a/crates/apple-note/tests/data/text_decorations_gzipped.bin b/crates/apple-note/tests/data/text_decorations_gzipped.bin deleted file mode 100644 index 4b4f1f2895..0000000000 Binary files a/crates/apple-note/tests/data/text_decorations_gzipped.bin and /dev/null differ diff --git a/crates/apple-note/tests/data/url_gzipped.bin b/crates/apple-note/tests/data/url_gzipped.bin deleted file mode 100644 index e4a0b3f310..0000000000 Binary files a/crates/apple-note/tests/data/url_gzipped.bin and /dev/null differ diff --git a/crates/apple-note/tests/data/wide_characters_gzipped.bin b/crates/apple-note/tests/data/wide_characters_gzipped.bin deleted file mode 100644 index 3670c426e9..0000000000 Binary files a/crates/apple-note/tests/data/wide_characters_gzipped.bin and /dev/null differ diff --git a/crates/apple-note/tests/embedded_tests.rs b/crates/apple-note/tests/embedded_tests.rs deleted file mode 100644 index 56e2a33639..0000000000 --- a/crates/apple-note/tests/embedded_tests.rs +++ /dev/null @@ -1,352 +0,0 @@ -//! Embedded object tests -//! -//! These tests correspond to the embedded object and UTI tests in the apple_cloud_notes_parser Ruby implementation: -//! - https://github.com/threeplanetssoftware/apple_cloud_notes_parser/blob/master/spec/utilities/apple_uniform_type_identifier.rb -//! - https://github.com/threeplanetssoftware/apple_cloud_notes_parser/blob/master/spec/embedded_objects/embedded_objects.rb -//! -//! Tests cover UTI type detection, embedded object extraction, and type classification. - -use apple_note::{EmbeddedObjectType, extract_embedded_objects}; - -#[test] -fn test_embedded_object_type_detection() { - // Test various UTI type mappings - assert_eq!( - EmbeddedObjectType::from_uti("com.apple.notes.table"), - EmbeddedObjectType::Table - ); - assert_eq!( - EmbeddedObjectType::from_uti("com.apple.notes.ICTable"), - EmbeddedObjectType::Table - ); - assert_eq!( - EmbeddedObjectType::from_uti("public.image"), - EmbeddedObjectType::Image - ); - assert_eq!( - EmbeddedObjectType::from_uti("com.apple.drawing.2"), - EmbeddedObjectType::Drawing - ); - assert_eq!( - EmbeddedObjectType::from_uti("public.url"), - EmbeddedObjectType::URL - ); - assert_eq!( - EmbeddedObjectType::from_uti("public.audio"), - EmbeddedObjectType::Audio - ); - assert_eq!( - EmbeddedObjectType::from_uti("public.movie"), - EmbeddedObjectType::Video - ); - assert_eq!( - EmbeddedObjectType::from_uti("com.adobe.pdf"), - EmbeddedObjectType::PDF - ); - assert_eq!( - EmbeddedObjectType::from_uti("unknown.type"), - EmbeddedObjectType::Unknown - ); -} - -#[test] -fn test_embedded_object_type_detection_partial_match() { - // Test partial matching for image, video, audio, pdf - assert_eq!( - EmbeddedObjectType::from_uti("public.jpeg.image"), - EmbeddedObjectType::Image - ); - assert_eq!( - EmbeddedObjectType::from_uti("public.mpeg4.video"), - EmbeddedObjectType::Video - ); - assert_eq!( - EmbeddedObjectType::from_uti("public.mp3.audio"), - EmbeddedObjectType::Audio - ); - assert_eq!( - EmbeddedObjectType::from_uti("public.pdf.document"), - EmbeddedObjectType::PDF - ); -} - -#[test] -fn test_extract_embedded_objects_from_note() { - // Create a simple note with attachment info in attribute runs - // We'll use a real test file if available, otherwise just test the logic - use apple_note::proto::{AttachmentInfo, AttributeRun, Note}; - - let mut note = Note { - note_text: "Test text with attachment\u{FFFC}".to_string(), - attribute_run: vec![], - }; - - // Add an attribute run without attachment - note.attribute_run.push(AttributeRun { - length: 25, - paragraph_style: None, - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }); - - // Add an attribute run with a table attachment - note.attribute_run.push(AttributeRun { - length: 1, - paragraph_style: None, - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: Some(AttachmentInfo { - attachment_identifier: Some("test-uuid-123".to_string()), - type_uti: Some("com.apple.notes.table".to_string()), - }), - unknown_identifier: None, - emphasis_style: None, - }); - - let objects = extract_embedded_objects(¬e); - - assert_eq!(objects.len(), 1); - assert_eq!(objects[0].object_type, EmbeddedObjectType::Table); - assert_eq!(objects[0].uuid, "test-uuid-123"); - assert_eq!(objects[0].type_uti, "com.apple.notes.table"); -} - -#[test] -fn test_extract_multiple_embedded_objects() { - use apple_note::proto::{AttachmentInfo, AttributeRun, Note}; - - let mut note = Note { - note_text: "Text\u{FFFC}more text\u{FFFC}".to_string(), - attribute_run: vec![], - }; - - // Add text run - note.attribute_run.push(AttributeRun { - length: 4, - paragraph_style: None, - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }); - - // Add table attachment - note.attribute_run.push(AttributeRun { - length: 1, - paragraph_style: None, - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: Some(AttachmentInfo { - attachment_identifier: Some("table-uuid".to_string()), - type_uti: Some("com.apple.notes.table".to_string()), - }), - unknown_identifier: None, - emphasis_style: None, - }); - - // Add more text - note.attribute_run.push(AttributeRun { - length: 9, - paragraph_style: None, - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }); - - // Add image attachment - note.attribute_run.push(AttributeRun { - length: 1, - paragraph_style: None, - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: Some(AttachmentInfo { - attachment_identifier: Some("image-uuid".to_string()), - type_uti: Some("public.image".to_string()), - }), - unknown_identifier: None, - emphasis_style: None, - }); - - let objects = extract_embedded_objects(¬e); - - assert_eq!(objects.len(), 2); - assert_eq!(objects[0].object_type, EmbeddedObjectType::Table); - assert_eq!(objects[0].uuid, "table-uuid"); - assert_eq!(objects[1].object_type, EmbeddedObjectType::Image); - assert_eq!(objects[1].uuid, "image-uuid"); -} - -// Additional tests based on Ruby apple_uniform_type_identifier.rb spec - -#[test] -fn test_uti_refuses_non_string() { - // In Rust, this is enforced by the type system, so we test with empty/invalid strings - // instead of non-String types - let uti_type = EmbeddedObjectType::from_uti(""); - assert_eq!(uti_type, EmbeddedObjectType::Unknown); -} - -#[test] -fn test_uti_identifies_unknown_type() { - let uti_type = EmbeddedObjectType::from_uti("thisisamadeuputi"); - assert_eq!(uti_type, EmbeddedObjectType::Unknown); -} - -#[test] -fn test_uti_recognizes_public_types() { - // Ruby test: recognizes 'public' UTIs - assert_eq!( - EmbeddedObjectType::from_uti("public.thisisamadeuputi"), - EmbeddedObjectType::Unknown - ); - - // Known public types should be recognized correctly - assert_eq!( - EmbeddedObjectType::from_uti("public.image"), - EmbeddedObjectType::Image - ); - assert_eq!( - EmbeddedObjectType::from_uti("public.audio"), - EmbeddedObjectType::Audio - ); - assert_eq!( - EmbeddedObjectType::from_uti("public.movie"), - EmbeddedObjectType::Video - ); -} - -#[test] -fn test_uti_recognizes_dynamic_types() { - // Ruby test: recognizes dynamic UTIs (dyn.* prefix) - // Dynamic UTIs should fall into Unknown category - let uti_type = EmbeddedObjectType::from_uti("dyn.thisisamadeuputi"); - assert_eq!(uti_type, EmbeddedObjectType::Unknown); - - let uti_type2 = EmbeddedObjectType::from_uti("dyn.aghsjdgsa"); - assert_eq!(uti_type2, EmbeddedObjectType::Unknown); -} - -#[test] -fn test_uti_handles_apple_specific_types() { - assert_eq!( - EmbeddedObjectType::from_uti("com.apple.notes.table"), - EmbeddedObjectType::Table - ); - assert_eq!( - EmbeddedObjectType::from_uti("com.apple.drawing.2"), - EmbeddedObjectType::Drawing - ); - assert_eq!( - EmbeddedObjectType::from_uti("com.apple.paper"), - EmbeddedObjectType::Document - ); - assert_eq!( - EmbeddedObjectType::from_uti("com.apple.notes.gallery"), - EmbeddedObjectType::Gallery - ); -} - -#[test] -fn test_uti_handles_third_party_types() { - assert_eq!( - EmbeddedObjectType::from_uti("com.adobe.pdf"), - EmbeddedObjectType::PDF - ); -} - -#[test] -fn test_embedded_object_new() { - let obj = apple_note::EmbeddedObject::new( - EmbeddedObjectType::Table, - "test-uuid".to_string(), - "com.apple.notes.table".to_string(), - ); - - assert_eq!(obj.object_type, EmbeddedObjectType::Table); - assert_eq!(obj.uuid, "test-uuid"); - assert_eq!(obj.type_uti, "com.apple.notes.table"); -} - -#[test] -fn test_embedded_object_clone() { - let obj = apple_note::EmbeddedObject::new( - EmbeddedObjectType::Image, - "image-123".to_string(), - "public.image".to_string(), - ); - - let cloned = obj.clone(); - assert_eq!(obj, cloned); - assert_eq!(obj.uuid, cloned.uuid); - assert_eq!(obj.type_uti, cloned.type_uti); -} - -#[test] -fn test_embedded_object_equality() { - let obj1 = apple_note::EmbeddedObject::new( - EmbeddedObjectType::Table, - "uuid-1".to_string(), - "com.apple.notes.table".to_string(), - ); - - let obj2 = apple_note::EmbeddedObject::new( - EmbeddedObjectType::Table, - "uuid-1".to_string(), - "com.apple.notes.table".to_string(), - ); - - assert_eq!(obj1, obj2); -} - -#[test] -fn test_embedded_object_inequality() { - let obj1 = apple_note::EmbeddedObject::new( - EmbeddedObjectType::Table, - "uuid-1".to_string(), - "com.apple.notes.table".to_string(), - ); - - let obj2 = apple_note::EmbeddedObject::new( - EmbeddedObjectType::Image, - "uuid-2".to_string(), - "public.image".to_string(), - ); - - assert_ne!(obj1, obj2); -} diff --git a/crates/apple-note/tests/parser_tests.rs b/crates/apple-note/tests/parser_tests.rs deleted file mode 100644 index 7c219be172..0000000000 --- a/crates/apple-note/tests/parser_tests.rs +++ /dev/null @@ -1,589 +0,0 @@ -//! Parser tests -//! -//! These tests correspond to the AppleNote tests in the apple_cloud_notes_parser Ruby implementation: -//! https://github.com/threeplanetssoftware/apple_cloud_notes_parser/blob/master/spec/base_classes/apple_note.rb -//! -//! Tests cover: -//! - Note parsing (simple notes, compressed/uncompressed) -//! - Text decorations (bold, italic, underline, strikethrough) -//! - Block quotes and list indents -//! - URL/link formatting -//! - Color formatting -//! - Emoji and wide character handling -//! - HTML content escaping -//! - Various heading and style types - -use apple_note::{note_to_markdown, parse_note_store_proto}; -use std::fs; - -#[test] -fn test_simple_note_parsing() { - let data = - fs::read("tests/data/simple_note_protobuf_gzipped.bin").expect("Failed to read test data"); - let proto = parse_note_store_proto(&data).expect("Failed to parse proto"); - - assert!(proto.document.version >= 0); - assert!(!proto.document.note.note_text.is_empty()); -} - -#[test] -fn test_simple_note_parsing_uncompressed() { - let data = fs::read("tests/data/simple_note_protobuf.bin").expect("Failed to read test data"); - let proto = parse_note_store_proto(&data).expect("Failed to parse proto"); - - assert!(proto.document.version >= 0); - assert!(!proto.document.note.note_text.is_empty()); -} - -#[test] -fn test_text_decorations() { - let data = - fs::read("tests/data/text_decorations_gzipped.bin").expect("Failed to read test data"); - let proto = parse_note_store_proto(&data).expect("Failed to parse proto"); - - // The note should have text and attribute runs - assert!(!proto.document.note.note_text.is_empty()); - assert!(!proto.document.note.attribute_run.is_empty()); - - // Convert to markdown to verify formatting - let markdown = note_to_markdown(&proto.document.note); - assert!(!markdown.is_empty()); -} - -#[test] -fn test_block_quotes() { - let data = fs::read("tests/data/block_quotes_gzipped.bin").expect("Failed to read test data"); - let proto = parse_note_store_proto(&data).expect("Failed to parse proto"); - - assert!(!proto.document.note.note_text.is_empty()); - - // Convert to markdown - should contain block quote markers - let markdown = note_to_markdown(&proto.document.note); - assert!(markdown.contains('>')); -} - -#[test] -fn test_list_indents() { - let data = fs::read("tests/data/list_indents_gzipped.bin").expect("Failed to read test data"); - let proto = parse_note_store_proto(&data).expect("Failed to parse proto"); - - assert!(!proto.document.note.note_text.is_empty()); - assert!(!proto.document.note.attribute_run.is_empty()); - - // Check that some attribute runs have indent amounts - let has_indents = proto.document.note.attribute_run.iter().any(|run| { - run.paragraph_style - .as_ref() - .and_then(|ps| ps.indent_amount) - .is_some() - }); - assert!(has_indents); -} - -#[test] -fn test_url_formatting() { - let data = fs::read("tests/data/url_gzipped.bin").expect("Failed to read test data"); - let proto = parse_note_store_proto(&data).expect("Failed to parse proto"); - - assert!(!proto.document.note.note_text.is_empty()); - - // Check that some attribute runs have links - let has_links = proto - .document - .note - .attribute_run - .iter() - .any(|run| run.link.is_some()); - assert!(has_links); - - // Convert to markdown - should contain markdown links - let markdown = note_to_markdown(&proto.document.note); - assert!(markdown.contains("](")); -} - -#[test] -fn test_color_formatting() { - let data = - fs::read("tests/data/color_formatting_gzipped.bin").expect("Failed to read test data"); - let proto = parse_note_store_proto(&data).expect("Failed to parse proto"); - - assert!(!proto.document.note.note_text.is_empty()); - - // Check that some attribute runs have colors - let has_colors = proto - .document - .note - .attribute_run - .iter() - .any(|run| run.color.is_some()); - assert!(has_colors); -} - -#[test] -fn test_emoji_formatting() { - let data = - fs::read("tests/data/emoji_formatting_1_gzipped.bin").expect("Failed to read test data"); - let proto = parse_note_store_proto(&data).expect("Failed to parse proto"); - - assert!(!proto.document.note.note_text.is_empty()); - - // Emojis should be preserved in the text - // Check that we can parse the note successfully - let markdown = note_to_markdown(&proto.document.note); - assert!(!markdown.is_empty()); -} - -#[test] -fn test_wide_characters() { - let data = - fs::read("tests/data/wide_characters_gzipped.bin").expect("Failed to read test data"); - let proto = parse_note_store_proto(&data).expect("Failed to parse proto"); - - assert!(!proto.document.note.note_text.is_empty()); - - // Wide characters should be preserved - let markdown = note_to_markdown(&proto.document.note); - assert!(!markdown.is_empty()); -} - -#[test] -fn test_html_content() { - let data = fs::read("tests/data/html_gzipped.bin").expect("Failed to read test data"); - let proto = parse_note_store_proto(&data).expect("Failed to parse proto"); - - assert!(!proto.document.note.note_text.is_empty()); - - // Should successfully parse notes with HTML-like content - let markdown = note_to_markdown(&proto.document.note); - assert!(!markdown.is_empty()); -} - -// Additional tests based on Ruby spec to ensure proper handling of various note types - -#[test] -fn test_title_heading_subheading_conversion() { - // Test that different heading levels are properly converted to markdown - use apple_note::proto::{AttributeRun, Note, ParagraphStyle}; - use apple_note::{ - STYLE_TYPE_HEADING, STYLE_TYPE_SUBHEADING, STYLE_TYPE_TITLE, note_to_markdown, - }; - - // Title (h1) - let title_note = Note { - note_text: "Title Text".to_string(), - attribute_run: vec![AttributeRun { - length: 10, - paragraph_style: Some(ParagraphStyle { - style_type: Some(STYLE_TYPE_TITLE), - alignment: None, - indent_amount: None, - checklist: None, - block_quote: None, - }), - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }], - }; - let markdown = note_to_markdown(&title_note); - assert!(markdown.contains("# "), "Title should convert to # (h1)"); - - // Heading (h2) - let heading_note = Note { - note_text: "Heading Text".to_string(), - attribute_run: vec![AttributeRun { - length: 12, - paragraph_style: Some(ParagraphStyle { - style_type: Some(STYLE_TYPE_HEADING), - alignment: None, - indent_amount: None, - checklist: None, - block_quote: None, - }), - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }], - }; - let markdown = note_to_markdown(&heading_note); - assert!( - markdown.contains("## "), - "Heading should convert to ## (h2)" - ); - - // Subheading (h3) - let subheading_note = Note { - note_text: "Subheading".to_string(), - attribute_run: vec![AttributeRun { - length: 10, - paragraph_style: Some(ParagraphStyle { - style_type: Some(STYLE_TYPE_SUBHEADING), - alignment: None, - indent_amount: None, - checklist: None, - block_quote: None, - }), - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }], - }; - let markdown = note_to_markdown(&subheading_note); - assert!( - markdown.contains("### "), - "Subheading should convert to ### (h3)" - ); -} - -#[test] -fn test_monospaced_code_block_conversion() { - use apple_note::proto::{AttributeRun, Note, ParagraphStyle}; - use apple_note::{STYLE_TYPE_MONOSPACED, note_to_markdown}; - - let code_note = Note { - note_text: "Code block content".to_string(), - attribute_run: vec![AttributeRun { - length: 18, - paragraph_style: Some(ParagraphStyle { - style_type: Some(STYLE_TYPE_MONOSPACED), - alignment: None, - indent_amount: None, - checklist: None, - block_quote: None, - }), - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }], - }; - - let markdown = note_to_markdown(&code_note); - assert!( - markdown.contains("```"), - "Monospaced should be wrapped in code blocks" - ); - assert!(markdown.contains("Code block content")); -} - -#[test] -fn test_checkbox_list_conversion() { - use apple_note::proto::{AttributeRun, Checklist, Note, ParagraphStyle}; - use apple_note::{STYLE_TYPE_CHECKBOX, note_to_markdown}; - - // Checked item - let checked_note = Note { - note_text: "Checked item".to_string(), - attribute_run: vec![AttributeRun { - length: 12, - paragraph_style: Some(ParagraphStyle { - style_type: Some(STYLE_TYPE_CHECKBOX), - alignment: None, - indent_amount: None, - checklist: Some(Checklist { - uuid: b"test-uuid".to_vec(), - done: 1, - }), - block_quote: None, - }), - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }], - }; - let markdown = note_to_markdown(&checked_note); - assert!(markdown.contains("- [x]"), "Checked item should show [x]"); - - // Unchecked item - let unchecked_note = Note { - note_text: "Unchecked item".to_string(), - attribute_run: vec![AttributeRun { - length: 14, - paragraph_style: Some(ParagraphStyle { - style_type: Some(STYLE_TYPE_CHECKBOX), - alignment: None, - indent_amount: None, - checklist: Some(Checklist { - uuid: b"test-uuid-2".to_vec(), - done: 0, - }), - block_quote: None, - }), - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }], - }; - let markdown = note_to_markdown(&unchecked_note); - assert!(markdown.contains("- [ ]"), "Unchecked item should show [ ]"); -} - -#[test] -fn test_list_types_conversion() { - use apple_note::proto::{AttributeRun, Note, ParagraphStyle}; - use apple_note::{ - STYLE_TYPE_DASHED_LIST, STYLE_TYPE_DOTTED_LIST, STYLE_TYPE_NUMBERED_LIST, note_to_markdown, - }; - - // Dotted list - let dotted_note = Note { - note_text: "Dotted item".to_string(), - attribute_run: vec![AttributeRun { - length: 11, - paragraph_style: Some(ParagraphStyle { - style_type: Some(STYLE_TYPE_DOTTED_LIST), - alignment: None, - indent_amount: None, - checklist: None, - block_quote: None, - }), - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }], - }; - let markdown = note_to_markdown(&dotted_note); - assert!( - markdown.contains("- "), - "Dotted list should use bullet points" - ); - - // Dashed list - let dashed_note = Note { - note_text: "Dashed item".to_string(), - attribute_run: vec![AttributeRun { - length: 11, - paragraph_style: Some(ParagraphStyle { - style_type: Some(STYLE_TYPE_DASHED_LIST), - alignment: None, - indent_amount: None, - checklist: None, - block_quote: None, - }), - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }], - }; - let markdown = note_to_markdown(&dashed_note); - assert!( - markdown.contains("- "), - "Dashed list should use bullet points" - ); - - // Numbered list - let numbered_note = Note { - note_text: "Numbered item".to_string(), - attribute_run: vec![AttributeRun { - length: 13, - paragraph_style: Some(ParagraphStyle { - style_type: Some(STYLE_TYPE_NUMBERED_LIST), - alignment: None, - indent_amount: None, - checklist: None, - block_quote: None, - }), - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }], - }; - let markdown = note_to_markdown(&numbered_note); - assert!(markdown.contains("1. "), "Numbered list should use numbers"); -} - -#[test] -fn test_indented_content() { - use apple_note::note_to_markdown; - use apple_note::proto::{AttributeRun, Note, ParagraphStyle}; - - let indented_note = Note { - note_text: "Indented text".to_string(), - attribute_run: vec![AttributeRun { - length: 13, - paragraph_style: Some(ParagraphStyle { - style_type: None, - alignment: None, - indent_amount: Some(2), // 2 levels of indentation - checklist: None, - block_quote: None, - }), - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }], - }; - - let markdown = note_to_markdown(&indented_note); - // Should have indentation (2 spaces per level = 4 spaces) - assert!( - markdown.contains(" ") || markdown.contains(" "), - "Should contain indentation" - ); -} - -#[test] -fn test_block_quote_with_indent() { - use apple_note::note_to_markdown; - use apple_note::proto::{AttributeRun, Note, ParagraphStyle}; - - let quote_note = Note { - note_text: "Quoted text".to_string(), - attribute_run: vec![AttributeRun { - length: 11, - paragraph_style: Some(ParagraphStyle { - style_type: None, - alignment: None, - indent_amount: Some(1), - checklist: None, - block_quote: Some(1), - }), - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }], - }; - - let markdown = note_to_markdown("e_note); - assert!( - markdown.contains(">"), - "Block quote should contain > marker" - ); -} - -#[test] -fn test_note_with_mixed_formatting() { - // This test mimics the Ruby test for handling multiple formatting types in one note - use apple_note::proto::{AttributeRun, Note}; - use apple_note::{FONT_TYPE_BOLD, FONT_TYPE_ITALIC, note_to_markdown}; - - let mixed_note = Note { - note_text: "Normal Bold Italic".to_string(), - attribute_run: vec![ - AttributeRun { - length: 7, // "Normal " - font_weight: None, - paragraph_style: None, - font: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }, - AttributeRun { - length: 5, // "Bold " - font_weight: Some(FONT_TYPE_BOLD), - paragraph_style: None, - font: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }, - AttributeRun { - length: 6, // "Italic" - font_weight: Some(FONT_TYPE_ITALIC), - paragraph_style: None, - font: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }, - ], - }; - - let markdown = note_to_markdown(&mixed_note); - assert!( - markdown.contains("**") || markdown.contains("*"), - "Should contain bold and/or italic markers" - ); -} diff --git a/crates/apple-note/tests/proto_tests.rs b/crates/apple-note/tests/proto_tests.rs deleted file mode 100644 index 1d92fe1693..0000000000 --- a/crates/apple-note/tests/proto_tests.rs +++ /dev/null @@ -1,544 +0,0 @@ -//! Proto type tests -//! -//! These tests correspond to the proto_patches tests in the apple_cloud_notes_parser Ruby implementation: -//! https://github.com/threeplanetssoftware/apple_cloud_notes_parser/blob/master/spec/base_classes/proto_patches.rb -//! -//! Tests cover Color hex string generation and AttributeRun style comparison. -//! Note: Some tests in this file overlap with color_tests.rs and attribute_run_tests.rs -//! for additional coverage of the proto module types. - -use apple_note::proto::{AttributeRun, Color, Font, ParagraphStyle}; - -#[cfg(test)] -mod color_tests { - use super::*; - - #[test] - fn test_red_hex_string() { - let color = Color { - red: 1.0, - green: 0.0, - blue: 0.0, - alpha: 0.0, - }; - assert_eq!(color.red_hex_string(), "FF"); - assert_eq!(color.green_hex_string(), "00"); - assert_eq!(color.blue_hex_string(), "00"); - assert_eq!(color.full_hex_string(), "#FF0000"); - } - - #[test] - fn test_zero_hex_string() { - let color = Color { - red: 0.0, - green: 0.0, - blue: 0.0, - alpha: 0.0, - }; - assert_eq!(color.red_hex_string(), "00"); - assert_eq!(color.green_hex_string(), "00"); - assert_eq!(color.blue_hex_string(), "00"); - assert_eq!(color.full_hex_string(), "#000000"); - } - - #[test] - fn test_blue_hex_string() { - let color = Color { - red: 0.0, - green: 0.0, - blue: 1.0, - alpha: 0.0, - }; - assert_eq!(color.red_hex_string(), "00"); - assert_eq!(color.green_hex_string(), "00"); - assert_eq!(color.blue_hex_string(), "FF"); - assert_eq!(color.full_hex_string(), "#0000FF"); - } - - #[test] - fn test_green_hex_string() { - let color = Color { - red: 0.0, - green: 1.0, - blue: 0.0, - alpha: 0.0, - }; - assert_eq!(color.red_hex_string(), "00"); - assert_eq!(color.green_hex_string(), "FF"); - assert_eq!(color.blue_hex_string(), "00"); - assert_eq!(color.full_hex_string(), "#00FF00"); - } -} - -#[cfg(test)] -mod attribute_run_tests { - use super::*; - - #[test] - fn test_same_style_no_style() { - let run1 = AttributeRun { - length: 1, - paragraph_style: None, - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - let run2 = AttributeRun { - length: 1, - paragraph_style: None, - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - assert!(run1.same_style(&run2)); - } - - #[test] - fn test_different_font_weight() { - let run1 = AttributeRun { - length: 1, - font_weight: Some(2), - paragraph_style: None, - font: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - let run2 = AttributeRun { - length: 1, - font_weight: Some(4), - paragraph_style: None, - font: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - assert!(!run1.same_style(&run2)); - } - - #[test] - fn test_different_underlined() { - let run1 = AttributeRun { - length: 1, - underlined: Some(2), - paragraph_style: None, - font: None, - font_weight: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - let run2 = AttributeRun { - length: 1, - underlined: Some(3), - paragraph_style: None, - font: None, - font_weight: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - assert!(!run1.same_style(&run2)); - } - - #[test] - fn test_different_strikethrough() { - let run1 = AttributeRun { - length: 1, - strikethrough: Some(2), - paragraph_style: None, - font: None, - font_weight: None, - underlined: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - let run2 = AttributeRun { - length: 1, - strikethrough: Some(3), - paragraph_style: None, - font: None, - font_weight: None, - underlined: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - assert!(!run1.same_style(&run2)); - } - - #[test] - fn test_different_superscript() { - let run1 = AttributeRun { - length: 1, - superscript: Some(2), - paragraph_style: None, - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - let run2 = AttributeRun { - length: 1, - superscript: Some(3), - paragraph_style: None, - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - assert!(!run1.same_style(&run2)); - } - - #[test] - fn test_different_link() { - let run1 = AttributeRun { - length: 1, - link: Some("https://google.com".to_string()), - paragraph_style: None, - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - let run2 = AttributeRun { - length: 1, - link: Some("http://yahoo.com".to_string()), - paragraph_style: None, - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - assert!(!run1.same_style(&run2)); - } - - #[test] - fn test_same_paragraph_style() { - let style1 = ParagraphStyle { - style_type: None, - alignment: None, - indent_amount: Some(1), - checklist: None, - block_quote: None, - }; - let style2 = ParagraphStyle { - style_type: None, - alignment: None, - indent_amount: Some(1), - checklist: None, - block_quote: None, - }; - let run1 = AttributeRun { - length: 1, - paragraph_style: Some(style1), - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - let run2 = AttributeRun { - length: 1, - paragraph_style: Some(style2), - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - assert!(run1.same_style(&run2)); - } - - #[test] - fn test_different_paragraph_style() { - let style1 = ParagraphStyle { - style_type: None, - alignment: None, - indent_amount: Some(1), - checklist: None, - block_quote: None, - }; - let style2 = ParagraphStyle { - style_type: None, - alignment: None, - indent_amount: Some(2), - checklist: None, - block_quote: None, - }; - let run1 = AttributeRun { - length: 1, - paragraph_style: Some(style1), - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - let run2 = AttributeRun { - length: 1, - paragraph_style: Some(style2), - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - assert!(!run1.same_style(&run2)); - } - - #[test] - fn test_same_font() { - let font1 = Font { - font_name: Some("Consolas".to_string()), - point_size: None, - font_hints: None, - }; - let font2 = Font { - font_name: Some("Consolas".to_string()), - point_size: None, - font_hints: None, - }; - let run1 = AttributeRun { - length: 1, - font: Some(font1), - paragraph_style: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - let run2 = AttributeRun { - length: 1, - font: Some(font2), - paragraph_style: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - assert!(run1.same_style(&run2)); - } - - #[test] - fn test_different_font() { - let font1 = Font { - font_name: Some("Consolas".to_string()), - point_size: None, - font_hints: None, - }; - let font2 = Font { - font_name: Some("Times New Roman".to_string()), - point_size: None, - font_hints: None, - }; - let run1 = AttributeRun { - length: 1, - font: Some(font1), - paragraph_style: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - let run2 = AttributeRun { - length: 1, - font: Some(font2), - paragraph_style: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - assert!(!run1.same_style(&run2)); - } - - #[test] - fn test_same_color() { - let color1 = Color { - red: 1.0, - green: 1.0, - blue: 1.0, - alpha: 1.0, - }; - let color2 = Color { - red: 1.0, - green: 1.0, - blue: 1.0, - alpha: 1.0, - }; - let run1 = AttributeRun { - length: 1, - color: Some(color1), - paragraph_style: None, - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - let run2 = AttributeRun { - length: 1, - color: Some(color2), - paragraph_style: None, - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - assert!(run1.same_style(&run2)); - } - - #[test] - fn test_different_color() { - let color1 = Color { - red: 1.0, - green: 1.0, - blue: 1.0, - alpha: 1.0, - }; - let color2 = Color { - red: 0.0, - green: 0.0, - blue: 0.0, - alpha: 0.0, - }; - let run1 = AttributeRun { - length: 1, - color: Some(color1), - paragraph_style: None, - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - let run2 = AttributeRun { - length: 1, - color: Some(color2), - paragraph_style: None, - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }; - assert!(!run1.same_style(&run2)); - } -} diff --git a/crates/apple-note/tests/table_tests.rs b/crates/apple-note/tests/table_tests.rs deleted file mode 100644 index 43d30d5ee9..0000000000 --- a/crates/apple-note/tests/table_tests.rs +++ /dev/null @@ -1,170 +0,0 @@ -//! Table tests -//! -//! These tests correspond to the table tests in the apple_cloud_notes_parser Ruby implementation: -//! https://github.com/threeplanetssoftware/apple_cloud_notes_parser/blob/master/spec/embedded_objects/tables.rb -//! -//! Tests cover table parsing, reconstruction, row ordering, RTL table support, and output generation. - -use apple_note::{parse_mergable_data_proto, table::Table, table::parse_table}; -use std::fs; - -#[test] -fn test_simple_table_parsing() { - let data = fs::read("tests/data/table_gzipped.bin").expect("Failed to read test data"); - let proto = parse_mergable_data_proto(&data).expect("Failed to parse proto"); - - // Table parsing may return None if the data structure doesn't match expected format - if let Some(table) = parse_table(&proto) { - assert_eq!(table.row_count(), 2); - assert_eq!(table.column_count(), 2); - - // Check table content - assert_eq!(table.rows[0][0], "Row 1 Column 1"); - assert_eq!(table.rows[0][1], "Row 1 Column 2"); - assert_eq!(table.rows[1][0], "Row 2 Column 1"); - assert_eq!(table.rows[1][1], "Row 2 Column 2"); - } -} - -#[test] -fn test_rectangular_table_parsing() { - let data = fs::read("tests/data/table_formats_gzipped.bin").expect("Failed to read test data"); - let proto = parse_mergable_data_proto(&data).expect("Failed to parse proto"); - - if let Some(table) = parse_table(&proto) { - assert_eq!(table.row_count(), 3); - assert_eq!(table.column_count(), 2); - } -} - -#[test] -fn test_right_to_left_table() { - let data = - fs::read("tests/data/right_to_left_table_gzipped.bin").expect("Failed to read test data"); - let proto = parse_mergable_data_proto(&data).expect("Failed to parse proto"); - - if let Some(table) = parse_table(&proto) { - // Check that RTL table has the correct content after direction reversal - assert_eq!(table.rows[0][1], "اول"); - assert_eq!(table.rows[1][0], "نهاية"); - } -} - -#[test] -fn test_table_with_formatting() { - let data = fs::read("tests/data/table_formats_gzipped.bin").expect("Failed to read test data"); - let proto = parse_mergable_data_proto(&data).expect("Failed to parse proto"); - - if let Some(table) = parse_table(&proto) { - // The table should contain text with formatting markers - // Note: The Rust implementation extracts plain text, not HTML formatted text - // So we're just checking that the table parses correctly - assert!(table.rows.len() > 0); - assert!(table.rows[0].len() > 0); - } -} - -#[test] -fn test_table_dimensions() { - let data = fs::read("tests/data/table_gzipped.bin").expect("Failed to read test data"); - let proto = parse_mergable_data_proto(&data).expect("Failed to parse proto"); - - if let Some(table) = parse_table(&proto) { - // Verify row and column counts match - assert_eq!(table.rows.len(), table.row_count()); - for row in &table.rows { - assert_eq!(row.len(), table.column_count()); - } - } -} - -#[test] -fn test_table_direction_field() { - let data = fs::read("tests/data/table_gzipped.bin").expect("Failed to read test data"); - let proto = parse_mergable_data_proto(&data).expect("Failed to parse proto"); - - if let Some(table) = parse_table(&proto) { - // Regular table should be left-to-right - assert_eq!(table.direction, "CRTableColumnDirectionLeftToRight"); - } -} - -#[test] -fn test_right_to_left_direction() { - let data = - fs::read("tests/data/right_to_left_table_gzipped.bin").expect("Failed to read test data"); - let proto = parse_mergable_data_proto(&data).expect("Failed to parse proto"); - - if let Some(table) = parse_table(&proto) { - // RTL table should be right-to-left - assert_eq!(table.direction, "CRTableColumnDirectionRightToLeft"); - } -} - -#[test] -fn test_empty_table_cells() { - let data = fs::read("tests/data/table_formats_gzipped.bin").expect("Failed to read test data"); - let proto = parse_mergable_data_proto(&data).expect("Failed to parse proto"); - - if let Some(table) = parse_table(&proto) { - // Check that we can handle empty cells - let has_empty = table - .rows - .iter() - .any(|row| row.iter().any(|cell| cell.is_empty())); - assert!(has_empty, "Table should have at least one empty cell"); - } -} - -#[test] -fn test_table_new() { - let table = Table::new(); - assert_eq!(table.row_count(), 0); - assert_eq!(table.column_count(), 0); - assert_eq!(table.direction, "CRTableColumnDirectionLeftToRight"); -} - -#[test] -fn test_table_default() { - let table = Table::default(); - assert_eq!(table.row_count(), 0); - assert_eq!(table.column_count(), 0); - assert_eq!(table.direction, "CRTableColumnDirectionLeftToRight"); -} - -#[test] -fn test_table_clone() { - let data = fs::read("tests/data/table_gzipped.bin").expect("Failed to read test data"); - let proto = parse_mergable_data_proto(&data).expect("Failed to parse proto"); - - if let Some(table) = parse_table(&proto) { - let cloned = table.clone(); - assert_eq!(table.rows, cloned.rows); - assert_eq!(table.direction, cloned.direction); - assert_eq!(table.row_count(), cloned.row_count()); - assert_eq!(table.column_count(), cloned.column_count()); - } -} - -#[test] -fn test_table_equality() { - let data = fs::read("tests/data/table_gzipped.bin").expect("Failed to read test data"); - let proto = parse_mergable_data_proto(&data).expect("Failed to parse proto"); - - if let Some(table1) = parse_table(&proto) { - let table2 = parse_table(&proto).expect("Second parse should also succeed"); - assert_eq!(table1, table2); - } -} - -#[test] -fn test_table_parse_returns_option() { - // Test that parse_table properly returns Option - let data = fs::read("tests/data/table_gzipped.bin").expect("Failed to read test data"); - let proto = parse_mergable_data_proto(&data).expect("Failed to parse proto"); - - // Should return Some or None without panicking - let result = parse_table(&proto); - // Just check it doesn't panic - value can be Some or None depending on data format - let _ = result.is_some(); -} diff --git a/crates/apple-note/tests/text_extraction_tests.rs b/crates/apple-note/tests/text_extraction_tests.rs deleted file mode 100644 index 40be485961..0000000000 --- a/crates/apple-note/tests/text_extraction_tests.rs +++ /dev/null @@ -1,424 +0,0 @@ -//! Text extraction tests -//! -//! These tests correspond to the content extraction tests in the apple_cloud_notes_parser Ruby implementation: -//! https://github.com/threeplanetssoftware/apple_cloud_notes_parser/blob/master/spec/base_classes/apple_note.rb -//! -//! Tests cover: -//! - Plaintext extraction from notes -//! - Text span extraction with formatting attributes -//! - Handling of attachments during extraction - -use apple_note::{ - AttributeRun, Note, ParagraphStyle, - extract::{TextSpan, extract_plaintext, extract_text_spans}, - parse_note_store_proto, -}; -use std::fs; - -#[test] -fn test_extract_plaintext_from_simple_note() { - let data = - fs::read("tests/data/simple_note_protobuf_gzipped.bin").expect("Failed to read test data"); - let proto = parse_note_store_proto(&data).expect("Failed to parse proto"); - let note = &proto.document.note; - - let text = extract_plaintext(note); - assert!(!text.is_empty(), "Plain text should not be empty"); -} - -#[test] -fn test_extract_text_spans_basic() { - let note = Note { - note_text: "Hello World".to_string(), - attribute_run: vec![AttributeRun { - length: 11, - paragraph_style: None, - font: None, - font_weight: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }], - }; - - let spans = extract_text_spans(¬e); - assert_eq!(spans.len(), 1); - assert_eq!(spans[0].text, "Hello World"); - assert!(!spans[0].bold); - assert!(!spans[0].italic); - assert!(!spans[0].underline); -} - -#[test] -fn test_extract_text_spans_with_bold() { - let note = Note { - note_text: "Bold text".to_string(), - attribute_run: vec![AttributeRun { - length: 9, - font_weight: Some(1), - paragraph_style: None, - font: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }], - }; - - let spans = extract_text_spans(¬e); - assert_eq!(spans.len(), 1); - assert_eq!(spans[0].text, "Bold text"); - assert!(spans[0].bold); - assert!(!spans[0].italic); -} - -#[test] -fn test_extract_text_spans_with_italic() { - let note = Note { - note_text: "Italic text".to_string(), - attribute_run: vec![AttributeRun { - length: 11, - font_weight: Some(2), - paragraph_style: None, - font: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }], - }; - - let spans = extract_text_spans(¬e); - assert_eq!(spans.len(), 1); - assert_eq!(spans[0].text, "Italic text"); - assert!(!spans[0].bold); - assert!(spans[0].italic); -} - -#[test] -fn test_extract_text_spans_with_bold_italic() { - let note = Note { - note_text: "Bold italic".to_string(), - attribute_run: vec![AttributeRun { - length: 11, - font_weight: Some(3), - paragraph_style: None, - font: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }], - }; - - let spans = extract_text_spans(¬e); - assert_eq!(spans.len(), 1); - assert_eq!(spans[0].text, "Bold italic"); - assert!(spans[0].bold); - assert!(spans[0].italic); -} - -#[test] -fn test_extract_text_spans_with_underline() { - let note = Note { - note_text: "Underlined".to_string(), - attribute_run: vec![AttributeRun { - length: 10, - underlined: Some(1), - font_weight: None, - paragraph_style: None, - font: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }], - }; - - let spans = extract_text_spans(¬e); - assert_eq!(spans.len(), 1); - assert_eq!(spans[0].text, "Underlined"); - assert!(spans[0].underline); -} - -#[test] -fn test_extract_text_spans_with_strikethrough() { - let note = Note { - note_text: "Strikethrough".to_string(), - attribute_run: vec![AttributeRun { - length: 13, - strikethrough: Some(1), - font_weight: None, - paragraph_style: None, - font: None, - underlined: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }], - }; - - let spans = extract_text_spans(¬e); - assert_eq!(spans.len(), 1); - assert_eq!(spans[0].text, "Strikethrough"); - assert!(spans[0].strikethrough); -} - -#[test] -fn test_extract_text_spans_with_link() { - let note = Note { - note_text: "Click here".to_string(), - attribute_run: vec![AttributeRun { - length: 10, - link: Some("https://example.com".to_string()), - font_weight: None, - paragraph_style: None, - font: None, - underlined: None, - strikethrough: None, - superscript: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }], - }; - - let spans = extract_text_spans(¬e); - assert_eq!(spans.len(), 1); - assert_eq!(spans[0].text, "Click here"); - assert_eq!(spans[0].link, Some("https://example.com".to_string())); -} - -#[test] -fn test_extract_text_spans_multiple_runs() { - let note = Note { - note_text: "Normal Bold Italic".to_string(), - attribute_run: vec![ - AttributeRun { - length: 7, - font_weight: None, - paragraph_style: None, - font: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }, - AttributeRun { - length: 5, - font_weight: Some(1), - paragraph_style: None, - font: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }, - AttributeRun { - length: 6, - font_weight: Some(2), - paragraph_style: None, - font: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }, - ], - }; - - let spans = extract_text_spans(¬e); - assert_eq!(spans.len(), 3); - assert_eq!(spans[0].text, "Normal "); - assert!(!spans[0].bold); - assert_eq!(spans[1].text, "Bold "); - assert!(spans[1].bold); - assert_eq!(spans[2].text, "Italic"); - assert!(spans[2].italic); -} - -#[test] -fn test_extract_text_spans_with_paragraph_style() { - let style = ParagraphStyle { - style_type: Some(1), - alignment: None, - indent_amount: None, - checklist: None, - block_quote: None, - }; - - let note = Note { - note_text: "Styled text".to_string(), - attribute_run: vec![AttributeRun { - length: 11, - paragraph_style: Some(style.clone()), - font_weight: None, - font: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }], - }; - - let spans = extract_text_spans(¬e); - assert_eq!(spans.len(), 1); - assert_eq!(spans[0].text, "Styled text"); - assert_eq!(spans[0].style_type, Some(1)); - assert!(spans[0].paragraph_style.is_some()); -} - -#[test] -fn test_extract_text_spans_skips_attachments() { - // Use a simple text string to avoid Unicode boundary issues - let text = "Text.More text"; - let note = Note { - note_text: text.to_string(), - attribute_run: vec![ - AttributeRun { - length: 5, // "Text." - font_weight: None, - paragraph_style: None, - font: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }, - AttributeRun { - length: 0, // Attachment with zero length in text - font_weight: None, - paragraph_style: None, - font: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: Some(apple_note::AttachmentInfo { - attachment_identifier: Some("test".to_string()), - type_uti: None, - }), - unknown_identifier: None, - emphasis_style: None, - }, - AttributeRun { - length: 9, // "More text" - font_weight: None, - paragraph_style: None, - font: None, - underlined: None, - strikethrough: None, - superscript: None, - link: None, - color: None, - attachment_info: None, - unknown_identifier: None, - emphasis_style: None, - }, - ], - }; - - let spans = extract_text_spans(¬e); - // Attachments are skipped during extraction - assert!(spans.len() >= 2, "Should have at least 2 text spans"); - assert_eq!(spans[0].text, "Text."); -} - -#[test] -fn test_text_span_new() { - let span = TextSpan::new("Test text".to_string()); - assert_eq!(span.text, "Test text"); - assert!(!span.bold); - assert!(!span.italic); - assert!(!span.underline); - assert!(!span.strikethrough); - assert_eq!(span.link, None); - assert_eq!(span.style_type, None); - assert_eq!(span.paragraph_style, None); -} - -#[test] -fn test_text_span_clone() { - let span = TextSpan { - text: "Test".to_string(), - bold: true, - italic: true, - underline: false, - strikethrough: false, - link: Some("https://test.com".to_string()), - style_type: Some(1), - paragraph_style: None, - }; - - let cloned = span.clone(); - assert_eq!(span.text, cloned.text); - assert_eq!(span.bold, cloned.bold); - assert_eq!(span.italic, cloned.italic); - assert_eq!(span.link, cloned.link); -} - -#[test] -fn test_extract_from_real_file() { - let data = - fs::read("tests/data/simple_note_protobuf_gzipped.bin").expect("Failed to read test data"); - let proto = parse_note_store_proto(&data).expect("Failed to parse proto"); - let note = &proto.document.note; - - let spans = extract_text_spans(note); - assert!(!spans.is_empty(), "Should extract at least one text span"); - - let plaintext = extract_plaintext(note); - assert!(!plaintext.is_empty(), "Plain text should not be empty"); - - // Verify that concatenating all span texts equals the plaintext (minus attachments) - let concatenated: String = spans.iter().map(|s| s.text.as_str()).collect(); - assert!( - plaintext.contains(&concatenated) || concatenated.contains(&plaintext), - "Concatenated spans should match plaintext content" - ); -} diff --git a/crates/apple-note/tests/utils_tests.rs b/crates/apple-note/tests/utils_tests.rs deleted file mode 100644 index a954aa2493..0000000000 --- a/crates/apple-note/tests/utils_tests.rs +++ /dev/null @@ -1,199 +0,0 @@ -//! Utility function tests -//! -//! These tests correspond to the helper tests in the apple_cloud_notes_parser Ruby implementation: -//! https://github.com/threeplanetssoftware/apple_cloud_notes_parser/blob/master/spec/base_classes/apple_note.rb -//! -//! Tests cover: -//! - Core time to Unix time conversion (convert_core_time in Ruby) -//! - GZIP detection (is_gzip in Ruby) -//! - GZIP decompression - -use apple_note::utils::{core_time_to_unix, decompress_gzip, is_gzip}; -use std::fs; - -#[test] -fn test_core_time_conversion() { - // iOS Core Time epoch is 2001-01-01 00:00:00 UTC - // Unix epoch is 1970-01-01 00:00:00 UTC - // The offset is 978307200 seconds - - // Test zero (2001-01-01 00:00:00) - assert_eq!(core_time_to_unix(0), 978307200); - - // Test a known timestamp - // 608413790 in Core Time should be 1586721790 in Unix time - assert_eq!(core_time_to_unix(608413790), 1586720990); -} - -#[test] -fn test_core_time_negative() { - // Test negative values (before Core Time epoch) - let negative_time = -1000; - assert_eq!(core_time_to_unix(negative_time), 978307200 - 1000); -} - -#[test] -fn test_core_time_large_value() { - // Test large positive timestamp - let large_time = 1_000_000_000_i64; - assert_eq!(core_time_to_unix(large_time), 978307200 + 1_000_000_000); -} - -#[test] -fn test_core_time_boundary_values() { - // Test boundary values that don't overflow - // i64::MIN would overflow when adding the offset, so we test a safe minimum - let safe_min = i64::MIN + 978307200; - assert_eq!(core_time_to_unix(safe_min - 978307200), safe_min); - - // Test a large positive value that doesn't overflow - let large_value = i64::MAX - 978307200; - assert_eq!(core_time_to_unix(large_value), i64::MAX); -} - -#[test] -fn test_is_gzip_detection() { - // Test with gzipped file - let gzipped_data = - fs::read("tests/data/simple_note_protobuf_gzipped.bin").expect("Failed to read test data"); - assert!(is_gzip(&gzipped_data)); - - // Test with non-gzipped file - let plain_data = - fs::read("tests/data/simple_note_protobuf.bin").expect("Failed to read test data"); - assert!(!is_gzip(&plain_data)); - - // Test with empty data - assert!(!is_gzip(&[])); - - // Test with single byte - assert!(!is_gzip(&[0x1f])); -} - -#[test] -fn test_is_gzip_with_magic_bytes() { - // Test with correct magic bytes (requires > 2 bytes) - assert!(is_gzip(&[0x1f, 0x8b, 0x08])); - assert!(is_gzip(&[0x1f, 0x8b, 0x00, 0x00])); - - // Test with incorrect magic bytes - assert!(!is_gzip(&[0x1f, 0x8c, 0x08])); - assert!(!is_gzip(&[0x1e, 0x8b, 0x08])); - assert!(!is_gzip(&[0x00, 0x00, 0x00])); -} - -#[test] -fn test_is_gzip_exact_length() { - // Test with exactly 3 bytes (minimum for gzip detection, since impl requires > 2) - assert!(is_gzip(&[0x1f, 0x8b, 0x00])); - assert!(!is_gzip(&[0x1f, 0x00, 0x00])); - - // Test that 2 bytes is not enough - assert!(!is_gzip(&[0x1f, 0x8b])); -} - -#[test] -fn test_gzip_decompression() { - let gzipped_data = - fs::read("tests/data/simple_note_protobuf_gzipped.bin").expect("Failed to read test data"); - let plain_data = - fs::read("tests/data/simple_note_protobuf.bin").expect("Failed to read test data"); - - let decompressed = decompress_gzip(&gzipped_data).expect("Failed to decompress"); - - // The decompressed data should match the plain data - assert_eq!(decompressed, plain_data); -} - -#[test] -fn test_gzip_decompression_error() { - // Try to decompress non-gzipped data - let plain_data = - fs::read("tests/data/simple_note_protobuf.bin").expect("Failed to read test data"); - - let result = decompress_gzip(&plain_data); - assert!(result.is_err()); -} - -#[test] -fn test_gzip_decompression_empty() { - // Try to decompress empty data - let result = decompress_gzip(&[]); - assert!(result.is_err()); -} - -#[test] -fn test_gzip_decompression_multiple_files() { - // Test decompression with various gzipped test files - let files = [ - "tests/data/simple_note_protobuf_gzipped.bin", - "tests/data/table_gzipped.bin", - "tests/data/table_formats_gzipped.bin", - "tests/data/right_to_left_table_gzipped.bin", - ]; - - for file in &files { - let data = fs::read(file).expect(&format!("Failed to read {}", file)); - assert!(is_gzip(&data), "{} should be detected as gzip", file); - - let decompressed = decompress_gzip(&data).expect(&format!("Failed to decompress {}", file)); - assert!( - !decompressed.is_empty(), - "{} should decompress to non-empty data", - file - ); - - // Verify decompressed data is not gzipped - assert!( - !is_gzip(&decompressed), - "Decompressed data from {} should not be gzipped", - file - ); - } -} - -#[test] -fn test_gzip_roundtrip_check() { - let gzipped_data = - fs::read("tests/data/simple_note_protobuf_gzipped.bin").expect("Failed to read test data"); - - // Decompress once - let decompressed = decompress_gzip(&gzipped_data).expect("Failed to decompress"); - - // The decompressed data should not be gzipped - assert!(!is_gzip(&decompressed)); - - // Trying to decompress again should fail - let result = decompress_gzip(&decompressed); - assert!( - result.is_err(), - "Decompressing already decompressed data should fail" - ); -} - -#[test] -fn test_gzip_decompression_size() { - let gzipped_data = - fs::read("tests/data/simple_note_protobuf_gzipped.bin").expect("Failed to read test data"); - let plain_data = - fs::read("tests/data/simple_note_protobuf.bin").expect("Failed to read test data"); - - let decompressed = decompress_gzip(&gzipped_data).expect("Failed to decompress"); - - // Verify the sizes match - assert_eq!( - decompressed.len(), - plain_data.len(), - "Decompressed size should match original" - ); -} - -#[test] -fn test_core_time_consistency() { - // Test that conversion is consistent - let times = [0, 100, 1000, 10000, 608413790, 1000000000]; - for &time in × { - let unix = core_time_to_unix(time); - assert_eq!(unix, time + 978307200); - } -} diff --git a/crates/chrome-native-host/AGENTS.md b/crates/chrome-native-host/AGENTS.md deleted file mode 100644 index d0f3b3f9e6..0000000000 --- a/crates/chrome-native-host/AGENTS.md +++ /dev/null @@ -1,147 +0,0 @@ -# chrome-native-host - -Rust binary that bridges the Chrome extension to the Char desktop app. -It is registered as a Chrome Native Messaging host, so Chrome spawns it as a subprocess and communicates over stdio. - -## System diagram - -``` -Google Meet tab - │ chrome.runtime.sendMessage(payload) - ▼ -background.js (service worker) - │ port.postMessage(payload) [Chrome Native Messaging] - ▼ -char-chrome-native-host ← this binary - │ writes atomically - ▼ -chrome_state.json (on disk) - │ polled every 500 ms - ▼ -GoogleMeetWatcher (crates/detect) - │ fires DetectEvent - ▼ -Tauri plugin → React/Zustand -``` - -## Native Messaging wire protocol - -Chrome Native Messaging uses **stdin/stdout** with a simple framing: - -``` -┌─────────────────┬──────────────────────────────┐ -│ 4 bytes LE u32 │ N bytes UTF-8 JSON │ -│ (message len) │ (message body) │ -└─────────────────┴──────────────────────────────┘ -``` - -- Length is **little-endian unsigned 32-bit** integer. -- Body is a UTF-8 JSON object. -- The binary reads messages in a loop until EOF (Chrome closed the port). - -## Incoming message contract - -Two message types are sent by `background.js`: - -### `meeting_state` — meeting is active - -```json -{ - "type": "meeting_state", - "url": "https://meet.google.com/abc-defg-hij", - "is_active": true, - "muted": false, - "participants": [ - { "name": "Alice", "is_self": true }, - { "name": "Bob", "is_self": false } - ] -} -``` - -### `meeting_ended` — tab closed / navigated away - -```json -{ - "type": "meeting_ended", - "url": "https://meet.google.com/abc-defg-hij", - "is_active": false -} -``` - -Any unknown `type` is silently ignored. - -## Output: `chrome_state.json` - -Written to `{data_dir}/char/chrome_state.json` after every message. -`data_dir` is platform-specific (`dirs::data_dir()`): -- macOS: `~/Library/Application Support` -- Linux: `~/.local/share` -- Windows: `C:\Users\{user}\AppData\Roaming` - -**Schema:** - -```json -{ - "version": 1, - "timestamp_ms": 1700000000000, - "meeting": { - "url": "https://meet.google.com/abc-defg-hij", - "is_active": true, - "muted": false, - "participants": [ - { "name": "Alice", "is_self": true } - ] - } -} -``` - -`meeting` is `null` when the meeting has ended or `is_active` is false. - -**Write is atomic** — a `NamedTempFile` in the same directory is written and then renamed, -so the watcher never reads a partial file. - -`timestamp_ms` is set to the current Unix epoch milliseconds on every write. -The watcher in `crates/detect` treats state older than 30 seconds as stale. - -## Testable units - -| Function | What it does | How to test | -|---|---|---| -| `read_message` | Decodes framed bytes from a `Read` | Feed a `Cursor>` | -| `process_message` | Maps `IncomingMessage` → `Option` | Pure function, no I/O | -| `write_state` | Atomically writes JSON to a `Path` | Pass a `tempdir` path | -| `run` | Full loop: read → process → write | Pipe encoded messages through a `Cursor`, assert file contents | - -## Bundle configuration - -For production, the binary is included in the app bundle as a Tauri `externalBin` -(`tauri.conf.json`), placing it at `Contents/MacOS/char-chrome-native-host` next -to the main executable. - -**How `prepare-binaries.mjs` works (runs as `beforeBundleCommand`):** - -1. Reads `TAURI_ENV_TARGET_TRIPLE` — the env var Tauri sets to the actual build - target, not the host. This matters for cross-compilation: the CI macOS job - builds both `aarch64-apple-darwin` and `x86_64-apple-darwin` from a single - ARM runner, so host ≠ target on the x86_64 build. - -2. Runs `cargo build --release --target $triple -p chrome-native-host` with - `cwd` set to `apps/desktop/src-tauri/` so that `.cargo/config.toml`'s - `target-dir = "target"` applies. Binary lands at - `apps/desktop/src-tauri/target/$triple/release/char-chrome-native-host[.exe]`. - -3. Copies it to `binaries/char-chrome-native-host-$triple[.exe]`, which is what - `externalBin: ["binaries/char-chrome-native-host"]` expects. - -No CI steps or `build.rs` changes are needed — `beforeBundleCommand` runs inside -`tauri build` after the main Cargo step, so the cross-compilation toolchain is -already present. - -For development (`debug_assertions`), no bundling is needed — the runtime resolves -directly to `target/debug/char-chrome-native-host` built by Cargo as part of the workspace. - -## Running tests - -``` -cargo test --package chrome-native-host -``` diff --git a/crates/chrome-native-host/Cargo.toml b/crates/chrome-native-host/Cargo.toml deleted file mode 100644 index 02bbf74410..0000000000 --- a/crates/chrome-native-host/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "chrome-native-host" -version = "0.1.0" -edition = "2024" - -[[bin]] -name = "char-chrome-native-host" -path = "src/main.rs" - -[dependencies] -dirs = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -tempfile = { workspace = true } diff --git a/crates/chrome-native-host/src/main.rs b/crates/chrome-native-host/src/main.rs deleted file mode 100644 index 6f6c855aef..0000000000 --- a/crates/chrome-native-host/src/main.rs +++ /dev/null @@ -1,487 +0,0 @@ -use std::io::{self, Read, Write}; -use std::path::{Path, PathBuf}; - -use serde::{Deserialize, Serialize}; - -const MAX_MESSAGE_SIZE: usize = 256 * 1024; -const MAX_URL_LENGTH: usize = 2048; -const MAX_PARTICIPANTS: usize = 30; -const MAX_PARTICIPANT_NAME_LENGTH: usize = 80; - -#[derive(Debug, Deserialize)] -struct IncomingMessage { - #[serde(rename = "type")] - msg_type: String, - url: Option, - is_active: Option, - muted: Option, - participants: Option>, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct Participant { - pub name: String, - pub is_self: bool, -} - -#[derive(Debug, Serialize, Deserialize)] -struct ChromeState { - version: u32, - timestamp_ms: u64, - meeting: Option, -} - -#[derive(Debug, Serialize, Deserialize, PartialEq)] -struct MeetingState { - url: String, - is_active: bool, - muted: bool, - participants: Vec, -} - -fn default_state_path() -> io::Result { - let data_dir = dirs::data_dir() - .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "data directory not found"))?; - - Ok(data_dir.join("char").join("chrome_state.json")) -} - -fn read_message(reader: &mut impl Read) -> io::Result>> { - let mut len_buf = [0u8; 4]; - match reader.read_exact(&mut len_buf) { - Ok(()) => {} - Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None), - Err(e) => return Err(e), - } - let len = u32::from_le_bytes(len_buf) as usize; - if len > MAX_MESSAGE_SIZE { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("message too large: {len}"), - )); - } - - let mut buf = vec![0u8; len]; - reader.read_exact(&mut buf)?; - Ok(Some(buf)) -} - -#[derive(Debug, PartialEq)] -enum ProcessedMessage { - Ignore, - Update(Option), -} - -fn normalize_url(url: Option) -> Option { - let value = url?.trim().to_owned(); - if value.is_empty() || value.len() > MAX_URL_LENGTH { - return None; - } - - if !value.starts_with("https://meet.google.com/") { - return None; - } - - Some(value) -} - -fn normalize_participants(participants: Option>) -> Vec { - participants - .unwrap_or_default() - .into_iter() - .filter_map(|participant| { - let name = participant.name.trim(); - if name.is_empty() || name.len() > MAX_PARTICIPANT_NAME_LENGTH { - return None; - } - - Some(Participant { - name: name.to_owned(), - is_self: participant.is_self, - }) - }) - .take(MAX_PARTICIPANTS) - .collect() -} - -fn process_message(msg: IncomingMessage) -> ProcessedMessage { - match msg.msg_type.as_str() { - "meeting_state" => { - if !msg.is_active.unwrap_or(false) { - return ProcessedMessage::Update(None); - } - - let Some(url) = normalize_url(msg.url) else { - return ProcessedMessage::Ignore; - }; - - ProcessedMessage::Update(Some(MeetingState { - url, - is_active: true, - muted: msg.muted.unwrap_or(false), - participants: normalize_participants(msg.participants), - })) - } - "meeting_ended" => { - if msg.is_active == Some(true) { - return ProcessedMessage::Ignore; - } - - ProcessedMessage::Update(None) - } - _ => ProcessedMessage::Ignore, - } -} - -fn write_state(state: &ChromeState, path: &Path) -> io::Result<()> { - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent)?; - } - let dir = path.parent().unwrap(); - let mut tmp = tempfile::NamedTempFile::new_in(dir)?; - serde_json::to_writer(&mut tmp, state)?; - tmp.as_file_mut().flush()?; - tmp.persist(path).map_err(|e| e.error)?; - Ok(()) -} - -fn timestamp_ms() -> u64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_millis() as u64 -} - -fn run(reader: &mut impl Read, state_path: &Path) { - loop { - match read_message(reader) { - Ok(Some(data)) => { - let msg: IncomingMessage = match serde_json::from_slice(&data) { - Ok(m) => m, - Err(_) => continue, - }; - - let meeting = match process_message(msg) { - ProcessedMessage::Ignore => continue, - ProcessedMessage::Update(meeting) => meeting, - }; - - let state = ChromeState { - version: 1, - timestamp_ms: timestamp_ms(), - meeting, - }; - - if let Err(e) = write_state(&state, state_path) { - eprintln!("failed to write state: {e}"); - } - } - Ok(None) => break, - Err(e) => { - eprintln!("error reading message: {e}"); - break; - } - } - } -} - -fn main() { - let mut stdin = io::stdin().lock(); - let state_path = match default_state_path() { - Ok(path) => path, - Err(error) => { - eprintln!("failed to resolve state path: {error}"); - return; - } - }; - - run(&mut stdin, &state_path); -} - -#[cfg(test)] -mod tests { - use super::*; - use std::io::Cursor; - - fn encode_message(json: &str) -> Vec { - let bytes = json.as_bytes(); - let len = bytes.len() as u32; - let mut buf = Vec::with_capacity(4 + bytes.len()); - buf.extend_from_slice(&len.to_le_bytes()); - buf.extend_from_slice(bytes); - buf - } - - // --- read_message --- - - #[test] - fn test_read_message_valid() { - let payload = r#"{"type":"meeting_state"}"#; - let encoded = encode_message(payload); - let mut cursor = Cursor::new(encoded); - let result = read_message(&mut cursor).unwrap(); - assert_eq!(result, Some(payload.as_bytes().to_vec())); - } - - #[test] - fn test_read_message_eof_returns_none() { - let mut cursor = Cursor::new(vec![]); - let result = read_message(&mut cursor).unwrap(); - assert!(result.is_none()); - } - - #[test] - fn test_read_message_truncated_body_errors() { - // 4-byte header says 10 bytes but body has only 3 - let mut buf = vec![]; - buf.extend_from_slice(&10u32.to_le_bytes()); - buf.extend_from_slice(b"abc"); - let mut cursor = Cursor::new(buf); - assert!(read_message(&mut cursor).is_err()); - } - - #[test] - fn test_read_message_rejects_oversized_message() { - let mut buf = vec![]; - buf.extend_from_slice(&((MAX_MESSAGE_SIZE + 1) as u32).to_le_bytes()); - let mut cursor = Cursor::new(buf); - assert!(read_message(&mut cursor).is_err()); - } - - // --- process_message --- - - #[test] - fn test_process_meeting_state_active() { - let msg = IncomingMessage { - msg_type: "meeting_state".into(), - url: Some("https://meet.google.com/abc".into()), - is_active: Some(true), - muted: Some(true), - participants: Some(vec![Participant { - name: "Alice".into(), - is_self: true, - }]), - }; - let result = process_message(msg); - match result { - ProcessedMessage::Update(Some(meeting)) => { - assert!(meeting.is_active); - assert!(meeting.muted); - assert_eq!(meeting.participants.len(), 1); - assert_eq!(meeting.url, "https://meet.google.com/abc"); - } - _ => panic!("expected active meeting state"), - } - } - - #[test] - fn test_process_meeting_ended() { - let msg = IncomingMessage { - msg_type: "meeting_ended".into(), - url: None, - is_active: Some(false), - muted: None, - participants: None, - }; - assert_eq!(process_message(msg), ProcessedMessage::Update(None)); - } - - #[test] - fn test_process_meeting_state_inactive_flag() { - let msg = IncomingMessage { - msg_type: "meeting_state".into(), - url: Some("https://meet.google.com/abc".into()), - is_active: Some(false), - muted: Some(false), - participants: None, - }; - assert_eq!(process_message(msg), ProcessedMessage::Update(None)); - } - - #[test] - fn test_process_defaults_muted_false() { - let msg = IncomingMessage { - msg_type: "meeting_state".into(), - url: Some("https://meet.google.com/abc".into()), - is_active: Some(true), - muted: None, - participants: None, - }; - let result = process_message(msg); - match result { - ProcessedMessage::Update(Some(meeting)) => { - assert!(!meeting.muted); - } - _ => panic!("expected active meeting state"), - } - } - - #[test] - fn test_process_unknown_type_is_ignored() { - let msg = IncomingMessage { - msg_type: "unknown".into(), - url: Some("https://meet.google.com/abc".into()), - is_active: Some(true), - muted: Some(false), - participants: None, - }; - assert_eq!(process_message(msg), ProcessedMessage::Ignore); - } - - #[test] - fn test_process_invalid_url_is_ignored() { - let msg = IncomingMessage { - msg_type: "meeting_state".into(), - url: Some("https://example.com/abc".into()), - is_active: Some(true), - muted: Some(false), - participants: None, - }; - assert_eq!(process_message(msg), ProcessedMessage::Ignore); - } - - #[test] - fn test_process_participants_are_sanitized() { - let msg = IncomingMessage { - msg_type: "meeting_state".into(), - url: Some("https://meet.google.com/abc".into()), - is_active: Some(true), - muted: Some(false), - participants: Some(vec![ - Participant { - name: " Alice ".into(), - is_self: false, - }, - Participant { - name: " ".into(), - is_self: false, - }, - ]), - }; - - let result = process_message(msg); - match result { - ProcessedMessage::Update(Some(meeting)) => { - assert_eq!(meeting.participants.len(), 1); - assert_eq!(meeting.participants[0].name, "Alice"); - } - _ => panic!("expected active meeting state"), - } - } - - // --- write_state + full round-trip --- - - #[test] - fn test_write_state_creates_valid_json() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("state.json"); - - let state = ChromeState { - version: 1, - timestamp_ms: 1000, - meeting: Some(MeetingState { - url: "https://meet.google.com/test".into(), - is_active: true, - muted: false, - participants: vec![], - }), - }; - - write_state(&state, &path).unwrap(); - - let contents = std::fs::read_to_string(&path).unwrap(); - let parsed: serde_json::Value = serde_json::from_str(&contents).unwrap(); - assert_eq!(parsed["version"], 1); - assert_eq!(parsed["meeting"]["is_active"], true); - } - - #[test] - fn test_write_state_creates_parent_dirs() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("nested").join("dirs").join("state.json"); - - let state = ChromeState { - version: 1, - timestamp_ms: 0, - meeting: None, - }; - write_state(&state, &path).unwrap(); - - assert!(path.exists()); - } - - #[test] - fn test_run_meeting_state_message() { - let dir = tempfile::tempdir().unwrap(); - let state_path = dir.path().join("chrome_state.json"); - - let msg = r#"{"type":"meeting_state","url":"https://meet.google.com/xyz","is_active":true,"muted":false,"participants":[{"name":"Bob","is_self":false}]}"#; - let input = encode_message(msg); - let mut cursor = Cursor::new(input); - - run(&mut cursor, &state_path); - - let contents = std::fs::read_to_string(&state_path).unwrap(); - let parsed: serde_json::Value = serde_json::from_str(&contents).unwrap(); - assert_eq!(parsed["meeting"]["url"], "https://meet.google.com/xyz"); - assert_eq!(parsed["meeting"]["participants"][0]["name"], "Bob"); - } - - #[test] - fn test_run_meeting_ended_clears_meeting() { - let dir = tempfile::tempdir().unwrap(); - let state_path = dir.path().join("chrome_state.json"); - - let active = r#"{"type":"meeting_state","is_active":true,"muted":false}"#; - let ended = r#"{"type":"meeting_ended","is_active":false}"#; - - let mut input = encode_message(active); - input.extend(encode_message(ended)); - - let mut cursor = Cursor::new(input); - run(&mut cursor, &state_path); - - let contents = std::fs::read_to_string(&state_path).unwrap(); - let parsed: serde_json::Value = serde_json::from_str(&contents).unwrap(); - assert!(parsed["meeting"].is_null()); - } - - #[test] - fn test_run_invalid_json_is_skipped() { - let dir = tempfile::tempdir().unwrap(); - let state_path = dir.path().join("chrome_state.json"); - - let bad = b"not json at all"; - let len = bad.len() as u32; - let mut input = len.to_le_bytes().to_vec(); - input.extend_from_slice(bad); - - let valid = r#"{"type":"meeting_state","url":"https://meet.google.com/xyz","is_active":true,"muted":true}"#; - input.extend(encode_message(valid)); - - let mut cursor = Cursor::new(input); - run(&mut cursor, &state_path); - - // second (valid) message should still produce output - assert!(state_path.exists()); - } - - #[test] - fn test_run_unknown_type_does_not_clear_existing_state() { - let dir = tempfile::tempdir().unwrap(); - let state_path = dir.path().join("chrome_state.json"); - - let active = r#"{"type":"meeting_state","url":"https://meet.google.com/xyz","is_active":true,"muted":false,"participants":[]}"#; - let unknown = r#"{"type":"something_else"}"#; - - let mut input = encode_message(active); - input.extend(encode_message(unknown)); - - let mut cursor = Cursor::new(input); - run(&mut cursor, &state_path); - - let contents = std::fs::read_to_string(&state_path).unwrap(); - let parsed: serde_json::Value = serde_json::from_str(&contents).unwrap(); - assert_eq!(parsed["meeting"]["url"], "https://meet.google.com/xyz"); - } -} diff --git a/crates/cli-docs/Cargo.toml b/crates/cli-docs/Cargo.toml deleted file mode 100644 index f6bbbcfa87..0000000000 --- a/crates/cli-docs/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "cli-docs" -version = "0.1.0" -edition = "2024" -publish = false - -[dependencies] -clap = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } diff --git a/crates/cli-docs/src/lib.rs b/crates/cli-docs/src/lib.rs deleted file mode 100644 index beb5bba268..0000000000 --- a/crates/cli-docs/src/lib.rs +++ /dev/null @@ -1,389 +0,0 @@ -use std::fmt::Write; - -use clap::Command; - -// --- JSON output --- - -#[derive(serde::Serialize)] -pub struct CommandDoc { - pub name: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub about: Option, - pub synopsis: String, - #[serde(skip_serializing_if = "Vec::is_empty")] - pub global_options: Vec, - #[serde(skip_serializing_if = "Vec::is_empty")] - pub options: Vec, - #[serde(skip_serializing_if = "Vec::is_empty")] - pub arguments: Vec, - #[serde(skip_serializing_if = "Vec::is_empty")] - pub subcommands: Vec, -} - -#[derive(serde::Serialize)] -pub struct OptionDoc { - pub flags: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub value_name: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub help: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub default: Option, - pub required: bool, - pub is_flag: bool, -} - -#[derive(serde::Serialize)] -pub struct ArgumentDoc { - pub name: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub help: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub default: Option, - pub required: bool, -} - -pub fn generate_json(cmd: &Command) -> String { - let mut cmd = cmd.clone(); - cmd.build(); - - let doc = build_command_doc(&cmd, cmd.get_name().to_string(), true); - serde_json::to_string_pretty(&doc).unwrap() -} - -fn build_command_doc(cmd: &Command, full_name: String, is_root: bool) -> CommandDoc { - let about = cmd - .get_long_about() - .or_else(|| cmd.get_about()) - .map(|a| a.to_string()); - - let synopsis = build_synopsis(cmd, &full_name, is_root); - - let global_options = if is_root { - sorted_args(cmd) - .filter(|a| !a.is_positional() && !a.is_hide_set() && a.is_global_set()) - .map(build_option_doc) - .collect() - } else { - vec![] - }; - - let options = sorted_args(cmd) - .filter(|a| !a.is_positional() && !a.is_hide_set() && !a.is_global_set()) - .map(build_option_doc) - .collect(); - - let arguments = sorted_args(cmd) - .filter(|a| a.is_positional() && !a.is_hide_set()) - .map(build_argument_doc) - .collect(); - - let subcommands = cmd - .get_subcommands() - .filter(|s| !s.is_hide_set() && s.get_name() != "help") - .map(|s| { - let child_name = format!("{full_name} {}", s.get_name()); - build_command_doc(s, child_name, false) - }) - .collect(); - - CommandDoc { - name: full_name, - about, - synopsis, - global_options, - options, - arguments, - subcommands, - } -} - -fn build_option_doc(arg: &clap::Arg) -> OptionDoc { - let mut flags = Vec::new(); - if let Some(short) = arg.get_short() { - flags.push(format!("-{short}")); - } - if let Some(long) = arg.get_long() { - flags.push(format!("--{long}")); - } - - let value_name = if is_flag(arg) { - None - } else { - arg.get_value_names().map(|names| { - names - .iter() - .map(|n| n.to_string()) - .collect::>() - .join(", ") - }) - }; - - let help = arg - .get_long_help() - .or_else(|| arg.get_help()) - .map(|h| h.to_string()); - - let default = get_default(arg); - - OptionDoc { - flags: flags.join(", "), - value_name, - help, - default, - required: arg.is_required_set(), - is_flag: is_flag(arg), - } -} - -fn build_argument_doc(arg: &clap::Arg) -> ArgumentDoc { - let name = arg - .get_value_names() - .and_then(|v| v.first().map(|s| s.to_string())) - .unwrap_or_else(|| arg.get_id().to_string().to_uppercase()); - - let help = arg - .get_long_help() - .or_else(|| arg.get_help()) - .map(|h| h.to_string()); - - let default = get_default(arg); - - ArgumentDoc { - name, - help, - default, - required: arg.is_required_set(), - } -} - -fn get_default(arg: &clap::Arg) -> Option { - if arg.is_hide_default_value_set() || is_flag(arg) { - return None; - } - let defaults: Vec<_> = arg - .get_default_values() - .iter() - .map(|v| v.to_string_lossy()) - .collect(); - if defaults.is_empty() { - None - } else { - Some(defaults.join(", ")) - } -} - -fn build_synopsis(cmd: &Command, full_name: &str, is_root: bool) -> String { - let mut out = full_name.to_string(); - - for arg in sorted_args(cmd) { - if arg.is_positional() || arg.is_hide_set() { - continue; - } - if !is_root && arg.is_global_set() { - continue; - } - let (open, close) = if arg.is_required_set() { - ("<", ">") - } else { - ("[", "]") - }; - out.push_str(&format!(" {open}")); - if let Some(short) = arg.get_short() { - out.push_str(&format!("-{short}")); - } else if let Some(long) = arg.get_long() { - out.push_str(&format!("--{long}")); - } - out.push_str(close); - } - - for arg in sorted_args(cmd) { - if !arg.is_positional() || arg.is_hide_set() { - continue; - } - let (open, close) = if arg.is_required_set() { - ("<", ">") - } else { - ("[", "]") - }; - out.push_str(&format!(" {open}{}{close}", arg.get_id())); - } - - if cmd.has_subcommands() { - out.push_str(" "); - } - - out -} - -// --- Markdown output --- - -pub fn generate(cmd: &Command) -> String { - let mut cmd = cmd.clone(); - cmd.build(); - - let mut out = String::new(); - render_command(&mut out, &cmd, cmd.get_name().to_string()); - out -} - -fn render_command(out: &mut String, cmd: &Command, full_name: String) { - let depth = full_name.matches(' ').count(); - let heading = if depth == 0 { "##" } else { "###" }; - - writeln!(out, "{heading} {full_name}\n").unwrap(); - - if let Some(about) = cmd.get_long_about().or_else(|| cmd.get_about()) { - writeln!(out, "{about}\n").unwrap(); - } - - let synopsis = build_synopsis(cmd, &full_name, depth == 0); - writeln!(out, "```\n{synopsis}\n```\n").unwrap(); - - render_options(out, cmd, depth == 0); - render_subcommands(out, cmd, &full_name); -} - -fn is_flag(arg: &clap::Arg) -> bool { - matches!( - arg.get_action(), - clap::ArgAction::SetTrue | clap::ArgAction::SetFalse | clap::ArgAction::Count - ) -} - -fn render_options(out: &mut String, cmd: &Command, is_root: bool) { - let locals: Vec<_> = sorted_args(cmd) - .filter(|a| !a.is_positional() && !a.is_hide_set() && !a.is_global_set()) - .collect(); - let positional: Vec<_> = sorted_args(cmd) - .filter(|a| a.is_positional() && !a.is_hide_set()) - .collect(); - - if is_root { - let globals: Vec<_> = sorted_args(cmd) - .filter(|a| !a.is_positional() && !a.is_hide_set() && a.is_global_set()) - .collect(); - - if !globals.is_empty() { - writeln!(out, "**Global options:**\n").unwrap(); - for arg in &globals { - render_arg(out, arg); - } - writeln!(out).unwrap(); - } - } - - if !locals.is_empty() { - writeln!(out, "**Options:**\n").unwrap(); - for arg in &locals { - render_arg(out, arg); - } - writeln!(out).unwrap(); - } - - if !positional.is_empty() { - writeln!(out, "**Arguments:**\n").unwrap(); - for arg in &positional { - render_positional(out, arg); - } - writeln!(out).unwrap(); - } -} - -fn render_arg(out: &mut String, arg: &clap::Arg) { - let mut flags = Vec::new(); - if let Some(short) = arg.get_short() { - flags.push(format!("-{short}")); - } - if let Some(long) = arg.get_long() { - flags.push(format!("--{long}")); - } - - let value = if is_flag(arg) { - String::new() - } else { - arg.get_value_names() - .map(|names| names.iter().map(|n| format!(" <{n}>")).collect::()) - .unwrap_or_default() - }; - - write!(out, "- `{}{value}`", flags.join(", ")).unwrap(); - if let Some(help) = arg.get_long_help().or_else(|| arg.get_help()) { - write!(out, " — {help}").unwrap(); - } - append_default(out, arg); - writeln!(out).unwrap(); -} - -fn render_positional(out: &mut String, arg: &clap::Arg) { - let name = arg - .get_value_names() - .and_then(|v| v.first().map(|s| s.to_string())) - .unwrap_or_else(|| arg.get_id().to_string().to_uppercase()); - - write!(out, "- `<{name}>`").unwrap(); - if let Some(help) = arg.get_long_help().or_else(|| arg.get_help()) { - write!(out, " — {help}").unwrap(); - } - append_default(out, arg); - writeln!(out).unwrap(); -} - -fn append_default(out: &mut String, arg: &clap::Arg) { - if arg.is_hide_default_value_set() || is_flag(arg) { - return; - } - let defaults: Vec<_> = arg - .get_default_values() - .iter() - .map(|v| v.to_string_lossy()) - .collect(); - if !defaults.is_empty() { - write!(out, " (default: `{}`)", defaults.join(", ")).unwrap(); - } -} - -fn render_subcommands(out: &mut String, cmd: &Command, full_name: &str) { - let subs: Vec<_> = cmd - .get_subcommands() - .filter(|s| !s.is_hide_set() && s.get_name() != "help") - .collect(); - - if subs.is_empty() { - return; - } - - writeln!(out, "**Subcommands:**\n").unwrap(); - for sub in &subs { - let name = sub.get_name(); - write!(out, "- `{name}`").unwrap(); - if let Some(about) = sub.get_about() { - write!(out, " — {about}").unwrap(); - } - writeln!(out).unwrap(); - } - writeln!(out).unwrap(); - - writeln!(out, "---\n").unwrap(); - - for sub in &subs { - let child_name = format!("{full_name} {}", sub.get_name()); - render_command(out, sub, child_name); - } -} - -fn sorted_args(cmd: &Command) -> impl Iterator { - let mut args: Vec<_> = cmd.get_arguments().filter(|a| !a.is_hide_set()).collect(); - args.sort_by_key(|a| { - let order = a.get_display_order(); - let key = if let Some(short) = a.get_short() { - short.to_lowercase().to_string() - } else if let Some(long) = a.get_long() { - long.to_string() - } else { - a.get_id().to_string() - }; - (order, key) - }); - args.into_iter() -} diff --git a/crates/cursor/Cargo.toml b/crates/cursor/Cargo.toml deleted file mode 100644 index 5063591f05..0000000000 --- a/crates/cursor/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "cursor" -version = "0.1.0" -edition = "2024" - -[dependencies] -base64 = { workspace = true } -reqwest = { workspace = true, features = ["json"] } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -thiserror = { workspace = true } -url = { workspace = true } - -[dev-dependencies] -tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/crates/cursor/src/client.rs b/crates/cursor/src/client.rs deleted file mode 100644 index 21a92895c7..0000000000 --- a/crates/cursor/src/client.rs +++ /dev/null @@ -1,211 +0,0 @@ -use base64::{Engine as _, engine::general_purpose::STANDARD}; -use reqwest::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE, HeaderValue}; - -use crate::{ - Agent, AgentConversation, AgentIdResponse, ApiKeyInfo, DownloadArtifactRequest, - DownloadArtifactResponse, Error, FollowupRequest, LaunchAgentRequest, ListAgentsRequest, - ListAgentsResponse, ListArtifactsResponse, ListModelsResponse, ListRepositoriesResponse, -}; - -#[derive(Default)] -pub struct CursorClientBuilder { - api_key: Option, - api_base: Option, - client: Option, -} - -impl CursorClientBuilder { - pub fn api_key(mut self, api_key: impl Into) -> Self { - self.api_key = Some(api_key.into()); - self - } - - pub fn api_base(mut self, api_base: impl Into) -> Self { - self.api_base = Some(api_base.into()); - self - } - - pub fn http_client(mut self, client: reqwest::Client) -> Self { - self.client = Some(client); - self - } - - pub fn build(self) -> Result { - let api_key = self.api_key.ok_or(Error::MissingApiKey)?; - let encoded = STANDARD.encode(format!("{api_key}:")); - let auth_header = - HeaderValue::from_str(&format!("Basic {encoded}")).map_err(|_| Error::InvalidApiKey)?; - - let client = self.client.unwrap_or_else(reqwest::Client::new); - - let mut api_base: url::Url = self - .api_base - .unwrap_or_else(|| "https://api.cursor.com".to_string()) - .parse()?; - if !api_base.path().ends_with('/') { - let path = format!("{}/", api_base.path()); - api_base.set_path(&path); - } - - Ok(CursorClient { - client, - api_base, - auth_header, - }) - } -} - -#[derive(Clone)] -pub struct CursorClient { - client: reqwest::Client, - api_base: url::Url, - auth_header: HeaderValue, -} - -impl CursorClient { - pub fn builder() -> CursorClientBuilder { - CursorClientBuilder::default() - } - - pub fn api_base(&self) -> &url::Url { - &self.api_base - } - - pub async fn list_agents(&self, req: ListAgentsRequest) -> Result { - let mut url = self.endpoint("/v0/agents")?; - { - let mut pairs = url.query_pairs_mut(); - if let Some(limit) = req.limit { - pairs.append_pair("limit", &limit.to_string()); - } - if let Some(cursor) = req.cursor.as_deref() { - pairs.append_pair("cursor", cursor); - } - if let Some(pr_url) = req.pr_url.as_deref() { - pairs.append_pair("prUrl", pr_url); - } - } - - self.send(self.client.get(url)).await - } - - pub async fn get_agent(&self, id: &str) -> Result { - let url = self.endpoint(&format!("/v0/agents/{id}"))?; - self.send(self.client.get(url)).await - } - - pub async fn get_agent_conversation(&self, id: &str) -> Result { - let url = self.endpoint(&format!("/v0/agents/{id}/conversation"))?; - self.send(self.client.get(url)).await - } - - pub async fn list_artifacts(&self, id: &str) -> Result { - let url = self.endpoint(&format!("/v0/agents/{id}/artifacts"))?; - self.send(self.client.get(url)).await - } - - pub async fn download_artifact( - &self, - id: &str, - req: DownloadArtifactRequest, - ) -> Result { - let mut url = self.endpoint(&format!("/v0/agents/{id}/artifacts/download"))?; - url.query_pairs_mut().append_pair("path", &req.path); - - self.send(self.client.get(url)).await - } - - pub async fn launch_agent(&self, req: &LaunchAgentRequest) -> Result { - let url = self.endpoint("/v0/agents")?; - self.send(self.client.post(url).json(req)).await - } - - pub async fn add_followup( - &self, - id: &str, - req: &FollowupRequest, - ) -> Result { - let url = self.endpoint(&format!("/v0/agents/{id}/followup"))?; - self.send(self.client.post(url).json(req)).await - } - - pub async fn stop_agent(&self, id: &str) -> Result { - let url = self.endpoint(&format!("/v0/agents/{id}/stop"))?; - self.send(self.client.post(url)).await - } - - pub async fn delete_agent(&self, id: &str) -> Result { - let url = self.endpoint(&format!("/v0/agents/{id}"))?; - self.send(self.client.delete(url)).await - } - - pub async fn me(&self) -> Result { - let url = self.endpoint("/v0/me")?; - self.send(self.client.get(url)).await - } - - pub async fn list_models(&self) -> Result { - let url = self.endpoint("/v0/models")?; - self.send(self.client.get(url)).await - } - - pub async fn list_repositories(&self) -> Result { - let url = self.endpoint("/v0/repositories")?; - self.send(self.client.get(url)).await - } - - fn endpoint(&self, path: &str) -> Result { - self.api_base - .join(path.trim_start_matches('/')) - .map_err(Error::from) - } - - async fn send(&self, request: reqwest::RequestBuilder) -> Result - where - T: serde::de::DeserializeOwned, - { - let mut auth_header = self.auth_header.clone(); - auth_header.set_sensitive(true); - - let response = request - .header(AUTHORIZATION, auth_header) - .header(ACCEPT, HeaderValue::from_static("application/json")) - .header(CONTENT_TYPE, HeaderValue::from_static("application/json")) - .send() - .await?; - let status = response.status(); - - if status.is_success() { - return Ok(response.json().await?); - } - - let message = response.text().await.unwrap_or_default(); - Err(Error::Api { - status: status.as_u16(), - message, - }) - } -} - -#[cfg(test)] -mod tests { - use super::CursorClient; - - #[test] - fn endpoint_uses_default_api_base() { - let client = CursorClient::builder().api_key("test").build().unwrap(); - let url = client.endpoint("/v0/agents").unwrap(); - assert_eq!(url.as_str(), "https://api.cursor.com/v0/agents"); - } - - #[test] - fn endpoint_preserves_path_prefix() { - let client = CursorClient::builder() - .api_key("test") - .api_base("https://example.com/proxy") - .build() - .unwrap(); - let url = client.endpoint("/v0/agents").unwrap(); - assert_eq!(url.as_str(), "https://example.com/proxy/v0/agents"); - } -} diff --git a/crates/cursor/src/error.rs b/crates/cursor/src/error.rs deleted file mode 100644 index 7136c38df5..0000000000 --- a/crates/cursor/src/error.rs +++ /dev/null @@ -1,13 +0,0 @@ -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("request error: {0}")] - Request(#[from] reqwest::Error), - #[error("url parse error: {0}")] - Url(#[from] url::ParseError), - #[error("missing api key")] - MissingApiKey, - #[error("invalid api key")] - InvalidApiKey, - #[error("api error (status {status}): {message}")] - Api { status: u16, message: String }, -} diff --git a/crates/cursor/src/lib.rs b/crates/cursor/src/lib.rs deleted file mode 100644 index 4e614b2bbe..0000000000 --- a/crates/cursor/src/lib.rs +++ /dev/null @@ -1,71 +0,0 @@ -mod client; -mod error; -mod types; - -pub use client::{CursorClient, CursorClientBuilder}; -pub use error::Error; -pub use types::*; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn agent_status_serialization() { - let status = AgentStatus::Finished; - let json = serde_json::to_string(&status).unwrap(); - assert_eq!(json, "\"FINISHED\""); - - let parsed: AgentStatus = serde_json::from_str("\"RUNNING\"").unwrap(); - assert_eq!(parsed, AgentStatus::Running); - } - - #[test] - fn agent_status_deserialization_accepts_unknown_variants() { - let parsed: AgentStatus = serde_json::from_str("\"PAUSED\"").unwrap(); - assert_eq!(parsed, AgentStatus::Unknown); - } - - #[test] - fn conversation_message_type_deserialization_accepts_unknown_variants() { - let parsed: ConversationMessageType = serde_json::from_str("\"tool_message\"").unwrap(); - assert_eq!(parsed, ConversationMessageType::Unknown); - } - - #[test] - fn launch_agent_request_serialization_omits_empty_fields() { - let value = serde_json::to_value(LaunchAgentRequest { - prompt: PromptInput { - text: "Add a README".to_string(), - images: None, - }, - model: None, - source: AgentSourceInput { - repository: Some("https://github.com/acme/repo".to_string()), - r#ref: Some("main".to_string()), - pr_url: None, - }, - target: Some(AgentTargetInput { - auto_create_pr: Some(true), - open_as_cursor_github_app: None, - skip_reviewer_request: None, - branch_name: Some("feature/add-readme".to_string()), - auto_branch: None, - }), - webhook: None, - }) - .unwrap(); - - assert_eq!(value["prompt"]["text"], "Add a README"); - assert_eq!( - value["source"]["repository"], - "https://github.com/acme/repo" - ); - assert_eq!(value["target"]["autoCreatePr"], true); - assert!(value.get("model").is_none()); - assert!(value["target"].get("openAsCursorGithubApp").is_none()); - assert!(value["target"].get("url").is_none()); - assert!(value["target"].get("prUrl").is_none()); - assert!(value["source"].get("prUrl").is_none()); - } -} diff --git a/crates/cursor/src/types.rs b/crates/cursor/src/types.rs deleted file mode 100644 index ca449fa4ac..0000000000 --- a/crates/cursor/src/types.rs +++ /dev/null @@ -1,232 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum AgentStatus { - Creating, - Running, - Finished, - Failed, - Stopped, - Deleting, - #[serde(other)] - Unknown, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum ConversationMessageType { - UserMessage, - AssistantMessage, - #[serde(other)] - Unknown, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PromptImageDimension { - pub width: u32, - pub height: u32, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PromptImage { - pub data: String, - pub dimension: PromptImageDimension, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PromptInput { - pub text: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub images: Option>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentSource { - #[serde(default)] - pub repository: Option, - #[serde(default)] - pub r#ref: Option, - #[serde(default)] - pub pr_url: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentSourceInput { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub repository: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub r#ref: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub pr_url: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentTarget { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub branch_name: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub url: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub pr_url: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub auto_create_pr: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub open_as_cursor_github_app: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub skip_reviewer_request: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub auto_branch: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentTargetInput { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub branch_name: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub auto_create_pr: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub open_as_cursor_github_app: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub skip_reviewer_request: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub auto_branch: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Agent { - pub id: String, - pub name: String, - pub status: AgentStatus, - pub source: AgentSource, - #[serde(default)] - pub target: Option, - #[serde(default)] - pub summary: Option, - pub created_at: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ListAgentsRequest { - #[serde(default)] - pub limit: Option, - #[serde(default)] - pub cursor: Option, - #[serde(default)] - pub pr_url: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ListAgentsResponse { - pub agents: Vec, - #[serde(default)] - pub next_cursor: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ConversationMessage { - pub id: String, - #[serde(rename = "type")] - pub message_type: ConversationMessageType, - pub text: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AgentConversation { - pub id: String, - pub messages: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Artifact { - pub absolute_path: String, - pub size_bytes: u64, - pub updated_at: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ListArtifactsResponse { - pub artifacts: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DownloadArtifactRequest { - pub path: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DownloadArtifactResponse { - pub url: String, - pub expires_at: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct WebhookConfig { - pub url: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub secret: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LaunchAgentRequest { - pub prompt: PromptInput, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub model: Option, - pub source: AgentSourceInput, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub target: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub webhook: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct FollowupRequest { - pub prompt: PromptInput, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AgentIdResponse { - pub id: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ApiKeyInfo { - pub api_key_name: String, - pub created_at: String, - pub user_email: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ListModelsResponse { - pub models: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Repository { - pub owner: String, - pub name: String, - pub repository: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ListRepositoriesResponse { - pub repositories: Vec, -} diff --git a/crates/db-cli/AGENTS.md b/crates/db-cli/AGENTS.md deleted file mode 100644 index 584df634ee..0000000000 --- a/crates/db-cli/AGENTS.md +++ /dev/null @@ -1,16 +0,0 @@ -# `db-cli` - -## Role - -CLI subcommand for inspecting and managing the desktop SQLite database. Exposes `db-app` operations via clap. - -## Owns - -- `Args` and clap subcommand definitions. -- DB path resolution (`--base` / `--db-path`). -- CLI output formatting. - -## Does Not Own - -- Schema, migrations, CRUD logic (owned by `db-app`). -- Database opening policy (owned by `db-core`). diff --git a/crates/db-cli/Cargo.toml b/crates/db-cli/Cargo.toml deleted file mode 100644 index 2e1e827d8c..0000000000 --- a/crates/db-cli/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "db-cli" -version = "0.1.0" -edition = "2024" - -[dependencies] -hypr-db-app = { workspace = true } -hypr-db-core = { workspace = true } -hypr-db-migrate = { workspace = true } - -clap = { workspace = true, features = ["derive", "env"] } -dirs = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -sqlx = { workspace = true, features = ["runtime-tokio", "sqlite", "sqlite-unbundled", "macros"] } -thiserror = { workspace = true } - -[dev-dependencies] -tempfile = { workspace = true } -tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/crates/db-cli/src/cli.rs b/crates/db-cli/src/cli.rs deleted file mode 100644 index ab790bee02..0000000000 --- a/crates/db-cli/src/cli.rs +++ /dev/null @@ -1,116 +0,0 @@ -use std::path::PathBuf; - -use clap::{Args as ClapArgs, Parser, Subcommand}; - -#[derive(ClapArgs, Debug)] -pub struct Args { - #[arg(long, env = "CHAR_BASE", hide_env_values = true, value_name = "DIR")] - pub base: Option, - - #[arg(long, value_name = "FILE")] - pub db_path: Option, - - #[command(subcommand)] - pub command: Commands, -} - -#[derive(Subcommand, Debug)] -pub enum Commands { - /// Manage templates stored in the desktop SQLite database - Templates { - #[command(subcommand)] - command: TemplateCommand, - }, -} - -#[derive(Subcommand, Debug)] -pub enum TemplateCommand { - /// List templates - List, - /// Fetch a template by id - Get { id: String }, - /// Insert or update a template - Upsert(UpsertTemplateArgs), - /// Delete a template by id - Delete { id: String }, -} - -#[derive(clap::Args, Debug)] -pub struct UpsertTemplateArgs { - #[arg(long)] - pub id: String, - #[arg(long)] - pub title: String, - #[arg(long, default_value = "")] - pub description: String, - #[arg(long, default_value_t = false)] - pub pinned: bool, - #[arg(long)] - pub pin_order: Option, - #[arg(long)] - pub category: Option, - #[arg(long)] - pub targets_json: Option, - #[arg(long)] - pub sections_json: String, -} - -#[derive(Parser)] -struct TestCli { - #[command(subcommand)] - command: RootCommands, -} - -#[derive(Subcommand)] -enum RootCommands { - Db { - #[command(flatten)] - args: Args, - }, -} - -#[cfg(test)] -mod tests { - use clap::CommandFactory; - - use super::*; - - #[test] - fn args_help_mentions_templates() { - let mut command = TestCli::command(); - let mut db = command.find_subcommand_mut("db").unwrap().clone(); - let mut bytes = Vec::new(); - db.write_long_help(&mut bytes).unwrap(); - let help = String::from_utf8(bytes).unwrap(); - - assert!(help.contains("templates")); - assert!(help.contains("--base ")); - assert!(help.contains("--db-path ")); - } - - #[test] - fn clap_parses_template_subcommands() { - let cli = TestCli::parse_from([ - "char", - "db", - "--base", - "/tmp/char", - "templates", - "get", - "template-1", - ]); - - let RootCommands::Db { args } = cli.command; - assert_eq!( - args.base.as_deref(), - Some(std::path::Path::new("/tmp/char")) - ); - assert!(args.db_path.is_none()); - match args.command { - Commands::Templates { command } => match command { - TemplateCommand::Get { id } => assert_eq!(id, "template-1"), - _ => panic!("expected get subcommand"), - }, - } - } -} diff --git a/crates/db-cli/src/error.rs b/crates/db-cli/src/error.rs deleted file mode 100644 index 0cf7e88f8e..0000000000 --- a/crates/db-cli/src/error.rs +++ /dev/null @@ -1,25 +0,0 @@ -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("{action} failed: {reason}")] - OperationFailed { - action: &'static str, - reason: String, - }, - #[error("{what} not found")] - NotFound { what: String }, -} - -pub type Result = std::result::Result; - -impl Error { - pub fn operation_failed(action: &'static str, reason: impl Into) -> Self { - Self::OperationFailed { - action, - reason: reason.into(), - } - } - - pub fn not_found(what: impl Into) -> Self { - Self::NotFound { what: what.into() } - } -} diff --git a/crates/db-cli/src/lib.rs b/crates/db-cli/src/lib.rs deleted file mode 100644 index b0910cecc2..0000000000 --- a/crates/db-cli/src/lib.rs +++ /dev/null @@ -1,10 +0,0 @@ -#![forbid(unsafe_code)] - -mod cli; -mod error; -mod output; -mod runtime; - -pub use cli::Args; -pub use error::{Error, Result}; -pub use runtime::run; diff --git a/crates/db-cli/src/output.rs b/crates/db-cli/src/output.rs deleted file mode 100644 index fa929586ce..0000000000 --- a/crates/db-cli/src/output.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub fn print_json(value: &impl serde::Serialize) -> crate::Result<()> { - let bytes = serde_json::to_vec_pretty(value) - .map_err(|e| crate::Error::operation_failed("serialize response", e.to_string()))?; - println!("{}", String::from_utf8_lossy(&bytes)); - Ok(()) -} diff --git a/crates/db-cli/src/runtime.rs b/crates/db-cli/src/runtime.rs deleted file mode 100644 index 565525ea7b..0000000000 --- a/crates/db-cli/src/runtime.rs +++ /dev/null @@ -1,257 +0,0 @@ -use std::path::{Path, PathBuf}; - -use serde::Serialize; - -use crate::cli::{Args, Commands, TemplateCommand, UpsertTemplateArgs}; -use crate::output::print_json; -use crate::{Error, Result}; - -#[derive(Serialize)] -struct Status<'a> { - status: &'a str, - id: &'a str, -} - -#[derive(Serialize)] -struct TemplateOutput { - id: String, - title: String, - description: String, - pinned: bool, - pin_order: Option, - category: Option, - targets_json: Option, - sections_json: String, - created_at: String, - updated_at: String, -} - -pub async fn run(args: Args) -> Result<()> { - let db_path = resolve_db_path(args.base.as_deref(), args.db_path.as_deref()); - - let db = hypr_db_core::Db::connect_local_plain(&db_path) - .await - .map_err(|e| Error::operation_failed("open database", e.to_string()))?; - - hypr_db_migrate::migrate(&db, hypr_db_app::schema()) - .await - .map_err(|e| Error::operation_failed("migrate database", e.to_string()))?; - - match args.command { - Commands::Templates { command } => run_templates(db.pool(), command).await, - } -} - -fn default_base_dir() -> PathBuf { - dirs::data_dir() - .unwrap_or_else(std::env::temp_dir) - .join("char") -} - -fn resolve_db_path(base: Option<&Path>, db_path: Option<&Path>) -> PathBuf { - match db_path { - Some(path) => path.to_path_buf(), - None => base - .map(Path::to_path_buf) - .unwrap_or_else(default_base_dir) - .join("app.db"), - } -} - -async fn run_templates(pool: &sqlx::SqlitePool, command: TemplateCommand) -> Result<()> { - match command { - TemplateCommand::List => { - let rows = hypr_db_app::list_templates(pool) - .await - .map_err(|e| Error::operation_failed("list templates", e.to_string()))?; - let rows: Vec<_> = rows.into_iter().map(TemplateOutput::from).collect(); - print_json(&rows) - } - TemplateCommand::Get { id } => { - let row = hypr_db_app::get_template(pool, &id) - .await - .map_err(|e| Error::operation_failed("get template", e.to_string()))? - .ok_or_else(|| Error::not_found(format!("template '{id}'")))?; - print_json(&TemplateOutput::from(row)) - } - TemplateCommand::Upsert(args) => upsert_template(pool, args).await, - TemplateCommand::Delete { id } => { - hypr_db_app::delete_template(pool, &id) - .await - .map_err(|e| Error::operation_failed("delete template", e.to_string()))?; - print_json(&Status { - status: "deleted", - id: &id, - }) - } - } -} - -impl From for TemplateOutput { - fn from(value: hypr_db_app::TemplateRow) -> Self { - Self { - id: value.id, - title: value.title, - description: value.description, - pinned: value.pinned, - pin_order: value.pin_order, - category: value.category, - targets_json: value.targets_json, - sections_json: value.sections_json, - created_at: value.created_at, - updated_at: value.updated_at, - } - } -} - -async fn upsert_template(pool: &sqlx::SqlitePool, args: UpsertTemplateArgs) -> Result<()> { - let id = args.id; - - hypr_db_app::upsert_template( - pool, - hypr_db_app::UpsertTemplate { - id: &id, - title: &args.title, - description: &args.description, - pinned: args.pinned, - pin_order: args.pin_order, - category: args.category.as_deref(), - targets_json: args.targets_json.as_deref(), - sections_json: &args.sections_json, - }, - ) - .await - .map_err(|e| Error::operation_failed("upsert template", e.to_string()))?; - - print_json(&Status { - status: "upserted", - id: &id, - }) -} - -#[cfg(test)] -mod tests { - use tempfile::tempdir; - - use super::*; - use crate::cli::{Commands, TemplateCommand}; - - #[test] - fn resolve_db_path_prefers_explicit_db_path() { - let resolved = resolve_db_path( - Some(Path::new("/tmp/char")), - Some(Path::new("/tmp/custom/app.db")), - ); - assert_eq!(resolved, PathBuf::from("/tmp/custom/app.db")); - } - - #[test] - fn resolve_db_path_defaults_to_base_app_db() { - let resolved = resolve_db_path(Some(Path::new("/tmp/char")), None); - assert_eq!(resolved, PathBuf::from("/tmp/char/app.db")); - } - - #[tokio::test] - async fn run_migrates_and_roundtrips_templates() { - let dir = tempdir().unwrap(); - let db_path = dir.path().join("templates.db"); - - run(Args { - base: None, - db_path: Some(db_path.clone()), - command: Commands::Templates { - command: TemplateCommand::Upsert(UpsertTemplateArgs { - id: "template-1".to_string(), - title: "Standup".to_string(), - description: "Daily sync".to_string(), - pinned: true, - pin_order: Some(1), - category: Some("meetings".to_string()), - targets_json: Some("[\"engineering\"]".to_string()), - sections_json: "[{\"title\":\"Notes\"}]".to_string(), - }), - }, - }) - .await - .unwrap(); - - let db = hypr_db_core::Db::connect_local_plain(&db_path) - .await - .unwrap(); - let row = hypr_db_app::get_template(db.pool(), "template-1") - .await - .unwrap() - .unwrap(); - - assert_eq!(row.title, "Standup"); - assert!(row.pinned); - } - - #[tokio::test] - async fn run_returns_not_found_for_missing_template() { - let dir = tempdir().unwrap(); - let db_path = dir.path().join("templates.db"); - - let error = run(Args { - base: None, - db_path: Some(db_path), - command: Commands::Templates { - command: TemplateCommand::Get { - id: "missing".to_string(), - }, - }, - }) - .await - .unwrap_err(); - - assert_eq!(error.to_string(), "template 'missing' not found"); - } - - #[tokio::test] - async fn run_deletes_templates() { - let dir = tempdir().unwrap(); - let db_path = dir.path().join("templates.db"); - let db = hypr_db_core::Db::connect_local_plain(&db_path) - .await - .unwrap(); - hypr_db_migrate::migrate(&db, hypr_db_app::schema()) - .await - .unwrap(); - hypr_db_app::upsert_template( - db.pool(), - hypr_db_app::UpsertTemplate { - id: "template-1", - title: "Standup", - description: "", - pinned: false, - pin_order: None, - category: None, - targets_json: None, - sections_json: "[]", - }, - ) - .await - .unwrap(); - drop(db); - - run(Args { - base: None, - db_path: Some(db_path.clone()), - command: Commands::Templates { - command: TemplateCommand::Delete { - id: "template-1".to_string(), - }, - }, - }) - .await - .unwrap(); - - let db = hypr_db_core::Db::connect_local_plain(&db_path) - .await - .unwrap(); - let row = hypr_db_app::get_template(db.pool(), "template-1") - .await - .unwrap(); - assert!(row.is_none()); - } -} diff --git a/crates/devin/Cargo.toml b/crates/devin/Cargo.toml deleted file mode 100644 index d97a6ce933..0000000000 --- a/crates/devin/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "devin" -version = "0.1.0" -edition = "2024" - -[dependencies] -reqwest = { workspace = true, features = ["json"] } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -thiserror = { workspace = true } -url = { workspace = true } - -[dev-dependencies] -tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } -wiremock = { workspace = true } diff --git a/crates/devin/src/client.rs b/crates/devin/src/client.rs deleted file mode 100644 index 64750e83c0..0000000000 --- a/crates/devin/src/client.rs +++ /dev/null @@ -1,273 +0,0 @@ -use std::time::Duration; - -use reqwest::header::{ACCEPT, CONTENT_TYPE, HeaderMap, HeaderValue}; - -use crate::{ - CursorPage, Error, ListSessionMessagesRequest, ListSessionsRequest, Session, SessionMessage, -}; - -const DEFAULT_API_BASE: &str = "https://api.devin.ai/"; -const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60); - -#[derive(Default)] -pub struct DevinClientBuilder { - api_key: Option, - api_base: Option, - client: Option, - timeout: Option, -} - -impl DevinClientBuilder { - pub fn api_key(mut self, api_key: impl Into) -> Self { - self.api_key = Some(api_key.into()); - self - } - - pub fn api_base(mut self, api_base: impl Into) -> Self { - self.api_base = Some(api_base.into()); - self - } - - pub fn http_client(mut self, client: reqwest::Client) -> Self { - self.client = Some(client); - self - } - - pub fn timeout(mut self, timeout: Duration) -> Self { - self.timeout = Some(timeout); - self - } - - pub fn build(self) -> Result { - let api_key = self.api_key.ok_or(Error::MissingApiKey)?; - - let _ = HeaderValue::from_str(&format!("Bearer {api_key}")) - .map_err(|_| Error::InvalidApiKey)?; - - let client = match self.client { - Some(client) => client, - None => { - let mut headers = HeaderMap::new(); - headers.insert(ACCEPT, HeaderValue::from_static("application/json")); - headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); - - reqwest::Client::builder() - .default_headers(headers) - .timeout(self.timeout.unwrap_or(DEFAULT_TIMEOUT)) - .build()? - } - }; - - let raw_base = self - .api_base - .unwrap_or_else(|| DEFAULT_API_BASE.to_string()); - let api_base = normalize_base(&raw_base)?; - - Ok(DevinClient { - client, - api_base, - api_key, - }) - } -} - -#[derive(Clone)] -pub struct DevinClient { - client: reqwest::Client, - api_base: url::Url, - api_key: String, -} - -impl DevinClient { - pub fn builder() -> DevinClientBuilder { - DevinClientBuilder::default() - } - - pub fn api_base(&self) -> &url::Url { - &self.api_base - } - - pub async fn list_sessions( - &self, - org_id: &str, - req: ListSessionsRequest, - ) -> Result, Error> { - let mut url = self.endpoint(&format!("v3/organizations/{org_id}/sessions"))?; - { - let mut pairs = url.query_pairs_mut(); - - if let Some(after) = req.after.as_deref() { - pairs.append_pair("after", after); - } - if let Some(first) = req.first { - pairs.append_pair("first", &first.to_string()); - } - if let Some(session_ids) = req.session_ids.as_ref() { - for session_id in session_ids { - pairs.append_pair("session_ids", session_id); - } - } - if let Some(created_after) = req.created_after { - pairs.append_pair("created_after", &created_after.to_string()); - } - if let Some(created_before) = req.created_before { - pairs.append_pair("created_before", &created_before.to_string()); - } - if let Some(updated_after) = req.updated_after { - pairs.append_pair("updated_after", &updated_after.to_string()); - } - if let Some(updated_before) = req.updated_before { - pairs.append_pair("updated_before", &updated_before.to_string()); - } - if let Some(tags) = req.tags.as_ref() { - for tag in tags { - pairs.append_pair("tags", tag); - } - } - if let Some(playbook_id) = req.playbook_id.as_deref() { - pairs.append_pair("playbook_id", playbook_id); - } - if let Some(origins) = req.origins.as_ref() { - for origin in origins { - pairs.append_pair("origins", origin.as_str()); - } - } - if let Some(schedule_id) = req.schedule_id.as_deref() { - pairs.append_pair("schedule_id", schedule_id); - } - if let Some(user_ids) = req.user_ids.as_ref() { - for user_id in user_ids { - pairs.append_pair("user_ids", user_id); - } - } - if let Some(service_user_ids) = req.service_user_ids.as_ref() { - for service_user_id in service_user_ids { - pairs.append_pair("service_user_ids", service_user_id); - } - } - } - - self.send(self.client.get(url)).await - } - - pub async fn get_session(&self, org_id: &str, devin_id: &str) -> Result { - let url = self.endpoint(&format!("v3/organizations/{org_id}/sessions/{devin_id}"))?; - self.send(self.client.get(url)).await - } - - pub async fn list_session_messages( - &self, - org_id: &str, - devin_id: &str, - req: ListSessionMessagesRequest, - ) -> Result, Error> { - let mut url = self.endpoint(&format!( - "v3/organizations/{org_id}/sessions/{devin_id}/messages" - ))?; - { - let mut pairs = url.query_pairs_mut(); - if let Some(after) = req.after.as_deref() { - pairs.append_pair("after", after); - } - if let Some(first) = req.first { - pairs.append_pair("first", &first.to_string()); - } - } - - self.send(self.client.get(url)).await - } - - pub async fn terminate_session( - &self, - org_id: &str, - devin_id: &str, - archive: bool, - ) -> Result { - let mut url = self.endpoint(&format!("v3/organizations/{org_id}/sessions/{devin_id}"))?; - if archive { - url.query_pairs_mut().append_pair("archive", "true"); - } - - self.send(self.client.delete(url)).await - } - - pub async fn archive_session(&self, org_id: &str, devin_id: &str) -> Result { - let url = self.endpoint(&format!( - "v3/organizations/{org_id}/sessions/{devin_id}/archive" - ))?; - self.send(self.client.post(url)).await - } - - fn endpoint(&self, path: &str) -> Result { - debug_assert!( - !path.starts_with('/'), - "endpoint paths must be relative to api_base", - ); - self.api_base.join(path).map_err(Error::from) - } - - async fn send(&self, request: reqwest::RequestBuilder) -> Result - where - T: serde::de::DeserializeOwned, - { - let response = request.bearer_auth(&self.api_key).send().await?; - let status = response.status(); - - if status.is_success() { - return Ok(response.json().await?); - } - - let message = response.text().await.unwrap_or_default(); - Err(Error::Api { - status: status.as_u16(), - message, - }) - } -} - -fn normalize_base(raw: &str) -> Result { - let normalized = if raw.ends_with('/') { - raw.to_string() - } else { - format!("{raw}/") - }; - Ok(normalized.parse()?) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn normalize_base_adds_trailing_slash() { - let url = normalize_base("https://api.devin.ai").unwrap(); - assert_eq!(url.as_str(), "https://api.devin.ai/"); - } - - #[test] - fn normalize_base_preserves_trailing_slash() { - let url = normalize_base("https://api.devin.ai/").unwrap(); - assert_eq!(url.as_str(), "https://api.devin.ai/"); - } - - #[test] - fn normalize_base_with_path_prefix_no_trailing_slash() { - let url = normalize_base("https://gateway.corp/devin").unwrap(); - assert_eq!(url.as_str(), "https://gateway.corp/devin/"); - } - - #[test] - fn endpoint_appends_to_prefixed_base() { - let client = DevinClient::builder() - .api_key("secret") - .api_base("https://gateway.corp/devin") - .build() - .unwrap(); - - let url = client.endpoint("v3/organizations/org_1/sessions").unwrap(); - assert_eq!( - url.as_str(), - "https://gateway.corp/devin/v3/organizations/org_1/sessions", - ); - } -} diff --git a/crates/devin/src/error.rs b/crates/devin/src/error.rs deleted file mode 100644 index 7136c38df5..0000000000 --- a/crates/devin/src/error.rs +++ /dev/null @@ -1,13 +0,0 @@ -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("request error: {0}")] - Request(#[from] reqwest::Error), - #[error("url parse error: {0}")] - Url(#[from] url::ParseError), - #[error("missing api key")] - MissingApiKey, - #[error("invalid api key")] - InvalidApiKey, - #[error("api error (status {status}): {message}")] - Api { status: u16, message: String }, -} diff --git a/crates/devin/src/lib.rs b/crates/devin/src/lib.rs deleted file mode 100644 index d2416b09f5..0000000000 --- a/crates/devin/src/lib.rs +++ /dev/null @@ -1,257 +0,0 @@ -mod client; -mod error; -mod types; - -pub use client::{DevinClient, DevinClientBuilder}; -pub use error::Error; -pub use types::*; - -#[cfg(test)] -mod tests { - use super::*; - - use serde_json::json; - use wiremock::{ - Mock, MockServer, ResponseTemplate, - matchers::{header, method, path, query_param}, - }; - - #[test] - fn session_status_serialization() { - let status = SessionStatus::Running; - let json = serde_json::to_string(&status).unwrap(); - assert_eq!(json, "\"running\""); - - let parsed: SessionStatus = serde_json::from_str("\"suspended\"").unwrap(); - assert_eq!(parsed, SessionStatus::Suspended); - } - - #[test] - fn session_status_unknown_variant_is_forward_compatible() { - let parsed: SessionStatus = serde_json::from_str("\"brand_new_state\"").unwrap(); - assert_eq!(parsed, SessionStatus::Unknown); - } - - #[test] - fn session_origin_serialization() { - let origin = SessionOrigin::Webapp; - let json = serde_json::to_string(&origin).unwrap(); - assert_eq!(json, "\"webapp\""); - - let parsed: SessionOrigin = serde_json::from_str("\"cli\"").unwrap(); - assert_eq!(parsed, SessionOrigin::Cli); - } - - #[test] - fn session_origin_as_str_matches_wire_format() { - for origin in [ - SessionOrigin::Webapp, - SessionOrigin::Slack, - SessionOrigin::Teams, - SessionOrigin::Api, - SessionOrigin::Linear, - SessionOrigin::Jira, - SessionOrigin::Scheduled, - SessionOrigin::Cli, - SessionOrigin::Other, - ] { - let wire = serde_json::to_string(&origin).unwrap(); - assert_eq!(wire.trim_matches('"'), origin.as_str()); - } - } - - #[test] - fn session_message_source_parses_known_values() { - let devin: SessionMessageSource = serde_json::from_str("\"devin\"").unwrap(); - assert_eq!(devin, SessionMessageSource::Devin); - - let user: SessionMessageSource = serde_json::from_str("\"user\"").unwrap(); - assert_eq!(user, SessionMessageSource::User); - - let other: SessionMessageSource = serde_json::from_str("\"system\"").unwrap(); - assert_eq!(other, SessionMessageSource::Unknown); - } - - #[test] - fn session_deserializes_with_null_pr_state() { - let payload = json!({ - "acus_consumed": 0.0, - "created_at": 0, - "org_id": "org_1", - "pull_requests": [ - { "pr_state": null, "pr_url": "https://github.com/a/b/pull/1" } - ], - "session_id": "devin-1", - "status": "running", - "tags": [], - "updated_at": 0, - "url": "https://app.devin.ai/sessions/devin-1" - }); - - let session: Session = serde_json::from_value(payload).unwrap(); - assert_eq!(session.pull_requests.len(), 1); - assert!(session.pull_requests[0].pr_state.is_none()); - } - - #[test] - fn list_sessions_request_serialization_omits_empty_fields() { - let value = serde_json::to_value(ListSessionsRequest { - first: Some(50), - tags: Some(vec!["bug".to_string(), "urgent".to_string()]), - origins: Some(vec![SessionOrigin::Api, SessionOrigin::Cli]), - ..Default::default() - }) - .unwrap(); - - assert_eq!(value["first"], 50); - assert_eq!(value["tags"][0], "bug"); - assert_eq!(value["origins"][1], "cli"); - assert!(value.get("after").is_none()); - } - - fn empty_page() -> serde_json::Value { - json!({ - "items": [], - "end_cursor": null, - "has_next_page": false, - "total": 0, - }) - } - - async fn client_for(server: &MockServer) -> DevinClient { - DevinClient::builder() - .api_key("sk_test_secret") - .api_base(server.uri()) - .build() - .unwrap() - } - - #[tokio::test] - async fn list_sessions_sends_bearer_auth_and_expected_query_params() { - let server = MockServer::start().await; - - Mock::given(method("GET")) - .and(path("/v3/organizations/org_1/sessions")) - .and(header("authorization", "Bearer sk_test_secret")) - .and(query_param("first", "25")) - .and(query_param("tags", "bug")) - .and(query_param("origins", "api")) - .and(query_param("origins", "cli")) - .and(query_param("user_ids", "u_1")) - .and(query_param("user_ids", "u_2")) - .respond_with(ResponseTemplate::new(200).set_body_json(empty_page())) - .expect(1) - .mount(&server) - .await; - - let client = client_for(&server).await; - client - .list_sessions( - "org_1", - ListSessionsRequest { - first: Some(25), - tags: Some(vec!["bug".to_string()]), - origins: Some(vec![SessionOrigin::Api, SessionOrigin::Cli]), - user_ids: Some(vec!["u_1".to_string(), "u_2".to_string()]), - ..Default::default() - }, - ) - .await - .unwrap(); - } - - #[tokio::test] - async fn terminate_session_with_archive_sends_query_param() { - let server = MockServer::start().await; - - Mock::given(method("DELETE")) - .and(path("/v3/organizations/org_1/sessions/devin-1")) - .and(query_param("archive", "true")) - .and(header("authorization", "Bearer sk_test_secret")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "acus_consumed": 0.0, - "created_at": 0, - "org_id": "org_1", - "pull_requests": [], - "session_id": "devin-1", - "status": "exit", - "tags": [], - "updated_at": 0, - "url": "https://app.devin.ai/sessions/devin-1", - }))) - .expect(1) - .mount(&server) - .await; - - let client = client_for(&server).await; - let session = client - .terminate_session("org_1", "devin-1", true) - .await - .unwrap(); - assert_eq!(session.status, SessionStatus::Exit); - } - - #[tokio::test] - async fn custom_http_client_still_receives_auth_header() { - let server = MockServer::start().await; - - Mock::given(method("GET")) - .and(path("/v3/organizations/org_1/sessions/devin-1")) - .and(header("authorization", "Bearer sk_test_secret")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "acus_consumed": 0.0, - "created_at": 0, - "org_id": "org_1", - "pull_requests": [], - "session_id": "devin-1", - "status": "running", - "tags": [], - "updated_at": 0, - "url": "https://app.devin.ai/sessions/devin-1", - }))) - .expect(1) - .mount(&server) - .await; - - let custom = reqwest::Client::builder().build().unwrap(); - let client = DevinClient::builder() - .api_key("sk_test_secret") - .api_base(server.uri()) - .http_client(custom) - .build() - .unwrap(); - - client.get_session("org_1", "devin-1").await.unwrap(); - } - - #[tokio::test] - async fn api_base_with_path_prefix_is_preserved() { - let server = MockServer::start().await; - - Mock::given(method("GET")) - .and(path("/devin/v3/organizations/org_1/sessions/devin-1")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "acus_consumed": 0.0, - "created_at": 0, - "org_id": "org_1", - "pull_requests": [], - "session_id": "devin-1", - "status": "running", - "tags": [], - "updated_at": 0, - "url": "https://app.devin.ai/sessions/devin-1", - }))) - .expect(1) - .mount(&server) - .await; - - let base = format!("{}/devin", server.uri()); - let client = DevinClient::builder() - .api_key("sk_test_secret") - .api_base(base) - .build() - .unwrap(); - - client.get_session("org_1", "devin-1").await.unwrap(); - } -} diff --git a/crates/devin/src/types.rs b/crates/devin/src/types.rs deleted file mode 100644 index e8931a8dc1..0000000000 --- a/crates/devin/src/types.rs +++ /dev/null @@ -1,175 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum SessionStatus { - New, - Creating, - Claimed, - Running, - Exit, - Error, - Suspended, - Resuming, - #[serde(other)] - Unknown, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum SessionStatusDetail { - Working, - WaitingForUser, - WaitingForApproval, - Finished, - Inactivity, - UserRequest, - UsageLimitExceeded, - OutOfCredits, - OutOfQuota, - NoQuotaAllocation, - PaymentDeclined, - OrgUsageLimitExceeded, - Error, - #[serde(other)] - Unknown, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum SessionOrigin { - Webapp, - Slack, - Teams, - Api, - Linear, - Jira, - Scheduled, - Cli, - Other, - #[serde(other)] - Unknown, -} - -impl SessionOrigin { - pub fn as_str(&self) -> &'static str { - match self { - Self::Webapp => "webapp", - Self::Slack => "slack", - Self::Teams => "teams", - Self::Api => "api", - Self::Linear => "linear", - Self::Jira => "jira", - Self::Scheduled => "scheduled", - Self::Cli => "cli", - Self::Other => "other", - Self::Unknown => "unknown", - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum SessionMessageSource { - Devin, - User, - #[serde(other)] - Unknown, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SessionPullRequest { - #[serde(default)] - pub pr_state: Option, - pub pr_url: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Session { - pub acus_consumed: f64, - pub created_at: i64, - pub org_id: String, - pub pull_requests: Vec, - pub session_id: String, - pub status: SessionStatus, - pub tags: Vec, - pub updated_at: i64, - pub url: String, - #[serde(default)] - pub child_session_ids: Option>, - #[serde(default)] - pub is_advanced: bool, - #[serde(default)] - pub is_archived: bool, - #[serde(default)] - pub parent_session_id: Option, - #[serde(default)] - pub playbook_id: Option, - #[serde(default)] - pub service_user_id: Option, - #[serde(default)] - pub status_detail: Option, - #[serde(default)] - pub structured_output: Option, - #[serde(default)] - pub title: Option, - #[serde(default)] - pub user_id: Option, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct ListSessionsRequest { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub after: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub first: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub session_ids: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub created_after: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub created_before: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub updated_after: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub updated_before: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub tags: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub playbook_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub origins: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub schedule_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub user_ids: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub service_user_ids: Option>, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct ListSessionMessagesRequest { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub after: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub first: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SessionMessage { - pub created_at: i64, - pub event_id: String, - pub message: String, - pub source: SessionMessageSource, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CursorPage { - pub items: Vec, - #[serde(default)] - pub end_cursor: Option, - #[serde(default)] - pub has_next_page: bool, - #[serde(default)] - pub total: Option, -} diff --git a/crates/email/Cargo.toml b/crates/email/Cargo.toml deleted file mode 100644 index 62883f8e60..0000000000 --- a/crates/email/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "email" -version = "0.1.0" -edition = "2024" - -[dependencies] -url = { workspace = true } - -[target."cfg(target_os = \"macos\")".dependencies] -objc2 = { workspace = true } -objc2-foundation = { workspace = true } -objc2-app-kit = { workspace = true } diff --git a/crates/email/src/lib.rs b/crates/email/src/lib.rs deleted file mode 100644 index e6ef5a3ce6..0000000000 --- a/crates/email/src/lib.rs +++ /dev/null @@ -1,185 +0,0 @@ -#[cfg(target_os = "macos")] -use objc2::{rc::Retained, runtime::NSObject}; -#[cfg(target_os = "macos")] -use objc2_app_kit::{NSSharingService, NSSharingServiceNameComposeEmail}; -#[cfg(target_os = "macos")] -use objc2_foundation::{NSArray, NSString, NSURL}; -use std::path::Path; - -pub struct EmailComposer; - -#[cfg(target_os = "macos")] -/* -NSString* htmlText = @"Hello, World!"; -NSData* textData = [NSData dataWithBytes:[htmlText UTF8String] length:[htmlText lengthOfBytesUsingEncoding:NSUTF8StringEncoding]]; -NSAttributedString* textAttributedString = [[NSAttributedString alloc] initWithHTML:textData options:options documentAttributes:nil]; - -// create a file to attach -NSUUID* uuid = [NSUUID new]; -NSString* tempDir = [NSTemporaryDirectory() stringByAppendingPathComponent:[uuid UUIDString]]; -NSFileManager* fm = [NSFileManager new]; -[fm createDirectoryAtPath:tempDir withIntermediateDirectories:YES attributes:nil error:nil]; -NSString* tempFile = [tempDir stringByAppendingPathComponent:@"report.csv"]; -NSURL* tempFileURL = [NSURL fileURLWithPath:tempFile]; -NSData* csv = ...; // generate the data here -[csv writeToURL:tempFileURL atomically:NO]; - -// share it -NSSharingService* mailShare = [NSSharingService sharingServiceNamed:NSSharingServiceNameComposeEmail]; -NSArray* shareItems = @[textAttributedString,tempFileURL]; -[mailShare performWithItems:shareItems]; -*/ -impl EmailComposer { - /// Open email client with HTML content and optional attachments - pub fn open_email_client( - recipients: &[&str], - subject: &str, - html_message: &str, - attachments: &[&Path], - ) -> Result<(), Box> { - unsafe { - // Create sharing service for email composition - let service = NSSharingService::sharingServiceNamed(NSSharingServiceNameComposeEmail) - .ok_or("Email service not available")?; - - // Set recipients - let ns_recipients = Self::create_ns_string_array(recipients)?; - service.setRecipients(Some(&ns_recipients)); - - // Set subject - let ns_subject = NSString::from_str(subject); - service.setSubject(Some(&ns_subject)); - - // Create items array for sharing - let items = Self::create_sharing_items(html_message, attachments)?; - - // Convert items to references for NSArray - let item_refs: Vec<&NSObject> = items.iter().map(|item| &**item).collect(); - let items_array = NSArray::from_slice(&item_refs); - - // Convert to NSArray - let any_array = - &*(&items_array as *const _ as *const NSArray); - - // Check if service can perform with these items - if !service.canPerformWithItems(Some(any_array)) { - return Err("Cannot perform email composition with provided items".into()); - } - - // Open email client - service.performWithItems(any_array); - - Ok(()) - } - } - - /// Create NSArray of NSString from slice of string references - unsafe fn create_ns_string_array( - strings: &[&str], - ) -> Result>, Box> { - let ns_strings: Vec> = - strings.iter().map(|s| NSString::from_str(s)).collect(); - let refs: Vec<&NSString> = ns_strings.iter().map(|s| &**s).collect(); - Ok(NSArray::from_slice(&refs)) - } - - /// Create items array with HTML content and attachments - unsafe fn create_sharing_items( - html_message: &str, - attachments: &[&Path], - ) -> Result>, Box> { - let mut items: Vec> = Vec::new(); - - // Add HTML content as plain string for now - let html_string = NSString::from_str(html_message); - items.push(unsafe { Retained::cast_unchecked(html_string) }); - - // Add attachments - for &path in attachments { - let file_url = unsafe { Self::create_file_url(path)? }; - items.push(unsafe { Retained::cast_unchecked(file_url) }); - } - - Ok(items) - } - - /// Create NSURL from file path - unsafe fn create_file_url(path: &Path) -> Result, Box> { - let path_str = path.to_str().ok_or("Invalid path")?; - let ns_path = NSString::from_str(path_str); - let file_url = NSURL::fileURLWithPath(&ns_path); - - if !file_url.isFileURL() { - return Err("Not a valid file URL".into()); - } - - Ok(file_url) - } - - /// Create a temporary file with given content (helper for attachments) - pub fn create_temp_file_with_content( - filename: &str, - content: &[u8], - ) -> Result> { - use std::fs::File; - use std::io::Write; - - let temp_dir = std::env::temp_dir(); - let file_path = temp_dir.join(filename); - - let mut file = File::create(&file_path)?; - file.write_all(content)?; - - Ok(file_path) - } -} - -#[cfg(not(target_os = "macos"))] -impl EmailComposer { - /// Open email client with HTML content and optional attachments (stub for non-macOS) - pub fn open_email_client( - _recipients: &[&str], - _subject: &str, - _html_message: &str, - _attachments: &[&Path], - ) -> Result<(), Box> { - Err("Email composition is only supported on macOS".into()) - } - - /// Create a temporary file with given content (helper for attachments) - pub fn create_temp_file_with_content( - filename: &str, - content: &[u8], - ) -> Result> { - use std::fs::File; - use std::io::Write; - - let temp_dir = std::env::temp_dir(); - let file_path = temp_dir.join(filename); - - let mut file = File::create(&file_path)?; - file.write_all(content)?; - - Ok(file_path) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_simple_email_client() -> Result<(), Box> { - let html_body = "

Simple test email

"; - - EmailComposer::open_email_client( - &["test@example.com"], - "Simple Test", - html_body, - &[], // No attachments - )?; - - println!("Email client opened successfully!"); - Ok(()) - } -} diff --git a/crates/embedding/Cargo.toml b/crates/embedding/Cargo.toml deleted file mode 100644 index bb1150e69d..0000000000 --- a/crates/embedding/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "embedding" -version = "0.1.0" -edition = "2024" - -[features] -default = ["onnx"] -onnx = ["dep:hypr-onnx", "dep:knf-rs"] -coreml = ["onnx", "hypr-onnx/coreml"] -directml = ["onnx", "hypr-onnx/directml"] - -[dependencies] -hypr-onnx = { workspace = true, optional = true } -knf-rs = { git = "https://github.com/thewh1teagle/pyannote-rs", rev = "e23bd29", package = "knf-rs", optional = true } - -serde = { workspace = true } -thiserror = { workspace = true } - -[dev-dependencies] -approx = { workspace = true } -hypr-data = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } diff --git a/crates/embedding/data/snapshots/embedding_english_1.json b/crates/embedding/data/snapshots/embedding_english_1.json deleted file mode 100644 index 3dc2779d72..0000000000 --- a/crates/embedding/data/snapshots/embedding_english_1.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "embedding_dim": 256, - "l2_norm": 2.487027189612397, - "mean": -0.00818082727073488, - "std_dev": 0.1552237699580072, - "head": [ - -0.014635704, - 0.07223707, - -0.20770884, - -0.30646443, - -0.045360893, - 0.13235542, - -0.058884114, - -0.050506853, - -0.028955884, - -0.04946351, - 0.051631033, - -0.036055148 - ], - "tail": [ - -0.17541476, - 0.15687603, - 0.19741337, - -0.29939428, - -0.20509183, - 0.14238471, - -0.16545762, - 0.052139387, - 0.19415584, - 0.018797744, - -0.13026087, - 0.34865823 - ] -} diff --git a/crates/embedding/data/snapshots/embedding_english_2.json b/crates/embedding/data/snapshots/embedding_english_2.json deleted file mode 100644 index c2130651e4..0000000000 --- a/crates/embedding/data/snapshots/embedding_english_2.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "embedding_dim": 256, - "l2_norm": 2.2085370908220887, - "mean": -0.002021630180934153, - "std_dev": 0.13801876305385088, - "head": [ - -0.22710933, - 0.0699223, - 0.09919612, - -0.20434314, - -0.0468402, - -0.22632219, - -0.14090794, - 0.006247841, - -0.05592034, - 0.027405234, - 0.11377749, - -0.07392204 - ], - "tail": [ - -0.123167984, - -0.14732648, - 0.16984981, - 0.047689058, - -0.044715367, - 0.018706344, - -0.010903906, - 0.10315953, - -0.06284194, - 0.1279184, - 0.20190841, - -0.029958438 - ] -} diff --git a/crates/embedding/data/snapshots/embedding_korean_1.json b/crates/embedding/data/snapshots/embedding_korean_1.json deleted file mode 100644 index 3e04d97638..0000000000 --- a/crates/embedding/data/snapshots/embedding_korean_1.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "embedding_dim": 256, - "l2_norm": 1.9103870121172195, - "mean": -0.00007086164987413213, - "std_dev": 0.11939916722965456, - "head": [ - -0.03958331, - 0.0037297532, - -0.09694438, - -0.14215036, - -0.019905314, - -0.19250247, - -0.2986868, - 0.1435427, - 0.030645711, - 0.20172921, - -0.029997509, - -0.06280857 - ], - "tail": [ - -0.2107257, - -0.111908495, - 0.07719177, - 0.06638661, - -0.06807611, - 0.048392233, - 0.19865629, - 0.111853585, - 0.08045067, - 0.054878432, - 0.14310688, - 0.065765016 - ] -} diff --git a/crates/embedding/src/error.rs b/crates/embedding/src/error.rs deleted file mode 100644 index 6d377cf959..0000000000 --- a/crates/embedding/src/error.rs +++ /dev/null @@ -1,48 +0,0 @@ -use serde::{Serialize, ser::Serializer}; - -pub type Result = std::result::Result; - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[cfg(feature = "onnx")] - #[error(transparent)] - HyprOnnxError(#[from] hypr_onnx::Error), - - #[cfg(feature = "onnx")] - #[error(transparent)] - OrtError(#[from] hypr_onnx::ort::Error), - - #[cfg(feature = "onnx")] - #[error(transparent)] - ShapeError(#[from] hypr_onnx::ndarray::ShapeError), - - #[error("knf error: {0}")] - KnfError(String), - - #[error("samples are empty")] - EmptyInput, - - #[error("mask length ({mask_len}) must match samples length ({samples_len})")] - MaskLengthMismatch { mask_len: usize, samples_len: usize }, - - #[error("audio is too short for embedding")] - TooShort, - - #[error("embedding model returned non-finite values")] - NonFiniteEmbedding, - - #[error("missing output tensor: {0}")] - MissingOutput(String), - - #[error("internal error: {0}")] - Internal(&'static str), -} - -impl Serialize for Error { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: Serializer, - { - serializer.serialize_str(self.to_string().as_ref()) - } -} diff --git a/crates/embedding/src/lib.rs b/crates/embedding/src/lib.rs deleted file mode 100644 index b4953df9f5..0000000000 --- a/crates/embedding/src/lib.rs +++ /dev/null @@ -1,9 +0,0 @@ -mod error; - -pub use error::*; - -#[cfg(feature = "onnx")] -pub mod onnx; - -#[cfg(feature = "onnx")] -pub use onnx::{EMBEDDING_DIM, EmbeddingConfig, EmbeddingExtractor, SAMPLE_RATE_HZ}; diff --git a/crates/embedding/src/onnx/embedding.onnx b/crates/embedding/src/onnx/embedding.onnx deleted file mode 100644 index a5f2d57142..0000000000 Binary files a/crates/embedding/src/onnx/embedding.onnx and /dev/null differ diff --git a/crates/embedding/src/onnx/mod.rs b/crates/embedding/src/onnx/mod.rs deleted file mode 100644 index 829a32d394..0000000000 --- a/crates/embedding/src/onnx/mod.rs +++ /dev/null @@ -1,366 +0,0 @@ -use hypr_onnx::{ - ndarray::{self, Array2, Array3, Axis}, - ort::{self, session::Session, value::TensorRef}, -}; - -use crate::{Error, Result}; - -pub mod model; - -pub use model::{EMBEDDING_DIM, SAMPLE_RATE_HZ}; - -#[derive(Debug, Clone)] -pub struct EmbeddingConfig { - pub scale_waveform_by_1_15: bool, - pub mask_threshold: f32, -} - -impl Default for EmbeddingConfig { - fn default() -> Self { - Self { - // Matches pyannote.audio's ONNXWeSpeakerPretrainedSpeakerEmbedding implementation. - scale_waveform_by_1_15: true, - mask_threshold: 0.5, - } - } -} - -pub struct EmbeddingExtractor { - session: Session, - config: EmbeddingConfig, -} - -impl EmbeddingExtractor { - pub fn new() -> Result { - Self::from_model_bytes(model::BYTES) - } - - pub fn from_model_bytes(model_bytes: &[u8]) -> Result { - let session = hypr_onnx::load_model_from_bytes(model_bytes)?; - Ok(Self { - session, - config: EmbeddingConfig::default(), - }) - } - - pub fn with_config(mut self, config: EmbeddingConfig) -> Self { - self.config = config; - self - } - - pub fn config(&self) -> &EmbeddingConfig { - &self.config - } - - pub fn compute(&mut self, samples_f32: &[f32]) -> Result> { - self.compute_optional(samples_f32)?.ok_or(Error::TooShort) - } - - pub fn compute_optional(&mut self, samples_f32: &[f32]) -> Result>> { - if samples_f32.is_empty() { - return Err(Error::EmptyInput); - } - - let Some(features) = - compute_fbank_optional(samples_f32, self.config.scale_waveform_by_1_15)? - else { - return Ok(None); - }; - - self.run_features(features) - } - - pub fn compute_with_mask_optional( - &mut self, - samples_f32: &[f32], - mask: &[f32], - ) -> Result>> { - if samples_f32.is_empty() { - return Err(Error::EmptyInput); - } - - if samples_f32.len() != mask.len() { - return Err(Error::MaskLengthMismatch { - mask_len: mask.len(), - samples_len: samples_f32.len(), - }); - } - - let Some(features) = - compute_fbank_optional(samples_f32, self.config.scale_waveform_by_1_15)? - else { - return Ok(None); - }; - let num_frames = features.nrows(); - if num_frames == 0 { - return Ok(None); - } - - let frame_mask = resample_mask_nearest(mask, num_frames, self.config.mask_threshold); - let masked = select_rows(&features, &frame_mask)?; - match masked { - None => Ok(None), - Some(masked) => self.run_features(masked), - } - } - - fn run_features(&mut self, features: Array2) -> Result>> { - let feats: Array3 = features.insert_axis(Axis(0)); - - let inputs = ort::inputs![model::INPUT_NAME => TensorRef::from_array_view(feats.view())?]; - let outputs = self.session.run(inputs)?; - - let out = outputs - .get(model::OUTPUT_NAME) - .ok_or_else(|| Error::MissingOutput(model::OUTPUT_NAME.to_string()))? - .try_extract_array::()?; - - let embs = out.iter().copied().collect::>(); - if embs.iter().all(|v| v.is_finite()) { - Ok(Some(embs)) - } else { - Ok(None) - } - } -} - -fn compute_fbank_optional( - samples_f32: &[f32], - scale_waveform_by_1_15: bool, -) -> Result>> { - let mut scaled = Vec::with_capacity(samples_f32.len()); - if scale_waveform_by_1_15 { - for &s in samples_f32 { - scaled.push(s * 32768.0); - } - } else { - scaled.extend_from_slice(samples_f32); - } - - let features_knf = match knf_rs::compute_fbank(&scaled) { - Ok(f) => f, - Err(e) => { - let msg = e.to_string(); - // kaldi-native-fbank returns zero frames when audio is too short - if msg.contains("frames array is empty") { - return Ok(None); - } - return Err(Error::KnfError(msg)); - } - }; - - let shape = features_knf.shape().to_vec(); - - let features: Array2 = ndarray::Array2::from_shape_vec( - (shape[0], shape[1]), - features_knf.iter().copied().collect(), - ) - .map_err(|e| Error::KnfError(e.to_string()))?; - - Ok(Some(features)) -} - -fn resample_mask_nearest(mask: &[f32], target_len: usize, threshold: f32) -> Vec { - if target_len == 0 { - return vec![]; - } - - let src_len = mask.len(); - if src_len == 0 { - return vec![false; target_len]; - } - - let mut out = Vec::with_capacity(target_len); - for t in 0..target_len { - let src_idx = (t * src_len) / target_len; - out.push(mask[src_idx] > threshold); - } - out -} - -fn select_rows(features: &Array2, keep: &[bool]) -> Result>> { - if keep.len() != features.nrows() { - return Err(Error::Internal("mask length does not match feature frames")); - } - - let bins = features.ncols(); - let mut out = Vec::new(); - let mut rows = 0usize; - - for (r, &k) in keep.iter().enumerate() { - if !k { - continue; - } - - out.extend(features.row(r).iter().copied()); - rows += 1; - } - - if rows == 0 { - return Ok(None); - } - - Ok(Some(Array2::from_shape_vec((rows, bins), out)?)) -} - -#[cfg(test)] -mod tests { - use super::*; - use approx::assert_abs_diff_eq; - use serde::{Deserialize, Serialize}; - use std::path::Path; - - fn xorshift32(seed: &mut u32) -> u32 { - let mut x = *seed; - x ^= x << 13; - x ^= x >> 17; - x ^= x << 5; - *seed = x; - x - } - - fn noise(len: usize) -> Vec { - let mut seed = 0x1234_5678; - let mut v = Vec::with_capacity(len); - for _ in 0..len { - let x = xorshift32(&mut seed); - let f = (x as f32 / u32::MAX as f32) * 2.0 - 1.0; - v.push(f); - } - v - } - - #[derive(Debug, Serialize, Deserialize)] - struct EmbeddingSnapshot { - embedding_dim: usize, - l2_norm: f64, - mean: f64, - std_dev: f64, - head: Vec, - tail: Vec, - } - - fn pcm_bytes_to_f32(bytes: &[u8]) -> Vec { - bytes - .chunks_exact(2) - .map(|chunk| i16::from_le_bytes([chunk[0], chunk[1]]) as f32 / 32768.0) - .collect() - } - - fn save_snapshot(snapshot: &EmbeddingSnapshot, path: &Path) { - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).unwrap(); - } - let json = serde_json::to_string_pretty(snapshot).unwrap(); - std::fs::write(path, json).unwrap(); - } - - fn load_snapshot(path: &Path) -> EmbeddingSnapshot { - let json = std::fs::read_to_string(path).unwrap_or_else(|_| { - panic!( - "Snapshot file not found: {} -Run with UPDATE_SNAPSHOTS=1 to generate baselines.", - path.display() - ) - }); - serde_json::from_str(&json).unwrap() - } - - fn assert_snapshot_eq(actual: &EmbeddingSnapshot, expected: &EmbeddingSnapshot) { - assert_eq!(actual.embedding_dim, expected.embedding_dim); - assert_eq!(actual.head.len(), expected.head.len()); - assert_eq!(actual.tail.len(), expected.tail.len()); - - assert_abs_diff_eq!(actual.l2_norm, expected.l2_norm, epsilon = 1e-4); - assert_abs_diff_eq!(actual.mean, expected.mean, epsilon = 1e-5); - assert_abs_diff_eq!(actual.std_dev, expected.std_dev, epsilon = 1e-5); - - for (a, e) in actual.head.iter().zip(expected.head.iter()) { - assert_abs_diff_eq!(*a as f64, *e as f64, epsilon = 5e-4); - } - - for (a, e) in actual.tail.iter().zip(expected.tail.iter()) { - assert_abs_diff_eq!(*a as f64, *e as f64, epsilon = 5e-4); - } - } - - fn run_snapshot_test(audio_bytes: &[u8], snapshot_name: &str) { - let samples = pcm_bytes_to_f32(audio_bytes); - let mut extractor = EmbeddingExtractor::new().unwrap(); - let embedding = extractor.compute(&samples).unwrap(); - - let l2_norm = embedding - .iter() - .map(|&v| (v as f64) * (v as f64)) - .sum::() - .sqrt(); - let mean = embedding.iter().map(|&v| v as f64).sum::() / embedding.len() as f64; - let variance = embedding - .iter() - .map(|&v| { - let d = v as f64 - mean; - d * d - }) - .sum::() - / embedding.len() as f64; - let std_dev = variance.sqrt(); - - let k = embedding.len().min(12); - let snapshot = EmbeddingSnapshot { - embedding_dim: embedding.len(), - l2_norm, - mean, - std_dev, - head: embedding[..k].to_vec(), - tail: embedding[embedding.len() - k..].to_vec(), - }; - - let snapshot_path = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("data") - .join("snapshots") - .join(format!("{snapshot_name}.json")); - - if std::env::var("UPDATE_SNAPSHOTS").is_ok() { - save_snapshot(&snapshot, &snapshot_path); - println!("Updated snapshot: {}", snapshot_path.display()); - } else { - let expected = load_snapshot(&snapshot_path); - assert_snapshot_eq(&snapshot, &expected); - } - } - - #[test] - fn embedding_has_expected_dim_and_is_finite() { - let samples = noise(SAMPLE_RATE_HZ as usize); - let mut extractor = EmbeddingExtractor::new().unwrap(); - let embs = extractor.compute_optional(&samples).unwrap().unwrap(); - assert_eq!(embs.len(), EMBEDDING_DIM); - assert!(embs.iter().all(|v| v.is_finite())); - } - - #[test] - fn masked_embedding_none_when_mask_is_all_zero() { - let samples = noise(SAMPLE_RATE_HZ as usize); - let mask = vec![0.0f32; samples.len()]; - let mut extractor = EmbeddingExtractor::new().unwrap(); - let embs = extractor - .compute_with_mask_optional(&samples, &mask) - .unwrap(); - assert!(embs.is_none()); - } - - #[test] - fn snapshot_english_1() { - run_snapshot_test(hypr_data::english_1::AUDIO, "embedding_english_1"); - } - - #[test] - fn snapshot_english_2() { - run_snapshot_test(hypr_data::english_2::AUDIO, "embedding_english_2"); - } - - #[test] - fn snapshot_korean_1() { - run_snapshot_test(hypr_data::korean_1::AUDIO, "embedding_korean_1"); - } -} diff --git a/crates/embedding/src/onnx/model.rs b/crates/embedding/src/onnx/model.rs deleted file mode 100644 index 206f6f845d..0000000000 --- a/crates/embedding/src/onnx/model.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub const BYTES: &[u8] = include_bytes!("./embedding.onnx"); - -pub const INPUT_NAME: &str = "feats"; -pub const OUTPUT_NAME: &str = "embs"; - -pub const SAMPLE_RATE_HZ: u32 = 16000; -pub const NUM_MEL_BINS: usize = 80; -pub const EMBEDDING_DIM: usize = 256; diff --git a/crates/exa/Cargo.toml b/crates/exa/Cargo.toml deleted file mode 100644 index a77bd532b7..0000000000 --- a/crates/exa/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "exa" -version = "0.1.0" -edition = "2024" - -[dependencies] -reqwest = { workspace = true, features = ["json"] } -schemars = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -specta = { workspace = true, features = ["derive", "serde_json"] } -thiserror = { workspace = true } -url = { workspace = true } - -[dev-dependencies] -tokio = { workspace = true, features = ["rt", "macros"] } diff --git a/crates/exa/src/answer.rs b/crates/exa/src/answer.rs deleted file mode 100644 index 1f9aa93165..0000000000 --- a/crates/exa/src/answer.rs +++ /dev/null @@ -1,32 +0,0 @@ -use crate::client::{ExaClient, parse_response}; -use crate::common_derives; -use crate::types::AnswerCitation; - -common_derives! { - #[serde(rename_all = "camelCase")] - pub struct AnswerRequest { - pub query: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub text: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub output_schema: Option, - } -} - -common_derives! { - #[serde(rename_all = "camelCase")] - pub struct AnswerResponse { - pub answer: serde_json::Value, - pub citations: Vec, - } -} - -impl ExaClient { - pub async fn answer(&self, req: AnswerRequest) -> Result { - let mut url = self.api_base.clone(); - url.set_path("/answer"); - - let response = self.client.post(url).json(&req).send().await?; - parse_response(response).await - } -} diff --git a/crates/exa/src/client.rs b/crates/exa/src/client.rs deleted file mode 100644 index 3537822cfd..0000000000 --- a/crates/exa/src/client.rs +++ /dev/null @@ -1,80 +0,0 @@ -use serde::de::DeserializeOwned; - -#[derive(Clone, Default)] -pub struct ExaClientBuilder { - api_key: Option, - api_base: Option, -} - -#[derive(Clone)] -pub struct ExaClient { - pub(crate) client: reqwest::Client, - pub(crate) api_base: url::Url, -} - -impl ExaClient { - pub fn builder() -> ExaClientBuilder { - ExaClientBuilder::default() - } -} - -impl ExaClientBuilder { - pub fn api_base(mut self, api_base: impl Into) -> Self { - self.api_base = Some(api_base.into()); - self - } - - pub fn api_key(mut self, api_key: impl Into) -> Self { - self.api_key = Some(api_key.into()); - self - } - - pub fn build(self) -> Result { - let api_key = self.api_key.ok_or(crate::Error::MissingApiKey)?; - let api_base = self - .api_base - .unwrap_or_else(|| "https://api.exa.ai".to_string()); - - let mut headers = reqwest::header::HeaderMap::new(); - - let auth_str = format!("Bearer {}", api_key); - let mut auth_value = reqwest::header::HeaderValue::from_str(&auth_str) - .map_err(|_| crate::Error::InvalidApiKey)?; - auth_value.set_sensitive(true); - - headers.insert(reqwest::header::AUTHORIZATION, auth_value); - headers.insert( - reqwest::header::CONTENT_TYPE, - reqwest::header::HeaderValue::from_static("application/json"), - ); - - let client = reqwest::Client::builder() - .default_headers(headers) - .build()?; - - Ok(ExaClient { - client, - api_base: api_base.parse().map_err(|_| crate::Error::InvalidApiBase)?, - }) - } -} - -pub(crate) async fn check_response( - response: reqwest::Response, -) -> Result { - let status = response.status(); - if status.is_success() { - Ok(response) - } else { - let status_code = status.as_u16(); - let body = response.text().await.unwrap_or_default(); - Err(crate::Error::Api(status_code, body)) - } -} - -pub(crate) async fn parse_response( - response: reqwest::Response, -) -> Result { - let response = check_response(response).await?; - Ok(response.json().await?) -} diff --git a/crates/exa/src/contents.rs b/crates/exa/src/contents.rs deleted file mode 100644 index f23047b1d7..0000000000 --- a/crates/exa/src/contents.rs +++ /dev/null @@ -1,59 +0,0 @@ -use crate::client::{ExaClient, parse_response}; -use crate::types::{ContentStatus, CostDollars, SearchResult}; - -crate::float_derives! { - #[serde(rename_all = "camelCase")] - pub struct GetContentsRequest { - pub urls: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub ids: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub text: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub highlights: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub summary: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub livecrawl: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub livecrawl_timeout: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub max_age_hours: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub subpages: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub subpage_target: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub extras: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub context: Option, - } -} - -crate::float_derives! { - #[serde(rename_all = "camelCase")] - pub struct GetContentsResponse { - #[serde(skip_serializing_if = "Option::is_none")] - pub request_id: Option, - pub results: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub context: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub statuses: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub cost_dollars: Option, - } -} - -impl ExaClient { - pub async fn get_contents( - &self, - req: GetContentsRequest, - ) -> Result { - let mut url = self.api_base.clone(); - url.set_path("/contents"); - - let response = self.client.post(url).json(&req).send().await?; - parse_response(response).await - } -} diff --git a/crates/exa/src/error.rs b/crates/exa/src/error.rs deleted file mode 100644 index 92cc360229..0000000000 --- a/crates/exa/src/error.rs +++ /dev/null @@ -1,24 +0,0 @@ -use serde::{Serialize, ser::Serializer}; - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("API error (status {0}): {1}")] - Api(u16, String), - #[error(transparent)] - Request(#[from] reqwest::Error), - #[error("missing api key")] - MissingApiKey, - #[error("invalid api key")] - InvalidApiKey, - #[error("invalid api base url")] - InvalidApiBase, -} - -impl Serialize for Error { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: Serializer, - { - serializer.serialize_str(self.to_string().as_ref()) - } -} diff --git a/crates/exa/src/find_similar.rs b/crates/exa/src/find_similar.rs deleted file mode 100644 index 3c752d2c11..0000000000 --- a/crates/exa/src/find_similar.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::client::{ExaClient, parse_response}; -use crate::types::{ContentsRequest, SearchResult}; - -crate::float_derives! { - #[serde(rename_all = "camelCase")] - pub struct FindSimilarRequest { - pub url: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub num_results: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub include_domains: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub exclude_domains: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub start_crawl_date: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub end_crawl_date: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub start_published_date: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub end_published_date: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub include_text: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub exclude_text: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub contents: Option, - } -} - -crate::float_derives! { - #[serde(rename_all = "camelCase")] - pub struct FindSimilarResponse { - pub results: Vec, - } -} - -impl ExaClient { - pub async fn find_similar( - &self, - req: FindSimilarRequest, - ) -> Result { - let mut url = self.api_base.clone(); - url.set_path("/findSimilar"); - - let response = self.client.post(url).json(&req).send().await?; - parse_response(response).await - } -} diff --git a/crates/exa/src/lib.rs b/crates/exa/src/lib.rs deleted file mode 100644 index c6cc91a24a..0000000000 --- a/crates/exa/src/lib.rs +++ /dev/null @@ -1,310 +0,0 @@ -mod answer; -mod client; -mod contents; -mod error; -mod find_similar; -mod search; -mod types; - -pub use answer::*; -pub use client::*; -pub use contents::*; -pub use error::*; -pub use find_similar::*; -pub use search::*; -pub use types::*; - -macro_rules! common_derives { - ($item:item) => { - #[derive( - Debug, - Eq, - PartialEq, - Clone, - serde::Serialize, - serde::Deserialize, - specta::Type, - schemars::JsonSchema, - )] - $item - }; -} - -macro_rules! float_derives { - ($item:item) => { - #[derive( - Debug, - PartialEq, - Clone, - serde::Serialize, - serde::Deserialize, - specta::Type, - schemars::JsonSchema, - )] - $item - }; -} - -pub(crate) use common_derives; -pub(crate) use float_derives; - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - - #[tokio::test] - #[ignore] - async fn test_search() { - let client = ExaClientBuilder::default() - .api_key("test-key") - .build() - .unwrap(); - - let _ = client - .search(SearchRequest { - query: "Latest AI developments".to_string(), - additional_queries: None, - stream: None, - output_schema: None, - system_prompt: None, - r#type: Some(SearchType::Auto), - category: None, - user_location: None, - num_results: Some(10), - include_domains: None, - exclude_domains: None, - start_crawl_date: None, - end_crawl_date: None, - start_published_date: None, - end_published_date: None, - include_text: None, - exclude_text: None, - moderation: None, - contents: None, - }) - .await; - } - - #[tokio::test] - #[ignore] - async fn test_get_contents() { - let client = ExaClientBuilder::default() - .api_key("test-key") - .build() - .unwrap(); - - let _ = client - .get_contents(GetContentsRequest { - urls: vec!["https://arxiv.org/pdf/2307.06435".to_string()], - ids: None, - text: None, - highlights: None, - summary: None, - livecrawl: None, - livecrawl_timeout: None, - max_age_hours: None, - subpages: None, - subpage_target: None, - extras: None, - context: None, - }) - .await; - } - - #[tokio::test] - #[ignore] - async fn test_find_similar() { - let client = ExaClientBuilder::default() - .api_key("test-key") - .build() - .unwrap(); - - let _ = client - .find_similar(FindSimilarRequest { - url: "https://arxiv.org/abs/2307.06435".to_string(), - num_results: Some(5), - include_domains: None, - exclude_domains: None, - start_crawl_date: None, - end_crawl_date: None, - start_published_date: None, - end_published_date: None, - include_text: None, - exclude_text: None, - contents: None, - }) - .await; - } - - #[tokio::test] - #[ignore] - async fn test_answer() { - let client = ExaClientBuilder::default() - .api_key("test-key") - .build() - .unwrap(); - - let _ = client - .answer(AnswerRequest { - query: "What is the latest valuation of SpaceX?".to_string(), - text: None, - output_schema: None, - }) - .await; - } - - #[test] - fn test_build_missing_api_key() { - let result = ExaClientBuilder::default().build(); - assert!(result.is_err()); - } - - #[test] - fn test_build_defaults_api_base() { - let client = ExaClientBuilder::default().api_key("key").build().unwrap(); - assert_eq!(client.api_base.as_str(), "https://api.exa.ai/"); - } - - #[test] - fn test_build_custom_api_base() { - let client = ExaClientBuilder::default() - .api_key("key") - .api_base("https://custom.exa.ai") - .build() - .unwrap(); - assert_eq!(client.api_base.as_str(), "https://custom.exa.ai/"); - } - - #[test] - fn test_search_request_serializes_new_fields() { - let request = SearchRequest { - query: "Who is the CEO of OpenAI?".to_string(), - additional_queries: Some(vec!["OpenAI leadership".to_string()]), - stream: Some(true), - output_schema: Some(json!({ - "type": "object", - "properties": { "leader": { "type": "string" } }, - })), - system_prompt: Some("Prefer official sources".to_string()), - r#type: Some(SearchType::DeepReasoning), - category: Some(Category::News), - user_location: Some("US".to_string()), - num_results: Some(5), - include_domains: Some(vec!["openai.com".to_string()]), - exclude_domains: None, - start_crawl_date: None, - end_crawl_date: None, - start_published_date: None, - end_published_date: None, - include_text: None, - exclude_text: None, - moderation: Some(true), - contents: Some(ContentsRequest { - text: Some(TextRequest::Bool(true)), - highlights: Some(HighlightsRequest::Options(HighlightsOptions { - max_characters: Some(500), - num_sentences: None, - highlights_per_url: None, - query: Some("official announcement".to_string()), - })), - summary: Some(SummaryRequest { - query: Some("role and source".to_string()), - schema: Some(json!({ "type": "object" })), - }), - livecrawl: None, - livecrawl_timeout: None, - max_age_hours: Some(-1), - subpages: Some(1), - subpage_target: Some(SubpageTarget::String("sources".to_string())), - extras: Some(ExtrasRequest { - links: Some(1), - image_links: Some(1), - }), - context: None, - }), - }; - - let value = serde_json::to_value(request).unwrap(); - assert_eq!(value["additionalQueries"][0], "OpenAI leadership"); - assert_eq!(value["type"], "deep-reasoning"); - assert_eq!(value["contents"]["text"], true); - assert_eq!(value["contents"]["maxAgeHours"], -1); - assert_eq!(value["contents"]["extras"]["imageLinks"], 1); - } - - #[test] - fn test_search_response_deserializes_output_and_cost() { - let payload = json!({ - "requestId": "req_123", - "results": [ - { - "id": "doc_1", - "url": "https://example.com", - "title": "Example", - "image": "https://example.com/image.png", - "favicon": "https://example.com/favicon.ico" - } - ], - "searchType": "deep-reasoning", - "output": { - "content": { - "leader": "Sam Altman" - }, - "grounding": [ - { - "field": "leader", - "citations": [ - { "url": "https://openai.com", "title": "OpenAI" } - ], - "confidence": "high" - } - ] - }, - "costDollars": { - "total": 0.007 - } - }); - - let parsed: SearchResponse = serde_json::from_value(payload).unwrap(); - assert_eq!(parsed.request_id.as_deref(), Some("req_123")); - assert_eq!(parsed.search_type, Some(SearchResponseType::DeepReasoning)); - assert!(parsed.output.is_some()); - assert_eq!(parsed.cost_dollars.and_then(|c| c.total), Some(0.007)); - } - - #[test] - fn test_contents_response_deserializes_statuses_and_cost() { - let payload = json!({ - "requestId": "req_456", - "results": [ - { - "id": "https://example.com", - "url": "https://example.com", - "text": "hello" - } - ], - "statuses": [ - { - "id": "https://example.com", - "status": "success", - "error": null - } - ], - "costDollars": { - "total": 0.001 - } - }); - - let parsed: GetContentsResponse = serde_json::from_value(payload).unwrap(); - assert_eq!(parsed.request_id.as_deref(), Some("req_456")); - assert_eq!( - parsed - .statuses - .and_then(|s| s.first().cloned()) - .map(|s| s.status), - Some(ContentStatusState::Success) - ); - assert_eq!(parsed.cost_dollars.and_then(|c| c.total), Some(0.001)); - } -} diff --git a/crates/exa/src/search.rs b/crates/exa/src/search.rs deleted file mode 100644 index f3bfb6cb24..0000000000 --- a/crates/exa/src/search.rs +++ /dev/null @@ -1,77 +0,0 @@ -use crate::client::{ExaClient, parse_response}; -use crate::types::{ - Category, ContentsRequest, CostDollars, SearchOutput, SearchResponseType, SearchResult, - SearchType, -}; - -crate::float_derives! { - #[serde(rename_all = "camelCase")] - pub struct SearchRequest { - pub query: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub additional_queries: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub stream: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub output_schema: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub system_prompt: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub r#type: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub category: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub user_location: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub num_results: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub include_domains: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub exclude_domains: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub start_crawl_date: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub end_crawl_date: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub start_published_date: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub end_published_date: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub include_text: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub exclude_text: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub moderation: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub contents: Option, - } -} - -crate::float_derives! { - #[serde(rename_all = "camelCase")] - pub struct SearchResponse { - #[serde(skip_serializing_if = "Option::is_none")] - pub request_id: Option, - pub results: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub search_type: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub context: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub output: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub cost_dollars: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub autoprompt_string: Option, - } -} - -impl ExaClient { - pub async fn search(&self, req: SearchRequest) -> Result { - let mut url = self.api_base.clone(); - url.set_path("/search"); - - let response = self.client.post(url).json(&req).send().await?; - parse_response(response).await - } -} diff --git a/crates/exa/src/types.rs b/crates/exa/src/types.rs deleted file mode 100644 index 9bf0438f62..0000000000 --- a/crates/exa/src/types.rs +++ /dev/null @@ -1,411 +0,0 @@ -use crate::common_derives; - -use crate::float_derives; - -float_derives! { - #[serde(rename_all = "camelCase")] - pub struct SearchResult { - pub id: String, - pub url: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub published_date: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub author: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub score: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub text: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub highlights: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub highlight_scores: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub summary: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub image: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub favicon: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub subpages: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub extras: Option, - } -} - -common_derives! { - #[serde(rename_all = "camelCase")] - pub struct ResultExtras { - #[serde(skip_serializing_if = "Option::is_none")] - pub links: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub image_links: Option>, - } -} - -float_derives! { - #[serde(rename_all = "camelCase")] - pub struct ContentsRequest { - #[serde(skip_serializing_if = "Option::is_none")] - pub text: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub highlights: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub summary: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub livecrawl: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub livecrawl_timeout: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub max_age_hours: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub subpages: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub subpage_target: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub extras: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub context: Option, - } -} - -common_derives! { - #[serde(untagged)] - pub enum TextRequest { - Bool(bool), - Options(TextOptions), - } -} - -common_derives! { - #[serde(rename_all = "camelCase")] - pub struct TextOptions { - #[serde(skip_serializing_if = "Option::is_none")] - pub max_characters: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub include_html_tags: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub verbosity: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub include_sections: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub exclude_sections: Option>, - } -} - -common_derives! { - pub enum TextVerbosity { - #[serde(rename = "compact")] - Compact, - #[serde(rename = "standard")] - Standard, - #[serde(rename = "full")] - Full, - } -} - -common_derives! { - pub enum ContentSection { - #[serde(rename = "header")] - Header, - #[serde(rename = "navigation")] - Navigation, - #[serde(rename = "banner")] - Banner, - #[serde(rename = "body")] - Body, - #[serde(rename = "sidebar")] - Sidebar, - #[serde(rename = "footer")] - Footer, - #[serde(rename = "metadata")] - Metadata, - } -} - -common_derives! { - #[serde(untagged)] - pub enum HighlightsRequest { - Bool(bool), - Options(HighlightsOptions), - } -} - -common_derives! { - #[serde(rename_all = "camelCase")] - pub struct HighlightsOptions { - #[serde(skip_serializing_if = "Option::is_none")] - pub max_characters: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub num_sentences: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub highlights_per_url: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub query: Option, - } -} - -float_derives! { - #[serde(rename_all = "camelCase")] - pub struct SummaryRequest { - #[serde(skip_serializing_if = "Option::is_none")] - pub query: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub schema: Option, - } -} - -common_derives! { - #[serde(rename_all = "camelCase")] - pub struct ExtrasRequest { - #[serde(skip_serializing_if = "Option::is_none")] - pub links: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub image_links: Option, - } -} - -common_derives! { - #[serde(untagged)] - pub enum SubpageTarget { - String(String), - Array(Vec), - } -} - -common_derives! { - #[serde(untagged)] - pub enum ContextRequest { - Bool(bool), - Options(ContextOptions), - } -} - -common_derives! { - #[serde(rename_all = "camelCase")] - pub struct ContextOptions { - #[serde(skip_serializing_if = "Option::is_none")] - pub max_characters: Option, - } -} - -common_derives! { - pub enum Livecrawl { - #[serde(rename = "never")] - Never, - #[serde(rename = "fallback")] - Fallback, - #[serde(rename = "preferred")] - Preferred, - #[serde(rename = "always")] - Always, - } -} - -common_derives! { - pub enum SearchType { - #[serde(rename = "neural")] - Neural, - #[serde(rename = "fast")] - Fast, - #[serde(rename = "auto")] - Auto, - #[serde(rename = "deep-lite")] - DeepLite, - #[serde(rename = "deep")] - Deep, - #[serde(rename = "deep-reasoning")] - DeepReasoning, - #[serde(rename = "instant")] - Instant, - } -} - -common_derives! { - pub enum Category { - #[serde(rename = "company")] - Company, - #[serde(rename = "research paper")] - ResearchPaper, - #[serde(rename = "news")] - News, - #[serde(rename = "personal site")] - PersonalSite, - #[serde(rename = "financial report")] - FinancialReport, - #[serde(rename = "people")] - People, - } -} - -float_derives! { - #[serde(rename_all = "camelCase")] - pub struct CostDollars { - #[serde(skip_serializing_if = "Option::is_none")] - pub total: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub break_down: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub per_request_prices: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub per_page_prices: Option, - } -} - -float_derives! { - #[serde(rename_all = "camelCase")] - pub struct CostBreakdownItem { - #[serde(skip_serializing_if = "Option::is_none")] - pub search: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub contents: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub breakdown: Option, - } -} - -float_derives! { - #[serde(rename_all = "camelCase")] - pub struct CostBreakdownDetails { - #[serde(skip_serializing_if = "Option::is_none")] - pub neural_search: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub deep_search: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub content_text: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub content_highlight: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub content_summary: Option, - } -} - -float_derives! { - #[serde(rename_all = "camelCase")] - pub struct PerRequestPrices { - #[serde(skip_serializing_if = "Option::is_none")] - pub neural_search_1_10_results: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub neural_search_additional_result: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub deep_search: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub deep_reasoning_search: Option, - } -} - -float_derives! { - #[serde(rename_all = "camelCase")] - pub struct PerPagePrices { - #[serde(skip_serializing_if = "Option::is_none")] - pub content_text: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub content_highlight: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub content_summary: Option, - } -} - -common_derives! { - pub enum SearchResponseType { - #[serde(rename = "neural")] - Neural, - #[serde(rename = "deep")] - Deep, - #[serde(rename = "deep-reasoning")] - DeepReasoning, - } -} - -float_derives! { - pub struct SearchOutput { - pub content: serde_json::Value, - pub grounding: Vec, - } -} - -common_derives! { - pub struct OutputGrounding { - pub field: String, - pub citations: Vec, - pub confidence: GroundingConfidence, - } -} - -common_derives! { - pub struct OutputGroundingCitation { - pub url: String, - pub title: String, - } -} - -common_derives! { - pub enum GroundingConfidence { - #[serde(rename = "low")] - Low, - #[serde(rename = "medium")] - Medium, - #[serde(rename = "high")] - High, - } -} - -common_derives! { - #[serde(rename_all = "camelCase")] - pub struct ContentStatus { - pub id: String, - pub status: ContentStatusState, - #[serde(skip_serializing_if = "Option::is_none")] - pub error: Option, - } -} - -common_derives! { - pub enum ContentStatusState { - #[serde(rename = "success")] - Success, - #[serde(rename = "error")] - Error, - } -} - -common_derives! { - #[serde(rename_all = "camelCase")] - pub struct ContentStatusError { - pub tag: CrawlErrorTag, - #[serde(skip_serializing_if = "Option::is_none")] - pub http_status_code: Option, - } -} - -common_derives! { - pub enum CrawlErrorTag { - #[serde(rename = "CRAWL_NOT_FOUND")] - CrawlNotFound, - #[serde(rename = "CRAWL_TIMEOUT")] - CrawlTimeout, - #[serde(rename = "CRAWL_LIVECRAWL_TIMEOUT")] - CrawlLivecrawlTimeout, - #[serde(rename = "SOURCE_NOT_AVAILABLE")] - SourceNotAvailable, - #[serde(rename = "UNSUPPORTED_URL")] - UnsupportedUrl, - #[serde(rename = "CRAWL_UNKNOWN_ERROR")] - CrawlUnknownError, - } -} - -common_derives! { - #[serde(rename_all = "camelCase")] - pub struct AnswerCitation { - pub id: String, - pub url: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub published_date: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub author: Option, - } -} diff --git a/crates/exedev/Cargo.toml b/crates/exedev/Cargo.toml deleted file mode 100644 index c57edbcfaf..0000000000 --- a/crates/exedev/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "exedev" -version = "0.1.0" -edition = "2024" - -[dependencies] -base64 = { workspace = true } -chrono = { workspace = true, features = ["serde"] } -reqwest = { workspace = true, features = ["json"] } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -shell-escape = "0.1" -ssh-key = { version = "0.6", default-features = false, features = ["alloc", "ed25519"] } -thiserror = { workspace = true } -url = { workspace = true } - -[dev-dependencies] -rand_core = { version = "0.6.4", features = ["getrandom"] } -tokio = { workspace = true, features = ["rt", "macros"] } diff --git a/crates/exedev/assets/claw-setup.sh b/crates/exedev/assets/claw-setup.sh deleted file mode 100644 index bc3166c1bf..0000000000 --- a/crates/exedev/assets/claw-setup.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -repo=${CLAW_REPO:-https://github.com/fastrepl/char} -ref=${CLAW_REF:-main} - -if ! command -v cargo >/dev/null 2>&1; then - curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal -fi - -source "${HOME}/.cargo/env" - -cargo install \ - --git "${repo}" \ - --branch "${ref}" \ - --locked \ - --bin claw \ - --force - -npm install -g @googleworkspace/cli -npm install -g char diff --git a/crates/exedev/src/client.rs b/crates/exedev/src/client.rs deleted file mode 100644 index 2aa5be39fd..0000000000 --- a/crates/exedev/src/client.rs +++ /dev/null @@ -1,130 +0,0 @@ -use serde::de::DeserializeOwned; - -use crate::token::{Exe0Token, NAMESPACE_API}; - -pub const DEFAULT_API_BASE: &str = "https://exe.dev"; - -#[derive(Default)] -pub struct ExedevClientBuilder { - api_base: Option, - token: Option, - signing_key: Option, - permissions: Option, - http: Option, -} - -/// Thin HTTPS client for `POST https://exe.dev/exec`. -/// -/// Clone is cheap: the underlying `reqwest::Client` is an `Arc`. -#[derive(Clone)] -pub struct ExedevClient { - pub(crate) client: reqwest::Client, - pub(crate) api_base: url::Url, - pub(crate) token: String, -} - -impl ExedevClient { - pub fn builder() -> ExedevClientBuilder { - ExedevClientBuilder::default() - } - - pub fn api_base(&self) -> &url::Url { - &self.api_base - } -} - -impl ExedevClientBuilder { - pub fn api_base(mut self, api_base: impl Into) -> Self { - self.api_base = Some(api_base.into()); - self - } - - pub fn token(mut self, token: impl Into) -> Self { - self.token = Some(token.into()); - self - } - - /// Raw OpenSSH PEM private key used to mint an exe0 token at build time. - pub fn signing_key(mut self, signing_key: impl Into) -> Self { - self.signing_key = Some(signing_key.into()); - self - } - - /// Permissions JSON used when minting an exe0 token from a signing key. - /// - /// The server's default `cmds` list does not include `rm`, `stat`, `rename`, - /// `tag`, `resize`, `restart`, or `share` subcommands; pass an explicit - /// `cmds` array if the caller needs them. - pub fn permissions(mut self, permissions: serde_json::Value) -> Self { - self.permissions = Some(permissions); - self - } - - /// Provide a pre-configured reqwest client (e.g. with custom timeouts). - pub fn http_client(mut self, http: reqwest::Client) -> Self { - self.http = Some(http); - self - } - - pub fn build(self) -> Result { - let api_base = self - .api_base - .unwrap_or_else(|| DEFAULT_API_BASE.to_string()); - let api_base: url::Url = api_base.parse().map_err(|_| crate::Error::InvalidApiBase)?; - - let token = match (self.token, self.signing_key, self.permissions) { - (Some(t), _, _) => t, - (None, Some(key), Some(perms)) => { - Exe0Token::mint(&perms, &key, NAMESPACE_API)?.into_string() - } - _ => return Err(crate::Error::MissingToken), - }; - - let client = match self.http { - Some(c) => c, - None => reqwest::Client::builder().build()?, - }; - Ok(ExedevClient { - client, - api_base, - token, - }) - } -} - -impl ExedevClient { - /// Execute a raw command string against `/exec` and return the text body. - /// - /// Callers generally want the typed helpers in `commands`; use this only - /// as an escape hatch for commands the SDK has not yet wrapped. - pub async fn exec_raw(&self, command: &str) -> Result { - let mut url = self.api_base.clone(); - url.set_path("/exec"); - - let response = self - .client - .post(url) - .bearer_auth(&self.token) - .header(reqwest::header::CONTENT_TYPE, "text/plain") - .body(command.to_owned()) - .send() - .await?; - - parse_text(response).await - } - - pub async fn exec_json(&self, command: &str) -> Result { - let text = self.exec_raw(command).await?; - Ok(serde_json::from_str(&text)?) - } -} - -pub(crate) async fn parse_text(response: reqwest::Response) -> Result { - let status = response.status(); - let body = response.text().await?; - if status.is_success() { - Ok(body) - } else { - Err(crate::Error::from_status(status.as_u16(), body)) - } -} diff --git a/crates/exedev/src/commands.rs b/crates/exedev/src/commands.rs deleted file mode 100644 index 53e9a5806e..0000000000 --- a/crates/exedev/src/commands.rs +++ /dev/null @@ -1,470 +0,0 @@ -use serde::{Deserialize, Serialize}; -use shell_escape::unix::escape; - -use crate::client::ExedevClient; -use crate::models::{GeneratedApiKey, SshKey, SshKeyList, Vm, VmList, VmStat, WhoAmI}; -use crate::token::Exe1Token; - -/// Arguments for `new`. Build with the chainable setters. -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct VmNewArgs { - pub name: Option, - pub image: Option, - pub disk: Option, - pub command: Option, - pub integrations: Vec, - pub env: Vec<(String, String)>, - pub no_email: bool, -} - -impl VmNewArgs { - pub fn new() -> Self { - Self::default() - } - - pub fn name(mut self, name: impl Into) -> Self { - self.name = Some(name.into()); - self - } - - pub fn image(mut self, image: impl Into) -> Self { - self.image = Some(image.into()); - self - } - - pub fn disk(mut self, disk: impl Into) -> Self { - self.disk = Some(disk.into()); - self - } - - pub fn command(mut self, command: impl Into) -> Self { - self.command = Some(command.into()); - self - } - - pub fn integration(mut self, integration: impl Into) -> Self { - self.integrations.push(integration.into()); - self - } - - pub fn env(mut self, k: impl Into, v: impl Into) -> Self { - self.env.push((k.into(), v.into())); - self - } - - pub fn envs(mut self, vars: I) -> Self - where - I: IntoIterator, - K: Into, - V: Into, - { - for (k, v) in vars { - self.env.push((k.into(), v.into())); - } - self - } - - pub fn no_email(mut self, yes: bool) -> Self { - self.no_email = yes; - self - } -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct VmResizeSpec { - pub disk: Option, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct VmCopySpec { - pub new_name: Option, - pub copy_tags: Option, - pub disk: Option, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ShareVisibility { - Public, - Private, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct GenerateApiKeyArgs { - pub label: Option, - pub vm: Option, - pub cmds: Vec, - pub exp: Option, -} - -fn quote(s: &str) -> String { - escape(std::borrow::Cow::Borrowed(s)).into_owned() -} - -fn validate_vm_name(name: &str) -> Result<(), crate::Error> { - if name.is_empty() { - return Err(crate::Error::InvalidVmName("empty".into())); - } - if name - .chars() - .any(|c| !(c.is_ascii_alphanumeric() || c == '-')) - || name.starts_with('-') - || name.ends_with('-') - { - return Err(crate::Error::InvalidVmName(name.to_string())); - } - Ok(()) -} - -impl ExedevClient { - // ===== VM lifecycle ===== - - pub async fn vm_list(&self) -> Result, crate::Error> { - let list: VmList = self.exec_json("ls -l -a --json").await?; - Ok(list.vms) - } - - /// List VMs matching a name or pattern. exe.dev's `ls` accepts either. - pub async fn vm_list_matching(&self, name_or_pattern: &str) -> Result, crate::Error> { - let cmd = format!("ls {} -l -a --json", quote(name_or_pattern)); - let list: VmList = self.exec_json(&cmd).await?; - Ok(list.vms) - } - - /// Look up a VM by exact name. Returns `None` if not found. - pub async fn vm_get(&self, name: &str) -> Result, crate::Error> { - validate_vm_name(name)?; - let vms = self.vm_list_matching(name).await?; - Ok(vms.into_iter().find(|v| v.name == name)) - } - - /// Create a VM. Returns the parsed `new --json` response. - /// - /// To get a strongly-typed `Vm`, follow up with `vm_get(name)` once the - /// VM reaches `running`. - pub async fn vm_new(&self, args: VmNewArgs) -> Result { - let cmd = vm_new_command(&args)?; - self.exec_json(&cmd).await - } - - /// Create a VM and block on a subsequent `ls` lookup to return the typed record. - /// - /// Requires that `args.name` is set — otherwise we can't look the VM up - /// deterministically. - pub async fn vm_new_typed(&self, args: VmNewArgs) -> Result { - let name = args.name.clone().ok_or_else(|| { - crate::Error::InvalidVmName("name is required for vm_new_typed".into()) - })?; - self.vm_new(args).await?; - self.vm_get(&name) - .await? - .ok_or_else(|| crate::Error::NotFound(format!("vm {name} not found after create"))) - } - - pub async fn vm_remove(&self, names: &[&str]) -> Result<(), crate::Error> { - if names.is_empty() { - return Ok(()); - } - let mut cmd = String::from("rm"); - for n in names { - validate_vm_name(n)?; - cmd.push(' '); - cmd.push_str("e(n)); - } - self.exec_raw(&cmd).await.map(|_| ()) - } - - pub async fn vm_restart(&self, name: &str) -> Result { - validate_vm_name(name)?; - let cmd = format!("restart {} --json", quote(name)); - self.exec_json(&cmd).await - } - - pub async fn vm_rename(&self, old: &str, new: &str) -> Result<(), crate::Error> { - validate_vm_name(old)?; - validate_vm_name(new)?; - let cmd = format!("rename {} {}", quote(old), quote(new)); - self.exec_raw(&cmd).await.map(|_| ()) - } - - pub async fn vm_tag_add(&self, name: &str, tag: &str) -> Result<(), crate::Error> { - validate_vm_name(name)?; - let cmd = format!("tag {} {}", quote(name), quote(tag)); - self.exec_raw(&cmd).await.map(|_| ()) - } - - pub async fn vm_tag_remove(&self, name: &str, tag: &str) -> Result<(), crate::Error> { - validate_vm_name(name)?; - let cmd = format!("tag -d {} {}", quote(name), quote(tag)); - self.exec_raw(&cmd).await.map(|_| ()) - } - - pub async fn vm_resize( - &self, - name: &str, - spec: VmResizeSpec, - ) -> Result { - validate_vm_name(name)?; - let mut cmd = format!("resize {}", quote(name)); - if let Some(disk) = &spec.disk { - cmd.push_str(&format!(" --disk {}", quote(disk))); - } - cmd.push_str(" --json"); - self.exec_json(&cmd).await - } - - pub async fn vm_copy(&self, source: &str, spec: VmCopySpec) -> Result { - validate_vm_name(source)?; - if let Some(n) = &spec.new_name { - validate_vm_name(n)?; - } - let mut cmd = format!("cp {}", quote(source)); - if let Some(new_name) = &spec.new_name { - cmd.push(' '); - cmd.push_str("e(new_name)); - } - if let Some(copy_tags) = spec.copy_tags { - cmd.push_str(&format!(" --copy-tags={copy_tags}")); - } - if let Some(disk) = &spec.disk { - cmd.push_str(&format!(" --disk {}", quote(disk))); - } - cmd.push_str(" --json"); - let value: serde_json::Value = self.exec_json(&cmd).await?; - if let Ok(vm) = serde_json::from_value::(value.clone()) { - return Ok(vm); - } - if let Some(name) = spec.new_name.as_deref() - && let Some(vm) = self.vm_get(name).await? - { - return Ok(vm); - } - Err(crate::Error::NotFound(format!( - "cp response did not include vm record: {value}" - ))) - } - - pub async fn vm_stat(&self, name: &str) -> Result { - validate_vm_name(name)?; - let cmd = format!("stat {}", quote(name)); - self.exec_json(&cmd).await - } - - // ===== Sharing / proxy ===== - - pub async fn share_port(&self, name: &str, port: u16) -> Result<(), crate::Error> { - validate_vm_name(name)?; - let cmd = format!("share port {} {port}", quote(name)); - self.exec_raw(&cmd).await.map(|_| ()) - } - - pub async fn share_visibility( - &self, - name: &str, - visibility: ShareVisibility, - ) -> Result<(), crate::Error> { - validate_vm_name(name)?; - let verb = match visibility { - ShareVisibility::Public => "set-public", - ShareVisibility::Private => "set-private", - }; - let cmd = format!("share {verb} {}", quote(name)); - self.exec_raw(&cmd).await.map(|_| ()) - } - - pub async fn share_show(&self, name: &str) -> Result { - validate_vm_name(name)?; - let cmd = format!("share show {} --json", quote(name)); - self.exec_json(&cmd).await - } - - // ===== SSH keys ===== - - pub async fn ssh_key_list(&self) -> Result, crate::Error> { - let list: SshKeyList = self.exec_json("ssh-key list --json").await?; - Ok(list.ssh_keys) - } - - pub async fn ssh_key_add(&self, public_key: &str) -> Result<(), crate::Error> { - let cmd = format!("ssh-key add {}", quote(public_key)); - self.exec_raw(&cmd).await.map(|_| ()) - } - - pub async fn ssh_key_remove(&self, name_or_fingerprint: &str) -> Result<(), crate::Error> { - let cmd = format!("ssh-key remove {}", quote(name_or_fingerprint)); - self.exec_raw(&cmd).await.map(|_| ()) - } - - pub async fn ssh_key_rename(&self, old: &str, new: &str) -> Result<(), crate::Error> { - let cmd = format!("ssh-key rename {} {}", quote(old), quote(new)); - self.exec_raw(&cmd).await.map(|_| ()) - } - - /// Thin wrapper around `ssh-key generate-api-key`. The server generates a - /// key and returns a token; the exact response shape is surfaced through - /// `GeneratedApiKey::extra` as well as the typed `token` field. - pub async fn ssh_key_generate_api_key( - &self, - args: GenerateApiKeyArgs, - ) -> Result { - let mut cmd = String::from("ssh-key generate-api-key"); - if let Some(label) = &args.label { - cmd.push_str(&format!(" --label={}", quote(label))); - } - if let Some(vm) = &args.vm { - validate_vm_name(vm)?; - cmd.push_str(&format!(" --vm={}", quote(vm))); - } - if !args.cmds.is_empty() { - cmd.push_str(&format!(" --cmds={}", quote(&args.cmds.join(",")))); - } - if let Some(exp) = &args.exp { - cmd.push_str(&format!(" --exp={}", quote(exp))); - } - cmd.push_str(" --json"); - self.exec_json(&cmd).await - } - - // ===== Identity / utility ===== - - pub async fn whoami(&self) -> Result { - self.exec_json("whoami --json").await - } - - pub async fn help(&self) -> Result { - self.exec_json("help --json").await - } - - pub async fn set_region(&self, region: &str) -> Result<(), crate::Error> { - let cmd = format!("set-region {}", quote(region)); - self.exec_raw(&cmd).await.map(|_| ()) - } - - /// Convert an exe0 token to a shorter exe1 handle. - /// - /// Pass `Some(vm)` if the exe0 token is VM-scoped. - pub async fn exe0_to_exe1( - &self, - exe0: &str, - vm: Option<&str>, - ) -> Result { - let mut cmd = String::from("exe0-to-exe1"); - if let Some(vm) = vm { - validate_vm_name(vm)?; - cmd.push_str(&format!(" --vm={}", quote(vm))); - } - cmd.push(' '); - cmd.push_str("e(exe0)); - let text = self.exec_raw(&cmd).await?; - Ok(Exe1Token::new(text.trim().to_string())) - } -} - -fn vm_new_command(args: &VmNewArgs) -> Result { - let mut cmd = String::from("new"); - if let Some(name) = &args.name { - validate_vm_name(name)?; - cmd.push_str(&format!(" --name {}", quote(name))); - } - if let Some(image) = &args.image { - cmd.push_str(&format!(" --image {}", quote(image))); - } - if let Some(disk) = &args.disk { - cmd.push_str(&format!(" --disk {}", quote(disk))); - } - if let Some(command) = &args.command { - cmd.push_str(&format!(" --command {}", quote(command))); - } - for integration in &args.integrations { - cmd.push_str(&format!(" --integration {}", quote(integration))); - } - for (k, v) in &args.env { - cmd.push_str(&format!(" --env {}", quote(&format!("{k}={v}")))); - } - if args.no_email { - cmd.push_str(" --no-email"); - } - cmd.push_str(" --json"); - Ok(cmd) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn quoting_handles_spaces_and_quotes() { - assert_eq!(quote("simple"), "simple"); - let q = quote("with space"); - assert!(q.contains(' ')); - assert!(q.starts_with('\'') || q.starts_with('"')); - let q = quote("it's"); - assert!(q.contains("it")); - } - - #[test] - fn rejects_bad_vm_names() { - assert!(validate_vm_name("").is_err()); - assert!(validate_vm_name("-foo").is_err()); - assert!(validate_vm_name("foo-").is_err()); - assert!(validate_vm_name("foo bar").is_err()); - assert!(validate_vm_name("foo_bar").is_err()); - assert!(validate_vm_name("foo.bar").is_err()); - assert!(validate_vm_name("claw-ab12cd34").is_ok()); - assert!(validate_vm_name("a").is_ok()); - } - - #[test] - fn vm_new_command_with_everything() { - let args = VmNewArgs::new() - .name("claw-ab12") - .image("ghcr.io/fastrepl/char-claw:latest") - .disk("20GB") - .env("OPENAI_API_KEY", "sk with space") - .env("FOO", "bar") - .integration("proxy") - .no_email(true); - let cmd = vm_new_command(&args).unwrap(); - assert!(cmd.starts_with("new ")); - assert!(cmd.contains("--name claw-ab12"), "cmd: {cmd}"); - assert!( - cmd.contains("ghcr.io/fastrepl/char-claw:latest"), - "cmd: {cmd}" - ); - assert!(cmd.contains("--disk 20GB"), "cmd: {cmd}"); - assert!(cmd.contains("--integration proxy"), "cmd: {cmd}"); - assert!(cmd.contains("OPENAI_API_KEY=sk with space"), "cmd: {cmd}"); - assert!(cmd.contains("FOO=bar"), "cmd: {cmd}"); - assert!(cmd.contains("--no-email"), "cmd: {cmd}"); - assert!(cmd.ends_with(" --json"), "cmd: {cmd}"); - } - - #[test] - fn vm_new_command_rejects_bad_name() { - let args = VmNewArgs::new().name("bad name!"); - assert!(vm_new_command(&args).is_err()); - } - - #[test] - fn vm_list_parses_live_response_shape() { - let body = r#"{"vms":[{"created_at":"2026-04-15T12:52:12Z","https_url":"https://openclaw-yujong.exe.xyz","image":"boldsoftware/exeuntu","region":"pdx","region_display":"Oregon, USA","shelley_url":"https://openclaw-yujong.shelley.exe.xyz","ssh_dest":"openclaw-yujong.exe.xyz","status":"running","vm_name":"openclaw-yujong"}]}"#; - let list: VmList = serde_json::from_str(body).unwrap(); - assert_eq!(list.vms.len(), 1); - let vm = &list.vms[0]; - assert_eq!(vm.name, "openclaw-yujong"); - assert_eq!(vm.image, "boldsoftware/exeuntu"); - assert!(matches!(vm.status, crate::models::VmStatus::Running)); - assert_eq!( - vm.https_url.as_deref(), - Some("https://openclaw-yujong.exe.xyz") - ); - } - - #[test] - fn whoami_parses_live_response_shape() { - let body = r#"{"email":"yujonglee.dev@gmail.com","region":"","region_display":"","ssh_keys":[{"public_key":"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMRZFrRLmFuyKeAegOReWZSy3Hc+3PUlzPzz+1emJHqP","fingerprint":"SHA256:cOXaFfYsFLGAryWgjY2JFz7IFpqQg6Zu5FOqJmi1tXo","name":"dev","current":false}]}"#; - let whoami: WhoAmI = serde_json::from_str(body).unwrap(); - assert_eq!(whoami.email, "yujonglee.dev@gmail.com"); - assert_eq!(whoami.ssh_keys.len(), 1); - assert_eq!(whoami.ssh_keys[0].name, "dev"); - } -} diff --git a/crates/exedev/src/error.rs b/crates/exedev/src/error.rs deleted file mode 100644 index 008ef8500a..0000000000 --- a/crates/exedev/src/error.rs +++ /dev/null @@ -1,82 +0,0 @@ -use serde::{Serialize, ser::Serializer}; - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("bad request (400): {0}")] - BadRequest(String), - #[error("unauthorized (401): {0}")] - Unauthorized(String), - #[error("forbidden (403): {0}")] - Forbidden(String), - #[error("not found (404): {0}")] - NotFound(String), - #[error("method not allowed (405): {0}")] - MethodNotAllowed(String), - #[error("request too large (413): {0}")] - RequestTooLarge(String), - #[error("command failed (422): {0}")] - CommandFailed(String), - #[error("rate limited (429): {0}")] - RateLimited(String), - #[error("internal server error (500): {0}")] - Internal(String), - #[error("timeout (504): {0}")] - Timeout(String), - #[error("API error (status {0}): {1}")] - Api(u16, String), - - #[error(transparent)] - Request(#[from] reqwest::Error), - #[error(transparent)] - Json(#[from] serde_json::Error), - - #[error("invalid signing key: {0}")] - InvalidSigningKey(String), - #[error("signing failed: {0}")] - Signing(String), - #[error("invalid permissions: {0}")] - InvalidPermissions(&'static str), - #[error("invalid vm name: {0}")] - InvalidVmName(String), - #[error("invalid api base url")] - InvalidApiBase, - #[error("invalid vm url")] - InvalidVmUrl, - #[error("missing token or signing key")] - MissingToken, -} - -impl Error { - pub(crate) fn from_status(status: u16, body: String) -> Self { - let message = extract_error_message(&body).unwrap_or(body); - match status { - 400 => Error::BadRequest(message), - 401 => Error::Unauthorized(message), - 403 => Error::Forbidden(message), - 404 => Error::NotFound(message), - 405 => Error::MethodNotAllowed(message), - 413 => Error::RequestTooLarge(message), - 422 => Error::CommandFailed(message), - 429 => Error::RateLimited(message), - 500 => Error::Internal(message), - 504 => Error::Timeout(message), - other => Error::Api(other, message), - } - } -} - -fn extract_error_message(body: &str) -> Option { - let v: serde_json::Value = serde_json::from_str(body).ok()?; - v.get("error") - .and_then(|e| e.as_str()) - .map(|s| s.to_string()) -} - -impl Serialize for Error { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: Serializer, - { - serializer.serialize_str(self.to_string().as_ref()) - } -} diff --git a/crates/exedev/src/http_vm.rs b/crates/exedev/src/http_vm.rs deleted file mode 100644 index 157fe511b5..0000000000 --- a/crates/exedev/src/http_vm.rs +++ /dev/null @@ -1,148 +0,0 @@ -use reqwest::Method; -use serde::{Serialize, de::DeserializeOwned}; - -use crate::client::parse_text; - -/// Authentication for requests against a VM's HTTPS proxy. -#[derive(Clone)] -pub enum VmAuth { - /// `Authorization: Bearer ` — either exe0 or exe1. - Bearer(String), - /// HTTP basic; username is ignored by the proxy, password is the token. - /// Useful for tools like `git` that can't send bearer headers. - Basic(String), -} - -impl std::fmt::Debug for VmAuth { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Bearer(_) => f.write_str("VmAuth::Bearer()"), - Self::Basic(_) => f.write_str("VmAuth::Basic()"), - } - } -} - -/// HTTPS client for a single VM's auth-proxy (`https://.exe.xyz`). -/// -/// The proxy verifies the bearer/basic token with exe.dev and, on success, -/// injects `X-ExeDev-UserID`, `X-ExeDev-Email`, and `X-ExeDev-Token-Ctx` into -/// the request forwarded to your VM's HTTP server. -#[derive(Clone)] -pub struct VmHttpClient { - client: reqwest::Client, - base: url::Url, - auth: VmAuth, -} - -impl std::fmt::Debug for VmHttpClient { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("VmHttpClient") - .field("base", &self.base.as_str()) - .field("auth", &self.auth) - .finish() - } -} - -impl VmHttpClient { - /// Build a client pointed at `https://.exe.xyz`. - pub fn for_vm(vm_name: &str, auth: VmAuth) -> Result { - let base = format!("https://{vm_name}.exe.xyz") - .parse() - .map_err(|_| crate::Error::InvalidVmUrl)?; - Self::with_base(base, auth) - } - - /// Build a client pointed at an explicit VM URL (useful for custom hosts). - pub fn with_base(base: url::Url, auth: VmAuth) -> Result { - let client = reqwest::Client::builder().build()?; - Ok(Self { client, base, auth }) - } - - pub fn base(&self) -> &url::Url { - &self.base - } - - fn url_for(&self, path: &str) -> Result { - let trimmed = path.trim_start_matches('/'); - self.base - .join(trimmed) - .map_err(|_| crate::Error::InvalidVmUrl) - } - - fn authed(&self, builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder { - match &self.auth { - VmAuth::Bearer(token) => builder.bearer_auth(token), - VmAuth::Basic(password) => builder.basic_auth("exe", Some(password)), - } - } - - pub async fn request( - &self, - method: Method, - path: &str, - ) -> Result { - let url = self.url_for(path)?; - Ok(self.authed(self.client.request(method, url))) - } - - pub async fn get_text(&self, path: &str) -> Result { - let resp = self.request(Method::GET, path).await?.send().await?; - parse_text(resp).await - } - - pub async fn get_json(&self, path: &str) -> Result { - let text = self.get_text(path).await?; - Ok(serde_json::from_str(&text)?) - } - - pub async fn post_json( - &self, - path: &str, - body: &B, - ) -> Result { - let resp = self - .request(Method::POST, path) - .await? - .json(body) - .send() - .await?; - parse_text(resp).await - } - - pub async fn post_json_for( - &self, - path: &str, - body: &B, - ) -> Result { - let text = self.post_json(path, body).await?; - Ok(serde_json::from_str(&text)?) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn for_vm_builds_https_base() { - let c = VmHttpClient::for_vm("claw-abc", VmAuth::Bearer("t".into())).unwrap(); - assert_eq!(c.base().as_str(), "https://claw-abc.exe.xyz/"); - } - - #[test] - fn joins_paths_without_stripping_base() { - let c = VmHttpClient::for_vm("claw-abc", VmAuth::Bearer("t".into())).unwrap(); - let url = c.url_for("/v1/health").unwrap(); - assert_eq!(url.as_str(), "https://claw-abc.exe.xyz/v1/health"); - let url = c.url_for("v1/health").unwrap(); - assert_eq!(url.as_str(), "https://claw-abc.exe.xyz/v1/health"); - } - - #[test] - fn vm_auth_redacts_in_debug() { - let b = VmAuth::Bearer("secret".into()); - assert!(!format!("{b:?}").contains("secret")); - let b = VmAuth::Basic("secret".into()); - assert!(!format!("{b:?}").contains("secret")); - } -} diff --git a/crates/exedev/src/lib.rs b/crates/exedev/src/lib.rs deleted file mode 100644 index bd01234652..0000000000 --- a/crates/exedev/src/lib.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! Typed client for the exe.dev HTTPS API (`POST /exec`) and VM HTTPS proxy. -//! -//! # Crate layout -//! -//! - [`ExedevClient`] drives `POST https://exe.dev/exec`. One of: -//! - a pre-minted bearer `token` (exe0 or exe1), or -//! - a `signing_key` + `permissions` to mint an exe0 at build time. -//! - [`VmHttpClient`] drives the per-VM auth proxy at `https://.exe.xyz`. -//! - [`Exe0Token`] / [`Exe1Token`] model the two token formats, including -//! [`Exe0Token::mint_for_vm`] for VM-scoped tokens that set -//! `X-ExeDev-Token-Ctx` on your VM's HTTP server. -//! -//! All live-wire response shapes are typed in [`models`]; see -//! `commands::tests` for fixtures derived from the live API. -mod client; -mod commands; -mod error; -mod http_vm; -pub mod models; -mod token; - -pub use client::{DEFAULT_API_BASE, ExedevClient, ExedevClientBuilder}; -pub use commands::{GenerateApiKeyArgs, ShareVisibility, VmCopySpec, VmNewArgs, VmResizeSpec}; -pub use error::Error; -pub use http_vm::{VmAuth, VmHttpClient}; -pub use models::{GeneratedApiKey, SshKey, Vm, VmStat, VmStatus, WhoAmI}; -pub use token::{Exe0Token, Exe1Token, NAMESPACE_API, Permissions, namespace_vm}; diff --git a/crates/exedev/src/models.rs b/crates/exedev/src/models.rs deleted file mode 100644 index ea8f7be5b6..0000000000 --- a/crates/exedev/src/models.rs +++ /dev/null @@ -1,91 +0,0 @@ -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] -pub enum VmStatus { - Running, - Stopped, - Starting, - Stopping, - #[serde(other)] - Unknown, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Vm { - #[serde(rename = "vm_name")] - pub name: String, - pub image: String, - pub status: VmStatus, - #[serde(default)] - pub region: String, - #[serde(default)] - pub region_display: String, - #[serde(default)] - pub https_url: Option, - #[serde(default)] - pub shelley_url: Option, - #[serde(default)] - pub ssh_dest: Option, - pub created_at: DateTime, -} - -#[derive(Debug, Clone, Deserialize)] -pub(crate) struct VmList { - #[serde(default)] - pub vms: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SshKey { - pub public_key: String, - pub fingerprint: String, - pub name: String, - #[serde(default)] - pub added_at: Option>, - #[serde(default)] - pub current: bool, -} - -#[derive(Debug, Clone, Deserialize)] -pub(crate) struct SshKeyList { - #[serde(default)] - pub ssh_keys: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WhoAmI { - #[serde(default)] - pub email: String, - #[serde(default)] - pub region: String, - #[serde(default)] - pub region_display: String, - #[serde(default)] - pub ssh_keys: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct VmStat { - #[serde(default)] - pub disk_used_gb: Option, - #[serde(default)] - pub disk_total_gb: Option, - #[serde(default)] - pub bandwidth_gb: Option, - #[serde(flatten)] - pub extra: serde_json::Map, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GeneratedApiKey { - #[serde(default)] - pub token: Option, - #[serde(default)] - pub label: Option, - #[serde(default)] - pub vm: Option, - #[serde(flatten)] - pub extra: serde_json::Map, -} diff --git a/crates/exedev/src/token.rs b/crates/exedev/src/token.rs deleted file mode 100644 index 40c1746df7..0000000000 --- a/crates/exedev/src/token.rs +++ /dev/null @@ -1,303 +0,0 @@ -use base64::{Engine, engine::general_purpose::STANDARD, engine::general_purpose::URL_SAFE_NO_PAD}; -use serde::{Deserialize, Serialize}; -use ssh_key::{HashAlg, LineEnding, PrivateKey}; - -pub const NAMESPACE_API: &str = "v0@exe.dev"; - -pub fn namespace_vm(vm_name: &str) -> String { - format!("v0@{vm_name}.exe.xyz") -} - -const MAX_TOKEN_BYTES: usize = 8 * 1024; -const MIN_TIMESTAMP: i64 = 946_684_800; -const MAX_TIMESTAMP: i64 = 4_102_444_800; -const ALLOWED_TOP_LEVEL: &[&str] = &["exp", "nbf", "cmds", "ctx"]; - -#[derive(Clone)] -pub struct Exe0Token(String); - -impl std::fmt::Debug for Exe0Token { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("Exe0Token").field(&"").finish() - } -} - -impl Exe0Token { - pub fn mint( - permissions: &serde_json::Value, - signing_key_pem: &str, - namespace: &str, - ) -> Result { - let obj = permissions - .as_object() - .ok_or(crate::Error::InvalidPermissions( - "permissions must be a JSON object", - ))?; - - for key in obj.keys() { - if !ALLOWED_TOP_LEVEL.contains(&key.as_str()) { - return Err(crate::Error::InvalidPermissions( - "unknown top-level field; allowed: exp, nbf, cmds, ctx", - )); - } - } - - for field in ["exp", "nbf"] { - if let Some(v) = obj.get(field) { - let n = v.as_i64().ok_or(crate::Error::InvalidPermissions( - "exp and nbf must be integers", - ))?; - if !(MIN_TIMESTAMP..=MAX_TIMESTAMP).contains(&n) { - return Err(crate::Error::InvalidPermissions( - "exp/nbf must be between 2000-01-01 and 2100-01-01", - )); - } - } - } - - let payload_bytes = serde_json::to_vec(permissions)?; - - if payload_bytes.is_empty() || payload_bytes.len() > MAX_TOKEN_BYTES { - return Err(crate::Error::InvalidPermissions( - "serialized permissions too large (>8KiB)", - )); - } - if payload_bytes - .iter() - .any(|b| *b == 0 || *b == b'\n' || *b == b'\r') - { - return Err(crate::Error::InvalidPermissions( - "serialized permissions may not contain null or newline bytes", - )); - } - - let payload_b64 = URL_SAFE_NO_PAD.encode(&payload_bytes); - - let key = PrivateKey::from_openssh(signing_key_pem) - .map_err(|e| crate::Error::InvalidSigningKey(e.to_string()))?; - - let sig = key - .sign(namespace, HashAlg::Sha512, &payload_bytes) - .map_err(|e| crate::Error::Signing(e.to_string()))?; - - let pem = sig - .to_pem(LineEnding::LF) - .map_err(|e| crate::Error::Signing(e.to_string()))?; - - let sig_bytes = decode_pem_body(&pem) - .ok_or(crate::Error::Signing("malformed SSH signature PEM".into()))?; - let sig_b64 = URL_SAFE_NO_PAD.encode(&sig_bytes); - - Ok(Self(format!("exe0.{payload_b64}.{sig_b64}"))) - } - - pub fn mint_for_api( - permissions: &serde_json::Value, - signing_key_pem: &str, - ) -> Result { - Self::mint(permissions, signing_key_pem, NAMESPACE_API) - } - - pub fn mint_for_vm( - vm_name: &str, - permissions: &serde_json::Value, - signing_key_pem: &str, - ) -> Result { - Self::mint(permissions, signing_key_pem, &namespace_vm(vm_name)) - } - - pub fn as_str(&self) -> &str { - &self.0 - } - - pub fn into_string(self) -> String { - self.0 - } -} - -/// Short, opaque token that acts as a handle to an `exe0` token. -/// -/// Obtained via `ExedevClient::exe0_to_exe1`; the server validates the exe0 on -/// every request, so revoking the exe0 (or its signing key) revokes the exe1. -#[derive(Clone)] -pub struct Exe1Token(String); - -impl std::fmt::Debug for Exe1Token { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("Exe1Token").field(&"").finish() - } -} - -impl Exe1Token { - pub fn new(raw: impl Into) -> Self { - Self(raw.into()) - } - - pub fn as_str(&self) -> &str { - &self.0 - } - - pub fn into_string(self) -> String { - self.0 - } -} - -/// Typed permissions shared across helpers. `Default` produces `{}` (server-defaults). -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct Permissions { - #[serde(skip_serializing_if = "Option::is_none")] - pub exp: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub nbf: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub cmds: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub ctx: Option, -} - -impl Permissions { - pub fn with_exp(mut self, exp: i64) -> Self { - self.exp = Some(exp); - self - } - - pub fn with_nbf(mut self, nbf: i64) -> Self { - self.nbf = Some(nbf); - self - } - - pub fn with_cmds(mut self, cmds: I) -> Self - where - I: IntoIterator, - S: Into, - { - self.cmds = Some(cmds.into_iter().map(Into::into).collect()); - self - } - - pub fn with_ctx(mut self, ctx: &C) -> Result { - self.ctx = Some(serde_json::to_value(ctx)?); - Ok(self) - } - - pub fn to_json(&self) -> Result { - Ok(serde_json::to_value(self)?) - } -} - -fn decode_pem_body(pem: &str) -> Option> { - let body: String = pem - .lines() - .filter(|line| !line.starts_with("-----")) - .collect(); - STANDARD.decode(body.as_bytes()).ok() -} - -#[cfg(test)] -mod tests { - use super::*; - use rand_core::OsRng; - use ssh_key::Algorithm; - - fn gen_key() -> String { - let key = PrivateKey::random(&mut OsRng, Algorithm::Ed25519).unwrap(); - key.to_openssh(LineEnding::LF).unwrap().to_string() - } - - #[test] - fn mint_assembles_three_parts() { - let pem = gen_key(); - let perms = serde_json::json!({ "exp": 1_922_918_400i64 }); - let token = Exe0Token::mint(&perms, &pem, NAMESPACE_API).unwrap(); - let parts: Vec<&str> = token.as_str().split('.').collect(); - assert_eq!(parts.len(), 3); - assert_eq!(parts[0], "exe0"); - let decoded = URL_SAFE_NO_PAD.decode(parts[1]).unwrap(); - let roundtrip: serde_json::Value = serde_json::from_slice(&decoded).unwrap(); - assert_eq!(roundtrip, perms); - } - - #[test] - fn empty_permissions_ok() { - let pem = gen_key(); - let token = Exe0Token::mint(&serde_json::json!({}), &pem, NAMESPACE_API).unwrap(); - assert!(token.as_str().starts_with("exe0.")); - } - - #[test] - fn rejects_unknown_field() { - let pem = gen_key(); - let perms = serde_json::json!({ "evil": true }); - let err = Exe0Token::mint(&perms, &pem, NAMESPACE_API).unwrap_err(); - assert!(matches!(err, crate::Error::InvalidPermissions(_))); - } - - #[test] - fn rejects_non_integer_exp() { - let pem = gen_key(); - let perms = serde_json::json!({ "exp": 1.5 }); - let err = Exe0Token::mint(&perms, &pem, NAMESPACE_API).unwrap_err(); - assert!(matches!(err, crate::Error::InvalidPermissions(_))); - } - - #[test] - fn rejects_out_of_range_exp() { - let pem = gen_key(); - let perms = serde_json::json!({ "exp": 1i64 }); - let err = Exe0Token::mint(&perms, &pem, NAMESPACE_API).unwrap_err(); - assert!(matches!(err, crate::Error::InvalidPermissions(_))); - } - - #[test] - fn signature_verifies() { - let pem = gen_key(); - let key = PrivateKey::from_openssh(&pem).unwrap(); - let perms = serde_json::json!({ "exp": 1_922_918_400i64 }); - - let payload_bytes = serde_json::to_vec(&perms).unwrap(); - let sig = key - .sign(NAMESPACE_API, HashAlg::Sha512, &payload_bytes) - .unwrap(); - key.public_key() - .verify(NAMESPACE_API, &payload_bytes, &sig) - .unwrap(); - - let token = Exe0Token::mint(&perms, &pem, NAMESPACE_API).unwrap(); - let parts: Vec<&str> = token.as_str().split('.').collect(); - let payload = URL_SAFE_NO_PAD.decode(parts[1]).unwrap(); - assert_eq!(payload, payload_bytes); - let _sig_bytes = URL_SAFE_NO_PAD.decode(parts[2]).unwrap(); - } - - #[test] - fn namespace_vm_format() { - assert_eq!(namespace_vm("claw-ab12"), "v0@claw-ab12.exe.xyz"); - } - - #[test] - fn mint_for_vm_uses_vm_namespace() { - let pem = gen_key(); - let perms = serde_json::json!({ "exp": 1_922_918_400i64 }); - - let api_token = Exe0Token::mint_for_api(&perms, &pem).unwrap(); - let vm_token = Exe0Token::mint_for_vm("claw-abc", &perms, &pem).unwrap(); - - let api_parts: Vec<&str> = api_token.as_str().split('.').collect(); - let vm_parts: Vec<&str> = vm_token.as_str().split('.').collect(); - - // same payload, different signature (namespace differs) - assert_eq!(api_parts[1], vm_parts[1]); - assert_ne!(api_parts[2], vm_parts[2]); - } - - #[test] - fn permissions_builder() { - let p = Permissions::default() - .with_exp(1_922_918_400) - .with_cmds(["ls", "new"]); - let v = p.to_json().unwrap(); - assert_eq!(v["exp"], 1_922_918_400); - assert_eq!(v["cmds"], serde_json::json!(["ls", "new"])); - assert!(v.get("nbf").is_none()); - } -} diff --git a/crates/exedev/tests/live_smoke.rs b/crates/exedev/tests/live_smoke.rs deleted file mode 100644 index 1d7576bf39..0000000000 --- a/crates/exedev/tests/live_smoke.rs +++ /dev/null @@ -1,61 +0,0 @@ -//! Live smoke tests. Gated behind `EXEDEV_TOKEN`; if unset, tests early-return. -//! -//! ```sh -//! EXEDEV_TOKEN=exe1.XXXXX cargo test -p exedev --tests live_smoke -- --nocapture -//! ``` - -use exedev::{ExedevClient, VmStatus}; - -fn client() -> Option { - let token = std::env::var("EXEDEV_TOKEN").ok()?; - Some( - ExedevClient::builder() - .token(token) - .build() - .expect("client build"), - ) -} - -#[tokio::test] -async fn whoami_is_typed() { - let Some(client) = client() else { - eprintln!("EXEDEV_TOKEN not set; skipping"); - return; - }; - let me = client.whoami().await.expect("whoami"); - assert!(!me.email.is_empty(), "email empty: {me:?}"); - assert!( - !me.ssh_keys.is_empty(), - "expected at least one ssh key: {me:?}" - ); -} - -#[tokio::test] -async fn vm_list_is_typed() { - let Some(client) = client() else { - eprintln!("EXEDEV_TOKEN not set; skipping"); - return; - }; - let vms = client.vm_list().await.expect("vm_list"); - for vm in &vms { - assert!(!vm.name.is_empty(), "name empty: {vm:?}"); - // Ensure we don't regress to Unknown for the live statuses we expect. - if matches!(vm.status, VmStatus::Unknown) { - eprintln!("WARN: unknown status for {}: extend VmStatus", vm.name); - } - } -} - -#[tokio::test] -async fn ssh_key_list_is_typed() { - let Some(client) = client() else { - eprintln!("EXEDEV_TOKEN not set; skipping"); - return; - }; - let keys = client.ssh_key_list().await.expect("ssh_key_list"); - assert!(!keys.is_empty()); - for k in &keys { - assert!(k.public_key.starts_with("ssh-"), "bad pubkey: {k:?}"); - assert!(k.fingerprint.starts_with("SHA256:"), "bad fp: {k:?}"); - } -} diff --git a/crates/google-drive/Cargo.toml b/crates/google-drive/Cargo.toml deleted file mode 100644 index 8f2ca55edd..0000000000 --- a/crates/google-drive/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "google-drive" -version = "0.1.0" -edition = "2024" - -[dependencies] -hypr-http = { workspace = true } - -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -thiserror = { workspace = true } -urlencoding = { workspace = true } - -[dev-dependencies] -tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/crates/google-drive/src/client.rs b/crates/google-drive/src/client.rs deleted file mode 100644 index 658717ad6c..0000000000 --- a/crates/google-drive/src/client.rs +++ /dev/null @@ -1,140 +0,0 @@ -use hypr_http::HttpClient; - -use crate::error::Error; -use crate::types::{ - CreateFolderRequest, GetFileRequest, GoogleDriveFile, ListFilesRequest, ListFilesResponse, - UpdateMetadataRequest, UploadFileRequest, -}; - -const FOLDER_MIME_TYPE: &str = "application/vnd.google-apps.folder"; - -pub struct GoogleDriveClient { - http: C, -} - -impl GoogleDriveClient { - pub fn new(http: C) -> Self { - Self { http } - } - - pub async fn list_files(&self, req: ListFilesRequest) -> Result { - let mut query_parts: Vec = Vec::new(); - - if let Some(ref q) = req.q { - query_parts.push(format!("q={}", urlencoding::encode(q))); - } - if let Some(page_size) = req.page_size { - query_parts.push(format!("pageSize={page_size}")); - } - if let Some(ref page_token) = req.page_token { - query_parts.push(format!("pageToken={}", urlencoding::encode(page_token))); - } - if let Some(ref order_by) = req.order_by { - query_parts.push(format!("orderBy={}", urlencoding::encode(order_by))); - } - if let Some(ref fields) = req.fields { - query_parts.push(format!("fields={}", urlencoding::encode(fields))); - } - - let path = if query_parts.is_empty() { - "/drive/v3/files".to_string() - } else { - format!("/drive/v3/files?{}", query_parts.join("&")) - }; - - let bytes = self.http.get(&path).await.map_err(Error::Http)?; - let response: ListFilesResponse = serde_json::from_slice(&bytes)?; - Ok(response) - } - - pub async fn get_file(&self, req: GetFileRequest) -> Result { - let file_id = &req.file_id; - let mut query_parts: Vec = Vec::new(); - - if let Some(ref fields) = req.fields { - query_parts.push(format!("fields={}", urlencoding::encode(fields))); - } - - let path = if query_parts.is_empty() { - format!("/drive/v3/files/{file_id}") - } else { - format!("/drive/v3/files/{file_id}?{}", query_parts.join("&")) - }; - - let bytes = self.http.get(&path).await.map_err(Error::Http)?; - let file: GoogleDriveFile = serde_json::from_slice(&bytes)?; - Ok(file) - } - - pub async fn download_file(&self, file_id: &str) -> Result, Error> { - let path = format!("/drive/v3/files/{file_id}?alt=media"); - let bytes = self.http.get(&path).await.map_err(Error::Http)?; - Ok(bytes) - } - - pub async fn create_folder(&self, req: CreateFolderRequest) -> Result { - let metadata = GoogleDriveFile { - name: Some(req.name), - mime_type: Some(FOLDER_MIME_TYPE.to_string()), - parents: req.parent_id.map(|id| vec![id]), - ..Default::default() - }; - - let body = serde_json::to_vec(&metadata)?; - let bytes = self - .http - .post("/drive/v3/files", body, "application/json") - .await - .map_err(Error::Http)?; - let file: GoogleDriveFile = serde_json::from_slice(&bytes)?; - Ok(file) - } - - pub async fn delete_file(&self, file_id: &str) -> Result<(), Error> { - let path = format!("/drive/v3/files/{file_id}"); - self.http.delete(&path).await.map_err(Error::Http)?; - Ok(()) - } - - pub async fn upload_file(&self, req: UploadFileRequest) -> Result { - let metadata = GoogleDriveFile { - name: Some(req.name), - parents: req.parent_id.map(|id| vec![id]), - ..Default::default() - }; - let metadata_json = serde_json::to_vec(&metadata)?; - - let boundary = "drive_upload_boundary"; - let content_type = format!("multipart/related; boundary={boundary}"); - - let mut body = Vec::new(); - body.extend_from_slice(format!("--{boundary}\r\n").as_bytes()); - body.extend_from_slice(b"Content-Type: application/json; charset=UTF-8\r\n\r\n"); - body.extend_from_slice(&metadata_json); - body.extend_from_slice(format!("\r\n--{boundary}\r\n").as_bytes()); - body.extend_from_slice(format!("Content-Type: {}\r\n\r\n", req.mime_type).as_bytes()); - body.extend_from_slice(&req.data); - body.extend_from_slice(format!("\r\n--{boundary}--").as_bytes()); - - let path = "/upload/drive/v3/files?uploadType=multipart"; - let bytes = self - .http - .post(path, body, &content_type) - .await - .map_err(Error::Http)?; - let file: GoogleDriveFile = serde_json::from_slice(&bytes)?; - Ok(file) - } - - pub async fn update_metadata( - &self, - file_id: &str, - req: UpdateMetadataRequest, - ) -> Result { - let path = format!("/drive/v3/files/{file_id}"); - let body = serde_json::to_vec(&req)?; - let bytes = self.http.patch(&path, body).await.map_err(Error::Http)?; - let file: GoogleDriveFile = serde_json::from_slice(&bytes)?; - Ok(file) - } -} diff --git a/crates/google-drive/src/error.rs b/crates/google-drive/src/error.rs deleted file mode 100644 index 7ba5d35df2..0000000000 --- a/crates/google-drive/src/error.rs +++ /dev/null @@ -1,10 +0,0 @@ -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum Error { - #[error("HTTP client error: {0}")] - Http(Box), - - #[error("JSON error: {0}")] - Json(#[from] serde_json::Error), -} diff --git a/crates/google-drive/src/lib.rs b/crates/google-drive/src/lib.rs deleted file mode 100644 index ec75adaf88..0000000000 --- a/crates/google-drive/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod client; -mod error; -mod types; - -pub use client::GoogleDriveClient; -pub use error::Error; -pub use types::*; diff --git a/crates/google-drive/src/types.rs b/crates/google-drive/src/types.rs deleted file mode 100644 index d6bb9d1c30..0000000000 --- a/crates/google-drive/src/types.rs +++ /dev/null @@ -1,82 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GoogleDriveFile { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub name: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub mime_type: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub kind: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub description: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub size: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub created_time: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub modified_time: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub parents: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub web_view_link: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub web_content_link: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub trashed: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub starred: Option, -} - -pub struct ListFilesRequest { - pub q: Option, - pub page_size: Option, - pub page_token: Option, - pub order_by: Option, - pub fields: Option, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ListFilesResponse { - pub kind: String, - #[serde(default)] - pub next_page_token: Option, - #[serde(default)] - pub incomplete_search: Option, - #[serde(default)] - pub files: Vec, -} - -pub struct GetFileRequest { - pub file_id: String, - pub fields: Option, -} - -pub struct CreateFolderRequest { - pub name: String, - pub parent_id: Option, -} - -pub struct UploadFileRequest { - pub name: String, - pub parent_id: Option, - pub mime_type: String, - pub data: Vec, -} - -#[derive(Debug, Default, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct UpdateMetadataRequest { - #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub starred: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub trashed: Option, -} diff --git a/crates/google-mail/Cargo.toml b/crates/google-mail/Cargo.toml deleted file mode 100644 index ad4d76422c..0000000000 --- a/crates/google-mail/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "google-mail" -version = "0.1.0" -edition = "2024" - -[features] -default = [] -specta = ["dep:specta"] -utoipa = ["dep:utoipa"] - -[dependencies] -hypr-http = { workspace = true } - -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -specta = { workspace = true, optional = true } -thiserror = { workspace = true } -urlencoding = { workspace = true } -utoipa = { workspace = true, optional = true } - -[dev-dependencies] -tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/crates/google-mail/src/client.rs b/crates/google-mail/src/client.rs deleted file mode 100644 index 4c88ee78ce..0000000000 --- a/crates/google-mail/src/client.rs +++ /dev/null @@ -1,216 +0,0 @@ -use hypr_http::HttpClient; - -use crate::error::Error; -use crate::types::{ - Attachment, GetMessageRequest, GetThreadRequest, HistoryType, ListHistoryRequest, - ListHistoryResponse, ListLabelsResponse, ListMessagesRequest, ListMessagesResponse, - ListThreadsRequest, ListThreadsResponse, Message, MessageFormat, Profile, Thread, -}; - -pub struct GoogleMailClient { - http: C, -} - -impl GoogleMailClient { - pub fn new(http: C) -> Self { - Self { http } - } - - pub async fn list_labels(&self) -> Result { - let bytes = self - .http - .get("/gmail/v1/users/me/labels") - .await - .map_err(Error::Http)?; - let response: ListLabelsResponse = serde_json::from_slice(&bytes)?; - Ok(response) - } - - pub async fn list_messages( - &self, - req: ListMessagesRequest, - ) -> Result { - let mut query_parts: Vec = Vec::new(); - - if let Some(ref q) = req.q { - query_parts.push(format!("q={}", urlencoding::encode(q))); - } - if let Some(max_results) = req.max_results { - query_parts.push(format!("maxResults={max_results}")); - } - if let Some(ref page_token) = req.page_token { - query_parts.push(format!("pageToken={}", urlencoding::encode(page_token))); - } - if let Some(ref label_ids) = req.label_ids { - for id in label_ids { - query_parts.push(format!("labelIds={}", urlencoding::encode(id))); - } - } - if let Some(include_spam_trash) = req.include_spam_trash { - query_parts.push(format!("includeSpamTrash={include_spam_trash}")); - } - - let path = if query_parts.is_empty() { - "/gmail/v1/users/me/messages".to_string() - } else { - format!("/gmail/v1/users/me/messages?{}", query_parts.join("&")) - }; - - let bytes = self.http.get(&path).await.map_err(Error::Http)?; - let response: ListMessagesResponse = serde_json::from_slice(&bytes)?; - Ok(response) - } - - pub async fn get_message(&self, req: GetMessageRequest) -> Result { - let id = &req.id; - let mut query_parts: Vec = Vec::new(); - - if let Some(ref format) = req.format { - let value = match format { - MessageFormat::Full => "full", - MessageFormat::Metadata => "metadata", - MessageFormat::Minimal => "minimal", - MessageFormat::Raw => "raw", - }; - query_parts.push(format!("format={value}")); - } - if let Some(ref headers) = req.metadata_headers { - for h in headers { - query_parts.push(format!("metadataHeaders={}", urlencoding::encode(h))); - } - } - - let path = if query_parts.is_empty() { - format!("/gmail/v1/users/me/messages/{id}") - } else { - format!("/gmail/v1/users/me/messages/{id}?{}", query_parts.join("&")) - }; - - let bytes = self.http.get(&path).await.map_err(Error::Http)?; - let message: Message = serde_json::from_slice(&bytes)?; - Ok(message) - } - - pub async fn get_attachment( - &self, - message_id: &str, - attachment_id: &str, - ) -> Result { - let path = format!("/gmail/v1/users/me/messages/{message_id}/attachments/{attachment_id}"); - let bytes = self.http.get(&path).await.map_err(Error::Http)?; - let attachment: Attachment = serde_json::from_slice(&bytes)?; - Ok(attachment) - } - - pub async fn get_profile(&self) -> Result { - let bytes = self - .http - .get("/gmail/v1/users/me/profile") - .await - .map_err(Error::Http)?; - let profile: Profile = serde_json::from_slice(&bytes)?; - Ok(profile) - } - - pub async fn list_threads( - &self, - req: ListThreadsRequest, - ) -> Result { - let mut query_parts: Vec = Vec::new(); - - if let Some(ref q) = req.q { - query_parts.push(format!("q={}", urlencoding::encode(q))); - } - if let Some(max_results) = req.max_results { - query_parts.push(format!("maxResults={max_results}")); - } - if let Some(ref page_token) = req.page_token { - query_parts.push(format!("pageToken={}", urlencoding::encode(page_token))); - } - if let Some(ref label_ids) = req.label_ids { - for id in label_ids { - query_parts.push(format!("labelIds={}", urlencoding::encode(id))); - } - } - if let Some(include_spam_trash) = req.include_spam_trash { - query_parts.push(format!("includeSpamTrash={include_spam_trash}")); - } - - let path = if query_parts.is_empty() { - "/gmail/v1/users/me/threads".to_string() - } else { - format!("/gmail/v1/users/me/threads?{}", query_parts.join("&")) - }; - - let bytes = self.http.get(&path).await.map_err(Error::Http)?; - let response: ListThreadsResponse = serde_json::from_slice(&bytes)?; - Ok(response) - } - - pub async fn get_thread(&self, req: GetThreadRequest) -> Result { - let id = &req.id; - let mut query_parts: Vec = Vec::new(); - - if let Some(ref format) = req.format { - let value = match format { - MessageFormat::Full => "full", - MessageFormat::Metadata => "metadata", - MessageFormat::Minimal => "minimal", - MessageFormat::Raw => "raw", - }; - query_parts.push(format!("format={value}")); - } - if let Some(ref headers) = req.metadata_headers { - for h in headers { - query_parts.push(format!("metadataHeaders={}", urlencoding::encode(h))); - } - } - - let path = if query_parts.is_empty() { - format!("/gmail/v1/users/me/threads/{id}") - } else { - format!("/gmail/v1/users/me/threads/{id}?{}", query_parts.join("&")) - }; - - let bytes = self.http.get(&path).await.map_err(Error::Http)?; - let thread: Thread = serde_json::from_slice(&bytes)?; - Ok(thread) - } - - pub async fn list_history( - &self, - req: ListHistoryRequest, - ) -> Result { - let mut query_parts: Vec = vec![format!( - "startHistoryId={}", - urlencoding::encode(&req.start_history_id) - )]; - - if let Some(max_results) = req.max_results { - query_parts.push(format!("maxResults={max_results}")); - } - if let Some(ref page_token) = req.page_token { - query_parts.push(format!("pageToken={}", urlencoding::encode(page_token))); - } - if let Some(ref label_id) = req.label_id { - query_parts.push(format!("labelId={}", urlencoding::encode(label_id))); - } - if let Some(ref history_types) = req.history_types { - for ht in history_types { - let value = match ht { - HistoryType::MessageAdded => "messageAdded", - HistoryType::MessageDeleted => "messageDeleted", - HistoryType::LabelAdded => "labelAdded", - HistoryType::LabelRemoved => "labelRemoved", - }; - query_parts.push(format!("historyTypes={value}")); - } - } - - let path = format!("/gmail/v1/users/me/history?{}", query_parts.join("&")); - - let bytes = self.http.get(&path).await.map_err(Error::Http)?; - let response: ListHistoryResponse = serde_json::from_slice(&bytes)?; - Ok(response) - } -} diff --git a/crates/google-mail/src/error.rs b/crates/google-mail/src/error.rs deleted file mode 100644 index 7ba5d35df2..0000000000 --- a/crates/google-mail/src/error.rs +++ /dev/null @@ -1,10 +0,0 @@ -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum Error { - #[error("HTTP client error: {0}")] - Http(Box), - - #[error("JSON error: {0}")] - Json(#[from] serde_json::Error), -} diff --git a/crates/google-mail/src/lib.rs b/crates/google-mail/src/lib.rs deleted file mode 100644 index 32a03103fc..0000000000 --- a/crates/google-mail/src/lib.rs +++ /dev/null @@ -1,10 +0,0 @@ -mod client; -mod error; -mod types; - -#[cfg(feature = "utoipa")] -pub mod openapi; - -pub use client::GoogleMailClient; -pub use error::Error; -pub use types::*; diff --git a/crates/google-mail/src/openapi.rs b/crates/google-mail/src/openapi.rs deleted file mode 100644 index 53756c09ac..0000000000 --- a/crates/google-mail/src/openapi.rs +++ /dev/null @@ -1,40 +0,0 @@ -use utoipa::OpenApi; - -#[derive(OpenApi)] -#[openapi(components(schemas( - crate::Attachment, - crate::GetMessageRequest, - crate::GetThreadRequest, - crate::History, - crate::HistoryLabelAdded, - crate::HistoryLabelRemoved, - crate::HistoryMessageAdded, - crate::HistoryMessageDeleted, - crate::HistoryType, - crate::Label, - crate::LabelColor, - crate::LabelListVisibility, - crate::LabelType, - crate::ListHistoryRequest, - crate::ListHistoryResponse, - crate::ListLabelsResponse, - crate::ListMessagesRequest, - crate::ListMessagesResponse, - crate::ListThreadsRequest, - crate::ListThreadsResponse, - crate::Message, - crate::MessageFormat, - crate::MessageListVisibility, - crate::MessagePart, - crate::MessagePartBody, - crate::MessagePartHeader, - crate::MessageRef, - crate::Profile, - crate::Thread, - crate::ThreadRef, -)))] -struct ApiDoc; - -pub fn openapi() -> utoipa::openapi::OpenApi { - ApiDoc::openapi() -} diff --git a/crates/google-mail/src/types.rs b/crates/google-mail/src/types.rs deleted file mode 100644 index 295b8bd985..0000000000 --- a/crates/google-mail/src/types.rs +++ /dev/null @@ -1,405 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[cfg(feature = "specta")] -use specta::Type; - -#[cfg(feature = "utoipa")] -use utoipa::ToSchema; - -// === Enums === - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[cfg_attr(feature = "specta", derive(Type))] -#[cfg_attr(feature = "utoipa", derive(ToSchema))] -#[serde(rename_all = "camelCase")] -pub enum MessageFormat { - #[default] - Full, - Metadata, - Minimal, - Raw, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(feature = "specta", derive(Type))] -#[cfg_attr(feature = "utoipa", derive(ToSchema))] -#[serde(rename_all = "camelCase")] -pub enum LabelType { - System, - User, - #[serde(other)] - Unknown, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(feature = "specta", derive(Type))] -#[cfg_attr(feature = "utoipa", derive(ToSchema))] -#[serde(rename_all = "camelCase")] -pub enum LabelListVisibility { - LabelShow, - LabelShowIfUnread, - LabelHide, - #[serde(other)] - Unknown, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(feature = "specta", derive(Type))] -#[cfg_attr(feature = "utoipa", derive(ToSchema))] -#[serde(rename_all = "camelCase")] -pub enum MessageListVisibility { - Show, - Hide, - #[serde(other)] - Unknown, -} - -// === Request types === - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[cfg_attr(feature = "specta", derive(Type))] -#[cfg_attr(feature = "utoipa", derive(ToSchema))] -pub struct ListMessagesRequest { - #[serde(default)] - pub q: Option, - #[serde(default)] - pub max_results: Option, - #[serde(default)] - pub page_token: Option, - #[serde(default)] - pub label_ids: Option>, - #[serde(default)] - pub include_spam_trash: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(feature = "specta", derive(Type))] -#[cfg_attr(feature = "utoipa", derive(ToSchema))] -pub struct GetMessageRequest { - pub id: String, - #[serde(default)] - pub format: Option, - #[serde(default)] - pub metadata_headers: Option>, -} - -// === Response types === - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(feature = "specta", derive(Type))] -#[cfg_attr(feature = "utoipa", derive(ToSchema))] -#[serde(rename_all = "camelCase")] -pub struct ListLabelsResponse { - #[serde(default)] - pub labels: Vec
- - - - - - - - - - - - - - - -
IdentifierDescription
- -`webhook:allow-todo` - - - -Enables the todo command without any pre-configured scope. - -
- -`webhook:deny-todo` - - - -Denies the todo command without any pre-configured scope. - -
diff --git a/plugins/webhook/permissions/default.toml b/plugins/webhook/permissions/default.toml deleted file mode 100644 index 8ea0a5f96e..0000000000 --- a/plugins/webhook/permissions/default.toml +++ /dev/null @@ -1,3 +0,0 @@ -[default] -description = "Default permissions for the plugin" -permissions = [] diff --git a/plugins/webhook/permissions/schemas/schema.json b/plugins/webhook/permissions/schemas/schema.json deleted file mode 100644 index dda73b0416..0000000000 --- a/plugins/webhook/permissions/schemas/schema.json +++ /dev/null @@ -1,318 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PermissionFile", - "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", - "type": "object", - "properties": { - "default": { - "description": "The default permission set for the plugin", - "anyOf": [ - { - "$ref": "#/definitions/DefaultPermission" - }, - { - "type": "null" - } - ] - }, - "set": { - "description": "A list of permissions sets defined", - "type": "array", - "items": { - "$ref": "#/definitions/PermissionSet" - } - }, - "permission": { - "description": "A list of inlined permissions", - "default": [], - "type": "array", - "items": { - "$ref": "#/definitions/Permission" - } - } - }, - "definitions": { - "DefaultPermission": { - "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", - "type": "object", - "required": [ - "permissions" - ], - "properties": { - "version": { - "description": "The version of the permission.", - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 1.0 - }, - "description": { - "description": "Human-readable description of what the permission does. Tauri convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", - "type": [ - "string", - "null" - ] - }, - "permissions": { - "description": "All permissions this set contains.", - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "PermissionSet": { - "description": "A set of direct permissions grouped together under a new name.", - "type": "object", - "required": [ - "description", - "identifier", - "permissions" - ], - "properties": { - "identifier": { - "description": "A unique identifier for the permission.", - "type": "string" - }, - "description": { - "description": "Human-readable description of what the permission does.", - "type": "string" - }, - "permissions": { - "description": "All permissions this set contains.", - "type": "array", - "items": { - "$ref": "#/definitions/PermissionKind" - } - } - } - }, - "Permission": { - "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", - "type": "object", - "required": [ - "identifier" - ], - "properties": { - "version": { - "description": "The version of the permission.", - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 1.0 - }, - "identifier": { - "description": "A unique identifier for the permission.", - "type": "string" - }, - "description": { - "description": "Human-readable description of what the permission does. Tauri internal convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", - "type": [ - "string", - "null" - ] - }, - "commands": { - "description": "Allowed or denied commands when using this permission.", - "default": { - "allow": [], - "deny": [] - }, - "allOf": [ - { - "$ref": "#/definitions/Commands" - } - ] - }, - "scope": { - "description": "Allowed or denied scoped when using this permission.", - "allOf": [ - { - "$ref": "#/definitions/Scopes" - } - ] - }, - "platforms": { - "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Target" - } - } - } - }, - "Commands": { - "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", - "type": "object", - "properties": { - "allow": { - "description": "Allowed command.", - "default": [], - "type": "array", - "items": { - "type": "string" - } - }, - "deny": { - "description": "Denied command, which takes priority.", - "default": [], - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "Scopes": { - "description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```", - "type": "object", - "properties": { - "allow": { - "description": "Data that defines what is allowed by the scope.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Value" - } - }, - "deny": { - "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Value" - } - } - } - }, - "Value": { - "description": "All supported ACL values.", - "anyOf": [ - { - "description": "Represents a null JSON value.", - "type": "null" - }, - { - "description": "Represents a [`bool`].", - "type": "boolean" - }, - { - "description": "Represents a valid ACL [`Number`].", - "allOf": [ - { - "$ref": "#/definitions/Number" - } - ] - }, - { - "description": "Represents a [`String`].", - "type": "string" - }, - { - "description": "Represents a list of other [`Value`]s.", - "type": "array", - "items": { - "$ref": "#/definitions/Value" - } - }, - { - "description": "Represents a map of [`String`] keys to [`Value`]s.", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Value" - } - } - ] - }, - "Number": { - "description": "A valid ACL number.", - "anyOf": [ - { - "description": "Represents an [`i64`].", - "type": "integer", - "format": "int64" - }, - { - "description": "Represents a [`f64`].", - "type": "number", - "format": "double" - } - ] - }, - "Target": { - "description": "Platform target.", - "oneOf": [ - { - "description": "MacOS.", - "type": "string", - "enum": [ - "macOS" - ] - }, - { - "description": "Windows.", - "type": "string", - "enum": [ - "windows" - ] - }, - { - "description": "Linux.", - "type": "string", - "enum": [ - "linux" - ] - }, - { - "description": "Android.", - "type": "string", - "enum": [ - "android" - ] - }, - { - "description": "iOS.", - "type": "string", - "enum": [ - "iOS" - ] - } - ] - }, - "PermissionKind": { - "type": "string", - "oneOf": [ - { - "description": "Enables the todo command without any pre-configured scope.", - "type": "string", - "const": "allow-todo", - "markdownDescription": "Enables the todo command without any pre-configured scope." - }, - { - "description": "Denies the todo command without any pre-configured scope.", - "type": "string", - "const": "deny-todo", - "markdownDescription": "Denies the todo command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin", - "type": "string", - "const": "default", - "markdownDescription": "Default permissions for the plugin" - } - ] - } - } -} \ No newline at end of file diff --git a/plugins/webhook/src/commands.rs b/plugins/webhook/src/commands.rs deleted file mode 100644 index 3235889d7a..0000000000 --- a/plugins/webhook/src/commands.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::WebhookPluginExt; - -#[tauri::command] -#[specta::specta] -pub async fn todo(app: tauri::AppHandle) -> Result { - app.webhook().todo() -} diff --git a/plugins/webhook/src/error.rs b/plugins/webhook/src/error.rs deleted file mode 100644 index d1bb7a2891..0000000000 --- a/plugins/webhook/src/error.rs +++ /dev/null @@ -1,15 +0,0 @@ -use serde::{Serialize, ser::Serializer}; - -pub type Result = std::result::Result; - -#[derive(Debug, thiserror::Error)] -pub enum Error {} - -impl Serialize for Error { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: Serializer, - { - serializer.serialize_str(self.to_string().as_ref()) - } -} diff --git a/plugins/webhook/src/ext.rs b/plugins/webhook/src/ext.rs deleted file mode 100644 index 4ea406d188..0000000000 --- a/plugins/webhook/src/ext.rs +++ /dev/null @@ -1,29 +0,0 @@ -pub struct Webhook<'a, R: tauri::Runtime, M: tauri::Manager> { - manager: &'a M, - _runtime: std::marker::PhantomData R>, -} - -impl<'a, R: tauri::Runtime, M: tauri::Manager> Webhook<'a, R, M> { - pub fn todo(&self) -> Result { - let _ = self.manager; - Ok("Webhook todo functionality not yet implemented".to_string()) - } -} - -pub trait WebhookPluginExt { - fn webhook(&self) -> Webhook<'_, R, Self> - where - Self: tauri::Manager + Sized; -} - -impl> WebhookPluginExt for T { - fn webhook(&self) -> Webhook<'_, R, Self> - where - Self: Sized, - { - Webhook { - manager: self, - _runtime: std::marker::PhantomData, - } - } -} diff --git a/plugins/webhook/src/lib.rs b/plugins/webhook/src/lib.rs deleted file mode 100644 index 262240af40..0000000000 --- a/plugins/webhook/src/lib.rs +++ /dev/null @@ -1,71 +0,0 @@ -mod commands; -mod error; -mod ext; -mod openapi; - -pub use error::*; -pub use ext::*; -pub use openapi::*; - -const PLUGIN_NAME: &str = "webhook"; - -use tauri::Manager; - -#[derive(Default)] -pub struct State {} - -fn make_specta_builder() -> tauri_specta::Builder { - tauri_specta::Builder::::new() - .plugin_name(PLUGIN_NAME) - .events(tauri_specta::collect_events![]) - .commands(tauri_specta::collect_commands![ - commands::todo::, - ]) - .error_handling(tauri_specta::ErrorHandlingMode::Result) -} - -pub fn init() -> tauri::plugin::TauriPlugin { - let specta_builder = make_specta_builder(); - - tauri::plugin::Builder::new(PLUGIN_NAME) - .invoke_handler(specta_builder.invoke_handler()) - .setup(move |app, _api| { - specta_builder.mount_events(app); - - { - app.manage(State::default()); - } - - Ok(()) - }) - .build() -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn export_types() { - const OUTPUT_FILE: &str = "./js/bindings.gen.ts"; - - make_specta_builder() - .export( - specta_typescript::Typescript::default() - .formatter(specta_typescript::formatter::prettier) - .bigint(specta_typescript::BigIntExportBehavior::Number), - OUTPUT_FILE, - ) - .unwrap(); - - let content = std::fs::read_to_string(OUTPUT_FILE).unwrap(); - std::fs::write(OUTPUT_FILE, format!("// @ts-nocheck\n{content}")).unwrap(); - } - - #[test] - fn export_openapi() { - let openapi_json = generate_openapi_json(); - let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("openapi.gen.json"); - std::fs::write(&path, openapi_json).unwrap(); - } -} diff --git a/plugins/webhook/src/openapi.rs b/plugins/webhook/src/openapi.rs deleted file mode 100644 index faeddf9139..0000000000 --- a/plugins/webhook/src/openapi.rs +++ /dev/null @@ -1,268 +0,0 @@ -use serde::{Deserialize, Serialize}; -use utoipa::{ - Modify, OpenApi, ToSchema, - openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}, -}; - -// Core webhook event structure -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -pub struct WebhookEvent { - /// Unique event identifier - #[schema(example = "evt_01234567890")] - pub id: String, - - /// Event type - #[schema(example = "note.created")] - pub event_type: String, - - /// ISO 8601 timestamp - #[schema(example = "2024-01-10T10:30:00Z")] - pub timestamp: String, - - /// Event payload - pub data: serde_json::Value, -} - -// Simplified event payloads -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -pub struct NoteEvent { - #[schema(example = "note_abc123")] - pub note_id: String, - - #[schema(example = "Meeting Notes")] - pub title: String, - - #[schema(example = "Discussion points...")] - pub content: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -pub struct RecordingEvent { - #[schema(example = "rec_xyz789")] - pub recording_id: String, - - #[schema(example = 300)] - pub duration_seconds: u32, - - #[schema(example = "completed")] - pub status: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -pub struct TranscriptionEvent { - #[schema(example = "rec_xyz789")] - pub recording_id: String, - - #[schema(example = "trans_qwe456")] - pub transcription_id: String, - - #[schema(example = "Hello, this is the transcribed text.")] - pub text: String, -} - -// Webhook configuration -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -pub struct WebhookConfig { - /// Your webhook endpoint URL - #[schema(example = "https://your-app.com/webhooks")] - pub url: String, - - /// Events to subscribe to - #[schema(example = json!(["note.created", "note.updated", "recording.completed"]))] - pub events: Vec, - - /// Whether the webhook is active - #[schema(example = true)] - pub active: bool, -} - -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -pub struct CreateWebhookRequest { - pub config: WebhookConfig, -} - -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -pub struct WebhookResponse { - #[schema(example = "webhook_123")] - pub id: String, - - pub config: WebhookConfig, - - /// Secret for verifying webhook signatures - #[schema(example = "whsec_1234567890abcdef")] - pub secret: String, - - #[schema(example = "2024-01-10T10:00:00Z")] - pub created_at: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -pub struct WebhookListResponse { - pub webhooks: Vec, - pub total: usize, -} - -// Webhook verification example -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -pub struct WebhookVerification { - /// HMAC-SHA256 signature - #[schema(example = "sha256=abcdef1234567890")] - pub signature: String, - - /// Unix timestamp - #[schema(example = "1704880200")] - pub timestamp: String, -} - -#[derive(OpenApi)] -#[openapi( - info( - title = "Webhook API Documentation", - version = "1.0.0", - description = "Webhook system for receiving real-time events", - contact( - name = "API Support", - email = "api@example.com" - ) - ), - paths( - create_webhook, - list_webhooks, - delete_webhook, - test_webhook, - webhook_receiver_example - ), - components( - schemas( - WebhookEvent, - NoteEvent, - RecordingEvent, - TranscriptionEvent, - WebhookConfig, - CreateWebhookRequest, - WebhookResponse, - WebhookListResponse, - WebhookVerification - ) - ), - modifiers(&SecurityAddon), - tags( - (name = "Webhooks", description = "Webhook management endpoints"), - (name = "Events", description = "Event types and payloads") - ) -)] -pub struct ApiDoc; - -#[utoipa::path( - post, - path = "/api/webhooks", - tag = "Webhooks", - request_body = CreateWebhookRequest, - responses( - (status = 201, description = "Webhook created", body = WebhookResponse), - (status = 400, description = "Invalid configuration") - ), - security( - ("api_key" = []) - ) -)] -#[allow(dead_code)] -async fn create_webhook() -> WebhookResponse { - unimplemented!() -} - -#[utoipa::path( - get, - path = "/api/webhooks", - tag = "Webhooks", - responses( - (status = 200, description = "List of webhooks", body = WebhookListResponse) - ), - security( - ("api_key" = []) - ) -)] -#[allow(dead_code)] -async fn list_webhooks() -> WebhookListResponse { - unimplemented!() -} - -#[utoipa::path( - delete, - path = "/api/webhooks/{id}", - tag = "Webhooks", - params( - ("id" = String, Path, description = "Webhook ID") - ), - responses( - (status = 204, description = "Webhook deleted"), - (status = 404, description = "Webhook not found") - ), - security( - ("api_key" = []) - ) -)] -#[allow(dead_code)] -async fn delete_webhook() { - unimplemented!() -} - -#[utoipa::path( - post, - path = "/api/webhooks/{id}/test", - tag = "Webhooks", - params( - ("id" = String, Path, description = "Webhook ID") - ), - responses( - (status = 200, description = "Test event sent"), - (status = 404, description = "Webhook not found") - ), - security( - ("api_key" = []) - ) -)] -#[allow(dead_code)] -async fn test_webhook() { - unimplemented!() -} - -#[utoipa::path( - post, - path = "/your-webhook-endpoint", - tag = "Events", - request_body = WebhookEvent, - responses( - (status = 200, description = "Event processed successfully"), - (status = 401, description = "Invalid signature"), - (status = 500, description = "Processing error") - ), - security( - ("webhook_signature" = []) - ) -)] -#[allow(dead_code)] -async fn webhook_receiver_example() { - unimplemented!() -} - -struct SecurityAddon; - -impl Modify for SecurityAddon { - fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { - if let Some(components) = openapi.components.as_mut() { - components.add_security_scheme( - "api_key", - SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("X-API-Key"))), - ); - components.add_security_scheme( - "webhook_signature", - SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("X-Webhook-Signature"))), - ); - } - } -} - -pub fn generate_openapi_json() -> String { - ApiDoc::openapi().to_pretty_json().unwrap() -} diff --git a/plugins/webhook/tsconfig.json b/plugins/webhook/tsconfig.json deleted file mode 100644 index 13b985325d..0000000000 --- a/plugins/webhook/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "../tsconfig.base.json", - "include": ["./js/*.ts"], - "exclude": ["node_modules"] -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 245132836b..75daf67147 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -70,46 +70,6 @@ importers: specifier: ^8.58.1 version: 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) - apps/chrome: - dependencies: - react: - specifier: ^19.2.5 - version: 19.2.5 - react-dom: - specifier: ^19.2.5 - version: 19.2.5(react@19.2.5) - devDependencies: - '@playwright/test': - specifier: ^1.59.1 - version: 1.59.1 - '@tailwindcss/vite': - specifier: ^4.2.2 - version: 4.2.2(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - '@types/react': - specifier: ^19.2.14 - version: 19.2.14 - '@types/react-dom': - specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.14) - '@wxt-dev/module-react': - specifier: ^1.2.2 - version: 1.2.2(babel-plugin-react-compiler@1.0.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(wxt@0.20.20(@types/node@25.6.0)(eslint@9.39.4(jiti@2.6.1))(jiti@2.6.1)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - happy-dom: - specifier: ^20.8.9 - version: 20.8.9 - tailwindcss: - specifier: ^4.2.2 - version: 4.2.2 - typescript: - specifier: ^5.9.3 - version: 5.9.3 - vitest: - specifier: ^4.1.4 - version: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(happy-dom@20.8.9)(jsdom@27.4.0(@noble/hashes@1.8.0))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - wxt: - specifier: ^0.20.20 - version: 0.20.20(@types/node@25.6.0)(eslint@9.39.4(jiti@2.6.1))(jiti@2.6.1)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - apps/desktop: dependencies: '@ai-sdk/amazon-bedrock': @@ -651,126 +611,6 @@ importers: specifier: ^4.1.4 version: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(happy-dom@20.8.9)(jsdom@27.4.0(@noble/hashes@1.8.0))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - apps/slack-internal: - dependencies: - '@hypr/agent-designer': - specifier: workspace:^ - version: link:../../packages/agent-designer - '@hypr/agent-support': - specifier: workspace:^ - version: link:../../packages/agent-support - '@langchain/core': - specifier: ^1.1.39 - version: 1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0) - '@langchain/langgraph': - specifier: ^1.2.8 - version: 1.2.8(@langchain/core@1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6) - '@sentry/bun': - specifier: ^10.48.0 - version: 10.48.0(@opentelemetry/exporter-trace-otlp-http@0.203.0(@opentelemetry/api@1.9.1)) - '@slack/bolt': - specifier: ^4.7.0 - version: 4.7.0(@types/express@5.0.6) - '@slack/types': - specifier: ^2.20.1 - version: 2.20.1 - '@slack/web-api': - specifier: ^7.15.0 - version: 7.15.0 - '@t3-oss/env-core': - specifier: ^0.13.11 - version: 0.13.11(typescript@5.9.3)(zod@4.3.6) - jsx-slack: - specifier: ^6.1.2 - version: 6.1.2 - marked: - specifier: ^17.0.6 - version: 17.0.6 - zod: - specifier: ^4.3.6 - version: 4.3.6 - devDependencies: - '@types/bun': - specifier: ^1.3.12 - version: 1.3.12 - '@types/marked': - specifier: ^6.0.0 - version: 6.0.0 - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - apps/stripe: - dependencies: - '@hypr/supabase': - specifier: workspace:* - version: link:../../packages/supabase - '@sentry/bun': - specifier: ^10.48.0 - version: 10.48.0(@opentelemetry/exporter-trace-otlp-http@0.203.0(@opentelemetry/api@1.9.1)) - '@supabase/stripe-sync-engine': - specifier: ^0.45.0 - version: 0.45.0(stripe@20.4.1(@types/node@25.6.0)) - '@supabase/supabase-js': - specifier: ^2.103.0 - version: 2.103.0 - '@t3-oss/env-core': - specifier: ^0.13.11 - version: 0.13.11(typescript@5.9.3)(zod@4.3.6) - effect: - specifier: ^3.21.0 - version: 3.21.0 - hono: - specifier: ^4.12.12 - version: 4.12.12 - pg: - specifier: ^8.20.0 - version: 8.20.0 - stripe: - specifier: ^20.4.1 - version: 20.4.1(@types/node@25.6.0) - zod: - specifier: ^4.3.6 - version: 4.3.6 - devDependencies: - '@dotenvx/dotenvx': - specifier: ^1.61.0 - version: 1.61.0 - '@types/bun': - specifier: ^1.3.12 - version: 1.3.12 - '@types/pg': - specifier: ^8.20.0 - version: 8.20.0 - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - apps/unsigned-web: - dependencies: - '@astrojs/mdx': - specifier: ^5.0.2 - version: 5.0.3(astro@6.1.5(@netlify/blobs@10.7.4)(@types/node@25.6.0)(aws4fetch@1.0.20)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.1)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.6.3)(yaml@2.8.3)) - astro: - specifier: ^6.0.8 - version: 6.1.5(@netlify/blobs@10.7.4)(@types/node@25.6.0)(aws4fetch@1.0.20)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.1)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.6.3)(yaml@2.8.3) - astro-seo: - specifier: ^1.1.0 - version: 1.1.0(prettier@3.8.2)(typescript@5.6.3) - devDependencies: - '@astrojs/check': - specifier: ^0.9.8 - version: 0.9.8(prettier@3.8.2)(typescript@5.6.3) - '@tailwindcss/vite': - specifier: ^4.1.0 - version: 4.2.2(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - tailwindcss: - specifier: ^4.1.0 - version: 4.2.2 - typescript: - specifier: ~5.6.3 - version: 5.6.3 - apps/web: dependencies: '@chenglou/pretext': @@ -1078,195 +918,6 @@ importers: specifier: ^5.2.0 version: 5.2.0 - e2e/blackbox: - devDependencies: - '@crabnebula/tauri-driver': - specifier: ^2.0.9 - version: 2.0.9 - '@crabnebula/test-runner-backend': - specifier: ^0.1.18 - version: 0.1.18 - '@wdio/cli': - specifier: ^9.27.0 - version: 9.27.0(@types/node@25.6.0)(expect-webdriverio@5.6.5)(puppeteer-core@22.15.0) - '@wdio/globals': - specifier: ^9.27.0 - version: 9.27.0(expect-webdriverio@5.6.5)(webdriverio@9.27.0(puppeteer-core@22.15.0)) - '@wdio/local-runner': - specifier: ^9.27.0 - version: 9.27.0(@wdio/globals@9.27.0)(webdriverio@9.27.0(puppeteer-core@22.15.0)) - '@wdio/mocha-framework': - specifier: ^9.27.0 - version: 9.27.0 - '@wdio/spec-reporter': - specifier: ^9.27.0 - version: 9.27.0 - '@wdio/types': - specifier: ^9.27.0 - version: 9.27.0 - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - examples/plugins/hello-world: - devDependencies: - '@hypr/plugin-sdk': - specifier: workspace:* - version: link:../../../packages/plugin-sdk - '@types/react': - specifier: ^19.2.14 - version: 19.2.14 - esbuild: - specifier: ^0.27.7 - version: 0.27.7 - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - packages/agent-core: - dependencies: - '@langchain/core': - specifier: ^1.1.39 - version: 1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0) - '@langchain/langgraph': - specifier: ^1.2.8 - version: 1.2.8(@langchain/core@1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6) - '@langchain/langgraph-checkpoint-postgres': - specifier: ^1.0.1 - version: 1.0.1(@langchain/core@1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(@langchain/langgraph-checkpoint@1.0.1(@langchain/core@1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))) - '@langchain/openai': - specifier: ^1.4.4 - version: 1.4.4(@langchain/core@1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(ws@8.20.0) - '@t3-oss/env-core': - specifier: ^0.13.11 - version: 0.13.11(typescript@5.9.3)(zod@4.3.6) - modal: - specifier: ^0.6.3 - version: 0.6.3 - pg: - specifier: ^8.20.0 - version: 8.20.0 - promptl-ai: - specifier: ^0.4.12 - version: 0.4.12 - zod: - specifier: ^4.3.6 - version: 4.3.6 - devDependencies: - '@types/bun': - specifier: ^1.3.12 - version: 1.3.12 - '@types/pg': - specifier: ^8.20.0 - version: 8.20.0 - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - packages/agent-designer: - dependencies: - '@hypr/agent-core': - specifier: workspace:^ - version: link:../agent-core - '@langchain/core': - specifier: ^1.1.39 - version: 1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0) - '@langchain/langgraph': - specifier: ^1.2.8 - version: 1.2.8(@langchain/core@1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6) - '@langchain/langgraph-checkpoint-postgres': - specifier: ^1.0.1 - version: 1.0.1(@langchain/core@1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(@langchain/langgraph-checkpoint@1.0.1(@langchain/core@1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))) - '@langchain/openai': - specifier: ^1.4.4 - version: 1.4.4(@langchain/core@1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(ws@8.20.0) - '@t3-oss/env-core': - specifier: ^0.13.11 - version: 0.13.11(typescript@5.9.3)(zod@4.3.6) - langsmith: - specifier: ^0.4.12 - version: 0.4.12(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6)) - pg: - specifier: ^8.20.0 - version: 8.20.0 - promptl-ai: - specifier: ^0.4.12 - version: 0.4.12 - zod: - specifier: ^4.3.6 - version: 4.3.6 - devDependencies: - '@langchain/langgraph-cli': - specifier: ^1.1.17 - version: 1.1.17(367962cb075712636ea2108ac4ab276a) - '@types/bun': - specifier: ^1.3.12 - version: 1.3.12 - '@types/pg': - specifier: ^8.20.0 - version: 8.20.0 - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - packages/agent-support: - dependencies: - '@hypr/agent-core': - specifier: workspace:^ - version: link:../agent-core - '@langchain/core': - specifier: ^1.1.39 - version: 1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0) - '@langchain/langgraph': - specifier: ^1.2.8 - version: 1.2.8(@langchain/core@1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6) - '@langchain/langgraph-checkpoint-postgres': - specifier: ^1.0.1 - version: 1.0.1(@langchain/core@1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(@langchain/langgraph-checkpoint@1.0.1(@langchain/core@1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))) - '@langchain/openai': - specifier: ^1.4.4 - version: 1.4.4(@langchain/core@1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(ws@8.20.0) - '@t3-oss/env-core': - specifier: ^0.13.11 - version: 0.13.11(typescript@5.9.3)(zod@4.3.6) - langsmith: - specifier: ^0.4.12 - version: 0.4.12(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6)) - loops: - specifier: ^6.3.0 - version: 6.3.0 - modal: - specifier: ^0.6.3 - version: 0.6.3 - pg: - specifier: ^8.20.0 - version: 8.20.0 - posthog-node: - specifier: ^5.29.2 - version: 5.29.2(rxjs@7.8.2) - promptl-ai: - specifier: ^0.4.12 - version: 0.4.12 - stripe: - specifier: ^20.4.1 - version: 20.4.1(@types/node@25.6.0) - zod: - specifier: ^4.3.6 - version: 4.3.6 - devDependencies: - '@langchain/langgraph-cli': - specifier: ^1.1.17 - version: 1.1.17(367962cb075712636ea2108ac4ab276a) - '@types/bun': - specifier: ^1.3.12 - version: 1.3.12 - '@types/pg': - specifier: ^8.20.0 - version: 8.20.0 - typescript: - specifier: ^5.9.3 - version: 5.9.3 - packages/api-client: devDependencies: '@hey-api/openapi-ts': @@ -1498,19 +1149,6 @@ importers: specifier: ^4.1.4 version: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@22.19.17)(happy-dom@20.8.9)(jsdom@26.1.0)(vite@8.0.8(@types/node@22.19.17)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - packages/plugin-sdk: - dependencies: - react: - specifier: ^19.2.4 - version: 19.2.5 - devDependencies: - '@types/react': - specifier: ^19.2.14 - version: 19.2.14 - typescript: - specifier: ^5.9.3 - version: 5.9.3 - packages/pricing: dependencies: '@hypr/ui': @@ -2114,12 +1752,6 @@ importers: specifier: ^2.10.1 version: 2.10.1 - plugins/webhook: - dependencies: - '@tauri-apps/api': - specifier: ^2.10.1 - version: 2.10.1 - plugins/windows: dependencies: '@tauri-apps/api': @@ -2128,9 +1760,6 @@ importers: packages: - '@1natsu/wait-element@4.2.0': - resolution: {integrity: sha512-Om0Q+WE9mNrpY4AwMTvkFiYHv8VM7TML3PvOqXy+w6kAjLjKhGYHYX+305+a6J8RVpds9s7IF2Z5aOPYwULFNw==} - '@acemir/cssom@0.9.31': resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==} @@ -2210,16 +1839,6 @@ packages: peerDependencies: react: ^18 || ~19.0.1 || ~19.1.2 || ^19.2.1 - '@aklinker1/rollup-plugin-visualizer@5.12.0': - resolution: {integrity: sha512-X24LvEGw6UFmy0lpGJDmXsMyBD58XmX1bbwsaMLhNoM+UMQfQ3b2RtC+nz4b/NoRK5r6QJSKJHBNVeUdwqybaQ==} - engines: {node: '>=14'} - hasBin: true - peerDependencies: - rollup: 2.x || 3.x || 4.x - peerDependenciesMeta: - rollup: - optional: true - '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -2280,53 +1899,6 @@ packages: '@asamuzakjp/nwsapi@2.3.9': resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} - '@astrojs/check@0.9.8': - resolution: {integrity: sha512-LDng8446QLS5ToKjRHd3bgUdirvemVVExV7nRyJfW2wV36xuv7vDxwy5NWN9zqeSEDgg0Tv84sP+T3yEq+Zlkw==} - hasBin: true - peerDependencies: - typescript: ^5.0.0 - - '@astrojs/compiler@2.13.1': - resolution: {integrity: sha512-f3FN83d2G/v32ipNClRKgYv30onQlMZX1vCeZMjPsMMPl1mDpmbl0+N5BYo4S/ofzqJyS5hvwacEo0CCVDn/Qg==} - - '@astrojs/compiler@3.0.1': - resolution: {integrity: sha512-z97oYbdebO5aoWzuJ/8q5hLK232+17KcLZ7cJ8BCWk6+qNzVxn/gftC0KzMBUTD8WAaBkPpNSQK6PXLnNrZ0CA==} - - '@astrojs/internal-helpers@0.8.0': - resolution: {integrity: sha512-J56GrhEiV+4dmrGLPNOl2pZjpHXAndWVyiVDYGDuw6MWKpBSEMLdFxHzeM/6sqaknw9M+HFfHZAcvi3OfT3D/w==} - - '@astrojs/language-server@2.16.6': - resolution: {integrity: sha512-N990lu+HSFiG57owR0XBkr02BYMgiLCshLf+4QG4v6jjSWkBeQGnzqi+E1L08xFPPJ7eEeXnxPXGLaVv5pa4Ug==} - hasBin: true - peerDependencies: - prettier: ^3.0.0 - prettier-plugin-astro: '>=0.11.0' - peerDependenciesMeta: - prettier: - optional: true - prettier-plugin-astro: - optional: true - - '@astrojs/markdown-remark@7.1.0': - resolution: {integrity: sha512-P+HnCsu2js3BoTc8kFmu+E9gOcFeMdPris75g+Zl4sY8+bBRbSQV6xzcBDbZ27eE7yBGEGQoqjpChx+KJYIPYQ==} - - '@astrojs/mdx@5.0.3': - resolution: {integrity: sha512-zv/OlM5sZZvyjHqJjR3FjJvoCgbxdqj3t4jO/gSEUNcck3BjdtMgNQw8UgPfAGe4yySdG4vjZ3OC5wUxhu7ckg==} - engines: {node: '>=22.12.0'} - peerDependencies: - astro: ^6.0.0 - - '@astrojs/prism@4.0.1': - resolution: {integrity: sha512-nksZQVjlferuWzhPsBpQ1JE5XuKAf1id1/9Hj4a9KG4+ofrlzxUUwX4YGQF/SuDiuiGKEnzopGOt38F3AnVWsQ==} - engines: {node: '>=22.12.0'} - - '@astrojs/telemetry@3.3.0': - resolution: {integrity: sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ==} - engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} - - '@astrojs/yaml2ts@0.2.3': - resolution: {integrity: sha512-PJzRmgQzUxI2uwpdX2lXSHtP4G8ocp24/t+bZyf5Fy0SZLSF9f9KXZoMlFM/XCGue+B0nH/2IZ7FpBYQATBsCg==} - '@aws-crypto/crc32@5.2.0': resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} engines: {node: '>=16.0.0'} @@ -2413,10 +1985,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime@7.28.2': - resolution: {integrity: sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==} - engines: {node: '>=6.9.0'} - '@babel/runtime@7.29.2': resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} @@ -2454,43 +2022,6 @@ packages: '@bugsnag/safe-json-stringify@6.1.0': resolution: {integrity: sha512-ImA35rnM7bGr+J30R979FQ95BhRB4UO1KfJA0J2sVqc8nwnrS9hhE5mkTmQWMs8Vh1Da+hkLKs5jJB4JjNZp4A==} - '@capsizecss/unpack@4.0.0': - resolution: {integrity: sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA==} - engines: {node: '>=18'} - - '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': - resolution: {integrity: sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA==} - cpu: [arm64] - os: [darwin] - - '@cbor-extract/cbor-extract-darwin-x64@2.2.2': - resolution: {integrity: sha512-32b1mgc+P61Js+KW9VZv/c+xRw5EfmOcPx990JbCBSkYJFY0l25VinvyyWfl+3KjibQmAcYwmyzKF9J4DyKP/Q==} - cpu: [x64] - os: [darwin] - - '@cbor-extract/cbor-extract-linux-arm64@2.2.2': - resolution: {integrity: sha512-wfqgzqCAy/Vn8i6WVIh7qZd0DdBFaWBjPdB6ma+Wihcjv0gHqD/mw3ouVv7kbbUNrab6dKEx/w3xQZEdeXIlzg==} - cpu: [arm64] - os: [linux] - - '@cbor-extract/cbor-extract-linux-arm@2.2.2': - resolution: {integrity: sha512-tNg0za41TpQfkhWjptD+0gSD2fggMiDCSacuIeELyb2xZhr7PrhPe5h66Jc67B/5dmpIhI2QOUtv4SBsricyYQ==} - cpu: [arm] - os: [linux] - - '@cbor-extract/cbor-extract-linux-x64@2.2.2': - resolution: {integrity: sha512-rpiLnVEsqtPJ+mXTdx1rfz4RtUGYIUg2rUAZgd1KjiC1SehYUSkJN7Yh+aVfSjvCGtVP0/bfkQkXpPXKbmSUaA==} - cpu: [x64] - os: [linux] - - '@cbor-extract/cbor-extract-win32-x64@2.2.2': - resolution: {integrity: sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA==} - cpu: [x64] - os: [win32] - - '@cfworker/json-schema@4.1.1': - resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==} - '@chenglou/pretext@0.0.4': resolution: {integrity: sha512-FnPAFMid1/p1j2V2gRPUVBarGUIb2PhkkC9YNnTOfPtTDgHKh8siO8PP9pCxpFfYlcodWPJpE1UbSHGQqt8pQQ==} @@ -2509,18 +2040,6 @@ packages: '@chevrotain/utils@12.0.0': resolution: {integrity: sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA==} - '@clack/core@0.4.1': - resolution: {integrity: sha512-Pxhij4UXg8KSr7rPek6Zowm+5M22rbd2g1nfojHJkxp5YkFqiZ2+YLEM/XGVIzvGOcM0nqjIFxrpDwWRZYWYjA==} - - '@clack/core@1.2.0': - resolution: {integrity: sha512-qfxof/3T3t9DPU/Rj3OmcFyZInceqj/NVtO9rwIuJqCUgh32gwPjpFQQp/ben07qKlhpwq7GzfWpST4qdJ5Drg==} - - '@clack/prompts@0.9.1': - resolution: {integrity: sha512-JIpyaboYZeWYlyP0H+OoPPxd6nqueG/CmN6ixBiNFsIDHREevjIf0n0Ohh5gr5C8pEDknzgvz+pIJ8dMhzWIeg==} - - '@clack/prompts@1.2.0': - resolution: {integrity: sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w==} - '@codemirror/autocomplete@6.20.1': resolution: {integrity: sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==} @@ -2561,11 +2080,6 @@ packages: resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} engines: {node: '>=0.1.90'} - '@commander-js/extra-typings@13.1.0': - resolution: {integrity: sha512-q5P52BYb1hwVWE6dtID7VvuJWrlfbCv4klj7BjUUOqMz4jbSZD4C9fJ9lRjL2jnBGTg+gDDlaXN51rkWcLk4fg==} - peerDependencies: - commander: ~13.1.0 - '@content-collections/core@0.15.0': resolution: {integrity: sha512-GCbG7p+7cGnPUAyoOPoT8ABsd9D6VWF17YJUl+IHJWr+VYTYNKyDaQtJ+6Pfg55GvtuW3gCfqJkOx6Pu2SB6YA==} peerDependencies: @@ -2589,77 +2103,17 @@ packages: '@content-collections/core': ^0.x vite: ^6 || ^7 || ^8 - '@crabnebula/tauri-driver-darwin-arm64@2.0.9': - resolution: {integrity: sha512-VvO7OC6UkuEP3eU3klyl+Ka/jgWFc3yIUeCw6e8BtibQWDXSwQ/r2tzqvC0d+5psF/mWYiCqNAKMlg4v5+muvg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - - '@crabnebula/tauri-driver-darwin-x64@2.0.9': - resolution: {integrity: sha512-AWm3CY+YjKRLxaaucYdBG9cA7sTPviBpuVjJW6rNsPHGIFlxiA3C7fOv0Eekf2L7ps6v9HRCgldjPjTLH0DvNQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} - '@crabnebula/tauri-driver-linux-x64-gnu@2.0.9': - resolution: {integrity: sha512-+H2HQtjKQrVJ+A9C5x4LaTl/6oX1QpEBlfRJxA4WqCdbJEE2NsmLgODGSQNueav+9QV7MOgjppRMl9fiW7IIqg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [glibc] + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} - '@crabnebula/tauri-driver-win32-x64-msvc@2.0.9': - resolution: {integrity: sha512-/xqSl8dmGy7u9FXXjtuxAe38PJuQ4g1yT6Txgft5SwRwicAVkcEvmtdLkHJ6wUqEcg64UBEsoja8Z0EbNqTIcg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - - '@crabnebula/tauri-driver@2.0.9': - resolution: {integrity: sha512-W+fOUkQnP0MOSu9jMykG7HSstYysTQLMnL2+b3IpenbyqPfBiBI/9uSoNC1nXjh0aYm46nPgwKC6RbiWsWa0XQ==} - engines: {node: '>= 10'} - hasBin: true - - '@crabnebula/test-runner-backend-darwin-arm64@0.1.18': - resolution: {integrity: sha512-OrzDE0TqGhPhuvO4MkzcM08GsITfaciRy6ksmyHjFQhKMHhsqJV2i3pcVOiVpqHXJR881iI43NhduL0RWb7z0Q==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - - '@crabnebula/test-runner-backend-darwin-x64@0.1.18': - resolution: {integrity: sha512-uAqbq+z5Qa9HYTi2+CFNPcYr1CSe37TNGk+2XR15zRu3h1+1+FfIDrKNX4Mh0FqmaKZB3PWKSya35idvC7LHOQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - - '@crabnebula/test-runner-backend-linux-x64-gnu@0.1.18': - resolution: {integrity: sha512-fr0qNibQ/r9aCAk+Hb/K+sfiGw194vsg1MuX9w+NH83ZTMzkYWsPdGxCA+qQJ94g6I3PMaCYGVoQlWD1v3WoGA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@crabnebula/test-runner-backend-win32-x64-msvc@0.1.18': - resolution: {integrity: sha512-CImNzn1KbWAmc/qJx8pSdI37+b0ipz+gF5catxZUL/yk1S5vJH3n/+HByMrHbFxDGCbpGbSO8po14h2D7aZxcw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - - '@crabnebula/test-runner-backend@0.1.18': - resolution: {integrity: sha512-ZxwxU3PuJ+Cl/ECn5PItGsLkyyZnFZeqW7wsmjh9yK7x5jHdwqx9pffOvkY+JhS0bgMoN/V6CkalacrHBFVrAw==} - engines: {node: '>= 10'} - hasBin: true - - '@cspotcode/source-map-support@0.8.1': - resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} - engines: {node: '>=12'} - - '@csstools/color-helpers@5.1.0': - resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} - engines: {node: '>=18'} - - '@csstools/color-helpers@6.0.2': - resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} - engines: {node: '>=20.19.0'} + '@csstools/color-helpers@6.0.2': + resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} + engines: {node: '>=20.19.0'} '@csstools/css-calc@2.1.4': resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} @@ -2735,19 +2189,6 @@ packages: resolution: {integrity: sha512-Y6+WUMsTFWE5jb20IFP4YGa5IrGY/+a/FbOSjDF/wz9gepU2hwCYSXRHP/vPwBvwcY3SVMASt4yXxbXNXigmZQ==} engines: {node: '>=18'} - '@devicefarmer/adbkit-logcat@2.1.3': - resolution: {integrity: sha512-yeaGFjNBc/6+svbDeul1tNHtNChw6h8pSHAt5D+JsedUrMTN7tla7B15WLDyekxsuS2XlZHRxpuC6m92wiwCNw==} - engines: {node: '>= 4'} - - '@devicefarmer/adbkit-monkey@1.2.1': - resolution: {integrity: sha512-ZzZY/b66W2Jd6NHbAhLyDWOEIBWC11VizGFk7Wx7M61JZRz7HR9Cq5P+65RKWUU7u6wgsE8Lmh9nE4Mz+U2eTg==} - engines: {node: '>= 0.10.4'} - - '@devicefarmer/adbkit@3.3.8': - resolution: {integrity: sha512-7rBLLzWQnBwutH2WZ0EWUkQdihqrnLYCUMaB44hSol9e0/cdIhuNFcqZO0xNheAU6qqHVA8sMiLofkYTgb+lmw==} - engines: {node: '>= 0.10.4'} - hasBin: true - '@dnd-kit/accessibility@3.1.1': resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} peerDependencies: @@ -2859,27 +2300,6 @@ packages: '@electric-sql/pglite@0.3.16': resolution: {integrity: sha512-mZkZfOd9OqTMHsK+1cje8OSzfAQcpD7JmILXTl5ahdempjUDdmg4euf1biDex5/LfQIDJ3gvCu6qDgdnDxfJmA==} - '@emmetio/abbreviation@2.3.3': - resolution: {integrity: sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA==} - - '@emmetio/css-abbreviation@2.1.8': - resolution: {integrity: sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw==} - - '@emmetio/css-parser@0.4.1': - resolution: {integrity: sha512-2bC6m0MV/voF4CTZiAbG5MWKbq5EBmDPKu9Sb7s7nVcEzNQlrZP6mFFFlIaISM8X6514H9shWMme1fCm8cWAfQ==} - - '@emmetio/html-matcher@1.3.0': - resolution: {integrity: sha512-NTbsvppE5eVyBMuyGfVu2CRrLvo7J4YHb6t9sBFLyY03WYhXET37qA4zOYUjBWFCRHO7pS1B9khERtY0f5JXPQ==} - - '@emmetio/scanner@1.0.4': - resolution: {integrity: sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA==} - - '@emmetio/stream-reader-utils@0.1.0': - resolution: {integrity: sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A==} - - '@emmetio/stream-reader@2.2.0': - resolution: {integrity: sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw==} - '@emnapi/core@1.9.2': resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} @@ -3835,15 +3255,6 @@ packages: react: ^16 || ^17 || ^18 || ^19 react-dom: ^16 || ^17 || ^18 || ^19 - '@grpc/grpc-js@1.14.3': - resolution: {integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==} - engines: {node: '>=12.10.0'} - - '@grpc/proto-loader@0.8.0': - resolution: {integrity: sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==} - engines: {node: '>=6'} - hasBin: true - '@handlewithcare/react-prosemirror@3.0.1': resolution: {integrity: sha512-09Mn1ayFqxtcgC7MxxKD3/TP8bhDbvZTP5f6/BhnD+oPaH0OjqxCzTwM7a6HkP64LiUvThFiG2VYludbhRe+5g==} engines: {node: '>=16.9'} @@ -3894,18 +3305,6 @@ packages: '@opentelemetry/context-zone': optional: true - '@hono/node-server@1.19.13': - resolution: {integrity: sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==} - engines: {node: '>=18.14.1'} - peerDependencies: - hono: ^4 - - '@hono/zod-validator@0.7.6': - resolution: {integrity: sha512-Io1B6d011Gj1KknV4rXYz4le5+5EubcWEU/speUjuw9XMMIaP3n78yXLhjd2A3PXaXaUwEAluOiAyLqhBEJgsw==} - peerDependencies: - hono: '>=3.9.0' - zod: ^3.25.0 || ^4.0.0 - '@hookform/resolvers@5.2.2': resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==} peerDependencies: @@ -4105,55 +3504,6 @@ packages: resolution: {integrity: sha512-1eh5fndM49SAIz/f3Zderzc7Dfc0J8So4cUxEvQpeM+IEsqh+vsyVH8t8JZjlJJArCs4EppPASk0isNz3GVuiw==} hasBin: true - '@inquirer/ansi@1.0.2': - resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} - engines: {node: '>=18'} - - '@inquirer/checkbox@4.3.2': - resolution: {integrity: sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/confirm@5.1.21': - resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/core@10.3.2': - resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/editor@4.2.23': - resolution: {integrity: sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/expand@4.0.23': - resolution: {integrity: sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/external-editor@1.0.3': resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} engines: {node: '>=18'} @@ -4163,82 +3513,6 @@ packages: '@types/node': optional: true - '@inquirer/figures@1.0.15': - resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} - engines: {node: '>=18'} - - '@inquirer/input@4.3.1': - resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/number@3.0.23': - resolution: {integrity: sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/password@4.0.23': - resolution: {integrity: sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/prompts@7.10.1': - resolution: {integrity: sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/rawlist@4.1.11': - resolution: {integrity: sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/search@3.2.2': - resolution: {integrity: sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/select@4.4.2': - resolution: {integrity: sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/type@3.0.10': - resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@ioredis/commands@1.5.0': resolution: {integrity: sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==} @@ -4250,30 +3524,6 @@ packages: resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} - '@jest/diff-sequences@30.3.0': - resolution: {integrity: sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - '@jest/expect-utils@30.3.0': - resolution: {integrity: sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - '@jest/get-type@30.1.0': - resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - '@jest/pattern@30.0.1': - resolution: {integrity: sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - '@jest/schemas@30.0.5': - resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - '@jest/types@30.3.0': - resolution: {integrity: sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -4296,92 +3546,12 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@js-sdsl/ordered-map@4.4.2': - resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} - '@jsdevtools/ono@7.1.3': resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} '@kurkle/color@0.3.4': resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} - '@langchain/core@1.1.39': - resolution: {integrity: sha512-DP9c7TREy6iA7HnywstmUAsNyJNYTFpRg2yBfQ+6H0l1HnvQzei9GsQ36GeOLxgRaD3vm9K8urCcawSC7yQpCw==} - engines: {node: '>=20'} - - '@langchain/langgraph-api@1.1.17': - resolution: {integrity: sha512-hu5V47k9dzxr2svDWb7Riro3QNSj+h7xRaixN6cBJPGvulUt5xfA7GJ7Z12NRTmLCBRhE0Fp+rPnO6qHSN2LXw==} - engines: {node: ^18.19.0 || >=20.16.0} - peerDependencies: - '@langchain/core': ^0.3.59 || ^1.0.1 - '@langchain/langgraph': ^0.2.57 || ^0.3.0 || ^0.4.0 || ^1.0.0-alpha || ^1.0.0 - '@langchain/langgraph-checkpoint': ~0.0.16 || ^0.1.0 || ~1.0.0 - '@langchain/langgraph-sdk': ^1.6.5 - typescript: ^5.5.4 - peerDependenciesMeta: - '@langchain/langgraph-sdk': - optional: true - - '@langchain/langgraph-checkpoint-postgres@1.0.1': - resolution: {integrity: sha512-cRXOLYZc0egMjyAQfBblWXdoBS+WKwEbCEuE+/f88XHJ1bq5jVz7aycbpjF/7F5EykG7xmybQOz5eUdg3neRzg==} - engines: {node: '>=18'} - peerDependencies: - '@langchain/core': ^1.0.1 - '@langchain/langgraph-checkpoint': ^1.0.0 - - '@langchain/langgraph-checkpoint@1.0.1': - resolution: {integrity: sha512-HM0cJLRpIsSlWBQ/xuDC67l52SqZ62Bh2Y61DX+Xorqwoh5e1KxYvfCD7GnSTbWWhjBOutvnR0vPhu4orFkZfw==} - engines: {node: '>=18'} - peerDependencies: - '@langchain/core': ^1.0.1 - - '@langchain/langgraph-cli@1.1.17': - resolution: {integrity: sha512-zpVfOwdW+I2fkv5ETDKYsb1nP5dRf/3JOdG3LLTMS2jrjlRSUU+DIlpgr3nimXOZcLLZSxmD6fvh9c635ULsRw==} - engines: {node: ^18.19.0 || >=20.16.0} - hasBin: true - - '@langchain/langgraph-sdk@1.8.8': - resolution: {integrity: sha512-4OoqFAvPloOTZ6oPxXbJngz4FLJO8QSXb+BQV3qvNTvmfu1LQA7cCEqSNLYX9MoC340PbnDkHNgUtjajwkDHRg==} - peerDependencies: - '@langchain/core': ^1.1.16 - react: ^18 || ^19 - react-dom: ^18 || ^19 - svelte: ^4.0.0 || ^5.0.0 - vue: ^3.0.0 - peerDependenciesMeta: - '@langchain/core': - optional: true - react: - optional: true - react-dom: - optional: true - svelte: - optional: true - vue: - optional: true - - '@langchain/langgraph-ui@1.1.17': - resolution: {integrity: sha512-M4CiGjM3wT+natby6vcHIotjFATtZXzRafRXx97+u/hOlhggIr4aLqxz2yOnL9Z97LL7gp2iEoMmsXvFuFX0Rw==} - engines: {node: ^18.19.0 || >=20.16.0} - hasBin: true - - '@langchain/langgraph@1.2.8': - resolution: {integrity: sha512-kKkRpC5xFz1e6vPivE7lwRJa5oahLAMaVQvVGZdTa6uJIchIYJDIuM1n93FqGvg8aYVcgYU4FENtKKC5Eh1JYw==} - engines: {node: '>=18'} - peerDependencies: - '@langchain/core': ^1.1.16 - zod: ^3.25.32 || ^4.2.0 - zod-to-json-schema: ^3.x - peerDependenciesMeta: - zod-to-json-schema: - optional: true - - '@langchain/openai@1.4.4': - resolution: {integrity: sha512-mRr/X5rvlwPj6cSXPxbL+CtOqYANO1/+CQ3Z+5t48kWnrlgPYOazmA+UAWvqQOuwJ6LaYn3SFrt43rR4lte/Ow==} - engines: {node: '>=20'} - peerDependencies: - '@langchain/core': ^1.1.39 - '@lezer/common@1.5.2': resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==} @@ -5361,9 +4531,6 @@ packages: '@orama/plugin-qps@3.1.18': resolution: {integrity: sha512-q19pFGF1jRk+dGR4j/4MscCACtZo6DKO8ZjnM3AWJICzjWVYJYBvlGuD2+Jt079iUW04cGh4EpMw5WxaTpZQCw==} - '@oslojs/encoding@1.1.0': - resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} - '@oxc-project/types@0.124.0': resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} @@ -5787,11 +4954,6 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@playwright/test@1.59.1': - resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==} - engines: {node: '>=18'} - hasBin: true - '@pnpm/config.env-replace@1.1.0': resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} engines: {node: '>=12.22.0'} @@ -5832,9 +4994,6 @@ packages: peerDependencies: '@opentelemetry/api': ^1.8 - '@promptbook/utils@0.69.5': - resolution: {integrity: sha512-xm5Ti/Hp3o4xHrsK9Yy3MS6KbDxYbq485hDsFvxqaNA7equHLPdo8H8faTitTeb14QCDfLW4iwCxdVYu5sn6YQ==} - '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -5865,11 +5024,6 @@ packages: '@protobufjs/utf8@1.1.0': resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} - '@puppeteer/browsers@2.13.0': - resolution: {integrity: sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==} - engines: {node: '>=18'} - hasBin: true - '@puppeteer/browsers@2.3.0': resolution: {integrity: sha512-ioXoq9gPxkss4MYhD+SFaU9p1IHFUX0ILAWFPyjGaBdjLsYAlZw6j1iLA0N/m12uVHLFDfSYNF7EQccjinIMDA==} engines: {node: '>=18'} @@ -7051,10 +6205,6 @@ packages: resolution: {integrity: sha512-OEn2eg8h3Mr7BmBGQ28BqbWehYA/NklZ0pAZB1FypPPl+kMd85AbaRdGTnaSjgmpc8bKbBO64edq4Y14sbCs5w==} engines: {node: '>=14.18'} - '@sentry/bun@10.48.0': - resolution: {integrity: sha512-ZIRhGIUUe8eRbD79iU42k90OyKyRx3pCvrOQ5g7jnq7zNhDEw1I4UUq3wjkiPLWpvKMN9WnWAlb4e+WM3q0SvA==} - engines: {node: '>=18'} - '@sentry/bundler-plugin-core@5.2.0': resolution: {integrity: sha512-+C0x4gEIJRgoMwyRFGx+TFiJ1Po2BZlT1v61+PnouiaprKL5qtZG8n5PXx/5LPLDsVjSIcXjnDrTz9aSm8SJ3w==} engines: {node: '>= 18'} @@ -7195,52 +6345,24 @@ packages: '@shikijs/core@3.23.0': resolution: {integrity: sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==} - '@shikijs/core@4.0.2': - resolution: {integrity: sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==} - engines: {node: '>=20'} - '@shikijs/engine-javascript@3.23.0': resolution: {integrity: sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==} - '@shikijs/engine-javascript@4.0.2': - resolution: {integrity: sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag==} - engines: {node: '>=20'} - '@shikijs/engine-oniguruma@3.23.0': resolution: {integrity: sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==} - '@shikijs/engine-oniguruma@4.0.2': - resolution: {integrity: sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg==} - engines: {node: '>=20'} - '@shikijs/langs@3.23.0': resolution: {integrity: sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==} - '@shikijs/langs@4.0.2': - resolution: {integrity: sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==} - engines: {node: '>=20'} - - '@shikijs/primitive@4.0.2': - resolution: {integrity: sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==} - engines: {node: '>=20'} - '@shikijs/themes@3.23.0': resolution: {integrity: sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==} - '@shikijs/themes@4.0.2': - resolution: {integrity: sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA==} - engines: {node: '>=20'} - '@shikijs/transformers@3.23.0': resolution: {integrity: sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ==} '@shikijs/types@3.23.0': resolution: {integrity: sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==} - '@shikijs/types@4.0.2': - resolution: {integrity: sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==} - engines: {node: '>=20'} - '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} @@ -7255,10 +6377,6 @@ packages: resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} engines: {node: '>=18'} - '@sindresorhus/merge-streams@4.0.0': - resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} - engines: {node: '>=18'} - '@sindresorhus/slugify@2.2.1': resolution: {integrity: sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw==} engines: {node: '>=12'} @@ -7267,32 +6385,6 @@ packages: resolution: {integrity: sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==} engines: {node: '>=12'} - '@slack/bolt@4.7.0': - resolution: {integrity: sha512-Xpf+gKegNvkHpft1z4YiuqZdciJ3tUp1bIRQxylW30Ovf+hzjb0M1zTHVtJsRw9jsjPxHTPoyanEXVvG6qVE1g==} - engines: {node: '>=18', npm: '>=8.6.0'} - peerDependencies: - '@types/express': ^5.0.0 - - '@slack/logger@4.0.1': - resolution: {integrity: sha512-6cmdPrV/RYfd2U0mDGiMK8S7OJqpCTm7enMLRR3edccsPX8j7zXTLnaEF4fhxxJJTAIOil6+qZrnUPTuaLvwrQ==} - engines: {node: '>= 18', npm: '>= 8.6.0'} - - '@slack/oauth@3.0.5': - resolution: {integrity: sha512-exqFQySKhNDptWYSWhvRUJ4/+ndu2gayIy7vg/JfmJq3wGtGdHk531P96fAZyBm5c1Le3yaPYqv92rL4COlU3A==} - engines: {node: '>=18', npm: '>=8.6.0'} - - '@slack/socket-mode@2.0.6': - resolution: {integrity: sha512-Aj5RO3MoYVJ+b2tUjHUXuA3tiIaCUMOf1Ss5tPiz29XYVUi6qNac2A8ulcU1pUPERpXVHTmT1XW6HzQIO74daQ==} - engines: {node: '>= 18', npm: '>= 8.6.0'} - - '@slack/types@2.20.1': - resolution: {integrity: sha512-eWX2mdt1ktpn8+40iiMc404uGrih+2fxiky3zBcPjtXKj6HLRdYlmhrPkJi7JTJm8dpXR6BWVWEDBXtaWMKD6A==} - engines: {node: '>= 12.13.0', npm: '>= 6.12.0'} - - '@slack/web-api@7.15.0': - resolution: {integrity: sha512-va7zYIt3QHG1x9M/jqXXRPFMoOVlVSSRHC5YH+DzKYsrz5xUKOA3lR4THsu/Zxha9N1jOndbKFKLtr0WOPW1Vw==} - engines: {node: '>= 18', npm: '>= 8.6.0'} - '@smithy/eventstream-codec@4.2.13': resolution: {integrity: sha512-vYahwBAtRaAcFbOmE9aLr12z7RiHYDSLcnogSdxfm7kKfsNa3wH+NU5r7vTeB5rKvLsWyPjVX8iH94brP7umiQ==} engines: {node: '>=18.0.0'} @@ -7408,11 +6500,6 @@ packages: resolution: {integrity: sha512-DHmlvdAXwtOmZNbkIZi4lkobPR3XjIzoOgzoz5duMf6G+sDeY015YrzMJCnqdccuYr7X5x4yYuSwF//RoN2dvQ==} engines: {node: '>=20.0.0'} - '@supabase/stripe-sync-engine@0.45.0': - resolution: {integrity: sha512-3yP6Lyqg+jBZdI3MyGM0gBGmVmaaSBI5QO4hwvmsdJmIztU/3Wvu/YF0S8Ga1waWdeG1/9YChvVJV7gRON+t1A==} - peerDependencies: - stripe: '> 11' - '@supabase/supabase-js@2.103.0': resolution: {integrity: sha512-j/6q5+LtXbR/YOLSLhy7Na74RD1cV2v+KwIIuuqMEjk1JpLEEyu0ynwDHpGoxMncDQl+R5FogaVqZm+85lZvtw==} engines: {node: '>=20.0.0'} @@ -8260,12 +7347,6 @@ packages: '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} - '@types/body-parser@1.19.6': - resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} - - '@types/bun@1.3.12': - resolution: {integrity: sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A==} - '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -8380,24 +7461,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/express-serve-static-core@5.1.1': - resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} - - '@types/express@5.0.6': - resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} - - '@types/filesystem@0.0.36': - resolution: {integrity: sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==} - - '@types/filewriter@0.0.33': - resolution: {integrity: sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==} - '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} - '@types/har-format@1.2.16': - resolution: {integrity: sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==} - '@types/hast@2.3.10': resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} @@ -8407,21 +7473,9 @@ packages: '@types/http-cache-semantics@4.2.0': resolution: {integrity: sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==} - '@types/http-errors@2.0.5': - resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} - '@types/http-proxy@1.17.17': resolution: {integrity: sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==} - '@types/istanbul-lib-coverage@2.0.6': - resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} - - '@types/istanbul-lib-report@3.0.3': - resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} - - '@types/istanbul-reports@3.0.4': - resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} - '@types/js-cookie@3.0.6': resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==} @@ -8434,9 +7488,6 @@ packages: '@types/json5@2.2.0': resolution: {integrity: sha512-NrVug5woqbvNZ0WX+Gv4R+L4TGddtmFek2u8RtccAgFZWtS9QXF2xCXY22/M4nzkaKF0q9Fc6M/5rxLDhfwc/A==} - '@types/jsonwebtoken@9.0.10': - resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} - '@types/katex@0.16.8': resolution: {integrity: sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==} @@ -8446,9 +7497,6 @@ packages: '@types/markdown-it@14.1.2': resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} - '@types/marked@6.0.0': - resolution: {integrity: sha512-jmjpa4BwUsmhxcfsgUit/7A9KbrC48Q0q8KvnY107ogcjGgTFDlIL3RpihNpx2Mu1hM4mdFQjoVc4O6JoGKHsA==} - '@types/mdast@3.0.15': resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} @@ -8461,12 +7509,6 @@ packages: '@types/mdx@2.0.13': resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} - '@types/minimatch@3.0.5': - resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} - - '@types/mocha@10.0.10': - resolution: {integrity: sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==} - '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -8482,9 +7524,6 @@ packages: '@types/node@18.19.130': resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} - '@types/node@20.19.39': - resolution: {integrity: sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==} - '@types/node@22.19.17': resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==} @@ -8509,12 +7548,6 @@ packages: '@types/pg@8.20.0': resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} - '@types/qs@6.15.0': - resolution: {integrity: sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==} - - '@types/range-parser@1.2.7': - resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -8526,9 +7559,6 @@ packages: '@types/resolve@1.20.6': resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} - '@types/retry@0.12.0': - resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} - '@types/retry@0.12.2': resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==} @@ -8538,18 +7568,6 @@ packages: '@types/semver@7.7.1': resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} - '@types/send@1.2.1': - resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} - - '@types/serve-static@2.2.0': - resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} - - '@types/sinonjs__fake-timers@8.1.5': - resolution: {integrity: sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==} - - '@types/stack-utils@2.0.3': - resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} - '@types/stopword@2.0.3': resolution: {integrity: sha512-hioMj0lOvISM+EDevf7ijG8EMbU+J3pj4SstCyfQC1t39uPYpAe7beSfBdU6c1d9jeECTQQtR3UJWtVoUO8Weg==} @@ -8571,24 +7589,12 @@ packages: '@types/use-sync-external-store@0.0.6': resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} - '@types/uuid@10.0.0': - resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} - '@types/whatwg-mimetype@3.0.2': resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} - '@types/which@2.0.2': - resolution: {integrity: sha512-113D3mDkZDjo+EeUEHCFy0qniNc1ZpecGiAU7WSo7YDoSzolZIQKpYFHrPpjkB2nuyahcKfrmLXeQlh7gqJYdw==} - '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@types/yargs-parser@21.0.3': - resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} - - '@types/yargs@17.0.35': - resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} @@ -8651,11 +7657,6 @@ packages: resolution: {integrity: sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript/vfs@1.6.4': - resolution: {integrity: sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ==} - peerDependencies: - typescript: '*' - '@uidotdev/usehooks@2.4.1': resolution: {integrity: sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==} engines: {node: '>=16'} @@ -8767,18 +7768,12 @@ packages: vite: optional: true - '@vitest/pretty-format@2.1.9': - resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} - '@vitest/pretty-format@4.1.4': resolution: {integrity: sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==} '@vitest/runner@4.1.4': resolution: {integrity: sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==} - '@vitest/snapshot@2.1.9': - resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} - '@vitest/snapshot@4.1.4': resolution: {integrity: sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==} @@ -8788,32 +7783,6 @@ packages: '@vitest/utils@4.1.4': resolution: {integrity: sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==} - '@volar/kit@2.4.28': - resolution: {integrity: sha512-cKX4vK9dtZvDRaAzeoUdaAJEew6IdxHNCRrdp5Kvcl6zZOqb6jTOfk3kXkIkG3T7oTFXguEMt5+9ptyqYR84Pg==} - peerDependencies: - typescript: '*' - - '@volar/language-core@2.4.28': - resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} - - '@volar/language-server@2.4.28': - resolution: {integrity: sha512-NqcLnE5gERKuS4PUFwlhMxf6vqYo7hXtbMFbViXcbVkbZ905AIVWhnSo0ZNBC2V127H1/2zP7RvVOVnyITFfBw==} - - '@volar/language-service@2.4.28': - resolution: {integrity: sha512-Rh/wYCZJrI5vCwMk9xyw/Z+MsWxlJY1rmMZPsxUoJKfzIRjS/NF1NmnuEcrMbEVGja00aVpCsInJfixQTMdvLw==} - - '@volar/source-map@2.4.28': - resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==} - - '@volar/typescript@2.4.28': - resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} - - '@vscode/emmet-helper@2.11.0': - resolution: {integrity: sha512-QLxjQR3imPZPQltfbWRnHU6JecWTF1QSWhx3GAKQpslx7y3Dp6sIIXhKjiUJ/BR9FX8PVthjr9PD6pNwOJfAzw==} - - '@vscode/l10n@0.0.18': - resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==} - '@vue/compiler-core@3.5.32': resolution: {integrity: sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==} @@ -8835,81 +7804,6 @@ packages: react: ^18.2.0 || ^19.0.0 wavesurfer.js: '>=7.7.14' - '@wdio/cli@9.27.0': - resolution: {integrity: sha512-k3kSs1sWTnDwdFLdBua7j5O//0N9k3qTj2nkyfMnkCEzOU00UMV2Y0f/yzNrn8BkkvohrJmwdEQPYx7rNhfj9g==} - engines: {node: '>=18.20.0'} - hasBin: true - - '@wdio/config@9.27.0': - resolution: {integrity: sha512-9y8z7ugIbU6ycKrA2SqCpKh1/hobut2rDq9CLt/BNVzSlebBBVOTMiAt1XroZzcPnA7/ZqpbkpOsbpPUaAQuNQ==} - engines: {node: '>=18.20.0'} - - '@wdio/dot-reporter@9.27.0': - resolution: {integrity: sha512-gYFCTeEHZxzqdXCn/L519HDAFpci+dsdfzLHg+2XMlzIWEGSgHxMIr4/iLjvCwDgCSLeh+XvsBsPm3U+qOtwdg==} - engines: {node: '>=18.20.0'} - - '@wdio/globals@9.27.0': - resolution: {integrity: sha512-yT6EAyvEqm+wFD11fg89BMxvFkYLgnIVCihfJx+k73Gm3utL/DfZQpSheQdwrlQzu5p7jHi/JwOD76740F5Peg==} - engines: {node: '>=18.20.0'} - peerDependencies: - expect-webdriverio: ^5.6.5 - webdriverio: ^9.0.0 - - '@wdio/local-runner@9.27.0': - resolution: {integrity: sha512-0AqAbz1UhZ9e72ebqH4/B9/qOy0LVm3iOOYp16Rz2zkE5DOudLPPn3DpakafqW22Z7Q+Wb/23KRttPMrq0rOxw==} - engines: {node: '>=18.20.0'} - - '@wdio/logger@9.18.0': - resolution: {integrity: sha512-HdzDrRs+ywAqbXGKqe1i/bLtCv47plz4TvsHFH3j729OooT5VH38ctFn5aLXgECmiAKDkmH/A6kOq2Zh5DIxww==} - engines: {node: '>=18.20.0'} - - '@wdio/mocha-framework@9.27.0': - resolution: {integrity: sha512-bngxUgV7FRcrcPsf5oeNBPHMBCiQoMAg6fdv0iaaOvmNQOzu2y5hB/dmLBH8FjXby4Ni/H6ea2oA2MPwJYW9FA==} - engines: {node: '>=18.20.0'} - - '@wdio/protocols@9.27.0': - resolution: {integrity: sha512-rIk69BsY1+6uU2PEN5FiRpI6K7HJ86YHzZRFBe4iRzKXQgGNk1zWzbdVJIuNFoOWsnmYUkK42KSSOT4Le6EmiQ==} - - '@wdio/repl@9.16.2': - resolution: {integrity: sha512-FLTF0VL6+o5BSTCO7yLSXocm3kUnu31zYwzdsz4n9s5YWt83sCtzGZlZpt7TaTzb3jVUfxuHNQDTb8UMkCu0lQ==} - engines: {node: '>=18.20.0'} - - '@wdio/reporter@9.27.0': - resolution: {integrity: sha512-JsazSrpdKrUEz0RkZcNzHHO9EaoJsWnjzi8Lk3hyI3e2T0M0d/EZTaYwLU+zZXr9VRJBulv8DhRfmBx+gbY2jw==} - engines: {node: '>=18.20.0'} - - '@wdio/runner@9.27.0': - resolution: {integrity: sha512-PAuvuq0GaziutDXO8pZkUmca/qFGnGY2O3e4mQtqDUZbkyxYF4W68WJWhkvwuDAvN5GH1V+K/FBmiwL8m+roxw==} - engines: {node: '>=18.20.0'} - peerDependencies: - expect-webdriverio: ^5.6.5 - webdriverio: ^9.0.0 - - '@wdio/spec-reporter@9.27.0': - resolution: {integrity: sha512-pSrSfflFGthCc14B/4VZqthrz6T5/N+PDqpIOf+bfwJwtPgVlzZoLzbkKYDmCYHGDlDFt1QrZS9WQ5Qw2Qz/Ow==} - engines: {node: '>=18.20.0'} - - '@wdio/types@9.27.0': - resolution: {integrity: sha512-DQJ+OdRBqUBcQ30DN2Z651hEVh3OoxnlDUSRqlWy9An2AY6v9rYWTj825B6zsj5pLLEToYO1tfwWq0ab183pXg==} - engines: {node: '>=18.20.0'} - - '@wdio/utils@9.27.0': - resolution: {integrity: sha512-fUasd5OKJTy2seJfWnYZ9xlxTtY0p/Kyeuh7Tbb8kcofBqmBi2fTvM3sfZlo1tGQX9yCh+IS2N7hlfyFMmuZ+w==} - engines: {node: '>=18.20.0'} - - '@wdio/xvfb@9.27.0': - resolution: {integrity: sha512-sumk8m5wzOPMs8TizfQkWG0MTqe0p1yfu77ouz+xy1hNW+gaSf99uiU3lvz4rSghloM1esKfqRCFQibJI4+d/w==} - engines: {node: '>=18'} - - '@webext-core/fake-browser@1.3.4': - resolution: {integrity: sha512-nZcVWr3JpwpS5E6hKpbAwAMBM/AXZShnfW0F76udW8oLd6Kv0nbW6vFS07md4Na/0ntQonk3hFnlQYGYBAlTrA==} - - '@webext-core/isolated-element@1.1.5': - resolution: {integrity: sha512-4m6oP8Vzm/68YO1QmkUOZqqUcmyBtA53tji2g00/nYXE3E3IceYgeub7eIqvXDV2Z7xU6cm6qO1IMt4XFVwtvQ==} - - '@webext-core/match-patterns@1.0.3': - resolution: {integrity: sha512-NY39ACqCxdKBmHgw361M9pfJma8e4AZo20w9AY+5ZjIj1W2dvXC8J31G5fjfOGbulW9w4WKpT8fPooi0mLkn9A==} - '@whatwg-node/disposablestack@0.0.6': resolution: {integrity: sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw==} engines: {node: '>=18.0.0'} @@ -8930,18 +7824,6 @@ packages: resolution: {integrity: sha512-kMwLlxUbduttIgaPdSkmEarFpP+mSY8FEm+QWMBRJwxOHWkri+cxd8KZHO9EMrB9vgUuz+5WEaCawaL5wGVoXg==} engines: {node: '>=18.0.0'} - '@wxt-dev/browser@0.1.40': - resolution: {integrity: sha512-h2/v/Hpkj5sz//h84ProqBaAcTsDFRKp9b/JVHOK/r7LT0XLE+ZDs5YN1BnFLUEHdM7G3fUjTyBG84cayXQshQ==} - - '@wxt-dev/module-react@1.2.2': - resolution: {integrity: sha512-+lRLi1r9dAXpLySWSIWHLJ1h/nFzR20iQnx3RNrKyA6oJg4+ClOluVXozHjfPg9Okfy/umtffiOopGayASrg6w==} - peerDependencies: - vite: ^5.4.19 || ^6.3.4 || ^7.0.0 || ^8.0.0-0 - wxt: '>=0.19.16' - - '@wxt-dev/storage@1.2.8': - resolution: {integrity: sha512-GWCFKgF5+d7eslOxUDFC70ypA9njupmJb1nQM8uZoX0J3sWT2BO5xJLzb1sYahWAfID9p2BMtnUBN1lkWxPsbQ==} - '@xhmikosr/archive-type@6.0.1': resolution: {integrity: sha512-PB3NeJL8xARZt52yDBupK0dNPn8uIVQDe15qNehUpoeeLWCZyAOam4vGXnoZGz2N9D1VXtjievJuCsXam2TmbQ==} engines: {node: ^14.14.0 || >=16.0.0} @@ -8985,17 +7867,10 @@ packages: solid-js: optional: true - '@zip.js/zip.js@2.8.26': - resolution: {integrity: sha512-RQ4h9F6DOiHxpdocUDrOl6xBM+yOtz+LkUol47AVWcfebGBDpZ7w7Xvz9PS24JgXvLGiXXzSAfdCdVy1tPlaFA==} - engines: {bun: '>=0.7.0', deno: '>=1.0.0', node: '>=18.0.0'} - abbrev@3.0.1: resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==} engines: {node: ^18.17.0 || >=20.5.0} - abort-controller-x@0.5.0: - resolution: {integrity: sha512-yTt9CI0x+nRfX6BFMenEGP8ooPvErGH6AbFz20C2IeOLIlDsrw/VHpgne3GsCEuTA410IiFiaLVFKmgM4bKEPQ==} - abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -9026,10 +7901,6 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - adm-zip@0.5.17: - resolution: {integrity: sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==} - engines: {node: '>=12.0'} - agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -9054,14 +7925,6 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 - ajv-draft-04@1.0.0: - resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} - peerDependencies: - ajv: ^8.5.0 - peerDependenciesMeta: - ajv: - optional: true - ajv-errors@3.0.0: resolution: {integrity: sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==} peerDependencies: @@ -9168,28 +8031,16 @@ packages: aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} - aria-query@5.3.2: - resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} - engines: {node: '>= 0.4'} - array-buffer-byte-length@1.0.2: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} - array-differ@4.0.0: - resolution: {integrity: sha512-Q6VPTLMsmXZ47ENG3V+wQyZS1ZxXMxFyYzA+Z/GMrJ6yIutAIEf9wTyroTzmGjNfox9/h3GdGBCVh43GVFx4Uw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - array-iterate@2.0.1: resolution: {integrity: sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==} array-timsort@1.0.3: resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} - array-union@3.0.1: - resolution: {integrity: sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==} - engines: {node: '>=12'} - array.prototype.flatmap@1.3.3: resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} engines: {node: '>= 0.4'} @@ -9221,25 +8072,10 @@ packages: resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} hasBin: true - astro-seo@1.1.0: - resolution: {integrity: sha512-G6LDNDyga30o+52v58jU7C35n3cORUzk7RSuY+xf/ZRbW6JiNplyaOO1VoYVKO+xzYNaBcxqNln2RFRR/wgJNQ==} - - astro@6.1.5: - resolution: {integrity: sha512-AJVw/JlssxUCBFi3Hp4djL8Pt7wUQqStBBawCd8cNGBBM2lBzp/rXGguzt4OcMfW+86fs0hpFwMyopHM2r6d3g==} - engines: {node: '>=22.12.0', npm: '>=9.6.5', pnpm: '>=7.1.0'} - hasBin: true - - async-exit-hook@2.0.1: - resolution: {integrity: sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==} - engines: {node: '>=0.12.0'} - async-function@1.0.0: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} - async-mutex@0.5.0: - resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} - async-sema@3.1.1: resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==} @@ -9280,13 +8116,6 @@ packages: axios@1.13.5: resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==} - axios@1.15.0: - resolution: {integrity: sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==} - - axobject-query@4.1.0: - resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} - engines: {node: '>= 0.4'} - b4a@1.8.0: resolution: {integrity: sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==} peerDependencies: @@ -9401,9 +8230,6 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - bluebird@3.7.2: - resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} - body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} @@ -9429,9 +8255,6 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browser-stdout@1.3.1: - resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} - browserslist@4.28.2: resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -9479,22 +8302,6 @@ packages: magicast: optional: true - c12@3.3.4: - resolution: {integrity: sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==} - peerDependencies: - magicast: '*' - peerDependenciesMeta: - magicast: - optional: true - - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - - cac@7.0.0: - resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} - engines: {node: '>=20.19.0'} - cacheable-lookup@7.0.0: resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} engines: {node: '>=14.16'} @@ -9522,10 +8329,6 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - camelcase@6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} - engines: {node: '>=10'} - camelcase@8.0.0: resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} engines: {node: '>=16'} @@ -9536,13 +8339,6 @@ packages: castable-video@1.1.12: resolution: {integrity: sha512-fSHLEq6cUb+5TMMxTrYV9Yc7RJvcyz0IvjQpGFmjX55CTNZ/OzPlp0GLQl3YWligG57H80q6FrbgOJ9+jFeIgA==} - cbor-extract@2.2.2: - resolution: {integrity: sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng==} - hasBin: true - - cbor-x@1.6.4: - resolution: {integrity: sha512-UGKHjp6RHC6QuZ2yy5LCKm7MojM4716DwoSaqwQpaH4DvZvbBTGcoDNTiG9Y2lByXZYFEs9WRkS5tLl96IrF1Q==} - ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -9621,11 +8417,6 @@ packages: chroma-js@3.2.0: resolution: {integrity: sha512-os/OippSlX1RlWWr+QDPcGUZs0uoqr32urfxESG9U93lhUfbnlyckte84Q8P1UQY/qth983AS1JONKmLS4T0nw==} - chrome-launcher@1.2.0: - resolution: {integrity: sha512-JbuGuBNss258bvGil7FT4HKdC3SC2K7UAEUqiPy3ACS3Yxo3hAW6bvFpCu2HsIJLgTqxgEX6BkujvzZfLpUD0Q==} - engines: {node: '>=12.13.0'} - hasBin: true - chromium-bidi@0.6.3: resolution: {integrity: sha512-qXlsCmpCZJAnoTYI83Iu6EdYQpMYdVkCfq08KDh2pmlVqK5t5IA9mGs4/LwCwp4fqisSOMXZxP3HIh8w8aRn0A==} peerDependencies: @@ -9685,13 +8476,6 @@ packages: resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} engines: {node: '>= 10'} - cli-width@4.1.0: - resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} - engines: {node: '>= 12'} - - cliui@7.0.4: - resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} - cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -9718,9 +8502,6 @@ packages: react: ^18 || ^19 || ^19.0.0-rc react-dom: ^18 || ^19 || ^19.0.0-rc - code-red@1.0.4: - resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==} - codemirror-readonly-ranges@0.1.0-alpha.2: resolution: {integrity: sha512-PjT/gDAn9cm7wN/YigxgBMsxYbO52bvPQLbN8dXG9seHNANNE9KELteFaS00/+1UR4fAnCq7Ueh+KmRTlPgFTw==} peerDependencies: @@ -9792,10 +8573,6 @@ packages: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} - commander@13.1.0: - resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} - engines: {node: '>=18'} - commander@14.0.2: resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} engines: {node: '>=20'} @@ -9807,10 +8584,6 @@ packages: commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - commander@2.9.0: - resolution: {integrity: sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A==} - engines: {node: '>= 0.6.x'} - commander@7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} @@ -9819,18 +8592,10 @@ packages: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} - commander@9.5.0: - resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} - engines: {node: ^12.20.0 || >=14} - comment-json@4.3.0: resolution: {integrity: sha512-DczdmbvWLd09KATFWY0xcihOO45b32+5V34vZg1oelxqgjtGJotaLrrdFpJRLOdG6Wb031qcg4zOKgnQoBWbEw==} engines: {node: '>= 6'} - common-ancestor-path@2.0.0: - resolution: {integrity: sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==} - engines: {node: '>= 18'} - common-path-prefix@3.0.0: resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==} @@ -9844,10 +8609,6 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - concat-stream@1.6.2: - resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} - engines: {'0': node >= 0.8} - confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -9865,9 +8626,6 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} - console-table-printer@2.15.0: - resolution: {integrity: sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw==} - content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -9908,10 +8666,6 @@ packages: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} - copy-anything@4.0.5: - resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} - engines: {node: '>=18'} - copy-file@11.1.0: resolution: {integrity: sha512-X8XDzyvYaA6msMyAM575CUoygY5b44QzLcGRKsK3MFmXcOvQa518dNPLsKYwkYsn72g3EiW+LE0ytd/FlqWmyw==} engines: {node: '>=18'} @@ -9954,18 +8708,9 @@ packages: resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} engines: {node: '>= 14'} - create-langgraph@1.1.5: - resolution: {integrity: sha512-BKKkSLvzeyoZCq3rM0F/YsPlG+p3EWEg9ZV1OgQpZpf4Fc7QWsdlCf/n/USS97m9hdMJTVjLiv7DMaqfKs9JgA==} - hasBin: true - create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - create-wdio@9.27.0: - resolution: {integrity: sha512-6ot1WVks07Otj+5jDsi/NU0L3avsIA9C1mh0MtlXsR6kSvZNxwc56NH6sX3M1p+5e8Ysl777Vs4PqmgHh7LrNg==} - engines: {node: '>=12.0.0'} - hasBin: true - crelt@1.0.6: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} @@ -9997,9 +8742,6 @@ packages: css-select@5.2.2: resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} - css-shorthand-properties@1.1.2: - resolution: {integrity: sha512-C2AugXIpRGQTxaCW0N7n5jD/p5irUmCrwl03TrnMFBHDbdq44CFWR2zO7rK9xPN4Eo3pUxC4vQzQgbIpzrD1PQ==} - css-tree@2.2.1: resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} @@ -10008,9 +8750,6 @@ packages: resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} - css-value@0.0.1: - resolution: {integrity: sha512-FUV3xaJ63buRLgHrLQVlVgQnQdR4yqdLGaDu7g8CQcWjInDfM9plBTPI9FRfpahju1UBSaMckeb2/46ApS/V1Q==} - css-what@6.2.2: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} @@ -10027,9 +8766,6 @@ packages: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} - cssom@0.5.0: - resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} - cssstyle@4.6.0: resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} engines: {node: '>=18'} @@ -10240,18 +8976,6 @@ packages: dayjs@1.11.20: resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} - debounce@1.2.1: - resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} - - debug@4.3.7: - resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -10264,18 +8988,6 @@ packages: decache@4.6.2: resolution: {integrity: sha512-2LPqkLeu8XWHU8qNCS3kcF6sCcb5zIzvWaAHYSvPfwhdd7mHuah29NssMzrTYyHN4F5oFy2ko9OBYxegtU0FEw==} - decamelize@1.2.0: - resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} - engines: {node: '>=0.10.0'} - - decamelize@4.0.0: - resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} - engines: {node: '>=10'} - - decamelize@6.0.1: - resolution: {integrity: sha512-G7Cqgaelq68XHJNGlZ7lrNQyhZGsFqpwtGFexqUv4IQdjKoSYF7ipZ9UuTJZUSQXFj/XaoBLuEVIVqr8EJngEQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} @@ -10298,10 +9010,6 @@ packages: babel-plugin-macros: optional: true - deep-eql@5.0.2: - resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} - engines: {node: '>=6'} - deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -10309,10 +9017,6 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - deepmerge-ts@7.1.5: - resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} - engines: {node: '>=16.0.0'} - deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -10336,10 +9040,6 @@ packages: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} - define-lazy-prop@2.0.0: - resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} - engines: {node: '>=8'} - define-lazy-prop@3.0.0: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} engines: {node: '>=12'} @@ -10434,9 +9134,6 @@ packages: dettle@1.0.5: resolution: {integrity: sha512-ZVyjhAJ7sCe1PNXEGveObOH9AC8QvMga3HJIghHawtG7mE4K5pW9nz/vDGAr/U7a3LWgdOzEE7ac9MURnyfaTA==} - devalue@5.7.1: - resolution: {integrity: sha512-MUbZ586EgQqdRnC4yDrlod3BEdyvE4TapGYHMW2CiaW+KkkFmWEFqBUaLltEZCGi0iFXCEjRF0OjF0DV2QHjOA==} - devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -10459,9 +9156,6 @@ packages: resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} engines: {node: '>=0.3.1'} - dlv@1.1.3: - resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} - dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} @@ -10489,10 +9183,6 @@ packages: resolution: {integrity: sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==} engines: {node: '>=18'} - dotenv-expand@12.0.3: - resolution: {integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==} - engines: {node: '>=12'} - dotenv@16.4.7: resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} @@ -10609,10 +9299,6 @@ packages: sqlite3: optional: true - dset@3.1.4: - resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} - engines: {node: '>=4'} - dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -10620,9 +9306,6 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - easy-table@1.2.0: - resolution: {integrity: sha512-OFzVOv03YpvtcWGe5AayU5G2hgybsg3iqA6drU8UaoZyB9jLGMTrz9+asnLp/E+6qPh88yEI1gvyZFZ41dmgww==} - ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} @@ -10630,26 +9313,12 @@ packages: resolution: {integrity: sha512-wG99Zcfcys9fZux7Cft8BAX/YrOJLJSZ3jyYPfhZHqN2E+Ffx+QXBDsv3gubEgPtV6dTzJMSQUwk1H98/t/0wQ==} engines: {bun: '>=1', deno: '>=2', node: '>=16'} - edge-paths@3.0.5: - resolution: {integrity: sha512-sB7vSrDnFa4ezWQk9nZ/n0FdpdUuC6R1EOrlU3DL+bovcNFK28rqu2emmAUjujYEJTWIgQGqgVVWUZXMnc8iWg==} - engines: {node: '>=14.0.0'} - - edgedriver@6.3.0: - resolution: {integrity: sha512-ggEQL+oEyIcM4nP2QC3AtCQ04o4kDNefRM3hja0odvlPSnsaxiruMxEZ93v3gDCKWYW6BXUr51PPradb+3nffw==} - engines: {node: '>=20.0.0'} - hasBin: true - ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} effect@3.21.0: resolution: {integrity: sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==} - ejs@3.1.10: - resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} - engines: {node: '>=0.10.0'} - hasBin: true - electron-to-chromium@1.5.335: resolution: {integrity: sha512-q9n5T4BR4Xwa2cwbrwcsDJtHD/enpQ5S1xF1IAtdqf5AAgqDFmR/aakqH3ChFdqd/QXJhS3rnnXFtexU7rax6Q==} @@ -10666,9 +9335,6 @@ packages: embla-carousel@8.6.0: resolution: {integrity: sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==} - emmet@2.4.11: - resolution: {integrity: sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==} - emoji-mart@5.6.0: resolution: {integrity: sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==} @@ -10782,18 +9448,12 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} - es6-error@4.1.1: - resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} - esast-util-from-estree@2.0.0: resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} esast-util-from-js@2.0.1: resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==} - esbuild-plugin-tailwindcss@2.2.0: - resolution: {integrity: sha512-xzLRHuDZfbDAld+PlQkY028juyfMrYaMRsB4yLfvF3hKBna/cq3bWAHNKz2WQ2YbwbYwYh3B33N1oqBUoX7aww==} - esbuild@0.18.20: resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} engines: {node: '>=12'} @@ -10834,10 +9494,6 @@ packages: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} - escape-string-regexp@2.0.0: - resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} - engines: {node: '>=8'} - escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -10977,30 +9633,10 @@ packages: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} - execa@9.6.1: - resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} - engines: {node: ^18.19.0 || >=20.5.0} - - exit-hook@4.0.0: - resolution: {integrity: sha512-Fqs7ChZm72y40wKjOFXBKg7nJZvQJmewP5/7LtePDdnah/+FH9Hp5sgMujSCMPXlxOAW2//1jrW9pnsY7o20vQ==} - engines: {node: '>=18'} - expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} - expect-webdriverio@5.6.5: - resolution: {integrity: sha512-5ot+Apo0bEvMD/nqzWymQpgyWnOdu0kVpmahLx5T7NzUc6RyifucZ24Gsfr6F6C8yRGBhmoFh7ZeY+W9kteEBQ==} - engines: {node: '>=20'} - peerDependencies: - '@wdio/globals': ^9.0.0 - '@wdio/logger': ^9.0.0 - webdriverio: ^9.0.0 - - expect@30.3.0: - resolution: {integrity: sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - express-logging@1.1.1: resolution: {integrity: sha512-1KboYwxxCG5kwkJHR5LjFDTD1Mgl8n4PIMcCuhhd/1OqaxlC68P3QKbvvAbZVUtVgtlxEdTgSUwf6yxwzRCuuA==} engines: {node: '>= 0.10.26'} @@ -11059,9 +9695,6 @@ packages: fast-decode-uri-component@1.0.1: resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} - fast-deep-equal@2.0.1: - resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} - fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -11088,38 +9721,15 @@ packages: fast-querystring@1.1.2: resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} - fast-redact@3.5.0: - resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} - engines: {node: '>=6'} - fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - fast-sha256@1.3.0: - resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} - - fast-string-truncated-width@1.2.1: - resolution: {integrity: sha512-Q9acT/+Uu3GwGj+5w/zsGuQjh9O1TyywhIwAxHudtWrgF09nHOPrvTLhQevPbttcxjr/SNN7mJmfOw/B1bXgow==} - - fast-string-width@1.1.0: - resolution: {integrity: sha512-O3fwIVIH5gKB38QNbdg+3760ZmGz0SZMgvwJbA1b2TGXceKE6A2cOlfogh1iw8lr049zPyd7YADHy+B7U4W9bQ==} - fast-stringify@4.0.0: resolution: {integrity: sha512-lE2DIivBaLysf6hK5WH/VfMgqRbvBVHcpGVVTmA5Zi8oWIjq9YxIt6lYGdUgP1HNSXxTIat7HEIDnrSvXSeKQw==} fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - fast-wrap-ansi@0.1.6: - resolution: {integrity: sha512-HlUwET7a5gqjURj70D5jl7aC3Zmy4weA1SHUfM0JFI0Ptq987NH2TwbBFLoERhfwk+E+eaq4EK3jXoT+R3yp3w==} - - fast-xml-builder@1.1.4: - resolution: {integrity: sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==} - - fast-xml-parser@5.5.11: - resolution: {integrity: sha512-QL0eb0YbSTVWF6tTf1+LEMSgtCEjBYPpnAjoLC8SscESlAjXEIRJ7cHtLG0pLeDFaZLa4VKZLArtA/60ZS7vyA==} - hasBin: true - fastest-levenshtein@1.0.16: resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} engines: {node: '>= 4.9.1'} @@ -11181,9 +9791,6 @@ packages: file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} - filelist@1.0.6: - resolution: {integrity: sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==} - filename-reserved-regex@3.0.0: resolution: {integrity: sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -11192,10 +9799,6 @@ packages: resolution: {integrity: sha512-M45CbrJLGACfrPOkrTp3j2EcO9OBkKUYME0eiqOCa7i2poaklU0jhlIaMlr8ijLorT0uLAzrn3qXOp5684CkfA==} engines: {node: '>=12.20'} - filesize@11.0.15: - resolution: {integrity: sha512-30TpbYxQxCpi4XdVjkwXYQ37CzZltV38+P7MYroQ+4NK/Dmx9mxixFNrolzcmEIBsjT/uowC9T7kiy2+C12r1A==} - engines: {node: '>= 10.8.0'} - fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -11227,10 +9830,6 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} - find-up@6.3.0: - resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - find-up@7.0.0: resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} engines: {node: '>=18'} @@ -11239,26 +9838,13 @@ packages: resolution: {integrity: sha512-JGG8pvDi2C+JxidYdIwQDyS/CgcrIdh18cvgxcBge3wSHRQOrooMD3GlFBcmMJAN9M42SAZjDp5zv1dglJjwww==} engines: {node: '>=20'} - firefox-profile@4.7.0: - resolution: {integrity: sha512-aGApEu5bfCNbA4PGUZiRJAIU6jKmghV2UVdklXAofnNtiDjqYw0czLS46W7IfFqVKgKhFB8Ao2YoNGHY4BoIMQ==} - engines: {node: '>=18'} - hasBin: true - flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} - flat@5.0.2: - resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} - hasBin: true - flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} - flattie@1.1.1: - resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==} - engines: {node: '>=8'} - fn.name@1.1.0: resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} @@ -11274,13 +9860,6 @@ packages: debug: optional: true - fontace@0.4.1: - resolution: {integrity: sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw==} - - fontkitten@1.0.3: - resolution: {integrity: sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==} - engines: {node: '>=20'} - for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -11297,10 +9876,6 @@ packages: resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} engines: {node: '>= 14.17'} - form-data-encoder@4.1.0: - resolution: {integrity: sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==} - engines: {node: '>= 18'} - form-data@4.0.5: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} @@ -11309,10 +9884,6 @@ packages: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} - formdata-node@6.0.3: - resolution: {integrity: sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg==} - engines: {node: '>= 18'} - formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -11348,22 +9919,10 @@ packages: from2@2.3.0: resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==} - fs-extra@11.3.4: - resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==} - engines: {node: '>=14.14'} - fs-minipass@2.1.0: resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} engines: {node: '>= 8'} - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - - fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -11383,10 +9942,6 @@ packages: resolution: {integrity: sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w==} engines: {node: '>= 0.6.0'} - fx-runner@1.4.0: - resolution: {integrity: sha512-rci1g6U0rdTg6bAaBboP7XdRu01dzTAaKXxFf+PUqGuCv6Xu7o8NZdY1D5MvKGIjb6EdS1g3VlXOgksir1uGkg==} - hasBin: true - gaxios@7.1.4: resolution: {integrity: sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==} engines: {node: '>=18'} @@ -11395,18 +9950,10 @@ packages: resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} engines: {node: '>=18'} - geckodriver@6.1.0: - resolution: {integrity: sha512-ZRXLa4ZaYTTgUO4Eefw+RsQCleugU2QLb1ME7qTYxxuRj51yAhfnXaItXNs5/vUzfIaDHuZ+YnSF005hfp07nQ==} - engines: {node: '>=20.0.0'} - hasBin: true - generator-function@2.0.1: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} - generic-names@4.0.0: - resolution: {integrity: sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==} - gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -11485,10 +10032,6 @@ packages: resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} hasBin: true - giget@3.2.0: - resolution: {integrity: sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==} - hasBin: true - giscus@1.6.0: resolution: {integrity: sha512-Zrsi8r4t1LVW950keaWcsURuZUQwUaMKjvJgTCY125vkW6OiEBkatE7ScJDbpqKHdZwb///7FVC21SE3iFK3PQ==} @@ -11510,9 +10053,6 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob-to-regexp@0.4.1: - resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} - glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} hasBin: true @@ -11521,10 +10061,6 @@ packages: resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} engines: {node: 18 || 20 || >=22} - glob@8.1.0: - resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} - engines: {node: '>=12'} - global-directory@4.0.1: resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} engines: {node: '>=18'} @@ -11573,19 +10109,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - graceful-readlink@1.0.1: - resolution: {integrity: sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==} - - grapheme-splitter@1.0.4: - resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} - gray-matter@4.0.3: resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} engines: {node: '>=6.0'} - growly@1.3.0: - resolution: {integrity: sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==} - h3@1.15.11: resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==} @@ -11706,10 +10233,6 @@ packages: hastscript@9.0.1: resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} - he@1.2.0: - resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} - hasBin: true - hls.js@1.6.15: resolution: {integrity: sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==} @@ -11720,9 +10243,6 @@ packages: resolution: {integrity: sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==} engines: {node: '>=16.9.0'} - hookable@6.1.0: - resolution: {integrity: sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw==} - hosted-git-info@7.0.2: resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} engines: {node: ^16.14.0 || >=18.0.0} @@ -11747,9 +10267,6 @@ packages: resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - html-escaper@3.0.3: - resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} - html-to-image@1.11.13: resolution: {integrity: sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==} @@ -11759,9 +10276,6 @@ packages: html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} - htmlfy@0.8.1: - resolution: {integrity: sha512-xWROBw9+MEGwxpotll0h672KCaLrKKiCYzsyN8ZgL9cQbVumFnyvsk2JqiB9ELAV1GLj1GG/jxZUjV9OZZi/yQ==} - htmlparser2@10.1.0: resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} @@ -11820,10 +10334,6 @@ packages: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} - human-signals@8.0.1: - resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} - engines: {node: '>=18.18.0'} - iceberg-js@0.8.1: resolution: {integrity: sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==} engines: {node: '>=20.0.0'} @@ -11843,12 +10353,6 @@ packages: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} - icss-utils@5.1.0: - resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -11888,9 +10392,6 @@ packages: resolution: {integrity: sha512-pYkiyXVL2Mf3pozdlDGV6NAObxQx13Ae8knZk1UJRJ6uRW/ZRmTGHlQYtrsSl7ubuE5F8CD1z+s1n4RHNuTtuA==} engines: {node: '>=18'} - import-meta-resolve@4.2.0: - resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} - imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -11903,9 +10404,6 @@ packages: resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} engines: {node: '>=18'} - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -11916,10 +10414,6 @@ packages: resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - ini@4.1.3: - resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - inline-style-parser@0.1.1: resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} @@ -11932,15 +10426,6 @@ packages: peerDependencies: inquirer: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - inquirer@12.11.1: - resolution: {integrity: sha512-9VF7mrY+3OmsAfjH3yKz/pLbJ5z22E23hENKw3/LNSaA/sAt3v49bDRY+Ygct1xwuKT+U+cBfTzjCPySna69Qw==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - inquirer@8.2.7: resolution: {integrity: sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA==} engines: {node: '>=12.0.0'} @@ -11986,10 +10471,6 @@ packages: iron-webcrypto@1.2.1: resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} - is-absolute@0.1.7: - resolution: {integrity: sha512-Xi9/ZSn4NFapG8RP98iNPMOeaV3mXPisxKxzKtHVqr3g56j/fBn+yZmnxSVAA8lmZbl2J9b/a4kJvfU3hqQYgA==} - engines: {node: '>=0.10.0'} - is-alphabetical@2.0.1: resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} @@ -12042,19 +10523,11 @@ packages: is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} - is-docker@2.2.1: - resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} - engines: {node: '>=8'} - hasBin: true - is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true - is-electron@2.2.2: - resolution: {integrity: sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==} - is-error-instance@2.0.0: resolution: {integrity: sha512-5RuM+oFY0P5MRa1nXJo6IcTx9m2VyXYhRtb4h0olsi2GHci4bqZ6akHk+GmCYvDrAR9yInbiYdr2pnoqiOMw/Q==} engines: {node: '>=16.17.0'} @@ -12170,24 +10643,13 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} - is-primitive@3.0.1: - resolution: {integrity: sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w==} - engines: {node: '>=0.10.0'} - is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - is-reference@3.0.3: - resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} - is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} - is-relative@0.1.3: - resolution: {integrity: sha512-wBOr+rNM4gkAZqoLRJI4myw5WzzIdQosFAAbnvfXP5z1LyzgAI3ivOKehC5KfqlQJZoihVhirgtCBj378Eg8GA==} - engines: {node: '>=0.10.0'} - is-safe-filename@0.1.1: resolution: {integrity: sha512-4SrR7AdnY11LHfDKTZY1u6Ga3RuxZdl3YKWWShO5iyuG5h8QS4GD2tOb04peBJ5I7pXbR+CGBNEhTcwK+FzN3g==} engines: {node: '>=20'} @@ -12251,14 +10713,6 @@ packages: resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} engines: {node: '>= 0.4'} - is-what@5.5.0: - resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} - engines: {node: '>=18'} - - is-wsl@2.2.0: - resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} - engines: {node: '>=8'} - is-wsl@3.1.0: resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} engines: {node: '>=16'} @@ -12280,9 +10734,6 @@ packages: iserror@0.0.2: resolution: {integrity: sha512-oKGGrFVaWwETimP3SiWwjDeY27ovZoyZPHtxblC4hCq9fXxed/jasx+ATWFFjCVSRZng8VTMsN1nDnGo6zMBSw==} - isexe@1.1.2: - resolution: {integrity: sha512-d2eJzK691yZwPHcv1LbeAOa91yMJ9QmfTgSO1oXB65ezVhXQsxBac2vEB4bMVms9cGzaA99n6V2viHMq82VLDw==} - isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -12294,10 +10745,6 @@ packages: resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} engines: {node: '>=18'} - isexe@4.0.0: - resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==} - engines: {node: '>=20'} - isobject@3.0.1: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} @@ -12305,35 +10752,6 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jake@10.9.4: - resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==} - engines: {node: '>=10'} - hasBin: true - - jest-diff@30.3.0: - resolution: {integrity: sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-matcher-utils@30.3.0: - resolution: {integrity: sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-message-util@30.3.0: - resolution: {integrity: sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-mock@30.3.0: - resolution: {integrity: sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-regex-util@30.0.1: - resolution: {integrity: sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-util@30.3.0: - resolution: {integrity: sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -12357,15 +10775,9 @@ packages: js-levenshtein-esm@2.0.0: resolution: {integrity: sha512-1n4LEPOL4wRXY8rOQcuA7Iuaphe5xCMayvufCzlLAi+hRsnBRDbSS6XPuV58CBVJxj5D9ApFLyjQ7KzFToyHBw==} - js-tiktoken@1.0.21: - resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==} - js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-tokens@9.0.1: - resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} - js-yaml@3.14.2: resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true @@ -12406,10 +10818,6 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - json-parse-even-better-errors@3.0.2: - resolution: {integrity: sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - json-schema-ref-resolver@3.0.0: resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} @@ -12436,15 +10844,6 @@ packages: engines: {node: '>=6'} hasBin: true - jsonc-parser@2.3.1: - resolution: {integrity: sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==} - - jsonc-parser@3.3.1: - resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} - - jsonfile@6.2.0: - resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} - jsonpointer@5.0.1: resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} engines: {node: '>=0.10.0'} @@ -12456,10 +10855,6 @@ packages: resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} engines: {node: '>=12', npm: '>=6'} - jsx-slack@6.1.2: - resolution: {integrity: sha512-IQ182499drvMKIhnrYDvYKKs/0vc9hk7yz0ssowfLJp9mRtXmwaVSxoceuWz76zjG2sl1uXaOX75nQPVXNMxXA==} - engines: {node: '>=14'} - jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} @@ -12495,10 +10890,6 @@ packages: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} - kleur@3.0.3: - resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} - engines: {node: '>=6'} - kleur@4.1.5: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} @@ -12519,43 +10910,6 @@ packages: resolution: {integrity: sha512-JUshTRAfHI4/MF9dH2WupvjSXyn8JBuUEWazB8ZVJUtXutT0doDlAv1XKbZ1Pb5sMexa8FF4CFBc0iiul7gbUQ==} engines: {node: '>=20.10.0', npm: '>=10.2.3'} - langsmith@0.4.12: - resolution: {integrity: sha512-YWt0jcGvKqjUgIvd78rd4QcdMss0lUkeUaqp0UpVRq7H2yNDx8H5jOUO/laWUmaPtWGgcip0qturykXe1g9Gqw==} - peerDependencies: - '@opentelemetry/api': '*' - '@opentelemetry/exporter-trace-otlp-proto': '*' - '@opentelemetry/sdk-trace-base': '*' - openai: '*' - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - '@opentelemetry/exporter-trace-otlp-proto': - optional: true - '@opentelemetry/sdk-trace-base': - optional: true - openai: - optional: true - - langsmith@0.5.18: - resolution: {integrity: sha512-3zuZUWffTHQ+73EAwnodADtf534VNEZUpXr9jC12qyG8/IQuJET7PRsCpTb9wX2lmBspakwLUpqpj3tNm/0bVA==} - peerDependencies: - '@opentelemetry/api': '*' - '@opentelemetry/exporter-trace-otlp-proto': '*' - '@opentelemetry/sdk-trace-base': '*' - openai: '*' - ws: '>=7' - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - '@opentelemetry/exporter-trace-otlp-proto': - optional: true - '@opentelemetry/sdk-trace-base': - optional: true - openai: - optional: true - ws: - optional: true - latest-version@9.0.0: resolution: {integrity: sha512-7W0vV3rqv5tokqkBAFV1LbR7HPOWzXQDpDgEuib/aJ1jsZZx6x3c2mBI+TJhJzOhkGeaLbCKEHXEXLfirtG2JA==} engines: {node: '>=18'} @@ -12590,9 +10944,6 @@ packages: light-my-request@6.6.0: resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} - lighthouse-logger@2.0.2: - resolution: {integrity: sha512-vWl2+u5jgOQuZR55Z1WM0XDdrJT6mzMP8zHUct7xTlWhuQs+eV0g+QL0RQdFjT54zVmbhLCP8vIVpy1wGn/gCg==} - lightningcss-darwin-arm64@1.30.1: resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} engines: {node: '>= 12.0.0'} @@ -12664,19 +11015,6 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - lines-and-columns@2.0.4: - resolution: {integrity: sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - linkedom@0.18.12: - resolution: {integrity: sha512-jalJsOwIKuQJSeTvsgzPe9iJzyfVaEJiEXl+25EkKevsULHvMJzpNqwvj1jOESWdmgKDiXObyjOYwlUqG7wo1Q==} - engines: {node: '>=16'} - peerDependencies: - canvas: '>= 2' - peerDependenciesMeta: - canvas: - optional: true - linkify-it@5.0.0: resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} @@ -12692,10 +11030,6 @@ packages: resolution: {integrity: sha512-4EhoyVcXEpNlY5HJRSQpH7Rba94M8N2JmI62ePjl0lrJKXSfG0F1FAgHGxBoz/T3pe41sUEwkIRRIcaUL0/Ofw==} hasBin: true - listr2@10.2.1: - resolution: {integrity: sha512-7I5knELsJKTUjXG+A6BkKAiGkW1i25fNa/xlUl9hFtk15WbE9jndA89xu5FzQKrY5llajE1hfZZFMILXkDHk/Q==} - engines: {node: '>=22.13.0'} - listr2@9.0.5: resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==} engines: {node: '>=20.0.0'} @@ -12709,20 +11043,6 @@ packages: lit@3.3.2: resolution: {integrity: sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==} - loader-utils@3.3.1: - resolution: {integrity: sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==} - engines: {node: '>= 12.13.0'} - - local-pkg@1.1.2: - resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} - engines: {node: '>=14'} - - locate-app@2.5.0: - resolution: {integrity: sha512-xIqbzPMBYArJRmPGUZD9CzV9wOqmVtQnaAn3wrj3s6WYW0bQvPI7x+sPYUGmDTYMHefVK//zc6HEYZ1qnxIK+Q==} - - locate-character@3.0.0: - resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} - locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -12756,21 +11076,12 @@ packages: lodash._stringtopath@4.8.0: resolution: {integrity: sha512-SXL66C731p0xPDC5LZg4wI5H+dJo/EO4KTqOMwLYCH3+FmmfAKJEZCm6ohGpI+T1xwsDsJCfL4OnhorllvlTPQ==} - lodash.camelcase@4.3.0: - resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} - - lodash.clonedeep@4.5.0: - resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} - lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} - lodash.flattendeep@4.4.0: - resolution: {integrity: sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==} - lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} @@ -12801,24 +11112,15 @@ packages: lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} - lodash.pickby@4.6.0: - resolution: {integrity: sha512-AZV+GsS/6ckvPOVQPXSiFFacKvKB4kOQu6ynt9wz0F3LO4R9Ij4K1ddYsIytDpSgLz88JHd9P+oaLeej5/Sl7Q==} - lodash.throttle@4.1.1: resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} lodash.transform@4.6.0: resolution: {integrity: sha512-LO37ZnhmBVx0GvOU/caQuipEh4GN82TcWv3yHlebGDgOxbxiwwzW5Pcx2AcvpIv2WmvmSMoC492yQFNhy/l/UQ==} - lodash.union@4.6.0: - resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==} - lodash.uniqby@4.5.0: resolution: {integrity: sha512-IRt7cfTtHy6f1aRVA5n7kT8rgN3N1nH6MOWLcHfpWG2SH19E3JksLK38MktLxZDhlAjCP9jpIXkOnRXlu6oByQ==} - lodash.zip@4.2.0: - resolution: {integrity: sha512-C7IOaBBK/0gMORRBd8OETNx3kmOkgIWIPvyDpZSCTwUrpYmgZwJkjZeOD8ww4xbOUOs4/attY+pciKvadNfFbg==} - lodash@4.17.23: resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} @@ -12841,23 +11143,12 @@ packages: resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} engines: {node: '>= 12.0.0'} - loglevel-plugin-prefix@0.8.4: - resolution: {integrity: sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==} - - loglevel@1.9.2: - resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==} - engines: {node: '>= 0.6.0'} - long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} - loops@6.3.0: - resolution: {integrity: sha512-T5oc66bM90LS+yHxTv9WoYtsadJC94QIax25Avl/XCq/DTonqtNd3g1nmOfuAqDKhkZzXE8GuXmSRvSFIgq0Xg==} - engines: {node: '>=18'} - loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -12928,10 +11219,6 @@ packages: make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - many-keys-map@3.0.3: - resolution: {integrity: sha512-1DiZmDHPXMBgMRjeUtHy1q1VYmeJscHxhIAexX9z/zjRMP80+0ETuPfssi8z+kMY4DwUgsKuHqpjxgmeA9gBNA==} - engines: {node: '>=18'} - map-obj@5.0.2: resolution: {integrity: sha512-K6K2NgKnTXimT3779/4KxSvobxOtMmx1LBZ3NwRxT/MDIR3Br/fQ4Q+WCX5QxjyUR8zg5+RV9Tbf2c5pAWTD2A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -12957,9 +11244,6 @@ packages: engines: {node: '>= 20'} hasBin: true - marky@1.3.0: - resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} - math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -12970,9 +11254,6 @@ packages: maxstache@1.0.7: resolution: {integrity: sha512-53ZBxHrZM+W//5AcRVewiLpDunHnucfdzZUGz54Fnvo4tE+J3p8EL66kBrs2UhBXvYKTWckWYYWBqJqoTcenqg==} - mdast-util-definitions@6.0.0: - resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==} - mdast-util-find-and-replace@3.0.2: resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} @@ -13432,18 +11713,6 @@ packages: mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} - mocha@10.8.2: - resolution: {integrity: sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==} - engines: {node: '>= 14.0.0'} - hasBin: true - - modal@0.6.3: - resolution: {integrity: sha512-Q33DYnOpIXgrkHE2xES58vR1ONyiYxKyMtga/ail85b8jH7cnyfADkgPtRPbSzslp9G3WqBUh00/lf3txG/yDg==} - - modern-tar@0.7.6: - resolution: {integrity: sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg==} - engines: {node: '>=18.0.0'} - module-definition@6.0.1: resolution: {integrity: sha512-FeVc50FTfVVQnolk/WQT8MX+2WVcDnTGiq6Wo+/+lJ2ET1bRVi3HG3YlJUfqagNMc/kUlFSoR96AJkxGpKz13g==} engines: {node: '>=18'} @@ -13480,28 +11749,13 @@ packages: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} - mrmime@2.0.1: - resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} - engines: {node: '>=10'} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - muggle-string@0.4.1: - resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} - - multimatch@6.0.0: - resolution: {integrity: sha512-I7tSVxHGPlmPN/enE3mS1aOSo6bWBfls+3HmuEeCUBCE7gWnm3cBXCBkpurzFjVRwC6Kld8lLaZ1Iv5vOcjvcQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - multiparty@4.2.3: resolution: {integrity: sha512-Ak6EUJZuhGS8hJ3c2fY6UW5MbkGUPMBEGd13djUzoY/BHqV/gTuFWtC6IuVA7A2+v3yjBS6c4or50xhzTQZImQ==} engines: {node: '>= 0.10'} - mustache@4.2.0: - resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} - hasBin: true - mutative@1.3.0: resolution: {integrity: sha512-8MJj6URmOZAV70dpFe1YnSppRTKC4DsMkXQiBDFayLcDI4ljGokHxmpqaBQuDWa4iAxWaJJ1PS8vAmbntjjKmQ==} engines: {node: '>=14.0'} @@ -13509,10 +11763,6 @@ packages: mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} - mute-stream@2.0.0: - resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} - engines: {node: ^18.17.0 || >=20.5.0} - mux-embed@5.18.0: resolution: {integrity: sha512-y/iRQIUIyRUL+5zUHynrMMMpRlWVXfEFvSwTx7h//qIguQ4BQdQtc3qhKEdGaSikTeVuDeVC5IwP4aDuHUpjIA==} @@ -13522,10 +11772,6 @@ packages: nan@2.26.2: resolution: {integrity: sha512-0tTvBTYkt3tdGw22nrAy50x7gpbGCCFH3AFcyS5WiUu7Eu4vWlri1woE6qHBSfy11vksDqkiwjOnlR7WV8G1Hw==} - nano-spawn@2.1.0: - resolution: {integrity: sha512-yTW+2okrElHiH4fsiz/+/zc0EDo9BDDoC3iKk8dpv1GeRc9nUWzUZHx6TofMWErchhUQR8hY9/Eu1Uja9x1nqA==} - engines: {node: '>=20.17'} - nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -13546,10 +11792,6 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} - neotraverse@0.6.18: - resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==} - engines: {node: '>= 10'} - netlify-redirector@0.5.0: resolution: {integrity: sha512-4zdzIP+6muqPCuE8avnrgDJ6KW/2+UpHTRcTbMXCIRxiRmyrX+IZ4WSJGZdHPWF3WmQpXpy603XxecZ9iygN7w==} @@ -13568,12 +11810,6 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - nice-grpc-common@2.0.3: - resolution: {integrity: sha512-MEhnD3JMah0mgyivpb9hpRDbOBuXBxI/TVO+OK1h6rC97WM42HsPMR+zzRNQ0C5BqYJTw1nyWiQRD0DucO+pjQ==} - - nice-grpc@2.1.15: - resolution: {integrity: sha512-agPIt7dtQASnN5p1X3c2S5amDhWRWRT0Jp62trmpMBKSbkm87l8bG2slqxSthlrGa4AnRPG8+lFf4y8nn+Pogw==} - nlcst-to-string@4.0.0: resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==} @@ -13608,10 +11844,6 @@ packages: resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==} engines: {node: '>= 6.13.0'} - node-gyp-build-optional-packages@5.1.1: - resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} - hasBin: true - node-gyp-build@4.8.4: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true @@ -13619,9 +11851,6 @@ packages: node-mock-http@1.0.4: resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==} - node-notifier@10.0.1: - resolution: {integrity: sha512-YX7TSyDukOZ0g+gmzjB6abKu+hTGvO8+8+gIFDsRCU2t8fLV/P2unmt+LGFaIa4y64aX98Qksa97rgz4vMNeLQ==} - node-releases@2.0.37: resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} @@ -13678,10 +11907,6 @@ packages: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - npm-run-path@6.0.0: - resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} - engines: {node: '>=18'} - nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -13775,18 +12000,10 @@ packages: oniguruma-to-es@4.3.5: resolution: {integrity: sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==} - open@10.2.0: - resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} - engines: {node: '>=18'} - open@11.0.0: resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} engines: {node: '>=20'} - open@8.4.2: - resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} - engines: {node: '>=12'} - openai@5.23.2: resolution: {integrity: sha512-MQBzmTulj+MM5O8SKEk/gL8a7s5mktS9zUtAkU257WjvobGc9nKcBuVwjyEEcb9SI8a8Y2G/mzn3vm9n1Jlleg==} hasBin: true @@ -13799,18 +12016,6 @@ packages: zod: optional: true - openai@6.34.0: - resolution: {integrity: sha512-yEr2jdGf4tVFYG6ohmr3pF6VJuveP0EA/sS8TBx+4Eq5NT10alu5zg2dmxMXMgqpihRDQlFGpRt2XwsGj+Fyxw==} - hasBin: true - peerDependencies: - ws: ^8.18.0 - zod: ^3.25 || ^4.0 - peerDependenciesMeta: - ws: - optional: true - zod: - optional: true - optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -13826,10 +12031,6 @@ packages: resolution: {integrity: sha512-zBd1G8HkewNd2A8oQ8c6BN/f/c9EId7rSUueOLGu28govmUctXmM+3765GwsByv9nYUdrLqHphXlYIc86saYsg==} engines: {node: '>=18'} - os-shim@0.1.3: - resolution: {integrity: sha512-jd0cvB8qQ5uVt0lvCIexBaROw1KyKm5sbulg2fWOHjETisuCzWyt+eTZKEMs8v6HwzoGs8xik26jg7eCM6pS+A==} - engines: {node: '>= 0.4.0'} - own-keys@1.0.1: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} @@ -13873,10 +12074,6 @@ packages: resolution: {integrity: sha512-37/tPdZ3oJwHaS3gNJdenCDB3Tz26i9sjhnguBtvN0vYlRIiDNnvTWkuh+0hETV9rLPdJ3rlL3yVOYPIAnM8rw==} engines: {node: '>=18'} - p-finally@1.0.0: - resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} - engines: {node: '>=4'} - p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -13889,10 +12086,6 @@ packages: resolution: {integrity: sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==} engines: {node: '>=18'} - p-limit@7.3.0: - resolution: {integrity: sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==} - engines: {node: '>=20'} - p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} @@ -13905,34 +12098,14 @@ packages: resolution: {integrity: sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==} engines: {node: '>=18'} - p-queue@6.6.2: - resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} - engines: {node: '>=8'} - - p-queue@9.1.2: - resolution: {integrity: sha512-ktsDOALzTYTWWF1PbkNVg2rOt+HaOaMWJMUnt7T3qf5tvZ1L8dBW3tObzprBcXNMKkwj+yFSLqHso0x+UFcJXw==} - engines: {node: '>=20'} - p-reduce@3.0.0: resolution: {integrity: sha512-xsrIUgI0Kn6iyDYm9StOpOeK29XM1aboGji26+QEortiFST1hGZaUQOLhtEbqHErPpGW/aSz6allwK2qcptp0Q==} engines: {node: '>=12'} - p-retry@4.6.2: - resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} - engines: {node: '>=8'} - p-retry@6.2.1: resolution: {integrity: sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==} engines: {node: '>=16.17'} - p-retry@7.1.1: - resolution: {integrity: sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==} - engines: {node: '>=20'} - - p-timeout@3.2.0: - resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} - engines: {node: '>=8'} - p-timeout@5.1.0: resolution: {integrity: sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew==} engines: {node: '>=12'} @@ -13941,10 +12114,6 @@ packages: resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==} engines: {node: '>=14.16'} - p-timeout@7.0.1: - resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==} - engines: {node: '>=20'} - p-wait-for@5.0.2: resolution: {integrity: sha512-lwx6u1CotQYPVju77R+D0vFomni/AqRfqLmqQ8hekklqZ6gAY9rONh7lBQ0uxWMkC2AuX9b2DVAl8To0NyP1JA==} engines: {node: '>=12'} @@ -14012,10 +12181,6 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} - parse-json@7.1.1: - resolution: {integrity: sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw==} - engines: {node: '>=16'} - parse-json@8.3.0: resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} engines: {node: '>=18'} @@ -14043,9 +12208,6 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} - path-browserify@1.0.1: - resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} - path-data-parser@0.1.0: resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} @@ -14057,10 +12219,6 @@ packages: resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - path-expression-matcher@1.5.0: - resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} - engines: {node: '>=14.0.0'} - path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -14107,9 +12265,6 @@ packages: perfect-debounce@2.1.0: resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} - periscopic@3.1.0: - resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} - pg-cloudflare@1.3.0: resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} @@ -14123,11 +12278,6 @@ packages: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} - pg-node-migrations@0.0.8: - resolution: {integrity: sha512-44cMl9umOmCv0hzZyEcvjEq8Bm8u7mrzggZ06qXTJVSsMMB4j2OsjG+rSp+uzeKWyP2Vu0K9Ye2wKtjFUJwrdw==} - engines: {node: '>10.17.0'} - hasBin: true - pg-pool@3.13.0: resolution: {integrity: sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==} peerDependencies: @@ -14152,9 +12302,6 @@ packages: pgpass@1.0.5: resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} - piccolore@0.1.3: - resolution: {integrity: sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -14169,9 +12316,6 @@ packages: picoquery@2.5.0: resolution: {integrity: sha512-j1kgOFxtaCyoFCkpoYG2Oj3OdGakadO7HZ7o5CqyRazlmBekKhbDoUnNnXASE07xSY4nDImWZkrZv7toSxMi/g==} - pino-abstract-transport@2.0.0: - resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} - pino-abstract-transport@3.0.0: resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} @@ -14182,10 +12326,6 @@ packages: resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} hasBin: true - pino@9.7.0: - resolution: {integrity: sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg==} - hasBin: true - pkce-challenge@5.0.1: resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} engines: {node: '>=16.20.0'} @@ -14199,16 +12339,6 @@ packages: player.style@0.3.1: resolution: {integrity: sha512-z/T8hJGaTkHT9vdXgWdOgF37eB1FV7/j52VXQZ2lgEhpru9oT8TaUWIxp6GoxTnhPBM4X6nSbpkAHrT7UTjUKg==} - playwright-core@1.59.1: - resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} - engines: {node: '>=18'} - hasBin: true - - playwright@1.59.1: - resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==} - engines: {node: '>=18'} - hasBin: true - plist@3.1.0: resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} engines: {node: '>=10.4.0'} @@ -14235,43 +12365,10 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} - postcss-modules-extract-imports@3.1.0: - resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-modules-local-by-default@4.2.0: - resolution: {integrity: sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-modules-scope@3.2.1: - resolution: {integrity: sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-modules-values@4.0.0: - resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-modules@6.0.1: - resolution: {integrity: sha512-zyo2sAkVvuZFFy0gc2+4O+xar5dYlaVy/ebO24KT0ftk/iJevSNyPyQellsBLlnccwh7f6V6Y4GvuKRYToNgpQ==} - peerDependencies: - postcss: ^8.0.0 - postcss-selector-parser@6.0.10: resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} engines: {node: '>=4'} - postcss-selector-parser@7.1.1: - resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} - engines: {node: '>=4'} - postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} @@ -14308,15 +12405,6 @@ packages: posthog-js@1.367.0: resolution: {integrity: sha512-jWNwB8XjlVUC9PbGaIlmsyohUDMBrwf7cvLuOY3lIOmWVO3L6VxTE3GZShjxpFKQtmWcPxFbf1hcbct1YCb6xg==} - posthog-node@5.29.2: - resolution: {integrity: sha512-rI7kkF0XqDc0G1qjx+Hb4iuY9NAlL+XQNoGOpnEpRNTUcXvjY6WlsRGZ9m2whgc39emrrYdszi/YT8wZkr2xsg==} - engines: {node: ^20.20.0 || >=22.22.0} - peerDependencies: - rxjs: ^7.0.0 - peerDependenciesMeta: - rxjs: - optional: true - powershell-utils@0.1.0: resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} engines: {node: '>=20'} @@ -14346,10 +12434,6 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - pretty-format@30.3.0: - resolution: {integrity: sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - pretty-ms@9.3.0: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} @@ -14358,10 +12442,6 @@ packages: resolution: {integrity: sha512-rksPWtoZb2ZpT5OVgtmy0KHVM+Dca3iVwWY9ifwhcexfjebtgjg3wmrUt9PvJ59XIYBcknQeYHD8IAnVlh9lAw==} hasBin: true - prismjs@1.30.0: - resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} - engines: {node: '>=6'} - proc-log@6.1.0: resolution: {integrity: sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==} engines: {node: ^20.17.0 || >=22.9.0} @@ -14383,17 +12463,6 @@ packages: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} - promise-toolbox@0.21.0: - resolution: {integrity: sha512-NV8aTmpwrZv+Iys54sSFOBx3tuVaOBvvrft5PNppnxy9xpU/akHbaWIril22AB22zaPgrgwKdD0KsrM0ptUtpg==} - engines: {node: '>=6'} - - promptl-ai@0.4.12: - resolution: {integrity: sha512-8iLalee/FucFdpsci3dDUd/edsAcPlr/ebgKoRIS5KDsHC9RMcJzYY60G2/vpxrsWQeqCC8hheoKEznNmJR7Pg==} - - prompts@2.4.2: - resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} - engines: {node: '>= 6'} - prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -14485,19 +12554,10 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - proxy-from-env@2.1.0: - resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} - engines: {node: '>=10'} - ps-list@8.1.1: resolution: {integrity: sha512-OPS9kEJYVmiO48u/B9qneqhkMvgCxT+Tm28VCEJpheTpl8cJ0ffZRRNgS5mrQRTrX5yRTpaJ+hRDeefXYmmorQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - publish-browser-extension@4.0.5: - resolution: {integrity: sha512-EePAn3VIHJS/jqCuvs1NgPgoecCT8+RsES76hbgYe2Ze1dyvB0tX60C1PCrV8Z8fv56mW3E59s9Gd/GwWiw7dw==} - engines: {node: '>=18.0.0'} - hasBin: true - pump@1.0.3: resolution: {integrity: sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==} @@ -14533,9 +12593,6 @@ packages: resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} engines: {node: '>=0.6'} - quansync@0.2.11: - resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} - query-selector-shadow-dom@1.0.1: resolution: {integrity: sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==} @@ -14566,9 +12623,6 @@ packages: resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==} engines: {node: '>= 0.8'} - randombytes@2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - range-analyzer@0.1.1-alpha.2: resolution: {integrity: sha512-Ad9J4t3zljH/DHNSDthAFyQdd5DniHTaqES/oTwAFB7prV7E4u01et6WYCpqbJSmUDC05XlMMR2ycO84Hi1IvA==} @@ -14653,9 +12707,6 @@ packages: rc9@2.1.2: resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} - rc9@3.0.1: - resolution: {integrity: sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==} - rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -14832,18 +12883,10 @@ packages: resolution: {integrity: sha512-Q5hMVBYur/eQNWDdbF4/Wqqr9Bjvtrw2kjGxxBbKLbx8bVCL8gcArjTy8zDUuLGQicftpMuU0riQNcAsbtOVsw==} engines: {node: '>=20'} - read-pkg-up@10.1.0: - resolution: {integrity: sha512-aNtBq4jR8NawpKJQldrQcSW9y/d+KWH4v24HWkHljOZ7H0av+YTGANBzRh9A5pw7v/bLVsLVPpOhJ7gHNVy8lA==} - engines: {node: '>=16'} - read-pkg@10.1.0: resolution: {integrity: sha512-I8g2lArQiP78ll51UeMZojewtYgIRCKCWqZEgOO8c/uefTI+XDXvCSXu3+YNUaTNvZzobrL5+SqHjBrByRRTdg==} engines: {node: '>=20'} - read-pkg@8.1.0: - resolution: {integrity: sha512-PORM8AgzXeskHO/WEv312k9U03B8K9JSiWF/8N9sUuFjBa+9SF2u6K7VClzXwDXab51jCd8Nd36CNM+zR97ScQ==} - engines: {node: '>=16'} - read-pkg@9.0.1: resolution: {integrity: sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==} engines: {node: '>=18'} @@ -14896,10 +12939,6 @@ packages: recma-stringify@1.0.0: resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==} - recursive-readdir@2.2.3: - resolution: {integrity: sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==} - engines: {node: '>=6.0.0'} - redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} @@ -14945,9 +12984,6 @@ packages: rehype-katex@7.0.1: resolution: {integrity: sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==} - rehype-parse@9.0.1: - resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==} - rehype-raw@7.0.0: resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} @@ -14960,12 +12996,6 @@ packages: rehype-slug@6.0.0: resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==} - rehype-stringify@10.0.1: - resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==} - - rehype@13.0.2: - resolution: {integrity: sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==} - remark-breaks@4.0.0: resolution: {integrity: sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==} @@ -15003,10 +13033,6 @@ packages: remark-rehype@11.1.2: resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} - remark-smartypants@3.0.2: - resolution: {integrity: sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==} - engines: {node: '>=16.0.0'} - remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} @@ -15016,12 +13042,6 @@ packages: remove-trailing-separator@1.1.0: resolution: {integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==} - request-light@0.5.8: - resolution: {integrity: sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg==} - - request-light@0.7.0: - resolution: {integrity: sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q==} - require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -15075,9 +13095,6 @@ packages: resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==} engines: {node: '>=14.16'} - resq@1.11.0: - resolution: {integrity: sha512-G10EBz+zAAy3zUd/CDoBbXRL6ia9kOo3xRHrMDsHljI0GDkhYlyjwoCx5+3eCC4swi1uCoZQhskuJkj7Gp57Bw==} - restore-cursor@3.1.0: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} @@ -15102,9 +13119,6 @@ packages: retext-pos@5.0.0: resolution: {integrity: sha512-1g0wgyGYOKPUMNjWEJQRaF9MRy3qRqMorhW0T3CxIeMaebVjGdnAYDl3v3E46sOvsCaSljGcymXW8wO/JcWt0g==} - retext-smartypants@6.2.0: - resolution: {integrity: sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==} - retext-stringify@4.0.0: resolution: {integrity: sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==} @@ -15126,9 +13140,6 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - rgb2hex@0.2.5: - resolution: {integrity: sha512-22MOP1Rh7sAo1BZpDG6R5RFYzR2lYEgwq7HEmyW2qcsOqR2lQKmn+O//xV3YG/0rrhMC6KVX2hU+ZXuaw9a5bw==} - robust-predicates@3.0.3: resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} @@ -15166,10 +13177,6 @@ packages: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} - run-async@4.0.6: - resolution: {integrity: sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==} - engines: {node: '>=0.12.0'} - run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -15187,10 +13194,6 @@ packages: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} - safaridriver@1.0.1: - resolution: {integrity: sha512-jkg4434cYgtrIF2AeY/X0Wmd2W73cK5qIEFE3hDrrQenJH/2SDJIXGvPAigfvQTcE9+H31zkiNHbUqcihEiMRA==} - engines: {node: '>=18.0.0'} - safe-array-concat@1.1.3: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} @@ -15241,9 +13244,6 @@ packages: scroll-into-view-if-needed@3.1.0: resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} - scule@1.3.0: - resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} - section-matter@1.0.0: resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} engines: {node: '>=4'} @@ -15286,13 +13286,6 @@ packages: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} - serialize-error@12.0.0: - resolution: {integrity: sha512-ZYkZLAvKTKQXWuh5XpBw7CdbSzagarX39WyZ2H07CDLC5/KfsRGlIXV8d4+tfqX1M7916mRqR1QfNHSij+c9Pw==} - engines: {node: '>=18'} - - serialize-javascript@6.0.2: - resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} - serialize-javascript@7.0.5: resolution: {integrity: sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==} engines: {node: '>=20.0.0'} @@ -15344,10 +13337,6 @@ packages: resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==} engines: {node: '>=0.10.0'} - set-value@4.1.0: - resolution: {integrity: sha512-zTEg4HL0RwVrqcWs3ztF+x1vkxfm0lP+MQQFPiMJTKVceBwEV0A569Ou8l9IYQG8jOZdMVI1hGsc0tmeD2o/Lw==} - engines: {node: '>=11.0'} - setimmediate@1.0.5: resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} @@ -15366,12 +13355,6 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shell-quote@1.7.3: - resolution: {integrity: sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==} - - shellwords@0.1.1: - resolution: {integrity: sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==} - shiki-stream@0.1.4: resolution: {integrity: sha512-4pz6JGSDmVTTkPJ/ueixHkFAXY4ySCc+unvCaDZV7hqq/sdJZirRxgIXSuNSKgiFlGTgRR97sdu2R8K55sPsrw==} peerDependencies: @@ -15389,10 +13372,6 @@ packages: shiki@3.23.0: resolution: {integrity: sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==} - shiki@4.0.2: - resolution: {integrity: sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==} - engines: {node: '>=20'} - shimmer@1.2.1: resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} @@ -15422,21 +13401,11 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - simple-wcswidth@1.1.2: - resolution: {integrity: sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==} - - sisteransi@1.0.5: - resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - sitemap@8.0.3: resolution: {integrity: sha512-9Ew1tR2WYw8RGE2XLy7GjkusvYXy8Rg6y8TYuBuQMfIEdGcWoJpY2Wr5DzsEiL/TKCw56+YKTCCUHglorEYK+A==} engines: {node: '>=14.0.0', npm: '>=6.0.0'} hasBin: true - slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} - slash@5.1.0: resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} engines: {node: '>=14.16'} @@ -15456,10 +13425,6 @@ packages: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} - smol-toml@1.6.1: - resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} - engines: {node: '>= 18'} - socks-proxy-agent@8.0.5: resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} engines: {node: '>= 14'} @@ -15510,12 +13475,6 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} - spacetrim@0.11.59: - resolution: {integrity: sha512-lLYsktklSRKprreOm7NXReW8YiX2VBjbgmXYEziOoGf/qsJqAEACaDvoTtUOycwjpaSh+bT8eu0KrJn7UNxiCg==} - - spawn-sync@1.0.15: - resolution: {integrity: sha512-9DWBgrgYZzNghseho0JOuh+5fg9u6QWhAWa51QC7+U5rCheZ/j1DrEZnyE0RBBRqZ9uEXGPgSSM0nky6burpVw==} - spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} @@ -15543,16 +13502,9 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} - split@1.0.1: - resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} - sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - sql-template-strings@2.2.2: - resolution: {integrity: sha512-UXhXR2869FQaD+GMly8jAMCRZ94nU5KcrFetZfWEMd+LVVG6y0ExgHAhatEcKZ/wk8YcKPdi+hiD2wm75lq3/Q==} - engines: {node: '>=4.0.0'} - srvx@0.11.15: resolution: {integrity: sha512-iXsux0UcOjdvs0LCMa2Ws3WwcDUozA3JN3BquNXkaFPP7TpRqgunKdEgoZ/uwb1J6xaYHfxtz9Twlh6yzwM6Tg==} engines: {node: '>=20.16.0'} @@ -15564,20 +13516,12 @@ packages: stack-trace@0.0.10: resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} - stack-utils@2.0.6: - resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} - engines: {node: '>=10'} - stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} stackframe@1.3.4: resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} - stacktrace-parser@0.1.11: - resolution: {integrity: sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==} - engines: {node: '>=6'} - standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} @@ -15607,10 +13551,6 @@ packages: stopword@3.1.5: resolution: {integrity: sha512-OgLYGVFCNa430WOrj9tYZhQge5yg6vd6JsKredveAqEhdLVQkfrpnQIGjx0L9lLqzL4Kq4J8yNTcfQR/MpBwhg==} - stream-buffers@3.0.3: - resolution: {integrity: sha512-pqMqwQCso0PBJt2PQmDO0cFj0lyqmiwOMiMSkVtRokl7e+ZTRYgDHKnuZNbqjiJXgsg4nuqtD/zxuo9KqTp0Yw==} - engines: {node: '>= 0.10.0'} - streamdown@2.5.0: resolution: {integrity: sha512-/tTnURfIOxZK/pqJAxsfCvETG/XCJHoWnk3jq9xLcuz6CSpnjjuxSRBTTL4PKGhxiZQf0lqPxGhImdpwcZ2XwA==} peerDependencies: @@ -15627,9 +13567,6 @@ packages: string-convert@0.2.1: resolution: {integrity: sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==} - string-hash@1.1.3: - resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==} - string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -15679,10 +13616,6 @@ packages: resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} engines: {node: '>=0.10.0'} - strip-bom@5.0.0: - resolution: {integrity: sha512-p+byADHF7SzEcVnLvc/r3uognM1hUhObuHXxJcgLCfD194XAkaLbjq3Wzb0N5G2tgIjH0dgT708Z51QxMeu60A==} - engines: {node: '>=12'} - strip-dirs@3.0.0: resolution: {integrity: sha512-I0sdgcFTfKQlUPZyAqPJmSG3HLO9rWDFnxonnIbskYNM3DwFOeTNB5KzVq3dA1GdRAc/25b5Y7UO2TQfKWw4aQ==} @@ -15694,10 +13627,6 @@ packages: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} - strip-final-newline@4.0.0: - resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} - engines: {node: '>=18'} - strip-json-comments@2.0.1: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} @@ -15706,13 +13635,6 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - strip-json-comments@5.0.2: - resolution: {integrity: sha512-4X2FR3UwhNUE9G49aIsJW5hRRR3GXGTBTZRMfv568O60ojM8HcWjV/VxAxCDW3SUND33O6ZY66ZuRcdkj73q2g==} - engines: {node: '>=14.16'} - - strip-literal@3.1.0: - resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} - strip-outer@2.0.0: resolution: {integrity: sha512-A21Xsm1XzUkK0qK1ZrytDUvqsQWict2Cykhvi0fBQntGG5JSprESasEyV1EZ/4CiR5WB5KjzLTrP/bO37B0wPg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -15726,9 +13648,6 @@ packages: '@types/node': optional: true - strnum@2.2.3: - resolution: {integrity: sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==} - strtok3@7.1.1: resolution: {integrity: sha512-mKX8HA/cdBqMKUr0MMZAFssCkIGoZeSCMXgnt79yKxNFguMLVFgRe6wB+fsL0NmoHDbeyZXczy7vEPSoo3rkzg==} engines: {node: '>=16'} @@ -15762,10 +13681,6 @@ packages: engines: {npm: '>=8'} hasBin: true - superjson@2.2.6: - resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} - engines: {node: '>=16'} - supports-color@10.2.2: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} @@ -15774,10 +13689,6 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - supports-hyperlinks@3.2.0: resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==} engines: {node: '>=14.18'} @@ -15877,9 +13788,6 @@ packages: text-hex@1.0.0: resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} - thread-stream@3.1.0: - resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} - thread-stream@4.0.0: resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} engines: {node: '>=20'} @@ -15898,9 +13806,6 @@ packages: through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - tiny-inflate@1.0.3: - resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} - tinybase@7.3.5: resolution: {integrity: sha512-MhSJ/EHidpMzBvUK0S7vlsOLF3g9qbukl00zXng5x4ZKdUqHZHbcYGK463/RTG6s52Evgjsbahrlh9O4xXpH3Q==} peerDependencies: @@ -16003,10 +13908,6 @@ packages: resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} engines: {node: ^20.0.0 || >=22.0.0} - tinyrainbow@1.2.0: - resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} - engines: {node: '>=14.0.0'} - tinyrainbow@3.1.0: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} @@ -16110,9 +14011,6 @@ packages: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} engines: {node: '>=6.10'} - ts-error@1.0.6: - resolution: {integrity: sha512-tLJxacIQUM82IR7JO1UUkKlYuUTmoY9HBJAmNWFzheSlDS5SPMcNIepejHJa4BpPQLAcbRhRf3GDJzyj6rbKvA==} - ts-md5@2.0.1: resolution: {integrity: sha512-yF35FCoEOFBzOclSkMNEUbFQZuv89KEQ+5Xz03HrMSGUGB1+r+El+JiGOFwsP4p9RFNzwlrydYoTLvPOuICl9w==} engines: {node: '>=18'} @@ -16131,16 +14029,6 @@ packages: '@swc/wasm': optional: true - tsconfck@3.1.6: - resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} - engines: {node: ^18 || >=20} - hasBin: true - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} @@ -16150,10 +14038,6 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsscmp@1.0.6: - resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} - engines: {node: '>=0.6.x'} - tsx@4.21.0: resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} engines: {node: '>=18.0.0'} @@ -16175,18 +14059,6 @@ packages: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} - type-fest@0.7.1: - resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} - engines: {node: '>=8'} - - type-fest@3.13.1: - resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==} - engines: {node: '>=14.16'} - - type-fest@4.26.0: - resolution: {integrity: sha512-OduNjVJsFbifKb57UqZ2EMP1i4u64Xwow3NYXUtBbD4vIwJdQd4+xl8YDou1dlm4DVrtwT/7Ky8z8WyCULVfxw==} - engines: {node: '>=16'} - type-fest@4.41.0: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} @@ -16215,15 +14087,6 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typedarray@0.0.6: - resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - - typesafe-path@0.2.2: - resolution: {integrity: sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==} - - typescript-auto-import-cache@0.3.6: - resolution: {integrity: sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ==} - typescript-eslint@8.58.1: resolution: {integrity: sha512-gf6/oHChByg9HJvhMO1iBexJh12AqqTfnuxscMDOVqfJW3htsdRJI/GfPpHTTcyeB8cSTUY2JcZmVgoyPqcrDg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -16256,9 +14119,6 @@ packages: ufo@1.6.3: resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} - uhyphen@0.2.0: - resolution: {integrity: sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==} - uid-safe@2.1.5: resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==} engines: {node: '>= 0.8'} @@ -16271,9 +14131,6 @@ packages: resolution: {integrity: sha512-yu26mwteFYzBAot7KVMqFGCVpsF6g8wXfJzQUHvu1no3+rRRSFcSV2nKeYvNPLD2J4b08jYBDhHUjeH0ygIl9w==} hasBin: true - ultrahtml@1.6.0: - resolution: {integrity: sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==} - unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -16296,10 +14153,6 @@ packages: undici-types@7.19.2: resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} - undici@6.24.1: - resolution: {integrity: sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==} - engines: {node: '>=18.17'} - undici@7.24.8: resolution: {integrity: sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==} engines: {node: '>=20.18.1'} @@ -16319,13 +14172,6 @@ packages: unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} - unifont@0.7.4: - resolution: {integrity: sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg==} - - unimport@6.0.2: - resolution: {integrity: sha512-ZSOkrDw380w+KIPniY3smyXh2h7H9v2MNr9zejDuh239o5sdea44DRAYrv+rfUi2QGT186P2h0GPGKvy8avQ5g==} - engines: {node: '>=18.12.0'} - unist-util-find-after@5.0.0: resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} @@ -16380,10 +14226,6 @@ packages: universal-user-agent@7.0.3: resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} - universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} - unix-dgram@2.0.7: resolution: {integrity: sha512-pWaQorcdxEUBFIKjCqqIlQaOoNVmchyoaNAJ/1LwyyfK2XSxcBhgJNiSE8ZRhR0xkNGyk4xInt1G03QPoKXY5A==} engines: {node: '>=0.10.48'} @@ -16399,18 +14241,10 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} - unplugin-utils@0.3.1: - resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==} - engines: {node: '>=20.19.0'} - unplugin@2.3.11: resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} engines: {node: '>=18.12.0'} - unplugin@3.0.0: - resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==} - engines: {node: ^20.19.0 || >=22.12.0} - unstorage@1.17.5: resolution: {integrity: sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==} peerDependencies: @@ -16549,17 +14383,9 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc - userhome@1.0.1: - resolution: {integrity: sha512-5cnLm4gseXjAclKowC4IjByaGsjtAoV6PrOQOljplNB54ReUYJP8HdAFq2muHinSDAh09PPX/uXDPfdxRHvuSA==} - engines: {node: '>= 0.8.0'} - util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - uuid@10.0.0: - resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} - hasBin: true - uuid@11.1.0: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true @@ -16568,10 +14394,6 @@ packages: resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} hasBin: true - uuid@8.3.2: - resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} - hasBin: true - uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true @@ -16616,51 +14438,6 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - vite-node@6.0.0: - resolution: {integrity: sha512-oj4PVrT+pDh6GYf5wfUXkcZyekYS8kKPfLPXVl8qe324Ec6l4K2DUKNadRbZ3LQl0qGcDz+PyOo7ZAh00Y+JjQ==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - - vite@7.3.2: - resolution: {integrity: sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - jiti: '>=1.21.0' - less: ^4.0.0 - lightningcss: 1.30.1 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - 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 - vite@8.0.8: resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -16753,75 +14530,6 @@ packages: jsdom: optional: true - volar-service-css@0.0.70: - resolution: {integrity: sha512-K1qyOvBpE3rzdAv3e4/6Rv5yizrYPy5R/ne3IWCAzLBuMO4qBMV3kSqWzj6KUVe6S0AnN6wxF7cRkiaKfYMYJw==} - peerDependencies: - '@volar/language-service': ~2.4.0 - peerDependenciesMeta: - '@volar/language-service': - optional: true - - volar-service-emmet@0.0.70: - resolution: {integrity: sha512-xi5bC4m/VyE3zy/n2CXspKeDZs3qA41tHLTw275/7dNWM/RqE2z3BnDICQybHIVp/6G1iOQj5c1qXMgQC08TNg==} - peerDependencies: - '@volar/language-service': ~2.4.0 - peerDependenciesMeta: - '@volar/language-service': - optional: true - - volar-service-html@0.0.70: - resolution: {integrity: sha512-eR6vCgMdmYAo4n+gcT7DSyBQbwB8S3HZZvSagTf0sxNaD4WppMCFfpqWnkrlGStPKMZvMiejRRVmqsX9dYcTvQ==} - peerDependencies: - '@volar/language-service': ~2.4.0 - peerDependenciesMeta: - '@volar/language-service': - optional: true - - volar-service-prettier@0.0.70: - resolution: {integrity: sha512-Z6BCFSpGVCd8BPAsZ785Kce1BGlWd5ODqmqZGVuB14MJvrR4+CYz6cDy4F+igmE1gMifqfvMhdgT8Aud4M5ngg==} - peerDependencies: - '@volar/language-service': ~2.4.0 - prettier: ^2.2 || ^3.0 - peerDependenciesMeta: - '@volar/language-service': - optional: true - prettier: - optional: true - - volar-service-typescript-twoslash-queries@0.0.70: - resolution: {integrity: sha512-IdD13Z9N2Bu8EM6CM0fDV1E69olEYGHDU25X51YXmq8Y0CmJ2LNj6gOiBJgpS5JGUqFzECVhMNBW7R0sPdRTMQ==} - peerDependencies: - '@volar/language-service': ~2.4.0 - peerDependenciesMeta: - '@volar/language-service': - optional: true - - volar-service-typescript@0.0.70: - resolution: {integrity: sha512-l46Bx4cokkUedTd74ojO5H/zqHZJ8SUuyZ0IB8JN4jfRqUM3bQFBHoOwlZCyZmOeO0A3RQNkMnFclxO4c++gsg==} - peerDependencies: - '@volar/language-service': ~2.4.0 - peerDependenciesMeta: - '@volar/language-service': - optional: true - - volar-service-yaml@0.0.70: - resolution: {integrity: sha512-0c8bXDBeoATF9F6iPIlOuYTuZAC4c+yi0siQo920u7eiBJk8oQmUmg9cDUbR4+Gl++bvGP4plj3fErbJuPqdcQ==} - peerDependencies: - '@volar/language-service': ~2.4.0 - peerDependenciesMeta: - '@volar/language-service': - optional: true - - vscode-css-languageservice@6.3.10: - resolution: {integrity: sha512-eq5N9Er3fC4vA9zd9EFhyBG90wtCCuXgRSpAndaOgXMh1Wgep5lBgRIeDgjZBW9pa+332yC9+49cZMW8jcL3MA==} - - vscode-html-languageservice@5.6.2: - resolution: {integrity: sha512-ulCrSnFnfQ16YzvwnYUgEbUEl/ZG7u2eV27YhvLObSHKkb8fw1Z9cgsnUwjTEeDIdJDoTDTDpxuhQwoenoLNMg==} - - vscode-json-languageservice@4.1.8: - resolution: {integrity: sha512-0vSpg6Xd9hfV+eZAaYN63xVVMOTmJ4GgHxXnkLCh+9RsQBkWKIghzLhW2B9ebfG+LQQg8uLtsQ2aUKjTgE+QOg==} - engines: {npm: '>=7.0.0'} - vscode-jsonrpc@8.2.0: resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} engines: {node: '>=14.0.0'} @@ -16839,9 +14547,6 @@ packages: resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} hasBin: true - vscode-nls@5.2.0: - resolution: {integrity: sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==} - vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} @@ -16852,25 +14557,12 @@ packages: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} - wait-port@1.1.0: - resolution: {integrity: sha512-3e04qkoN3LxTMLakdqeWth8nih8usyg+sf1Bgdf9wwUkp05iuK1eSY/QpLvscT/+F/gA89+LpUmmgBtesbqI2Q==} - engines: {node: '>=10'} - hasBin: true - - watchpack@2.4.4: - resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==} - engines: {node: '>=10.13.0'} - wavesurfer.js@7.12.6: resolution: {integrity: sha512-zSxPgOFprtyJ31ppHQF0+E9jAmjAhi1rR36yIW6h1GOYdpRxDe6mbkYtlChqLK0Iz8ROBweiEFw2zus7tDFibA==} wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} - web-ext-run@0.2.4: - resolution: {integrity: sha512-rQicL7OwuqWdQWI33JkSXKcp7cuv1mJG8u3jRQwx/8aDsmhbTHs9ZRmNYOL+LX0wX8edIEQX8jj4bB60GoXtKA==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} @@ -16881,19 +14573,6 @@ packages: web-vitals@5.2.0: resolution: {integrity: sha512-i2z98bEmaCqSDiHEDu+gHl/dmR4Q+TxFmG3/13KkMO+o8UxQzCqWaDRCiLgEa41nlO4VpXSI0ASa1xWmO9sBlA==} - webdriver@9.27.0: - resolution: {integrity: sha512-w07ThZND48SIr0b4S7eFougYUyclmoUwdmju8yXvEJiXYjDjeYUpl8wZrYPEYRBylxpSx+sBHfEUBrPQkcTTRQ==} - engines: {node: '>=18.20.0'} - - webdriverio@9.27.0: - resolution: {integrity: sha512-Y4FbMf4bKBXpPB0lYpglzQ2GfDDe6uojmMZl85uPyrDx18NW7mqN84ZawGoIg/FRvcLaVhcOzc98WOPo725Rag==} - engines: {node: '>=18.20.0'} - peerDependencies: - puppeteer-core: '>=22.x || <=24.x' - peerDependenciesMeta: - puppeteer-core: - optional: true - webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -16941,9 +14620,6 @@ packages: when-exit@2.1.5: resolution: {integrity: sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==} - when@3.7.7: - resolution: {integrity: sha512-9lFZp/KHoqH6bPKjbWqa+3Dg/K/r2v0X/3/G2x4DBGchVS2QX2VXL3cZV994WQVnTM1/PD71Az25nAzryEUugw==} - which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -16956,18 +14632,10 @@ packages: resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} engines: {node: '>= 0.4'} - which-pm-runs@1.1.0: - resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==} - engines: {node: '>=4'} - which-typed-array@1.1.20: resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} engines: {node: '>= 0.4'} - which@1.2.4: - resolution: {integrity: sha512-zDRAqDSBudazdfM9zpiI30Fu9ve47htYXcGi3ln0wfKu2a7SmrT6F3VDoYONu//48V8Vz4TdCRNPjtvyRO3yBA==} - hasBin: true - which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -16978,11 +14646,6 @@ packages: engines: {node: ^16.13.0 || >=18.0.0} hasBin: true - which@6.0.1: - resolution: {integrity: sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==} - engines: {node: ^20.17.0 || >=22.9.0} - hasBin: true - why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} @@ -16999,12 +14662,6 @@ packages: resolution: {integrity: sha512-1lOb3qdzw6OFmOzoY0nauhLG72TpWtb5qgYPiSh/62rjc1XidBSDio2qw0pwHh17VINF217ebIkZJdFLZFn9SA==} engines: {node: '>=18'} - winreg@0.0.12: - resolution: {integrity: sha512-typ/+JRmi7RqP1NanzFULK36vczznSNN8kWVA9vIqXyv8GhghUlwhGp1Xj3Nms1FsPcNnsQrJOR10N58/nQ9hQ==} - - winston-console-format@1.0.8: - resolution: {integrity: sha512-dq7t/E0D0QRi4XIOwu6HM1+5e//WPqylH88GVjKEhQVrzGFg34MCz+G7pMJcXFBen9C0kBsu5GYgbYsE2LDwKw==} - winston-transport@4.9.0: resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} engines: {node: '>= 12.0.0'} @@ -17017,13 +14674,6 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - workerpool@6.5.1: - resolution: {integrity: sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==} - - wrap-ansi@10.0.0: - resolution: {integrity: sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==} - engines: {node: '>=20'} - wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -17075,23 +14725,10 @@ packages: utf-8-validate: optional: true - wsl-utils@0.1.0: - resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} - engines: {node: '>=18'} - wsl-utils@0.3.1: resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} engines: {node: '>=20'} - wxt@0.20.20: - resolution: {integrity: sha512-OGvOD1YEXwasjlOmfYzCGlIa88Jm9mxjM+hqx7zw+Xctg+TKjhF1bIt7vVJ1oT1t4RqvczTAcD2mUduiDltZaw==} - hasBin: true - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - eslint: - optional: true - xdg-basedir@5.1.0: resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} engines: {node: '>=12'} @@ -17100,18 +14737,10 @@ packages: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} - xml2js@0.6.2: - resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} - engines: {node: '>=4.0.0'} - xmlbuilder2@4.0.3: resolution: {integrity: sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA==} engines: {node: '>=20.0'} - xmlbuilder@11.0.1: - resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} - engines: {node: '>=4.0'} - xmlbuilder@15.1.1: resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} engines: {node: '>=8.0'} @@ -17128,9 +14757,6 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} - xxhash-wasm@1.1.0: - resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} - y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -17145,44 +14771,19 @@ packages: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} - yaml-language-server@1.20.0: - resolution: {integrity: sha512-qhjK/bzSRZ6HtTvgeFvjNPJGWdZ0+x5NREV/9XZWFjIGezew2b4r5JPy66IfOhd5OA7KeFwk1JfmEbnTvev0cA==} - hasBin: true - yaml@1.10.3: resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} engines: {node: '>= 6'} - yaml@2.7.1: - resolution: {integrity: sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==} - engines: {node: '>= 14'} - hasBin: true - yaml@2.8.3: resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} engines: {node: '>= 14.6'} hasBin: true - yargs-parser@20.2.9: - resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} - engines: {node: '>=10'} - yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} - yargs-parser@22.0.0: - resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} - engines: {node: ^20.19.0 || ^22.12.0 || >=23} - - yargs-unparser@2.0.0: - resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} - engines: {node: '>=10'} - - yargs@16.2.0: - resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} - engines: {node: '>=10'} - yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} @@ -17194,9 +14795,6 @@ packages: resolution: {integrity: sha512-PtGEvEP30p7sbIBJKUBjUnqgTVOyMURc4dLo9iNyAJnNIEz9pm88cCXF21w94Kg3k6RXkeZh5DHOGS0qEONvNQ==} engines: {node: '>=12'} - yesql@7.0.0: - resolution: {integrity: sha512-sosfr7agy4ibLM7BvXBkM6BpBmKMGuBO8DUYQEuey+QqaqrgW+2bsSg6D050ocBYIz0PuHxUyehyzEztZTU4pw==} - yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} @@ -17213,17 +14811,10 @@ packages: resolution: {integrity: sha512-/BY0AUXnS7IKO354uLLA2eRcWiqDifEbd6unXCsOxkFDAkhgUL3PH9X2bFoaU0YchnDXsF+iKleeTLJGckbXfA==} engines: {node: '>=18.19'} - yoctocolors-cjs@2.1.3: - resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} - engines: {node: '>=18'} - yoctocolors@2.1.2: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} - zip-dir@2.0.0: - resolution: {integrity: sha512-uhlsJZWz26FLYXOD6WVuq+fIcZ3aBPGo/cFdiLlv3KNwpa52IF3ISV8fLhQLiqVu5No3VhlqlgthN6gehil1Dg==} - zip-stream@6.0.1: resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} engines: {node: '>= 14'} @@ -17277,11 +14868,6 @@ packages: snapshots: - '@1natsu/wait-element@4.2.0': - dependencies: - defu: 6.1.7 - many-keys-map: 3.0.3 - '@acemir/cssom@0.9.31': {} '@ai-sdk/amazon-bedrock@4.0.93(zod@4.3.6)': @@ -17378,15 +14964,6 @@ snapshots: transitivePeerDependencies: - zod - '@aklinker1/rollup-plugin-visualizer@5.12.0(rollup@4.60.1)': - dependencies: - open: 8.4.2 - picomatch: 2.3.2 - source-map: 0.7.6 - yargs: 17.7.2 - optionalDependencies: - rollup: 4.60.1 - '@alloc/quick-lru@5.2.0': {} '@ant-design/colors@8.0.1': @@ -17478,115 +15055,6 @@ snapshots: '@asamuzakjp/nwsapi@2.3.9': {} - '@astrojs/check@0.9.8(prettier@3.8.2)(typescript@5.6.3)': - dependencies: - '@astrojs/language-server': 2.16.6(prettier@3.8.2)(typescript@5.6.3) - chokidar: 4.0.3 - kleur: 4.1.5 - typescript: 5.6.3 - yargs: 17.7.2 - transitivePeerDependencies: - - prettier - - prettier-plugin-astro - - '@astrojs/compiler@2.13.1': {} - - '@astrojs/compiler@3.0.1': {} - - '@astrojs/internal-helpers@0.8.0': - dependencies: - picomatch: 4.0.4 - - '@astrojs/language-server@2.16.6(prettier@3.8.2)(typescript@5.6.3)': - dependencies: - '@astrojs/compiler': 2.13.1 - '@astrojs/yaml2ts': 0.2.3 - '@jridgewell/sourcemap-codec': 1.5.5 - '@volar/kit': 2.4.28(typescript@5.6.3) - '@volar/language-core': 2.4.28 - '@volar/language-server': 2.4.28 - '@volar/language-service': 2.4.28 - muggle-string: 0.4.1 - tinyglobby: 0.2.16 - volar-service-css: 0.0.70(@volar/language-service@2.4.28) - volar-service-emmet: 0.0.70(@volar/language-service@2.4.28) - volar-service-html: 0.0.70(@volar/language-service@2.4.28) - volar-service-prettier: 0.0.70(@volar/language-service@2.4.28)(prettier@3.8.2) - volar-service-typescript: 0.0.70(@volar/language-service@2.4.28) - volar-service-typescript-twoslash-queries: 0.0.70(@volar/language-service@2.4.28) - volar-service-yaml: 0.0.70(@volar/language-service@2.4.28) - vscode-html-languageservice: 5.6.2 - vscode-uri: 3.1.0 - optionalDependencies: - prettier: 3.8.2 - transitivePeerDependencies: - - typescript - - '@astrojs/markdown-remark@7.1.0': - dependencies: - '@astrojs/internal-helpers': 0.8.0 - '@astrojs/prism': 4.0.1 - github-slugger: 2.0.0 - hast-util-from-html: 2.0.3 - hast-util-to-text: 4.0.2 - js-yaml: 4.1.1 - mdast-util-definitions: 6.0.0 - rehype-raw: 7.0.0 - rehype-stringify: 10.0.1 - remark-gfm: 4.0.1 - remark-parse: 11.0.0 - remark-rehype: 11.1.2 - remark-smartypants: 3.0.2 - retext-smartypants: 6.2.0 - shiki: 4.0.2 - smol-toml: 1.6.1 - unified: 11.0.5 - unist-util-remove-position: 5.0.0 - unist-util-visit: 5.1.0 - unist-util-visit-parents: 6.0.2 - vfile: 6.0.3 - transitivePeerDependencies: - - supports-color - - '@astrojs/mdx@5.0.3(astro@6.1.5(@netlify/blobs@10.7.4)(@types/node@25.6.0)(aws4fetch@1.0.20)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.1)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.6.3)(yaml@2.8.3))': - dependencies: - '@astrojs/markdown-remark': 7.1.0 - '@mdx-js/mdx': 3.1.1 - acorn: 8.16.0 - astro: 6.1.5(@netlify/blobs@10.7.4)(@types/node@25.6.0)(aws4fetch@1.0.20)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.1)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.6.3)(yaml@2.8.3) - es-module-lexer: 2.0.0 - estree-util-visit: 2.0.0 - hast-util-to-html: 9.0.5 - piccolore: 0.1.3 - rehype-raw: 7.0.0 - remark-gfm: 4.0.1 - remark-smartypants: 3.0.2 - source-map: 0.7.6 - unist-util-visit: 5.1.0 - vfile: 6.0.3 - transitivePeerDependencies: - - supports-color - - '@astrojs/prism@4.0.1': - dependencies: - prismjs: 1.30.0 - - '@astrojs/telemetry@3.3.0': - dependencies: - ci-info: 4.4.0 - debug: 4.4.3(supports-color@8.1.1) - dlv: 1.1.3 - dset: 3.1.4 - is-docker: 3.0.0 - is-wsl: 3.1.1 - which-pm-runs: 1.1.0 - transitivePeerDependencies: - - supports-color - - '@astrojs/yaml2ts@0.2.3': - dependencies: - yaml: 2.8.3 - '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 @@ -17631,7 +15099,7 @@ snapshots: '@babel/types': 7.29.0 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -17699,8 +15167,6 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/runtime@7.28.2': {} - '@babel/runtime@7.29.2': {} '@babel/template@7.28.6': @@ -17717,7 +15183,7 @@ snapshots: '@babel/parser': 7.29.2 '@babel/template': 7.28.6 '@babel/types': 7.29.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color @@ -17758,30 +15224,6 @@ snapshots: '@bugsnag/safe-json-stringify@6.1.0': {} - '@capsizecss/unpack@4.0.0': - dependencies: - fontkitten: 1.0.3 - - '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': - optional: true - - '@cbor-extract/cbor-extract-darwin-x64@2.2.2': - optional: true - - '@cbor-extract/cbor-extract-linux-arm64@2.2.2': - optional: true - - '@cbor-extract/cbor-extract-linux-arm@2.2.2': - optional: true - - '@cbor-extract/cbor-extract-linux-x64@2.2.2': - optional: true - - '@cbor-extract/cbor-extract-win32-x64@2.2.2': - optional: true - - '@cfworker/json-schema@4.1.1': {} - '@chenglou/pretext@0.0.4': {} '@chevrotain/cst-dts-gen@12.0.0': @@ -17799,29 +15241,6 @@ snapshots: '@chevrotain/utils@12.0.0': {} - '@clack/core@0.4.1': - dependencies: - picocolors: 1.1.1 - sisteransi: 1.0.5 - - '@clack/core@1.2.0': - dependencies: - fast-wrap-ansi: 0.1.6 - sisteransi: 1.0.5 - - '@clack/prompts@0.9.1': - dependencies: - '@clack/core': 0.4.1 - picocolors: 1.1.1 - sisteransi: 1.0.5 - - '@clack/prompts@1.2.0': - dependencies: - '@clack/core': 1.2.0 - fast-string-width: 1.1.0 - fast-wrap-ansi: 0.1.6 - sisteransi: 1.0.5 - '@codemirror/autocomplete@6.20.1': dependencies: '@codemirror/language': 6.12.3 @@ -17915,10 +15334,6 @@ snapshots: '@colors/colors@1.6.0': {} - '@commander-js/extra-typings@13.1.0(commander@13.1.0)': - dependencies: - commander: 13.1.0 - '@content-collections/core@0.15.0(typescript@5.9.3)': dependencies: '@standard-schema/spec': 1.1.0 @@ -17955,44 +15370,6 @@ snapshots: '@content-collections/integrations': 0.5.0(@content-collections/core@0.15.0(typescript@5.9.3)) vite: 8.0.8(@types/node@22.19.17)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - '@crabnebula/tauri-driver-darwin-arm64@2.0.9': - optional: true - - '@crabnebula/tauri-driver-darwin-x64@2.0.9': - optional: true - - '@crabnebula/tauri-driver-linux-x64-gnu@2.0.9': - optional: true - - '@crabnebula/tauri-driver-win32-x64-msvc@2.0.9': - optional: true - - '@crabnebula/tauri-driver@2.0.9': - optionalDependencies: - '@crabnebula/tauri-driver-darwin-arm64': 2.0.9 - '@crabnebula/tauri-driver-darwin-x64': 2.0.9 - '@crabnebula/tauri-driver-linux-x64-gnu': 2.0.9 - '@crabnebula/tauri-driver-win32-x64-msvc': 2.0.9 - - '@crabnebula/test-runner-backend-darwin-arm64@0.1.18': - optional: true - - '@crabnebula/test-runner-backend-darwin-x64@0.1.18': - optional: true - - '@crabnebula/test-runner-backend-linux-x64-gnu@0.1.18': - optional: true - - '@crabnebula/test-runner-backend-win32-x64-msvc@0.1.18': - optional: true - - '@crabnebula/test-runner-backend@0.1.18': - optionalDependencies: - '@crabnebula/test-runner-backend-darwin-arm64': 0.1.18 - '@crabnebula/test-runner-backend-darwin-x64': 0.1.18 - '@crabnebula/test-runner-backend-linux-x64-gnu': 0.1.18 - '@crabnebula/test-runner-backend-win32-x64-msvc': 0.1.18 - '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -18071,22 +15448,6 @@ snapshots: gonzales-pe: 4.3.0 node-source-walk: 7.0.1 - '@devicefarmer/adbkit-logcat@2.1.3': {} - - '@devicefarmer/adbkit-monkey@1.2.1': {} - - '@devicefarmer/adbkit@3.3.8': - dependencies: - '@devicefarmer/adbkit-logcat': 2.1.3 - '@devicefarmer/adbkit-monkey': 1.2.1 - bluebird: 3.7.2 - commander: 9.5.0 - debug: 4.3.7 - node-forge: 1.4.0 - split: 1.0.1 - transitivePeerDependencies: - - supports-color - '@dnd-kit/accessibility@3.1.1(react@19.2.5)': dependencies: react: 19.2.5 @@ -18178,29 +15539,6 @@ snapshots: '@electric-sql/pglite@0.3.16': {} - '@emmetio/abbreviation@2.3.3': - dependencies: - '@emmetio/scanner': 1.0.4 - - '@emmetio/css-abbreviation@2.1.8': - dependencies: - '@emmetio/scanner': 1.0.4 - - '@emmetio/css-parser@0.4.1': - dependencies: - '@emmetio/stream-reader': 2.2.0 - '@emmetio/stream-reader-utils': 0.1.0 - - '@emmetio/html-matcher@1.3.0': - dependencies: - '@emmetio/scanner': 1.0.4 - - '@emmetio/scanner@1.0.4': {} - - '@emmetio/stream-reader-utils@0.1.0': {} - - '@emmetio/stream-reader@2.2.0': {} - '@emnapi/core@1.9.2': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -18320,7 +15658,7 @@ snapshots: '@esbuild-plugins/node-resolve@0.2.2(esbuild@0.25.12)': dependencies: '@types/resolve': 1.20.6 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) esbuild: 0.25.12 escape-string-regexp: 4.0.0 resolve: 1.22.12 @@ -18715,7 +16053,7 @@ snapshots: '@eslint/config-array@0.21.2': dependencies: '@eslint/object-schema': 2.1.7 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) minimatch: 3.1.5 transitivePeerDependencies: - supports-color @@ -18731,7 +16069,7 @@ snapshots: '@eslint/eslintrc@3.3.5': dependencies: ajv: 6.14.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -18844,18 +16182,6 @@ snapshots: react: 19.2.5 react-dom: 19.2.5(react@19.2.5) - '@grpc/grpc-js@1.14.3': - dependencies: - '@grpc/proto-loader': 0.8.0 - '@js-sdsl/ordered-map': 4.4.2 - - '@grpc/proto-loader@0.8.0': - dependencies: - lodash.camelcase: 4.3.0 - long: 5.3.2 - protobufjs: 7.5.4 - yargs: 17.7.2 - '@handlewithcare/react-prosemirror@3.0.1(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.7)(react-dom@19.2.5(react@19.2.5))(react-reconciler@0.33.0(react@19.2.5))(react@19.2.5)': dependencies: classnames: 2.5.1 @@ -18937,15 +16263,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@hono/node-server@1.19.13(hono@4.12.12)': - dependencies: - hono: 4.12.12 - - '@hono/zod-validator@0.7.6(hono@4.12.12)(zod@4.3.6)': - dependencies: - hono: 4.12.12 - zod: 4.3.6 - '@hookform/resolvers@5.2.2(react-hook-form@7.72.1(react@19.2.5))': dependencies: '@standard-schema/utils': 0.3.0 @@ -19082,54 +16399,6 @@ snapshots: tar: 6.2.1 yauzl: 3.3.0 - '@inquirer/ansi@1.0.2': {} - - '@inquirer/checkbox@4.3.2(@types/node@25.6.0)': - dependencies: - '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@25.6.0) - '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@25.6.0) - yoctocolors-cjs: 2.1.3 - optionalDependencies: - '@types/node': 25.6.0 - - '@inquirer/confirm@5.1.21(@types/node@25.6.0)': - dependencies: - '@inquirer/core': 10.3.2(@types/node@25.6.0) - '@inquirer/type': 3.0.10(@types/node@25.6.0) - optionalDependencies: - '@types/node': 25.6.0 - - '@inquirer/core@10.3.2(@types/node@25.6.0)': - dependencies: - '@inquirer/ansi': 1.0.2 - '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@25.6.0) - cli-width: 4.1.0 - mute-stream: 2.0.0 - signal-exit: 4.1.0 - wrap-ansi: 6.2.0 - yoctocolors-cjs: 2.1.3 - optionalDependencies: - '@types/node': 25.6.0 - - '@inquirer/editor@4.2.23(@types/node@25.6.0)': - dependencies: - '@inquirer/core': 10.3.2(@types/node@25.6.0) - '@inquirer/external-editor': 1.0.3(@types/node@25.6.0) - '@inquirer/type': 3.0.10(@types/node@25.6.0) - optionalDependencies: - '@types/node': 25.6.0 - - '@inquirer/expand@4.0.23(@types/node@25.6.0)': - dependencies: - '@inquirer/core': 10.3.2(@types/node@25.6.0) - '@inquirer/type': 3.0.10(@types/node@25.6.0) - yoctocolors-cjs: 2.1.3 - optionalDependencies: - '@types/node': 25.6.0 - '@inquirer/external-editor@1.0.3(@types/node@22.19.17)': dependencies: chardet: 2.1.1 @@ -19137,83 +16406,6 @@ snapshots: optionalDependencies: '@types/node': 22.19.17 - '@inquirer/external-editor@1.0.3(@types/node@25.6.0)': - dependencies: - chardet: 2.1.1 - iconv-lite: 0.7.2 - optionalDependencies: - '@types/node': 25.6.0 - - '@inquirer/figures@1.0.15': {} - - '@inquirer/input@4.3.1(@types/node@25.6.0)': - dependencies: - '@inquirer/core': 10.3.2(@types/node@25.6.0) - '@inquirer/type': 3.0.10(@types/node@25.6.0) - optionalDependencies: - '@types/node': 25.6.0 - - '@inquirer/number@3.0.23(@types/node@25.6.0)': - dependencies: - '@inquirer/core': 10.3.2(@types/node@25.6.0) - '@inquirer/type': 3.0.10(@types/node@25.6.0) - optionalDependencies: - '@types/node': 25.6.0 - - '@inquirer/password@4.0.23(@types/node@25.6.0)': - dependencies: - '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@25.6.0) - '@inquirer/type': 3.0.10(@types/node@25.6.0) - optionalDependencies: - '@types/node': 25.6.0 - - '@inquirer/prompts@7.10.1(@types/node@25.6.0)': - dependencies: - '@inquirer/checkbox': 4.3.2(@types/node@25.6.0) - '@inquirer/confirm': 5.1.21(@types/node@25.6.0) - '@inquirer/editor': 4.2.23(@types/node@25.6.0) - '@inquirer/expand': 4.0.23(@types/node@25.6.0) - '@inquirer/input': 4.3.1(@types/node@25.6.0) - '@inquirer/number': 3.0.23(@types/node@25.6.0) - '@inquirer/password': 4.0.23(@types/node@25.6.0) - '@inquirer/rawlist': 4.1.11(@types/node@25.6.0) - '@inquirer/search': 3.2.2(@types/node@25.6.0) - '@inquirer/select': 4.4.2(@types/node@25.6.0) - optionalDependencies: - '@types/node': 25.6.0 - - '@inquirer/rawlist@4.1.11(@types/node@25.6.0)': - dependencies: - '@inquirer/core': 10.3.2(@types/node@25.6.0) - '@inquirer/type': 3.0.10(@types/node@25.6.0) - yoctocolors-cjs: 2.1.3 - optionalDependencies: - '@types/node': 25.6.0 - - '@inquirer/search@3.2.2(@types/node@25.6.0)': - dependencies: - '@inquirer/core': 10.3.2(@types/node@25.6.0) - '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@25.6.0) - yoctocolors-cjs: 2.1.3 - optionalDependencies: - '@types/node': 25.6.0 - - '@inquirer/select@4.4.2(@types/node@25.6.0)': - dependencies: - '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@25.6.0) - '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@25.6.0) - yoctocolors-cjs: 2.1.3 - optionalDependencies: - '@types/node': 25.6.0 - - '@inquirer/type@3.0.10(@types/node@25.6.0)': - optionalDependencies: - '@types/node': 25.6.0 - '@ioredis/commands@1.5.0': optional: true @@ -19230,216 +16422,39 @@ snapshots: dependencies: minipass: 7.1.3 - '@jest/diff-sequences@30.3.0': {} - - '@jest/expect-utils@30.3.0': - dependencies: - '@jest/get-type': 30.1.0 - - '@jest/get-type@30.1.0': {} - - '@jest/pattern@30.0.1': - dependencies: - '@types/node': 24.12.2 - jest-regex-util: 30.0.1 - - '@jest/schemas@30.0.5': - dependencies: - '@sinclair/typebox': 0.34.49 - - '@jest/types@30.3.0': - dependencies: - '@jest/pattern': 30.0.1 - '@jest/schemas': 30.0.5 - '@types/istanbul-lib-coverage': 2.0.6 - '@types/istanbul-reports': 3.0.4 - '@types/node': 24.12.2 - '@types/yargs': 17.0.35 - chalk: 4.1.2 - - '@jridgewell/gen-mapping@0.3.13': - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/remapping@2.3.5': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/source-map@0.3.11': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - optional: true - - '@jridgewell/sourcemap-codec@1.5.5': {} - - '@jridgewell/trace-mapping@0.3.31': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - - '@jridgewell/trace-mapping@0.3.9': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - - '@js-sdsl/ordered-map@4.4.2': {} - - '@jsdevtools/ono@7.1.3': {} - - '@kurkle/color@0.3.4': {} - - '@langchain/core@1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0)': - dependencies: - '@cfworker/json-schema': 4.1.1 - '@standard-schema/spec': 1.1.0 - ansi-styles: 5.2.0 - camelcase: 6.3.0 - decamelize: 1.2.0 - js-tiktoken: 1.0.21 - langsmith: 0.5.18(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0) - mustache: 4.2.0 - p-queue: 6.6.2 - uuid: 11.1.0 - zod: 4.3.6 - transitivePeerDependencies: - - '@opentelemetry/api' - - '@opentelemetry/exporter-trace-otlp-proto' - - '@opentelemetry/sdk-trace-base' - - openai - - ws - - '@langchain/langgraph-api@1.1.17(367962cb075712636ea2108ac4ab276a)': - dependencies: - '@babel/code-frame': 7.29.0 - '@hono/node-server': 1.19.13(hono@4.12.12) - '@hono/zod-validator': 0.7.6(hono@4.12.12)(zod@4.3.6) - '@langchain/core': 1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0) - '@langchain/langgraph': 1.2.8(@langchain/core@1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6) - '@langchain/langgraph-checkpoint': 1.0.1(@langchain/core@1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0)) - '@langchain/langgraph-ui': 1.1.17 - '@types/json-schema': 7.0.15 - '@typescript/vfs': 1.6.4(typescript@5.9.3) - dedent: 1.7.2(babel-plugin-macros@3.1.0) - dotenv: 16.6.1 - exit-hook: 4.0.0 - hono: 4.12.12 - langsmith: 0.4.12(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6)) - open: 10.2.0 - semver: 7.7.4 - stacktrace-parser: 0.1.11 - superjson: 2.2.6 - tsx: 4.21.0 - typescript: 5.9.3 - uuid: 10.0.0 - winston: 3.19.0 - winston-console-format: 1.0.8 - zod: 4.3.6 - optionalDependencies: - '@langchain/langgraph-sdk': 1.8.8(@langchain/core@1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - transitivePeerDependencies: - - '@opentelemetry/api' - - '@opentelemetry/exporter-trace-otlp-proto' - - '@opentelemetry/sdk-trace-base' - - babel-plugin-macros - - openai - - supports-color - - '@langchain/langgraph-checkpoint-postgres@1.0.1(@langchain/core@1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(@langchain/langgraph-checkpoint@1.0.1(@langchain/core@1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0)))': - dependencies: - '@langchain/core': 1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0) - '@langchain/langgraph-checkpoint': 1.0.1(@langchain/core@1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0)) - pg: 8.20.0 - transitivePeerDependencies: - - pg-native - - '@langchain/langgraph-checkpoint@1.0.1(@langchain/core@1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))': - dependencies: - '@langchain/core': 1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0) - uuid: 10.0.0 - - '@langchain/langgraph-cli@1.1.17(367962cb075712636ea2108ac4ab276a)': - dependencies: - '@babel/code-frame': 7.29.0 - '@commander-js/extra-typings': 13.1.0(commander@13.1.0) - '@langchain/langgraph-api': 1.1.17(367962cb075712636ea2108ac4ab276a) - chokidar: 4.0.3 - commander: 13.1.0 - create-langgraph: 1.1.5(babel-plugin-macros@3.1.0) - dedent: 1.7.2(babel-plugin-macros@3.1.0) - dotenv: 16.6.1 - execa: 9.6.1 - exit-hook: 4.0.0 - extract-zip: 2.0.1 - langsmith: 0.4.12(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6)) - open: 10.2.0 - package-manager-detector: 1.6.0 - stacktrace-parser: 0.1.11 - tar: 7.5.13 - winston: 3.19.0 - winston-console-format: 1.0.8 - yaml: 2.8.3 - zod: 4.3.6 - transitivePeerDependencies: - - '@langchain/core' - - '@langchain/langgraph' - - '@langchain/langgraph-checkpoint' - - '@langchain/langgraph-sdk' - - '@opentelemetry/api' - - '@opentelemetry/exporter-trace-otlp-proto' - - '@opentelemetry/sdk-trace-base' - - babel-plugin-macros - - openai - - supports-color - - typescript - - '@langchain/langgraph-sdk@1.8.8(@langchain/core@1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@jridgewell/gen-mapping@0.3.13': dependencies: - '@types/json-schema': 7.0.15 - p-queue: 9.1.2 - p-retry: 7.1.1 - uuid: 13.0.0 - optionalDependencies: - '@langchain/core': 1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 - '@langchain/langgraph-ui@1.1.17': + '@jridgewell/remapping@2.3.5': dependencies: - '@commander-js/extra-typings': 13.1.0(commander@13.1.0) - commander: 13.1.0 - esbuild: 0.25.12 - esbuild-plugin-tailwindcss: 2.2.0 - zod: 4.3.6 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} - '@langchain/langgraph@1.2.8(@langchain/core@1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6)': + '@jridgewell/source-map@0.3.11': dependencies: - '@langchain/core': 1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0) - '@langchain/langgraph-checkpoint': 1.0.1(@langchain/core@1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0)) - '@langchain/langgraph-sdk': 1.8.8(@langchain/core@1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@standard-schema/spec': 1.1.0 - uuid: 10.0.0 - zod: 4.3.6 - optionalDependencies: - zod-to-json-schema: 3.25.2(zod@4.3.6) - transitivePeerDependencies: - - react - - react-dom - - svelte - - vue + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + optional: true + + '@jridgewell/sourcemap-codec@1.5.5': {} - '@langchain/openai@1.4.4(@langchain/core@1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(ws@8.20.0)': + '@jridgewell/trace-mapping@0.3.31': dependencies: - '@langchain/core': 1.1.39(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0) - js-tiktoken: 1.0.21 - openai: 6.34.0(ws@8.20.0)(zod@4.3.6) - zod: 4.3.6 - transitivePeerDependencies: - - ws + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@jsdevtools/ono@7.1.3': {} + + '@kurkle/color@0.3.4': {} '@lezer/common@1.5.2': {} @@ -19613,19 +16628,6 @@ snapshots: '@lukeed/ms@2.0.2': {} - '@mapbox/node-pre-gyp@2.0.3': - dependencies: - consola: 3.4.2 - detect-libc: 2.1.2 - https-proxy-agent: 7.0.6 - node-fetch: 2.7.0 - nopt: 8.1.0 - semver: 7.6.3 - tar: 7.5.13 - transitivePeerDependencies: - - encoding - - supports-color - '@mapbox/node-pre-gyp@2.0.3(supports-color@10.2.2)': dependencies: consola: 3.4.2 @@ -19779,14 +16781,6 @@ snapshots: '@netlify/dev-utils': 4.3.0 '@netlify/runtime-utils': 2.2.0 - '@netlify/blobs@10.7.4': - dependencies: - '@netlify/dev-utils': 4.4.3 - '@netlify/otel': 5.1.5 - '@netlify/runtime-utils': 2.3.0 - transitivePeerDependencies: - - supports-color - '@netlify/blobs@10.7.4(supports-color@10.2.2)': dependencies: '@netlify/dev-utils': 4.4.3 @@ -20004,7 +16998,7 @@ snapshots: '@netlify/dev@4.16.5(aws4fetch@1.0.20)(ioredis@5.9.2)(rollup@4.60.1)(srvx@0.11.15)': dependencies: '@netlify/ai': 0.4.1 - '@netlify/blobs': 10.7.4 + '@netlify/blobs': 10.7.4(supports-color@10.2.2) '@netlify/config': 24.4.4 '@netlify/db-dev': 0.7.0 '@netlify/dev-utils': 4.4.3 @@ -20114,10 +17108,10 @@ snapshots: '@netlify/functions-dev@1.2.5(rollup@4.60.1)': dependencies: - '@netlify/blobs': 10.7.4 + '@netlify/blobs': 10.7.4(supports-color@10.2.2) '@netlify/dev-utils': 4.4.3 '@netlify/functions': 5.2.0 - '@netlify/zip-it-and-ship-it': 14.5.3(rollup@4.60.1) + '@netlify/zip-it-and-ship-it': 14.5.3(rollup@4.60.1)(supports-color@10.2.2) cron-parser: 4.9.0 decache: 4.6.2 extract-zip: 2.0.1 @@ -20288,16 +17282,6 @@ snapshots: dependencies: '@opentelemetry/api': 1.8.0 - '@netlify/otel@5.1.5': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-node': 1.30.1(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - '@netlify/otel@5.1.5(supports-color@10.2.2)': dependencies: '@opentelemetry/api': 1.9.0 @@ -20342,7 +17326,7 @@ snapshots: '@netlify/runtime@4.1.20': dependencies: - '@netlify/blobs': 10.7.4 + '@netlify/blobs': 10.7.4(supports-color@10.2.2) '@netlify/cache': 3.4.4 '@netlify/runtime-utils': 2.3.0 '@netlify/types': 2.6.0 @@ -20429,48 +17413,6 @@ snapshots: - supports-color - uploadthing - '@netlify/zip-it-and-ship-it@14.3.1(rollup@4.60.1)': - dependencies: - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 - '@netlify/binary-info': 1.0.0 - '@netlify/serverless-functions-api': 2.14.0 - '@vercel/nft': 0.29.4(rollup@4.60.1) - archiver: 7.0.1 - common-path-prefix: 3.0.0 - copy-file: 11.1.0 - es-module-lexer: 1.7.0 - esbuild: 0.27.2 - execa: 8.0.1 - fast-glob: 3.3.3 - filter-obj: 6.1.0 - find-up: 7.0.0 - is-path-inside: 4.0.0 - junk: 4.0.1 - locate-path: 7.2.0 - merge-options: 3.0.4 - minimatch: 9.0.9 - normalize-path: 3.0.0 - p-map: 7.0.3 - path-exists: 5.0.0 - precinct: 12.2.0 - require-package-name: 2.0.1 - resolve: 2.0.0-next.6 - semver: 7.7.2 - tmp-promise: 3.0.3 - toml: 3.0.0 - unixify: 1.0.0 - urlpattern-polyfill: 8.0.2 - yargs: 17.7.2 - zod: 3.25.76 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - encoding - - react-native-b4a - - rollup - - supports-color - '@netlify/zip-it-and-ship-it@14.3.1(rollup@4.60.1)(supports-color@10.2.2)': dependencies: '@babel/parser': 7.29.2 @@ -20513,48 +17455,6 @@ snapshots: - rollup - supports-color - '@netlify/zip-it-and-ship-it@14.5.3(rollup@4.60.1)': - dependencies: - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 - '@netlify/binary-info': 1.0.0 - '@netlify/serverless-functions-api': 2.14.0 - '@vercel/nft': 0.29.4(rollup@4.60.1) - archiver: 7.0.1 - common-path-prefix: 3.0.0 - copy-file: 11.1.0 - es-module-lexer: 1.7.0 - esbuild: 0.27.3 - execa: 8.0.1 - fast-glob: 3.3.3 - filter-obj: 6.1.0 - find-up: 7.0.0 - is-path-inside: 4.0.0 - junk: 4.0.1 - locate-path: 7.2.0 - merge-options: 3.0.4 - minimatch: 10.2.5 - normalize-path: 3.0.0 - p-map: 7.0.3 - path-exists: 5.0.0 - precinct: 12.2.0 - require-package-name: 2.0.1 - resolve: 2.0.0-next.6 - semver: 7.6.3 - tmp-promise: 3.0.3 - toml: 3.0.0 - unixify: 1.0.0 - urlpattern-polyfill: 8.0.2 - yargs: 17.7.2 - zod: 3.25.76 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - encoding - - react-native-b4a - - rollup - - supports-color - '@netlify/zip-it-and-ship-it@14.5.3(rollup@4.60.1)(supports-color@10.2.2)': dependencies: '@babel/parser': 7.29.2 @@ -21032,15 +17932,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation@0.203.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.203.0 - import-in-the-middle: 1.15.0 - require-in-the-middle: 7.5.2 - transitivePeerDependencies: - - supports-color - '@opentelemetry/instrumentation@0.203.0(@opentelemetry/api@1.9.0)(supports-color@10.2.2)': dependencies: '@opentelemetry/api': 1.9.0 @@ -21055,7 +17946,7 @@ snapshots: '@opentelemetry/api': 1.9.1 '@opentelemetry/api-logs': 0.203.0 import-in-the-middle: 1.15.0 - require-in-the-middle: 7.5.2 + require-in-the-middle: 7.5.2(supports-color@10.2.2) transitivePeerDependencies: - supports-color @@ -21261,8 +18152,6 @@ snapshots: dependencies: '@orama/orama': 3.1.18 - '@oslojs/encoding@1.1.0': {} - '@oxc-project/types@0.124.0': {} '@oxfmt/binding-android-arm-eabi@0.45.0': @@ -21501,10 +18390,6 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@playwright/test@1.59.1': - dependencies: - playwright: 1.59.1 - '@pnpm/config.env-replace@1.1.0': {} '@pnpm/network.ca-file@1.0.2': @@ -21519,7 +18404,7 @@ snapshots: '@pnpm/tabtab@0.5.4': dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) enquirer: 2.4.1 minimist: 1.2.8 untildify: 4.0.0 @@ -21548,10 +18433,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@promptbook/utils@0.69.5': - dependencies: - spacetrim: 0.11.59 - '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -21575,24 +18456,9 @@ snapshots: '@protobufjs/utf8@1.1.0': {} - '@puppeteer/browsers@2.13.0': - dependencies: - debug: 4.4.3(supports-color@8.1.1) - extract-zip: 2.0.1 - progress: 2.0.3 - proxy-agent: 6.5.0 - semver: 7.7.4 - tar-fs: 3.1.2 - yargs: 17.7.2 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - react-native-b4a - - supports-color - '@puppeteer/browsers@2.3.0': dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.5.0 @@ -22766,14 +19632,6 @@ snapshots: '@sentry-internal/replay-canvas': 8.55.1 '@sentry/core': 8.55.1 - '@sentry/bun@10.48.0(@opentelemetry/exporter-trace-otlp-http@0.203.0(@opentelemetry/api@1.9.1))': - dependencies: - '@sentry/core': 10.48.0 - '@sentry/node': 10.48.0(@opentelemetry/exporter-trace-otlp-http@0.203.0(@opentelemetry/api@1.9.1)) - transitivePeerDependencies: - - '@opentelemetry/exporter-trace-otlp-http' - - supports-color - '@sentry/bundler-plugin-core@5.2.0': dependencies: '@babel/core': 7.29.0 @@ -22954,58 +19812,25 @@ snapshots: '@types/hast': 3.0.4 hast-util-to-html: 9.0.5 - '@shikijs/core@4.0.2': - dependencies: - '@shikijs/primitive': 4.0.2 - '@shikijs/types': 4.0.2 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - hast-util-to-html: 9.0.5 - '@shikijs/engine-javascript@3.23.0': dependencies: '@shikijs/types': 3.23.0 '@shikijs/vscode-textmate': 10.0.2 oniguruma-to-es: 4.3.5 - '@shikijs/engine-javascript@4.0.2': - dependencies: - '@shikijs/types': 4.0.2 - '@shikijs/vscode-textmate': 10.0.2 - oniguruma-to-es: 4.3.5 - '@shikijs/engine-oniguruma@3.23.0': dependencies: '@shikijs/types': 3.23.0 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/engine-oniguruma@4.0.2': - dependencies: - '@shikijs/types': 4.0.2 - '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@3.23.0': dependencies: '@shikijs/types': 3.23.0 - '@shikijs/langs@4.0.2': - dependencies: - '@shikijs/types': 4.0.2 - - '@shikijs/primitive@4.0.2': - dependencies: - '@shikijs/types': 4.0.2 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - '@shikijs/themes@3.23.0': dependencies: '@shikijs/types': 3.23.0 - '@shikijs/themes@4.0.2': - dependencies: - '@shikijs/types': 4.0.2 - '@shikijs/transformers@3.23.0': dependencies: '@shikijs/core': 3.23.0 @@ -23016,94 +19841,23 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 - '@shikijs/types@4.0.2': - dependencies: - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - '@shikijs/vscode-textmate@10.0.2': {} - '@sinclair/typebox@0.34.49': {} + '@sinclair/typebox@0.34.49': + optional: true '@sindresorhus/is@5.6.0': {} '@sindresorhus/merge-streams@2.3.0': {} - '@sindresorhus/merge-streams@4.0.0': {} - '@sindresorhus/slugify@2.2.1': dependencies: '@sindresorhus/transliterate': 1.6.0 escape-string-regexp: 5.0.0 '@sindresorhus/transliterate@1.6.0': - dependencies: - escape-string-regexp: 5.0.0 - - '@slack/bolt@4.7.0(@types/express@5.0.6)': - dependencies: - '@slack/logger': 4.0.1 - '@slack/oauth': 3.0.5 - '@slack/socket-mode': 2.0.6 - '@slack/types': 2.20.1 - '@slack/web-api': 7.15.0 - '@types/express': 5.0.6 - axios: 1.15.0 - express: 5.2.1 - path-to-regexp: 8.4.2 - raw-body: 3.0.2 - tsscmp: 1.0.6 - transitivePeerDependencies: - - bufferutil - - debug - - supports-color - - utf-8-validate - - '@slack/logger@4.0.1': - dependencies: - '@types/node': 24.12.2 - - '@slack/oauth@3.0.5': - dependencies: - '@slack/logger': 4.0.1 - '@slack/web-api': 7.15.0 - '@types/jsonwebtoken': 9.0.10 - '@types/node': 24.12.2 - jsonwebtoken: 9.0.3 - transitivePeerDependencies: - - debug - - '@slack/socket-mode@2.0.6': - dependencies: - '@slack/logger': 4.0.1 - '@slack/web-api': 7.15.0 - '@types/node': 24.12.2 - '@types/ws': 8.18.1 - eventemitter3: 5.0.4 - ws: 8.20.0 - transitivePeerDependencies: - - bufferutil - - debug - - utf-8-validate - - '@slack/types@2.20.1': {} - - '@slack/web-api@7.15.0': - dependencies: - '@slack/logger': 4.0.1 - '@slack/types': 2.20.1 - '@types/node': 25.6.0 - '@types/retry': 0.12.0 - axios: 1.15.0 - eventemitter3: 5.0.4 - form-data: 4.0.5 - is-electron: 2.2.2 - is-stream: 2.0.1 - p-queue: 6.6.2 - p-retry: 4.6.2 - retry: 0.13.1 - transitivePeerDependencies: - - debug + dependencies: + escape-string-regexp: 5.0.0 '@smithy/eventstream-codec@4.2.13': dependencies: @@ -23236,15 +19990,6 @@ snapshots: iceberg-js: 0.8.1 tslib: 2.8.1 - '@supabase/stripe-sync-engine@0.45.0(stripe@20.4.1(@types/node@25.6.0))': - dependencies: - pg: 8.20.0 - pg-node-migrations: 0.0.8 - stripe: 20.4.1(@types/node@25.6.0) - yesql: 7.0.0 - transitivePeerDependencies: - - pg-native - '@supabase/supabase-js@2.103.0': dependencies: '@supabase/auth-js': 2.103.0 @@ -23425,13 +20170,6 @@ snapshots: tailwindcss: 4.2.2 vite: 8.0.8(@types/node@22.19.17)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - '@tailwindcss/vite@4.2.2(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': - dependencies: - '@tailwindcss/node': 4.2.2 - '@tailwindcss/oxide': 4.2.2 - tailwindcss: 4.2.2 - vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - '@tanstack/devtools-client@0.0.3': dependencies: '@tanstack/devtools-event-client': 0.3.5 @@ -24109,7 +20847,8 @@ snapshots: '@tokenizer/token@0.3.0': {} - '@tootallnate/quickjs-emscripten@0.23.0': {} + '@tootallnate/quickjs-emscripten@0.23.0': + optional: true '@transloadit/prettier-bytes@0.3.5': {} @@ -24151,15 +20890,6 @@ snapshots: '@types/aria-query@5.0.4': {} - '@types/body-parser@1.19.6': - dependencies: - '@types/connect': 3.4.38 - '@types/node': 24.12.2 - - '@types/bun@1.3.12': - dependencies: - bun-types: 1.3.12 - '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -24300,29 +21030,8 @@ snapshots: '@types/estree@1.0.8': {} - '@types/express-serve-static-core@5.1.1': - dependencies: - '@types/node': 24.12.2 - '@types/qs': 6.15.0 - '@types/range-parser': 1.2.7 - '@types/send': 1.2.1 - - '@types/express@5.0.6': - dependencies: - '@types/body-parser': 1.19.6 - '@types/express-serve-static-core': 5.1.1 - '@types/serve-static': 2.2.0 - - '@types/filesystem@0.0.36': - dependencies: - '@types/filewriter': 0.0.33 - - '@types/filewriter@0.0.33': {} - '@types/geojson@7946.0.16': {} - '@types/har-format@1.2.16': {} - '@types/hast@2.3.10': dependencies: '@types/unist': 2.0.11 @@ -24334,22 +21043,10 @@ snapshots: '@types/http-cache-semantics@4.2.0': {} - '@types/http-errors@2.0.5': {} - '@types/http-proxy@1.17.17': dependencies: '@types/node': 24.12.2 - '@types/istanbul-lib-coverage@2.0.6': {} - - '@types/istanbul-lib-report@3.0.3': - dependencies: - '@types/istanbul-lib-coverage': 2.0.6 - - '@types/istanbul-reports@3.0.4': - dependencies: - '@types/istanbul-lib-report': 3.0.3 - '@types/js-cookie@3.0.6': {} '@types/js-yaml@4.0.9': {} @@ -24360,11 +21057,6 @@ snapshots: dependencies: json5: 2.2.3 - '@types/jsonwebtoken@9.0.10': - dependencies: - '@types/ms': 2.1.0 - '@types/node': 24.12.2 - '@types/katex@0.16.8': {} '@types/linkify-it@5.0.0': {} @@ -24374,10 +21066,6 @@ snapshots: '@types/linkify-it': 5.0.0 '@types/mdurl': 2.0.0 - '@types/marked@6.0.0': - dependencies: - marked: 17.0.6 - '@types/mdast@3.0.15': dependencies: '@types/unist': 2.0.11 @@ -24391,10 +21079,6 @@ snapshots: '@types/mdx@2.0.13': {} - '@types/minimatch@3.0.5': {} - - '@types/mocha@10.0.10': {} - '@types/ms@2.1.0': {} '@types/mysql@2.15.27': @@ -24411,10 +21095,6 @@ snapshots: dependencies: undici-types: 5.26.5 - '@types/node@20.19.39': - dependencies: - undici-types: 6.21.0 - '@types/node@22.19.17': dependencies: undici-types: 6.21.0 @@ -24426,6 +21106,7 @@ snapshots: '@types/node@25.6.0': dependencies: undici-types: 7.19.2 + optional: true '@types/normalize-package-data@2.4.4': {} @@ -24443,13 +21124,10 @@ snapshots: '@types/pg@8.20.0': dependencies: - '@types/node': 25.6.0 + '@types/node': 24.12.2 pg-protocol: 1.13.0 pg-types: 2.2.0 - - '@types/qs@6.15.0': {} - - '@types/range-parser@1.2.7': {} + optional: true '@types/react-dom@19.2.3(@types/react@19.2.14)': dependencies: @@ -24461,8 +21139,6 @@ snapshots: '@types/resolve@1.20.6': {} - '@types/retry@0.12.0': {} - '@types/retry@0.12.2': {} '@types/sax@1.2.7': @@ -24471,19 +21147,6 @@ snapshots: '@types/semver@7.7.1': {} - '@types/send@1.2.1': - dependencies: - '@types/node': 24.12.2 - - '@types/serve-static@2.2.0': - dependencies: - '@types/http-errors': 2.0.5 - '@types/node': 24.12.2 - - '@types/sinonjs__fake-timers@8.1.5': {} - - '@types/stack-utils@2.0.3': {} - '@types/stopword@2.0.3': {} '@types/tedious@4.0.14': @@ -24500,22 +21163,12 @@ snapshots: '@types/use-sync-external-store@0.0.6': {} - '@types/uuid@10.0.0': {} - '@types/whatwg-mimetype@3.0.2': {} - '@types/which@2.0.2': {} - '@types/ws@8.18.1': dependencies: '@types/node': 24.12.2 - '@types/yargs-parser@21.0.3': {} - - '@types/yargs@17.0.35': - dependencies: - '@types/yargs-parser': 21.0.3 - '@types/yauzl@2.10.3': dependencies: '@types/node': 24.12.2 @@ -24541,9 +21194,9 @@ snapshots: dependencies: '@typescript-eslint/scope-manager': 8.58.1 '@typescript-eslint/types': 8.58.1 - '@typescript-eslint/typescript-estree': 8.58.1(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.58.1(supports-color@10.2.2)(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.58.1 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) eslint: 9.39.4(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: @@ -24558,15 +21211,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.58.1(typescript@5.9.3)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@5.9.3) - '@typescript-eslint/types': 8.58.1 - debug: 4.4.3(supports-color@8.1.1) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/scope-manager@8.58.1': dependencies: '@typescript-eslint/types': 8.58.1 @@ -24579,9 +21223,9 @@ snapshots: '@typescript-eslint/type-utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.58.1 - '@typescript-eslint/typescript-estree': 8.58.1(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.58.1(supports-color@10.2.2)(typescript@5.9.3) '@typescript-eslint/utils': 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) eslint: 9.39.4(jiti@2.6.1) ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 @@ -24605,27 +21249,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.58.1(typescript@5.9.3)': - dependencies: - '@typescript-eslint/project-service': 8.58.1(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@5.9.3) - '@typescript-eslint/types': 8.58.1 - '@typescript-eslint/visitor-keys': 8.58.1 - debug: 4.4.3(supports-color@8.1.1) - minimatch: 10.2.5 - semver: 7.7.4 - tinyglobby: 0.2.16 - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) '@typescript-eslint/scope-manager': 8.58.1 '@typescript-eslint/types': 8.58.1 - '@typescript-eslint/typescript-estree': 8.58.1(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.58.1(supports-color@10.2.2)(typescript@5.9.3) eslint: 9.39.4(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: @@ -24636,13 +21265,6 @@ snapshots: '@typescript-eslint/types': 8.58.1 eslint-visitor-keys: 5.0.1 - '@typescript/vfs@1.6.4(typescript@5.9.3)': - dependencies: - debug: 4.4.3(supports-color@8.1.1) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@uidotdev/usehooks@2.4.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: react: 19.2.5 @@ -24731,25 +21353,6 @@ snapshots: '@use-gesture/core': 10.3.1 react: 19.2.5 - '@vercel/nft@0.29.4(rollup@4.60.1)': - dependencies: - '@mapbox/node-pre-gyp': 2.0.3 - '@rollup/pluginutils': 5.3.0(rollup@4.60.1) - acorn: 8.16.0 - acorn-import-attributes: 1.9.5(acorn@8.16.0) - async-sema: 3.1.1 - bindings: 1.5.0 - estree-walker: 2.0.2 - glob: 10.5.0 - graceful-fs: 4.2.11 - node-gyp-build: 4.8.4 - picomatch: 4.0.4 - resolve-from: 5.0.0 - transitivePeerDependencies: - - encoding - - rollup - - supports-color - '@vercel/nft@0.29.4(rollup@4.60.1)(supports-color@10.2.2)': dependencies: '@mapbox/node-pre-gyp': 2.0.3(supports-color@10.2.2) @@ -24785,13 +21388,6 @@ snapshots: optionalDependencies: babel-plugin-react-compiler: 1.0.0 - '@vitejs/plugin-react@6.0.1(babel-plugin-react-compiler@1.0.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': - dependencies: - '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - optionalDependencies: - babel-plugin-react-compiler: 1.0.0 - '@vitest/expect@4.1.4': dependencies: '@standard-schema/spec': 1.1.0 @@ -24825,10 +21421,6 @@ snapshots: optionalDependencies: vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - '@vitest/pretty-format@2.1.9': - dependencies: - tinyrainbow: 1.2.0 - '@vitest/pretty-format@4.1.4': dependencies: tinyrainbow: 3.1.0 @@ -24838,12 +21430,6 @@ snapshots: '@vitest/utils': 4.1.4 pathe: 2.0.3 - '@vitest/snapshot@2.1.9': - dependencies: - '@vitest/pretty-format': 2.1.9 - magic-string: 0.30.21 - pathe: 1.1.2 - '@vitest/snapshot@4.1.4': dependencies: '@vitest/pretty-format': 4.1.4 @@ -24859,56 +21445,6 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 - '@volar/kit@2.4.28(typescript@5.6.3)': - dependencies: - '@volar/language-service': 2.4.28 - '@volar/typescript': 2.4.28 - typesafe-path: 0.2.2 - typescript: 5.6.3 - vscode-languageserver-textdocument: 1.0.12 - vscode-uri: 3.1.0 - - '@volar/language-core@2.4.28': - dependencies: - '@volar/source-map': 2.4.28 - - '@volar/language-server@2.4.28': - dependencies: - '@volar/language-core': 2.4.28 - '@volar/language-service': 2.4.28 - '@volar/typescript': 2.4.28 - path-browserify: 1.0.1 - request-light: 0.7.0 - vscode-languageserver: 9.0.1 - vscode-languageserver-protocol: 3.17.5 - vscode-languageserver-textdocument: 1.0.12 - vscode-uri: 3.1.0 - - '@volar/language-service@2.4.28': - dependencies: - '@volar/language-core': 2.4.28 - vscode-languageserver-protocol: 3.17.5 - vscode-languageserver-textdocument: 1.0.12 - vscode-uri: 3.1.0 - - '@volar/source-map@2.4.28': {} - - '@volar/typescript@2.4.28': - dependencies: - '@volar/language-core': 2.4.28 - path-browserify: 1.0.1 - vscode-uri: 3.1.0 - - '@vscode/emmet-helper@2.11.0': - dependencies: - emmet: 2.4.11 - jsonc-parser: 2.3.1 - vscode-languageserver-textdocument: 1.0.12 - vscode-languageserver-types: 3.17.5 - vscode-uri: 3.1.0 - - '@vscode/l10n@0.0.18': {} - '@vue/compiler-core@3.5.32': dependencies: '@babel/parser': 7.29.2 @@ -24946,192 +21482,6 @@ snapshots: react: 19.2.5 wavesurfer.js: 7.12.6 - '@wdio/cli@9.27.0(@types/node@25.6.0)(expect-webdriverio@5.6.5)(puppeteer-core@22.15.0)': - dependencies: - '@vitest/snapshot': 2.1.9 - '@wdio/config': 9.27.0 - '@wdio/globals': 9.27.0(expect-webdriverio@5.6.5)(webdriverio@9.27.0(puppeteer-core@22.15.0)) - '@wdio/logger': 9.18.0 - '@wdio/protocols': 9.27.0 - '@wdio/types': 9.27.0 - '@wdio/utils': 9.27.0 - async-exit-hook: 2.0.1 - chalk: 5.6.2 - chokidar: 4.0.3 - create-wdio: 9.27.0(@types/node@25.6.0) - dotenv: 17.4.2 - import-meta-resolve: 4.2.0 - lodash.flattendeep: 4.4.0 - lodash.pickby: 4.6.0 - lodash.union: 4.6.0 - read-pkg-up: 10.1.0 - tsx: 4.21.0 - webdriverio: 9.27.0(puppeteer-core@22.15.0) - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - bare-abort-controller - - bare-buffer - - bufferutil - - expect-webdriverio - - puppeteer-core - - react-native-b4a - - supports-color - - utf-8-validate - - '@wdio/config@9.27.0': - dependencies: - '@wdio/logger': 9.18.0 - '@wdio/types': 9.27.0 - '@wdio/utils': 9.27.0 - deepmerge-ts: 7.1.5 - glob: 10.5.0 - import-meta-resolve: 4.2.0 - jiti: 2.6.1 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - react-native-b4a - - supports-color - - '@wdio/dot-reporter@9.27.0': - dependencies: - '@wdio/reporter': 9.27.0 - '@wdio/types': 9.27.0 - chalk: 5.6.2 - - '@wdio/globals@9.27.0(expect-webdriverio@5.6.5)(webdriverio@9.27.0(puppeteer-core@22.15.0))': - dependencies: - expect-webdriverio: 5.6.5(@wdio/globals@9.27.0)(@wdio/logger@9.18.0)(webdriverio@9.27.0(puppeteer-core@22.15.0)) - webdriverio: 9.27.0(puppeteer-core@22.15.0) - - '@wdio/local-runner@9.27.0(@wdio/globals@9.27.0)(webdriverio@9.27.0(puppeteer-core@22.15.0))': - dependencies: - '@types/node': 20.19.39 - '@wdio/logger': 9.18.0 - '@wdio/repl': 9.16.2 - '@wdio/runner': 9.27.0(expect-webdriverio@5.6.5)(webdriverio@9.27.0(puppeteer-core@22.15.0)) - '@wdio/types': 9.27.0 - '@wdio/xvfb': 9.27.0 - exit-hook: 4.0.0 - expect-webdriverio: 5.6.5(@wdio/globals@9.27.0)(@wdio/logger@9.18.0)(webdriverio@9.27.0(puppeteer-core@22.15.0)) - split2: 4.2.0 - stream-buffers: 3.0.3 - transitivePeerDependencies: - - '@wdio/globals' - - bare-abort-controller - - bare-buffer - - bufferutil - - react-native-b4a - - supports-color - - utf-8-validate - - webdriverio - - '@wdio/logger@9.18.0': - dependencies: - chalk: 5.6.2 - loglevel: 1.9.2 - loglevel-plugin-prefix: 0.8.4 - safe-regex2: 5.1.0 - strip-ansi: 7.2.0 - - '@wdio/mocha-framework@9.27.0': - dependencies: - '@types/mocha': 10.0.10 - '@types/node': 20.19.39 - '@wdio/logger': 9.18.0 - '@wdio/types': 9.27.0 - '@wdio/utils': 9.27.0 - mocha: 10.8.2 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - react-native-b4a - - supports-color - - '@wdio/protocols@9.27.0': {} - - '@wdio/repl@9.16.2': - dependencies: - '@types/node': 20.19.39 - - '@wdio/reporter@9.27.0': - dependencies: - '@types/node': 20.19.39 - '@wdio/logger': 9.18.0 - '@wdio/types': 9.27.0 - diff: 8.0.4 - object-inspect: 1.13.4 - - '@wdio/runner@9.27.0(expect-webdriverio@5.6.5)(webdriverio@9.27.0(puppeteer-core@22.15.0))': - dependencies: - '@types/node': 20.19.39 - '@wdio/config': 9.27.0 - '@wdio/dot-reporter': 9.27.0 - '@wdio/globals': 9.27.0(expect-webdriverio@5.6.5)(webdriverio@9.27.0(puppeteer-core@22.15.0)) - '@wdio/logger': 9.18.0 - '@wdio/types': 9.27.0 - '@wdio/utils': 9.27.0 - deepmerge-ts: 7.1.5 - expect-webdriverio: 5.6.5(@wdio/globals@9.27.0)(@wdio/logger@9.18.0)(webdriverio@9.27.0(puppeteer-core@22.15.0)) - webdriver: 9.27.0 - webdriverio: 9.27.0(puppeteer-core@22.15.0) - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - bufferutil - - react-native-b4a - - supports-color - - utf-8-validate - - '@wdio/spec-reporter@9.27.0': - dependencies: - '@wdio/reporter': 9.27.0 - '@wdio/types': 9.27.0 - chalk: 5.6.2 - easy-table: 1.2.0 - pretty-ms: 9.3.0 - - '@wdio/types@9.27.0': - dependencies: - '@types/node': 20.19.39 - - '@wdio/utils@9.27.0': - dependencies: - '@puppeteer/browsers': 2.13.0 - '@wdio/logger': 9.18.0 - '@wdio/types': 9.27.0 - decamelize: 6.0.1 - deepmerge-ts: 7.1.5 - edgedriver: 6.3.0 - geckodriver: 6.1.0 - get-port: 7.2.0 - import-meta-resolve: 4.2.0 - locate-app: 2.5.0 - mitt: 3.0.1 - safaridriver: 1.0.1 - split2: 4.2.0 - wait-port: 1.1.0 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - react-native-b4a - - supports-color - - '@wdio/xvfb@9.27.0': - dependencies: - '@wdio/logger': 9.18.0 - - '@webext-core/fake-browser@1.3.4': - dependencies: - lodash.merge: 4.6.2 - - '@webext-core/isolated-element@1.1.5': - dependencies: - is-potential-custom-element-name: 1.0.1 - - '@webext-core/match-patterns@1.0.3': {} - '@whatwg-node/disposablestack@0.0.6': dependencies: '@whatwg-node/promise-helpers': 1.3.2 @@ -25157,29 +21507,9 @@ snapshots: dependencies: '@envelop/instrumentation': 1.0.0 '@whatwg-node/disposablestack': 0.0.6 - '@whatwg-node/fetch': 0.10.13 - '@whatwg-node/promise-helpers': 1.3.2 - tslib: 2.8.1 - - '@wxt-dev/browser@0.1.40': - dependencies: - '@types/filesystem': 0.0.36 - '@types/har-format': 1.2.16 - - '@wxt-dev/module-react@1.2.2(babel-plugin-react-compiler@1.0.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(wxt@0.20.20(@types/node@25.6.0)(eslint@9.39.4(jiti@2.6.1))(jiti@2.6.1)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': - dependencies: - '@vitejs/plugin-react': 6.0.1(babel-plugin-react-compiler@1.0.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - wxt: 0.20.20(@types/node@25.6.0)(eslint@9.39.4(jiti@2.6.1))(jiti@2.6.1)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - transitivePeerDependencies: - - '@rolldown/plugin-babel' - - babel-plugin-react-compiler - - '@wxt-dev/storage@1.2.8': - dependencies: - '@wxt-dev/browser': 0.1.40 - async-mutex: 0.5.0 - dequal: 2.0.3 + '@whatwg-node/fetch': 0.10.13 + '@whatwg-node/promise-helpers': 1.3.2 + tslib: 2.8.1 '@xhmikosr/archive-type@6.0.1': dependencies: @@ -25261,12 +21591,8 @@ snapshots: react: 19.2.5 solid-js: 1.9.10 - '@zip.js/zip.js@2.8.26': {} - abbrev@3.0.1: {} - abort-controller-x@0.5.0: {} - abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -25292,11 +21618,9 @@ snapshots: acorn@8.16.0: {} - adm-zip@0.5.17: {} - agent-base@6.0.2: dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color @@ -25327,10 +21651,6 @@ snapshots: '@opentelemetry/api': 1.9.0 zod: 4.3.6 - ajv-draft-04@1.0.0(ajv@8.18.0): - optionalDependencies: - ajv: 8.18.0 - ajv-errors@3.0.0(ajv@8.18.0): dependencies: ajv: 8.18.0 @@ -25506,21 +21826,15 @@ snapshots: dependencies: dequal: 2.0.3 - aria-query@5.3.2: {} - array-buffer-byte-length@1.0.2: dependencies: call-bound: 1.0.4 is-array-buffer: 3.0.5 - array-differ@4.0.0: {} - array-iterate@2.0.1: {} array-timsort@1.0.3: {} - array-union@3.0.1: {} - array.prototype.flatmap@1.3.3: dependencies: call-bind: 1.0.9 @@ -25549,118 +21863,12 @@ snapshots: ast-types@0.13.4: dependencies: tslib: 2.8.1 + optional: true astring@1.9.0: {} - astro-seo@1.1.0(prettier@3.8.2)(typescript@5.6.3): - dependencies: - '@astrojs/check': 0.9.8(prettier@3.8.2)(typescript@5.6.3) - transitivePeerDependencies: - - prettier - - prettier-plugin-astro - - typescript - - astro@6.1.5(@netlify/blobs@10.7.4)(@types/node@25.6.0)(aws4fetch@1.0.20)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.1)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.6.3)(yaml@2.8.3): - dependencies: - '@astrojs/compiler': 3.0.1 - '@astrojs/internal-helpers': 0.8.0 - '@astrojs/markdown-remark': 7.1.0 - '@astrojs/telemetry': 3.3.0 - '@capsizecss/unpack': 4.0.0 - '@clack/prompts': 1.2.0 - '@oslojs/encoding': 1.1.0 - '@rollup/pluginutils': 5.3.0(rollup@4.60.1) - aria-query: 5.3.2 - axobject-query: 4.1.0 - ci-info: 4.4.0 - clsx: 2.1.1 - common-ancestor-path: 2.0.0 - cookie: 1.1.1 - devalue: 5.7.1 - diff: 8.0.4 - dset: 3.1.4 - es-module-lexer: 2.0.0 - esbuild: 0.27.7 - flattie: 1.1.1 - fontace: 0.4.1 - github-slugger: 2.0.0 - html-escaper: 3.0.3 - http-cache-semantics: 4.2.0 - js-yaml: 4.1.1 - magic-string: 0.30.21 - magicast: 0.5.2 - mrmime: 2.0.1 - neotraverse: 0.6.18 - obug: 2.1.1 - p-limit: 7.3.0 - p-queue: 9.1.2 - package-manager-detector: 1.6.0 - piccolore: 0.1.3 - picomatch: 4.0.4 - rehype: 13.0.2 - semver: 7.7.4 - shiki: 4.0.2 - smol-toml: 1.6.1 - svgo: 4.0.1 - tinyclip: 0.1.12 - tinyexec: 1.1.1 - tinyglobby: 0.2.16 - tsconfck: 3.1.6(typescript@5.6.3) - ultrahtml: 1.6.0 - unifont: 0.7.4 - unist-util-visit: 5.1.0 - unstorage: 1.17.5(@netlify/blobs@10.7.4)(aws4fetch@1.0.20)(ioredis@5.9.2) - vfile: 6.0.3 - vite: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - vitefu: 1.1.3(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - xxhash-wasm: 1.1.0 - yargs-parser: 22.0.0 - zod: 4.3.6 - optionalDependencies: - sharp: 0.34.5 - transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@deno/kv' - - '@netlify/blobs' - - '@planetscale/database' - - '@types/node' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' - - aws4fetch - - db0 - - idb-keyval - - ioredis - - jiti - - less - - lightningcss - - rollup - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - typescript - - uploadthing - - yaml - - async-exit-hook@2.0.1: {} - async-function@1.0.0: {} - async-mutex@0.5.0: - dependencies: - tslib: 2.8.1 - async-sema@3.1.1: {} async@3.2.6: {} @@ -25704,16 +21912,6 @@ snapshots: transitivePeerDependencies: - debug - axios@1.15.0: - dependencies: - follow-redirects: 1.15.11(debug@4.4.3) - form-data: 4.0.5 - proxy-from-env: 2.1.0 - transitivePeerDependencies: - - debug - - axobject-query@4.1.0: {} - b4a@1.8.0: {} babel-dead-code-elimination@1.0.12: @@ -25781,7 +21979,8 @@ snapshots: baseline-browser-mapping@2.10.18: {} - basic-ftp@5.2.2: {} + basic-ftp@5.2.2: + optional: true before-after-hook@4.0.0: {} @@ -25820,13 +22019,11 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - bluebird@3.7.2: {} - body-parser@2.2.2: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) http-errors: 2.0.1 iconv-lite: 0.7.2 on-finished: 2.4.1 @@ -25866,8 +22063,6 @@ snapshots: dependencies: fill-range: 7.1.1 - browser-stdout@1.3.1: {} - browserslist@4.28.2: dependencies: baseline-browser-mapping: 2.10.18 @@ -25897,6 +22092,7 @@ snapshots: bun-types@1.3.12: dependencies: '@types/node': 24.12.2 + optional: true bundle-name@4.1.0: dependencies: @@ -25923,27 +22119,6 @@ snapshots: optionalDependencies: magicast: 0.5.2 - c12@3.3.4(magicast@0.5.2): - dependencies: - chokidar: 5.0.0 - confbox: 0.2.4 - defu: 6.1.7 - dotenv: 17.4.2 - exsolve: 1.0.8 - giget: 3.2.0 - jiti: 2.6.1 - ohash: 2.0.11 - pathe: 2.0.3 - perfect-debounce: 2.1.0 - pkg-types: 2.3.0 - rc9: 3.0.1 - optionalDependencies: - magicast: 0.5.2 - - cac@6.7.14: {} - - cac@7.0.0: {} - cacheable-lookup@7.0.0: {} cacheable-request@10.2.14: @@ -25977,8 +22152,6 @@ snapshots: callsites@3.1.0: {} - camelcase@6.3.0: {} - camelcase@8.0.0: {} caniuse-lite@1.0.30001787: {} @@ -25987,22 +22160,6 @@ snapshots: dependencies: custom-media-element: 1.4.6 - cbor-extract@2.2.2: - dependencies: - node-gyp-build-optional-packages: 5.1.1 - optionalDependencies: - '@cbor-extract/cbor-extract-darwin-arm64': 2.2.2 - '@cbor-extract/cbor-extract-darwin-x64': 2.2.2 - '@cbor-extract/cbor-extract-linux-arm': 2.2.2 - '@cbor-extract/cbor-extract-linux-arm64': 2.2.2 - '@cbor-extract/cbor-extract-linux-x64': 2.2.2 - '@cbor-extract/cbor-extract-win32-x64': 2.2.2 - optional: true - - cbor-x@1.6.4: - optionalDependencies: - cbor-extract: 2.2.2 - ccount@2.0.1: {} ce-la-react@0.3.2(react@19.2.5): @@ -26094,15 +22251,6 @@ snapshots: chroma-js@3.2.0: {} - chrome-launcher@1.2.0: - dependencies: - '@types/node': 24.12.2 - escape-string-regexp: 4.0.0 - is-wsl: 2.2.0 - lighthouse-logger: 2.0.2 - transitivePeerDependencies: - - supports-color - chromium-bidi@0.6.3(devtools-protocol@0.0.1312386): dependencies: devtools-protocol: 0.0.1312386 @@ -26158,14 +22306,6 @@ snapshots: cli-width@3.0.0: {} - cli-width@4.1.0: {} - - cliui@7.0.4: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -26193,14 +22333,6 @@ snapshots: - '@types/react' - '@types/react-dom' - code-red@1.0.4: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@types/estree': 1.0.8 - acorn: 8.16.0 - estree-walker: 3.0.3 - periscopic: 3.1.0 - codemirror-readonly-ranges@0.1.0-alpha.2(@codemirror/state@6.6.0)(@codemirror/view@6.41.0): dependencies: '@codemirror/state': 6.6.0 @@ -26265,32 +22397,22 @@ snapshots: commander@12.1.0: {} - commander@13.1.0: {} - commander@14.0.2: {} commander@14.0.3: {} commander@2.20.3: {} - commander@2.9.0: - dependencies: - graceful-readlink: 1.0.1 - commander@7.2.0: {} commander@8.3.0: {} - commander@9.5.0: {} - comment-json@4.3.0: dependencies: array-timsort: 1.0.3 core-util-is: 1.0.3 esprima: 4.0.1 - common-ancestor-path@2.0.0: {} - common-path-prefix@3.0.0: {} compress-commons@6.0.2: @@ -26305,13 +22427,6 @@ snapshots: concat-map@0.0.1: {} - concat-stream@1.6.2: - dependencies: - buffer-from: 1.1.2 - inherits: 2.0.4 - readable-stream: 2.3.8 - typedarray: 0.0.6 - confbox@0.1.8: {} confbox@0.2.4: {} @@ -26330,10 +22445,6 @@ snapshots: consola@3.4.2: {} - console-table-printer@2.15.0: - dependencies: - simple-wcswidth: 1.1.2 - content-disposition@0.5.4: dependencies: safe-buffer: 5.2.1 @@ -26358,10 +22469,6 @@ snapshots: cookie@1.1.1: {} - copy-anything@4.0.5: - dependencies: - is-what: 5.5.0 - copy-file@11.1.0: dependencies: graceful-fs: 4.2.11 @@ -26413,38 +22520,8 @@ snapshots: crc-32: 1.2.2 readable-stream: 4.7.0 - create-langgraph@1.1.5(babel-plugin-macros@3.1.0): - dependencies: - '@clack/prompts': 0.9.1 - '@commander-js/extra-typings': 13.1.0(commander@13.1.0) - commander: 13.1.0 - dedent: 1.7.2(babel-plugin-macros@3.1.0) - extract-zip: 2.0.1 - picocolors: 1.1.1 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - create-require@1.1.1: {} - create-wdio@9.27.0(@types/node@25.6.0): - dependencies: - chalk: 5.6.2 - commander: 14.0.3 - cross-spawn: 7.0.6 - ejs: 3.1.10 - execa: 9.6.1 - import-meta-resolve: 4.2.0 - inquirer: 12.11.1(@types/node@25.6.0) - normalize-package-data: 7.0.1 - read-pkg-up: 10.1.0 - recursive-readdir: 2.2.3 - semver: 7.7.4 - type-fest: 4.41.0 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - crelt@1.0.6: {} cron-parser@4.9.0: @@ -26485,8 +22562,6 @@ snapshots: domutils: 3.2.2 nth-check: 2.1.1 - css-shorthand-properties@1.1.2: {} - css-tree@2.2.1: dependencies: mdn-data: 2.0.28 @@ -26497,8 +22572,6 @@ snapshots: mdn-data: 2.27.1 source-map-js: 1.2.1 - css-value@0.0.1: {} - css-what@6.2.2: {} cssesc@3.0.0: {} @@ -26509,8 +22582,6 @@ snapshots: dependencies: css-tree: 2.2.1 - cssom@0.5.0: {} - cssstyle@4.6.0: dependencies: '@asamuzakjp/css-color': 3.2.0 @@ -26717,7 +22788,8 @@ snapshots: data-uri-to-buffer@4.0.1: {} - data-uri-to-buffer@6.0.2: {} + data-uri-to-buffer@6.0.2: + optional: true data-urls@5.0.0: dependencies: @@ -26751,34 +22823,16 @@ snapshots: dayjs@1.11.20: {} - debounce@1.2.1: {} - - debug@4.3.7: - dependencies: - ms: 2.1.3 - debug@4.4.3(supports-color@10.2.2): dependencies: ms: 2.1.3 optionalDependencies: supports-color: 10.2.2 - debug@4.4.3(supports-color@8.1.1): - dependencies: - ms: 2.1.3 - optionalDependencies: - supports-color: 8.1.1 - decache@4.6.2: dependencies: callsite: 1.0.0 - decamelize@1.2.0: {} - - decamelize@4.0.0: {} - - decamelize@6.0.1: {} - decimal.js@10.6.0: {} decode-named-character-reference@1.3.0: @@ -26795,14 +22849,10 @@ snapshots: optionalDependencies: babel-plugin-macros: 3.1.0 - deep-eql@5.0.2: {} - deep-extend@0.6.0: {} deep-is@0.1.4: {} - deepmerge-ts@7.1.5: {} - deepmerge@4.3.1: {} default-browser-id@5.0.1: {} @@ -26824,8 +22874,6 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - define-lazy-prop@2.0.0: {} - define-lazy-prop@3.0.0: {} define-properties@1.2.1: @@ -26841,6 +22889,7 @@ snapshots: ast-types: 0.13.4 escodegen: 2.1.0 esprima: 4.0.1 + optional: true delaunator@5.1.0: dependencies: @@ -26906,15 +22955,6 @@ snapshots: transitivePeerDependencies: - supports-color - detective-typescript@14.0.0(typescript@5.9.3): - dependencies: - '@typescript-eslint/typescript-estree': 8.58.1(typescript@5.9.3) - ast-module-types: 6.0.1 - node-source-walk: 7.0.1 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - detective-vue2@2.2.0(supports-color@10.2.2)(typescript@5.9.3): dependencies: '@dependents/detective-less': 5.0.1 @@ -26928,23 +22968,8 @@ snapshots: transitivePeerDependencies: - supports-color - detective-vue2@2.2.0(typescript@5.9.3): - dependencies: - '@dependents/detective-less': 5.0.1 - '@vue/compiler-sfc': 3.5.32 - detective-es6: 5.0.1 - detective-sass: 6.0.1 - detective-scss: 5.0.1 - detective-stylus: 5.0.1 - detective-typescript: 14.0.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - dettle@1.0.5: {} - devalue@5.7.1: {} - devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -26954,14 +22979,13 @@ snapshots: diff@4.0.4: {} - diff@5.2.2: {} + diff@5.2.2: + optional: true diff@8.0.3: {} diff@8.0.4: {} - dlv@1.1.3: {} - dom-accessibility-api@0.5.16: {} dom-serializer@2.0.0: @@ -26994,10 +23018,6 @@ snapshots: dependencies: type-fest: 4.41.0 - dotenv-expand@12.0.3: - dependencies: - dotenv: 16.6.1 - dotenv@16.4.7: {} dotenv@16.6.1: {} @@ -27036,8 +23056,6 @@ snapshots: pg: 8.20.0 postgres: 3.4.9 - dset@3.1.4: {} - dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -27046,12 +23064,6 @@ snapshots: eastasianwidth@0.2.0: {} - easy-table@1.2.0: - dependencies: - ansi-regex: 5.0.1 - optionalDependencies: - wcwidth: 1.0.1 - ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer: 5.2.1 @@ -27063,24 +23075,6 @@ snapshots: '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 - edge-paths@3.0.5: - dependencies: - '@types/which': 2.0.2 - which: 2.0.2 - - edgedriver@6.3.0: - dependencies: - '@wdio/logger': 9.18.0 - '@zip.js/zip.js': 2.8.26 - decamelize: 6.0.1 - edge-paths: 3.0.5 - fast-xml-parser: 5.5.11 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 - which: 6.0.1 - transitivePeerDependencies: - - supports-color - ee-first@1.1.1: {} effect@3.21.0: @@ -27088,10 +23082,6 @@ snapshots: '@standard-schema/spec': 1.1.0 fast-check: 3.23.2 - ejs@3.1.10: - dependencies: - jake: 10.9.4 - electron-to-chromium@1.5.335: {} embla-carousel-react@8.6.0(react@19.2.5): @@ -27106,11 +23096,6 @@ snapshots: embla-carousel@8.6.0: {} - emmet@2.4.11: - dependencies: - '@emmetio/abbreviation': 2.3.3 - '@emmetio/css-abbreviation': 2.1.8 - emoji-mart@5.6.0: {} emoji-regex@10.6.0: {} @@ -27259,8 +23244,6 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - es6-error@4.1.1: {} - esast-util-from-estree@2.0.0: dependencies: '@types/estree-jsx': 1.0.5 @@ -27275,13 +23258,6 @@ snapshots: esast-util-from-estree: 2.0.0 vfile-message: 4.0.3 - esbuild-plugin-tailwindcss@2.2.0: - dependencies: - '@tailwindcss/postcss': 4.2.2 - autoprefixer: 10.4.27(postcss@8.5.9) - postcss: 8.5.9 - postcss-modules: 6.0.1(postcss@8.5.9) - esbuild@0.18.20: optionalDependencies: '@esbuild/android-arm': 0.18.20 @@ -27431,8 +23407,6 @@ snapshots: escape-string-regexp@1.0.5: {} - escape-string-regexp@2.0.0: {} - escape-string-regexp@4.0.0: {} escape-string-regexp@5.0.0: {} @@ -27473,7 +23447,7 @@ snapshots: ajv: 6.14.0 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -27630,44 +23604,8 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 - execa@9.6.1: - dependencies: - '@sindresorhus/merge-streams': 4.0.0 - cross-spawn: 7.0.6 - figures: 6.1.0 - get-stream: 9.0.1 - human-signals: 8.0.1 - is-plain-obj: 4.1.0 - is-stream: 4.0.1 - npm-run-path: 6.0.0 - pretty-ms: 9.3.0 - signal-exit: 4.1.0 - strip-final-newline: 4.0.0 - yoctocolors: 2.1.2 - - exit-hook@4.0.0: {} - expect-type@1.3.0: {} - expect-webdriverio@5.6.5(@wdio/globals@9.27.0)(@wdio/logger@9.18.0)(webdriverio@9.27.0(puppeteer-core@22.15.0)): - dependencies: - '@vitest/snapshot': 4.1.4 - '@wdio/globals': 9.27.0(expect-webdriverio@5.6.5)(webdriverio@9.27.0(puppeteer-core@22.15.0)) - '@wdio/logger': 9.18.0 - deep-eql: 5.0.2 - expect: 30.3.0 - jest-matcher-utils: 30.3.0 - webdriverio: 9.27.0(puppeteer-core@22.15.0) - - expect@30.3.0: - dependencies: - '@jest/expect-utils': 30.3.0 - '@jest/get-type': 30.1.0 - jest-matcher-utils: 30.3.0 - jest-message-util: 30.3.0 - jest-mock: 30.3.0 - jest-util: 30.3.0 - express-logging@1.1.1: dependencies: on-headers: 1.1.0 @@ -27680,7 +23618,7 @@ snapshots: content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 @@ -27729,7 +23667,7 @@ snapshots: extract-zip@2.0.1: dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: @@ -27752,8 +23690,6 @@ snapshots: fast-decode-uri-component@1.0.1: {} - fast-deep-equal@2.0.1: {} - fast-deep-equal@3.1.3: {} fast-equals@5.4.0: {} @@ -27781,40 +23717,16 @@ snapshots: fast-levenshtein@2.0.6: {} - fast-querystring@1.1.2: - dependencies: - fast-decode-uri-component: 1.0.1 - - fast-redact@3.5.0: {} - - fast-safe-stringify@2.1.1: {} - - fast-sha256@1.3.0: {} - - fast-string-truncated-width@1.2.1: {} - - fast-string-width@1.1.0: + fast-querystring@1.1.2: dependencies: - fast-string-truncated-width: 1.2.1 + fast-decode-uri-component: 1.0.1 + + fast-safe-stringify@2.1.1: {} fast-stringify@4.0.0: {} fast-uri@3.1.0: {} - fast-wrap-ansi@0.1.6: - dependencies: - fast-string-width: 1.1.0 - - fast-xml-builder@1.1.4: - dependencies: - path-expression-matcher: 1.5.0 - - fast-xml-parser@5.5.11: - dependencies: - fast-xml-builder: 1.1.4 - path-expression-matcher: 1.5.0 - strnum: 2.2.3 - fastest-levenshtein@1.0.16: {} fastify-plugin@5.1.0: {} @@ -27886,10 +23798,6 @@ snapshots: file-uri-to-path@1.0.0: {} - filelist@1.0.6: - dependencies: - minimatch: 5.1.9 - filename-reserved-regex@3.0.0: {} filenamify@5.1.1: @@ -27898,8 +23806,6 @@ snapshots: strip-outer: 2.0.0 trim-repeated: 2.0.0 - filesize@11.0.15: {} - fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -27910,7 +23816,7 @@ snapshots: finalhandler@2.1.1: dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -27934,11 +23840,6 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 - find-up@6.3.0: - dependencies: - locate-path: 7.2.0 - path-exists: 5.0.0 - find-up@7.0.0: dependencies: locate-path: 7.2.0 @@ -27950,25 +23851,13 @@ snapshots: locate-path: 8.0.0 unicorn-magic: 0.3.0 - firefox-profile@4.7.0: - dependencies: - adm-zip: 0.5.17 - fs-extra: 11.3.4 - ini: 4.1.3 - minimist: 1.2.8 - xml2js: 0.6.2 - flat-cache@4.0.1: dependencies: flatted: 3.4.2 keyv: 4.5.4 - flat@5.0.2: {} - flatted@3.4.2: {} - flattie@1.1.1: {} - fn.name@1.1.0: {} folder-walker@3.2.0: @@ -27977,15 +23866,7 @@ snapshots: follow-redirects@1.15.11(debug@4.4.3): optionalDependencies: - debug: 4.4.3(supports-color@8.1.1) - - fontace@0.4.1: - dependencies: - fontkitten: 1.0.3 - - fontkitten@1.0.3: - dependencies: - tiny-inflate: 1.0.3 + debug: 4.4.3(supports-color@10.2.2) for-each@0.3.5: dependencies: @@ -28000,8 +23881,6 @@ snapshots: form-data-encoder@2.1.4: {} - form-data-encoder@4.1.0: {} - form-data@4.0.5: dependencies: asynckit: 0.4.0 @@ -28012,8 +23891,6 @@ snapshots: format@0.2.2: {} - formdata-node@6.0.3: {} - formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 @@ -28040,21 +23917,10 @@ snapshots: inherits: 2.0.4 readable-stream: 2.3.8 - fs-extra@11.3.4: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.2.0 - universalify: 2.0.1 - fs-minipass@2.1.0: dependencies: minipass: 3.3.6 - fs.realpath@1.0.0: {} - - fsevents@2.3.2: - optional: true - fsevents@2.3.3: optional: true @@ -28073,19 +23939,10 @@ snapshots: fuzzy@0.1.3: {} - fx-runner@1.4.0: - dependencies: - commander: 2.9.0 - shell-quote: 1.7.3 - spawn-sync: 1.0.15 - when: 3.7.7 - which: 1.2.4 - winreg: 0.0.12 - gaxios@7.1.4: dependencies: extend: 3.0.2 - https-proxy-agent: 7.0.6 + https-proxy-agent: 7.0.6(supports-color@10.2.2) node-fetch: 3.3.2 transitivePeerDependencies: - supports-color @@ -28098,23 +23955,8 @@ snapshots: transitivePeerDependencies: - supports-color - geckodriver@6.1.0: - dependencies: - '@wdio/logger': 9.18.0 - '@zip.js/zip.js': 2.8.26 - decamelize: 6.0.1 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 - modern-tar: 0.7.6 - transitivePeerDependencies: - - supports-color - generator-function@2.0.1: {} - generic-names@4.0.0: - dependencies: - loader-utils: 3.3.1 - gensync@1.0.0-beta.2: {} get-amd-module-type@6.0.1: @@ -28179,9 +24021,10 @@ snapshots: dependencies: basic-ftp: 5.2.2 data-uri-to-buffer: 6.0.2 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color + optional: true get-value@2.0.6: {} @@ -28204,8 +24047,6 @@ snapshots: nypm: 0.6.5 pathe: 2.0.3 - giget@3.2.0: {} - giscus@1.6.0: dependencies: lit: 3.3.2 @@ -28226,8 +24067,6 @@ snapshots: dependencies: is-glob: 4.0.3 - glob-to-regexp@0.4.1: {} - glob@10.5.0: dependencies: foreground-child: 3.3.1 @@ -28243,14 +24082,6 @@ snapshots: minipass: 7.1.3 path-scurry: 2.0.2 - glob@8.1.0: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 5.1.9 - once: 1.4.0 - global-directory@4.0.1: dependencies: ini: 4.1.1 @@ -28312,10 +24143,6 @@ snapshots: graceful-fs@4.2.11: {} - graceful-readlink@1.0.1: {} - - grapheme-splitter@1.0.4: {} - gray-matter@4.0.3: dependencies: js-yaml: 3.14.2 @@ -28323,8 +24150,6 @@ snapshots: section-matter: 1.0.0 strip-bom-string: 1.0.0 - growly@1.3.0: {} - h3@1.15.11: dependencies: cookie-es: 1.2.3 @@ -28348,7 +24173,7 @@ snapshots: happy-dom@20.8.9: dependencies: - '@types/node': 25.6.0 + '@types/node': 24.12.2 '@types/whatwg-mimetype': 3.0.2 '@types/ws': 8.18.1 entities: 7.0.1 @@ -28595,8 +24420,6 @@ snapshots: property-information: 7.1.0 space-separated-tokens: 2.0.2 - he@1.2.0: {} - hls.js@1.6.15: {} hoist-non-react-statics@3.3.2: @@ -28605,8 +24428,6 @@ snapshots: hono@4.12.12: {} - hookable@6.1.0: {} - hosted-git-info@7.0.2: dependencies: lru-cache: 10.4.3 @@ -28633,16 +24454,12 @@ snapshots: transitivePeerDependencies: - '@noble/hashes' - html-escaper@3.0.3: {} - html-to-image@1.11.13: {} html-url-attributes@3.0.1: {} html-void-elements@3.0.0: {} - htmlfy@0.8.1: {} - htmlparser2@10.1.0: dependencies: domelementtype: 2.3.0 @@ -28679,14 +24496,14 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color http-proxy-middleware@3.0.5: dependencies: '@types/http-proxy': 1.17.17 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) http-proxy: 1.18.1(debug@4.4.3) is-glob: 4.0.3 is-plain-object: 5.0.0 @@ -28712,14 +24529,7 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.4.3(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color - - https-proxy-agent@7.0.6: - dependencies: - agent-base: 7.1.4 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color @@ -28733,7 +24543,7 @@ snapshots: https-proxy-agent@9.0.0: dependencies: agent-base: 9.0.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color @@ -28741,8 +24551,6 @@ snapshots: human-signals@5.0.0: {} - human-signals@8.0.1: {} - iceberg-js@0.8.1: {} iconify-icon@3.0.2: @@ -28761,10 +24569,6 @@ snapshots: dependencies: safer-buffer: 2.1.2 - icss-utils@5.1.0(postcss@8.5.9): - dependencies: - postcss: 8.5.9 - ieee754@1.2.1: {} ignore@5.3.2: {} @@ -28805,27 +24609,18 @@ snapshots: cjs-module-lexer: 2.2.0 module-details-from-path: 1.0.4 - import-meta-resolve@4.2.0: {} - imurmurhash@0.1.4: {} indent-string@5.0.0: {} index-to-position@1.2.0: {} - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - inherits@2.0.4: {} ini@1.3.8: {} ini@4.1.1: {} - ini@4.1.3: {} - inline-style-parser@0.1.1: optional: true @@ -28840,18 +24635,6 @@ snapshots: run-async: 2.4.1 rxjs: 6.6.7 - inquirer@12.11.1(@types/node@25.6.0): - dependencies: - '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@25.6.0) - '@inquirer/prompts': 7.10.1(@types/node@25.6.0) - '@inquirer/type': 3.0.10(@types/node@25.6.0) - mute-stream: 2.0.0 - run-async: 4.0.6 - rxjs: 7.8.2 - optionalDependencies: - '@types/node': 25.6.0 - inquirer@8.2.7(@types/node@22.19.17): dependencies: '@inquirer/external-editor': 1.0.3(@types/node@22.19.17) @@ -28892,7 +24675,7 @@ snapshots: dependencies: '@ioredis/commands': 1.5.0 cluster-key-slot: 1.1.2 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -28903,7 +24686,8 @@ snapshots: - supports-color optional: true - ip-address@10.1.0: {} + ip-address@10.1.0: + optional: true ipaddr.js@1.9.1: {} @@ -28991,10 +24775,6 @@ snapshots: iron-webcrypto@1.2.1: {} - is-absolute@0.1.7: - dependencies: - is-relative: 0.1.3 - is-alphabetical@2.0.1: {} is-alphanumerical@2.0.1: @@ -29053,12 +24833,8 @@ snapshots: is-decimal@2.0.1: {} - is-docker@2.2.1: {} - is-docker@3.0.0: {} - is-electron@2.2.2: {} - is-error-instance@2.0.0: {} is-extendable@0.1.1: {} @@ -29141,14 +24917,8 @@ snapshots: is-potential-custom-element-name@1.0.1: {} - is-primitive@3.0.1: {} - is-promise@4.0.0: {} - is-reference@3.0.3: - dependencies: - '@types/estree': 1.0.8 - is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -29156,8 +24926,6 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 - is-relative@0.1.3: {} - is-safe-filename@0.1.1: {} is-set@2.0.3: {} @@ -29206,12 +24974,6 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 - is-what@5.5.0: {} - - is-wsl@2.2.0: - dependencies: - is-docker: 2.2.1 - is-wsl@3.1.0: dependencies: is-inside-container: 1.0.0 @@ -29228,16 +24990,12 @@ snapshots: iserror@0.0.2: {} - isexe@1.1.2: {} - isexe@2.0.0: {} isexe@3.1.1: {} isexe@3.1.5: {} - isexe@4.0.0: {} - isobject@3.0.1: {} jackspeak@3.4.3: @@ -29246,55 +25004,6 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 - jake@10.9.4: - dependencies: - async: 3.2.6 - filelist: 1.0.6 - picocolors: 1.1.1 - - jest-diff@30.3.0: - dependencies: - '@jest/diff-sequences': 30.3.0 - '@jest/get-type': 30.1.0 - chalk: 4.1.2 - pretty-format: 30.3.0 - - jest-matcher-utils@30.3.0: - dependencies: - '@jest/get-type': 30.1.0 - chalk: 4.1.2 - jest-diff: 30.3.0 - pretty-format: 30.3.0 - - jest-message-util@30.3.0: - dependencies: - '@babel/code-frame': 7.29.0 - '@jest/types': 30.3.0 - '@types/stack-utils': 2.0.3 - chalk: 4.1.2 - graceful-fs: 4.2.11 - picomatch: 4.0.4 - pretty-format: 30.3.0 - slash: 3.0.0 - stack-utils: 2.0.6 - - jest-mock@30.3.0: - dependencies: - '@jest/types': 30.3.0 - '@types/node': 24.12.2 - jest-util: 30.3.0 - - jest-regex-util@30.0.1: {} - - jest-util@30.3.0: - dependencies: - '@jest/types': 30.3.0 - '@types/node': 24.12.2 - chalk: 4.1.2 - ci-info: 4.4.0 - graceful-fs: 4.2.11 - picomatch: 4.0.4 - jiti@2.6.1: {} jose@6.2.2: {} @@ -29311,14 +25020,8 @@ snapshots: js-levenshtein-esm@2.0.0: {} - js-tiktoken@1.0.21: - dependencies: - base64-js: 1.5.1 - js-tokens@4.0.0: {} - js-tokens@9.0.1: {} - js-yaml@3.14.2: dependencies: argparse: 1.0.10 @@ -29335,7 +25038,7 @@ snapshots: decimal.js: 10.6.0 html-encoding-sniffer: 4.0.0 http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 + https-proxy-agent: 7.0.6(supports-color@10.2.2) is-potential-custom-element-name: 1.0.1 nwsapi: 2.2.23 parse5: 7.3.0 @@ -29365,7 +25068,7 @@ snapshots: decimal.js: 10.6.0 html-encoding-sniffer: 6.0.0(@noble/hashes@1.8.0) http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 + https-proxy-agent: 7.0.6(supports-color@10.2.2) is-potential-custom-element-name: 1.0.1 parse5: 8.0.0 saxes: 6.0.0 @@ -29393,8 +25096,6 @@ snapshots: json-parse-even-better-errors@2.3.1: {} - json-parse-even-better-errors@3.0.2: {} - json-schema-ref-resolver@3.0.0: dependencies: dequal: 2.0.3 @@ -29415,16 +25116,6 @@ snapshots: json5@2.2.3: {} - jsonc-parser@2.3.1: {} - - jsonc-parser@3.3.1: {} - - jsonfile@6.2.0: - dependencies: - universalify: 2.0.1 - optionalDependencies: - graceful-fs: 4.2.11 - jsonpointer@5.0.1: {} jsonschema@1.5.0: {} @@ -29442,10 +25133,6 @@ snapshots: ms: 2.1.3 semver: 7.7.2 - jsx-slack@6.1.2: - dependencies: - '@slack/types': 2.20.1 - jszip@3.10.1: dependencies: lie: 3.3.0 @@ -29484,9 +25171,8 @@ snapshots: kind-of@6.0.3: {} - kleur@3.0.3: {} - - kleur@4.1.5: {} + kleur@4.1.5: + optional: true kuler@2.0.0: {} @@ -29507,29 +25193,6 @@ snapshots: vscode-languageserver-textdocument: 1.0.12 vscode-uri: 3.1.0 - langsmith@0.4.12(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6)): - dependencies: - '@types/uuid': 10.0.0 - chalk: 4.1.2 - console-table-printer: 2.15.0 - p-queue: 6.6.2 - semver: 7.7.4 - uuid: 10.0.0 - optionalDependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/sdk-trace-base': 2.6.1(@opentelemetry/api@1.9.1) - openai: 6.34.0(ws@8.20.0)(zod@4.3.6) - - langsmith@0.5.18(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(openai@6.34.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0): - dependencies: - p-queue: 6.6.2 - uuid: 10.0.0 - optionalDependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/sdk-trace-base': 2.6.1(@opentelemetry/api@1.9.1) - openai: 6.34.0(ws@8.20.0)(zod@4.3.6) - ws: 8.20.0 - latest-version@9.0.0: dependencies: package-json: 10.0.1 @@ -29578,13 +25241,6 @@ snapshots: process-warning: 4.0.1 set-cookie-parser: 2.7.2 - lighthouse-logger@2.0.2: - dependencies: - debug: 4.4.3(supports-color@8.1.1) - marky: 1.3.0 - transitivePeerDependencies: - - supports-color - lightningcss-darwin-arm64@1.30.1: optional: true @@ -29632,16 +25288,6 @@ snapshots: lines-and-columns@1.2.4: {} - lines-and-columns@2.0.4: {} - - linkedom@0.18.12: - dependencies: - css-select: 5.2.2 - cssom: 0.5.0 - html-escaper: 3.0.3 - htmlparser2: 10.1.0 - uhyphen: 0.2.0 - linkify-it@5.0.0: dependencies: uc.micro: 2.1.0 @@ -29680,14 +25326,6 @@ snapshots: transitivePeerDependencies: - srvx - listr2@10.2.1: - dependencies: - cli-truncate: 5.2.0 - eventemitter3: 5.0.4 - log-update: 6.1.0 - rfdc: 1.4.1 - wrap-ansi: 10.0.0 - listr2@9.0.5: dependencies: cli-truncate: 5.2.0 @@ -29713,22 +25351,6 @@ snapshots: lit-element: 4.2.2 lit-html: 3.3.2 - loader-utils@3.3.1: {} - - local-pkg@1.1.2: - dependencies: - mlly: 1.8.2 - pkg-types: 2.3.0 - quansync: 0.2.11 - - locate-app@2.5.0: - dependencies: - '@promptbook/utils': 0.69.5 - type-fest: 4.26.0 - userhome: 1.0.1 - - locate-character@3.0.0: {} - locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -29762,17 +25384,11 @@ snapshots: dependencies: lodash._basetostring: 4.12.0 - lodash.camelcase@4.3.0: {} - - lodash.clonedeep@4.5.0: {} - lodash.debounce@4.0.8: {} lodash.defaults@4.2.0: optional: true - lodash.flattendeep@4.4.0: {} - lodash.includes@4.3.0: {} lodash.isarguments@3.1.0: @@ -29794,21 +25410,15 @@ snapshots: lodash.once@4.1.1: {} - lodash.pickby@4.6.0: {} - lodash.throttle@4.1.1: {} lodash.transform@4.6.0: {} - lodash.union@4.6.0: {} - lodash.uniqby@4.5.0: dependencies: lodash._baseiteratee: 4.7.0 lodash._baseuniq: 4.6.0 - lodash.zip@4.2.0: {} - lodash@4.17.23: {} lodash@4.18.1: {} @@ -29842,16 +25452,10 @@ snapshots: safe-stable-stringify: 2.5.0 triple-beam: 1.4.1 - loglevel-plugin-prefix@0.8.4: {} - - loglevel@1.9.2: {} - long@5.3.2: {} longest-streak@3.1.0: {} - loops@6.3.0: {} - loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -29866,7 +25470,8 @@ snapshots: dependencies: yallist: 3.1.1 - lru-cache@7.18.3: {} + lru-cache@7.18.3: + optional: true lru_map@0.4.1: {} @@ -29901,6 +25506,7 @@ snapshots: '@babel/parser': 7.29.2 '@babel/types': 7.29.0 source-map-js: 1.2.1 + optional: true make-dir@4.0.0: dependencies: @@ -29908,8 +25514,6 @@ snapshots: make-error@1.3.6: {} - many-keys-map@3.0.3: {} - map-obj@5.0.2: {} markdown-extensions@2.0.0: {} @@ -29929,8 +25533,6 @@ snapshots: marked@17.0.6: {} - marky@1.3.0: {} - math-intrinsics@1.1.0: {} maxstache-stream@1.0.4: @@ -29942,12 +25544,6 @@ snapshots: maxstache@1.0.7: {} - mdast-util-definitions@6.0.0: - dependencies: - '@types/mdast': 4.0.4 - '@types/unist': 3.0.3 - unist-util-visit: 5.1.0 - mdast-util-find-and-replace@3.0.2: dependencies: '@types/mdast': 4.0.4 @@ -30831,7 +26427,7 @@ snapshots: micromark@3.2.0: dependencies: '@types/debug': 4.1.13 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) decode-named-character-reference: 1.3.0 micromark-core-commonmark: 1.1.0 micromark-factory-space: 1.1.0 @@ -30854,7 +26450,7 @@ snapshots: micromark@4.0.2: dependencies: '@types/debug': 4.1.13 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) decode-named-character-reference: 1.3.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -30941,7 +26537,8 @@ snapshots: dependencies: minipass: 7.1.3 - mitt@3.0.1: {} + mitt@3.0.1: + optional: true mixin-deep@1.3.2: dependencies: @@ -30957,40 +26554,6 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.3 - mocha@10.8.2: - dependencies: - ansi-colors: 4.1.3 - browser-stdout: 1.3.1 - chokidar: 3.6.0 - debug: 4.4.3(supports-color@8.1.1) - diff: 5.2.2 - escape-string-regexp: 4.0.0 - find-up: 5.0.0 - glob: 8.1.0 - he: 1.2.0 - js-yaml: 4.1.1 - log-symbols: 4.1.0 - minimatch: 5.1.9 - ms: 2.1.3 - serialize-javascript: 6.0.2 - strip-json-comments: 3.1.1 - supports-color: 8.1.1 - workerpool: 6.5.1 - yargs: 16.2.0 - yargs-parser: 20.2.9 - yargs-unparser: 2.0.0 - - modal@0.6.3: - dependencies: - cbor-x: 1.6.4 - long: 5.3.2 - nice-grpc: 2.1.15 - protobufjs: 7.5.4 - smol-toml: 1.6.1 - uuid: 11.1.0 - - modern-tar@0.7.6: {} - module-definition@6.0.1: dependencies: ast-module-types: 6.0.1 @@ -31018,33 +26581,18 @@ snapshots: mri@1.2.0: {} - mrmime@2.0.1: {} - ms@2.1.3: {} - muggle-string@0.4.1: {} - - multimatch@6.0.0: - dependencies: - '@types/minimatch': 3.0.5 - array-differ: 4.0.0 - array-union: 3.0.1 - minimatch: 3.1.5 - multiparty@4.2.3: dependencies: http-errors: 1.8.1 safe-buffer: 5.2.1 uid-safe: 2.1.5 - mustache@4.2.0: {} - mutative@1.3.0: {} mute-stream@0.0.8: {} - mute-stream@2.0.0: {} - mux-embed@5.18.0: {} namespace-emitter@2.0.1: {} @@ -31052,8 +26600,6 @@ snapshots: nan@2.26.2: optional: true - nano-spawn@2.1.0: {} - nanoid@3.3.11: {} nanoid@5.1.7: {} @@ -31066,8 +26612,6 @@ snapshots: negotiator@1.0.0: {} - neotraverse@0.6.18: {} - netlify-redirector@0.5.0: {} netlify@23.15.1(@swc/core@1.13.2(@swc/helpers@0.5.21))(@types/node@22.19.17)(aws4fetch@1.0.20)(ioredis@5.9.2)(picomatch@4.0.4)(rollup@4.60.1)(srvx@0.11.15): @@ -31087,7 +26631,7 @@ snapshots: '@netlify/images': 1.2.5(@netlify/blobs@10.1.0)(aws4fetch@1.0.20)(ioredis@5.9.2)(srvx@0.11.15) '@netlify/local-functions-proxy': 2.0.3 '@netlify/redirect-parser': 15.0.3 - '@netlify/zip-it-and-ship-it': 14.3.1(rollup@4.60.1) + '@netlify/zip-it-and-ship-it': 14.3.1(rollup@4.60.1)(supports-color@10.2.2) '@octokit/rest': 22.0.0 '@opentelemetry/api': 1.8.0 '@pnpm/tabtab': 0.5.4 @@ -31105,7 +26649,7 @@ snapshots: content-type: 1.0.5 cookie: 1.0.2 cron-parser: 4.9.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) decache: 4.6.2 dot-prop: 10.1.0 dotenv: 17.2.3 @@ -31127,7 +26671,7 @@ snapshots: gitconfiglocal: 2.1.0 http-proxy: 1.18.1(debug@4.4.3) http-proxy-middleware: 3.0.5 - https-proxy-agent: 7.0.6 + https-proxy-agent: 7.0.6(supports-color@10.2.2) inquirer: 8.2.7(@types/node@22.19.17) inquirer-autocomplete-prompt: 1.4.0(inquirer@8.2.7(@types/node@22.19.17)) is-docker: 3.0.0 @@ -31201,23 +26745,14 @@ snapshots: - uploadthing - utf-8-validate - netmask@2.1.1: {} + netmask@2.1.1: + optional: true next-themes@0.4.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: react: 19.2.5 react-dom: 19.2.5(react@19.2.5) - nice-grpc-common@2.0.3: - dependencies: - ts-error: 1.0.6 - - nice-grpc@2.1.15: - dependencies: - '@grpc/grpc-js': 1.14.3 - abort-controller-x: 0.5.0 - nice-grpc-common: 2.0.3 - nlcst-to-string@4.0.0: dependencies: '@types/nlcst': 2.0.3 @@ -31247,23 +26782,9 @@ snapshots: node-forge@1.4.0: {} - node-gyp-build-optional-packages@5.1.1: - dependencies: - detect-libc: 2.1.2 - optional: true - node-gyp-build@4.8.4: {} - node-mock-http@1.0.4: {} - - node-notifier@10.0.1: - dependencies: - growly: 1.3.0 - is-wsl: 2.2.0 - semver: 7.6.3 - shellwords: 0.1.1 - uuid: 8.3.2 - which: 2.0.2 + node-mock-http@1.0.4: {} node-releases@2.0.37: {} @@ -31318,11 +26839,6 @@ snapshots: dependencies: path-key: 4.0.0 - npm-run-path@6.0.0: - dependencies: - path-key: 4.0.0 - unicorn-magic: 0.3.0 - nth-check@2.1.1: dependencies: boolbase: 1.0.0 @@ -31415,13 +26931,6 @@ snapshots: regex: 6.1.0 regex-recursion: 6.0.2 - open@10.2.0: - dependencies: - default-browser: 5.5.0 - define-lazy-prop: 3.0.0 - is-inside-container: 1.0.0 - wsl-utils: 0.1.0 - open@11.0.0: dependencies: default-browser: 5.5.0 @@ -31431,22 +26940,11 @@ snapshots: powershell-utils: 0.1.0 wsl-utils: 0.3.1 - open@8.4.2: - dependencies: - define-lazy-prop: 2.0.0 - is-docker: 2.2.1 - is-wsl: 2.2.0 - openai@5.23.2(ws@8.20.0)(zod@3.25.76): optionalDependencies: ws: 8.20.0 zod: 3.25.76 - openai@6.34.0(ws@8.20.0)(zod@4.3.6): - optionalDependencies: - ws: 8.20.0 - zod: 4.3.6 - optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -31475,8 +26973,6 @@ snapshots: macos-release: 3.4.0 windows-release: 6.1.0 - os-shim@0.1.3: {} - own-keys@1.0.1: dependencies: get-intrinsic: 1.3.0 @@ -31555,8 +27051,6 @@ snapshots: dependencies: p-map: 7.0.3 - p-finally@1.0.0: {} - p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -31569,10 +27063,6 @@ snapshots: dependencies: yocto-queue: 1.2.2 - p-limit@7.3.0: - dependencies: - yocto-queue: 1.2.2 - p-locate@5.0.0: dependencies: p-limit: 3.1.0 @@ -31583,43 +27073,18 @@ snapshots: p-map@7.0.3: {} - p-queue@6.6.2: - dependencies: - eventemitter3: 4.0.7 - p-timeout: 3.2.0 - - p-queue@9.1.2: - dependencies: - eventemitter3: 5.0.4 - p-timeout: 7.0.1 - p-reduce@3.0.0: {} - p-retry@4.6.2: - dependencies: - '@types/retry': 0.12.0 - retry: 0.13.1 - p-retry@6.2.1: dependencies: '@types/retry': 0.12.2 is-network-error: 1.3.1 retry: 0.13.1 - p-retry@7.1.1: - dependencies: - is-network-error: 1.3.1 - - p-timeout@3.2.0: - dependencies: - p-finally: 1.0.0 - p-timeout@5.1.0: {} p-timeout@6.1.4: {} - p-timeout@7.0.1: {} - p-wait-for@5.0.2: dependencies: p-timeout: 6.1.4 @@ -31630,19 +27095,21 @@ snapshots: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 agent-base: 7.1.4 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) get-uri: 6.0.5 http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 + https-proxy-agent: 7.0.6(supports-color@10.2.2) pac-resolver: 7.0.1 socks-proxy-agent: 8.0.5 transitivePeerDependencies: - supports-color + optional: true pac-resolver@7.0.1: dependencies: degenerator: 5.0.1 netmask: 2.1.1 + optional: true package-directory@8.2.0: dependencies: @@ -31715,14 +27182,6 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 - parse-json@7.1.1: - dependencies: - '@babel/code-frame': 7.29.0 - error-ex: 1.3.4 - json-parse-even-better-errors: 3.0.2 - lines-and-columns: 2.0.4 - type-fest: 3.13.1 - parse-json@8.3.0: dependencies: '@babel/code-frame': 7.29.0 @@ -31759,16 +27218,12 @@ snapshots: parseurl@1.3.3: {} - path-browserify@1.0.1: {} - path-data-parser@0.1.0: {} path-exists@4.0.0: {} path-exists@5.0.0: {} - path-expression-matcher@1.5.0: {} - path-key@3.1.1: {} path-key@4.0.0: {} @@ -31801,31 +27256,20 @@ snapshots: perfect-debounce@2.1.0: {} - periscopic@3.1.0: - dependencies: - '@types/estree': 1.0.8 - estree-walker: 3.0.3 - is-reference: 3.0.3 - pg-cloudflare@1.3.0: optional: true - pg-connection-string@2.12.0: {} + pg-connection-string@2.12.0: + optional: true pg-gateway@0.3.0-beta.4: {} pg-int8@1.0.1: {} - pg-node-migrations@0.0.8: - dependencies: - pg: 8.20.0 - sql-template-strings: 2.2.2 - transitivePeerDependencies: - - pg-native - pg-pool@3.13.0(pg@8.20.0): dependencies: pg: 8.20.0 + optional: true pg-protocol@1.13.0: {} @@ -31846,12 +27290,12 @@ snapshots: pgpass: 1.0.5 optionalDependencies: pg-cloudflare: 1.3.0 + optional: true pgpass@1.0.5: dependencies: split2: 4.2.0 - - piccolore@0.1.3: {} + optional: true picocolors@1.1.1: {} @@ -31861,10 +27305,6 @@ snapshots: picoquery@2.5.0: {} - pino-abstract-transport@2.0.0: - dependencies: - split2: 4.2.0 - pino-abstract-transport@3.0.0: dependencies: split2: 4.2.0 @@ -31885,20 +27325,6 @@ snapshots: sonic-boom: 4.2.1 thread-stream: 4.0.0 - pino@9.7.0: - dependencies: - atomic-sleep: 1.0.0 - fast-redact: 3.5.0 - on-exit-leak-free: 2.1.2 - pino-abstract-transport: 2.0.0 - pino-std-serializers: 7.1.0 - process-warning: 5.0.0 - quick-format-unescaped: 4.0.4 - real-require: 0.2.0 - safe-stable-stringify: 2.5.0 - sonic-boom: 4.2.1 - thread-stream: 3.1.0 - pkce-challenge@5.0.1: {} pkg-types@1.3.1: @@ -31919,14 +27345,6 @@ snapshots: transitivePeerDependencies: - react - playwright-core@1.59.1: {} - - playwright@1.59.1: - dependencies: - playwright-core: 1.59.1 - optionalDependencies: - fsevents: 2.3.2 - plist@3.1.0: dependencies: '@xmldom/xmldom': 0.8.12 @@ -31950,49 +27368,11 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-modules-extract-imports@3.1.0(postcss@8.5.9): - dependencies: - postcss: 8.5.9 - - postcss-modules-local-by-default@4.2.0(postcss@8.5.9): - dependencies: - icss-utils: 5.1.0(postcss@8.5.9) - postcss: 8.5.9 - postcss-selector-parser: 7.1.1 - postcss-value-parser: 4.2.0 - - postcss-modules-scope@3.2.1(postcss@8.5.9): - dependencies: - postcss: 8.5.9 - postcss-selector-parser: 7.1.1 - - postcss-modules-values@4.0.0(postcss@8.5.9): - dependencies: - icss-utils: 5.1.0(postcss@8.5.9) - postcss: 8.5.9 - - postcss-modules@6.0.1(postcss@8.5.9): - dependencies: - generic-names: 4.0.0 - icss-utils: 5.1.0(postcss@8.5.9) - lodash.camelcase: 4.3.0 - postcss: 8.5.9 - postcss-modules-extract-imports: 3.1.0(postcss@8.5.9) - postcss-modules-local-by-default: 4.2.0(postcss@8.5.9) - postcss-modules-scope: 3.2.1(postcss@8.5.9) - postcss-modules-values: 4.0.0(postcss@8.5.9) - string-hash: 1.1.3 - postcss-selector-parser@6.0.10: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-selector-parser@7.1.1: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - postcss-value-parser@4.2.0: {} postcss-values-parser@6.0.2(postcss@8.5.9): @@ -32036,36 +27416,10 @@ snapshots: query-selector-shadow-dom: 1.0.1 web-vitals: 5.2.0 - posthog-node@5.29.2(rxjs@7.8.2): - dependencies: - '@posthog/core': 1.25.2 - optionalDependencies: - rxjs: 7.8.2 - powershell-utils@0.1.0: {} preact@10.29.1: {} - precinct@12.2.0: - dependencies: - '@dependents/detective-less': 5.0.1 - commander: 12.1.0 - detective-amd: 6.0.1 - detective-cjs: 6.1.0 - detective-es6: 5.0.1 - detective-postcss: 7.0.1(postcss@8.5.9) - detective-sass: 6.0.1 - detective-scss: 5.0.1 - detective-stylus: 5.0.1 - detective-typescript: 14.0.0(typescript@5.9.3) - detective-vue2: 2.2.0(typescript@5.9.3) - module-definition: 6.0.1 - node-source-walk: 7.0.1 - postcss: 8.5.9 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - precinct@12.2.0(supports-color@10.2.2): dependencies: '@dependents/detective-less': 5.0.1 @@ -32098,12 +27452,6 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 - pretty-format@30.3.0: - dependencies: - '@jest/schemas': 30.0.5 - ansi-styles: 5.2.0 - react-is: 18.3.1 - pretty-ms@9.3.0: dependencies: parse-ms: 4.0.0 @@ -32113,8 +27461,6 @@ snapshots: colors: 1.4.0 minimist: 1.2.8 - prismjs@1.30.0: {} - proc-log@6.1.0: {} process-nextick-args@2.0.1: {} @@ -32127,24 +27473,6 @@ snapshots: progress@2.0.3: {} - promise-toolbox@0.21.0: - dependencies: - make-error: 1.3.6 - - promptl-ai@0.4.12: - dependencies: - acorn: 8.16.0 - code-red: 1.0.4 - fast-sha256: 1.3.0 - locate-character: 3.0.0 - yaml: 2.8.3 - zod: 3.25.76 - - prompts@2.4.2: - dependencies: - kleur: 3.0.3 - sisteransi: 1.0.5 - prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -32296,34 +27624,21 @@ snapshots: proxy-agent@6.5.0: dependencies: agent-base: 7.1.4 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 + https-proxy-agent: 7.0.6(supports-color@10.2.2) lru-cache: 7.18.3 pac-proxy-agent: 7.2.0 proxy-from-env: 1.1.0 socks-proxy-agent: 8.0.5 transitivePeerDependencies: - supports-color + optional: true proxy-from-env@1.1.0: {} - proxy-from-env@2.1.0: {} - ps-list@8.1.1: {} - publish-browser-extension@4.0.5: - dependencies: - cac: 6.7.14 - consola: 3.4.2 - dotenv: 17.4.2 - form-data-encoder: 4.1.0 - formdata-node: 6.0.3 - jsonwebtoken: 9.0.3 - listr2: 10.2.1 - ofetch: 1.5.1 - zod: 4.3.6 - pump@1.0.3: dependencies: end-of-stream: 1.4.5 @@ -32346,7 +27661,7 @@ snapshots: dependencies: '@puppeteer/browsers': 2.3.0 chromium-bidi: 0.6.3(devtools-protocol@0.0.1312386) - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) devtools-protocol: 0.0.1312386 ws: 8.20.0 transitivePeerDependencies: @@ -32380,8 +27695,6 @@ snapshots: dependencies: side-channel: 1.1.0 - quansync@0.2.11: {} - query-selector-shadow-dom@1.0.1: {} query-string@9.3.1: @@ -32404,10 +27717,6 @@ snapshots: random-bytes@1.0.0: {} - randombytes@2.1.0: - dependencies: - safe-buffer: 5.2.1 - range-analyzer@0.1.1-alpha.2: {} range-parser@1.2.1: {} @@ -32530,11 +27839,6 @@ snapshots: defu: 6.1.7 destr: 2.0.5 - rc9@3.0.1: - dependencies: - defu: 6.1.7 - destr: 2.0.5 - rc@1.2.8: dependencies: deep-extend: 0.6.0 @@ -32711,12 +28015,6 @@ snapshots: read-pkg: 10.1.0 type-fest: 5.5.0 - read-pkg-up@10.1.0: - dependencies: - find-up: 6.3.0 - read-pkg: 8.1.0 - type-fest: 4.41.0 - read-pkg@10.1.0: dependencies: '@types/normalize-package-data': 2.4.4 @@ -32725,13 +28023,6 @@ snapshots: type-fest: 5.5.0 unicorn-magic: 0.4.0 - read-pkg@8.1.0: - dependencies: - '@types/normalize-package-data': 2.4.4 - normalize-package-data: 6.0.2 - parse-json: 7.1.1 - type-fest: 4.41.0 - read-pkg@9.0.1: dependencies: '@types/normalize-package-data': 2.4.4 @@ -32811,10 +28102,6 @@ snapshots: unified: 11.0.5 vfile: 6.0.3 - recursive-readdir@2.2.3: - dependencies: - minimatch: 3.1.5 - redis-errors@1.2.0: optional: true @@ -32891,12 +28178,6 @@ snapshots: unist-util-visit-parents: 6.0.2 vfile: 6.0.3 - rehype-parse@9.0.1: - dependencies: - '@types/hast': 3.0.4 - hast-util-from-html: 2.0.3 - unified: 11.0.5 - rehype-raw@7.0.0: dependencies: '@types/hast': 3.0.4 @@ -32924,19 +28205,6 @@ snapshots: hast-util-to-string: 3.0.1 unist-util-visit: 5.1.0 - rehype-stringify@10.0.1: - dependencies: - '@types/hast': 3.0.4 - hast-util-to-html: 9.0.5 - unified: 11.0.5 - - rehype@13.0.2: - dependencies: - '@types/hast': 3.0.4 - rehype-parse: 9.0.1 - rehype-stringify: 10.0.1 - unified: 11.0.5 - remark-breaks@4.0.0: dependencies: '@types/mdast': 4.0.4 @@ -33024,13 +28292,6 @@ snapshots: unified: 11.0.5 vfile: 6.0.3 - remark-smartypants@3.0.2: - dependencies: - retext: 9.0.0 - retext-smartypants: 6.2.0 - unified: 11.0.5 - unist-util-visit: 5.1.0 - remark-stringify@11.0.0: dependencies: '@types/mdast': 4.0.4 @@ -33041,22 +28302,10 @@ snapshots: remove-trailing-separator@1.1.0: {} - request-light@0.5.8: {} - - request-light@0.7.0: {} - require-directory@2.1.1: {} require-from-string@2.0.2: {} - require-in-the-middle@7.5.2: - dependencies: - debug: 4.4.3(supports-color@8.1.1) - module-details-from-path: 1.0.4 - resolve: 1.22.12 - transitivePeerDependencies: - - supports-color - require-in-the-middle@7.5.2(supports-color@10.2.2): dependencies: debug: 4.4.3(supports-color@10.2.2) @@ -33067,7 +28316,7 @@ snapshots: require-in-the-middle@8.0.1: dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) module-details-from-path: 1.0.4 transitivePeerDependencies: - supports-color @@ -33106,10 +28355,6 @@ snapshots: dependencies: lowercase-keys: 3.0.0 - resq@1.11.0: - dependencies: - fast-deep-equal: 2.0.1 - restore-cursor@3.1.0: dependencies: onetime: 5.1.2 @@ -33149,12 +28394,6 @@ snapshots: pos: 0.4.2 unist-util-visit: 5.1.0 - retext-smartypants@6.2.0: - dependencies: - '@types/nlcst': 2.0.3 - nlcst-to-string: 4.0.0 - unist-util-visit: 5.1.0 - retext-stringify@4.0.0: dependencies: '@types/nlcst': 2.0.3 @@ -33176,8 +28415,6 @@ snapshots: rfdc@1.4.1: {} - rgb2hex@0.2.5: {} - robust-predicates@3.0.3: {} rolldown@1.0.0-rc.15: @@ -33231,6 +28468,7 @@ snapshots: '@rollup/rollup-win32-x64-gnu': 4.60.1 '@rollup/rollup-win32-x64-msvc': 4.60.1 fsevents: 2.3.3 + optional: true rope-sequence@1.3.4: {} @@ -33245,7 +28483,7 @@ snapshots: router@2.2.0: dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -33259,8 +28497,6 @@ snapshots: run-async@2.4.1: {} - run-async@4.0.6: {} - run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -33280,8 +28516,6 @@ snapshots: mri: 1.2.0 optional: true - safaridriver@1.0.1: {} - safe-array-concat@1.1.3: dependencies: call-bind: 1.0.9 @@ -33329,8 +28563,6 @@ snapshots: dependencies: compute-scroll-into-view: 3.1.1 - scule@1.3.0: {} - section-matter@1.0.0: dependencies: extend-shallow: 2.0.1 @@ -33356,7 +28588,7 @@ snapshots: send@1.2.1: dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -33370,14 +28602,6 @@ snapshots: transitivePeerDependencies: - supports-color - serialize-error@12.0.0: - dependencies: - type-fest: 4.41.0 - - serialize-javascript@6.0.2: - dependencies: - randombytes: 2.1.0 - serialize-javascript@7.0.5: {} seroval-plugins@1.3.3(seroval@1.3.2): @@ -33436,11 +28660,6 @@ snapshots: is-plain-object: 2.0.4 split-string: 3.1.0 - set-value@4.1.0: - dependencies: - is-plain-object: 2.0.4 - is-primitive: 3.0.1 - setimmediate@1.0.5: {} setprototypeof@1.2.0: {} @@ -33482,10 +28701,6 @@ snapshots: shebang-regex@3.0.0: {} - shell-quote@1.7.3: {} - - shellwords@0.1.1: {} - shiki-stream@0.1.4(react@19.2.5)(solid-js@1.9.10): dependencies: '@shikijs/core': 3.23.0 @@ -33504,17 +28719,6 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 - shiki@4.0.2: - dependencies: - '@shikijs/core': 4.0.2 - '@shikijs/engine-javascript': 4.0.2 - '@shikijs/engine-oniguruma': 4.0.2 - '@shikijs/langs': 4.0.2 - '@shikijs/themes': 4.0.2 - '@shikijs/types': 4.0.2 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - shimmer@1.2.1: {} side-channel-list@1.0.1: @@ -33551,10 +28755,6 @@ snapshots: signal-exit@4.1.0: {} - simple-wcswidth@1.1.2: {} - - sisteransi@1.0.5: {} - sitemap@8.0.3: dependencies: '@types/node': 17.0.45 @@ -33562,8 +28762,6 @@ snapshots: arg: 5.0.2 sax: 1.6.0 - slash@3.0.0: {} - slash@5.1.0: {} slashes@3.0.12: {} @@ -33578,22 +28776,23 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 - smart-buffer@4.2.0: {} - - smol-toml@1.6.1: {} + smart-buffer@4.2.0: + optional: true socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.4 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) socks: 2.8.7 transitivePeerDependencies: - supports-color + optional: true socks@2.8.7: dependencies: ip-address: 10.1.0 smart-buffer: 4.2.0 + optional: true solid-js@1.9.10: dependencies: @@ -33633,13 +28832,6 @@ snapshots: space-separated-tokens@2.0.2: {} - spacetrim@0.11.59: {} - - spawn-sync@1.0.15: - dependencies: - concat-stream: 1.6.2 - os-shim: 0.1.3 - spdx-correct@3.2.0: dependencies: spdx-expression-parse: 3.0.1 @@ -33666,14 +28858,8 @@ snapshots: split2@4.2.0: {} - split@1.0.1: - dependencies: - through: 2.3.8 - sprintf-js@1.0.3: {} - sql-template-strings@2.2.2: {} - srvx@0.11.15: {} stack-generator@2.0.10: @@ -33682,18 +28868,10 @@ snapshots: stack-trace@0.0.10: {} - stack-utils@2.0.6: - dependencies: - escape-string-regexp: 2.0.0 - stackback@0.0.2: {} stackframe@1.3.4: {} - stacktrace-parser@0.1.11: - dependencies: - type-fest: 0.7.1 - standard-as-callback@2.1.0: optional: true @@ -33714,8 +28892,6 @@ snapshots: stopword@3.1.5: {} - stream-buffers@3.0.3: {} - streamdown@2.5.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: clsx: 2.1.1 @@ -33752,8 +28928,6 @@ snapshots: string-convert@0.2.1: {} - string-hash@1.1.3: {} - string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -33823,8 +28997,6 @@ snapshots: strip-bom-string@1.0.0: {} - strip-bom@5.0.0: {} - strip-dirs@3.0.0: dependencies: inspect-with-kind: 1.0.5 @@ -33834,18 +29006,10 @@ snapshots: strip-final-newline@3.0.0: {} - strip-final-newline@4.0.0: {} - strip-json-comments@2.0.1: {} strip-json-comments@3.1.1: {} - strip-json-comments@5.0.2: {} - - strip-literal@3.1.0: - dependencies: - js-tokens: 9.0.1 - strip-outer@2.0.0: {} stripe@20.4.1(@types/node@22.19.17): @@ -33856,12 +29020,6 @@ snapshots: optionalDependencies: '@types/node': 24.12.2 - stripe@20.4.1(@types/node@25.6.0): - optionalDependencies: - '@types/node': 25.6.0 - - strnum@2.2.3: {} - strtok3@7.1.1: dependencies: '@tokenizer/token': 0.3.0 @@ -33899,11 +29057,7 @@ snapshots: node-fetch: 3.3.2 tar: 7.5.13 transitivePeerDependencies: - - supports-color - - superjson@2.2.6: - dependencies: - copy-anything: 4.0.5 + - supports-color supports-color@10.2.2: {} @@ -33911,10 +29065,6 @@ snapshots: dependencies: has-flag: 4.0.0 - supports-color@8.1.1: - dependencies: - has-flag: 4.0.0 - supports-hyperlinks@3.2.0: dependencies: has-flag: 4.0.0 @@ -33976,6 +29126,7 @@ snapshots: - bare-abort-controller - bare-buffer - react-native-b4a + optional: true tar-stream@3.1.8: dependencies: @@ -34047,10 +29198,6 @@ snapshots: text-hex@1.0.0: {} - thread-stream@3.1.0: - dependencies: - real-require: 0.2.0 - thread-stream@4.0.0: dependencies: real-require: 0.2.0 @@ -34066,8 +29213,6 @@ snapshots: through@2.3.8: {} - tiny-inflate@1.0.3: {} - tinybase@7.3.5(@electric-sql/pglite@0.3.16)(@sinclair/typebox@0.34.49)(effect@3.21.0)(postgres@3.4.9)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(ws@8.20.0)(zod@4.3.6): optionalDependencies: '@electric-sql/pglite': 0.3.16 @@ -34092,8 +29237,6 @@ snapshots: tinypool@2.1.0: {} - tinyrainbow@1.2.0: {} - tinyrainbow@3.1.0: {} tinytick@1.2.8: {} @@ -34175,8 +29318,6 @@ snapshots: ts-dedent@2.2.0: {} - ts-error@1.0.6: {} - ts-md5@2.0.1: {} ts-node@10.9.2(@swc/core@1.13.2(@swc/helpers@0.5.21))(@types/node@22.19.17)(typescript@5.9.3): @@ -34199,18 +29340,12 @@ snapshots: optionalDependencies: '@swc/core': 1.13.2(@swc/helpers@0.5.21) - tsconfck@3.1.6(typescript@5.6.3): - optionalDependencies: - typescript: 5.6.3 - tslib@1.14.1: {} tslib@2.6.2: {} tslib@2.8.1: {} - tsscmp@1.0.6: {} - tsx@4.21.0: dependencies: esbuild: 0.27.7 @@ -34243,12 +29378,6 @@ snapshots: type-fest@0.21.3: {} - type-fest@0.7.1: {} - - type-fest@3.13.1: {} - - type-fest@4.26.0: {} - type-fest@4.41.0: {} type-fest@5.5.0: @@ -34294,19 +29423,11 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typedarray@0.0.6: {} - - typesafe-path@0.2.2: {} - - typescript-auto-import-cache@0.3.6: - dependencies: - semver: 7.7.4 - typescript-eslint@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3): dependencies: '@typescript-eslint/eslint-plugin': 8.58.1(@typescript-eslint/parser@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/parser': 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.58.1(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.58.1(supports-color@10.2.2)(typescript@5.9.3) '@typescript-eslint/utils': 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.4(jiti@2.6.1) typescript: 5.9.3 @@ -34325,8 +29446,6 @@ snapshots: ufo@1.6.3: {} - uhyphen@0.2.0: {} - uid-safe@2.1.5: dependencies: random-bytes: 1.0.0 @@ -34335,8 +29454,6 @@ snapshots: ulid@3.0.2: {} - ultrahtml@1.6.0: {} - unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -34357,9 +29474,8 @@ snapshots: undici-types@7.16.0: {} - undici-types@7.19.2: {} - - undici@6.24.1: {} + undici-types@7.19.2: + optional: true undici@7.24.8: {} @@ -34379,29 +29495,6 @@ snapshots: trough: 2.2.0 vfile: 6.0.3 - unifont@0.7.4: - dependencies: - css-tree: 3.2.1 - ofetch: 1.5.1 - ohash: 2.0.11 - - unimport@6.0.2: - dependencies: - acorn: 8.16.0 - escape-string-regexp: 5.0.0 - estree-walker: 3.0.3 - local-pkg: 1.1.2 - magic-string: 0.30.21 - mlly: 1.8.2 - pathe: 2.0.3 - picomatch: 4.0.4 - pkg-types: 2.3.0 - scule: 1.3.0 - strip-literal: 3.1.0 - tinyglobby: 0.2.16 - unplugin: 3.0.0 - unplugin-utils: 0.3.1 - unist-util-find-after@5.0.0: dependencies: '@types/unist': 3.0.3 @@ -34489,8 +29582,6 @@ snapshots: universal-user-agent@7.0.3: {} - universalify@2.0.1: {} - unix-dgram@2.0.7: dependencies: bindings: 1.5.0 @@ -34505,11 +29596,6 @@ snapshots: unpipe@1.0.0: {} - unplugin-utils@0.3.1: - dependencies: - pathe: 2.0.3 - picomatch: 4.0.4 - unplugin@2.3.11: dependencies: '@jridgewell/remapping': 2.3.5 @@ -34517,12 +29603,6 @@ snapshots: picomatch: 4.0.4 webpack-virtual-modules: 0.6.2 - unplugin@3.0.0: - dependencies: - '@jridgewell/remapping': 2.3.5 - picomatch: 4.0.4 - webpack-virtual-modules: 0.6.2 - unstorage@1.17.5(@netlify/blobs@10.1.0)(aws4fetch@1.0.20)(ioredis@5.9.2): dependencies: anymatch: 3.1.3 @@ -34549,7 +29629,7 @@ snapshots: ofetch: 1.5.1 ufo: 1.6.3 optionalDependencies: - '@netlify/blobs': 10.7.4 + '@netlify/blobs': 10.7.4(supports-color@10.2.2) aws4fetch: 1.0.20 ioredis: 5.9.2 @@ -34628,18 +29708,12 @@ snapshots: lodash.debounce: 4.0.8 react: 19.2.5 - userhome@1.0.1: {} - util-deprecate@1.0.2: {} - uuid@10.0.0: {} - uuid@11.1.0: {} uuid@13.0.0: {} - uuid@8.3.2: {} - uuid@9.0.1: {} uvu@0.5.6: @@ -34698,44 +29772,6 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-node@6.0.0(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3): - dependencies: - cac: 7.0.0 - es-module-lexer: 2.0.0 - obug: 2.1.1 - pathe: 2.0.3 - vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - transitivePeerDependencies: - - '@types/node' - - '@vitejs/devtools' - - esbuild - - jiti - - less - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - yaml - - vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3): - dependencies: - esbuild: 0.27.7 - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - postcss: 8.5.9 - rollup: 4.60.1 - tinyglobby: 0.2.16 - optionalDependencies: - '@types/node': 25.6.0 - fsevents: 2.3.3 - jiti: 2.6.1 - lightningcss: 1.30.1 - terser: 5.46.1 - tsx: 4.21.0 - yaml: 2.8.3 - vite@8.0.8(@types/node@22.19.17)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3): dependencies: lightningcss: 1.30.1 @@ -34800,10 +29836,6 @@ snapshots: tsx: 4.21.0 yaml: 2.8.3 - vitefu@1.1.3(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): - optionalDependencies: - vite: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - vitefu@1.1.3(vite@8.0.8(@types/node@22.19.17)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): optionalDependencies: vite: 8.0.8(@types/node@22.19.17)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) @@ -34958,84 +29990,6 @@ snapshots: transitivePeerDependencies: - msw - volar-service-css@0.0.70(@volar/language-service@2.4.28): - dependencies: - vscode-css-languageservice: 6.3.10 - vscode-languageserver-textdocument: 1.0.12 - vscode-uri: 3.1.0 - optionalDependencies: - '@volar/language-service': 2.4.28 - - volar-service-emmet@0.0.70(@volar/language-service@2.4.28): - dependencies: - '@emmetio/css-parser': 0.4.1 - '@emmetio/html-matcher': 1.3.0 - '@vscode/emmet-helper': 2.11.0 - vscode-uri: 3.1.0 - optionalDependencies: - '@volar/language-service': 2.4.28 - - volar-service-html@0.0.70(@volar/language-service@2.4.28): - dependencies: - vscode-html-languageservice: 5.6.2 - vscode-languageserver-textdocument: 1.0.12 - vscode-uri: 3.1.0 - optionalDependencies: - '@volar/language-service': 2.4.28 - - volar-service-prettier@0.0.70(@volar/language-service@2.4.28)(prettier@3.8.2): - dependencies: - vscode-uri: 3.1.0 - optionalDependencies: - '@volar/language-service': 2.4.28 - prettier: 3.8.2 - - volar-service-typescript-twoslash-queries@0.0.70(@volar/language-service@2.4.28): - dependencies: - vscode-uri: 3.1.0 - optionalDependencies: - '@volar/language-service': 2.4.28 - - volar-service-typescript@0.0.70(@volar/language-service@2.4.28): - dependencies: - path-browserify: 1.0.1 - semver: 7.7.4 - typescript-auto-import-cache: 0.3.6 - vscode-languageserver-textdocument: 1.0.12 - vscode-nls: 5.2.0 - vscode-uri: 3.1.0 - optionalDependencies: - '@volar/language-service': 2.4.28 - - volar-service-yaml@0.0.70(@volar/language-service@2.4.28): - dependencies: - vscode-uri: 3.1.0 - yaml-language-server: 1.20.0 - optionalDependencies: - '@volar/language-service': 2.4.28 - - vscode-css-languageservice@6.3.10: - dependencies: - '@vscode/l10n': 0.0.18 - vscode-languageserver-textdocument: 1.0.12 - vscode-languageserver-types: 3.17.5 - vscode-uri: 3.1.0 - - vscode-html-languageservice@5.6.2: - dependencies: - '@vscode/l10n': 0.0.18 - vscode-languageserver-textdocument: 1.0.12 - vscode-languageserver-types: 3.17.5 - vscode-uri: 3.1.0 - - vscode-json-languageservice@4.1.8: - dependencies: - jsonc-parser: 3.3.1 - vscode-languageserver-textdocument: 1.0.12 - vscode-languageserver-types: 3.17.5 - vscode-nls: 5.2.0 - vscode-uri: 3.1.0 - vscode-jsonrpc@8.2.0: {} vscode-languageserver-protocol@3.17.5: @@ -35051,8 +30005,6 @@ snapshots: dependencies: vscode-languageserver-protocol: 3.17.5 - vscode-nls@5.2.0: {} - vscode-uri@3.1.0: {} w3c-keyname@2.2.8: {} @@ -35061,114 +30013,18 @@ snapshots: dependencies: xml-name-validator: 5.0.0 - wait-port@1.1.0: - dependencies: - chalk: 4.1.2 - commander: 9.5.0 - debug: 4.4.3(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color - - watchpack@2.4.4: - dependencies: - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - wavesurfer.js@7.12.6: {} wcwidth@1.0.1: dependencies: defaults: 1.0.4 - web-ext-run@0.2.4: - dependencies: - '@babel/runtime': 7.28.2 - '@devicefarmer/adbkit': 3.3.8 - chrome-launcher: 1.2.0 - debounce: 1.2.1 - es6-error: 4.1.1 - firefox-profile: 4.7.0 - fx-runner: 1.4.0 - multimatch: 6.0.0 - node-notifier: 10.0.1 - parse-json: 7.1.1 - pino: 9.7.0 - promise-toolbox: 0.21.0 - set-value: 4.1.0 - source-map-support: 0.5.21 - strip-bom: 5.0.0 - strip-json-comments: 5.0.2 - tmp: 0.2.5 - update-notifier: 7.3.1 - watchpack: 2.4.4 - zip-dir: 2.0.0 - transitivePeerDependencies: - - supports-color - web-namespaces@2.0.1: {} web-streams-polyfill@3.3.3: {} web-vitals@5.2.0: {} - webdriver@9.27.0: - dependencies: - '@types/node': 20.19.39 - '@types/ws': 8.18.1 - '@wdio/config': 9.27.0 - '@wdio/logger': 9.18.0 - '@wdio/protocols': 9.27.0 - '@wdio/types': 9.27.0 - '@wdio/utils': 9.27.0 - deepmerge-ts: 7.1.5 - https-proxy-agent: 7.0.6 - undici: 6.24.1 - ws: 8.20.0 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - bufferutil - - react-native-b4a - - supports-color - - utf-8-validate - - webdriverio@9.27.0(puppeteer-core@22.15.0): - dependencies: - '@types/node': 20.19.39 - '@types/sinonjs__fake-timers': 8.1.5 - '@wdio/config': 9.27.0 - '@wdio/logger': 9.18.0 - '@wdio/protocols': 9.27.0 - '@wdio/repl': 9.16.2 - '@wdio/types': 9.27.0 - '@wdio/utils': 9.27.0 - archiver: 7.0.1 - aria-query: 5.3.2 - cheerio: 1.2.0 - css-shorthand-properties: 1.1.2 - css-value: 0.0.1 - grapheme-splitter: 1.0.4 - htmlfy: 0.8.1 - is-plain-obj: 4.1.0 - jszip: 3.10.1 - lodash.clonedeep: 4.5.0 - lodash.zip: 4.2.0 - query-selector-shadow-dom: 1.0.1 - resq: 1.11.0 - rgb2hex: 0.2.5 - serialize-error: 12.0.0 - urlpattern-polyfill: 10.1.0 - webdriver: 9.27.0 - optionalDependencies: - puppeteer-core: 22.15.0 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - bufferutil - - react-native-b4a - - supports-color - - utf-8-validate - webidl-conversions@3.0.1: {} webidl-conversions@7.0.0: {} @@ -35206,8 +30062,6 @@ snapshots: when-exit@2.1.5: {} - when@3.7.7: {} - which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -35239,8 +30093,6 @@ snapshots: is-weakmap: 2.0.2 is-weakset: 2.0.4 - which-pm-runs@1.1.0: {} - which-typed-array@1.1.20: dependencies: available-typed-arrays: 1.0.7 @@ -35251,11 +30103,6 @@ snapshots: gopd: 1.2.0 has-tostringtag: 1.0.2 - which@1.2.4: - dependencies: - is-absolute: 0.1.7 - isexe: 1.1.2 - which@2.0.2: dependencies: isexe: 2.0.0 @@ -35264,10 +30111,6 @@ snapshots: dependencies: isexe: 3.1.5 - which@6.0.1: - dependencies: - isexe: 4.0.0 - why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 @@ -35283,14 +30126,6 @@ snapshots: dependencies: execa: 8.0.1 - winreg@0.0.12: {} - - winston-console-format@1.0.8: - dependencies: - colors: 1.4.0 - logform: 2.7.0 - triple-beam: 1.4.1 - winston-transport@4.9.0: dependencies: logform: 2.7.0 @@ -35313,14 +30148,6 @@ snapshots: word-wrap@1.2.5: {} - workerpool@6.5.1: {} - - wrap-ansi@10.0.0: - dependencies: - ansi-styles: 6.2.3 - string-width: 8.2.0 - strip-ansi: 7.2.0 - wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -35360,88 +30187,15 @@ snapshots: ws@8.20.0: {} - wsl-utils@0.1.0: - dependencies: - is-wsl: 3.1.1 - wsl-utils@0.3.1: dependencies: is-wsl: 3.1.1 powershell-utils: 0.1.0 - wxt@0.20.20(@types/node@25.6.0)(eslint@9.39.4(jiti@2.6.1))(jiti@2.6.1)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3): - dependencies: - '@1natsu/wait-element': 4.2.0 - '@aklinker1/rollup-plugin-visualizer': 5.12.0(rollup@4.60.1) - '@webext-core/fake-browser': 1.3.4 - '@webext-core/isolated-element': 1.1.5 - '@webext-core/match-patterns': 1.0.3 - '@wxt-dev/browser': 0.1.40 - '@wxt-dev/storage': 1.2.8 - async-mutex: 0.5.0 - c12: 3.3.4(magicast@0.5.2) - cac: 7.0.0 - chokidar: 5.0.0 - ci-info: 4.4.0 - consola: 3.4.2 - defu: 6.1.7 - dotenv: 17.4.2 - dotenv-expand: 12.0.3 - esbuild: 0.27.7 - filesize: 11.0.15 - get-port-please: 3.2.0 - giget: 3.2.0 - hookable: 6.1.0 - import-meta-resolve: 4.2.0 - is-wsl: 3.1.1 - json5: 2.2.3 - jszip: 3.10.1 - linkedom: 0.18.12 - magicast: 0.5.2 - minimatch: 10.2.5 - nano-spawn: 2.1.0 - nanospinner: 1.2.2 - normalize-path: 3.0.0 - nypm: 0.6.5 - ohash: 2.0.11 - open: 11.0.0 - perfect-debounce: 2.1.0 - picocolors: 1.1.1 - prompts: 2.4.2 - publish-browser-extension: 4.0.5 - scule: 1.3.0 - tinyglobby: 0.2.16 - unimport: 6.0.2 - vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - vite-node: 6.0.0(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - web-ext-run: 0.2.4 - optionalDependencies: - eslint: 9.39.4(jiti@2.6.1) - transitivePeerDependencies: - - '@types/node' - - '@vitejs/devtools' - - canvas - - jiti - - less - - rollup - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - xdg-basedir@5.1.0: {} xml-name-validator@5.0.0: {} - xml2js@0.6.2: - dependencies: - sax: 1.6.0 - xmlbuilder: 11.0.1 - xmlbuilder2@4.0.3: dependencies: '@oozcitak/dom': 2.0.2 @@ -35449,8 +30203,6 @@ snapshots: '@oozcitak/util': 10.0.0 js-yaml: 4.1.1 - xmlbuilder@11.0.1: {} - xmlbuilder@15.1.1: {} xmlchars@2.2.0: {} @@ -35462,8 +30214,6 @@ snapshots: xtend@4.0.2: {} - xxhash-wasm@1.1.0: {} - y18n@5.0.8: {} yallist@3.1.1: {} @@ -35472,49 +30222,12 @@ snapshots: yallist@5.0.0: {} - yaml-language-server@1.20.0: - dependencies: - '@vscode/l10n': 0.0.18 - ajv: 8.18.0 - ajv-draft-04: 1.0.0(ajv@8.18.0) - prettier: 3.8.2 - request-light: 0.5.8 - vscode-json-languageservice: 4.1.8 - vscode-languageserver: 9.0.1 - vscode-languageserver-textdocument: 1.0.12 - vscode-languageserver-types: 3.17.5 - vscode-uri: 3.1.0 - yaml: 2.7.1 - yaml@1.10.3: {} - yaml@2.7.1: {} - yaml@2.8.3: {} - yargs-parser@20.2.9: {} - yargs-parser@21.1.1: {} - yargs-parser@22.0.0: {} - - yargs-unparser@2.0.0: - dependencies: - camelcase: 6.3.0 - decamelize: 4.0.0 - flat: 5.0.2 - is-plain-obj: 2.1.0 - - yargs@16.2.0: - dependencies: - cliui: 7.0.4 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 20.2.9 - yargs@17.7.2: dependencies: cliui: 8.0.1 @@ -35535,8 +30248,6 @@ snapshots: buffer-crc32: 0.2.13 pend: 1.2.0 - yesql@7.0.0: {} - yn@3.1.1: {} yocto-queue@0.1.0: {} @@ -35547,15 +30258,8 @@ snapshots: dependencies: yoctocolors: 2.1.2 - yoctocolors-cjs@2.1.3: {} - yoctocolors@2.1.2: {} - zip-dir@2.0.0: - dependencies: - async: 3.2.6 - jszip: 3.10.1 - zip-stream@6.0.1: dependencies: archiver-utils: 5.0.2 @@ -35566,11 +30270,6 @@ snapshots: dependencies: zod: 3.25.76 - zod-to-json-schema@3.25.2(zod@4.3.6): - dependencies: - zod: 4.3.6 - optional: true - zod@3.23.8: optional: true diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 256d6804fe..069b4f3d54 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,7 +2,6 @@ packages: - apps/* - packages/* - plugins/* - - examples/plugins/* - e2e/* onlyBuiltDependencies: diff --git a/render.yaml b/render.yaml index 6aa772ea1f..9ee2f0435e 100644 --- a/render.yaml +++ b/render.yaml @@ -35,15 +35,3 @@ services: sync: false - key: HONEYCOMB_API_ENDPOINT value: api.honeycomb.io:443 - - - type: web - name: hyprnote-api - runtime: docker - plan: starter - region: oregon - numInstances: 1 - dockerfilePath: ./apps/api/Dockerfile - dockerContext: . - autoDeployTrigger: commit - healthCheckPath: /health - maxShutdownDelaySeconds: 30 diff --git a/skills/cli/SKILL.md b/skills/cli/SKILL.md new file mode 100644 index 0000000000..e76cf16ec9 --- /dev/null +++ b/skills/cli/SKILL.md @@ -0,0 +1,9 @@ +--- +name: char-cli +description: Live transcription, recording, and audio tools CLI. Use when the user needs to transcribe audio, record, play audio files, or manage speech-to-text models. +--- + +# Resources + +- `https://cli.char.com` +- `https://github.com/fastrepl/char/tree/main/apps/cli/src`