diff --git a/.github/workflows/.size-baseline b/.github/workflows/.size-baseline index f920373a836..af915611f12 100644 --- a/.github/workflows/.size-baseline +++ b/.github/workflows/.size-baseline @@ -26,8 +26,6 @@ 10005 dsw-swe-verified-release.yml 12340 e2e.yml 11394 finalize-release.yml -15871 live-host-release.yml -6384 live-host.yml 7642 main-ci-failure-issue.yml 1686 npm-cache.yml 7299 pr-force-push-reminder.yml @@ -56,7 +54,6 @@ 17013 serve-ab.yml 2641 stale.yml 10920 sync-desktop-to-oss.yml -10018 sync-live-host-to-oss.yml 10138 sync-release-to-oss.yml 3303 update-ecs-runner-qwen.yml 2307 web-shell-visuals-cleanup.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a7e4026e9d..a4d83733d3c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -389,10 +389,6 @@ jobs: if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}" run: 'npm run check:desktop-isolation' - - name: 'Check voice guard mirror sync' - if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}" - run: 'npm run check:voice-guard-sync' - - name: 'Install linters' if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}" run: 'node scripts/lint.js --setup' diff --git a/.github/workflows/live-host-release.yml b/.github/workflows/live-host-release.yml deleted file mode 100644 index 290d2c59b31..00000000000 --- a/.github/workflows/live-host-release.yml +++ /dev/null @@ -1,363 +0,0 @@ -name: 'Qwen Live Host Release' - -run-name: "Qwen Live Host release ${{ inputs.version || format('PR #{0} dry run', github.event.pull_request.number) }}" - -on: - pull_request: - paths: - - '.github/workflows/live-host-release.yml' - - 'packages/desktop/apps/live-host/**' - - 'packages/desktop/bun.lock' - - 'packages/desktop/package.json' - - 'packages/desktop/scripts/bump-live-host-version.ts' - workflow_dispatch: - inputs: - version: - description: 'Live Host version, for example 0.1.0 or v0.1.0.' - required: true - type: 'string' - dry_run: - description: 'Build unsigned packages without publishing.' - required: true - default: true - type: 'boolean' - draft: - description: 'Create a draft GitHub release.' - required: true - default: true - type: 'boolean' - prerelease: - description: 'Mark the release as a prerelease.' - required: true - default: false - type: 'boolean' - clobber: - description: 'Replace same-named assets in an existing release.' - required: true - default: false - type: 'boolean' - -permissions: - actions: 'read' - contents: 'read' - -concurrency: - group: 'live-host-release-${{ github.event.pull_request.number || inputs.version }}' - cancel-in-progress: false - -env: - BUN_VERSION: '1.3.9' - NODE_VERSION: '22.20.0' - LIVE_HOST_FEED_TAG: 'live-host-latest' - -jobs: - prepare: - name: 'Prepare Live Host release' - runs-on: 'ubuntu-latest' - timeout-minutes: 10 - outputs: - tag: '${{ steps.version.outputs.tag }}' - version: '${{ steps.version.outputs.version }}' - steps: - - name: 'Resolve version' - id: 'version' - shell: 'bash' - env: - INPUT_VERSION: '${{ inputs.version }}' - PR_NUMBER: '${{ github.event.pull_request.number }}' - run: | - set -euo pipefail - if [ "$GITHUB_EVENT_NAME" = 'pull_request' ]; then - version="0.0.$PR_NUMBER" - else - version="${INPUT_VERSION#v}" - fi - if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([+-][0-9A-Za-z.-]+)?$ ]]; then - echo "::error::Live Host version must be valid SemVer: $INPUT_VERSION" - exit 1 - fi - if [[ "$version" == *+* ]]; then - echo '::error::Live Host releases do not support SemVer build metadata.' - exit 1 - fi - if [ "$GITHUB_EVENT_NAME" = 'workflow_dispatch' ] && [ "${{ inputs.dry_run }}" = 'false' ] && [ "$GITHUB_REF_NAME" != 'main' ]; then - echo '::error::Published Live Host releases must run from main.' - exit 1 - fi - echo "version=$version" >> "$GITHUB_OUTPUT" - echo "tag=live-host-v$version" >> "$GITHUB_OUTPUT" - - build: - name: 'Build Qwen Live Host' - needs: 'prepare' - runs-on: 'macos-latest' - timeout-minutes: 60 - steps: - - uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3 - - - uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0 - with: - node-version: '${{ env.NODE_VERSION }}' - - - uses: 'oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6' # v2 - with: - bun-version: '${{ env.BUN_VERSION }}' - - - name: 'Install dependencies' - working-directory: 'packages/desktop' - run: 'bun install --frozen-lockfile' - - - name: 'Set Live Host version' - working-directory: 'packages/desktop' - run: 'bun run bump-live-host-version "${{ needs.prepare.outputs.version }}"' - - - name: 'Test Live Host' - working-directory: 'packages/desktop' - run: 'bun run live-host:typecheck && bun run live-host:test' - - - name: 'Import macOS certificate' - if: "${{ github.event_name == 'workflow_dispatch' && inputs.dry_run == false }}" - shell: 'bash' - env: - APPLE_CERTIFICATE: '${{ secrets.APPLE_CERTIFICATE }}' - APPLE_CERTIFICATE_PASSWORD: '${{ secrets.APPLE_CERTIFICATE_PASSWORD }}' - LEGACY_APPLE_CERTIFICATE: '${{ secrets.MAC_CSC_LINK }}' - LEGACY_APPLE_CERTIFICATE_PASSWORD: '${{ secrets.MAC_CSC_KEY_PASSWORD }}' - KEYCHAIN_PASSWORD: '${{ secrets.APPLE_KEYCHAIN_PASSWORD }}' - run: | - set -euo pipefail - if [ -n "$APPLE_CERTIFICATE" ] && [ -n "$APPLE_CERTIFICATE_PASSWORD" ]; then - certificate_data="$APPLE_CERTIFICATE" - certificate_password="$APPLE_CERTIFICATE_PASSWORD" - elif [ -n "$LEGACY_APPLE_CERTIFICATE" ] && [ -n "$LEGACY_APPLE_CERTIFICATE_PASSWORD" ]; then - certificate_data="$LEGACY_APPLE_CERTIFICATE" - certificate_password="$LEGACY_APPLE_CERTIFICATE_PASSWORD" - else - echo '::error::A complete APPLE_CERTIFICATE pair or MAC_CSC pair is required for published Qwen Live Host releases.' - exit 1 - fi - keychain_password="${KEYCHAIN_PASSWORD:-$(openssl rand -hex 32)}" - certificate="$RUNNER_TEMP/qwen-live-host.p12" - keychain="$RUNNER_TEMP/qwen-live-host.keychain-db" - certificate_data="${certificate_data#*base64,}" - printf '%s' "$certificate_data" | base64 --decode > "$certificate" - security create-keychain -p "$keychain_password" "$keychain" - security set-keychain-settings -lut 21600 "$keychain" - security unlock-keychain -p "$keychain_password" "$keychain" - security import "$certificate" -P "$certificate_password" -A -t cert -f pkcs12 -k "$keychain" - security list-keychains -d user -s "$keychain" login.keychain-db - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$keychain_password" "$keychain" - identity="$(security find-identity -v -p codesigning "$keychain" | sed -n 's/.*"\(Developer ID Application:.*\)"/\1/p' | head -n 1)" - if [ -z "$identity" ]; then echo '::error::Developer ID Application identity was not found.'; exit 1; fi - identity_name="${identity#Developer ID Application: }" - { - echo "CSC_NAME=$identity_name" - echo 'CSC_IDENTITY_AUTO_DISCOVERY=true' - } >> "$GITHUB_ENV" - - - name: 'Configure notarization' - if: "${{ github.event_name == 'workflow_dispatch' && inputs.dry_run == false }}" - shell: 'bash' - env: - APPLE_API_ISSUER: '${{ secrets.APPLE_API_ISSUER }}' - APPLE_API_KEY_ID: '${{ secrets.APPLE_API_KEY }}' - APPLE_API_KEY_P8: '${{ secrets.APPLE_API_KEY_P8 }}' - LEGACY_APPLE_API_ISSUER: '${{ secrets.APPLE_NOTARY_ISSUER_ID }}' - LEGACY_APPLE_API_KEY_ID: '${{ secrets.APPLE_NOTARY_KEY_ID }}' - LEGACY_APPLE_API_KEY_P8: '${{ secrets.APPLE_NOTARY_API_KEY_P8_BASE64 }}' - APPLE_TEAM_ID: '${{ secrets.APPLE_TEAM_ID }}' - run: | - set -euo pipefail - if [ -n "$APPLE_API_ISSUER" ] && [ -n "$APPLE_API_KEY_ID" ] && [ -n "$APPLE_API_KEY_P8" ]; then - api_issuer="$APPLE_API_ISSUER" - api_key_id="$APPLE_API_KEY_ID" - key_path="$RUNNER_TEMP/AuthKey_${api_key_id}.p8" - printf '%s' "$APPLE_API_KEY_P8" > "$key_path" - elif [ -n "$LEGACY_APPLE_API_ISSUER" ] && [ -n "$LEGACY_APPLE_API_KEY_ID" ] && [ -n "$LEGACY_APPLE_API_KEY_P8" ]; then - api_issuer="$LEGACY_APPLE_API_ISSUER" - api_key_id="$LEGACY_APPLE_API_KEY_ID" - key_path="$RUNNER_TEMP/AuthKey_${api_key_id}.p8" - printf '%s' "$LEGACY_APPLE_API_KEY_P8" | base64 --decode > "$key_path" - else - echo '::error::A complete APPLE_API notarization set or APPLE_NOTARY set is required for Qwen Live Host notarization.' - exit 1 - fi - if [ -z "$APPLE_TEAM_ID" ]; then echo '::error::APPLE_TEAM_ID is required for Qwen Live Host notarization.'; exit 1; fi - { - echo "APPLE_API_KEY=$key_path" - echo "APPLE_API_KEY_ID=$api_key_id" - echo "APPLE_API_ISSUER=$api_issuer" - echo "APPLE_TEAM_ID=$APPLE_TEAM_ID" - } >> "$GITHUB_ENV" - - - name: 'Build packages' - working-directory: 'packages/desktop' - env: - CSC_IDENTITY_AUTO_DISCOVERY: "${{ github.event_name == 'workflow_dispatch' && inputs.dry_run == false }}" - run: 'bun run live-host:dist:mac:no-publish' - - - name: 'Verify release assets' - working-directory: 'packages/desktop/apps/live-host' - env: - RELEASE_VERSION: '${{ needs.prepare.outputs.version }}' - shell: 'bash' - run: | - set -euo pipefail - for asset in \ - Qwen-Live-Host-arm64.dmg \ - Qwen-Live-Host-x64.dmg \ - Qwen-Live-Host-arm64.zip \ - Qwen-Live-Host-x64.zip \ - Qwen-Live-Host-manifest.json; do - test -f "release/$asset" - done - node --input-type=module -e ' - import { createHash } from "node:crypto"; - import { readFileSync, statSync } from "node:fs"; - const manifest = JSON.parse(readFileSync("release/Qwen-Live-Host-manifest.json", "utf8")); - if (manifest.version !== process.env.RELEASE_VERSION) { - throw new Error("Manifest version " + manifest.version + " does not match " + process.env.RELEASE_VERSION + "."); - } - for (const architecture of ["arm64", "x64"]) { - const name = "Qwen-Live-Host-" + architecture + ".zip"; - const path = "release/" + name; - const bytes = readFileSync(path); - const asset = manifest.assets?.[architecture]; - if (asset?.name !== name || asset.size !== statSync(path).size || asset.sha256 !== createHash("sha256").update(bytes).digest("hex")) { - throw new Error("Manifest asset verification failed for " + architecture + "."); - } - } - ' - - - name: 'Verify signing and notarization' - if: "${{ github.event_name == 'workflow_dispatch' && inputs.dry_run == false }}" - working-directory: 'packages/desktop' - shell: 'bash' - run: | - set -euo pipefail - app_count=0 - while IFS= read -r -d '' app; do - app_count=$((app_count + 1)) - codesign --verify --deep --strict --verbose=2 "$app" - signature="$(codesign -dv --verbose=4 "$app" 2>&1)" - if ! grep -q '^Authority=Developer ID Application:' <<<"$signature" || ! grep -qx 'TeamIdentifier=NF4574S59H' <<<"$signature"; then - echo '::error::Qwen Live Host was not signed by the expected Developer ID team.' - exit 1 - fi - spctl -a -vv -t exec "$app" - xcrun stapler validate "$app" - done < <(find apps/live-host/release -mindepth 2 -maxdepth 2 -type d -name '*.app' -print0) - if [ "$app_count" -eq 0 ]; then echo '::error::No packaged Qwen Live Host application was found.'; exit 1; fi - dmg_count=0 - while IFS= read -r -d '' dmg; do - dmg_count=$((dmg_count + 1)) - hdiutil verify "$dmg" - done < <(find apps/live-host/release -type f -name '*.dmg' -print0) - if [ "$dmg_count" -eq 0 ]; then echo '::error::No Qwen Live Host disk image was found.'; exit 1; fi - - - uses: 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' # v7.0.1 - with: - name: 'qwen-live-host-macos' - path: | - packages/desktop/apps/live-host/release/*.dmg - packages/desktop/apps/live-host/release/*-manifest.json - packages/desktop/apps/live-host/release/*.zip - if-no-files-found: 'error' - retention-days: 14 - - publish: - name: 'Publish Qwen Live Host release' - if: "${{ github.event_name == 'workflow_dispatch' && inputs.dry_run == false && github.repository == 'QwenLM/qwen-code' }}" - needs: - - 'prepare' - - 'build' - runs-on: 'ubuntu-latest' - timeout-minutes: 20 - permissions: - contents: 'write' - env: - GH_REPO: '${{ github.repository }}' - steps: - - uses: 'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c' # v8.0.1 - with: - path: 'release-assets' - merge-multiple: true - - - name: 'Generate checksums' - working-directory: 'release-assets' - run: 'sha256sum -- * > SHA256SUMS.txt' - - - name: 'Create GitHub release' - id: 'release' - env: - GH_TOKEN: '${{ github.token }}' - RELEASE_TAG: '${{ needs.prepare.outputs.tag }}' - RELEASE_VERSION: '${{ needs.prepare.outputs.version }}' - RELEASE_DRAFT: '${{ inputs.draft }}' - RELEASE_PRERELEASE: '${{ inputs.prerelease }}' - RELEASE_CLOBBER: '${{ inputs.clobber }}' - run: | - set -euo pipefail - args=("$RELEASE_TAG" release-assets/* --target "$GITHUB_SHA" --title "Qwen Live Host v$RELEASE_VERSION" --generate-notes --latest=false) - if [ "$RELEASE_DRAFT" = 'true' ]; then args+=(--draft); fi - if [ "$RELEASE_PRERELEASE" = 'true' ]; then args+=(--prerelease); fi - if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then - if [ "$RELEASE_CLOBBER" != 'true' ]; then - echo "::error::Release $RELEASE_TAG already exists. Re-run with clobber=true to replace its assets." - exit 1 - fi - gh release upload "$RELEASE_TAG" release-assets/* --clobber - edit_args=() - if [ "$RELEASE_DRAFT" = 'true' ]; then edit_args+=(--draft); else edit_args+=(--draft=false); fi - if [ "$RELEASE_PRERELEASE" = 'true' ]; then edit_args+=(--prerelease); else edit_args+=(--prerelease=false); fi - gh release edit "$RELEASE_TAG" "${edit_args[@]}" - release_url="$(gh release view "$RELEASE_TAG" --json url --jq '.url')" - else - release_url="$(gh release create "${args[@]}")" - fi - echo "url=$release_url" >> "$GITHUB_OUTPUT" - - - name: 'Update stable Live Host feed' - if: '${{ inputs.draft == false && inputs.prerelease == false }}' - env: - GH_TOKEN: '${{ github.token }}' - FEED_TAG: '${{ env.LIVE_HOST_FEED_TAG }}' - run: | - set -euo pipefail - stable_assets=( - release-assets/Qwen-Live-Host-manifest.json - release-assets/Qwen-Live-Host-arm64.zip - release-assets/Qwen-Live-Host-x64.zip - ) - if gh release view "$FEED_TAG" >/dev/null 2>&1; then - gh release upload "$FEED_TAG" "${stable_assets[@]}" --clobber - else - gh release create "$FEED_TAG" "${stable_assets[@]}" --title 'Qwen Live Host latest' --notes 'Stable Qwen Live Host installer feed.' --latest=false - fi - - - name: 'Publish release summary' - env: - RELEASE_URL: '${{ steps.release.outputs.url }}' - RELEASE_VERSION: '${{ needs.prepare.outputs.version }}' - run: | - { - echo '## Qwen Live Host release' - echo - echo "Version: $RELEASE_VERSION" - echo "Release: $RELEASE_URL" - } >> "$GITHUB_STEP_SUMMARY" - - sync-oss: - name: 'Mirror stable Qwen Live Host release to Aliyun OSS' - if: "${{ github.event_name == 'workflow_dispatch' && inputs.dry_run == false && inputs.draft == false && inputs.prerelease == false && github.repository == 'QwenLM/qwen-code' }}" - needs: - - 'prepare' - - 'build' - - 'publish' - uses: './.github/workflows/sync-live-host-to-oss.yml' - with: - version: '${{ needs.prepare.outputs.version }}' - source: 'artifact' - secrets: - ALIYUN_OSS_ACCESS_KEY_ID: '${{ secrets.ALIYUN_OSS_ACCESS_KEY_ID }}' - ALIYUN_OSS_ACCESS_KEY_SECRET: '${{ secrets.ALIYUN_OSS_ACCESS_KEY_SECRET }}' diff --git a/.github/workflows/live-host.yml b/.github/workflows/live-host.yml deleted file mode 100644 index deb5db5a4ce..00000000000 --- a/.github/workflows/live-host.yml +++ /dev/null @@ -1,148 +0,0 @@ -name: 'Qwen Live Host CI' - -on: - pull_request: - paths: - - '.github/workflows/live-host.yml' - - '.github/workflows/live-host-release.yml' - - 'packages/cli/src/serve/conversations/**' - - 'packages/cli/src/serve/live/**' - - 'packages/desktop/apps/live-host/**' - - 'packages/desktop/bun.lock' - - 'packages/desktop/package.json' - - 'packages/desktop/scripts/bump-live-host-version.ts' - - 'packages/sdk-typescript/src/daemon/types.ts' - merge_group: - workflow_dispatch: - -permissions: - contents: 'read' - -concurrency: - group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}' - cancel-in-progress: true - -jobs: - test: - name: 'Live Host (macos-latest)' - runs-on: 'macos-latest' - timeout-minutes: 30 - steps: - - name: 'Check out source' - uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3 - - - name: 'Set up Node' - uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0 - with: - node-version-file: '.nvmrc' - - - name: 'Set up Bun' - uses: 'oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6' # v2 - with: - bun-version: '1.3.9' - - - name: 'Install desktop dependencies' - working-directory: 'packages/desktop' - run: 'bun install --frozen-lockfile' - - - name: 'Typecheck Live Host' - working-directory: 'packages/desktop' - run: 'bun run live-host:typecheck' - - - name: 'Test Live Host' - working-directory: 'packages/desktop' - run: 'bun run live-host:test' - - - name: 'Build Live Host' - working-directory: 'packages/desktop' - run: 'bun run live-host:build' - - - name: 'Package unsigned Live Host app' - working-directory: 'packages/desktop/apps/live-host' - run: | - set -euo pipefail - - case "$(uname -m)" in - arm64) builder_arch='--arm64' ;; - x86_64) builder_arch='--x64' ;; - *) echo "::error::Unsupported runner architecture: $(uname -m)"; exit 1 ;; - esac - - CSC_IDENTITY_AUTO_DISCOVERY=false bunx electron-builder --config electron-builder.yml --mac dir "$builder_arch" - - app="$(find release -type d -name 'Qwen Live Host.app' -print -quit)" - if [ -z "$app" ]; then - echo '::error::Packaged Qwen Live Host.app was not found.' - exit 1 - fi - - test -x "$app/Contents/MacOS/Qwen Live Host" - test -f "$app/Contents/Resources/app.asar" - - asar_entries="$(node ../../node_modules/@electron/asar/bin/asar.mjs list "$app/Contents/Resources/app.asar")" - if printf '%s\n' "$asar_entries" | grep -Eiq '(^|/)(native/|.*command[-_]?monitor|.*commandtap)'; then - echo '::error::Packaged app.asar contains a removed keyboard monitor or native helper.' - exit 1 - fi - native_files="$(find "$app/Contents/Resources/native" -type f -print 2>/dev/null || true)" - expected_native="$app/Contents/Resources/native/qwen-live-appshot.node" - if [ "$native_files" != "$expected_native" ] || find "$app/Contents/Resources" -type f -print | grep -Eiq 'command[-_]?monitor|commandtap'; then - echo '::error::Packaged resources do not contain exactly the built-in Appshot module.' - exit 1 - fi - - extracted_asar="$(mktemp -d)" - packaged_entitlements="$(mktemp)" - trap 'rm -rf "$extracted_asar"; rm -f "$packaged_entitlements"' EXIT - node ../../node_modules/@electron/asar/bin/asar.mjs extract "$app/Contents/Resources/app.asar" "$extracted_asar" - test -d "$extracted_asar/dist" - if grep -RIEq 'openWebShellWindow|web-shell-security|host\.open_session|inputMonitoring|installUrl|loadURL\(|@modelcontextprotocol|node:child_process' "$extracted_asar/dist"; then - echo '::error::Packaged app contains a forbidden external backend, process launcher, WebShell window, or Input Monitoring path.' - exit 1 - fi - - info_plist="$app/Contents/Info.plist" - if /usr/libexec/PlistBuddy -c 'Print :NSInputMonitoringUsageDescription' "$info_plist" >/dev/null 2>&1; then - echo '::error::Packaged app declares an Input Monitoring usage description.' - exit 1 - fi - for unused_permission in NSBluetoothAlwaysUsageDescription NSBluetoothPeripheralUsageDescription NSCameraUsageDescription; do - if /usr/libexec/PlistBuddy -c "Print :$unused_permission" "$info_plist" >/dev/null 2>&1; then - echo "::error::Packaged app declares unused permission $unused_permission." - exit 1 - fi - done - /usr/libexec/PlistBuddy -c 'Print :NSMicrophoneUsageDescription' "$info_plist" >/dev/null - - assert_live_host_entitlements() { - local entitlements_file="$1" - /usr/bin/plutil -convert json -o - "$entitlements_file" | node -e ' - const chunks = []; - process.stdin.on("data", (chunk) => chunks.push(chunk)); - process.stdin.on("end", () => { - const value = JSON.parse(Buffer.concat(chunks).toString("utf8")); - const expected = [ - "com.apple.security.cs.allow-jit", - "com.apple.security.cs.allow-unsigned-executable-memory", - "com.apple.security.device.audio-input", - ]; - const actual = Object.keys(value).sort(); - if ( - actual.length !== expected.length || - actual.some((key, index) => key !== expected[index]) || - expected.some((key) => value[key] !== true) - ) { - console.error( - "Unexpected Live Host entitlements: " + JSON.stringify(value), - ); - process.exit(1); - } - }); - ' - } - - assert_live_host_entitlements build/entitlements.mac.plist - /usr/bin/codesign --force --deep --sign - --entitlements build/entitlements.mac.plist "$app" - /usr/bin/codesign --verify --deep --strict "$app" - /usr/bin/codesign -d --entitlements :- "$app" >"$packaged_entitlements" 2>/dev/null - assert_live_host_entitlements "$packaged_entitlements" diff --git a/.github/workflows/sync-live-host-to-oss.yml b/.github/workflows/sync-live-host-to-oss.yml deleted file mode 100644 index ef2d38105f8..00000000000 --- a/.github/workflows/sync-live-host-to-oss.yml +++ /dev/null @@ -1,222 +0,0 @@ -name: 'Sync Qwen Live Host to Aliyun OSS' - -on: - workflow_call: - inputs: - version: - required: true - type: 'string' - source: - required: true - type: 'string' - secrets: - ALIYUN_OSS_ACCESS_KEY_ID: - required: true - ALIYUN_OSS_ACCESS_KEY_SECRET: - required: true - workflow_dispatch: - inputs: - version: - description: 'Stable Live Host version to mirror, for example 0.1.0 or v0.1.0.' - required: true - type: 'string' - source: - description: 'Download the assets from the matching GitHub release.' - required: true - default: 'release' - type: 'choice' - options: - - 'release' - -concurrency: - group: 'sync-live-host-to-oss' - cancel-in-progress: false - -jobs: - sync: - name: 'Mirror Qwen Live Host to Aliyun OSS' - if: "${{ github.repository == 'QwenLM/qwen-code' }}" - runs-on: 'ubuntu-latest' - timeout-minutes: 30 - environment: - name: 'production-release' - permissions: - actions: 'read' - contents: 'read' - steps: - - name: 'Checkout' - uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3 - - - name: 'Resolve release' - id: 'release' - env: - INPUT_VERSION: '${{ inputs.version }}' - INPUT_SOURCE: '${{ inputs.source }}' - run: | - set -euo pipefail - version="${INPUT_VERSION#v}" - if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "::error::Live Host OSS mirrors require a stable X.Y.Z version (got '$INPUT_VERSION')." - exit 1 - fi - if [[ "$INPUT_SOURCE" != 'artifact' && "$INPUT_SOURCE" != 'release' ]]; then - echo "::error::Live Host mirror source must be artifact or release (got '$INPUT_SOURCE')." - exit 1 - fi - echo "version=$version" >> "$GITHUB_OUTPUT" - echo "source=$INPUT_SOURCE" >> "$GITHUB_OUTPUT" - - - name: 'Download release workflow artifact' - if: "${{ steps.release.outputs.source == 'artifact' }}" - uses: 'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c' # v8.0.1 - with: - name: 'qwen-live-host-macos' - path: 'dist/live-host' - - - name: 'Download GitHub release assets' - if: "${{ steps.release.outputs.source == 'release' }}" - env: - GH_TOKEN: '${{ github.token }}' - VERSION: '${{ steps.release.outputs.version }}' - run: | - set -euo pipefail - metadata="$(gh release view "live-host-v${VERSION}" --json isDraft,isPrerelease)" - if ! jq -e '.isDraft == false and .isPrerelease == false' <<<"$metadata" >/dev/null; then - echo "::error::live-host-v${VERSION} is not a published stable release." - exit 1 - fi - mkdir -p dist/live-host - gh release download "live-host-v${VERSION}" \ - --dir dist/live-host \ - --pattern 'Qwen-Live-Host-manifest.json' \ - --pattern 'Qwen-Live-Host-arm64.zip' \ - --pattern 'Qwen-Live-Host-x64.zip' - - - name: 'Verify release assets' - env: - VERSION: '${{ steps.release.outputs.version }}' - run: | - set -euo pipefail - # shellcheck disable=SC2016 - node --input-type=module -e ' - import { createHash } from "node:crypto"; - import { readFileSync, statSync } from "node:fs"; - const directory = "dist/live-host"; - const manifest = JSON.parse(readFileSync(`${directory}/Qwen-Live-Host-manifest.json`, "utf8")); - if (manifest.version !== process.env.VERSION) throw new Error(`Manifest version ${manifest.version} does not match ${process.env.VERSION}.`); - for (const architecture of ["arm64", "x64"]) { - const name = `Qwen-Live-Host-${architecture}.zip`; - const file = `${directory}/${name}`; - const asset = manifest.assets?.[architecture]; - const checksum = createHash("sha256").update(readFileSync(file)).digest("hex"); - if (asset?.name !== name || asset.size !== statSync(file).size || asset.sha256 !== checksum) throw new Error(`Manifest asset verification failed for ${architecture}.`); - } - ' - - - name: 'Install ossutil' - env: - OSSUTIL_URL: "${{ vars.OSSUTIL_URL || 'https://gosspublic.alicdn.com/ossutil/1.7.19/ossutil-v1.7.19-linux-amd64.zip' }}" - OSSUTIL_SHA256: "${{ vars.OSSUTIL_SHA256 || 'dcc512e4a893e16bbee63bc769339d8e56b21744fd83c8212a9d8baf28767343' }}" - run: | - set -euo pipefail - tmp_dir="$(mktemp -d)" - curl -fsSL --connect-timeout 15 --max-time 300 "$OSSUTIL_URL" -o "$tmp_dir/ossutil.zip" - echo "$OSSUTIL_SHA256 $tmp_dir/ossutil.zip" | sha256sum -c - - unzip -q "$tmp_dir/ossutil.zip" -d "$tmp_dir" - ossutil_path="$(find "$tmp_dir" -type f \( -name 'ossutil' -o -name 'ossutil64' \) -print -quit)" - if [[ -z "$ossutil_path" ]]; then echo '::error::ossutil binary not found'; exit 1; fi - chmod +x "$ossutil_path" - mkdir -p "$HOME/.local/bin" - install -m 0755 "$ossutil_path" "$HOME/.local/bin/ossutil" - echo "$HOME/.local/bin" >> "$GITHUB_PATH" - rm -rf "$tmp_dir" - "$HOME/.local/bin/ossutil" >/dev/null - - - name: 'Configure Aliyun OSS credentials' - env: - ALIYUN_OSS_ACCESS_KEY_ID: '${{ secrets.ALIYUN_OSS_ACCESS_KEY_ID }}' - ALIYUN_OSS_ACCESS_KEY_SECRET: '${{ secrets.ALIYUN_OSS_ACCESS_KEY_SECRET }}' - ALIYUN_OSS_ENDPOINT: "${{ vars.ALIYUN_OSS_ENDPOINT || 'https://oss-cn-hangzhou.aliyuncs.com' }}" - run: | - set -euo pipefail - if [[ -z "$ALIYUN_OSS_ACCESS_KEY_ID" || -z "$ALIYUN_OSS_ACCESS_KEY_SECRET" ]]; then - echo '::error::Missing Aliyun OSS credentials in the production-release environment.' - exit 1 - fi - ossutil config -e "$ALIYUN_OSS_ENDPOINT" -i "$ALIYUN_OSS_ACCESS_KEY_ID" -k "$ALIYUN_OSS_ACCESS_KEY_SECRET" -L EN -c "$RUNNER_TEMP/.ossutilconfig" - - - name: 'Upload versioned assets to Aliyun OSS' - env: - ALIYUN_OSS_BUCKET: "${{ vars.ALIYUN_OSS_BUCKET || 'qwen-code-assets' }}" - VERSION: '${{ steps.release.outputs.version }}' - run: | - set -euo pipefail - node scripts/upload-aliyun-oss-assets.js \ - --bucket "$ALIYUN_OSS_BUCKET" \ - --config "$RUNNER_TEMP/.ossutilconfig" \ - --prefix "live-host/v${VERSION}" \ - dist/live-host/Qwen-Live-Host-arm64.zip \ - dist/live-host/Qwen-Live-Host-x64.zip \ - dist/live-host/Qwen-Live-Host-manifest.json - - - name: 'Verify versioned assets on Aliyun OSS' - env: - ALIYUN_OSS_PUBLIC_BASE_URL: "${{ vars.ALIYUN_OSS_PUBLIC_BASE_URL || 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com' }}" - VERSION: '${{ steps.release.outputs.version }}' - run: | - set -euo pipefail - base="$ALIYUN_OSS_PUBLIC_BASE_URL/live-host/v${VERSION}" - directory="$(mktemp -d)" - trap 'rm -rf "$directory"' EXIT - for asset in Qwen-Live-Host-manifest.json Qwen-Live-Host-arm64.zip Qwen-Live-Host-x64.zip; do - curl -fsSL --connect-timeout 15 --max-time 3600 "$base/$asset" -o "$directory/$asset" - done - # shellcheck disable=SC2016 - VERSION="$VERSION" DIRECTORY="$directory" node --input-type=module -e ' - import { createHash } from "node:crypto"; - import { readFileSync, statSync } from "node:fs"; - const manifest = JSON.parse(readFileSync(`${process.env.DIRECTORY}/Qwen-Live-Host-manifest.json`, "utf8")); - if (manifest.version !== process.env.VERSION) throw new Error("Mirrored manifest version mismatch."); - for (const architecture of ["arm64", "x64"]) { - const name = `Qwen-Live-Host-${architecture}.zip`; - const file = `${process.env.DIRECTORY}/${name}`; - const asset = manifest.assets?.[architecture]; - const checksum = createHash("sha256").update(readFileSync(file)).digest("hex"); - if (asset?.name !== name || asset.size !== statSync(file).size || asset.sha256 !== checksum) throw new Error(`Mirrored asset verification failed for ${architecture}.`); - } - ' - - - name: 'Confirm latest manifest matches GitHub stable feed' - env: - GH_TOKEN: '${{ github.token }}' - run: | - set -euo pipefail - directory="$(mktemp -d)" - trap 'rm -rf "$directory"' EXIT - gh release download 'live-host-latest' \ - --dir "$directory" \ - --pattern 'Qwen-Live-Host-manifest.json' - cmp dist/live-host/Qwen-Live-Host-manifest.json \ - "$directory/Qwen-Live-Host-manifest.json" - - - name: 'Publish latest manifest to Aliyun OSS' - env: - ALIYUN_OSS_BUCKET: "${{ vars.ALIYUN_OSS_BUCKET || 'qwen-code-assets' }}" - run: | - node scripts/upload-aliyun-oss-assets.js \ - --bucket "$ALIYUN_OSS_BUCKET" \ - --config "$RUNNER_TEMP/.ossutilconfig" \ - --prefix 'live-host/latest' \ - dist/live-host/Qwen-Live-Host-manifest.json - - - name: 'Verify latest manifest on Aliyun OSS' - env: - ALIYUN_OSS_PUBLIC_BASE_URL: "${{ vars.ALIYUN_OSS_PUBLIC_BASE_URL || 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com' }}" - run: | - set -euo pipefail - curl -fsSL --connect-timeout 15 --max-time 300 "$ALIYUN_OSS_PUBLIC_BASE_URL/live-host/latest/Qwen-Live-Host-manifest.json" -o "$RUNNER_TEMP/Qwen-Live-Host-manifest.json" - cmp dist/live-host/Qwen-Live-Host-manifest.json "$RUNNER_TEMP/Qwen-Live-Host-manifest.json" - - - name: 'Cleanup Aliyun OSS credentials' - if: '${{ always() }}' - run: 'rm -f "$RUNNER_TEMP/.ossutilconfig"' diff --git a/.prettierignore b/.prettierignore index 03f5711300b..bb330c55794 100644 --- a/.prettierignore +++ b/.prettierignore @@ -23,4 +23,3 @@ Thumbs.db packages/vscode-ide-companion/schemas/settings.schema.json packages/cli/src/services/insight/templates/insightTemplate.ts packages/cua-driver/ -packages/desktop/ diff --git a/.qwen/skills/desktop-pet/scripts/gen_spritesheet.py b/.qwen/skills/desktop-pet/scripts/gen_spritesheet.py index ede60f07d37..e9efb7602f8 100644 --- a/.qwen/skills/desktop-pet/scripts/gen_spritesheet.py +++ b/.qwen/skills/desktop-pet/scripts/gen_spritesheet.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -Desktop Pet Spritesheet Generator for OpenWork. +Desktop Pet Spritesheet Generator for Qwen Code. Generates a 1536×1872 pixel-art chibi spritesheet (8 cols × 9 rows, 192×208 px cells) customized via a JSON config describing colors, headgear, and features. diff --git a/.qwen/skills/openwork-desktop-sync/SKILL.md b/.qwen/skills/openwork-desktop-sync/SKILL.md deleted file mode 100644 index 51ae9dbe4a3..00000000000 --- a/.qwen/skills/openwork-desktop-sync/SKILL.md +++ /dev/null @@ -1,102 +0,0 @@ ---- -name: openwork-desktop-sync -description: Sync qwen-code packages/desktop with modelstudioai/openwork using commit-by-commit path migration, not subtree split or tree overwrite. Use when exporting qwen-code desktop changes to OpenWork, importing OpenWork desktop changes into qwen-code, preserving target-owned overlay files such as README.md, resolving sync conflicts, or preparing sync PR branches between the two repositories. ---- - -# OpenWork Desktop Sync - -Use this skill to sync desktop changes between this qwen-code repo and an -OpenWork checkout. The repository script owns the Git mechanics: - -```bash -OPENWORK_DIR=/path/to/openwork bun run desktop-openwork-sync --mode export -``` - -Default overlay is `README.md`. Overlay paths are excluded from migrated -commits and stay target-owned. - -```bash -OPENWORK_OVERLAY_PATHS='README.md' -``` - -## Contract - -This is commit-by-commit path migration, not snapshot replacement. The script -walks source commits from `source-base..source-head`, rewrites paths between -qwen-code `packages/desktop` and the OpenWork repository root, then applies each -commit with `git apply -3`. - -Commits that already came from the receiving repository are skipped by their -sync trailers. During import, qwen-code-origin export commits are skipped; -during export, OpenWork-origin import commits are skipped. - -Merge commits are not migrated as merge commits. The script migrates the regular -commits inside the merged branch; when it later sees the merge wrapper, it -checks that the regular commits were already handled and that the merge tree -matches Git's automatic merge result. If the merge wrapper contains manual -resolution changes, the sync stops so the agent can convert that resolution into -a normal follow-up commit. - -Target-side changes are preserved unless a migrated source commit touches the -same hunk. If that happens, Git leaves a normal conflict for the agent to -resolve. Do not use `git subtree split` or full tree replacement for normal -sync. - -Successful sync commits include trailers such as `Qwen-Code-Commit` or -`OpenWork-Commit`. Later syncs can use the latest trailer as the next source -base. The first sync needs an explicit source base when no previous sync trailer -exists: - -```bash -bun run desktop-openwork-sync --mode export --source-base -bun run desktop-openwork-sync --mode import --source-base -``` - -## Modes - -- `--mode export`: qwen-code `packages/desktop` commits -> OpenWork. -- `--mode import`: OpenWork commits -> qwen-code `packages/desktop`. -- `--mode auto`: guardrail only; use explicit directions for real sync. - -## Workflow - -1. Confirm repo paths and clean worktrees: - - ```bash - git rev-parse --show-toplevel - git -C /path/to/openwork rev-parse --show-toplevel - git status --short - git -C /path/to/openwork status --short - ``` - -2. Run the requested direction: - - ```bash - OPENWORK_DIR=/path/to/openwork \ - OPENWORK_OVERLAY_PATHS='README.md' \ - bun run desktop-openwork-sync --mode export --source-base - ``` - -3. If Git reports conflicts, resolve only the conflicted hunks, preserving - target-owned repository metadata unless the source change intentionally - updates that same behavior. - -4. After sync, verify: - - ```bash - git status --short - git diff --check HEAD - git diff --name-status ..HEAD - ``` - -5. If the user asked to publish, push the branch and create a PR after the - branch is clean. - -## Rules - -- Keep only `README.md` as the default overlay unless the user adds paths to - `OPENWORK_OVERLAY_PATHS`. -- OpenWork-specific files not touched by source commits must remain unchanged. -- Prefer PR branches. The script prints the push command for export branches. -- Do not manually import PR merge commits. Let the script migrate regular - commits and treat merge commits as wrappers. diff --git a/.yamllint.yml b/.yamllint.yml index b01f2c813b2..a98b6dbba8f 100644 --- a/.yamllint.yml +++ b/.yamllint.yml @@ -88,5 +88,3 @@ ignore: - 'vendor/' - 'node_modules/' - 'integration-tests/terminal-bench/' - - 'packages/desktop/.github/' - - 'packages/desktop/apps/electron/electron-builder.yml' diff --git a/docs/developers/architecture.md b/docs/developers/architecture.md index 36c565a5a8d..f3ba6ee0be6 100644 --- a/docs/developers/architecture.md +++ b/docs/developers/architecture.md @@ -77,23 +77,23 @@ an HTTP daemon. See the ## Repository layout -| Path | Responsibility | -| ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `packages/cli` | The `qwen` executable, argument parsing, configuration assembly, Ink TUI, headless output, ACP entry point, `qwen serve`, and command-specific adapters. | -| `packages/core` | UI-independent agent orchestration, model-provider integration, prompt and context construction, tool registration and execution, permissions, sessions, memory, telemetry, and shared services. | -| `packages/acp-bridge` | ACP channel lifecycle, session multiplexing, event delivery, permission mediation, process spawning, and the filesystem seam shared by daemon and adapter hosts. | -| `packages/sdk-typescript` | Programmatic process execution through `query()` plus HTTP/SSE clients and transcript projection for `qwen serve`. | -| `packages/webui` | Shared React components and the daemon React adapter built on the TypeScript SDK. | -| `packages/web-shell` | The terminal-style browser UI built on `packages/webui` and the daemon SDK. | -| `packages/web-templates` | Web templates packaged as embeddable JavaScript and CSS strings. | -| `packages/audio-capture` | Native microphone capture for voice input. | -| `packages/channels` | The shared channel runtime and platform adapters for messaging services. | -| `packages/desktop`, `packages/vscode-ide-companion`, `packages/chrome-extension`, `packages/zed-extension` | Product and editor surfaces that adapt Qwen Code to their host environments. | -| `packages/sdk-java`, `packages/sdk-python` | Language-specific programmatic clients. | -| `packages/cua-driver`, `packages/mobile-mcp` | Computer-use and mobile-device integrations exposed through MCP-compatible boundaries. | -| `integration-tests` | End-to-end coverage for CLI, interactive, SDK, sandbox, hook, and terminal behavior. | -| `docs` and `docs-site` | User, developer, protocol, and design documentation plus the documentation site. | -| `scripts` | Build, packaging, release, validation, and repository-maintenance automation. | +| Path | Responsibility | +| ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `packages/cli` | The `qwen` executable, argument parsing, configuration assembly, Ink TUI, headless output, ACP entry point, `qwen serve`, and command-specific adapters. | +| `packages/core` | UI-independent agent orchestration, model-provider integration, prompt and context construction, tool registration and execution, permissions, sessions, memory, telemetry, and shared services. | +| `packages/acp-bridge` | ACP channel lifecycle, session multiplexing, event delivery, permission mediation, process spawning, and the filesystem seam shared by daemon and adapter hosts. | +| `packages/sdk-typescript` | Programmatic process execution through `query()` plus HTTP/SSE clients and transcript projection for `qwen serve`. | +| `packages/webui` | Shared React components and the daemon React adapter built on the TypeScript SDK. | +| `packages/web-shell` | The terminal-style browser UI built on `packages/webui` and the daemon SDK. | +| `packages/web-templates` | Web templates packaged as embeddable JavaScript and CSS strings. | +| `packages/audio-capture` | Native microphone capture for voice input. | +| `packages/channels` | The shared channel runtime and platform adapters for messaging services. | +| `packages/desktop-shell`, `packages/vscode-ide-companion`, `packages/chrome-extension`, `packages/zed-extension` | Product and editor surfaces that adapt Qwen Code to their host environments. | +| `packages/sdk-java`, `packages/sdk-python` | Language-specific programmatic clients. | +| `packages/cua-driver`, `packages/mobile-mcp` | Computer-use and mobile-device integrations exposed through MCP-compatible boundaries. | +| `integration-tests` | End-to-end coverage for CLI, interactive, SDK, sandbox, hook, and terminal behavior. | +| `docs` and `docs-site` | User, developer, protocol, and design documentation plus the documentation site. | +| `scripts` | Build, packaging, release, validation, and repository-maintenance automation. | Most code lives in npm workspaces under `packages/`. A package should depend on another package through its declared public exports rather than through a diff --git a/eslint.config.js b/eslint.config.js index 3d45b97157d..c4643163260 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -51,7 +51,6 @@ export default tseslint.config( 'docs-site/.next/**', 'docs-site/out/**', '.qwen/**', - 'packages/desktop/**', 'packages/desktop-shell/runtime/**', 'packages/desktop-shell/src-tauri/target/**', 'packages/cua-driver/**', // vendored trycua/cua driver (Rust + scripts); not qwen-code TS diff --git a/integration-tests/tsconfig.json b/integration-tests/tsconfig.json index b0c6caded8e..514c02518a7 100644 --- a/integration-tests/tsconfig.json +++ b/integration-tests/tsconfig.json @@ -11,7 +11,7 @@ "composite": false, // The root turns `noPropertyAccessFromIndexSignature` on, which forced // bracket-access rewrites in production SDK sources just to satisfy this - // test program (packages/desktop already sets it false). Relax it here so + // test program. Relax it here so // packages keep their own compiler regime and the tests keep dot access. "noPropertyAccessFromIndexSignature": false, // Matches packages/cli. The suite drives browser-side code in diff --git a/package-lock.json b/package-lock.json index ea8b7f61cef..b34a4bf984f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,6 @@ "packages/channels/gitlab", "packages/channels/plugin-example", "integrations/external-context", - "!packages/desktop", "!packages/desktop-shell" ], "dependencies": { diff --git a/package.json b/package.json index 3798dff83c4..ddfded0d8c8 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,6 @@ "packages/channels/gitlab", "packages/channels/plugin-example", "integrations/external-context", - "!packages/desktop", "!packages/desktop-shell" ], "repository": { @@ -90,9 +89,7 @@ "telemetry": "node scripts/telemetry.js", "check:lockfile": "node scripts/check-lockfile.js", "check:desktop-isolation": "node scripts/check-desktop-isolation.js", - "check:voice-guard-sync": "node scripts/check-voice-guard-sync.js", "check:serve-fast-path-bundle": "node scripts/clean-package-build-artifacts.js && npm run build -- --cli-only && cross-env DEV=true npm run bundle && node scripts/check-serve-fast-path-bundle.js", - "desktop-openwork-sync": "bun run scripts/desktop-openwork-sync.ts", "clean": "node scripts/clean.js", "pre-commit": "node scripts/pre-commit.js" }, diff --git a/packages/cli/src/commands/review/build-test.test.ts b/packages/cli/src/commands/review/build-test.test.ts index 8299a633a34..b29224d139d 100644 --- a/packages/cli/src/commands/review/build-test.test.ts +++ b/packages/cli/src/commands/review/build-test.test.ts @@ -2045,7 +2045,7 @@ describe('runBuildTest', () => { }); it('discloses a diff inside a negated member — softly, never as an incomplete scope', () => { - // packages/desktop is a separate toolchain (its own lockfile); a diff + // packages/desktop-shell is a separate toolchain (its own lockfile); a diff // inside it cannot fail any npm workspace's suite, so "nothing to run" // stays the answer — disclosed softly (its own suite did not run), never // as an incomplete scope. @@ -2053,7 +2053,7 @@ describe('runBuildTest', () => { join(root, 'package.json'), JSON.stringify({ name: 'r', - workspaces: ['packages/*', '!packages/desktop'], + workspaces: ['packages/*', '!packages/desktop-shell'], scripts: { test: 'exit 0' }, }), ); @@ -2061,11 +2061,11 @@ describe('runBuildTest', () => { name: '@x/core', scripts: { build: 'exit 0', test: 'exit 0' }, }); - pkg('packages/desktop', { + pkg('packages/desktop-shell', { name: '@x/desktop', scripts: { build: 'exit 0', test: 'exit 0' }, }); - writePlan(['packages/desktop/src/main.rs']); + writePlan(['packages/desktop-shell/src/main.rs']); const rep = runBuildTest({ plan: planPath, @@ -2077,7 +2077,9 @@ describe('runBuildTest', () => { expect(rep.build).toEqual([]); expect(rep.test).toEqual([]); expect(rep.testScope?.workspaces).toEqual([]); - expect(rep.testScope?.caveat).toContain('packages/desktop/src/main.rs'); + expect(rep.testScope?.caveat).toContain( + 'packages/desktop-shell/src/main.rs', + ); expect(rep.testScope?.caveat).toContain('were not run'); expect(rep.note).toContain('were not run'); }); @@ -2321,7 +2323,7 @@ describe('runBuildTest', () => { it('excludes a negated workspace from the build set (integration)', () => { // `!packages/excluded` must keep that package out — building it could fail on a - // repo where it is a separate toolchain (e.g. packages/desktop, its own lockfile). + // repo where it is a separate toolchain (e.g. packages/desktop-shell, its own lockfile). writeFileSync( join(root, 'package.json'), JSON.stringify({ diff --git a/packages/cli/src/commands/review/lib/diff-plan.test.ts b/packages/cli/src/commands/review/lib/diff-plan.test.ts index d9ac06ba7b4..2ed0e27a9ea 100644 --- a/packages/cli/src/commands/review/lib/diff-plan.test.ts +++ b/packages/cli/src/commands/review/lib/diff-plan.test.ts @@ -56,7 +56,7 @@ describe('classifyPath', () => { it('recognises generated and vendored files', () => { const paths = [ 'package-lock.json', - 'packages/desktop/bun.lock', + 'packages/desktop-shell/bun.lock', 'packages/vscode-ide-companion/NOTICES.txt', 'dist/bundle.min.js', 'vendor/lib.go', diff --git a/packages/cli/src/commands/review/lib/workspace-scope.test.ts b/packages/cli/src/commands/review/lib/workspace-scope.test.ts index 3db9c278f36..f0c06a17ad0 100644 --- a/packages/cli/src/commands/review/lib/workspace-scope.test.ts +++ b/packages/cli/src/commands/review/lib/workspace-scope.test.ts @@ -136,18 +136,18 @@ describe('resolveTestScope', () => { }); it('discloses — softly, not as incompleteness — a member a negation excludes', () => { - // !packages/desktop is a separate toolchain with its own lockfile; a diff + // !packages/desktop-shell is a separate toolchain with its own lockfile; a diff // inside it cannot fail any included workspace's suite, so it earns no // incomplete-scope caveat. But its own suite was not run either, and // "nothing is silent" covers that: a softer line says what did not run. const scope = resolveTestScope({ - changed: ['packages/desktop/src/main.rs'], - globs: ['packages/*', '!packages/desktop'], + changed: ['packages/desktop-shell/src/main.rs'], + globs: ['packages/*', '!packages/desktop-shell'], packages: PKGS, skipped: [], }); expect(scope.workspaces).toEqual([]); - expect(scope.caveat).toContain('packages/desktop/src/main.rs'); + expect(scope.caveat).toContain('packages/desktop-shell/src/main.rs'); expect(scope.caveat).toContain('were not run'); expect(scope.caveat).not.toContain('outside every workspace'); }); diff --git a/packages/cli/src/commands/review/lib/workspace-scope.ts b/packages/cli/src/commands/review/lib/workspace-scope.ts index d277ef88536..40bc5e8e651 100644 --- a/packages/cli/src/commands/review/lib/workspace-scope.ts +++ b/packages/cli/src/commands/review/lib/workspace-scope.ts @@ -196,7 +196,7 @@ export function resolveTestScope(input: { const unmapped = affected.filter( (d) => d !== '.' && !scriptsOf.has(d) && !skipped.includes(d), ); - // Files a negation excludes (!packages/desktop — a separate toolchain with + // Files a negation excludes (!packages/desktop-shell — a separate toolchain with // its own lockfile) cannot affect any included workspace's tests, so they // earn no incomplete-scope caveat — but their own suites were not run // either, and "nothing is silent" covers that too: disclose it as the diff --git a/packages/cli/src/commands/review/lib/workspaces.test.ts b/packages/cli/src/commands/review/lib/workspaces.test.ts index e07a7963cce..f60624b834e 100644 --- a/packages/cli/src/commands/review/lib/workspaces.test.ts +++ b/packages/cli/src/commands/review/lib/workspaces.test.ts @@ -35,7 +35,7 @@ const GLOBS = [ 'packages/channels/base', 'packages/channels/telegram', 'packages/channels/qqbot', - '!packages/desktop', + '!packages/desktop-shell', ]; describe('workspaceDirFor', () => { @@ -54,37 +54,48 @@ describe('workspaceDirFor', () => { ); }); - it('honours a negation, so a separate bun workspace is not a member', () => { - // packages/desktop has its own lockfile and is not part of this npm workspace. + it('honours a negation, so a separate toolchain is not a member', () => { + // packages/desktop-shell has its own lockfile and is not part of this npm workspace. // Building it from the root fails. expect( - workspaceDirFor('packages/desktop/apps/electron/src/main.ts', GLOBS), + workspaceDirFor( + 'packages/desktop-shell/apps/electron/src/main.ts', + GLOBS, + ), ).toBeNull(); - expect(isWorkspaceMember('packages/desktop/src/a.test.ts', GLOBS)).toBe( - false, - ); + expect( + isWorkspaceMember('packages/desktop-shell/src/a.test.ts', GLOBS), + ).toBe(false); }); it('re-includes what a negation excluded when a later glob matches again', () => { // npm's own rule: last match wins, whichever direction it points. - const globs = ['packages/*', '!packages/desktop', 'packages/desktop']; - expect(workspaceDirFor('packages/desktop/src/a.ts', globs)).toBe( - 'packages/desktop', + const globs = [ + 'packages/*', + '!packages/desktop-shell', + 'packages/desktop-shell', + ]; + expect(workspaceDirFor('packages/desktop-shell/src/a.ts', globs)).toBe( + 'packages/desktop-shell', ); }); it('falls back to the surviving OUTER member when a negation excludes a nested one', () => { - // npm keeps packages/desktop in the graph — only src is excluded — and + // npm keeps packages/desktop-shell in the graph — only src is excluded — and // desktop's test runner collects src/**, so the file is felt by the outer // member's suite. Declaring it felt by NOTHING would certify "a complete // answer" over a suite that can fail. - const globs = ['packages/*', 'packages/desktop/*', '!packages/desktop/src']; - expect(workspaceDirFor('packages/desktop/src/x.test.ts', globs)).toBe( - 'packages/desktop', - ); - expect(isNegationExcluded('packages/desktop/src/x.test.ts', globs)).toBe( - false, + const globs = [ + 'packages/*', + 'packages/desktop-shell/*', + '!packages/desktop-shell/src', + ]; + expect(workspaceDirFor('packages/desktop-shell/src/x.test.ts', globs)).toBe( + 'packages/desktop-shell', ); + expect( + isNegationExcluded('packages/desktop-shell/src/x.test.ts', globs), + ).toBe(false); }); it('treats a ./-prefixed glob like its bare form', () => { @@ -201,9 +212,9 @@ describe('readRootPackage', () => { describe('isNegationExcluded', () => { it('is true when a positive glob claims the file but a negation excludes it', () => { - expect(isNegationExcluded('packages/desktop/src/main.rs', GLOBS)).toBe( - true, - ); + expect( + isNegationExcluded('packages/desktop-shell/src/main.rs', GLOBS), + ).toBe(true); }); it('is false for a file inside an included workspace', () => { @@ -216,21 +227,27 @@ describe('isNegationExcluded', () => { }); it('is false when a later glob re-includes what the negation excluded', () => { - const globs = ['packages/*', '!packages/desktop', 'packages/desktop']; - expect(isNegationExcluded('packages/desktop/src/a.ts', globs)).toBe(false); + const globs = [ + 'packages/*', + '!packages/desktop-shell', + 'packages/desktop-shell', + ]; + expect(isNegationExcluded('packages/desktop-shell/src/a.ts', globs)).toBe( + false, + ); }); - it('keeps a member owned under a partial negation (`!packages/desktop/*`)', () => { - // npm keeps packages/desktop itself a member — a glob with a subpath + it('keeps a member owned under a partial negation (`!packages/desktop-shell/*`)', () => { + // npm keeps packages/desktop-shell itself a member — a glob with a subpath // cannot match the dir itself — so a file under it is still owned and its // suite can feel a change there; it is NOT negation-excluded. - const globs = ['packages/*', '!packages/desktop/*']; - expect(workspaceDirFor('packages/desktop/src/main.ts', globs)).toBe( - 'packages/desktop', - ); - expect(isNegationExcluded('packages/desktop/src/main.ts', globs)).toBe( - false, + const globs = ['packages/*', '!packages/desktop-shell/*']; + expect(workspaceDirFor('packages/desktop-shell/src/main.ts', globs)).toBe( + 'packages/desktop-shell', ); + expect( + isNegationExcluded('packages/desktop-shell/src/main.ts', globs), + ).toBe(false); }); }); @@ -334,9 +351,9 @@ describe('readWorkspacePackages', () => { }); it('ignores a broken manifest in a NEGATED dir — not a workspace, not our graph', () => { - setup(['packages/*', '!packages/desktop']); + setup(['packages/*', '!packages/desktop-shell']); write('packages/good', { name: '@x/good' }); - write('packages/desktop', '{ not json'); + write('packages/desktop-shell', '{ not json'); const { packages, skipped } = readWorkspacePackages(root); expect(packages.map((p) => p.dir)).toEqual(['packages/good']); expect(skipped).toEqual([]); @@ -442,7 +459,7 @@ describe('hasUnmodeledWorkspaceGlob', () => { it('is false for the shapes the walker models — literals and a trailing /*', () => { expect(hasUnmodeledWorkspaceGlob(GLOBS)).toBe(false); expect(hasUnmodeledWorkspaceGlob(['packages/*', 'apps/web'])).toBe(false); - expect(hasUnmodeledWorkspaceGlob(['!packages/desktop'])).toBe(false); + expect(hasUnmodeledWorkspaceGlob(['!packages/desktop-shell'])).toBe(false); }); it('is true for `**`, an inner `*`, or a `foo-*` prefix the walker cannot model', () => { diff --git a/packages/cli/src/commands/review/lib/workspaces.ts b/packages/cli/src/commands/review/lib/workspaces.ts index 6975d1fe89a..49b2c9224bf 100644 --- a/packages/cli/src/commands/review/lib/workspaces.ts +++ b/packages/cli/src/commands/review/lib/workspaces.ts @@ -79,10 +79,10 @@ export function workspaceDirFor( const norm = filePath.replace(/^\.\//, ''); let owner: string | null = null; // The owners that came before the current one — where a negation falls back - // TO. When a negation excludes a NESTED member (`packages/desktop/*` claimed - // `packages/desktop/src`, then `!packages/desktop/src` excluded it), the + // TO. When a negation excludes a NESTED member (`packages/desktop-shell/*` claimed + // `packages/desktop-shell/src`, then `!packages/desktop-shell/src` excluded it), the // still-included outer member keeps owning the file: npm keeps - // `packages/desktop` in the graph, and its test runner collects `src/**`. + // `packages/desktop-shell` in the graph, and its test runner collects `src/**`. // Falling back to the previous owner is what lets that suite feel the // change instead of the file being declared felt by nothing. const previous: Array = []; @@ -111,15 +111,15 @@ export function workspaceDirFor( } } else if (dir === owner) { // A negation only excludes the file when it excludes the member that - // currently owns it. `!packages/desktop/*` matches a deeper pseudo-dir + // currently owns it. `!packages/desktop-shell/*` matches a deeper pseudo-dir // than `packages/*` does, and npm keeps the member itself in the graph // (a glob with a subpath cannot match a dir with no subpath), so the // member's suite can still feel a change there. When the negation DOES // exclude the owner, ownership falls back to the previous, outer member // — only a negation of THAT one leaves the file owned by nothing. (The // pop does not re-check the popped owner against negations already - // walked past: a contrived ordering like `!packages/desktop` BEFORE - // `!packages/desktop/src` can resurrect an excluded owner. Realistic + // walked past: a contrived ordering like `!packages/desktop-shell` BEFORE + // `!packages/desktop-shell/src` can resurrect an excluded owner. Realistic // orderings — the outer negation written last — are exact.) owner = previous.pop() ?? null; } @@ -132,11 +132,11 @@ export function workspaceDirFor( * every member. * * Such a file belongs to a workspace the npm graph does not contain — this - * repo's `!packages/desktop` is a separate bun workspace with its own + * repo's `!packages/desktop-shell` is a separate toolchain with its own * lockfile — so no included workspace's tests can feel a change to it, and it * must not earn the incomplete-scope caveat a genuinely outside file does. A * file whose nested member is negated while an OUTER member survives - * (`!packages/desktop/src` under `packages/desktop`) is NOT excluded here: + * (`!packages/desktop-shell/src` under `packages/desktop-shell`) is NOT excluded here: * `workspaceDirFor` falls back to the outer member, whose suite collects it. */ export function isNegationExcluded( @@ -351,8 +351,8 @@ export function readWorkspacePackages(root: string): WorkspaceGraph { if (!existsSync(manifest)) continue; if (workspaceDirFor(`${dir}/package.json`, globs) !== dir) { // A directory a negation excludes is not a workspace, and its own - // `package.json` says nothing about that — `packages/desktop` is a - // separate bun workspace with its own lockfile, and building it from + // `package.json` says nothing about that — `packages/desktop-shell` is a + // separate toolchain with its own lockfile, and building it from // here fails. The tell: the POSITIVE globs alone still make the dir its // own owner, so a negation is what took the ownership away. const positives = globs.filter((g) => !g.startsWith('!')); diff --git a/packages/cli/src/commands/review/test-efficacy.test.ts b/packages/cli/src/commands/review/test-efficacy.test.ts index 67d85373be2..b7f1772b01e 100644 --- a/packages/cli/src/commands/review/test-efficacy.test.ts +++ b/packages/cli/src/commands/review/test-efficacy.test.ts @@ -51,7 +51,7 @@ const GLOBS = [ 'packages/*', 'packages/channels/base', 'packages/channels/telegram', - '!packages/desktop', + '!packages/desktop-shell', ]; describe('isWorkspaceMember', () => { @@ -77,22 +77,30 @@ describe('isWorkspaceMember', () => { }); it('honours a negated glob', () => { - expect(isWorkspaceMember('packages/desktop/src/a.test.ts', GLOBS)).toBe( - false, - ); + expect( + isWorkspaceMember('packages/desktop-shell/src/a.test.ts', GLOBS), + ).toBe(false); }); it('honours workspace-glob ORDER — a positive after a negation re-includes', () => { // npm evaluates the list in order. Filtering all negations first let a // negation win wherever it sat, which would file a false `unreachable`. - const globs = ['packages/*', '!packages/desktop', 'packages/desktop']; - expect(isWorkspaceMember('packages/desktop/src/a.test.ts', globs)).toBe( - true, - ); - const reordered = ['packages/*', 'packages/desktop', '!packages/desktop']; - expect(isWorkspaceMember('packages/desktop/src/a.test.ts', reordered)).toBe( - false, - ); + const globs = [ + 'packages/*', + '!packages/desktop-shell', + 'packages/desktop-shell', + ]; + expect( + isWorkspaceMember('packages/desktop-shell/src/a.test.ts', globs), + ).toBe(true); + const reordered = [ + 'packages/*', + 'packages/desktop-shell', + '!packages/desktop-shell', + ]; + expect( + isWorkspaceMember('packages/desktop-shell/src/a.test.ts', reordered), + ).toBe(false); }); it('does not match a sibling directory by prefix', () => { diff --git a/packages/cli/src/services/voice-transcriber.ts b/packages/cli/src/services/voice-transcriber.ts index 6996397f29e..74ba38713b6 100644 --- a/packages/cli/src/services/voice-transcriber.ts +++ b/packages/cli/src/services/voice-transcriber.ts @@ -24,9 +24,6 @@ const MIN_KEYTERM_ECHO_TOKENS = 8; const MIN_ABSOLUTE_KEYTERM_ECHO_TOKENS = 10; const MIN_KEYTERM_SET_ECHO_RATIO = 0.3; const debugLogger = createDebugLogger('VOICE_TRANSCRIBER'); -// The address classification in this file is mirrored in -// packages/desktop/packages/server-core/src/voice/net-guard.ts. The bun -// workspace boundary prevents sharing a module; keep the two in sync. const BLOCKED_TRANSITION_IPV6_ADDRESSES = new BlockList(); for (const [address, prefix] of [ ['64:ff9b:1::', 48], diff --git a/packages/core/src/tools/agent/agent.ts b/packages/core/src/tools/agent/agent.ts index d00a4bdaf2e..1fa436f2c3f 100644 --- a/packages/core/src/tools/agent/agent.ts +++ b/packages/core/src/tools/agent/agent.ts @@ -2695,13 +2695,11 @@ class AgentToolInvocation extends BaseToolInvocation { // agent-level background flag retains its existing meaning, and safe // ordinary one-shot launches default to background. // - // This is the source of truth for the background-classification rule. Two - // UI classifiers replicate it from tool-call args (they cannot see + // This is the source of truth for the background-classification rule. The + // web-shell classifier replicates it from tool-call args (it cannot see // subagentConfig.background) and must be kept in sync when it changes: // - packages/web-shell/client/adapters/toolClassification.ts // (isBackgroundSubAgentToolCall) - // - packages/desktop/packages/shared/src/agent/tool-matching.ts - // (detectBackgroundEvents) // // Background delegation is top-level-only in v1. A nested launcher would // be handed a completion contract it cannot honor — the success guidance diff --git a/packages/desktop/.agents/skills/desktop-brand-builder/SKILL.md b/packages/desktop/.agents/skills/desktop-brand-builder/SKILL.md deleted file mode 100644 index c22d0230506..00000000000 --- a/packages/desktop/.agents/skills/desktop-brand-builder/SKILL.md +++ /dev/null @@ -1,151 +0,0 @@ ---- -name: desktop-brand-builder -description: Generate a branded qwen-code desktop package from a minimal brandId and logo. Use when the user wants a custom, white-label, rebranded, ModelStudio/OpenWork/Qwen Code desktop client, installer, DMG/EXE/AppImage, or one-click brand build. ---- - -# Desktop Brand Builder - -## Goal - -Create a branded desktop package with the least user input possible. The user -should usually provide only: - -```text -brandId: acme-ai -logo: /absolute/path/to/logo.png -website: https://acme.ai -``` - -`website` is optional. Do not ask for app name, app id, artifact name, -copyright, dock icon, renderer symbol, signing, or local installation unless -the user explicitly asks to override them. - -## Input Rules - -Required fields: - -- `brandId`: must match `^[a-z][a-z0-9-]*$` -- `logo`: local file path; must exist - -Optional overrides: - -- `website` -- `appName` -- `appId` -- `artifactPrefix` -- `target`: `mac`, `win`, `linux`, or `all` - -If required input is missing, ask once: - -```text -请提供: -brandId: 例如 acme-ai,只能小写字母、数字、短横线 -logo: 本地 logo 文件路径 -website: 可选 -``` - -Once the required fields are present, proceed without a confirmation step. - -## Derived Defaults - -Infer missing values deterministically: - -- `appName`: title-case the hyphen-separated `brandId`; `acme-ai` becomes - `Acme AI` -- `artifactPrefix`: title-case the hyphen-separated `brandId` and join with - hyphens; `acme-ai` becomes `Acme-AI` -- `appId`: if `website` has a valid host, reverse the host labels and append - `.desktop`; `https://acme.ai` becomes `ai.acme.desktop` -- fallback `appId`: `app..desktop` -- `copyright`: `Copyright © ` -- all brand images: generate icon, dock icon, and renderer symbol from `logo` - -Use explicit user-provided override values as-is after basic validation. - -## Build Workflow - -Use an isolated build directory under the current working directory so user -changes in the current worktree are not mutated. Default to the qwen-code main -branch; do not clone from `craft-agents-oss`, OpenWork, or another local -checkout unless the user explicitly asks for that source: - -```bash -BUILD_ROOT="$PWD/brand-builds/-" -mkdir -p "$BUILD_ROOT" -git clone --branch main --single-branch \ - https://github.com/QwenLM/qwen-code.git \ - "$BUILD_ROOT/qwen-code" -cd "$BUILD_ROOT/qwen-code" -git checkout -B brand- origin/main -``` - -If the branch fetch or checkout fails, stop and report the failure. Do not -continue as if `brand-` was created. - -Create a temporary `brand.json` in the build directory: - -```json -{ - "brandId": "acme-ai", - "logo": "/absolute/path/to/logo.png", - "website": "https://acme.ai", - "appName": "Acme AI", - "appId": "ai.acme.desktop", - "artifactPrefix": "Acme-AI", - "copyright": "Copyright © 2026 Acme AI" -} -``` - -Install desktop dependencies if `packages/desktop/node_modules` is missing: - -```bash -cd packages/desktop -bun install -``` - -Then run this skill's bundled brand creation script: - -```bash -cd /absolute/path/to/qwen-code -bun run packages/desktop/.agents/skills/desktop-brand-builder/scripts/brand-create.ts \ - --desktop-root /absolute/path/to/qwen-code/packages/desktop \ - --config /absolute/path/to/brand.json -``` - -The agent should not hand-edit `branding.ts` or brand asset files when this -bundled script is available. The bundled script is the source of truth for -patching code and generating resources. - -Package with the current host target unless the user requested a target: - -```bash -CRAFT_BRAND= bun run electron:dist:mac -CRAFT_BRAND= bun run electron:dist:win -CRAFT_BRAND= bun run electron:dist:linux -``` - -For `target: all`, run only targets supported by the current machine or CI -environment. Do not claim cross-platform artifacts were produced unless the -files exist. - -## Validation - -After packaging: - -1. Confirm the expected artifact exists under - `packages/desktop/apps/electron/release/`. -2. Compute `sha256sum` or `shasum -a 256` for each artifact. -3. On macOS, run `hdiutil verify` for generated DMG files. -4. Report the artifact path, SHA-256, app name, app id, and build directory. - -## Failure Handling - -- Invalid `brandId`: show the regex and ask for a corrected value. -- Missing `logo`: ask for a valid local path. -- Missing bundled script: report that - `packages/desktop/.agents/skills/desktop-brand-builder/scripts/brand-create.ts` - is missing, and include the expected command. -- Build failure: preserve the build directory, return the last useful error - lines, and include the full log path or command that produced the failure. - -Do not delete the build directory on failure. diff --git a/packages/desktop/.agents/skills/desktop-brand-builder/scripts/brand-create.ts b/packages/desktop/.agents/skills/desktop-brand-builder/scripts/brand-create.ts deleted file mode 100644 index 8112a423ee4..00000000000 --- a/packages/desktop/.agents/skills/desktop-brand-builder/scripts/brand-create.ts +++ /dev/null @@ -1,381 +0,0 @@ -import { createRequire } from 'node:module'; -import { - copyFileSync, - existsSync, - mkdirSync, - mkdtempSync, - readFileSync, - rmSync, - writeFileSync, -} from 'node:fs'; -import { tmpdir } from 'node:os'; -import { extname, join, resolve } from 'node:path'; - -interface BrandInput { - brandId?: string; - logo?: string; - website?: string; - appName?: string; - appId?: string; - artifactPrefix?: string; - copyright?: string; -} - -interface BrandConfig { - brandId: string; - logo: string; - website?: string; - appName: string; - appId: string; - artifactPrefix: string; - copyright: string; -} - -const BRAND_ID_RE = /^[a-z][a-z0-9-]*$/; - -function argValue(name: string): string | undefined { - const index = process.argv.indexOf(name); - return index >= 0 ? process.argv[index + 1] : undefined; -} - -function configPathFromArgs(): string { - const value = argValue('--config'); - if (!value) { - throw new Error( - 'Usage: bun run scripts/brand-create.ts --desktop-root /path/to/packages/desktop --config /path/to/brand.json', - ); - } - return resolve(value); -} - -function desktopRootFromArgs(): string { - const value = argValue('--desktop-root'); - if (!value) { - throw new Error( - 'Usage: bun run scripts/brand-create.ts --desktop-root /path/to/packages/desktop --config /path/to/brand.json', - ); - } - - const desktopRoot = resolve(value); - if (!existsSync(join(desktopRoot, 'package.json'))) { - throw new Error(`Desktop package not found: ${desktopRoot}`); - } - return desktopRoot; -} - -function titleWords(brandId: string): string[] { - return brandId - .split('-') - .filter(Boolean) - .map((part) => part[0]!.toUpperCase() + part.slice(1)); -} - -function deriveAppId(website: string | undefined, brandId: string): string { - if (!website) return `app.${brandId}.desktop`; - - try { - const withProtocol = website.includes('://') - ? website - : `https://${website}`; - const host = new URL(withProtocol).hostname.replace(/^www\./, ''); - const parts = host.split('.').filter(Boolean); - if (parts.length >= 2) { - return `${parts.reverse().join('.')}.desktop`; - } - } catch { - // Fall through to the deterministic fallback. - } - - return `app.${brandId}.desktop`; -} - -function loadConfig(path: string): BrandConfig { - const input = JSON.parse(readFileSync(path, 'utf8')) as BrandInput; - const brandId = input.brandId?.trim(); - const logo = input.logo ? resolve(input.logo) : undefined; - - if (!brandId || !BRAND_ID_RE.test(brandId)) { - throw new Error(`brandId must match ${BRAND_ID_RE}`); - } - if (!logo || !existsSync(logo)) { - throw new Error(`Logo file not found: ${logo ?? '(missing)'}`); - } - - const words = titleWords(brandId); - const appName = input.appName?.trim() || words.join(' '); - const artifactPrefix = input.artifactPrefix?.trim() || words.join('-'); - - return { - brandId, - logo, - website: input.website?.trim() || undefined, - appName, - appId: input.appId?.trim() || deriveAppId(input.website, brandId), - artifactPrefix, - copyright: - input.copyright?.trim() || - `Copyright \u00a9 ${new Date().getFullYear()} ${appName}`, - }; -} - -async function run(cmd: string[], cwd: string): Promise { - const proc = Bun.spawn({ - cmd, - cwd, - stdout: 'inherit', - stderr: 'inherit', - stdin: 'inherit', - }); - const exitCode = await proc.exited; - if (exitCode !== 0) { - throw new Error(`${cmd.join(' ')} failed with exit code ${exitCode}`); - } -} - -interface BrandAssetsResult { - macIcon: string; - hasAssetsCar: boolean; -} - -async function writeBrandAssets( - config: BrandConfig, - desktopRoot: string, -): Promise { - const requireFromDesktop = createRequire(join(desktopRoot, 'package.json')); - const sharp = requireFromDesktop('sharp') as typeof import('sharp'); - const electronDir = join(desktopRoot, 'apps', 'electron'); - const brandDir = join(electronDir, 'resources', 'brands', config.brandId); - mkdirSync(brandDir, { recursive: true }); - - async function writePng(output: string, size: number) { - await sharp(config.logo) - .resize(size, size, { - fit: 'contain', - background: { r: 0, g: 0, b: 0, alpha: 0 }, - }) - .png() - .toFile(output); - } - - const sourceExt = extname(config.logo) || '.logo'; - copyFileSync(config.logo, join(brandDir, `source${sourceExt}`)); - - await writePng(join(brandDir, 'icon.png'), 512); - await writePng(join(brandDir, 'dock.png'), 512); - await writePng(join(brandDir, 'symbol.png'), 512); - - if (process.platform !== 'darwin') return { macIcon: 'icon.png', hasAssetsCar: false }; - - const iconset = join(brandDir, 'icon.iconset'); - rmSync(iconset, { recursive: true, force: true }); - mkdirSync(iconset, { recursive: true }); - - const sizes = [ - ['icon_16x16.png', 16], - ['icon_16x16@2x.png', 32], - ['icon_32x32.png', 32], - ['icon_32x32@2x.png', 64], - ['icon_128x128.png', 128], - ['icon_128x128@2x.png', 256], - ['icon_256x256.png', 256], - ['icon_256x256@2x.png', 512], - ['icon_512x512.png', 512], - ['icon_512x512@2x.png', 1024], - ] as const; - - for (const [file, size] of sizes) { - await writePng(join(iconset, file), size); - } - - await run( - ['iconutil', '-c', 'icns', iconset, '-o', join(brandDir, 'icon.icns')], - brandDir, - ); - - const hasAssetsCar = await compileAssetsCar(config, brandDir, writePng); - return { macIcon: 'icon.icns', hasAssetsCar }; -} - -async function compileAssetsCar( - config: BrandConfig, - brandDir: string, - writePng: (output: string, size: number) => Promise, -): Promise { - const xcassets = join(brandDir, 'Assets.xcassets'); - const appiconset = join(xcassets, 'AppIcon.appiconset'); - rmSync(xcassets, { recursive: true, force: true }); - mkdirSync(appiconset, { recursive: true }); - - writeFileSync( - join(xcassets, 'Contents.json'), - JSON.stringify({ info: { author: 'xcode', version: 1 } }), - ); - - const entries = [ - { file: 'icon_16.png', size: 16, scale: '1x', dims: '16x16' }, - { file: 'icon_32.png', size: 32, scale: '2x', dims: '16x16' }, - { file: 'icon_32.png', size: 32, scale: '1x', dims: '32x32' }, - { file: 'icon_64.png', size: 64, scale: '2x', dims: '32x32' }, - { file: 'icon_128.png', size: 128, scale: '1x', dims: '128x128' }, - { file: 'icon_256.png', size: 256, scale: '2x', dims: '128x128' }, - { file: 'icon_256.png', size: 256, scale: '1x', dims: '256x256' }, - { file: 'icon_512.png', size: 512, scale: '2x', dims: '256x256' }, - { file: 'icon_512.png', size: 512, scale: '1x', dims: '512x512' }, - { file: 'icon_1024.png', size: 1024, scale: '2x', dims: '512x512' }, - ]; - - const uniqueSizes = new Set(entries.map((e) => e.size)); - for (const size of uniqueSizes) { - await writePng(join(appiconset, `icon_${size}.png`), size); - } - - writeFileSync( - join(appiconset, 'Contents.json'), - JSON.stringify({ - images: entries.map((e) => ({ - filename: e.file, - idiom: 'mac', - scale: e.scale, - size: e.dims, - })), - info: { author: 'xcode', version: 1 }, - }), - ); - - const outDir = mkdtempSync(join(tmpdir(), 'assets-car-')); - const partialPlist = join(outDir, 'partial-info.plist'); - const proc = Bun.spawn({ - cmd: [ - 'xcrun', 'actool', xcassets, - '--compile', outDir, - '--app-icon', 'AppIcon', - '--platform', 'macosx', - '--minimum-deployment-target', '14.0', - '--output-partial-info-plist', partialPlist, - ], - cwd: brandDir, - stdout: 'pipe', - stderr: 'pipe', - }); - const exitCode = await proc.exited; - if (exitCode !== 0) { - console.log('Warning: actool compilation failed, skipping Assets.car'); - rmSync(xcassets, { recursive: true, force: true }); - return false; - } - - const compiledCar = join(outDir, 'Assets.car'); - if (!existsSync(compiledCar)) { - console.log('Warning: actool produced no Assets.car, skipping'); - rmSync(xcassets, { recursive: true, force: true }); - return false; - } - - copyFileSync(compiledCar, join(brandDir, 'Assets.car')); - rmSync(xcassets, { recursive: true, force: true }); - rmSync(outDir, { recursive: true, force: true }); - console.log('Assets.car compiled successfully'); - return true; -} - -function tsString(value: string): string { - return `'${value.replaceAll('\\', '\\\\').replaceAll("'", "\\'")}'`; -} - -function helpMenuLinks(config: BrandConfig): string { - if (!config.website) return '[]'; - - return `[ - { - labelKey: 'menu.homepage', - url: ${tsString(config.website)}, - icon: 'House', - }, - ]`; -} - -function brandBlock(config: BrandConfig, macIcon: string, hasAssetsCar: boolean): string { - const resourceDir = `resources/brands/${config.brandId}`; - const liquidGlassLine = hasAssetsCar - ? `\n liquidGlassAssetsCar: ${tsString(`${resourceDir}/Assets.car`)},` - : ''; - - return ` ${tsString(config.brandId)}: { - id: ${tsString(config.brandId)}, - appName: ${tsString(config.appName)}, - appId: ${tsString(config.appId)}, - productName: ${tsString(config.appName)}, - artifactPrefix: ${tsString(config.artifactPrefix)}, - copyright: ${tsString(config.copyright)}, - coAuthorLine: ${tsString(`Co-Authored-By: ${config.appName} `)}, - selfReferName: ${tsString(config.appName)}, - viewerUrl: 'https://agents.craft.do', - helpMenuLinks: ${helpMenuLinks(config)}, - assets: { - resourceDir: ${tsString(resourceDir)}, - rendererSymbol: ${tsString(`${resourceDir}/symbol.png`)}, - macIcon: ${tsString(`${resourceDir}/${macIcon}`)}, - winIcon: ${tsString(`${resourceDir}/icon.png`)}, - linuxIcon: ${tsString(`${resourceDir}/icon.png`)}, - devDockIcon: ${tsString(`${resourceDir}/dock.png`)},${liquidGlassLine} - }, - credits: '', - creditsShort: '', - creditsEntries: [], - }, -`; -} - -function registerBrand( - config: BrandConfig, - desktopRoot: string, - macIcon: string, - hasAssetsCar: boolean, -): void { - const brandingPath = join( - desktopRoot, - 'packages', - 'shared', - 'src', - 'branding.ts', - ); - const source = readFileSync(brandingPath, 'utf8'); - if ( - source.includes(`${tsString(config.brandId)}:`) || - source.includes(`id: ${tsString(config.brandId)}`) - ) { - throw new Error(`Brand already exists in branding.ts: ${config.brandId}`); - } - - const marker = '\n};\n\n/** Active brand'; - if (!source.includes(marker)) { - throw new Error(`Could not find BRANDS insertion point in ${brandingPath}`); - } - - writeFileSync( - brandingPath, - source.replace(marker, `\n${brandBlock(config, macIcon, hasAssetsCar)}${marker}`), - ); -} - -async function main(): Promise { - const desktopRoot = desktopRootFromArgs(); - const config = loadConfig(configPathFromArgs()); - const { macIcon, hasAssetsCar } = await writeBrandAssets(config, desktopRoot); - registerBrand(config, desktopRoot, macIcon, hasAssetsCar); - - console.log(`Created brand ${config.brandId}`); - console.log(`App name: ${config.appName}`); - console.log(`App ID: ${config.appId}`); - console.log( - `Assets: ${join(desktopRoot, 'apps', 'electron', 'resources', 'brands', config.brandId)}`, - ); - if (hasAssetsCar) { - console.log('Assets.car: generated (macOS 26+ Liquid Glass icon)'); - } -} - -main().catch((error: unknown) => { - console.error(error instanceof Error ? error.message : error); - process.exit(1); -}); diff --git a/packages/desktop/.agents/skills/desktop-develop/SKILL.md b/packages/desktop/.agents/skills/desktop-develop/SKILL.md deleted file mode 100644 index ee91353da8e..00000000000 --- a/packages/desktop/.agents/skills/desktop-develop/SKILL.md +++ /dev/null @@ -1,121 +0,0 @@ ---- -name: desktop-develop -description: Develop, debug, and verify the OpenWork desktop/Electron app with an agent-readable harness. Use when working on packages/desktop, Electron renderer/main/preload code, desktop UI bugs, local desktop runtime failures, Chrome DevTools MCP investigation, desktop logs, messaging gateway issues, or when improving the development feedback loop for desktop features. ---- - -# Desktop Development Harness - -## Overview - -Use this skill to turn desktop work into a tight harness loop: gather runtime -context, reproduce with the UI and logs visible to the agent, make the smallest -fix, verify through the same path, and encode any missing affordance back into -the repo. - -Read `references/harness-principles.md` when the task changes the development -workflow, observability, docs, tests, or agent-facing harness itself. - -## Quick Context - -For bug reports, UI failures, hangs, startup problems, messaging issues, or -anything involving the running desktop app, inspect the runtime logs directly. -Important paths: - -- `~/Library/Logs/@craft-agent/electron/main.log` -- `~/Library/Logs/@craft-agent/electron/main.old.log` -- `~/.craft-agent/logs/messaging-gateway.log` - -Search logs before guessing: - -```bash -rg -n "error|warn|failed|exception|crash|Unhandled|rejection|browser-cdp|messaging-gateway" \ - "$HOME/Library/Logs/@craft-agent/electron/main.log" \ - "$HOME/.craft-agent/logs/messaging-gateway.log" -``` - -## Harness Loop - -1. **Map the surface.** Identify whether the task touches Electron main, - preload, renderer, shared desktop packages, server, messaging, or browser - CDP. Read nearby code and tests before editing. -2. **Collect live evidence.** Read and tail the relevant log while reproducing. - Treat missing or ambiguous logs as part of the bug. -3. **Drive the UI.** Use Chrome DevTools MCP when a browser/renderer page is - involved: `list_pages`, `select_page`, `take_snapshot`, then console/network - inspection. Prefer accessibility snapshots over screenshots for reasoning. -4. **Reproduce first.** For bugs, capture the exact observed behavior and the - evidence that proves it. If reproduction differs from the user's report, - compare environment, app state, build artifact, account, timing, and logs. -5. **Patch narrowly.** Keep changes scoped to the proven cause. Add structure - only when it removes real repeated work or makes the app more readable to - future agents. -6. **Verify through the same path.** Re-run the reproduction, inspect logs and - DevTools again, then run focused tests/typechecks for touched packages. -7. **Improve the harness when needed.** If the fix required hidden knowledge, - add a small doc, test, log field, or skill update so the next agent - can see it directly. - -## Running Desktop - -Use desktop-specific commands from `packages/desktop`: - -```bash -cd packages/desktop -bun run electron:dev -bun run electron:dev:terminal -bun run electron:dev:logs -``` - -Use `electron:dev:terminal` when the bug involves process output, startup, or -shutdown. Use `electron:dev:logs` when the app is already running and you need a -live log tail. - -## Chrome DevTools MCP - -If DevTools tools are not loaded, search for `chrome-devtools` tools first. -Then: - -1. Call `mcp__chrome_devtools.list_pages`. -2. Select the relevant page with `mcp__chrome_devtools.select_page`. -3. Capture an accessibility snapshot with - `mcp__chrome_devtools.take_snapshot`. -4. Inspect runtime failures with - `mcp__chrome_devtools.list_console_messages`, then - `mcp__chrome_devtools.get_console_message` for important entries. -5. Inspect selected network requests with - `mcp__chrome_devtools.get_network_request` when network state is involved. -6. For memory issues, save a heap snapshot with - `mcp__chrome_devtools.take_heapsnapshot` and keep it under `.qwen/` or - `/tmp`, not in source directories. - -Always take a fresh snapshot after each UI-changing action. Do not rely on stale -element ids or old console state. - -## Focused Verification - -Choose the narrowest checks that cover the touched surface: - -```bash -cd packages/desktop && bun run typecheck:electron -cd packages/desktop && bun run typecheck:all -cd packages/desktop && bun run validate:dev -cd packages/desktop/apps/electron && bun run lint -cd packages/desktop/packages/shared && bun test path/to/file.test.ts -``` - -For root CLI/core changes, use the root repository commands from `AGENTS.md` -instead. For desktop-only changes, prefer desktop package commands first. - -## Agent-Readable Changes - -Favor changes that future agents can inspect and verify: - -- Add structured log fields near failure boundaries instead of vague messages. -- Add accessible names or stable UI affordances when DevTools snapshots are - hard to interpret. -- Keep docs as maps with links to deeper sources. Do not create giant manuals. -- Convert repeated manual debugging steps into docs, tests, or structured logs. -- Record non-trivial investigation notes in `.qwen/investigations/`. - -Stop and ask the user only when the missing input cannot be discovered locally -and a reasonable assumption would risk changing the wrong behavior. diff --git a/packages/desktop/.agents/skills/desktop-develop/references/harness-principles.md b/packages/desktop/.agents/skills/desktop-develop/references/harness-principles.md deleted file mode 100644 index 9f9c18c61ff..00000000000 --- a/packages/desktop/.agents/skills/desktop-develop/references/harness-principles.md +++ /dev/null @@ -1,42 +0,0 @@ -# Desktop Harness Principles - -Use this reference when designing desktop development workflows, debugging -loops, observability, or agent-facing docs. - -## Source - -OpenAI, "Engineering: Harnessing Codex in an agent-first world" -https://openai.com/zh-Hans-CN/index/harness-engineering/ - -## Principles - -- Give the agent a map, not a giant manual. Keep `AGENTS.md` and skills concise - entry points that route to focused docs, tests, and logs. -- Make the application readable to agents. UI snapshots, logs, metrics, traces, - and runtime state should be directly inspectable without asking a human to - copy/paste observations. -- Treat the repo as the system of record. If a fix depends on tribal knowledge, - encode that knowledge as versioned docs, tests, lint rules, or structured - logging. -- Build feedback loops, not one-off heroics. Reproduce, observe, patch, - restart, and verify through the same harness until the evidence changes. -- Prefer enforceable constraints over vague preference. If a pattern matters - repeatedly, turn it into a test, linter, helper, or review checklist. -- Use human attention for judgment. Let agents collect evidence, run tools, - draft fixes, and verify; ask humans when product intent or risk cannot be - inferred locally. - -## Desktop Application Pattern - -For qwen-code desktop work, the harness is: - -1. Desktop runtime logs under `~/Library/Logs/@craft-agent/electron/`. -2. Domain-specific logs such as `~/.craft-agent/logs/messaging-gateway.log`. -3. Chrome DevTools MCP snapshots, console messages, network details, and heap - snapshots. -4. Focused package tests and typechecks under `packages/desktop`. -5. Small repo artifacts under `.qwen/` for investigations, E2E notes, and skill - improvements. - -When one of these is missing or hard to read, consider improving the harness as -part of the development task. diff --git a/packages/desktop/.agents/skills/desktop-pet/SKILL.md b/packages/desktop/.agents/skills/desktop-pet/SKILL.md deleted file mode 100644 index 1f7b7324f4f..00000000000 --- a/packages/desktop/.agents/skills/desktop-pet/SKILL.md +++ /dev/null @@ -1,235 +0,0 @@ ---- -name: desktop-pet -description: Create pixel-art desktop pet companions for OpenWork/Qwen Code. Generates a customized chibi spritesheet (1536×1872, 8×9 grid) for any character the user names — F1 drivers, anime characters, celebrities, fictional characters, animals, etc. Use when the user says "桌面宠物", "desktop pet", "想要XXX当桌宠", "换个宠物" or similar. -version: 1.0.0 ---- - -# Desktop Pet Creator - -Create pixel-art chibi desktop pet companions for OpenWork's floating pet window. -Given any character name, generate a complete pet package with animated spritesheet -and place it in `~/.qwen/pets/` where OpenWork auto-discovers it. - -## Workflow - -### Step 1: Identify the Character - -Ask the user who they want as their desktop pet if not already specified. -Then research the character's visual appearance: - -- **Team/organization colors** (e.g., McLaren papaya orange, Ferrari red) -- **Outfit/uniform** (racing suit, school uniform, armor, etc.) -- **Distinguishing features** (hair color/style, accessories, number, helmet) -- **Personality traits** (for animation style — energetic, calm, goofy, serious) -- **Iconic items** (steering wheel, lightsaber, guitar, etc.) - -Use web search if needed to gather visual reference. For well-known characters -(F1 drivers, popular anime, etc.), rely on training knowledge. - -### Step 2: Design the Color Palette - -Define 8-12 colors for the character: - -| Color Role | Example (F1 Driver) | Example (Anime Character) | -| -------------- | -------------------- | ------------------------- | -| Primary outfit | Team color (papaya) | Uniform color (navy) | -| Outfit dark | Darker shade | Darker shade | -| Outfit light | Lighter shade | Lighter shade | -| Skin | Warm skin tone | Skin tone | -| Skin dark | Shadow skin | Shadow skin | -| Hair | Character hair color | Character hair color | -| Accent | Number/logo color | Eye color / accessory | -| Shoe | Dark grey/black | Character shoe color | - -**Important:** All colors must be distinct and work at small pixel scale (3x = 9px details). - -### Step 3: Generate the Spritesheet - -Use the template script at `scripts/gen_spritesheet.py` as a starting point. -The script generates a **1536×1872 pixel RGBA spritesheet** (8 columns × 9 rows, -192×208 px cells) — the exact format OpenWork expects. - -**Run it like this:** - -```bash -python3 /scripts/gen_spritesheet.py \ - --output ~/.qwen/pets//spritesheet.webp \ - --config '{"name":"...","colors":{...},"features":{...}}' -``` - -Or copy and customize the script for characters that need unique visual elements -not covered by the parameterized version. - -**The 9 animation rows are:** - -| Row | State | Description | -| --- | ------------- | --------------------------------- | -| 0 | idle | Breathing + blinking (8 frames) | -| 1 | running-right | Running to the right (8 frames) | -| 2 | running-left | Running to the left (8 frames) | -| 3 | waving | Waving at user (8 frames) | -| 4 | jumping | Jumping celebration (8 frames) | -| 5 | failed | Sad/collapsed on error (8 frames) | -| 6 | waiting | Idle tapping (8 frames) | -| 7 | running | Generic running (8 frames) | -| 8 | review | Thinking/examining (8 frames) | - -### Step 4: Create pet.json - -Write the manifest to `~/.qwen/pets//pet.json`: - -```json -{ - "id": "", - "displayName": "", - "description": "", - "spritesheetPath": "spritesheet.webp" -} -``` - -Rules: - -- `id`: lowercase, no spaces, URL-safe (e.g., `piastri`, `satoru`, `goku`) -- `displayName`: The name shown in the UI (e.g., "Piastri", "五条悟", "悟空") -- `description`: One short sentence describing the character - -### Step 5: Verify and Activate - -1. Confirm the files exist: - - ```bash - ls -lh ~/.qwen/pets// - ``` - -2. Open the spritesheet in Preview for the user to check: - - ```bash - open ~/.qwen/pets//spritesheet.webp - ``` - -3. Tell the user to activate: - > Open **OpenWork → Settings → Appearance → Pet Companion**, - > click **Refresh**, then select ****. - -## Character Design Guidelines - -### Chibi Proportions - -- **Head**: ~40% of total height (big head = cute) -- **Body**: ~30% of total height -- **Legs**: ~25% of total height -- **Scale**: Each "pixel" in the art = 3×3 actual pixels (scale=3) -- **Character center**: approximately (96, 124) within the 192×208 cell - -### Drawing Order (back to front) - -1. Legs (behind body) -2. Body / outfit -3. Arms -4. Head shape -5. Hair (back layer) -6. Hair (front/top layer) -7. Face features (eyes, mouth, expression) -8. Accessories (hat, helmet, glasses, etc.) -9. Foreground details (number, logo, badge) - -### Animation Tips - -- **Idle**: subtle Y bob (0 to -2px) + blink every 3rd-4th frame -- **Running**: alternating leg offset (±4px), body tilt (±2px), arm swing -- **Waving**: one arm raised high, alternating frames -- **Jumping**: Y offset curve (0 → -30 → 0), arms up -- **Failed**: body tilt increases, then collapse to sitting pose -- **Happy expression**: curved eyes (∧ shape), blush marks on cheeks -- **Sad expression**: straight eyebrows, downturned mouth - -### Headgear Variants - -The template supports several headgear types. Set via `features.headgear`: - -- `cap` — baseball cap with brim (default for F1 drivers) -- `helmet` — full racing helmet with visor -- `none` — no headgear (just hair) -- `hat` — generic hat -- `hood` — hooded outfit -- `crown` — royal crown -- `horns` — devil/dragon horns -- `ears` — animal ears (cat, dog, etc.) -- `halo` — angel halo -- `headband` — ninja/sports headband - -### Special Features - -Set via `features.extras` (list): - -- `glasses` — round or rectangular glasses -- `scarf` — neck scarf -- `tail` — animal tail -- `wings` — small wings on back -- `number` — chest number (set `features.number` to the number string) -- `logo` — chest badge/logo area -- `sweat_drop` — anime sweat drop (in waiting/failed states) - -## Example Characters - -### F1 Driver (e.g., Piastri, Norris, Verstappen) - -```json -{ - "colors": { - "outfit": [255, 135, 32], - "outfit_dark": [220, 110, 20], - "outfit_light": [255, 170, 80], - "hair": [120, 80, 40], - "number": [30, 30, 30] - }, - "features": { - "headgear": "cap", - "number": "81", - "extras": ["logo"] - } -} -``` - -### Anime Character (e.g., Gojo Satoru) - -```json -{ - "colors": { - "outfit": [30, 30, 50], - "outfit_dark": [20, 20, 35], - "outfit_light": [60, 60, 80], - "hair": [230, 230, 250], - "accent": [100, 180, 255] - }, - "features": { - "headgear": "none", - "extras": ["glasses"] - } -} -``` - -### Animal (e.g., Shiba Inu) - -```json -{ - "colors": { - "outfit": [220, 170, 100], - "outfit_dark": [180, 130, 70], - "outfit_light": [240, 200, 140], - "hair": [220, 170, 100], - "accent": [255, 255, 255] - }, - "features": { - "headgear": "ears", - "extras": ["tail"] - } -} -``` - -## Troubleshooting - -- **Pet not showing**: Click Refresh in Settings → Appearance → Pet Companion -- **Colors look wrong**: Check that RGB values are tuples, not hex strings -- **Spritesheet too large**: Must be under 5MB (webp lossless usually ~8-50KB) -- **Animation jittery**: Ensure all 8 frames per row are visually distinct but not jarring diff --git a/packages/desktop/.agents/skills/desktop-pet/scripts/gen_spritesheet.py b/packages/desktop/.agents/skills/desktop-pet/scripts/gen_spritesheet.py deleted file mode 100644 index ede60f07d37..00000000000 --- a/packages/desktop/.agents/skills/desktop-pet/scripts/gen_spritesheet.py +++ /dev/null @@ -1,558 +0,0 @@ -#!/usr/bin/env python3 -""" -Desktop Pet Spritesheet Generator for OpenWork. - -Generates a 1536×1872 pixel-art chibi spritesheet (8 cols × 9 rows, 192×208 px cells) -customized via a JSON config describing colors, headgear, and features. - -Usage: - python3 gen_spritesheet.py --output ~/.qwen/pets/mychar/spritesheet.webp --config '{...}' - python3 gen_spritesheet.py --output out.webp --config-file config.json -""" - -import argparse -import json -import sys -import math - -try: - from PIL import Image, ImageDraw, ImageFont -except ImportError: - print("ERROR: Pillow is required. Install with: pip3 install Pillow") - sys.exit(1) - -# --- Atlas layout --- -COLS, ROWS = 8, 9 -CW, CH = 192, 208 -W, H = COLS * CW, ROWS * CH - -# --- Default color palette (Qwen capybara-like neutral) --- -DEFAULT_COLORS = { - "outfit": [100, 120, 180], - "outfit_dark": [70, 85, 140], - "outfit_light": [140, 160, 210], - "skin": [255, 218, 185], - "skin_dark": [230, 190, 155], - "hair": [80, 55, 35], - "hair_light": [110, 80, 55], - "accent": [30, 30, 30], - "shoe": [50, 50, 50], - "eye": [50, 70, 90], - "eye_white": [245, 245, 245], - "blush": [255, 160, 140], - "mouth": [180, 80, 80], -} - -DEFAULT_FEATURES = { - "headgear": "none", - "extras": [], - "number": "", - "hair_style": "short", - "helmet_color": None, -} - -def tuple_color(c): - if isinstance(c, list): - return tuple(c) - if isinstance(c, str) and c.startswith("#"): - h = c.lstrip("#") - return tuple(int(h[i:i+2], 16) for i in (0, 2, 4)) - return c - -def darken(color, amount=40): - return tuple(max(0, c - amount) for c in color[:3]) - -def lighten(color, amount=40): - return tuple(min(255, c + amount) for c in color[:3]) - -def fill_rect(draw, x, y, w, h, color): - draw.rectangle([x, y, x + w - 1, y + h - 1], fill=color) - - -def draw_headgear(draw, hx, hy, s, colors, features, flip=False): - """Draw headgear on top of the head.""" - hg = features.get("headgear", "none") - outfit = colors["outfit"] - outfit_dark = colors["outfit_dark"] - helmet_c = tuple_color(features.get("helmet_color") or outfit) - - if hg == "cap": - cap_y = hy - 7 * s - fill_rect(draw, hx - 2*s, cap_y, 24*s, 5*s, outfit) - fill_rect(draw, hx + 2*s, cap_y - 2*s, 16*s, 3*s, outfit) - fill_rect(draw, hx + 4*s, cap_y - 3*s, 12*s, 2*s, outfit_dark) - if not flip: - fill_rect(draw, hx - 4*s, cap_y + 4*s, 26*s, 2*s, outfit_dark) - else: - fill_rect(draw, hx - 2*s, cap_y + 4*s, 26*s, 2*s, outfit_dark) - fill_rect(draw, hx + 7*s, cap_y + s, 6*s, 2*s, colors["accent"]) - - elif hg == "helmet": - cap_y = hy - 8 * s - fill_rect(draw, hx - 3*s, cap_y, 26*s, 8*s, helmet_c) - fill_rect(draw, hx + 2*s, cap_y - 2*s, 16*s, 3*s, helmet_c) - fill_rect(draw, hx + 4*s, cap_y - 3*s, 12*s, 2*s, darken(helmet_c, 20)) - fill_rect(draw, hx, cap_y + 5*s, 20*s, 3*s, darken(helmet_c, 60)) - fill_rect(draw, hx + 2*s, cap_y + 6*s, 16*s, s, lighten(helmet_c, 60)) - - elif hg == "hat": - cap_y = hy - 6 * s - fill_rect(draw, hx - 4*s, cap_y + 3*s, 28*s, 3*s, outfit_dark) - fill_rect(draw, hx + 2*s, cap_y - 2*s, 16*s, 6*s, outfit) - fill_rect(draw, hx + 4*s, cap_y - 3*s, 12*s, 2*s, outfit_dark) - - elif hg == "hood": - cap_y = hy - 5 * s - fill_rect(draw, hx - 3*s, cap_y, 26*s, 4*s, outfit) - fill_rect(draw, hx - 4*s, cap_y + 2*s, 4*s, 8*s, outfit) - fill_rect(draw, hx + 20*s, cap_y + 2*s, 4*s, 8*s, outfit) - fill_rect(draw, hx + 4*s, cap_y - 2*s, 12*s, 3*s, outfit_dark) - - elif hg == "crown": - cap_y = hy - 8 * s - gold = (255, 215, 0) - gold_dark = (200, 170, 0) - fill_rect(draw, hx + 2*s, cap_y + 2*s, 16*s, 4*s, gold) - fill_rect(draw, hx + 2*s, cap_y, 3*s, 3*s, gold) - fill_rect(draw, hx + 8*s, cap_y - s, 4*s, 3*s, gold) - fill_rect(draw, hx + 15*s, cap_y, 3*s, 3*s, gold) - fill_rect(draw, hx + 3*s, cap_y + s, s, s, (200, 50, 50)) - fill_rect(draw, hx + 9*s, cap_y, 2*s, s, (50, 150, 200)) - fill_rect(draw, hx + 16*s, cap_y + s, s, s, (50, 200, 50)) - fill_rect(draw, hx + 2*s, cap_y + 5*s, 16*s, s, gold_dark) - - elif hg == "horns": - horn_c = (80, 60, 50) - fill_rect(draw, hx - 2*s, hy - 6*s, 3*s, 8*s, horn_c) - fill_rect(draw, hx - 3*s, hy - 8*s, 2*s, 3*s, horn_c) - fill_rect(draw, hx + 19*s, hy - 6*s, 3*s, 8*s, horn_c) - fill_rect(draw, hx + 21*s, hy - 8*s, 2*s, 3*s, horn_c) - - elif hg == "ears": - hair_c = colors["hair"] - inner = colors["skin"] - fill_rect(draw, hx - 3*s, hy - 8*s, 5*s, 8*s, hair_c) - fill_rect(draw, hx - 2*s, hy - 6*s, 3*s, 5*s, inner) - fill_rect(draw, hx + 18*s, hy - 8*s, 5*s, 8*s, hair_c) - fill_rect(draw, hx + 19*s, hy - 6*s, 3*s, 5*s, inner) - - elif hg == "halo": - halo_c = (255, 255, 200) - cap_y = hy - 10 * s - fill_rect(draw, hx + 3*s, cap_y, 14*s, 2*s, halo_c) - fill_rect(draw, hx + 2*s, cap_y + s, s, s, halo_c) - fill_rect(draw, hx + 17*s, cap_y + s, s, s, halo_c) - fill_rect(draw, hx + 3*s, cap_y + 2*s, 14*s, s, darken(halo_c, 30)) - - elif hg == "headband": - fill_rect(draw, hx - 2*s, hy - 2*s, 24*s, 2*s, colors["accent"]) - fill_rect(draw, hx + 18*s, hy - 2*s, 4*s, 6*s, colors["accent"]) - - -def draw_extras(draw, cx, cy, s, colors, features, bx, by, expression): - """Draw extra features like glasses, scarf, tail, wings.""" - extras = features.get("extras", []) - hx = cx - 10 * s - hy = cy - 16 * s - - if "glasses" in extras: - ey = hy + 8 * s - glass_c = (60, 60, 80) - fill_rect(draw, hx + 3*s, ey - s, 6*s, 5*s, glass_c) - fill_rect(draw, hx + 4*s, ey, 4*s, 3*s, (200, 220, 240)) - fill_rect(draw, hx + 11*s, ey - s, 6*s, 5*s, glass_c) - fill_rect(draw, hx + 12*s, ey, 4*s, 3*s, (200, 220, 240)) - fill_rect(draw, hx + 9*s, ey + s, 2*s, s, glass_c) - - if "scarf" in extras: - scarf_c = lighten(colors["outfit"], 30) - fill_rect(draw, bx + 3*s, by - 2*s, 10*s, 3*s, scarf_c) - fill_rect(draw, bx + 4*s, by + s, 3*s, 6*s, scarf_c) - - if "tail" in extras: - tail_c = colors["hair"] - fill_rect(draw, bx + 16*s, by + 10*s, 3*s, 3*s, tail_c) - fill_rect(draw, bx + 18*s, by + 8*s, 3*s, 3*s, tail_c) - fill_rect(draw, bx + 20*s, by + 6*s, 3*s, 3*s, tail_c) - fill_rect(draw, bx + 21*s, by + 4*s, 2*s, 3*s, tail_c) - - if "wings" in extras: - wing_c = (240, 240, 255) - wing_dark = (200, 200, 220) - fill_rect(draw, bx - 6*s, by + 2*s, 5*s, 8*s, wing_c) - fill_rect(draw, bx - 8*s, by + 4*s, 3*s, 5*s, wing_c) - fill_rect(draw, bx - 5*s, by + 3*s, 3*s, 5*s, wing_dark) - fill_rect(draw, bx + 17*s, by + 2*s, 5*s, 8*s, wing_c) - fill_rect(draw, bx + 21*s, by + 4*s, 3*s, 5*s, wing_c) - fill_rect(draw, bx + 18*s, by + 3*s, 3*s, 5*s, wing_dark) - - if "sweat_drop" in extras and expression in ("waiting", "failed"): - fill_rect(draw, hx + 18*s, hy + 2*s, 2*s, 3*s, (150, 200, 255)) - fill_rect(draw, hx + 18*s, hy + s, s, s, (150, 200, 255)) - - -def draw_character(draw, cx, cy, colors, features, scale=3, flip=False, - arm_angle=0, leg_offset=0, body_tilt=0, head_tilt=0, - expression="normal", arm_wave=False, jump_y=0, collapsed=False): - """Draw a chibi character centered at (cx, cy).""" - s = scale - cy += jump_y - - skin = colors["skin"] - skin_dark = colors["skin_dark"] - hair_c = colors["hair"] - hair_light = colors.get("hair_light", lighten(hair_c, 30)) - outfit = colors["outfit"] - outfit_dark = colors["outfit_dark"] - outfit_light = colors["outfit_light"] - eye_c = colors["eye"] - eye_w = colors["eye_white"] - blush_c = colors["blush"] - mouth_c = colors.get("mouth", darken(skin, 80)) - accent_c = colors["accent"] - shoe_c = colors["shoe"] - - # --- LEGS --- - leg_spread = 4 * s - leg_left_x = cx - leg_spread - 2*s + body_tilt - leg_right_x = cx + leg_spread - 2*s + body_tilt - - if collapsed: - fill_rect(draw, leg_left_x, cy + 18*s, 5*s, 6*s, outfit_dark) - fill_rect(draw, leg_right_x, cy + 18*s, 5*s, 6*s, outfit_dark) - fill_rect(draw, leg_left_x, cy + 24*s, 5*s, 2*s, shoe_c) - fill_rect(draw, leg_right_x, cy + 24*s, 5*s, 2*s, shoe_c) - else: - fill_rect(draw, leg_left_x + leg_offset, cy + 16*s, 5*s, 10*s, outfit_dark) - fill_rect(draw, leg_right_x - leg_offset, cy + 16*s, 5*s, 10*s, outfit_dark) - fill_rect(draw, leg_left_x + leg_offset - s, cy + 26*s, 7*s, 3*s, shoe_c) - fill_rect(draw, leg_right_x - leg_offset - s, cy + 26*s, 7*s, 3*s, shoe_c) - - # --- BODY --- - bx = cx - 8*s + body_tilt - by = cy + 2*s - fill_rect(draw, bx, by, 16*s, 16*s, outfit) - fill_rect(draw, bx + s, by + s, 14*s, 2*s, outfit_light) - fill_rect(draw, bx + 5*s, by - s, 6*s, 2*s, (255, 255, 255)) - - # Number on chest - num = features.get("number", "") - if num: - try: - font_size = max(5 * s, 8) - font = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", font_size) - except Exception: - font = ImageFont.load_default() - bbox = draw.textbbox((0, 0), num, font=font) - tw = bbox[2] - bbox[0] - th = bbox[3] - bbox[1] - tx = bx + (16*s - tw) // 2 - ty = by + (14*s - th) // 2 + s - draw.text((tx, ty), num, fill=accent_c, font=font) - - # Logo area - if "logo" in features.get("extras", []) and not num: - fill_rect(draw, bx + 5*s, by + 5*s, 6*s, 4*s, accent_c) - fill_rect(draw, bx + 6*s, by + 6*s, 4*s, 2*s, outfit_light) - - # --- ARMS --- - arm_y = by + 3*s - left_arm_x = bx - 5*s - right_arm_x = bx + 16*s - - if arm_wave: - fill_rect(draw, left_arm_x, arm_y + 2*s, 5*s, 8*s, outfit) - fill_rect(draw, left_arm_x - s, arm_y + 10*s, 5*s, 4*s, skin) - fill_rect(draw, right_arm_x, arm_y - 8*s, 5*s, 10*s, outfit) - fill_rect(draw, right_arm_x, arm_y - 10*s, 6*s, 4*s, skin) - elif arm_angle > 0: - fill_rect(draw, left_arm_x - arm_angle, arm_y, 5*s, 10*s, outfit) - fill_rect(draw, left_arm_x - arm_angle - s, arm_y + 10*s, 5*s, 4*s, skin) - fill_rect(draw, right_arm_x + arm_angle, arm_y, 5*s, 10*s, outfit) - fill_rect(draw, right_arm_x + arm_angle + s, arm_y + 10*s, 5*s, 4*s, skin) - else: - fill_rect(draw, left_arm_x, arm_y, 5*s, 10*s, outfit) - fill_rect(draw, left_arm_x - s, arm_y + 10*s, 5*s, 4*s, skin) - fill_rect(draw, right_arm_x, arm_y, 5*s, 10*s, outfit) - fill_rect(draw, right_arm_x + s, arm_y + 10*s, 5*s, 4*s, skin) - - # --- Scarf (drawn between body and head) --- - draw_extras(draw, cx, cy, s, colors, features, bx, by, expression) - - # --- HEAD --- - hx = cx - 10*s + head_tilt - hy = cy - 16*s - - # Hair back - fill_rect(draw, hx - s, hy - 2*s, 22*s, 6*s, hair_c) - # Head shape - fill_rect(draw, hx, hy, 20*s, 18*s, skin) - - # Hair style - hair_style = features.get("hair_style", "short") - if hair_style == "long": - fill_rect(draw, hx - s, hy - 4*s, 22*s, 6*s, hair_c) - fill_rect(draw, hx + 2*s, hy - 5*s, 16*s, 3*s, hair_c) - fill_rect(draw, hx + 4*s, hy - 6*s, 12*s, 2*s, hair_light) - fill_rect(draw, hx - 2*s, hy, 3*s, 16*s, hair_c) - fill_rect(draw, hx + 19*s, hy, 3*s, 16*s, hair_c) - fill_rect(draw, hx - 3*s, hy + 14*s, 4*s, 4*s, hair_c) - fill_rect(draw, hx + 19*s, hy + 14*s, 4*s, 4*s, hair_c) - elif hair_style == "spiky": - fill_rect(draw, hx - s, hy - 4*s, 22*s, 6*s, hair_c) - fill_rect(draw, hx + s, hy - 7*s, 4*s, 4*s, hair_c) - fill_rect(draw, hx + 6*s, hy - 8*s, 4*s, 5*s, hair_c) - fill_rect(draw, hx + 11*s, hy - 7*s, 4*s, 4*s, hair_c) - fill_rect(draw, hx + 16*s, hy - 6*s, 3*s, 3*s, hair_c) - fill_rect(draw, hx - 2*s, hy, 3*s, 10*s, hair_c) - fill_rect(draw, hx + 19*s, hy, 3*s, 10*s, hair_c) - elif hair_style == "ponytail": - fill_rect(draw, hx - s, hy - 4*s, 22*s, 6*s, hair_c) - fill_rect(draw, hx + 2*s, hy - 5*s, 16*s, 3*s, hair_c) - fill_rect(draw, hx - 2*s, hy, 3*s, 10*s, hair_c) - fill_rect(draw, hx + 19*s, hy, 3*s, 10*s, hair_c) - fill_rect(draw, hx + 18*s, hy + 8*s, 3*s, 3*s, hair_c) - fill_rect(draw, hx + 19*s, hy + 10*s, 3*s, 8*s, hair_c) - fill_rect(draw, hx + 20*s, hy + 16*s, 2*s, 4*s, hair_light) - elif hair_style == "bald": - fill_rect(draw, hx + 2*s, hy - 2*s, 16*s, 2*s, skin_dark) - else: # short (default) - fill_rect(draw, hx - s, hy - 4*s, 22*s, 6*s, hair_c) - fill_rect(draw, hx + 2*s, hy - 5*s, 16*s, 3*s, hair_c) - fill_rect(draw, hx + 4*s, hy - 6*s, 12*s, 2*s, hair_light) - fill_rect(draw, hx - 2*s, hy, 3*s, 10*s, hair_c) - fill_rect(draw, hx + 19*s, hy, 3*s, 10*s, hair_c) - - # --- FACE --- - ey = hy + 8*s - - if expression == "blink": - fill_rect(draw, hx + 4*s, ey + s, 4*s, s, eye_c) - fill_rect(draw, hx + 12*s, ey + s, 4*s, s, eye_c) - elif expression == "happy": - fill_rect(draw, hx + 4*s, ey, 4*s, s, eye_c) - fill_rect(draw, hx + 3*s, ey + s, s, s, eye_c) - fill_rect(draw, hx + 8*s, ey + s, s, s, eye_c) - fill_rect(draw, hx + 12*s, ey, 4*s, s, eye_c) - fill_rect(draw, hx + 11*s, ey + s, s, s, eye_c) - fill_rect(draw, hx + 16*s, ey + s, s, s, eye_c) - fill_rect(draw, hx + 7*s, ey + 5*s, 6*s, s, mouth_c) - fill_rect(draw, hx + 6*s, ey + 4*s, s, s, mouth_c) - fill_rect(draw, hx + 13*s, ey + 4*s, s, s, mouth_c) - fill_rect(draw, hx + 2*s, ey + 3*s, 3*s, 2*s, blush_c) - fill_rect(draw, hx + 15*s, ey + 3*s, 3*s, 2*s, blush_c) - elif expression == "sad": - fill_rect(draw, hx + 4*s, ey, 4*s, 3*s, eye_w) - fill_rect(draw, hx + 5*s, ey + s, 2*s, 2*s, eye_c) - fill_rect(draw, hx + 12*s, ey, 4*s, 3*s, eye_w) - fill_rect(draw, hx + 13*s, ey + s, 2*s, 2*s, eye_c) - fill_rect(draw, hx + 3*s, ey - 2*s, 5*s, s, hair_c) - fill_rect(draw, hx + 12*s, ey - 2*s, 5*s, s, hair_c) - fill_rect(draw, hx + 8*s, ey + 5*s, 4*s, s, mouth_c) - fill_rect(draw, hx + 7*s, ey + 6*s, s, s, mouth_c) - fill_rect(draw, hx + 12*s, ey + 6*s, s, s, mouth_c) - elif expression == "surprised": - fill_rect(draw, hx + 3*s, ey - s, 5*s, 4*s, eye_w) - fill_rect(draw, hx + 4*s, ey, 3*s, 3*s, eye_c) - fill_rect(draw, hx + 5*s, ey + s, s, s, (255, 255, 255)) - fill_rect(draw, hx + 12*s, ey - s, 5*s, 4*s, eye_w) - fill_rect(draw, hx + 13*s, ey, 3*s, 3*s, eye_c) - fill_rect(draw, hx + 14*s, ey + s, s, s, (255, 255, 255)) - fill_rect(draw, hx + 8*s, ey + 4*s, 4*s, 3*s, mouth_c) - fill_rect(draw, hx + 9*s, ey + 5*s, 2*s, s, (180, 80, 80)) - elif expression == "determined": - fill_rect(draw, hx + 4*s, ey, 4*s, 3*s, eye_w) - fill_rect(draw, hx + 6*s, ey + s, 2*s, 2*s, eye_c) - fill_rect(draw, hx + 12*s, ey, 4*s, 3*s, eye_w) - fill_rect(draw, hx + 14*s, ey + s, 2*s, 2*s, eye_c) - fill_rect(draw, hx + 3*s, ey - 2*s, 6*s, s, hair_c) - fill_rect(draw, hx + 11*s, ey - 2*s, 6*s, s, hair_c) - fill_rect(draw, hx + 8*s, ey + 5*s, 4*s, s, mouth_c) - else: # normal - fill_rect(draw, hx + 4*s, ey, 4*s, 3*s, eye_w) - fill_rect(draw, hx + 5*s, ey + s, 2*s, 2*s, eye_c) - fill_rect(draw, hx + 5*s, ey + s, s, s, (255, 255, 255)) - fill_rect(draw, hx + 12*s, ey, 4*s, 3*s, eye_w) - fill_rect(draw, hx + 13*s, ey + s, 2*s, 2*s, eye_c) - fill_rect(draw, hx + 13*s, ey + s, s, s, (255, 255, 255)) - fill_rect(draw, hx + 8*s, ey + 5*s, 4*s, s, mouth_c) - fill_rect(draw, hx + 7*s, ey + 4*s, s, s, mouth_c) - fill_rect(draw, hx + 12*s, ey + 4*s, s, s, mouth_c) - - # --- HEADGEAR --- - draw_headgear(draw, hx, hy, s, colors, features, flip) - - -def gen_row(sheet, row, colors, features, frame_configs): - """Generate one animation row from frame configs.""" - for col, fc in enumerate(frame_configs): - img = Image.new("RGBA", (CW, CH), (0, 0, 0, 0)) - d = ImageDraw.Draw(img) - draw_character( - d, CW // 2 + fc.get("dx", 0), CH // 2 + 20, - colors, features, scale=3, - flip=fc.get("flip", False), - arm_angle=fc.get("arm_angle", 0), - leg_offset=fc.get("leg_offset", 0), - body_tilt=fc.get("body_tilt", 0), - head_tilt=fc.get("head_tilt", 0), - expression=fc.get("expression", "normal"), - arm_wave=fc.get("arm_wave", False), - jump_y=fc.get("jump_y", 0), - collapsed=fc.get("collapsed", False), - ) - sheet.paste(img, (col * CW, row * CH), img) - - -def gen_all_animations(sheet, colors, features): - """Generate all 9 animation rows.""" - - # Row 0: idle - gen_row(sheet, 0, colors, features, [ - {"jump_y": 0, "expression": "normal"}, - {"jump_y": -1, "expression": "normal"}, - {"jump_y": -2, "expression": "blink"}, - {"jump_y": -1, "expression": "normal"}, - {"jump_y": 0, "expression": "normal"}, - {"jump_y": -1, "expression": "normal"}, - {"jump_y": 0, "expression": "normal"}, - {"jump_y": 0, "expression": "blink"}, - ]) - - # Row 1: running right - gen_row(sheet, 1, colors, features, [ - {"leg_offset": 0, "body_tilt": 0, "head_tilt": 0, "arm_angle": 0, "jump_y": 0, "dx": 8}, - {"leg_offset": 4, "body_tilt": 1, "head_tilt": 1, "arm_angle": 2, "jump_y": -2, "dx": 8}, - {"leg_offset": 0, "body_tilt": 2, "head_tilt": 2, "arm_angle": 3, "jump_y": -3, "dx": 8}, - {"leg_offset": -4, "body_tilt": 1, "head_tilt": 1, "arm_angle": 2, "jump_y": -2, "dx": 8}, - {"leg_offset": 0, "body_tilt": 0, "head_tilt": 0, "arm_angle": 0, "jump_y": 0, "dx": 8}, - {"leg_offset": 4, "body_tilt": -1, "head_tilt": -1, "arm_angle": -2, "jump_y": -2, "dx": 8}, - {"leg_offset": 0, "body_tilt": -2, "head_tilt": -2, "arm_angle": -3, "jump_y": -3, "dx": 8}, - {"leg_offset": -4, "body_tilt": -1, "head_tilt": -1, "arm_angle": -2, "jump_y": -2, "dx": 8}, - ]) - - # Row 2: running left - gen_row(sheet, 2, colors, features, [ - {"leg_offset": 0, "body_tilt": 0, "head_tilt": 0, "arm_angle": 0, "jump_y": 0, "flip": True, "dx": -8}, - {"leg_offset": 4, "body_tilt": -1, "head_tilt": -1, "arm_angle": 2, "jump_y": -2, "flip": True, "dx": -8}, - {"leg_offset": 0, "body_tilt": -2, "head_tilt": -2, "arm_angle": 3, "jump_y": -3, "flip": True, "dx": -8}, - {"leg_offset": -4, "body_tilt": -1, "head_tilt": -1, "arm_angle": 2, "jump_y": -2, "flip": True, "dx": -8}, - {"leg_offset": 0, "body_tilt": 0, "head_tilt": 0, "arm_angle": 0, "jump_y": 0, "flip": True, "dx": -8}, - {"leg_offset": 4, "body_tilt": 1, "head_tilt": 1, "arm_angle": -2, "jump_y": -2, "flip": True, "dx": -8}, - {"leg_offset": 0, "body_tilt": 2, "head_tilt": 2, "arm_angle": -3, "jump_y": -3, "flip": True, "dx": -8}, - {"leg_offset": -4, "body_tilt": 1, "head_tilt": 1, "arm_angle": -2, "jump_y": -2, "flip": True, "dx": -8}, - ]) - - # Row 3: waving - gen_row(sheet, 3, colors, features, [ - {"arm_wave": True, "jump_y": 0, "expression": "happy"}, - {"arm_wave": False, "jump_y": -1, "expression": "happy"}, - {"arm_wave": True, "jump_y": 0, "expression": "happy"}, - {"arm_wave": False, "jump_y": -1, "expression": "happy"}, - {"arm_wave": True, "jump_y": 0, "expression": "happy"}, - {"arm_wave": False, "jump_y": 0, "expression": "happy"}, - {"arm_wave": True, "jump_y": 0, "expression": "normal"}, - {"arm_wave": False, "jump_y": 0, "expression": "normal"}, - ]) - - # Row 4: jumping - gen_row(sheet, 4, colors, features, [ - {"jump_y": 0, "arm_angle": 0, "expression": "normal"}, - {"jump_y": -8, "arm_angle": 3, "expression": "happy"}, - {"jump_y": -20, "arm_angle": 5, "expression": "happy"}, - {"jump_y": -30, "arm_angle": 5, "expression": "happy"}, - {"jump_y": -35, "arm_angle": 5, "expression": "happy"}, - {"jump_y": -25, "arm_angle": 3, "expression": "happy"}, - {"jump_y": -10, "arm_angle": 0, "expression": "happy"}, - {"jump_y": 0, "arm_angle": 0, "expression": "normal"}, - ]) - - # Row 5: failed - gen_row(sheet, 5, colors, features, [ - {"body_tilt": 0, "head_tilt": 0, "expression": "sad"}, - {"body_tilt": -1, "head_tilt": -2, "expression": "sad"}, - {"body_tilt": -2, "head_tilt": -4, "expression": "sad"}, - {"body_tilt": -3, "head_tilt": -6, "expression": "sad"}, - {"body_tilt": -3, "head_tilt": -6, "expression": "sad", "collapsed": True}, - {"body_tilt": -2, "head_tilt": -4, "expression": "sad", "collapsed": True}, - {"body_tilt": -1, "head_tilt": -2, "expression": "sad"}, - {"body_tilt": 0, "head_tilt": 0, "expression": "sad"}, - ]) - - # Row 6: waiting - gen_row(sheet, 6, colors, features, [ - {"jump_y": 0, "head_tilt": 0, "expression": "normal"}, - {"jump_y": -1, "head_tilt": 0, "expression": "normal"}, - {"jump_y": 0, "head_tilt": 2, "expression": "normal"}, - {"jump_y": -1, "head_tilt": 0, "expression": "normal"}, - {"jump_y": 0, "head_tilt": -2, "expression": "blink"}, - {"jump_y": 0, "head_tilt": 0, "expression": "blink"}, - {"jump_y": 0, "head_tilt": 0, "expression": "normal"}, - {"jump_y": 0, "head_tilt": 0, "expression": "normal"}, - ]) - - # Row 7: running (generic, same as row 1) - gen_row(sheet, 7, colors, features, [ - {"leg_offset": 0, "body_tilt": 0, "head_tilt": 0, "arm_angle": 0, "jump_y": 0, "dx": 8}, - {"leg_offset": 4, "body_tilt": 1, "head_tilt": 1, "arm_angle": 2, "jump_y": -2, "dx": 8}, - {"leg_offset": 0, "body_tilt": 2, "head_tilt": 2, "arm_angle": 3, "jump_y": -3, "dx": 8}, - {"leg_offset": -4, "body_tilt": 1, "head_tilt": 1, "arm_angle": 2, "jump_y": -2, "dx": 8}, - {"leg_offset": 0, "body_tilt": 0, "head_tilt": 0, "arm_angle": 0, "jump_y": 0, "dx": 8}, - {"leg_offset": 4, "body_tilt": -1, "head_tilt": -1, "arm_angle": -2, "jump_y": -2, "dx": 8}, - {"leg_offset": 0, "body_tilt": -2, "head_tilt": -2, "arm_angle": -3, "jump_y": -3, "dx": 8}, - {"leg_offset": -4, "body_tilt": -1, "head_tilt": -1, "arm_angle": -2, "jump_y": -2, "dx": 8}, - ]) - - # Row 8: review/thinking - gen_row(sheet, 8, colors, features, [ - {"head_tilt": 0, "arm_angle": 2, "expression": "surprised"}, - {"head_tilt": 2, "arm_angle": 2, "expression": "surprised", "jump_y": -1}, - {"head_tilt": 4, "arm_angle": 2, "expression": "surprised", "jump_y": -1}, - {"head_tilt": 4, "arm_angle": 0, "expression": "normal"}, - {"head_tilt": 2, "arm_angle": 0, "expression": "happy"}, - {"head_tilt": 0, "arm_angle": 0, "expression": "happy"}, - {"head_tilt": 0, "arm_angle": 0, "expression": "normal"}, - {"head_tilt": 0, "arm_angle": 0, "expression": "normal"}, - ]) - - -def main(): - parser = argparse.ArgumentParser(description="Generate desktop pet spritesheet") - parser.add_argument("--output", "-o", required=True, help="Output .webp file path") - parser.add_argument("--config", "-c", type=str, help="JSON config string") - parser.add_argument("--config-file", "-f", help="Path to JSON config file") - args = parser.parse_args() - - config = {} - if args.config_file: - with open(args.config_file) as f: - config = json.load(f) - elif args.config: - config = json.loads(args.config) - - # Merge colors with defaults (convert all to tuples) - colors = {k: tuple_color(v) for k, v in DEFAULT_COLORS.items()} - for k, v in config.get("colors", {}).items(): - colors[k] = tuple_color(v) - - # Auto-derive missing colors - if "hair_light" not in config.get("colors", {}): - colors["hair_light"] = lighten(colors["hair"], 30) - if "mouth" not in config.get("colors", {}): - colors["mouth"] = darken(colors["skin"], 80) - - # Merge features with defaults - features = dict(DEFAULT_FEATURES) - features.update(config.get("features", {})) - - sheet = Image.new("RGBA", (W, H), (0, 0, 0, 0)) - gen_all_animations(sheet, colors, features) - - import os - os.makedirs(os.path.dirname(os.path.abspath(args.output)), exist_ok=True) - sheet.save(args.output, "WEBP", quality=95, lossless=True) - print(f"Saved spritesheet to {args.output} ({sheet.size[0]}x{sheet.size[1]})") - - -if __name__ == "__main__": - main() diff --git a/packages/desktop/.dockerignore b/packages/desktop/.dockerignore deleted file mode 100644 index 1079fcd9da1..00000000000 --- a/packages/desktop/.dockerignore +++ /dev/null @@ -1,50 +0,0 @@ -# Dependencies (rebuilt inside container) -node_modules/ - -# Build artifacts -dist/ -*.tgz - -# Electron app output (not needed for server) -apps/electron/release/ -apps/electron/vendor/ - -# Git -.git/ -.gitignore - -# IDE -.vscode/ -.idea/ -*.swp -*.swo - -# OS -.DS_Store -Thumbs.db - -# CI/CD -.github/ - -# Tests -**/*.test.ts -**/*.spec.ts -**/tests/ -**/test/ -**/__tests__/ - -# Top-level docs (not resource docs used by server) -/docs/ -apps/online-docs/ -README*.md -CONTRIBUTING*.md -CHANGELOG*.md -LICENSE* - -# Docker files (avoid recursive context) -Dockerfile* -docker-compose*.yml -.dockerignore - -# Smoke test script (not needed inside image) -scripts/docker-smoke-test.sh diff --git a/packages/desktop/.github/ISSUE_TEMPLATE/bug_report.yml b/packages/desktop/.github/ISSUE_TEMPLATE/bug_report.yml deleted file mode 100644 index 6b832b8b74a..00000000000 --- a/packages/desktop/.github/ISSUE_TEMPLATE/bug_report.yml +++ /dev/null @@ -1,123 +0,0 @@ -name: Bug Report -description: Report a bug or unexpected behavior in OpenWork -labels: ["bug"] -body: - - type: markdown - attributes: - value: | - Thanks for taking the time to report a bug! Please fill out the sections below so we can reproduce and fix the issue. - - - type: input - id: version - attributes: - label: OpenWork Version - description: "Found in Settings or the title bar (e.g. 0.4.6)" - placeholder: "0.4.6" - validations: - required: true - - - type: dropdown - id: os - attributes: - label: Operating System - options: - - macOS (Apple Silicon) - - macOS (Intel) - - Windows 11 - - Windows 10 - - Linux (Ubuntu/Debian) - - Linux (Fedora/RHEL) - - Linux (Arch) - - Linux (Other) - validations: - required: true - - - type: input - id: os_version - attributes: - label: OS Version - description: "e.g. macOS 15.3, Windows 11 24H2, Ubuntu 24.04" - placeholder: "macOS 15.3" - validations: - required: true - - - type: dropdown - id: ai_provider - attributes: - label: AI Provider - description: Which AI provider/connection are you using? - options: - - Anthropic API (direct) - - Anthropic API (custom endpoint) - - OpenAI / Codex - - Copilot (GitHub) - - Other - validations: - required: true - - - type: input - id: model - attributes: - label: Model - description: "Which model are you using? (e.g. Claude Opus 4.7, GPT-4.1)" - placeholder: "Claude Opus 4.7" - - - type: textarea - id: description - attributes: - label: Description - description: A clear description of what the bug is. - validations: - required: true - - - type: textarea - id: steps - attributes: - label: Steps to Reproduce - description: Step-by-step instructions to reproduce the behavior. - value: | - 1. - 2. - 3. - validations: - required: true - - - type: textarea - id: expected - attributes: - label: Expected Behavior - description: What did you expect to happen? - validations: - required: true - - - type: textarea - id: actual - attributes: - label: Actual Behavior - description: What actually happened? - validations: - required: true - - - type: textarea - id: screenshots - attributes: - label: Screenshots / Screen Recordings - description: | - For **UI issues**, please attach screenshots or screen recordings showing the problem. - You can drag and drop images/videos directly into this field. - - - type: textarea - id: logs - attributes: - label: Debug Logs - description: | - For **non-UI issues** (crashes, errors, connection problems), please attach relevant logs from a debug session. - - Launch the app with `-- --debug` and reproduce the issue. - render: shell - - - type: textarea - id: additional - attributes: - label: Additional Context - description: Any other context about the problem (MCP sources used, workspace config, etc.) diff --git a/packages/desktop/.github/ISSUE_TEMPLATE/feature_request.yml b/packages/desktop/.github/ISSUE_TEMPLATE/feature_request.yml deleted file mode 100644 index e275776d94e..00000000000 --- a/packages/desktop/.github/ISSUE_TEMPLATE/feature_request.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Feature Request -description: Suggest a new feature or enhancement -labels: ["enhancement"] -body: - - type: markdown - attributes: - value: | - Have an idea for OpenWork? We'd love to hear it! - - - type: textarea - id: problem - attributes: - label: Problem or Motivation - description: What problem does this feature solve, or what workflow does it improve? - validations: - required: true - - - type: textarea - id: solution - attributes: - label: Proposed Solution - description: Describe the feature or change you'd like to see. - validations: - required: true - - - type: textarea - id: alternatives - attributes: - label: Alternatives Considered - description: Any workarounds or alternative approaches you've tried. - - - type: textarea - id: additional - attributes: - label: Additional Context - description: Screenshots, mockups, links, or any other context. diff --git a/packages/desktop/.github/workflows/desktop-release.yml b/packages/desktop/.github/workflows/desktop-release.yml deleted file mode 100644 index accb7ac1c21..00000000000 --- a/packages/desktop/.github/workflows/desktop-release.yml +++ /dev/null @@ -1,592 +0,0 @@ -name: Desktop Release - -run-name: Desktop release ${{ inputs.version }} - -on: - workflow_dispatch: - inputs: - version: - description: "Desktop app version to release, for example 0.0.2 or v0.0.2" - required: true - type: string - release_name: - description: "Release title. Defaults to the tag." - required: false - type: string - qwen_code_source: - description: "Qwen Code runtime source to vendor into the desktop app." - required: true - default: pinned_package_version - type: choice - options: - - npm_latest - - source_branch - - pinned_package_version - qwen_code_ref: - description: "QwenLM/qwen-code branch, tag, or commit when qwen_code_source is source_branch." - required: false - default: main - type: string - qwen_code_version: - description: "Optional exact @qwen-code/qwen-code npm version for pinned_package_version. Defaults to package.json qwenCodeRuntime.version." - required: false - type: string - dry_run: - description: "Build installers only. Do not create or update a GitHub Release." - required: true - default: true - type: boolean - draft: - description: "Create a draft release." - required: true - default: true - type: boolean - prerelease: - description: "Mark the release as a prerelease." - required: true - default: false - type: boolean - clobber: - description: "Replace same-named assets when uploading to an existing release." - required: true - default: false - type: boolean - -permissions: - contents: read - -concurrency: - group: desktop-release-${{ inputs.version }} - cancel-in-progress: false - -env: - BUN_VERSION: 1.3.9 - CRAFT_BRAND: openwork - -jobs: - release_metadata: - name: Prepare Release Source - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - contents: write - outputs: - release_branch: ${{ steps.release-branch.outputs.branch }} - release_ref: ${{ steps.release-branch.outputs.ref }} - tag: ${{ steps.release-version.outputs.tag }} - version: ${{ steps.release-version.outputs.version }} - - steps: - - name: Check out source - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up Node - uses: actions/setup-node@v6 - with: - node-version-file: '.nvmrc' - - - name: Set up Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: ${{ env.BUN_VERSION }} - - - name: Install dependencies - run: bun install --frozen-lockfile - - - name: Configure Git user - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - - name: Require main for publishing - if: ${{ inputs.dry_run == false }} - env: - SOURCE_REF: ${{ github.ref_name }} - run: | - set -euo pipefail - - if [ "$SOURCE_REF" != "main" ]; then - echo "::error::Desktop releases with dry_run=false must be run from main. Current ref: $SOURCE_REF" - exit 1 - fi - - - name: Bump desktop version - env: - INPUT_VERSION: ${{ inputs.version }} - run: bun run bump-desktop-version "$INPUT_VERSION" - - - name: Validate release version - id: release-version - env: - INPUT_VERSION: ${{ inputs.version }} - run: bun run check-release-version --version "$INPUT_VERSION" - - - name: Create release branch - id: release-branch - env: - IS_DRY_RUN: ${{ inputs.dry_run }} - RELEASE_TAG: ${{ steps.release-version.outputs.tag }} - run: | - set -euo pipefail - - branch="release/desktop-${RELEASE_TAG}" - git switch -C "$branch" - git add package.json apps/electron/package.json packages/shared/package.json - - if git diff --staged --quiet; then - echo "No desktop version changes to commit." - else - git commit -m "chore(release): desktop ${RELEASE_TAG}" - fi - - echo "branch=$branch" >> "$GITHUB_OUTPUT" - - if [ "$IS_DRY_RUN" = "false" ]; then - remote_sha="$(git ls-remote --heads origin "$branch" | awk '{print $1}')" - if [ -n "$remote_sha" ]; then - git push --force-with-lease="refs/heads/$branch:$remote_sha" origin "HEAD:refs/heads/$branch" - else - git push origin "HEAD:refs/heads/$branch" - fi - echo "ref=$branch" >> "$GITHUB_OUTPUT" - else - echo "Dry run enabled. Skipping release branch push." - echo "ref=$GITHUB_SHA" >> "$GITHUB_OUTPUT" - fi - - build: - name: Build ${{ matrix.name }} - runs-on: ${{ matrix.os }} - timeout-minutes: 90 - needs: release_metadata - env: - RELEASE_TAG: ${{ needs.release_metadata.outputs.tag }} - RELEASE_VERSION: ${{ needs.release_metadata.outputs.version }} - strategy: - fail-fast: false - matrix: - include: - - name: macOS - os: macos-latest - command: bun run dist:mac:no-publish - - name: Windows - os: windows-latest - command: bun run dist:win:no-publish - - name: Linux - os: ubuntu-22.04 - command: bun run dist:linux:no-publish - - steps: - - name: Check out source - uses: actions/checkout@v4 - with: - ref: ${{ needs.release_metadata.outputs.release_ref }} - - - name: Set up Node - uses: actions/setup-node@v6 - with: - node-version-file: '.nvmrc' - - - name: Check out Qwen Code source - if: ${{ inputs.qwen_code_source == 'source_branch' }} - shell: bash - env: - QWEN_CODE_REF_INPUT: ${{ inputs.qwen_code_ref }} - QWEN_CODE_SOURCE_ROOT: ${{ runner.temp }}/qwen-code-source - run: | - set -euo pipefail - - if [ -z "$QWEN_CODE_REF_INPUT" ]; then - echo "::error::qwen_code_ref is required when qwen_code_source is source_branch." - exit 1 - fi - - rm -rf "$QWEN_CODE_SOURCE_ROOT" - git init "$QWEN_CODE_SOURCE_ROOT" - git -C "$QWEN_CODE_SOURCE_ROOT" remote add origin https://github.com/QwenLM/qwen-code.git - - if ! git -C "$QWEN_CODE_SOURCE_ROOT" fetch --depth=1 origin "$QWEN_CODE_REF_INPUT"; then - if ! git -C "$QWEN_CODE_SOURCE_ROOT" fetch --depth=1 origin "refs/heads/$QWEN_CODE_REF_INPUT"; then - git -C "$QWEN_CODE_SOURCE_ROOT" fetch --depth=1 origin "refs/tags/$QWEN_CODE_REF_INPUT" - fi - fi - - git -C "$QWEN_CODE_SOURCE_ROOT" checkout --detach FETCH_HEAD - git config --global --add safe.directory "$QWEN_CODE_SOURCE_ROOT" - - - name: Set up Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: ${{ env.BUN_VERSION }} - - - name: Install Linux packaging dependencies - if: runner.os == 'Linux' - run: | - sudo apt-get update - sudo apt-get install -y libfuse2 - - - name: Install dependencies - run: bun install --frozen-lockfile - - - name: Install Qwen Code source dependencies - if: ${{ inputs.qwen_code_source == 'source_branch' }} - working-directory: ${{ runner.temp }}/qwen-code-source - run: npm ci - - - name: Bump desktop version - run: bun run bump-desktop-version "${{ needs.release_metadata.outputs.version }}" - - - name: Confirm release version - run: bun run check-release-version --version "${{ needs.release_metadata.outputs.version }}" - - - name: Configure Qwen Code runtime source - shell: bash - env: - QWEN_CODE_REF_INPUT: ${{ inputs.qwen_code_ref }} - QWEN_CODE_SOURCE_INPUT: ${{ inputs.qwen_code_source }} - QWEN_CODE_SOURCE_ROOT: ${{ runner.temp }}/qwen-code-source - QWEN_CODE_VERSION_INPUT: ${{ inputs.qwen_code_version }} - run: | - set -euo pipefail - - case "$QWEN_CODE_SOURCE_INPUT" in - npm_latest) - echo "QWEN_CODE_VERSION=latest" >> "$GITHUB_ENV" - echo "Using Qwen Code runtime from npm dist-tag: latest" - ;; - source_branch) - if [ -z "$QWEN_CODE_REF_INPUT" ]; then - echo "::error::qwen_code_ref is required when qwen_code_source is source_branch." - exit 1 - fi - echo "QWEN_CODE_ROOT=$QWEN_CODE_SOURCE_ROOT" >> "$GITHUB_ENV" - echo "Using Qwen Code runtime from QwenLM/qwen-code ref: $QWEN_CODE_REF_INPUT" - ;; - pinned_package_version) - if [ -n "$QWEN_CODE_VERSION_INPUT" ]; then - echo "QWEN_CODE_VERSION=$QWEN_CODE_VERSION_INPUT" >> "$GITHUB_ENV" - echo "Using exact Qwen Code npm version: $QWEN_CODE_VERSION_INPUT" - else - echo "Using Qwen Code runtime from package.json qwenCodeRuntime.version" - fi - ;; - *) - echo "::error::Unknown qwen_code_source: $QWEN_CODE_SOURCE_INPUT" - exit 1 - ;; - esac - - - name: Configure optional signing secrets - shell: bash - env: - IS_DRY_RUN: ${{ inputs.dry_run }} - APPLE_APP_SPECIFIC_PASSWORD_SECRET: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} - APPLE_ID_SECRET: ${{ secrets.APPLE_ID }} - APPLE_TEAM_ID_SECRET: ${{ secrets.APPLE_TEAM_ID }} - IS_DRAFT: ${{ inputs.draft }} - MAC_CSC_KEY_PASSWORD_SECRET: ${{ secrets.MAC_CSC_KEY_PASSWORD }} - MAC_CSC_LINK_SECRET: ${{ secrets.MAC_CSC_LINK }} - CSC_KEY_PASSWORD_SECRET: ${{ secrets.CSC_KEY_PASSWORD }} - CSC_LINK_SECRET: ${{ secrets.CSC_LINK }} - WIN_CSC_KEY_PASSWORD_SECRET: ${{ secrets.WIN_CSC_KEY_PASSWORD }} - WIN_CSC_LINK_SECRET: ${{ secrets.WIN_CSC_LINK }} - SENTRY_ELECTRON_INGEST_URL_SECRET: ${{ secrets.SENTRY_ELECTRON_INGEST_URL }} - run: | - set -euo pipefail - - append_env() { - local name="$1" - local value="$2" - - if [ -z "$value" ]; then - return - fi - - { - echo "$name<<__${name}__" - printf '%s\n' "$value" - echo "__${name}__" - } >> "$GITHUB_ENV" - } - - mac_csc_link="${MAC_CSC_LINK_SECRET:-$CSC_LINK_SECRET}" - mac_csc_key_password="${MAC_CSC_KEY_PASSWORD_SECRET:-$CSC_KEY_PASSWORD_SECRET}" - - allow_unsigned_artifacts() { - if [ "$IS_DRY_RUN" = "true" ]; then - return 0 - fi - - if [ "$IS_DRAFT" = "true" ]; then - return 0 - fi - - return 1 - } - - if [ "$RUNNER_OS" = "macOS" ]; then - if [ -n "$mac_csc_link" ]; then - if [ -z "$mac_csc_key_password" ]; then - echo "::error::MAC_CSC_LINK/CSC_LINK is configured, but MAC_CSC_KEY_PASSWORD/CSC_KEY_PASSWORD is missing." - exit 1 - fi - - append_env "CSC_LINK" "$mac_csc_link" - append_env "CSC_KEY_PASSWORD" "$mac_csc_key_password" - append_env "APPLE_ID" "$APPLE_ID_SECRET" - append_env "APPLE_APP_SPECIFIC_PASSWORD" "$APPLE_APP_SPECIFIC_PASSWORD_SECRET" - append_env "APPLE_TEAM_ID" "$APPLE_TEAM_ID_SECRET" - echo "CSC_IDENTITY_AUTO_DISCOVERY=true" >> "$GITHUB_ENV" - else - if ! allow_unsigned_artifacts; then - echo "::error::Published macOS desktop releases require MAC_CSC_LINK/CSC_LINK and MAC_CSC_KEY_PASSWORD/CSC_KEY_PASSWORD so auto-update signature validation can pass." - exit 1 - fi - - if [ "$IS_DRY_RUN" = "false" ]; then - echo "::warning::Publishing an unsigned macOS draft release for maintainer testing. Auto-update validation is not supported for this artifact." - fi - - echo "CSC_IDENTITY_AUTO_DISCOVERY=false" >> "$GITHUB_ENV" - fi - elif [ "$RUNNER_OS" = "Windows" ]; then - if [ -n "$WIN_CSC_LINK_SECRET" ]; then - if [ -z "$WIN_CSC_KEY_PASSWORD_SECRET" ]; then - echo "::error::WIN_CSC_LINK is configured, but WIN_CSC_KEY_PASSWORD is missing." - exit 1 - fi - - append_env "WIN_CSC_LINK" "$WIN_CSC_LINK_SECRET" - append_env "WIN_CSC_KEY_PASSWORD" "$WIN_CSC_KEY_PASSWORD_SECRET" - else - if ! allow_unsigned_artifacts; then - echo "::error::Published Windows desktop releases require WIN_CSC_LINK and WIN_CSC_KEY_PASSWORD." - exit 1 - fi - - if [ "$IS_DRY_RUN" = "false" ]; then - echo "::warning::Publishing an unsigned Windows draft release for maintainer testing. Windows may show Unknown Publisher / SmartScreen warnings." - else - echo "Windows signing certificate is not configured; Windows dry-run artifacts will be unsigned." - fi - fi - else - if [ "$RUNNER_OS" != "Linux" ] && [ -n "$CSC_LINK_SECRET" ]; then - echo "::warning::CSC_LINK is configured but not used on $RUNNER_OS." - fi - fi - - append_env "SENTRY_ELECTRON_INGEST_URL" "$SENTRY_ELECTRON_INGEST_URL_SECRET" - - - name: Build desktop installer - # Build jobs only produce artifacts. The publish job below owns GitHub - # Release creation/upload so dry-run, draft, prerelease, and replace - # behavior stays centralized. - run: ${{ matrix.command }} - - - name: Upload installer artifacts - uses: actions/upload-artifact@v4 - with: - name: desktop-${{ matrix.name }} - if-no-files-found: error - retention-days: 14 - path: | - apps/electron/release/*.AppImage - apps/electron/release/*.blockmap - apps/electron/release/*.dmg - apps/electron/release/*.exe - apps/electron/release/*.yml - apps/electron/release/*.zip - - publish: - name: Publish GitHub Release - runs-on: ubuntu-latest - timeout-minutes: 20 - needs: - - build - - release_metadata - if: ${{ inputs.dry_run == false }} - permissions: - contents: write - env: - RELEASE_TAG: ${{ needs.release_metadata.outputs.tag }} - RELEASE_VERSION: ${{ needs.release_metadata.outputs.version }} - - steps: - - name: Download installer artifacts - uses: actions/download-artifact@v4 - with: - path: release-assets - merge-multiple: true - - - name: Publish release assets - env: - GH_REPO: ${{ github.repository }} - GH_TOKEN: ${{ github.token }} - RELEASE_DRAFT: ${{ inputs.draft }} - RELEASE_NAME: ${{ inputs.release_name }} - RELEASE_PRERELEASE: ${{ inputs.prerelease }} - RELEASE_TARGET: ${{ needs.release_metadata.outputs.release_ref }} - UPLOAD_CLOBBER: ${{ inputs.clobber }} - run: | - set -euo pipefail - - assets=() - while IFS= read -r -d '' file; do - assets+=("$file") - done < <(find release-assets -type f -print0 | sort -z) - - if [ "${#assets[@]}" -eq 0 ]; then - echo "No release assets were downloaded." - exit 1 - fi - - printf 'Release assets:\n' - printf ' %s\n' "${assets[@]}" - - title="${RELEASE_NAME:-$RELEASE_TAG}" - - if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then - upload_args=("$RELEASE_TAG" "${assets[@]}") - if [ "$UPLOAD_CLOBBER" = "true" ]; then - upload_args+=(--clobber) - fi - gh release upload "${upload_args[@]}" - else - previous_tag="$( - gh release list \ - --repo "$GH_REPO" \ - --limit 100 \ - --json tagName,isDraft,isPrerelease \ - --jq '.[] | select(.isDraft == false and .isPrerelease == false) | .tagName' \ - | grep -vxF "$RELEASE_TAG" \ - | head -n 1 \ - || true - )" - - create_args=( - "$RELEASE_TAG" - "${assets[@]}" - --generate-notes - --target "$RELEASE_TARGET" - --title "$title" - ) - if [ -n "$previous_tag" ]; then - echo "Using $previous_tag as the release notes start tag." - create_args+=(--notes-start-tag "$previous_tag") - else - echo "No previous published stable release found for release notes." - fi - if [ "$RELEASE_DRAFT" = "true" ]; then - create_args+=(--draft) - fi - if [ "$RELEASE_PRERELEASE" = "true" ]; then - create_args+=(--prerelease) - fi - gh release create "${create_args[@]}" - fi - - sync-version: - name: Sync Release Version to Main - runs-on: ubuntu-latest - timeout-minutes: 10 - needs: - - publish - - release_metadata - if: ${{ inputs.dry_run == false && inputs.draft == false && inputs.prerelease == false }} - permissions: - contents: write - pull-requests: write - - steps: - - name: Create version sync PR - id: version-pr - env: - GH_TOKEN: ${{ secrets.CI_BOT_PAT || github.token }} - RELEASE_BRANCH: ${{ needs.release_metadata.outputs.release_branch }} - RELEASE_TAG: ${{ needs.release_metadata.outputs.tag }} - run: | - set -euo pipefail - - pr_url="$(gh pr list \ - --repo "$GITHUB_REPOSITORY" \ - --head "$RELEASE_BRANCH" \ - --base main \ - --json url \ - --jq '.[0].url')" - - if [ -z "$pr_url" ]; then - pr_url="$(gh pr create \ - --repo "$GITHUB_REPOSITORY" \ - --base main \ - --head "$RELEASE_BRANCH" \ - --title "chore(release): desktop ${RELEASE_TAG}" \ - --body "Automated desktop release PR for ${RELEASE_TAG}. Syncs desktop package versions on main.")" - fi - - echo "url=$pr_url" >> "$GITHUB_OUTPUT" - - - name: Enable auto-merge - env: - GH_TOKEN: ${{ secrets.CI_BOT_PAT || github.token }} - PR_URL: ${{ steps.version-pr.outputs.url }} - RELEASE_TAG: ${{ needs.release_metadata.outputs.tag }} - run: | - set -euo pipefail - - gh pr merge "$PR_URL" \ - --squash \ - --auto \ - --delete-branch \ - --subject "chore(release): desktop ${RELEASE_TAG} [skip ci]" - - dry-run-summary: - name: Dry Run Summary - runs-on: ubuntu-latest - timeout-minutes: 10 - needs: - - build - - release_metadata - if: ${{ inputs.dry_run }} - env: - RELEASE_TAG: ${{ needs.release_metadata.outputs.tag }} - RELEASE_VERSION: ${{ needs.release_metadata.outputs.version }} - - steps: - - name: Download installer artifacts - uses: actions/download-artifact@v4 - with: - path: release-assets - merge-multiple: true - - - name: List release assets - run: | - set -euo pipefail - - assets=() - while IFS= read -r -d '' file; do - assets+=("$file") - done < <(find release-assets -type f -print0 | sort -z) - - if [ "${#assets[@]}" -eq 0 ]; then - echo "No release assets were downloaded." - exit 1 - fi - - { - echo "## Desktop release dry run" - echo - echo "Version: $RELEASE_VERSION" - echo "Release tag: $RELEASE_TAG" - echo - echo "Built ${#assets[@]} asset(s). No GitHub Release was created or updated." - echo - echo "| Asset | Size |" - echo "| --- | ---: |" - for file in "${assets[@]}"; do - size=$(du -h "$file" | cut -f1) - echo "| $(basename "$file") | $size |" - done - } >> "$GITHUB_STEP_SUMMARY" diff --git a/packages/desktop/.gitignore b/packages/desktop/.gitignore deleted file mode 100644 index 8c0b6f7574a..00000000000 --- a/packages/desktop/.gitignore +++ /dev/null @@ -1,75 +0,0 @@ -# dependencies (bun install) -node_modules - -# output -out -dist -*.tgz -.build -apps/electron/release -apps/electron/electron-builder.generated.yml -apps/electron/resources/session-mcp-server/ -apps/electron/resources/pi-agent-server/ - -# code coverage -coverage -*.lcov - -# logs -logs -_.log -report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# caches -.eslintcache -.cache -*.tsbuildinfo -.wrangler -.mintlify - -# OSS sync temp directory -.oss-sync-temp/ - -# IntelliJ based IDEs -.idea - -# Python bytecode -__pycache__/ -*.pyc - -# Finder (MacOS) folder config -.DS_Store - -# Claude Code local settings (user-specific) -.claude/settings.local.json - -# Accidental literal tilde directories (Node.js doesn't expand ~) -~/ - -# Lock files from other package managers -pnpm-lock.yaml - -# Craft Agent local data (sessions, credentials, config) -.craft-agent/ - -# SSH keys and secrets -*.pem -*.key -sshkey -id_rsa* -id_ed25519* - -# Credentials files -credentials.enc -*.credentials - -# act (local GitHub Actions runner) -.secrets -.actrc diff --git a/packages/desktop/.nvmrc b/packages/desktop/.nvmrc deleted file mode 100644 index 2bd5a0a98a3..00000000000 --- a/packages/desktop/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -22 diff --git a/packages/desktop/CODE_OF_CONDUCT.md b/packages/desktop/CODE_OF_CONDUCT.md deleted file mode 100644 index 81156dcc986..00000000000 --- a/packages/desktop/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,28 +0,0 @@ -# Code of Conduct - -## Our Pledge - -We are committed to providing a welcoming and inclusive environment for everyone who participates in OpenWork. - -## Our Standards - -We expect all participants to: - -- Be respectful and considerate -- Welcome newcomers and help them learn -- Accept constructive feedback gracefully -- Focus on what is best for the community - -## Enforcement - -OpenWork does not currently maintain a dedicated private conduct-reporting inbox. - -If you need to report unacceptable behavior, please contact a repository maintainer through the current GitHub repository channels. If the report includes sensitive personal information, avoid posting those details publicly and ask a maintainer for a private contact path first. - -Reports will be reviewed in good faith by the maintainers who are currently responsible for the project. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1. - -For the full text, see: https://www.contributor-covenant.org/version/2/1/code_of_conduct/ diff --git a/packages/desktop/CONTRIBUTING.md b/packages/desktop/CONTRIBUTING.md deleted file mode 100644 index aa5faa3d9aa..00000000000 --- a/packages/desktop/CONTRIBUTING.md +++ /dev/null @@ -1,103 +0,0 @@ -# Contributing to OpenWork - -Thank you for your interest in contributing to OpenWork. This guide covers the -local development workflow for the desktop app and shared packages. - -## Prerequisites - -- [Bun](https://bun.sh/) 1.3 or newer -- Node.js 22 or newer for the Qwen Code runtime and related tooling -- macOS, Linux, or Windows - -## Development Setup - -1. Clone the repository: - - ```bash - git clone https://github.com/modelstudioai/openwork.git - cd openwork - ``` - -2. Install dependencies: - - ```bash - bun install - ``` - -3. Start the Electron app in development mode: - - ```bash - CRAFT_BRAND=modelstudio bun run electron:dev - ``` - - To run against a local Qwen Code checkout instead of a vendored or npm - runtime, pass the source root: - - ```bash - CRAFT_BRAND=modelstudio \ - QWEN_CODE_ROOT=/path/to/qwen-code \ - bun run electron:dev - ``` - -## Useful Commands - -Run focused checks whenever possible: - -```bash -bun run typecheck:shared -bun run typecheck:electron -bun run typecheck:all -``` - -Build the desktop app resources: - -```bash -bun run electron:build -``` - -Package a dev build: - -```bash -CRAFT_DEV_RUNTIME=1 bun run electron:dist:dev:mac -``` - -## Project Structure - -```text -openwork/ -├── apps/ -│ ├── electron/ # Electron desktop app -│ ├── cli/ # Command-line entry points -│ ├── viewer/ # Shared session viewer -│ └── webui/ # Web UI build -├── packages/ -│ ├── shared/ # Shared app logic and protocol types -│ ├── server-core/ # Server/session orchestration -│ ├── server/ # Server entry point -│ ├── ui/ # React UI components -│ └── core/ # Shared lower-level utilities -└── scripts/ # Build, dev, and packaging scripts -``` - -## Contribution Guidelines - -- Keep changes focused and minimal. -- Follow existing TypeScript and React patterns. -- Prefer existing shared helpers over introducing new abstractions. -- Include screenshots or a short screen recording for visible UI changes. -- Mention the commands you ran in the PR description. -- Do not include generated build artifacts unless the project explicitly tracks - them. - -## Pull Requests - -1. Create a branch from the target branch. -2. Make the smallest change that solves the problem. -3. Run the relevant focused checks. -4. Open a pull request with a clear summary, testing notes, and screenshots for - UI changes. - -## License - -By contributing, you agree that your contributions are licensed under the same -license as this repository. diff --git a/packages/desktop/LICENSE b/packages/desktop/LICENSE deleted file mode 100644 index 557a2f9750f..00000000000 --- a/packages/desktop/LICENSE +++ /dev/null @@ -1,191 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to the Licensor for inclusion in the Work by the copyright - owner or by an individual or Legal Entity authorized to submit on - behalf of the copyright owner. For the purposes of this definition, - "submitted" means any form of electronic, verbal, or written - communication sent to the Licensor or its representatives, including - but not limited to communication on electronic mailing lists, source - code control systems, and issue tracking systems that are managed by, - or on behalf of, the Licensor for the purpose of discussing and - improving the Work, but excluding communication that is conspicuously - marked or otherwise designated in writing by the copyright owner as - "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - Copyright 2026 Craft Docs Ltd. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/packages/desktop/NOTICE b/packages/desktop/NOTICE deleted file mode 100644 index ac4a5f93d70..00000000000 --- a/packages/desktop/NOTICE +++ /dev/null @@ -1,23 +0,0 @@ -Craft Agents -Copyright 2026 Craft Docs Ltd. - -This product includes software developed by Craft Docs Ltd. -https://craft.do - ---- - -This project uses the Claude Agent SDK, which is subject to Anthropic's -Commercial Terms of Service: https://www.anthropic.com/legal/commercial-terms - ---- - -OpenWork -Copyright 2026 Model Studio AI - -OpenWork includes modifications and extensions developed by Model Studio AI. -https://github.com/modelstudioai/openwork - ---- - -Third-party dependencies are listed in package.json files and are subject -to their respective licenses. diff --git a/packages/desktop/README.md b/packages/desktop/README.md deleted file mode 100644 index a4745ae03e2..00000000000 --- a/packages/desktop/README.md +++ /dev/null @@ -1,189 +0,0 @@ -# Qwen Code - -Qwen Code is a desktop and headless agent workspace. It provides multi-session chat, source connections, skills, file previews, automations, and permission modes in a local-first application. - -## Backend - -This fork is Qwen-only: - -- Agent sessions run through Qwen Code over ACP. -- The app does not store third-party LLM API keys. -- The built-in LLM connection is `qwen-code`. -- Legacy multi-provider backends and package/runtime wiring have been removed. - -## Qwen Code CLI Runtime - -The desktop app talks to the Qwen Code CLI over ACP. Treat the CLI as a -runtime artifact, not as desktop source code. A packaged app must bundle a -known CLI build so users can launch it without installing `qwen` separately. - -Use one of these workflows depending on what you are developing: - -| Workflow | Use it when | Commands | -| --------------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------- | -| Default desktop development | You are developing desktop only. | `bun run dev` | -| Published npm package | You want a specific published CLI version for dev, CI, or release builds. | `QWEN_CODE_VERSION=0.15.12-acp.0 bun run dev` | -| Local npm tarball | You need to verify the exact package contents before publishing. | `QWEN_CODE_TARBALL=/path/to/qwen-code-0.15.12-acp.0.tgz bun run dev` | -| Local qwen-code checkout | You are changing ACP or other CLI behavior while testing desktop. | `QWEN_CODE_ROOT=/path/to/qwen-code bun run dev` | -| Explicit CLI entry | You need to point at a specific CLI file. | `QWEN_CODE_CLI=/path/to/qwen-code/scripts/dev.js bun run dev` | - -`electron:dev` uses local overrides first. If no override is set and this -repository is not inside the qwen-code monorepo, it vendors the default version -from `qwenCodeRuntime.version` in `package.json` and points Electron at the -vendored CLI automatically. - -If you are preparing a package without publishing it, create the tarball from -the Qwen Code repository and point desktop at it: - -```bash -cd /path/to/qwen-code -npm run build -npm run bundle -npm run prepare:package -npm pack - -cd /path/to/desktop -QWEN_CODE_TARBALL=/path/to/qwen-code/qwen-code-0.15.12-acp.0.tgz bun run dist:mac -``` - -Distribution builds run `electron:vendor:qwen` automatically. Set -`QWEN_CODE_VERSION` or `QWEN_CODE_TARBALL` when you want the packaged app to use -a published or packed CLI artifact. If neither is set, this monorepo builds -from the local checkout; a standalone desktop checkout uses -`qwenCodeRuntime.version` from `package.json`. - -Development runtime resolution checks sources in this order: - -```text -QWEN_CODE_CLI / QWEN_CODE_ROOT / QWEN_CODE_PATH -QWEN_CODE_TARBALL -QWEN_CODE_VERSION -local monorepo checkout -existing vendored CLI -qwenCodeRuntime.version from package.json -``` - -Distribution vendoring checks sources in this order: - -```text -QWEN_CODE_TARBALL -QWEN_CODE_VERSION -QWEN_CODE_ROOT / QWEN_CODE_PATH -local monorepo checkout -qwenCodeRuntime.version from package.json -``` - -## Installation - -```bash -bun install -bun run dev -``` - -## Common Commands - -```bash -bun run typecheck:all -bun run test:shared -bun run dev -bun run server:start -``` - -## Building for Distribution - -All build commands run from `packages/desktop/`. - -### Prerequisites - -- [Bun](https://bun.sh) (see `.bun-version` for exact version) -- `bun install` — install all workspace dependencies - -### Developer Build (no code signing) - -Use this for local testing. Produces an ad-hoc signed app. - -```bash -# macOS (arm64 + x64) -bun run electron:dist:dev:mac - -# Windows -bun run electron:dist:dev:win - -# Linux -bun run electron:dist:dev:linux -``` - -### Release Build (with code signing) - -```bash -bun run electron:dist:mac -bun run electron:dist:win -bun run electron:dist:linux -``` - -Release builds require signing credentials via environment variables: - -| Variable | Purpose | -| ----------------------------- | --------------------------- | -| `CSC_LINK` | Path to signing certificate | -| `APPLE_ID` | Apple ID for notarization | -| `APPLE_APP_SPECIFIC_PASSWORD` | App-specific password | -| `APPLE_TEAM_ID` | Team ID for notarization | - -### Build Output - -All artifacts are written to `apps/electron/release/`: - -| Platform | Artifact | -| -------- | ------------------------------------------------------------------------ | -| macOS | `Qwen-Code-Desktop-{arm64,x64}.dmg`, `Qwen-Code-Desktop-{arm64,x64}.zip` | -| Windows | `Qwen-Code-Desktop-x64.exe` | -| Linux | `Qwen-Code-Desktop-x64.AppImage` | - -### What the Build Does - -Each `electron:dist:*` command runs three stages: - -1. **`electron:vendor:qwen`** — vendors a Qwen Code CLI runtime into `vendor/qwen-code/`. Set `QWEN_CODE_VERSION` to download a published npm version, or `QWEN_CODE_TARBALL` to use a local `npm pack` tarball. If neither is set in this monorepo, it builds from the local checkout. -2. **`electron:build`** — compiles the app via esbuild (main + preload), Vite (renderer), and copies resources/assets. -3. **`electron-builder`** — downloads the Electron runtime, packages the app, signs it, and produces distributable installers (DMG, NSIS, AppImage). - -## CLI - -```bash -bun run apps/cli/src/index.ts run "Hello from Qwen" -bun run apps/cli/src/index.ts run --workspace-dir ./project "Summarize this repo" -``` - -The `run` command spawns a headless server, creates a temporary session, streams the response, and exits. Provider flags are accepted only for compatibility; the backend remains Qwen Code. - -## Repository Layout - -```text -apps/ - electron/ Desktop app - cli/ Terminal client - webui/ Web adapter -packages/ - shared/ Agent, config, prompts, sessions, sources - server-core/ RPC handlers and session manager - core/ Shared types - ui/ Shared UI components - session-tools-core/ - session-mcp-server/ -scripts/ Build and packaging helpers -``` - -## Capabilities - -- Multi-session inbox with streaming responses and tool visualization -- Qwen Code model discovery through ACP -- MCP, REST API, and local filesystem sources -- Skills stored per workspace -- Permission modes for planning, asking before edits, and autonomous execution -- File attachments and in-app previews for images, PDFs, Office files, and diffs -- Event-driven automations and messaging integrations - -## License - -Apache 2.0. Third-party dependencies are listed in package manifests and are subject to their respective licenses. diff --git a/packages/desktop/SECURITY.md b/packages/desktop/SECURITY.md deleted file mode 100644 index 65d144b77bb..00000000000 --- a/packages/desktop/SECURITY.md +++ /dev/null @@ -1,58 +0,0 @@ -# Security Policy - -## Reporting a Vulnerability - -We take security seriously. If you discover a security vulnerability in OpenWork, please report it responsibly. - -### How to Report - -Please do not publish exploit details in public GitHub issues. - -OpenWork does not currently maintain a dedicated security email address. If GitHub private vulnerability reporting is available for this repository, use that channel. Otherwise, open a minimal public issue that requests maintainer contact without including exploit details, secrets, or proof-of-concept code. - -Include the following information: - -- Description of the vulnerability -- Steps to reproduce the issue -- Potential impact -- Any suggested fixes (optional) - -### What to Expect - -Maintainers will review reports on a best-effort basis. Response and resolution timelines depend on maintainer availability and the severity of the issue. - -### Scope - -This policy applies to: - -- The OpenWork desktop application -- OpenWork server and shared packages -- Official OpenWork repositories - -### Out of Scope - -- Third-party dependencies (report to their maintainers) -- Social engineering attacks -- Denial of service attacks - -## Supported Versions - -| Version | Supported | -| -------- | ------------------ | -| Latest | :white_check_mark: | -| < Latest | :x: | - -We currently provide security updates for the latest version only. Please keep your installation up to date. - -## Security Best Practices - -When using OpenWork: - -1. **Keep credentials secure**: Never commit `.env` files or credentials -2. **Use environment variables**: Store secrets in environment variables -3. **Review permissions**: Be cautious with "Execute" permission mode -4. **Update regularly**: Keep the application updated - -## Acknowledgments - -We appreciate responsible disclosure and will acknowledge security researchers who report valid vulnerabilities (with their permission). diff --git a/packages/desktop/TRADEMARK.md b/packages/desktop/TRADEMARK.md deleted file mode 100644 index 4911ce43927..00000000000 --- a/packages/desktop/TRADEMARK.md +++ /dev/null @@ -1,103 +0,0 @@ -# Trademark Policy - -This trademark policy describes how the OpenWork name, Model Studio AI branding, and related project identifiers may be used. - -## Trademarks - -The following names and branding are associated with this project: - -- **OpenWork** -- **Model Studio AI** -- OpenWork logos, icons, and visual branding - -## What You Can Do - -### Use the Code Freely - -The OpenWork source code is licensed under the Apache License 2.0. You are free to: - -- Use, modify, and distribute the code -- Create derivative works -- Use the software for any purpose, including commercial use - -The license for the code does not grant permission to use OpenWork or Model Studio AI branding in a way that suggests endorsement or official status. - -### Make Factual Statements - -You may make accurate, factual statements about your relationship to the project: - -- "Based on OpenWork" -- "Built with OpenWork technology" -- "Compatible with OpenWork" -- "Fork of OpenWork" - -You may also make accurate attribution statements about upstream projects, such as Craft Agents OSS and Qwen Code, when describing the technical foundations of OpenWork. - -### Contribute to the Project - -Contributors may use the project name when discussing their contributions to OpenWork. - -## What You Cannot Do - -### Use OpenWork or Model Studio AI Branding for Forks - -If you create a fork or derivative work, you **must**: - -- Choose a different name that does not imply it is the official OpenWork project -- Remove or replace OpenWork and Model Studio AI logos and icons -- Update bundle identifiers, package metadata, and product names to your own -- Avoid using Model Studio AI branding unless you have explicit permission - -### Imply Official Endorsement - -You may not: - -- Use "OpenWork" or "Model Studio AI" as your product name in a confusing way -- Use OpenWork or Model Studio AI logos as your application icon -- Suggest that your fork is the official version -- Imply that Model Studio AI endorses your product - -### Create Confusion - -You may not use the trademarks in any way that: - -- Suggests your product is created by or affiliated with Model Studio AI -- Could cause confusion between your product and the official OpenWork project -- Misrepresents the relationship between your product, OpenWork, Model Studio AI, Craft Agents OSS, or Qwen Code - -## Branding Locations - -For those creating forks, the following files contain branding that should be updated: - -| File | Contains | -| ------------------------------------ | ---------------------------------- | -| `apps/electron/electron-builder.yml` | Product name, bundle ID, copyright | -| `apps/electron/resources/` | Application icons | -| `packages/shared/src/branding.ts` | Product name, bundle ID, URLs | - -## Examples - -### Acceptable - -- "MyAgent - based on OpenWork" -- "This project is a fork of OpenWork" -- "Compatible with OpenWork" - -### Not Acceptable - -- "OpenWork Pro" -- "OpenWork for Linux" -- "Better OpenWork" -- Using OpenWork or Model Studio AI logos for your fork without permission - -## Questions - -OpenWork does not currently maintain a dedicated trademark or legal contact email. If you have questions about this policy or want to request permission for a specific use, please open an issue in the repository. - -## Changes - -This policy may be updated from time to time. The current version will always be available in this repository. - ---- - -_This trademark policy is inspired by similar policies from Mozilla, WordPress, and the Apache Software Foundation._ diff --git a/packages/desktop/apps/cli/package.json b/packages/desktop/apps/cli/package.json deleted file mode 100644 index ce6649fc404..00000000000 --- a/packages/desktop/apps/cli/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "@craft-agent/cli", - "version": "0.0.1", - "license": "Apache-2.0", - "description": "Terminal client for Qwen Code server", - "type": "module", - "main": "src/index.ts", - "bin": { - "craft-cli": "src/index.ts" - }, - "scripts": { - "start": "bun run src/index.ts", - "typecheck": "tsc --noEmit", - "test": "bun test src/" - }, - "dependencies": { - "@craft-agent/shared": "workspace:*", - "@craft-agent/server-core": "workspace:*" - }, - "devDependencies": { - "typescript": "^5.8.2", - "@types/node": "^22.0.0", - "@types/bun": "latest" - } -} diff --git a/packages/desktop/apps/cli/src/client.test.ts b/packages/desktop/apps/cli/src/client.test.ts deleted file mode 100644 index 4ebef9a2f58..00000000000 --- a/packages/desktop/apps/cli/src/client.test.ts +++ /dev/null @@ -1,409 +0,0 @@ -import { describe, it, expect, afterEach } from 'bun:test' -import { CliRpcClient } from './client.ts' -import { - serializeEnvelope, - deserializeEnvelope, -} from '@craft-agent/server-core/transport' -import type { MessageEnvelope } from '@craft-agent/shared/protocol' - -// --------------------------------------------------------------------------- -// Mock WS server helpers -// --------------------------------------------------------------------------- - -interface MockServer { - url: string - port: number - close: () => void - lastMessage: () => MessageEnvelope | null - sendToAll: (envelope: MessageEnvelope) => void -} - -function createMockServer(opts?: { - rejectAuth?: boolean - noAck?: boolean - tls?: { cert: string; key: string } -}): MockServer { - let lastMsg: MessageEnvelope | null = null - const clients = new Set() - - const server = Bun.serve({ - port: 0, - tls: opts?.tls, - fetch(req, server) { - if (server.upgrade(req)) return undefined - return new Response('Not found', { status: 404 }) - }, - websocket: { - message(ws, message) { - const raw = typeof message === 'string' ? message : new TextDecoder().decode(message) - const envelope = deserializeEnvelope(raw) - lastMsg = envelope - - if (envelope.type === 'handshake') { - if (opts?.rejectAuth) { - const error: MessageEnvelope = { - id: envelope.id, - type: 'error', - error: { code: 'AUTH_FAILED', message: 'Invalid token' }, - } - ws.send(serializeEnvelope(error)) - ws.close() - return - } - - if (opts?.noAck) return // Simulate timeout - - const ack: MessageEnvelope = { - id: crypto.randomUUID(), - type: 'handshake_ack', - clientId: 'test-client-001', - protocolVersion: '1.0', - } - ws.send(serializeEnvelope(ack)) - return - } - - if (envelope.type === 'request') { - // Default: echo args back as result - const response: MessageEnvelope = { - id: envelope.id, - type: 'response', - channel: envelope.channel, - result: envelope.args, - } - ws.send(serializeEnvelope(response)) - } - }, - open(ws) { - clients.add(ws) - }, - close(ws) { - clients.delete(ws) - }, - }, - }) - - const protocol = opts?.tls ? 'wss' : 'ws' - const port = server.port! - return { - url: `${protocol}://127.0.0.1:${port}`, - port, - close: () => server.stop(true), - lastMessage: () => lastMsg, - sendToAll: (envelope: MessageEnvelope) => { - const data = serializeEnvelope(envelope) - for (const ws of clients) ws.send(data) - }, - } -} - -function createErrorServer(): MockServer { - let lastMsg: MessageEnvelope | null = null - const clients = new Set() - - const server = Bun.serve({ - port: 0, - fetch(req, server) { - if (server.upgrade(req)) return undefined - return new Response('Not found', { status: 404 }) - }, - websocket: { - message(ws, message) { - const raw = typeof message === 'string' ? message : new TextDecoder().decode(message) - const envelope = deserializeEnvelope(raw) - lastMsg = envelope - - if (envelope.type === 'handshake') { - const ack: MessageEnvelope = { - id: crypto.randomUUID(), - type: 'handshake_ack', - clientId: 'test-client-err', - protocolVersion: '1.0', - } - ws.send(serializeEnvelope(ack)) - return - } - - if (envelope.type === 'request') { - // Respond with error - const response: MessageEnvelope = { - id: envelope.id, - type: 'response', - channel: envelope.channel, - error: { code: 'HANDLER_ERROR', message: 'test error' }, - } - ws.send(serializeEnvelope(response)) - } - }, - open(ws) { - clients.add(ws) - }, - close(ws) { - clients.delete(ws) - }, - }, - }) - - const port = server.port! - return { - url: `ws://127.0.0.1:${port}`, - port, - close: () => server.stop(true), - lastMessage: () => lastMsg, - sendToAll: (envelope: MessageEnvelope) => { - const data = serializeEnvelope(envelope) - for (const ws of clients) ws.send(data) - }, - } -} - -// --------------------------------------------------------------------------- -// Tests -// --------------------------------------------------------------------------- - -let server: MockServer | null = null - -afterEach(() => { - server?.close() - server = null -}) - -describe('CliRpcClient', () => { - it('connects and completes handshake', async () => { - server = createMockServer() - const client = new CliRpcClient(server.url, { token: 'test-token' }) - const clientId = await client.connect() - expect(clientId).toBe('test-client-001') - expect(client.isConnected).toBe(true) - expect(client.clientId).toBe('test-client-001') - client.destroy() - }) - - it('sends token in handshake', async () => { - server = createMockServer() - const client = new CliRpcClient(server.url, { token: 'my-secret' }) - await client.connect() - const hs = server.lastMessage() - expect(hs?.type).toBe('handshake') - expect(hs?.token).toBe('my-secret') - client.destroy() - }) - - it('rejects on auth failure', async () => { - server = createMockServer({ rejectAuth: true }) - const client = new CliRpcClient(server.url, { token: 'bad-token' }) - await expect(client.connect()).rejects.toThrow('Invalid token') - client.destroy() - }) - - it('rejects on connect timeout', async () => { - server = createMockServer({ noAck: true }) - const client = new CliRpcClient(server.url, { connectTimeout: 200 }) - await expect(client.connect()).rejects.toThrow('Connection timeout') - client.destroy() - }) - - it('invoke sends request and receives response', async () => { - server = createMockServer() - const client = new CliRpcClient(server.url) - await client.connect() - const result = await client.invoke('system:homeDir') - // Mock server echoes args — no args means empty array - expect(result).toEqual([]) - client.destroy() - }) - - it('invoke passes args correctly', async () => { - server = createMockServer() - const client = new CliRpcClient(server.url) - await client.connect() - const result = await client.invoke('sessions:get', 'workspace-1') - expect(result).toEqual(['workspace-1']) - client.destroy() - }) - - it('invoke rejects on server error', async () => { - server = createErrorServer() - const client = new CliRpcClient(server.url) - await client.connect() - await expect(client.invoke('system:versions')).rejects.toThrow('test error') - client.destroy() - }) - - it('invoke rejects on timeout', async () => { - server = createMockServer({ noAck: false }) - // Create a server that acks handshake but never responds to requests - server.close() - - const silentServer = Bun.serve({ - port: 0, - fetch(req, svr) { - if (svr.upgrade(req)) return undefined - return new Response('Not found', { status: 404 }) - }, - websocket: { - message(ws, message) { - const raw = typeof message === 'string' ? message : new TextDecoder().decode(message) - const envelope = deserializeEnvelope(raw) - if (envelope.type === 'handshake') { - const ack: MessageEnvelope = { - id: crypto.randomUUID(), - type: 'handshake_ack', - clientId: 'silent-client', - protocolVersion: '1.0', - } - ws.send(serializeEnvelope(ack)) - } - // Never respond to requests - }, - }, - }) - - const client = new CliRpcClient(`ws://127.0.0.1:${silentServer.port}`, { requestTimeout: 200 }) - await client.connect() - await expect(client.invoke('system:homeDir')).rejects.toThrow('Request timeout') - client.destroy() - silentServer.stop(true) - }) - - it('invoke throws when not connected', async () => { - const client = new CliRpcClient('ws://127.0.0.1:1') - await expect(client.invoke('system:homeDir')).rejects.toThrow('Not connected') - client.destroy() - }) - - it('receives push events via on()', async () => { - server = createMockServer() - const client = new CliRpcClient(server.url) - await client.connect() - - const events: unknown[][] = [] - const unsub = client.on('session:event', (...args) => { - events.push(args) - }) - - // Push an event from server - server.sendToAll({ - id: crypto.randomUUID(), - type: 'event', - channel: 'session:event', - args: [{ type: 'text_delta', sessionId: 's1', delta: 'hello' }], - }) - - // Give it a tick - await new Promise((r) => setTimeout(r, 50)) - - expect(events.length).toBe(1) - expect((events[0][0] as any).delta).toBe('hello') - - // Unsubscribe stops delivery - unsub() - server.sendToAll({ - id: crypto.randomUUID(), - type: 'event', - channel: 'session:event', - args: [{ type: 'text_delta', sessionId: 's1', delta: 'world' }], - }) - - await new Promise((r) => setTimeout(r, 50)) - expect(events.length).toBe(1) // Still 1 - client.destroy() - }) - - it('destroy closes connection and rejects pending', async () => { - server = createMockServer({ noAck: false }) - // Use a server that acks but never responds - server.close() - - const silentServer = Bun.serve({ - port: 0, - fetch(req, svr) { - if (svr.upgrade(req)) return undefined - return new Response('Not found', { status: 404 }) - }, - websocket: { - message(ws, message) { - const raw = typeof message === 'string' ? message : new TextDecoder().decode(message) - const envelope = deserializeEnvelope(raw) - if (envelope.type === 'handshake') { - ws.send(serializeEnvelope({ - id: crypto.randomUUID(), - type: 'handshake_ack', - clientId: 'destroy-test', - protocolVersion: '1.0', - })) - } - }, - }, - }) - - const client = new CliRpcClient(`ws://127.0.0.1:${silentServer.port}`, { requestTimeout: 5000 }) - await client.connect() - - const pending = client.invoke('system:homeDir') - client.destroy() - - await expect(pending).rejects.toThrow('Client destroyed') - expect(client.isConnected).toBe(false) - silentServer.stop(true) - }) - - it('throws on invoke after destroy', async () => { - server = createMockServer() - const client = new CliRpcClient(server.url) - await client.connect() - client.destroy() - await expect(client.invoke('system:homeDir')).rejects.toThrow('Not connected') - }) - - it('connects over wss:// with TLS', async () => { - const tls = generateSelfSignedCert() - if (!tls) { - // openssl not available — skip TLS test - console.log(' (skipped: openssl not available)') - return - } - server = createMockServer({ tls }) - - const prev = process.env.NODE_TLS_REJECT_UNAUTHORIZED - process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' - try { - const client = new CliRpcClient(server.url) - const clientId = await client.connect() - expect(clientId).toBe('test-client-001') - expect(server.url.startsWith('wss://')).toBe(true) - client.destroy() - } finally { - if (prev === undefined) { - delete process.env.NODE_TLS_REJECT_UNAUTHORIZED - } else { - process.env.NODE_TLS_REJECT_UNAUTHORIZED = prev - } - } - }) -}) - -// --------------------------------------------------------------------------- -// TLS cert helper — generates a real self-signed cert via openssl -// --------------------------------------------------------------------------- - -function generateSelfSignedCert(): { cert: string; key: string } | null { - try { - const keyResult = Bun.spawnSync({ - cmd: ['openssl', 'req', '-x509', '-newkey', 'ec', '-pkeyopt', 'ec_paramgen_curve:prime256v1', - '-keyout', '/dev/stdout', '-out', '/dev/stdout', - '-days', '1', '-nodes', '-subj', '/CN=localhost', '-batch'], - stderr: 'pipe', - }) - if (keyResult.exitCode !== 0) return null - - const pem = keyResult.stdout.toString() - const certMatch = pem.match(/(-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----)/) - const keyMatch = pem.match(/(-----BEGIN (?:EC )?PRIVATE KEY-----[\s\S]+?-----END (?:EC )?PRIVATE KEY-----)/) - if (!certMatch || !keyMatch) return null - - return { cert: certMatch[1], key: keyMatch[1] } - } catch { - return null - } -} diff --git a/packages/desktop/apps/cli/src/client.ts b/packages/desktop/apps/cli/src/client.ts deleted file mode 100644 index 2a28267b2e2..00000000000 --- a/packages/desktop/apps/cli/src/client.ts +++ /dev/null @@ -1,239 +0,0 @@ -/** - * CliRpcClient — Minimal WebSocket RPC client for CLI usage. - * - * Stripped-down version of WsRpcClient: no auto-reconnect, no capabilities, - * no connection state listeners. Connect, work, exit. - */ - -import { - PROTOCOL_VERSION, - type MessageEnvelope, -} from '@craft-agent/shared/protocol' -import { - serializeEnvelope, - deserializeEnvelope, -} from '@craft-agent/server-core/transport' - -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - -interface PendingRequest { - resolve: (value: unknown) => void - reject: (error: Error) => void - timeout: ReturnType -} - -export interface CliClientOptions { - token?: string - workspaceId?: string - requestTimeout?: number - connectTimeout?: number -} - -// --------------------------------------------------------------------------- -// Client -// --------------------------------------------------------------------------- - -export class CliRpcClient { - private ws: WebSocket | null = null - private pending = new Map() - private listeners = new Map void>>() - private _clientId: string | null = null - private _connected = false - private _destroyed = false - - private readonly url: string - private readonly token: string | undefined - private readonly workspaceId: string | undefined - private readonly requestTimeout: number - private readonly connectTimeout: number - - constructor(url: string, opts?: CliClientOptions) { - this.url = url - this.token = opts?.token - this.workspaceId = opts?.workspaceId - this.requestTimeout = opts?.requestTimeout ?? 10_000 - this.connectTimeout = opts?.connectTimeout ?? 10_000 - } - - /** Connect to the server and complete the handshake. Returns the assigned clientId. */ - async connect(): Promise { - if (this._destroyed) throw new Error('Client destroyed') - - return new Promise((resolve, reject) => { - const timer = setTimeout(() => { - reject(new Error(`Connection timeout (${this.connectTimeout}ms)`)) - this.ws?.close() - }, this.connectTimeout) - - this.ws = new WebSocket(this.url) - - this.ws.onopen = () => { - const handshake: MessageEnvelope = { - id: crypto.randomUUID(), - type: 'handshake', - protocolVersion: PROTOCOL_VERSION, - workspaceId: this.workspaceId, - token: this.token, - } - this.ws!.send(serializeEnvelope(handshake)) - } - - this.ws.onmessage = (event) => { - const raw = typeof event.data === 'string' ? event.data : String(event.data) - let envelope: MessageEnvelope - try { - envelope = deserializeEnvelope(raw) - } catch { - return - } - - if (envelope.type === 'handshake_ack') { - clearTimeout(timer) - this._clientId = envelope.clientId ?? null - this._connected = true - // Switch to normal message handler - this.ws!.onmessage = (e) => { - this.onMessage(typeof e.data === 'string' ? e.data : String(e.data)) - } - resolve(this._clientId!) - } else if (envelope.type === 'error') { - clearTimeout(timer) - const err = new Error(envelope.error?.message ?? 'Connection rejected') - ;(err as any).code = envelope.error?.code - reject(err) - } - } - - this.ws.onerror = () => { - if (!this._connected) { - clearTimeout(timer) - reject(new Error('WebSocket connection error')) - } - } - - this.ws.onclose = () => { - if (!this._connected) { - clearTimeout(timer) - reject(new Error('WebSocket closed before handshake')) - } - this._connected = false - for (const [, req] of this.pending) { - clearTimeout(req.timeout) - req.reject(new Error('Disconnected')) - } - this.pending.clear() - } - }) - } - - /** Send an RPC request and await the response. */ - async invoke(channel: string, ...args: unknown[]): Promise { - if (!this._connected || !this.ws) { - throw new Error(`Not connected (channel: ${channel})`) - } - - return new Promise((resolve, reject) => { - const id = crypto.randomUUID() - const timeout = setTimeout(() => { - this.pending.delete(id) - reject(new Error(`Request timeout: ${channel} (${this.requestTimeout}ms)`)) - }, this.requestTimeout) - - this.pending.set(id, { resolve, reject, timeout }) - - const envelope: MessageEnvelope = { - id, - type: 'request', - channel, - args, - } - this.ws!.send(serializeEnvelope(envelope)) - }) - } - - /** Subscribe to push events on a channel. Returns an unsubscribe function. */ - on(channel: string, callback: (...args: unknown[]) => void): () => void { - let set = this.listeners.get(channel) - if (!set) { - set = new Set() - this.listeners.set(channel, set) - } - set.add(callback) - - return () => { - set!.delete(callback) - if (set!.size === 0) this.listeners.delete(channel) - } - } - - /** Close the connection and reject all pending requests. */ - destroy(): void { - this._destroyed = true - for (const [, req] of this.pending) { - clearTimeout(req.timeout) - req.reject(new Error('Client destroyed')) - } - this.pending.clear() - this.ws?.close() - this.ws = null - this._connected = false - } - - get isConnected(): boolean { - return this._connected - } - - get clientId(): string | null { - return this._clientId - } - - // ------------------------------------------------------------------------- - // Internal message routing - // ------------------------------------------------------------------------- - - private onMessage(raw: string): void { - let envelope: MessageEnvelope - try { - envelope = deserializeEnvelope(raw) - } catch { - return - } - - switch (envelope.type) { - case 'response': { - const req = this.pending.get(envelope.id) - if (req) { - this.pending.delete(envelope.id) - clearTimeout(req.timeout) - if (envelope.error) { - const err = new Error(envelope.error.message) - ;(err as any).code = envelope.error.code - ;(err as any).data = envelope.error.data - req.reject(err) - } else { - req.resolve(envelope.result) - } - } - break - } - - case 'event': { - if (envelope.channel) { - const set = this.listeners.get(envelope.channel) - if (set) { - for (const cb of set) { - try { - cb(...(envelope.args ?? [])) - } catch { - // Listener errors shouldn't break the client - } - } - } - } - break - } - } - } -} diff --git a/packages/desktop/apps/cli/src/commands.test.ts b/packages/desktop/apps/cli/src/commands.test.ts deleted file mode 100644 index 86034c02238..00000000000 --- a/packages/desktop/apps/cli/src/commands.test.ts +++ /dev/null @@ -1,391 +0,0 @@ -import { describe, it, expect } from 'bun:test' -import { parseArgs, resolveApiKey, shouldSetupLlmConnection } from './index.ts' - -// --------------------------------------------------------------------------- -// Arg parsing tests -// --------------------------------------------------------------------------- - -describe('parseArgs', () => { - it('parses --url, --token, --workspace', () => { - const args = parseArgs([ - 'bun', 'index.ts', - '--url', 'ws://localhost:3000', - '--token', 'secret123', - '--workspace', 'ws-1', - 'ping', - ]) - expect(args.url).toBe('ws://localhost:3000') - expect(args.token).toBe('secret123') - expect(args.workspace).toBe('ws-1') - expect(args.command).toBe('ping') - }) - - it('parses --timeout and --json', () => { - const args = parseArgs([ - 'bun', 'index.ts', - '--timeout', '5000', - '--json', - 'workspaces', - ]) - expect(args.timeout).toBe(5000) - expect(args.json).toBe(true) - expect(args.command).toBe('workspaces') - }) - - it('parses --tls-ca', () => { - const args = parseArgs([ - 'bun', 'index.ts', - '--tls-ca', '/path/to/ca.pem', - 'ping', - ]) - expect(args.tlsCa).toBe('/path/to/ca.pem') - }) - - it('parses --send-timeout', () => { - const args = parseArgs([ - 'bun', 'index.ts', - '--send-timeout', '60000', - 'send', 'session-1', 'hello', - ]) - expect(args.sendTimeout).toBe(60000) - expect(args.command).toBe('send') - expect(args.rest).toEqual(['session-1', 'hello']) - }) - - it('falls back to env vars for url and token', () => { - const prevUrl = process.env.CRAFT_SERVER_URL - const prevToken = process.env.CRAFT_SERVER_TOKEN - const prevCa = process.env.CRAFT_TLS_CA - - process.env.CRAFT_SERVER_URL = 'ws://env-server:8080' - process.env.CRAFT_SERVER_TOKEN = 'env-token' - process.env.CRAFT_TLS_CA = '/env/ca.pem' - - try { - const args = parseArgs(['bun', 'index.ts', 'ping']) - expect(args.url).toBe('ws://env-server:8080') - expect(args.token).toBe('env-token') - expect(args.tlsCa).toBe('/env/ca.pem') - } finally { - if (prevUrl === undefined) delete process.env.CRAFT_SERVER_URL - else process.env.CRAFT_SERVER_URL = prevUrl - if (prevToken === undefined) delete process.env.CRAFT_SERVER_TOKEN - else process.env.CRAFT_SERVER_TOKEN = prevToken - if (prevCa === undefined) delete process.env.CRAFT_TLS_CA - else process.env.CRAFT_TLS_CA = prevCa - } - }) - - it('explicit flags override env vars', () => { - const prevUrl = process.env.CRAFT_SERVER_URL - process.env.CRAFT_SERVER_URL = 'ws://env-server:8080' - - try { - const args = parseArgs(['bun', 'index.ts', '--url', 'ws://flag-server:9090', 'ping']) - expect(args.url).toBe('ws://flag-server:9090') - } finally { - if (prevUrl === undefined) delete process.env.CRAFT_SERVER_URL - else process.env.CRAFT_SERVER_URL = prevUrl - } - }) - - it('parses --help as command', () => { - const args = parseArgs(['bun', 'index.ts', '--help']) - expect(args.command).toBe('help') - }) - - it('parses --version as command', () => { - const args = parseArgs(['bun', 'index.ts', '--version']) - expect(args.command).toBe('version') - }) - - it('parses --validate-server as command', () => { - const args = parseArgs(['bun', 'index.ts', '--validate-server']) - expect(args.command).toBe('validate') - }) - - it('parses session subcommand with args', () => { - const args = parseArgs([ - 'bun', 'index.ts', - 'session', 'create', '--name', 'test', '--mode', 'safe', - ]) - expect(args.command).toBe('session') - // --mode is now a global flag, consumed at top level - expect(args.rest).toEqual(['create', '--name', 'test']) - expect(args.mode).toBe('safe') - }) - - it('parses send with message text', () => { - const args = parseArgs([ - 'bun', 'index.ts', - 'send', 'sess-123', 'What', 'files', 'are', 'here?', - ]) - expect(args.command).toBe('send') - expect(args.rest).toEqual(['sess-123', 'What', 'files', 'are', 'here?']) - }) - - it('parses invoke with channel and JSON args', () => { - const args = parseArgs([ - 'bun', 'index.ts', - 'invoke', 'sessions:get', '["workspace-1"]', - ]) - expect(args.command).toBe('invoke') - expect(args.rest).toEqual(['sessions:get', '["workspace-1"]']) - }) - - it('defaults to empty command (shows help)', () => { - const args = parseArgs(['bun', 'index.ts']) - expect(args.command).toBe('') - }) - - it('defaults timeout to 10000', () => { - const args = parseArgs(['bun', 'index.ts', 'ping']) - expect(args.timeout).toBe(10000) - }) - - it('defaults sendTimeout to 300000', () => { - const args = parseArgs(['bun', 'index.ts', 'send', 's1', 'hi']) - expect(args.sendTimeout).toBe(300000) - }) - - it('defaults json to false', () => { - const args = parseArgs(['bun', 'index.ts', 'ping']) - expect(args.json).toBe(false) - }) - - // --- run-specific flags --- - - it('parses run command with positional args', () => { - const args = parseArgs(['bun', 'index.ts', 'run', 'hello', 'world']) - expect(args.command).toBe('run') - expect(args.rest).toEqual(['hello', 'world']) - }) - - it('--source accumulates into array', () => { - const args = parseArgs([ - 'bun', 'index.ts', - '--source', 'craft-kb', - '--source', 'github', - 'run', 'do stuff', - ]) - expect(args.sources).toEqual(['craft-kb', 'github']) - }) - - it('defaults sources to empty array', () => { - const args = parseArgs(['bun', 'index.ts', 'run', 'hello']) - expect(args.sources).toEqual([]) - }) - - it('--mode sets mode', () => { - const args = parseArgs(['bun', 'index.ts', '--mode', 'safe', 'run', 'hello']) - expect(args.mode).toBe('safe') - }) - - it('defaults mode to empty (run defaults to allow-all)', () => { - const args = parseArgs(['bun', 'index.ts', 'run', 'hello']) - expect(args.mode).toBe('') - }) - - it('--output-format sets outputFormat', () => { - const args = parseArgs(['bun', 'index.ts', '--output-format', 'stream-json', 'run', 'hello']) - expect(args.outputFormat).toBe('stream-json') - }) - - it('defaults outputFormat to text', () => { - const args = parseArgs(['bun', 'index.ts', 'run', 'hello']) - expect(args.outputFormat).toBe('text') - }) - - it('--no-cleanup sets noCleanup', () => { - const args = parseArgs(['bun', 'index.ts', '--no-cleanup', 'run', 'hello']) - expect(args.noCleanup).toBe(true) - }) - - it('defaults noCleanup to false', () => { - const args = parseArgs(['bun', 'index.ts', 'run', 'hello']) - expect(args.noCleanup).toBe(false) - }) - - it('--server-entry sets serverEntry', () => { - const args = parseArgs(['bun', 'index.ts', '--server-entry', '/path/to/server.ts', 'run', 'hello']) - expect(args.serverEntry).toBe('/path/to/server.ts') - }) - - it('defaults serverEntry to undefined', () => { - const args = parseArgs(['bun', 'index.ts', 'run', 'hello']) - expect(args.serverEntry).toBeUndefined() - }) - - it('--workspace-dir sets workspaceDir', () => { - const args = parseArgs(['bun', 'index.ts', '--workspace-dir', '/tmp/ws', 'run', 'hello']) - expect(args.workspaceDir).toBe('/tmp/ws') - }) - - it('defaults workspaceDir to undefined', () => { - const args = parseArgs(['bun', 'index.ts', 'run', 'hello']) - expect(args.workspaceDir).toBeUndefined() - }) - - it('parses --provider deepseek for run', () => { - const args = parseArgs(['bun', 'index.ts', '--provider', 'deepseek', 'run', 'hello']) - expect(args.provider).toBe('deepseek') - }) -}) - -// --------------------------------------------------------------------------- -// Provider credential resolution tests -// --------------------------------------------------------------------------- - -describe('resolveApiKey', () => { - it('keeps explicit key passthrough for script compatibility', () => { - expect(resolveApiKey('qwen', 'unused-key')).toBe('unused-key') - }) -}) - -describe('shouldSetupLlmConnection', () => { - it('sets up Qwen when no connection exists', () => { - expect(shouldSetupLlmConnection(0, { provider: 'qwen', baseUrl: '' })).toBe(true) - }) - - it('skips setup when a connection already exists', () => { - expect(shouldSetupLlmConnection(2, { provider: 'qwen', baseUrl: '' })).toBe(false) - }) -}) - -// --------------------------------------------------------------------------- -// Validate steps structure tests -// --------------------------------------------------------------------------- - -import { getValidateSteps } from './index.ts' - -describe('getValidateSteps', () => { - it('returns a non-empty array of steps', () => { - const steps = getValidateSteps() - expect(steps.length).toBeGreaterThan(0) - // Sanity: at least the known lifecycle groups exist - expect(steps.length).toBeGreaterThanOrEqual(20) - }) - - it('first step is handshake', () => { - const steps = getValidateSteps() - expect(steps[0].name).toBe('Connect + handshake') - }) - - it('last step is disconnect', () => { - const steps = getValidateSteps() - expect(steps[steps.length - 1].name).toBe('Disconnect') - }) - - it('has no duplicate step names', () => { - const names = getValidateSteps().map((s) => s.name) - expect(new Set(names).size).toBe(names.length) - }) - - it('includes session lifecycle steps (create, read, delete)', () => { - const steps = getValidateSteps() - const names = steps.map((s) => s.name) - expect(names).toContain('sessions:create') - expect(names).toContain('sessions:getMessages') - expect(names).toContain('sessions:delete') - }) - - it('includes send message + stream step', () => { - const steps = getValidateSteps() - const names = steps.map((s) => s.name) - expect(names).toContain('send message + stream') - }) - - it('includes send message + tool use step', () => { - const steps = getValidateSteps() - const names = steps.map((s) => s.name) - expect(names).toContain('send message + tool use') - }) - - it('includes source lifecycle steps (create, mention, delete)', () => { - const steps = getValidateSteps() - const names = steps.map((s) => s.name) - expect(names).toContain('sources:create') - expect(names).toContain('send + source mention') - expect(names).toContain('sources:delete') - }) - - it('includes skill lifecycle steps (create, mention, delete)', () => { - const steps = getValidateSteps() - const names = steps.map((s) => s.name) - expect(names).toContain('send + skill create') - expect(names).toContain('send + skill mention') - expect(names).toContain('skills:delete') - }) - - it('includes automation lifecycle steps', () => { - const names = getValidateSteps().map((s) => s.name) - expect(names).toContain('automation:create') - expect(names).toContain('automation:trigger (status change)') - expect(names).toContain('automation:verify session') - expect(names).toContain('automation:verify labels') - expect(names).toContain('automations:getLastExecuted') - expect(names).toContain('automation:cleanup') - }) - - it('includes session tool validation steps', () => { - const names = getValidateSteps().map((s) => s.name) - expect(names).toContain('session-tools:set_session_labels') - expect(names).toContain('session-tools:get_session_info') - expect(names).toContain('session-tools:list_sessions') - }) - - it('session tool steps come after tool use and before branching', () => { - const names = getValidateSteps().map((s) => s.name) - const toolUse = names.indexOf('send message + tool use') - const labels = names.indexOf('session-tools:set_session_labels') - const branch = names.indexOf('sessions:branch') - expect(labels).toBeGreaterThan(toolUse) - expect(branch).toBeGreaterThan(labels) - }) - - it('includes session branching steps', () => { - const names = getValidateSteps().map((s) => s.name) - expect(names).toContain('sessions:branch') - expect(names).toContain('sessions:branch verify') - expect(names).toContain('sessions:branch send') - }) - - it('includes webhook validation steps', () => { - const names = getValidateSteps().map((s) => s.name) - expect(names).toContain('webhook:test (RPC)') - expect(names).toContain('webhook:verify failure') - }) - - it('creates session with allow-all permission mode', () => { - const steps = getValidateSteps() - const createStep = steps.find((s) => s.name === 'sessions:create') - expect(createStep).toBeDefined() - }) - - it('cleanup steps come after send steps', () => { - const steps = getValidateSteps() - const names = steps.map((s) => s.name) - const skillDelete = names.indexOf('skills:delete') - const sourceDelete = names.indexOf('sources:delete') - const sessionDelete = names.indexOf('sessions:delete') - const skillMention = names.indexOf('send + skill mention') - expect(skillDelete).toBeGreaterThan(skillMention) - expect(sourceDelete).toBeGreaterThan(skillDelete) - expect(sessionDelete).toBeGreaterThan(sourceDelete) - }) - - it('branching steps come after send message + tool use', () => { - const names = getValidateSteps().map((s) => s.name) - const toolUse = names.indexOf('send message + tool use') - const branch = names.indexOf('sessions:branch') - expect(branch).toBeGreaterThan(toolUse) - }) - - it('automation cleanup comes before sources:delete', () => { - const names = getValidateSteps().map((s) => s.name) - const cleanup = names.indexOf('automation:cleanup') - const srcDelete = names.indexOf('sources:delete') - expect(cleanup).toBeGreaterThan(-1) - expect(cleanup).toBeLessThan(srcDelete) - }) -}) diff --git a/packages/desktop/apps/cli/src/index.ts b/packages/desktop/apps/cli/src/index.ts deleted file mode 100755 index bd8fdcd5753..00000000000 --- a/packages/desktop/apps/cli/src/index.ts +++ /dev/null @@ -1,1922 +0,0 @@ -#!/usr/bin/env bun -/** - * craft-cli — Terminal client for Qwen Code server. - * - * Connects over WebSocket (ws:// or wss://) to a running Qwen Code server - * and provides commands for listing resources, managing sessions, sending - * messages with real-time streaming, and validating server health. - */ - -import { resolve } from 'path' -import { CliRpcClient } from './client.ts' - -// --------------------------------------------------------------------------- -// Arg parsing -// --------------------------------------------------------------------------- - -export interface CliArgs { - url: string - token: string - workspace?: string - timeout: number - json: boolean - tlsCa?: string - sendTimeout: number - command: string - rest: string[] - // run-specific flags - sources: string[] - mode: string - outputFormat: string - noCleanup: boolean - noSpinner: boolean - verbose: boolean - serverEntry?: string - workspaceDir?: string - // LLM configuration - provider: string - model: string - apiKey: string - baseUrl: string -} - -export function parseArgs(argv: string[]): CliArgs { - const args = argv.slice(2) // skip bun + script path - let url = '' - let token = '' - let workspace: string | undefined - let timeout = 10_000 - let json = false - let tlsCa: string | undefined - let sendTimeout = 300_000 // 5 min - const rest: string[] = [] - let command = '' - const sources: string[] = [] - let mode = '' - let outputFormat = 'text' - let noCleanup = false - let noSpinner = false - let verbose = false - let serverEntry: string | undefined - let workspaceDir: string | undefined - let provider = '' - let model = '' - let apiKey = '' - let baseUrl = '' - - for (let i = 0; i < args.length; i++) { - const arg = args[i] - switch (arg) { - case '--url': - url = args[++i] ?? '' - break - case '--token': - token = args[++i] ?? '' - break - case '--workspace': - workspace = args[++i] - break - case '--timeout': - timeout = parseInt(args[++i] ?? '10000', 10) - break - case '--json': - json = true - break - case '--tls-ca': - tlsCa = args[++i] - break - case '--send-timeout': - sendTimeout = parseInt(args[++i] ?? '300000', 10) - break - case '--source': - sources.push(args[++i] ?? '') - break - case '--mode': - mode = args[++i] ?? '' - break - case '--output-format': - outputFormat = args[++i] ?? 'text' - break - case '--no-cleanup': - noCleanup = true - break - case '--disable-spinner': - case '--no-spinner': - noSpinner = true - break - case '--verbose': - case '-v': - verbose = true - break - case '--server-entry': - serverEntry = args[++i] - break - case '--workspace-dir': - workspaceDir = args[++i] - break - case '--provider': - provider = args[++i] ?? '' - break - case '--model': - model = args[++i] ?? '' - break - case '--api-key': - apiKey = args[++i] ?? '' - break - case '--base-url': - baseUrl = args[++i] ?? '' - break - case '--help': - case '-h': - command = 'help' - break - case '--version': - command = 'version' - break - case '--validate-server': - command = 'validate' - break - default: - if (!command && !arg.startsWith('-')) { - command = arg - } else { - rest.push(arg) - } - } - } - - // Env var fallbacks - if (!url) url = process.env.CRAFT_SERVER_URL ?? '' - if (!token) token = process.env.CRAFT_SERVER_TOKEN ?? '' - if (!tlsCa) tlsCa = process.env.CRAFT_TLS_CA - if (!provider) provider = process.env.LLM_PROVIDER ?? 'qwen' - if (!model) model = process.env.LLM_MODEL ?? '' - if (!apiKey) apiKey = process.env.LLM_API_KEY ?? '' - if (!baseUrl) baseUrl = process.env.LLM_BASE_URL ?? '' - - return { url, token, workspace, timeout, json, tlsCa, sendTimeout, command, rest, sources, mode, outputFormat, noCleanup, noSpinner, verbose, serverEntry, workspaceDir, provider, model, apiKey, baseUrl } -} - -// --------------------------------------------------------------------------- -// Auto workspace resolution -// --------------------------------------------------------------------------- - -async function resolveWorkspace( - client: CliRpcClient, - explicit?: string, -): Promise { - if (explicit) { - // Bind client to the workspace so push events reach us - await client.invoke('window:switchWorkspace', explicit).catch(() => {}) - return explicit - } - try { - const workspaces = (await client.invoke('workspaces:get')) as any[] - if (workspaces?.length > 0) { - const id = workspaces[0].id - await client.invoke('window:switchWorkspace', id).catch(() => {}) - return id - } - } catch { - // Fall through — workspace may not be needed - } - return undefined -} - -// --------------------------------------------------------------------------- -// Output helpers -// --------------------------------------------------------------------------- - -function out(data: unknown, jsonMode: boolean): void { - if (jsonMode) { - process.stdout.write(JSON.stringify(data, null, 2) + '\n') - } else if (typeof data === 'string') { - process.stdout.write(data + '\n') - } else { - process.stdout.write(JSON.stringify(data, null, 2) + '\n') - } -} - -function err(msg: string): void { - process.stderr.write(`Error: ${msg}\n`) -} - -// --------------------------------------------------------------------------- -// ANSI colors (disabled when NO_COLOR is set or stdout is not a TTY) -// --------------------------------------------------------------------------- - -const _useColor = !process.env.NO_COLOR && process.stdout.isTTY !== false -const c = { - dim: (s: string) => _useColor ? `\x1b[2m${s}\x1b[22m` : s, - green: (s: string) => _useColor ? `\x1b[32m${s}\x1b[39m` : s, - red: (s: string) => _useColor ? `\x1b[31m${s}\x1b[39m` : s, - cyan: (s: string) => _useColor ? `\x1b[36m${s}\x1b[39m` : s, - bold: (s: string) => _useColor ? `\x1b[1m${s}\x1b[22m` : s, - yellow: (s: string) => _useColor ? `\x1b[33m${s}\x1b[39m` : s, - blue: (s: string) => _useColor ? `\x1b[34m${s}\x1b[39m` : s, -} - -// --------------------------------------------------------------------------- -// Spinner (TTY only — skipped when piped or NO_COLOR) -// --------------------------------------------------------------------------- - -const _spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] - -function createSpinner(text: string): { stop(): void } { - let i = 0 - let stopped = false - // Render first frame immediately — setInterval alone misses fast steps - process.stdout.write(`${text} ${c.dim(_spinnerFrames[i++ % _spinnerFrames.length])}`) - const timer = setInterval(() => { - process.stdout.write(`\r\x1b[2K${text} ${c.dim(_spinnerFrames[i++ % _spinnerFrames.length])}`) - }, 80) - return { - stop() { - if (stopped) return - stopped = true - clearInterval(timer) - process.stdout.write('\r\x1b[2K') - }, - } -} - -// --------------------------------------------------------------------------- -// Commands -// --------------------------------------------------------------------------- - -async function cmdPing(client: CliRpcClient, args: CliArgs): Promise { - const start = performance.now() - const clientId = await client.connect() - const latency = Math.round(performance.now() - start) - out( - args.json - ? { clientId, latencyMs: latency } - : `Connected: clientId=${clientId} latency=${latency}ms`, - args.json, - ) -} - -async function cmdHealth(client: CliRpcClient, args: CliArgs): Promise { - await client.connect() - const result = await client.invoke('credentials:healthCheck') - out(result, args.json) -} - -async function cmdVersions(client: CliRpcClient, args: CliArgs): Promise { - await client.connect() - const result = await client.invoke('system:versions') - out(result, args.json) -} - -async function cmdWorkspaces(client: CliRpcClient, args: CliArgs): Promise { - await client.connect() - const result = (await client.invoke('workspaces:get')) as any[] - if (args.json) { - out(result, true) - } else { - if (!result?.length) { - out('No workspaces found', false) - return - } - for (const ws of result) { - out(`${ws.id} ${ws.name ?? '(unnamed)'} ${ws.path ?? ''}`, false) - } - } -} - -async function cmdSessions(client: CliRpcClient, args: CliArgs): Promise { - await client.connect() - const workspaceId = await resolveWorkspace(client, args.workspace) - if (!workspaceId) { - err('No workspace available. Use --workspace ') - process.exit(1) - } - const result = (await client.invoke('sessions:get', workspaceId)) as any[] - if (args.json) { - out(result, true) - } else { - if (!result?.length) { - out('No sessions found', false) - return - } - for (const s of result) { - const name = s.name ?? '(unnamed)' - const preview = s.preview ? ` ${s.preview.slice(0, 60)}` : '' - const status = s.isProcessing ? ' [processing]' : '' - out(`${s.id} ${name}${preview}${status}`, false) - } - } -} - -async function cmdConnections(client: CliRpcClient, args: CliArgs): Promise { - await client.connect() - const result = await client.invoke('LLM_Connection:list') - out(result, args.json) -} - -async function cmdSources(client: CliRpcClient, args: CliArgs): Promise { - await client.connect() - const workspaceId = await resolveWorkspace(client, args.workspace) - if (!workspaceId) { - err('No workspace available. Use --workspace ') - process.exit(1) - } - const result = await client.invoke('sources:get', workspaceId) - out(result, args.json) -} - -async function cmdSessionCreate(client: CliRpcClient, args: CliArgs): Promise { - await client.connect() - const workspaceId = await resolveWorkspace(client, args.workspace) - if (!workspaceId) { - err('No workspace available. Use --workspace ') - process.exit(1) - } - - // Parse sub-args: --name - let name: string | undefined - for (let i = 0; i < args.rest.length; i++) { - if (args.rest[i] === '--name') name = args.rest[++i] - } - - const opts: Record = {} - if (name) opts.name = name - if (args.mode) opts.permissionMode = args.mode - - const result = await client.invoke('sessions:create', workspaceId, opts) - out(result, args.json) -} - -async function cmdSessionMessages(client: CliRpcClient, args: CliArgs): Promise { - const sessionId = args.rest[0] - if (!sessionId) { - err('Usage: session messages ') - process.exit(1) - } - await client.connect() - const result = await client.invoke('sessions:getMessages', sessionId) - out(result, args.json) -} - -async function cmdSessionDelete(client: CliRpcClient, args: CliArgs): Promise { - const sessionId = args.rest[0] - if (!sessionId) { - err('Usage: session delete ') - process.exit(1) - } - await client.connect() - await client.invoke('sessions:delete', sessionId) - out(args.json ? { deleted: sessionId } : `Deleted session: ${sessionId}`, args.json) -} - -/** - * Read prompt text from positional args + stdin. - * If there are positional words, they become the base message. - * Reads stdin when: --stdin flag is present, or no message and stdin is piped (not a TTY). - */ -async function readPrompt(words: string[], restArgs?: string[]): Promise { - let message = words.join(' ') - - const wantsStdin = restArgs?.includes('--stdin') - const isTTY = typeof process.stdin.isTTY === 'boolean' ? process.stdin.isTTY : false - if (wantsStdin || (!message && !isTTY)) { - const chunks: string[] = [] - const reader = Bun.stdin.stream().getReader() - const decoder = new TextDecoder() - while (true) { - const { done, value } = await reader.read() - if (done) break - chunks.push(decoder.decode(value, { stream: true })) - } - const stdinText = chunks.join('') - message = message ? `${message}\n${stdinText}` : stdinText - } - - return message -} - -/** - * Subscribe to session events, send the message, stream output, wait for completion. - * Returns the exit code (0 = success, 1 = error, 130 = interrupted). - */ -async function sendAndStream( - client: CliRpcClient, - sessionId: string, - message: string, - args: CliArgs, -): Promise { - let exitCode = 0 - let finished = false - const streamJson = args.outputFormat === 'stream-json' - - const unsub = client.on('session:event', (event: unknown) => { - const ev = event as { type: string; sessionId: string; [key: string]: unknown } - if (ev.sessionId !== sessionId) return - - if (streamJson) { - process.stdout.write(JSON.stringify(ev) + '\n') - } - - switch (ev.type) { - case 'text_delta': - if (!streamJson) process.stdout.write(ev.delta as string) - break - case 'tool_start': - if (!streamJson) process.stdout.write(`\n[tool: ${ev.toolName}${ev.toolIntent ? ` — ${ev.toolIntent}` : ''}]\n`) - break - case 'tool_result': { - if (!streamJson) { - const result = String(ev.result ?? '') - if (result.length > 200) { - process.stdout.write(`${result.slice(0, 200)}...\n`) - } else if (result) { - process.stdout.write(`${result}\n`) - } - } - break - } - case 'error': - if (!streamJson) err(String(ev.error)) - exitCode = 1 - finished = true - break - case 'complete': - if (!streamJson) process.stdout.write('\n') - finished = true - break - case 'interrupted': - if (!streamJson) process.stdout.write('\n[interrupted]\n') - exitCode = 130 - finished = true - break - } - }) - - await client.invoke('sessions:sendMessage', sessionId, message) - - const deadline = Date.now() + args.sendTimeout - while (!finished && Date.now() < deadline) { - await new Promise((r) => setTimeout(r, 100)) - } - - unsub() - - if (!finished) { - err('Send timeout — no completion event received') - exitCode = 1 - } - - return exitCode -} - -async function cmdSend(client: CliRpcClient, args: CliArgs): Promise { - const sessionId = args.rest[0] - if (!sessionId) { - err('Usage: send ') - process.exit(1) - } - - const message = await readPrompt(args.rest.slice(1), args.rest) - if (!message.trim()) { - err('No message provided') - process.exit(1) - } - - await client.connect() - const exitCode = await sendAndStream(client, sessionId, message, args) - client.destroy() - process.exit(exitCode) -} - -interface LocalServer { - client: CliRpcClient - stop: () => Promise -} - -async function spawnLocalServer(args: CliArgs, opts?: { quiet?: boolean }): Promise { - const { spawnServer } = await import('./server-spawner.ts') - process.stderr.write('Starting server...\n') - const server = await spawnServer({ - serverEntry: args.serverEntry, - startupTimeout: args.timeout > 30_000 ? args.timeout : 30_000, - quiet: opts?.quiet, - }) - process.stderr.write(`Server ready: ${server.url}\n`) - const client = new CliRpcClient(server.url, { - token: server.token, - requestTimeout: args.timeout, - }) - return { client, stop: server.stop } -} - -// --------------------------------------------------------------------------- -// LLM connection helpers -// --------------------------------------------------------------------------- - -function getProviderDisplayName(provider: string): string { - return provider === 'qwen' ? 'Qwen Code' : provider.charAt(0).toUpperCase() + provider.slice(1) -} - -export function resolveApiKey(_provider: string, explicit: string): string { - return explicit -} - -export function shouldSetupLlmConnection(existingConnectionCount: number, _args: Pick): boolean { - return existingConnectionCount === 0 -} - -async function setupLlmConnection( - client: CliRpcClient, - _args: CliArgs, -): Promise<{ connectionSlug: string }> { - const provider = 'qwen' - const connectionSlug = 'qwen-code' - - await client.invoke('LLM_Connection:save', { - slug: connectionSlug, - name: getProviderDisplayName(provider), - providerType: 'qwen', - authType: 'none', - createdAt: Date.now(), - }) - const setupResult = await client.invoke('settings:setupLlmConnection', { slug: connectionSlug }) as { success: boolean; error?: string } - if (!setupResult?.success) { - throw new Error(`LLM connection setup failed: ${setupResult?.error ?? 'unknown error'}`) - } - await client.invoke('LLM_Connection:setDefault', connectionSlug) - process.stderr.write('LLM connection configured: Qwen Code\n') - - return { connectionSlug } -} - -async function cmdRun(args: CliArgs): Promise { - // Prompt = all positional args (no session ID needed, unlike send) - const message = await readPrompt(args.rest, args.rest) - if (!message.trim()) { - err('No prompt provided. Usage: run ') - process.exit(1) - } - - const server = await spawnLocalServer(args) - - let client: CliRpcClient | undefined = server.client - let sessionId: string | undefined - - const cleanup = async () => { - if (sessionId && client?.isConnected && !args.noCleanup) { - await client.invoke('sessions:delete', sessionId).catch(() => {}) - } - client?.destroy() - await server.stop() - } - - // Signal handling — cancel + clean up on SIGINT/SIGTERM - const onSignal = async () => { - if (sessionId && client?.isConnected) { - await client.invoke('sessions:cancel', sessionId).catch(() => {}) - } - await cleanup() - process.exit(130) - } - process.on('SIGINT', onSignal) - process.on('SIGTERM', onSignal) - - try { - await client.connect() - - // Bootstrap workspace from directory if specified - let bootstrappedWorkspaceId: string | undefined - if (args.workspaceDir) { - const absPath = resolve(args.workspaceDir) - const ws = (await client.invoke('workspaces:create', absPath, 'ci-workspace')) as { id: string } - bootstrappedWorkspaceId = ws.id - process.stderr.write(`Workspace registered: ${absPath}\n`) - } - - // Auto-setup the Qwen Code connection when no connection exists yet. - const connections = (await client.invoke('LLM_Connection:list')) as any[] - let connectionSlug: string | undefined - if (shouldSetupLlmConnection(connections?.length ?? 0, args)) { - const result = await setupLlmConnection(client, args) - connectionSlug = result.connectionSlug - } - - const workspaceId = bootstrappedWorkspaceId - ?? await resolveWorkspace(client, args.workspace) - if (bootstrappedWorkspaceId) { - await client.invoke('window:switchWorkspace', bootstrappedWorkspaceId).catch(() => {}) - } - if (!workspaceId) { - err('No workspace found on server') - process.exit(1) - } - - const session = (await client.invoke('sessions:create', workspaceId, { - permissionMode: args.mode || 'allow-all', - enabledSourceSlugs: args.sources.length > 0 ? args.sources : undefined, - slugHint: message, - })) as { id: string } - sessionId = session.id - - if (args.model) { - await client.invoke('session:setModel', sessionId, workspaceId, args.model, connectionSlug) - } - - const exitCode = await sendAndStream(client, sessionId, message, args) - await cleanup() - process.exit(exitCode) - } catch (e) { - const msg = e instanceof Error ? e.message : String(e) - err(msg) - await cleanup() - process.exit(1) - } finally { - process.off('SIGINT', onSignal) - process.off('SIGTERM', onSignal) - } -} - -async function cmdValidate(args: CliArgs): Promise { - let server: LocalServer | undefined - let client: CliRpcClient - - // Use a generous timeout for validation steps — source creation and MCP - // server startup can be slow on Windows. - const validateArgs = { ...args, timeout: Math.max(args.timeout, 30_000) } - - if (args.url) { - client = new CliRpcClient(args.url, { - token: args.token || undefined, - requestTimeout: validateArgs.timeout, - connectTimeout: validateArgs.timeout, - }) - } else { - server = await spawnLocalServer(validateArgs, { quiet: !args.verbose }) - client = server.client - } - - try { - const exitCode = await runValidation(client, args.json, args.noSpinner, args.workspaceDir, { - baseUrl: args.baseUrl, - apiKey: args.apiKey, - provider: args.provider, - }) - client.destroy() - if (server) await server.stop() - process.exit(exitCode) - } catch (e) { - const msg = e instanceof Error ? e.message : String(e) - err(msg) - client.destroy() - if (server) await server.stop() - process.exit(1) - } -} - -async function cmdCancel(client: CliRpcClient, args: CliArgs): Promise { - const sessionId = args.rest[0] - if (!sessionId) { - err('Usage: cancel ') - process.exit(1) - } - await client.connect() - await client.invoke('sessions:cancel', sessionId) - out(args.json ? { cancelled: sessionId } : `Cancelled: ${sessionId}`, args.json) -} - -async function cmdInvoke(client: CliRpcClient, args: CliArgs): Promise { - const channel = args.rest[0] - if (!channel) { - err('Usage: invoke [json-args...]') - process.exit(1) - } - await client.connect() - - // Parse remaining args as JSON - const invokeArgs: unknown[] = [] - for (let i = 1; i < args.rest.length; i++) { - try { - invokeArgs.push(JSON.parse(args.rest[i])) - } catch { - invokeArgs.push(args.rest[i]) - } - } - - const result = await client.invoke(channel, ...invokeArgs) - out(result, args.json) -} - -async function cmdListen(client: CliRpcClient, args: CliArgs): Promise { - const channel = args.rest[0] - if (!channel) { - err('Usage: listen ') - process.exit(1) - } - await client.connect() - - client.on(channel, (...eventArgs: unknown[]) => { - out({ channel, args: eventArgs, timestamp: new Date().toISOString() }, true) - }) - - process.stdout.write(`Listening on ${channel} (Ctrl+C to stop)\n`) - - // Keep alive - await new Promise(() => { - // Never resolves — Ctrl+C exits - }) -} - -// --------------------------------------------------------------------------- -// Validate server -// --------------------------------------------------------------------------- - -export interface ValidateStep { - name: string - fn: (client: CliRpcClient, ctx: ValidateContext) => Promise -} - -export interface ValidateContext { - /** Pre-existing workspace directory (from --workspace-dir) */ - workspaceDir?: string - /** Custom endpoint URL (from --base-url) */ - baseUrl?: string - /** API key override (from --api-key) */ - apiKey?: string - /** Provider hint (from --provider, default 'qwen') */ - provider?: string - workspaceId?: string - workspaceRootPath?: string - createdWorkspace?: boolean - createdSessionId?: string - createdSourceSlug?: string - createdSkillSlug?: string - createdAutomation?: boolean - automationTestSessionId?: string - /** Session created by automation that should be blocked by failing condition (if bug occurs) */ - automationBlockedSessionId?: string - automationName?: string - automationBlockedName?: string - createdLabelId?: string - /** Backup of existing automations.json before overwrite (undefined = didn't exist) */ - automationsJsonBackup?: string | null - /** Backup of existing automations-history.jsonl before overwrite (undefined = didn't exist) */ - automationsHistoryBackup?: string | null - branchedSessionId?: string - /** Label ID for e2e-test label created for session tool validation */ - e2eTestLabelId?: string - onEvent?: (ev: { type: string; [key: string]: unknown }) => void -} - -/** Minimal shapes for RPC responses used in validation steps. */ -interface ValidateStatus { - id?: string - label?: string -} - -interface ValidateSession { - id: string - name?: string - labels?: string[] -} - -interface ValidateLabel { - id?: string - name?: string -} - -interface ValidateMessageBlock { - type: string - text?: string -} - -interface ValidateMessage { - role: string - content: string | ValidateMessageBlock[] -} - -interface ValidateMessagesResponse { - messages?: ValidateMessage[] - conversation?: ValidateMessage[] -} - -/** - * Send a message and wait for streaming events. - * Returns a summary of received event types. - * If expectTool is true, validates that tool_start + tool_result events arrived. - */ -async function waitForSendEvents( - client: CliRpcClient, - sessionId: string, - message: string, - timeoutMs: number, - expectTool: boolean, - sendOptions?: Record, - onEvent?: (ev: { type: string; [key: string]: unknown }) => void, - expectToolName?: string, -): Promise { - const seen = new Set() - let textChunks = 0 - let toolName = '' - let finished = false - - const unsub = client.on('session:event', (event: unknown) => { - const ev = event as { type: string; sessionId: string; [key: string]: unknown } - if (ev.sessionId !== sessionId) return - - seen.add(ev.type) - if (ev.type === 'text_delta') textChunks++ - if (ev.type === 'tool_start') toolName = String(ev.toolName ?? '') - if (ev.type === 'complete' || ev.type === 'error' || ev.type === 'interrupted') { - finished = true - } - onEvent?.(ev) - }) - - try { - await client.invoke('sessions:sendMessage', sessionId, message, - undefined, undefined, sendOptions) - - const deadline = Date.now() + timeoutMs - while (!finished && Date.now() < deadline) { - await new Promise((r) => setTimeout(r, 100)) - } - - if (!finished) throw new Error('Timed out waiting for completion') - - // Only treat as failure if error was the terminal event (no complete followed) - if (seen.has('error') && !seen.has('complete')) throw new Error('Session returned an error event') - - if (expectTool) { - if (!seen.has('tool_start')) throw new Error('No tool_start event received') - if (!seen.has('tool_result')) throw new Error('No tool_result event received') - if (expectToolName && !toolName.includes(expectToolName)) { - throw new Error(`Expected tool containing "${expectToolName}", got "${toolName}"`) - } - return `tool=${toolName}, ${textChunks} text deltas, events: ${[...seen].join(', ')}` - } - - if (!seen.has('text_delta')) throw new Error('No text_delta events received') - return `${textChunks} text deltas, events: ${[...seen].join(', ')}` - } finally { - unsub() - } -} - -/** - * Clean up automation test artifacts (config files, session, label). - * Shared between the automation:cleanup test step and runValidation error recovery. - */ -async function cleanupAutomationArtifacts( - client: CliRpcClient, - ctx: ValidateContext, -): Promise { - const cleaned: string[] = [] - - // Restore or remove automation config files - if (ctx.workspaceRootPath && ctx.createdAutomation) { - try { - const { writeFile, unlink } = await import('fs/promises') - const configPath = `${ctx.workspaceRootPath}/automations.json` - const historyPath = `${ctx.workspaceRootPath}/automations-history.jsonl` - if (ctx.automationsJsonBackup != null) { - await writeFile(configPath, ctx.automationsJsonBackup).catch(() => {}) - cleaned.push('automations.json (restored)') - } else { - await unlink(configPath).catch(() => {}) - cleaned.push('automations.json (removed)') - } - if (ctx.automationsHistoryBackup != null) { - await writeFile(historyPath, ctx.automationsHistoryBackup).catch(() => {}) - } else { - await unlink(historyPath).catch(() => {}) - } - ctx.createdAutomation = false - } catch { /* best effort */ } - } - - // Delete automation-triggered sessions - for (const key of ['automationTestSessionId', 'automationBlockedSessionId'] as const) { - const id = ctx[key] - if (!id || !client.isConnected) continue - try { - await client.invoke('sessions:delete', id) - cleaned.push(`session ${id}`) - ctx[key] = undefined - } catch { /* best effort */ } - } - - // Delete test label - if (ctx.workspaceId && ctx.createdLabelId && client.isConnected) { - try { - await client.invoke('labels:delete', ctx.workspaceId, ctx.createdLabelId) - cleaned.push(`label ${ctx.createdLabelId}`) - ctx.createdLabelId = undefined - } catch { /* best effort */ } - } - - return cleaned -} - -export function getValidateSteps(): ValidateStep[] { - return [ - { - name: 'Connect + handshake', - fn: async (client) => { - const start = performance.now() - const clientId = await client.connect() - const ms = Math.round(performance.now() - start) - return `clientId: ${clientId}, ${ms}ms` - }, - }, - { - name: 'credentials:healthCheck', - fn: async (client) => { - const r = (await client.invoke('credentials:healthCheck')) as any - return JSON.stringify(r) - }, - }, - { - name: 'system:versions', - fn: async (client) => { - const r = (await client.invoke('system:versions')) as any - return r?.node ? `node=${r.node}` : JSON.stringify(r) - }, - }, - { - name: 'system:homeDir', - fn: async (client) => { - const r = await client.invoke('system:homeDir') - return String(r) - }, - }, - { - name: 'workspaces:get', - fn: async (client, ctx) => { - // Register workspace from --workspace-dir if provided - if (ctx.workspaceDir) { - const { resolve } = await import('path') - const absPath = resolve(ctx.workspaceDir) - const ws = (await client.invoke('workspaces:create', absPath, 'ci-workspace')) as { id: string } - ctx.workspaceId = ws.id - ctx.workspaceRootPath = absPath - await client.invoke('window:switchWorkspace', ws.id) - return `registered: ${absPath}` - } - const r = (await client.invoke('workspaces:get')) as any[] - if (r?.length > 0) { - ctx.workspaceId = r[0].id - ctx.workspaceRootPath = r[0].rootPath ?? r[0].path - // Bind this client to the workspace so push events (e.g. session:event) - // routed { to: 'workspace' } reach us. - await client.invoke('window:switchWorkspace', r[0].id) - return `${r.length} workspaces` - } - // Auto-bootstrap a temp workspace for CI environments - const { mkdtemp } = await import('fs/promises') - const { tmpdir } = await import('os') - const tmpDir = await mkdtemp(`${tmpdir()}/craft-validate-`) - const ws = (await client.invoke('workspaces:create', tmpDir, 'validate-workspace')) as { id: string } - ctx.workspaceId = ws.id - ctx.workspaceRootPath = tmpDir - ctx.createdWorkspace = true - await client.invoke('window:switchWorkspace', ws.id) - return `0 found → created temp workspace` - }, - }, - { - name: 'sessions:get', - fn: async (client, ctx) => { - if (!ctx.workspaceId) return 'skipped (no workspace)' - const r = (await client.invoke('sessions:get', ctx.workspaceId)) as any[] - return `${r?.length ?? 0} sessions` - }, - }, - { - name: 'LLM_Connection:list', - fn: async (client, ctx) => { - const r = (await client.invoke('LLM_Connection:list')) as any[] - const provider = ctx.provider || 'qwen' - if (!shouldSetupLlmConnection(r?.length ?? 0, { provider, baseUrl: ctx.baseUrl ?? '' })) { - return `${r.length} connections` - } - const slug = 'qwen-code' - await client.invoke('LLM_Connection:save', { - slug, - name: 'Qwen Code', - providerType: 'qwen', - authType: 'none', - createdAt: Date.now(), - }) - const result = await client.invoke('settings:setupLlmConnection', { slug }) as { success: boolean; error?: string } - if (!result?.success) return `setup failed: ${result?.error ?? 'unknown'}` - await client.invoke('LLM_Connection:setDefault', slug) - return '0 found -> created Qwen Code connection' - }, - }, - { - name: 'sources:get', - fn: async (client, ctx) => { - if (!ctx.workspaceId) return 'skipped (no workspace)' - const r = (await client.invoke('sources:get', ctx.workspaceId)) as any[] - return `${r?.length ?? 0} sources` - }, - }, - { - name: 'sessions:create', - fn: async (client, ctx) => { - if (!ctx.workspaceId) return 'skipped (no workspace)' - const name = `__cli-validate-${Date.now()}` - const r = (await client.invoke('sessions:create', ctx.workspaceId, { - name, - permissionMode: 'allow-all', - })) as any - ctx.createdSessionId = r?.id - return ctx.createdSessionId ?? 'created' - }, - }, - { - name: 'sessions:getMessages', - fn: async (client, ctx) => { - if (!ctx.createdSessionId) return 'skipped (no session)' - await client.invoke('sessions:getMessages', ctx.createdSessionId) - return 'session readable' - }, - }, - { - name: 'send message + stream', - fn: async (client, ctx) => { - if (!ctx.createdSessionId) return 'skipped (no session)' - return await waitForSendEvents(client, ctx.createdSessionId, - 'Reply with exactly: VALIDATION_OK', 60_000, false, undefined, ctx.onEvent) - }, - }, - { - name: 'send message + tool use', - fn: async (client, ctx) => { - if (!ctx.createdSessionId) return 'skipped (no session)' - return await waitForSendEvents(client, ctx.createdSessionId, - 'Use the Bash tool to run: echo TOOL_VALIDATION_OK', 90_000, true, undefined, ctx.onEvent) - }, - }, - // ----- Session tool validation (guards against #511 regression) ----- - { - name: 'labels:create (e2e-test)', - fn: async (client, ctx) => { - if (!ctx.workspaceId) return 'skipped (no workspace)' - const r = (await client.invoke('labels:create', ctx.workspaceId, { - name: 'e2e-test', - color: 'gray', - })) as any - ctx.e2eTestLabelId = r?.id - return `label created: ${r?.id}` - }, - }, - { - name: 'session-tools:set_session_labels', - fn: async (client, ctx) => { - if (!ctx.createdSessionId) return 'skipped (no session)' - if (!ctx.e2eTestLabelId) return 'skipped (no e2e-test label)' - const result = await waitForSendEvents(client, ctx.createdSessionId, - 'Use the set_session_labels tool to set labels: ["e2e-test"] on the current session. Do NOT use any other tool.', - 90_000, true, undefined, ctx.onEvent, 'set_session_labels') - // Verify labels were actually applied - const sessions = (await client.invoke('sessions:get', ctx.workspaceId)) as any[] - const session = sessions?.find((s: any) => s.id === ctx.createdSessionId) - const labels = session?.labels ?? session?.labelIds ?? [] - if (!labels.length) throw new Error('Labels not applied to session') - return `${result} — labels verified: ${JSON.stringify(labels)}` - }, - }, - { - name: 'session-tools:get_session_info', - fn: async (client, ctx) => { - if (!ctx.createdSessionId) return 'skipped (no session)' - return await waitForSendEvents(client, ctx.createdSessionId, - 'Use the get_session_info tool to get info about the current session. Do NOT use any other tool.', - 90_000, true, undefined, ctx.onEvent, 'get_session_info') - }, - }, - { - name: 'session-tools:list_sessions', - fn: async (client, ctx) => { - if (!ctx.createdSessionId) return 'skipped (no session)' - return await waitForSendEvents(client, ctx.createdSessionId, - 'Use the list_sessions tool to list all sessions. Do NOT use any other tool.', - 90_000, true, undefined, ctx.onEvent, 'list_sessions') - }, - }, - // ----- Session branching ----- - { - name: 'sessions:branch', - fn: async (client, ctx) => { - if (!ctx.createdSessionId || !ctx.workspaceId) return 'skipped (no session)' - const r = (await client.invoke('sessions:getMessages', ctx.createdSessionId)) as ValidateMessagesResponse - const messages = r?.messages ?? r?.conversation ?? [] - const firstAssistant = messages.find((m) => m.role === 'assistant') as any - if (!firstAssistant?.id) throw new Error('No assistant message found to branch from') - const branch = (await client.invoke('sessions:create', ctx.workspaceId, { - name: `__cli-validate-branch-${Date.now()}`, - permissionMode: 'allow-all', - branchFromSessionId: ctx.createdSessionId, - branchFromMessageId: firstAssistant.id, - })) as any - ctx.branchedSessionId = branch?.id - return `branched at message ${firstAssistant.id} → session ${branch?.id}` - }, - }, - { - name: 'sessions:branch verify', - fn: async (client, ctx) => { - if (!ctx.branchedSessionId) return 'skipped (no branch)' - const r = (await client.invoke('sessions:getMessages', ctx.branchedSessionId)) as ValidateMessagesResponse - const messages = r?.messages ?? r?.conversation ?? [] - const hasAssistant = messages.some((m) => m.role === 'assistant') - if (!hasAssistant) throw new Error('Branch missing assistant message') - const origR = (await client.invoke('sessions:getMessages', ctx.createdSessionId!)) as ValidateMessagesResponse - const origMessages = origR?.messages ?? origR?.conversation ?? [] - if (messages.length >= origMessages.length) { - throw new Error(`Branch has ${messages.length} messages, expected fewer than original (${origMessages.length})`) - } - return `branch has ${messages.length} messages (original has ${origMessages.length})` - }, - }, - { - name: 'sessions:branch send', - fn: async (client, ctx) => { - if (!ctx.branchedSessionId) return 'skipped (no branch)' - return await waitForSendEvents(client, ctx.branchedSessionId, - 'Reply with exactly: BRANCH_OK', 60_000, false, undefined, ctx.onEvent) - }, - }, - // ----- Source lifecycle ----- - { - name: 'sources:create', - fn: async (client, ctx) => { - if (!ctx.workspaceId) return 'skipped (no workspace)' - const r = (await client.invoke('sources:create', ctx.workspaceId, { - name: 'Cat Facts', - provider: 'catfact', - type: 'api', - api: { baseUrl: 'https://catfact.ninja', authType: 'none' }, - icon: '🐱', - })) as any - ctx.createdSourceSlug = r?.slug - return ctx.createdSourceSlug ? `slug=${ctx.createdSourceSlug}` : JSON.stringify(r) - }, - }, - { - name: 'send + source mention', - fn: async (client, ctx) => { - if (!ctx.createdSessionId || !ctx.createdSourceSlug) return 'skipped (no session or source)' - // Enable the source on the session - await client.invoke('sessions:command', ctx.createdSessionId, { - type: 'setSources', - sourceSlugs: [ctx.createdSourceSlug], - }) - return await waitForSendEvents(client, ctx.createdSessionId, - `[source:${ctx.createdSourceSlug}] Get me a cat fact`, 90_000, false, undefined, ctx.onEvent) - }, - }, - // ----- MCP source validation (pre-committed in .github/agents/sources/) ----- - { - name: 'mcp:craft-public (auth:none)', - fn: async (client, ctx) => { - if (!ctx.createdSessionId) return 'skipped (no session)' - // Enable the pre-committed craft-public MCP source on the session - const enableSlugs = [ctx.createdSourceSlug, 'craft-public'].filter(Boolean) as string[] - await client.invoke('sessions:command', ctx.createdSessionId, { - type: 'setSources', - sourceSlugs: enableSlugs, - }) - return await waitForSendEvents(client, ctx.createdSessionId, - `[source:craft-public] List the documents under the "CraftAgents E2E Test" folder inside the "CraftAgents" folder. Just list their names.`, - 180_000, false, undefined, ctx.onEvent) - }, - }, - { - name: 'mcp:stitch-mcp (header-auth)', - fn: async (client, ctx) => { - if (!ctx.createdSessionId) return 'skipped (no session)' - const apiKey = process.env.STITCH_API_KEY - if (!apiKey) return 'skipped (no STITCH_API_KEY)' - // Inject credential into store (multi-header JSON format, same as API headerNames) - await client.invoke('sources:saveCredentials', ctx.workspaceId, 'stitch-mcp', JSON.stringify({ 'X-Goog-Api-Key': apiKey })) - // Enable stitch-mcp + existing sources on session - const enableSlugs = [ctx.createdSourceSlug, 'craft-public', 'stitch-mcp'].filter(Boolean) as string[] - await client.invoke('sessions:command', ctx.createdSessionId, { - type: 'setSources', - sourceSlugs: enableSlugs, - }) - return await waitForSendEvents(client, ctx.createdSessionId, - `Use the source_test tool to test the stitch-mcp source. Report the result.`, - 90_000, false, undefined, ctx.onEvent) - }, - }, - // ----- Skill lifecycle ----- - { - name: 'send + skill create', - fn: async (client, ctx) => { - if (!ctx.createdSessionId || !ctx.workspaceRootPath) return 'skipped (no session or workspace)' - ctx.createdSkillSlug = '__cli-validate-skill' - const sourceSlug = ctx.createdSourceSlug ?? 'cat-facts' - const skillDir = `${ctx.workspaceRootPath}/skills/${ctx.createdSkillSlug}` - // Use bash to create the skill file deterministically - return await waitForSendEvents(client, ctx.createdSessionId, - `Use the Bash tool to run this exact command: -mkdir -p "${skillDir}" && cat > "${skillDir}/SKILL.md" << 'SKILLEOF' ---- -name: "CLI Validate Skill" -description: "Validation skill created by craft-cli" -requiredSources: - - "${sourceSlug}" ---- - -This skill does two things: -1. Check the current water temperature of Lake Balaton (search the web or estimate based on the season) -2. Use the Cat Facts source to get a random cat fact - -Always perform both steps when this skill is invoked. -SKILLEOF`, 90_000, true, undefined, ctx.onEvent) - }, - }, - { - name: 'skills:get (verify)', - fn: async (client, ctx) => { - if (!ctx.workspaceId || !ctx.createdSkillSlug) return 'skipped (no skill)' - const r = (await client.invoke('skills:get', ctx.workspaceId)) as any[] - const found = r?.find((s: any) => s.slug === ctx.createdSkillSlug) - if (!found) throw new Error(`Skill '${ctx.createdSkillSlug}' not found in skills list`) - return `found: ${found.name ?? found.slug}` - }, - }, - { - name: 'send + skill mention', - fn: async (client, ctx) => { - if (!ctx.createdSessionId || !ctx.createdSkillSlug) return 'skipped (no session or skill)' - return await waitForSendEvents(client, ctx.createdSessionId, - `[skill:${ctx.createdSkillSlug}] Run the skill`, 120_000, false, - { skillSlugs: [ctx.createdSkillSlug] }, ctx.onEvent) - }, - }, - { - name: 'skills:delete', - fn: async (client, ctx) => { - if (!ctx.workspaceId || !ctx.createdSkillSlug) return 'skipped (no skill)' - await client.invoke('skills:delete', ctx.workspaceId, ctx.createdSkillSlug) - return `deleted skill: ${ctx.createdSkillSlug}` - }, - }, - // ----- Automation lifecycle ----- - { - name: 'automation:create', - fn: async (client, ctx) => { - if (!ctx.createdSessionId || !ctx.workspaceRootPath) return 'skipped (no session or workspace)' - const configPath = `${ctx.workspaceRootPath}/automations.json` - const historyPath = `${ctx.workspaceRootPath}/automations-history.jsonl` - const { readFile, writeFile } = await import('fs/promises') - - // Always backup + overwrite with deterministic validation config, - // then restore during cleanup. - const existingConfig = await readFile(configPath, 'utf-8').catch(() => null) - ctx.automationsJsonBackup = existingConfig - ctx.automationsHistoryBackup = await readFile(historyPath, 'utf-8').catch(() => null) - - const templatePath = `${process.cwd()}/.github/agents/automations.json` - const templateConfig = await readFile(templatePath, 'utf-8').catch(() => null) - if (!templateConfig) { - throw new Error(`Missing automation template at ${templatePath}`) - } - - const parsed = JSON.parse(templateConfig) as { - automations?: { SessionStatusChange?: Array<{ name?: string }> } - } - const entries = parsed?.automations?.SessionStatusChange - if (!Array.isArray(entries) || entries.length === 0) { - throw new Error('Automation template missing automations.SessionStatusChange entries') - } - - const blocked = entries.find((e) => e.name === 'CLI Validate Condition Blocked') - const pass = entries.find((e) => e.name === 'CLI Validate Condition Pass') - if (!blocked?.name || !pass?.name) { - throw new Error('Automation template must define both "CLI Validate Condition Blocked" and "CLI Validate Condition Pass"') - } - - ctx.automationBlockedName = blocked.name - ctx.automationName = pass.name - - await writeFile(configPath, templateConfig) - ctx.createdAutomation = true - // ConfigWatcher auto-detects automations.json changes (debounced) - await new Promise((r) => setTimeout(r, 2000)) - return `wrote config from template (blocked=${ctx.automationBlockedName}, pass=${ctx.automationName})` - }, - }, - { - name: 'automation:trigger (status change)', - fn: async (client, ctx) => { - if (!ctx.createdSessionId || !ctx.workspaceId) return 'skipped (no session or workspace)' - // Get available statuses to find one containing "in-progress" - const statuses = (await client.invoke('statuses:list', ctx.workspaceId)) as ValidateStatus[] - const inProgress = statuses?.find((s) => - (s.id ?? '').toLowerCase().includes('in-progress') || - (s.label ?? '').toLowerCase().includes('in progress') - ) - const statusValue = inProgress?.id ?? 'in-progress' - - // Change session status to trigger the automations - await client.invoke('sessions:command', ctx.createdSessionId, { - type: 'setSessionStatus', - state: statusValue, - }) - - // Poll for expected automation behavior: - // - pass automation MUST create a session - // - blocked automation MUST NOT create a session - let delay = 1000 - const deadline = Date.now() + 60_000 - while (Date.now() < deadline) { - await new Promise((r) => setTimeout(r, delay)) - delay = Math.min(delay * 1.5, 10_000) - const sessions = (await client.invoke('sessions:get', ctx.workspaceId)) as ValidateSession[] - - const blockedSession = sessions?.find((s) => - s.name === ctx.automationBlockedName && s.id !== ctx.createdSessionId - ) - if (blockedSession) { - ctx.automationBlockedSessionId = blockedSession.id - throw new Error(`Blocked automation unexpectedly triggered (session=${blockedSession.id})`) - } - - const passSession = sessions?.find((s) => - s.name === ctx.automationName && s.id !== ctx.createdSessionId - ) - if (passSession) { - ctx.automationTestSessionId = passSession.id - - // Guard against delayed blocked-automation session creation. - await new Promise((r) => setTimeout(r, 2000)) - const sessionsAfter = (await client.invoke('sessions:get', ctx.workspaceId)) as ValidateSession[] - const blockedAfter = sessionsAfter?.find((s) => - s.name === ctx.automationBlockedName && s.id !== ctx.createdSessionId - ) - if (blockedAfter) { - ctx.automationBlockedSessionId = blockedAfter.id - throw new Error(`Blocked automation unexpectedly triggered after delay (session=${blockedAfter.id})`) - } - - return `pass triggered → session ${passSession.id}; blocked automation did not trigger (status=${statusValue})` - } - } - throw new Error('Passing automation-created session not found within 60s') - }, - }, - { - name: 'automation:verify session', - fn: async (client, ctx) => { - if (!ctx.automationTestSessionId) return 'skipped (no automation session)' - // Wait for the automation session to complete - let delay = 1000 - const deadline = Date.now() + 90_000 - while (Date.now() < deadline) { - const session = (await client.invoke('sessions:getMessages', ctx.automationTestSessionId)) as ValidateMessagesResponse - const messages = session?.messages ?? session?.conversation ?? [] - const hasAssistant = messages.some((m) => m.role === 'assistant') - if (hasAssistant) { - const lastAssistant = [...messages].reverse().find((m) => m.role === 'assistant') - const text = typeof lastAssistant?.content === 'string' - ? lastAssistant.content - : Array.isArray(lastAssistant?.content) - ? lastAssistant.content.filter((b) => b.type === 'text').map((b) => b.text ?? '').join(' ') - : '' - return `session has assistant response (${text.slice(0, 80).trim()})` - } - await new Promise((r) => setTimeout(r, delay)) - delay = Math.min(delay * 1.5, 10_000) - } - throw new Error('Automation session did not complete within 90s') - }, - }, - { - name: 'automation:verify labels', - fn: async (client, ctx) => { - if (!ctx.automationTestSessionId || !ctx.workspaceId) return 'skipped (no automation session)' - // Verify label was auto-created - const labels = (await client.invoke('labels:list', ctx.workspaceId)) as ValidateLabel[] - const found = labels?.find((l) => (l.id ?? l.name ?? '') === 'cli-validate-label') - if (!found) throw new Error('Label cli-validate-label was not auto-created') - ctx.createdLabelId = found.id ?? 'cli-validate-label' - - // Verify the automation session has the label - const sessions = (await client.invoke('sessions:get', ctx.workspaceId)) as ValidateSession[] - const automationSession = sessions?.find((s) => s.id === ctx.automationTestSessionId) - const sessionLabels: string[] = automationSession?.labels ?? [] - const hasLabel = sessionLabels.some((l: string) => l.includes('cli-validate-label')) - if (!hasLabel) throw new Error(`Automation session missing label (has: ${sessionLabels.join(', ')})`) - return `label created and assigned: ${ctx.createdLabelId}` - }, - }, - { - name: 'automations:getLastExecuted', - fn: async (client, ctx) => { - if (!ctx.workspaceId) return 'skipped (no workspace)' - const history = (await client.invoke('automations:getLastExecuted', ctx.workspaceId)) as Record - const entries = Object.entries(history) - if (entries.length === 0) throw new Error('No automation execution history found') - // Verify at least one automation ran recently (within last 2 minutes) - const recentThreshold = Date.now() - 120_000 - const recent = entries.find(([, ts]) => ts > recentThreshold) - if (!recent) throw new Error(`No recent automation execution (latest: ${Math.max(...entries.map(([, ts]) => ts))})`) - return `${entries.length} automation(s), latest ran ${Math.round((Date.now() - recent[1]) / 1000)}s ago` - }, - }, - // ----- Webhook validation ----- - { - name: 'webhook:test (RPC)', - fn: async (client, ctx) => { - if (!ctx.workspaceId) return 'skipped (no workspace)' - const r = (await client.invoke('automations:test', { - workspaceId: ctx.workspaceId, - actions: [{ - type: 'webhook', - url: 'http://127.0.0.1:19999/validate-test', - method: 'GET', - }], - })) as any - const result = r?.actions?.[0] - if (result?.success) throw new Error('Expected webhook to fail (nothing listening)') - if (!result?.error && result?.statusCode !== 0) throw new Error('Expected error or statusCode 0 in result') - return `correctly failed: ${(result.error ?? `statusCode=${result.statusCode}`).slice(0, 80)}` - }, - }, - { - name: 'webhook:verify failure', - fn: async (client, ctx) => { - if (!ctx.workspaceRootPath) return 'skipped (no workspace root)' - const { readFile } = await import('fs/promises') - const historyPath = `${ctx.workspaceRootPath}/automations-history.jsonl` - - const start = Date.now() - const deadline = start + 15_000 - let delay = 200 - - let lastLineCount = 0 - let lastWebhookCount = 0 - let lastSummary = 'no entries' - - while (Date.now() < deadline) { - const content = await readFile(historyPath, 'utf-8').catch(() => '') - const lines = content.trim().split('\n').filter(Boolean) - lastLineCount = lines.length - - const entries = lines - .map((l) => { - try { - return JSON.parse(l) - } catch { - return null - } - }) - .filter(Boolean) as Array> - - const webhookEntries = entries.filter((e) => !!e.webhook) - lastWebhookCount = webhookEntries.length - - if (webhookEntries.length > 0) { - const recentThreshold = Date.now() - 120_000 - const recentFailed = webhookEntries.find((e: any) => - !e.ok && e.ts > recentThreshold && e.webhook?.method === 'POST' - ) as any - if (recentFailed) { - return `webhook failure recorded: method=${recentFailed.webhook.method}, url=${recentFailed.webhook.url?.slice(0, 50)}` - } - - const latest = webhookEntries[webhookEntries.length - 1] as any - lastSummary = `latest: ok=${String(latest?.ok)} method=${String(latest?.webhook?.method ?? 'n/a')} ts=${String(latest?.ts ?? 'n/a')}` - } - - await new Promise((r) => setTimeout(r, delay)) - delay = Math.min(Math.round(delay * 1.8), 1500) - } - - const waitedMs = Date.now() - start - throw new Error( - `No recent failed POST webhook history entry after ${waitedMs}ms (lines=${lastLineCount}, webhookEntries=${lastWebhookCount}, ${lastSummary})`, - ) - }, - }, - { - name: 'automation:cleanup', - fn: async (client, ctx) => { - const cleaned = await cleanupAutomationArtifacts(client, ctx) - return cleaned.length > 0 ? `cleaned: ${cleaned.join(', ')}` : 'nothing to clean' - }, - }, - { - name: 'sessions:branch delete', - fn: async (client, ctx) => { - if (!ctx.branchedSessionId) return 'skipped (no branch)' - await client.invoke('sessions:delete', ctx.branchedSessionId) - const id = ctx.branchedSessionId - ctx.branchedSessionId = undefined - return `deleted branch session: ${id}` - }, - }, - { - name: 'sources:delete', - fn: async (client, ctx) => { - if (!ctx.workspaceId || !ctx.createdSourceSlug) return 'skipped (no source)' - await client.invoke('sources:delete', ctx.workspaceId, ctx.createdSourceSlug) - return `deleted source: ${ctx.createdSourceSlug}` - }, - }, - { - name: 'labels:delete (e2e-test)', - fn: async (client, ctx) => { - if (!ctx.workspaceId || !ctx.e2eTestLabelId) return 'skipped (no e2e-test label)' - await client.invoke('labels:delete', ctx.workspaceId, ctx.e2eTestLabelId) - return `deleted label: ${ctx.e2eTestLabelId}` - }, - }, - { - name: 'sessions:delete', - fn: async (client, ctx) => { - if (!ctx.createdSessionId) return 'skipped (no session)' - await client.invoke('sessions:delete', ctx.createdSessionId) - return `deleted session: ${ctx.createdSessionId}` - }, - }, - { - name: 'Disconnect', - fn: async (client) => { - client.destroy() - return 'OK' - }, - }, - ] -} - -export async function runValidation( - client: CliRpcClient, - jsonMode: boolean, - noSpinner?: boolean, - workspaceDir?: string, - validateOptions?: { baseUrl?: string; apiKey?: string; provider?: string }, -): Promise { - const steps = getValidateSteps() - const total = steps.length - const ctx: ValidateContext = { - workspaceDir, - baseUrl: validateOptions?.baseUrl, - apiKey: validateOptions?.apiKey, - provider: validateOptions?.provider, - } - let passed = 0 - let failed = 0 - const results: Array<{ step: string; status: string; detail: string; elapsed: number }> = [] - const totalStart = performance.now() - - for (let i = 0; i < steps.length; i++) { - const step = steps[i] - const num = `[${i + 1}/${total}]` - const plainLen = num.length + 1 + step.name.length - - // Spinner + live event printer - // Spinner keeps running until the agent produces real output (text_delta/tool_start). - // Early events (user_message, connection_changed, usage_update) are buffered or ignored - // so the spinner stays visible while the agent is thinking. - let spinner: { stop(): void } | undefined - if (!jsonMode) { - let headerPrinted = false - let accText = '' - let textFlushed = false - let bufferedPrompt = '' - - if (_useColor && !noSpinner) { - spinner = createSpinner(`${c.cyan(num)} ${step.name}`) - } - - const flushText = () => { - if (textFlushed || !accText) return - const clean = accText.replace(/\n/g, ' ').trim() - if (!clean) return - const display = clean.length > 120 ? clean.slice(0, 120) + '…' : clean - process.stdout.write(` ${c.dim('↳')} ${c.yellow(display)}\n`) - textFlushed = true - } - - const ensureHeader = () => { - if (headerPrinted) return - spinner?.stop() - process.stdout.write(`${c.cyan(num)} ${step.name}\n`) - if (bufferedPrompt) { - process.stdout.write(` ${c.dim('→')} ${c.blue(`"${bufferedPrompt}"`)}\n`) - } - headerPrinted = true - } - - ctx.onEvent = (ev) => { - switch (ev.type) { - // Buffer prompt — shown when agent starts responding - case 'user_message': { - const msg = ev.message as any - let text = '' - if (typeof msg?.content === 'string') { - text = msg.content - } else if (Array.isArray(msg?.content)) { - text = msg.content.filter((b: any) => b.type === 'text').map((b: any) => b.text).join(' ') - } - const clean = text.replace(/\n/g, ' ').trim() - bufferedPrompt = clean.length > 100 ? clean.slice(0, 100) + '…' : clean - break - } - // Agent text — stop spinner, show header + prompt + text - case 'text_delta': - ensureHeader() - accText += String(ev.delta ?? '') - if (!textFlushed && accText.length > 40) flushText() - break - case 'text_complete': - ensureHeader() - flushText() - break - // Tool use — stop spinner, show header + prompt + tool - case 'tool_start': { - ensureHeader() - flushText() - const name = String(ev.toolName ?? '?') - const intent = ev.toolIntent ? ` — "${ev.toolIntent}"` : '' - process.stdout.write(` ${c.dim('↳')} ${c.dim(`tool: ${name}${intent}`)}\n`) - accText = '' - textFlushed = false - break - } - // Ignore internal events (connection_changed, usage_update, etc.) - } - } - } else { - ctx.onEvent = undefined - } - - const stepStart = performance.now() - try { - const detail = await step.fn(client, ctx) - const elapsed = (performance.now() - stepStart) / 1000 - passed++ - results.push({ step: step.name, status: 'OK', detail, elapsed }) - spinner?.stop() - if (!jsonMode) { - const dots = c.dim('.'.repeat(Math.max(1, 50 - plainLen))) - const time = c.dim(elapsed < 1 ? `(${Math.round(elapsed * 1000)}ms)` : `(${elapsed.toFixed(1)}s)`) - process.stdout.write(`${c.cyan(num)} ${step.name} ${dots} ${c.green('✓')} ${detail} ${time}\n`) - } - } catch (e) { - const elapsed = (performance.now() - stepStart) / 1000 - failed++ - const msg = e instanceof Error ? e.message : String(e) - results.push({ step: step.name, status: 'FAIL', detail: msg, elapsed }) - spinner?.stop() - if (!jsonMode) { - const dots = c.dim('.'.repeat(Math.max(1, 50 - plainLen))) - const time = c.dim(elapsed < 1 ? `(${Math.round(elapsed * 1000)}ms)` : `(${elapsed.toFixed(1)}s)`) - process.stderr.write(`${c.cyan(num)} ${step.name} ${dots} ${c.red('✗')} ${msg} ${time}\n`) - } - } - } - - // Cleanup: branched session - if (ctx.branchedSessionId && client.isConnected) { - try { - await client.invoke('sessions:delete', ctx.branchedSessionId) - } catch { - // best effort - } - } - - // Cleanup: if a session was created but delete step hasn't run or failed - if (ctx.createdSessionId && client.isConnected) { - try { - await client.invoke('sessions:delete', ctx.createdSessionId) - } catch { - // best effort - } - } - - // Cleanup: automation artifacts - await cleanupAutomationArtifacts(client, ctx) - - // Cleanup: if we auto-created a temp workspace, remove it - if (ctx.createdWorkspace && ctx.workspaceId && client.isConnected) { - try { - await client.invoke('workspaces:delete', ctx.workspaceId) - } catch { - // best effort - } - if (ctx.workspaceRootPath) { - try { - const { rm } = await import('fs/promises') - await rm(ctx.workspaceRootPath, { recursive: true, force: true }) - } catch { - // best effort - } - } - } - - const totalSec = ((performance.now() - totalStart) / 1000).toFixed(1) - - if (jsonMode) { - out({ total, passed, failed, results, elapsedSeconds: parseFloat(totalSec) }, true) - } else { - if (failed === 0) { - process.stdout.write(`\n${c.green(`✓ ${passed}/${total} passed`)} ${c.dim(`in ${totalSec}s`)}\n`) - } else { - process.stdout.write(`\n${c.red(`✗ ${passed}/${total} passed, ${failed} failed`)} ${c.dim(`in ${totalSec}s`)}\n`) - } - } - - return failed > 0 ? 1 : 0 -} - -// --------------------------------------------------------------------------- -// Help -// --------------------------------------------------------------------------- - -function printHelp(): void { - process.stdout.write(`craft-cli — Terminal client for Qwen Code server - -Usage: craft-cli [options] [args...] - -Connection: - --url Server URL (default: $CRAFT_SERVER_URL) - --token Auth token (default: $CRAFT_SERVER_TOKEN) - --workspace Workspace ID (auto-detected if omitted) - --timeout Request timeout (default: 10000) - --tls-ca Custom CA cert for self-signed TLS - --json Raw JSON output for scripting - -LLM Configuration (for 'run' command): - --provider LLM provider (default: qwen, or $LLM_PROVIDER) - Supported: qwen - --model Model to use (or $LLM_MODEL) - --api-key Ignored for Qwen Code; kept for script compatibility - --base-url Ignored for Qwen Code; kept for script compatibility - -Commands: - run Spawn server, send message, stream response, exit - --workspace-dir Use directory as workspace (creates if needed) - --source Enable source (repeatable) - --mode Permission mode (default: allow-all) - --output-format text or stream-json (default: text) - --no-cleanup Keep session after completion - --server-entry Path to server/index.ts - ping Verify connectivity (clientId + latency) - health Check credential store health - versions Show server runtime versions - workspaces List workspaces - sessions List sessions in workspace - connections List LLM connections - sources List configured sources - session create Create a session (--name, --mode) - session messages Print session message history - session delete Delete a session - send Send message and stream AI response - cancel Cancel in-progress processing - invoke [...] Raw RPC call with JSON args - listen Subscribe to push events (Ctrl+C to stop) - --validate-server Multi-step server integration test - --verbose, -v Show server stderr output - -Examples: - craft-cli run "What files are in the current directory?" - craft-cli run --source craft-kb "Summarize today's daily note" - craft-cli run --workspace-dir .github/agents --source craft-public "Read the doc" - craft-cli run --provider qwen --model qwen3-coder "Summarize this repo" - echo "Analyze this code" | craft-cli run - craft-cli ping - craft-cli sessions - craft-cli send abc-123 "What files are in the current directory?" - echo "Summarize this" | craft-cli send abc-123 - craft-cli --validate-server - craft-cli invoke system:homeDir - craft-cli --json workspaces | jq '.[].name' -`) -} - -// --------------------------------------------------------------------------- -// Main -// --------------------------------------------------------------------------- - -export async function main(argv: string[] = process.argv): Promise { - const args = parseArgs(argv) - - // Set custom CA before any WS connections - if (args.tlsCa) { - process.env.NODE_EXTRA_CA_CERTS = args.tlsCa - } - - if (args.command === 'help' || args.command === '') { - printHelp() - return - } - - if (args.command === 'version') { - const pkg = await import('../package.json') - out(pkg.version ?? pkg.default?.version ?? 'unknown', false) - return - } - - // run is self-contained — spawns its own server - if (args.command === 'run') { - await cmdRun(args) - return - } - - // validate can spawn its own server or use --url - if (args.command === 'validate') { - await cmdValidate(args) - return - } - - // All other commands need a server URL - if (!args.url) { - err('No server URL. Use --url or set $CRAFT_SERVER_URL') - process.exit(1) - } - - const client = new CliRpcClient(args.url, { - token: args.token || undefined, - workspaceId: args.workspace, - requestTimeout: args.timeout, - connectTimeout: args.timeout, - }) - - try { - switch (args.command) { - case 'ping': - await cmdPing(client, args) - break - case 'health': - await cmdHealth(client, args) - break - case 'versions': - await cmdVersions(client, args) - break - case 'workspaces': - await cmdWorkspaces(client, args) - break - case 'sessions': - await cmdSessions(client, args) - break - case 'connections': - await cmdConnections(client, args) - break - case 'sources': - await cmdSources(client, args) - break - case 'session': { - const subCmd = args.rest.shift() - switch (subCmd) { - case 'create': - await cmdSessionCreate(client, args) - break - case 'messages': - await cmdSessionMessages(client, args) - break - case 'delete': - await cmdSessionDelete(client, args) - break - default: - err(`Unknown session subcommand: ${subCmd}`) - process.exit(1) - } - break - } - case 'send': - await cmdSend(client, args) - break // cmdSend calls process.exit - case 'cancel': - await cmdCancel(client, args) - break - case 'invoke': - await cmdInvoke(client, args) - break - case 'listen': - await cmdListen(client, args) - break // never returns - default: - err(`Unknown command: ${args.command}`) - printHelp() - process.exit(1) - } - } catch (e) { - const msg = e instanceof Error ? e.message : String(e) - err(msg) - process.exit(1) - } finally { - client.destroy() - } -} - -// Run if executed directly (not when imported by tests) -if (import.meta.main) { - main() -} diff --git a/packages/desktop/apps/cli/src/run.test.ts b/packages/desktop/apps/cli/src/run.test.ts deleted file mode 100644 index e54e4dc11b9..00000000000 --- a/packages/desktop/apps/cli/src/run.test.ts +++ /dev/null @@ -1,427 +0,0 @@ -import { describe, it, expect, afterEach, mock, beforeEach } from 'bun:test' -import { - serializeEnvelope, - deserializeEnvelope, -} from '@craft-agent/server-core/transport' -import type { SpawnedServer } from './server-spawner.ts' - -// --------------------------------------------------------------------------- -// Mock WS server for run command tests -// --------------------------------------------------------------------------- - -interface MockServerOptions { - /** What LLM_Connection:list returns */ - connections?: unknown[] -} - -interface MockServer { - url: string - token: string - close: () => void - /** Channels invoked by the client, in order */ - invokedChannels: string[] - /** Arguments passed to sessions:create */ - createSessionArgs?: unknown[] - /** All invocation args, keyed by channel */ - invokeArgs: Record -} - -function pushSessionEvents( - ws: any, - sessionId: string, - events: Array>, -): void { - setTimeout(() => { - for (const ev of events) { - ws.send(serializeEnvelope({ - id: crypto.randomUUID(), - type: 'event', - channel: 'session:event', - args: [{ sessionId, ...ev }], - })) - } - }, 10) -} - -function createMockServer(opts?: MockServerOptions): MockServer { - const token = 'test-token' - const invokedChannels: string[] = [] - const invokeArgs: Record = {} - let createSessionArgs: unknown[] | undefined - const connections = opts?.connections ?? [] - - const server = Bun.serve({ - port: 0, - fetch(req, svr) { - if (svr.upgrade(req)) return undefined - return new Response('Not found', { status: 404 }) - }, - websocket: { - message(ws, message) { - const raw = typeof message === 'string' ? message : new TextDecoder().decode(message) - const envelope = deserializeEnvelope(raw) - - if (envelope.type === 'handshake') { - ws.send(serializeEnvelope({ - id: crypto.randomUUID(), - type: 'handshake_ack', - clientId: 'run-test-client', - protocolVersion: '1.0', - })) - return - } - - if (envelope.type === 'request') { - const ch = envelope.channel! - invokedChannels.push(ch) - if (!invokeArgs[ch]) invokeArgs[ch] = [] - invokeArgs[ch].push(envelope.args ?? []) - - let result: unknown - switch (ch) { - case 'workspaces:get': - result = [{ id: 'ws-1', name: 'Test Workspace' }] - break - case 'workspaces:create': - result = { id: 'ws-1', name: 'ci-workspace' } - break - case 'window:switchWorkspace': - result = { ok: true } - break - case 'LLM_Connection:list': - result = connections - break - case 'LLM_Connection:save': - result = { ok: true } - break - case 'settings:setupLlmConnection': - result = { ok: true } - break - case 'LLM_Connection:setDefault': - result = { ok: true } - break - case 'sessions:create': - createSessionArgs = envelope.args - result = { id: 'run-session-1', name: 'run-test' } - break - case 'sessions:sendMessage': { - ws.send(serializeEnvelope({ - id: envelope.id, - type: 'response', - channel: ch, - result: { started: true }, - })) - pushSessionEvents(ws, 'run-session-1', [ - { type: 'text_delta', delta: 'Hello ' }, - { type: 'text_delta', delta: 'World' }, - { type: 'complete' }, - ]) - return // already sent response - } - case 'sessions:delete': - result = { deleted: true } - break - case 'sessions:cancel': - result = { cancelled: true } - break - default: - result = null - } - - ws.send(serializeEnvelope({ - id: envelope.id, - type: 'response', - channel: ch, - result, - })) - } - }, - }, - }) - - return { - url: `ws://localhost:${server.port}`, - token, - close: () => server.stop(), - invokedChannels, - invokeArgs, - get createSessionArgs() { return createSessionArgs }, - } -} - -// --------------------------------------------------------------------------- -// Mock spawnServer so cmdRun doesn't actually launch a child process -// --------------------------------------------------------------------------- - -let mockWsServer: MockServer | null = null - -mock.module('./server-spawner.ts', () => ({ - spawnServer: async (): Promise => { - if (!mockWsServer) throw new Error('mockWsServer not initialized') - return { - url: mockWsServer.url, - token: mockWsServer.token, - stop: async () => {}, - } - }, -})) - -// Import main AFTER mocking -const { parseArgs } = await import('./index.ts') - -// --------------------------------------------------------------------------- -// Tests -// --------------------------------------------------------------------------- - -describe('run command', () => { - beforeEach(() => { - mockWsServer = createMockServer() - }) - - afterEach(() => { - mockWsServer?.close() - mockWsServer = null - }) - - it('parseArgs: run with --source accumulates sources', () => { - const args = parseArgs([ - 'bun', 'index.ts', - '--source', 'craft-kb', - '--source', 'github', - 'run', 'do', 'stuff', - ]) - expect(args.command).toBe('run') - expect(args.sources).toEqual(['craft-kb', 'github']) - expect(args.rest).toEqual(['do', 'stuff']) - }) - - it('parseArgs: --output-format stream-json', () => { - const args = parseArgs([ - 'bun', 'index.ts', - '--output-format', 'stream-json', - 'run', 'test', - ]) - expect(args.outputFormat).toBe('stream-json') - }) - - it('parseArgs: --no-cleanup flag', () => { - const args = parseArgs([ - 'bun', 'index.ts', - '--no-cleanup', - 'run', 'test', - ]) - expect(args.noCleanup).toBe(true) - }) - - it('creates session with correct workspace and options', async () => { - // We can't easily call cmdRun directly since it calls process.exit. - // Instead, test the mock server interaction via CliRpcClient to verify - // the channels and args that cmdRun would invoke. - const { CliRpcClient } = await import('./client.ts') - - const client = new CliRpcClient(mockWsServer!.url, { - token: mockWsServer!.token, - requestTimeout: 5_000, - }) - await client.connect() - - // Simulate what cmdRun does: resolve workspace, create session - const workspaces = await client.invoke('workspaces:get') as any[] - expect(workspaces).toHaveLength(1) - - await client.invoke('window:switchWorkspace', workspaces[0].id) - - const session = await client.invoke('sessions:create', 'ws-1', { - permissionMode: 'allow-all', - enabledSourceSlugs: ['craft-kb'], - }) as { id: string } - expect(session.id).toBe('run-session-1') - - // Verify the create args - expect(mockWsServer!.createSessionArgs).toEqual([ - 'ws-1', - { permissionMode: 'allow-all', enabledSourceSlugs: ['craft-kb'] }, - ]) - - // Verify channel order - expect(mockWsServer!.invokedChannels).toEqual([ - 'workspaces:get', - 'window:switchWorkspace', - 'sessions:create', - ]) - - client.destroy() - }) - - it('streams text events from session', async () => { - const { CliRpcClient } = await import('./client.ts') - - const client = new CliRpcClient(mockWsServer!.url, { - token: mockWsServer!.token, - requestTimeout: 5_000, - }) - await client.connect() - - // Subscribe and collect text deltas - const deltas: string[] = [] - let completed = false - - const unsub = client.on('session:event', (event: unknown) => { - const ev = event as { type: string; sessionId: string; delta?: string } - if (ev.sessionId !== 'run-session-1') return - if (ev.type === 'text_delta') deltas.push(ev.delta!) - if (ev.type === 'complete') completed = true - }) - - await client.invoke('sessions:sendMessage', 'run-session-1', 'test') - - // Wait for events - const deadline = Date.now() + 5_000 - while (!completed && Date.now() < deadline) { - await new Promise((r) => setTimeout(r, 50)) - } - - unsub() - expect(completed).toBe(true) - expect(deltas).toEqual(['Hello ', 'World']) - - client.destroy() - }) - - it('session delete is called in lifecycle', async () => { - const { CliRpcClient } = await import('./client.ts') - - const client = new CliRpcClient(mockWsServer!.url, { - token: mockWsServer!.token, - requestTimeout: 5_000, - }) - await client.connect() - - await client.invoke('sessions:create', 'ws-1', { permissionMode: 'allow-all' }) - await client.invoke('sessions:delete', 'run-session-1') - - expect(mockWsServer!.invokedChannels).toContain('sessions:create') - expect(mockWsServer!.invokedChannels).toContain('sessions:delete') - - client.destroy() - }) - - it('spawnServer mock returns expected url and token', async () => { - const { spawnServer } = await import('./server-spawner.ts') - const server = await spawnServer() - - expect(server.url).toBe(mockWsServer!.url) - expect(server.token).toBe(mockWsServer!.token) - expect(typeof server.stop).toBe('function') - }) - - it('parseArgs: --workspace-dir sets workspaceDir', () => { - const args = parseArgs([ - 'bun', 'index.ts', - '--workspace-dir', '/tmp/my-workspace', - 'run', 'hello', - ]) - expect(args.workspaceDir).toBe('/tmp/my-workspace') - expect(args.command).toBe('run') - }) - - it('parseArgs: workspaceDir defaults to undefined', () => { - const args = parseArgs(['bun', 'index.ts', 'run', 'hello']) - expect(args.workspaceDir).toBeUndefined() - }) - - it('workspace:create returns ID used directly (no workspaces:get needed)', async () => { - const { CliRpcClient } = await import('./client.ts') - - const client = new CliRpcClient(mockWsServer!.url, { - token: mockWsServer!.token, - requestTimeout: 5_000, - }) - await client.connect() - - // Simulate the workspace bootstrap path from cmdRun: - // workspaces:create returns { id }, which is used directly - const ws = (await client.invoke('workspaces:create', '/tmp/ws', 'ci-workspace')) as { id: string } - expect(ws.id).toBe('ws-1') - - // Then switchWorkspace is called with the returned ID - await client.invoke('window:switchWorkspace', ws.id) - - // Session is created with the bootstrapped workspace ID - await client.invoke('sessions:create', ws.id, { - permissionMode: 'allow-all', - enabledSourceSlugs: ['craft-public'], - }) - - expect(mockWsServer!.invokedChannels).toEqual([ - 'workspaces:create', - 'window:switchWorkspace', - 'sessions:create', - ]) - expect(mockWsServer!.invokeArgs['workspaces:create']![0]).toEqual(['/tmp/ws', 'ci-workspace']) - - client.destroy() - }) - - it('LLM bootstrap calls save, setup, and setDefault when no connections exist', async () => { - // Server returns empty connections list - mockWsServer?.close() - mockWsServer = createMockServer({ connections: [] }) - - const { CliRpcClient } = await import('./client.ts') - const client = new CliRpcClient(mockWsServer!.url, { - token: mockWsServer!.token, - requestTimeout: 5_000, - }) - await client.connect() - - // Simulate the LLM bootstrap path from cmdRun - const connections = (await client.invoke('LLM_Connection:list')) as any[] - expect(connections).toEqual([]) - - await client.invoke('LLM_Connection:save', { - slug: 'qwen-code', - name: 'Qwen Code', - providerType: 'qwen', - authType: 'none', - createdAt: 123, - }) - await client.invoke('settings:setupLlmConnection', { - slug: 'qwen-code', - }) - await client.invoke('LLM_Connection:setDefault', 'qwen-code') - - expect(mockWsServer!.invokedChannels).toEqual([ - 'LLM_Connection:list', - 'LLM_Connection:save', - 'settings:setupLlmConnection', - 'LLM_Connection:setDefault', - ]) - - client.destroy() - }) - - it('LLM bootstrap is skipped when connections already exist', async () => { - // Server returns existing connection - mockWsServer?.close() - mockWsServer = createMockServer({ - connections: [{ slug: 'existing', name: 'Existing' }], - }) - - const { CliRpcClient } = await import('./client.ts') - const client = new CliRpcClient(mockWsServer!.url, { - token: mockWsServer!.token, - requestTimeout: 5_000, - }) - await client.connect() - - // Simulate: check connections — they exist, so skip bootstrap - const connections = (await client.invoke('LLM_Connection:list')) as any[] - expect(connections).toHaveLength(1) - - // No further LLM calls should be needed - expect(mockWsServer!.invokedChannels).toEqual(['LLM_Connection:list']) - - client.destroy() - }) -}) diff --git a/packages/desktop/apps/cli/src/server-spawner.ts b/packages/desktop/apps/cli/src/server-spawner.ts deleted file mode 100644 index 941e21be200..00000000000 --- a/packages/desktop/apps/cli/src/server-spawner.ts +++ /dev/null @@ -1,146 +0,0 @@ -/** - * Server spawner — start a headless Qwen Code server as a child process. - * - * Spawns `bun run `, reads stdout for the `CRAFT_SERVER_URL=` - * and `CRAFT_SERVER_TOKEN=` lines, and returns a handle to stop the server. - */ - -import { resolve, join } from 'node:path' -import type { Subprocess } from 'bun' - -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - -export interface SpawnedServer { - url: string - token: string - stop: () => Promise -} - -export interface SpawnServerOptions { - /** Path to the server entry file. Auto-detected from monorepo root if omitted. */ - serverEntry?: string - /** Extra env vars to pass to the server process. */ - env?: Record - /** How long to wait for the server to print its URL (ms). Default: 30000. */ - startupTimeout?: number - /** Suppress server stderr output (useful for validation where only test output matters). */ - quiet?: boolean -} - -// --------------------------------------------------------------------------- -// Auto-detect server entry -// --------------------------------------------------------------------------- - -function findServerEntry(): string { - // Walk up from this file's directory to find the monorepo root. - // Expected layout: apps/cli/src/server-spawner.ts → root/packages/server/src/index.ts - let dir = import.meta.dir - for (let i = 0; i < 10; i++) { - const candidate = join(dir, 'packages', 'server', 'src', 'index.ts') - if (Bun.file(candidate).size > 0) return candidate - dir = resolve(dir, '..') - } - throw new Error( - 'Could not auto-detect server entry. ' + - 'Pass --server-entry or ensure the monorepo layout includes packages/server/src/index.ts', - ) -} - -// --------------------------------------------------------------------------- -// Spawn -// --------------------------------------------------------------------------- - -export async function spawnServer(opts?: SpawnServerOptions): Promise { - const serverEntry = opts?.serverEntry ?? findServerEntry() - const startupTimeout = opts?.startupTimeout ?? 30_000 - const token = crypto.randomUUID() - - const proc: Subprocess = Bun.spawn(['bun', 'run', serverEntry], { - env: { - ...process.env, - ...opts?.env, - CRAFT_SERVER_TOKEN: token, - CRAFT_RPC_PORT: '0', - CRAFT_RPC_HOST: '127.0.0.1', - }, - stdout: 'pipe', - stderr: 'pipe', - }) - - // Pipe server stderr to our stderr so --debug logs are visible (unless quiet) - if (proc.stderr && !opts?.quiet) { - ;(async () => { - // @ts-expect-error — Bun Subprocess types don't narrow stderr to ReadableStream when stderr: 'pipe' - const reader = proc.stderr.getReader() - try { - while (true) { - const { done, value } = await reader.read() - if (done) break - process.stderr.write(value) - } - } catch { - // Server exited — normal - } - })() - } - - // Read stdout line by line looking for CRAFT_SERVER_URL= - return new Promise((resolve, reject) => { - const timer = setTimeout(() => { - proc.kill() - reject(new Error(`Server did not start within ${startupTimeout}ms`)) - }, startupTimeout) - - let url = '' - let buffer = '' - - const processLines = () => { - const lines = buffer.split('\n') - buffer = lines.pop() ?? '' // keep incomplete last line in buffer - for (const line of lines) { - if (line.startsWith('CRAFT_SERVER_URL=')) { - url = line.slice('CRAFT_SERVER_URL='.length).trim() - } - if (line.startsWith('CRAFT_SERVER_TOKEN=')) { - // Server echoes the token — we already have it but this confirms ready - } - // Once we have the URL, the server is ready - if (url) { - clearTimeout(timer) - resolve({ - url, - token, - stop: async () => { - proc.kill('SIGTERM') - await proc.exited - }, - }) - return - } - } - } - - ;(async () => { - // @ts-expect-error — Bun Subprocess types don't narrow stdout to ReadableStream when stdout: 'pipe' - const reader = proc.stdout.getReader() - const decoder = new TextDecoder() - try { - while (true) { - const { done, value } = await reader.read() - if (done) break - buffer += decoder.decode(value, { stream: true }) - processLines() - } - } catch { - // Stream closed - } - // If we get here without resolving, the process exited before printing the URL - clearTimeout(timer) - if (!url) { - reject(new Error('Server process exited before printing CRAFT_SERVER_URL')) - } - })() - }) -} diff --git a/packages/desktop/apps/cli/tsconfig.json b/packages/desktop/apps/cli/tsconfig.json deleted file mode 100644 index 1c0952eea2a..00000000000 --- a/packages/desktop/apps/cli/tsconfig.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "compilerOptions": { - "lib": ["ESNext"], - "target": "ESNext", - "module": "ESNext", - "moduleDetection": "force", - "allowJs": true, - "esModuleInterop": true, - "resolveJsonModule": true, - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "types": ["node", "bun"], - "noFallthroughCasesInSwitch": true, - "noUnusedLocals": false, - "noUnusedParameters": false, - "baseUrl": ".", - "paths": { - "@craft-agent/shared": ["../../packages/shared/src/index.ts"], - "@craft-agent/shared/*": ["../../packages/shared/src/*"], - "@craft-agent/server-core": ["../../packages/server-core/src/index.ts"], - "@craft-agent/server-core/*": ["../../packages/server-core/src/*"] - } - }, - "include": ["src/**/*", "../../packages/shared/src/**/*.d.ts"], - "exclude": ["node_modules", "dist"] -} diff --git a/packages/desktop/apps/electron/.gitignore b/packages/desktop/apps/electron/.gitignore deleted file mode 100644 index 4f50e570fb9..00000000000 --- a/packages/desktop/apps/electron/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -# Build artifacts for DMG packaging -vendor/ -packages/ -release/ -node_modules/@anthropic-ai/ - -# Platform-specific uv binaries (downloaded during build) -resources/bin/darwin-arm64/ -resources/bin/darwin-x64/ -resources/bin/win32-x64/ -resources/bin/linux-x64/ diff --git a/packages/desktop/apps/electron/README.md b/packages/desktop/apps/electron/README.md deleted file mode 100644 index 3aa57a0a7c2..00000000000 --- a/packages/desktop/apps/electron/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# Qwen Code Electron App - -Electron + React desktop interface for Qwen Code. - -The desktop app provides: - -- Qwen-backed multi-session chat -- Workspace and source management -- Onboarding for local Qwen Code setup -- Permission modes and plan approval flow -- File previews, diffs, browser panes, and automations - -## Development - -```bash -bun install -bun run electron:start -``` - -## Structure - -```text -src/main/ Electron main process -src/preload/ Context bridge -src/renderer/ React UI -src/transport/ RPC client/server transport -resources/ Built-in docs and release assets -``` diff --git a/packages/desktop/apps/electron/build/entitlements.mac.plist b/packages/desktop/apps/electron/build/entitlements.mac.plist deleted file mode 100644 index 369ba01ad37..00000000000 --- a/packages/desktop/apps/electron/build/entitlements.mac.plist +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - com.apple.security.cs.allow-jit - - com.apple.security.cs.allow-unsigned-executable-memory - - - com.apple.security.cs.disable-library-validation - - - com.apple.security.device.audio-input - - - diff --git a/packages/desktop/apps/electron/components.json b/packages/desktop/apps/electron/components.json deleted file mode 100644 index b69345d8fc8..00000000000 --- a/packages/desktop/apps/electron/components.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", - "rsc": false, - "tsx": true, - "tailwind": { - "config": "", - "css": "src/renderer/index.css", - "baseColor": "neutral", - "cssVariables": true - }, - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - } -} diff --git a/packages/desktop/apps/electron/electron-builder.yml b/packages/desktop/apps/electron/electron-builder.yml deleted file mode 100644 index 7a2b8ba3655..00000000000 --- a/packages/desktop/apps/electron/electron-builder.yml +++ /dev/null @@ -1,192 +0,0 @@ -appId: com.alibaba.qwen-code -productName: Qwen Code Desktop -copyright: Copyright © 2026 Alibaba Group. - -electronVersion: "39.2.7" - -# Hook to compile macOS 26+ Liquid Glass icon after packaging -afterPack: scripts/afterPack.cjs - -directories: - output: release - buildResources: resources - -files: - - dist/**/* - - "!dist/renderer/src/**" - - "!**/*.map" - - package.json - # Include bundled MCP servers (bridge + session) for agent sessions - - resources/bridge-mcp-server/**/* - - resources/session-mcp-server/**/* - # Note: Bundled assets (docs, themes, permissions, tool-icons) are in resources/ - # and copied to dist/resources/ by build:copy. They're included via dist/**/* above. - - packages/shared/src/interceptor-common.ts - - packages/shared/src/feature-flags.ts - - packages/shared/src/interceptor-request-utils.ts - # Include CLI tool Python scripts (platform-independent) - - resources/scripts/**/* - # Include CLI tool shell wrappers (platform-independent, both Unix and Windows) - - resources/bin/markitdown - - resources/bin/markitdown.cmd - - resources/bin/pdf-tool - - resources/bin/pdf-tool.cmd - - resources/bin/xlsx-tool - - resources/bin/xlsx-tool.cmd - - resources/bin/doc-diff - - resources/bin/doc-diff.cmd - - resources/bin/img-tool - - resources/bin/img-tool.cmd - - resources/bin/docx-tool - - resources/bin/docx-tool.cmd - - resources/bin/pptx-tool - - resources/bin/pptx-tool.cmd - - resources/bin/ical-tool - - resources/bin/ical-tool.cmd - # Include bundled uv binary (platform-specific, only the relevant one ships per build) - - resources/bin/darwin-arm64/**/* - - resources/bin/darwin-x64/**/* - - resources/bin/win32-x64/**/* - - resources/bin/linux-x64/**/* - # Include bundled Bun runtime (platform-specific, set by build script) - - vendor/bun/**/* - # Include bundled Codex binary (platform-specific, downloaded by build script) - - vendor/codex/**/* - # Include bundled Qwen Code CLI package (downloaded by build script) - - vendor/qwen-code/**/* - # Exclude everything from node_modules. - - "!node_modules/**/*" - -extraMetadata: - main: dist/main.cjs - -# Auto-update publish metadata is generated from the active brand config. - -# Disable ASAR to avoid decompression overhead and click delays -asar: false - -mac: - category: public.app-category.productivity - icon: resources/brands/qwen-code/icon.icns - # macOS 26+ Liquid Glass: Tell macOS to look for icon in Assets.car - # The value must match --app-icon used in actool (see afterPack.js) - extendInfo: - CFBundleIconName: AppIcon - # Voice dictation: shown in the macOS microphone permission prompt. - NSMicrophoneUsageDescription: Qwen Code uses the microphone for voice dictation in the prompt composer. - target: - - target: dmg - arch: - - arm64 - - x64 - - target: zip - arch: - - arm64 - - x64 - hardenedRuntime: true - gatekeeperAssess: false - entitlements: build/entitlements.mac.plist - entitlementsInherit: build/entitlements.mac.plist - extraResources: - # WhatsApp worker subprocess (self-contained; Baileys bundled in). - - from: ../../packages/messaging-whatsapp-worker/dist/worker.cjs - to: messaging-whatsapp-worker/worker.cjs - # Exclude binaries for other platforms - files: - - "!**/vendor/codex/linux-*/**" - - "!**/vendor/codex/win32-*/**" - - "!**/resources/bin/win32-*/**" - - "!**/resources/bin/linux-*/**" - # Use predictable naming for macOS packages (applies to zip; dmg uses its own config below) - artifactName: "Qwen-Code-Desktop-${arch}.${ext}" - # Code signing & notarization (disabled by default for local builds) - # To enable: set CSC_LINK, APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID - # notarize: - # teamId: ${APPLE_TEAM_ID} - -dmg: - # Use predictable naming: Qwen-Code-Desktop-arm64.dmg, Qwen-Code-Desktop-x64.dmg - artifactName: "Qwen-Code-Desktop-${arch}.dmg" - # Custom background (multi-resolution TIFF with 1x+2x for retina support) - background: resources/dmg-background.tiff - # Use app icon as the mounted volume icon in Finder - icon: resources/brands/qwen-code/icon.icns - iconSize: 80 - title: "Qwen Code Desktop" - contents: - - x: 130 - y: 200 - - x: 410 - y: 200 - type: link - path: /Applications - window: - width: 540 - height: 380 - -win: - icon: resources/brands/qwen-code/icon.ico - # Windows releases are currently unsigned, so keep this update channel unsigned - # from the first release. Re-enable only with a planned signed-channel migration. - verifyUpdateCodeSignature: false - target: - - target: nsis - arch: - - x64 - # Use predictable naming: Qwen-Code-Desktop-x64.exe - artifactName: "Qwen-Code-Desktop-${arch}.${ext}" - files: - # Exclude binaries for other platforms - - "!**/vendor/codex/darwin-*/**" - - "!**/vendor/codex/linux-*/**" - - "!**/resources/bin/darwin-*/**" - - "!**/resources/bin/linux-*/**" - # WORKAROUND: Exclude bun, codex, uv, and Qwen Code from regular files on Windows. - # electron-builder's npm node module collector causes EBUSY errors when copying - # files because it scans/locks files while simultaneously trying to copy them. - # Moving them to extraResources avoids this by copying before the collector runs. - # See: https://github.com/electron-userland/electron-builder/issues/8250 - - "!vendor/bun/**/*" - - "!vendor/codex/**/*" - - "!vendor/qwen-code/**/*" - - "!**/resources/bin/win32-x64/**" - # Copy executables as extraResources to avoid EBUSY file locking (see comment above) - extraResources: - - from: vendor/bun/bun.exe - to: vendor/bun/bun.exe - - from: vendor/codex/win32-x64 - to: app/vendor/codex/win32-x64 - - from: vendor/qwen-code - to: app/vendor/qwen-code - - from: resources/bin/win32-x64 - to: app/resources/bin/win32-x64 - # WhatsApp worker subprocess (self-contained; Baileys bundled in). - - from: ../../packages/messaging-whatsapp-worker/dist/worker.cjs - to: messaging-whatsapp-worker/worker.cjs - -nsis: - oneClick: true - # Per-user install to %LOCALAPPDATA%\Programs\ (not Program Files). - # Bun subprocess cannot read/write files in Program Files due to Windows permissions. - perMachine: false - deleteAppDataOnUninstall: true - -linux: - icon: resources/brands/qwen-code/icon.png - category: Utility - maintainer: "Alibaba Group" - target: - - target: AppImage - arch: - - x64 - artifactName: "Qwen-Code-Desktop-${arch}.${ext}" - extraResources: - # WhatsApp worker subprocess (self-contained; Baileys bundled in). - - from: ../../packages/messaging-whatsapp-worker/dist/worker.cjs - to: messaging-whatsapp-worker/worker.cjs - # Exclude binaries for other platforms - files: - - "!**/vendor/codex/darwin-*/**" - - "!**/vendor/codex/win32-*/**" - - "!**/resources/bin/darwin-*/**" - - "!**/resources/bin/win32-*/**" diff --git a/packages/desktop/apps/electron/eslint-rules/__tests__/no-hardcoded-z-index.test.ts b/packages/desktop/apps/electron/eslint-rules/__tests__/no-hardcoded-z-index.test.ts deleted file mode 100644 index b5288159067..00000000000 --- a/packages/desktop/apps/electron/eslint-rules/__tests__/no-hardcoded-z-index.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, expect, it } from 'bun:test' -import { Linter } from 'eslint' -import { createRequire } from 'node:module' - -const require = createRequire(import.meta.url) -const rule = require('../no-hardcoded-z-index.cjs') - -function runRule(code: string) { - const linter = new Linter({ configType: 'eslintrc' }) - linter.defineRule('craft-styles/no-hardcoded-z-index', rule) - - return linter.verify(code, { - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - }, - rules: { - 'craft-styles/no-hardcoded-z-index': 'error', - }, - }) -} - -describe('no-hardcoded-z-index (electron)', () => { - it('flags hardcoded default in destructured props', () => { - const messages = runRule('function Comp({ zIndex = 50 }) { return zIndex }') - expect(messages.length).toBe(1) - expect(messages[0]?.message).toContain('Avoid hardcoded zIndex values') - }) - - it('flags hardcoded default in object destructuring', () => { - const messages = runRule('const { zIndex = 50 } = props') - expect(messages.length).toBe(1) - }) - - it('allows token-based zIndex default values', () => { - const messages = runRule("const { zIndex = 'var(--z-floating-menu, 400)' } = props") - expect(messages.length).toBe(0) - }) - - it('continues to flag hardcoded style zIndex literals', () => { - const messages = runRule('const style = { zIndex: 400 }') - expect(messages.length).toBe(1) - }) -}) diff --git a/packages/desktop/apps/electron/eslint-rules/no-direct-file-open.cjs b/packages/desktop/apps/electron/eslint-rules/no-direct-file-open.cjs deleted file mode 100644 index f820e54d0de..00000000000 --- a/packages/desktop/apps/electron/eslint-rules/no-direct-file-open.cjs +++ /dev/null @@ -1,65 +0,0 @@ -/** - * ESLint Rule: no-direct-file-open - * - * Prevents calling window.electronAPI.openFile() directly in renderer code. - * All file-open calls should go through the link interceptor (via AppShellContext - * or PlatformContext's onOpenFile) so the app can show in-app previews for - * supported file types instead of always opening in the default external app. - * - * Allowed in: - * - App.tsx (link interceptor implementation) - * - useLinkInterceptor.ts (link interceptor fallback) - * - * Bad: - * window.electronAPI.openFile(path) - * - * Good: - * const { onOpenFile } = useAppShellContext() - * onOpenFile(path) - */ - -/** @type {import('eslint').Rule.RuleModule} */ -module.exports = { - meta: { - type: 'problem', - docs: { - description: 'Disallow direct window.electronAPI.openFile() calls. Use onOpenFile from context instead.', - category: 'Best Practices', - recommended: true, - }, - messages: { - noDirectFileOpen: - 'Use onOpenFile from AppShellContext or PlatformContext instead of calling window.electronAPI.openFile() directly. This ensures the link interceptor can show in-app previews for supported file types.', - }, - schema: [], - }, - - create(context) { - // Allow direct calls in the interceptor implementation files - const filename = context.filename || context.getFilename() - const basename = filename.split('/').pop() || '' - if (basename === 'App.tsx' || basename === 'useLinkInterceptor.ts') { - return {} - } - - return { - // Match: window.electronAPI.openFile(...) - CallExpression(node) { - const callee = node.callee - if ( - callee.type === 'MemberExpression' && - callee.property.type === 'Identifier' && - callee.property.name === 'openFile' && - callee.object.type === 'MemberExpression' && - callee.object.property.type === 'Identifier' && - callee.object.property.name === 'electronAPI' - ) { - context.report({ - node, - messageId: 'noDirectFileOpen', - }) - } - }, - } - }, -} diff --git a/packages/desktop/apps/electron/eslint-rules/no-direct-navigation-state.cjs b/packages/desktop/apps/electron/eslint-rules/no-direct-navigation-state.cjs deleted file mode 100644 index f125914847d..00000000000 --- a/packages/desktop/apps/electron/eslint-rules/no-direct-navigation-state.cjs +++ /dev/null @@ -1,103 +0,0 @@ -/** - * ESLint Rule: no-direct-navigation-state - * - * Prevents direct calls to navigation state setters outside of the - * SIDEBAR_NAVIGATE_EVENT listener in AppShell.tsx. All navigation should - * go through navigate(routes.xxx()) to ensure: - * - * 1. URL/deep link consistency - * 2. History tracking for back/forward - * 3. Auto-selection of first item in new views - * - * Bad (in click handlers): - * setSidebarMode({ type: 'sources' }) - * - * Good: - * navigate(routes.view.sources()) - * navigate(routes.view.agent(agentId)) - * - * Note: This rule only checks AppShell.tsx where the navigation state is defined. - * The setSidebarMode function is not exported, so other files can't use it anyway. - */ - -/** - * Get ancestor nodes for a given node in ESLint 9+ - * ESLint 9 removed context.getAncestors(), so we use sourceCode.getAncestors() - */ -function getAncestors(context, node) { - const sourceCode = context.sourceCode || context.getSourceCode() - if (sourceCode.getAncestors) { - return sourceCode.getAncestors(node) - } - // Fallback for older versions - if (context.getAncestors) { - return context.getAncestors() - } - return [] -} - -/** @type {import('eslint').Rule.RuleModule} */ -module.exports = { - meta: { - type: 'problem', - docs: { - description: 'Disallow direct calls to setSidebarMode outside the navigation event handler.', - category: 'Best Practices', - recommended: true, - }, - messages: { - noDirectCall: - "Do not call 'setSidebarMode()' directly. Use navigate(routes.xxx()) instead to ensure URL consistency, history tracking, and auto-selection. Only the handleSidebarNavigate event listener should call setSidebarMode. See the comment at line ~595 in AppShell.tsx.", - }, - schema: [], - }, - - create(context) { - // Get the filename - only check AppShell.tsx - const filename = context.filename || context.getFilename() - const isAppShell = filename.includes('AppShell.tsx') - - // Only apply this rule to AppShell.tsx - if (!isAppShell) { - return {} - } - - return { - CallExpression(node) { - // Only check setSidebarMode calls - if ( - node.callee.type === 'Identifier' && - node.callee.name === 'setSidebarMode' - ) { - const ancestors = getAncestors(context, node) - - // Allow if inside handleSidebarNavigate - for (const ancestor of ancestors) { - // Check for: const handleSidebarNavigate = useCallback(...) - if ( - ancestor.type === 'VariableDeclarator' && - ancestor.id.type === 'Identifier' && - ancestor.id.name === 'handleSidebarNavigate' - ) { - return // Allowed - inside the event handler - } - // Check for: function handleSidebarNavigate(...) - if ( - ancestor.type === 'FunctionDeclaration' && - ancestor.id && - ancestor.id.name === 'handleSidebarNavigate' - ) { - return // Allowed - } - } - - // Not inside allowed context - report error - context.report({ - node, - messageId: 'noDirectCall', - }) - } - }, - } - }, -} diff --git a/packages/desktop/apps/electron/eslint-rules/no-direct-platform-check.cjs b/packages/desktop/apps/electron/eslint-rules/no-direct-platform-check.cjs deleted file mode 100644 index e7434aef2a2..00000000000 --- a/packages/desktop/apps/electron/eslint-rules/no-direct-platform-check.cjs +++ /dev/null @@ -1,59 +0,0 @@ -/** - * ESLint Rule: no-direct-platform-check - * - * Prevents direct access to navigator.platform. Use the platform utilities instead: - * - * import { isMac, isWindows, isLinux, PATH_SEP } from '@/lib/platform' - * - * This ensures consistent platform detection across the codebase and proper - * handling of path separators on different operating systems. - * - * Bad: - * navigator.platform.toLowerCase().includes('mac') - * navigator.platform.toUpperCase().indexOf('MAC') >= 0 - * - * Good: - * import { isMac } from '@/lib/platform' - * if (isMac) { ... } - */ - -/** @type {import('eslint').Rule.RuleModule} */ -module.exports = { - meta: { - type: 'problem', - docs: { - description: 'Disallow direct access to navigator.platform', - category: 'Best Practices', - recommended: true, - }, - messages: { - noDirectAccess: - "Don't access 'navigator.platform' directly. Import from '@/lib/platform' instead: { isMac, isWindows, isLinux, PATH_SEP }", - }, - schema: [], - }, - - create(context) { - return { - MemberExpression(node) { - if ( - node.object.type === 'Identifier' && - node.object.name === 'navigator' && - node.property.type === 'Identifier' && - node.property.name === 'platform' - ) { - // Allow in platform.ts itself (the source of truth) - const filename = context.filename || context.getFilename() - if (filename.includes('platform.ts')) { - return - } - - context.report({ - node, - messageId: 'noDirectAccess', - }) - } - }, - } - }, -} diff --git a/packages/desktop/apps/electron/eslint-rules/no-hardcoded-path-separator.cjs b/packages/desktop/apps/electron/eslint-rules/no-hardcoded-path-separator.cjs deleted file mode 100644 index 43a17c39b77..00000000000 --- a/packages/desktop/apps/electron/eslint-rules/no-hardcoded-path-separator.cjs +++ /dev/null @@ -1,65 +0,0 @@ -/** - * ESLint Rule: no-hardcoded-path-separator - * - * Prevents hardcoded path separators in path comparison operations. - * This catches patterns like `path.startsWith(dir + '/')` which fail on Windows. - * - * Bad: - * filePath.startsWith(dir + '/') - * path.startsWith(prefix + '/') - * - * Good: - * import { pathStartsWith } from '@craft-agent/core/utils' - * pathStartsWith(filePath, dir) - * - * Or in Node.js main process: - * import { sep } from 'path' - * filePath.startsWith(dir + sep) - */ - -/** @type {import('eslint').Rule.RuleModule} */ -module.exports = { - meta: { - type: 'problem', - docs: { - description: 'Disallow hardcoded path separators in path operations', - category: 'Cross-Platform', - recommended: true, - }, - messages: { - hardcodedSeparator: - "Avoid hardcoded '/' or '\\\\' in path operations - this breaks on Windows/Unix. " + - "Use pathStartsWith() from @craft-agent/core/utils, or path.sep in Node.js code.", - }, - schema: [], - }, - - create(context) { - return { - BinaryExpression(node) { - // Detect: someVar + '/' or someVar + '\\' - if ( - node.operator === '+' && - node.right.type === 'Literal' && - (node.right.value === '/' || node.right.value === '\\') - ) { - // Check if parent is a path operation (startsWith, endsWith, includes) - const parent = node.parent - if (parent?.type === 'CallExpression') { - const callee = parent.callee - if ( - callee?.type === 'MemberExpression' && - callee.property?.type === 'Identifier' && - ['startsWith', 'endsWith', 'includes'].includes(callee.property.name) - ) { - context.report({ - node, - messageId: 'hardcodedSeparator', - }) - } - } - } - }, - } - }, -} diff --git a/packages/desktop/apps/electron/eslint-rules/no-hardcoded-z-index.cjs b/packages/desktop/apps/electron/eslint-rules/no-hardcoded-z-index.cjs deleted file mode 100644 index eaeb813dd27..00000000000 --- a/packages/desktop/apps/electron/eslint-rules/no-hardcoded-z-index.cjs +++ /dev/null @@ -1,133 +0,0 @@ -/** - * ESLint Rule: no-hardcoded-z-index - * - * Enforces the centralized z-index token system by blocking hardcoded - * literal values in JS/TS style objects and style assignments. - * - * Allowed examples: - * style={{ zIndex: 'var(--z-floating-menu, 400)' }} - * style={{ zIndex: 'calc(var(--z-floating-menu, 400) + 1)' }} - * style={{ zIndex: Z_FULLSCREEN }} - * style={{ zIndex: index + 1 }} - * - * Disallowed examples: - * style={{ zIndex: 400 }} - * style={{ zIndex: '400' }} - * el.style.zIndex = 9999 - */ - -/** @type {import('eslint').Rule.RuleModule} */ -module.exports = { - meta: { - type: 'suggestion', - docs: { - description: 'Disallow hardcoded zIndex literals. Use centralized z-index tokens/constants.', - category: 'Best Practices', - recommended: true, - }, - schema: [], - messages: { - noHardcodedZIndex: - 'Avoid hardcoded zIndex values. Use z-index tokens (for example var(--z-floating-menu, 400)), Tailwind z-* utilities, or a named constant.', - }, - }, - - create(context) { - function isZIndexPropertyName(node) { - if (!node) return false - if (node.type === 'Identifier') return node.name === 'zIndex' - if (node.type === 'Literal') return node.value === 'zIndex' - return false - } - - function getStaticTemplateValue(node) { - if (node.type !== 'TemplateLiteral') return null - if (node.expressions.length > 0) return null - return node.quasis.map((q) => q.value.cooked ?? '').join('') - } - - function isAllowedZIndexString(value) { - const normalized = value.trim().toLowerCase() - - // CSS variable/token usage from centralized z-scale - if (normalized.includes('var(--z-')) return true - - // Common CSS keywords - if ( - normalized === 'auto' || - normalized === 'inherit' || - normalized === 'initial' || - normalized === 'unset' || - normalized === 'revert' || - normalized === 'revert-layer' - ) { - return true - } - - return false - } - - function isHardcodedLiteralValue(node) { - if (!node) return false - - if (node.type === 'Literal') { - if (typeof node.value === 'number') return true - if (typeof node.value === 'string') return !isAllowedZIndexString(node.value) - return false - } - - if (node.type === 'TemplateLiteral') { - const staticValue = getStaticTemplateValue(node) - if (staticValue == null) return false - return !isAllowedZIndexString(staticValue) - } - - return false - } - - function isStyleZIndexMemberExpression(node) { - // Matches: something.style.zIndex - return ( - node && - node.type === 'MemberExpression' && - !node.computed && - node.property && - node.property.type === 'Identifier' && - node.property.name === 'zIndex' && - node.object && - node.object.type === 'MemberExpression' && - !node.object.computed && - node.object.property && - node.object.property.type === 'Identifier' && - node.object.property.name === 'style' - ) - } - - function isZIndexIdentifier(node) { - return node && node.type === 'Identifier' && node.name === 'zIndex' - } - - return { - Property(node) { - if (!isZIndexPropertyName(node.key)) return - if (isHardcodedLiteralValue(node.value)) { - context.report({ node: node.value, messageId: 'noHardcodedZIndex' }) - } - }, - - AssignmentPattern(node) { - if (!isZIndexIdentifier(node.left)) return - if (isHardcodedLiteralValue(node.right)) { - context.report({ node: node.right, messageId: 'noHardcodedZIndex' }) - } - }, - - AssignmentExpression(node) { - if (!isStyleZIndexMemberExpression(node.left)) return - if (isHardcodedLiteralValue(node.right)) { - context.report({ node: node.right, messageId: 'noHardcodedZIndex' }) - } - }, - } - }, -} diff --git a/packages/desktop/apps/electron/eslint-rules/no-inline-source-auth-check.cjs b/packages/desktop/apps/electron/eslint-rules/no-inline-source-auth-check.cjs deleted file mode 100644 index a8b80474a84..00000000000 --- a/packages/desktop/apps/electron/eslint-rules/no-inline-source-auth-check.cjs +++ /dev/null @@ -1,85 +0,0 @@ -/** - * ESLint Rule: no-inline-source-auth-check - * - * Prevents inline checks of source.config.isAuthenticated. - * Use the centralized isSourceUsable() helper instead. - * - * The isSourceUsable() helper correctly handles: - * - Sources with authType: 'none' (no auth required) - * - Sources with undefined authType (no auth required) - * - Sources with OAuth/Bearer auth (requires isAuthenticated) - * - * Inline checks often miss the authType: 'none' case, causing bugs where - * no-auth sources are incorrectly filtered out. - * - * Allowed in: - * - storage.ts (where isSourceUsable is defined) - * - credential-manager.ts (state-setting operations, inverse check) - * - server-builder.ts (documented exceptions for OAuth providers) - * - * Bad: - * source.config.isAuthenticated - * s.config.enabled && s.config.isAuthenticated - * - * Good: - * isSourceUsable(source) - */ - -/** @type {import('eslint').Rule.RuleModule} */ -module.exports = { - meta: { - type: 'suggestion', - docs: { - description: - 'Disallow inline source.config.isAuthenticated checks. Use isSourceUsable() from storage.ts instead.', - category: 'Best Practices', - recommended: true, - }, - messages: { - useIsSourceUsable: - 'Do not check source.config.isAuthenticated directly. Use isSourceUsable() from sources/storage.ts instead. ' + - 'Direct checks often miss sources with authType: "none" which should be considered authenticated.', - }, - schema: [], - }, - - create(context) { - // Files where direct isAuthenticated access is allowed - const allowedFiles = [ - 'storage.ts', // isSourceUsable is defined here - 'credential-manager.ts', // State-setting and inverse check - 'server-builder.ts', // OAuth provider checks (documented) - ] - - const filename = context.filename || context.getFilename() - const basename = filename.split('/').pop() || '' - - // Allow in specific files - if (allowedFiles.includes(basename)) { - return {} - } - - return { - // Match: .config.isAuthenticated access - MemberExpression(node) { - // Check if property is 'isAuthenticated' - if ( - node.property.type === 'Identifier' && - node.property.name === 'isAuthenticated' - ) { - // Check if accessed via .config.isAuthenticated pattern - if ( - node.object.type === 'MemberExpression' && - node.object.property.type === 'Identifier' && - node.object.property.name === 'config' - ) { - context.report({ - node, - messageId: 'useIsSourceUsable', - }) - } - } - }, - } - }, -} diff --git a/packages/desktop/apps/electron/eslint-rules/no-localstorage.cjs b/packages/desktop/apps/electron/eslint-rules/no-localstorage.cjs deleted file mode 100644 index c975c49119f..00000000000 --- a/packages/desktop/apps/electron/eslint-rules/no-localstorage.cjs +++ /dev/null @@ -1,101 +0,0 @@ -/** - * ESLint Rule: no-localstorage - * - * Warns against using localStorage in Qwen Code codebase. - * All persistent user settings should be stored in file-based configs - * (preferences.json, workspace configs) for consistency with Qwen Code - * architecture principles. - * - * Bad: - * localStorage.getItem('key') - * localStorage.setItem('key', 'value') - * window.localStorage.getItem('key') - * - * Good: - * // Use IPC to read/write preferences - * window.electronAPI.readPreferences() - * window.electronAPI.writePreferences(content) - * - * Why: File-based configs are: - * - Portable (sync via cloud services) - * - Editable (users can manually modify) - * - Consistent (all settings in one place) - * - Inspectable (easy debugging) - */ - -/** @type {import('eslint').Rule.RuleModule} */ -module.exports = { - meta: { - type: 'suggestion', - docs: { - description: 'Disallow localStorage usage. Use file-based preferences instead.', - category: 'Best Practices', - recommended: true, - }, - messages: { - noLocalStorage: - "Avoid localStorage in Qwen Code. Store settings in ~/.craft-agent/preferences.json using window.electronAPI.readPreferences/writePreferences. See packages/shared/src/config/preferences.ts for the preferences API.", - }, - schema: [], - }, - - create(context) { - /** - * Check if a node is a localStorage access (localStorage or window.localStorage) - */ - function isLocalStorageAccess(node) { - // Direct: localStorage.getItem - if (node.type === 'Identifier' && node.name === 'localStorage') { - return true - } - - // window.localStorage - if ( - node.type === 'MemberExpression' && - node.object.type === 'Identifier' && - node.object.name === 'window' && - node.property.type === 'Identifier' && - node.property.name === 'localStorage' - ) { - return true - } - - return false - } - - return { - // Catch localStorage.getItem(), localStorage.setItem(), etc. - MemberExpression(node) { - if (isLocalStorageAccess(node.object)) { - context.report({ - node, - messageId: 'noLocalStorage', - }) - } - }, - - // Catch direct localStorage references (e.g., passing it as argument) - Identifier(node) { - if (node.name === 'localStorage') { - // Only report if it's being used (not just referenced in a type) - const parent = node.parent - if ( - parent && - parent.type === 'MemberExpression' && - parent.object === node - ) { - // Already handled by MemberExpression rule - return - } - // Report standalone localStorage references - if (parent && parent.type !== 'TSTypeReference') { - context.report({ - node, - messageId: 'noLocalStorage', - }) - } - } - }, - } - }, -} diff --git a/packages/desktop/apps/electron/eslint-rules/no-nonstandard-shadows.cjs b/packages/desktop/apps/electron/eslint-rules/no-nonstandard-shadows.cjs deleted file mode 100644 index 59fa5677aae..00000000000 --- a/packages/desktop/apps/electron/eslint-rules/no-nonstandard-shadows.cjs +++ /dev/null @@ -1,156 +0,0 @@ -/** - * ESLint Rule: no-nonstandard-shadows - * - * Enforces approved shadow usage: - * - Allows only specific shadow-* utility classes - * - Disallows arbitrary shadow classes (shadow-[...]) unless explicitly allowlisted - * - Disallows inline style boxShadow values - * - Disallows direct style assignments (el.style.boxShadow = ...) - */ - -/** @type {import('eslint').Rule.RuleModule} */ -module.exports = { - meta: { - type: 'suggestion', - docs: { - description: 'Allow only approved shadow utilities and block inline boxShadow usage.', - category: 'Best Practices', - recommended: true, - }, - schema: [ - { - type: 'object', - properties: { - allowedClasses: { - type: 'array', - items: { type: 'string' }, - }, - allowInlineNone: { - type: 'boolean', - }, - }, - additionalProperties: false, - }, - ], - messages: { - disallowedClass: - 'Disallowed shadow class "{{className}}". Use approved shadow classes only: {{allowed}}.', - disallowedInline: - 'Avoid inline boxShadow usage. Use approved shadow utility classes (for example shadow-minimal/shadow-modal-small).', - }, - }, - - create(context) { - const options = context.options[0] || {} - const allowedClasses = new Set( - options.allowedClasses || [ - 'shadow-none', - 'shadow-minimal', - 'shadow-tinted', - 'shadow-thin', - 'shadow-middle', - 'shadow-strong', - 'shadow-panel-focused', - 'shadow-modal-small', - 'shadow-bottom-border', - 'shadow-bottom-border-thin', - ] - ) - - const allowInlineNone = options.allowInlineNone !== false - const allowedSummary = Array.from(allowedClasses).sort().join(', ') - - function reportDisallowedClass(node, className) { - context.report({ - node, - messageId: 'disallowedClass', - data: { - className, - allowed: allowedSummary, - }, - }) - } - - function checkStringForShadowTokens(node, text) { - if (!text || !text.includes('shadow-')) return - - const regex = /shadow-[^\s'"`]+/g - let match - - while ((match = regex.exec(text)) !== null) { - const token = match[0] - const start = match.index - const prev = start > 0 ? text[start - 1] : '' - - // Ignore CSS custom property names like --shadow-color, --shadow-minimal-flat - if (prev === '-') continue - - if (token.startsWith('shadow-[') && !allowedClasses.has(token)) { - reportDisallowedClass(node, token) - continue - } - if (!allowedClasses.has(token)) { - reportDisallowedClass(node, token) - } - } - } - - function isBoxShadowPropertyKey(node) { - if (!node) return false - if (node.type === 'Identifier') return node.name === 'boxShadow' - if (node.type === 'Literal') return node.value === 'boxShadow' - return false - } - - function isStyleBoxShadowAssignment(node) { - return ( - node && - node.type === 'MemberExpression' && - !node.computed && - node.property && - node.property.type === 'Identifier' && - node.property.name === 'boxShadow' && - node.object && - node.object.type === 'MemberExpression' && - !node.object.computed && - node.object.property && - node.object.property.type === 'Identifier' && - node.object.property.name === 'style' - ) - } - - function isNoneLiteral(node) { - return ( - node && - node.type === 'Literal' && - typeof node.value === 'string' && - node.value.trim().toLowerCase() === 'none' - ) - } - - return { - Literal(node) { - if (typeof node.value !== 'string') return - checkStringForShadowTokens(node, node.value) - }, - - TemplateLiteral(node) { - if (node.expressions.length > 0) return - const text = node.quasis.map((q) => q.value.cooked ?? '').join('') - checkStringForShadowTokens(node, text) - }, - - Property(node) { - if (!isBoxShadowPropertyKey(node.key)) return - if (allowInlineNone && isNoneLiteral(node.value)) return - context.report({ node: node.value, messageId: 'disallowedInline' }) - }, - - AssignmentExpression(node) { - if (!isStyleBoxShadowAssignment(node.left)) return - if (allowInlineNone && isNoneLiteral(node.right)) return - context.report({ node: node.right, messageId: 'disallowedInline' }) - }, - } - }, -} diff --git a/packages/desktop/apps/electron/eslint.config.mjs b/packages/desktop/apps/electron/eslint.config.mjs deleted file mode 100644 index c9a3819bff3..00000000000 --- a/packages/desktop/apps/electron/eslint.config.mjs +++ /dev/null @@ -1,186 +0,0 @@ -/** - * ESLint Configuration for Electron App - * - * Uses flat config format (ESLint 9+). - * Includes custom navigation rule to enforce navigate() usage. - */ - -import tsParser from '@typescript-eslint/parser' -import tsPlugin from '@typescript-eslint/eslint-plugin' -import reactPlugin from 'eslint-plugin-react' -import reactHooksPlugin from 'eslint-plugin-react-hooks' -import noDirectNavigationState from './eslint-rules/no-direct-navigation-state.cjs' -import noLocalStorage from './eslint-rules/no-localstorage.cjs' -import noDirectPlatformCheck from './eslint-rules/no-direct-platform-check.cjs' -import noHardcodedPathSeparator from './eslint-rules/no-hardcoded-path-separator.cjs' -import noDirectFileOpen from './eslint-rules/no-direct-file-open.cjs' -import noInlineSourceAuthCheck from './eslint-rules/no-inline-source-auth-check.cjs' -import noHardcodedZIndex from './eslint-rules/no-hardcoded-z-index.cjs' -import noNonstandardShadows from './eslint-rules/no-nonstandard-shadows.cjs' - -export default [ - // Ignore patterns - { - ignores: [ - 'dist/**', - 'node_modules/**', - 'release/**', - '*.cjs', - 'eslint-rules/**', - ], - }, - - // TypeScript/React files - { - files: ['src/**/*.{ts,tsx}'], - languageOptions: { - parser: tsParser, - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - ecmaFeatures: { - jsx: true, - }, - }, - }, - plugins: { - '@typescript-eslint': tsPlugin, - react: reactPlugin, - 'react-hooks': reactHooksPlugin, - // Custom plugin for Qwen Code rules - 'craft-agent': { - rules: { - 'no-direct-navigation-state': noDirectNavigationState, - 'no-localstorage': noLocalStorage, - }, - }, - // Custom plugin for platform detection rules - 'craft-platform': { - rules: { - 'no-direct-platform-check': noDirectPlatformCheck, - }, - }, - // Custom plugin for cross-platform path rules - 'craft-paths': { - rules: { - 'no-hardcoded-path-separator': noHardcodedPathSeparator, - }, - }, - // Custom plugin for link interceptor enforcement - 'craft-links': { - rules: { - 'no-direct-file-open': noDirectFileOpen, - }, - }, - // Custom plugin for source auth checks (shared with packages/shared) - 'craft-sources': { - rules: { - 'no-inline-source-auth-check': noInlineSourceAuthCheck, - }, - }, - // Custom style rules - 'craft-styles': { - rules: { - 'no-hardcoded-z-index': noHardcodedZIndex, - 'no-nonstandard-shadows': noNonstandardShadows, - }, - }, - }, - settings: { - react: { - version: 'detect', - }, - }, - rules: { - // React Hooks rules - 'react-hooks/rules-of-hooks': 'error', - 'react-hooks/exhaustive-deps': 'warn', - - // Custom Qwen Code rules - 'craft-agent/no-direct-navigation-state': 'error', - 'craft-agent/no-localstorage': 'warn', - - // Custom platform detection rule - 'craft-platform/no-direct-platform-check': 'error', - - // Custom cross-platform path rule - 'craft-paths/no-hardcoded-path-separator': 'warn', - - // Custom link interceptor rule — prevents bypassing in-app file preview - 'craft-links/no-direct-file-open': 'error', - - // Custom source auth check rule — use isSourceUsable() instead of inline checks - 'craft-sources/no-inline-source-auth-check': 'error', - - // Custom style rule — use z-index token scale instead of hardcoded literals - 'craft-styles/no-hardcoded-z-index': 'error', - - // Custom style rule — enforce approved shadow classes/tokens only - 'craft-styles/no-nonstandard-shadows': ['error', { - allowedClasses: [ - 'shadow-none', - 'shadow-xs', - 'shadow-minimal', - 'shadow-tinted', - 'shadow-thin', - 'shadow-middle', - 'shadow-strong', - 'shadow-panel-focused', - 'shadow-modal-small', - 'shadow-bottom-border', - 'shadow-bottom-border-thin', - ], - allowInlineNone: true, - }], - - // Enforce centralized action registry for keyboard shortcuts - 'no-restricted-imports': ['error', { - paths: [ - { - name: 'react-hotkeys-hook', - message: 'Use useAction from @/actions instead. See actions/index.ts' - } - ], - }], - }, - }, - - // Temporary exceptions for unresolved shadow migrations. - { - files: [ - 'src/renderer/components/ui/sortable-list.tsx', - 'src/main/browser-pane-manager.ts', - 'src/shared/browser-live-fx.ts', - 'src/renderer/components/KeyboardShortcutsDialog.tsx', - 'src/renderer/playground/**/*.{ts,tsx}', - ], - rules: { - 'craft-styles/no-nonstandard-shadows': 'off', - }, - }, - - // Enforce backend abstraction boundary in Electron main process. - { - files: ['src/main/**/*.{ts,tsx}'], - rules: { - 'no-restricted-imports': ['error', { - paths: [ - { name: '@craft-agent/shared/agent/qwen-agent', message: 'Use backend factory APIs from @craft-agent/shared/agent/backend instead.' }, - ], - }], - }, - }, - - // Keep main model fetchers provider-agnostic (delegate to shared backend APIs only). - { - files: ['src/main/model-fetchers/**/*.{ts,tsx}'], - rules: { - 'no-restricted-syntax': ['error', - { - selector: "CallExpression[callee.name='fetch']", - message: 'Do not call provider APIs directly in Electron model fetchers. Delegate to fetchBackendModels() from @craft-agent/shared/agent/backend.', - }, - ], - }, - }, -] diff --git a/packages/desktop/apps/electron/package.json b/packages/desktop/apps/electron/package.json deleted file mode 100644 index e15aeced017..00000000000 --- a/packages/desktop/apps/electron/package.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "name": "@craft-agent/electron", - "version": "0.0.5", - "description": "Electron desktop app for Qwen Code", - "main": "dist/main.cjs", - "private": true, - "author": { - "name": "Craft Docs Ltd.", - "email": "support@craft.do" - }, - "homepage": "https://agents.craft.do", - "license": "Apache-2.0", - "type": "module", - "engines": { - "node": ">=18.0.0" - }, - "scripts": { - "build:main": "source ../../.env 2>/dev/null || true; esbuild src/main/index.ts --bundle --platform=node --format=cjs --outfile=dist/main.cjs --external:electron --alias:node-fetch=./src/main/shims/node-fetch.cjs --alias:abort-controller=./src/main/shims/abort-controller.cjs --define:process.env.GOOGLE_OAUTH_CLIENT_ID=\\\"${GOOGLE_OAUTH_CLIENT_ID:-}\\\" --define:process.env.GOOGLE_OAUTH_CLIENT_SECRET=\\\"${GOOGLE_OAUTH_CLIENT_SECRET:-}\\\" --define:process.env.SLACK_OAUTH_CLIENT_ID=\\\"${SLACK_OAUTH_CLIENT_ID:-}\\\" --define:process.env.SLACK_OAUTH_CLIENT_SECRET=\\\"${SLACK_OAUTH_CLIENT_SECRET:-}\\\" --define:process.env.MICROSOFT_OAUTH_CLIENT_ID=\\\"${MICROSOFT_OAUTH_CLIENT_ID:-}\\\"", - "build:main:win": "esbuild src/main/index.ts --bundle --platform=node --format=cjs --outfile=dist/main.cjs --external:electron --alias:node-fetch=./src/main/shims/node-fetch.cjs --alias:abort-controller=./src/main/shims/abort-controller.cjs", - "build:preload": "esbuild src/preload/bootstrap.ts --bundle --platform=node --format=cjs --outfile=dist/bootstrap-preload.cjs --external:electron", - "build:preload-toolbar": "esbuild src/preload/browser-toolbar.ts --bundle --platform=node --format=cjs --outfile=dist/browser-toolbar-preload.cjs --external:electron", - "build:renderer": "vite build", - "build:copy": "bun scripts/copy-assets.ts", - "build:validate": "bun scripts/validate-assets.ts", - "build": "bun run lint && bun run build:main && bun run build:preload && bun run build:preload-toolbar && bun run build:renderer && bun run build:copy && bun run build:validate", - "build:win": "bun run build:main:win && bun run build:preload && bun run build:preload-toolbar && bun run build:renderer && bun run build:copy && bun run build:validate", - "start": "bun run build && electron .", - "start:win": "bun run build:win && electron .", - "dev": "vite dev", - "typecheck": "tsc --noEmit", - "dist:mac": "bash scripts/build-dmg.sh arm64", - "dist:mac:x64": "bash scripts/build-dmg.sh x64", - "dist:win": "powershell -ExecutionPolicy Bypass -File scripts/build-win.ps1", - "lint": "eslint src/", - "lint:fix": "eslint src/ --fix" - }, - "dependencies": { - "@craft-agent/core": "workspace:*", - "@craft-agent/messaging-gateway": "workspace:*", - "@craft-agent/server-core": "workspace:*", - "@craft-agent/shared": "workspace:*", - "@craft-agent/ui": "workspace:*", - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^10.0.0", - "@dnd-kit/utilities": "^3.2.2", - "@paper-design/shaders-react": "^0.0.69", - "@pierre/diffs": "^1.0.4", - "@radix-ui/react-context-menu": "^2.2.16", - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-dropdown-menu": "^2.1.16", - "@radix-ui/react-label": "^2.1.8", - "@radix-ui/react-popover": "^1.1.15", - "@radix-ui/react-switch": "^1.2.6", - "@tanstack/react-table": "^8.21.3", - "chrono-node": "^2.9.0", - "cmdk": "^1.1.1", - "electron-log": "^5.4.3", - "electron-updater": "^6.8.0", - "i18next-browser-languagedetector": "^8.2.1", - "jotai-family": "^1.0.1", - "motion": "^12.23.26", - "next-themes": "^0.4.6", - "qrcode.react": "^4.2.0", - "react": "^18.3.1", - "react-day-picker": "^9.13.0", - "react-dom": "^18.3.1", - "react-i18next": "^17.0.2", - "react-pdf": "^10.3.0", - "react-simple-code-editor": "^0.14.1", - "remark": "^15.0.1", - "sharp": "0.34.5", - "sonner": "^2.0.7", - "strip-markdown": "^6.0.0", - "undici": "^6.22.0", - "unist-util-visit": "^5.0.0", - "vaul": "^1.1.2", - "ws": "^8.19.0" - }, - "devDependencies": { - "@types/ws": "^8.18.1" - } -} diff --git a/packages/desktop/apps/electron/resources/AGENTS.md b/packages/desktop/apps/electron/resources/AGENTS.md deleted file mode 100644 index 9ca2ebb2651..00000000000 --- a/packages/desktop/apps/electron/resources/AGENTS.md +++ /dev/null @@ -1,50 +0,0 @@ -# Bundled Resources - -This folder contains assets that are bundled with the Electron app and synced to the user's `~/.craft-agent/` directory on every launch. - -## How It Works - -1. **Build time**: `scripts/copy-assets.ts` copies this folder to `dist/resources/` -2. **Package time**: electron-builder includes `dist/resources/` in the app bundle -3. **Runtime**: `getBundledAssetsDir()` resolves paths to these bundled assets -4. **Launch**: Each asset type syncs to the user's home directory - -## Asset Types - -| Folder/File | Synced To | Sync Behavior | -|-------------|-----------|---------------| -| `docs/` | `~/.craft-agent/docs/` | Always overwrite on launch | -| `themes/` | `~/.craft-agent/themes/` | Always overwrite on launch | -| `permissions/` | `~/.craft-agent/permissions/` | Always overwrite on launch | -| `tool-icons/` | `~/.craft-agent/tool-icons/` | Always overwrite on launch | -| `config-defaults.json` | `~/.craft-agent/config-defaults.json` | Always overwrite on launch | - -## Why Sync on Every Launch? - -- Ensures users always have the latest defaults/docs when the app updates -- Consistent behavior between debug and release builds -- No stale configuration causing confusion - -## Other Files (Not Synced) - -These files are used by electron-builder or the app directly, not synced to user home: - -| File | Purpose | -|------|---------| -| `icon.*` | App icons (icns, ico, png, svg) | -| `Assets.car` | Optional macOS compiled asset catalog | -| `dmg-background.*` | DMG installer background | -| `craft-logos/` | Branding assets | -| `source.png` | Default source icon | -| `generate-icons.sh` | Icon generation script | -| `bridge-mcp-server/` | Bundled MCP server for API source bridge | -| `session-mcp-server/` | Bundled MCP server for session tools | - -## Single Source of Truth - -The files in this folder are the **source of truth** for bundled defaults: -- Edit `config-defaults.json` here to change default settings -- Edit files in `docs/` to update documentation -- Edit files in `themes/` to update bundled themes - -There is no TypeScript fallback - if the bundled JSON file is missing, the app will fail with a clear error. diff --git a/packages/desktop/apps/electron/resources/bin/craft-agent b/packages/desktop/apps/electron/resources/bin/craft-agent deleted file mode 100755 index 245cf8144c0..00000000000 --- a/packages/desktop/apps/electron/resources/bin/craft-agent +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -ENTRY="${CRAFT_COMMANDS_ENTRY:-$CRAFT_CLI_ENTRY}" -export CRAFT_CLI_JSON_ONLY="${CRAFT_CLI_JSON_ONLY:-1}" -exec "${CRAFT_BUN:-bun}" run "$ENTRY" "$@" diff --git a/packages/desktop/apps/electron/resources/bin/craft-agent.cmd b/packages/desktop/apps/electron/resources/bin/craft-agent.cmd deleted file mode 100644 index 16ee6853ce7..00000000000 --- a/packages/desktop/apps/electron/resources/bin/craft-agent.cmd +++ /dev/null @@ -1,7 +0,0 @@ -@echo off -set "CRAFT_BUN_BIN=%CRAFT_BUN%" -if "%CRAFT_BUN_BIN%"=="" set "CRAFT_BUN_BIN=bun" -set "CRAFT_COMMANDS_BIN=%CRAFT_COMMANDS_ENTRY%" -if "%CRAFT_COMMANDS_BIN%"=="" set "CRAFT_COMMANDS_BIN=%CRAFT_CLI_ENTRY%" -if "%CRAFT_CLI_JSON_ONLY%"=="" set "CRAFT_CLI_JSON_ONLY=1" -"%CRAFT_BUN_BIN%" run "%CRAFT_COMMANDS_BIN%" %* diff --git a/packages/desktop/apps/electron/resources/bin/doc-diff b/packages/desktop/apps/electron/resources/bin/doc-diff deleted file mode 100755 index b1859fd0d07..00000000000 --- a/packages/desktop/apps/electron/resources/bin/doc-diff +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -exec "$CRAFT_UV" run --python 3.12 "$CRAFT_SCRIPTS/doc_diff.py" "$@" diff --git a/packages/desktop/apps/electron/resources/bin/doc-diff.cmd b/packages/desktop/apps/electron/resources/bin/doc-diff.cmd deleted file mode 100644 index be7527f5ad3..00000000000 --- a/packages/desktop/apps/electron/resources/bin/doc-diff.cmd +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -"%CRAFT_UV%" run --python 3.12 "%CRAFT_SCRIPTS%\doc_diff.py" %* diff --git a/packages/desktop/apps/electron/resources/bin/docx-tool b/packages/desktop/apps/electron/resources/bin/docx-tool deleted file mode 100755 index 24f66291e92..00000000000 --- a/packages/desktop/apps/electron/resources/bin/docx-tool +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -exec "$CRAFT_UV" run --python 3.12 "$CRAFT_SCRIPTS/docx_tool.py" "$@" diff --git a/packages/desktop/apps/electron/resources/bin/docx-tool.cmd b/packages/desktop/apps/electron/resources/bin/docx-tool.cmd deleted file mode 100644 index 2e52d00384f..00000000000 --- a/packages/desktop/apps/electron/resources/bin/docx-tool.cmd +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -"%CRAFT_UV%" run --python 3.12 "%CRAFT_SCRIPTS%\docx_tool.py" %* diff --git a/packages/desktop/apps/electron/resources/bin/ical-tool b/packages/desktop/apps/electron/resources/bin/ical-tool deleted file mode 100755 index c654c26c401..00000000000 --- a/packages/desktop/apps/electron/resources/bin/ical-tool +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -exec "$CRAFT_UV" run --python 3.12 "$CRAFT_SCRIPTS/ical_tool.py" "$@" diff --git a/packages/desktop/apps/electron/resources/bin/ical-tool.cmd b/packages/desktop/apps/electron/resources/bin/ical-tool.cmd deleted file mode 100644 index 65c9a8f86cc..00000000000 --- a/packages/desktop/apps/electron/resources/bin/ical-tool.cmd +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -"%CRAFT_UV%" run --python 3.12 "%CRAFT_SCRIPTS%\ical_tool.py" %* diff --git a/packages/desktop/apps/electron/resources/bin/img-tool b/packages/desktop/apps/electron/resources/bin/img-tool deleted file mode 100755 index 25e78ca5c74..00000000000 --- a/packages/desktop/apps/electron/resources/bin/img-tool +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -exec "$CRAFT_UV" run --python 3.12 "$CRAFT_SCRIPTS/img_tool.py" "$@" diff --git a/packages/desktop/apps/electron/resources/bin/img-tool.cmd b/packages/desktop/apps/electron/resources/bin/img-tool.cmd deleted file mode 100644 index 07043287165..00000000000 --- a/packages/desktop/apps/electron/resources/bin/img-tool.cmd +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -"%CRAFT_UV%" run --python 3.12 "%CRAFT_SCRIPTS%\img_tool.py" %* diff --git a/packages/desktop/apps/electron/resources/bin/markitdown b/packages/desktop/apps/electron/resources/bin/markitdown deleted file mode 100755 index 77921af795d..00000000000 --- a/packages/desktop/apps/electron/resources/bin/markitdown +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -exec "$CRAFT_UV" run --python 3.12 "$CRAFT_SCRIPTS/markitdown_cli.py" "$@" diff --git a/packages/desktop/apps/electron/resources/bin/markitdown.cmd b/packages/desktop/apps/electron/resources/bin/markitdown.cmd deleted file mode 100644 index 1ff4b1ac79d..00000000000 --- a/packages/desktop/apps/electron/resources/bin/markitdown.cmd +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -"%CRAFT_UV%" run --python 3.12 "%CRAFT_SCRIPTS%\markitdown_cli.py" %* diff --git a/packages/desktop/apps/electron/resources/bin/pdf-tool b/packages/desktop/apps/electron/resources/bin/pdf-tool deleted file mode 100755 index e4c9612a465..00000000000 --- a/packages/desktop/apps/electron/resources/bin/pdf-tool +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -exec "$CRAFT_UV" run --python 3.12 "$CRAFT_SCRIPTS/pdf_tool.py" "$@" diff --git a/packages/desktop/apps/electron/resources/bin/pdf-tool.cmd b/packages/desktop/apps/electron/resources/bin/pdf-tool.cmd deleted file mode 100644 index 709fb7cf537..00000000000 --- a/packages/desktop/apps/electron/resources/bin/pdf-tool.cmd +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -"%CRAFT_UV%" run --python 3.12 "%CRAFT_SCRIPTS%\pdf_tool.py" %* diff --git a/packages/desktop/apps/electron/resources/bin/pptx-tool b/packages/desktop/apps/electron/resources/bin/pptx-tool deleted file mode 100755 index 5abbf94c2b3..00000000000 --- a/packages/desktop/apps/electron/resources/bin/pptx-tool +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -exec "$CRAFT_UV" run --python 3.12 "$CRAFT_SCRIPTS/pptx_tool.py" "$@" diff --git a/packages/desktop/apps/electron/resources/bin/pptx-tool.cmd b/packages/desktop/apps/electron/resources/bin/pptx-tool.cmd deleted file mode 100644 index f2321142370..00000000000 --- a/packages/desktop/apps/electron/resources/bin/pptx-tool.cmd +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -"%CRAFT_UV%" run --python 3.12 "%CRAFT_SCRIPTS%\pptx_tool.py" %* diff --git a/packages/desktop/apps/electron/resources/bin/xlsx-tool b/packages/desktop/apps/electron/resources/bin/xlsx-tool deleted file mode 100755 index d71beb50c54..00000000000 --- a/packages/desktop/apps/electron/resources/bin/xlsx-tool +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -exec "$CRAFT_UV" run --python 3.12 "$CRAFT_SCRIPTS/xlsx_tool.py" "$@" diff --git a/packages/desktop/apps/electron/resources/bin/xlsx-tool.cmd b/packages/desktop/apps/electron/resources/bin/xlsx-tool.cmd deleted file mode 100644 index be7347ef47d..00000000000 --- a/packages/desktop/apps/electron/resources/bin/xlsx-tool.cmd +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -"%CRAFT_UV%" run --python 3.12 "%CRAFT_SCRIPTS%\xlsx_tool.py" %* diff --git a/packages/desktop/apps/electron/resources/brands/openwork/dock.png b/packages/desktop/apps/electron/resources/brands/openwork/dock.png deleted file mode 100644 index 787bb52b0c1..00000000000 Binary files a/packages/desktop/apps/electron/resources/brands/openwork/dock.png and /dev/null differ diff --git a/packages/desktop/apps/electron/resources/brands/openwork/icon.icns b/packages/desktop/apps/electron/resources/brands/openwork/icon.icns deleted file mode 100644 index 9c9808c7811..00000000000 Binary files a/packages/desktop/apps/electron/resources/brands/openwork/icon.icns and /dev/null differ diff --git a/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_128x128.png b/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_128x128.png deleted file mode 100644 index f29faafc843..00000000000 Binary files a/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_128x128.png and /dev/null differ diff --git a/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_128x128@2x.png b/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_128x128@2x.png deleted file mode 100644 index 0ceea896495..00000000000 Binary files a/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_128x128@2x.png and /dev/null differ diff --git a/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_16x16.png b/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_16x16.png deleted file mode 100644 index 7e0e521c0a0..00000000000 Binary files a/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_16x16.png and /dev/null differ diff --git a/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_16x16@2x.png b/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_16x16@2x.png deleted file mode 100644 index 0518c344858..00000000000 Binary files a/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_16x16@2x.png and /dev/null differ diff --git a/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_256x256.png b/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_256x256.png deleted file mode 100644 index 0ceea896495..00000000000 Binary files a/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_256x256.png and /dev/null differ diff --git a/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_256x256@2x.png b/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_256x256@2x.png deleted file mode 100644 index c0fe63baa07..00000000000 Binary files a/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_256x256@2x.png and /dev/null differ diff --git a/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_32x32.png b/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_32x32.png deleted file mode 100644 index 0518c344858..00000000000 Binary files a/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_32x32.png and /dev/null differ diff --git a/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_32x32@2x.png b/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_32x32@2x.png deleted file mode 100644 index 528009d576a..00000000000 Binary files a/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_32x32@2x.png and /dev/null differ diff --git a/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_512x512.png b/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_512x512.png deleted file mode 100644 index c0fe63baa07..00000000000 Binary files a/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_512x512.png and /dev/null differ diff --git a/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_512x512@2x.png b/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_512x512@2x.png deleted file mode 100644 index 14e02ce5379..00000000000 Binary files a/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_512x512@2x.png and /dev/null differ diff --git a/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_64x64.png b/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_64x64.png deleted file mode 100644 index 528009d576a..00000000000 Binary files a/packages/desktop/apps/electron/resources/brands/openwork/icon.iconset/icon_64x64.png and /dev/null differ diff --git a/packages/desktop/apps/electron/resources/brands/openwork/icon.png b/packages/desktop/apps/electron/resources/brands/openwork/icon.png deleted file mode 100644 index c0fe63baa07..00000000000 Binary files a/packages/desktop/apps/electron/resources/brands/openwork/icon.png and /dev/null differ diff --git a/packages/desktop/apps/electron/resources/brands/openwork/symbol.png b/packages/desktop/apps/electron/resources/brands/openwork/symbol.png deleted file mode 100644 index 8f2dad1872c..00000000000 Binary files a/packages/desktop/apps/electron/resources/brands/openwork/symbol.png and /dev/null differ diff --git a/packages/desktop/apps/electron/resources/brands/qwen-code/dock.png b/packages/desktop/apps/electron/resources/brands/qwen-code/dock.png deleted file mode 100644 index 45724f39812..00000000000 Binary files a/packages/desktop/apps/electron/resources/brands/qwen-code/dock.png and /dev/null differ diff --git a/packages/desktop/apps/electron/resources/brands/qwen-code/icon.icns b/packages/desktop/apps/electron/resources/brands/qwen-code/icon.icns deleted file mode 100644 index 4ef481b3611..00000000000 Binary files a/packages/desktop/apps/electron/resources/brands/qwen-code/icon.icns and /dev/null differ diff --git a/packages/desktop/apps/electron/resources/brands/qwen-code/icon.ico b/packages/desktop/apps/electron/resources/brands/qwen-code/icon.ico deleted file mode 100644 index 840fda9577e..00000000000 Binary files a/packages/desktop/apps/electron/resources/brands/qwen-code/icon.ico and /dev/null differ diff --git a/packages/desktop/apps/electron/resources/brands/qwen-code/icon.icon/Assets/icon.svg b/packages/desktop/apps/electron/resources/brands/qwen-code/icon.icon/Assets/icon.svg deleted file mode 100644 index efb2e4f3179..00000000000 --- a/packages/desktop/apps/electron/resources/brands/qwen-code/icon.icon/Assets/icon.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/packages/desktop/apps/electron/resources/brands/qwen-code/icon.icon/icon.json b/packages/desktop/apps/electron/resources/brands/qwen-code/icon.icon/icon.json deleted file mode 100644 index a9d5f95e752..00000000000 --- a/packages/desktop/apps/electron/resources/brands/qwen-code/icon.icon/icon.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "fill" : { - "automatic-gradient" : "srgb:1.00000,1.00000,1.00000,1.00000" - }, - "groups" : [ - { - "layers" : [ - { - "image-name" : "icon.svg", - "name" : "icon", - "position" : { - "scale" : 37, - "translation-in-points" : [ - 0, - 0 - ] - } - } - ], - "shadow" : { - "kind" : "neutral", - "opacity" : 0.5 - }, - "specular" : false, - "translucency" : { - "enabled" : false, - "value" : 0.5 - } - } - ], - "supported-platforms" : { - "circles" : [ - "watchOS" - ], - "squares" : "shared" - } -} \ No newline at end of file diff --git a/packages/desktop/apps/electron/resources/brands/qwen-code/icon.png b/packages/desktop/apps/electron/resources/brands/qwen-code/icon.png deleted file mode 100644 index 4e17a46ce52..00000000000 Binary files a/packages/desktop/apps/electron/resources/brands/qwen-code/icon.png and /dev/null differ diff --git a/packages/desktop/apps/electron/resources/brands/qwen-code/icon.svg b/packages/desktop/apps/electron/resources/brands/qwen-code/icon.svg deleted file mode 100644 index efb2e4f3179..00000000000 --- a/packages/desktop/apps/electron/resources/brands/qwen-code/icon.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/packages/desktop/apps/electron/resources/bridge-mcp-server/index.js b/packages/desktop/apps/electron/resources/bridge-mcp-server/index.js deleted file mode 100755 index 2ee0c7aea72..00000000000 --- a/packages/desktop/apps/electron/resources/bridge-mcp-server/index.js +++ /dev/null @@ -1,18276 +0,0 @@ -#!/usr/bin/env node -var __create = Object.create; -var __getProtoOf = Object.getPrototypeOf; -var __defProp = Object.defineProperty; -var __getOwnPropNames = Object.getOwnPropertyNames; -var __hasOwnProp = Object.prototype.hasOwnProperty; -var __toESM = (mod, isNodeMode, target) => { - target = mod != null ? __create(__getProtoOf(mod)) : {}; - const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target; - for (let key of __getOwnPropNames(mod)) - if (!__hasOwnProp.call(to, key)) - __defProp(to, key, { - get: () => mod[key], - enumerable: true - }); - return to; -}; -var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports); -var __export = (target, all) => { - for (var name in all) - __defProp(target, name, { - get: all[name], - enumerable: true, - configurable: true, - set: (newValue) => all[name] = () => newValue - }); -}; - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/compile/codegen/code.js -var require_code = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.regexpCode = exports.getEsmExportName = exports.getProperty = exports.safeStringify = exports.stringify = exports.strConcat = exports.addCodeArg = exports.str = exports._ = exports.nil = exports._Code = exports.Name = exports.IDENTIFIER = exports._CodeOrName = undefined; - - class _CodeOrName { - } - exports._CodeOrName = _CodeOrName; - exports.IDENTIFIER = /^[a-z$_][a-z$_0-9]*$/i; - - class Name extends _CodeOrName { - constructor(s) { - super(); - if (!exports.IDENTIFIER.test(s)) - throw new Error("CodeGen: name must be a valid identifier"); - this.str = s; - } - toString() { - return this.str; - } - emptyStr() { - return false; - } - get names() { - return { [this.str]: 1 }; - } - } - exports.Name = Name; - - class _Code extends _CodeOrName { - constructor(code) { - super(); - this._items = typeof code === "string" ? [code] : code; - } - toString() { - return this.str; - } - emptyStr() { - if (this._items.length > 1) - return false; - const item = this._items[0]; - return item === "" || item === '""'; - } - get str() { - var _a; - return (_a = this._str) !== null && _a !== undefined ? _a : this._str = this._items.reduce((s, c) => `${s}${c}`, ""); - } - get names() { - var _a; - return (_a = this._names) !== null && _a !== undefined ? _a : this._names = this._items.reduce((names, c) => { - if (c instanceof Name) - names[c.str] = (names[c.str] || 0) + 1; - return names; - }, {}); - } - } - exports._Code = _Code; - exports.nil = new _Code(""); - function _(strs, ...args) { - const code = [strs[0]]; - let i = 0; - while (i < args.length) { - addCodeArg(code, args[i]); - code.push(strs[++i]); - } - return new _Code(code); - } - exports._ = _; - var plus = new _Code("+"); - function str(strs, ...args) { - const expr = [safeStringify(strs[0])]; - let i = 0; - while (i < args.length) { - expr.push(plus); - addCodeArg(expr, args[i]); - expr.push(plus, safeStringify(strs[++i])); - } - optimize(expr); - return new _Code(expr); - } - exports.str = str; - function addCodeArg(code, arg) { - if (arg instanceof _Code) - code.push(...arg._items); - else if (arg instanceof Name) - code.push(arg); - else - code.push(interpolate(arg)); - } - exports.addCodeArg = addCodeArg; - function optimize(expr) { - let i = 1; - while (i < expr.length - 1) { - if (expr[i] === plus) { - const res = mergeExprItems(expr[i - 1], expr[i + 1]); - if (res !== undefined) { - expr.splice(i - 1, 3, res); - continue; - } - expr[i++] = "+"; - } - i++; - } - } - function mergeExprItems(a, b) { - if (b === '""') - return a; - if (a === '""') - return b; - if (typeof a == "string") { - if (b instanceof Name || a[a.length - 1] !== '"') - return; - if (typeof b != "string") - return `${a.slice(0, -1)}${b}"`; - if (b[0] === '"') - return a.slice(0, -1) + b.slice(1); - return; - } - if (typeof b == "string" && b[0] === '"' && !(a instanceof Name)) - return `"${a}${b.slice(1)}`; - return; - } - function strConcat(c1, c2) { - return c2.emptyStr() ? c1 : c1.emptyStr() ? c2 : str`${c1}${c2}`; - } - exports.strConcat = strConcat; - function interpolate(x) { - return typeof x == "number" || typeof x == "boolean" || x === null ? x : safeStringify(Array.isArray(x) ? x.join(",") : x); - } - function stringify(x) { - return new _Code(safeStringify(x)); - } - exports.stringify = stringify; - function safeStringify(x) { - return JSON.stringify(x).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029"); - } - exports.safeStringify = safeStringify; - function getProperty(key) { - return typeof key == "string" && exports.IDENTIFIER.test(key) ? new _Code(`.${key}`) : _`[${key}]`; - } - exports.getProperty = getProperty; - function getEsmExportName(key) { - if (typeof key == "string" && exports.IDENTIFIER.test(key)) { - return new _Code(`${key}`); - } - throw new Error(`CodeGen: invalid export name: ${key}, use explicit $id name mapping`); - } - exports.getEsmExportName = getEsmExportName; - function regexpCode(rx) { - return new _Code(rx.toString()); - } - exports.regexpCode = regexpCode; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/compile/codegen/scope.js -var require_scope = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.ValueScope = exports.ValueScopeName = exports.Scope = exports.varKinds = exports.UsedValueState = undefined; - var code_1 = require_code(); - - class ValueError extends Error { - constructor(name) { - super(`CodeGen: "code" for ${name} not defined`); - this.value = name.value; - } - } - var UsedValueState; - (function(UsedValueState2) { - UsedValueState2[UsedValueState2["Started"] = 0] = "Started"; - UsedValueState2[UsedValueState2["Completed"] = 1] = "Completed"; - })(UsedValueState || (exports.UsedValueState = UsedValueState = {})); - exports.varKinds = { - const: new code_1.Name("const"), - let: new code_1.Name("let"), - var: new code_1.Name("var") - }; - - class Scope { - constructor({ prefixes, parent } = {}) { - this._names = {}; - this._prefixes = prefixes; - this._parent = parent; - } - toName(nameOrPrefix) { - return nameOrPrefix instanceof code_1.Name ? nameOrPrefix : this.name(nameOrPrefix); - } - name(prefix) { - return new code_1.Name(this._newName(prefix)); - } - _newName(prefix) { - const ng = this._names[prefix] || this._nameGroup(prefix); - return `${prefix}${ng.index++}`; - } - _nameGroup(prefix) { - var _a, _b; - if (((_b = (_a = this._parent) === null || _a === undefined ? undefined : _a._prefixes) === null || _b === undefined ? undefined : _b.has(prefix)) || this._prefixes && !this._prefixes.has(prefix)) { - throw new Error(`CodeGen: prefix "${prefix}" is not allowed in this scope`); - } - return this._names[prefix] = { prefix, index: 0 }; - } - } - exports.Scope = Scope; - - class ValueScopeName extends code_1.Name { - constructor(prefix, nameStr) { - super(nameStr); - this.prefix = prefix; - } - setValue(value, { property, itemIndex }) { - this.value = value; - this.scopePath = (0, code_1._)`.${new code_1.Name(property)}[${itemIndex}]`; - } - } - exports.ValueScopeName = ValueScopeName; - var line = (0, code_1._)`\n`; - - class ValueScope extends Scope { - constructor(opts) { - super(opts); - this._values = {}; - this._scope = opts.scope; - this.opts = { ...opts, _n: opts.lines ? line : code_1.nil }; - } - get() { - return this._scope; - } - name(prefix) { - return new ValueScopeName(prefix, this._newName(prefix)); - } - value(nameOrPrefix, value) { - var _a; - if (value.ref === undefined) - throw new Error("CodeGen: ref must be passed in value"); - const name = this.toName(nameOrPrefix); - const { prefix } = name; - const valueKey = (_a = value.key) !== null && _a !== undefined ? _a : value.ref; - let vs = this._values[prefix]; - if (vs) { - const _name = vs.get(valueKey); - if (_name) - return _name; - } else { - vs = this._values[prefix] = new Map; - } - vs.set(valueKey, name); - const s = this._scope[prefix] || (this._scope[prefix] = []); - const itemIndex = s.length; - s[itemIndex] = value.ref; - name.setValue(value, { property: prefix, itemIndex }); - return name; - } - getValue(prefix, keyOrRef) { - const vs = this._values[prefix]; - if (!vs) - return; - return vs.get(keyOrRef); - } - scopeRefs(scopeName, values = this._values) { - return this._reduceValues(values, (name) => { - if (name.scopePath === undefined) - throw new Error(`CodeGen: name "${name}" has no value`); - return (0, code_1._)`${scopeName}${name.scopePath}`; - }); - } - scopeCode(values = this._values, usedValues, getCode) { - return this._reduceValues(values, (name) => { - if (name.value === undefined) - throw new Error(`CodeGen: name "${name}" has no value`); - return name.value.code; - }, usedValues, getCode); - } - _reduceValues(values, valueCode, usedValues = {}, getCode) { - let code = code_1.nil; - for (const prefix in values) { - const vs = values[prefix]; - if (!vs) - continue; - const nameSet = usedValues[prefix] = usedValues[prefix] || new Map; - vs.forEach((name) => { - if (nameSet.has(name)) - return; - nameSet.set(name, UsedValueState.Started); - let c = valueCode(name); - if (c) { - const def = this.opts.es5 ? exports.varKinds.var : exports.varKinds.const; - code = (0, code_1._)`${code}${def} ${name} = ${c};${this.opts._n}`; - } else if (c = getCode === null || getCode === undefined ? undefined : getCode(name)) { - code = (0, code_1._)`${code}${c}${this.opts._n}`; - } else { - throw new ValueError(name); - } - nameSet.set(name, UsedValueState.Completed); - }); - } - return code; - } - } - exports.ValueScope = ValueScope; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/compile/codegen/index.js -var require_codegen = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.or = exports.and = exports.not = exports.CodeGen = exports.operators = exports.varKinds = exports.ValueScopeName = exports.ValueScope = exports.Scope = exports.Name = exports.regexpCode = exports.stringify = exports.getProperty = exports.nil = exports.strConcat = exports.str = exports._ = undefined; - var code_1 = require_code(); - var scope_1 = require_scope(); - var code_2 = require_code(); - Object.defineProperty(exports, "_", { enumerable: true, get: function() { - return code_2._; - } }); - Object.defineProperty(exports, "str", { enumerable: true, get: function() { - return code_2.str; - } }); - Object.defineProperty(exports, "strConcat", { enumerable: true, get: function() { - return code_2.strConcat; - } }); - Object.defineProperty(exports, "nil", { enumerable: true, get: function() { - return code_2.nil; - } }); - Object.defineProperty(exports, "getProperty", { enumerable: true, get: function() { - return code_2.getProperty; - } }); - Object.defineProperty(exports, "stringify", { enumerable: true, get: function() { - return code_2.stringify; - } }); - Object.defineProperty(exports, "regexpCode", { enumerable: true, get: function() { - return code_2.regexpCode; - } }); - Object.defineProperty(exports, "Name", { enumerable: true, get: function() { - return code_2.Name; - } }); - var scope_2 = require_scope(); - Object.defineProperty(exports, "Scope", { enumerable: true, get: function() { - return scope_2.Scope; - } }); - Object.defineProperty(exports, "ValueScope", { enumerable: true, get: function() { - return scope_2.ValueScope; - } }); - Object.defineProperty(exports, "ValueScopeName", { enumerable: true, get: function() { - return scope_2.ValueScopeName; - } }); - Object.defineProperty(exports, "varKinds", { enumerable: true, get: function() { - return scope_2.varKinds; - } }); - exports.operators = { - GT: new code_1._Code(">"), - GTE: new code_1._Code(">="), - LT: new code_1._Code("<"), - LTE: new code_1._Code("<="), - EQ: new code_1._Code("==="), - NEQ: new code_1._Code("!=="), - NOT: new code_1._Code("!"), - OR: new code_1._Code("||"), - AND: new code_1._Code("&&"), - ADD: new code_1._Code("+") - }; - - class Node { - optimizeNodes() { - return this; - } - optimizeNames(_names, _constants) { - return this; - } - } - - class Def extends Node { - constructor(varKind, name, rhs) { - super(); - this.varKind = varKind; - this.name = name; - this.rhs = rhs; - } - render({ es5, _n }) { - const varKind = es5 ? scope_1.varKinds.var : this.varKind; - const rhs = this.rhs === undefined ? "" : ` = ${this.rhs}`; - return `${varKind} ${this.name}${rhs};` + _n; - } - optimizeNames(names, constants) { - if (!names[this.name.str]) - return; - if (this.rhs) - this.rhs = optimizeExpr(this.rhs, names, constants); - return this; - } - get names() { - return this.rhs instanceof code_1._CodeOrName ? this.rhs.names : {}; - } - } - - class Assign extends Node { - constructor(lhs, rhs, sideEffects) { - super(); - this.lhs = lhs; - this.rhs = rhs; - this.sideEffects = sideEffects; - } - render({ _n }) { - return `${this.lhs} = ${this.rhs};` + _n; - } - optimizeNames(names, constants) { - if (this.lhs instanceof code_1.Name && !names[this.lhs.str] && !this.sideEffects) - return; - this.rhs = optimizeExpr(this.rhs, names, constants); - return this; - } - get names() { - const names = this.lhs instanceof code_1.Name ? {} : { ...this.lhs.names }; - return addExprNames(names, this.rhs); - } - } - - class AssignOp extends Assign { - constructor(lhs, op, rhs, sideEffects) { - super(lhs, rhs, sideEffects); - this.op = op; - } - render({ _n }) { - return `${this.lhs} ${this.op}= ${this.rhs};` + _n; - } - } - - class Label extends Node { - constructor(label) { - super(); - this.label = label; - this.names = {}; - } - render({ _n }) { - return `${this.label}:` + _n; - } - } - - class Break extends Node { - constructor(label) { - super(); - this.label = label; - this.names = {}; - } - render({ _n }) { - const label = this.label ? ` ${this.label}` : ""; - return `break${label};` + _n; - } - } - - class Throw extends Node { - constructor(error2) { - super(); - this.error = error2; - } - render({ _n }) { - return `throw ${this.error};` + _n; - } - get names() { - return this.error.names; - } - } - - class AnyCode extends Node { - constructor(code) { - super(); - this.code = code; - } - render({ _n }) { - return `${this.code};` + _n; - } - optimizeNodes() { - return `${this.code}` ? this : undefined; - } - optimizeNames(names, constants) { - this.code = optimizeExpr(this.code, names, constants); - return this; - } - get names() { - return this.code instanceof code_1._CodeOrName ? this.code.names : {}; - } - } - - class ParentNode extends Node { - constructor(nodes = []) { - super(); - this.nodes = nodes; - } - render(opts) { - return this.nodes.reduce((code, n) => code + n.render(opts), ""); - } - optimizeNodes() { - const { nodes } = this; - let i = nodes.length; - while (i--) { - const n = nodes[i].optimizeNodes(); - if (Array.isArray(n)) - nodes.splice(i, 1, ...n); - else if (n) - nodes[i] = n; - else - nodes.splice(i, 1); - } - return nodes.length > 0 ? this : undefined; - } - optimizeNames(names, constants) { - const { nodes } = this; - let i = nodes.length; - while (i--) { - const n = nodes[i]; - if (n.optimizeNames(names, constants)) - continue; - subtractNames(names, n.names); - nodes.splice(i, 1); - } - return nodes.length > 0 ? this : undefined; - } - get names() { - return this.nodes.reduce((names, n) => addNames(names, n.names), {}); - } - } - - class BlockNode extends ParentNode { - render(opts) { - return "{" + opts._n + super.render(opts) + "}" + opts._n; - } - } - - class Root extends ParentNode { - } - - class Else extends BlockNode { - } - Else.kind = "else"; - - class If extends BlockNode { - constructor(condition, nodes) { - super(nodes); - this.condition = condition; - } - render(opts) { - let code = `if(${this.condition})` + super.render(opts); - if (this.else) - code += "else " + this.else.render(opts); - return code; - } - optimizeNodes() { - super.optimizeNodes(); - const cond = this.condition; - if (cond === true) - return this.nodes; - let e = this.else; - if (e) { - const ns = e.optimizeNodes(); - e = this.else = Array.isArray(ns) ? new Else(ns) : ns; - } - if (e) { - if (cond === false) - return e instanceof If ? e : e.nodes; - if (this.nodes.length) - return this; - return new If(not(cond), e instanceof If ? [e] : e.nodes); - } - if (cond === false || !this.nodes.length) - return; - return this; - } - optimizeNames(names, constants) { - var _a; - this.else = (_a = this.else) === null || _a === undefined ? undefined : _a.optimizeNames(names, constants); - if (!(super.optimizeNames(names, constants) || this.else)) - return; - this.condition = optimizeExpr(this.condition, names, constants); - return this; - } - get names() { - const names = super.names; - addExprNames(names, this.condition); - if (this.else) - addNames(names, this.else.names); - return names; - } - } - If.kind = "if"; - - class For extends BlockNode { - } - For.kind = "for"; - - class ForLoop extends For { - constructor(iteration) { - super(); - this.iteration = iteration; - } - render(opts) { - return `for(${this.iteration})` + super.render(opts); - } - optimizeNames(names, constants) { - if (!super.optimizeNames(names, constants)) - return; - this.iteration = optimizeExpr(this.iteration, names, constants); - return this; - } - get names() { - return addNames(super.names, this.iteration.names); - } - } - - class ForRange extends For { - constructor(varKind, name, from, to) { - super(); - this.varKind = varKind; - this.name = name; - this.from = from; - this.to = to; - } - render(opts) { - const varKind = opts.es5 ? scope_1.varKinds.var : this.varKind; - const { name, from, to } = this; - return `for(${varKind} ${name}=${from}; ${name}<${to}; ${name}++)` + super.render(opts); - } - get names() { - const names = addExprNames(super.names, this.from); - return addExprNames(names, this.to); - } - } - - class ForIter extends For { - constructor(loop, varKind, name, iterable) { - super(); - this.loop = loop; - this.varKind = varKind; - this.name = name; - this.iterable = iterable; - } - render(opts) { - return `for(${this.varKind} ${this.name} ${this.loop} ${this.iterable})` + super.render(opts); - } - optimizeNames(names, constants) { - if (!super.optimizeNames(names, constants)) - return; - this.iterable = optimizeExpr(this.iterable, names, constants); - return this; - } - get names() { - return addNames(super.names, this.iterable.names); - } - } - - class Func extends BlockNode { - constructor(name, args, async) { - super(); - this.name = name; - this.args = args; - this.async = async; - } - render(opts) { - const _async = this.async ? "async " : ""; - return `${_async}function ${this.name}(${this.args})` + super.render(opts); - } - } - Func.kind = "func"; - - class Return extends ParentNode { - render(opts) { - return "return " + super.render(opts); - } - } - Return.kind = "return"; - - class Try extends BlockNode { - render(opts) { - let code = "try" + super.render(opts); - if (this.catch) - code += this.catch.render(opts); - if (this.finally) - code += this.finally.render(opts); - return code; - } - optimizeNodes() { - var _a, _b; - super.optimizeNodes(); - (_a = this.catch) === null || _a === undefined || _a.optimizeNodes(); - (_b = this.finally) === null || _b === undefined || _b.optimizeNodes(); - return this; - } - optimizeNames(names, constants) { - var _a, _b; - super.optimizeNames(names, constants); - (_a = this.catch) === null || _a === undefined || _a.optimizeNames(names, constants); - (_b = this.finally) === null || _b === undefined || _b.optimizeNames(names, constants); - return this; - } - get names() { - const names = super.names; - if (this.catch) - addNames(names, this.catch.names); - if (this.finally) - addNames(names, this.finally.names); - return names; - } - } - - class Catch extends BlockNode { - constructor(error2) { - super(); - this.error = error2; - } - render(opts) { - return `catch(${this.error})` + super.render(opts); - } - } - Catch.kind = "catch"; - - class Finally extends BlockNode { - render(opts) { - return "finally" + super.render(opts); - } - } - Finally.kind = "finally"; - - class CodeGen { - constructor(extScope, opts = {}) { - this._values = {}; - this._blockStarts = []; - this._constants = {}; - this.opts = { ...opts, _n: opts.lines ? ` -` : "" }; - this._extScope = extScope; - this._scope = new scope_1.Scope({ parent: extScope }); - this._nodes = [new Root]; - } - toString() { - return this._root.render(this.opts); - } - name(prefix) { - return this._scope.name(prefix); - } - scopeName(prefix) { - return this._extScope.name(prefix); - } - scopeValue(prefixOrName, value) { - const name = this._extScope.value(prefixOrName, value); - const vs = this._values[name.prefix] || (this._values[name.prefix] = new Set); - vs.add(name); - return name; - } - getScopeValue(prefix, keyOrRef) { - return this._extScope.getValue(prefix, keyOrRef); - } - scopeRefs(scopeName) { - return this._extScope.scopeRefs(scopeName, this._values); - } - scopeCode() { - return this._extScope.scopeCode(this._values); - } - _def(varKind, nameOrPrefix, rhs, constant) { - const name = this._scope.toName(nameOrPrefix); - if (rhs !== undefined && constant) - this._constants[name.str] = rhs; - this._leafNode(new Def(varKind, name, rhs)); - return name; - } - const(nameOrPrefix, rhs, _constant) { - return this._def(scope_1.varKinds.const, nameOrPrefix, rhs, _constant); - } - let(nameOrPrefix, rhs, _constant) { - return this._def(scope_1.varKinds.let, nameOrPrefix, rhs, _constant); - } - var(nameOrPrefix, rhs, _constant) { - return this._def(scope_1.varKinds.var, nameOrPrefix, rhs, _constant); - } - assign(lhs, rhs, sideEffects) { - return this._leafNode(new Assign(lhs, rhs, sideEffects)); - } - add(lhs, rhs) { - return this._leafNode(new AssignOp(lhs, exports.operators.ADD, rhs)); - } - code(c) { - if (typeof c == "function") - c(); - else if (c !== code_1.nil) - this._leafNode(new AnyCode(c)); - return this; - } - object(...keyValues) { - const code = ["{"]; - for (const [key, value] of keyValues) { - if (code.length > 1) - code.push(","); - code.push(key); - if (key !== value || this.opts.es5) { - code.push(":"); - (0, code_1.addCodeArg)(code, value); - } - } - code.push("}"); - return new code_1._Code(code); - } - if(condition, thenBody, elseBody) { - this._blockNode(new If(condition)); - if (thenBody && elseBody) { - this.code(thenBody).else().code(elseBody).endIf(); - } else if (thenBody) { - this.code(thenBody).endIf(); - } else if (elseBody) { - throw new Error('CodeGen: "else" body without "then" body'); - } - return this; - } - elseIf(condition) { - return this._elseNode(new If(condition)); - } - else() { - return this._elseNode(new Else); - } - endIf() { - return this._endBlockNode(If, Else); - } - _for(node, forBody) { - this._blockNode(node); - if (forBody) - this.code(forBody).endFor(); - return this; - } - for(iteration, forBody) { - return this._for(new ForLoop(iteration), forBody); - } - forRange(nameOrPrefix, from, to, forBody, varKind = this.opts.es5 ? scope_1.varKinds.var : scope_1.varKinds.let) { - const name = this._scope.toName(nameOrPrefix); - return this._for(new ForRange(varKind, name, from, to), () => forBody(name)); - } - forOf(nameOrPrefix, iterable, forBody, varKind = scope_1.varKinds.const) { - const name = this._scope.toName(nameOrPrefix); - if (this.opts.es5) { - const arr = iterable instanceof code_1.Name ? iterable : this.var("_arr", iterable); - return this.forRange("_i", 0, (0, code_1._)`${arr}.length`, (i) => { - this.var(name, (0, code_1._)`${arr}[${i}]`); - forBody(name); - }); - } - return this._for(new ForIter("of", varKind, name, iterable), () => forBody(name)); - } - forIn(nameOrPrefix, obj, forBody, varKind = this.opts.es5 ? scope_1.varKinds.var : scope_1.varKinds.const) { - if (this.opts.ownProperties) { - return this.forOf(nameOrPrefix, (0, code_1._)`Object.keys(${obj})`, forBody); - } - const name = this._scope.toName(nameOrPrefix); - return this._for(new ForIter("in", varKind, name, obj), () => forBody(name)); - } - endFor() { - return this._endBlockNode(For); - } - label(label) { - return this._leafNode(new Label(label)); - } - break(label) { - return this._leafNode(new Break(label)); - } - return(value) { - const node = new Return; - this._blockNode(node); - this.code(value); - if (node.nodes.length !== 1) - throw new Error('CodeGen: "return" should have one node'); - return this._endBlockNode(Return); - } - try(tryBody, catchCode, finallyCode) { - if (!catchCode && !finallyCode) - throw new Error('CodeGen: "try" without "catch" and "finally"'); - const node = new Try; - this._blockNode(node); - this.code(tryBody); - if (catchCode) { - const error2 = this.name("e"); - this._currNode = node.catch = new Catch(error2); - catchCode(error2); - } - if (finallyCode) { - this._currNode = node.finally = new Finally; - this.code(finallyCode); - } - return this._endBlockNode(Catch, Finally); - } - throw(error2) { - return this._leafNode(new Throw(error2)); - } - block(body, nodeCount) { - this._blockStarts.push(this._nodes.length); - if (body) - this.code(body).endBlock(nodeCount); - return this; - } - endBlock(nodeCount) { - const len = this._blockStarts.pop(); - if (len === undefined) - throw new Error("CodeGen: not in self-balancing block"); - const toClose = this._nodes.length - len; - if (toClose < 0 || nodeCount !== undefined && toClose !== nodeCount) { - throw new Error(`CodeGen: wrong number of nodes: ${toClose} vs ${nodeCount} expected`); - } - this._nodes.length = len; - return this; - } - func(name, args = code_1.nil, async, funcBody) { - this._blockNode(new Func(name, args, async)); - if (funcBody) - this.code(funcBody).endFunc(); - return this; - } - endFunc() { - return this._endBlockNode(Func); - } - optimize(n = 1) { - while (n-- > 0) { - this._root.optimizeNodes(); - this._root.optimizeNames(this._root.names, this._constants); - } - } - _leafNode(node) { - this._currNode.nodes.push(node); - return this; - } - _blockNode(node) { - this._currNode.nodes.push(node); - this._nodes.push(node); - } - _endBlockNode(N1, N2) { - const n = this._currNode; - if (n instanceof N1 || N2 && n instanceof N2) { - this._nodes.pop(); - return this; - } - throw new Error(`CodeGen: not in block "${N2 ? `${N1.kind}/${N2.kind}` : N1.kind}"`); - } - _elseNode(node) { - const n = this._currNode; - if (!(n instanceof If)) { - throw new Error('CodeGen: "else" without "if"'); - } - this._currNode = n.else = node; - return this; - } - get _root() { - return this._nodes[0]; - } - get _currNode() { - const ns = this._nodes; - return ns[ns.length - 1]; - } - set _currNode(node) { - const ns = this._nodes; - ns[ns.length - 1] = node; - } - } - exports.CodeGen = CodeGen; - function addNames(names, from) { - for (const n in from) - names[n] = (names[n] || 0) + (from[n] || 0); - return names; - } - function addExprNames(names, from) { - return from instanceof code_1._CodeOrName ? addNames(names, from.names) : names; - } - function optimizeExpr(expr, names, constants) { - if (expr instanceof code_1.Name) - return replaceName(expr); - if (!canOptimize(expr)) - return expr; - return new code_1._Code(expr._items.reduce((items, c) => { - if (c instanceof code_1.Name) - c = replaceName(c); - if (c instanceof code_1._Code) - items.push(...c._items); - else - items.push(c); - return items; - }, [])); - function replaceName(n) { - const c = constants[n.str]; - if (c === undefined || names[n.str] !== 1) - return n; - delete names[n.str]; - return c; - } - function canOptimize(e) { - return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 && constants[c.str] !== undefined); - } - } - function subtractNames(names, from) { - for (const n in from) - names[n] = (names[n] || 0) - (from[n] || 0); - } - function not(x) { - return typeof x == "boolean" || typeof x == "number" || x === null ? !x : (0, code_1._)`!${par(x)}`; - } - exports.not = not; - var andCode = mappend(exports.operators.AND); - function and(...args) { - return args.reduce(andCode); - } - exports.and = and; - var orCode = mappend(exports.operators.OR); - function or(...args) { - return args.reduce(orCode); - } - exports.or = or; - function mappend(op) { - return (x, y) => x === code_1.nil ? y : y === code_1.nil ? x : (0, code_1._)`${par(x)} ${op} ${par(y)}`; - } - function par(x) { - return x instanceof code_1.Name ? x : (0, code_1._)`(${x})`; - } -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/compile/util.js -var require_util = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.checkStrictMode = exports.getErrorPath = exports.Type = exports.useFunc = exports.setEvaluated = exports.evaluatedPropsToName = exports.mergeEvaluated = exports.eachItem = exports.unescapeJsonPointer = exports.escapeJsonPointer = exports.escapeFragment = exports.unescapeFragment = exports.schemaRefOrVal = exports.schemaHasRulesButRef = exports.schemaHasRules = exports.checkUnknownRules = exports.alwaysValidSchema = exports.toHash = undefined; - var codegen_1 = require_codegen(); - var code_1 = require_code(); - function toHash(arr) { - const hash = {}; - for (const item of arr) - hash[item] = true; - return hash; - } - exports.toHash = toHash; - function alwaysValidSchema(it, schema) { - if (typeof schema == "boolean") - return schema; - if (Object.keys(schema).length === 0) - return true; - checkUnknownRules(it, schema); - return !schemaHasRules(schema, it.self.RULES.all); - } - exports.alwaysValidSchema = alwaysValidSchema; - function checkUnknownRules(it, schema = it.schema) { - const { opts, self } = it; - if (!opts.strictSchema) - return; - if (typeof schema === "boolean") - return; - const rules = self.RULES.keywords; - for (const key in schema) { - if (!rules[key]) - checkStrictMode(it, `unknown keyword: "${key}"`); - } - } - exports.checkUnknownRules = checkUnknownRules; - function schemaHasRules(schema, rules) { - if (typeof schema == "boolean") - return !schema; - for (const key in schema) - if (rules[key]) - return true; - return false; - } - exports.schemaHasRules = schemaHasRules; - function schemaHasRulesButRef(schema, RULES) { - if (typeof schema == "boolean") - return !schema; - for (const key in schema) - if (key !== "$ref" && RULES.all[key]) - return true; - return false; - } - exports.schemaHasRulesButRef = schemaHasRulesButRef; - function schemaRefOrVal({ topSchemaRef, schemaPath }, schema, keyword, $data) { - if (!$data) { - if (typeof schema == "number" || typeof schema == "boolean") - return schema; - if (typeof schema == "string") - return (0, codegen_1._)`${schema}`; - } - return (0, codegen_1._)`${topSchemaRef}${schemaPath}${(0, codegen_1.getProperty)(keyword)}`; - } - exports.schemaRefOrVal = schemaRefOrVal; - function unescapeFragment(str) { - return unescapeJsonPointer(decodeURIComponent(str)); - } - exports.unescapeFragment = unescapeFragment; - function escapeFragment(str) { - return encodeURIComponent(escapeJsonPointer(str)); - } - exports.escapeFragment = escapeFragment; - function escapeJsonPointer(str) { - if (typeof str == "number") - return `${str}`; - return str.replace(/~/g, "~0").replace(/\//g, "~1"); - } - exports.escapeJsonPointer = escapeJsonPointer; - function unescapeJsonPointer(str) { - return str.replace(/~1/g, "/").replace(/~0/g, "~"); - } - exports.unescapeJsonPointer = unescapeJsonPointer; - function eachItem(xs, f) { - if (Array.isArray(xs)) { - for (const x of xs) - f(x); - } else { - f(xs); - } - } - exports.eachItem = eachItem; - function makeMergeEvaluated({ mergeNames, mergeToName, mergeValues: mergeValues2, resultToName }) { - return (gen, from, to, toName) => { - const res = to === undefined ? from : to instanceof codegen_1.Name ? (from instanceof codegen_1.Name ? mergeNames(gen, from, to) : mergeToName(gen, from, to), to) : from instanceof codegen_1.Name ? (mergeToName(gen, to, from), from) : mergeValues2(from, to); - return toName === codegen_1.Name && !(res instanceof codegen_1.Name) ? resultToName(gen, res) : res; - }; - } - exports.mergeEvaluated = { - props: makeMergeEvaluated({ - mergeNames: (gen, from, to) => gen.if((0, codegen_1._)`${to} !== true && ${from} !== undefined`, () => { - gen.if((0, codegen_1._)`${from} === true`, () => gen.assign(to, true), () => gen.assign(to, (0, codegen_1._)`${to} || {}`).code((0, codegen_1._)`Object.assign(${to}, ${from})`)); - }), - mergeToName: (gen, from, to) => gen.if((0, codegen_1._)`${to} !== true`, () => { - if (from === true) { - gen.assign(to, true); - } else { - gen.assign(to, (0, codegen_1._)`${to} || {}`); - setEvaluated(gen, to, from); - } - }), - mergeValues: (from, to) => from === true ? true : { ...from, ...to }, - resultToName: evaluatedPropsToName - }), - items: makeMergeEvaluated({ - mergeNames: (gen, from, to) => gen.if((0, codegen_1._)`${to} !== true && ${from} !== undefined`, () => gen.assign(to, (0, codegen_1._)`${from} === true ? true : ${to} > ${from} ? ${to} : ${from}`)), - mergeToName: (gen, from, to) => gen.if((0, codegen_1._)`${to} !== true`, () => gen.assign(to, from === true ? true : (0, codegen_1._)`${to} > ${from} ? ${to} : ${from}`)), - mergeValues: (from, to) => from === true ? true : Math.max(from, to), - resultToName: (gen, items) => gen.var("items", items) - }) - }; - function evaluatedPropsToName(gen, ps) { - if (ps === true) - return gen.var("props", true); - const props = gen.var("props", (0, codegen_1._)`{}`); - if (ps !== undefined) - setEvaluated(gen, props, ps); - return props; - } - exports.evaluatedPropsToName = evaluatedPropsToName; - function setEvaluated(gen, props, ps) { - Object.keys(ps).forEach((p) => gen.assign((0, codegen_1._)`${props}${(0, codegen_1.getProperty)(p)}`, true)); - } - exports.setEvaluated = setEvaluated; - var snippets = {}; - function useFunc(gen, f) { - return gen.scopeValue("func", { - ref: f, - code: snippets[f.code] || (snippets[f.code] = new code_1._Code(f.code)) - }); - } - exports.useFunc = useFunc; - var Type; - (function(Type2) { - Type2[Type2["Num"] = 0] = "Num"; - Type2[Type2["Str"] = 1] = "Str"; - })(Type || (exports.Type = Type = {})); - function getErrorPath(dataProp, dataPropType, jsPropertySyntax) { - if (dataProp instanceof codegen_1.Name) { - const isNumber = dataPropType === Type.Num; - return jsPropertySyntax ? isNumber ? (0, codegen_1._)`"[" + ${dataProp} + "]"` : (0, codegen_1._)`"['" + ${dataProp} + "']"` : isNumber ? (0, codegen_1._)`"/" + ${dataProp}` : (0, codegen_1._)`"/" + ${dataProp}.replace(/~/g, "~0").replace(/\\//g, "~1")`; - } - return jsPropertySyntax ? (0, codegen_1.getProperty)(dataProp).toString() : "/" + escapeJsonPointer(dataProp); - } - exports.getErrorPath = getErrorPath; - function checkStrictMode(it, msg, mode = it.opts.strictSchema) { - if (!mode) - return; - msg = `strict mode: ${msg}`; - if (mode === true) - throw new Error(msg); - it.self.logger.warn(msg); - } - exports.checkStrictMode = checkStrictMode; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/compile/names.js -var require_names = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var codegen_1 = require_codegen(); - var names = { - data: new codegen_1.Name("data"), - valCxt: new codegen_1.Name("valCxt"), - instancePath: new codegen_1.Name("instancePath"), - parentData: new codegen_1.Name("parentData"), - parentDataProperty: new codegen_1.Name("parentDataProperty"), - rootData: new codegen_1.Name("rootData"), - dynamicAnchors: new codegen_1.Name("dynamicAnchors"), - vErrors: new codegen_1.Name("vErrors"), - errors: new codegen_1.Name("errors"), - this: new codegen_1.Name("this"), - self: new codegen_1.Name("self"), - scope: new codegen_1.Name("scope"), - json: new codegen_1.Name("json"), - jsonPos: new codegen_1.Name("jsonPos"), - jsonLen: new codegen_1.Name("jsonLen"), - jsonPart: new codegen_1.Name("jsonPart") - }; - exports.default = names; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/compile/errors.js -var require_errors = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.extendErrors = exports.resetErrorsCount = exports.reportExtraError = exports.reportError = exports.keyword$DataError = exports.keywordError = undefined; - var codegen_1 = require_codegen(); - var util_1 = require_util(); - var names_1 = require_names(); - exports.keywordError = { - message: ({ keyword }) => (0, codegen_1.str)`must pass "${keyword}" keyword validation` - }; - exports.keyword$DataError = { - message: ({ keyword, schemaType }) => schemaType ? (0, codegen_1.str)`"${keyword}" keyword must be ${schemaType} ($data)` : (0, codegen_1.str)`"${keyword}" keyword is invalid ($data)` - }; - function reportError(cxt, error2 = exports.keywordError, errorPaths, overrideAllErrors) { - const { it } = cxt; - const { gen, compositeRule, allErrors } = it; - const errObj = errorObjectCode(cxt, error2, errorPaths); - if (overrideAllErrors !== null && overrideAllErrors !== undefined ? overrideAllErrors : compositeRule || allErrors) { - addError(gen, errObj); - } else { - returnErrors(it, (0, codegen_1._)`[${errObj}]`); - } - } - exports.reportError = reportError; - function reportExtraError(cxt, error2 = exports.keywordError, errorPaths) { - const { it } = cxt; - const { gen, compositeRule, allErrors } = it; - const errObj = errorObjectCode(cxt, error2, errorPaths); - addError(gen, errObj); - if (!(compositeRule || allErrors)) { - returnErrors(it, names_1.default.vErrors); - } - } - exports.reportExtraError = reportExtraError; - function resetErrorsCount(gen, errsCount) { - gen.assign(names_1.default.errors, errsCount); - gen.if((0, codegen_1._)`${names_1.default.vErrors} !== null`, () => gen.if(errsCount, () => gen.assign((0, codegen_1._)`${names_1.default.vErrors}.length`, errsCount), () => gen.assign(names_1.default.vErrors, null))); - } - exports.resetErrorsCount = resetErrorsCount; - function extendErrors({ gen, keyword, schemaValue, data, errsCount, it }) { - if (errsCount === undefined) - throw new Error("ajv implementation error"); - const err = gen.name("err"); - gen.forRange("i", errsCount, names_1.default.errors, (i) => { - gen.const(err, (0, codegen_1._)`${names_1.default.vErrors}[${i}]`); - gen.if((0, codegen_1._)`${err}.instancePath === undefined`, () => gen.assign((0, codegen_1._)`${err}.instancePath`, (0, codegen_1.strConcat)(names_1.default.instancePath, it.errorPath))); - gen.assign((0, codegen_1._)`${err}.schemaPath`, (0, codegen_1.str)`${it.errSchemaPath}/${keyword}`); - if (it.opts.verbose) { - gen.assign((0, codegen_1._)`${err}.schema`, schemaValue); - gen.assign((0, codegen_1._)`${err}.data`, data); - } - }); - } - exports.extendErrors = extendErrors; - function addError(gen, errObj) { - const err = gen.const("err", errObj); - gen.if((0, codegen_1._)`${names_1.default.vErrors} === null`, () => gen.assign(names_1.default.vErrors, (0, codegen_1._)`[${err}]`), (0, codegen_1._)`${names_1.default.vErrors}.push(${err})`); - gen.code((0, codegen_1._)`${names_1.default.errors}++`); - } - function returnErrors(it, errs) { - const { gen, validateName, schemaEnv } = it; - if (schemaEnv.$async) { - gen.throw((0, codegen_1._)`new ${it.ValidationError}(${errs})`); - } else { - gen.assign((0, codegen_1._)`${validateName}.errors`, errs); - gen.return(false); - } - } - var E = { - keyword: new codegen_1.Name("keyword"), - schemaPath: new codegen_1.Name("schemaPath"), - params: new codegen_1.Name("params"), - propertyName: new codegen_1.Name("propertyName"), - message: new codegen_1.Name("message"), - schema: new codegen_1.Name("schema"), - parentSchema: new codegen_1.Name("parentSchema") - }; - function errorObjectCode(cxt, error2, errorPaths) { - const { createErrors } = cxt.it; - if (createErrors === false) - return (0, codegen_1._)`{}`; - return errorObject(cxt, error2, errorPaths); - } - function errorObject(cxt, error2, errorPaths = {}) { - const { gen, it } = cxt; - const keyValues = [ - errorInstancePath(it, errorPaths), - errorSchemaPath(cxt, errorPaths) - ]; - extraErrorProps(cxt, error2, keyValues); - return gen.object(...keyValues); - } - function errorInstancePath({ errorPath }, { instancePath }) { - const instPath = instancePath ? (0, codegen_1.str)`${errorPath}${(0, util_1.getErrorPath)(instancePath, util_1.Type.Str)}` : errorPath; - return [names_1.default.instancePath, (0, codegen_1.strConcat)(names_1.default.instancePath, instPath)]; - } - function errorSchemaPath({ keyword, it: { errSchemaPath } }, { schemaPath, parentSchema }) { - let schPath = parentSchema ? errSchemaPath : (0, codegen_1.str)`${errSchemaPath}/${keyword}`; - if (schemaPath) { - schPath = (0, codegen_1.str)`${schPath}${(0, util_1.getErrorPath)(schemaPath, util_1.Type.Str)}`; - } - return [E.schemaPath, schPath]; - } - function extraErrorProps(cxt, { params, message }, keyValues) { - const { keyword, data, schemaValue, it } = cxt; - const { opts, propertyName, topSchemaRef, schemaPath } = it; - keyValues.push([E.keyword, keyword], [E.params, typeof params == "function" ? params(cxt) : params || (0, codegen_1._)`{}`]); - if (opts.messages) { - keyValues.push([E.message, typeof message == "function" ? message(cxt) : message]); - } - if (opts.verbose) { - keyValues.push([E.schema, schemaValue], [E.parentSchema, (0, codegen_1._)`${topSchemaRef}${schemaPath}`], [names_1.default.data, data]); - } - if (propertyName) - keyValues.push([E.propertyName, propertyName]); - } -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/compile/validate/boolSchema.js -var require_boolSchema = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.boolOrEmptySchema = exports.topBoolOrEmptySchema = undefined; - var errors_1 = require_errors(); - var codegen_1 = require_codegen(); - var names_1 = require_names(); - var boolError = { - message: "boolean schema is false" - }; - function topBoolOrEmptySchema(it) { - const { gen, schema, validateName } = it; - if (schema === false) { - falseSchemaError(it, false); - } else if (typeof schema == "object" && schema.$async === true) { - gen.return(names_1.default.data); - } else { - gen.assign((0, codegen_1._)`${validateName}.errors`, null); - gen.return(true); - } - } - exports.topBoolOrEmptySchema = topBoolOrEmptySchema; - function boolOrEmptySchema(it, valid) { - const { gen, schema } = it; - if (schema === false) { - gen.var(valid, false); - falseSchemaError(it); - } else { - gen.var(valid, true); - } - } - exports.boolOrEmptySchema = boolOrEmptySchema; - function falseSchemaError(it, overrideAllErrors) { - const { gen, data } = it; - const cxt = { - gen, - keyword: "false schema", - data, - schema: false, - schemaCode: false, - schemaValue: false, - params: {}, - it - }; - (0, errors_1.reportError)(cxt, boolError, undefined, overrideAllErrors); - } -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/compile/rules.js -var require_rules = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.getRules = exports.isJSONType = undefined; - var _jsonTypes = ["string", "number", "integer", "boolean", "null", "object", "array"]; - var jsonTypes = new Set(_jsonTypes); - function isJSONType(x) { - return typeof x == "string" && jsonTypes.has(x); - } - exports.isJSONType = isJSONType; - function getRules() { - const groups = { - number: { type: "number", rules: [] }, - string: { type: "string", rules: [] }, - array: { type: "array", rules: [] }, - object: { type: "object", rules: [] } - }; - return { - types: { ...groups, integer: true, boolean: true, null: true }, - rules: [{ rules: [] }, groups.number, groups.string, groups.array, groups.object], - post: { rules: [] }, - all: {}, - keywords: {} - }; - } - exports.getRules = getRules; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/compile/validate/applicability.js -var require_applicability = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.shouldUseRule = exports.shouldUseGroup = exports.schemaHasRulesForType = undefined; - function schemaHasRulesForType({ schema, self }, type) { - const group = self.RULES.types[type]; - return group && group !== true && shouldUseGroup(schema, group); - } - exports.schemaHasRulesForType = schemaHasRulesForType; - function shouldUseGroup(schema, group) { - return group.rules.some((rule) => shouldUseRule(schema, rule)); - } - exports.shouldUseGroup = shouldUseGroup; - function shouldUseRule(schema, rule) { - var _a; - return schema[rule.keyword] !== undefined || ((_a = rule.definition.implements) === null || _a === undefined ? undefined : _a.some((kwd) => schema[kwd] !== undefined)); - } - exports.shouldUseRule = shouldUseRule; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/compile/validate/dataType.js -var require_dataType = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.reportTypeError = exports.checkDataTypes = exports.checkDataType = exports.coerceAndCheckDataType = exports.getJSONTypes = exports.getSchemaTypes = exports.DataType = undefined; - var rules_1 = require_rules(); - var applicability_1 = require_applicability(); - var errors_1 = require_errors(); - var codegen_1 = require_codegen(); - var util_1 = require_util(); - var DataType; - (function(DataType2) { - DataType2[DataType2["Correct"] = 0] = "Correct"; - DataType2[DataType2["Wrong"] = 1] = "Wrong"; - })(DataType || (exports.DataType = DataType = {})); - function getSchemaTypes(schema) { - const types = getJSONTypes(schema.type); - const hasNull = types.includes("null"); - if (hasNull) { - if (schema.nullable === false) - throw new Error("type: null contradicts nullable: false"); - } else { - if (!types.length && schema.nullable !== undefined) { - throw new Error('"nullable" cannot be used without "type"'); - } - if (schema.nullable === true) - types.push("null"); - } - return types; - } - exports.getSchemaTypes = getSchemaTypes; - function getJSONTypes(ts) { - const types = Array.isArray(ts) ? ts : ts ? [ts] : []; - if (types.every(rules_1.isJSONType)) - return types; - throw new Error("type must be JSONType or JSONType[]: " + types.join(",")); - } - exports.getJSONTypes = getJSONTypes; - function coerceAndCheckDataType(it, types) { - const { gen, data, opts } = it; - const coerceTo = coerceToTypes(types, opts.coerceTypes); - const checkTypes = types.length > 0 && !(coerceTo.length === 0 && types.length === 1 && (0, applicability_1.schemaHasRulesForType)(it, types[0])); - if (checkTypes) { - const wrongType = checkDataTypes(types, data, opts.strictNumbers, DataType.Wrong); - gen.if(wrongType, () => { - if (coerceTo.length) - coerceData(it, types, coerceTo); - else - reportTypeError(it); - }); - } - return checkTypes; - } - exports.coerceAndCheckDataType = coerceAndCheckDataType; - var COERCIBLE = new Set(["string", "number", "integer", "boolean", "null"]); - function coerceToTypes(types, coerceTypes) { - return coerceTypes ? types.filter((t) => COERCIBLE.has(t) || coerceTypes === "array" && t === "array") : []; - } - function coerceData(it, types, coerceTo) { - const { gen, data, opts } = it; - const dataType = gen.let("dataType", (0, codegen_1._)`typeof ${data}`); - const coerced = gen.let("coerced", (0, codegen_1._)`undefined`); - if (opts.coerceTypes === "array") { - gen.if((0, codegen_1._)`${dataType} == 'object' && Array.isArray(${data}) && ${data}.length == 1`, () => gen.assign(data, (0, codegen_1._)`${data}[0]`).assign(dataType, (0, codegen_1._)`typeof ${data}`).if(checkDataTypes(types, data, opts.strictNumbers), () => gen.assign(coerced, data))); - } - gen.if((0, codegen_1._)`${coerced} !== undefined`); - for (const t of coerceTo) { - if (COERCIBLE.has(t) || t === "array" && opts.coerceTypes === "array") { - coerceSpecificType(t); - } - } - gen.else(); - reportTypeError(it); - gen.endIf(); - gen.if((0, codegen_1._)`${coerced} !== undefined`, () => { - gen.assign(data, coerced); - assignParentData(it, coerced); - }); - function coerceSpecificType(t) { - switch (t) { - case "string": - gen.elseIf((0, codegen_1._)`${dataType} == "number" || ${dataType} == "boolean"`).assign(coerced, (0, codegen_1._)`"" + ${data}`).elseIf((0, codegen_1._)`${data} === null`).assign(coerced, (0, codegen_1._)`""`); - return; - case "number": - gen.elseIf((0, codegen_1._)`${dataType} == "boolean" || ${data} === null - || (${dataType} == "string" && ${data} && ${data} == +${data})`).assign(coerced, (0, codegen_1._)`+${data}`); - return; - case "integer": - gen.elseIf((0, codegen_1._)`${dataType} === "boolean" || ${data} === null - || (${dataType} === "string" && ${data} && ${data} == +${data} && !(${data} % 1))`).assign(coerced, (0, codegen_1._)`+${data}`); - return; - case "boolean": - gen.elseIf((0, codegen_1._)`${data} === "false" || ${data} === 0 || ${data} === null`).assign(coerced, false).elseIf((0, codegen_1._)`${data} === "true" || ${data} === 1`).assign(coerced, true); - return; - case "null": - gen.elseIf((0, codegen_1._)`${data} === "" || ${data} === 0 || ${data} === false`); - gen.assign(coerced, null); - return; - case "array": - gen.elseIf((0, codegen_1._)`${dataType} === "string" || ${dataType} === "number" - || ${dataType} === "boolean" || ${data} === null`).assign(coerced, (0, codegen_1._)`[${data}]`); - } - } - } - function assignParentData({ gen, parentData, parentDataProperty }, expr) { - gen.if((0, codegen_1._)`${parentData} !== undefined`, () => gen.assign((0, codegen_1._)`${parentData}[${parentDataProperty}]`, expr)); - } - function checkDataType(dataType, data, strictNums, correct = DataType.Correct) { - const EQ = correct === DataType.Correct ? codegen_1.operators.EQ : codegen_1.operators.NEQ; - let cond; - switch (dataType) { - case "null": - return (0, codegen_1._)`${data} ${EQ} null`; - case "array": - cond = (0, codegen_1._)`Array.isArray(${data})`; - break; - case "object": - cond = (0, codegen_1._)`${data} && typeof ${data} == "object" && !Array.isArray(${data})`; - break; - case "integer": - cond = numCond((0, codegen_1._)`!(${data} % 1) && !isNaN(${data})`); - break; - case "number": - cond = numCond(); - break; - default: - return (0, codegen_1._)`typeof ${data} ${EQ} ${dataType}`; - } - return correct === DataType.Correct ? cond : (0, codegen_1.not)(cond); - function numCond(_cond = codegen_1.nil) { - return (0, codegen_1.and)((0, codegen_1._)`typeof ${data} == "number"`, _cond, strictNums ? (0, codegen_1._)`isFinite(${data})` : codegen_1.nil); - } - } - exports.checkDataType = checkDataType; - function checkDataTypes(dataTypes, data, strictNums, correct) { - if (dataTypes.length === 1) { - return checkDataType(dataTypes[0], data, strictNums, correct); - } - let cond; - const types = (0, util_1.toHash)(dataTypes); - if (types.array && types.object) { - const notObj = (0, codegen_1._)`typeof ${data} != "object"`; - cond = types.null ? notObj : (0, codegen_1._)`!${data} || ${notObj}`; - delete types.null; - delete types.array; - delete types.object; - } else { - cond = codegen_1.nil; - } - if (types.number) - delete types.integer; - for (const t in types) - cond = (0, codegen_1.and)(cond, checkDataType(t, data, strictNums, correct)); - return cond; - } - exports.checkDataTypes = checkDataTypes; - var typeError = { - message: ({ schema }) => `must be ${schema}`, - params: ({ schema, schemaValue }) => typeof schema == "string" ? (0, codegen_1._)`{type: ${schema}}` : (0, codegen_1._)`{type: ${schemaValue}}` - }; - function reportTypeError(it) { - const cxt = getTypeErrorContext(it); - (0, errors_1.reportError)(cxt, typeError); - } - exports.reportTypeError = reportTypeError; - function getTypeErrorContext(it) { - const { gen, data, schema } = it; - const schemaCode = (0, util_1.schemaRefOrVal)(it, schema, "type"); - return { - gen, - keyword: "type", - data, - schema: schema.type, - schemaCode, - schemaValue: schemaCode, - parentSchema: schema, - params: {}, - it - }; - } -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/compile/validate/defaults.js -var require_defaults = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.assignDefaults = undefined; - var codegen_1 = require_codegen(); - var util_1 = require_util(); - function assignDefaults(it, ty) { - const { properties, items } = it.schema; - if (ty === "object" && properties) { - for (const key in properties) { - assignDefault(it, key, properties[key].default); - } - } else if (ty === "array" && Array.isArray(items)) { - items.forEach((sch, i) => assignDefault(it, i, sch.default)); - } - } - exports.assignDefaults = assignDefaults; - function assignDefault(it, prop, defaultValue) { - const { gen, compositeRule, data, opts } = it; - if (defaultValue === undefined) - return; - const childData = (0, codegen_1._)`${data}${(0, codegen_1.getProperty)(prop)}`; - if (compositeRule) { - (0, util_1.checkStrictMode)(it, `default is ignored for: ${childData}`); - return; - } - let condition = (0, codegen_1._)`${childData} === undefined`; - if (opts.useDefaults === "empty") { - condition = (0, codegen_1._)`${condition} || ${childData} === null || ${childData} === ""`; - } - gen.if(condition, (0, codegen_1._)`${childData} = ${(0, codegen_1.stringify)(defaultValue)}`); - } -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/code.js -var require_code2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.validateUnion = exports.validateArray = exports.usePattern = exports.callValidateCode = exports.schemaProperties = exports.allSchemaProperties = exports.noPropertyInData = exports.propertyInData = exports.isOwnProperty = exports.hasPropFunc = exports.reportMissingProp = exports.checkMissingProp = exports.checkReportMissingProp = undefined; - var codegen_1 = require_codegen(); - var util_1 = require_util(); - var names_1 = require_names(); - var util_2 = require_util(); - function checkReportMissingProp(cxt, prop) { - const { gen, data, it } = cxt; - gen.if(noPropertyInData(gen, data, prop, it.opts.ownProperties), () => { - cxt.setParams({ missingProperty: (0, codegen_1._)`${prop}` }, true); - cxt.error(); - }); - } - exports.checkReportMissingProp = checkReportMissingProp; - function checkMissingProp({ gen, data, it: { opts } }, properties, missing) { - return (0, codegen_1.or)(...properties.map((prop) => (0, codegen_1.and)(noPropertyInData(gen, data, prop, opts.ownProperties), (0, codegen_1._)`${missing} = ${prop}`))); - } - exports.checkMissingProp = checkMissingProp; - function reportMissingProp(cxt, missing) { - cxt.setParams({ missingProperty: missing }, true); - cxt.error(); - } - exports.reportMissingProp = reportMissingProp; - function hasPropFunc(gen) { - return gen.scopeValue("func", { - ref: Object.prototype.hasOwnProperty, - code: (0, codegen_1._)`Object.prototype.hasOwnProperty` - }); - } - exports.hasPropFunc = hasPropFunc; - function isOwnProperty(gen, data, property) { - return (0, codegen_1._)`${hasPropFunc(gen)}.call(${data}, ${property})`; - } - exports.isOwnProperty = isOwnProperty; - function propertyInData(gen, data, property, ownProperties) { - const cond = (0, codegen_1._)`${data}${(0, codegen_1.getProperty)(property)} !== undefined`; - return ownProperties ? (0, codegen_1._)`${cond} && ${isOwnProperty(gen, data, property)}` : cond; - } - exports.propertyInData = propertyInData; - function noPropertyInData(gen, data, property, ownProperties) { - const cond = (0, codegen_1._)`${data}${(0, codegen_1.getProperty)(property)} === undefined`; - return ownProperties ? (0, codegen_1.or)(cond, (0, codegen_1.not)(isOwnProperty(gen, data, property))) : cond; - } - exports.noPropertyInData = noPropertyInData; - function allSchemaProperties(schemaMap) { - return schemaMap ? Object.keys(schemaMap).filter((p) => p !== "__proto__") : []; - } - exports.allSchemaProperties = allSchemaProperties; - function schemaProperties(it, schemaMap) { - return allSchemaProperties(schemaMap).filter((p) => !(0, util_1.alwaysValidSchema)(it, schemaMap[p])); - } - exports.schemaProperties = schemaProperties; - function callValidateCode({ schemaCode, data, it: { gen, topSchemaRef, schemaPath, errorPath }, it }, func, context, passSchema) { - const dataAndSchema = passSchema ? (0, codegen_1._)`${schemaCode}, ${data}, ${topSchemaRef}${schemaPath}` : data; - const valCxt = [ - [names_1.default.instancePath, (0, codegen_1.strConcat)(names_1.default.instancePath, errorPath)], - [names_1.default.parentData, it.parentData], - [names_1.default.parentDataProperty, it.parentDataProperty], - [names_1.default.rootData, names_1.default.rootData] - ]; - if (it.opts.dynamicRef) - valCxt.push([names_1.default.dynamicAnchors, names_1.default.dynamicAnchors]); - const args = (0, codegen_1._)`${dataAndSchema}, ${gen.object(...valCxt)}`; - return context !== codegen_1.nil ? (0, codegen_1._)`${func}.call(${context}, ${args})` : (0, codegen_1._)`${func}(${args})`; - } - exports.callValidateCode = callValidateCode; - var newRegExp = (0, codegen_1._)`new RegExp`; - function usePattern({ gen, it: { opts } }, pattern) { - const u = opts.unicodeRegExp ? "u" : ""; - const { regExp } = opts.code; - const rx = regExp(pattern, u); - return gen.scopeValue("pattern", { - key: rx.toString(), - ref: rx, - code: (0, codegen_1._)`${regExp.code === "new RegExp" ? newRegExp : (0, util_2.useFunc)(gen, regExp)}(${pattern}, ${u})` - }); - } - exports.usePattern = usePattern; - function validateArray(cxt) { - const { gen, data, keyword, it } = cxt; - const valid = gen.name("valid"); - if (it.allErrors) { - const validArr = gen.let("valid", true); - validateItems(() => gen.assign(validArr, false)); - return validArr; - } - gen.var(valid, true); - validateItems(() => gen.break()); - return valid; - function validateItems(notValid) { - const len = gen.const("len", (0, codegen_1._)`${data}.length`); - gen.forRange("i", 0, len, (i) => { - cxt.subschema({ - keyword, - dataProp: i, - dataPropType: util_1.Type.Num - }, valid); - gen.if((0, codegen_1.not)(valid), notValid); - }); - } - } - exports.validateArray = validateArray; - function validateUnion(cxt) { - const { gen, schema, keyword, it } = cxt; - if (!Array.isArray(schema)) - throw new Error("ajv implementation error"); - const alwaysValid = schema.some((sch) => (0, util_1.alwaysValidSchema)(it, sch)); - if (alwaysValid && !it.opts.unevaluated) - return; - const valid = gen.let("valid", false); - const schValid = gen.name("_valid"); - gen.block(() => schema.forEach((_sch, i) => { - const schCxt = cxt.subschema({ - keyword, - schemaProp: i, - compositeRule: true - }, schValid); - gen.assign(valid, (0, codegen_1._)`${valid} || ${schValid}`); - const merged = cxt.mergeValidEvaluated(schCxt, schValid); - if (!merged) - gen.if((0, codegen_1.not)(valid)); - })); - cxt.result(valid, () => cxt.reset(), () => cxt.error(true)); - } - exports.validateUnion = validateUnion; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/compile/validate/keyword.js -var require_keyword = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.validateKeywordUsage = exports.validSchemaType = exports.funcKeywordCode = exports.macroKeywordCode = undefined; - var codegen_1 = require_codegen(); - var names_1 = require_names(); - var code_1 = require_code2(); - var errors_1 = require_errors(); - function macroKeywordCode(cxt, def) { - const { gen, keyword, schema, parentSchema, it } = cxt; - const macroSchema = def.macro.call(it.self, schema, parentSchema, it); - const schemaRef = useKeyword(gen, keyword, macroSchema); - if (it.opts.validateSchema !== false) - it.self.validateSchema(macroSchema, true); - const valid = gen.name("valid"); - cxt.subschema({ - schema: macroSchema, - schemaPath: codegen_1.nil, - errSchemaPath: `${it.errSchemaPath}/${keyword}`, - topSchemaRef: schemaRef, - compositeRule: true - }, valid); - cxt.pass(valid, () => cxt.error(true)); - } - exports.macroKeywordCode = macroKeywordCode; - function funcKeywordCode(cxt, def) { - var _a; - const { gen, keyword, schema, parentSchema, $data, it } = cxt; - checkAsyncKeyword(it, def); - const validate = !$data && def.compile ? def.compile.call(it.self, schema, parentSchema, it) : def.validate; - const validateRef = useKeyword(gen, keyword, validate); - const valid = gen.let("valid"); - cxt.block$data(valid, validateKeyword); - cxt.ok((_a = def.valid) !== null && _a !== undefined ? _a : valid); - function validateKeyword() { - if (def.errors === false) { - assignValid(); - if (def.modifying) - modifyData(cxt); - reportErrs(() => cxt.error()); - } else { - const ruleErrs = def.async ? validateAsync() : validateSync(); - if (def.modifying) - modifyData(cxt); - reportErrs(() => addErrs(cxt, ruleErrs)); - } - } - function validateAsync() { - const ruleErrs = gen.let("ruleErrs", null); - gen.try(() => assignValid((0, codegen_1._)`await `), (e) => gen.assign(valid, false).if((0, codegen_1._)`${e} instanceof ${it.ValidationError}`, () => gen.assign(ruleErrs, (0, codegen_1._)`${e}.errors`), () => gen.throw(e))); - return ruleErrs; - } - function validateSync() { - const validateErrs = (0, codegen_1._)`${validateRef}.errors`; - gen.assign(validateErrs, null); - assignValid(codegen_1.nil); - return validateErrs; - } - function assignValid(_await = def.async ? (0, codegen_1._)`await ` : codegen_1.nil) { - const passCxt = it.opts.passContext ? names_1.default.this : names_1.default.self; - const passSchema = !(("compile" in def) && !$data || def.schema === false); - gen.assign(valid, (0, codegen_1._)`${_await}${(0, code_1.callValidateCode)(cxt, validateRef, passCxt, passSchema)}`, def.modifying); - } - function reportErrs(errors3) { - var _a2; - gen.if((0, codegen_1.not)((_a2 = def.valid) !== null && _a2 !== undefined ? _a2 : valid), errors3); - } - } - exports.funcKeywordCode = funcKeywordCode; - function modifyData(cxt) { - const { gen, data, it } = cxt; - gen.if(it.parentData, () => gen.assign(data, (0, codegen_1._)`${it.parentData}[${it.parentDataProperty}]`)); - } - function addErrs(cxt, errs) { - const { gen } = cxt; - gen.if((0, codegen_1._)`Array.isArray(${errs})`, () => { - gen.assign(names_1.default.vErrors, (0, codegen_1._)`${names_1.default.vErrors} === null ? ${errs} : ${names_1.default.vErrors}.concat(${errs})`).assign(names_1.default.errors, (0, codegen_1._)`${names_1.default.vErrors}.length`); - (0, errors_1.extendErrors)(cxt); - }, () => cxt.error()); - } - function checkAsyncKeyword({ schemaEnv }, def) { - if (def.async && !schemaEnv.$async) - throw new Error("async keyword in sync schema"); - } - function useKeyword(gen, keyword, result) { - if (result === undefined) - throw new Error(`keyword "${keyword}" failed to compile`); - return gen.scopeValue("keyword", typeof result == "function" ? { ref: result } : { ref: result, code: (0, codegen_1.stringify)(result) }); - } - function validSchemaType(schema, schemaType, allowUndefined = false) { - return !schemaType.length || schemaType.some((st) => st === "array" ? Array.isArray(schema) : st === "object" ? schema && typeof schema == "object" && !Array.isArray(schema) : typeof schema == st || allowUndefined && typeof schema == "undefined"); - } - exports.validSchemaType = validSchemaType; - function validateKeywordUsage({ schema, opts, self, errSchemaPath }, def, keyword) { - if (Array.isArray(def.keyword) ? !def.keyword.includes(keyword) : def.keyword !== keyword) { - throw new Error("ajv implementation error"); - } - const deps = def.dependencies; - if (deps === null || deps === undefined ? undefined : deps.some((kwd) => !Object.prototype.hasOwnProperty.call(schema, kwd))) { - throw new Error(`parent schema must have dependencies of ${keyword}: ${deps.join(",")}`); - } - if (def.validateSchema) { - const valid = def.validateSchema(schema[keyword]); - if (!valid) { - const msg = `keyword "${keyword}" value is invalid at path "${errSchemaPath}": ` + self.errorsText(def.validateSchema.errors); - if (opts.validateSchema === "log") - self.logger.error(msg); - else - throw new Error(msg); - } - } - } - exports.validateKeywordUsage = validateKeywordUsage; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/compile/validate/subschema.js -var require_subschema = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.extendSubschemaMode = exports.extendSubschemaData = exports.getSubschema = undefined; - var codegen_1 = require_codegen(); - var util_1 = require_util(); - function getSubschema(it, { keyword, schemaProp, schema, schemaPath, errSchemaPath, topSchemaRef }) { - if (keyword !== undefined && schema !== undefined) { - throw new Error('both "keyword" and "schema" passed, only one allowed'); - } - if (keyword !== undefined) { - const sch = it.schema[keyword]; - return schemaProp === undefined ? { - schema: sch, - schemaPath: (0, codegen_1._)`${it.schemaPath}${(0, codegen_1.getProperty)(keyword)}`, - errSchemaPath: `${it.errSchemaPath}/${keyword}` - } : { - schema: sch[schemaProp], - schemaPath: (0, codegen_1._)`${it.schemaPath}${(0, codegen_1.getProperty)(keyword)}${(0, codegen_1.getProperty)(schemaProp)}`, - errSchemaPath: `${it.errSchemaPath}/${keyword}/${(0, util_1.escapeFragment)(schemaProp)}` - }; - } - if (schema !== undefined) { - if (schemaPath === undefined || errSchemaPath === undefined || topSchemaRef === undefined) { - throw new Error('"schemaPath", "errSchemaPath" and "topSchemaRef" are required with "schema"'); - } - return { - schema, - schemaPath, - topSchemaRef, - errSchemaPath - }; - } - throw new Error('either "keyword" or "schema" must be passed'); - } - exports.getSubschema = getSubschema; - function extendSubschemaData(subschema, it, { dataProp, dataPropType: dpType, data, dataTypes, propertyName }) { - if (data !== undefined && dataProp !== undefined) { - throw new Error('both "data" and "dataProp" passed, only one allowed'); - } - const { gen } = it; - if (dataProp !== undefined) { - const { errorPath, dataPathArr, opts } = it; - const nextData = gen.let("data", (0, codegen_1._)`${it.data}${(0, codegen_1.getProperty)(dataProp)}`, true); - dataContextProps(nextData); - subschema.errorPath = (0, codegen_1.str)`${errorPath}${(0, util_1.getErrorPath)(dataProp, dpType, opts.jsPropertySyntax)}`; - subschema.parentDataProperty = (0, codegen_1._)`${dataProp}`; - subschema.dataPathArr = [...dataPathArr, subschema.parentDataProperty]; - } - if (data !== undefined) { - const nextData = data instanceof codegen_1.Name ? data : gen.let("data", data, true); - dataContextProps(nextData); - if (propertyName !== undefined) - subschema.propertyName = propertyName; - } - if (dataTypes) - subschema.dataTypes = dataTypes; - function dataContextProps(_nextData) { - subschema.data = _nextData; - subschema.dataLevel = it.dataLevel + 1; - subschema.dataTypes = []; - it.definedProperties = new Set; - subschema.parentData = it.data; - subschema.dataNames = [...it.dataNames, _nextData]; - } - } - exports.extendSubschemaData = extendSubschemaData; - function extendSubschemaMode(subschema, { jtdDiscriminator, jtdMetadata, compositeRule, createErrors, allErrors }) { - if (compositeRule !== undefined) - subschema.compositeRule = compositeRule; - if (createErrors !== undefined) - subschema.createErrors = createErrors; - if (allErrors !== undefined) - subschema.allErrors = allErrors; - subschema.jtdDiscriminator = jtdDiscriminator; - subschema.jtdMetadata = jtdMetadata; - } - exports.extendSubschemaMode = extendSubschemaMode; -}); - -// node_modules/fast-deep-equal/index.js -var require_fast_deep_equal = __commonJS((exports, module) => { - module.exports = function equal(a, b) { - if (a === b) - return true; - if (a && b && typeof a == "object" && typeof b == "object") { - if (a.constructor !== b.constructor) - return false; - var length, i, keys; - if (Array.isArray(a)) { - length = a.length; - if (length != b.length) - return false; - for (i = length;i-- !== 0; ) - if (!equal(a[i], b[i])) - return false; - return true; - } - if (a.constructor === RegExp) - return a.source === b.source && a.flags === b.flags; - if (a.valueOf !== Object.prototype.valueOf) - return a.valueOf() === b.valueOf(); - if (a.toString !== Object.prototype.toString) - return a.toString() === b.toString(); - keys = Object.keys(a); - length = keys.length; - if (length !== Object.keys(b).length) - return false; - for (i = length;i-- !== 0; ) - if (!Object.prototype.hasOwnProperty.call(b, keys[i])) - return false; - for (i = length;i-- !== 0; ) { - var key = keys[i]; - if (!equal(a[key], b[key])) - return false; - } - return true; - } - return a !== a && b !== b; - }; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/node_modules/json-schema-traverse/index.js -var require_json_schema_traverse = __commonJS((exports, module) => { - var traverse = module.exports = function(schema, opts, cb) { - if (typeof opts == "function") { - cb = opts; - opts = {}; - } - cb = opts.cb || cb; - var pre = typeof cb == "function" ? cb : cb.pre || function() {}; - var post = cb.post || function() {}; - _traverse(opts, pre, post, schema, "", schema); - }; - traverse.keywords = { - additionalItems: true, - items: true, - contains: true, - additionalProperties: true, - propertyNames: true, - not: true, - if: true, - then: true, - else: true - }; - traverse.arrayKeywords = { - items: true, - allOf: true, - anyOf: true, - oneOf: true - }; - traverse.propsKeywords = { - $defs: true, - definitions: true, - properties: true, - patternProperties: true, - dependencies: true - }; - traverse.skipKeywords = { - default: true, - enum: true, - const: true, - required: true, - maximum: true, - minimum: true, - exclusiveMaximum: true, - exclusiveMinimum: true, - multipleOf: true, - maxLength: true, - minLength: true, - pattern: true, - format: true, - maxItems: true, - minItems: true, - uniqueItems: true, - maxProperties: true, - minProperties: true - }; - function _traverse(opts, pre, post, schema, jsonPtr, rootSchema, parentJsonPtr, parentKeyword, parentSchema, keyIndex) { - if (schema && typeof schema == "object" && !Array.isArray(schema)) { - pre(schema, jsonPtr, rootSchema, parentJsonPtr, parentKeyword, parentSchema, keyIndex); - for (var key in schema) { - var sch = schema[key]; - if (Array.isArray(sch)) { - if (key in traverse.arrayKeywords) { - for (var i = 0;i < sch.length; i++) - _traverse(opts, pre, post, sch[i], jsonPtr + "/" + key + "/" + i, rootSchema, jsonPtr, key, schema, i); - } - } else if (key in traverse.propsKeywords) { - if (sch && typeof sch == "object") { - for (var prop in sch) - _traverse(opts, pre, post, sch[prop], jsonPtr + "/" + key + "/" + escapeJsonPtr(prop), rootSchema, jsonPtr, key, schema, prop); - } - } else if (key in traverse.keywords || opts.allKeys && !(key in traverse.skipKeywords)) { - _traverse(opts, pre, post, sch, jsonPtr + "/" + key, rootSchema, jsonPtr, key, schema); - } - } - post(schema, jsonPtr, rootSchema, parentJsonPtr, parentKeyword, parentSchema, keyIndex); - } - } - function escapeJsonPtr(str) { - return str.replace(/~/g, "~0").replace(/\//g, "~1"); - } -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/compile/resolve.js -var require_resolve = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.getSchemaRefs = exports.resolveUrl = exports.normalizeId = exports._getFullPath = exports.getFullPath = exports.inlineRef = undefined; - var util_1 = require_util(); - var equal = require_fast_deep_equal(); - var traverse = require_json_schema_traverse(); - var SIMPLE_INLINED = new Set([ - "type", - "format", - "pattern", - "maxLength", - "minLength", - "maxProperties", - "minProperties", - "maxItems", - "minItems", - "maximum", - "minimum", - "uniqueItems", - "multipleOf", - "required", - "enum", - "const" - ]); - function inlineRef(schema, limit = true) { - if (typeof schema == "boolean") - return true; - if (limit === true) - return !hasRef(schema); - if (!limit) - return false; - return countKeys(schema) <= limit; - } - exports.inlineRef = inlineRef; - var REF_KEYWORDS = new Set([ - "$ref", - "$recursiveRef", - "$recursiveAnchor", - "$dynamicRef", - "$dynamicAnchor" - ]); - function hasRef(schema) { - for (const key in schema) { - if (REF_KEYWORDS.has(key)) - return true; - const sch = schema[key]; - if (Array.isArray(sch) && sch.some(hasRef)) - return true; - if (typeof sch == "object" && hasRef(sch)) - return true; - } - return false; - } - function countKeys(schema) { - let count = 0; - for (const key in schema) { - if (key === "$ref") - return Infinity; - count++; - if (SIMPLE_INLINED.has(key)) - continue; - if (typeof schema[key] == "object") { - (0, util_1.eachItem)(schema[key], (sch) => count += countKeys(sch)); - } - if (count === Infinity) - return Infinity; - } - return count; - } - function getFullPath(resolver, id = "", normalize) { - if (normalize !== false) - id = normalizeId(id); - const p = resolver.parse(id); - return _getFullPath(resolver, p); - } - exports.getFullPath = getFullPath; - function _getFullPath(resolver, p) { - const serialized = resolver.serialize(p); - return serialized.split("#")[0] + "#"; - } - exports._getFullPath = _getFullPath; - var TRAILING_SLASH_HASH = /#\/?$/; - function normalizeId(id) { - return id ? id.replace(TRAILING_SLASH_HASH, "") : ""; - } - exports.normalizeId = normalizeId; - function resolveUrl(resolver, baseId, id) { - id = normalizeId(id); - return resolver.resolve(baseId, id); - } - exports.resolveUrl = resolveUrl; - var ANCHOR = /^[a-z_][-a-z0-9._]*$/i; - function getSchemaRefs(schema, baseId) { - if (typeof schema == "boolean") - return {}; - const { schemaId, uriResolver } = this.opts; - const schId = normalizeId(schema[schemaId] || baseId); - const baseIds = { "": schId }; - const pathPrefix = getFullPath(uriResolver, schId, false); - const localRefs = {}; - const schemaRefs = new Set; - traverse(schema, { allKeys: true }, (sch, jsonPtr, _, parentJsonPtr) => { - if (parentJsonPtr === undefined) - return; - const fullPath = pathPrefix + jsonPtr; - let innerBaseId = baseIds[parentJsonPtr]; - if (typeof sch[schemaId] == "string") - innerBaseId = addRef.call(this, sch[schemaId]); - addAnchor.call(this, sch.$anchor); - addAnchor.call(this, sch.$dynamicAnchor); - baseIds[jsonPtr] = innerBaseId; - function addRef(ref) { - const _resolve = this.opts.uriResolver.resolve; - ref = normalizeId(innerBaseId ? _resolve(innerBaseId, ref) : ref); - if (schemaRefs.has(ref)) - throw ambiguos(ref); - schemaRefs.add(ref); - let schOrRef = this.refs[ref]; - if (typeof schOrRef == "string") - schOrRef = this.refs[schOrRef]; - if (typeof schOrRef == "object") { - checkAmbiguosRef(sch, schOrRef.schema, ref); - } else if (ref !== normalizeId(fullPath)) { - if (ref[0] === "#") { - checkAmbiguosRef(sch, localRefs[ref], ref); - localRefs[ref] = sch; - } else { - this.refs[ref] = fullPath; - } - } - return ref; - } - function addAnchor(anchor) { - if (typeof anchor == "string") { - if (!ANCHOR.test(anchor)) - throw new Error(`invalid anchor "${anchor}"`); - addRef.call(this, `#${anchor}`); - } - } - }); - return localRefs; - function checkAmbiguosRef(sch1, sch2, ref) { - if (sch2 !== undefined && !equal(sch1, sch2)) - throw ambiguos(ref); - } - function ambiguos(ref) { - return new Error(`reference "${ref}" resolves to more than one schema`); - } - } - exports.getSchemaRefs = getSchemaRefs; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/compile/validate/index.js -var require_validate = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.getData = exports.KeywordCxt = exports.validateFunctionCode = undefined; - var boolSchema_1 = require_boolSchema(); - var dataType_1 = require_dataType(); - var applicability_1 = require_applicability(); - var dataType_2 = require_dataType(); - var defaults_1 = require_defaults(); - var keyword_1 = require_keyword(); - var subschema_1 = require_subschema(); - var codegen_1 = require_codegen(); - var names_1 = require_names(); - var resolve_1 = require_resolve(); - var util_1 = require_util(); - var errors_1 = require_errors(); - function validateFunctionCode(it) { - if (isSchemaObj(it)) { - checkKeywords(it); - if (schemaCxtHasRules(it)) { - topSchemaObjCode(it); - return; - } - } - validateFunction(it, () => (0, boolSchema_1.topBoolOrEmptySchema)(it)); - } - exports.validateFunctionCode = validateFunctionCode; - function validateFunction({ gen, validateName, schema, schemaEnv, opts }, body) { - if (opts.code.es5) { - gen.func(validateName, (0, codegen_1._)`${names_1.default.data}, ${names_1.default.valCxt}`, schemaEnv.$async, () => { - gen.code((0, codegen_1._)`"use strict"; ${funcSourceUrl(schema, opts)}`); - destructureValCxtES5(gen, opts); - gen.code(body); - }); - } else { - gen.func(validateName, (0, codegen_1._)`${names_1.default.data}, ${destructureValCxt(opts)}`, schemaEnv.$async, () => gen.code(funcSourceUrl(schema, opts)).code(body)); - } - } - function destructureValCxt(opts) { - return (0, codegen_1._)`{${names_1.default.instancePath}="", ${names_1.default.parentData}, ${names_1.default.parentDataProperty}, ${names_1.default.rootData}=${names_1.default.data}${opts.dynamicRef ? (0, codegen_1._)`, ${names_1.default.dynamicAnchors}={}` : codegen_1.nil}}={}`; - } - function destructureValCxtES5(gen, opts) { - gen.if(names_1.default.valCxt, () => { - gen.var(names_1.default.instancePath, (0, codegen_1._)`${names_1.default.valCxt}.${names_1.default.instancePath}`); - gen.var(names_1.default.parentData, (0, codegen_1._)`${names_1.default.valCxt}.${names_1.default.parentData}`); - gen.var(names_1.default.parentDataProperty, (0, codegen_1._)`${names_1.default.valCxt}.${names_1.default.parentDataProperty}`); - gen.var(names_1.default.rootData, (0, codegen_1._)`${names_1.default.valCxt}.${names_1.default.rootData}`); - if (opts.dynamicRef) - gen.var(names_1.default.dynamicAnchors, (0, codegen_1._)`${names_1.default.valCxt}.${names_1.default.dynamicAnchors}`); - }, () => { - gen.var(names_1.default.instancePath, (0, codegen_1._)`""`); - gen.var(names_1.default.parentData, (0, codegen_1._)`undefined`); - gen.var(names_1.default.parentDataProperty, (0, codegen_1._)`undefined`); - gen.var(names_1.default.rootData, names_1.default.data); - if (opts.dynamicRef) - gen.var(names_1.default.dynamicAnchors, (0, codegen_1._)`{}`); - }); - } - function topSchemaObjCode(it) { - const { schema, opts, gen } = it; - validateFunction(it, () => { - if (opts.$comment && schema.$comment) - commentKeyword(it); - checkNoDefault(it); - gen.let(names_1.default.vErrors, null); - gen.let(names_1.default.errors, 0); - if (opts.unevaluated) - resetEvaluated(it); - typeAndKeywords(it); - returnResults(it); - }); - return; - } - function resetEvaluated(it) { - const { gen, validateName } = it; - it.evaluated = gen.const("evaluated", (0, codegen_1._)`${validateName}.evaluated`); - gen.if((0, codegen_1._)`${it.evaluated}.dynamicProps`, () => gen.assign((0, codegen_1._)`${it.evaluated}.props`, (0, codegen_1._)`undefined`)); - gen.if((0, codegen_1._)`${it.evaluated}.dynamicItems`, () => gen.assign((0, codegen_1._)`${it.evaluated}.items`, (0, codegen_1._)`undefined`)); - } - function funcSourceUrl(schema, opts) { - const schId = typeof schema == "object" && schema[opts.schemaId]; - return schId && (opts.code.source || opts.code.process) ? (0, codegen_1._)`/*# sourceURL=${schId} */` : codegen_1.nil; - } - function subschemaCode(it, valid) { - if (isSchemaObj(it)) { - checkKeywords(it); - if (schemaCxtHasRules(it)) { - subSchemaObjCode(it, valid); - return; - } - } - (0, boolSchema_1.boolOrEmptySchema)(it, valid); - } - function schemaCxtHasRules({ schema, self }) { - if (typeof schema == "boolean") - return !schema; - for (const key in schema) - if (self.RULES.all[key]) - return true; - return false; - } - function isSchemaObj(it) { - return typeof it.schema != "boolean"; - } - function subSchemaObjCode(it, valid) { - const { schema, gen, opts } = it; - if (opts.$comment && schema.$comment) - commentKeyword(it); - updateContext(it); - checkAsyncSchema(it); - const errsCount = gen.const("_errs", names_1.default.errors); - typeAndKeywords(it, errsCount); - gen.var(valid, (0, codegen_1._)`${errsCount} === ${names_1.default.errors}`); - } - function checkKeywords(it) { - (0, util_1.checkUnknownRules)(it); - checkRefsAndKeywords(it); - } - function typeAndKeywords(it, errsCount) { - if (it.opts.jtd) - return schemaKeywords(it, [], false, errsCount); - const types = (0, dataType_1.getSchemaTypes)(it.schema); - const checkedTypes = (0, dataType_1.coerceAndCheckDataType)(it, types); - schemaKeywords(it, types, !checkedTypes, errsCount); - } - function checkRefsAndKeywords(it) { - const { schema, errSchemaPath, opts, self } = it; - if (schema.$ref && opts.ignoreKeywordsWithRef && (0, util_1.schemaHasRulesButRef)(schema, self.RULES)) { - self.logger.warn(`$ref: keywords ignored in schema at path "${errSchemaPath}"`); - } - } - function checkNoDefault(it) { - const { schema, opts } = it; - if (schema.default !== undefined && opts.useDefaults && opts.strictSchema) { - (0, util_1.checkStrictMode)(it, "default is ignored in the schema root"); - } - } - function updateContext(it) { - const schId = it.schema[it.opts.schemaId]; - if (schId) - it.baseId = (0, resolve_1.resolveUrl)(it.opts.uriResolver, it.baseId, schId); - } - function checkAsyncSchema(it) { - if (it.schema.$async && !it.schemaEnv.$async) - throw new Error("async schema in sync schema"); - } - function commentKeyword({ gen, schemaEnv, schema, errSchemaPath, opts }) { - const msg = schema.$comment; - if (opts.$comment === true) { - gen.code((0, codegen_1._)`${names_1.default.self}.logger.log(${msg})`); - } else if (typeof opts.$comment == "function") { - const schemaPath = (0, codegen_1.str)`${errSchemaPath}/$comment`; - const rootName = gen.scopeValue("root", { ref: schemaEnv.root }); - gen.code((0, codegen_1._)`${names_1.default.self}.opts.$comment(${msg}, ${schemaPath}, ${rootName}.schema)`); - } - } - function returnResults(it) { - const { gen, schemaEnv, validateName, ValidationError, opts } = it; - if (schemaEnv.$async) { - gen.if((0, codegen_1._)`${names_1.default.errors} === 0`, () => gen.return(names_1.default.data), () => gen.throw((0, codegen_1._)`new ${ValidationError}(${names_1.default.vErrors})`)); - } else { - gen.assign((0, codegen_1._)`${validateName}.errors`, names_1.default.vErrors); - if (opts.unevaluated) - assignEvaluated(it); - gen.return((0, codegen_1._)`${names_1.default.errors} === 0`); - } - } - function assignEvaluated({ gen, evaluated, props, items }) { - if (props instanceof codegen_1.Name) - gen.assign((0, codegen_1._)`${evaluated}.props`, props); - if (items instanceof codegen_1.Name) - gen.assign((0, codegen_1._)`${evaluated}.items`, items); - } - function schemaKeywords(it, types, typeErrors, errsCount) { - const { gen, schema, data, allErrors, opts, self } = it; - const { RULES } = self; - if (schema.$ref && (opts.ignoreKeywordsWithRef || !(0, util_1.schemaHasRulesButRef)(schema, RULES))) { - gen.block(() => keywordCode(it, "$ref", RULES.all.$ref.definition)); - return; - } - if (!opts.jtd) - checkStrictTypes(it, types); - gen.block(() => { - for (const group of RULES.rules) - groupKeywords(group); - groupKeywords(RULES.post); - }); - function groupKeywords(group) { - if (!(0, applicability_1.shouldUseGroup)(schema, group)) - return; - if (group.type) { - gen.if((0, dataType_2.checkDataType)(group.type, data, opts.strictNumbers)); - iterateKeywords(it, group); - if (types.length === 1 && types[0] === group.type && typeErrors) { - gen.else(); - (0, dataType_2.reportTypeError)(it); - } - gen.endIf(); - } else { - iterateKeywords(it, group); - } - if (!allErrors) - gen.if((0, codegen_1._)`${names_1.default.errors} === ${errsCount || 0}`); - } - } - function iterateKeywords(it, group) { - const { gen, schema, opts: { useDefaults } } = it; - if (useDefaults) - (0, defaults_1.assignDefaults)(it, group.type); - gen.block(() => { - for (const rule of group.rules) { - if ((0, applicability_1.shouldUseRule)(schema, rule)) { - keywordCode(it, rule.keyword, rule.definition, group.type); - } - } - }); - } - function checkStrictTypes(it, types) { - if (it.schemaEnv.meta || !it.opts.strictTypes) - return; - checkContextTypes(it, types); - if (!it.opts.allowUnionTypes) - checkMultipleTypes(it, types); - checkKeywordTypes(it, it.dataTypes); - } - function checkContextTypes(it, types) { - if (!types.length) - return; - if (!it.dataTypes.length) { - it.dataTypes = types; - return; - } - types.forEach((t) => { - if (!includesType(it.dataTypes, t)) { - strictTypesError(it, `type "${t}" not allowed by context "${it.dataTypes.join(",")}"`); - } - }); - narrowSchemaTypes(it, types); - } - function checkMultipleTypes(it, ts) { - if (ts.length > 1 && !(ts.length === 2 && ts.includes("null"))) { - strictTypesError(it, "use allowUnionTypes to allow union type keyword"); - } - } - function checkKeywordTypes(it, ts) { - const rules = it.self.RULES.all; - for (const keyword in rules) { - const rule = rules[keyword]; - if (typeof rule == "object" && (0, applicability_1.shouldUseRule)(it.schema, rule)) { - const { type } = rule.definition; - if (type.length && !type.some((t) => hasApplicableType(ts, t))) { - strictTypesError(it, `missing type "${type.join(",")}" for keyword "${keyword}"`); - } - } - } - } - function hasApplicableType(schTs, kwdT) { - return schTs.includes(kwdT) || kwdT === "number" && schTs.includes("integer"); - } - function includesType(ts, t) { - return ts.includes(t) || t === "integer" && ts.includes("number"); - } - function narrowSchemaTypes(it, withTypes) { - const ts = []; - for (const t of it.dataTypes) { - if (includesType(withTypes, t)) - ts.push(t); - else if (withTypes.includes("integer") && t === "number") - ts.push("integer"); - } - it.dataTypes = ts; - } - function strictTypesError(it, msg) { - const schemaPath = it.schemaEnv.baseId + it.errSchemaPath; - msg += ` at "${schemaPath}" (strictTypes)`; - (0, util_1.checkStrictMode)(it, msg, it.opts.strictTypes); - } - - class KeywordCxt { - constructor(it, def, keyword) { - (0, keyword_1.validateKeywordUsage)(it, def, keyword); - this.gen = it.gen; - this.allErrors = it.allErrors; - this.keyword = keyword; - this.data = it.data; - this.schema = it.schema[keyword]; - this.$data = def.$data && it.opts.$data && this.schema && this.schema.$data; - this.schemaValue = (0, util_1.schemaRefOrVal)(it, this.schema, keyword, this.$data); - this.schemaType = def.schemaType; - this.parentSchema = it.schema; - this.params = {}; - this.it = it; - this.def = def; - if (this.$data) { - this.schemaCode = it.gen.const("vSchema", getData(this.$data, it)); - } else { - this.schemaCode = this.schemaValue; - if (!(0, keyword_1.validSchemaType)(this.schema, def.schemaType, def.allowUndefined)) { - throw new Error(`${keyword} value must be ${JSON.stringify(def.schemaType)}`); - } - } - if ("code" in def ? def.trackErrors : def.errors !== false) { - this.errsCount = it.gen.const("_errs", names_1.default.errors); - } - } - result(condition, successAction, failAction) { - this.failResult((0, codegen_1.not)(condition), successAction, failAction); - } - failResult(condition, successAction, failAction) { - this.gen.if(condition); - if (failAction) - failAction(); - else - this.error(); - if (successAction) { - this.gen.else(); - successAction(); - if (this.allErrors) - this.gen.endIf(); - } else { - if (this.allErrors) - this.gen.endIf(); - else - this.gen.else(); - } - } - pass(condition, failAction) { - this.failResult((0, codegen_1.not)(condition), undefined, failAction); - } - fail(condition) { - if (condition === undefined) { - this.error(); - if (!this.allErrors) - this.gen.if(false); - return; - } - this.gen.if(condition); - this.error(); - if (this.allErrors) - this.gen.endIf(); - else - this.gen.else(); - } - fail$data(condition) { - if (!this.$data) - return this.fail(condition); - const { schemaCode } = this; - this.fail((0, codegen_1._)`${schemaCode} !== undefined && (${(0, codegen_1.or)(this.invalid$data(), condition)})`); - } - error(append, errorParams, errorPaths) { - if (errorParams) { - this.setParams(errorParams); - this._error(append, errorPaths); - this.setParams({}); - return; - } - this._error(append, errorPaths); - } - _error(append, errorPaths) { - (append ? errors_1.reportExtraError : errors_1.reportError)(this, this.def.error, errorPaths); - } - $dataError() { - (0, errors_1.reportError)(this, this.def.$dataError || errors_1.keyword$DataError); - } - reset() { - if (this.errsCount === undefined) - throw new Error('add "trackErrors" to keyword definition'); - (0, errors_1.resetErrorsCount)(this.gen, this.errsCount); - } - ok(cond) { - if (!this.allErrors) - this.gen.if(cond); - } - setParams(obj, assign) { - if (assign) - Object.assign(this.params, obj); - else - this.params = obj; - } - block$data(valid, codeBlock, $dataValid = codegen_1.nil) { - this.gen.block(() => { - this.check$data(valid, $dataValid); - codeBlock(); - }); - } - check$data(valid = codegen_1.nil, $dataValid = codegen_1.nil) { - if (!this.$data) - return; - const { gen, schemaCode, schemaType, def } = this; - gen.if((0, codegen_1.or)((0, codegen_1._)`${schemaCode} === undefined`, $dataValid)); - if (valid !== codegen_1.nil) - gen.assign(valid, true); - if (schemaType.length || def.validateSchema) { - gen.elseIf(this.invalid$data()); - this.$dataError(); - if (valid !== codegen_1.nil) - gen.assign(valid, false); - } - gen.else(); - } - invalid$data() { - const { gen, schemaCode, schemaType, def, it } = this; - return (0, codegen_1.or)(wrong$DataType(), invalid$DataSchema()); - function wrong$DataType() { - if (schemaType.length) { - if (!(schemaCode instanceof codegen_1.Name)) - throw new Error("ajv implementation error"); - const st = Array.isArray(schemaType) ? schemaType : [schemaType]; - return (0, codegen_1._)`${(0, dataType_2.checkDataTypes)(st, schemaCode, it.opts.strictNumbers, dataType_2.DataType.Wrong)}`; - } - return codegen_1.nil; - } - function invalid$DataSchema() { - if (def.validateSchema) { - const validateSchemaRef = gen.scopeValue("validate$data", { ref: def.validateSchema }); - return (0, codegen_1._)`!${validateSchemaRef}(${schemaCode})`; - } - return codegen_1.nil; - } - } - subschema(appl, valid) { - const subschema = (0, subschema_1.getSubschema)(this.it, appl); - (0, subschema_1.extendSubschemaData)(subschema, this.it, appl); - (0, subschema_1.extendSubschemaMode)(subschema, appl); - const nextContext = { ...this.it, ...subschema, items: undefined, props: undefined }; - subschemaCode(nextContext, valid); - return nextContext; - } - mergeEvaluated(schemaCxt, toName) { - const { it, gen } = this; - if (!it.opts.unevaluated) - return; - if (it.props !== true && schemaCxt.props !== undefined) { - it.props = util_1.mergeEvaluated.props(gen, schemaCxt.props, it.props, toName); - } - if (it.items !== true && schemaCxt.items !== undefined) { - it.items = util_1.mergeEvaluated.items(gen, schemaCxt.items, it.items, toName); - } - } - mergeValidEvaluated(schemaCxt, valid) { - const { it, gen } = this; - if (it.opts.unevaluated && (it.props !== true || it.items !== true)) { - gen.if(valid, () => this.mergeEvaluated(schemaCxt, codegen_1.Name)); - return true; - } - } - } - exports.KeywordCxt = KeywordCxt; - function keywordCode(it, keyword, def, ruleType) { - const cxt = new KeywordCxt(it, def, keyword); - if ("code" in def) { - def.code(cxt, ruleType); - } else if (cxt.$data && def.validate) { - (0, keyword_1.funcKeywordCode)(cxt, def); - } else if ("macro" in def) { - (0, keyword_1.macroKeywordCode)(cxt, def); - } else if (def.compile || def.validate) { - (0, keyword_1.funcKeywordCode)(cxt, def); - } - } - var JSON_POINTER = /^\/(?:[^~]|~0|~1)*$/; - var RELATIVE_JSON_POINTER = /^([0-9]+)(#|\/(?:[^~]|~0|~1)*)?$/; - function getData($data, { dataLevel, dataNames, dataPathArr }) { - let jsonPointer; - let data; - if ($data === "") - return names_1.default.rootData; - if ($data[0] === "/") { - if (!JSON_POINTER.test($data)) - throw new Error(`Invalid JSON-pointer: ${$data}`); - jsonPointer = $data; - data = names_1.default.rootData; - } else { - const matches = RELATIVE_JSON_POINTER.exec($data); - if (!matches) - throw new Error(`Invalid JSON-pointer: ${$data}`); - const up = +matches[1]; - jsonPointer = matches[2]; - if (jsonPointer === "#") { - if (up >= dataLevel) - throw new Error(errorMsg("property/index", up)); - return dataPathArr[dataLevel - up]; - } - if (up > dataLevel) - throw new Error(errorMsg("data", up)); - data = dataNames[dataLevel - up]; - if (!jsonPointer) - return data; - } - let expr = data; - const segments = jsonPointer.split("/"); - for (const segment of segments) { - if (segment) { - data = (0, codegen_1._)`${data}${(0, codegen_1.getProperty)((0, util_1.unescapeJsonPointer)(segment))}`; - expr = (0, codegen_1._)`${expr} && ${data}`; - } - } - return expr; - function errorMsg(pointerType, up) { - return `Cannot access ${pointerType} ${up} levels up, current level is ${dataLevel}`; - } - } - exports.getData = getData; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/runtime/validation_error.js -var require_validation_error = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - - class ValidationError extends Error { - constructor(errors3) { - super("validation failed"); - this.errors = errors3; - this.ajv = this.validation = true; - } - } - exports.default = ValidationError; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/compile/ref_error.js -var require_ref_error = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var resolve_1 = require_resolve(); - - class MissingRefError extends Error { - constructor(resolver, baseId, ref, msg) { - super(msg || `can't resolve reference ${ref} from id ${baseId}`); - this.missingRef = (0, resolve_1.resolveUrl)(resolver, baseId, ref); - this.missingSchema = (0, resolve_1.normalizeId)((0, resolve_1.getFullPath)(resolver, this.missingRef)); - } - } - exports.default = MissingRefError; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/compile/index.js -var require_compile = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.resolveSchema = exports.getCompilingSchema = exports.resolveRef = exports.compileSchema = exports.SchemaEnv = undefined; - var codegen_1 = require_codegen(); - var validation_error_1 = require_validation_error(); - var names_1 = require_names(); - var resolve_1 = require_resolve(); - var util_1 = require_util(); - var validate_1 = require_validate(); - - class SchemaEnv { - constructor(env) { - var _a; - this.refs = {}; - this.dynamicAnchors = {}; - let schema; - if (typeof env.schema == "object") - schema = env.schema; - this.schema = env.schema; - this.schemaId = env.schemaId; - this.root = env.root || this; - this.baseId = (_a = env.baseId) !== null && _a !== undefined ? _a : (0, resolve_1.normalizeId)(schema === null || schema === undefined ? undefined : schema[env.schemaId || "$id"]); - this.schemaPath = env.schemaPath; - this.localRefs = env.localRefs; - this.meta = env.meta; - this.$async = schema === null || schema === undefined ? undefined : schema.$async; - this.refs = {}; - } - } - exports.SchemaEnv = SchemaEnv; - function compileSchema(sch) { - const _sch = getCompilingSchema.call(this, sch); - if (_sch) - return _sch; - const rootId = (0, resolve_1.getFullPath)(this.opts.uriResolver, sch.root.baseId); - const { es5, lines } = this.opts.code; - const { ownProperties } = this.opts; - const gen = new codegen_1.CodeGen(this.scope, { es5, lines, ownProperties }); - let _ValidationError; - if (sch.$async) { - _ValidationError = gen.scopeValue("Error", { - ref: validation_error_1.default, - code: (0, codegen_1._)`require("ajv/dist/runtime/validation_error").default` - }); - } - const validateName = gen.scopeName("validate"); - sch.validateName = validateName; - const schemaCxt = { - gen, - allErrors: this.opts.allErrors, - data: names_1.default.data, - parentData: names_1.default.parentData, - parentDataProperty: names_1.default.parentDataProperty, - dataNames: [names_1.default.data], - dataPathArr: [codegen_1.nil], - dataLevel: 0, - dataTypes: [], - definedProperties: new Set, - topSchemaRef: gen.scopeValue("schema", this.opts.code.source === true ? { ref: sch.schema, code: (0, codegen_1.stringify)(sch.schema) } : { ref: sch.schema }), - validateName, - ValidationError: _ValidationError, - schema: sch.schema, - schemaEnv: sch, - rootId, - baseId: sch.baseId || rootId, - schemaPath: codegen_1.nil, - errSchemaPath: sch.schemaPath || (this.opts.jtd ? "" : "#"), - errorPath: (0, codegen_1._)`""`, - opts: this.opts, - self: this - }; - let sourceCode; - try { - this._compilations.add(sch); - (0, validate_1.validateFunctionCode)(schemaCxt); - gen.optimize(this.opts.code.optimize); - const validateCode = gen.toString(); - sourceCode = `${gen.scopeRefs(names_1.default.scope)}return ${validateCode}`; - if (this.opts.code.process) - sourceCode = this.opts.code.process(sourceCode, sch); - const makeValidate = new Function(`${names_1.default.self}`, `${names_1.default.scope}`, sourceCode); - const validate = makeValidate(this, this.scope.get()); - this.scope.value(validateName, { ref: validate }); - validate.errors = null; - validate.schema = sch.schema; - validate.schemaEnv = sch; - if (sch.$async) - validate.$async = true; - if (this.opts.code.source === true) { - validate.source = { validateName, validateCode, scopeValues: gen._values }; - } - if (this.opts.unevaluated) { - const { props, items } = schemaCxt; - validate.evaluated = { - props: props instanceof codegen_1.Name ? undefined : props, - items: items instanceof codegen_1.Name ? undefined : items, - dynamicProps: props instanceof codegen_1.Name, - dynamicItems: items instanceof codegen_1.Name - }; - if (validate.source) - validate.source.evaluated = (0, codegen_1.stringify)(validate.evaluated); - } - sch.validate = validate; - return sch; - } catch (e) { - delete sch.validate; - delete sch.validateName; - if (sourceCode) - this.logger.error("Error compiling schema, function code:", sourceCode); - throw e; - } finally { - this._compilations.delete(sch); - } - } - exports.compileSchema = compileSchema; - function resolveRef(root, baseId, ref) { - var _a; - ref = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, ref); - const schOrFunc = root.refs[ref]; - if (schOrFunc) - return schOrFunc; - let _sch = resolve.call(this, root, ref); - if (_sch === undefined) { - const schema = (_a = root.localRefs) === null || _a === undefined ? undefined : _a[ref]; - const { schemaId } = this.opts; - if (schema) - _sch = new SchemaEnv({ schema, schemaId, root, baseId }); - } - if (_sch === undefined) - return; - return root.refs[ref] = inlineOrCompile.call(this, _sch); - } - exports.resolveRef = resolveRef; - function inlineOrCompile(sch) { - if ((0, resolve_1.inlineRef)(sch.schema, this.opts.inlineRefs)) - return sch.schema; - return sch.validate ? sch : compileSchema.call(this, sch); - } - function getCompilingSchema(schEnv) { - for (const sch of this._compilations) { - if (sameSchemaEnv(sch, schEnv)) - return sch; - } - } - exports.getCompilingSchema = getCompilingSchema; - function sameSchemaEnv(s1, s2) { - return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId; - } - function resolve(root, ref) { - let sch; - while (typeof (sch = this.refs[ref]) == "string") - ref = sch; - return sch || this.schemas[ref] || resolveSchema.call(this, root, ref); - } - function resolveSchema(root, ref) { - const p = this.opts.uriResolver.parse(ref); - const refPath = (0, resolve_1._getFullPath)(this.opts.uriResolver, p); - let baseId = (0, resolve_1.getFullPath)(this.opts.uriResolver, root.baseId, undefined); - if (Object.keys(root.schema).length > 0 && refPath === baseId) { - return getJsonPointer.call(this, p, root); - } - const id = (0, resolve_1.normalizeId)(refPath); - const schOrRef = this.refs[id] || this.schemas[id]; - if (typeof schOrRef == "string") { - const sch = resolveSchema.call(this, root, schOrRef); - if (typeof (sch === null || sch === undefined ? undefined : sch.schema) !== "object") - return; - return getJsonPointer.call(this, p, sch); - } - if (typeof (schOrRef === null || schOrRef === undefined ? undefined : schOrRef.schema) !== "object") - return; - if (!schOrRef.validate) - compileSchema.call(this, schOrRef); - if (id === (0, resolve_1.normalizeId)(ref)) { - const { schema } = schOrRef; - const { schemaId } = this.opts; - const schId = schema[schemaId]; - if (schId) - baseId = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, schId); - return new SchemaEnv({ schema, schemaId, root, baseId }); - } - return getJsonPointer.call(this, p, schOrRef); - } - exports.resolveSchema = resolveSchema; - var PREVENT_SCOPE_CHANGE = new Set([ - "properties", - "patternProperties", - "enum", - "dependencies", - "definitions" - ]); - function getJsonPointer(parsedRef, { baseId, schema, root }) { - var _a; - if (((_a = parsedRef.fragment) === null || _a === undefined ? undefined : _a[0]) !== "/") - return; - for (const part of parsedRef.fragment.slice(1).split("/")) { - if (typeof schema === "boolean") - return; - const partSchema = schema[(0, util_1.unescapeFragment)(part)]; - if (partSchema === undefined) - return; - schema = partSchema; - const schId = typeof schema === "object" && schema[this.opts.schemaId]; - if (!PREVENT_SCOPE_CHANGE.has(part) && schId) { - baseId = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, schId); - } - } - let env; - if (typeof schema != "boolean" && schema.$ref && !(0, util_1.schemaHasRulesButRef)(schema, this.RULES)) { - const $ref = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, schema.$ref); - env = resolveSchema.call(this, root, $ref); - } - const { schemaId } = this.opts; - env = env || new SchemaEnv({ schema, schemaId, root, baseId }); - if (env.schema !== env.root.schema) - return env; - return; - } -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/refs/data.json -var require_data = __commonJS((exports, module) => { - module.exports = { - $id: "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/data.json#", - description: "Meta-schema for $data reference (JSON AnySchema extension proposal)", - type: "object", - required: ["$data"], - properties: { - $data: { - type: "string", - anyOf: [{ format: "relative-json-pointer" }, { format: "json-pointer" }] - } - }, - additionalProperties: false - }; -}); - -// node_modules/fast-uri/lib/utils.js -var require_utils = __commonJS((exports, module) => { - var isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu); - var isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u); - function stringArrayToHexStripped(input) { - let acc = ""; - let code = 0; - let i = 0; - for (i = 0;i < input.length; i++) { - code = input[i].charCodeAt(0); - if (code === 48) { - continue; - } - if (!(code >= 48 && code <= 57 || code >= 65 && code <= 70 || code >= 97 && code <= 102)) { - return ""; - } - acc += input[i]; - break; - } - for (i += 1;i < input.length; i++) { - code = input[i].charCodeAt(0); - if (!(code >= 48 && code <= 57 || code >= 65 && code <= 70 || code >= 97 && code <= 102)) { - return ""; - } - acc += input[i]; - } - return acc; - } - var nonSimpleDomain = RegExp.prototype.test.bind(/[^!"$&'()*+,\-.;=_`a-z{}~]/u); - function consumeIsZone(buffer) { - buffer.length = 0; - return true; - } - function consumeHextets(buffer, address, output) { - if (buffer.length) { - const hex = stringArrayToHexStripped(buffer); - if (hex !== "") { - address.push(hex); - } else { - output.error = true; - return false; - } - buffer.length = 0; - } - return true; - } - function getIPV6(input) { - let tokenCount = 0; - const output = { error: false, address: "", zone: "" }; - const address = []; - const buffer = []; - let endipv6Encountered = false; - let endIpv6 = false; - let consume = consumeHextets; - for (let i = 0;i < input.length; i++) { - const cursor = input[i]; - if (cursor === "[" || cursor === "]") { - continue; - } - if (cursor === ":") { - if (endipv6Encountered === true) { - endIpv6 = true; - } - if (!consume(buffer, address, output)) { - break; - } - if (++tokenCount > 7) { - output.error = true; - break; - } - if (i > 0 && input[i - 1] === ":") { - endipv6Encountered = true; - } - address.push(":"); - continue; - } else if (cursor === "%") { - if (!consume(buffer, address, output)) { - break; - } - consume = consumeIsZone; - } else { - buffer.push(cursor); - continue; - } - } - if (buffer.length) { - if (consume === consumeIsZone) { - output.zone = buffer.join(""); - } else if (endIpv6) { - address.push(buffer.join("")); - } else { - address.push(stringArrayToHexStripped(buffer)); - } - } - output.address = address.join(""); - return output; - } - function normalizeIPv6(host) { - if (findToken(host, ":") < 2) { - return { host, isIPV6: false }; - } - const ipv62 = getIPV6(host); - if (!ipv62.error) { - let newHost = ipv62.address; - let escapedHost = ipv62.address; - if (ipv62.zone) { - newHost += "%" + ipv62.zone; - escapedHost += "%25" + ipv62.zone; - } - return { host: newHost, isIPV6: true, escapedHost }; - } else { - return { host, isIPV6: false }; - } - } - function findToken(str, token) { - let ind = 0; - for (let i = 0;i < str.length; i++) { - if (str[i] === token) - ind++; - } - return ind; - } - function removeDotSegments(path) { - let input = path; - const output = []; - let nextSlash = -1; - let len = 0; - while (len = input.length) { - if (len === 1) { - if (input === ".") { - break; - } else if (input === "/") { - output.push("/"); - break; - } else { - output.push(input); - break; - } - } else if (len === 2) { - if (input[0] === ".") { - if (input[1] === ".") { - break; - } else if (input[1] === "/") { - input = input.slice(2); - continue; - } - } else if (input[0] === "/") { - if (input[1] === "." || input[1] === "/") { - output.push("/"); - break; - } - } - } else if (len === 3) { - if (input === "/..") { - if (output.length !== 0) { - output.pop(); - } - output.push("/"); - break; - } - } - if (input[0] === ".") { - if (input[1] === ".") { - if (input[2] === "/") { - input = input.slice(3); - continue; - } - } else if (input[1] === "/") { - input = input.slice(2); - continue; - } - } else if (input[0] === "/") { - if (input[1] === ".") { - if (input[2] === "/") { - input = input.slice(2); - continue; - } else if (input[2] === ".") { - if (input[3] === "/") { - input = input.slice(3); - if (output.length !== 0) { - output.pop(); - } - continue; - } - } - } - } - if ((nextSlash = input.indexOf("/", 1)) === -1) { - output.push(input); - break; - } else { - output.push(input.slice(0, nextSlash)); - input = input.slice(nextSlash); - } - } - return output.join(""); - } - function normalizeComponentEncoding(component, esc2) { - const func = esc2 !== true ? escape : unescape; - if (component.scheme !== undefined) { - component.scheme = func(component.scheme); - } - if (component.userinfo !== undefined) { - component.userinfo = func(component.userinfo); - } - if (component.host !== undefined) { - component.host = func(component.host); - } - if (component.path !== undefined) { - component.path = func(component.path); - } - if (component.query !== undefined) { - component.query = func(component.query); - } - if (component.fragment !== undefined) { - component.fragment = func(component.fragment); - } - return component; - } - function recomposeAuthority(component) { - const uriTokens = []; - if (component.userinfo !== undefined) { - uriTokens.push(component.userinfo); - uriTokens.push("@"); - } - if (component.host !== undefined) { - let host = unescape(component.host); - if (!isIPv4(host)) { - const ipV6res = normalizeIPv6(host); - if (ipV6res.isIPV6 === true) { - host = `[${ipV6res.escapedHost}]`; - } else { - host = component.host; - } - } - uriTokens.push(host); - } - if (typeof component.port === "number" || typeof component.port === "string") { - uriTokens.push(":"); - uriTokens.push(String(component.port)); - } - return uriTokens.length ? uriTokens.join("") : undefined; - } - module.exports = { - nonSimpleDomain, - recomposeAuthority, - normalizeComponentEncoding, - removeDotSegments, - isIPv4, - isUUID, - normalizeIPv6, - stringArrayToHexStripped - }; -}); - -// node_modules/fast-uri/lib/schemes.js -var require_schemes = __commonJS((exports, module) => { - var { isUUID } = require_utils(); - var URN_REG = /([\da-z][\d\-a-z]{0,31}):((?:[\w!$'()*+,\-.:;=@]|%[\da-f]{2})+)/iu; - var supportedSchemeNames = [ - "http", - "https", - "ws", - "wss", - "urn", - "urn:uuid" - ]; - function isValidSchemeName(name) { - return supportedSchemeNames.indexOf(name) !== -1; - } - function wsIsSecure(wsComponent) { - if (wsComponent.secure === true) { - return true; - } else if (wsComponent.secure === false) { - return false; - } else if (wsComponent.scheme) { - return wsComponent.scheme.length === 3 && (wsComponent.scheme[0] === "w" || wsComponent.scheme[0] === "W") && (wsComponent.scheme[1] === "s" || wsComponent.scheme[1] === "S") && (wsComponent.scheme[2] === "s" || wsComponent.scheme[2] === "S"); - } else { - return false; - } - } - function httpParse(component) { - if (!component.host) { - component.error = component.error || "HTTP URIs must have a host."; - } - return component; - } - function httpSerialize(component) { - const secure = String(component.scheme).toLowerCase() === "https"; - if (component.port === (secure ? 443 : 80) || component.port === "") { - component.port = undefined; - } - if (!component.path) { - component.path = "/"; - } - return component; - } - function wsParse(wsComponent) { - wsComponent.secure = wsIsSecure(wsComponent); - wsComponent.resourceName = (wsComponent.path || "/") + (wsComponent.query ? "?" + wsComponent.query : ""); - wsComponent.path = undefined; - wsComponent.query = undefined; - return wsComponent; - } - function wsSerialize(wsComponent) { - if (wsComponent.port === (wsIsSecure(wsComponent) ? 443 : 80) || wsComponent.port === "") { - wsComponent.port = undefined; - } - if (typeof wsComponent.secure === "boolean") { - wsComponent.scheme = wsComponent.secure ? "wss" : "ws"; - wsComponent.secure = undefined; - } - if (wsComponent.resourceName) { - const [path, query] = wsComponent.resourceName.split("?"); - wsComponent.path = path && path !== "/" ? path : undefined; - wsComponent.query = query; - wsComponent.resourceName = undefined; - } - wsComponent.fragment = undefined; - return wsComponent; - } - function urnParse(urnComponent, options) { - if (!urnComponent.path) { - urnComponent.error = "URN can not be parsed"; - return urnComponent; - } - const matches = urnComponent.path.match(URN_REG); - if (matches) { - const scheme = options.scheme || urnComponent.scheme || "urn"; - urnComponent.nid = matches[1].toLowerCase(); - urnComponent.nss = matches[2]; - const urnScheme = `${scheme}:${options.nid || urnComponent.nid}`; - const schemeHandler = getSchemeHandler(urnScheme); - urnComponent.path = undefined; - if (schemeHandler) { - urnComponent = schemeHandler.parse(urnComponent, options); - } - } else { - urnComponent.error = urnComponent.error || "URN can not be parsed."; - } - return urnComponent; - } - function urnSerialize(urnComponent, options) { - if (urnComponent.nid === undefined) { - throw new Error("URN without nid cannot be serialized"); - } - const scheme = options.scheme || urnComponent.scheme || "urn"; - const nid = urnComponent.nid.toLowerCase(); - const urnScheme = `${scheme}:${options.nid || nid}`; - const schemeHandler = getSchemeHandler(urnScheme); - if (schemeHandler) { - urnComponent = schemeHandler.serialize(urnComponent, options); - } - const uriComponent = urnComponent; - const nss = urnComponent.nss; - uriComponent.path = `${nid || options.nid}:${nss}`; - options.skipEscape = true; - return uriComponent; - } - function urnuuidParse(urnComponent, options) { - const uuidComponent = urnComponent; - uuidComponent.uuid = uuidComponent.nss; - uuidComponent.nss = undefined; - if (!options.tolerant && (!uuidComponent.uuid || !isUUID(uuidComponent.uuid))) { - uuidComponent.error = uuidComponent.error || "UUID is not valid."; - } - return uuidComponent; - } - function urnuuidSerialize(uuidComponent) { - const urnComponent = uuidComponent; - urnComponent.nss = (uuidComponent.uuid || "").toLowerCase(); - return urnComponent; - } - var http = { - scheme: "http", - domainHost: true, - parse: httpParse, - serialize: httpSerialize - }; - var https = { - scheme: "https", - domainHost: http.domainHost, - parse: httpParse, - serialize: httpSerialize - }; - var ws = { - scheme: "ws", - domainHost: true, - parse: wsParse, - serialize: wsSerialize - }; - var wss = { - scheme: "wss", - domainHost: ws.domainHost, - parse: ws.parse, - serialize: ws.serialize - }; - var urn = { - scheme: "urn", - parse: urnParse, - serialize: urnSerialize, - skipNormalize: true - }; - var urnuuid = { - scheme: "urn:uuid", - parse: urnuuidParse, - serialize: urnuuidSerialize, - skipNormalize: true - }; - var SCHEMES = { - http, - https, - ws, - wss, - urn, - "urn:uuid": urnuuid - }; - Object.setPrototypeOf(SCHEMES, null); - function getSchemeHandler(scheme) { - return scheme && (SCHEMES[scheme] || SCHEMES[scheme.toLowerCase()]) || undefined; - } - module.exports = { - wsIsSecure, - SCHEMES, - isValidSchemeName, - getSchemeHandler - }; -}); - -// node_modules/fast-uri/index.js -var require_fast_uri = __commonJS((exports, module) => { - var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizeComponentEncoding, isIPv4, nonSimpleDomain } = require_utils(); - var { SCHEMES, getSchemeHandler } = require_schemes(); - function normalize(uri, options) { - if (typeof uri === "string") { - uri = serialize(parse5(uri, options), options); - } else if (typeof uri === "object") { - uri = parse5(serialize(uri, options), options); - } - return uri; - } - function resolve(baseURI, relativeURI, options) { - const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" }; - const resolved = resolveComponent(parse5(baseURI, schemelessOptions), parse5(relativeURI, schemelessOptions), schemelessOptions, true); - schemelessOptions.skipEscape = true; - return serialize(resolved, schemelessOptions); - } - function resolveComponent(base, relative, options, skipNormalization) { - const target = {}; - if (!skipNormalization) { - base = parse5(serialize(base, options), options); - relative = parse5(serialize(relative, options), options); - } - options = options || {}; - if (!options.tolerant && relative.scheme) { - target.scheme = relative.scheme; - target.userinfo = relative.userinfo; - target.host = relative.host; - target.port = relative.port; - target.path = removeDotSegments(relative.path || ""); - target.query = relative.query; - } else { - if (relative.userinfo !== undefined || relative.host !== undefined || relative.port !== undefined) { - target.userinfo = relative.userinfo; - target.host = relative.host; - target.port = relative.port; - target.path = removeDotSegments(relative.path || ""); - target.query = relative.query; - } else { - if (!relative.path) { - target.path = base.path; - if (relative.query !== undefined) { - target.query = relative.query; - } else { - target.query = base.query; - } - } else { - if (relative.path[0] === "/") { - target.path = removeDotSegments(relative.path); - } else { - if ((base.userinfo !== undefined || base.host !== undefined || base.port !== undefined) && !base.path) { - target.path = "/" + relative.path; - } else if (!base.path) { - target.path = relative.path; - } else { - target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative.path; - } - target.path = removeDotSegments(target.path); - } - target.query = relative.query; - } - target.userinfo = base.userinfo; - target.host = base.host; - target.port = base.port; - } - target.scheme = base.scheme; - } - target.fragment = relative.fragment; - return target; - } - function equal(uriA, uriB, options) { - if (typeof uriA === "string") { - uriA = unescape(uriA); - uriA = serialize(normalizeComponentEncoding(parse5(uriA, options), true), { ...options, skipEscape: true }); - } else if (typeof uriA === "object") { - uriA = serialize(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true }); - } - if (typeof uriB === "string") { - uriB = unescape(uriB); - uriB = serialize(normalizeComponentEncoding(parse5(uriB, options), true), { ...options, skipEscape: true }); - } else if (typeof uriB === "object") { - uriB = serialize(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true }); - } - return uriA.toLowerCase() === uriB.toLowerCase(); - } - function serialize(cmpts, opts) { - const component = { - host: cmpts.host, - scheme: cmpts.scheme, - userinfo: cmpts.userinfo, - port: cmpts.port, - path: cmpts.path, - query: cmpts.query, - nid: cmpts.nid, - nss: cmpts.nss, - uuid: cmpts.uuid, - fragment: cmpts.fragment, - reference: cmpts.reference, - resourceName: cmpts.resourceName, - secure: cmpts.secure, - error: "" - }; - const options = Object.assign({}, opts); - const uriTokens = []; - const schemeHandler = getSchemeHandler(options.scheme || component.scheme); - if (schemeHandler && schemeHandler.serialize) - schemeHandler.serialize(component, options); - if (component.path !== undefined) { - if (!options.skipEscape) { - component.path = escape(component.path); - if (component.scheme !== undefined) { - component.path = component.path.split("%3A").join(":"); - } - } else { - component.path = unescape(component.path); - } - } - if (options.reference !== "suffix" && component.scheme) { - uriTokens.push(component.scheme, ":"); - } - const authority = recomposeAuthority(component); - if (authority !== undefined) { - if (options.reference !== "suffix") { - uriTokens.push("//"); - } - uriTokens.push(authority); - if (component.path && component.path[0] !== "/") { - uriTokens.push("/"); - } - } - if (component.path !== undefined) { - let s = component.path; - if (!options.absolutePath && (!schemeHandler || !schemeHandler.absolutePath)) { - s = removeDotSegments(s); - } - if (authority === undefined && s[0] === "/" && s[1] === "/") { - s = "/%2F" + s.slice(2); - } - uriTokens.push(s); - } - if (component.query !== undefined) { - uriTokens.push("?", component.query); - } - if (component.fragment !== undefined) { - uriTokens.push("#", component.fragment); - } - return uriTokens.join(""); - } - var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u; - function parse5(uri, opts) { - const options = Object.assign({}, opts); - const parsed = { - scheme: undefined, - userinfo: undefined, - host: "", - port: undefined, - path: "", - query: undefined, - fragment: undefined - }; - let isIP = false; - if (options.reference === "suffix") { - if (options.scheme) { - uri = options.scheme + ":" + uri; - } else { - uri = "//" + uri; - } - } - const matches = uri.match(URI_PARSE); - if (matches) { - parsed.scheme = matches[1]; - parsed.userinfo = matches[3]; - parsed.host = matches[4]; - parsed.port = parseInt(matches[5], 10); - parsed.path = matches[6] || ""; - parsed.query = matches[7]; - parsed.fragment = matches[8]; - if (isNaN(parsed.port)) { - parsed.port = matches[5]; - } - if (parsed.host) { - const ipv4result = isIPv4(parsed.host); - if (ipv4result === false) { - const ipv6result = normalizeIPv6(parsed.host); - parsed.host = ipv6result.host.toLowerCase(); - isIP = ipv6result.isIPV6; - } else { - isIP = true; - } - } - if (parsed.scheme === undefined && parsed.userinfo === undefined && parsed.host === undefined && parsed.port === undefined && parsed.query === undefined && !parsed.path) { - parsed.reference = "same-document"; - } else if (parsed.scheme === undefined) { - parsed.reference = "relative"; - } else if (parsed.fragment === undefined) { - parsed.reference = "absolute"; - } else { - parsed.reference = "uri"; - } - if (options.reference && options.reference !== "suffix" && options.reference !== parsed.reference) { - parsed.error = parsed.error || "URI is not a " + options.reference + " reference."; - } - const schemeHandler = getSchemeHandler(options.scheme || parsed.scheme); - if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport)) { - if (parsed.host && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP === false && nonSimpleDomain(parsed.host)) { - try { - parsed.host = URL.domainToASCII(parsed.host.toLowerCase()); - } catch (e) { - parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e; - } - } - } - if (!schemeHandler || schemeHandler && !schemeHandler.skipNormalize) { - if (uri.indexOf("%") !== -1) { - if (parsed.scheme !== undefined) { - parsed.scheme = unescape(parsed.scheme); - } - if (parsed.host !== undefined) { - parsed.host = unescape(parsed.host); - } - } - if (parsed.path) { - parsed.path = escape(unescape(parsed.path)); - } - if (parsed.fragment) { - parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment)); - } - } - if (schemeHandler && schemeHandler.parse) { - schemeHandler.parse(parsed, options); - } - } else { - parsed.error = parsed.error || "URI can not be parsed."; - } - return parsed; - } - var fastUri = { - SCHEMES, - normalize, - resolve, - resolveComponent, - equal, - serialize, - parse: parse5 - }; - module.exports = fastUri; - module.exports.default = fastUri; - module.exports.fastUri = fastUri; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/runtime/uri.js -var require_uri = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var uri = require_fast_uri(); - uri.code = 'require("ajv/dist/runtime/uri").default'; - exports.default = uri; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/core.js -var require_core = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.CodeGen = exports.Name = exports.nil = exports.stringify = exports.str = exports._ = exports.KeywordCxt = undefined; - var validate_1 = require_validate(); - Object.defineProperty(exports, "KeywordCxt", { enumerable: true, get: function() { - return validate_1.KeywordCxt; - } }); - var codegen_1 = require_codegen(); - Object.defineProperty(exports, "_", { enumerable: true, get: function() { - return codegen_1._; - } }); - Object.defineProperty(exports, "str", { enumerable: true, get: function() { - return codegen_1.str; - } }); - Object.defineProperty(exports, "stringify", { enumerable: true, get: function() { - return codegen_1.stringify; - } }); - Object.defineProperty(exports, "nil", { enumerable: true, get: function() { - return codegen_1.nil; - } }); - Object.defineProperty(exports, "Name", { enumerable: true, get: function() { - return codegen_1.Name; - } }); - Object.defineProperty(exports, "CodeGen", { enumerable: true, get: function() { - return codegen_1.CodeGen; - } }); - var validation_error_1 = require_validation_error(); - var ref_error_1 = require_ref_error(); - var rules_1 = require_rules(); - var compile_1 = require_compile(); - var codegen_2 = require_codegen(); - var resolve_1 = require_resolve(); - var dataType_1 = require_dataType(); - var util_1 = require_util(); - var $dataRefSchema = require_data(); - var uri_1 = require_uri(); - var defaultRegExp = (str, flags) => new RegExp(str, flags); - defaultRegExp.code = "new RegExp"; - var META_IGNORE_OPTIONS = ["removeAdditional", "useDefaults", "coerceTypes"]; - var EXT_SCOPE_NAMES = new Set([ - "validate", - "serialize", - "parse", - "wrapper", - "root", - "schema", - "keyword", - "pattern", - "formats", - "validate$data", - "func", - "obj", - "Error" - ]); - var removedOptions = { - errorDataPath: "", - format: "`validateFormats: false` can be used instead.", - nullable: '"nullable" keyword is supported by default.', - jsonPointers: "Deprecated jsPropertySyntax can be used instead.", - extendRefs: "Deprecated ignoreKeywordsWithRef can be used instead.", - missingRefs: "Pass empty schema with $id that should be ignored to ajv.addSchema.", - processCode: "Use option `code: {process: (code, schemaEnv: object) => string}`", - sourceCode: "Use option `code: {source: true}`", - strictDefaults: "It is default now, see option `strict`.", - strictKeywords: "It is default now, see option `strict`.", - uniqueItems: '"uniqueItems" keyword is always validated.', - unknownFormats: "Disable strict mode or pass `true` to `ajv.addFormat` (or `formats` option).", - cache: "Map is used as cache, schema object as key.", - serialize: "Map is used as cache, schema object as key.", - ajvErrors: "It is default now." - }; - var deprecatedOptions = { - ignoreKeywordsWithRef: "", - jsPropertySyntax: "", - unicode: '"minLength"/"maxLength" account for unicode characters by default.' - }; - var MAX_EXPRESSION = 200; - function requiredOptions(o) { - var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0; - const s = o.strict; - const _optz = (_a = o.code) === null || _a === undefined ? undefined : _a.optimize; - const optimize = _optz === true || _optz === undefined ? 1 : _optz || 0; - const regExp = (_c = (_b = o.code) === null || _b === undefined ? undefined : _b.regExp) !== null && _c !== undefined ? _c : defaultRegExp; - const uriResolver = (_d = o.uriResolver) !== null && _d !== undefined ? _d : uri_1.default; - return { - strictSchema: (_f = (_e = o.strictSchema) !== null && _e !== undefined ? _e : s) !== null && _f !== undefined ? _f : true, - strictNumbers: (_h = (_g = o.strictNumbers) !== null && _g !== undefined ? _g : s) !== null && _h !== undefined ? _h : true, - strictTypes: (_k = (_j = o.strictTypes) !== null && _j !== undefined ? _j : s) !== null && _k !== undefined ? _k : "log", - strictTuples: (_m = (_l = o.strictTuples) !== null && _l !== undefined ? _l : s) !== null && _m !== undefined ? _m : "log", - strictRequired: (_p = (_o = o.strictRequired) !== null && _o !== undefined ? _o : s) !== null && _p !== undefined ? _p : false, - code: o.code ? { ...o.code, optimize, regExp } : { optimize, regExp }, - loopRequired: (_q = o.loopRequired) !== null && _q !== undefined ? _q : MAX_EXPRESSION, - loopEnum: (_r = o.loopEnum) !== null && _r !== undefined ? _r : MAX_EXPRESSION, - meta: (_s = o.meta) !== null && _s !== undefined ? _s : true, - messages: (_t = o.messages) !== null && _t !== undefined ? _t : true, - inlineRefs: (_u = o.inlineRefs) !== null && _u !== undefined ? _u : true, - schemaId: (_v = o.schemaId) !== null && _v !== undefined ? _v : "$id", - addUsedSchema: (_w = o.addUsedSchema) !== null && _w !== undefined ? _w : true, - validateSchema: (_x = o.validateSchema) !== null && _x !== undefined ? _x : true, - validateFormats: (_y = o.validateFormats) !== null && _y !== undefined ? _y : true, - unicodeRegExp: (_z = o.unicodeRegExp) !== null && _z !== undefined ? _z : true, - int32range: (_0 = o.int32range) !== null && _0 !== undefined ? _0 : true, - uriResolver - }; - } - - class Ajv { - constructor(opts = {}) { - this.schemas = {}; - this.refs = {}; - this.formats = {}; - this._compilations = new Set; - this._loading = {}; - this._cache = new Map; - opts = this.opts = { ...opts, ...requiredOptions(opts) }; - const { es5, lines } = this.opts.code; - this.scope = new codegen_2.ValueScope({ scope: {}, prefixes: EXT_SCOPE_NAMES, es5, lines }); - this.logger = getLogger(opts.logger); - const formatOpt = opts.validateFormats; - opts.validateFormats = false; - this.RULES = (0, rules_1.getRules)(); - checkOptions.call(this, removedOptions, opts, "NOT SUPPORTED"); - checkOptions.call(this, deprecatedOptions, opts, "DEPRECATED", "warn"); - this._metaOpts = getMetaSchemaOptions.call(this); - if (opts.formats) - addInitialFormats.call(this); - this._addVocabularies(); - this._addDefaultMetaSchema(); - if (opts.keywords) - addInitialKeywords.call(this, opts.keywords); - if (typeof opts.meta == "object") - this.addMetaSchema(opts.meta); - addInitialSchemas.call(this); - opts.validateFormats = formatOpt; - } - _addVocabularies() { - this.addKeyword("$async"); - } - _addDefaultMetaSchema() { - const { $data, meta, schemaId } = this.opts; - let _dataRefSchema = $dataRefSchema; - if (schemaId === "id") { - _dataRefSchema = { ...$dataRefSchema }; - _dataRefSchema.id = _dataRefSchema.$id; - delete _dataRefSchema.$id; - } - if (meta && $data) - this.addMetaSchema(_dataRefSchema, _dataRefSchema[schemaId], false); - } - defaultMeta() { - const { meta, schemaId } = this.opts; - return this.opts.defaultMeta = typeof meta == "object" ? meta[schemaId] || meta : undefined; - } - validate(schemaKeyRef, data) { - let v; - if (typeof schemaKeyRef == "string") { - v = this.getSchema(schemaKeyRef); - if (!v) - throw new Error(`no schema with key or ref "${schemaKeyRef}"`); - } else { - v = this.compile(schemaKeyRef); - } - const valid = v(data); - if (!("$async" in v)) - this.errors = v.errors; - return valid; - } - compile(schema, _meta) { - const sch = this._addSchema(schema, _meta); - return sch.validate || this._compileSchemaEnv(sch); - } - compileAsync(schema, meta) { - if (typeof this.opts.loadSchema != "function") { - throw new Error("options.loadSchema should be a function"); - } - const { loadSchema } = this.opts; - return runCompileAsync.call(this, schema, meta); - async function runCompileAsync(_schema, _meta) { - await loadMetaSchema.call(this, _schema.$schema); - const sch = this._addSchema(_schema, _meta); - return sch.validate || _compileAsync.call(this, sch); - } - async function loadMetaSchema($ref) { - if ($ref && !this.getSchema($ref)) { - await runCompileAsync.call(this, { $ref }, true); - } - } - async function _compileAsync(sch) { - try { - return this._compileSchemaEnv(sch); - } catch (e) { - if (!(e instanceof ref_error_1.default)) - throw e; - checkLoaded.call(this, e); - await loadMissingSchema.call(this, e.missingSchema); - return _compileAsync.call(this, sch); - } - } - function checkLoaded({ missingSchema: ref, missingRef }) { - if (this.refs[ref]) { - throw new Error(`AnySchema ${ref} is loaded but ${missingRef} cannot be resolved`); - } - } - async function loadMissingSchema(ref) { - const _schema = await _loadSchema.call(this, ref); - if (!this.refs[ref]) - await loadMetaSchema.call(this, _schema.$schema); - if (!this.refs[ref]) - this.addSchema(_schema, ref, meta); - } - async function _loadSchema(ref) { - const p = this._loading[ref]; - if (p) - return p; - try { - return await (this._loading[ref] = loadSchema(ref)); - } finally { - delete this._loading[ref]; - } - } - } - addSchema(schema, key, _meta, _validateSchema = this.opts.validateSchema) { - if (Array.isArray(schema)) { - for (const sch of schema) - this.addSchema(sch, undefined, _meta, _validateSchema); - return this; - } - let id; - if (typeof schema === "object") { - const { schemaId } = this.opts; - id = schema[schemaId]; - if (id !== undefined && typeof id != "string") { - throw new Error(`schema ${schemaId} must be string`); - } - } - key = (0, resolve_1.normalizeId)(key || id); - this._checkUnique(key); - this.schemas[key] = this._addSchema(schema, _meta, key, _validateSchema, true); - return this; - } - addMetaSchema(schema, key, _validateSchema = this.opts.validateSchema) { - this.addSchema(schema, key, true, _validateSchema); - return this; - } - validateSchema(schema, throwOrLogError) { - if (typeof schema == "boolean") - return true; - let $schema; - $schema = schema.$schema; - if ($schema !== undefined && typeof $schema != "string") { - throw new Error("$schema must be a string"); - } - $schema = $schema || this.opts.defaultMeta || this.defaultMeta(); - if (!$schema) { - this.logger.warn("meta-schema not available"); - this.errors = null; - return true; - } - const valid = this.validate($schema, schema); - if (!valid && throwOrLogError) { - const message = "schema is invalid: " + this.errorsText(); - if (this.opts.validateSchema === "log") - this.logger.error(message); - else - throw new Error(message); - } - return valid; - } - getSchema(keyRef) { - let sch; - while (typeof (sch = getSchEnv.call(this, keyRef)) == "string") - keyRef = sch; - if (sch === undefined) { - const { schemaId } = this.opts; - const root = new compile_1.SchemaEnv({ schema: {}, schemaId }); - sch = compile_1.resolveSchema.call(this, root, keyRef); - if (!sch) - return; - this.refs[keyRef] = sch; - } - return sch.validate || this._compileSchemaEnv(sch); - } - removeSchema(schemaKeyRef) { - if (schemaKeyRef instanceof RegExp) { - this._removeAllSchemas(this.schemas, schemaKeyRef); - this._removeAllSchemas(this.refs, schemaKeyRef); - return this; - } - switch (typeof schemaKeyRef) { - case "undefined": - this._removeAllSchemas(this.schemas); - this._removeAllSchemas(this.refs); - this._cache.clear(); - return this; - case "string": { - const sch = getSchEnv.call(this, schemaKeyRef); - if (typeof sch == "object") - this._cache.delete(sch.schema); - delete this.schemas[schemaKeyRef]; - delete this.refs[schemaKeyRef]; - return this; - } - case "object": { - const cacheKey = schemaKeyRef; - this._cache.delete(cacheKey); - let id = schemaKeyRef[this.opts.schemaId]; - if (id) { - id = (0, resolve_1.normalizeId)(id); - delete this.schemas[id]; - delete this.refs[id]; - } - return this; - } - default: - throw new Error("ajv.removeSchema: invalid parameter"); - } - } - addVocabulary(definitions) { - for (const def of definitions) - this.addKeyword(def); - return this; - } - addKeyword(kwdOrDef, def) { - let keyword; - if (typeof kwdOrDef == "string") { - keyword = kwdOrDef; - if (typeof def == "object") { - this.logger.warn("these parameters are deprecated, see docs for addKeyword"); - def.keyword = keyword; - } - } else if (typeof kwdOrDef == "object" && def === undefined) { - def = kwdOrDef; - keyword = def.keyword; - if (Array.isArray(keyword) && !keyword.length) { - throw new Error("addKeywords: keyword must be string or non-empty array"); - } - } else { - throw new Error("invalid addKeywords parameters"); - } - checkKeyword.call(this, keyword, def); - if (!def) { - (0, util_1.eachItem)(keyword, (kwd) => addRule.call(this, kwd)); - return this; - } - keywordMetaschema.call(this, def); - const definition = { - ...def, - type: (0, dataType_1.getJSONTypes)(def.type), - schemaType: (0, dataType_1.getJSONTypes)(def.schemaType) - }; - (0, util_1.eachItem)(keyword, definition.type.length === 0 ? (k) => addRule.call(this, k, definition) : (k) => definition.type.forEach((t) => addRule.call(this, k, definition, t))); - return this; - } - getKeyword(keyword) { - const rule = this.RULES.all[keyword]; - return typeof rule == "object" ? rule.definition : !!rule; - } - removeKeyword(keyword) { - const { RULES } = this; - delete RULES.keywords[keyword]; - delete RULES.all[keyword]; - for (const group of RULES.rules) { - const i = group.rules.findIndex((rule) => rule.keyword === keyword); - if (i >= 0) - group.rules.splice(i, 1); - } - return this; - } - addFormat(name, format) { - if (typeof format == "string") - format = new RegExp(format); - this.formats[name] = format; - return this; - } - errorsText(errors3 = this.errors, { separator = ", ", dataVar = "data" } = {}) { - if (!errors3 || errors3.length === 0) - return "No errors"; - return errors3.map((e) => `${dataVar}${e.instancePath} ${e.message}`).reduce((text, msg) => text + separator + msg); - } - $dataMetaSchema(metaSchema, keywordsJsonPointers) { - const rules = this.RULES.all; - metaSchema = JSON.parse(JSON.stringify(metaSchema)); - for (const jsonPointer of keywordsJsonPointers) { - const segments = jsonPointer.split("/").slice(1); - let keywords = metaSchema; - for (const seg of segments) - keywords = keywords[seg]; - for (const key in rules) { - const rule = rules[key]; - if (typeof rule != "object") - continue; - const { $data } = rule.definition; - const schema = keywords[key]; - if ($data && schema) - keywords[key] = schemaOrData(schema); - } - } - return metaSchema; - } - _removeAllSchemas(schemas3, regex) { - for (const keyRef in schemas3) { - const sch = schemas3[keyRef]; - if (!regex || regex.test(keyRef)) { - if (typeof sch == "string") { - delete schemas3[keyRef]; - } else if (sch && !sch.meta) { - this._cache.delete(sch.schema); - delete schemas3[keyRef]; - } - } - } - } - _addSchema(schema, meta, baseId, validateSchema = this.opts.validateSchema, addSchema = this.opts.addUsedSchema) { - let id; - const { schemaId } = this.opts; - if (typeof schema == "object") { - id = schema[schemaId]; - } else { - if (this.opts.jtd) - throw new Error("schema must be object"); - else if (typeof schema != "boolean") - throw new Error("schema must be object or boolean"); - } - let sch = this._cache.get(schema); - if (sch !== undefined) - return sch; - baseId = (0, resolve_1.normalizeId)(id || baseId); - const localRefs = resolve_1.getSchemaRefs.call(this, schema, baseId); - sch = new compile_1.SchemaEnv({ schema, schemaId, meta, baseId, localRefs }); - this._cache.set(sch.schema, sch); - if (addSchema && !baseId.startsWith("#")) { - if (baseId) - this._checkUnique(baseId); - this.refs[baseId] = sch; - } - if (validateSchema) - this.validateSchema(schema, true); - return sch; - } - _checkUnique(id) { - if (this.schemas[id] || this.refs[id]) { - throw new Error(`schema with key or id "${id}" already exists`); - } - } - _compileSchemaEnv(sch) { - if (sch.meta) - this._compileMetaSchema(sch); - else - compile_1.compileSchema.call(this, sch); - if (!sch.validate) - throw new Error("ajv implementation error"); - return sch.validate; - } - _compileMetaSchema(sch) { - const currentOpts = this.opts; - this.opts = this._metaOpts; - try { - compile_1.compileSchema.call(this, sch); - } finally { - this.opts = currentOpts; - } - } - } - Ajv.ValidationError = validation_error_1.default; - Ajv.MissingRefError = ref_error_1.default; - exports.default = Ajv; - function checkOptions(checkOpts, options, msg, log = "error") { - for (const key in checkOpts) { - const opt = key; - if (opt in options) - this.logger[log](`${msg}: option ${key}. ${checkOpts[opt]}`); - } - } - function getSchEnv(keyRef) { - keyRef = (0, resolve_1.normalizeId)(keyRef); - return this.schemas[keyRef] || this.refs[keyRef]; - } - function addInitialSchemas() { - const optsSchemas = this.opts.schemas; - if (!optsSchemas) - return; - if (Array.isArray(optsSchemas)) - this.addSchema(optsSchemas); - else - for (const key in optsSchemas) - this.addSchema(optsSchemas[key], key); - } - function addInitialFormats() { - for (const name in this.opts.formats) { - const format = this.opts.formats[name]; - if (format) - this.addFormat(name, format); - } - } - function addInitialKeywords(defs) { - if (Array.isArray(defs)) { - this.addVocabulary(defs); - return; - } - this.logger.warn("keywords option as map is deprecated, pass array"); - for (const keyword in defs) { - const def = defs[keyword]; - if (!def.keyword) - def.keyword = keyword; - this.addKeyword(def); - } - } - function getMetaSchemaOptions() { - const metaOpts = { ...this.opts }; - for (const opt of META_IGNORE_OPTIONS) - delete metaOpts[opt]; - return metaOpts; - } - var noLogs = { log() {}, warn() {}, error() {} }; - function getLogger(logger) { - if (logger === false) - return noLogs; - if (logger === undefined) - return console; - if (logger.log && logger.warn && logger.error) - return logger; - throw new Error("logger must implement log, warn and error methods"); - } - var KEYWORD_NAME = /^[a-z_$][a-z0-9_$:-]*$/i; - function checkKeyword(keyword, def) { - const { RULES } = this; - (0, util_1.eachItem)(keyword, (kwd) => { - if (RULES.keywords[kwd]) - throw new Error(`Keyword ${kwd} is already defined`); - if (!KEYWORD_NAME.test(kwd)) - throw new Error(`Keyword ${kwd} has invalid name`); - }); - if (!def) - return; - if (def.$data && !(("code" in def) || ("validate" in def))) { - throw new Error('$data keyword must have "code" or "validate" function'); - } - } - function addRule(keyword, definition, dataType) { - var _a; - const post = definition === null || definition === undefined ? undefined : definition.post; - if (dataType && post) - throw new Error('keyword with "post" flag cannot have "type"'); - const { RULES } = this; - let ruleGroup = post ? RULES.post : RULES.rules.find(({ type: t }) => t === dataType); - if (!ruleGroup) { - ruleGroup = { type: dataType, rules: [] }; - RULES.rules.push(ruleGroup); - } - RULES.keywords[keyword] = true; - if (!definition) - return; - const rule = { - keyword, - definition: { - ...definition, - type: (0, dataType_1.getJSONTypes)(definition.type), - schemaType: (0, dataType_1.getJSONTypes)(definition.schemaType) - } - }; - if (definition.before) - addBeforeRule.call(this, ruleGroup, rule, definition.before); - else - ruleGroup.rules.push(rule); - RULES.all[keyword] = rule; - (_a = definition.implements) === null || _a === undefined || _a.forEach((kwd) => this.addKeyword(kwd)); - } - function addBeforeRule(ruleGroup, rule, before) { - const i = ruleGroup.rules.findIndex((_rule) => _rule.keyword === before); - if (i >= 0) { - ruleGroup.rules.splice(i, 0, rule); - } else { - ruleGroup.rules.push(rule); - this.logger.warn(`rule ${before} is not defined`); - } - } - function keywordMetaschema(def) { - let { metaSchema } = def; - if (metaSchema === undefined) - return; - if (def.$data && this.opts.$data) - metaSchema = schemaOrData(metaSchema); - def.validateSchema = this.compile(metaSchema, true); - } - var $dataRef = { - $ref: "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/data.json#" - }; - function schemaOrData(schema) { - return { anyOf: [schema, $dataRef] }; - } -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/core/id.js -var require_id = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var def = { - keyword: "id", - code() { - throw new Error('NOT SUPPORTED: keyword "id", use "$id" for schema ID'); - } - }; - exports.default = def; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/core/ref.js -var require_ref = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.callRef = exports.getValidate = undefined; - var ref_error_1 = require_ref_error(); - var code_1 = require_code2(); - var codegen_1 = require_codegen(); - var names_1 = require_names(); - var compile_1 = require_compile(); - var util_1 = require_util(); - var def = { - keyword: "$ref", - schemaType: "string", - code(cxt) { - const { gen, schema: $ref, it } = cxt; - const { baseId, schemaEnv: env, validateName, opts, self } = it; - const { root } = env; - if (($ref === "#" || $ref === "#/") && baseId === root.baseId) - return callRootRef(); - const schOrEnv = compile_1.resolveRef.call(self, root, baseId, $ref); - if (schOrEnv === undefined) - throw new ref_error_1.default(it.opts.uriResolver, baseId, $ref); - if (schOrEnv instanceof compile_1.SchemaEnv) - return callValidate(schOrEnv); - return inlineRefSchema(schOrEnv); - function callRootRef() { - if (env === root) - return callRef(cxt, validateName, env, env.$async); - const rootName = gen.scopeValue("root", { ref: root }); - return callRef(cxt, (0, codegen_1._)`${rootName}.validate`, root, root.$async); - } - function callValidate(sch) { - const v = getValidate(cxt, sch); - callRef(cxt, v, sch, sch.$async); - } - function inlineRefSchema(sch) { - const schName = gen.scopeValue("schema", opts.code.source === true ? { ref: sch, code: (0, codegen_1.stringify)(sch) } : { ref: sch }); - const valid = gen.name("valid"); - const schCxt = cxt.subschema({ - schema: sch, - dataTypes: [], - schemaPath: codegen_1.nil, - topSchemaRef: schName, - errSchemaPath: $ref - }, valid); - cxt.mergeEvaluated(schCxt); - cxt.ok(valid); - } - } - }; - function getValidate(cxt, sch) { - const { gen } = cxt; - return sch.validate ? gen.scopeValue("validate", { ref: sch.validate }) : (0, codegen_1._)`${gen.scopeValue("wrapper", { ref: sch })}.validate`; - } - exports.getValidate = getValidate; - function callRef(cxt, v, sch, $async) { - const { gen, it } = cxt; - const { allErrors, schemaEnv: env, opts } = it; - const passCxt = opts.passContext ? names_1.default.this : codegen_1.nil; - if ($async) - callAsyncRef(); - else - callSyncRef(); - function callAsyncRef() { - if (!env.$async) - throw new Error("async schema referenced by sync schema"); - const valid = gen.let("valid"); - gen.try(() => { - gen.code((0, codegen_1._)`await ${(0, code_1.callValidateCode)(cxt, v, passCxt)}`); - addEvaluatedFrom(v); - if (!allErrors) - gen.assign(valid, true); - }, (e) => { - gen.if((0, codegen_1._)`!(${e} instanceof ${it.ValidationError})`, () => gen.throw(e)); - addErrorsFrom(e); - if (!allErrors) - gen.assign(valid, false); - }); - cxt.ok(valid); - } - function callSyncRef() { - cxt.result((0, code_1.callValidateCode)(cxt, v, passCxt), () => addEvaluatedFrom(v), () => addErrorsFrom(v)); - } - function addErrorsFrom(source) { - const errs = (0, codegen_1._)`${source}.errors`; - gen.assign(names_1.default.vErrors, (0, codegen_1._)`${names_1.default.vErrors} === null ? ${errs} : ${names_1.default.vErrors}.concat(${errs})`); - gen.assign(names_1.default.errors, (0, codegen_1._)`${names_1.default.vErrors}.length`); - } - function addEvaluatedFrom(source) { - var _a; - if (!it.opts.unevaluated) - return; - const schEvaluated = (_a = sch === null || sch === undefined ? undefined : sch.validate) === null || _a === undefined ? undefined : _a.evaluated; - if (it.props !== true) { - if (schEvaluated && !schEvaluated.dynamicProps) { - if (schEvaluated.props !== undefined) { - it.props = util_1.mergeEvaluated.props(gen, schEvaluated.props, it.props); - } - } else { - const props = gen.var("props", (0, codegen_1._)`${source}.evaluated.props`); - it.props = util_1.mergeEvaluated.props(gen, props, it.props, codegen_1.Name); - } - } - if (it.items !== true) { - if (schEvaluated && !schEvaluated.dynamicItems) { - if (schEvaluated.items !== undefined) { - it.items = util_1.mergeEvaluated.items(gen, schEvaluated.items, it.items); - } - } else { - const items = gen.var("items", (0, codegen_1._)`${source}.evaluated.items`); - it.items = util_1.mergeEvaluated.items(gen, items, it.items, codegen_1.Name); - } - } - } - } - exports.callRef = callRef; - exports.default = def; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/core/index.js -var require_core2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var id_1 = require_id(); - var ref_1 = require_ref(); - var core2 = [ - "$schema", - "$id", - "$defs", - "$vocabulary", - { keyword: "$comment" }, - "definitions", - id_1.default, - ref_1.default - ]; - exports.default = core2; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/validation/limitNumber.js -var require_limitNumber = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var codegen_1 = require_codegen(); - var ops = codegen_1.operators; - var KWDs = { - maximum: { okStr: "<=", ok: ops.LTE, fail: ops.GT }, - minimum: { okStr: ">=", ok: ops.GTE, fail: ops.LT }, - exclusiveMaximum: { okStr: "<", ok: ops.LT, fail: ops.GTE }, - exclusiveMinimum: { okStr: ">", ok: ops.GT, fail: ops.LTE } - }; - var error2 = { - message: ({ keyword, schemaCode }) => (0, codegen_1.str)`must be ${KWDs[keyword].okStr} ${schemaCode}`, - params: ({ keyword, schemaCode }) => (0, codegen_1._)`{comparison: ${KWDs[keyword].okStr}, limit: ${schemaCode}}` - }; - var def = { - keyword: Object.keys(KWDs), - type: "number", - schemaType: "number", - $data: true, - error: error2, - code(cxt) { - const { keyword, data, schemaCode } = cxt; - cxt.fail$data((0, codegen_1._)`${data} ${KWDs[keyword].fail} ${schemaCode} || isNaN(${data})`); - } - }; - exports.default = def; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/validation/multipleOf.js -var require_multipleOf = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var codegen_1 = require_codegen(); - var error2 = { - message: ({ schemaCode }) => (0, codegen_1.str)`must be multiple of ${schemaCode}`, - params: ({ schemaCode }) => (0, codegen_1._)`{multipleOf: ${schemaCode}}` - }; - var def = { - keyword: "multipleOf", - type: "number", - schemaType: "number", - $data: true, - error: error2, - code(cxt) { - const { gen, data, schemaCode, it } = cxt; - const prec = it.opts.multipleOfPrecision; - const res = gen.let("res"); - const invalid = prec ? (0, codegen_1._)`Math.abs(Math.round(${res}) - ${res}) > 1e-${prec}` : (0, codegen_1._)`${res} !== parseInt(${res})`; - cxt.fail$data((0, codegen_1._)`(${schemaCode} === 0 || (${res} = ${data}/${schemaCode}, ${invalid}))`); - } - }; - exports.default = def; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/runtime/ucs2length.js -var require_ucs2length = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - function ucs2length(str) { - const len = str.length; - let length = 0; - let pos = 0; - let value; - while (pos < len) { - length++; - value = str.charCodeAt(pos++); - if (value >= 55296 && value <= 56319 && pos < len) { - value = str.charCodeAt(pos); - if ((value & 64512) === 56320) - pos++; - } - } - return length; - } - exports.default = ucs2length; - ucs2length.code = 'require("ajv/dist/runtime/ucs2length").default'; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/validation/limitLength.js -var require_limitLength = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var codegen_1 = require_codegen(); - var util_1 = require_util(); - var ucs2length_1 = require_ucs2length(); - var error2 = { - message({ keyword, schemaCode }) { - const comp = keyword === "maxLength" ? "more" : "fewer"; - return (0, codegen_1.str)`must NOT have ${comp} than ${schemaCode} characters`; - }, - params: ({ schemaCode }) => (0, codegen_1._)`{limit: ${schemaCode}}` - }; - var def = { - keyword: ["maxLength", "minLength"], - type: "string", - schemaType: "number", - $data: true, - error: error2, - code(cxt) { - const { keyword, data, schemaCode, it } = cxt; - const op = keyword === "maxLength" ? codegen_1.operators.GT : codegen_1.operators.LT; - const len = it.opts.unicode === false ? (0, codegen_1._)`${data}.length` : (0, codegen_1._)`${(0, util_1.useFunc)(cxt.gen, ucs2length_1.default)}(${data})`; - cxt.fail$data((0, codegen_1._)`${len} ${op} ${schemaCode}`); - } - }; - exports.default = def; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/validation/pattern.js -var require_pattern = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var code_1 = require_code2(); - var codegen_1 = require_codegen(); - var error2 = { - message: ({ schemaCode }) => (0, codegen_1.str)`must match pattern "${schemaCode}"`, - params: ({ schemaCode }) => (0, codegen_1._)`{pattern: ${schemaCode}}` - }; - var def = { - keyword: "pattern", - type: "string", - schemaType: "string", - $data: true, - error: error2, - code(cxt) { - const { data, $data, schema, schemaCode, it } = cxt; - const u = it.opts.unicodeRegExp ? "u" : ""; - const regExp = $data ? (0, codegen_1._)`(new RegExp(${schemaCode}, ${u}))` : (0, code_1.usePattern)(cxt, schema); - cxt.fail$data((0, codegen_1._)`!${regExp}.test(${data})`); - } - }; - exports.default = def; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/validation/limitProperties.js -var require_limitProperties = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var codegen_1 = require_codegen(); - var error2 = { - message({ keyword, schemaCode }) { - const comp = keyword === "maxProperties" ? "more" : "fewer"; - return (0, codegen_1.str)`must NOT have ${comp} than ${schemaCode} properties`; - }, - params: ({ schemaCode }) => (0, codegen_1._)`{limit: ${schemaCode}}` - }; - var def = { - keyword: ["maxProperties", "minProperties"], - type: "object", - schemaType: "number", - $data: true, - error: error2, - code(cxt) { - const { keyword, data, schemaCode } = cxt; - const op = keyword === "maxProperties" ? codegen_1.operators.GT : codegen_1.operators.LT; - cxt.fail$data((0, codegen_1._)`Object.keys(${data}).length ${op} ${schemaCode}`); - } - }; - exports.default = def; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/validation/required.js -var require_required = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var code_1 = require_code2(); - var codegen_1 = require_codegen(); - var util_1 = require_util(); - var error2 = { - message: ({ params: { missingProperty } }) => (0, codegen_1.str)`must have required property '${missingProperty}'`, - params: ({ params: { missingProperty } }) => (0, codegen_1._)`{missingProperty: ${missingProperty}}` - }; - var def = { - keyword: "required", - type: "object", - schemaType: "array", - $data: true, - error: error2, - code(cxt) { - const { gen, schema, schemaCode, data, $data, it } = cxt; - const { opts } = it; - if (!$data && schema.length === 0) - return; - const useLoop = schema.length >= opts.loopRequired; - if (it.allErrors) - allErrorsMode(); - else - exitOnErrorMode(); - if (opts.strictRequired) { - const props = cxt.parentSchema.properties; - const { definedProperties } = cxt.it; - for (const requiredKey of schema) { - if ((props === null || props === undefined ? undefined : props[requiredKey]) === undefined && !definedProperties.has(requiredKey)) { - const schemaPath = it.schemaEnv.baseId + it.errSchemaPath; - const msg = `required property "${requiredKey}" is not defined at "${schemaPath}" (strictRequired)`; - (0, util_1.checkStrictMode)(it, msg, it.opts.strictRequired); - } - } - } - function allErrorsMode() { - if (useLoop || $data) { - cxt.block$data(codegen_1.nil, loopAllRequired); - } else { - for (const prop of schema) { - (0, code_1.checkReportMissingProp)(cxt, prop); - } - } - } - function exitOnErrorMode() { - const missing = gen.let("missing"); - if (useLoop || $data) { - const valid = gen.let("valid", true); - cxt.block$data(valid, () => loopUntilMissing(missing, valid)); - cxt.ok(valid); - } else { - gen.if((0, code_1.checkMissingProp)(cxt, schema, missing)); - (0, code_1.reportMissingProp)(cxt, missing); - gen.else(); - } - } - function loopAllRequired() { - gen.forOf("prop", schemaCode, (prop) => { - cxt.setParams({ missingProperty: prop }); - gen.if((0, code_1.noPropertyInData)(gen, data, prop, opts.ownProperties), () => cxt.error()); - }); - } - function loopUntilMissing(missing, valid) { - cxt.setParams({ missingProperty: missing }); - gen.forOf(missing, schemaCode, () => { - gen.assign(valid, (0, code_1.propertyInData)(gen, data, missing, opts.ownProperties)); - gen.if((0, codegen_1.not)(valid), () => { - cxt.error(); - gen.break(); - }); - }, codegen_1.nil); - } - } - }; - exports.default = def; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/validation/limitItems.js -var require_limitItems = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var codegen_1 = require_codegen(); - var error2 = { - message({ keyword, schemaCode }) { - const comp = keyword === "maxItems" ? "more" : "fewer"; - return (0, codegen_1.str)`must NOT have ${comp} than ${schemaCode} items`; - }, - params: ({ schemaCode }) => (0, codegen_1._)`{limit: ${schemaCode}}` - }; - var def = { - keyword: ["maxItems", "minItems"], - type: "array", - schemaType: "number", - $data: true, - error: error2, - code(cxt) { - const { keyword, data, schemaCode } = cxt; - const op = keyword === "maxItems" ? codegen_1.operators.GT : codegen_1.operators.LT; - cxt.fail$data((0, codegen_1._)`${data}.length ${op} ${schemaCode}`); - } - }; - exports.default = def; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/runtime/equal.js -var require_equal = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var equal = require_fast_deep_equal(); - equal.code = 'require("ajv/dist/runtime/equal").default'; - exports.default = equal; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/validation/uniqueItems.js -var require_uniqueItems = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var dataType_1 = require_dataType(); - var codegen_1 = require_codegen(); - var util_1 = require_util(); - var equal_1 = require_equal(); - var error2 = { - message: ({ params: { i, j } }) => (0, codegen_1.str)`must NOT have duplicate items (items ## ${j} and ${i} are identical)`, - params: ({ params: { i, j } }) => (0, codegen_1._)`{i: ${i}, j: ${j}}` - }; - var def = { - keyword: "uniqueItems", - type: "array", - schemaType: "boolean", - $data: true, - error: error2, - code(cxt) { - const { gen, data, $data, schema, parentSchema, schemaCode, it } = cxt; - if (!$data && !schema) - return; - const valid = gen.let("valid"); - const itemTypes = parentSchema.items ? (0, dataType_1.getSchemaTypes)(parentSchema.items) : []; - cxt.block$data(valid, validateUniqueItems, (0, codegen_1._)`${schemaCode} === false`); - cxt.ok(valid); - function validateUniqueItems() { - const i = gen.let("i", (0, codegen_1._)`${data}.length`); - const j = gen.let("j"); - cxt.setParams({ i, j }); - gen.assign(valid, true); - gen.if((0, codegen_1._)`${i} > 1`, () => (canOptimize() ? loopN : loopN2)(i, j)); - } - function canOptimize() { - return itemTypes.length > 0 && !itemTypes.some((t) => t === "object" || t === "array"); - } - function loopN(i, j) { - const item = gen.name("item"); - const wrongType = (0, dataType_1.checkDataTypes)(itemTypes, item, it.opts.strictNumbers, dataType_1.DataType.Wrong); - const indices = gen.const("indices", (0, codegen_1._)`{}`); - gen.for((0, codegen_1._)`;${i}--;`, () => { - gen.let(item, (0, codegen_1._)`${data}[${i}]`); - gen.if(wrongType, (0, codegen_1._)`continue`); - if (itemTypes.length > 1) - gen.if((0, codegen_1._)`typeof ${item} == "string"`, (0, codegen_1._)`${item} += "_"`); - gen.if((0, codegen_1._)`typeof ${indices}[${item}] == "number"`, () => { - gen.assign(j, (0, codegen_1._)`${indices}[${item}]`); - cxt.error(); - gen.assign(valid, false).break(); - }).code((0, codegen_1._)`${indices}[${item}] = ${i}`); - }); - } - function loopN2(i, j) { - const eql = (0, util_1.useFunc)(gen, equal_1.default); - const outer = gen.name("outer"); - gen.label(outer).for((0, codegen_1._)`;${i}--;`, () => gen.for((0, codegen_1._)`${j} = ${i}; ${j}--;`, () => gen.if((0, codegen_1._)`${eql}(${data}[${i}], ${data}[${j}])`, () => { - cxt.error(); - gen.assign(valid, false).break(outer); - }))); - } - } - }; - exports.default = def; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/validation/const.js -var require_const = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var codegen_1 = require_codegen(); - var util_1 = require_util(); - var equal_1 = require_equal(); - var error2 = { - message: "must be equal to constant", - params: ({ schemaCode }) => (0, codegen_1._)`{allowedValue: ${schemaCode}}` - }; - var def = { - keyword: "const", - $data: true, - error: error2, - code(cxt) { - const { gen, data, $data, schemaCode, schema } = cxt; - if ($data || schema && typeof schema == "object") { - cxt.fail$data((0, codegen_1._)`!${(0, util_1.useFunc)(gen, equal_1.default)}(${data}, ${schemaCode})`); - } else { - cxt.fail((0, codegen_1._)`${schema} !== ${data}`); - } - } - }; - exports.default = def; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/validation/enum.js -var require_enum = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var codegen_1 = require_codegen(); - var util_1 = require_util(); - var equal_1 = require_equal(); - var error2 = { - message: "must be equal to one of the allowed values", - params: ({ schemaCode }) => (0, codegen_1._)`{allowedValues: ${schemaCode}}` - }; - var def = { - keyword: "enum", - schemaType: "array", - $data: true, - error: error2, - code(cxt) { - const { gen, data, $data, schema, schemaCode, it } = cxt; - if (!$data && schema.length === 0) - throw new Error("enum must have non-empty array"); - const useLoop = schema.length >= it.opts.loopEnum; - let eql; - const getEql = () => eql !== null && eql !== undefined ? eql : eql = (0, util_1.useFunc)(gen, equal_1.default); - let valid; - if (useLoop || $data) { - valid = gen.let("valid"); - cxt.block$data(valid, loopEnum); - } else { - if (!Array.isArray(schema)) - throw new Error("ajv implementation error"); - const vSchema = gen.const("vSchema", schemaCode); - valid = (0, codegen_1.or)(...schema.map((_x, i) => equalCode(vSchema, i))); - } - cxt.pass(valid); - function loopEnum() { - gen.assign(valid, false); - gen.forOf("v", schemaCode, (v) => gen.if((0, codegen_1._)`${getEql()}(${data}, ${v})`, () => gen.assign(valid, true).break())); - } - function equalCode(vSchema, i) { - const sch = schema[i]; - return typeof sch === "object" && sch !== null ? (0, codegen_1._)`${getEql()}(${data}, ${vSchema}[${i}])` : (0, codegen_1._)`${data} === ${sch}`; - } - } - }; - exports.default = def; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/validation/index.js -var require_validation = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var limitNumber_1 = require_limitNumber(); - var multipleOf_1 = require_multipleOf(); - var limitLength_1 = require_limitLength(); - var pattern_1 = require_pattern(); - var limitProperties_1 = require_limitProperties(); - var required_1 = require_required(); - var limitItems_1 = require_limitItems(); - var uniqueItems_1 = require_uniqueItems(); - var const_1 = require_const(); - var enum_1 = require_enum(); - var validation = [ - limitNumber_1.default, - multipleOf_1.default, - limitLength_1.default, - pattern_1.default, - limitProperties_1.default, - required_1.default, - limitItems_1.default, - uniqueItems_1.default, - { keyword: "type", schemaType: ["string", "array"] }, - { keyword: "nullable", schemaType: "boolean" }, - const_1.default, - enum_1.default - ]; - exports.default = validation; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/applicator/additionalItems.js -var require_additionalItems = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.validateAdditionalItems = undefined; - var codegen_1 = require_codegen(); - var util_1 = require_util(); - var error2 = { - message: ({ params: { len } }) => (0, codegen_1.str)`must NOT have more than ${len} items`, - params: ({ params: { len } }) => (0, codegen_1._)`{limit: ${len}}` - }; - var def = { - keyword: "additionalItems", - type: "array", - schemaType: ["boolean", "object"], - before: "uniqueItems", - error: error2, - code(cxt) { - const { parentSchema, it } = cxt; - const { items } = parentSchema; - if (!Array.isArray(items)) { - (0, util_1.checkStrictMode)(it, '"additionalItems" is ignored when "items" is not an array of schemas'); - return; - } - validateAdditionalItems(cxt, items); - } - }; - function validateAdditionalItems(cxt, items) { - const { gen, schema, data, keyword, it } = cxt; - it.items = true; - const len = gen.const("len", (0, codegen_1._)`${data}.length`); - if (schema === false) { - cxt.setParams({ len: items.length }); - cxt.pass((0, codegen_1._)`${len} <= ${items.length}`); - } else if (typeof schema == "object" && !(0, util_1.alwaysValidSchema)(it, schema)) { - const valid = gen.var("valid", (0, codegen_1._)`${len} <= ${items.length}`); - gen.if((0, codegen_1.not)(valid), () => validateItems(valid)); - cxt.ok(valid); - } - function validateItems(valid) { - gen.forRange("i", items.length, len, (i) => { - cxt.subschema({ keyword, dataProp: i, dataPropType: util_1.Type.Num }, valid); - if (!it.allErrors) - gen.if((0, codegen_1.not)(valid), () => gen.break()); - }); - } - } - exports.validateAdditionalItems = validateAdditionalItems; - exports.default = def; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/applicator/items.js -var require_items = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.validateTuple = undefined; - var codegen_1 = require_codegen(); - var util_1 = require_util(); - var code_1 = require_code2(); - var def = { - keyword: "items", - type: "array", - schemaType: ["object", "array", "boolean"], - before: "uniqueItems", - code(cxt) { - const { schema, it } = cxt; - if (Array.isArray(schema)) - return validateTuple(cxt, "additionalItems", schema); - it.items = true; - if ((0, util_1.alwaysValidSchema)(it, schema)) - return; - cxt.ok((0, code_1.validateArray)(cxt)); - } - }; - function validateTuple(cxt, extraItems, schArr = cxt.schema) { - const { gen, parentSchema, data, keyword, it } = cxt; - checkStrictTuple(parentSchema); - if (it.opts.unevaluated && schArr.length && it.items !== true) { - it.items = util_1.mergeEvaluated.items(gen, schArr.length, it.items); - } - const valid = gen.name("valid"); - const len = gen.const("len", (0, codegen_1._)`${data}.length`); - schArr.forEach((sch, i) => { - if ((0, util_1.alwaysValidSchema)(it, sch)) - return; - gen.if((0, codegen_1._)`${len} > ${i}`, () => cxt.subschema({ - keyword, - schemaProp: i, - dataProp: i - }, valid)); - cxt.ok(valid); - }); - function checkStrictTuple(sch) { - const { opts, errSchemaPath } = it; - const l = schArr.length; - const fullTuple = l === sch.minItems && (l === sch.maxItems || sch[extraItems] === false); - if (opts.strictTuples && !fullTuple) { - const msg = `"${keyword}" is ${l}-tuple, but minItems or maxItems/${extraItems} are not specified or different at path "${errSchemaPath}"`; - (0, util_1.checkStrictMode)(it, msg, opts.strictTuples); - } - } - } - exports.validateTuple = validateTuple; - exports.default = def; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/applicator/prefixItems.js -var require_prefixItems = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var items_1 = require_items(); - var def = { - keyword: "prefixItems", - type: "array", - schemaType: ["array"], - before: "uniqueItems", - code: (cxt) => (0, items_1.validateTuple)(cxt, "items") - }; - exports.default = def; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/applicator/items2020.js -var require_items2020 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var codegen_1 = require_codegen(); - var util_1 = require_util(); - var code_1 = require_code2(); - var additionalItems_1 = require_additionalItems(); - var error2 = { - message: ({ params: { len } }) => (0, codegen_1.str)`must NOT have more than ${len} items`, - params: ({ params: { len } }) => (0, codegen_1._)`{limit: ${len}}` - }; - var def = { - keyword: "items", - type: "array", - schemaType: ["object", "boolean"], - before: "uniqueItems", - error: error2, - code(cxt) { - const { schema, parentSchema, it } = cxt; - const { prefixItems } = parentSchema; - it.items = true; - if ((0, util_1.alwaysValidSchema)(it, schema)) - return; - if (prefixItems) - (0, additionalItems_1.validateAdditionalItems)(cxt, prefixItems); - else - cxt.ok((0, code_1.validateArray)(cxt)); - } - }; - exports.default = def; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/applicator/contains.js -var require_contains = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var codegen_1 = require_codegen(); - var util_1 = require_util(); - var error2 = { - message: ({ params: { min, max } }) => max === undefined ? (0, codegen_1.str)`must contain at least ${min} valid item(s)` : (0, codegen_1.str)`must contain at least ${min} and no more than ${max} valid item(s)`, - params: ({ params: { min, max } }) => max === undefined ? (0, codegen_1._)`{minContains: ${min}}` : (0, codegen_1._)`{minContains: ${min}, maxContains: ${max}}` - }; - var def = { - keyword: "contains", - type: "array", - schemaType: ["object", "boolean"], - before: "uniqueItems", - trackErrors: true, - error: error2, - code(cxt) { - const { gen, schema, parentSchema, data, it } = cxt; - let min; - let max; - const { minContains, maxContains } = parentSchema; - if (it.opts.next) { - min = minContains === undefined ? 1 : minContains; - max = maxContains; - } else { - min = 1; - } - const len = gen.const("len", (0, codegen_1._)`${data}.length`); - cxt.setParams({ min, max }); - if (max === undefined && min === 0) { - (0, util_1.checkStrictMode)(it, `"minContains" == 0 without "maxContains": "contains" keyword ignored`); - return; - } - if (max !== undefined && min > max) { - (0, util_1.checkStrictMode)(it, `"minContains" > "maxContains" is always invalid`); - cxt.fail(); - return; - } - if ((0, util_1.alwaysValidSchema)(it, schema)) { - let cond = (0, codegen_1._)`${len} >= ${min}`; - if (max !== undefined) - cond = (0, codegen_1._)`${cond} && ${len} <= ${max}`; - cxt.pass(cond); - return; - } - it.items = true; - const valid = gen.name("valid"); - if (max === undefined && min === 1) { - validateItems(valid, () => gen.if(valid, () => gen.break())); - } else if (min === 0) { - gen.let(valid, true); - if (max !== undefined) - gen.if((0, codegen_1._)`${data}.length > 0`, validateItemsWithCount); - } else { - gen.let(valid, false); - validateItemsWithCount(); - } - cxt.result(valid, () => cxt.reset()); - function validateItemsWithCount() { - const schValid = gen.name("_valid"); - const count = gen.let("count", 0); - validateItems(schValid, () => gen.if(schValid, () => checkLimits(count))); - } - function validateItems(_valid, block) { - gen.forRange("i", 0, len, (i) => { - cxt.subschema({ - keyword: "contains", - dataProp: i, - dataPropType: util_1.Type.Num, - compositeRule: true - }, _valid); - block(); - }); - } - function checkLimits(count) { - gen.code((0, codegen_1._)`${count}++`); - if (max === undefined) { - gen.if((0, codegen_1._)`${count} >= ${min}`, () => gen.assign(valid, true).break()); - } else { - gen.if((0, codegen_1._)`${count} > ${max}`, () => gen.assign(valid, false).break()); - if (min === 1) - gen.assign(valid, true); - else - gen.if((0, codegen_1._)`${count} >= ${min}`, () => gen.assign(valid, true)); - } - } - } - }; - exports.default = def; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/applicator/dependencies.js -var require_dependencies = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.validateSchemaDeps = exports.validatePropertyDeps = exports.error = undefined; - var codegen_1 = require_codegen(); - var util_1 = require_util(); - var code_1 = require_code2(); - exports.error = { - message: ({ params: { property, depsCount, deps } }) => { - const property_ies = depsCount === 1 ? "property" : "properties"; - return (0, codegen_1.str)`must have ${property_ies} ${deps} when property ${property} is present`; - }, - params: ({ params: { property, depsCount, deps, missingProperty } }) => (0, codegen_1._)`{property: ${property}, - missingProperty: ${missingProperty}, - depsCount: ${depsCount}, - deps: ${deps}}` - }; - var def = { - keyword: "dependencies", - type: "object", - schemaType: "object", - error: exports.error, - code(cxt) { - const [propDeps, schDeps] = splitDependencies(cxt); - validatePropertyDeps(cxt, propDeps); - validateSchemaDeps(cxt, schDeps); - } - }; - function splitDependencies({ schema }) { - const propertyDeps = {}; - const schemaDeps = {}; - for (const key in schema) { - if (key === "__proto__") - continue; - const deps = Array.isArray(schema[key]) ? propertyDeps : schemaDeps; - deps[key] = schema[key]; - } - return [propertyDeps, schemaDeps]; - } - function validatePropertyDeps(cxt, propertyDeps = cxt.schema) { - const { gen, data, it } = cxt; - if (Object.keys(propertyDeps).length === 0) - return; - const missing = gen.let("missing"); - for (const prop in propertyDeps) { - const deps = propertyDeps[prop]; - if (deps.length === 0) - continue; - const hasProperty = (0, code_1.propertyInData)(gen, data, prop, it.opts.ownProperties); - cxt.setParams({ - property: prop, - depsCount: deps.length, - deps: deps.join(", ") - }); - if (it.allErrors) { - gen.if(hasProperty, () => { - for (const depProp of deps) { - (0, code_1.checkReportMissingProp)(cxt, depProp); - } - }); - } else { - gen.if((0, codegen_1._)`${hasProperty} && (${(0, code_1.checkMissingProp)(cxt, deps, missing)})`); - (0, code_1.reportMissingProp)(cxt, missing); - gen.else(); - } - } - } - exports.validatePropertyDeps = validatePropertyDeps; - function validateSchemaDeps(cxt, schemaDeps = cxt.schema) { - const { gen, data, keyword, it } = cxt; - const valid = gen.name("valid"); - for (const prop in schemaDeps) { - if ((0, util_1.alwaysValidSchema)(it, schemaDeps[prop])) - continue; - gen.if((0, code_1.propertyInData)(gen, data, prop, it.opts.ownProperties), () => { - const schCxt = cxt.subschema({ keyword, schemaProp: prop }, valid); - cxt.mergeValidEvaluated(schCxt, valid); - }, () => gen.var(valid, true)); - cxt.ok(valid); - } - } - exports.validateSchemaDeps = validateSchemaDeps; - exports.default = def; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/applicator/propertyNames.js -var require_propertyNames = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var codegen_1 = require_codegen(); - var util_1 = require_util(); - var error2 = { - message: "property name must be valid", - params: ({ params }) => (0, codegen_1._)`{propertyName: ${params.propertyName}}` - }; - var def = { - keyword: "propertyNames", - type: "object", - schemaType: ["object", "boolean"], - error: error2, - code(cxt) { - const { gen, schema, data, it } = cxt; - if ((0, util_1.alwaysValidSchema)(it, schema)) - return; - const valid = gen.name("valid"); - gen.forIn("key", data, (key) => { - cxt.setParams({ propertyName: key }); - cxt.subschema({ - keyword: "propertyNames", - data: key, - dataTypes: ["string"], - propertyName: key, - compositeRule: true - }, valid); - gen.if((0, codegen_1.not)(valid), () => { - cxt.error(true); - if (!it.allErrors) - gen.break(); - }); - }); - cxt.ok(valid); - } - }; - exports.default = def; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/applicator/additionalProperties.js -var require_additionalProperties = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var code_1 = require_code2(); - var codegen_1 = require_codegen(); - var names_1 = require_names(); - var util_1 = require_util(); - var error2 = { - message: "must NOT have additional properties", - params: ({ params }) => (0, codegen_1._)`{additionalProperty: ${params.additionalProperty}}` - }; - var def = { - keyword: "additionalProperties", - type: ["object"], - schemaType: ["boolean", "object"], - allowUndefined: true, - trackErrors: true, - error: error2, - code(cxt) { - const { gen, schema, parentSchema, data, errsCount, it } = cxt; - if (!errsCount) - throw new Error("ajv implementation error"); - const { allErrors, opts } = it; - it.props = true; - if (opts.removeAdditional !== "all" && (0, util_1.alwaysValidSchema)(it, schema)) - return; - const props = (0, code_1.allSchemaProperties)(parentSchema.properties); - const patProps = (0, code_1.allSchemaProperties)(parentSchema.patternProperties); - checkAdditionalProperties(); - cxt.ok((0, codegen_1._)`${errsCount} === ${names_1.default.errors}`); - function checkAdditionalProperties() { - gen.forIn("key", data, (key) => { - if (!props.length && !patProps.length) - additionalPropertyCode(key); - else - gen.if(isAdditional(key), () => additionalPropertyCode(key)); - }); - } - function isAdditional(key) { - let definedProp; - if (props.length > 8) { - const propsSchema = (0, util_1.schemaRefOrVal)(it, parentSchema.properties, "properties"); - definedProp = (0, code_1.isOwnProperty)(gen, propsSchema, key); - } else if (props.length) { - definedProp = (0, codegen_1.or)(...props.map((p) => (0, codegen_1._)`${key} === ${p}`)); - } else { - definedProp = codegen_1.nil; - } - if (patProps.length) { - definedProp = (0, codegen_1.or)(definedProp, ...patProps.map((p) => (0, codegen_1._)`${(0, code_1.usePattern)(cxt, p)}.test(${key})`)); - } - return (0, codegen_1.not)(definedProp); - } - function deleteAdditional(key) { - gen.code((0, codegen_1._)`delete ${data}[${key}]`); - } - function additionalPropertyCode(key) { - if (opts.removeAdditional === "all" || opts.removeAdditional && schema === false) { - deleteAdditional(key); - return; - } - if (schema === false) { - cxt.setParams({ additionalProperty: key }); - cxt.error(); - if (!allErrors) - gen.break(); - return; - } - if (typeof schema == "object" && !(0, util_1.alwaysValidSchema)(it, schema)) { - const valid = gen.name("valid"); - if (opts.removeAdditional === "failing") { - applyAdditionalSchema(key, valid, false); - gen.if((0, codegen_1.not)(valid), () => { - cxt.reset(); - deleteAdditional(key); - }); - } else { - applyAdditionalSchema(key, valid); - if (!allErrors) - gen.if((0, codegen_1.not)(valid), () => gen.break()); - } - } - } - function applyAdditionalSchema(key, valid, errors3) { - const subschema = { - keyword: "additionalProperties", - dataProp: key, - dataPropType: util_1.Type.Str - }; - if (errors3 === false) { - Object.assign(subschema, { - compositeRule: true, - createErrors: false, - allErrors: false - }); - } - cxt.subschema(subschema, valid); - } - } - }; - exports.default = def; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/applicator/properties.js -var require_properties = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var validate_1 = require_validate(); - var code_1 = require_code2(); - var util_1 = require_util(); - var additionalProperties_1 = require_additionalProperties(); - var def = { - keyword: "properties", - type: "object", - schemaType: "object", - code(cxt) { - const { gen, schema, parentSchema, data, it } = cxt; - if (it.opts.removeAdditional === "all" && parentSchema.additionalProperties === undefined) { - additionalProperties_1.default.code(new validate_1.KeywordCxt(it, additionalProperties_1.default, "additionalProperties")); - } - const allProps = (0, code_1.allSchemaProperties)(schema); - for (const prop of allProps) { - it.definedProperties.add(prop); - } - if (it.opts.unevaluated && allProps.length && it.props !== true) { - it.props = util_1.mergeEvaluated.props(gen, (0, util_1.toHash)(allProps), it.props); - } - const properties = allProps.filter((p) => !(0, util_1.alwaysValidSchema)(it, schema[p])); - if (properties.length === 0) - return; - const valid = gen.name("valid"); - for (const prop of properties) { - if (hasDefault(prop)) { - applyPropertySchema(prop); - } else { - gen.if((0, code_1.propertyInData)(gen, data, prop, it.opts.ownProperties)); - applyPropertySchema(prop); - if (!it.allErrors) - gen.else().var(valid, true); - gen.endIf(); - } - cxt.it.definedProperties.add(prop); - cxt.ok(valid); - } - function hasDefault(prop) { - return it.opts.useDefaults && !it.compositeRule && schema[prop].default !== undefined; - } - function applyPropertySchema(prop) { - cxt.subschema({ - keyword: "properties", - schemaProp: prop, - dataProp: prop - }, valid); - } - } - }; - exports.default = def; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/applicator/patternProperties.js -var require_patternProperties = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var code_1 = require_code2(); - var codegen_1 = require_codegen(); - var util_1 = require_util(); - var util_2 = require_util(); - var def = { - keyword: "patternProperties", - type: "object", - schemaType: "object", - code(cxt) { - const { gen, schema, data, parentSchema, it } = cxt; - const { opts } = it; - const patterns = (0, code_1.allSchemaProperties)(schema); - const alwaysValidPatterns = patterns.filter((p) => (0, util_1.alwaysValidSchema)(it, schema[p])); - if (patterns.length === 0 || alwaysValidPatterns.length === patterns.length && (!it.opts.unevaluated || it.props === true)) { - return; - } - const checkProperties = opts.strictSchema && !opts.allowMatchingProperties && parentSchema.properties; - const valid = gen.name("valid"); - if (it.props !== true && !(it.props instanceof codegen_1.Name)) { - it.props = (0, util_2.evaluatedPropsToName)(gen, it.props); - } - const { props } = it; - validatePatternProperties(); - function validatePatternProperties() { - for (const pat of patterns) { - if (checkProperties) - checkMatchingProperties(pat); - if (it.allErrors) { - validateProperties(pat); - } else { - gen.var(valid, true); - validateProperties(pat); - gen.if(valid); - } - } - } - function checkMatchingProperties(pat) { - for (const prop in checkProperties) { - if (new RegExp(pat).test(prop)) { - (0, util_1.checkStrictMode)(it, `property ${prop} matches pattern ${pat} (use allowMatchingProperties)`); - } - } - } - function validateProperties(pat) { - gen.forIn("key", data, (key) => { - gen.if((0, codegen_1._)`${(0, code_1.usePattern)(cxt, pat)}.test(${key})`, () => { - const alwaysValid = alwaysValidPatterns.includes(pat); - if (!alwaysValid) { - cxt.subschema({ - keyword: "patternProperties", - schemaProp: pat, - dataProp: key, - dataPropType: util_2.Type.Str - }, valid); - } - if (it.opts.unevaluated && props !== true) { - gen.assign((0, codegen_1._)`${props}[${key}]`, true); - } else if (!alwaysValid && !it.allErrors) { - gen.if((0, codegen_1.not)(valid), () => gen.break()); - } - }); - }); - } - } - }; - exports.default = def; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/applicator/not.js -var require_not = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var util_1 = require_util(); - var def = { - keyword: "not", - schemaType: ["object", "boolean"], - trackErrors: true, - code(cxt) { - const { gen, schema, it } = cxt; - if ((0, util_1.alwaysValidSchema)(it, schema)) { - cxt.fail(); - return; - } - const valid = gen.name("valid"); - cxt.subschema({ - keyword: "not", - compositeRule: true, - createErrors: false, - allErrors: false - }, valid); - cxt.failResult(valid, () => cxt.reset(), () => cxt.error()); - }, - error: { message: "must NOT be valid" } - }; - exports.default = def; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/applicator/anyOf.js -var require_anyOf = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var code_1 = require_code2(); - var def = { - keyword: "anyOf", - schemaType: "array", - trackErrors: true, - code: code_1.validateUnion, - error: { message: "must match a schema in anyOf" } - }; - exports.default = def; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/applicator/oneOf.js -var require_oneOf = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var codegen_1 = require_codegen(); - var util_1 = require_util(); - var error2 = { - message: "must match exactly one schema in oneOf", - params: ({ params }) => (0, codegen_1._)`{passingSchemas: ${params.passing}}` - }; - var def = { - keyword: "oneOf", - schemaType: "array", - trackErrors: true, - error: error2, - code(cxt) { - const { gen, schema, parentSchema, it } = cxt; - if (!Array.isArray(schema)) - throw new Error("ajv implementation error"); - if (it.opts.discriminator && parentSchema.discriminator) - return; - const schArr = schema; - const valid = gen.let("valid", false); - const passing = gen.let("passing", null); - const schValid = gen.name("_valid"); - cxt.setParams({ passing }); - gen.block(validateOneOf); - cxt.result(valid, () => cxt.reset(), () => cxt.error(true)); - function validateOneOf() { - schArr.forEach((sch, i) => { - let schCxt; - if ((0, util_1.alwaysValidSchema)(it, sch)) { - gen.var(schValid, true); - } else { - schCxt = cxt.subschema({ - keyword: "oneOf", - schemaProp: i, - compositeRule: true - }, schValid); - } - if (i > 0) { - gen.if((0, codegen_1._)`${schValid} && ${valid}`).assign(valid, false).assign(passing, (0, codegen_1._)`[${passing}, ${i}]`).else(); - } - gen.if(schValid, () => { - gen.assign(valid, true); - gen.assign(passing, i); - if (schCxt) - cxt.mergeEvaluated(schCxt, codegen_1.Name); - }); - }); - } - } - }; - exports.default = def; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/applicator/allOf.js -var require_allOf = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var util_1 = require_util(); - var def = { - keyword: "allOf", - schemaType: "array", - code(cxt) { - const { gen, schema, it } = cxt; - if (!Array.isArray(schema)) - throw new Error("ajv implementation error"); - const valid = gen.name("valid"); - schema.forEach((sch, i) => { - if ((0, util_1.alwaysValidSchema)(it, sch)) - return; - const schCxt = cxt.subschema({ keyword: "allOf", schemaProp: i }, valid); - cxt.ok(valid); - cxt.mergeEvaluated(schCxt); - }); - } - }; - exports.default = def; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/applicator/if.js -var require_if = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var codegen_1 = require_codegen(); - var util_1 = require_util(); - var error2 = { - message: ({ params }) => (0, codegen_1.str)`must match "${params.ifClause}" schema`, - params: ({ params }) => (0, codegen_1._)`{failingKeyword: ${params.ifClause}}` - }; - var def = { - keyword: "if", - schemaType: ["object", "boolean"], - trackErrors: true, - error: error2, - code(cxt) { - const { gen, parentSchema, it } = cxt; - if (parentSchema.then === undefined && parentSchema.else === undefined) { - (0, util_1.checkStrictMode)(it, '"if" without "then" and "else" is ignored'); - } - const hasThen = hasSchema(it, "then"); - const hasElse = hasSchema(it, "else"); - if (!hasThen && !hasElse) - return; - const valid = gen.let("valid", true); - const schValid = gen.name("_valid"); - validateIf(); - cxt.reset(); - if (hasThen && hasElse) { - const ifClause = gen.let("ifClause"); - cxt.setParams({ ifClause }); - gen.if(schValid, validateClause("then", ifClause), validateClause("else", ifClause)); - } else if (hasThen) { - gen.if(schValid, validateClause("then")); - } else { - gen.if((0, codegen_1.not)(schValid), validateClause("else")); - } - cxt.pass(valid, () => cxt.error(true)); - function validateIf() { - const schCxt = cxt.subschema({ - keyword: "if", - compositeRule: true, - createErrors: false, - allErrors: false - }, schValid); - cxt.mergeEvaluated(schCxt); - } - function validateClause(keyword, ifClause) { - return () => { - const schCxt = cxt.subschema({ keyword }, schValid); - gen.assign(valid, schValid); - cxt.mergeValidEvaluated(schCxt, valid); - if (ifClause) - gen.assign(ifClause, (0, codegen_1._)`${keyword}`); - else - cxt.setParams({ ifClause: keyword }); - }; - } - } - }; - function hasSchema(it, keyword) { - const schema = it.schema[keyword]; - return schema !== undefined && !(0, util_1.alwaysValidSchema)(it, schema); - } - exports.default = def; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/applicator/thenElse.js -var require_thenElse = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var util_1 = require_util(); - var def = { - keyword: ["then", "else"], - schemaType: ["object", "boolean"], - code({ keyword, parentSchema, it }) { - if (parentSchema.if === undefined) - (0, util_1.checkStrictMode)(it, `"${keyword}" without "if" is ignored`); - } - }; - exports.default = def; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/applicator/index.js -var require_applicator = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var additionalItems_1 = require_additionalItems(); - var prefixItems_1 = require_prefixItems(); - var items_1 = require_items(); - var items2020_1 = require_items2020(); - var contains_1 = require_contains(); - var dependencies_1 = require_dependencies(); - var propertyNames_1 = require_propertyNames(); - var additionalProperties_1 = require_additionalProperties(); - var properties_1 = require_properties(); - var patternProperties_1 = require_patternProperties(); - var not_1 = require_not(); - var anyOf_1 = require_anyOf(); - var oneOf_1 = require_oneOf(); - var allOf_1 = require_allOf(); - var if_1 = require_if(); - var thenElse_1 = require_thenElse(); - function getApplicator(draft2020 = false) { - const applicator = [ - not_1.default, - anyOf_1.default, - oneOf_1.default, - allOf_1.default, - if_1.default, - thenElse_1.default, - propertyNames_1.default, - additionalProperties_1.default, - dependencies_1.default, - properties_1.default, - patternProperties_1.default - ]; - if (draft2020) - applicator.push(prefixItems_1.default, items2020_1.default); - else - applicator.push(additionalItems_1.default, items_1.default); - applicator.push(contains_1.default); - return applicator; - } - exports.default = getApplicator; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/format/format.js -var require_format = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var codegen_1 = require_codegen(); - var error2 = { - message: ({ schemaCode }) => (0, codegen_1.str)`must match format "${schemaCode}"`, - params: ({ schemaCode }) => (0, codegen_1._)`{format: ${schemaCode}}` - }; - var def = { - keyword: "format", - type: ["number", "string"], - schemaType: "string", - $data: true, - error: error2, - code(cxt, ruleType) { - const { gen, data, $data, schema, schemaCode, it } = cxt; - const { opts, errSchemaPath, schemaEnv, self } = it; - if (!opts.validateFormats) - return; - if ($data) - validate$DataFormat(); - else - validateFormat(); - function validate$DataFormat() { - const fmts = gen.scopeValue("formats", { - ref: self.formats, - code: opts.code.formats - }); - const fDef = gen.const("fDef", (0, codegen_1._)`${fmts}[${schemaCode}]`); - const fType = gen.let("fType"); - const format = gen.let("format"); - gen.if((0, codegen_1._)`typeof ${fDef} == "object" && !(${fDef} instanceof RegExp)`, () => gen.assign(fType, (0, codegen_1._)`${fDef}.type || "string"`).assign(format, (0, codegen_1._)`${fDef}.validate`), () => gen.assign(fType, (0, codegen_1._)`"string"`).assign(format, fDef)); - cxt.fail$data((0, codegen_1.or)(unknownFmt(), invalidFmt())); - function unknownFmt() { - if (opts.strictSchema === false) - return codegen_1.nil; - return (0, codegen_1._)`${schemaCode} && !${format}`; - } - function invalidFmt() { - const callFormat = schemaEnv.$async ? (0, codegen_1._)`(${fDef}.async ? await ${format}(${data}) : ${format}(${data}))` : (0, codegen_1._)`${format}(${data})`; - const validData = (0, codegen_1._)`(typeof ${format} == "function" ? ${callFormat} : ${format}.test(${data}))`; - return (0, codegen_1._)`${format} && ${format} !== true && ${fType} === ${ruleType} && !${validData}`; - } - } - function validateFormat() { - const formatDef = self.formats[schema]; - if (!formatDef) { - unknownFormat(); - return; - } - if (formatDef === true) - return; - const [fmtType, format, fmtRef] = getFormat(formatDef); - if (fmtType === ruleType) - cxt.pass(validCondition()); - function unknownFormat() { - if (opts.strictSchema === false) { - self.logger.warn(unknownMsg()); - return; - } - throw new Error(unknownMsg()); - function unknownMsg() { - return `unknown format "${schema}" ignored in schema at path "${errSchemaPath}"`; - } - } - function getFormat(fmtDef) { - const code = fmtDef instanceof RegExp ? (0, codegen_1.regexpCode)(fmtDef) : opts.code.formats ? (0, codegen_1._)`${opts.code.formats}${(0, codegen_1.getProperty)(schema)}` : undefined; - const fmt = gen.scopeValue("formats", { key: schema, ref: fmtDef, code }); - if (typeof fmtDef == "object" && !(fmtDef instanceof RegExp)) { - return [fmtDef.type || "string", fmtDef.validate, (0, codegen_1._)`${fmt}.validate`]; - } - return ["string", fmtDef, fmt]; - } - function validCondition() { - if (typeof formatDef == "object" && !(formatDef instanceof RegExp) && formatDef.async) { - if (!schemaEnv.$async) - throw new Error("async format in sync schema"); - return (0, codegen_1._)`await ${fmtRef}(${data})`; - } - return typeof format == "function" ? (0, codegen_1._)`${fmtRef}(${data})` : (0, codegen_1._)`${fmtRef}.test(${data})`; - } - } - } - }; - exports.default = def; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/format/index.js -var require_format2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var format_1 = require_format(); - var format = [format_1.default]; - exports.default = format; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/metadata.js -var require_metadata = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.contentVocabulary = exports.metadataVocabulary = undefined; - exports.metadataVocabulary = [ - "title", - "description", - "default", - "deprecated", - "readOnly", - "writeOnly", - "examples" - ]; - exports.contentVocabulary = [ - "contentMediaType", - "contentEncoding", - "contentSchema" - ]; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/draft7.js -var require_draft7 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var core_1 = require_core2(); - var validation_1 = require_validation(); - var applicator_1 = require_applicator(); - var format_1 = require_format2(); - var metadata_1 = require_metadata(); - var draft7Vocabularies = [ - core_1.default, - validation_1.default, - (0, applicator_1.default)(), - format_1.default, - metadata_1.metadataVocabulary, - metadata_1.contentVocabulary - ]; - exports.default = draft7Vocabularies; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/discriminator/types.js -var require_types = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.DiscrError = undefined; - var DiscrError; - (function(DiscrError2) { - DiscrError2["Tag"] = "tag"; - DiscrError2["Mapping"] = "mapping"; - })(DiscrError || (exports.DiscrError = DiscrError = {})); -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/discriminator/index.js -var require_discriminator = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var codegen_1 = require_codegen(); - var types_1 = require_types(); - var compile_1 = require_compile(); - var ref_error_1 = require_ref_error(); - var util_1 = require_util(); - var error2 = { - message: ({ params: { discrError, tagName } }) => discrError === types_1.DiscrError.Tag ? `tag "${tagName}" must be string` : `value of tag "${tagName}" must be in oneOf`, - params: ({ params: { discrError, tag, tagName } }) => (0, codegen_1._)`{error: ${discrError}, tag: ${tagName}, tagValue: ${tag}}` - }; - var def = { - keyword: "discriminator", - type: "object", - schemaType: "object", - error: error2, - code(cxt) { - const { gen, data, schema, parentSchema, it } = cxt; - const { oneOf } = parentSchema; - if (!it.opts.discriminator) { - throw new Error("discriminator: requires discriminator option"); - } - const tagName = schema.propertyName; - if (typeof tagName != "string") - throw new Error("discriminator: requires propertyName"); - if (schema.mapping) - throw new Error("discriminator: mapping is not supported"); - if (!oneOf) - throw new Error("discriminator: requires oneOf keyword"); - const valid = gen.let("valid", false); - const tag = gen.const("tag", (0, codegen_1._)`${data}${(0, codegen_1.getProperty)(tagName)}`); - gen.if((0, codegen_1._)`typeof ${tag} == "string"`, () => validateMapping(), () => cxt.error(false, { discrError: types_1.DiscrError.Tag, tag, tagName })); - cxt.ok(valid); - function validateMapping() { - const mapping = getMapping(); - gen.if(false); - for (const tagValue in mapping) { - gen.elseIf((0, codegen_1._)`${tag} === ${tagValue}`); - gen.assign(valid, applyTagSchema(mapping[tagValue])); - } - gen.else(); - cxt.error(false, { discrError: types_1.DiscrError.Mapping, tag, tagName }); - gen.endIf(); - } - function applyTagSchema(schemaProp) { - const _valid = gen.name("valid"); - const schCxt = cxt.subschema({ keyword: "oneOf", schemaProp }, _valid); - cxt.mergeEvaluated(schCxt, codegen_1.Name); - return _valid; - } - function getMapping() { - var _a; - const oneOfMapping = {}; - const topRequired = hasRequired(parentSchema); - let tagRequired = true; - for (let i = 0;i < oneOf.length; i++) { - let sch = oneOf[i]; - if ((sch === null || sch === undefined ? undefined : sch.$ref) && !(0, util_1.schemaHasRulesButRef)(sch, it.self.RULES)) { - const ref = sch.$ref; - sch = compile_1.resolveRef.call(it.self, it.schemaEnv.root, it.baseId, ref); - if (sch instanceof compile_1.SchemaEnv) - sch = sch.schema; - if (sch === undefined) - throw new ref_error_1.default(it.opts.uriResolver, it.baseId, ref); - } - const propSch = (_a = sch === null || sch === undefined ? undefined : sch.properties) === null || _a === undefined ? undefined : _a[tagName]; - if (typeof propSch != "object") { - throw new Error(`discriminator: oneOf subschemas (or referenced schemas) must have "properties/${tagName}"`); - } - tagRequired = tagRequired && (topRequired || hasRequired(sch)); - addMappings(propSch, i); - } - if (!tagRequired) - throw new Error(`discriminator: "${tagName}" must be required`); - return oneOfMapping; - function hasRequired({ required: required2 }) { - return Array.isArray(required2) && required2.includes(tagName); - } - function addMappings(sch, i) { - if (sch.const) { - addMapping(sch.const, i); - } else if (sch.enum) { - for (const tagValue of sch.enum) { - addMapping(tagValue, i); - } - } else { - throw new Error(`discriminator: "properties/${tagName}" must have "const" or "enum"`); - } - } - function addMapping(tagValue, i) { - if (typeof tagValue != "string" || tagValue in oneOfMapping) { - throw new Error(`discriminator: "${tagName}" values must be unique strings`); - } - oneOfMapping[tagValue] = i; - } - } - } - }; - exports.default = def; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/refs/json-schema-draft-07.json -var require_json_schema_draft_07 = __commonJS((exports, module) => { - module.exports = { - $schema: "http://json-schema.org/draft-07/schema#", - $id: "http://json-schema.org/draft-07/schema#", - title: "Core schema meta-schema", - definitions: { - schemaArray: { - type: "array", - minItems: 1, - items: { $ref: "#" } - }, - nonNegativeInteger: { - type: "integer", - minimum: 0 - }, - nonNegativeIntegerDefault0: { - allOf: [{ $ref: "#/definitions/nonNegativeInteger" }, { default: 0 }] - }, - simpleTypes: { - enum: ["array", "boolean", "integer", "null", "number", "object", "string"] - }, - stringArray: { - type: "array", - items: { type: "string" }, - uniqueItems: true, - default: [] - } - }, - type: ["object", "boolean"], - properties: { - $id: { - type: "string", - format: "uri-reference" - }, - $schema: { - type: "string", - format: "uri" - }, - $ref: { - type: "string", - format: "uri-reference" - }, - $comment: { - type: "string" - }, - title: { - type: "string" - }, - description: { - type: "string" - }, - default: true, - readOnly: { - type: "boolean", - default: false - }, - examples: { - type: "array", - items: true - }, - multipleOf: { - type: "number", - exclusiveMinimum: 0 - }, - maximum: { - type: "number" - }, - exclusiveMaximum: { - type: "number" - }, - minimum: { - type: "number" - }, - exclusiveMinimum: { - type: "number" - }, - maxLength: { $ref: "#/definitions/nonNegativeInteger" }, - minLength: { $ref: "#/definitions/nonNegativeIntegerDefault0" }, - pattern: { - type: "string", - format: "regex" - }, - additionalItems: { $ref: "#" }, - items: { - anyOf: [{ $ref: "#" }, { $ref: "#/definitions/schemaArray" }], - default: true - }, - maxItems: { $ref: "#/definitions/nonNegativeInteger" }, - minItems: { $ref: "#/definitions/nonNegativeIntegerDefault0" }, - uniqueItems: { - type: "boolean", - default: false - }, - contains: { $ref: "#" }, - maxProperties: { $ref: "#/definitions/nonNegativeInteger" }, - minProperties: { $ref: "#/definitions/nonNegativeIntegerDefault0" }, - required: { $ref: "#/definitions/stringArray" }, - additionalProperties: { $ref: "#" }, - definitions: { - type: "object", - additionalProperties: { $ref: "#" }, - default: {} - }, - properties: { - type: "object", - additionalProperties: { $ref: "#" }, - default: {} - }, - patternProperties: { - type: "object", - additionalProperties: { $ref: "#" }, - propertyNames: { format: "regex" }, - default: {} - }, - dependencies: { - type: "object", - additionalProperties: { - anyOf: [{ $ref: "#" }, { $ref: "#/definitions/stringArray" }] - } - }, - propertyNames: { $ref: "#" }, - const: true, - enum: { - type: "array", - items: true, - minItems: 1, - uniqueItems: true - }, - type: { - anyOf: [ - { $ref: "#/definitions/simpleTypes" }, - { - type: "array", - items: { $ref: "#/definitions/simpleTypes" }, - minItems: 1, - uniqueItems: true - } - ] - }, - format: { type: "string" }, - contentMediaType: { type: "string" }, - contentEncoding: { type: "string" }, - if: { $ref: "#" }, - then: { $ref: "#" }, - else: { $ref: "#" }, - allOf: { $ref: "#/definitions/schemaArray" }, - anyOf: { $ref: "#/definitions/schemaArray" }, - oneOf: { $ref: "#/definitions/schemaArray" }, - not: { $ref: "#" } - }, - default: true - }; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/ajv.js -var require_ajv = __commonJS((exports, module) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.MissingRefError = exports.ValidationError = exports.CodeGen = exports.Name = exports.nil = exports.stringify = exports.str = exports._ = exports.KeywordCxt = exports.Ajv = undefined; - var core_1 = require_core(); - var draft7_1 = require_draft7(); - var discriminator_1 = require_discriminator(); - var draft7MetaSchema = require_json_schema_draft_07(); - var META_SUPPORT_DATA = ["/properties"]; - var META_SCHEMA_ID = "http://json-schema.org/draft-07/schema"; - - class Ajv extends core_1.default { - _addVocabularies() { - super._addVocabularies(); - draft7_1.default.forEach((v) => this.addVocabulary(v)); - if (this.opts.discriminator) - this.addKeyword(discriminator_1.default); - } - _addDefaultMetaSchema() { - super._addDefaultMetaSchema(); - if (!this.opts.meta) - return; - const metaSchema = this.opts.$data ? this.$dataMetaSchema(draft7MetaSchema, META_SUPPORT_DATA) : draft7MetaSchema; - this.addMetaSchema(metaSchema, META_SCHEMA_ID, false); - this.refs["http://json-schema.org/schema"] = META_SCHEMA_ID; - } - defaultMeta() { - return this.opts.defaultMeta = super.defaultMeta() || (this.getSchema(META_SCHEMA_ID) ? META_SCHEMA_ID : undefined); - } - } - exports.Ajv = Ajv; - module.exports = exports = Ajv; - module.exports.Ajv = Ajv; - Object.defineProperty(exports, "__esModule", { value: true }); - exports.default = Ajv; - var validate_1 = require_validate(); - Object.defineProperty(exports, "KeywordCxt", { enumerable: true, get: function() { - return validate_1.KeywordCxt; - } }); - var codegen_1 = require_codegen(); - Object.defineProperty(exports, "_", { enumerable: true, get: function() { - return codegen_1._; - } }); - Object.defineProperty(exports, "str", { enumerable: true, get: function() { - return codegen_1.str; - } }); - Object.defineProperty(exports, "stringify", { enumerable: true, get: function() { - return codegen_1.stringify; - } }); - Object.defineProperty(exports, "nil", { enumerable: true, get: function() { - return codegen_1.nil; - } }); - Object.defineProperty(exports, "Name", { enumerable: true, get: function() { - return codegen_1.Name; - } }); - Object.defineProperty(exports, "CodeGen", { enumerable: true, get: function() { - return codegen_1.CodeGen; - } }); - var validation_error_1 = require_validation_error(); - Object.defineProperty(exports, "ValidationError", { enumerable: true, get: function() { - return validation_error_1.default; - } }); - var ref_error_1 = require_ref_error(); - Object.defineProperty(exports, "MissingRefError", { enumerable: true, get: function() { - return ref_error_1.default; - } }); -}); - -// node_modules/ajv-formats/dist/formats.js -var require_formats = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.formatNames = exports.fastFormats = exports.fullFormats = undefined; - function fmtDef(validate, compare) { - return { validate, compare }; - } - exports.fullFormats = { - date: fmtDef(date4, compareDate), - time: fmtDef(getTime(true), compareTime), - "date-time": fmtDef(getDateTime(true), compareDateTime), - "iso-time": fmtDef(getTime(), compareIsoTime), - "iso-date-time": fmtDef(getDateTime(), compareIsoDateTime), - duration: /^P(?!$)((\d+Y)?(\d+M)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?|(\d+W)?)$/, - uri, - "uri-reference": /^(?:[a-z][a-z0-9+\-.]*:)?(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'"()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?(?:\?(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i, - "uri-template": /^(?:(?:[^\x00-\x20"'<>%\\^`{|}]|%[0-9a-f]{2})|\{[+#./;?&=,!@|]?(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?(?:,(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?)*\})*$/i, - url: /^(?:https?|ftp):\/\/(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u{00a1}-\u{ffff}]+-)*[a-z0-9\u{00a1}-\u{ffff}]+)(?:\.(?:[a-z0-9\u{00a1}-\u{ffff}]+-)*[a-z0-9\u{00a1}-\u{ffff}]+)*(?:\.(?:[a-z\u{00a1}-\u{ffff}]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/iu, - email: /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i, - hostname: /^(?=.{1,253}\.?$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[-0-9a-z]{0,61}[0-9a-z])?)*\.?$/i, - ipv4: /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/, - ipv6: /^((([0-9a-f]{1,4}:){7}([0-9a-f]{1,4}|:))|(([0-9a-f]{1,4}:){6}(:[0-9a-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){5}(((:[0-9a-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){4}(((:[0-9a-f]{1,4}){1,3})|((:[0-9a-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){3}(((:[0-9a-f]{1,4}){1,4})|((:[0-9a-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){2}(((:[0-9a-f]{1,4}){1,5})|((:[0-9a-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){1}(((:[0-9a-f]{1,4}){1,6})|((:[0-9a-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-f]{1,4}){1,7})|((:[0-9a-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))$/i, - regex, - uuid: /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i, - "json-pointer": /^(?:\/(?:[^~/]|~0|~1)*)*$/, - "json-pointer-uri-fragment": /^#(?:\/(?:[a-z0-9_\-.!$&'()*+,;:=@]|%[0-9a-f]{2}|~0|~1)*)*$/i, - "relative-json-pointer": /^(?:0|[1-9][0-9]*)(?:#|(?:\/(?:[^~/]|~0|~1)*)*)$/, - byte, - int32: { type: "number", validate: validateInt32 }, - int64: { type: "number", validate: validateInt64 }, - float: { type: "number", validate: validateNumber }, - double: { type: "number", validate: validateNumber }, - password: true, - binary: true - }; - exports.fastFormats = { - ...exports.fullFormats, - date: fmtDef(/^\d\d\d\d-[0-1]\d-[0-3]\d$/, compareDate), - time: fmtDef(/^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i, compareTime), - "date-time": fmtDef(/^\d\d\d\d-[0-1]\d-[0-3]\dt(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i, compareDateTime), - "iso-time": fmtDef(/^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)?$/i, compareIsoTime), - "iso-date-time": fmtDef(/^\d\d\d\d-[0-1]\d-[0-3]\d[t\s](?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)?$/i, compareIsoDateTime), - uri: /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/)?[^\s]*$/i, - "uri-reference": /^(?:(?:[a-z][a-z0-9+\-.]*:)?\/?\/)?(?:[^\\\s#][^\s#]*)?(?:#[^\\\s]*)?$/i, - email: /^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i - }; - exports.formatNames = Object.keys(exports.fullFormats); - function isLeapYear(year) { - return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); - } - var DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/; - var DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; - function date4(str) { - const matches = DATE.exec(str); - if (!matches) - return false; - const year = +matches[1]; - const month = +matches[2]; - const day = +matches[3]; - return month >= 1 && month <= 12 && day >= 1 && day <= (month === 2 && isLeapYear(year) ? 29 : DAYS[month]); - } - function compareDate(d1, d2) { - if (!(d1 && d2)) - return; - if (d1 > d2) - return 1; - if (d1 < d2) - return -1; - return 0; - } - var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i; - function getTime(strictTimeZone) { - return function time3(str) { - const matches = TIME.exec(str); - if (!matches) - return false; - const hr = +matches[1]; - const min = +matches[2]; - const sec = +matches[3]; - const tz = matches[4]; - const tzSign = matches[5] === "-" ? -1 : 1; - const tzH = +(matches[6] || 0); - const tzM = +(matches[7] || 0); - if (tzH > 23 || tzM > 59 || strictTimeZone && !tz) - return false; - if (hr <= 23 && min <= 59 && sec < 60) - return true; - const utcMin = min - tzM * tzSign; - const utcHr = hr - tzH * tzSign - (utcMin < 0 ? 1 : 0); - return (utcHr === 23 || utcHr === -1) && (utcMin === 59 || utcMin === -1) && sec < 61; - }; - } - function compareTime(s1, s2) { - if (!(s1 && s2)) - return; - const t1 = new Date("2020-01-01T" + s1).valueOf(); - const t2 = new Date("2020-01-01T" + s2).valueOf(); - if (!(t1 && t2)) - return; - return t1 - t2; - } - function compareIsoTime(t1, t2) { - if (!(t1 && t2)) - return; - const a1 = TIME.exec(t1); - const a2 = TIME.exec(t2); - if (!(a1 && a2)) - return; - t1 = a1[1] + a1[2] + a1[3]; - t2 = a2[1] + a2[2] + a2[3]; - if (t1 > t2) - return 1; - if (t1 < t2) - return -1; - return 0; - } - var DATE_TIME_SEPARATOR = /t|\s/i; - function getDateTime(strictTimeZone) { - const time3 = getTime(strictTimeZone); - return function date_time(str) { - const dateTime = str.split(DATE_TIME_SEPARATOR); - return dateTime.length === 2 && date4(dateTime[0]) && time3(dateTime[1]); - }; - } - function compareDateTime(dt1, dt2) { - if (!(dt1 && dt2)) - return; - const d1 = new Date(dt1).valueOf(); - const d2 = new Date(dt2).valueOf(); - if (!(d1 && d2)) - return; - return d1 - d2; - } - function compareIsoDateTime(dt1, dt2) { - if (!(dt1 && dt2)) - return; - const [d1, t1] = dt1.split(DATE_TIME_SEPARATOR); - const [d2, t2] = dt2.split(DATE_TIME_SEPARATOR); - const res = compareDate(d1, d2); - if (res === undefined) - return; - return res || compareTime(t1, t2); - } - var NOT_URI_FRAGMENT = /\/|:/; - var URI = /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i; - function uri(str) { - return NOT_URI_FRAGMENT.test(str) && URI.test(str); - } - var BYTE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/gm; - function byte(str) { - BYTE.lastIndex = 0; - return BYTE.test(str); - } - var MIN_INT32 = -(2 ** 31); - var MAX_INT32 = 2 ** 31 - 1; - function validateInt32(value) { - return Number.isInteger(value) && value <= MAX_INT32 && value >= MIN_INT32; - } - function validateInt64(value) { - return Number.isInteger(value); - } - function validateNumber() { - return true; - } - var Z_ANCHOR = /[^\\]\\Z/; - function regex(str) { - if (Z_ANCHOR.test(str)) - return false; - try { - new RegExp(str); - return true; - } catch (e) { - return false; - } - } -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/compile/codegen/code.js -var require_code3 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.regexpCode = exports.getEsmExportName = exports.getProperty = exports.safeStringify = exports.stringify = exports.strConcat = exports.addCodeArg = exports.str = exports._ = exports.nil = exports._Code = exports.Name = exports.IDENTIFIER = exports._CodeOrName = undefined; - - class _CodeOrName { - } - exports._CodeOrName = _CodeOrName; - exports.IDENTIFIER = /^[a-z$_][a-z$_0-9]*$/i; - - class Name extends _CodeOrName { - constructor(s) { - super(); - if (!exports.IDENTIFIER.test(s)) - throw new Error("CodeGen: name must be a valid identifier"); - this.str = s; - } - toString() { - return this.str; - } - emptyStr() { - return false; - } - get names() { - return { [this.str]: 1 }; - } - } - exports.Name = Name; - - class _Code extends _CodeOrName { - constructor(code) { - super(); - this._items = typeof code === "string" ? [code] : code; - } - toString() { - return this.str; - } - emptyStr() { - if (this._items.length > 1) - return false; - const item = this._items[0]; - return item === "" || item === '""'; - } - get str() { - var _a; - return (_a = this._str) !== null && _a !== undefined ? _a : this._str = this._items.reduce((s, c) => `${s}${c}`, ""); - } - get names() { - var _a; - return (_a = this._names) !== null && _a !== undefined ? _a : this._names = this._items.reduce((names, c) => { - if (c instanceof Name) - names[c.str] = (names[c.str] || 0) + 1; - return names; - }, {}); - } - } - exports._Code = _Code; - exports.nil = new _Code(""); - function _(strs, ...args) { - const code = [strs[0]]; - let i = 0; - while (i < args.length) { - addCodeArg(code, args[i]); - code.push(strs[++i]); - } - return new _Code(code); - } - exports._ = _; - var plus = new _Code("+"); - function str(strs, ...args) { - const expr = [safeStringify(strs[0])]; - let i = 0; - while (i < args.length) { - expr.push(plus); - addCodeArg(expr, args[i]); - expr.push(plus, safeStringify(strs[++i])); - } - optimize(expr); - return new _Code(expr); - } - exports.str = str; - function addCodeArg(code, arg) { - if (arg instanceof _Code) - code.push(...arg._items); - else if (arg instanceof Name) - code.push(arg); - else - code.push(interpolate(arg)); - } - exports.addCodeArg = addCodeArg; - function optimize(expr) { - let i = 1; - while (i < expr.length - 1) { - if (expr[i] === plus) { - const res = mergeExprItems(expr[i - 1], expr[i + 1]); - if (res !== undefined) { - expr.splice(i - 1, 3, res); - continue; - } - expr[i++] = "+"; - } - i++; - } - } - function mergeExprItems(a, b) { - if (b === '""') - return a; - if (a === '""') - return b; - if (typeof a == "string") { - if (b instanceof Name || a[a.length - 1] !== '"') - return; - if (typeof b != "string") - return `${a.slice(0, -1)}${b}"`; - if (b[0] === '"') - return a.slice(0, -1) + b.slice(1); - return; - } - if (typeof b == "string" && b[0] === '"' && !(a instanceof Name)) - return `"${a}${b.slice(1)}`; - return; - } - function strConcat(c1, c2) { - return c2.emptyStr() ? c1 : c1.emptyStr() ? c2 : str`${c1}${c2}`; - } - exports.strConcat = strConcat; - function interpolate(x) { - return typeof x == "number" || typeof x == "boolean" || x === null ? x : safeStringify(Array.isArray(x) ? x.join(",") : x); - } - function stringify(x) { - return new _Code(safeStringify(x)); - } - exports.stringify = stringify; - function safeStringify(x) { - return JSON.stringify(x).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029"); - } - exports.safeStringify = safeStringify; - function getProperty(key) { - return typeof key == "string" && exports.IDENTIFIER.test(key) ? new _Code(`.${key}`) : _`[${key}]`; - } - exports.getProperty = getProperty; - function getEsmExportName(key) { - if (typeof key == "string" && exports.IDENTIFIER.test(key)) { - return new _Code(`${key}`); - } - throw new Error(`CodeGen: invalid export name: ${key}, use explicit $id name mapping`); - } - exports.getEsmExportName = getEsmExportName; - function regexpCode(rx) { - return new _Code(rx.toString()); - } - exports.regexpCode = regexpCode; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/compile/codegen/scope.js -var require_scope2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.ValueScope = exports.ValueScopeName = exports.Scope = exports.varKinds = exports.UsedValueState = undefined; - var code_1 = require_code3(); - - class ValueError extends Error { - constructor(name) { - super(`CodeGen: "code" for ${name} not defined`); - this.value = name.value; - } - } - var UsedValueState; - (function(UsedValueState2) { - UsedValueState2[UsedValueState2["Started"] = 0] = "Started"; - UsedValueState2[UsedValueState2["Completed"] = 1] = "Completed"; - })(UsedValueState || (exports.UsedValueState = UsedValueState = {})); - exports.varKinds = { - const: new code_1.Name("const"), - let: new code_1.Name("let"), - var: new code_1.Name("var") - }; - - class Scope { - constructor({ prefixes, parent } = {}) { - this._names = {}; - this._prefixes = prefixes; - this._parent = parent; - } - toName(nameOrPrefix) { - return nameOrPrefix instanceof code_1.Name ? nameOrPrefix : this.name(nameOrPrefix); - } - name(prefix) { - return new code_1.Name(this._newName(prefix)); - } - _newName(prefix) { - const ng = this._names[prefix] || this._nameGroup(prefix); - return `${prefix}${ng.index++}`; - } - _nameGroup(prefix) { - var _a, _b; - if (((_b = (_a = this._parent) === null || _a === undefined ? undefined : _a._prefixes) === null || _b === undefined ? undefined : _b.has(prefix)) || this._prefixes && !this._prefixes.has(prefix)) { - throw new Error(`CodeGen: prefix "${prefix}" is not allowed in this scope`); - } - return this._names[prefix] = { prefix, index: 0 }; - } - } - exports.Scope = Scope; - - class ValueScopeName extends code_1.Name { - constructor(prefix, nameStr) { - super(nameStr); - this.prefix = prefix; - } - setValue(value, { property, itemIndex }) { - this.value = value; - this.scopePath = (0, code_1._)`.${new code_1.Name(property)}[${itemIndex}]`; - } - } - exports.ValueScopeName = ValueScopeName; - var line = (0, code_1._)`\n`; - - class ValueScope extends Scope { - constructor(opts) { - super(opts); - this._values = {}; - this._scope = opts.scope; - this.opts = { ...opts, _n: opts.lines ? line : code_1.nil }; - } - get() { - return this._scope; - } - name(prefix) { - return new ValueScopeName(prefix, this._newName(prefix)); - } - value(nameOrPrefix, value) { - var _a; - if (value.ref === undefined) - throw new Error("CodeGen: ref must be passed in value"); - const name = this.toName(nameOrPrefix); - const { prefix } = name; - const valueKey = (_a = value.key) !== null && _a !== undefined ? _a : value.ref; - let vs = this._values[prefix]; - if (vs) { - const _name = vs.get(valueKey); - if (_name) - return _name; - } else { - vs = this._values[prefix] = new Map; - } - vs.set(valueKey, name); - const s = this._scope[prefix] || (this._scope[prefix] = []); - const itemIndex = s.length; - s[itemIndex] = value.ref; - name.setValue(value, { property: prefix, itemIndex }); - return name; - } - getValue(prefix, keyOrRef) { - const vs = this._values[prefix]; - if (!vs) - return; - return vs.get(keyOrRef); - } - scopeRefs(scopeName, values = this._values) { - return this._reduceValues(values, (name) => { - if (name.scopePath === undefined) - throw new Error(`CodeGen: name "${name}" has no value`); - return (0, code_1._)`${scopeName}${name.scopePath}`; - }); - } - scopeCode(values = this._values, usedValues, getCode) { - return this._reduceValues(values, (name) => { - if (name.value === undefined) - throw new Error(`CodeGen: name "${name}" has no value`); - return name.value.code; - }, usedValues, getCode); - } - _reduceValues(values, valueCode, usedValues = {}, getCode) { - let code = code_1.nil; - for (const prefix in values) { - const vs = values[prefix]; - if (!vs) - continue; - const nameSet = usedValues[prefix] = usedValues[prefix] || new Map; - vs.forEach((name) => { - if (nameSet.has(name)) - return; - nameSet.set(name, UsedValueState.Started); - let c = valueCode(name); - if (c) { - const def = this.opts.es5 ? exports.varKinds.var : exports.varKinds.const; - code = (0, code_1._)`${code}${def} ${name} = ${c};${this.opts._n}`; - } else if (c = getCode === null || getCode === undefined ? undefined : getCode(name)) { - code = (0, code_1._)`${code}${c}${this.opts._n}`; - } else { - throw new ValueError(name); - } - nameSet.set(name, UsedValueState.Completed); - }); - } - return code; - } - } - exports.ValueScope = ValueScope; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/compile/codegen/index.js -var require_codegen2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.or = exports.and = exports.not = exports.CodeGen = exports.operators = exports.varKinds = exports.ValueScopeName = exports.ValueScope = exports.Scope = exports.Name = exports.regexpCode = exports.stringify = exports.getProperty = exports.nil = exports.strConcat = exports.str = exports._ = undefined; - var code_1 = require_code3(); - var scope_1 = require_scope2(); - var code_2 = require_code3(); - Object.defineProperty(exports, "_", { enumerable: true, get: function() { - return code_2._; - } }); - Object.defineProperty(exports, "str", { enumerable: true, get: function() { - return code_2.str; - } }); - Object.defineProperty(exports, "strConcat", { enumerable: true, get: function() { - return code_2.strConcat; - } }); - Object.defineProperty(exports, "nil", { enumerable: true, get: function() { - return code_2.nil; - } }); - Object.defineProperty(exports, "getProperty", { enumerable: true, get: function() { - return code_2.getProperty; - } }); - Object.defineProperty(exports, "stringify", { enumerable: true, get: function() { - return code_2.stringify; - } }); - Object.defineProperty(exports, "regexpCode", { enumerable: true, get: function() { - return code_2.regexpCode; - } }); - Object.defineProperty(exports, "Name", { enumerable: true, get: function() { - return code_2.Name; - } }); - var scope_2 = require_scope2(); - Object.defineProperty(exports, "Scope", { enumerable: true, get: function() { - return scope_2.Scope; - } }); - Object.defineProperty(exports, "ValueScope", { enumerable: true, get: function() { - return scope_2.ValueScope; - } }); - Object.defineProperty(exports, "ValueScopeName", { enumerable: true, get: function() { - return scope_2.ValueScopeName; - } }); - Object.defineProperty(exports, "varKinds", { enumerable: true, get: function() { - return scope_2.varKinds; - } }); - exports.operators = { - GT: new code_1._Code(">"), - GTE: new code_1._Code(">="), - LT: new code_1._Code("<"), - LTE: new code_1._Code("<="), - EQ: new code_1._Code("==="), - NEQ: new code_1._Code("!=="), - NOT: new code_1._Code("!"), - OR: new code_1._Code("||"), - AND: new code_1._Code("&&"), - ADD: new code_1._Code("+") - }; - - class Node { - optimizeNodes() { - return this; - } - optimizeNames(_names, _constants) { - return this; - } - } - - class Def extends Node { - constructor(varKind, name, rhs) { - super(); - this.varKind = varKind; - this.name = name; - this.rhs = rhs; - } - render({ es5, _n }) { - const varKind = es5 ? scope_1.varKinds.var : this.varKind; - const rhs = this.rhs === undefined ? "" : ` = ${this.rhs}`; - return `${varKind} ${this.name}${rhs};` + _n; - } - optimizeNames(names, constants) { - if (!names[this.name.str]) - return; - if (this.rhs) - this.rhs = optimizeExpr(this.rhs, names, constants); - return this; - } - get names() { - return this.rhs instanceof code_1._CodeOrName ? this.rhs.names : {}; - } - } - - class Assign extends Node { - constructor(lhs, rhs, sideEffects) { - super(); - this.lhs = lhs; - this.rhs = rhs; - this.sideEffects = sideEffects; - } - render({ _n }) { - return `${this.lhs} = ${this.rhs};` + _n; - } - optimizeNames(names, constants) { - if (this.lhs instanceof code_1.Name && !names[this.lhs.str] && !this.sideEffects) - return; - this.rhs = optimizeExpr(this.rhs, names, constants); - return this; - } - get names() { - const names = this.lhs instanceof code_1.Name ? {} : { ...this.lhs.names }; - return addExprNames(names, this.rhs); - } - } - - class AssignOp extends Assign { - constructor(lhs, op, rhs, sideEffects) { - super(lhs, rhs, sideEffects); - this.op = op; - } - render({ _n }) { - return `${this.lhs} ${this.op}= ${this.rhs};` + _n; - } - } - - class Label extends Node { - constructor(label) { - super(); - this.label = label; - this.names = {}; - } - render({ _n }) { - return `${this.label}:` + _n; - } - } - - class Break extends Node { - constructor(label) { - super(); - this.label = label; - this.names = {}; - } - render({ _n }) { - const label = this.label ? ` ${this.label}` : ""; - return `break${label};` + _n; - } - } - - class Throw extends Node { - constructor(error2) { - super(); - this.error = error2; - } - render({ _n }) { - return `throw ${this.error};` + _n; - } - get names() { - return this.error.names; - } - } - - class AnyCode extends Node { - constructor(code) { - super(); - this.code = code; - } - render({ _n }) { - return `${this.code};` + _n; - } - optimizeNodes() { - return `${this.code}` ? this : undefined; - } - optimizeNames(names, constants) { - this.code = optimizeExpr(this.code, names, constants); - return this; - } - get names() { - return this.code instanceof code_1._CodeOrName ? this.code.names : {}; - } - } - - class ParentNode extends Node { - constructor(nodes = []) { - super(); - this.nodes = nodes; - } - render(opts) { - return this.nodes.reduce((code, n) => code + n.render(opts), ""); - } - optimizeNodes() { - const { nodes } = this; - let i = nodes.length; - while (i--) { - const n = nodes[i].optimizeNodes(); - if (Array.isArray(n)) - nodes.splice(i, 1, ...n); - else if (n) - nodes[i] = n; - else - nodes.splice(i, 1); - } - return nodes.length > 0 ? this : undefined; - } - optimizeNames(names, constants) { - const { nodes } = this; - let i = nodes.length; - while (i--) { - const n = nodes[i]; - if (n.optimizeNames(names, constants)) - continue; - subtractNames(names, n.names); - nodes.splice(i, 1); - } - return nodes.length > 0 ? this : undefined; - } - get names() { - return this.nodes.reduce((names, n) => addNames(names, n.names), {}); - } - } - - class BlockNode extends ParentNode { - render(opts) { - return "{" + opts._n + super.render(opts) + "}" + opts._n; - } - } - - class Root extends ParentNode { - } - - class Else extends BlockNode { - } - Else.kind = "else"; - - class If extends BlockNode { - constructor(condition, nodes) { - super(nodes); - this.condition = condition; - } - render(opts) { - let code = `if(${this.condition})` + super.render(opts); - if (this.else) - code += "else " + this.else.render(opts); - return code; - } - optimizeNodes() { - super.optimizeNodes(); - const cond = this.condition; - if (cond === true) - return this.nodes; - let e = this.else; - if (e) { - const ns = e.optimizeNodes(); - e = this.else = Array.isArray(ns) ? new Else(ns) : ns; - } - if (e) { - if (cond === false) - return e instanceof If ? e : e.nodes; - if (this.nodes.length) - return this; - return new If(not(cond), e instanceof If ? [e] : e.nodes); - } - if (cond === false || !this.nodes.length) - return; - return this; - } - optimizeNames(names, constants) { - var _a; - this.else = (_a = this.else) === null || _a === undefined ? undefined : _a.optimizeNames(names, constants); - if (!(super.optimizeNames(names, constants) || this.else)) - return; - this.condition = optimizeExpr(this.condition, names, constants); - return this; - } - get names() { - const names = super.names; - addExprNames(names, this.condition); - if (this.else) - addNames(names, this.else.names); - return names; - } - } - If.kind = "if"; - - class For extends BlockNode { - } - For.kind = "for"; - - class ForLoop extends For { - constructor(iteration) { - super(); - this.iteration = iteration; - } - render(opts) { - return `for(${this.iteration})` + super.render(opts); - } - optimizeNames(names, constants) { - if (!super.optimizeNames(names, constants)) - return; - this.iteration = optimizeExpr(this.iteration, names, constants); - return this; - } - get names() { - return addNames(super.names, this.iteration.names); - } - } - - class ForRange extends For { - constructor(varKind, name, from, to) { - super(); - this.varKind = varKind; - this.name = name; - this.from = from; - this.to = to; - } - render(opts) { - const varKind = opts.es5 ? scope_1.varKinds.var : this.varKind; - const { name, from, to } = this; - return `for(${varKind} ${name}=${from}; ${name}<${to}; ${name}++)` + super.render(opts); - } - get names() { - const names = addExprNames(super.names, this.from); - return addExprNames(names, this.to); - } - } - - class ForIter extends For { - constructor(loop, varKind, name, iterable) { - super(); - this.loop = loop; - this.varKind = varKind; - this.name = name; - this.iterable = iterable; - } - render(opts) { - return `for(${this.varKind} ${this.name} ${this.loop} ${this.iterable})` + super.render(opts); - } - optimizeNames(names, constants) { - if (!super.optimizeNames(names, constants)) - return; - this.iterable = optimizeExpr(this.iterable, names, constants); - return this; - } - get names() { - return addNames(super.names, this.iterable.names); - } - } - - class Func extends BlockNode { - constructor(name, args, async) { - super(); - this.name = name; - this.args = args; - this.async = async; - } - render(opts) { - const _async = this.async ? "async " : ""; - return `${_async}function ${this.name}(${this.args})` + super.render(opts); - } - } - Func.kind = "func"; - - class Return extends ParentNode { - render(opts) { - return "return " + super.render(opts); - } - } - Return.kind = "return"; - - class Try extends BlockNode { - render(opts) { - let code = "try" + super.render(opts); - if (this.catch) - code += this.catch.render(opts); - if (this.finally) - code += this.finally.render(opts); - return code; - } - optimizeNodes() { - var _a, _b; - super.optimizeNodes(); - (_a = this.catch) === null || _a === undefined || _a.optimizeNodes(); - (_b = this.finally) === null || _b === undefined || _b.optimizeNodes(); - return this; - } - optimizeNames(names, constants) { - var _a, _b; - super.optimizeNames(names, constants); - (_a = this.catch) === null || _a === undefined || _a.optimizeNames(names, constants); - (_b = this.finally) === null || _b === undefined || _b.optimizeNames(names, constants); - return this; - } - get names() { - const names = super.names; - if (this.catch) - addNames(names, this.catch.names); - if (this.finally) - addNames(names, this.finally.names); - return names; - } - } - - class Catch extends BlockNode { - constructor(error2) { - super(); - this.error = error2; - } - render(opts) { - return `catch(${this.error})` + super.render(opts); - } - } - Catch.kind = "catch"; - - class Finally extends BlockNode { - render(opts) { - return "finally" + super.render(opts); - } - } - Finally.kind = "finally"; - - class CodeGen { - constructor(extScope, opts = {}) { - this._values = {}; - this._blockStarts = []; - this._constants = {}; - this.opts = { ...opts, _n: opts.lines ? ` -` : "" }; - this._extScope = extScope; - this._scope = new scope_1.Scope({ parent: extScope }); - this._nodes = [new Root]; - } - toString() { - return this._root.render(this.opts); - } - name(prefix) { - return this._scope.name(prefix); - } - scopeName(prefix) { - return this._extScope.name(prefix); - } - scopeValue(prefixOrName, value) { - const name = this._extScope.value(prefixOrName, value); - const vs = this._values[name.prefix] || (this._values[name.prefix] = new Set); - vs.add(name); - return name; - } - getScopeValue(prefix, keyOrRef) { - return this._extScope.getValue(prefix, keyOrRef); - } - scopeRefs(scopeName) { - return this._extScope.scopeRefs(scopeName, this._values); - } - scopeCode() { - return this._extScope.scopeCode(this._values); - } - _def(varKind, nameOrPrefix, rhs, constant) { - const name = this._scope.toName(nameOrPrefix); - if (rhs !== undefined && constant) - this._constants[name.str] = rhs; - this._leafNode(new Def(varKind, name, rhs)); - return name; - } - const(nameOrPrefix, rhs, _constant) { - return this._def(scope_1.varKinds.const, nameOrPrefix, rhs, _constant); - } - let(nameOrPrefix, rhs, _constant) { - return this._def(scope_1.varKinds.let, nameOrPrefix, rhs, _constant); - } - var(nameOrPrefix, rhs, _constant) { - return this._def(scope_1.varKinds.var, nameOrPrefix, rhs, _constant); - } - assign(lhs, rhs, sideEffects) { - return this._leafNode(new Assign(lhs, rhs, sideEffects)); - } - add(lhs, rhs) { - return this._leafNode(new AssignOp(lhs, exports.operators.ADD, rhs)); - } - code(c) { - if (typeof c == "function") - c(); - else if (c !== code_1.nil) - this._leafNode(new AnyCode(c)); - return this; - } - object(...keyValues) { - const code = ["{"]; - for (const [key, value] of keyValues) { - if (code.length > 1) - code.push(","); - code.push(key); - if (key !== value || this.opts.es5) { - code.push(":"); - (0, code_1.addCodeArg)(code, value); - } - } - code.push("}"); - return new code_1._Code(code); - } - if(condition, thenBody, elseBody) { - this._blockNode(new If(condition)); - if (thenBody && elseBody) { - this.code(thenBody).else().code(elseBody).endIf(); - } else if (thenBody) { - this.code(thenBody).endIf(); - } else if (elseBody) { - throw new Error('CodeGen: "else" body without "then" body'); - } - return this; - } - elseIf(condition) { - return this._elseNode(new If(condition)); - } - else() { - return this._elseNode(new Else); - } - endIf() { - return this._endBlockNode(If, Else); - } - _for(node, forBody) { - this._blockNode(node); - if (forBody) - this.code(forBody).endFor(); - return this; - } - for(iteration, forBody) { - return this._for(new ForLoop(iteration), forBody); - } - forRange(nameOrPrefix, from, to, forBody, varKind = this.opts.es5 ? scope_1.varKinds.var : scope_1.varKinds.let) { - const name = this._scope.toName(nameOrPrefix); - return this._for(new ForRange(varKind, name, from, to), () => forBody(name)); - } - forOf(nameOrPrefix, iterable, forBody, varKind = scope_1.varKinds.const) { - const name = this._scope.toName(nameOrPrefix); - if (this.opts.es5) { - const arr = iterable instanceof code_1.Name ? iterable : this.var("_arr", iterable); - return this.forRange("_i", 0, (0, code_1._)`${arr}.length`, (i) => { - this.var(name, (0, code_1._)`${arr}[${i}]`); - forBody(name); - }); - } - return this._for(new ForIter("of", varKind, name, iterable), () => forBody(name)); - } - forIn(nameOrPrefix, obj, forBody, varKind = this.opts.es5 ? scope_1.varKinds.var : scope_1.varKinds.const) { - if (this.opts.ownProperties) { - return this.forOf(nameOrPrefix, (0, code_1._)`Object.keys(${obj})`, forBody); - } - const name = this._scope.toName(nameOrPrefix); - return this._for(new ForIter("in", varKind, name, obj), () => forBody(name)); - } - endFor() { - return this._endBlockNode(For); - } - label(label) { - return this._leafNode(new Label(label)); - } - break(label) { - return this._leafNode(new Break(label)); - } - return(value) { - const node = new Return; - this._blockNode(node); - this.code(value); - if (node.nodes.length !== 1) - throw new Error('CodeGen: "return" should have one node'); - return this._endBlockNode(Return); - } - try(tryBody, catchCode, finallyCode) { - if (!catchCode && !finallyCode) - throw new Error('CodeGen: "try" without "catch" and "finally"'); - const node = new Try; - this._blockNode(node); - this.code(tryBody); - if (catchCode) { - const error2 = this.name("e"); - this._currNode = node.catch = new Catch(error2); - catchCode(error2); - } - if (finallyCode) { - this._currNode = node.finally = new Finally; - this.code(finallyCode); - } - return this._endBlockNode(Catch, Finally); - } - throw(error2) { - return this._leafNode(new Throw(error2)); - } - block(body, nodeCount) { - this._blockStarts.push(this._nodes.length); - if (body) - this.code(body).endBlock(nodeCount); - return this; - } - endBlock(nodeCount) { - const len = this._blockStarts.pop(); - if (len === undefined) - throw new Error("CodeGen: not in self-balancing block"); - const toClose = this._nodes.length - len; - if (toClose < 0 || nodeCount !== undefined && toClose !== nodeCount) { - throw new Error(`CodeGen: wrong number of nodes: ${toClose} vs ${nodeCount} expected`); - } - this._nodes.length = len; - return this; - } - func(name, args = code_1.nil, async, funcBody) { - this._blockNode(new Func(name, args, async)); - if (funcBody) - this.code(funcBody).endFunc(); - return this; - } - endFunc() { - return this._endBlockNode(Func); - } - optimize(n = 1) { - while (n-- > 0) { - this._root.optimizeNodes(); - this._root.optimizeNames(this._root.names, this._constants); - } - } - _leafNode(node) { - this._currNode.nodes.push(node); - return this; - } - _blockNode(node) { - this._currNode.nodes.push(node); - this._nodes.push(node); - } - _endBlockNode(N1, N2) { - const n = this._currNode; - if (n instanceof N1 || N2 && n instanceof N2) { - this._nodes.pop(); - return this; - } - throw new Error(`CodeGen: not in block "${N2 ? `${N1.kind}/${N2.kind}` : N1.kind}"`); - } - _elseNode(node) { - const n = this._currNode; - if (!(n instanceof If)) { - throw new Error('CodeGen: "else" without "if"'); - } - this._currNode = n.else = node; - return this; - } - get _root() { - return this._nodes[0]; - } - get _currNode() { - const ns = this._nodes; - return ns[ns.length - 1]; - } - set _currNode(node) { - const ns = this._nodes; - ns[ns.length - 1] = node; - } - } - exports.CodeGen = CodeGen; - function addNames(names, from) { - for (const n in from) - names[n] = (names[n] || 0) + (from[n] || 0); - return names; - } - function addExprNames(names, from) { - return from instanceof code_1._CodeOrName ? addNames(names, from.names) : names; - } - function optimizeExpr(expr, names, constants) { - if (expr instanceof code_1.Name) - return replaceName(expr); - if (!canOptimize(expr)) - return expr; - return new code_1._Code(expr._items.reduce((items, c) => { - if (c instanceof code_1.Name) - c = replaceName(c); - if (c instanceof code_1._Code) - items.push(...c._items); - else - items.push(c); - return items; - }, [])); - function replaceName(n) { - const c = constants[n.str]; - if (c === undefined || names[n.str] !== 1) - return n; - delete names[n.str]; - return c; - } - function canOptimize(e) { - return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 && constants[c.str] !== undefined); - } - } - function subtractNames(names, from) { - for (const n in from) - names[n] = (names[n] || 0) - (from[n] || 0); - } - function not(x) { - return typeof x == "boolean" || typeof x == "number" || x === null ? !x : (0, code_1._)`!${par(x)}`; - } - exports.not = not; - var andCode = mappend(exports.operators.AND); - function and(...args) { - return args.reduce(andCode); - } - exports.and = and; - var orCode = mappend(exports.operators.OR); - function or(...args) { - return args.reduce(orCode); - } - exports.or = or; - function mappend(op) { - return (x, y) => x === code_1.nil ? y : y === code_1.nil ? x : (0, code_1._)`${par(x)} ${op} ${par(y)}`; - } - function par(x) { - return x instanceof code_1.Name ? x : (0, code_1._)`(${x})`; - } -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/compile/util.js -var require_util2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.checkStrictMode = exports.getErrorPath = exports.Type = exports.useFunc = exports.setEvaluated = exports.evaluatedPropsToName = exports.mergeEvaluated = exports.eachItem = exports.unescapeJsonPointer = exports.escapeJsonPointer = exports.escapeFragment = exports.unescapeFragment = exports.schemaRefOrVal = exports.schemaHasRulesButRef = exports.schemaHasRules = exports.checkUnknownRules = exports.alwaysValidSchema = exports.toHash = undefined; - var codegen_1 = require_codegen2(); - var code_1 = require_code3(); - function toHash(arr) { - const hash = {}; - for (const item of arr) - hash[item] = true; - return hash; - } - exports.toHash = toHash; - function alwaysValidSchema(it, schema) { - if (typeof schema == "boolean") - return schema; - if (Object.keys(schema).length === 0) - return true; - checkUnknownRules(it, schema); - return !schemaHasRules(schema, it.self.RULES.all); - } - exports.alwaysValidSchema = alwaysValidSchema; - function checkUnknownRules(it, schema = it.schema) { - const { opts, self } = it; - if (!opts.strictSchema) - return; - if (typeof schema === "boolean") - return; - const rules = self.RULES.keywords; - for (const key in schema) { - if (!rules[key]) - checkStrictMode(it, `unknown keyword: "${key}"`); - } - } - exports.checkUnknownRules = checkUnknownRules; - function schemaHasRules(schema, rules) { - if (typeof schema == "boolean") - return !schema; - for (const key in schema) - if (rules[key]) - return true; - return false; - } - exports.schemaHasRules = schemaHasRules; - function schemaHasRulesButRef(schema, RULES) { - if (typeof schema == "boolean") - return !schema; - for (const key in schema) - if (key !== "$ref" && RULES.all[key]) - return true; - return false; - } - exports.schemaHasRulesButRef = schemaHasRulesButRef; - function schemaRefOrVal({ topSchemaRef, schemaPath }, schema, keyword, $data) { - if (!$data) { - if (typeof schema == "number" || typeof schema == "boolean") - return schema; - if (typeof schema == "string") - return (0, codegen_1._)`${schema}`; - } - return (0, codegen_1._)`${topSchemaRef}${schemaPath}${(0, codegen_1.getProperty)(keyword)}`; - } - exports.schemaRefOrVal = schemaRefOrVal; - function unescapeFragment(str) { - return unescapeJsonPointer(decodeURIComponent(str)); - } - exports.unescapeFragment = unescapeFragment; - function escapeFragment(str) { - return encodeURIComponent(escapeJsonPointer(str)); - } - exports.escapeFragment = escapeFragment; - function escapeJsonPointer(str) { - if (typeof str == "number") - return `${str}`; - return str.replace(/~/g, "~0").replace(/\//g, "~1"); - } - exports.escapeJsonPointer = escapeJsonPointer; - function unescapeJsonPointer(str) { - return str.replace(/~1/g, "/").replace(/~0/g, "~"); - } - exports.unescapeJsonPointer = unescapeJsonPointer; - function eachItem(xs, f) { - if (Array.isArray(xs)) { - for (const x of xs) - f(x); - } else { - f(xs); - } - } - exports.eachItem = eachItem; - function makeMergeEvaluated({ mergeNames, mergeToName, mergeValues: mergeValues2, resultToName }) { - return (gen, from, to, toName) => { - const res = to === undefined ? from : to instanceof codegen_1.Name ? (from instanceof codegen_1.Name ? mergeNames(gen, from, to) : mergeToName(gen, from, to), to) : from instanceof codegen_1.Name ? (mergeToName(gen, to, from), from) : mergeValues2(from, to); - return toName === codegen_1.Name && !(res instanceof codegen_1.Name) ? resultToName(gen, res) : res; - }; - } - exports.mergeEvaluated = { - props: makeMergeEvaluated({ - mergeNames: (gen, from, to) => gen.if((0, codegen_1._)`${to} !== true && ${from} !== undefined`, () => { - gen.if((0, codegen_1._)`${from} === true`, () => gen.assign(to, true), () => gen.assign(to, (0, codegen_1._)`${to} || {}`).code((0, codegen_1._)`Object.assign(${to}, ${from})`)); - }), - mergeToName: (gen, from, to) => gen.if((0, codegen_1._)`${to} !== true`, () => { - if (from === true) { - gen.assign(to, true); - } else { - gen.assign(to, (0, codegen_1._)`${to} || {}`); - setEvaluated(gen, to, from); - } - }), - mergeValues: (from, to) => from === true ? true : { ...from, ...to }, - resultToName: evaluatedPropsToName - }), - items: makeMergeEvaluated({ - mergeNames: (gen, from, to) => gen.if((0, codegen_1._)`${to} !== true && ${from} !== undefined`, () => gen.assign(to, (0, codegen_1._)`${from} === true ? true : ${to} > ${from} ? ${to} : ${from}`)), - mergeToName: (gen, from, to) => gen.if((0, codegen_1._)`${to} !== true`, () => gen.assign(to, from === true ? true : (0, codegen_1._)`${to} > ${from} ? ${to} : ${from}`)), - mergeValues: (from, to) => from === true ? true : Math.max(from, to), - resultToName: (gen, items) => gen.var("items", items) - }) - }; - function evaluatedPropsToName(gen, ps) { - if (ps === true) - return gen.var("props", true); - const props = gen.var("props", (0, codegen_1._)`{}`); - if (ps !== undefined) - setEvaluated(gen, props, ps); - return props; - } - exports.evaluatedPropsToName = evaluatedPropsToName; - function setEvaluated(gen, props, ps) { - Object.keys(ps).forEach((p) => gen.assign((0, codegen_1._)`${props}${(0, codegen_1.getProperty)(p)}`, true)); - } - exports.setEvaluated = setEvaluated; - var snippets = {}; - function useFunc(gen, f) { - return gen.scopeValue("func", { - ref: f, - code: snippets[f.code] || (snippets[f.code] = new code_1._Code(f.code)) - }); - } - exports.useFunc = useFunc; - var Type; - (function(Type2) { - Type2[Type2["Num"] = 0] = "Num"; - Type2[Type2["Str"] = 1] = "Str"; - })(Type || (exports.Type = Type = {})); - function getErrorPath(dataProp, dataPropType, jsPropertySyntax) { - if (dataProp instanceof codegen_1.Name) { - const isNumber = dataPropType === Type.Num; - return jsPropertySyntax ? isNumber ? (0, codegen_1._)`"[" + ${dataProp} + "]"` : (0, codegen_1._)`"['" + ${dataProp} + "']"` : isNumber ? (0, codegen_1._)`"/" + ${dataProp}` : (0, codegen_1._)`"/" + ${dataProp}.replace(/~/g, "~0").replace(/\\//g, "~1")`; - } - return jsPropertySyntax ? (0, codegen_1.getProperty)(dataProp).toString() : "/" + escapeJsonPointer(dataProp); - } - exports.getErrorPath = getErrorPath; - function checkStrictMode(it, msg, mode = it.opts.strictSchema) { - if (!mode) - return; - msg = `strict mode: ${msg}`; - if (mode === true) - throw new Error(msg); - it.self.logger.warn(msg); - } - exports.checkStrictMode = checkStrictMode; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/compile/names.js -var require_names2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var codegen_1 = require_codegen2(); - var names = { - data: new codegen_1.Name("data"), - valCxt: new codegen_1.Name("valCxt"), - instancePath: new codegen_1.Name("instancePath"), - parentData: new codegen_1.Name("parentData"), - parentDataProperty: new codegen_1.Name("parentDataProperty"), - rootData: new codegen_1.Name("rootData"), - dynamicAnchors: new codegen_1.Name("dynamicAnchors"), - vErrors: new codegen_1.Name("vErrors"), - errors: new codegen_1.Name("errors"), - this: new codegen_1.Name("this"), - self: new codegen_1.Name("self"), - scope: new codegen_1.Name("scope"), - json: new codegen_1.Name("json"), - jsonPos: new codegen_1.Name("jsonPos"), - jsonLen: new codegen_1.Name("jsonLen"), - jsonPart: new codegen_1.Name("jsonPart") - }; - exports.default = names; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/compile/errors.js -var require_errors2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.extendErrors = exports.resetErrorsCount = exports.reportExtraError = exports.reportError = exports.keyword$DataError = exports.keywordError = undefined; - var codegen_1 = require_codegen2(); - var util_1 = require_util2(); - var names_1 = require_names2(); - exports.keywordError = { - message: ({ keyword }) => (0, codegen_1.str)`must pass "${keyword}" keyword validation` - }; - exports.keyword$DataError = { - message: ({ keyword, schemaType }) => schemaType ? (0, codegen_1.str)`"${keyword}" keyword must be ${schemaType} ($data)` : (0, codegen_1.str)`"${keyword}" keyword is invalid ($data)` - }; - function reportError(cxt, error2 = exports.keywordError, errorPaths, overrideAllErrors) { - const { it } = cxt; - const { gen, compositeRule, allErrors } = it; - const errObj = errorObjectCode(cxt, error2, errorPaths); - if (overrideAllErrors !== null && overrideAllErrors !== undefined ? overrideAllErrors : compositeRule || allErrors) { - addError(gen, errObj); - } else { - returnErrors(it, (0, codegen_1._)`[${errObj}]`); - } - } - exports.reportError = reportError; - function reportExtraError(cxt, error2 = exports.keywordError, errorPaths) { - const { it } = cxt; - const { gen, compositeRule, allErrors } = it; - const errObj = errorObjectCode(cxt, error2, errorPaths); - addError(gen, errObj); - if (!(compositeRule || allErrors)) { - returnErrors(it, names_1.default.vErrors); - } - } - exports.reportExtraError = reportExtraError; - function resetErrorsCount(gen, errsCount) { - gen.assign(names_1.default.errors, errsCount); - gen.if((0, codegen_1._)`${names_1.default.vErrors} !== null`, () => gen.if(errsCount, () => gen.assign((0, codegen_1._)`${names_1.default.vErrors}.length`, errsCount), () => gen.assign(names_1.default.vErrors, null))); - } - exports.resetErrorsCount = resetErrorsCount; - function extendErrors({ gen, keyword, schemaValue, data, errsCount, it }) { - if (errsCount === undefined) - throw new Error("ajv implementation error"); - const err = gen.name("err"); - gen.forRange("i", errsCount, names_1.default.errors, (i) => { - gen.const(err, (0, codegen_1._)`${names_1.default.vErrors}[${i}]`); - gen.if((0, codegen_1._)`${err}.instancePath === undefined`, () => gen.assign((0, codegen_1._)`${err}.instancePath`, (0, codegen_1.strConcat)(names_1.default.instancePath, it.errorPath))); - gen.assign((0, codegen_1._)`${err}.schemaPath`, (0, codegen_1.str)`${it.errSchemaPath}/${keyword}`); - if (it.opts.verbose) { - gen.assign((0, codegen_1._)`${err}.schema`, schemaValue); - gen.assign((0, codegen_1._)`${err}.data`, data); - } - }); - } - exports.extendErrors = extendErrors; - function addError(gen, errObj) { - const err = gen.const("err", errObj); - gen.if((0, codegen_1._)`${names_1.default.vErrors} === null`, () => gen.assign(names_1.default.vErrors, (0, codegen_1._)`[${err}]`), (0, codegen_1._)`${names_1.default.vErrors}.push(${err})`); - gen.code((0, codegen_1._)`${names_1.default.errors}++`); - } - function returnErrors(it, errs) { - const { gen, validateName, schemaEnv } = it; - if (schemaEnv.$async) { - gen.throw((0, codegen_1._)`new ${it.ValidationError}(${errs})`); - } else { - gen.assign((0, codegen_1._)`${validateName}.errors`, errs); - gen.return(false); - } - } - var E = { - keyword: new codegen_1.Name("keyword"), - schemaPath: new codegen_1.Name("schemaPath"), - params: new codegen_1.Name("params"), - propertyName: new codegen_1.Name("propertyName"), - message: new codegen_1.Name("message"), - schema: new codegen_1.Name("schema"), - parentSchema: new codegen_1.Name("parentSchema") - }; - function errorObjectCode(cxt, error2, errorPaths) { - const { createErrors } = cxt.it; - if (createErrors === false) - return (0, codegen_1._)`{}`; - return errorObject(cxt, error2, errorPaths); - } - function errorObject(cxt, error2, errorPaths = {}) { - const { gen, it } = cxt; - const keyValues = [ - errorInstancePath(it, errorPaths), - errorSchemaPath(cxt, errorPaths) - ]; - extraErrorProps(cxt, error2, keyValues); - return gen.object(...keyValues); - } - function errorInstancePath({ errorPath }, { instancePath }) { - const instPath = instancePath ? (0, codegen_1.str)`${errorPath}${(0, util_1.getErrorPath)(instancePath, util_1.Type.Str)}` : errorPath; - return [names_1.default.instancePath, (0, codegen_1.strConcat)(names_1.default.instancePath, instPath)]; - } - function errorSchemaPath({ keyword, it: { errSchemaPath } }, { schemaPath, parentSchema }) { - let schPath = parentSchema ? errSchemaPath : (0, codegen_1.str)`${errSchemaPath}/${keyword}`; - if (schemaPath) { - schPath = (0, codegen_1.str)`${schPath}${(0, util_1.getErrorPath)(schemaPath, util_1.Type.Str)}`; - } - return [E.schemaPath, schPath]; - } - function extraErrorProps(cxt, { params, message }, keyValues) { - const { keyword, data, schemaValue, it } = cxt; - const { opts, propertyName, topSchemaRef, schemaPath } = it; - keyValues.push([E.keyword, keyword], [E.params, typeof params == "function" ? params(cxt) : params || (0, codegen_1._)`{}`]); - if (opts.messages) { - keyValues.push([E.message, typeof message == "function" ? message(cxt) : message]); - } - if (opts.verbose) { - keyValues.push([E.schema, schemaValue], [E.parentSchema, (0, codegen_1._)`${topSchemaRef}${schemaPath}`], [names_1.default.data, data]); - } - if (propertyName) - keyValues.push([E.propertyName, propertyName]); - } -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/compile/validate/boolSchema.js -var require_boolSchema2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.boolOrEmptySchema = exports.topBoolOrEmptySchema = undefined; - var errors_1 = require_errors2(); - var codegen_1 = require_codegen2(); - var names_1 = require_names2(); - var boolError = { - message: "boolean schema is false" - }; - function topBoolOrEmptySchema(it) { - const { gen, schema, validateName } = it; - if (schema === false) { - falseSchemaError(it, false); - } else if (typeof schema == "object" && schema.$async === true) { - gen.return(names_1.default.data); - } else { - gen.assign((0, codegen_1._)`${validateName}.errors`, null); - gen.return(true); - } - } - exports.topBoolOrEmptySchema = topBoolOrEmptySchema; - function boolOrEmptySchema(it, valid) { - const { gen, schema } = it; - if (schema === false) { - gen.var(valid, false); - falseSchemaError(it); - } else { - gen.var(valid, true); - } - } - exports.boolOrEmptySchema = boolOrEmptySchema; - function falseSchemaError(it, overrideAllErrors) { - const { gen, data } = it; - const cxt = { - gen, - keyword: "false schema", - data, - schema: false, - schemaCode: false, - schemaValue: false, - params: {}, - it - }; - (0, errors_1.reportError)(cxt, boolError, undefined, overrideAllErrors); - } -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/compile/rules.js -var require_rules2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.getRules = exports.isJSONType = undefined; - var _jsonTypes = ["string", "number", "integer", "boolean", "null", "object", "array"]; - var jsonTypes = new Set(_jsonTypes); - function isJSONType(x) { - return typeof x == "string" && jsonTypes.has(x); - } - exports.isJSONType = isJSONType; - function getRules() { - const groups = { - number: { type: "number", rules: [] }, - string: { type: "string", rules: [] }, - array: { type: "array", rules: [] }, - object: { type: "object", rules: [] } - }; - return { - types: { ...groups, integer: true, boolean: true, null: true }, - rules: [{ rules: [] }, groups.number, groups.string, groups.array, groups.object], - post: { rules: [] }, - all: {}, - keywords: {} - }; - } - exports.getRules = getRules; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/compile/validate/applicability.js -var require_applicability2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.shouldUseRule = exports.shouldUseGroup = exports.schemaHasRulesForType = undefined; - function schemaHasRulesForType({ schema, self }, type) { - const group = self.RULES.types[type]; - return group && group !== true && shouldUseGroup(schema, group); - } - exports.schemaHasRulesForType = schemaHasRulesForType; - function shouldUseGroup(schema, group) { - return group.rules.some((rule) => shouldUseRule(schema, rule)); - } - exports.shouldUseGroup = shouldUseGroup; - function shouldUseRule(schema, rule) { - var _a; - return schema[rule.keyword] !== undefined || ((_a = rule.definition.implements) === null || _a === undefined ? undefined : _a.some((kwd) => schema[kwd] !== undefined)); - } - exports.shouldUseRule = shouldUseRule; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/compile/validate/dataType.js -var require_dataType2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.reportTypeError = exports.checkDataTypes = exports.checkDataType = exports.coerceAndCheckDataType = exports.getJSONTypes = exports.getSchemaTypes = exports.DataType = undefined; - var rules_1 = require_rules2(); - var applicability_1 = require_applicability2(); - var errors_1 = require_errors2(); - var codegen_1 = require_codegen2(); - var util_1 = require_util2(); - var DataType; - (function(DataType2) { - DataType2[DataType2["Correct"] = 0] = "Correct"; - DataType2[DataType2["Wrong"] = 1] = "Wrong"; - })(DataType || (exports.DataType = DataType = {})); - function getSchemaTypes(schema) { - const types = getJSONTypes(schema.type); - const hasNull = types.includes("null"); - if (hasNull) { - if (schema.nullable === false) - throw new Error("type: null contradicts nullable: false"); - } else { - if (!types.length && schema.nullable !== undefined) { - throw new Error('"nullable" cannot be used without "type"'); - } - if (schema.nullable === true) - types.push("null"); - } - return types; - } - exports.getSchemaTypes = getSchemaTypes; - function getJSONTypes(ts) { - const types = Array.isArray(ts) ? ts : ts ? [ts] : []; - if (types.every(rules_1.isJSONType)) - return types; - throw new Error("type must be JSONType or JSONType[]: " + types.join(",")); - } - exports.getJSONTypes = getJSONTypes; - function coerceAndCheckDataType(it, types) { - const { gen, data, opts } = it; - const coerceTo = coerceToTypes(types, opts.coerceTypes); - const checkTypes = types.length > 0 && !(coerceTo.length === 0 && types.length === 1 && (0, applicability_1.schemaHasRulesForType)(it, types[0])); - if (checkTypes) { - const wrongType = checkDataTypes(types, data, opts.strictNumbers, DataType.Wrong); - gen.if(wrongType, () => { - if (coerceTo.length) - coerceData(it, types, coerceTo); - else - reportTypeError(it); - }); - } - return checkTypes; - } - exports.coerceAndCheckDataType = coerceAndCheckDataType; - var COERCIBLE = new Set(["string", "number", "integer", "boolean", "null"]); - function coerceToTypes(types, coerceTypes) { - return coerceTypes ? types.filter((t) => COERCIBLE.has(t) || coerceTypes === "array" && t === "array") : []; - } - function coerceData(it, types, coerceTo) { - const { gen, data, opts } = it; - const dataType = gen.let("dataType", (0, codegen_1._)`typeof ${data}`); - const coerced = gen.let("coerced", (0, codegen_1._)`undefined`); - if (opts.coerceTypes === "array") { - gen.if((0, codegen_1._)`${dataType} == 'object' && Array.isArray(${data}) && ${data}.length == 1`, () => gen.assign(data, (0, codegen_1._)`${data}[0]`).assign(dataType, (0, codegen_1._)`typeof ${data}`).if(checkDataTypes(types, data, opts.strictNumbers), () => gen.assign(coerced, data))); - } - gen.if((0, codegen_1._)`${coerced} !== undefined`); - for (const t of coerceTo) { - if (COERCIBLE.has(t) || t === "array" && opts.coerceTypes === "array") { - coerceSpecificType(t); - } - } - gen.else(); - reportTypeError(it); - gen.endIf(); - gen.if((0, codegen_1._)`${coerced} !== undefined`, () => { - gen.assign(data, coerced); - assignParentData(it, coerced); - }); - function coerceSpecificType(t) { - switch (t) { - case "string": - gen.elseIf((0, codegen_1._)`${dataType} == "number" || ${dataType} == "boolean"`).assign(coerced, (0, codegen_1._)`"" + ${data}`).elseIf((0, codegen_1._)`${data} === null`).assign(coerced, (0, codegen_1._)`""`); - return; - case "number": - gen.elseIf((0, codegen_1._)`${dataType} == "boolean" || ${data} === null - || (${dataType} == "string" && ${data} && ${data} == +${data})`).assign(coerced, (0, codegen_1._)`+${data}`); - return; - case "integer": - gen.elseIf((0, codegen_1._)`${dataType} === "boolean" || ${data} === null - || (${dataType} === "string" && ${data} && ${data} == +${data} && !(${data} % 1))`).assign(coerced, (0, codegen_1._)`+${data}`); - return; - case "boolean": - gen.elseIf((0, codegen_1._)`${data} === "false" || ${data} === 0 || ${data} === null`).assign(coerced, false).elseIf((0, codegen_1._)`${data} === "true" || ${data} === 1`).assign(coerced, true); - return; - case "null": - gen.elseIf((0, codegen_1._)`${data} === "" || ${data} === 0 || ${data} === false`); - gen.assign(coerced, null); - return; - case "array": - gen.elseIf((0, codegen_1._)`${dataType} === "string" || ${dataType} === "number" - || ${dataType} === "boolean" || ${data} === null`).assign(coerced, (0, codegen_1._)`[${data}]`); - } - } - } - function assignParentData({ gen, parentData, parentDataProperty }, expr) { - gen.if((0, codegen_1._)`${parentData} !== undefined`, () => gen.assign((0, codegen_1._)`${parentData}[${parentDataProperty}]`, expr)); - } - function checkDataType(dataType, data, strictNums, correct = DataType.Correct) { - const EQ = correct === DataType.Correct ? codegen_1.operators.EQ : codegen_1.operators.NEQ; - let cond; - switch (dataType) { - case "null": - return (0, codegen_1._)`${data} ${EQ} null`; - case "array": - cond = (0, codegen_1._)`Array.isArray(${data})`; - break; - case "object": - cond = (0, codegen_1._)`${data} && typeof ${data} == "object" && !Array.isArray(${data})`; - break; - case "integer": - cond = numCond((0, codegen_1._)`!(${data} % 1) && !isNaN(${data})`); - break; - case "number": - cond = numCond(); - break; - default: - return (0, codegen_1._)`typeof ${data} ${EQ} ${dataType}`; - } - return correct === DataType.Correct ? cond : (0, codegen_1.not)(cond); - function numCond(_cond = codegen_1.nil) { - return (0, codegen_1.and)((0, codegen_1._)`typeof ${data} == "number"`, _cond, strictNums ? (0, codegen_1._)`isFinite(${data})` : codegen_1.nil); - } - } - exports.checkDataType = checkDataType; - function checkDataTypes(dataTypes, data, strictNums, correct) { - if (dataTypes.length === 1) { - return checkDataType(dataTypes[0], data, strictNums, correct); - } - let cond; - const types = (0, util_1.toHash)(dataTypes); - if (types.array && types.object) { - const notObj = (0, codegen_1._)`typeof ${data} != "object"`; - cond = types.null ? notObj : (0, codegen_1._)`!${data} || ${notObj}`; - delete types.null; - delete types.array; - delete types.object; - } else { - cond = codegen_1.nil; - } - if (types.number) - delete types.integer; - for (const t in types) - cond = (0, codegen_1.and)(cond, checkDataType(t, data, strictNums, correct)); - return cond; - } - exports.checkDataTypes = checkDataTypes; - var typeError = { - message: ({ schema }) => `must be ${schema}`, - params: ({ schema, schemaValue }) => typeof schema == "string" ? (0, codegen_1._)`{type: ${schema}}` : (0, codegen_1._)`{type: ${schemaValue}}` - }; - function reportTypeError(it) { - const cxt = getTypeErrorContext(it); - (0, errors_1.reportError)(cxt, typeError); - } - exports.reportTypeError = reportTypeError; - function getTypeErrorContext(it) { - const { gen, data, schema } = it; - const schemaCode = (0, util_1.schemaRefOrVal)(it, schema, "type"); - return { - gen, - keyword: "type", - data, - schema: schema.type, - schemaCode, - schemaValue: schemaCode, - parentSchema: schema, - params: {}, - it - }; - } -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/compile/validate/defaults.js -var require_defaults2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.assignDefaults = undefined; - var codegen_1 = require_codegen2(); - var util_1 = require_util2(); - function assignDefaults(it, ty) { - const { properties, items } = it.schema; - if (ty === "object" && properties) { - for (const key in properties) { - assignDefault(it, key, properties[key].default); - } - } else if (ty === "array" && Array.isArray(items)) { - items.forEach((sch, i) => assignDefault(it, i, sch.default)); - } - } - exports.assignDefaults = assignDefaults; - function assignDefault(it, prop, defaultValue) { - const { gen, compositeRule, data, opts } = it; - if (defaultValue === undefined) - return; - const childData = (0, codegen_1._)`${data}${(0, codegen_1.getProperty)(prop)}`; - if (compositeRule) { - (0, util_1.checkStrictMode)(it, `default is ignored for: ${childData}`); - return; - } - let condition = (0, codegen_1._)`${childData} === undefined`; - if (opts.useDefaults === "empty") { - condition = (0, codegen_1._)`${condition} || ${childData} === null || ${childData} === ""`; - } - gen.if(condition, (0, codegen_1._)`${childData} = ${(0, codegen_1.stringify)(defaultValue)}`); - } -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/code.js -var require_code4 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.validateUnion = exports.validateArray = exports.usePattern = exports.callValidateCode = exports.schemaProperties = exports.allSchemaProperties = exports.noPropertyInData = exports.propertyInData = exports.isOwnProperty = exports.hasPropFunc = exports.reportMissingProp = exports.checkMissingProp = exports.checkReportMissingProp = undefined; - var codegen_1 = require_codegen2(); - var util_1 = require_util2(); - var names_1 = require_names2(); - var util_2 = require_util2(); - function checkReportMissingProp(cxt, prop) { - const { gen, data, it } = cxt; - gen.if(noPropertyInData(gen, data, prop, it.opts.ownProperties), () => { - cxt.setParams({ missingProperty: (0, codegen_1._)`${prop}` }, true); - cxt.error(); - }); - } - exports.checkReportMissingProp = checkReportMissingProp; - function checkMissingProp({ gen, data, it: { opts } }, properties, missing) { - return (0, codegen_1.or)(...properties.map((prop) => (0, codegen_1.and)(noPropertyInData(gen, data, prop, opts.ownProperties), (0, codegen_1._)`${missing} = ${prop}`))); - } - exports.checkMissingProp = checkMissingProp; - function reportMissingProp(cxt, missing) { - cxt.setParams({ missingProperty: missing }, true); - cxt.error(); - } - exports.reportMissingProp = reportMissingProp; - function hasPropFunc(gen) { - return gen.scopeValue("func", { - ref: Object.prototype.hasOwnProperty, - code: (0, codegen_1._)`Object.prototype.hasOwnProperty` - }); - } - exports.hasPropFunc = hasPropFunc; - function isOwnProperty(gen, data, property) { - return (0, codegen_1._)`${hasPropFunc(gen)}.call(${data}, ${property})`; - } - exports.isOwnProperty = isOwnProperty; - function propertyInData(gen, data, property, ownProperties) { - const cond = (0, codegen_1._)`${data}${(0, codegen_1.getProperty)(property)} !== undefined`; - return ownProperties ? (0, codegen_1._)`${cond} && ${isOwnProperty(gen, data, property)}` : cond; - } - exports.propertyInData = propertyInData; - function noPropertyInData(gen, data, property, ownProperties) { - const cond = (0, codegen_1._)`${data}${(0, codegen_1.getProperty)(property)} === undefined`; - return ownProperties ? (0, codegen_1.or)(cond, (0, codegen_1.not)(isOwnProperty(gen, data, property))) : cond; - } - exports.noPropertyInData = noPropertyInData; - function allSchemaProperties(schemaMap) { - return schemaMap ? Object.keys(schemaMap).filter((p) => p !== "__proto__") : []; - } - exports.allSchemaProperties = allSchemaProperties; - function schemaProperties(it, schemaMap) { - return allSchemaProperties(schemaMap).filter((p) => !(0, util_1.alwaysValidSchema)(it, schemaMap[p])); - } - exports.schemaProperties = schemaProperties; - function callValidateCode({ schemaCode, data, it: { gen, topSchemaRef, schemaPath, errorPath }, it }, func, context, passSchema) { - const dataAndSchema = passSchema ? (0, codegen_1._)`${schemaCode}, ${data}, ${topSchemaRef}${schemaPath}` : data; - const valCxt = [ - [names_1.default.instancePath, (0, codegen_1.strConcat)(names_1.default.instancePath, errorPath)], - [names_1.default.parentData, it.parentData], - [names_1.default.parentDataProperty, it.parentDataProperty], - [names_1.default.rootData, names_1.default.rootData] - ]; - if (it.opts.dynamicRef) - valCxt.push([names_1.default.dynamicAnchors, names_1.default.dynamicAnchors]); - const args = (0, codegen_1._)`${dataAndSchema}, ${gen.object(...valCxt)}`; - return context !== codegen_1.nil ? (0, codegen_1._)`${func}.call(${context}, ${args})` : (0, codegen_1._)`${func}(${args})`; - } - exports.callValidateCode = callValidateCode; - var newRegExp = (0, codegen_1._)`new RegExp`; - function usePattern({ gen, it: { opts } }, pattern) { - const u = opts.unicodeRegExp ? "u" : ""; - const { regExp } = opts.code; - const rx = regExp(pattern, u); - return gen.scopeValue("pattern", { - key: rx.toString(), - ref: rx, - code: (0, codegen_1._)`${regExp.code === "new RegExp" ? newRegExp : (0, util_2.useFunc)(gen, regExp)}(${pattern}, ${u})` - }); - } - exports.usePattern = usePattern; - function validateArray(cxt) { - const { gen, data, keyword, it } = cxt; - const valid = gen.name("valid"); - if (it.allErrors) { - const validArr = gen.let("valid", true); - validateItems(() => gen.assign(validArr, false)); - return validArr; - } - gen.var(valid, true); - validateItems(() => gen.break()); - return valid; - function validateItems(notValid) { - const len = gen.const("len", (0, codegen_1._)`${data}.length`); - gen.forRange("i", 0, len, (i) => { - cxt.subschema({ - keyword, - dataProp: i, - dataPropType: util_1.Type.Num - }, valid); - gen.if((0, codegen_1.not)(valid), notValid); - }); - } - } - exports.validateArray = validateArray; - function validateUnion(cxt) { - const { gen, schema, keyword, it } = cxt; - if (!Array.isArray(schema)) - throw new Error("ajv implementation error"); - const alwaysValid = schema.some((sch) => (0, util_1.alwaysValidSchema)(it, sch)); - if (alwaysValid && !it.opts.unevaluated) - return; - const valid = gen.let("valid", false); - const schValid = gen.name("_valid"); - gen.block(() => schema.forEach((_sch, i) => { - const schCxt = cxt.subschema({ - keyword, - schemaProp: i, - compositeRule: true - }, schValid); - gen.assign(valid, (0, codegen_1._)`${valid} || ${schValid}`); - const merged = cxt.mergeValidEvaluated(schCxt, schValid); - if (!merged) - gen.if((0, codegen_1.not)(valid)); - })); - cxt.result(valid, () => cxt.reset(), () => cxt.error(true)); - } - exports.validateUnion = validateUnion; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/compile/validate/keyword.js -var require_keyword2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.validateKeywordUsage = exports.validSchemaType = exports.funcKeywordCode = exports.macroKeywordCode = undefined; - var codegen_1 = require_codegen2(); - var names_1 = require_names2(); - var code_1 = require_code4(); - var errors_1 = require_errors2(); - function macroKeywordCode(cxt, def) { - const { gen, keyword, schema, parentSchema, it } = cxt; - const macroSchema = def.macro.call(it.self, schema, parentSchema, it); - const schemaRef = useKeyword(gen, keyword, macroSchema); - if (it.opts.validateSchema !== false) - it.self.validateSchema(macroSchema, true); - const valid = gen.name("valid"); - cxt.subschema({ - schema: macroSchema, - schemaPath: codegen_1.nil, - errSchemaPath: `${it.errSchemaPath}/${keyword}`, - topSchemaRef: schemaRef, - compositeRule: true - }, valid); - cxt.pass(valid, () => cxt.error(true)); - } - exports.macroKeywordCode = macroKeywordCode; - function funcKeywordCode(cxt, def) { - var _a; - const { gen, keyword, schema, parentSchema, $data, it } = cxt; - checkAsyncKeyword(it, def); - const validate = !$data && def.compile ? def.compile.call(it.self, schema, parentSchema, it) : def.validate; - const validateRef = useKeyword(gen, keyword, validate); - const valid = gen.let("valid"); - cxt.block$data(valid, validateKeyword); - cxt.ok((_a = def.valid) !== null && _a !== undefined ? _a : valid); - function validateKeyword() { - if (def.errors === false) { - assignValid(); - if (def.modifying) - modifyData(cxt); - reportErrs(() => cxt.error()); - } else { - const ruleErrs = def.async ? validateAsync() : validateSync(); - if (def.modifying) - modifyData(cxt); - reportErrs(() => addErrs(cxt, ruleErrs)); - } - } - function validateAsync() { - const ruleErrs = gen.let("ruleErrs", null); - gen.try(() => assignValid((0, codegen_1._)`await `), (e) => gen.assign(valid, false).if((0, codegen_1._)`${e} instanceof ${it.ValidationError}`, () => gen.assign(ruleErrs, (0, codegen_1._)`${e}.errors`), () => gen.throw(e))); - return ruleErrs; - } - function validateSync() { - const validateErrs = (0, codegen_1._)`${validateRef}.errors`; - gen.assign(validateErrs, null); - assignValid(codegen_1.nil); - return validateErrs; - } - function assignValid(_await = def.async ? (0, codegen_1._)`await ` : codegen_1.nil) { - const passCxt = it.opts.passContext ? names_1.default.this : names_1.default.self; - const passSchema = !(("compile" in def) && !$data || def.schema === false); - gen.assign(valid, (0, codegen_1._)`${_await}${(0, code_1.callValidateCode)(cxt, validateRef, passCxt, passSchema)}`, def.modifying); - } - function reportErrs(errors3) { - var _a2; - gen.if((0, codegen_1.not)((_a2 = def.valid) !== null && _a2 !== undefined ? _a2 : valid), errors3); - } - } - exports.funcKeywordCode = funcKeywordCode; - function modifyData(cxt) { - const { gen, data, it } = cxt; - gen.if(it.parentData, () => gen.assign(data, (0, codegen_1._)`${it.parentData}[${it.parentDataProperty}]`)); - } - function addErrs(cxt, errs) { - const { gen } = cxt; - gen.if((0, codegen_1._)`Array.isArray(${errs})`, () => { - gen.assign(names_1.default.vErrors, (0, codegen_1._)`${names_1.default.vErrors} === null ? ${errs} : ${names_1.default.vErrors}.concat(${errs})`).assign(names_1.default.errors, (0, codegen_1._)`${names_1.default.vErrors}.length`); - (0, errors_1.extendErrors)(cxt); - }, () => cxt.error()); - } - function checkAsyncKeyword({ schemaEnv }, def) { - if (def.async && !schemaEnv.$async) - throw new Error("async keyword in sync schema"); - } - function useKeyword(gen, keyword, result) { - if (result === undefined) - throw new Error(`keyword "${keyword}" failed to compile`); - return gen.scopeValue("keyword", typeof result == "function" ? { ref: result } : { ref: result, code: (0, codegen_1.stringify)(result) }); - } - function validSchemaType(schema, schemaType, allowUndefined = false) { - return !schemaType.length || schemaType.some((st) => st === "array" ? Array.isArray(schema) : st === "object" ? schema && typeof schema == "object" && !Array.isArray(schema) : typeof schema == st || allowUndefined && typeof schema == "undefined"); - } - exports.validSchemaType = validSchemaType; - function validateKeywordUsage({ schema, opts, self, errSchemaPath }, def, keyword) { - if (Array.isArray(def.keyword) ? !def.keyword.includes(keyword) : def.keyword !== keyword) { - throw new Error("ajv implementation error"); - } - const deps = def.dependencies; - if (deps === null || deps === undefined ? undefined : deps.some((kwd) => !Object.prototype.hasOwnProperty.call(schema, kwd))) { - throw new Error(`parent schema must have dependencies of ${keyword}: ${deps.join(",")}`); - } - if (def.validateSchema) { - const valid = def.validateSchema(schema[keyword]); - if (!valid) { - const msg = `keyword "${keyword}" value is invalid at path "${errSchemaPath}": ` + self.errorsText(def.validateSchema.errors); - if (opts.validateSchema === "log") - self.logger.error(msg); - else - throw new Error(msg); - } - } - } - exports.validateKeywordUsage = validateKeywordUsage; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/compile/validate/subschema.js -var require_subschema2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.extendSubschemaMode = exports.extendSubschemaData = exports.getSubschema = undefined; - var codegen_1 = require_codegen2(); - var util_1 = require_util2(); - function getSubschema(it, { keyword, schemaProp, schema, schemaPath, errSchemaPath, topSchemaRef }) { - if (keyword !== undefined && schema !== undefined) { - throw new Error('both "keyword" and "schema" passed, only one allowed'); - } - if (keyword !== undefined) { - const sch = it.schema[keyword]; - return schemaProp === undefined ? { - schema: sch, - schemaPath: (0, codegen_1._)`${it.schemaPath}${(0, codegen_1.getProperty)(keyword)}`, - errSchemaPath: `${it.errSchemaPath}/${keyword}` - } : { - schema: sch[schemaProp], - schemaPath: (0, codegen_1._)`${it.schemaPath}${(0, codegen_1.getProperty)(keyword)}${(0, codegen_1.getProperty)(schemaProp)}`, - errSchemaPath: `${it.errSchemaPath}/${keyword}/${(0, util_1.escapeFragment)(schemaProp)}` - }; - } - if (schema !== undefined) { - if (schemaPath === undefined || errSchemaPath === undefined || topSchemaRef === undefined) { - throw new Error('"schemaPath", "errSchemaPath" and "topSchemaRef" are required with "schema"'); - } - return { - schema, - schemaPath, - topSchemaRef, - errSchemaPath - }; - } - throw new Error('either "keyword" or "schema" must be passed'); - } - exports.getSubschema = getSubschema; - function extendSubschemaData(subschema, it, { dataProp, dataPropType: dpType, data, dataTypes, propertyName }) { - if (data !== undefined && dataProp !== undefined) { - throw new Error('both "data" and "dataProp" passed, only one allowed'); - } - const { gen } = it; - if (dataProp !== undefined) { - const { errorPath, dataPathArr, opts } = it; - const nextData = gen.let("data", (0, codegen_1._)`${it.data}${(0, codegen_1.getProperty)(dataProp)}`, true); - dataContextProps(nextData); - subschema.errorPath = (0, codegen_1.str)`${errorPath}${(0, util_1.getErrorPath)(dataProp, dpType, opts.jsPropertySyntax)}`; - subschema.parentDataProperty = (0, codegen_1._)`${dataProp}`; - subschema.dataPathArr = [...dataPathArr, subschema.parentDataProperty]; - } - if (data !== undefined) { - const nextData = data instanceof codegen_1.Name ? data : gen.let("data", data, true); - dataContextProps(nextData); - if (propertyName !== undefined) - subschema.propertyName = propertyName; - } - if (dataTypes) - subschema.dataTypes = dataTypes; - function dataContextProps(_nextData) { - subschema.data = _nextData; - subschema.dataLevel = it.dataLevel + 1; - subschema.dataTypes = []; - it.definedProperties = new Set; - subschema.parentData = it.data; - subschema.dataNames = [...it.dataNames, _nextData]; - } - } - exports.extendSubschemaData = extendSubschemaData; - function extendSubschemaMode(subschema, { jtdDiscriminator, jtdMetadata, compositeRule, createErrors, allErrors }) { - if (compositeRule !== undefined) - subschema.compositeRule = compositeRule; - if (createErrors !== undefined) - subschema.createErrors = createErrors; - if (allErrors !== undefined) - subschema.allErrors = allErrors; - subschema.jtdDiscriminator = jtdDiscriminator; - subschema.jtdMetadata = jtdMetadata; - } - exports.extendSubschemaMode = extendSubschemaMode; -}); - -// node_modules/ajv-formats/node_modules/ajv/node_modules/json-schema-traverse/index.js -var require_json_schema_traverse2 = __commonJS((exports, module) => { - var traverse = module.exports = function(schema, opts, cb) { - if (typeof opts == "function") { - cb = opts; - opts = {}; - } - cb = opts.cb || cb; - var pre = typeof cb == "function" ? cb : cb.pre || function() {}; - var post = cb.post || function() {}; - _traverse(opts, pre, post, schema, "", schema); - }; - traverse.keywords = { - additionalItems: true, - items: true, - contains: true, - additionalProperties: true, - propertyNames: true, - not: true, - if: true, - then: true, - else: true - }; - traverse.arrayKeywords = { - items: true, - allOf: true, - anyOf: true, - oneOf: true - }; - traverse.propsKeywords = { - $defs: true, - definitions: true, - properties: true, - patternProperties: true, - dependencies: true - }; - traverse.skipKeywords = { - default: true, - enum: true, - const: true, - required: true, - maximum: true, - minimum: true, - exclusiveMaximum: true, - exclusiveMinimum: true, - multipleOf: true, - maxLength: true, - minLength: true, - pattern: true, - format: true, - maxItems: true, - minItems: true, - uniqueItems: true, - maxProperties: true, - minProperties: true - }; - function _traverse(opts, pre, post, schema, jsonPtr, rootSchema, parentJsonPtr, parentKeyword, parentSchema, keyIndex) { - if (schema && typeof schema == "object" && !Array.isArray(schema)) { - pre(schema, jsonPtr, rootSchema, parentJsonPtr, parentKeyword, parentSchema, keyIndex); - for (var key in schema) { - var sch = schema[key]; - if (Array.isArray(sch)) { - if (key in traverse.arrayKeywords) { - for (var i = 0;i < sch.length; i++) - _traverse(opts, pre, post, sch[i], jsonPtr + "/" + key + "/" + i, rootSchema, jsonPtr, key, schema, i); - } - } else if (key in traverse.propsKeywords) { - if (sch && typeof sch == "object") { - for (var prop in sch) - _traverse(opts, pre, post, sch[prop], jsonPtr + "/" + key + "/" + escapeJsonPtr(prop), rootSchema, jsonPtr, key, schema, prop); - } - } else if (key in traverse.keywords || opts.allKeys && !(key in traverse.skipKeywords)) { - _traverse(opts, pre, post, sch, jsonPtr + "/" + key, rootSchema, jsonPtr, key, schema); - } - } - post(schema, jsonPtr, rootSchema, parentJsonPtr, parentKeyword, parentSchema, keyIndex); - } - } - function escapeJsonPtr(str) { - return str.replace(/~/g, "~0").replace(/\//g, "~1"); - } -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/compile/resolve.js -var require_resolve2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.getSchemaRefs = exports.resolveUrl = exports.normalizeId = exports._getFullPath = exports.getFullPath = exports.inlineRef = undefined; - var util_1 = require_util2(); - var equal = require_fast_deep_equal(); - var traverse = require_json_schema_traverse2(); - var SIMPLE_INLINED = new Set([ - "type", - "format", - "pattern", - "maxLength", - "minLength", - "maxProperties", - "minProperties", - "maxItems", - "minItems", - "maximum", - "minimum", - "uniqueItems", - "multipleOf", - "required", - "enum", - "const" - ]); - function inlineRef(schema, limit = true) { - if (typeof schema == "boolean") - return true; - if (limit === true) - return !hasRef(schema); - if (!limit) - return false; - return countKeys(schema) <= limit; - } - exports.inlineRef = inlineRef; - var REF_KEYWORDS = new Set([ - "$ref", - "$recursiveRef", - "$recursiveAnchor", - "$dynamicRef", - "$dynamicAnchor" - ]); - function hasRef(schema) { - for (const key in schema) { - if (REF_KEYWORDS.has(key)) - return true; - const sch = schema[key]; - if (Array.isArray(sch) && sch.some(hasRef)) - return true; - if (typeof sch == "object" && hasRef(sch)) - return true; - } - return false; - } - function countKeys(schema) { - let count = 0; - for (const key in schema) { - if (key === "$ref") - return Infinity; - count++; - if (SIMPLE_INLINED.has(key)) - continue; - if (typeof schema[key] == "object") { - (0, util_1.eachItem)(schema[key], (sch) => count += countKeys(sch)); - } - if (count === Infinity) - return Infinity; - } - return count; - } - function getFullPath(resolver, id = "", normalize) { - if (normalize !== false) - id = normalizeId(id); - const p = resolver.parse(id); - return _getFullPath(resolver, p); - } - exports.getFullPath = getFullPath; - function _getFullPath(resolver, p) { - const serialized = resolver.serialize(p); - return serialized.split("#")[0] + "#"; - } - exports._getFullPath = _getFullPath; - var TRAILING_SLASH_HASH = /#\/?$/; - function normalizeId(id) { - return id ? id.replace(TRAILING_SLASH_HASH, "") : ""; - } - exports.normalizeId = normalizeId; - function resolveUrl(resolver, baseId, id) { - id = normalizeId(id); - return resolver.resolve(baseId, id); - } - exports.resolveUrl = resolveUrl; - var ANCHOR = /^[a-z_][-a-z0-9._]*$/i; - function getSchemaRefs(schema, baseId) { - if (typeof schema == "boolean") - return {}; - const { schemaId, uriResolver } = this.opts; - const schId = normalizeId(schema[schemaId] || baseId); - const baseIds = { "": schId }; - const pathPrefix = getFullPath(uriResolver, schId, false); - const localRefs = {}; - const schemaRefs = new Set; - traverse(schema, { allKeys: true }, (sch, jsonPtr, _, parentJsonPtr) => { - if (parentJsonPtr === undefined) - return; - const fullPath = pathPrefix + jsonPtr; - let innerBaseId = baseIds[parentJsonPtr]; - if (typeof sch[schemaId] == "string") - innerBaseId = addRef.call(this, sch[schemaId]); - addAnchor.call(this, sch.$anchor); - addAnchor.call(this, sch.$dynamicAnchor); - baseIds[jsonPtr] = innerBaseId; - function addRef(ref) { - const _resolve = this.opts.uriResolver.resolve; - ref = normalizeId(innerBaseId ? _resolve(innerBaseId, ref) : ref); - if (schemaRefs.has(ref)) - throw ambiguos(ref); - schemaRefs.add(ref); - let schOrRef = this.refs[ref]; - if (typeof schOrRef == "string") - schOrRef = this.refs[schOrRef]; - if (typeof schOrRef == "object") { - checkAmbiguosRef(sch, schOrRef.schema, ref); - } else if (ref !== normalizeId(fullPath)) { - if (ref[0] === "#") { - checkAmbiguosRef(sch, localRefs[ref], ref); - localRefs[ref] = sch; - } else { - this.refs[ref] = fullPath; - } - } - return ref; - } - function addAnchor(anchor) { - if (typeof anchor == "string") { - if (!ANCHOR.test(anchor)) - throw new Error(`invalid anchor "${anchor}"`); - addRef.call(this, `#${anchor}`); - } - } - }); - return localRefs; - function checkAmbiguosRef(sch1, sch2, ref) { - if (sch2 !== undefined && !equal(sch1, sch2)) - throw ambiguos(ref); - } - function ambiguos(ref) { - return new Error(`reference "${ref}" resolves to more than one schema`); - } - } - exports.getSchemaRefs = getSchemaRefs; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/compile/validate/index.js -var require_validate2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.getData = exports.KeywordCxt = exports.validateFunctionCode = undefined; - var boolSchema_1 = require_boolSchema2(); - var dataType_1 = require_dataType2(); - var applicability_1 = require_applicability2(); - var dataType_2 = require_dataType2(); - var defaults_1 = require_defaults2(); - var keyword_1 = require_keyword2(); - var subschema_1 = require_subschema2(); - var codegen_1 = require_codegen2(); - var names_1 = require_names2(); - var resolve_1 = require_resolve2(); - var util_1 = require_util2(); - var errors_1 = require_errors2(); - function validateFunctionCode(it) { - if (isSchemaObj(it)) { - checkKeywords(it); - if (schemaCxtHasRules(it)) { - topSchemaObjCode(it); - return; - } - } - validateFunction(it, () => (0, boolSchema_1.topBoolOrEmptySchema)(it)); - } - exports.validateFunctionCode = validateFunctionCode; - function validateFunction({ gen, validateName, schema, schemaEnv, opts }, body) { - if (opts.code.es5) { - gen.func(validateName, (0, codegen_1._)`${names_1.default.data}, ${names_1.default.valCxt}`, schemaEnv.$async, () => { - gen.code((0, codegen_1._)`"use strict"; ${funcSourceUrl(schema, opts)}`); - destructureValCxtES5(gen, opts); - gen.code(body); - }); - } else { - gen.func(validateName, (0, codegen_1._)`${names_1.default.data}, ${destructureValCxt(opts)}`, schemaEnv.$async, () => gen.code(funcSourceUrl(schema, opts)).code(body)); - } - } - function destructureValCxt(opts) { - return (0, codegen_1._)`{${names_1.default.instancePath}="", ${names_1.default.parentData}, ${names_1.default.parentDataProperty}, ${names_1.default.rootData}=${names_1.default.data}${opts.dynamicRef ? (0, codegen_1._)`, ${names_1.default.dynamicAnchors}={}` : codegen_1.nil}}={}`; - } - function destructureValCxtES5(gen, opts) { - gen.if(names_1.default.valCxt, () => { - gen.var(names_1.default.instancePath, (0, codegen_1._)`${names_1.default.valCxt}.${names_1.default.instancePath}`); - gen.var(names_1.default.parentData, (0, codegen_1._)`${names_1.default.valCxt}.${names_1.default.parentData}`); - gen.var(names_1.default.parentDataProperty, (0, codegen_1._)`${names_1.default.valCxt}.${names_1.default.parentDataProperty}`); - gen.var(names_1.default.rootData, (0, codegen_1._)`${names_1.default.valCxt}.${names_1.default.rootData}`); - if (opts.dynamicRef) - gen.var(names_1.default.dynamicAnchors, (0, codegen_1._)`${names_1.default.valCxt}.${names_1.default.dynamicAnchors}`); - }, () => { - gen.var(names_1.default.instancePath, (0, codegen_1._)`""`); - gen.var(names_1.default.parentData, (0, codegen_1._)`undefined`); - gen.var(names_1.default.parentDataProperty, (0, codegen_1._)`undefined`); - gen.var(names_1.default.rootData, names_1.default.data); - if (opts.dynamicRef) - gen.var(names_1.default.dynamicAnchors, (0, codegen_1._)`{}`); - }); - } - function topSchemaObjCode(it) { - const { schema, opts, gen } = it; - validateFunction(it, () => { - if (opts.$comment && schema.$comment) - commentKeyword(it); - checkNoDefault(it); - gen.let(names_1.default.vErrors, null); - gen.let(names_1.default.errors, 0); - if (opts.unevaluated) - resetEvaluated(it); - typeAndKeywords(it); - returnResults(it); - }); - return; - } - function resetEvaluated(it) { - const { gen, validateName } = it; - it.evaluated = gen.const("evaluated", (0, codegen_1._)`${validateName}.evaluated`); - gen.if((0, codegen_1._)`${it.evaluated}.dynamicProps`, () => gen.assign((0, codegen_1._)`${it.evaluated}.props`, (0, codegen_1._)`undefined`)); - gen.if((0, codegen_1._)`${it.evaluated}.dynamicItems`, () => gen.assign((0, codegen_1._)`${it.evaluated}.items`, (0, codegen_1._)`undefined`)); - } - function funcSourceUrl(schema, opts) { - const schId = typeof schema == "object" && schema[opts.schemaId]; - return schId && (opts.code.source || opts.code.process) ? (0, codegen_1._)`/*# sourceURL=${schId} */` : codegen_1.nil; - } - function subschemaCode(it, valid) { - if (isSchemaObj(it)) { - checkKeywords(it); - if (schemaCxtHasRules(it)) { - subSchemaObjCode(it, valid); - return; - } - } - (0, boolSchema_1.boolOrEmptySchema)(it, valid); - } - function schemaCxtHasRules({ schema, self }) { - if (typeof schema == "boolean") - return !schema; - for (const key in schema) - if (self.RULES.all[key]) - return true; - return false; - } - function isSchemaObj(it) { - return typeof it.schema != "boolean"; - } - function subSchemaObjCode(it, valid) { - const { schema, gen, opts } = it; - if (opts.$comment && schema.$comment) - commentKeyword(it); - updateContext(it); - checkAsyncSchema(it); - const errsCount = gen.const("_errs", names_1.default.errors); - typeAndKeywords(it, errsCount); - gen.var(valid, (0, codegen_1._)`${errsCount} === ${names_1.default.errors}`); - } - function checkKeywords(it) { - (0, util_1.checkUnknownRules)(it); - checkRefsAndKeywords(it); - } - function typeAndKeywords(it, errsCount) { - if (it.opts.jtd) - return schemaKeywords(it, [], false, errsCount); - const types = (0, dataType_1.getSchemaTypes)(it.schema); - const checkedTypes = (0, dataType_1.coerceAndCheckDataType)(it, types); - schemaKeywords(it, types, !checkedTypes, errsCount); - } - function checkRefsAndKeywords(it) { - const { schema, errSchemaPath, opts, self } = it; - if (schema.$ref && opts.ignoreKeywordsWithRef && (0, util_1.schemaHasRulesButRef)(schema, self.RULES)) { - self.logger.warn(`$ref: keywords ignored in schema at path "${errSchemaPath}"`); - } - } - function checkNoDefault(it) { - const { schema, opts } = it; - if (schema.default !== undefined && opts.useDefaults && opts.strictSchema) { - (0, util_1.checkStrictMode)(it, "default is ignored in the schema root"); - } - } - function updateContext(it) { - const schId = it.schema[it.opts.schemaId]; - if (schId) - it.baseId = (0, resolve_1.resolveUrl)(it.opts.uriResolver, it.baseId, schId); - } - function checkAsyncSchema(it) { - if (it.schema.$async && !it.schemaEnv.$async) - throw new Error("async schema in sync schema"); - } - function commentKeyword({ gen, schemaEnv, schema, errSchemaPath, opts }) { - const msg = schema.$comment; - if (opts.$comment === true) { - gen.code((0, codegen_1._)`${names_1.default.self}.logger.log(${msg})`); - } else if (typeof opts.$comment == "function") { - const schemaPath = (0, codegen_1.str)`${errSchemaPath}/$comment`; - const rootName = gen.scopeValue("root", { ref: schemaEnv.root }); - gen.code((0, codegen_1._)`${names_1.default.self}.opts.$comment(${msg}, ${schemaPath}, ${rootName}.schema)`); - } - } - function returnResults(it) { - const { gen, schemaEnv, validateName, ValidationError, opts } = it; - if (schemaEnv.$async) { - gen.if((0, codegen_1._)`${names_1.default.errors} === 0`, () => gen.return(names_1.default.data), () => gen.throw((0, codegen_1._)`new ${ValidationError}(${names_1.default.vErrors})`)); - } else { - gen.assign((0, codegen_1._)`${validateName}.errors`, names_1.default.vErrors); - if (opts.unevaluated) - assignEvaluated(it); - gen.return((0, codegen_1._)`${names_1.default.errors} === 0`); - } - } - function assignEvaluated({ gen, evaluated, props, items }) { - if (props instanceof codegen_1.Name) - gen.assign((0, codegen_1._)`${evaluated}.props`, props); - if (items instanceof codegen_1.Name) - gen.assign((0, codegen_1._)`${evaluated}.items`, items); - } - function schemaKeywords(it, types, typeErrors, errsCount) { - const { gen, schema, data, allErrors, opts, self } = it; - const { RULES } = self; - if (schema.$ref && (opts.ignoreKeywordsWithRef || !(0, util_1.schemaHasRulesButRef)(schema, RULES))) { - gen.block(() => keywordCode(it, "$ref", RULES.all.$ref.definition)); - return; - } - if (!opts.jtd) - checkStrictTypes(it, types); - gen.block(() => { - for (const group of RULES.rules) - groupKeywords(group); - groupKeywords(RULES.post); - }); - function groupKeywords(group) { - if (!(0, applicability_1.shouldUseGroup)(schema, group)) - return; - if (group.type) { - gen.if((0, dataType_2.checkDataType)(group.type, data, opts.strictNumbers)); - iterateKeywords(it, group); - if (types.length === 1 && types[0] === group.type && typeErrors) { - gen.else(); - (0, dataType_2.reportTypeError)(it); - } - gen.endIf(); - } else { - iterateKeywords(it, group); - } - if (!allErrors) - gen.if((0, codegen_1._)`${names_1.default.errors} === ${errsCount || 0}`); - } - } - function iterateKeywords(it, group) { - const { gen, schema, opts: { useDefaults } } = it; - if (useDefaults) - (0, defaults_1.assignDefaults)(it, group.type); - gen.block(() => { - for (const rule of group.rules) { - if ((0, applicability_1.shouldUseRule)(schema, rule)) { - keywordCode(it, rule.keyword, rule.definition, group.type); - } - } - }); - } - function checkStrictTypes(it, types) { - if (it.schemaEnv.meta || !it.opts.strictTypes) - return; - checkContextTypes(it, types); - if (!it.opts.allowUnionTypes) - checkMultipleTypes(it, types); - checkKeywordTypes(it, it.dataTypes); - } - function checkContextTypes(it, types) { - if (!types.length) - return; - if (!it.dataTypes.length) { - it.dataTypes = types; - return; - } - types.forEach((t) => { - if (!includesType(it.dataTypes, t)) { - strictTypesError(it, `type "${t}" not allowed by context "${it.dataTypes.join(",")}"`); - } - }); - narrowSchemaTypes(it, types); - } - function checkMultipleTypes(it, ts) { - if (ts.length > 1 && !(ts.length === 2 && ts.includes("null"))) { - strictTypesError(it, "use allowUnionTypes to allow union type keyword"); - } - } - function checkKeywordTypes(it, ts) { - const rules = it.self.RULES.all; - for (const keyword in rules) { - const rule = rules[keyword]; - if (typeof rule == "object" && (0, applicability_1.shouldUseRule)(it.schema, rule)) { - const { type } = rule.definition; - if (type.length && !type.some((t) => hasApplicableType(ts, t))) { - strictTypesError(it, `missing type "${type.join(",")}" for keyword "${keyword}"`); - } - } - } - } - function hasApplicableType(schTs, kwdT) { - return schTs.includes(kwdT) || kwdT === "number" && schTs.includes("integer"); - } - function includesType(ts, t) { - return ts.includes(t) || t === "integer" && ts.includes("number"); - } - function narrowSchemaTypes(it, withTypes) { - const ts = []; - for (const t of it.dataTypes) { - if (includesType(withTypes, t)) - ts.push(t); - else if (withTypes.includes("integer") && t === "number") - ts.push("integer"); - } - it.dataTypes = ts; - } - function strictTypesError(it, msg) { - const schemaPath = it.schemaEnv.baseId + it.errSchemaPath; - msg += ` at "${schemaPath}" (strictTypes)`; - (0, util_1.checkStrictMode)(it, msg, it.opts.strictTypes); - } - - class KeywordCxt { - constructor(it, def, keyword) { - (0, keyword_1.validateKeywordUsage)(it, def, keyword); - this.gen = it.gen; - this.allErrors = it.allErrors; - this.keyword = keyword; - this.data = it.data; - this.schema = it.schema[keyword]; - this.$data = def.$data && it.opts.$data && this.schema && this.schema.$data; - this.schemaValue = (0, util_1.schemaRefOrVal)(it, this.schema, keyword, this.$data); - this.schemaType = def.schemaType; - this.parentSchema = it.schema; - this.params = {}; - this.it = it; - this.def = def; - if (this.$data) { - this.schemaCode = it.gen.const("vSchema", getData(this.$data, it)); - } else { - this.schemaCode = this.schemaValue; - if (!(0, keyword_1.validSchemaType)(this.schema, def.schemaType, def.allowUndefined)) { - throw new Error(`${keyword} value must be ${JSON.stringify(def.schemaType)}`); - } - } - if ("code" in def ? def.trackErrors : def.errors !== false) { - this.errsCount = it.gen.const("_errs", names_1.default.errors); - } - } - result(condition, successAction, failAction) { - this.failResult((0, codegen_1.not)(condition), successAction, failAction); - } - failResult(condition, successAction, failAction) { - this.gen.if(condition); - if (failAction) - failAction(); - else - this.error(); - if (successAction) { - this.gen.else(); - successAction(); - if (this.allErrors) - this.gen.endIf(); - } else { - if (this.allErrors) - this.gen.endIf(); - else - this.gen.else(); - } - } - pass(condition, failAction) { - this.failResult((0, codegen_1.not)(condition), undefined, failAction); - } - fail(condition) { - if (condition === undefined) { - this.error(); - if (!this.allErrors) - this.gen.if(false); - return; - } - this.gen.if(condition); - this.error(); - if (this.allErrors) - this.gen.endIf(); - else - this.gen.else(); - } - fail$data(condition) { - if (!this.$data) - return this.fail(condition); - const { schemaCode } = this; - this.fail((0, codegen_1._)`${schemaCode} !== undefined && (${(0, codegen_1.or)(this.invalid$data(), condition)})`); - } - error(append, errorParams, errorPaths) { - if (errorParams) { - this.setParams(errorParams); - this._error(append, errorPaths); - this.setParams({}); - return; - } - this._error(append, errorPaths); - } - _error(append, errorPaths) { - (append ? errors_1.reportExtraError : errors_1.reportError)(this, this.def.error, errorPaths); - } - $dataError() { - (0, errors_1.reportError)(this, this.def.$dataError || errors_1.keyword$DataError); - } - reset() { - if (this.errsCount === undefined) - throw new Error('add "trackErrors" to keyword definition'); - (0, errors_1.resetErrorsCount)(this.gen, this.errsCount); - } - ok(cond) { - if (!this.allErrors) - this.gen.if(cond); - } - setParams(obj, assign) { - if (assign) - Object.assign(this.params, obj); - else - this.params = obj; - } - block$data(valid, codeBlock, $dataValid = codegen_1.nil) { - this.gen.block(() => { - this.check$data(valid, $dataValid); - codeBlock(); - }); - } - check$data(valid = codegen_1.nil, $dataValid = codegen_1.nil) { - if (!this.$data) - return; - const { gen, schemaCode, schemaType, def } = this; - gen.if((0, codegen_1.or)((0, codegen_1._)`${schemaCode} === undefined`, $dataValid)); - if (valid !== codegen_1.nil) - gen.assign(valid, true); - if (schemaType.length || def.validateSchema) { - gen.elseIf(this.invalid$data()); - this.$dataError(); - if (valid !== codegen_1.nil) - gen.assign(valid, false); - } - gen.else(); - } - invalid$data() { - const { gen, schemaCode, schemaType, def, it } = this; - return (0, codegen_1.or)(wrong$DataType(), invalid$DataSchema()); - function wrong$DataType() { - if (schemaType.length) { - if (!(schemaCode instanceof codegen_1.Name)) - throw new Error("ajv implementation error"); - const st = Array.isArray(schemaType) ? schemaType : [schemaType]; - return (0, codegen_1._)`${(0, dataType_2.checkDataTypes)(st, schemaCode, it.opts.strictNumbers, dataType_2.DataType.Wrong)}`; - } - return codegen_1.nil; - } - function invalid$DataSchema() { - if (def.validateSchema) { - const validateSchemaRef = gen.scopeValue("validate$data", { ref: def.validateSchema }); - return (0, codegen_1._)`!${validateSchemaRef}(${schemaCode})`; - } - return codegen_1.nil; - } - } - subschema(appl, valid) { - const subschema = (0, subschema_1.getSubschema)(this.it, appl); - (0, subschema_1.extendSubschemaData)(subschema, this.it, appl); - (0, subschema_1.extendSubschemaMode)(subschema, appl); - const nextContext = { ...this.it, ...subschema, items: undefined, props: undefined }; - subschemaCode(nextContext, valid); - return nextContext; - } - mergeEvaluated(schemaCxt, toName) { - const { it, gen } = this; - if (!it.opts.unevaluated) - return; - if (it.props !== true && schemaCxt.props !== undefined) { - it.props = util_1.mergeEvaluated.props(gen, schemaCxt.props, it.props, toName); - } - if (it.items !== true && schemaCxt.items !== undefined) { - it.items = util_1.mergeEvaluated.items(gen, schemaCxt.items, it.items, toName); - } - } - mergeValidEvaluated(schemaCxt, valid) { - const { it, gen } = this; - if (it.opts.unevaluated && (it.props !== true || it.items !== true)) { - gen.if(valid, () => this.mergeEvaluated(schemaCxt, codegen_1.Name)); - return true; - } - } - } - exports.KeywordCxt = KeywordCxt; - function keywordCode(it, keyword, def, ruleType) { - const cxt = new KeywordCxt(it, def, keyword); - if ("code" in def) { - def.code(cxt, ruleType); - } else if (cxt.$data && def.validate) { - (0, keyword_1.funcKeywordCode)(cxt, def); - } else if ("macro" in def) { - (0, keyword_1.macroKeywordCode)(cxt, def); - } else if (def.compile || def.validate) { - (0, keyword_1.funcKeywordCode)(cxt, def); - } - } - var JSON_POINTER = /^\/(?:[^~]|~0|~1)*$/; - var RELATIVE_JSON_POINTER = /^([0-9]+)(#|\/(?:[^~]|~0|~1)*)?$/; - function getData($data, { dataLevel, dataNames, dataPathArr }) { - let jsonPointer; - let data; - if ($data === "") - return names_1.default.rootData; - if ($data[0] === "/") { - if (!JSON_POINTER.test($data)) - throw new Error(`Invalid JSON-pointer: ${$data}`); - jsonPointer = $data; - data = names_1.default.rootData; - } else { - const matches = RELATIVE_JSON_POINTER.exec($data); - if (!matches) - throw new Error(`Invalid JSON-pointer: ${$data}`); - const up = +matches[1]; - jsonPointer = matches[2]; - if (jsonPointer === "#") { - if (up >= dataLevel) - throw new Error(errorMsg("property/index", up)); - return dataPathArr[dataLevel - up]; - } - if (up > dataLevel) - throw new Error(errorMsg("data", up)); - data = dataNames[dataLevel - up]; - if (!jsonPointer) - return data; - } - let expr = data; - const segments = jsonPointer.split("/"); - for (const segment of segments) { - if (segment) { - data = (0, codegen_1._)`${data}${(0, codegen_1.getProperty)((0, util_1.unescapeJsonPointer)(segment))}`; - expr = (0, codegen_1._)`${expr} && ${data}`; - } - } - return expr; - function errorMsg(pointerType, up) { - return `Cannot access ${pointerType} ${up} levels up, current level is ${dataLevel}`; - } - } - exports.getData = getData; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/runtime/validation_error.js -var require_validation_error2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - - class ValidationError extends Error { - constructor(errors3) { - super("validation failed"); - this.errors = errors3; - this.ajv = this.validation = true; - } - } - exports.default = ValidationError; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/compile/ref_error.js -var require_ref_error2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var resolve_1 = require_resolve2(); - - class MissingRefError extends Error { - constructor(resolver, baseId, ref, msg) { - super(msg || `can't resolve reference ${ref} from id ${baseId}`); - this.missingRef = (0, resolve_1.resolveUrl)(resolver, baseId, ref); - this.missingSchema = (0, resolve_1.normalizeId)((0, resolve_1.getFullPath)(resolver, this.missingRef)); - } - } - exports.default = MissingRefError; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/compile/index.js -var require_compile2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.resolveSchema = exports.getCompilingSchema = exports.resolveRef = exports.compileSchema = exports.SchemaEnv = undefined; - var codegen_1 = require_codegen2(); - var validation_error_1 = require_validation_error2(); - var names_1 = require_names2(); - var resolve_1 = require_resolve2(); - var util_1 = require_util2(); - var validate_1 = require_validate2(); - - class SchemaEnv { - constructor(env) { - var _a; - this.refs = {}; - this.dynamicAnchors = {}; - let schema; - if (typeof env.schema == "object") - schema = env.schema; - this.schema = env.schema; - this.schemaId = env.schemaId; - this.root = env.root || this; - this.baseId = (_a = env.baseId) !== null && _a !== undefined ? _a : (0, resolve_1.normalizeId)(schema === null || schema === undefined ? undefined : schema[env.schemaId || "$id"]); - this.schemaPath = env.schemaPath; - this.localRefs = env.localRefs; - this.meta = env.meta; - this.$async = schema === null || schema === undefined ? undefined : schema.$async; - this.refs = {}; - } - } - exports.SchemaEnv = SchemaEnv; - function compileSchema(sch) { - const _sch = getCompilingSchema.call(this, sch); - if (_sch) - return _sch; - const rootId = (0, resolve_1.getFullPath)(this.opts.uriResolver, sch.root.baseId); - const { es5, lines } = this.opts.code; - const { ownProperties } = this.opts; - const gen = new codegen_1.CodeGen(this.scope, { es5, lines, ownProperties }); - let _ValidationError; - if (sch.$async) { - _ValidationError = gen.scopeValue("Error", { - ref: validation_error_1.default, - code: (0, codegen_1._)`require("ajv/dist/runtime/validation_error").default` - }); - } - const validateName = gen.scopeName("validate"); - sch.validateName = validateName; - const schemaCxt = { - gen, - allErrors: this.opts.allErrors, - data: names_1.default.data, - parentData: names_1.default.parentData, - parentDataProperty: names_1.default.parentDataProperty, - dataNames: [names_1.default.data], - dataPathArr: [codegen_1.nil], - dataLevel: 0, - dataTypes: [], - definedProperties: new Set, - topSchemaRef: gen.scopeValue("schema", this.opts.code.source === true ? { ref: sch.schema, code: (0, codegen_1.stringify)(sch.schema) } : { ref: sch.schema }), - validateName, - ValidationError: _ValidationError, - schema: sch.schema, - schemaEnv: sch, - rootId, - baseId: sch.baseId || rootId, - schemaPath: codegen_1.nil, - errSchemaPath: sch.schemaPath || (this.opts.jtd ? "" : "#"), - errorPath: (0, codegen_1._)`""`, - opts: this.opts, - self: this - }; - let sourceCode; - try { - this._compilations.add(sch); - (0, validate_1.validateFunctionCode)(schemaCxt); - gen.optimize(this.opts.code.optimize); - const validateCode = gen.toString(); - sourceCode = `${gen.scopeRefs(names_1.default.scope)}return ${validateCode}`; - if (this.opts.code.process) - sourceCode = this.opts.code.process(sourceCode, sch); - const makeValidate = new Function(`${names_1.default.self}`, `${names_1.default.scope}`, sourceCode); - const validate = makeValidate(this, this.scope.get()); - this.scope.value(validateName, { ref: validate }); - validate.errors = null; - validate.schema = sch.schema; - validate.schemaEnv = sch; - if (sch.$async) - validate.$async = true; - if (this.opts.code.source === true) { - validate.source = { validateName, validateCode, scopeValues: gen._values }; - } - if (this.opts.unevaluated) { - const { props, items } = schemaCxt; - validate.evaluated = { - props: props instanceof codegen_1.Name ? undefined : props, - items: items instanceof codegen_1.Name ? undefined : items, - dynamicProps: props instanceof codegen_1.Name, - dynamicItems: items instanceof codegen_1.Name - }; - if (validate.source) - validate.source.evaluated = (0, codegen_1.stringify)(validate.evaluated); - } - sch.validate = validate; - return sch; - } catch (e) { - delete sch.validate; - delete sch.validateName; - if (sourceCode) - this.logger.error("Error compiling schema, function code:", sourceCode); - throw e; - } finally { - this._compilations.delete(sch); - } - } - exports.compileSchema = compileSchema; - function resolveRef(root, baseId, ref) { - var _a; - ref = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, ref); - const schOrFunc = root.refs[ref]; - if (schOrFunc) - return schOrFunc; - let _sch = resolve.call(this, root, ref); - if (_sch === undefined) { - const schema = (_a = root.localRefs) === null || _a === undefined ? undefined : _a[ref]; - const { schemaId } = this.opts; - if (schema) - _sch = new SchemaEnv({ schema, schemaId, root, baseId }); - } - if (_sch === undefined) - return; - return root.refs[ref] = inlineOrCompile.call(this, _sch); - } - exports.resolveRef = resolveRef; - function inlineOrCompile(sch) { - if ((0, resolve_1.inlineRef)(sch.schema, this.opts.inlineRefs)) - return sch.schema; - return sch.validate ? sch : compileSchema.call(this, sch); - } - function getCompilingSchema(schEnv) { - for (const sch of this._compilations) { - if (sameSchemaEnv(sch, schEnv)) - return sch; - } - } - exports.getCompilingSchema = getCompilingSchema; - function sameSchemaEnv(s1, s2) { - return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId; - } - function resolve(root, ref) { - let sch; - while (typeof (sch = this.refs[ref]) == "string") - ref = sch; - return sch || this.schemas[ref] || resolveSchema.call(this, root, ref); - } - function resolveSchema(root, ref) { - const p = this.opts.uriResolver.parse(ref); - const refPath = (0, resolve_1._getFullPath)(this.opts.uriResolver, p); - let baseId = (0, resolve_1.getFullPath)(this.opts.uriResolver, root.baseId, undefined); - if (Object.keys(root.schema).length > 0 && refPath === baseId) { - return getJsonPointer.call(this, p, root); - } - const id = (0, resolve_1.normalizeId)(refPath); - const schOrRef = this.refs[id] || this.schemas[id]; - if (typeof schOrRef == "string") { - const sch = resolveSchema.call(this, root, schOrRef); - if (typeof (sch === null || sch === undefined ? undefined : sch.schema) !== "object") - return; - return getJsonPointer.call(this, p, sch); - } - if (typeof (schOrRef === null || schOrRef === undefined ? undefined : schOrRef.schema) !== "object") - return; - if (!schOrRef.validate) - compileSchema.call(this, schOrRef); - if (id === (0, resolve_1.normalizeId)(ref)) { - const { schema } = schOrRef; - const { schemaId } = this.opts; - const schId = schema[schemaId]; - if (schId) - baseId = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, schId); - return new SchemaEnv({ schema, schemaId, root, baseId }); - } - return getJsonPointer.call(this, p, schOrRef); - } - exports.resolveSchema = resolveSchema; - var PREVENT_SCOPE_CHANGE = new Set([ - "properties", - "patternProperties", - "enum", - "dependencies", - "definitions" - ]); - function getJsonPointer(parsedRef, { baseId, schema, root }) { - var _a; - if (((_a = parsedRef.fragment) === null || _a === undefined ? undefined : _a[0]) !== "/") - return; - for (const part of parsedRef.fragment.slice(1).split("/")) { - if (typeof schema === "boolean") - return; - const partSchema = schema[(0, util_1.unescapeFragment)(part)]; - if (partSchema === undefined) - return; - schema = partSchema; - const schId = typeof schema === "object" && schema[this.opts.schemaId]; - if (!PREVENT_SCOPE_CHANGE.has(part) && schId) { - baseId = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, schId); - } - } - let env; - if (typeof schema != "boolean" && schema.$ref && !(0, util_1.schemaHasRulesButRef)(schema, this.RULES)) { - const $ref = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, schema.$ref); - env = resolveSchema.call(this, root, $ref); - } - const { schemaId } = this.opts; - env = env || new SchemaEnv({ schema, schemaId, root, baseId }); - if (env.schema !== env.root.schema) - return env; - return; - } -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/refs/data.json -var require_data2 = __commonJS((exports, module) => { - module.exports = { - $id: "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/data.json#", - description: "Meta-schema for $data reference (JSON AnySchema extension proposal)", - type: "object", - required: ["$data"], - properties: { - $data: { - type: "string", - anyOf: [{ format: "relative-json-pointer" }, { format: "json-pointer" }] - } - }, - additionalProperties: false - }; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/runtime/uri.js -var require_uri2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var uri = require_fast_uri(); - uri.code = 'require("ajv/dist/runtime/uri").default'; - exports.default = uri; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/core.js -var require_core3 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.CodeGen = exports.Name = exports.nil = exports.stringify = exports.str = exports._ = exports.KeywordCxt = undefined; - var validate_1 = require_validate2(); - Object.defineProperty(exports, "KeywordCxt", { enumerable: true, get: function() { - return validate_1.KeywordCxt; - } }); - var codegen_1 = require_codegen2(); - Object.defineProperty(exports, "_", { enumerable: true, get: function() { - return codegen_1._; - } }); - Object.defineProperty(exports, "str", { enumerable: true, get: function() { - return codegen_1.str; - } }); - Object.defineProperty(exports, "stringify", { enumerable: true, get: function() { - return codegen_1.stringify; - } }); - Object.defineProperty(exports, "nil", { enumerable: true, get: function() { - return codegen_1.nil; - } }); - Object.defineProperty(exports, "Name", { enumerable: true, get: function() { - return codegen_1.Name; - } }); - Object.defineProperty(exports, "CodeGen", { enumerable: true, get: function() { - return codegen_1.CodeGen; - } }); - var validation_error_1 = require_validation_error2(); - var ref_error_1 = require_ref_error2(); - var rules_1 = require_rules2(); - var compile_1 = require_compile2(); - var codegen_2 = require_codegen2(); - var resolve_1 = require_resolve2(); - var dataType_1 = require_dataType2(); - var util_1 = require_util2(); - var $dataRefSchema = require_data2(); - var uri_1 = require_uri2(); - var defaultRegExp = (str, flags) => new RegExp(str, flags); - defaultRegExp.code = "new RegExp"; - var META_IGNORE_OPTIONS = ["removeAdditional", "useDefaults", "coerceTypes"]; - var EXT_SCOPE_NAMES = new Set([ - "validate", - "serialize", - "parse", - "wrapper", - "root", - "schema", - "keyword", - "pattern", - "formats", - "validate$data", - "func", - "obj", - "Error" - ]); - var removedOptions = { - errorDataPath: "", - format: "`validateFormats: false` can be used instead.", - nullable: '"nullable" keyword is supported by default.', - jsonPointers: "Deprecated jsPropertySyntax can be used instead.", - extendRefs: "Deprecated ignoreKeywordsWithRef can be used instead.", - missingRefs: "Pass empty schema with $id that should be ignored to ajv.addSchema.", - processCode: "Use option `code: {process: (code, schemaEnv: object) => string}`", - sourceCode: "Use option `code: {source: true}`", - strictDefaults: "It is default now, see option `strict`.", - strictKeywords: "It is default now, see option `strict`.", - uniqueItems: '"uniqueItems" keyword is always validated.', - unknownFormats: "Disable strict mode or pass `true` to `ajv.addFormat` (or `formats` option).", - cache: "Map is used as cache, schema object as key.", - serialize: "Map is used as cache, schema object as key.", - ajvErrors: "It is default now." - }; - var deprecatedOptions = { - ignoreKeywordsWithRef: "", - jsPropertySyntax: "", - unicode: '"minLength"/"maxLength" account for unicode characters by default.' - }; - var MAX_EXPRESSION = 200; - function requiredOptions(o) { - var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0; - const s = o.strict; - const _optz = (_a = o.code) === null || _a === undefined ? undefined : _a.optimize; - const optimize = _optz === true || _optz === undefined ? 1 : _optz || 0; - const regExp = (_c = (_b = o.code) === null || _b === undefined ? undefined : _b.regExp) !== null && _c !== undefined ? _c : defaultRegExp; - const uriResolver = (_d = o.uriResolver) !== null && _d !== undefined ? _d : uri_1.default; - return { - strictSchema: (_f = (_e = o.strictSchema) !== null && _e !== undefined ? _e : s) !== null && _f !== undefined ? _f : true, - strictNumbers: (_h = (_g = o.strictNumbers) !== null && _g !== undefined ? _g : s) !== null && _h !== undefined ? _h : true, - strictTypes: (_k = (_j = o.strictTypes) !== null && _j !== undefined ? _j : s) !== null && _k !== undefined ? _k : "log", - strictTuples: (_m = (_l = o.strictTuples) !== null && _l !== undefined ? _l : s) !== null && _m !== undefined ? _m : "log", - strictRequired: (_p = (_o = o.strictRequired) !== null && _o !== undefined ? _o : s) !== null && _p !== undefined ? _p : false, - code: o.code ? { ...o.code, optimize, regExp } : { optimize, regExp }, - loopRequired: (_q = o.loopRequired) !== null && _q !== undefined ? _q : MAX_EXPRESSION, - loopEnum: (_r = o.loopEnum) !== null && _r !== undefined ? _r : MAX_EXPRESSION, - meta: (_s = o.meta) !== null && _s !== undefined ? _s : true, - messages: (_t = o.messages) !== null && _t !== undefined ? _t : true, - inlineRefs: (_u = o.inlineRefs) !== null && _u !== undefined ? _u : true, - schemaId: (_v = o.schemaId) !== null && _v !== undefined ? _v : "$id", - addUsedSchema: (_w = o.addUsedSchema) !== null && _w !== undefined ? _w : true, - validateSchema: (_x = o.validateSchema) !== null && _x !== undefined ? _x : true, - validateFormats: (_y = o.validateFormats) !== null && _y !== undefined ? _y : true, - unicodeRegExp: (_z = o.unicodeRegExp) !== null && _z !== undefined ? _z : true, - int32range: (_0 = o.int32range) !== null && _0 !== undefined ? _0 : true, - uriResolver - }; - } - - class Ajv { - constructor(opts = {}) { - this.schemas = {}; - this.refs = {}; - this.formats = {}; - this._compilations = new Set; - this._loading = {}; - this._cache = new Map; - opts = this.opts = { ...opts, ...requiredOptions(opts) }; - const { es5, lines } = this.opts.code; - this.scope = new codegen_2.ValueScope({ scope: {}, prefixes: EXT_SCOPE_NAMES, es5, lines }); - this.logger = getLogger(opts.logger); - const formatOpt = opts.validateFormats; - opts.validateFormats = false; - this.RULES = (0, rules_1.getRules)(); - checkOptions.call(this, removedOptions, opts, "NOT SUPPORTED"); - checkOptions.call(this, deprecatedOptions, opts, "DEPRECATED", "warn"); - this._metaOpts = getMetaSchemaOptions.call(this); - if (opts.formats) - addInitialFormats.call(this); - this._addVocabularies(); - this._addDefaultMetaSchema(); - if (opts.keywords) - addInitialKeywords.call(this, opts.keywords); - if (typeof opts.meta == "object") - this.addMetaSchema(opts.meta); - addInitialSchemas.call(this); - opts.validateFormats = formatOpt; - } - _addVocabularies() { - this.addKeyword("$async"); - } - _addDefaultMetaSchema() { - const { $data, meta, schemaId } = this.opts; - let _dataRefSchema = $dataRefSchema; - if (schemaId === "id") { - _dataRefSchema = { ...$dataRefSchema }; - _dataRefSchema.id = _dataRefSchema.$id; - delete _dataRefSchema.$id; - } - if (meta && $data) - this.addMetaSchema(_dataRefSchema, _dataRefSchema[schemaId], false); - } - defaultMeta() { - const { meta, schemaId } = this.opts; - return this.opts.defaultMeta = typeof meta == "object" ? meta[schemaId] || meta : undefined; - } - validate(schemaKeyRef, data) { - let v; - if (typeof schemaKeyRef == "string") { - v = this.getSchema(schemaKeyRef); - if (!v) - throw new Error(`no schema with key or ref "${schemaKeyRef}"`); - } else { - v = this.compile(schemaKeyRef); - } - const valid = v(data); - if (!("$async" in v)) - this.errors = v.errors; - return valid; - } - compile(schema, _meta) { - const sch = this._addSchema(schema, _meta); - return sch.validate || this._compileSchemaEnv(sch); - } - compileAsync(schema, meta) { - if (typeof this.opts.loadSchema != "function") { - throw new Error("options.loadSchema should be a function"); - } - const { loadSchema } = this.opts; - return runCompileAsync.call(this, schema, meta); - async function runCompileAsync(_schema, _meta) { - await loadMetaSchema.call(this, _schema.$schema); - const sch = this._addSchema(_schema, _meta); - return sch.validate || _compileAsync.call(this, sch); - } - async function loadMetaSchema($ref) { - if ($ref && !this.getSchema($ref)) { - await runCompileAsync.call(this, { $ref }, true); - } - } - async function _compileAsync(sch) { - try { - return this._compileSchemaEnv(sch); - } catch (e) { - if (!(e instanceof ref_error_1.default)) - throw e; - checkLoaded.call(this, e); - await loadMissingSchema.call(this, e.missingSchema); - return _compileAsync.call(this, sch); - } - } - function checkLoaded({ missingSchema: ref, missingRef }) { - if (this.refs[ref]) { - throw new Error(`AnySchema ${ref} is loaded but ${missingRef} cannot be resolved`); - } - } - async function loadMissingSchema(ref) { - const _schema = await _loadSchema.call(this, ref); - if (!this.refs[ref]) - await loadMetaSchema.call(this, _schema.$schema); - if (!this.refs[ref]) - this.addSchema(_schema, ref, meta); - } - async function _loadSchema(ref) { - const p = this._loading[ref]; - if (p) - return p; - try { - return await (this._loading[ref] = loadSchema(ref)); - } finally { - delete this._loading[ref]; - } - } - } - addSchema(schema, key, _meta, _validateSchema = this.opts.validateSchema) { - if (Array.isArray(schema)) { - for (const sch of schema) - this.addSchema(sch, undefined, _meta, _validateSchema); - return this; - } - let id; - if (typeof schema === "object") { - const { schemaId } = this.opts; - id = schema[schemaId]; - if (id !== undefined && typeof id != "string") { - throw new Error(`schema ${schemaId} must be string`); - } - } - key = (0, resolve_1.normalizeId)(key || id); - this._checkUnique(key); - this.schemas[key] = this._addSchema(schema, _meta, key, _validateSchema, true); - return this; - } - addMetaSchema(schema, key, _validateSchema = this.opts.validateSchema) { - this.addSchema(schema, key, true, _validateSchema); - return this; - } - validateSchema(schema, throwOrLogError) { - if (typeof schema == "boolean") - return true; - let $schema; - $schema = schema.$schema; - if ($schema !== undefined && typeof $schema != "string") { - throw new Error("$schema must be a string"); - } - $schema = $schema || this.opts.defaultMeta || this.defaultMeta(); - if (!$schema) { - this.logger.warn("meta-schema not available"); - this.errors = null; - return true; - } - const valid = this.validate($schema, schema); - if (!valid && throwOrLogError) { - const message = "schema is invalid: " + this.errorsText(); - if (this.opts.validateSchema === "log") - this.logger.error(message); - else - throw new Error(message); - } - return valid; - } - getSchema(keyRef) { - let sch; - while (typeof (sch = getSchEnv.call(this, keyRef)) == "string") - keyRef = sch; - if (sch === undefined) { - const { schemaId } = this.opts; - const root = new compile_1.SchemaEnv({ schema: {}, schemaId }); - sch = compile_1.resolveSchema.call(this, root, keyRef); - if (!sch) - return; - this.refs[keyRef] = sch; - } - return sch.validate || this._compileSchemaEnv(sch); - } - removeSchema(schemaKeyRef) { - if (schemaKeyRef instanceof RegExp) { - this._removeAllSchemas(this.schemas, schemaKeyRef); - this._removeAllSchemas(this.refs, schemaKeyRef); - return this; - } - switch (typeof schemaKeyRef) { - case "undefined": - this._removeAllSchemas(this.schemas); - this._removeAllSchemas(this.refs); - this._cache.clear(); - return this; - case "string": { - const sch = getSchEnv.call(this, schemaKeyRef); - if (typeof sch == "object") - this._cache.delete(sch.schema); - delete this.schemas[schemaKeyRef]; - delete this.refs[schemaKeyRef]; - return this; - } - case "object": { - const cacheKey = schemaKeyRef; - this._cache.delete(cacheKey); - let id = schemaKeyRef[this.opts.schemaId]; - if (id) { - id = (0, resolve_1.normalizeId)(id); - delete this.schemas[id]; - delete this.refs[id]; - } - return this; - } - default: - throw new Error("ajv.removeSchema: invalid parameter"); - } - } - addVocabulary(definitions) { - for (const def of definitions) - this.addKeyword(def); - return this; - } - addKeyword(kwdOrDef, def) { - let keyword; - if (typeof kwdOrDef == "string") { - keyword = kwdOrDef; - if (typeof def == "object") { - this.logger.warn("these parameters are deprecated, see docs for addKeyword"); - def.keyword = keyword; - } - } else if (typeof kwdOrDef == "object" && def === undefined) { - def = kwdOrDef; - keyword = def.keyword; - if (Array.isArray(keyword) && !keyword.length) { - throw new Error("addKeywords: keyword must be string or non-empty array"); - } - } else { - throw new Error("invalid addKeywords parameters"); - } - checkKeyword.call(this, keyword, def); - if (!def) { - (0, util_1.eachItem)(keyword, (kwd) => addRule.call(this, kwd)); - return this; - } - keywordMetaschema.call(this, def); - const definition = { - ...def, - type: (0, dataType_1.getJSONTypes)(def.type), - schemaType: (0, dataType_1.getJSONTypes)(def.schemaType) - }; - (0, util_1.eachItem)(keyword, definition.type.length === 0 ? (k) => addRule.call(this, k, definition) : (k) => definition.type.forEach((t) => addRule.call(this, k, definition, t))); - return this; - } - getKeyword(keyword) { - const rule = this.RULES.all[keyword]; - return typeof rule == "object" ? rule.definition : !!rule; - } - removeKeyword(keyword) { - const { RULES } = this; - delete RULES.keywords[keyword]; - delete RULES.all[keyword]; - for (const group of RULES.rules) { - const i = group.rules.findIndex((rule) => rule.keyword === keyword); - if (i >= 0) - group.rules.splice(i, 1); - } - return this; - } - addFormat(name, format) { - if (typeof format == "string") - format = new RegExp(format); - this.formats[name] = format; - return this; - } - errorsText(errors3 = this.errors, { separator = ", ", dataVar = "data" } = {}) { - if (!errors3 || errors3.length === 0) - return "No errors"; - return errors3.map((e) => `${dataVar}${e.instancePath} ${e.message}`).reduce((text, msg) => text + separator + msg); - } - $dataMetaSchema(metaSchema, keywordsJsonPointers) { - const rules = this.RULES.all; - metaSchema = JSON.parse(JSON.stringify(metaSchema)); - for (const jsonPointer of keywordsJsonPointers) { - const segments = jsonPointer.split("/").slice(1); - let keywords = metaSchema; - for (const seg of segments) - keywords = keywords[seg]; - for (const key in rules) { - const rule = rules[key]; - if (typeof rule != "object") - continue; - const { $data } = rule.definition; - const schema = keywords[key]; - if ($data && schema) - keywords[key] = schemaOrData(schema); - } - } - return metaSchema; - } - _removeAllSchemas(schemas3, regex) { - for (const keyRef in schemas3) { - const sch = schemas3[keyRef]; - if (!regex || regex.test(keyRef)) { - if (typeof sch == "string") { - delete schemas3[keyRef]; - } else if (sch && !sch.meta) { - this._cache.delete(sch.schema); - delete schemas3[keyRef]; - } - } - } - } - _addSchema(schema, meta, baseId, validateSchema = this.opts.validateSchema, addSchema = this.opts.addUsedSchema) { - let id; - const { schemaId } = this.opts; - if (typeof schema == "object") { - id = schema[schemaId]; - } else { - if (this.opts.jtd) - throw new Error("schema must be object"); - else if (typeof schema != "boolean") - throw new Error("schema must be object or boolean"); - } - let sch = this._cache.get(schema); - if (sch !== undefined) - return sch; - baseId = (0, resolve_1.normalizeId)(id || baseId); - const localRefs = resolve_1.getSchemaRefs.call(this, schema, baseId); - sch = new compile_1.SchemaEnv({ schema, schemaId, meta, baseId, localRefs }); - this._cache.set(sch.schema, sch); - if (addSchema && !baseId.startsWith("#")) { - if (baseId) - this._checkUnique(baseId); - this.refs[baseId] = sch; - } - if (validateSchema) - this.validateSchema(schema, true); - return sch; - } - _checkUnique(id) { - if (this.schemas[id] || this.refs[id]) { - throw new Error(`schema with key or id "${id}" already exists`); - } - } - _compileSchemaEnv(sch) { - if (sch.meta) - this._compileMetaSchema(sch); - else - compile_1.compileSchema.call(this, sch); - if (!sch.validate) - throw new Error("ajv implementation error"); - return sch.validate; - } - _compileMetaSchema(sch) { - const currentOpts = this.opts; - this.opts = this._metaOpts; - try { - compile_1.compileSchema.call(this, sch); - } finally { - this.opts = currentOpts; - } - } - } - Ajv.ValidationError = validation_error_1.default; - Ajv.MissingRefError = ref_error_1.default; - exports.default = Ajv; - function checkOptions(checkOpts, options, msg, log = "error") { - for (const key in checkOpts) { - const opt = key; - if (opt in options) - this.logger[log](`${msg}: option ${key}. ${checkOpts[opt]}`); - } - } - function getSchEnv(keyRef) { - keyRef = (0, resolve_1.normalizeId)(keyRef); - return this.schemas[keyRef] || this.refs[keyRef]; - } - function addInitialSchemas() { - const optsSchemas = this.opts.schemas; - if (!optsSchemas) - return; - if (Array.isArray(optsSchemas)) - this.addSchema(optsSchemas); - else - for (const key in optsSchemas) - this.addSchema(optsSchemas[key], key); - } - function addInitialFormats() { - for (const name in this.opts.formats) { - const format = this.opts.formats[name]; - if (format) - this.addFormat(name, format); - } - } - function addInitialKeywords(defs) { - if (Array.isArray(defs)) { - this.addVocabulary(defs); - return; - } - this.logger.warn("keywords option as map is deprecated, pass array"); - for (const keyword in defs) { - const def = defs[keyword]; - if (!def.keyword) - def.keyword = keyword; - this.addKeyword(def); - } - } - function getMetaSchemaOptions() { - const metaOpts = { ...this.opts }; - for (const opt of META_IGNORE_OPTIONS) - delete metaOpts[opt]; - return metaOpts; - } - var noLogs = { log() {}, warn() {}, error() {} }; - function getLogger(logger) { - if (logger === false) - return noLogs; - if (logger === undefined) - return console; - if (logger.log && logger.warn && logger.error) - return logger; - throw new Error("logger must implement log, warn and error methods"); - } - var KEYWORD_NAME = /^[a-z_$][a-z0-9_$:-]*$/i; - function checkKeyword(keyword, def) { - const { RULES } = this; - (0, util_1.eachItem)(keyword, (kwd) => { - if (RULES.keywords[kwd]) - throw new Error(`Keyword ${kwd} is already defined`); - if (!KEYWORD_NAME.test(kwd)) - throw new Error(`Keyword ${kwd} has invalid name`); - }); - if (!def) - return; - if (def.$data && !(("code" in def) || ("validate" in def))) { - throw new Error('$data keyword must have "code" or "validate" function'); - } - } - function addRule(keyword, definition, dataType) { - var _a; - const post = definition === null || definition === undefined ? undefined : definition.post; - if (dataType && post) - throw new Error('keyword with "post" flag cannot have "type"'); - const { RULES } = this; - let ruleGroup = post ? RULES.post : RULES.rules.find(({ type: t }) => t === dataType); - if (!ruleGroup) { - ruleGroup = { type: dataType, rules: [] }; - RULES.rules.push(ruleGroup); - } - RULES.keywords[keyword] = true; - if (!definition) - return; - const rule = { - keyword, - definition: { - ...definition, - type: (0, dataType_1.getJSONTypes)(definition.type), - schemaType: (0, dataType_1.getJSONTypes)(definition.schemaType) - } - }; - if (definition.before) - addBeforeRule.call(this, ruleGroup, rule, definition.before); - else - ruleGroup.rules.push(rule); - RULES.all[keyword] = rule; - (_a = definition.implements) === null || _a === undefined || _a.forEach((kwd) => this.addKeyword(kwd)); - } - function addBeforeRule(ruleGroup, rule, before) { - const i = ruleGroup.rules.findIndex((_rule) => _rule.keyword === before); - if (i >= 0) { - ruleGroup.rules.splice(i, 0, rule); - } else { - ruleGroup.rules.push(rule); - this.logger.warn(`rule ${before} is not defined`); - } - } - function keywordMetaschema(def) { - let { metaSchema } = def; - if (metaSchema === undefined) - return; - if (def.$data && this.opts.$data) - metaSchema = schemaOrData(metaSchema); - def.validateSchema = this.compile(metaSchema, true); - } - var $dataRef = { - $ref: "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/data.json#" - }; - function schemaOrData(schema) { - return { anyOf: [schema, $dataRef] }; - } -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/core/id.js -var require_id2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var def = { - keyword: "id", - code() { - throw new Error('NOT SUPPORTED: keyword "id", use "$id" for schema ID'); - } - }; - exports.default = def; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/core/ref.js -var require_ref2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.callRef = exports.getValidate = undefined; - var ref_error_1 = require_ref_error2(); - var code_1 = require_code4(); - var codegen_1 = require_codegen2(); - var names_1 = require_names2(); - var compile_1 = require_compile2(); - var util_1 = require_util2(); - var def = { - keyword: "$ref", - schemaType: "string", - code(cxt) { - const { gen, schema: $ref, it } = cxt; - const { baseId, schemaEnv: env, validateName, opts, self } = it; - const { root } = env; - if (($ref === "#" || $ref === "#/") && baseId === root.baseId) - return callRootRef(); - const schOrEnv = compile_1.resolveRef.call(self, root, baseId, $ref); - if (schOrEnv === undefined) - throw new ref_error_1.default(it.opts.uriResolver, baseId, $ref); - if (schOrEnv instanceof compile_1.SchemaEnv) - return callValidate(schOrEnv); - return inlineRefSchema(schOrEnv); - function callRootRef() { - if (env === root) - return callRef(cxt, validateName, env, env.$async); - const rootName = gen.scopeValue("root", { ref: root }); - return callRef(cxt, (0, codegen_1._)`${rootName}.validate`, root, root.$async); - } - function callValidate(sch) { - const v = getValidate(cxt, sch); - callRef(cxt, v, sch, sch.$async); - } - function inlineRefSchema(sch) { - const schName = gen.scopeValue("schema", opts.code.source === true ? { ref: sch, code: (0, codegen_1.stringify)(sch) } : { ref: sch }); - const valid = gen.name("valid"); - const schCxt = cxt.subschema({ - schema: sch, - dataTypes: [], - schemaPath: codegen_1.nil, - topSchemaRef: schName, - errSchemaPath: $ref - }, valid); - cxt.mergeEvaluated(schCxt); - cxt.ok(valid); - } - } - }; - function getValidate(cxt, sch) { - const { gen } = cxt; - return sch.validate ? gen.scopeValue("validate", { ref: sch.validate }) : (0, codegen_1._)`${gen.scopeValue("wrapper", { ref: sch })}.validate`; - } - exports.getValidate = getValidate; - function callRef(cxt, v, sch, $async) { - const { gen, it } = cxt; - const { allErrors, schemaEnv: env, opts } = it; - const passCxt = opts.passContext ? names_1.default.this : codegen_1.nil; - if ($async) - callAsyncRef(); - else - callSyncRef(); - function callAsyncRef() { - if (!env.$async) - throw new Error("async schema referenced by sync schema"); - const valid = gen.let("valid"); - gen.try(() => { - gen.code((0, codegen_1._)`await ${(0, code_1.callValidateCode)(cxt, v, passCxt)}`); - addEvaluatedFrom(v); - if (!allErrors) - gen.assign(valid, true); - }, (e) => { - gen.if((0, codegen_1._)`!(${e} instanceof ${it.ValidationError})`, () => gen.throw(e)); - addErrorsFrom(e); - if (!allErrors) - gen.assign(valid, false); - }); - cxt.ok(valid); - } - function callSyncRef() { - cxt.result((0, code_1.callValidateCode)(cxt, v, passCxt), () => addEvaluatedFrom(v), () => addErrorsFrom(v)); - } - function addErrorsFrom(source) { - const errs = (0, codegen_1._)`${source}.errors`; - gen.assign(names_1.default.vErrors, (0, codegen_1._)`${names_1.default.vErrors} === null ? ${errs} : ${names_1.default.vErrors}.concat(${errs})`); - gen.assign(names_1.default.errors, (0, codegen_1._)`${names_1.default.vErrors}.length`); - } - function addEvaluatedFrom(source) { - var _a; - if (!it.opts.unevaluated) - return; - const schEvaluated = (_a = sch === null || sch === undefined ? undefined : sch.validate) === null || _a === undefined ? undefined : _a.evaluated; - if (it.props !== true) { - if (schEvaluated && !schEvaluated.dynamicProps) { - if (schEvaluated.props !== undefined) { - it.props = util_1.mergeEvaluated.props(gen, schEvaluated.props, it.props); - } - } else { - const props = gen.var("props", (0, codegen_1._)`${source}.evaluated.props`); - it.props = util_1.mergeEvaluated.props(gen, props, it.props, codegen_1.Name); - } - } - if (it.items !== true) { - if (schEvaluated && !schEvaluated.dynamicItems) { - if (schEvaluated.items !== undefined) { - it.items = util_1.mergeEvaluated.items(gen, schEvaluated.items, it.items); - } - } else { - const items = gen.var("items", (0, codegen_1._)`${source}.evaluated.items`); - it.items = util_1.mergeEvaluated.items(gen, items, it.items, codegen_1.Name); - } - } - } - } - exports.callRef = callRef; - exports.default = def; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/core/index.js -var require_core4 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var id_1 = require_id2(); - var ref_1 = require_ref2(); - var core2 = [ - "$schema", - "$id", - "$defs", - "$vocabulary", - { keyword: "$comment" }, - "definitions", - id_1.default, - ref_1.default - ]; - exports.default = core2; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/validation/limitNumber.js -var require_limitNumber2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var codegen_1 = require_codegen2(); - var ops = codegen_1.operators; - var KWDs = { - maximum: { okStr: "<=", ok: ops.LTE, fail: ops.GT }, - minimum: { okStr: ">=", ok: ops.GTE, fail: ops.LT }, - exclusiveMaximum: { okStr: "<", ok: ops.LT, fail: ops.GTE }, - exclusiveMinimum: { okStr: ">", ok: ops.GT, fail: ops.LTE } - }; - var error2 = { - message: ({ keyword, schemaCode }) => (0, codegen_1.str)`must be ${KWDs[keyword].okStr} ${schemaCode}`, - params: ({ keyword, schemaCode }) => (0, codegen_1._)`{comparison: ${KWDs[keyword].okStr}, limit: ${schemaCode}}` - }; - var def = { - keyword: Object.keys(KWDs), - type: "number", - schemaType: "number", - $data: true, - error: error2, - code(cxt) { - const { keyword, data, schemaCode } = cxt; - cxt.fail$data((0, codegen_1._)`${data} ${KWDs[keyword].fail} ${schemaCode} || isNaN(${data})`); - } - }; - exports.default = def; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/validation/multipleOf.js -var require_multipleOf2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var codegen_1 = require_codegen2(); - var error2 = { - message: ({ schemaCode }) => (0, codegen_1.str)`must be multiple of ${schemaCode}`, - params: ({ schemaCode }) => (0, codegen_1._)`{multipleOf: ${schemaCode}}` - }; - var def = { - keyword: "multipleOf", - type: "number", - schemaType: "number", - $data: true, - error: error2, - code(cxt) { - const { gen, data, schemaCode, it } = cxt; - const prec = it.opts.multipleOfPrecision; - const res = gen.let("res"); - const invalid = prec ? (0, codegen_1._)`Math.abs(Math.round(${res}) - ${res}) > 1e-${prec}` : (0, codegen_1._)`${res} !== parseInt(${res})`; - cxt.fail$data((0, codegen_1._)`(${schemaCode} === 0 || (${res} = ${data}/${schemaCode}, ${invalid}))`); - } - }; - exports.default = def; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/runtime/ucs2length.js -var require_ucs2length2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - function ucs2length(str) { - const len = str.length; - let length = 0; - let pos = 0; - let value; - while (pos < len) { - length++; - value = str.charCodeAt(pos++); - if (value >= 55296 && value <= 56319 && pos < len) { - value = str.charCodeAt(pos); - if ((value & 64512) === 56320) - pos++; - } - } - return length; - } - exports.default = ucs2length; - ucs2length.code = 'require("ajv/dist/runtime/ucs2length").default'; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/validation/limitLength.js -var require_limitLength2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var codegen_1 = require_codegen2(); - var util_1 = require_util2(); - var ucs2length_1 = require_ucs2length2(); - var error2 = { - message({ keyword, schemaCode }) { - const comp = keyword === "maxLength" ? "more" : "fewer"; - return (0, codegen_1.str)`must NOT have ${comp} than ${schemaCode} characters`; - }, - params: ({ schemaCode }) => (0, codegen_1._)`{limit: ${schemaCode}}` - }; - var def = { - keyword: ["maxLength", "minLength"], - type: "string", - schemaType: "number", - $data: true, - error: error2, - code(cxt) { - const { keyword, data, schemaCode, it } = cxt; - const op = keyword === "maxLength" ? codegen_1.operators.GT : codegen_1.operators.LT; - const len = it.opts.unicode === false ? (0, codegen_1._)`${data}.length` : (0, codegen_1._)`${(0, util_1.useFunc)(cxt.gen, ucs2length_1.default)}(${data})`; - cxt.fail$data((0, codegen_1._)`${len} ${op} ${schemaCode}`); - } - }; - exports.default = def; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/validation/pattern.js -var require_pattern2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var code_1 = require_code4(); - var codegen_1 = require_codegen2(); - var error2 = { - message: ({ schemaCode }) => (0, codegen_1.str)`must match pattern "${schemaCode}"`, - params: ({ schemaCode }) => (0, codegen_1._)`{pattern: ${schemaCode}}` - }; - var def = { - keyword: "pattern", - type: "string", - schemaType: "string", - $data: true, - error: error2, - code(cxt) { - const { data, $data, schema, schemaCode, it } = cxt; - const u = it.opts.unicodeRegExp ? "u" : ""; - const regExp = $data ? (0, codegen_1._)`(new RegExp(${schemaCode}, ${u}))` : (0, code_1.usePattern)(cxt, schema); - cxt.fail$data((0, codegen_1._)`!${regExp}.test(${data})`); - } - }; - exports.default = def; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/validation/limitProperties.js -var require_limitProperties2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var codegen_1 = require_codegen2(); - var error2 = { - message({ keyword, schemaCode }) { - const comp = keyword === "maxProperties" ? "more" : "fewer"; - return (0, codegen_1.str)`must NOT have ${comp} than ${schemaCode} properties`; - }, - params: ({ schemaCode }) => (0, codegen_1._)`{limit: ${schemaCode}}` - }; - var def = { - keyword: ["maxProperties", "minProperties"], - type: "object", - schemaType: "number", - $data: true, - error: error2, - code(cxt) { - const { keyword, data, schemaCode } = cxt; - const op = keyword === "maxProperties" ? codegen_1.operators.GT : codegen_1.operators.LT; - cxt.fail$data((0, codegen_1._)`Object.keys(${data}).length ${op} ${schemaCode}`); - } - }; - exports.default = def; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/validation/required.js -var require_required2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var code_1 = require_code4(); - var codegen_1 = require_codegen2(); - var util_1 = require_util2(); - var error2 = { - message: ({ params: { missingProperty } }) => (0, codegen_1.str)`must have required property '${missingProperty}'`, - params: ({ params: { missingProperty } }) => (0, codegen_1._)`{missingProperty: ${missingProperty}}` - }; - var def = { - keyword: "required", - type: "object", - schemaType: "array", - $data: true, - error: error2, - code(cxt) { - const { gen, schema, schemaCode, data, $data, it } = cxt; - const { opts } = it; - if (!$data && schema.length === 0) - return; - const useLoop = schema.length >= opts.loopRequired; - if (it.allErrors) - allErrorsMode(); - else - exitOnErrorMode(); - if (opts.strictRequired) { - const props = cxt.parentSchema.properties; - const { definedProperties } = cxt.it; - for (const requiredKey of schema) { - if ((props === null || props === undefined ? undefined : props[requiredKey]) === undefined && !definedProperties.has(requiredKey)) { - const schemaPath = it.schemaEnv.baseId + it.errSchemaPath; - const msg = `required property "${requiredKey}" is not defined at "${schemaPath}" (strictRequired)`; - (0, util_1.checkStrictMode)(it, msg, it.opts.strictRequired); - } - } - } - function allErrorsMode() { - if (useLoop || $data) { - cxt.block$data(codegen_1.nil, loopAllRequired); - } else { - for (const prop of schema) { - (0, code_1.checkReportMissingProp)(cxt, prop); - } - } - } - function exitOnErrorMode() { - const missing = gen.let("missing"); - if (useLoop || $data) { - const valid = gen.let("valid", true); - cxt.block$data(valid, () => loopUntilMissing(missing, valid)); - cxt.ok(valid); - } else { - gen.if((0, code_1.checkMissingProp)(cxt, schema, missing)); - (0, code_1.reportMissingProp)(cxt, missing); - gen.else(); - } - } - function loopAllRequired() { - gen.forOf("prop", schemaCode, (prop) => { - cxt.setParams({ missingProperty: prop }); - gen.if((0, code_1.noPropertyInData)(gen, data, prop, opts.ownProperties), () => cxt.error()); - }); - } - function loopUntilMissing(missing, valid) { - cxt.setParams({ missingProperty: missing }); - gen.forOf(missing, schemaCode, () => { - gen.assign(valid, (0, code_1.propertyInData)(gen, data, missing, opts.ownProperties)); - gen.if((0, codegen_1.not)(valid), () => { - cxt.error(); - gen.break(); - }); - }, codegen_1.nil); - } - } - }; - exports.default = def; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/validation/limitItems.js -var require_limitItems2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var codegen_1 = require_codegen2(); - var error2 = { - message({ keyword, schemaCode }) { - const comp = keyword === "maxItems" ? "more" : "fewer"; - return (0, codegen_1.str)`must NOT have ${comp} than ${schemaCode} items`; - }, - params: ({ schemaCode }) => (0, codegen_1._)`{limit: ${schemaCode}}` - }; - var def = { - keyword: ["maxItems", "minItems"], - type: "array", - schemaType: "number", - $data: true, - error: error2, - code(cxt) { - const { keyword, data, schemaCode } = cxt; - const op = keyword === "maxItems" ? codegen_1.operators.GT : codegen_1.operators.LT; - cxt.fail$data((0, codegen_1._)`${data}.length ${op} ${schemaCode}`); - } - }; - exports.default = def; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/runtime/equal.js -var require_equal2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var equal = require_fast_deep_equal(); - equal.code = 'require("ajv/dist/runtime/equal").default'; - exports.default = equal; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/validation/uniqueItems.js -var require_uniqueItems2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var dataType_1 = require_dataType2(); - var codegen_1 = require_codegen2(); - var util_1 = require_util2(); - var equal_1 = require_equal2(); - var error2 = { - message: ({ params: { i, j } }) => (0, codegen_1.str)`must NOT have duplicate items (items ## ${j} and ${i} are identical)`, - params: ({ params: { i, j } }) => (0, codegen_1._)`{i: ${i}, j: ${j}}` - }; - var def = { - keyword: "uniqueItems", - type: "array", - schemaType: "boolean", - $data: true, - error: error2, - code(cxt) { - const { gen, data, $data, schema, parentSchema, schemaCode, it } = cxt; - if (!$data && !schema) - return; - const valid = gen.let("valid"); - const itemTypes = parentSchema.items ? (0, dataType_1.getSchemaTypes)(parentSchema.items) : []; - cxt.block$data(valid, validateUniqueItems, (0, codegen_1._)`${schemaCode} === false`); - cxt.ok(valid); - function validateUniqueItems() { - const i = gen.let("i", (0, codegen_1._)`${data}.length`); - const j = gen.let("j"); - cxt.setParams({ i, j }); - gen.assign(valid, true); - gen.if((0, codegen_1._)`${i} > 1`, () => (canOptimize() ? loopN : loopN2)(i, j)); - } - function canOptimize() { - return itemTypes.length > 0 && !itemTypes.some((t) => t === "object" || t === "array"); - } - function loopN(i, j) { - const item = gen.name("item"); - const wrongType = (0, dataType_1.checkDataTypes)(itemTypes, item, it.opts.strictNumbers, dataType_1.DataType.Wrong); - const indices = gen.const("indices", (0, codegen_1._)`{}`); - gen.for((0, codegen_1._)`;${i}--;`, () => { - gen.let(item, (0, codegen_1._)`${data}[${i}]`); - gen.if(wrongType, (0, codegen_1._)`continue`); - if (itemTypes.length > 1) - gen.if((0, codegen_1._)`typeof ${item} == "string"`, (0, codegen_1._)`${item} += "_"`); - gen.if((0, codegen_1._)`typeof ${indices}[${item}] == "number"`, () => { - gen.assign(j, (0, codegen_1._)`${indices}[${item}]`); - cxt.error(); - gen.assign(valid, false).break(); - }).code((0, codegen_1._)`${indices}[${item}] = ${i}`); - }); - } - function loopN2(i, j) { - const eql = (0, util_1.useFunc)(gen, equal_1.default); - const outer = gen.name("outer"); - gen.label(outer).for((0, codegen_1._)`;${i}--;`, () => gen.for((0, codegen_1._)`${j} = ${i}; ${j}--;`, () => gen.if((0, codegen_1._)`${eql}(${data}[${i}], ${data}[${j}])`, () => { - cxt.error(); - gen.assign(valid, false).break(outer); - }))); - } - } - }; - exports.default = def; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/validation/const.js -var require_const2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var codegen_1 = require_codegen2(); - var util_1 = require_util2(); - var equal_1 = require_equal2(); - var error2 = { - message: "must be equal to constant", - params: ({ schemaCode }) => (0, codegen_1._)`{allowedValue: ${schemaCode}}` - }; - var def = { - keyword: "const", - $data: true, - error: error2, - code(cxt) { - const { gen, data, $data, schemaCode, schema } = cxt; - if ($data || schema && typeof schema == "object") { - cxt.fail$data((0, codegen_1._)`!${(0, util_1.useFunc)(gen, equal_1.default)}(${data}, ${schemaCode})`); - } else { - cxt.fail((0, codegen_1._)`${schema} !== ${data}`); - } - } - }; - exports.default = def; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/validation/enum.js -var require_enum2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var codegen_1 = require_codegen2(); - var util_1 = require_util2(); - var equal_1 = require_equal2(); - var error2 = { - message: "must be equal to one of the allowed values", - params: ({ schemaCode }) => (0, codegen_1._)`{allowedValues: ${schemaCode}}` - }; - var def = { - keyword: "enum", - schemaType: "array", - $data: true, - error: error2, - code(cxt) { - const { gen, data, $data, schema, schemaCode, it } = cxt; - if (!$data && schema.length === 0) - throw new Error("enum must have non-empty array"); - const useLoop = schema.length >= it.opts.loopEnum; - let eql; - const getEql = () => eql !== null && eql !== undefined ? eql : eql = (0, util_1.useFunc)(gen, equal_1.default); - let valid; - if (useLoop || $data) { - valid = gen.let("valid"); - cxt.block$data(valid, loopEnum); - } else { - if (!Array.isArray(schema)) - throw new Error("ajv implementation error"); - const vSchema = gen.const("vSchema", schemaCode); - valid = (0, codegen_1.or)(...schema.map((_x, i) => equalCode(vSchema, i))); - } - cxt.pass(valid); - function loopEnum() { - gen.assign(valid, false); - gen.forOf("v", schemaCode, (v) => gen.if((0, codegen_1._)`${getEql()}(${data}, ${v})`, () => gen.assign(valid, true).break())); - } - function equalCode(vSchema, i) { - const sch = schema[i]; - return typeof sch === "object" && sch !== null ? (0, codegen_1._)`${getEql()}(${data}, ${vSchema}[${i}])` : (0, codegen_1._)`${data} === ${sch}`; - } - } - }; - exports.default = def; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/validation/index.js -var require_validation2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var limitNumber_1 = require_limitNumber2(); - var multipleOf_1 = require_multipleOf2(); - var limitLength_1 = require_limitLength2(); - var pattern_1 = require_pattern2(); - var limitProperties_1 = require_limitProperties2(); - var required_1 = require_required2(); - var limitItems_1 = require_limitItems2(); - var uniqueItems_1 = require_uniqueItems2(); - var const_1 = require_const2(); - var enum_1 = require_enum2(); - var validation = [ - limitNumber_1.default, - multipleOf_1.default, - limitLength_1.default, - pattern_1.default, - limitProperties_1.default, - required_1.default, - limitItems_1.default, - uniqueItems_1.default, - { keyword: "type", schemaType: ["string", "array"] }, - { keyword: "nullable", schemaType: "boolean" }, - const_1.default, - enum_1.default - ]; - exports.default = validation; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/applicator/additionalItems.js -var require_additionalItems2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.validateAdditionalItems = undefined; - var codegen_1 = require_codegen2(); - var util_1 = require_util2(); - var error2 = { - message: ({ params: { len } }) => (0, codegen_1.str)`must NOT have more than ${len} items`, - params: ({ params: { len } }) => (0, codegen_1._)`{limit: ${len}}` - }; - var def = { - keyword: "additionalItems", - type: "array", - schemaType: ["boolean", "object"], - before: "uniqueItems", - error: error2, - code(cxt) { - const { parentSchema, it } = cxt; - const { items } = parentSchema; - if (!Array.isArray(items)) { - (0, util_1.checkStrictMode)(it, '"additionalItems" is ignored when "items" is not an array of schemas'); - return; - } - validateAdditionalItems(cxt, items); - } - }; - function validateAdditionalItems(cxt, items) { - const { gen, schema, data, keyword, it } = cxt; - it.items = true; - const len = gen.const("len", (0, codegen_1._)`${data}.length`); - if (schema === false) { - cxt.setParams({ len: items.length }); - cxt.pass((0, codegen_1._)`${len} <= ${items.length}`); - } else if (typeof schema == "object" && !(0, util_1.alwaysValidSchema)(it, schema)) { - const valid = gen.var("valid", (0, codegen_1._)`${len} <= ${items.length}`); - gen.if((0, codegen_1.not)(valid), () => validateItems(valid)); - cxt.ok(valid); - } - function validateItems(valid) { - gen.forRange("i", items.length, len, (i) => { - cxt.subschema({ keyword, dataProp: i, dataPropType: util_1.Type.Num }, valid); - if (!it.allErrors) - gen.if((0, codegen_1.not)(valid), () => gen.break()); - }); - } - } - exports.validateAdditionalItems = validateAdditionalItems; - exports.default = def; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/applicator/items.js -var require_items2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.validateTuple = undefined; - var codegen_1 = require_codegen2(); - var util_1 = require_util2(); - var code_1 = require_code4(); - var def = { - keyword: "items", - type: "array", - schemaType: ["object", "array", "boolean"], - before: "uniqueItems", - code(cxt) { - const { schema, it } = cxt; - if (Array.isArray(schema)) - return validateTuple(cxt, "additionalItems", schema); - it.items = true; - if ((0, util_1.alwaysValidSchema)(it, schema)) - return; - cxt.ok((0, code_1.validateArray)(cxt)); - } - }; - function validateTuple(cxt, extraItems, schArr = cxt.schema) { - const { gen, parentSchema, data, keyword, it } = cxt; - checkStrictTuple(parentSchema); - if (it.opts.unevaluated && schArr.length && it.items !== true) { - it.items = util_1.mergeEvaluated.items(gen, schArr.length, it.items); - } - const valid = gen.name("valid"); - const len = gen.const("len", (0, codegen_1._)`${data}.length`); - schArr.forEach((sch, i) => { - if ((0, util_1.alwaysValidSchema)(it, sch)) - return; - gen.if((0, codegen_1._)`${len} > ${i}`, () => cxt.subschema({ - keyword, - schemaProp: i, - dataProp: i - }, valid)); - cxt.ok(valid); - }); - function checkStrictTuple(sch) { - const { opts, errSchemaPath } = it; - const l = schArr.length; - const fullTuple = l === sch.minItems && (l === sch.maxItems || sch[extraItems] === false); - if (opts.strictTuples && !fullTuple) { - const msg = `"${keyword}" is ${l}-tuple, but minItems or maxItems/${extraItems} are not specified or different at path "${errSchemaPath}"`; - (0, util_1.checkStrictMode)(it, msg, opts.strictTuples); - } - } - } - exports.validateTuple = validateTuple; - exports.default = def; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/applicator/prefixItems.js -var require_prefixItems2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var items_1 = require_items2(); - var def = { - keyword: "prefixItems", - type: "array", - schemaType: ["array"], - before: "uniqueItems", - code: (cxt) => (0, items_1.validateTuple)(cxt, "items") - }; - exports.default = def; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/applicator/items2020.js -var require_items20202 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var codegen_1 = require_codegen2(); - var util_1 = require_util2(); - var code_1 = require_code4(); - var additionalItems_1 = require_additionalItems2(); - var error2 = { - message: ({ params: { len } }) => (0, codegen_1.str)`must NOT have more than ${len} items`, - params: ({ params: { len } }) => (0, codegen_1._)`{limit: ${len}}` - }; - var def = { - keyword: "items", - type: "array", - schemaType: ["object", "boolean"], - before: "uniqueItems", - error: error2, - code(cxt) { - const { schema, parentSchema, it } = cxt; - const { prefixItems } = parentSchema; - it.items = true; - if ((0, util_1.alwaysValidSchema)(it, schema)) - return; - if (prefixItems) - (0, additionalItems_1.validateAdditionalItems)(cxt, prefixItems); - else - cxt.ok((0, code_1.validateArray)(cxt)); - } - }; - exports.default = def; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/applicator/contains.js -var require_contains2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var codegen_1 = require_codegen2(); - var util_1 = require_util2(); - var error2 = { - message: ({ params: { min, max } }) => max === undefined ? (0, codegen_1.str)`must contain at least ${min} valid item(s)` : (0, codegen_1.str)`must contain at least ${min} and no more than ${max} valid item(s)`, - params: ({ params: { min, max } }) => max === undefined ? (0, codegen_1._)`{minContains: ${min}}` : (0, codegen_1._)`{minContains: ${min}, maxContains: ${max}}` - }; - var def = { - keyword: "contains", - type: "array", - schemaType: ["object", "boolean"], - before: "uniqueItems", - trackErrors: true, - error: error2, - code(cxt) { - const { gen, schema, parentSchema, data, it } = cxt; - let min; - let max; - const { minContains, maxContains } = parentSchema; - if (it.opts.next) { - min = minContains === undefined ? 1 : minContains; - max = maxContains; - } else { - min = 1; - } - const len = gen.const("len", (0, codegen_1._)`${data}.length`); - cxt.setParams({ min, max }); - if (max === undefined && min === 0) { - (0, util_1.checkStrictMode)(it, `"minContains" == 0 without "maxContains": "contains" keyword ignored`); - return; - } - if (max !== undefined && min > max) { - (0, util_1.checkStrictMode)(it, `"minContains" > "maxContains" is always invalid`); - cxt.fail(); - return; - } - if ((0, util_1.alwaysValidSchema)(it, schema)) { - let cond = (0, codegen_1._)`${len} >= ${min}`; - if (max !== undefined) - cond = (0, codegen_1._)`${cond} && ${len} <= ${max}`; - cxt.pass(cond); - return; - } - it.items = true; - const valid = gen.name("valid"); - if (max === undefined && min === 1) { - validateItems(valid, () => gen.if(valid, () => gen.break())); - } else if (min === 0) { - gen.let(valid, true); - if (max !== undefined) - gen.if((0, codegen_1._)`${data}.length > 0`, validateItemsWithCount); - } else { - gen.let(valid, false); - validateItemsWithCount(); - } - cxt.result(valid, () => cxt.reset()); - function validateItemsWithCount() { - const schValid = gen.name("_valid"); - const count = gen.let("count", 0); - validateItems(schValid, () => gen.if(schValid, () => checkLimits(count))); - } - function validateItems(_valid, block) { - gen.forRange("i", 0, len, (i) => { - cxt.subschema({ - keyword: "contains", - dataProp: i, - dataPropType: util_1.Type.Num, - compositeRule: true - }, _valid); - block(); - }); - } - function checkLimits(count) { - gen.code((0, codegen_1._)`${count}++`); - if (max === undefined) { - gen.if((0, codegen_1._)`${count} >= ${min}`, () => gen.assign(valid, true).break()); - } else { - gen.if((0, codegen_1._)`${count} > ${max}`, () => gen.assign(valid, false).break()); - if (min === 1) - gen.assign(valid, true); - else - gen.if((0, codegen_1._)`${count} >= ${min}`, () => gen.assign(valid, true)); - } - } - } - }; - exports.default = def; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/applicator/dependencies.js -var require_dependencies2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.validateSchemaDeps = exports.validatePropertyDeps = exports.error = undefined; - var codegen_1 = require_codegen2(); - var util_1 = require_util2(); - var code_1 = require_code4(); - exports.error = { - message: ({ params: { property, depsCount, deps } }) => { - const property_ies = depsCount === 1 ? "property" : "properties"; - return (0, codegen_1.str)`must have ${property_ies} ${deps} when property ${property} is present`; - }, - params: ({ params: { property, depsCount, deps, missingProperty } }) => (0, codegen_1._)`{property: ${property}, - missingProperty: ${missingProperty}, - depsCount: ${depsCount}, - deps: ${deps}}` - }; - var def = { - keyword: "dependencies", - type: "object", - schemaType: "object", - error: exports.error, - code(cxt) { - const [propDeps, schDeps] = splitDependencies(cxt); - validatePropertyDeps(cxt, propDeps); - validateSchemaDeps(cxt, schDeps); - } - }; - function splitDependencies({ schema }) { - const propertyDeps = {}; - const schemaDeps = {}; - for (const key in schema) { - if (key === "__proto__") - continue; - const deps = Array.isArray(schema[key]) ? propertyDeps : schemaDeps; - deps[key] = schema[key]; - } - return [propertyDeps, schemaDeps]; - } - function validatePropertyDeps(cxt, propertyDeps = cxt.schema) { - const { gen, data, it } = cxt; - if (Object.keys(propertyDeps).length === 0) - return; - const missing = gen.let("missing"); - for (const prop in propertyDeps) { - const deps = propertyDeps[prop]; - if (deps.length === 0) - continue; - const hasProperty = (0, code_1.propertyInData)(gen, data, prop, it.opts.ownProperties); - cxt.setParams({ - property: prop, - depsCount: deps.length, - deps: deps.join(", ") - }); - if (it.allErrors) { - gen.if(hasProperty, () => { - for (const depProp of deps) { - (0, code_1.checkReportMissingProp)(cxt, depProp); - } - }); - } else { - gen.if((0, codegen_1._)`${hasProperty} && (${(0, code_1.checkMissingProp)(cxt, deps, missing)})`); - (0, code_1.reportMissingProp)(cxt, missing); - gen.else(); - } - } - } - exports.validatePropertyDeps = validatePropertyDeps; - function validateSchemaDeps(cxt, schemaDeps = cxt.schema) { - const { gen, data, keyword, it } = cxt; - const valid = gen.name("valid"); - for (const prop in schemaDeps) { - if ((0, util_1.alwaysValidSchema)(it, schemaDeps[prop])) - continue; - gen.if((0, code_1.propertyInData)(gen, data, prop, it.opts.ownProperties), () => { - const schCxt = cxt.subschema({ keyword, schemaProp: prop }, valid); - cxt.mergeValidEvaluated(schCxt, valid); - }, () => gen.var(valid, true)); - cxt.ok(valid); - } - } - exports.validateSchemaDeps = validateSchemaDeps; - exports.default = def; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/applicator/propertyNames.js -var require_propertyNames2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var codegen_1 = require_codegen2(); - var util_1 = require_util2(); - var error2 = { - message: "property name must be valid", - params: ({ params }) => (0, codegen_1._)`{propertyName: ${params.propertyName}}` - }; - var def = { - keyword: "propertyNames", - type: "object", - schemaType: ["object", "boolean"], - error: error2, - code(cxt) { - const { gen, schema, data, it } = cxt; - if ((0, util_1.alwaysValidSchema)(it, schema)) - return; - const valid = gen.name("valid"); - gen.forIn("key", data, (key) => { - cxt.setParams({ propertyName: key }); - cxt.subschema({ - keyword: "propertyNames", - data: key, - dataTypes: ["string"], - propertyName: key, - compositeRule: true - }, valid); - gen.if((0, codegen_1.not)(valid), () => { - cxt.error(true); - if (!it.allErrors) - gen.break(); - }); - }); - cxt.ok(valid); - } - }; - exports.default = def; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/applicator/additionalProperties.js -var require_additionalProperties2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var code_1 = require_code4(); - var codegen_1 = require_codegen2(); - var names_1 = require_names2(); - var util_1 = require_util2(); - var error2 = { - message: "must NOT have additional properties", - params: ({ params }) => (0, codegen_1._)`{additionalProperty: ${params.additionalProperty}}` - }; - var def = { - keyword: "additionalProperties", - type: ["object"], - schemaType: ["boolean", "object"], - allowUndefined: true, - trackErrors: true, - error: error2, - code(cxt) { - const { gen, schema, parentSchema, data, errsCount, it } = cxt; - if (!errsCount) - throw new Error("ajv implementation error"); - const { allErrors, opts } = it; - it.props = true; - if (opts.removeAdditional !== "all" && (0, util_1.alwaysValidSchema)(it, schema)) - return; - const props = (0, code_1.allSchemaProperties)(parentSchema.properties); - const patProps = (0, code_1.allSchemaProperties)(parentSchema.patternProperties); - checkAdditionalProperties(); - cxt.ok((0, codegen_1._)`${errsCount} === ${names_1.default.errors}`); - function checkAdditionalProperties() { - gen.forIn("key", data, (key) => { - if (!props.length && !patProps.length) - additionalPropertyCode(key); - else - gen.if(isAdditional(key), () => additionalPropertyCode(key)); - }); - } - function isAdditional(key) { - let definedProp; - if (props.length > 8) { - const propsSchema = (0, util_1.schemaRefOrVal)(it, parentSchema.properties, "properties"); - definedProp = (0, code_1.isOwnProperty)(gen, propsSchema, key); - } else if (props.length) { - definedProp = (0, codegen_1.or)(...props.map((p) => (0, codegen_1._)`${key} === ${p}`)); - } else { - definedProp = codegen_1.nil; - } - if (patProps.length) { - definedProp = (0, codegen_1.or)(definedProp, ...patProps.map((p) => (0, codegen_1._)`${(0, code_1.usePattern)(cxt, p)}.test(${key})`)); - } - return (0, codegen_1.not)(definedProp); - } - function deleteAdditional(key) { - gen.code((0, codegen_1._)`delete ${data}[${key}]`); - } - function additionalPropertyCode(key) { - if (opts.removeAdditional === "all" || opts.removeAdditional && schema === false) { - deleteAdditional(key); - return; - } - if (schema === false) { - cxt.setParams({ additionalProperty: key }); - cxt.error(); - if (!allErrors) - gen.break(); - return; - } - if (typeof schema == "object" && !(0, util_1.alwaysValidSchema)(it, schema)) { - const valid = gen.name("valid"); - if (opts.removeAdditional === "failing") { - applyAdditionalSchema(key, valid, false); - gen.if((0, codegen_1.not)(valid), () => { - cxt.reset(); - deleteAdditional(key); - }); - } else { - applyAdditionalSchema(key, valid); - if (!allErrors) - gen.if((0, codegen_1.not)(valid), () => gen.break()); - } - } - } - function applyAdditionalSchema(key, valid, errors3) { - const subschema = { - keyword: "additionalProperties", - dataProp: key, - dataPropType: util_1.Type.Str - }; - if (errors3 === false) { - Object.assign(subschema, { - compositeRule: true, - createErrors: false, - allErrors: false - }); - } - cxt.subschema(subschema, valid); - } - } - }; - exports.default = def; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/applicator/properties.js -var require_properties2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var validate_1 = require_validate2(); - var code_1 = require_code4(); - var util_1 = require_util2(); - var additionalProperties_1 = require_additionalProperties2(); - var def = { - keyword: "properties", - type: "object", - schemaType: "object", - code(cxt) { - const { gen, schema, parentSchema, data, it } = cxt; - if (it.opts.removeAdditional === "all" && parentSchema.additionalProperties === undefined) { - additionalProperties_1.default.code(new validate_1.KeywordCxt(it, additionalProperties_1.default, "additionalProperties")); - } - const allProps = (0, code_1.allSchemaProperties)(schema); - for (const prop of allProps) { - it.definedProperties.add(prop); - } - if (it.opts.unevaluated && allProps.length && it.props !== true) { - it.props = util_1.mergeEvaluated.props(gen, (0, util_1.toHash)(allProps), it.props); - } - const properties = allProps.filter((p) => !(0, util_1.alwaysValidSchema)(it, schema[p])); - if (properties.length === 0) - return; - const valid = gen.name("valid"); - for (const prop of properties) { - if (hasDefault(prop)) { - applyPropertySchema(prop); - } else { - gen.if((0, code_1.propertyInData)(gen, data, prop, it.opts.ownProperties)); - applyPropertySchema(prop); - if (!it.allErrors) - gen.else().var(valid, true); - gen.endIf(); - } - cxt.it.definedProperties.add(prop); - cxt.ok(valid); - } - function hasDefault(prop) { - return it.opts.useDefaults && !it.compositeRule && schema[prop].default !== undefined; - } - function applyPropertySchema(prop) { - cxt.subschema({ - keyword: "properties", - schemaProp: prop, - dataProp: prop - }, valid); - } - } - }; - exports.default = def; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/applicator/patternProperties.js -var require_patternProperties2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var code_1 = require_code4(); - var codegen_1 = require_codegen2(); - var util_1 = require_util2(); - var util_2 = require_util2(); - var def = { - keyword: "patternProperties", - type: "object", - schemaType: "object", - code(cxt) { - const { gen, schema, data, parentSchema, it } = cxt; - const { opts } = it; - const patterns = (0, code_1.allSchemaProperties)(schema); - const alwaysValidPatterns = patterns.filter((p) => (0, util_1.alwaysValidSchema)(it, schema[p])); - if (patterns.length === 0 || alwaysValidPatterns.length === patterns.length && (!it.opts.unevaluated || it.props === true)) { - return; - } - const checkProperties = opts.strictSchema && !opts.allowMatchingProperties && parentSchema.properties; - const valid = gen.name("valid"); - if (it.props !== true && !(it.props instanceof codegen_1.Name)) { - it.props = (0, util_2.evaluatedPropsToName)(gen, it.props); - } - const { props } = it; - validatePatternProperties(); - function validatePatternProperties() { - for (const pat of patterns) { - if (checkProperties) - checkMatchingProperties(pat); - if (it.allErrors) { - validateProperties(pat); - } else { - gen.var(valid, true); - validateProperties(pat); - gen.if(valid); - } - } - } - function checkMatchingProperties(pat) { - for (const prop in checkProperties) { - if (new RegExp(pat).test(prop)) { - (0, util_1.checkStrictMode)(it, `property ${prop} matches pattern ${pat} (use allowMatchingProperties)`); - } - } - } - function validateProperties(pat) { - gen.forIn("key", data, (key) => { - gen.if((0, codegen_1._)`${(0, code_1.usePattern)(cxt, pat)}.test(${key})`, () => { - const alwaysValid = alwaysValidPatterns.includes(pat); - if (!alwaysValid) { - cxt.subschema({ - keyword: "patternProperties", - schemaProp: pat, - dataProp: key, - dataPropType: util_2.Type.Str - }, valid); - } - if (it.opts.unevaluated && props !== true) { - gen.assign((0, codegen_1._)`${props}[${key}]`, true); - } else if (!alwaysValid && !it.allErrors) { - gen.if((0, codegen_1.not)(valid), () => gen.break()); - } - }); - }); - } - } - }; - exports.default = def; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/applicator/not.js -var require_not2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var util_1 = require_util2(); - var def = { - keyword: "not", - schemaType: ["object", "boolean"], - trackErrors: true, - code(cxt) { - const { gen, schema, it } = cxt; - if ((0, util_1.alwaysValidSchema)(it, schema)) { - cxt.fail(); - return; - } - const valid = gen.name("valid"); - cxt.subschema({ - keyword: "not", - compositeRule: true, - createErrors: false, - allErrors: false - }, valid); - cxt.failResult(valid, () => cxt.reset(), () => cxt.error()); - }, - error: { message: "must NOT be valid" } - }; - exports.default = def; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/applicator/anyOf.js -var require_anyOf2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var code_1 = require_code4(); - var def = { - keyword: "anyOf", - schemaType: "array", - trackErrors: true, - code: code_1.validateUnion, - error: { message: "must match a schema in anyOf" } - }; - exports.default = def; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/applicator/oneOf.js -var require_oneOf2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var codegen_1 = require_codegen2(); - var util_1 = require_util2(); - var error2 = { - message: "must match exactly one schema in oneOf", - params: ({ params }) => (0, codegen_1._)`{passingSchemas: ${params.passing}}` - }; - var def = { - keyword: "oneOf", - schemaType: "array", - trackErrors: true, - error: error2, - code(cxt) { - const { gen, schema, parentSchema, it } = cxt; - if (!Array.isArray(schema)) - throw new Error("ajv implementation error"); - if (it.opts.discriminator && parentSchema.discriminator) - return; - const schArr = schema; - const valid = gen.let("valid", false); - const passing = gen.let("passing", null); - const schValid = gen.name("_valid"); - cxt.setParams({ passing }); - gen.block(validateOneOf); - cxt.result(valid, () => cxt.reset(), () => cxt.error(true)); - function validateOneOf() { - schArr.forEach((sch, i) => { - let schCxt; - if ((0, util_1.alwaysValidSchema)(it, sch)) { - gen.var(schValid, true); - } else { - schCxt = cxt.subschema({ - keyword: "oneOf", - schemaProp: i, - compositeRule: true - }, schValid); - } - if (i > 0) { - gen.if((0, codegen_1._)`${schValid} && ${valid}`).assign(valid, false).assign(passing, (0, codegen_1._)`[${passing}, ${i}]`).else(); - } - gen.if(schValid, () => { - gen.assign(valid, true); - gen.assign(passing, i); - if (schCxt) - cxt.mergeEvaluated(schCxt, codegen_1.Name); - }); - }); - } - } - }; - exports.default = def; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/applicator/allOf.js -var require_allOf2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var util_1 = require_util2(); - var def = { - keyword: "allOf", - schemaType: "array", - code(cxt) { - const { gen, schema, it } = cxt; - if (!Array.isArray(schema)) - throw new Error("ajv implementation error"); - const valid = gen.name("valid"); - schema.forEach((sch, i) => { - if ((0, util_1.alwaysValidSchema)(it, sch)) - return; - const schCxt = cxt.subschema({ keyword: "allOf", schemaProp: i }, valid); - cxt.ok(valid); - cxt.mergeEvaluated(schCxt); - }); - } - }; - exports.default = def; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/applicator/if.js -var require_if2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var codegen_1 = require_codegen2(); - var util_1 = require_util2(); - var error2 = { - message: ({ params }) => (0, codegen_1.str)`must match "${params.ifClause}" schema`, - params: ({ params }) => (0, codegen_1._)`{failingKeyword: ${params.ifClause}}` - }; - var def = { - keyword: "if", - schemaType: ["object", "boolean"], - trackErrors: true, - error: error2, - code(cxt) { - const { gen, parentSchema, it } = cxt; - if (parentSchema.then === undefined && parentSchema.else === undefined) { - (0, util_1.checkStrictMode)(it, '"if" without "then" and "else" is ignored'); - } - const hasThen = hasSchema(it, "then"); - const hasElse = hasSchema(it, "else"); - if (!hasThen && !hasElse) - return; - const valid = gen.let("valid", true); - const schValid = gen.name("_valid"); - validateIf(); - cxt.reset(); - if (hasThen && hasElse) { - const ifClause = gen.let("ifClause"); - cxt.setParams({ ifClause }); - gen.if(schValid, validateClause("then", ifClause), validateClause("else", ifClause)); - } else if (hasThen) { - gen.if(schValid, validateClause("then")); - } else { - gen.if((0, codegen_1.not)(schValid), validateClause("else")); - } - cxt.pass(valid, () => cxt.error(true)); - function validateIf() { - const schCxt = cxt.subschema({ - keyword: "if", - compositeRule: true, - createErrors: false, - allErrors: false - }, schValid); - cxt.mergeEvaluated(schCxt); - } - function validateClause(keyword, ifClause) { - return () => { - const schCxt = cxt.subschema({ keyword }, schValid); - gen.assign(valid, schValid); - cxt.mergeValidEvaluated(schCxt, valid); - if (ifClause) - gen.assign(ifClause, (0, codegen_1._)`${keyword}`); - else - cxt.setParams({ ifClause: keyword }); - }; - } - } - }; - function hasSchema(it, keyword) { - const schema = it.schema[keyword]; - return schema !== undefined && !(0, util_1.alwaysValidSchema)(it, schema); - } - exports.default = def; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/applicator/thenElse.js -var require_thenElse2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var util_1 = require_util2(); - var def = { - keyword: ["then", "else"], - schemaType: ["object", "boolean"], - code({ keyword, parentSchema, it }) { - if (parentSchema.if === undefined) - (0, util_1.checkStrictMode)(it, `"${keyword}" without "if" is ignored`); - } - }; - exports.default = def; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/applicator/index.js -var require_applicator2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var additionalItems_1 = require_additionalItems2(); - var prefixItems_1 = require_prefixItems2(); - var items_1 = require_items2(); - var items2020_1 = require_items20202(); - var contains_1 = require_contains2(); - var dependencies_1 = require_dependencies2(); - var propertyNames_1 = require_propertyNames2(); - var additionalProperties_1 = require_additionalProperties2(); - var properties_1 = require_properties2(); - var patternProperties_1 = require_patternProperties2(); - var not_1 = require_not2(); - var anyOf_1 = require_anyOf2(); - var oneOf_1 = require_oneOf2(); - var allOf_1 = require_allOf2(); - var if_1 = require_if2(); - var thenElse_1 = require_thenElse2(); - function getApplicator(draft2020 = false) { - const applicator = [ - not_1.default, - anyOf_1.default, - oneOf_1.default, - allOf_1.default, - if_1.default, - thenElse_1.default, - propertyNames_1.default, - additionalProperties_1.default, - dependencies_1.default, - properties_1.default, - patternProperties_1.default - ]; - if (draft2020) - applicator.push(prefixItems_1.default, items2020_1.default); - else - applicator.push(additionalItems_1.default, items_1.default); - applicator.push(contains_1.default); - return applicator; - } - exports.default = getApplicator; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/format/format.js -var require_format3 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var codegen_1 = require_codegen2(); - var error2 = { - message: ({ schemaCode }) => (0, codegen_1.str)`must match format "${schemaCode}"`, - params: ({ schemaCode }) => (0, codegen_1._)`{format: ${schemaCode}}` - }; - var def = { - keyword: "format", - type: ["number", "string"], - schemaType: "string", - $data: true, - error: error2, - code(cxt, ruleType) { - const { gen, data, $data, schema, schemaCode, it } = cxt; - const { opts, errSchemaPath, schemaEnv, self } = it; - if (!opts.validateFormats) - return; - if ($data) - validate$DataFormat(); - else - validateFormat(); - function validate$DataFormat() { - const fmts = gen.scopeValue("formats", { - ref: self.formats, - code: opts.code.formats - }); - const fDef = gen.const("fDef", (0, codegen_1._)`${fmts}[${schemaCode}]`); - const fType = gen.let("fType"); - const format = gen.let("format"); - gen.if((0, codegen_1._)`typeof ${fDef} == "object" && !(${fDef} instanceof RegExp)`, () => gen.assign(fType, (0, codegen_1._)`${fDef}.type || "string"`).assign(format, (0, codegen_1._)`${fDef}.validate`), () => gen.assign(fType, (0, codegen_1._)`"string"`).assign(format, fDef)); - cxt.fail$data((0, codegen_1.or)(unknownFmt(), invalidFmt())); - function unknownFmt() { - if (opts.strictSchema === false) - return codegen_1.nil; - return (0, codegen_1._)`${schemaCode} && !${format}`; - } - function invalidFmt() { - const callFormat = schemaEnv.$async ? (0, codegen_1._)`(${fDef}.async ? await ${format}(${data}) : ${format}(${data}))` : (0, codegen_1._)`${format}(${data})`; - const validData = (0, codegen_1._)`(typeof ${format} == "function" ? ${callFormat} : ${format}.test(${data}))`; - return (0, codegen_1._)`${format} && ${format} !== true && ${fType} === ${ruleType} && !${validData}`; - } - } - function validateFormat() { - const formatDef = self.formats[schema]; - if (!formatDef) { - unknownFormat(); - return; - } - if (formatDef === true) - return; - const [fmtType, format, fmtRef] = getFormat(formatDef); - if (fmtType === ruleType) - cxt.pass(validCondition()); - function unknownFormat() { - if (opts.strictSchema === false) { - self.logger.warn(unknownMsg()); - return; - } - throw new Error(unknownMsg()); - function unknownMsg() { - return `unknown format "${schema}" ignored in schema at path "${errSchemaPath}"`; - } - } - function getFormat(fmtDef) { - const code = fmtDef instanceof RegExp ? (0, codegen_1.regexpCode)(fmtDef) : opts.code.formats ? (0, codegen_1._)`${opts.code.formats}${(0, codegen_1.getProperty)(schema)}` : undefined; - const fmt = gen.scopeValue("formats", { key: schema, ref: fmtDef, code }); - if (typeof fmtDef == "object" && !(fmtDef instanceof RegExp)) { - return [fmtDef.type || "string", fmtDef.validate, (0, codegen_1._)`${fmt}.validate`]; - } - return ["string", fmtDef, fmt]; - } - function validCondition() { - if (typeof formatDef == "object" && !(formatDef instanceof RegExp) && formatDef.async) { - if (!schemaEnv.$async) - throw new Error("async format in sync schema"); - return (0, codegen_1._)`await ${fmtRef}(${data})`; - } - return typeof format == "function" ? (0, codegen_1._)`${fmtRef}(${data})` : (0, codegen_1._)`${fmtRef}.test(${data})`; - } - } - } - }; - exports.default = def; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/format/index.js -var require_format4 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var format_1 = require_format3(); - var format = [format_1.default]; - exports.default = format; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/metadata.js -var require_metadata2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.contentVocabulary = exports.metadataVocabulary = undefined; - exports.metadataVocabulary = [ - "title", - "description", - "default", - "deprecated", - "readOnly", - "writeOnly", - "examples" - ]; - exports.contentVocabulary = [ - "contentMediaType", - "contentEncoding", - "contentSchema" - ]; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/draft7.js -var require_draft72 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var core_1 = require_core4(); - var validation_1 = require_validation2(); - var applicator_1 = require_applicator2(); - var format_1 = require_format4(); - var metadata_1 = require_metadata2(); - var draft7Vocabularies = [ - core_1.default, - validation_1.default, - (0, applicator_1.default)(), - format_1.default, - metadata_1.metadataVocabulary, - metadata_1.contentVocabulary - ]; - exports.default = draft7Vocabularies; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/discriminator/types.js -var require_types2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.DiscrError = undefined; - var DiscrError; - (function(DiscrError2) { - DiscrError2["Tag"] = "tag"; - DiscrError2["Mapping"] = "mapping"; - })(DiscrError || (exports.DiscrError = DiscrError = {})); -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/discriminator/index.js -var require_discriminator2 = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var codegen_1 = require_codegen2(); - var types_1 = require_types2(); - var compile_1 = require_compile2(); - var ref_error_1 = require_ref_error2(); - var util_1 = require_util2(); - var error2 = { - message: ({ params: { discrError, tagName } }) => discrError === types_1.DiscrError.Tag ? `tag "${tagName}" must be string` : `value of tag "${tagName}" must be in oneOf`, - params: ({ params: { discrError, tag, tagName } }) => (0, codegen_1._)`{error: ${discrError}, tag: ${tagName}, tagValue: ${tag}}` - }; - var def = { - keyword: "discriminator", - type: "object", - schemaType: "object", - error: error2, - code(cxt) { - const { gen, data, schema, parentSchema, it } = cxt; - const { oneOf } = parentSchema; - if (!it.opts.discriminator) { - throw new Error("discriminator: requires discriminator option"); - } - const tagName = schema.propertyName; - if (typeof tagName != "string") - throw new Error("discriminator: requires propertyName"); - if (schema.mapping) - throw new Error("discriminator: mapping is not supported"); - if (!oneOf) - throw new Error("discriminator: requires oneOf keyword"); - const valid = gen.let("valid", false); - const tag = gen.const("tag", (0, codegen_1._)`${data}${(0, codegen_1.getProperty)(tagName)}`); - gen.if((0, codegen_1._)`typeof ${tag} == "string"`, () => validateMapping(), () => cxt.error(false, { discrError: types_1.DiscrError.Tag, tag, tagName })); - cxt.ok(valid); - function validateMapping() { - const mapping = getMapping(); - gen.if(false); - for (const tagValue in mapping) { - gen.elseIf((0, codegen_1._)`${tag} === ${tagValue}`); - gen.assign(valid, applyTagSchema(mapping[tagValue])); - } - gen.else(); - cxt.error(false, { discrError: types_1.DiscrError.Mapping, tag, tagName }); - gen.endIf(); - } - function applyTagSchema(schemaProp) { - const _valid = gen.name("valid"); - const schCxt = cxt.subschema({ keyword: "oneOf", schemaProp }, _valid); - cxt.mergeEvaluated(schCxt, codegen_1.Name); - return _valid; - } - function getMapping() { - var _a; - const oneOfMapping = {}; - const topRequired = hasRequired(parentSchema); - let tagRequired = true; - for (let i = 0;i < oneOf.length; i++) { - let sch = oneOf[i]; - if ((sch === null || sch === undefined ? undefined : sch.$ref) && !(0, util_1.schemaHasRulesButRef)(sch, it.self.RULES)) { - const ref = sch.$ref; - sch = compile_1.resolveRef.call(it.self, it.schemaEnv.root, it.baseId, ref); - if (sch instanceof compile_1.SchemaEnv) - sch = sch.schema; - if (sch === undefined) - throw new ref_error_1.default(it.opts.uriResolver, it.baseId, ref); - } - const propSch = (_a = sch === null || sch === undefined ? undefined : sch.properties) === null || _a === undefined ? undefined : _a[tagName]; - if (typeof propSch != "object") { - throw new Error(`discriminator: oneOf subschemas (or referenced schemas) must have "properties/${tagName}"`); - } - tagRequired = tagRequired && (topRequired || hasRequired(sch)); - addMappings(propSch, i); - } - if (!tagRequired) - throw new Error(`discriminator: "${tagName}" must be required`); - return oneOfMapping; - function hasRequired({ required: required2 }) { - return Array.isArray(required2) && required2.includes(tagName); - } - function addMappings(sch, i) { - if (sch.const) { - addMapping(sch.const, i); - } else if (sch.enum) { - for (const tagValue of sch.enum) { - addMapping(tagValue, i); - } - } else { - throw new Error(`discriminator: "properties/${tagName}" must have "const" or "enum"`); - } - } - function addMapping(tagValue, i) { - if (typeof tagValue != "string" || tagValue in oneOfMapping) { - throw new Error(`discriminator: "${tagName}" values must be unique strings`); - } - oneOfMapping[tagValue] = i; - } - } - } - }; - exports.default = def; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/refs/json-schema-draft-07.json -var require_json_schema_draft_072 = __commonJS((exports, module) => { - module.exports = { - $schema: "http://json-schema.org/draft-07/schema#", - $id: "http://json-schema.org/draft-07/schema#", - title: "Core schema meta-schema", - definitions: { - schemaArray: { - type: "array", - minItems: 1, - items: { $ref: "#" } - }, - nonNegativeInteger: { - type: "integer", - minimum: 0 - }, - nonNegativeIntegerDefault0: { - allOf: [{ $ref: "#/definitions/nonNegativeInteger" }, { default: 0 }] - }, - simpleTypes: { - enum: ["array", "boolean", "integer", "null", "number", "object", "string"] - }, - stringArray: { - type: "array", - items: { type: "string" }, - uniqueItems: true, - default: [] - } - }, - type: ["object", "boolean"], - properties: { - $id: { - type: "string", - format: "uri-reference" - }, - $schema: { - type: "string", - format: "uri" - }, - $ref: { - type: "string", - format: "uri-reference" - }, - $comment: { - type: "string" - }, - title: { - type: "string" - }, - description: { - type: "string" - }, - default: true, - readOnly: { - type: "boolean", - default: false - }, - examples: { - type: "array", - items: true - }, - multipleOf: { - type: "number", - exclusiveMinimum: 0 - }, - maximum: { - type: "number" - }, - exclusiveMaximum: { - type: "number" - }, - minimum: { - type: "number" - }, - exclusiveMinimum: { - type: "number" - }, - maxLength: { $ref: "#/definitions/nonNegativeInteger" }, - minLength: { $ref: "#/definitions/nonNegativeIntegerDefault0" }, - pattern: { - type: "string", - format: "regex" - }, - additionalItems: { $ref: "#" }, - items: { - anyOf: [{ $ref: "#" }, { $ref: "#/definitions/schemaArray" }], - default: true - }, - maxItems: { $ref: "#/definitions/nonNegativeInteger" }, - minItems: { $ref: "#/definitions/nonNegativeIntegerDefault0" }, - uniqueItems: { - type: "boolean", - default: false - }, - contains: { $ref: "#" }, - maxProperties: { $ref: "#/definitions/nonNegativeInteger" }, - minProperties: { $ref: "#/definitions/nonNegativeIntegerDefault0" }, - required: { $ref: "#/definitions/stringArray" }, - additionalProperties: { $ref: "#" }, - definitions: { - type: "object", - additionalProperties: { $ref: "#" }, - default: {} - }, - properties: { - type: "object", - additionalProperties: { $ref: "#" }, - default: {} - }, - patternProperties: { - type: "object", - additionalProperties: { $ref: "#" }, - propertyNames: { format: "regex" }, - default: {} - }, - dependencies: { - type: "object", - additionalProperties: { - anyOf: [{ $ref: "#" }, { $ref: "#/definitions/stringArray" }] - } - }, - propertyNames: { $ref: "#" }, - const: true, - enum: { - type: "array", - items: true, - minItems: 1, - uniqueItems: true - }, - type: { - anyOf: [ - { $ref: "#/definitions/simpleTypes" }, - { - type: "array", - items: { $ref: "#/definitions/simpleTypes" }, - minItems: 1, - uniqueItems: true - } - ] - }, - format: { type: "string" }, - contentMediaType: { type: "string" }, - contentEncoding: { type: "string" }, - if: { $ref: "#" }, - then: { $ref: "#" }, - else: { $ref: "#" }, - allOf: { $ref: "#/definitions/schemaArray" }, - anyOf: { $ref: "#/definitions/schemaArray" }, - oneOf: { $ref: "#/definitions/schemaArray" }, - not: { $ref: "#" } - }, - default: true - }; -}); - -// node_modules/ajv-formats/node_modules/ajv/dist/ajv.js -var require_ajv2 = __commonJS((exports, module) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.MissingRefError = exports.ValidationError = exports.CodeGen = exports.Name = exports.nil = exports.stringify = exports.str = exports._ = exports.KeywordCxt = exports.Ajv = undefined; - var core_1 = require_core3(); - var draft7_1 = require_draft72(); - var discriminator_1 = require_discriminator2(); - var draft7MetaSchema = require_json_schema_draft_072(); - var META_SUPPORT_DATA = ["/properties"]; - var META_SCHEMA_ID = "http://json-schema.org/draft-07/schema"; - - class Ajv extends core_1.default { - _addVocabularies() { - super._addVocabularies(); - draft7_1.default.forEach((v) => this.addVocabulary(v)); - if (this.opts.discriminator) - this.addKeyword(discriminator_1.default); - } - _addDefaultMetaSchema() { - super._addDefaultMetaSchema(); - if (!this.opts.meta) - return; - const metaSchema = this.opts.$data ? this.$dataMetaSchema(draft7MetaSchema, META_SUPPORT_DATA) : draft7MetaSchema; - this.addMetaSchema(metaSchema, META_SCHEMA_ID, false); - this.refs["http://json-schema.org/schema"] = META_SCHEMA_ID; - } - defaultMeta() { - return this.opts.defaultMeta = super.defaultMeta() || (this.getSchema(META_SCHEMA_ID) ? META_SCHEMA_ID : undefined); - } - } - exports.Ajv = Ajv; - module.exports = exports = Ajv; - module.exports.Ajv = Ajv; - Object.defineProperty(exports, "__esModule", { value: true }); - exports.default = Ajv; - var validate_1 = require_validate2(); - Object.defineProperty(exports, "KeywordCxt", { enumerable: true, get: function() { - return validate_1.KeywordCxt; - } }); - var codegen_1 = require_codegen2(); - Object.defineProperty(exports, "_", { enumerable: true, get: function() { - return codegen_1._; - } }); - Object.defineProperty(exports, "str", { enumerable: true, get: function() { - return codegen_1.str; - } }); - Object.defineProperty(exports, "stringify", { enumerable: true, get: function() { - return codegen_1.stringify; - } }); - Object.defineProperty(exports, "nil", { enumerable: true, get: function() { - return codegen_1.nil; - } }); - Object.defineProperty(exports, "Name", { enumerable: true, get: function() { - return codegen_1.Name; - } }); - Object.defineProperty(exports, "CodeGen", { enumerable: true, get: function() { - return codegen_1.CodeGen; - } }); - var validation_error_1 = require_validation_error2(); - Object.defineProperty(exports, "ValidationError", { enumerable: true, get: function() { - return validation_error_1.default; - } }); - var ref_error_1 = require_ref_error2(); - Object.defineProperty(exports, "MissingRefError", { enumerable: true, get: function() { - return ref_error_1.default; - } }); -}); - -// node_modules/ajv-formats/dist/limit.js -var require_limit = __commonJS((exports) => { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.formatLimitDefinition = undefined; - var ajv_1 = require_ajv2(); - var codegen_1 = require_codegen2(); - var ops = codegen_1.operators; - var KWDs = { - formatMaximum: { okStr: "<=", ok: ops.LTE, fail: ops.GT }, - formatMinimum: { okStr: ">=", ok: ops.GTE, fail: ops.LT }, - formatExclusiveMaximum: { okStr: "<", ok: ops.LT, fail: ops.GTE }, - formatExclusiveMinimum: { okStr: ">", ok: ops.GT, fail: ops.LTE } - }; - var error2 = { - message: ({ keyword, schemaCode }) => (0, codegen_1.str)`should be ${KWDs[keyword].okStr} ${schemaCode}`, - params: ({ keyword, schemaCode }) => (0, codegen_1._)`{comparison: ${KWDs[keyword].okStr}, limit: ${schemaCode}}` - }; - exports.formatLimitDefinition = { - keyword: Object.keys(KWDs), - type: "string", - schemaType: "string", - $data: true, - error: error2, - code(cxt) { - const { gen, data, schemaCode, keyword, it } = cxt; - const { opts, self } = it; - if (!opts.validateFormats) - return; - const fCxt = new ajv_1.KeywordCxt(it, self.RULES.all.format.definition, "format"); - if (fCxt.$data) - validate$DataFormat(); - else - validateFormat(); - function validate$DataFormat() { - const fmts = gen.scopeValue("formats", { - ref: self.formats, - code: opts.code.formats - }); - const fmt = gen.const("fmt", (0, codegen_1._)`${fmts}[${fCxt.schemaCode}]`); - cxt.fail$data((0, codegen_1.or)((0, codegen_1._)`typeof ${fmt} != "object"`, (0, codegen_1._)`${fmt} instanceof RegExp`, (0, codegen_1._)`typeof ${fmt}.compare != "function"`, compareCode(fmt))); - } - function validateFormat() { - const format = fCxt.schema; - const fmtDef = self.formats[format]; - if (!fmtDef || fmtDef === true) - return; - if (typeof fmtDef != "object" || fmtDef instanceof RegExp || typeof fmtDef.compare != "function") { - throw new Error(`"${keyword}": format "${format}" does not define "compare" function`); - } - const fmt = gen.scopeValue("formats", { - key: format, - ref: fmtDef, - code: opts.code.formats ? (0, codegen_1._)`${opts.code.formats}${(0, codegen_1.getProperty)(format)}` : undefined - }); - cxt.fail$data(compareCode(fmt)); - } - function compareCode(fmt) { - return (0, codegen_1._)`${fmt}.compare(${data}, ${schemaCode}) ${KWDs[keyword].fail} 0`; - } - }, - dependencies: ["format"] - }; - var formatLimitPlugin = (ajv) => { - ajv.addKeyword(exports.formatLimitDefinition); - return ajv; - }; - exports.default = formatLimitPlugin; -}); - -// node_modules/ajv-formats/dist/index.js -var require_dist = __commonJS((exports, module) => { - Object.defineProperty(exports, "__esModule", { value: true }); - var formats_1 = require_formats(); - var limit_1 = require_limit(); - var codegen_1 = require_codegen2(); - var fullName = new codegen_1.Name("fullFormats"); - var fastName = new codegen_1.Name("fastFormats"); - var formatsPlugin = (ajv, opts = { keywords: true }) => { - if (Array.isArray(opts)) { - addFormats(ajv, opts, formats_1.fullFormats, fullName); - return ajv; - } - const [formats, exportName] = opts.mode === "fast" ? [formats_1.fastFormats, fastName] : [formats_1.fullFormats, fullName]; - const list = opts.formats || formats_1.formatNames; - addFormats(ajv, list, formats, exportName); - if (opts.keywords) - (0, limit_1.default)(ajv); - return ajv; - }; - formatsPlugin.get = (name, mode = "full") => { - const formats = mode === "fast" ? formats_1.fastFormats : formats_1.fullFormats; - const f = formats[name]; - if (!f) - throw new Error(`Unknown format "${name}"`); - return f; - }; - function addFormats(ajv, list, fs, exportName) { - var _a; - var _b; - (_a = (_b = ajv.opts.code).formats) !== null && _a !== undefined || (_b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`); - for (const f of list) - ajv.addFormat(f, fs[f]); - } - module.exports = exports = formatsPlugin; - Object.defineProperty(exports, "__esModule", { value: true }); - exports.default = formatsPlugin; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/zod/v4/core/core.js -var NEVER = Object.freeze({ - status: "aborted" -}); -function $constructor(name, initializer, params) { - function init(inst, def) { - var _a; - Object.defineProperty(inst, "_zod", { - value: inst._zod ?? {}, - enumerable: false - }); - (_a = inst._zod).traits ?? (_a.traits = new Set); - inst._zod.traits.add(name); - initializer(inst, def); - for (const k in _.prototype) { - if (!(k in inst)) - Object.defineProperty(inst, k, { value: _.prototype[k].bind(inst) }); - } - inst._zod.constr = _; - inst._zod.def = def; - } - const Parent = params?.Parent ?? Object; - - class Definition extends Parent { - } - Object.defineProperty(Definition, "name", { value: name }); - function _(def) { - var _a; - const inst = params?.Parent ? new Definition : this; - init(inst, def); - (_a = inst._zod).deferred ?? (_a.deferred = []); - for (const fn of inst._zod.deferred) { - fn(); - } - return inst; - } - Object.defineProperty(_, "init", { value: init }); - Object.defineProperty(_, Symbol.hasInstance, { - value: (inst) => { - if (params?.Parent && inst instanceof params.Parent) - return true; - return inst?._zod?.traits?.has(name); - } - }); - Object.defineProperty(_, "name", { value: name }); - return _; -} -var $brand = Symbol("zod_brand"); - -class $ZodAsyncError extends Error { - constructor() { - super(`Encountered Promise during synchronous parse. Use .parseAsync() instead.`); - } -} -var globalConfig = {}; -function config(newConfig) { - if (newConfig) - Object.assign(globalConfig, newConfig); - return globalConfig; -} -// node_modules/@modelcontextprotocol/sdk/node_modules/zod/v4/core/util.js -var exports_util = {}; -__export(exports_util, { - unwrapMessage: () => unwrapMessage, - stringifyPrimitive: () => stringifyPrimitive, - required: () => required, - randomString: () => randomString, - propertyKeyTypes: () => propertyKeyTypes, - promiseAllObject: () => promiseAllObject, - primitiveTypes: () => primitiveTypes, - prefixIssues: () => prefixIssues, - pick: () => pick, - partial: () => partial, - optionalKeys: () => optionalKeys, - omit: () => omit, - numKeys: () => numKeys, - nullish: () => nullish, - normalizeParams: () => normalizeParams, - merge: () => merge, - jsonStringifyReplacer: () => jsonStringifyReplacer, - joinValues: () => joinValues, - issue: () => issue, - isPlainObject: () => isPlainObject, - isObject: () => isObject, - getSizableOrigin: () => getSizableOrigin, - getParsedType: () => getParsedType, - getLengthableOrigin: () => getLengthableOrigin, - getEnumValues: () => getEnumValues, - getElementAtPath: () => getElementAtPath, - floatSafeRemainder: () => floatSafeRemainder, - finalizeIssue: () => finalizeIssue, - extend: () => extend, - escapeRegex: () => escapeRegex, - esc: () => esc, - defineLazy: () => defineLazy, - createTransparentProxy: () => createTransparentProxy, - clone: () => clone, - cleanRegex: () => cleanRegex, - cleanEnum: () => cleanEnum, - captureStackTrace: () => captureStackTrace, - cached: () => cached, - assignProp: () => assignProp, - assertNotEqual: () => assertNotEqual, - assertNever: () => assertNever, - assertIs: () => assertIs, - assertEqual: () => assertEqual, - assert: () => assert, - allowsEval: () => allowsEval, - aborted: () => aborted, - NUMBER_FORMAT_RANGES: () => NUMBER_FORMAT_RANGES, - Class: () => Class, - BIGINT_FORMAT_RANGES: () => BIGINT_FORMAT_RANGES -}); -function assertEqual(val) { - return val; -} -function assertNotEqual(val) { - return val; -} -function assertIs(_arg) {} -function assertNever(_x) { - throw new Error; -} -function assert(_) {} -function getEnumValues(entries) { - const numericValues = Object.values(entries).filter((v) => typeof v === "number"); - const values = Object.entries(entries).filter(([k, _]) => numericValues.indexOf(+k) === -1).map(([_, v]) => v); - return values; -} -function joinValues(array, separator = "|") { - return array.map((val) => stringifyPrimitive(val)).join(separator); -} -function jsonStringifyReplacer(_, value) { - if (typeof value === "bigint") - return value.toString(); - return value; -} -function cached(getter) { - const set = false; - return { - get value() { - if (!set) { - const value = getter(); - Object.defineProperty(this, "value", { value }); - return value; - } - throw new Error("cached value already set"); - } - }; -} -function nullish(input) { - return input === null || input === undefined; -} -function cleanRegex(source) { - const start = source.startsWith("^") ? 1 : 0; - const end = source.endsWith("$") ? source.length - 1 : source.length; - return source.slice(start, end); -} -function floatSafeRemainder(val, step) { - const valDecCount = (val.toString().split(".")[1] || "").length; - const stepDecCount = (step.toString().split(".")[1] || "").length; - const decCount = valDecCount > stepDecCount ? valDecCount : stepDecCount; - const valInt = Number.parseInt(val.toFixed(decCount).replace(".", "")); - const stepInt = Number.parseInt(step.toFixed(decCount).replace(".", "")); - return valInt % stepInt / 10 ** decCount; -} -function defineLazy(object, key, getter) { - const set = false; - Object.defineProperty(object, key, { - get() { - if (!set) { - const value = getter(); - object[key] = value; - return value; - } - throw new Error("cached value already set"); - }, - set(v) { - Object.defineProperty(object, key, { - value: v - }); - }, - configurable: true - }); -} -function assignProp(target, prop, value) { - Object.defineProperty(target, prop, { - value, - writable: true, - enumerable: true, - configurable: true - }); -} -function getElementAtPath(obj, path) { - if (!path) - return obj; - return path.reduce((acc, key) => acc?.[key], obj); -} -function promiseAllObject(promisesObj) { - const keys = Object.keys(promisesObj); - const promises = keys.map((key) => promisesObj[key]); - return Promise.all(promises).then((results) => { - const resolvedObj = {}; - for (let i = 0;i < keys.length; i++) { - resolvedObj[keys[i]] = results[i]; - } - return resolvedObj; - }); -} -function randomString(length = 10) { - const chars = "abcdefghijklmnopqrstuvwxyz"; - let str = ""; - for (let i = 0;i < length; i++) { - str += chars[Math.floor(Math.random() * chars.length)]; - } - return str; -} -function esc(str) { - return JSON.stringify(str); -} -var captureStackTrace = Error.captureStackTrace ? Error.captureStackTrace : (..._args) => {}; -function isObject(data) { - return typeof data === "object" && data !== null && !Array.isArray(data); -} -var allowsEval = cached(() => { - if (typeof navigator !== "undefined" && navigator?.userAgent?.includes("Cloudflare")) { - return false; - } - try { - const F = Function; - new F(""); - return true; - } catch (_) { - return false; - } -}); -function isPlainObject(o) { - if (isObject(o) === false) - return false; - const ctor = o.constructor; - if (ctor === undefined) - return true; - const prot = ctor.prototype; - if (isObject(prot) === false) - return false; - if (Object.prototype.hasOwnProperty.call(prot, "isPrototypeOf") === false) { - return false; - } - return true; -} -function numKeys(data) { - let keyCount = 0; - for (const key in data) { - if (Object.prototype.hasOwnProperty.call(data, key)) { - keyCount++; - } - } - return keyCount; -} -var getParsedType = (data) => { - const t = typeof data; - switch (t) { - case "undefined": - return "undefined"; - case "string": - return "string"; - case "number": - return Number.isNaN(data) ? "nan" : "number"; - case "boolean": - return "boolean"; - case "function": - return "function"; - case "bigint": - return "bigint"; - case "symbol": - return "symbol"; - case "object": - if (Array.isArray(data)) { - return "array"; - } - if (data === null) { - return "null"; - } - if (data.then && typeof data.then === "function" && data.catch && typeof data.catch === "function") { - return "promise"; - } - if (typeof Map !== "undefined" && data instanceof Map) { - return "map"; - } - if (typeof Set !== "undefined" && data instanceof Set) { - return "set"; - } - if (typeof Date !== "undefined" && data instanceof Date) { - return "date"; - } - if (typeof File !== "undefined" && data instanceof File) { - return "file"; - } - return "object"; - default: - throw new Error(`Unknown data type: ${t}`); - } -}; -var propertyKeyTypes = new Set(["string", "number", "symbol"]); -var primitiveTypes = new Set(["string", "number", "bigint", "boolean", "symbol", "undefined"]); -function escapeRegex(str) { - return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} -function clone(inst, def, params) { - const cl = new inst._zod.constr(def ?? inst._zod.def); - if (!def || params?.parent) - cl._zod.parent = inst; - return cl; -} -function normalizeParams(_params) { - const params = _params; - if (!params) - return {}; - if (typeof params === "string") - return { error: () => params }; - if (params?.message !== undefined) { - if (params?.error !== undefined) - throw new Error("Cannot specify both `message` and `error` params"); - params.error = params.message; - } - delete params.message; - if (typeof params.error === "string") - return { ...params, error: () => params.error }; - return params; -} -function createTransparentProxy(getter) { - let target; - return new Proxy({}, { - get(_, prop, receiver) { - target ?? (target = getter()); - return Reflect.get(target, prop, receiver); - }, - set(_, prop, value, receiver) { - target ?? (target = getter()); - return Reflect.set(target, prop, value, receiver); - }, - has(_, prop) { - target ?? (target = getter()); - return Reflect.has(target, prop); - }, - deleteProperty(_, prop) { - target ?? (target = getter()); - return Reflect.deleteProperty(target, prop); - }, - ownKeys(_) { - target ?? (target = getter()); - return Reflect.ownKeys(target); - }, - getOwnPropertyDescriptor(_, prop) { - target ?? (target = getter()); - return Reflect.getOwnPropertyDescriptor(target, prop); - }, - defineProperty(_, prop, descriptor) { - target ?? (target = getter()); - return Reflect.defineProperty(target, prop, descriptor); - } - }); -} -function stringifyPrimitive(value) { - if (typeof value === "bigint") - return value.toString() + "n"; - if (typeof value === "string") - return `"${value}"`; - return `${value}`; -} -function optionalKeys(shape) { - return Object.keys(shape).filter((k) => { - return shape[k]._zod.optin === "optional" && shape[k]._zod.optout === "optional"; - }); -} -var NUMBER_FORMAT_RANGES = { - safeint: [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER], - int32: [-2147483648, 2147483647], - uint32: [0, 4294967295], - float32: [-340282346638528860000000000000000000000, 340282346638528860000000000000000000000], - float64: [-Number.MAX_VALUE, Number.MAX_VALUE] -}; -var BIGINT_FORMAT_RANGES = { - int64: [/* @__PURE__ */ BigInt("-9223372036854775808"), /* @__PURE__ */ BigInt("9223372036854775807")], - uint64: [/* @__PURE__ */ BigInt(0), /* @__PURE__ */ BigInt("18446744073709551615")] -}; -function pick(schema, mask) { - const newShape = {}; - const currDef = schema._zod.def; - for (const key in mask) { - if (!(key in currDef.shape)) { - throw new Error(`Unrecognized key: "${key}"`); - } - if (!mask[key]) - continue; - newShape[key] = currDef.shape[key]; - } - return clone(schema, { - ...schema._zod.def, - shape: newShape, - checks: [] - }); -} -function omit(schema, mask) { - const newShape = { ...schema._zod.def.shape }; - const currDef = schema._zod.def; - for (const key in mask) { - if (!(key in currDef.shape)) { - throw new Error(`Unrecognized key: "${key}"`); - } - if (!mask[key]) - continue; - delete newShape[key]; - } - return clone(schema, { - ...schema._zod.def, - shape: newShape, - checks: [] - }); -} -function extend(schema, shape) { - if (!isPlainObject(shape)) { - throw new Error("Invalid input to extend: expected a plain object"); - } - const def = { - ...schema._zod.def, - get shape() { - const _shape = { ...schema._zod.def.shape, ...shape }; - assignProp(this, "shape", _shape); - return _shape; - }, - checks: [] - }; - return clone(schema, def); -} -function merge(a, b) { - return clone(a, { - ...a._zod.def, - get shape() { - const _shape = { ...a._zod.def.shape, ...b._zod.def.shape }; - assignProp(this, "shape", _shape); - return _shape; - }, - catchall: b._zod.def.catchall, - checks: [] - }); -} -function partial(Class, schema, mask) { - const oldShape = schema._zod.def.shape; - const shape = { ...oldShape }; - if (mask) { - for (const key in mask) { - if (!(key in oldShape)) { - throw new Error(`Unrecognized key: "${key}"`); - } - if (!mask[key]) - continue; - shape[key] = Class ? new Class({ - type: "optional", - innerType: oldShape[key] - }) : oldShape[key]; - } - } else { - for (const key in oldShape) { - shape[key] = Class ? new Class({ - type: "optional", - innerType: oldShape[key] - }) : oldShape[key]; - } - } - return clone(schema, { - ...schema._zod.def, - shape, - checks: [] - }); -} -function required(Class, schema, mask) { - const oldShape = schema._zod.def.shape; - const shape = { ...oldShape }; - if (mask) { - for (const key in mask) { - if (!(key in shape)) { - throw new Error(`Unrecognized key: "${key}"`); - } - if (!mask[key]) - continue; - shape[key] = new Class({ - type: "nonoptional", - innerType: oldShape[key] - }); - } - } else { - for (const key in oldShape) { - shape[key] = new Class({ - type: "nonoptional", - innerType: oldShape[key] - }); - } - } - return clone(schema, { - ...schema._zod.def, - shape, - checks: [] - }); -} -function aborted(x, startIndex = 0) { - for (let i = startIndex;i < x.issues.length; i++) { - if (x.issues[i]?.continue !== true) - return true; - } - return false; -} -function prefixIssues(path, issues) { - return issues.map((iss) => { - var _a; - (_a = iss).path ?? (_a.path = []); - iss.path.unshift(path); - return iss; - }); -} -function unwrapMessage(message) { - return typeof message === "string" ? message : message?.message; -} -function finalizeIssue(iss, ctx, config2) { - const full = { ...iss, path: iss.path ?? [] }; - if (!iss.message) { - const message = unwrapMessage(iss.inst?._zod.def?.error?.(iss)) ?? unwrapMessage(ctx?.error?.(iss)) ?? unwrapMessage(config2.customError?.(iss)) ?? unwrapMessage(config2.localeError?.(iss)) ?? "Invalid input"; - full.message = message; - } - delete full.inst; - delete full.continue; - if (!ctx?.reportInput) { - delete full.input; - } - return full; -} -function getSizableOrigin(input) { - if (input instanceof Set) - return "set"; - if (input instanceof Map) - return "map"; - if (input instanceof File) - return "file"; - return "unknown"; -} -function getLengthableOrigin(input) { - if (Array.isArray(input)) - return "array"; - if (typeof input === "string") - return "string"; - return "unknown"; -} -function issue(...args) { - const [iss, input, inst] = args; - if (typeof iss === "string") { - return { - message: iss, - code: "custom", - input, - inst - }; - } - return { ...iss }; -} -function cleanEnum(obj) { - return Object.entries(obj).filter(([k, _]) => { - return Number.isNaN(Number.parseInt(k, 10)); - }).map((el) => el[1]); -} - -class Class { - constructor(..._args) {} -} - -// node_modules/@modelcontextprotocol/sdk/node_modules/zod/v4/core/errors.js -var initializer = (inst, def) => { - inst.name = "$ZodError"; - Object.defineProperty(inst, "_zod", { - value: inst._zod, - enumerable: false - }); - Object.defineProperty(inst, "issues", { - value: def, - enumerable: false - }); - Object.defineProperty(inst, "message", { - get() { - return JSON.stringify(def, jsonStringifyReplacer, 2); - }, - enumerable: true - }); - Object.defineProperty(inst, "toString", { - value: () => inst.message, - enumerable: false - }); -}; -var $ZodError = $constructor("$ZodError", initializer); -var $ZodRealError = $constructor("$ZodError", initializer, { Parent: Error }); -function flattenError(error, mapper = (issue2) => issue2.message) { - const fieldErrors = {}; - const formErrors = []; - for (const sub of error.issues) { - if (sub.path.length > 0) { - fieldErrors[sub.path[0]] = fieldErrors[sub.path[0]] || []; - fieldErrors[sub.path[0]].push(mapper(sub)); - } else { - formErrors.push(mapper(sub)); - } - } - return { formErrors, fieldErrors }; -} -function formatError(error, _mapper) { - const mapper = _mapper || function(issue2) { - return issue2.message; - }; - const fieldErrors = { _errors: [] }; - const processError = (error2) => { - for (const issue2 of error2.issues) { - if (issue2.code === "invalid_union" && issue2.errors.length) { - issue2.errors.map((issues) => processError({ issues })); - } else if (issue2.code === "invalid_key") { - processError({ issues: issue2.issues }); - } else if (issue2.code === "invalid_element") { - processError({ issues: issue2.issues }); - } else if (issue2.path.length === 0) { - fieldErrors._errors.push(mapper(issue2)); - } else { - let curr = fieldErrors; - let i = 0; - while (i < issue2.path.length) { - const el = issue2.path[i]; - const terminal = i === issue2.path.length - 1; - if (!terminal) { - curr[el] = curr[el] || { _errors: [] }; - } else { - curr[el] = curr[el] || { _errors: [] }; - curr[el]._errors.push(mapper(issue2)); - } - curr = curr[el]; - i++; - } - } - } - }; - processError(error); - return fieldErrors; -} - -// node_modules/@modelcontextprotocol/sdk/node_modules/zod/v4/core/parse.js -var _parse = (_Err) => (schema, value, _ctx, _params) => { - const ctx = _ctx ? Object.assign(_ctx, { async: false }) : { async: false }; - const result = schema._zod.run({ value, issues: [] }, ctx); - if (result instanceof Promise) { - throw new $ZodAsyncError; - } - if (result.issues.length) { - const e = new (_params?.Err ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config()))); - captureStackTrace(e, _params?.callee); - throw e; - } - return result.value; -}; -var _parseAsync = (_Err) => async (schema, value, _ctx, params) => { - const ctx = _ctx ? Object.assign(_ctx, { async: true }) : { async: true }; - let result = schema._zod.run({ value, issues: [] }, ctx); - if (result instanceof Promise) - result = await result; - if (result.issues.length) { - const e = new (params?.Err ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config()))); - captureStackTrace(e, params?.callee); - throw e; - } - return result.value; -}; -var _safeParse = (_Err) => (schema, value, _ctx) => { - const ctx = _ctx ? { ..._ctx, async: false } : { async: false }; - const result = schema._zod.run({ value, issues: [] }, ctx); - if (result instanceof Promise) { - throw new $ZodAsyncError; - } - return result.issues.length ? { - success: false, - error: new (_Err ?? $ZodError)(result.issues.map((iss) => finalizeIssue(iss, ctx, config()))) - } : { success: true, data: result.value }; -}; -var safeParse = /* @__PURE__ */ _safeParse($ZodRealError); -var _safeParseAsync = (_Err) => async (schema, value, _ctx) => { - const ctx = _ctx ? Object.assign(_ctx, { async: true }) : { async: true }; - let result = schema._zod.run({ value, issues: [] }, ctx); - if (result instanceof Promise) - result = await result; - return result.issues.length ? { - success: false, - error: new _Err(result.issues.map((iss) => finalizeIssue(iss, ctx, config()))) - } : { success: true, data: result.value }; -}; -var safeParseAsync = /* @__PURE__ */ _safeParseAsync($ZodRealError); -// node_modules/@modelcontextprotocol/sdk/node_modules/zod/v4/core/regexes.js -var cuid = /^[cC][^\s-]{8,}$/; -var cuid2 = /^[0-9a-z]+$/; -var ulid = /^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$/; -var xid = /^[0-9a-vA-V]{20}$/; -var ksuid = /^[A-Za-z0-9]{27}$/; -var nanoid = /^[a-zA-Z0-9_-]{21}$/; -var duration = /^P(?:(\d+W)|(?!.*W)(?=\d|T\d)(\d+Y)?(\d+M)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+([.,]\d+)?S)?)?)$/; -var guid = /^([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})$/; -var uuid = (version) => { - if (!version) - return /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$/; - return new RegExp(`^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-${version}[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$`); -}; -var email = /^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$/; -var _emoji = `^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$`; -function emoji() { - return new RegExp(_emoji, "u"); -} -var ipv4 = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/; -var ipv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})$/; -var cidrv4 = /^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/([0-9]|[1-2][0-9]|3[0-2])$/; -var cidrv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/; -var base64 = /^$|^(?:[0-9a-zA-Z+/]{4})*(?:(?:[0-9a-zA-Z+/]{2}==)|(?:[0-9a-zA-Z+/]{3}=))?$/; -var base64url = /^[A-Za-z0-9_-]*$/; -var hostname = /^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+$/; -var e164 = /^\+(?:[0-9]){6,14}[0-9]$/; -var dateSource = `(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))`; -var date = /* @__PURE__ */ new RegExp(`^${dateSource}$`); -function timeSource(args) { - const hhmm = `(?:[01]\\d|2[0-3]):[0-5]\\d`; - const regex = typeof args.precision === "number" ? args.precision === -1 ? `${hhmm}` : args.precision === 0 ? `${hhmm}:[0-5]\\d` : `${hhmm}:[0-5]\\d\\.\\d{${args.precision}}` : `${hhmm}(?::[0-5]\\d(?:\\.\\d+)?)?`; - return regex; -} -function time(args) { - return new RegExp(`^${timeSource(args)}$`); -} -function datetime(args) { - const time2 = timeSource({ precision: args.precision }); - const opts = ["Z"]; - if (args.local) - opts.push(""); - if (args.offset) - opts.push(`([+-]\\d{2}:\\d{2})`); - const timeRegex = `${time2}(?:${opts.join("|")})`; - return new RegExp(`^${dateSource}T(?:${timeRegex})$`); -} -var string = (params) => { - const regex = params ? `[\\s\\S]{${params?.minimum ?? 0},${params?.maximum ?? ""}}` : `[\\s\\S]*`; - return new RegExp(`^${regex}$`); -}; -var integer = /^\d+$/; -var number = /^-?\d+(?:\.\d+)?/i; -var boolean = /true|false/i; -var _null = /null/i; -var lowercase = /^[^A-Z]*$/; -var uppercase = /^[^a-z]*$/; - -// node_modules/@modelcontextprotocol/sdk/node_modules/zod/v4/core/checks.js -var $ZodCheck = /* @__PURE__ */ $constructor("$ZodCheck", (inst, def) => { - var _a; - inst._zod ?? (inst._zod = {}); - inst._zod.def = def; - (_a = inst._zod).onattach ?? (_a.onattach = []); -}); -var numericOriginMap = { - number: "number", - bigint: "bigint", - object: "date" -}; -var $ZodCheckLessThan = /* @__PURE__ */ $constructor("$ZodCheckLessThan", (inst, def) => { - $ZodCheck.init(inst, def); - const origin = numericOriginMap[typeof def.value]; - inst._zod.onattach.push((inst2) => { - const bag = inst2._zod.bag; - const curr = (def.inclusive ? bag.maximum : bag.exclusiveMaximum) ?? Number.POSITIVE_INFINITY; - if (def.value < curr) { - if (def.inclusive) - bag.maximum = def.value; - else - bag.exclusiveMaximum = def.value; - } - }); - inst._zod.check = (payload) => { - if (def.inclusive ? payload.value <= def.value : payload.value < def.value) { - return; - } - payload.issues.push({ - origin, - code: "too_big", - maximum: def.value, - input: payload.value, - inclusive: def.inclusive, - inst, - continue: !def.abort - }); - }; -}); -var $ZodCheckGreaterThan = /* @__PURE__ */ $constructor("$ZodCheckGreaterThan", (inst, def) => { - $ZodCheck.init(inst, def); - const origin = numericOriginMap[typeof def.value]; - inst._zod.onattach.push((inst2) => { - const bag = inst2._zod.bag; - const curr = (def.inclusive ? bag.minimum : bag.exclusiveMinimum) ?? Number.NEGATIVE_INFINITY; - if (def.value > curr) { - if (def.inclusive) - bag.minimum = def.value; - else - bag.exclusiveMinimum = def.value; - } - }); - inst._zod.check = (payload) => { - if (def.inclusive ? payload.value >= def.value : payload.value > def.value) { - return; - } - payload.issues.push({ - origin, - code: "too_small", - minimum: def.value, - input: payload.value, - inclusive: def.inclusive, - inst, - continue: !def.abort - }); - }; -}); -var $ZodCheckMultipleOf = /* @__PURE__ */ $constructor("$ZodCheckMultipleOf", (inst, def) => { - $ZodCheck.init(inst, def); - inst._zod.onattach.push((inst2) => { - var _a; - (_a = inst2._zod.bag).multipleOf ?? (_a.multipleOf = def.value); - }); - inst._zod.check = (payload) => { - if (typeof payload.value !== typeof def.value) - throw new Error("Cannot mix number and bigint in multiple_of check."); - const isMultiple = typeof payload.value === "bigint" ? payload.value % def.value === BigInt(0) : floatSafeRemainder(payload.value, def.value) === 0; - if (isMultiple) - return; - payload.issues.push({ - origin: typeof payload.value, - code: "not_multiple_of", - divisor: def.value, - input: payload.value, - inst, - continue: !def.abort - }); - }; -}); -var $ZodCheckNumberFormat = /* @__PURE__ */ $constructor("$ZodCheckNumberFormat", (inst, def) => { - $ZodCheck.init(inst, def); - def.format = def.format || "float64"; - const isInt = def.format?.includes("int"); - const origin = isInt ? "int" : "number"; - const [minimum, maximum] = NUMBER_FORMAT_RANGES[def.format]; - inst._zod.onattach.push((inst2) => { - const bag = inst2._zod.bag; - bag.format = def.format; - bag.minimum = minimum; - bag.maximum = maximum; - if (isInt) - bag.pattern = integer; - }); - inst._zod.check = (payload) => { - const input = payload.value; - if (isInt) { - if (!Number.isInteger(input)) { - payload.issues.push({ - expected: origin, - format: def.format, - code: "invalid_type", - input, - inst - }); - return; - } - if (!Number.isSafeInteger(input)) { - if (input > 0) { - payload.issues.push({ - input, - code: "too_big", - maximum: Number.MAX_SAFE_INTEGER, - note: "Integers must be within the safe integer range.", - inst, - origin, - continue: !def.abort - }); - } else { - payload.issues.push({ - input, - code: "too_small", - minimum: Number.MIN_SAFE_INTEGER, - note: "Integers must be within the safe integer range.", - inst, - origin, - continue: !def.abort - }); - } - return; - } - } - if (input < minimum) { - payload.issues.push({ - origin: "number", - input, - code: "too_small", - minimum, - inclusive: true, - inst, - continue: !def.abort - }); - } - if (input > maximum) { - payload.issues.push({ - origin: "number", - input, - code: "too_big", - maximum, - inst - }); - } - }; -}); -var $ZodCheckMaxLength = /* @__PURE__ */ $constructor("$ZodCheckMaxLength", (inst, def) => { - var _a; - $ZodCheck.init(inst, def); - (_a = inst._zod.def).when ?? (_a.when = (payload) => { - const val = payload.value; - return !nullish(val) && val.length !== undefined; - }); - inst._zod.onattach.push((inst2) => { - const curr = inst2._zod.bag.maximum ?? Number.POSITIVE_INFINITY; - if (def.maximum < curr) - inst2._zod.bag.maximum = def.maximum; - }); - inst._zod.check = (payload) => { - const input = payload.value; - const length = input.length; - if (length <= def.maximum) - return; - const origin = getLengthableOrigin(input); - payload.issues.push({ - origin, - code: "too_big", - maximum: def.maximum, - inclusive: true, - input, - inst, - continue: !def.abort - }); - }; -}); -var $ZodCheckMinLength = /* @__PURE__ */ $constructor("$ZodCheckMinLength", (inst, def) => { - var _a; - $ZodCheck.init(inst, def); - (_a = inst._zod.def).when ?? (_a.when = (payload) => { - const val = payload.value; - return !nullish(val) && val.length !== undefined; - }); - inst._zod.onattach.push((inst2) => { - const curr = inst2._zod.bag.minimum ?? Number.NEGATIVE_INFINITY; - if (def.minimum > curr) - inst2._zod.bag.minimum = def.minimum; - }); - inst._zod.check = (payload) => { - const input = payload.value; - const length = input.length; - if (length >= def.minimum) - return; - const origin = getLengthableOrigin(input); - payload.issues.push({ - origin, - code: "too_small", - minimum: def.minimum, - inclusive: true, - input, - inst, - continue: !def.abort - }); - }; -}); -var $ZodCheckLengthEquals = /* @__PURE__ */ $constructor("$ZodCheckLengthEquals", (inst, def) => { - var _a; - $ZodCheck.init(inst, def); - (_a = inst._zod.def).when ?? (_a.when = (payload) => { - const val = payload.value; - return !nullish(val) && val.length !== undefined; - }); - inst._zod.onattach.push((inst2) => { - const bag = inst2._zod.bag; - bag.minimum = def.length; - bag.maximum = def.length; - bag.length = def.length; - }); - inst._zod.check = (payload) => { - const input = payload.value; - const length = input.length; - if (length === def.length) - return; - const origin = getLengthableOrigin(input); - const tooBig = length > def.length; - payload.issues.push({ - origin, - ...tooBig ? { code: "too_big", maximum: def.length } : { code: "too_small", minimum: def.length }, - inclusive: true, - exact: true, - input: payload.value, - inst, - continue: !def.abort - }); - }; -}); -var $ZodCheckStringFormat = /* @__PURE__ */ $constructor("$ZodCheckStringFormat", (inst, def) => { - var _a, _b; - $ZodCheck.init(inst, def); - inst._zod.onattach.push((inst2) => { - const bag = inst2._zod.bag; - bag.format = def.format; - if (def.pattern) { - bag.patterns ?? (bag.patterns = new Set); - bag.patterns.add(def.pattern); - } - }); - if (def.pattern) - (_a = inst._zod).check ?? (_a.check = (payload) => { - def.pattern.lastIndex = 0; - if (def.pattern.test(payload.value)) - return; - payload.issues.push({ - origin: "string", - code: "invalid_format", - format: def.format, - input: payload.value, - ...def.pattern ? { pattern: def.pattern.toString() } : {}, - inst, - continue: !def.abort - }); - }); - else - (_b = inst._zod).check ?? (_b.check = () => {}); -}); -var $ZodCheckRegex = /* @__PURE__ */ $constructor("$ZodCheckRegex", (inst, def) => { - $ZodCheckStringFormat.init(inst, def); - inst._zod.check = (payload) => { - def.pattern.lastIndex = 0; - if (def.pattern.test(payload.value)) - return; - payload.issues.push({ - origin: "string", - code: "invalid_format", - format: "regex", - input: payload.value, - pattern: def.pattern.toString(), - inst, - continue: !def.abort - }); - }; -}); -var $ZodCheckLowerCase = /* @__PURE__ */ $constructor("$ZodCheckLowerCase", (inst, def) => { - def.pattern ?? (def.pattern = lowercase); - $ZodCheckStringFormat.init(inst, def); -}); -var $ZodCheckUpperCase = /* @__PURE__ */ $constructor("$ZodCheckUpperCase", (inst, def) => { - def.pattern ?? (def.pattern = uppercase); - $ZodCheckStringFormat.init(inst, def); -}); -var $ZodCheckIncludes = /* @__PURE__ */ $constructor("$ZodCheckIncludes", (inst, def) => { - $ZodCheck.init(inst, def); - const escapedRegex = escapeRegex(def.includes); - const pattern = new RegExp(typeof def.position === "number" ? `^.{${def.position}}${escapedRegex}` : escapedRegex); - def.pattern = pattern; - inst._zod.onattach.push((inst2) => { - const bag = inst2._zod.bag; - bag.patterns ?? (bag.patterns = new Set); - bag.patterns.add(pattern); - }); - inst._zod.check = (payload) => { - if (payload.value.includes(def.includes, def.position)) - return; - payload.issues.push({ - origin: "string", - code: "invalid_format", - format: "includes", - includes: def.includes, - input: payload.value, - inst, - continue: !def.abort - }); - }; -}); -var $ZodCheckStartsWith = /* @__PURE__ */ $constructor("$ZodCheckStartsWith", (inst, def) => { - $ZodCheck.init(inst, def); - const pattern = new RegExp(`^${escapeRegex(def.prefix)}.*`); - def.pattern ?? (def.pattern = pattern); - inst._zod.onattach.push((inst2) => { - const bag = inst2._zod.bag; - bag.patterns ?? (bag.patterns = new Set); - bag.patterns.add(pattern); - }); - inst._zod.check = (payload) => { - if (payload.value.startsWith(def.prefix)) - return; - payload.issues.push({ - origin: "string", - code: "invalid_format", - format: "starts_with", - prefix: def.prefix, - input: payload.value, - inst, - continue: !def.abort - }); - }; -}); -var $ZodCheckEndsWith = /* @__PURE__ */ $constructor("$ZodCheckEndsWith", (inst, def) => { - $ZodCheck.init(inst, def); - const pattern = new RegExp(`.*${escapeRegex(def.suffix)}$`); - def.pattern ?? (def.pattern = pattern); - inst._zod.onattach.push((inst2) => { - const bag = inst2._zod.bag; - bag.patterns ?? (bag.patterns = new Set); - bag.patterns.add(pattern); - }); - inst._zod.check = (payload) => { - if (payload.value.endsWith(def.suffix)) - return; - payload.issues.push({ - origin: "string", - code: "invalid_format", - format: "ends_with", - suffix: def.suffix, - input: payload.value, - inst, - continue: !def.abort - }); - }; -}); -var $ZodCheckOverwrite = /* @__PURE__ */ $constructor("$ZodCheckOverwrite", (inst, def) => { - $ZodCheck.init(inst, def); - inst._zod.check = (payload) => { - payload.value = def.tx(payload.value); - }; -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/zod/v4/core/doc.js -class Doc { - constructor(args = []) { - this.content = []; - this.indent = 0; - if (this) - this.args = args; - } - indented(fn) { - this.indent += 1; - fn(this); - this.indent -= 1; - } - write(arg) { - if (typeof arg === "function") { - arg(this, { execution: "sync" }); - arg(this, { execution: "async" }); - return; - } - const content = arg; - const lines = content.split(` -`).filter((x) => x); - const minIndent = Math.min(...lines.map((x) => x.length - x.trimStart().length)); - const dedented = lines.map((x) => x.slice(minIndent)).map((x) => " ".repeat(this.indent * 2) + x); - for (const line of dedented) { - this.content.push(line); - } - } - compile() { - const F = Function; - const args = this?.args; - const content = this?.content ?? [``]; - const lines = [...content.map((x) => ` ${x}`)]; - return new F(...args, lines.join(` -`)); - } -} - -// node_modules/@modelcontextprotocol/sdk/node_modules/zod/v4/core/versions.js -var version = { - major: 4, - minor: 0, - patch: 0 -}; - -// node_modules/@modelcontextprotocol/sdk/node_modules/zod/v4/core/schemas.js -var $ZodType = /* @__PURE__ */ $constructor("$ZodType", (inst, def) => { - var _a; - inst ?? (inst = {}); - inst._zod.def = def; - inst._zod.bag = inst._zod.bag || {}; - inst._zod.version = version; - const checks = [...inst._zod.def.checks ?? []]; - if (inst._zod.traits.has("$ZodCheck")) { - checks.unshift(inst); - } - for (const ch of checks) { - for (const fn of ch._zod.onattach) { - fn(inst); - } - } - if (checks.length === 0) { - (_a = inst._zod).deferred ?? (_a.deferred = []); - inst._zod.deferred?.push(() => { - inst._zod.run = inst._zod.parse; - }); - } else { - const runChecks = (payload, checks2, ctx) => { - let isAborted = aborted(payload); - let asyncResult; - for (const ch of checks2) { - if (ch._zod.def.when) { - const shouldRun = ch._zod.def.when(payload); - if (!shouldRun) - continue; - } else if (isAborted) { - continue; - } - const currLen = payload.issues.length; - const _ = ch._zod.check(payload); - if (_ instanceof Promise && ctx?.async === false) { - throw new $ZodAsyncError; - } - if (asyncResult || _ instanceof Promise) { - asyncResult = (asyncResult ?? Promise.resolve()).then(async () => { - await _; - const nextLen = payload.issues.length; - if (nextLen === currLen) - return; - if (!isAborted) - isAborted = aborted(payload, currLen); - }); - } else { - const nextLen = payload.issues.length; - if (nextLen === currLen) - continue; - if (!isAborted) - isAborted = aborted(payload, currLen); - } - } - if (asyncResult) { - return asyncResult.then(() => { - return payload; - }); - } - return payload; - }; - inst._zod.run = (payload, ctx) => { - const result = inst._zod.parse(payload, ctx); - if (result instanceof Promise) { - if (ctx.async === false) - throw new $ZodAsyncError; - return result.then((result2) => runChecks(result2, checks, ctx)); - } - return runChecks(result, checks, ctx); - }; - } - inst["~standard"] = { - validate: (value) => { - try { - const r = safeParse(inst, value); - return r.success ? { value: r.data } : { issues: r.error?.issues }; - } catch (_) { - return safeParseAsync(inst, value).then((r) => r.success ? { value: r.data } : { issues: r.error?.issues }); - } - }, - vendor: "zod", - version: 1 - }; -}); -var $ZodString = /* @__PURE__ */ $constructor("$ZodString", (inst, def) => { - $ZodType.init(inst, def); - inst._zod.pattern = [...inst?._zod.bag?.patterns ?? []].pop() ?? string(inst._zod.bag); - inst._zod.parse = (payload, _) => { - if (def.coerce) - try { - payload.value = String(payload.value); - } catch (_2) {} - if (typeof payload.value === "string") - return payload; - payload.issues.push({ - expected: "string", - code: "invalid_type", - input: payload.value, - inst - }); - return payload; - }; -}); -var $ZodStringFormat = /* @__PURE__ */ $constructor("$ZodStringFormat", (inst, def) => { - $ZodCheckStringFormat.init(inst, def); - $ZodString.init(inst, def); -}); -var $ZodGUID = /* @__PURE__ */ $constructor("$ZodGUID", (inst, def) => { - def.pattern ?? (def.pattern = guid); - $ZodStringFormat.init(inst, def); -}); -var $ZodUUID = /* @__PURE__ */ $constructor("$ZodUUID", (inst, def) => { - if (def.version) { - const versionMap = { - v1: 1, - v2: 2, - v3: 3, - v4: 4, - v5: 5, - v6: 6, - v7: 7, - v8: 8 - }; - const v = versionMap[def.version]; - if (v === undefined) - throw new Error(`Invalid UUID version: "${def.version}"`); - def.pattern ?? (def.pattern = uuid(v)); - } else - def.pattern ?? (def.pattern = uuid()); - $ZodStringFormat.init(inst, def); -}); -var $ZodEmail = /* @__PURE__ */ $constructor("$ZodEmail", (inst, def) => { - def.pattern ?? (def.pattern = email); - $ZodStringFormat.init(inst, def); -}); -var $ZodURL = /* @__PURE__ */ $constructor("$ZodURL", (inst, def) => { - $ZodStringFormat.init(inst, def); - inst._zod.check = (payload) => { - try { - const orig = payload.value; - const url = new URL(orig); - const href = url.href; - if (def.hostname) { - def.hostname.lastIndex = 0; - if (!def.hostname.test(url.hostname)) { - payload.issues.push({ - code: "invalid_format", - format: "url", - note: "Invalid hostname", - pattern: hostname.source, - input: payload.value, - inst, - continue: !def.abort - }); - } - } - if (def.protocol) { - def.protocol.lastIndex = 0; - if (!def.protocol.test(url.protocol.endsWith(":") ? url.protocol.slice(0, -1) : url.protocol)) { - payload.issues.push({ - code: "invalid_format", - format: "url", - note: "Invalid protocol", - pattern: def.protocol.source, - input: payload.value, - inst, - continue: !def.abort - }); - } - } - if (!orig.endsWith("/") && href.endsWith("/")) { - payload.value = href.slice(0, -1); - } else { - payload.value = href; - } - return; - } catch (_) { - payload.issues.push({ - code: "invalid_format", - format: "url", - input: payload.value, - inst, - continue: !def.abort - }); - } - }; -}); -var $ZodEmoji = /* @__PURE__ */ $constructor("$ZodEmoji", (inst, def) => { - def.pattern ?? (def.pattern = emoji()); - $ZodStringFormat.init(inst, def); -}); -var $ZodNanoID = /* @__PURE__ */ $constructor("$ZodNanoID", (inst, def) => { - def.pattern ?? (def.pattern = nanoid); - $ZodStringFormat.init(inst, def); -}); -var $ZodCUID = /* @__PURE__ */ $constructor("$ZodCUID", (inst, def) => { - def.pattern ?? (def.pattern = cuid); - $ZodStringFormat.init(inst, def); -}); -var $ZodCUID2 = /* @__PURE__ */ $constructor("$ZodCUID2", (inst, def) => { - def.pattern ?? (def.pattern = cuid2); - $ZodStringFormat.init(inst, def); -}); -var $ZodULID = /* @__PURE__ */ $constructor("$ZodULID", (inst, def) => { - def.pattern ?? (def.pattern = ulid); - $ZodStringFormat.init(inst, def); -}); -var $ZodXID = /* @__PURE__ */ $constructor("$ZodXID", (inst, def) => { - def.pattern ?? (def.pattern = xid); - $ZodStringFormat.init(inst, def); -}); -var $ZodKSUID = /* @__PURE__ */ $constructor("$ZodKSUID", (inst, def) => { - def.pattern ?? (def.pattern = ksuid); - $ZodStringFormat.init(inst, def); -}); -var $ZodISODateTime = /* @__PURE__ */ $constructor("$ZodISODateTime", (inst, def) => { - def.pattern ?? (def.pattern = datetime(def)); - $ZodStringFormat.init(inst, def); -}); -var $ZodISODate = /* @__PURE__ */ $constructor("$ZodISODate", (inst, def) => { - def.pattern ?? (def.pattern = date); - $ZodStringFormat.init(inst, def); -}); -var $ZodISOTime = /* @__PURE__ */ $constructor("$ZodISOTime", (inst, def) => { - def.pattern ?? (def.pattern = time(def)); - $ZodStringFormat.init(inst, def); -}); -var $ZodISODuration = /* @__PURE__ */ $constructor("$ZodISODuration", (inst, def) => { - def.pattern ?? (def.pattern = duration); - $ZodStringFormat.init(inst, def); -}); -var $ZodIPv4 = /* @__PURE__ */ $constructor("$ZodIPv4", (inst, def) => { - def.pattern ?? (def.pattern = ipv4); - $ZodStringFormat.init(inst, def); - inst._zod.onattach.push((inst2) => { - const bag = inst2._zod.bag; - bag.format = `ipv4`; - }); -}); -var $ZodIPv6 = /* @__PURE__ */ $constructor("$ZodIPv6", (inst, def) => { - def.pattern ?? (def.pattern = ipv6); - $ZodStringFormat.init(inst, def); - inst._zod.onattach.push((inst2) => { - const bag = inst2._zod.bag; - bag.format = `ipv6`; - }); - inst._zod.check = (payload) => { - try { - new URL(`http://[${payload.value}]`); - } catch { - payload.issues.push({ - code: "invalid_format", - format: "ipv6", - input: payload.value, - inst, - continue: !def.abort - }); - } - }; -}); -var $ZodCIDRv4 = /* @__PURE__ */ $constructor("$ZodCIDRv4", (inst, def) => { - def.pattern ?? (def.pattern = cidrv4); - $ZodStringFormat.init(inst, def); -}); -var $ZodCIDRv6 = /* @__PURE__ */ $constructor("$ZodCIDRv6", (inst, def) => { - def.pattern ?? (def.pattern = cidrv6); - $ZodStringFormat.init(inst, def); - inst._zod.check = (payload) => { - const [address, prefix] = payload.value.split("/"); - try { - if (!prefix) - throw new Error; - const prefixNum = Number(prefix); - if (`${prefixNum}` !== prefix) - throw new Error; - if (prefixNum < 0 || prefixNum > 128) - throw new Error; - new URL(`http://[${address}]`); - } catch { - payload.issues.push({ - code: "invalid_format", - format: "cidrv6", - input: payload.value, - inst, - continue: !def.abort - }); - } - }; -}); -function isValidBase64(data) { - if (data === "") - return true; - if (data.length % 4 !== 0) - return false; - try { - atob(data); - return true; - } catch { - return false; - } -} -var $ZodBase64 = /* @__PURE__ */ $constructor("$ZodBase64", (inst, def) => { - def.pattern ?? (def.pattern = base64); - $ZodStringFormat.init(inst, def); - inst._zod.onattach.push((inst2) => { - inst2._zod.bag.contentEncoding = "base64"; - }); - inst._zod.check = (payload) => { - if (isValidBase64(payload.value)) - return; - payload.issues.push({ - code: "invalid_format", - format: "base64", - input: payload.value, - inst, - continue: !def.abort - }); - }; -}); -function isValidBase64URL(data) { - if (!base64url.test(data)) - return false; - const base642 = data.replace(/[-_]/g, (c) => c === "-" ? "+" : "/"); - const padded = base642.padEnd(Math.ceil(base642.length / 4) * 4, "="); - return isValidBase64(padded); -} -var $ZodBase64URL = /* @__PURE__ */ $constructor("$ZodBase64URL", (inst, def) => { - def.pattern ?? (def.pattern = base64url); - $ZodStringFormat.init(inst, def); - inst._zod.onattach.push((inst2) => { - inst2._zod.bag.contentEncoding = "base64url"; - }); - inst._zod.check = (payload) => { - if (isValidBase64URL(payload.value)) - return; - payload.issues.push({ - code: "invalid_format", - format: "base64url", - input: payload.value, - inst, - continue: !def.abort - }); - }; -}); -var $ZodE164 = /* @__PURE__ */ $constructor("$ZodE164", (inst, def) => { - def.pattern ?? (def.pattern = e164); - $ZodStringFormat.init(inst, def); -}); -function isValidJWT(token, algorithm = null) { - try { - const tokensParts = token.split("."); - if (tokensParts.length !== 3) - return false; - const [header] = tokensParts; - if (!header) - return false; - const parsedHeader = JSON.parse(atob(header)); - if ("typ" in parsedHeader && parsedHeader?.typ !== "JWT") - return false; - if (!parsedHeader.alg) - return false; - if (algorithm && (!("alg" in parsedHeader) || parsedHeader.alg !== algorithm)) - return false; - return true; - } catch { - return false; - } -} -var $ZodJWT = /* @__PURE__ */ $constructor("$ZodJWT", (inst, def) => { - $ZodStringFormat.init(inst, def); - inst._zod.check = (payload) => { - if (isValidJWT(payload.value, def.alg)) - return; - payload.issues.push({ - code: "invalid_format", - format: "jwt", - input: payload.value, - inst, - continue: !def.abort - }); - }; -}); -var $ZodNumber = /* @__PURE__ */ $constructor("$ZodNumber", (inst, def) => { - $ZodType.init(inst, def); - inst._zod.pattern = inst._zod.bag.pattern ?? number; - inst._zod.parse = (payload, _ctx) => { - if (def.coerce) - try { - payload.value = Number(payload.value); - } catch (_) {} - const input = payload.value; - if (typeof input === "number" && !Number.isNaN(input) && Number.isFinite(input)) { - return payload; - } - const received = typeof input === "number" ? Number.isNaN(input) ? "NaN" : !Number.isFinite(input) ? "Infinity" : undefined : undefined; - payload.issues.push({ - expected: "number", - code: "invalid_type", - input, - inst, - ...received ? { received } : {} - }); - return payload; - }; -}); -var $ZodNumberFormat = /* @__PURE__ */ $constructor("$ZodNumber", (inst, def) => { - $ZodCheckNumberFormat.init(inst, def); - $ZodNumber.init(inst, def); -}); -var $ZodBoolean = /* @__PURE__ */ $constructor("$ZodBoolean", (inst, def) => { - $ZodType.init(inst, def); - inst._zod.pattern = boolean; - inst._zod.parse = (payload, _ctx) => { - if (def.coerce) - try { - payload.value = Boolean(payload.value); - } catch (_) {} - const input = payload.value; - if (typeof input === "boolean") - return payload; - payload.issues.push({ - expected: "boolean", - code: "invalid_type", - input, - inst - }); - return payload; - }; -}); -var $ZodNull = /* @__PURE__ */ $constructor("$ZodNull", (inst, def) => { - $ZodType.init(inst, def); - inst._zod.pattern = _null; - inst._zod.values = new Set([null]); - inst._zod.parse = (payload, _ctx) => { - const input = payload.value; - if (input === null) - return payload; - payload.issues.push({ - expected: "null", - code: "invalid_type", - input, - inst - }); - return payload; - }; -}); -var $ZodUnknown = /* @__PURE__ */ $constructor("$ZodUnknown", (inst, def) => { - $ZodType.init(inst, def); - inst._zod.parse = (payload) => payload; -}); -var $ZodNever = /* @__PURE__ */ $constructor("$ZodNever", (inst, def) => { - $ZodType.init(inst, def); - inst._zod.parse = (payload, _ctx) => { - payload.issues.push({ - expected: "never", - code: "invalid_type", - input: payload.value, - inst - }); - return payload; - }; -}); -function handleArrayResult(result, final, index) { - if (result.issues.length) { - final.issues.push(...prefixIssues(index, result.issues)); - } - final.value[index] = result.value; -} -var $ZodArray = /* @__PURE__ */ $constructor("$ZodArray", (inst, def) => { - $ZodType.init(inst, def); - inst._zod.parse = (payload, ctx) => { - const input = payload.value; - if (!Array.isArray(input)) { - payload.issues.push({ - expected: "array", - code: "invalid_type", - input, - inst - }); - return payload; - } - payload.value = Array(input.length); - const proms = []; - for (let i = 0;i < input.length; i++) { - const item = input[i]; - const result = def.element._zod.run({ - value: item, - issues: [] - }, ctx); - if (result instanceof Promise) { - proms.push(result.then((result2) => handleArrayResult(result2, payload, i))); - } else { - handleArrayResult(result, payload, i); - } - } - if (proms.length) { - return Promise.all(proms).then(() => payload); - } - return payload; - }; -}); -function handleObjectResult(result, final, key) { - if (result.issues.length) { - final.issues.push(...prefixIssues(key, result.issues)); - } - final.value[key] = result.value; -} -function handleOptionalObjectResult(result, final, key, input) { - if (result.issues.length) { - if (input[key] === undefined) { - if (key in input) { - final.value[key] = undefined; - } else { - final.value[key] = result.value; - } - } else { - final.issues.push(...prefixIssues(key, result.issues)); - } - } else if (result.value === undefined) { - if (key in input) - final.value[key] = undefined; - } else { - final.value[key] = result.value; - } -} -var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => { - $ZodType.init(inst, def); - const _normalized = cached(() => { - const keys = Object.keys(def.shape); - for (const k of keys) { - if (!(def.shape[k] instanceof $ZodType)) { - throw new Error(`Invalid element at key "${k}": expected a Zod schema`); - } - } - const okeys = optionalKeys(def.shape); - return { - shape: def.shape, - keys, - keySet: new Set(keys), - numKeys: keys.length, - optionalKeys: new Set(okeys) - }; - }); - defineLazy(inst._zod, "propValues", () => { - const shape = def.shape; - const propValues = {}; - for (const key in shape) { - const field = shape[key]._zod; - if (field.values) { - propValues[key] ?? (propValues[key] = new Set); - for (const v of field.values) - propValues[key].add(v); - } - } - return propValues; - }); - const generateFastpass = (shape) => { - const doc = new Doc(["shape", "payload", "ctx"]); - const normalized = _normalized.value; - const parseStr = (key) => { - const k = esc(key); - return `shape[${k}]._zod.run({ value: input[${k}], issues: [] }, ctx)`; - }; - doc.write(`const input = payload.value;`); - const ids = Object.create(null); - let counter = 0; - for (const key of normalized.keys) { - ids[key] = `key_${counter++}`; - } - doc.write(`const newResult = {}`); - for (const key of normalized.keys) { - if (normalized.optionalKeys.has(key)) { - const id = ids[key]; - doc.write(`const ${id} = ${parseStr(key)};`); - const k = esc(key); - doc.write(` - if (${id}.issues.length) { - if (input[${k}] === undefined) { - if (${k} in input) { - newResult[${k}] = undefined; - } - } else { - payload.issues = payload.issues.concat( - ${id}.issues.map((iss) => ({ - ...iss, - path: iss.path ? [${k}, ...iss.path] : [${k}], - })) - ); - } - } else if (${id}.value === undefined) { - if (${k} in input) newResult[${k}] = undefined; - } else { - newResult[${k}] = ${id}.value; - } - `); - } else { - const id = ids[key]; - doc.write(`const ${id} = ${parseStr(key)};`); - doc.write(` - if (${id}.issues.length) payload.issues = payload.issues.concat(${id}.issues.map(iss => ({ - ...iss, - path: iss.path ? [${esc(key)}, ...iss.path] : [${esc(key)}] - })));`); - doc.write(`newResult[${esc(key)}] = ${id}.value`); - } - } - doc.write(`payload.value = newResult;`); - doc.write(`return payload;`); - const fn = doc.compile(); - return (payload, ctx) => fn(shape, payload, ctx); - }; - let fastpass; - const isObject2 = isObject; - const jit = !globalConfig.jitless; - const allowsEval2 = allowsEval; - const fastEnabled = jit && allowsEval2.value; - const catchall = def.catchall; - let value; - inst._zod.parse = (payload, ctx) => { - value ?? (value = _normalized.value); - const input = payload.value; - if (!isObject2(input)) { - payload.issues.push({ - expected: "object", - code: "invalid_type", - input, - inst - }); - return payload; - } - const proms = []; - if (jit && fastEnabled && ctx?.async === false && ctx.jitless !== true) { - if (!fastpass) - fastpass = generateFastpass(def.shape); - payload = fastpass(payload, ctx); - } else { - payload.value = {}; - const shape = value.shape; - for (const key of value.keys) { - const el = shape[key]; - const r = el._zod.run({ value: input[key], issues: [] }, ctx); - const isOptional = el._zod.optin === "optional" && el._zod.optout === "optional"; - if (r instanceof Promise) { - proms.push(r.then((r2) => isOptional ? handleOptionalObjectResult(r2, payload, key, input) : handleObjectResult(r2, payload, key))); - } else if (isOptional) { - handleOptionalObjectResult(r, payload, key, input); - } else { - handleObjectResult(r, payload, key); - } - } - } - if (!catchall) { - return proms.length ? Promise.all(proms).then(() => payload) : payload; - } - const unrecognized = []; - const keySet = value.keySet; - const _catchall = catchall._zod; - const t = _catchall.def.type; - for (const key of Object.keys(input)) { - if (keySet.has(key)) - continue; - if (t === "never") { - unrecognized.push(key); - continue; - } - const r = _catchall.run({ value: input[key], issues: [] }, ctx); - if (r instanceof Promise) { - proms.push(r.then((r2) => handleObjectResult(r2, payload, key))); - } else { - handleObjectResult(r, payload, key); - } - } - if (unrecognized.length) { - payload.issues.push({ - code: "unrecognized_keys", - keys: unrecognized, - input, - inst - }); - } - if (!proms.length) - return payload; - return Promise.all(proms).then(() => { - return payload; - }); - }; -}); -function handleUnionResults(results, final, inst, ctx) { - for (const result of results) { - if (result.issues.length === 0) { - final.value = result.value; - return final; - } - } - final.issues.push({ - code: "invalid_union", - input: final.value, - inst, - errors: results.map((result) => result.issues.map((iss) => finalizeIssue(iss, ctx, config()))) - }); - return final; -} -var $ZodUnion = /* @__PURE__ */ $constructor("$ZodUnion", (inst, def) => { - $ZodType.init(inst, def); - defineLazy(inst._zod, "optin", () => def.options.some((o) => o._zod.optin === "optional") ? "optional" : undefined); - defineLazy(inst._zod, "optout", () => def.options.some((o) => o._zod.optout === "optional") ? "optional" : undefined); - defineLazy(inst._zod, "values", () => { - if (def.options.every((o) => o._zod.values)) { - return new Set(def.options.flatMap((option) => Array.from(option._zod.values))); - } - return; - }); - defineLazy(inst._zod, "pattern", () => { - if (def.options.every((o) => o._zod.pattern)) { - const patterns = def.options.map((o) => o._zod.pattern); - return new RegExp(`^(${patterns.map((p) => cleanRegex(p.source)).join("|")})$`); - } - return; - }); - inst._zod.parse = (payload, ctx) => { - let async = false; - const results = []; - for (const option of def.options) { - const result = option._zod.run({ - value: payload.value, - issues: [] - }, ctx); - if (result instanceof Promise) { - results.push(result); - async = true; - } else { - if (result.issues.length === 0) - return result; - results.push(result); - } - } - if (!async) - return handleUnionResults(results, payload, inst, ctx); - return Promise.all(results).then((results2) => { - return handleUnionResults(results2, payload, inst, ctx); - }); - }; -}); -var $ZodDiscriminatedUnion = /* @__PURE__ */ $constructor("$ZodDiscriminatedUnion", (inst, def) => { - $ZodUnion.init(inst, def); - const _super = inst._zod.parse; - defineLazy(inst._zod, "propValues", () => { - const propValues = {}; - for (const option of def.options) { - const pv = option._zod.propValues; - if (!pv || Object.keys(pv).length === 0) - throw new Error(`Invalid discriminated union option at index "${def.options.indexOf(option)}"`); - for (const [k, v] of Object.entries(pv)) { - if (!propValues[k]) - propValues[k] = new Set; - for (const val of v) { - propValues[k].add(val); - } - } - } - return propValues; - }); - const disc = cached(() => { - const opts = def.options; - const map = new Map; - for (const o of opts) { - const values = o._zod.propValues[def.discriminator]; - if (!values || values.size === 0) - throw new Error(`Invalid discriminated union option at index "${def.options.indexOf(o)}"`); - for (const v of values) { - if (map.has(v)) { - throw new Error(`Duplicate discriminator value "${String(v)}"`); - } - map.set(v, o); - } - } - return map; - }); - inst._zod.parse = (payload, ctx) => { - const input = payload.value; - if (!isObject(input)) { - payload.issues.push({ - code: "invalid_type", - expected: "object", - input, - inst - }); - return payload; - } - const opt = disc.value.get(input?.[def.discriminator]); - if (opt) { - return opt._zod.run(payload, ctx); - } - if (def.unionFallback) { - return _super(payload, ctx); - } - payload.issues.push({ - code: "invalid_union", - errors: [], - note: "No matching discriminator", - input, - path: [def.discriminator], - inst - }); - return payload; - }; -}); -var $ZodIntersection = /* @__PURE__ */ $constructor("$ZodIntersection", (inst, def) => { - $ZodType.init(inst, def); - inst._zod.parse = (payload, ctx) => { - const input = payload.value; - const left = def.left._zod.run({ value: input, issues: [] }, ctx); - const right = def.right._zod.run({ value: input, issues: [] }, ctx); - const async = left instanceof Promise || right instanceof Promise; - if (async) { - return Promise.all([left, right]).then(([left2, right2]) => { - return handleIntersectionResults(payload, left2, right2); - }); - } - return handleIntersectionResults(payload, left, right); - }; -}); -function mergeValues(a, b) { - if (a === b) { - return { valid: true, data: a }; - } - if (a instanceof Date && b instanceof Date && +a === +b) { - return { valid: true, data: a }; - } - if (isPlainObject(a) && isPlainObject(b)) { - const bKeys = Object.keys(b); - const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1); - const newObj = { ...a, ...b }; - for (const key of sharedKeys) { - const sharedValue = mergeValues(a[key], b[key]); - if (!sharedValue.valid) { - return { - valid: false, - mergeErrorPath: [key, ...sharedValue.mergeErrorPath] - }; - } - newObj[key] = sharedValue.data; - } - return { valid: true, data: newObj }; - } - if (Array.isArray(a) && Array.isArray(b)) { - if (a.length !== b.length) { - return { valid: false, mergeErrorPath: [] }; - } - const newArray = []; - for (let index = 0;index < a.length; index++) { - const itemA = a[index]; - const itemB = b[index]; - const sharedValue = mergeValues(itemA, itemB); - if (!sharedValue.valid) { - return { - valid: false, - mergeErrorPath: [index, ...sharedValue.mergeErrorPath] - }; - } - newArray.push(sharedValue.data); - } - return { valid: true, data: newArray }; - } - return { valid: false, mergeErrorPath: [] }; -} -function handleIntersectionResults(result, left, right) { - if (left.issues.length) { - result.issues.push(...left.issues); - } - if (right.issues.length) { - result.issues.push(...right.issues); - } - if (aborted(result)) - return result; - const merged = mergeValues(left.value, right.value); - if (!merged.valid) { - throw new Error(`Unmergable intersection. Error path: ` + `${JSON.stringify(merged.mergeErrorPath)}`); - } - result.value = merged.data; - return result; -} -var $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => { - $ZodType.init(inst, def); - inst._zod.parse = (payload, ctx) => { - const input = payload.value; - if (!isPlainObject(input)) { - payload.issues.push({ - expected: "record", - code: "invalid_type", - input, - inst - }); - return payload; - } - const proms = []; - if (def.keyType._zod.values) { - const values = def.keyType._zod.values; - payload.value = {}; - for (const key of values) { - if (typeof key === "string" || typeof key === "number" || typeof key === "symbol") { - const result = def.valueType._zod.run({ value: input[key], issues: [] }, ctx); - if (result instanceof Promise) { - proms.push(result.then((result2) => { - if (result2.issues.length) { - payload.issues.push(...prefixIssues(key, result2.issues)); - } - payload.value[key] = result2.value; - })); - } else { - if (result.issues.length) { - payload.issues.push(...prefixIssues(key, result.issues)); - } - payload.value[key] = result.value; - } - } - } - let unrecognized; - for (const key in input) { - if (!values.has(key)) { - unrecognized = unrecognized ?? []; - unrecognized.push(key); - } - } - if (unrecognized && unrecognized.length > 0) { - payload.issues.push({ - code: "unrecognized_keys", - input, - inst, - keys: unrecognized - }); - } - } else { - payload.value = {}; - for (const key of Reflect.ownKeys(input)) { - if (key === "__proto__") - continue; - const keyResult = def.keyType._zod.run({ value: key, issues: [] }, ctx); - if (keyResult instanceof Promise) { - throw new Error("Async schemas not supported in object keys currently"); - } - if (keyResult.issues.length) { - payload.issues.push({ - origin: "record", - code: "invalid_key", - issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config())), - input: key, - path: [key], - inst - }); - payload.value[keyResult.value] = keyResult.value; - continue; - } - const result = def.valueType._zod.run({ value: input[key], issues: [] }, ctx); - if (result instanceof Promise) { - proms.push(result.then((result2) => { - if (result2.issues.length) { - payload.issues.push(...prefixIssues(key, result2.issues)); - } - payload.value[keyResult.value] = result2.value; - })); - } else { - if (result.issues.length) { - payload.issues.push(...prefixIssues(key, result.issues)); - } - payload.value[keyResult.value] = result.value; - } - } - } - if (proms.length) { - return Promise.all(proms).then(() => payload); - } - return payload; - }; -}); -var $ZodEnum = /* @__PURE__ */ $constructor("$ZodEnum", (inst, def) => { - $ZodType.init(inst, def); - const values = getEnumValues(def.entries); - inst._zod.values = new Set(values); - inst._zod.pattern = new RegExp(`^(${values.filter((k) => propertyKeyTypes.has(typeof k)).map((o) => typeof o === "string" ? escapeRegex(o) : o.toString()).join("|")})$`); - inst._zod.parse = (payload, _ctx) => { - const input = payload.value; - if (inst._zod.values.has(input)) { - return payload; - } - payload.issues.push({ - code: "invalid_value", - values, - input, - inst - }); - return payload; - }; -}); -var $ZodLiteral = /* @__PURE__ */ $constructor("$ZodLiteral", (inst, def) => { - $ZodType.init(inst, def); - inst._zod.values = new Set(def.values); - inst._zod.pattern = new RegExp(`^(${def.values.map((o) => typeof o === "string" ? escapeRegex(o) : o ? o.toString() : String(o)).join("|")})$`); - inst._zod.parse = (payload, _ctx) => { - const input = payload.value; - if (inst._zod.values.has(input)) { - return payload; - } - payload.issues.push({ - code: "invalid_value", - values: def.values, - input, - inst - }); - return payload; - }; -}); -var $ZodTransform = /* @__PURE__ */ $constructor("$ZodTransform", (inst, def) => { - $ZodType.init(inst, def); - inst._zod.parse = (payload, _ctx) => { - const _out = def.transform(payload.value, payload); - if (_ctx.async) { - const output = _out instanceof Promise ? _out : Promise.resolve(_out); - return output.then((output2) => { - payload.value = output2; - return payload; - }); - } - if (_out instanceof Promise) { - throw new $ZodAsyncError; - } - payload.value = _out; - return payload; - }; -}); -var $ZodOptional = /* @__PURE__ */ $constructor("$ZodOptional", (inst, def) => { - $ZodType.init(inst, def); - inst._zod.optin = "optional"; - inst._zod.optout = "optional"; - defineLazy(inst._zod, "values", () => { - return def.innerType._zod.values ? new Set([...def.innerType._zod.values, undefined]) : undefined; - }); - defineLazy(inst._zod, "pattern", () => { - const pattern = def.innerType._zod.pattern; - return pattern ? new RegExp(`^(${cleanRegex(pattern.source)})?$`) : undefined; - }); - inst._zod.parse = (payload, ctx) => { - if (def.innerType._zod.optin === "optional") { - return def.innerType._zod.run(payload, ctx); - } - if (payload.value === undefined) { - return payload; - } - return def.innerType._zod.run(payload, ctx); - }; -}); -var $ZodNullable = /* @__PURE__ */ $constructor("$ZodNullable", (inst, def) => { - $ZodType.init(inst, def); - defineLazy(inst._zod, "optin", () => def.innerType._zod.optin); - defineLazy(inst._zod, "optout", () => def.innerType._zod.optout); - defineLazy(inst._zod, "pattern", () => { - const pattern = def.innerType._zod.pattern; - return pattern ? new RegExp(`^(${cleanRegex(pattern.source)}|null)$`) : undefined; - }); - defineLazy(inst._zod, "values", () => { - return def.innerType._zod.values ? new Set([...def.innerType._zod.values, null]) : undefined; - }); - inst._zod.parse = (payload, ctx) => { - if (payload.value === null) - return payload; - return def.innerType._zod.run(payload, ctx); - }; -}); -var $ZodDefault = /* @__PURE__ */ $constructor("$ZodDefault", (inst, def) => { - $ZodType.init(inst, def); - inst._zod.optin = "optional"; - defineLazy(inst._zod, "values", () => def.innerType._zod.values); - inst._zod.parse = (payload, ctx) => { - if (payload.value === undefined) { - payload.value = def.defaultValue; - return payload; - } - const result = def.innerType._zod.run(payload, ctx); - if (result instanceof Promise) { - return result.then((result2) => handleDefaultResult(result2, def)); - } - return handleDefaultResult(result, def); - }; -}); -function handleDefaultResult(payload, def) { - if (payload.value === undefined) { - payload.value = def.defaultValue; - } - return payload; -} -var $ZodPrefault = /* @__PURE__ */ $constructor("$ZodPrefault", (inst, def) => { - $ZodType.init(inst, def); - inst._zod.optin = "optional"; - defineLazy(inst._zod, "values", () => def.innerType._zod.values); - inst._zod.parse = (payload, ctx) => { - if (payload.value === undefined) { - payload.value = def.defaultValue; - } - return def.innerType._zod.run(payload, ctx); - }; -}); -var $ZodNonOptional = /* @__PURE__ */ $constructor("$ZodNonOptional", (inst, def) => { - $ZodType.init(inst, def); - defineLazy(inst._zod, "values", () => { - const v = def.innerType._zod.values; - return v ? new Set([...v].filter((x) => x !== undefined)) : undefined; - }); - inst._zod.parse = (payload, ctx) => { - const result = def.innerType._zod.run(payload, ctx); - if (result instanceof Promise) { - return result.then((result2) => handleNonOptionalResult(result2, inst)); - } - return handleNonOptionalResult(result, inst); - }; -}); -function handleNonOptionalResult(payload, inst) { - if (!payload.issues.length && payload.value === undefined) { - payload.issues.push({ - code: "invalid_type", - expected: "nonoptional", - input: payload.value, - inst - }); - } - return payload; -} -var $ZodCatch = /* @__PURE__ */ $constructor("$ZodCatch", (inst, def) => { - $ZodType.init(inst, def); - inst._zod.optin = "optional"; - defineLazy(inst._zod, "optout", () => def.innerType._zod.optout); - defineLazy(inst._zod, "values", () => def.innerType._zod.values); - inst._zod.parse = (payload, ctx) => { - const result = def.innerType._zod.run(payload, ctx); - if (result instanceof Promise) { - return result.then((result2) => { - payload.value = result2.value; - if (result2.issues.length) { - payload.value = def.catchValue({ - ...payload, - error: { - issues: result2.issues.map((iss) => finalizeIssue(iss, ctx, config())) - }, - input: payload.value - }); - payload.issues = []; - } - return payload; - }); - } - payload.value = result.value; - if (result.issues.length) { - payload.value = def.catchValue({ - ...payload, - error: { - issues: result.issues.map((iss) => finalizeIssue(iss, ctx, config())) - }, - input: payload.value - }); - payload.issues = []; - } - return payload; - }; -}); -var $ZodPipe = /* @__PURE__ */ $constructor("$ZodPipe", (inst, def) => { - $ZodType.init(inst, def); - defineLazy(inst._zod, "values", () => def.in._zod.values); - defineLazy(inst._zod, "optin", () => def.in._zod.optin); - defineLazy(inst._zod, "optout", () => def.out._zod.optout); - inst._zod.parse = (payload, ctx) => { - const left = def.in._zod.run(payload, ctx); - if (left instanceof Promise) { - return left.then((left2) => handlePipeResult(left2, def, ctx)); - } - return handlePipeResult(left, def, ctx); - }; -}); -function handlePipeResult(left, def, ctx) { - if (aborted(left)) { - return left; - } - return def.out._zod.run({ value: left.value, issues: left.issues }, ctx); -} -var $ZodReadonly = /* @__PURE__ */ $constructor("$ZodReadonly", (inst, def) => { - $ZodType.init(inst, def); - defineLazy(inst._zod, "propValues", () => def.innerType._zod.propValues); - defineLazy(inst._zod, "values", () => def.innerType._zod.values); - defineLazy(inst._zod, "optin", () => def.innerType._zod.optin); - defineLazy(inst._zod, "optout", () => def.innerType._zod.optout); - inst._zod.parse = (payload, ctx) => { - const result = def.innerType._zod.run(payload, ctx); - if (result instanceof Promise) { - return result.then(handleReadonlyResult); - } - return handleReadonlyResult(result); - }; -}); -function handleReadonlyResult(payload) { - payload.value = Object.freeze(payload.value); - return payload; -} -var $ZodCustom = /* @__PURE__ */ $constructor("$ZodCustom", (inst, def) => { - $ZodCheck.init(inst, def); - $ZodType.init(inst, def); - inst._zod.parse = (payload, _) => { - return payload; - }; - inst._zod.check = (payload) => { - const input = payload.value; - const r = def.fn(input); - if (r instanceof Promise) { - return r.then((r2) => handleRefineResult(r2, payload, input, inst)); - } - handleRefineResult(r, payload, input, inst); - return; - }; -}); -function handleRefineResult(result, payload, input, inst) { - if (!result) { - const _iss = { - code: "custom", - input, - inst, - path: [...inst._zod.def.path ?? []], - continue: !inst._zod.def.abort - }; - if (inst._zod.def.params) - _iss.params = inst._zod.def.params; - payload.issues.push(issue(_iss)); - } -} -// node_modules/@modelcontextprotocol/sdk/node_modules/zod/v4/locales/en.js -var parsedType = (data) => { - const t = typeof data; - switch (t) { - case "number": { - return Number.isNaN(data) ? "NaN" : "number"; - } - case "object": { - if (Array.isArray(data)) { - return "array"; - } - if (data === null) { - return "null"; - } - if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) { - return data.constructor.name; - } - } - } - return t; -}; -var error = () => { - const Sizable = { - string: { unit: "characters", verb: "to have" }, - file: { unit: "bytes", verb: "to have" }, - array: { unit: "items", verb: "to have" }, - set: { unit: "items", verb: "to have" } - }; - function getSizing(origin) { - return Sizable[origin] ?? null; - } - const Nouns = { - regex: "input", - email: "email address", - url: "URL", - emoji: "emoji", - uuid: "UUID", - uuidv4: "UUIDv4", - uuidv6: "UUIDv6", - nanoid: "nanoid", - guid: "GUID", - cuid: "cuid", - cuid2: "cuid2", - ulid: "ULID", - xid: "XID", - ksuid: "KSUID", - datetime: "ISO datetime", - date: "ISO date", - time: "ISO time", - duration: "ISO duration", - ipv4: "IPv4 address", - ipv6: "IPv6 address", - cidrv4: "IPv4 range", - cidrv6: "IPv6 range", - base64: "base64-encoded string", - base64url: "base64url-encoded string", - json_string: "JSON string", - e164: "E.164 number", - jwt: "JWT", - template_literal: "input" - }; - return (issue2) => { - switch (issue2.code) { - case "invalid_type": - return `Invalid input: expected ${issue2.expected}, received ${parsedType(issue2.input)}`; - case "invalid_value": - if (issue2.values.length === 1) - return `Invalid input: expected ${stringifyPrimitive(issue2.values[0])}`; - return `Invalid option: expected one of ${joinValues(issue2.values, "|")}`; - case "too_big": { - const adj = issue2.inclusive ? "<=" : "<"; - const sizing = getSizing(issue2.origin); - if (sizing) - return `Too big: expected ${issue2.origin ?? "value"} to have ${adj}${issue2.maximum.toString()} ${sizing.unit ?? "elements"}`; - return `Too big: expected ${issue2.origin ?? "value"} to be ${adj}${issue2.maximum.toString()}`; - } - case "too_small": { - const adj = issue2.inclusive ? ">=" : ">"; - const sizing = getSizing(issue2.origin); - if (sizing) { - return `Too small: expected ${issue2.origin} to have ${adj}${issue2.minimum.toString()} ${sizing.unit}`; - } - return `Too small: expected ${issue2.origin} to be ${adj}${issue2.minimum.toString()}`; - } - case "invalid_format": { - const _issue = issue2; - if (_issue.format === "starts_with") { - return `Invalid string: must start with "${_issue.prefix}"`; - } - if (_issue.format === "ends_with") - return `Invalid string: must end with "${_issue.suffix}"`; - if (_issue.format === "includes") - return `Invalid string: must include "${_issue.includes}"`; - if (_issue.format === "regex") - return `Invalid string: must match pattern ${_issue.pattern}`; - return `Invalid ${Nouns[_issue.format] ?? issue2.format}`; - } - case "not_multiple_of": - return `Invalid number: must be a multiple of ${issue2.divisor}`; - case "unrecognized_keys": - return `Unrecognized key${issue2.keys.length > 1 ? "s" : ""}: ${joinValues(issue2.keys, ", ")}`; - case "invalid_key": - return `Invalid key in ${issue2.origin}`; - case "invalid_union": - return "Invalid input"; - case "invalid_element": - return `Invalid value in ${issue2.origin}`; - default: - return `Invalid input`; - } - }; -}; -function en_default() { - return { - localeError: error() - }; -} -// node_modules/@modelcontextprotocol/sdk/node_modules/zod/v4/core/registries.js -var $output = Symbol("ZodOutput"); -var $input = Symbol("ZodInput"); - -class $ZodRegistry { - constructor() { - this._map = new Map; - this._idmap = new Map; - } - add(schema, ..._meta) { - const meta = _meta[0]; - this._map.set(schema, meta); - if (meta && typeof meta === "object" && "id" in meta) { - if (this._idmap.has(meta.id)) { - throw new Error(`ID ${meta.id} already exists in the registry`); - } - this._idmap.set(meta.id, schema); - } - return this; - } - clear() { - this._map = new Map; - this._idmap = new Map; - return this; - } - remove(schema) { - const meta = this._map.get(schema); - if (meta && typeof meta === "object" && "id" in meta) { - this._idmap.delete(meta.id); - } - this._map.delete(schema); - return this; - } - get(schema) { - const p = schema._zod.parent; - if (p) { - const pm = { ...this.get(p) ?? {} }; - delete pm.id; - return { ...pm, ...this._map.get(schema) }; - } - return this._map.get(schema); - } - has(schema) { - return this._map.has(schema); - } -} -function registry() { - return new $ZodRegistry; -} -var globalRegistry = /* @__PURE__ */ registry(); -// node_modules/@modelcontextprotocol/sdk/node_modules/zod/v4/core/api.js -function _string(Class2, params) { - return new Class2({ - type: "string", - ...normalizeParams(params) - }); -} -function _email(Class2, params) { - return new Class2({ - type: "string", - format: "email", - check: "string_format", - abort: false, - ...normalizeParams(params) - }); -} -function _guid(Class2, params) { - return new Class2({ - type: "string", - format: "guid", - check: "string_format", - abort: false, - ...normalizeParams(params) - }); -} -function _uuid(Class2, params) { - return new Class2({ - type: "string", - format: "uuid", - check: "string_format", - abort: false, - ...normalizeParams(params) - }); -} -function _uuidv4(Class2, params) { - return new Class2({ - type: "string", - format: "uuid", - check: "string_format", - abort: false, - version: "v4", - ...normalizeParams(params) - }); -} -function _uuidv6(Class2, params) { - return new Class2({ - type: "string", - format: "uuid", - check: "string_format", - abort: false, - version: "v6", - ...normalizeParams(params) - }); -} -function _uuidv7(Class2, params) { - return new Class2({ - type: "string", - format: "uuid", - check: "string_format", - abort: false, - version: "v7", - ...normalizeParams(params) - }); -} -function _url(Class2, params) { - return new Class2({ - type: "string", - format: "url", - check: "string_format", - abort: false, - ...normalizeParams(params) - }); -} -function _emoji2(Class2, params) { - return new Class2({ - type: "string", - format: "emoji", - check: "string_format", - abort: false, - ...normalizeParams(params) - }); -} -function _nanoid(Class2, params) { - return new Class2({ - type: "string", - format: "nanoid", - check: "string_format", - abort: false, - ...normalizeParams(params) - }); -} -function _cuid(Class2, params) { - return new Class2({ - type: "string", - format: "cuid", - check: "string_format", - abort: false, - ...normalizeParams(params) - }); -} -function _cuid2(Class2, params) { - return new Class2({ - type: "string", - format: "cuid2", - check: "string_format", - abort: false, - ...normalizeParams(params) - }); -} -function _ulid(Class2, params) { - return new Class2({ - type: "string", - format: "ulid", - check: "string_format", - abort: false, - ...normalizeParams(params) - }); -} -function _xid(Class2, params) { - return new Class2({ - type: "string", - format: "xid", - check: "string_format", - abort: false, - ...normalizeParams(params) - }); -} -function _ksuid(Class2, params) { - return new Class2({ - type: "string", - format: "ksuid", - check: "string_format", - abort: false, - ...normalizeParams(params) - }); -} -function _ipv4(Class2, params) { - return new Class2({ - type: "string", - format: "ipv4", - check: "string_format", - abort: false, - ...normalizeParams(params) - }); -} -function _ipv6(Class2, params) { - return new Class2({ - type: "string", - format: "ipv6", - check: "string_format", - abort: false, - ...normalizeParams(params) - }); -} -function _cidrv4(Class2, params) { - return new Class2({ - type: "string", - format: "cidrv4", - check: "string_format", - abort: false, - ...normalizeParams(params) - }); -} -function _cidrv6(Class2, params) { - return new Class2({ - type: "string", - format: "cidrv6", - check: "string_format", - abort: false, - ...normalizeParams(params) - }); -} -function _base64(Class2, params) { - return new Class2({ - type: "string", - format: "base64", - check: "string_format", - abort: false, - ...normalizeParams(params) - }); -} -function _base64url(Class2, params) { - return new Class2({ - type: "string", - format: "base64url", - check: "string_format", - abort: false, - ...normalizeParams(params) - }); -} -function _e164(Class2, params) { - return new Class2({ - type: "string", - format: "e164", - check: "string_format", - abort: false, - ...normalizeParams(params) - }); -} -function _jwt(Class2, params) { - return new Class2({ - type: "string", - format: "jwt", - check: "string_format", - abort: false, - ...normalizeParams(params) - }); -} -function _isoDateTime(Class2, params) { - return new Class2({ - type: "string", - format: "datetime", - check: "string_format", - offset: false, - local: false, - precision: null, - ...normalizeParams(params) - }); -} -function _isoDate(Class2, params) { - return new Class2({ - type: "string", - format: "date", - check: "string_format", - ...normalizeParams(params) - }); -} -function _isoTime(Class2, params) { - return new Class2({ - type: "string", - format: "time", - check: "string_format", - precision: null, - ...normalizeParams(params) - }); -} -function _isoDuration(Class2, params) { - return new Class2({ - type: "string", - format: "duration", - check: "string_format", - ...normalizeParams(params) - }); -} -function _number(Class2, params) { - return new Class2({ - type: "number", - checks: [], - ...normalizeParams(params) - }); -} -function _int(Class2, params) { - return new Class2({ - type: "number", - check: "number_format", - abort: false, - format: "safeint", - ...normalizeParams(params) - }); -} -function _boolean(Class2, params) { - return new Class2({ - type: "boolean", - ...normalizeParams(params) - }); -} -function _null2(Class2, params) { - return new Class2({ - type: "null", - ...normalizeParams(params) - }); -} -function _unknown(Class2) { - return new Class2({ - type: "unknown" - }); -} -function _never(Class2, params) { - return new Class2({ - type: "never", - ...normalizeParams(params) - }); -} -function _lt(value, params) { - return new $ZodCheckLessThan({ - check: "less_than", - ...normalizeParams(params), - value, - inclusive: false - }); -} -function _lte(value, params) { - return new $ZodCheckLessThan({ - check: "less_than", - ...normalizeParams(params), - value, - inclusive: true - }); -} -function _gt(value, params) { - return new $ZodCheckGreaterThan({ - check: "greater_than", - ...normalizeParams(params), - value, - inclusive: false - }); -} -function _gte(value, params) { - return new $ZodCheckGreaterThan({ - check: "greater_than", - ...normalizeParams(params), - value, - inclusive: true - }); -} -function _multipleOf(value, params) { - return new $ZodCheckMultipleOf({ - check: "multiple_of", - ...normalizeParams(params), - value - }); -} -function _maxLength(maximum, params) { - const ch = new $ZodCheckMaxLength({ - check: "max_length", - ...normalizeParams(params), - maximum - }); - return ch; -} -function _minLength(minimum, params) { - return new $ZodCheckMinLength({ - check: "min_length", - ...normalizeParams(params), - minimum - }); -} -function _length(length, params) { - return new $ZodCheckLengthEquals({ - check: "length_equals", - ...normalizeParams(params), - length - }); -} -function _regex(pattern, params) { - return new $ZodCheckRegex({ - check: "string_format", - format: "regex", - ...normalizeParams(params), - pattern - }); -} -function _lowercase(params) { - return new $ZodCheckLowerCase({ - check: "string_format", - format: "lowercase", - ...normalizeParams(params) - }); -} -function _uppercase(params) { - return new $ZodCheckUpperCase({ - check: "string_format", - format: "uppercase", - ...normalizeParams(params) - }); -} -function _includes(includes, params) { - return new $ZodCheckIncludes({ - check: "string_format", - format: "includes", - ...normalizeParams(params), - includes - }); -} -function _startsWith(prefix, params) { - return new $ZodCheckStartsWith({ - check: "string_format", - format: "starts_with", - ...normalizeParams(params), - prefix - }); -} -function _endsWith(suffix, params) { - return new $ZodCheckEndsWith({ - check: "string_format", - format: "ends_with", - ...normalizeParams(params), - suffix - }); -} -function _overwrite(tx) { - return new $ZodCheckOverwrite({ - check: "overwrite", - tx - }); -} -function _normalize(form) { - return _overwrite((input) => input.normalize(form)); -} -function _trim() { - return _overwrite((input) => input.trim()); -} -function _toLowerCase() { - return _overwrite((input) => input.toLowerCase()); -} -function _toUpperCase() { - return _overwrite((input) => input.toUpperCase()); -} -function _array(Class2, element, params) { - return new Class2({ - type: "array", - element, - ...normalizeParams(params) - }); -} -function _custom(Class2, fn, _params) { - const norm = normalizeParams(_params); - norm.abort ?? (norm.abort = true); - const schema = new Class2({ - type: "custom", - check: "custom", - fn, - ...norm - }); - return schema; -} -function _refine(Class2, fn, _params) { - const schema = new Class2({ - type: "custom", - check: "custom", - fn, - ...normalizeParams(_params) - }); - return schema; -} -// node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.js -function isZ4Schema(s) { - const schema = s; - return !!schema._zod; -} -function safeParse2(schema, data) { - if (isZ4Schema(schema)) { - const result2 = safeParse(schema, data); - return result2; - } - const v3Schema = schema; - const result = v3Schema.safeParse(data); - return result; -} -function getObjectShape(schema) { - if (!schema) - return; - let rawShape; - if (isZ4Schema(schema)) { - const v4Schema = schema; - rawShape = v4Schema._zod?.def?.shape; - } else { - const v3Schema = schema; - rawShape = v3Schema.shape; - } - if (!rawShape) - return; - if (typeof rawShape === "function") { - try { - return rawShape(); - } catch { - return; - } - } - return rawShape; -} -function getLiteralValue(schema) { - if (isZ4Schema(schema)) { - const v4Schema = schema; - const def2 = v4Schema._zod?.def; - if (def2) { - if (def2.value !== undefined) - return def2.value; - if (Array.isArray(def2.values) && def2.values.length > 0) { - return def2.values[0]; - } - } - } - const v3Schema = schema; - const def = v3Schema._def; - if (def) { - if (def.value !== undefined) - return def.value; - if (Array.isArray(def.values) && def.values.length > 0) { - return def.values[0]; - } - } - const directValue = schema.value; - if (directValue !== undefined) - return directValue; - return; -} -// node_modules/@modelcontextprotocol/sdk/node_modules/zod/v4/classic/iso.js -var exports_iso = {}; -__export(exports_iso, { - time: () => time2, - duration: () => duration2, - datetime: () => datetime2, - date: () => date2, - ZodISOTime: () => ZodISOTime, - ZodISODuration: () => ZodISODuration, - ZodISODateTime: () => ZodISODateTime, - ZodISODate: () => ZodISODate -}); -var ZodISODateTime = /* @__PURE__ */ $constructor("ZodISODateTime", (inst, def) => { - $ZodISODateTime.init(inst, def); - ZodStringFormat.init(inst, def); -}); -function datetime2(params) { - return _isoDateTime(ZodISODateTime, params); -} -var ZodISODate = /* @__PURE__ */ $constructor("ZodISODate", (inst, def) => { - $ZodISODate.init(inst, def); - ZodStringFormat.init(inst, def); -}); -function date2(params) { - return _isoDate(ZodISODate, params); -} -var ZodISOTime = /* @__PURE__ */ $constructor("ZodISOTime", (inst, def) => { - $ZodISOTime.init(inst, def); - ZodStringFormat.init(inst, def); -}); -function time2(params) { - return _isoTime(ZodISOTime, params); -} -var ZodISODuration = /* @__PURE__ */ $constructor("ZodISODuration", (inst, def) => { - $ZodISODuration.init(inst, def); - ZodStringFormat.init(inst, def); -}); -function duration2(params) { - return _isoDuration(ZodISODuration, params); -} - -// node_modules/@modelcontextprotocol/sdk/node_modules/zod/v4/classic/errors.js -var initializer2 = (inst, issues) => { - $ZodError.init(inst, issues); - inst.name = "ZodError"; - Object.defineProperties(inst, { - format: { - value: (mapper) => formatError(inst, mapper) - }, - flatten: { - value: (mapper) => flattenError(inst, mapper) - }, - addIssue: { - value: (issue2) => inst.issues.push(issue2) - }, - addIssues: { - value: (issues2) => inst.issues.push(...issues2) - }, - isEmpty: { - get() { - return inst.issues.length === 0; - } - } - }); -}; -var ZodError = $constructor("ZodError", initializer2); -var ZodRealError = $constructor("ZodError", initializer2, { - Parent: Error -}); - -// node_modules/@modelcontextprotocol/sdk/node_modules/zod/v4/classic/parse.js -var parse3 = /* @__PURE__ */ _parse(ZodRealError); -var parseAsync2 = /* @__PURE__ */ _parseAsync(ZodRealError); -var safeParse3 = /* @__PURE__ */ _safeParse(ZodRealError); -var safeParseAsync2 = /* @__PURE__ */ _safeParseAsync(ZodRealError); - -// node_modules/@modelcontextprotocol/sdk/node_modules/zod/v4/classic/schemas.js -var ZodType = /* @__PURE__ */ $constructor("ZodType", (inst, def) => { - $ZodType.init(inst, def); - inst.def = def; - Object.defineProperty(inst, "_def", { value: def }); - inst.check = (...checks2) => { - return inst.clone({ - ...def, - checks: [ - ...def.checks ?? [], - ...checks2.map((ch) => typeof ch === "function" ? { _zod: { check: ch, def: { check: "custom" }, onattach: [] } } : ch) - ] - }); - }; - inst.clone = (def2, params) => clone(inst, def2, params); - inst.brand = () => inst; - inst.register = (reg, meta) => { - reg.add(inst, meta); - return inst; - }; - inst.parse = (data, params) => parse3(inst, data, params, { callee: inst.parse }); - inst.safeParse = (data, params) => safeParse3(inst, data, params); - inst.parseAsync = async (data, params) => parseAsync2(inst, data, params, { callee: inst.parseAsync }); - inst.safeParseAsync = async (data, params) => safeParseAsync2(inst, data, params); - inst.spa = inst.safeParseAsync; - inst.refine = (check, params) => inst.check(refine(check, params)); - inst.superRefine = (refinement) => inst.check(superRefine(refinement)); - inst.overwrite = (fn) => inst.check(_overwrite(fn)); - inst.optional = () => optional(inst); - inst.nullable = () => nullable(inst); - inst.nullish = () => optional(nullable(inst)); - inst.nonoptional = (params) => nonoptional(inst, params); - inst.array = () => array(inst); - inst.or = (arg) => union([inst, arg]); - inst.and = (arg) => intersection(inst, arg); - inst.transform = (tx) => pipe(inst, transform(tx)); - inst.default = (def2) => _default(inst, def2); - inst.prefault = (def2) => prefault(inst, def2); - inst.catch = (params) => _catch(inst, params); - inst.pipe = (target) => pipe(inst, target); - inst.readonly = () => readonly(inst); - inst.describe = (description) => { - const cl = inst.clone(); - globalRegistry.add(cl, { description }); - return cl; - }; - Object.defineProperty(inst, "description", { - get() { - return globalRegistry.get(inst)?.description; - }, - configurable: true - }); - inst.meta = (...args) => { - if (args.length === 0) { - return globalRegistry.get(inst); - } - const cl = inst.clone(); - globalRegistry.add(cl, args[0]); - return cl; - }; - inst.isOptional = () => inst.safeParse(undefined).success; - inst.isNullable = () => inst.safeParse(null).success; - return inst; -}); -var _ZodString = /* @__PURE__ */ $constructor("_ZodString", (inst, def) => { - $ZodString.init(inst, def); - ZodType.init(inst, def); - const bag = inst._zod.bag; - inst.format = bag.format ?? null; - inst.minLength = bag.minimum ?? null; - inst.maxLength = bag.maximum ?? null; - inst.regex = (...args) => inst.check(_regex(...args)); - inst.includes = (...args) => inst.check(_includes(...args)); - inst.startsWith = (...args) => inst.check(_startsWith(...args)); - inst.endsWith = (...args) => inst.check(_endsWith(...args)); - inst.min = (...args) => inst.check(_minLength(...args)); - inst.max = (...args) => inst.check(_maxLength(...args)); - inst.length = (...args) => inst.check(_length(...args)); - inst.nonempty = (...args) => inst.check(_minLength(1, ...args)); - inst.lowercase = (params) => inst.check(_lowercase(params)); - inst.uppercase = (params) => inst.check(_uppercase(params)); - inst.trim = () => inst.check(_trim()); - inst.normalize = (...args) => inst.check(_normalize(...args)); - inst.toLowerCase = () => inst.check(_toLowerCase()); - inst.toUpperCase = () => inst.check(_toUpperCase()); -}); -var ZodString = /* @__PURE__ */ $constructor("ZodString", (inst, def) => { - $ZodString.init(inst, def); - _ZodString.init(inst, def); - inst.email = (params) => inst.check(_email(ZodEmail, params)); - inst.url = (params) => inst.check(_url(ZodURL, params)); - inst.jwt = (params) => inst.check(_jwt(ZodJWT, params)); - inst.emoji = (params) => inst.check(_emoji2(ZodEmoji, params)); - inst.guid = (params) => inst.check(_guid(ZodGUID, params)); - inst.uuid = (params) => inst.check(_uuid(ZodUUID, params)); - inst.uuidv4 = (params) => inst.check(_uuidv4(ZodUUID, params)); - inst.uuidv6 = (params) => inst.check(_uuidv6(ZodUUID, params)); - inst.uuidv7 = (params) => inst.check(_uuidv7(ZodUUID, params)); - inst.nanoid = (params) => inst.check(_nanoid(ZodNanoID, params)); - inst.guid = (params) => inst.check(_guid(ZodGUID, params)); - inst.cuid = (params) => inst.check(_cuid(ZodCUID, params)); - inst.cuid2 = (params) => inst.check(_cuid2(ZodCUID2, params)); - inst.ulid = (params) => inst.check(_ulid(ZodULID, params)); - inst.base64 = (params) => inst.check(_base64(ZodBase64, params)); - inst.base64url = (params) => inst.check(_base64url(ZodBase64URL, params)); - inst.xid = (params) => inst.check(_xid(ZodXID, params)); - inst.ksuid = (params) => inst.check(_ksuid(ZodKSUID, params)); - inst.ipv4 = (params) => inst.check(_ipv4(ZodIPv4, params)); - inst.ipv6 = (params) => inst.check(_ipv6(ZodIPv6, params)); - inst.cidrv4 = (params) => inst.check(_cidrv4(ZodCIDRv4, params)); - inst.cidrv6 = (params) => inst.check(_cidrv6(ZodCIDRv6, params)); - inst.e164 = (params) => inst.check(_e164(ZodE164, params)); - inst.datetime = (params) => inst.check(datetime2(params)); - inst.date = (params) => inst.check(date2(params)); - inst.time = (params) => inst.check(time2(params)); - inst.duration = (params) => inst.check(duration2(params)); -}); -function string2(params) { - return _string(ZodString, params); -} -var ZodStringFormat = /* @__PURE__ */ $constructor("ZodStringFormat", (inst, def) => { - $ZodStringFormat.init(inst, def); - _ZodString.init(inst, def); -}); -var ZodEmail = /* @__PURE__ */ $constructor("ZodEmail", (inst, def) => { - $ZodEmail.init(inst, def); - ZodStringFormat.init(inst, def); -}); -var ZodGUID = /* @__PURE__ */ $constructor("ZodGUID", (inst, def) => { - $ZodGUID.init(inst, def); - ZodStringFormat.init(inst, def); -}); -var ZodUUID = /* @__PURE__ */ $constructor("ZodUUID", (inst, def) => { - $ZodUUID.init(inst, def); - ZodStringFormat.init(inst, def); -}); -var ZodURL = /* @__PURE__ */ $constructor("ZodURL", (inst, def) => { - $ZodURL.init(inst, def); - ZodStringFormat.init(inst, def); -}); -var ZodEmoji = /* @__PURE__ */ $constructor("ZodEmoji", (inst, def) => { - $ZodEmoji.init(inst, def); - ZodStringFormat.init(inst, def); -}); -var ZodNanoID = /* @__PURE__ */ $constructor("ZodNanoID", (inst, def) => { - $ZodNanoID.init(inst, def); - ZodStringFormat.init(inst, def); -}); -var ZodCUID = /* @__PURE__ */ $constructor("ZodCUID", (inst, def) => { - $ZodCUID.init(inst, def); - ZodStringFormat.init(inst, def); -}); -var ZodCUID2 = /* @__PURE__ */ $constructor("ZodCUID2", (inst, def) => { - $ZodCUID2.init(inst, def); - ZodStringFormat.init(inst, def); -}); -var ZodULID = /* @__PURE__ */ $constructor("ZodULID", (inst, def) => { - $ZodULID.init(inst, def); - ZodStringFormat.init(inst, def); -}); -var ZodXID = /* @__PURE__ */ $constructor("ZodXID", (inst, def) => { - $ZodXID.init(inst, def); - ZodStringFormat.init(inst, def); -}); -var ZodKSUID = /* @__PURE__ */ $constructor("ZodKSUID", (inst, def) => { - $ZodKSUID.init(inst, def); - ZodStringFormat.init(inst, def); -}); -var ZodIPv4 = /* @__PURE__ */ $constructor("ZodIPv4", (inst, def) => { - $ZodIPv4.init(inst, def); - ZodStringFormat.init(inst, def); -}); -var ZodIPv6 = /* @__PURE__ */ $constructor("ZodIPv6", (inst, def) => { - $ZodIPv6.init(inst, def); - ZodStringFormat.init(inst, def); -}); -var ZodCIDRv4 = /* @__PURE__ */ $constructor("ZodCIDRv4", (inst, def) => { - $ZodCIDRv4.init(inst, def); - ZodStringFormat.init(inst, def); -}); -var ZodCIDRv6 = /* @__PURE__ */ $constructor("ZodCIDRv6", (inst, def) => { - $ZodCIDRv6.init(inst, def); - ZodStringFormat.init(inst, def); -}); -var ZodBase64 = /* @__PURE__ */ $constructor("ZodBase64", (inst, def) => { - $ZodBase64.init(inst, def); - ZodStringFormat.init(inst, def); -}); -var ZodBase64URL = /* @__PURE__ */ $constructor("ZodBase64URL", (inst, def) => { - $ZodBase64URL.init(inst, def); - ZodStringFormat.init(inst, def); -}); -var ZodE164 = /* @__PURE__ */ $constructor("ZodE164", (inst, def) => { - $ZodE164.init(inst, def); - ZodStringFormat.init(inst, def); -}); -var ZodJWT = /* @__PURE__ */ $constructor("ZodJWT", (inst, def) => { - $ZodJWT.init(inst, def); - ZodStringFormat.init(inst, def); -}); -var ZodNumber = /* @__PURE__ */ $constructor("ZodNumber", (inst, def) => { - $ZodNumber.init(inst, def); - ZodType.init(inst, def); - inst.gt = (value, params) => inst.check(_gt(value, params)); - inst.gte = (value, params) => inst.check(_gte(value, params)); - inst.min = (value, params) => inst.check(_gte(value, params)); - inst.lt = (value, params) => inst.check(_lt(value, params)); - inst.lte = (value, params) => inst.check(_lte(value, params)); - inst.max = (value, params) => inst.check(_lte(value, params)); - inst.int = (params) => inst.check(int(params)); - inst.safe = (params) => inst.check(int(params)); - inst.positive = (params) => inst.check(_gt(0, params)); - inst.nonnegative = (params) => inst.check(_gte(0, params)); - inst.negative = (params) => inst.check(_lt(0, params)); - inst.nonpositive = (params) => inst.check(_lte(0, params)); - inst.multipleOf = (value, params) => inst.check(_multipleOf(value, params)); - inst.step = (value, params) => inst.check(_multipleOf(value, params)); - inst.finite = () => inst; - const bag = inst._zod.bag; - inst.minValue = Math.max(bag.minimum ?? Number.NEGATIVE_INFINITY, bag.exclusiveMinimum ?? Number.NEGATIVE_INFINITY) ?? null; - inst.maxValue = Math.min(bag.maximum ?? Number.POSITIVE_INFINITY, bag.exclusiveMaximum ?? Number.POSITIVE_INFINITY) ?? null; - inst.isInt = (bag.format ?? "").includes("int") || Number.isSafeInteger(bag.multipleOf ?? 0.5); - inst.isFinite = true; - inst.format = bag.format ?? null; -}); -function number2(params) { - return _number(ZodNumber, params); -} -var ZodNumberFormat = /* @__PURE__ */ $constructor("ZodNumberFormat", (inst, def) => { - $ZodNumberFormat.init(inst, def); - ZodNumber.init(inst, def); -}); -function int(params) { - return _int(ZodNumberFormat, params); -} -var ZodBoolean = /* @__PURE__ */ $constructor("ZodBoolean", (inst, def) => { - $ZodBoolean.init(inst, def); - ZodType.init(inst, def); -}); -function boolean2(params) { - return _boolean(ZodBoolean, params); -} -var ZodNull = /* @__PURE__ */ $constructor("ZodNull", (inst, def) => { - $ZodNull.init(inst, def); - ZodType.init(inst, def); -}); -function _null3(params) { - return _null2(ZodNull, params); -} -var ZodUnknown = /* @__PURE__ */ $constructor("ZodUnknown", (inst, def) => { - $ZodUnknown.init(inst, def); - ZodType.init(inst, def); -}); -function unknown() { - return _unknown(ZodUnknown); -} -var ZodNever = /* @__PURE__ */ $constructor("ZodNever", (inst, def) => { - $ZodNever.init(inst, def); - ZodType.init(inst, def); -}); -function never(params) { - return _never(ZodNever, params); -} -var ZodArray = /* @__PURE__ */ $constructor("ZodArray", (inst, def) => { - $ZodArray.init(inst, def); - ZodType.init(inst, def); - inst.element = def.element; - inst.min = (minLength, params) => inst.check(_minLength(minLength, params)); - inst.nonempty = (params) => inst.check(_minLength(1, params)); - inst.max = (maxLength, params) => inst.check(_maxLength(maxLength, params)); - inst.length = (len, params) => inst.check(_length(len, params)); - inst.unwrap = () => inst.element; -}); -function array(element, params) { - return _array(ZodArray, element, params); -} -var ZodObject = /* @__PURE__ */ $constructor("ZodObject", (inst, def) => { - $ZodObject.init(inst, def); - ZodType.init(inst, def); - exports_util.defineLazy(inst, "shape", () => def.shape); - inst.keyof = () => _enum(Object.keys(inst._zod.def.shape)); - inst.catchall = (catchall) => inst.clone({ ...inst._zod.def, catchall }); - inst.passthrough = () => inst.clone({ ...inst._zod.def, catchall: unknown() }); - inst.loose = () => inst.clone({ ...inst._zod.def, catchall: unknown() }); - inst.strict = () => inst.clone({ ...inst._zod.def, catchall: never() }); - inst.strip = () => inst.clone({ ...inst._zod.def, catchall: undefined }); - inst.extend = (incoming) => { - return exports_util.extend(inst, incoming); - }; - inst.merge = (other) => exports_util.merge(inst, other); - inst.pick = (mask) => exports_util.pick(inst, mask); - inst.omit = (mask) => exports_util.omit(inst, mask); - inst.partial = (...args) => exports_util.partial(ZodOptional, inst, args[0]); - inst.required = (...args) => exports_util.required(ZodNonOptional, inst, args[0]); -}); -function object2(shape, params) { - const def = { - type: "object", - get shape() { - exports_util.assignProp(this, "shape", { ...shape }); - return this.shape; - }, - ...exports_util.normalizeParams(params) - }; - return new ZodObject(def); -} -function looseObject(shape, params) { - return new ZodObject({ - type: "object", - get shape() { - exports_util.assignProp(this, "shape", { ...shape }); - return this.shape; - }, - catchall: unknown(), - ...exports_util.normalizeParams(params) - }); -} -var ZodUnion = /* @__PURE__ */ $constructor("ZodUnion", (inst, def) => { - $ZodUnion.init(inst, def); - ZodType.init(inst, def); - inst.options = def.options; -}); -function union(options, params) { - return new ZodUnion({ - type: "union", - options, - ...exports_util.normalizeParams(params) - }); -} -var ZodDiscriminatedUnion = /* @__PURE__ */ $constructor("ZodDiscriminatedUnion", (inst, def) => { - ZodUnion.init(inst, def); - $ZodDiscriminatedUnion.init(inst, def); -}); -function discriminatedUnion(discriminator, options, params) { - return new ZodDiscriminatedUnion({ - type: "union", - options, - discriminator, - ...exports_util.normalizeParams(params) - }); -} -var ZodIntersection = /* @__PURE__ */ $constructor("ZodIntersection", (inst, def) => { - $ZodIntersection.init(inst, def); - ZodType.init(inst, def); -}); -function intersection(left, right) { - return new ZodIntersection({ - type: "intersection", - left, - right - }); -} -var ZodRecord = /* @__PURE__ */ $constructor("ZodRecord", (inst, def) => { - $ZodRecord.init(inst, def); - ZodType.init(inst, def); - inst.keyType = def.keyType; - inst.valueType = def.valueType; -}); -function record(keyType, valueType, params) { - return new ZodRecord({ - type: "record", - keyType, - valueType, - ...exports_util.normalizeParams(params) - }); -} -var ZodEnum = /* @__PURE__ */ $constructor("ZodEnum", (inst, def) => { - $ZodEnum.init(inst, def); - ZodType.init(inst, def); - inst.enum = def.entries; - inst.options = Object.values(def.entries); - const keys = new Set(Object.keys(def.entries)); - inst.extract = (values, params) => { - const newEntries = {}; - for (const value of values) { - if (keys.has(value)) { - newEntries[value] = def.entries[value]; - } else - throw new Error(`Key ${value} not found in enum`); - } - return new ZodEnum({ - ...def, - checks: [], - ...exports_util.normalizeParams(params), - entries: newEntries - }); - }; - inst.exclude = (values, params) => { - const newEntries = { ...def.entries }; - for (const value of values) { - if (keys.has(value)) { - delete newEntries[value]; - } else - throw new Error(`Key ${value} not found in enum`); - } - return new ZodEnum({ - ...def, - checks: [], - ...exports_util.normalizeParams(params), - entries: newEntries - }); - }; -}); -function _enum(values, params) { - const entries = Array.isArray(values) ? Object.fromEntries(values.map((v) => [v, v])) : values; - return new ZodEnum({ - type: "enum", - entries, - ...exports_util.normalizeParams(params) - }); -} -var ZodLiteral = /* @__PURE__ */ $constructor("ZodLiteral", (inst, def) => { - $ZodLiteral.init(inst, def); - ZodType.init(inst, def); - inst.values = new Set(def.values); - Object.defineProperty(inst, "value", { - get() { - if (def.values.length > 1) { - throw new Error("This schema contains multiple valid literal values. Use `.values` instead."); - } - return def.values[0]; - } - }); -}); -function literal(value, params) { - return new ZodLiteral({ - type: "literal", - values: Array.isArray(value) ? value : [value], - ...exports_util.normalizeParams(params) - }); -} -var ZodTransform = /* @__PURE__ */ $constructor("ZodTransform", (inst, def) => { - $ZodTransform.init(inst, def); - ZodType.init(inst, def); - inst._zod.parse = (payload, _ctx) => { - payload.addIssue = (issue2) => { - if (typeof issue2 === "string") { - payload.issues.push(exports_util.issue(issue2, payload.value, def)); - } else { - const _issue = issue2; - if (_issue.fatal) - _issue.continue = false; - _issue.code ?? (_issue.code = "custom"); - _issue.input ?? (_issue.input = payload.value); - _issue.inst ?? (_issue.inst = inst); - _issue.continue ?? (_issue.continue = true); - payload.issues.push(exports_util.issue(_issue)); - } - }; - const output = def.transform(payload.value, payload); - if (output instanceof Promise) { - return output.then((output2) => { - payload.value = output2; - return payload; - }); - } - payload.value = output; - return payload; - }; -}); -function transform(fn) { - return new ZodTransform({ - type: "transform", - transform: fn - }); -} -var ZodOptional = /* @__PURE__ */ $constructor("ZodOptional", (inst, def) => { - $ZodOptional.init(inst, def); - ZodType.init(inst, def); - inst.unwrap = () => inst._zod.def.innerType; -}); -function optional(innerType) { - return new ZodOptional({ - type: "optional", - innerType - }); -} -var ZodNullable = /* @__PURE__ */ $constructor("ZodNullable", (inst, def) => { - $ZodNullable.init(inst, def); - ZodType.init(inst, def); - inst.unwrap = () => inst._zod.def.innerType; -}); -function nullable(innerType) { - return new ZodNullable({ - type: "nullable", - innerType - }); -} -var ZodDefault = /* @__PURE__ */ $constructor("ZodDefault", (inst, def) => { - $ZodDefault.init(inst, def); - ZodType.init(inst, def); - inst.unwrap = () => inst._zod.def.innerType; - inst.removeDefault = inst.unwrap; -}); -function _default(innerType, defaultValue) { - return new ZodDefault({ - type: "default", - innerType, - get defaultValue() { - return typeof defaultValue === "function" ? defaultValue() : defaultValue; - } - }); -} -var ZodPrefault = /* @__PURE__ */ $constructor("ZodPrefault", (inst, def) => { - $ZodPrefault.init(inst, def); - ZodType.init(inst, def); - inst.unwrap = () => inst._zod.def.innerType; -}); -function prefault(innerType, defaultValue) { - return new ZodPrefault({ - type: "prefault", - innerType, - get defaultValue() { - return typeof defaultValue === "function" ? defaultValue() : defaultValue; - } - }); -} -var ZodNonOptional = /* @__PURE__ */ $constructor("ZodNonOptional", (inst, def) => { - $ZodNonOptional.init(inst, def); - ZodType.init(inst, def); - inst.unwrap = () => inst._zod.def.innerType; -}); -function nonoptional(innerType, params) { - return new ZodNonOptional({ - type: "nonoptional", - innerType, - ...exports_util.normalizeParams(params) - }); -} -var ZodCatch = /* @__PURE__ */ $constructor("ZodCatch", (inst, def) => { - $ZodCatch.init(inst, def); - ZodType.init(inst, def); - inst.unwrap = () => inst._zod.def.innerType; - inst.removeCatch = inst.unwrap; -}); -function _catch(innerType, catchValue) { - return new ZodCatch({ - type: "catch", - innerType, - catchValue: typeof catchValue === "function" ? catchValue : () => catchValue - }); -} -var ZodPipe = /* @__PURE__ */ $constructor("ZodPipe", (inst, def) => { - $ZodPipe.init(inst, def); - ZodType.init(inst, def); - inst.in = def.in; - inst.out = def.out; -}); -function pipe(in_, out) { - return new ZodPipe({ - type: "pipe", - in: in_, - out - }); -} -var ZodReadonly = /* @__PURE__ */ $constructor("ZodReadonly", (inst, def) => { - $ZodReadonly.init(inst, def); - ZodType.init(inst, def); -}); -function readonly(innerType) { - return new ZodReadonly({ - type: "readonly", - innerType - }); -} -var ZodCustom = /* @__PURE__ */ $constructor("ZodCustom", (inst, def) => { - $ZodCustom.init(inst, def); - ZodType.init(inst, def); -}); -function check(fn) { - const ch = new $ZodCheck({ - check: "custom" - }); - ch._zod.check = fn; - return ch; -} -function custom(fn, _params) { - return _custom(ZodCustom, fn ?? (() => true), _params); -} -function refine(fn, _params = {}) { - return _refine(ZodCustom, fn, _params); -} -function superRefine(fn) { - const ch = check((payload) => { - payload.addIssue = (issue2) => { - if (typeof issue2 === "string") { - payload.issues.push(exports_util.issue(issue2, payload.value, ch._zod.def)); - } else { - const _issue = issue2; - if (_issue.fatal) - _issue.continue = false; - _issue.code ?? (_issue.code = "custom"); - _issue.input ?? (_issue.input = payload.value); - _issue.inst ?? (_issue.inst = ch); - _issue.continue ?? (_issue.continue = !ch._zod.def.abort); - payload.issues.push(exports_util.issue(_issue)); - } - }; - return fn(payload.value, payload); - }); - return ch; -} -function preprocess(fn, schema) { - return pipe(transform(fn), schema); -} -// node_modules/@modelcontextprotocol/sdk/node_modules/zod/v4/classic/external.js -config(en_default()); - -// node_modules/@modelcontextprotocol/sdk/dist/esm/types.js -var LATEST_PROTOCOL_VERSION = "2025-11-25"; -var SUPPORTED_PROTOCOL_VERSIONS = [LATEST_PROTOCOL_VERSION, "2025-06-18", "2025-03-26", "2024-11-05", "2024-10-07"]; -var RELATED_TASK_META_KEY = "io.modelcontextprotocol/related-task"; -var JSONRPC_VERSION = "2.0"; -var AssertObjectSchema = custom((v) => v !== null && (typeof v === "object" || typeof v === "function")); -var ProgressTokenSchema = union([string2(), number2().int()]); -var CursorSchema = string2(); -var TaskCreationParamsSchema = looseObject({ - ttl: union([number2(), _null3()]).optional(), - pollInterval: number2().optional() -}); -var TaskMetadataSchema = object2({ - ttl: number2().optional() -}); -var RelatedTaskMetadataSchema = object2({ - taskId: string2() -}); -var RequestMetaSchema = looseObject({ - progressToken: ProgressTokenSchema.optional(), - [RELATED_TASK_META_KEY]: RelatedTaskMetadataSchema.optional() -}); -var BaseRequestParamsSchema = object2({ - _meta: RequestMetaSchema.optional() -}); -var TaskAugmentedRequestParamsSchema = BaseRequestParamsSchema.extend({ - task: TaskMetadataSchema.optional() -}); -var isTaskAugmentedRequestParams = (value) => TaskAugmentedRequestParamsSchema.safeParse(value).success; -var RequestSchema = object2({ - method: string2(), - params: BaseRequestParamsSchema.loose().optional() -}); -var NotificationsParamsSchema = object2({ - _meta: RequestMetaSchema.optional() -}); -var NotificationSchema = object2({ - method: string2(), - params: NotificationsParamsSchema.loose().optional() -}); -var ResultSchema = looseObject({ - _meta: RequestMetaSchema.optional() -}); -var RequestIdSchema = union([string2(), number2().int()]); -var JSONRPCRequestSchema = object2({ - jsonrpc: literal(JSONRPC_VERSION), - id: RequestIdSchema, - ...RequestSchema.shape -}).strict(); -var isJSONRPCRequest = (value) => JSONRPCRequestSchema.safeParse(value).success; -var JSONRPCNotificationSchema = object2({ - jsonrpc: literal(JSONRPC_VERSION), - ...NotificationSchema.shape -}).strict(); -var isJSONRPCNotification = (value) => JSONRPCNotificationSchema.safeParse(value).success; -var JSONRPCResultResponseSchema = object2({ - jsonrpc: literal(JSONRPC_VERSION), - id: RequestIdSchema, - result: ResultSchema -}).strict(); -var isJSONRPCResultResponse = (value) => JSONRPCResultResponseSchema.safeParse(value).success; -var ErrorCode; -(function(ErrorCode2) { - ErrorCode2[ErrorCode2["ConnectionClosed"] = -32000] = "ConnectionClosed"; - ErrorCode2[ErrorCode2["RequestTimeout"] = -32001] = "RequestTimeout"; - ErrorCode2[ErrorCode2["ParseError"] = -32700] = "ParseError"; - ErrorCode2[ErrorCode2["InvalidRequest"] = -32600] = "InvalidRequest"; - ErrorCode2[ErrorCode2["MethodNotFound"] = -32601] = "MethodNotFound"; - ErrorCode2[ErrorCode2["InvalidParams"] = -32602] = "InvalidParams"; - ErrorCode2[ErrorCode2["InternalError"] = -32603] = "InternalError"; - ErrorCode2[ErrorCode2["UrlElicitationRequired"] = -32042] = "UrlElicitationRequired"; -})(ErrorCode || (ErrorCode = {})); -var JSONRPCErrorResponseSchema = object2({ - jsonrpc: literal(JSONRPC_VERSION), - id: RequestIdSchema.optional(), - error: object2({ - code: number2().int(), - message: string2(), - data: unknown().optional() - }) -}).strict(); -var isJSONRPCErrorResponse = (value) => JSONRPCErrorResponseSchema.safeParse(value).success; -var JSONRPCMessageSchema = union([ - JSONRPCRequestSchema, - JSONRPCNotificationSchema, - JSONRPCResultResponseSchema, - JSONRPCErrorResponseSchema -]); -var JSONRPCResponseSchema = union([JSONRPCResultResponseSchema, JSONRPCErrorResponseSchema]); -var EmptyResultSchema = ResultSchema.strict(); -var CancelledNotificationParamsSchema = NotificationsParamsSchema.extend({ - requestId: RequestIdSchema.optional(), - reason: string2().optional() -}); -var CancelledNotificationSchema = NotificationSchema.extend({ - method: literal("notifications/cancelled"), - params: CancelledNotificationParamsSchema -}); -var IconSchema = object2({ - src: string2(), - mimeType: string2().optional(), - sizes: array(string2()).optional(), - theme: _enum(["light", "dark"]).optional() -}); -var IconsSchema = object2({ - icons: array(IconSchema).optional() -}); -var BaseMetadataSchema = object2({ - name: string2(), - title: string2().optional() -}); -var ImplementationSchema = BaseMetadataSchema.extend({ - ...BaseMetadataSchema.shape, - ...IconsSchema.shape, - version: string2(), - websiteUrl: string2().optional(), - description: string2().optional() -}); -var FormElicitationCapabilitySchema = intersection(object2({ - applyDefaults: boolean2().optional() -}), record(string2(), unknown())); -var ElicitationCapabilitySchema = preprocess((value) => { - if (value && typeof value === "object" && !Array.isArray(value)) { - if (Object.keys(value).length === 0) { - return { form: {} }; - } - } - return value; -}, intersection(object2({ - form: FormElicitationCapabilitySchema.optional(), - url: AssertObjectSchema.optional() -}), record(string2(), unknown()).optional())); -var ClientTasksCapabilitySchema = looseObject({ - list: AssertObjectSchema.optional(), - cancel: AssertObjectSchema.optional(), - requests: looseObject({ - sampling: looseObject({ - createMessage: AssertObjectSchema.optional() - }).optional(), - elicitation: looseObject({ - create: AssertObjectSchema.optional() - }).optional() - }).optional() -}); -var ServerTasksCapabilitySchema = looseObject({ - list: AssertObjectSchema.optional(), - cancel: AssertObjectSchema.optional(), - requests: looseObject({ - tools: looseObject({ - call: AssertObjectSchema.optional() - }).optional() - }).optional() -}); -var ClientCapabilitiesSchema = object2({ - experimental: record(string2(), AssertObjectSchema).optional(), - sampling: object2({ - context: AssertObjectSchema.optional(), - tools: AssertObjectSchema.optional() - }).optional(), - elicitation: ElicitationCapabilitySchema.optional(), - roots: object2({ - listChanged: boolean2().optional() - }).optional(), - tasks: ClientTasksCapabilitySchema.optional() -}); -var InitializeRequestParamsSchema = BaseRequestParamsSchema.extend({ - protocolVersion: string2(), - capabilities: ClientCapabilitiesSchema, - clientInfo: ImplementationSchema -}); -var InitializeRequestSchema = RequestSchema.extend({ - method: literal("initialize"), - params: InitializeRequestParamsSchema -}); -var ServerCapabilitiesSchema = object2({ - experimental: record(string2(), AssertObjectSchema).optional(), - logging: AssertObjectSchema.optional(), - completions: AssertObjectSchema.optional(), - prompts: object2({ - listChanged: boolean2().optional() - }).optional(), - resources: object2({ - subscribe: boolean2().optional(), - listChanged: boolean2().optional() - }).optional(), - tools: object2({ - listChanged: boolean2().optional() - }).optional(), - tasks: ServerTasksCapabilitySchema.optional() -}); -var InitializeResultSchema = ResultSchema.extend({ - protocolVersion: string2(), - capabilities: ServerCapabilitiesSchema, - serverInfo: ImplementationSchema, - instructions: string2().optional() -}); -var InitializedNotificationSchema = NotificationSchema.extend({ - method: literal("notifications/initialized"), - params: NotificationsParamsSchema.optional() -}); -var PingRequestSchema = RequestSchema.extend({ - method: literal("ping"), - params: BaseRequestParamsSchema.optional() -}); -var ProgressSchema = object2({ - progress: number2(), - total: optional(number2()), - message: optional(string2()) -}); -var ProgressNotificationParamsSchema = object2({ - ...NotificationsParamsSchema.shape, - ...ProgressSchema.shape, - progressToken: ProgressTokenSchema -}); -var ProgressNotificationSchema = NotificationSchema.extend({ - method: literal("notifications/progress"), - params: ProgressNotificationParamsSchema -}); -var PaginatedRequestParamsSchema = BaseRequestParamsSchema.extend({ - cursor: CursorSchema.optional() -}); -var PaginatedRequestSchema = RequestSchema.extend({ - params: PaginatedRequestParamsSchema.optional() -}); -var PaginatedResultSchema = ResultSchema.extend({ - nextCursor: CursorSchema.optional() -}); -var TaskStatusSchema = _enum(["working", "input_required", "completed", "failed", "cancelled"]); -var TaskSchema = object2({ - taskId: string2(), - status: TaskStatusSchema, - ttl: union([number2(), _null3()]), - createdAt: string2(), - lastUpdatedAt: string2(), - pollInterval: optional(number2()), - statusMessage: optional(string2()) -}); -var CreateTaskResultSchema = ResultSchema.extend({ - task: TaskSchema -}); -var TaskStatusNotificationParamsSchema = NotificationsParamsSchema.merge(TaskSchema); -var TaskStatusNotificationSchema = NotificationSchema.extend({ - method: literal("notifications/tasks/status"), - params: TaskStatusNotificationParamsSchema -}); -var GetTaskRequestSchema = RequestSchema.extend({ - method: literal("tasks/get"), - params: BaseRequestParamsSchema.extend({ - taskId: string2() - }) -}); -var GetTaskResultSchema = ResultSchema.merge(TaskSchema); -var GetTaskPayloadRequestSchema = RequestSchema.extend({ - method: literal("tasks/result"), - params: BaseRequestParamsSchema.extend({ - taskId: string2() - }) -}); -var GetTaskPayloadResultSchema = ResultSchema.loose(); -var ListTasksRequestSchema = PaginatedRequestSchema.extend({ - method: literal("tasks/list") -}); -var ListTasksResultSchema = PaginatedResultSchema.extend({ - tasks: array(TaskSchema) -}); -var CancelTaskRequestSchema = RequestSchema.extend({ - method: literal("tasks/cancel"), - params: BaseRequestParamsSchema.extend({ - taskId: string2() - }) -}); -var CancelTaskResultSchema = ResultSchema.merge(TaskSchema); -var ResourceContentsSchema = object2({ - uri: string2(), - mimeType: optional(string2()), - _meta: record(string2(), unknown()).optional() -}); -var TextResourceContentsSchema = ResourceContentsSchema.extend({ - text: string2() -}); -var Base64Schema = string2().refine((val) => { - try { - atob(val); - return true; - } catch { - return false; - } -}, { message: "Invalid Base64 string" }); -var BlobResourceContentsSchema = ResourceContentsSchema.extend({ - blob: Base64Schema -}); -var RoleSchema = _enum(["user", "assistant"]); -var AnnotationsSchema = object2({ - audience: array(RoleSchema).optional(), - priority: number2().min(0).max(1).optional(), - lastModified: exports_iso.datetime({ offset: true }).optional() -}); -var ResourceSchema = object2({ - ...BaseMetadataSchema.shape, - ...IconsSchema.shape, - uri: string2(), - description: optional(string2()), - mimeType: optional(string2()), - annotations: AnnotationsSchema.optional(), - _meta: optional(looseObject({})) -}); -var ResourceTemplateSchema = object2({ - ...BaseMetadataSchema.shape, - ...IconsSchema.shape, - uriTemplate: string2(), - description: optional(string2()), - mimeType: optional(string2()), - annotations: AnnotationsSchema.optional(), - _meta: optional(looseObject({})) -}); -var ListResourcesRequestSchema = PaginatedRequestSchema.extend({ - method: literal("resources/list") -}); -var ListResourcesResultSchema = PaginatedResultSchema.extend({ - resources: array(ResourceSchema) -}); -var ListResourceTemplatesRequestSchema = PaginatedRequestSchema.extend({ - method: literal("resources/templates/list") -}); -var ListResourceTemplatesResultSchema = PaginatedResultSchema.extend({ - resourceTemplates: array(ResourceTemplateSchema) -}); -var ResourceRequestParamsSchema = BaseRequestParamsSchema.extend({ - uri: string2() -}); -var ReadResourceRequestParamsSchema = ResourceRequestParamsSchema; -var ReadResourceRequestSchema = RequestSchema.extend({ - method: literal("resources/read"), - params: ReadResourceRequestParamsSchema -}); -var ReadResourceResultSchema = ResultSchema.extend({ - contents: array(union([TextResourceContentsSchema, BlobResourceContentsSchema])) -}); -var ResourceListChangedNotificationSchema = NotificationSchema.extend({ - method: literal("notifications/resources/list_changed"), - params: NotificationsParamsSchema.optional() -}); -var SubscribeRequestParamsSchema = ResourceRequestParamsSchema; -var SubscribeRequestSchema = RequestSchema.extend({ - method: literal("resources/subscribe"), - params: SubscribeRequestParamsSchema -}); -var UnsubscribeRequestParamsSchema = ResourceRequestParamsSchema; -var UnsubscribeRequestSchema = RequestSchema.extend({ - method: literal("resources/unsubscribe"), - params: UnsubscribeRequestParamsSchema -}); -var ResourceUpdatedNotificationParamsSchema = NotificationsParamsSchema.extend({ - uri: string2() -}); -var ResourceUpdatedNotificationSchema = NotificationSchema.extend({ - method: literal("notifications/resources/updated"), - params: ResourceUpdatedNotificationParamsSchema -}); -var PromptArgumentSchema = object2({ - name: string2(), - description: optional(string2()), - required: optional(boolean2()) -}); -var PromptSchema = object2({ - ...BaseMetadataSchema.shape, - ...IconsSchema.shape, - description: optional(string2()), - arguments: optional(array(PromptArgumentSchema)), - _meta: optional(looseObject({})) -}); -var ListPromptsRequestSchema = PaginatedRequestSchema.extend({ - method: literal("prompts/list") -}); -var ListPromptsResultSchema = PaginatedResultSchema.extend({ - prompts: array(PromptSchema) -}); -var GetPromptRequestParamsSchema = BaseRequestParamsSchema.extend({ - name: string2(), - arguments: record(string2(), string2()).optional() -}); -var GetPromptRequestSchema = RequestSchema.extend({ - method: literal("prompts/get"), - params: GetPromptRequestParamsSchema -}); -var TextContentSchema = object2({ - type: literal("text"), - text: string2(), - annotations: AnnotationsSchema.optional(), - _meta: record(string2(), unknown()).optional() -}); -var ImageContentSchema = object2({ - type: literal("image"), - data: Base64Schema, - mimeType: string2(), - annotations: AnnotationsSchema.optional(), - _meta: record(string2(), unknown()).optional() -}); -var AudioContentSchema = object2({ - type: literal("audio"), - data: Base64Schema, - mimeType: string2(), - annotations: AnnotationsSchema.optional(), - _meta: record(string2(), unknown()).optional() -}); -var ToolUseContentSchema = object2({ - type: literal("tool_use"), - name: string2(), - id: string2(), - input: record(string2(), unknown()), - _meta: record(string2(), unknown()).optional() -}); -var EmbeddedResourceSchema = object2({ - type: literal("resource"), - resource: union([TextResourceContentsSchema, BlobResourceContentsSchema]), - annotations: AnnotationsSchema.optional(), - _meta: record(string2(), unknown()).optional() -}); -var ResourceLinkSchema = ResourceSchema.extend({ - type: literal("resource_link") -}); -var ContentBlockSchema = union([ - TextContentSchema, - ImageContentSchema, - AudioContentSchema, - ResourceLinkSchema, - EmbeddedResourceSchema -]); -var PromptMessageSchema = object2({ - role: RoleSchema, - content: ContentBlockSchema -}); -var GetPromptResultSchema = ResultSchema.extend({ - description: string2().optional(), - messages: array(PromptMessageSchema) -}); -var PromptListChangedNotificationSchema = NotificationSchema.extend({ - method: literal("notifications/prompts/list_changed"), - params: NotificationsParamsSchema.optional() -}); -var ToolAnnotationsSchema = object2({ - title: string2().optional(), - readOnlyHint: boolean2().optional(), - destructiveHint: boolean2().optional(), - idempotentHint: boolean2().optional(), - openWorldHint: boolean2().optional() -}); -var ToolExecutionSchema = object2({ - taskSupport: _enum(["required", "optional", "forbidden"]).optional() -}); -var ToolSchema = object2({ - ...BaseMetadataSchema.shape, - ...IconsSchema.shape, - description: string2().optional(), - inputSchema: object2({ - type: literal("object"), - properties: record(string2(), AssertObjectSchema).optional(), - required: array(string2()).optional() - }).catchall(unknown()), - outputSchema: object2({ - type: literal("object"), - properties: record(string2(), AssertObjectSchema).optional(), - required: array(string2()).optional() - }).catchall(unknown()).optional(), - annotations: ToolAnnotationsSchema.optional(), - execution: ToolExecutionSchema.optional(), - _meta: record(string2(), unknown()).optional() -}); -var ListToolsRequestSchema = PaginatedRequestSchema.extend({ - method: literal("tools/list") -}); -var ListToolsResultSchema = PaginatedResultSchema.extend({ - tools: array(ToolSchema) -}); -var CallToolResultSchema = ResultSchema.extend({ - content: array(ContentBlockSchema).default([]), - structuredContent: record(string2(), unknown()).optional(), - isError: boolean2().optional() -}); -var CompatibilityCallToolResultSchema = CallToolResultSchema.or(ResultSchema.extend({ - toolResult: unknown() -})); -var CallToolRequestParamsSchema = TaskAugmentedRequestParamsSchema.extend({ - name: string2(), - arguments: record(string2(), unknown()).optional() -}); -var CallToolRequestSchema = RequestSchema.extend({ - method: literal("tools/call"), - params: CallToolRequestParamsSchema -}); -var ToolListChangedNotificationSchema = NotificationSchema.extend({ - method: literal("notifications/tools/list_changed"), - params: NotificationsParamsSchema.optional() -}); -var ListChangedOptionsBaseSchema = object2({ - autoRefresh: boolean2().default(true), - debounceMs: number2().int().nonnegative().default(300) -}); -var LoggingLevelSchema = _enum(["debug", "info", "notice", "warning", "error", "critical", "alert", "emergency"]); -var SetLevelRequestParamsSchema = BaseRequestParamsSchema.extend({ - level: LoggingLevelSchema -}); -var SetLevelRequestSchema = RequestSchema.extend({ - method: literal("logging/setLevel"), - params: SetLevelRequestParamsSchema -}); -var LoggingMessageNotificationParamsSchema = NotificationsParamsSchema.extend({ - level: LoggingLevelSchema, - logger: string2().optional(), - data: unknown() -}); -var LoggingMessageNotificationSchema = NotificationSchema.extend({ - method: literal("notifications/message"), - params: LoggingMessageNotificationParamsSchema -}); -var ModelHintSchema = object2({ - name: string2().optional() -}); -var ModelPreferencesSchema = object2({ - hints: array(ModelHintSchema).optional(), - costPriority: number2().min(0).max(1).optional(), - speedPriority: number2().min(0).max(1).optional(), - intelligencePriority: number2().min(0).max(1).optional() -}); -var ToolChoiceSchema = object2({ - mode: _enum(["auto", "required", "none"]).optional() -}); -var ToolResultContentSchema = object2({ - type: literal("tool_result"), - toolUseId: string2().describe("The unique identifier for the corresponding tool call."), - content: array(ContentBlockSchema).default([]), - structuredContent: object2({}).loose().optional(), - isError: boolean2().optional(), - _meta: record(string2(), unknown()).optional() -}); -var SamplingContentSchema = discriminatedUnion("type", [TextContentSchema, ImageContentSchema, AudioContentSchema]); -var SamplingMessageContentBlockSchema = discriminatedUnion("type", [ - TextContentSchema, - ImageContentSchema, - AudioContentSchema, - ToolUseContentSchema, - ToolResultContentSchema -]); -var SamplingMessageSchema = object2({ - role: RoleSchema, - content: union([SamplingMessageContentBlockSchema, array(SamplingMessageContentBlockSchema)]), - _meta: record(string2(), unknown()).optional() -}); -var CreateMessageRequestParamsSchema = TaskAugmentedRequestParamsSchema.extend({ - messages: array(SamplingMessageSchema), - modelPreferences: ModelPreferencesSchema.optional(), - systemPrompt: string2().optional(), - includeContext: _enum(["none", "thisServer", "allServers"]).optional(), - temperature: number2().optional(), - maxTokens: number2().int(), - stopSequences: array(string2()).optional(), - metadata: AssertObjectSchema.optional(), - tools: array(ToolSchema).optional(), - toolChoice: ToolChoiceSchema.optional() -}); -var CreateMessageRequestSchema = RequestSchema.extend({ - method: literal("sampling/createMessage"), - params: CreateMessageRequestParamsSchema -}); -var CreateMessageResultSchema = ResultSchema.extend({ - model: string2(), - stopReason: optional(_enum(["endTurn", "stopSequence", "maxTokens"]).or(string2())), - role: RoleSchema, - content: SamplingContentSchema -}); -var CreateMessageResultWithToolsSchema = ResultSchema.extend({ - model: string2(), - stopReason: optional(_enum(["endTurn", "stopSequence", "maxTokens", "toolUse"]).or(string2())), - role: RoleSchema, - content: union([SamplingMessageContentBlockSchema, array(SamplingMessageContentBlockSchema)]) -}); -var BooleanSchemaSchema = object2({ - type: literal("boolean"), - title: string2().optional(), - description: string2().optional(), - default: boolean2().optional() -}); -var StringSchemaSchema = object2({ - type: literal("string"), - title: string2().optional(), - description: string2().optional(), - minLength: number2().optional(), - maxLength: number2().optional(), - format: _enum(["email", "uri", "date", "date-time"]).optional(), - default: string2().optional() -}); -var NumberSchemaSchema = object2({ - type: _enum(["number", "integer"]), - title: string2().optional(), - description: string2().optional(), - minimum: number2().optional(), - maximum: number2().optional(), - default: number2().optional() -}); -var UntitledSingleSelectEnumSchemaSchema = object2({ - type: literal("string"), - title: string2().optional(), - description: string2().optional(), - enum: array(string2()), - default: string2().optional() -}); -var TitledSingleSelectEnumSchemaSchema = object2({ - type: literal("string"), - title: string2().optional(), - description: string2().optional(), - oneOf: array(object2({ - const: string2(), - title: string2() - })), - default: string2().optional() -}); -var LegacyTitledEnumSchemaSchema = object2({ - type: literal("string"), - title: string2().optional(), - description: string2().optional(), - enum: array(string2()), - enumNames: array(string2()).optional(), - default: string2().optional() -}); -var SingleSelectEnumSchemaSchema = union([UntitledSingleSelectEnumSchemaSchema, TitledSingleSelectEnumSchemaSchema]); -var UntitledMultiSelectEnumSchemaSchema = object2({ - type: literal("array"), - title: string2().optional(), - description: string2().optional(), - minItems: number2().optional(), - maxItems: number2().optional(), - items: object2({ - type: literal("string"), - enum: array(string2()) - }), - default: array(string2()).optional() -}); -var TitledMultiSelectEnumSchemaSchema = object2({ - type: literal("array"), - title: string2().optional(), - description: string2().optional(), - minItems: number2().optional(), - maxItems: number2().optional(), - items: object2({ - anyOf: array(object2({ - const: string2(), - title: string2() - })) - }), - default: array(string2()).optional() -}); -var MultiSelectEnumSchemaSchema = union([UntitledMultiSelectEnumSchemaSchema, TitledMultiSelectEnumSchemaSchema]); -var EnumSchemaSchema = union([LegacyTitledEnumSchemaSchema, SingleSelectEnumSchemaSchema, MultiSelectEnumSchemaSchema]); -var PrimitiveSchemaDefinitionSchema = union([EnumSchemaSchema, BooleanSchemaSchema, StringSchemaSchema, NumberSchemaSchema]); -var ElicitRequestFormParamsSchema = TaskAugmentedRequestParamsSchema.extend({ - mode: literal("form").optional(), - message: string2(), - requestedSchema: object2({ - type: literal("object"), - properties: record(string2(), PrimitiveSchemaDefinitionSchema), - required: array(string2()).optional() - }) -}); -var ElicitRequestURLParamsSchema = TaskAugmentedRequestParamsSchema.extend({ - mode: literal("url"), - message: string2(), - elicitationId: string2(), - url: string2().url() -}); -var ElicitRequestParamsSchema = union([ElicitRequestFormParamsSchema, ElicitRequestURLParamsSchema]); -var ElicitRequestSchema = RequestSchema.extend({ - method: literal("elicitation/create"), - params: ElicitRequestParamsSchema -}); -var ElicitationCompleteNotificationParamsSchema = NotificationsParamsSchema.extend({ - elicitationId: string2() -}); -var ElicitationCompleteNotificationSchema = NotificationSchema.extend({ - method: literal("notifications/elicitation/complete"), - params: ElicitationCompleteNotificationParamsSchema -}); -var ElicitResultSchema = ResultSchema.extend({ - action: _enum(["accept", "decline", "cancel"]), - content: preprocess((val) => val === null ? undefined : val, record(string2(), union([string2(), number2(), boolean2(), array(string2())])).optional()) -}); -var ResourceTemplateReferenceSchema = object2({ - type: literal("ref/resource"), - uri: string2() -}); -var PromptReferenceSchema = object2({ - type: literal("ref/prompt"), - name: string2() -}); -var CompleteRequestParamsSchema = BaseRequestParamsSchema.extend({ - ref: union([PromptReferenceSchema, ResourceTemplateReferenceSchema]), - argument: object2({ - name: string2(), - value: string2() - }), - context: object2({ - arguments: record(string2(), string2()).optional() - }).optional() -}); -var CompleteRequestSchema = RequestSchema.extend({ - method: literal("completion/complete"), - params: CompleteRequestParamsSchema -}); -var CompleteResultSchema = ResultSchema.extend({ - completion: looseObject({ - values: array(string2()).max(100), - total: optional(number2().int()), - hasMore: optional(boolean2()) - }) -}); -var RootSchema = object2({ - uri: string2().startsWith("file://"), - name: string2().optional(), - _meta: record(string2(), unknown()).optional() -}); -var ListRootsRequestSchema = RequestSchema.extend({ - method: literal("roots/list"), - params: BaseRequestParamsSchema.optional() -}); -var ListRootsResultSchema = ResultSchema.extend({ - roots: array(RootSchema) -}); -var RootsListChangedNotificationSchema = NotificationSchema.extend({ - method: literal("notifications/roots/list_changed"), - params: NotificationsParamsSchema.optional() -}); -var ClientRequestSchema = union([ - PingRequestSchema, - InitializeRequestSchema, - CompleteRequestSchema, - SetLevelRequestSchema, - GetPromptRequestSchema, - ListPromptsRequestSchema, - ListResourcesRequestSchema, - ListResourceTemplatesRequestSchema, - ReadResourceRequestSchema, - SubscribeRequestSchema, - UnsubscribeRequestSchema, - CallToolRequestSchema, - ListToolsRequestSchema, - GetTaskRequestSchema, - GetTaskPayloadRequestSchema, - ListTasksRequestSchema, - CancelTaskRequestSchema -]); -var ClientNotificationSchema = union([ - CancelledNotificationSchema, - ProgressNotificationSchema, - InitializedNotificationSchema, - RootsListChangedNotificationSchema, - TaskStatusNotificationSchema -]); -var ClientResultSchema = union([ - EmptyResultSchema, - CreateMessageResultSchema, - CreateMessageResultWithToolsSchema, - ElicitResultSchema, - ListRootsResultSchema, - GetTaskResultSchema, - ListTasksResultSchema, - CreateTaskResultSchema -]); -var ServerRequestSchema = union([ - PingRequestSchema, - CreateMessageRequestSchema, - ElicitRequestSchema, - ListRootsRequestSchema, - GetTaskRequestSchema, - GetTaskPayloadRequestSchema, - ListTasksRequestSchema, - CancelTaskRequestSchema -]); -var ServerNotificationSchema = union([ - CancelledNotificationSchema, - ProgressNotificationSchema, - LoggingMessageNotificationSchema, - ResourceUpdatedNotificationSchema, - ResourceListChangedNotificationSchema, - ToolListChangedNotificationSchema, - PromptListChangedNotificationSchema, - TaskStatusNotificationSchema, - ElicitationCompleteNotificationSchema -]); -var ServerResultSchema = union([ - EmptyResultSchema, - InitializeResultSchema, - CompleteResultSchema, - GetPromptResultSchema, - ListPromptsResultSchema, - ListResourcesResultSchema, - ListResourceTemplatesResultSchema, - ReadResourceResultSchema, - CallToolResultSchema, - ListToolsResultSchema, - GetTaskResultSchema, - ListTasksResultSchema, - CreateTaskResultSchema -]); - -class McpError extends Error { - constructor(code, message, data) { - super(`MCP error ${code}: ${message}`); - this.code = code; - this.data = data; - this.name = "McpError"; - } - static fromError(code, message, data) { - if (code === ErrorCode.UrlElicitationRequired && data) { - const errorData = data; - if (errorData.elicitations) { - return new UrlElicitationRequiredError(errorData.elicitations, message); - } - } - return new McpError(code, message, data); - } -} - -class UrlElicitationRequiredError extends McpError { - constructor(elicitations, message = `URL elicitation${elicitations.length > 1 ? "s" : ""} required`) { - super(ErrorCode.UrlElicitationRequired, message, { - elicitations - }); - } - get elicitations() { - return this.data?.elicitations ?? []; - } -} - -// node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/interfaces.js -function isTerminal(status) { - return status === "completed" || status === "failed" || status === "cancelled"; -} - -// node_modules/zod-to-json-schema/dist/esm/Options.js -var ignoreOverride = Symbol("Let zodToJsonSchema decide on which parser to use"); -// node_modules/zod-to-json-schema/dist/esm/parsers/string.js -var ALPHA_NUMERIC = new Set("ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvxyz0123456789"); -// node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-json-schema-compat.js -function getMethodLiteral(schema) { - const shape = getObjectShape(schema); - const methodSchema = shape?.method; - if (!methodSchema) { - throw new Error("Schema is missing a method literal"); - } - const value = getLiteralValue(methodSchema); - if (typeof value !== "string") { - throw new Error("Schema method literal must be a string"); - } - return value; -} -function parseWithCompat(schema, data) { - const result = safeParse2(schema, data); - if (!result.success) { - throw result.error; - } - return result.data; -} - -// node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js -var DEFAULT_REQUEST_TIMEOUT_MSEC = 60000; - -class Protocol { - constructor(_options) { - this._options = _options; - this._requestMessageId = 0; - this._requestHandlers = new Map; - this._requestHandlerAbortControllers = new Map; - this._notificationHandlers = new Map; - this._responseHandlers = new Map; - this._progressHandlers = new Map; - this._timeoutInfo = new Map; - this._pendingDebouncedNotifications = new Set; - this._taskProgressTokens = new Map; - this._requestResolvers = new Map; - this.setNotificationHandler(CancelledNotificationSchema, (notification) => { - this._oncancel(notification); - }); - this.setNotificationHandler(ProgressNotificationSchema, (notification) => { - this._onprogress(notification); - }); - this.setRequestHandler(PingRequestSchema, (_request) => ({})); - this._taskStore = _options?.taskStore; - this._taskMessageQueue = _options?.taskMessageQueue; - if (this._taskStore) { - this.setRequestHandler(GetTaskRequestSchema, async (request, extra) => { - const task = await this._taskStore.getTask(request.params.taskId, extra.sessionId); - if (!task) { - throw new McpError(ErrorCode.InvalidParams, "Failed to retrieve task: Task not found"); - } - return { - ...task - }; - }); - this.setRequestHandler(GetTaskPayloadRequestSchema, async (request, extra) => { - const handleTaskResult = async () => { - const taskId = request.params.taskId; - if (this._taskMessageQueue) { - let queuedMessage; - while (queuedMessage = await this._taskMessageQueue.dequeue(taskId, extra.sessionId)) { - if (queuedMessage.type === "response" || queuedMessage.type === "error") { - const message = queuedMessage.message; - const requestId = message.id; - const resolver = this._requestResolvers.get(requestId); - if (resolver) { - this._requestResolvers.delete(requestId); - if (queuedMessage.type === "response") { - resolver(message); - } else { - const errorMessage = message; - const error2 = new McpError(errorMessage.error.code, errorMessage.error.message, errorMessage.error.data); - resolver(error2); - } - } else { - const messageType = queuedMessage.type === "response" ? "Response" : "Error"; - this._onerror(new Error(`${messageType} handler missing for request ${requestId}`)); - } - continue; - } - await this._transport?.send(queuedMessage.message, { relatedRequestId: extra.requestId }); - } - } - const task = await this._taskStore.getTask(taskId, extra.sessionId); - if (!task) { - throw new McpError(ErrorCode.InvalidParams, `Task not found: ${taskId}`); - } - if (!isTerminal(task.status)) { - await this._waitForTaskUpdate(taskId, extra.signal); - return await handleTaskResult(); - } - if (isTerminal(task.status)) { - const result = await this._taskStore.getTaskResult(taskId, extra.sessionId); - this._clearTaskQueue(taskId); - return { - ...result, - _meta: { - ...result._meta, - [RELATED_TASK_META_KEY]: { - taskId - } - } - }; - } - return await handleTaskResult(); - }; - return await handleTaskResult(); - }); - this.setRequestHandler(ListTasksRequestSchema, async (request, extra) => { - try { - const { tasks, nextCursor } = await this._taskStore.listTasks(request.params?.cursor, extra.sessionId); - return { - tasks, - nextCursor, - _meta: {} - }; - } catch (error2) { - throw new McpError(ErrorCode.InvalidParams, `Failed to list tasks: ${error2 instanceof Error ? error2.message : String(error2)}`); - } - }); - this.setRequestHandler(CancelTaskRequestSchema, async (request, extra) => { - try { - const task = await this._taskStore.getTask(request.params.taskId, extra.sessionId); - if (!task) { - throw new McpError(ErrorCode.InvalidParams, `Task not found: ${request.params.taskId}`); - } - if (isTerminal(task.status)) { - throw new McpError(ErrorCode.InvalidParams, `Cannot cancel task in terminal status: ${task.status}`); - } - await this._taskStore.updateTaskStatus(request.params.taskId, "cancelled", "Client cancelled task execution.", extra.sessionId); - this._clearTaskQueue(request.params.taskId); - const cancelledTask = await this._taskStore.getTask(request.params.taskId, extra.sessionId); - if (!cancelledTask) { - throw new McpError(ErrorCode.InvalidParams, `Task not found after cancellation: ${request.params.taskId}`); - } - return { - _meta: {}, - ...cancelledTask - }; - } catch (error2) { - if (error2 instanceof McpError) { - throw error2; - } - throw new McpError(ErrorCode.InvalidRequest, `Failed to cancel task: ${error2 instanceof Error ? error2.message : String(error2)}`); - } - }); - } - } - async _oncancel(notification) { - if (!notification.params.requestId) { - return; - } - const controller = this._requestHandlerAbortControllers.get(notification.params.requestId); - controller?.abort(notification.params.reason); - } - _setupTimeout(messageId, timeout, maxTotalTimeout, onTimeout, resetTimeoutOnProgress = false) { - this._timeoutInfo.set(messageId, { - timeoutId: setTimeout(onTimeout, timeout), - startTime: Date.now(), - timeout, - maxTotalTimeout, - resetTimeoutOnProgress, - onTimeout - }); - } - _resetTimeout(messageId) { - const info = this._timeoutInfo.get(messageId); - if (!info) - return false; - const totalElapsed = Date.now() - info.startTime; - if (info.maxTotalTimeout && totalElapsed >= info.maxTotalTimeout) { - this._timeoutInfo.delete(messageId); - throw McpError.fromError(ErrorCode.RequestTimeout, "Maximum total timeout exceeded", { - maxTotalTimeout: info.maxTotalTimeout, - totalElapsed - }); - } - clearTimeout(info.timeoutId); - info.timeoutId = setTimeout(info.onTimeout, info.timeout); - return true; - } - _cleanupTimeout(messageId) { - const info = this._timeoutInfo.get(messageId); - if (info) { - clearTimeout(info.timeoutId); - this._timeoutInfo.delete(messageId); - } - } - async connect(transport) { - this._transport = transport; - const _onclose = this.transport?.onclose; - this._transport.onclose = () => { - _onclose?.(); - this._onclose(); - }; - const _onerror = this.transport?.onerror; - this._transport.onerror = (error2) => { - _onerror?.(error2); - this._onerror(error2); - }; - const _onmessage = this._transport?.onmessage; - this._transport.onmessage = (message, extra) => { - _onmessage?.(message, extra); - if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) { - this._onresponse(message); - } else if (isJSONRPCRequest(message)) { - this._onrequest(message, extra); - } else if (isJSONRPCNotification(message)) { - this._onnotification(message); - } else { - this._onerror(new Error(`Unknown message type: ${JSON.stringify(message)}`)); - } - }; - await this._transport.start(); - } - _onclose() { - const responseHandlers = this._responseHandlers; - this._responseHandlers = new Map; - this._progressHandlers.clear(); - this._taskProgressTokens.clear(); - this._pendingDebouncedNotifications.clear(); - const error2 = McpError.fromError(ErrorCode.ConnectionClosed, "Connection closed"); - this._transport = undefined; - this.onclose?.(); - for (const handler of responseHandlers.values()) { - handler(error2); - } - } - _onerror(error2) { - this.onerror?.(error2); - } - _onnotification(notification) { - const handler = this._notificationHandlers.get(notification.method) ?? this.fallbackNotificationHandler; - if (handler === undefined) { - return; - } - Promise.resolve().then(() => handler(notification)).catch((error2) => this._onerror(new Error(`Uncaught error in notification handler: ${error2}`))); - } - _onrequest(request, extra) { - const handler = this._requestHandlers.get(request.method) ?? this.fallbackRequestHandler; - const capturedTransport = this._transport; - const relatedTaskId = request.params?._meta?.[RELATED_TASK_META_KEY]?.taskId; - if (handler === undefined) { - const errorResponse = { - jsonrpc: "2.0", - id: request.id, - error: { - code: ErrorCode.MethodNotFound, - message: "Method not found" - } - }; - if (relatedTaskId && this._taskMessageQueue) { - this._enqueueTaskMessage(relatedTaskId, { - type: "error", - message: errorResponse, - timestamp: Date.now() - }, capturedTransport?.sessionId).catch((error2) => this._onerror(new Error(`Failed to enqueue error response: ${error2}`))); - } else { - capturedTransport?.send(errorResponse).catch((error2) => this._onerror(new Error(`Failed to send an error response: ${error2}`))); - } - return; - } - const abortController = new AbortController; - this._requestHandlerAbortControllers.set(request.id, abortController); - const taskCreationParams = isTaskAugmentedRequestParams(request.params) ? request.params.task : undefined; - const taskStore = this._taskStore ? this.requestTaskStore(request, capturedTransport?.sessionId) : undefined; - const fullExtra = { - signal: abortController.signal, - sessionId: capturedTransport?.sessionId, - _meta: request.params?._meta, - sendNotification: async (notification) => { - const notificationOptions = { relatedRequestId: request.id }; - if (relatedTaskId) { - notificationOptions.relatedTask = { taskId: relatedTaskId }; - } - await this.notification(notification, notificationOptions); - }, - sendRequest: async (r, resultSchema, options) => { - const requestOptions = { ...options, relatedRequestId: request.id }; - if (relatedTaskId && !requestOptions.relatedTask) { - requestOptions.relatedTask = { taskId: relatedTaskId }; - } - const effectiveTaskId = requestOptions.relatedTask?.taskId ?? relatedTaskId; - if (effectiveTaskId && taskStore) { - await taskStore.updateTaskStatus(effectiveTaskId, "input_required"); - } - return await this.request(r, resultSchema, requestOptions); - }, - authInfo: extra?.authInfo, - requestId: request.id, - requestInfo: extra?.requestInfo, - taskId: relatedTaskId, - taskStore, - taskRequestedTtl: taskCreationParams?.ttl, - closeSSEStream: extra?.closeSSEStream, - closeStandaloneSSEStream: extra?.closeStandaloneSSEStream - }; - Promise.resolve().then(() => { - if (taskCreationParams) { - this.assertTaskHandlerCapability(request.method); - } - }).then(() => handler(request, fullExtra)).then(async (result) => { - if (abortController.signal.aborted) { - return; - } - const response = { - result, - jsonrpc: "2.0", - id: request.id - }; - if (relatedTaskId && this._taskMessageQueue) { - await this._enqueueTaskMessage(relatedTaskId, { - type: "response", - message: response, - timestamp: Date.now() - }, capturedTransport?.sessionId); - } else { - await capturedTransport?.send(response); - } - }, async (error2) => { - if (abortController.signal.aborted) { - return; - } - const errorResponse = { - jsonrpc: "2.0", - id: request.id, - error: { - code: Number.isSafeInteger(error2["code"]) ? error2["code"] : ErrorCode.InternalError, - message: error2.message ?? "Internal error", - ...error2["data"] !== undefined && { data: error2["data"] } - } - }; - if (relatedTaskId && this._taskMessageQueue) { - await this._enqueueTaskMessage(relatedTaskId, { - type: "error", - message: errorResponse, - timestamp: Date.now() - }, capturedTransport?.sessionId); - } else { - await capturedTransport?.send(errorResponse); - } - }).catch((error2) => this._onerror(new Error(`Failed to send response: ${error2}`))).finally(() => { - this._requestHandlerAbortControllers.delete(request.id); - }); - } - _onprogress(notification) { - const { progressToken, ...params } = notification.params; - const messageId = Number(progressToken); - const handler = this._progressHandlers.get(messageId); - if (!handler) { - this._onerror(new Error(`Received a progress notification for an unknown token: ${JSON.stringify(notification)}`)); - return; - } - const responseHandler = this._responseHandlers.get(messageId); - const timeoutInfo = this._timeoutInfo.get(messageId); - if (timeoutInfo && responseHandler && timeoutInfo.resetTimeoutOnProgress) { - try { - this._resetTimeout(messageId); - } catch (error2) { - this._responseHandlers.delete(messageId); - this._progressHandlers.delete(messageId); - this._cleanupTimeout(messageId); - responseHandler(error2); - return; - } - } - handler(params); - } - _onresponse(response) { - const messageId = Number(response.id); - const resolver = this._requestResolvers.get(messageId); - if (resolver) { - this._requestResolvers.delete(messageId); - if (isJSONRPCResultResponse(response)) { - resolver(response); - } else { - const error2 = new McpError(response.error.code, response.error.message, response.error.data); - resolver(error2); - } - return; - } - const handler = this._responseHandlers.get(messageId); - if (handler === undefined) { - this._onerror(new Error(`Received a response for an unknown message ID: ${JSON.stringify(response)}`)); - return; - } - this._responseHandlers.delete(messageId); - this._cleanupTimeout(messageId); - let isTaskResponse = false; - if (isJSONRPCResultResponse(response) && response.result && typeof response.result === "object") { - const result = response.result; - if (result.task && typeof result.task === "object") { - const task = result.task; - if (typeof task.taskId === "string") { - isTaskResponse = true; - this._taskProgressTokens.set(task.taskId, messageId); - } - } - } - if (!isTaskResponse) { - this._progressHandlers.delete(messageId); - } - if (isJSONRPCResultResponse(response)) { - handler(response); - } else { - const error2 = McpError.fromError(response.error.code, response.error.message, response.error.data); - handler(error2); - } - } - get transport() { - return this._transport; - } - async close() { - await this._transport?.close(); - } - async* requestStream(request, resultSchema, options) { - const { task } = options ?? {}; - if (!task) { - try { - const result = await this.request(request, resultSchema, options); - yield { type: "result", result }; - } catch (error2) { - yield { - type: "error", - error: error2 instanceof McpError ? error2 : new McpError(ErrorCode.InternalError, String(error2)) - }; - } - return; - } - let taskId; - try { - const createResult = await this.request(request, CreateTaskResultSchema, options); - if (createResult.task) { - taskId = createResult.task.taskId; - yield { type: "taskCreated", task: createResult.task }; - } else { - throw new McpError(ErrorCode.InternalError, "Task creation did not return a task"); - } - while (true) { - const task2 = await this.getTask({ taskId }, options); - yield { type: "taskStatus", task: task2 }; - if (isTerminal(task2.status)) { - if (task2.status === "completed") { - const result = await this.getTaskResult({ taskId }, resultSchema, options); - yield { type: "result", result }; - } else if (task2.status === "failed") { - yield { - type: "error", - error: new McpError(ErrorCode.InternalError, `Task ${taskId} failed`) - }; - } else if (task2.status === "cancelled") { - yield { - type: "error", - error: new McpError(ErrorCode.InternalError, `Task ${taskId} was cancelled`) - }; - } - return; - } - if (task2.status === "input_required") { - const result = await this.getTaskResult({ taskId }, resultSchema, options); - yield { type: "result", result }; - return; - } - const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1000; - await new Promise((resolve) => setTimeout(resolve, pollInterval)); - options?.signal?.throwIfAborted(); - } - } catch (error2) { - yield { - type: "error", - error: error2 instanceof McpError ? error2 : new McpError(ErrorCode.InternalError, String(error2)) - }; - } - } - request(request, resultSchema, options) { - const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {}; - return new Promise((resolve, reject) => { - const earlyReject = (error2) => { - reject(error2); - }; - if (!this._transport) { - earlyReject(new Error("Not connected")); - return; - } - if (this._options?.enforceStrictCapabilities === true) { - try { - this.assertCapabilityForMethod(request.method); - if (task) { - this.assertTaskCapability(request.method); - } - } catch (e) { - earlyReject(e); - return; - } - } - options?.signal?.throwIfAborted(); - const messageId = this._requestMessageId++; - const jsonrpcRequest = { - ...request, - jsonrpc: "2.0", - id: messageId - }; - if (options?.onprogress) { - this._progressHandlers.set(messageId, options.onprogress); - jsonrpcRequest.params = { - ...request.params, - _meta: { - ...request.params?._meta || {}, - progressToken: messageId - } - }; - } - if (task) { - jsonrpcRequest.params = { - ...jsonrpcRequest.params, - task - }; - } - if (relatedTask) { - jsonrpcRequest.params = { - ...jsonrpcRequest.params, - _meta: { - ...jsonrpcRequest.params?._meta || {}, - [RELATED_TASK_META_KEY]: relatedTask - } - }; - } - const cancel = (reason) => { - this._responseHandlers.delete(messageId); - this._progressHandlers.delete(messageId); - this._cleanupTimeout(messageId); - this._transport?.send({ - jsonrpc: "2.0", - method: "notifications/cancelled", - params: { - requestId: messageId, - reason: String(reason) - } - }, { relatedRequestId, resumptionToken, onresumptiontoken }).catch((error3) => this._onerror(new Error(`Failed to send cancellation: ${error3}`))); - const error2 = reason instanceof McpError ? reason : new McpError(ErrorCode.RequestTimeout, String(reason)); - reject(error2); - }; - this._responseHandlers.set(messageId, (response) => { - if (options?.signal?.aborted) { - return; - } - if (response instanceof Error) { - return reject(response); - } - try { - const parseResult = safeParse2(resultSchema, response.result); - if (!parseResult.success) { - reject(parseResult.error); - } else { - resolve(parseResult.data); - } - } catch (error2) { - reject(error2); - } - }); - options?.signal?.addEventListener("abort", () => { - cancel(options?.signal?.reason); - }); - const timeout = options?.timeout ?? DEFAULT_REQUEST_TIMEOUT_MSEC; - const timeoutHandler = () => cancel(McpError.fromError(ErrorCode.RequestTimeout, "Request timed out", { timeout })); - this._setupTimeout(messageId, timeout, options?.maxTotalTimeout, timeoutHandler, options?.resetTimeoutOnProgress ?? false); - const relatedTaskId = relatedTask?.taskId; - if (relatedTaskId) { - const responseResolver = (response) => { - const handler = this._responseHandlers.get(messageId); - if (handler) { - handler(response); - } else { - this._onerror(new Error(`Response handler missing for side-channeled request ${messageId}`)); - } - }; - this._requestResolvers.set(messageId, responseResolver); - this._enqueueTaskMessage(relatedTaskId, { - type: "request", - message: jsonrpcRequest, - timestamp: Date.now() - }).catch((error2) => { - this._cleanupTimeout(messageId); - reject(error2); - }); - } else { - this._transport.send(jsonrpcRequest, { relatedRequestId, resumptionToken, onresumptiontoken }).catch((error2) => { - this._cleanupTimeout(messageId); - reject(error2); - }); - } - }); - } - async getTask(params, options) { - return this.request({ method: "tasks/get", params }, GetTaskResultSchema, options); - } - async getTaskResult(params, resultSchema, options) { - return this.request({ method: "tasks/result", params }, resultSchema, options); - } - async listTasks(params, options) { - return this.request({ method: "tasks/list", params }, ListTasksResultSchema, options); - } - async cancelTask(params, options) { - return this.request({ method: "tasks/cancel", params }, CancelTaskResultSchema, options); - } - async notification(notification, options) { - if (!this._transport) { - throw new Error("Not connected"); - } - this.assertNotificationCapability(notification.method); - const relatedTaskId = options?.relatedTask?.taskId; - if (relatedTaskId) { - const jsonrpcNotification2 = { - ...notification, - jsonrpc: "2.0", - params: { - ...notification.params, - _meta: { - ...notification.params?._meta || {}, - [RELATED_TASK_META_KEY]: options.relatedTask - } - } - }; - await this._enqueueTaskMessage(relatedTaskId, { - type: "notification", - message: jsonrpcNotification2, - timestamp: Date.now() - }); - return; - } - const debouncedMethods = this._options?.debouncedNotificationMethods ?? []; - const canDebounce = debouncedMethods.includes(notification.method) && !notification.params && !options?.relatedRequestId && !options?.relatedTask; - if (canDebounce) { - if (this._pendingDebouncedNotifications.has(notification.method)) { - return; - } - this._pendingDebouncedNotifications.add(notification.method); - Promise.resolve().then(() => { - this._pendingDebouncedNotifications.delete(notification.method); - if (!this._transport) { - return; - } - let jsonrpcNotification2 = { - ...notification, - jsonrpc: "2.0" - }; - if (options?.relatedTask) { - jsonrpcNotification2 = { - ...jsonrpcNotification2, - params: { - ...jsonrpcNotification2.params, - _meta: { - ...jsonrpcNotification2.params?._meta || {}, - [RELATED_TASK_META_KEY]: options.relatedTask - } - } - }; - } - this._transport?.send(jsonrpcNotification2, options).catch((error2) => this._onerror(error2)); - }); - return; - } - let jsonrpcNotification = { - ...notification, - jsonrpc: "2.0" - }; - if (options?.relatedTask) { - jsonrpcNotification = { - ...jsonrpcNotification, - params: { - ...jsonrpcNotification.params, - _meta: { - ...jsonrpcNotification.params?._meta || {}, - [RELATED_TASK_META_KEY]: options.relatedTask - } - } - }; - } - await this._transport.send(jsonrpcNotification, options); - } - setRequestHandler(requestSchema, handler) { - const method = getMethodLiteral(requestSchema); - this.assertRequestHandlerCapability(method); - this._requestHandlers.set(method, (request, extra) => { - const parsed = parseWithCompat(requestSchema, request); - return Promise.resolve(handler(parsed, extra)); - }); - } - removeRequestHandler(method) { - this._requestHandlers.delete(method); - } - assertCanSetRequestHandler(method) { - if (this._requestHandlers.has(method)) { - throw new Error(`A request handler for ${method} already exists, which would be overridden`); - } - } - setNotificationHandler(notificationSchema, handler) { - const method = getMethodLiteral(notificationSchema); - this._notificationHandlers.set(method, (notification) => { - const parsed = parseWithCompat(notificationSchema, notification); - return Promise.resolve(handler(parsed)); - }); - } - removeNotificationHandler(method) { - this._notificationHandlers.delete(method); - } - _cleanupTaskProgressHandler(taskId) { - const progressToken = this._taskProgressTokens.get(taskId); - if (progressToken !== undefined) { - this._progressHandlers.delete(progressToken); - this._taskProgressTokens.delete(taskId); - } - } - async _enqueueTaskMessage(taskId, message, sessionId) { - if (!this._taskStore || !this._taskMessageQueue) { - throw new Error("Cannot enqueue task message: taskStore and taskMessageQueue are not configured"); - } - const maxQueueSize = this._options?.maxTaskQueueSize; - await this._taskMessageQueue.enqueue(taskId, message, sessionId, maxQueueSize); - } - async _clearTaskQueue(taskId, sessionId) { - if (this._taskMessageQueue) { - const messages = await this._taskMessageQueue.dequeueAll(taskId, sessionId); - for (const message of messages) { - if (message.type === "request" && isJSONRPCRequest(message.message)) { - const requestId = message.message.id; - const resolver = this._requestResolvers.get(requestId); - if (resolver) { - resolver(new McpError(ErrorCode.InternalError, "Task cancelled or completed")); - this._requestResolvers.delete(requestId); - } else { - this._onerror(new Error(`Resolver missing for request ${requestId} during task ${taskId} cleanup`)); - } - } - } - } - } - async _waitForTaskUpdate(taskId, signal) { - let interval = this._options?.defaultTaskPollInterval ?? 1000; - try { - const task = await this._taskStore?.getTask(taskId); - if (task?.pollInterval) { - interval = task.pollInterval; - } - } catch {} - return new Promise((resolve, reject) => { - if (signal.aborted) { - reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled")); - return; - } - const timeoutId = setTimeout(resolve, interval); - signal.addEventListener("abort", () => { - clearTimeout(timeoutId); - reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled")); - }, { once: true }); - }); - } - requestTaskStore(request, sessionId) { - const taskStore = this._taskStore; - if (!taskStore) { - throw new Error("No task store configured"); - } - return { - createTask: async (taskParams) => { - if (!request) { - throw new Error("No request provided"); - } - return await taskStore.createTask(taskParams, request.id, { - method: request.method, - params: request.params - }, sessionId); - }, - getTask: async (taskId) => { - const task = await taskStore.getTask(taskId, sessionId); - if (!task) { - throw new McpError(ErrorCode.InvalidParams, "Failed to retrieve task: Task not found"); - } - return task; - }, - storeTaskResult: async (taskId, status, result) => { - await taskStore.storeTaskResult(taskId, status, result, sessionId); - const task = await taskStore.getTask(taskId, sessionId); - if (task) { - const notification = TaskStatusNotificationSchema.parse({ - method: "notifications/tasks/status", - params: task - }); - await this.notification(notification); - if (isTerminal(task.status)) { - this._cleanupTaskProgressHandler(taskId); - } - } - }, - getTaskResult: (taskId) => { - return taskStore.getTaskResult(taskId, sessionId); - }, - updateTaskStatus: async (taskId, status, statusMessage) => { - const task = await taskStore.getTask(taskId, sessionId); - if (!task) { - throw new McpError(ErrorCode.InvalidParams, `Task "${taskId}" not found - it may have been cleaned up`); - } - if (isTerminal(task.status)) { - throw new McpError(ErrorCode.InvalidParams, `Cannot update task "${taskId}" from terminal status "${task.status}" to "${status}". Terminal states (completed, failed, cancelled) cannot transition to other states.`); - } - await taskStore.updateTaskStatus(taskId, status, statusMessage, sessionId); - const updatedTask = await taskStore.getTask(taskId, sessionId); - if (updatedTask) { - const notification = TaskStatusNotificationSchema.parse({ - method: "notifications/tasks/status", - params: updatedTask - }); - await this.notification(notification); - if (isTerminal(updatedTask.status)) { - this._cleanupTaskProgressHandler(taskId); - } - } - }, - listTasks: (cursor) => { - return taskStore.listTasks(cursor, sessionId); - } - }; - } -} -function isPlainObject2(value) { - return value !== null && typeof value === "object" && !Array.isArray(value); -} -function mergeCapabilities(base, additional) { - const result = { ...base }; - for (const key in additional) { - const k = key; - const addValue = additional[k]; - if (addValue === undefined) - continue; - const baseValue = result[k]; - if (isPlainObject2(baseValue) && isPlainObject2(addValue)) { - result[k] = { ...baseValue, ...addValue }; - } else { - result[k] = addValue; - } - } - return result; -} - -// node_modules/@modelcontextprotocol/sdk/dist/esm/validation/ajv-provider.js -var import_ajv = __toESM(require_ajv(), 1); -var import_ajv_formats = __toESM(require_dist(), 1); -function createDefaultAjvInstance() { - const ajv = new import_ajv.default({ - strict: false, - validateFormats: true, - validateSchema: false, - allErrors: true - }); - const addFormats = import_ajv_formats.default; - addFormats(ajv); - return ajv; -} - -class AjvJsonSchemaValidator { - constructor(ajv) { - this._ajv = ajv ?? createDefaultAjvInstance(); - } - getValidator(schema) { - const ajvValidator = "$id" in schema && typeof schema.$id === "string" ? this._ajv.getSchema(schema.$id) ?? this._ajv.compile(schema) : this._ajv.compile(schema); - return (input) => { - const valid = ajvValidator(input); - if (valid) { - return { - valid: true, - data: input, - errorMessage: undefined - }; - } else { - return { - valid: false, - data: undefined, - errorMessage: this._ajv.errorsText(ajvValidator.errors) - }; - } - }; - } -} - -// node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/server.js -class ExperimentalServerTasks { - constructor(_server) { - this._server = _server; - } - requestStream(request, resultSchema, options) { - return this._server.requestStream(request, resultSchema, options); - } - async getTask(taskId, options) { - return this._server.getTask({ taskId }, options); - } - async getTaskResult(taskId, resultSchema, options) { - return this._server.getTaskResult({ taskId }, resultSchema, options); - } - async listTasks(cursor, options) { - return this._server.listTasks(cursor ? { cursor } : undefined, options); - } - async cancelTask(taskId, options) { - return this._server.cancelTask({ taskId }, options); - } -} - -// node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/helpers.js -function assertToolsCallTaskCapability(requests, method, entityName) { - if (!requests) { - throw new Error(`${entityName} does not support task creation (required for ${method})`); - } - switch (method) { - case "tools/call": - if (!requests.tools?.call) { - throw new Error(`${entityName} does not support task creation for tools/call (required for ${method})`); - } - break; - default: - break; - } -} -function assertClientRequestTaskCapability(requests, method, entityName) { - if (!requests) { - throw new Error(`${entityName} does not support task creation (required for ${method})`); - } - switch (method) { - case "sampling/createMessage": - if (!requests.sampling?.createMessage) { - throw new Error(`${entityName} does not support task creation for sampling/createMessage (required for ${method})`); - } - break; - case "elicitation/create": - if (!requests.elicitation?.create) { - throw new Error(`${entityName} does not support task creation for elicitation/create (required for ${method})`); - } - break; - default: - break; - } -} - -// node_modules/@modelcontextprotocol/sdk/dist/esm/server/index.js -class Server extends Protocol { - constructor(_serverInfo, options) { - super(options); - this._serverInfo = _serverInfo; - this._loggingLevels = new Map; - this.LOG_LEVEL_SEVERITY = new Map(LoggingLevelSchema.options.map((level, index) => [level, index])); - this.isMessageIgnored = (level, sessionId) => { - const currentLevel = this._loggingLevels.get(sessionId); - return currentLevel ? this.LOG_LEVEL_SEVERITY.get(level) < this.LOG_LEVEL_SEVERITY.get(currentLevel) : false; - }; - this._capabilities = options?.capabilities ?? {}; - this._instructions = options?.instructions; - this._jsonSchemaValidator = options?.jsonSchemaValidator ?? new AjvJsonSchemaValidator; - this.setRequestHandler(InitializeRequestSchema, (request) => this._oninitialize(request)); - this.setNotificationHandler(InitializedNotificationSchema, () => this.oninitialized?.()); - if (this._capabilities.logging) { - this.setRequestHandler(SetLevelRequestSchema, async (request, extra) => { - const transportSessionId = extra.sessionId || extra.requestInfo?.headers["mcp-session-id"] || undefined; - const { level } = request.params; - const parseResult = LoggingLevelSchema.safeParse(level); - if (parseResult.success) { - this._loggingLevels.set(transportSessionId, parseResult.data); - } - return {}; - }); - } - } - get experimental() { - if (!this._experimental) { - this._experimental = { - tasks: new ExperimentalServerTasks(this) - }; - } - return this._experimental; - } - registerCapabilities(capabilities) { - if (this.transport) { - throw new Error("Cannot register capabilities after connecting to transport"); - } - this._capabilities = mergeCapabilities(this._capabilities, capabilities); - } - setRequestHandler(requestSchema, handler) { - const shape = getObjectShape(requestSchema); - const methodSchema = shape?.method; - if (!methodSchema) { - throw new Error("Schema is missing a method literal"); - } - let methodValue; - if (isZ4Schema(methodSchema)) { - const v4Schema = methodSchema; - const v4Def = v4Schema._zod?.def; - methodValue = v4Def?.value ?? v4Schema.value; - } else { - const v3Schema = methodSchema; - const legacyDef = v3Schema._def; - methodValue = legacyDef?.value ?? v3Schema.value; - } - if (typeof methodValue !== "string") { - throw new Error("Schema method literal must be a string"); - } - const method = methodValue; - if (method === "tools/call") { - const wrappedHandler = async (request, extra) => { - const validatedRequest = safeParse2(CallToolRequestSchema, request); - if (!validatedRequest.success) { - const errorMessage = validatedRequest.error instanceof Error ? validatedRequest.error.message : String(validatedRequest.error); - throw new McpError(ErrorCode.InvalidParams, `Invalid tools/call request: ${errorMessage}`); - } - const { params } = validatedRequest.data; - const result = await Promise.resolve(handler(request, extra)); - if (params.task) { - const taskValidationResult = safeParse2(CreateTaskResultSchema, result); - if (!taskValidationResult.success) { - const errorMessage = taskValidationResult.error instanceof Error ? taskValidationResult.error.message : String(taskValidationResult.error); - throw new McpError(ErrorCode.InvalidParams, `Invalid task creation result: ${errorMessage}`); - } - return taskValidationResult.data; - } - const validationResult = safeParse2(CallToolResultSchema, result); - if (!validationResult.success) { - const errorMessage = validationResult.error instanceof Error ? validationResult.error.message : String(validationResult.error); - throw new McpError(ErrorCode.InvalidParams, `Invalid tools/call result: ${errorMessage}`); - } - return validationResult.data; - }; - return super.setRequestHandler(requestSchema, wrappedHandler); - } - return super.setRequestHandler(requestSchema, handler); - } - assertCapabilityForMethod(method) { - switch (method) { - case "sampling/createMessage": - if (!this._clientCapabilities?.sampling) { - throw new Error(`Client does not support sampling (required for ${method})`); - } - break; - case "elicitation/create": - if (!this._clientCapabilities?.elicitation) { - throw new Error(`Client does not support elicitation (required for ${method})`); - } - break; - case "roots/list": - if (!this._clientCapabilities?.roots) { - throw new Error(`Client does not support listing roots (required for ${method})`); - } - break; - case "ping": - break; - } - } - assertNotificationCapability(method) { - switch (method) { - case "notifications/message": - if (!this._capabilities.logging) { - throw new Error(`Server does not support logging (required for ${method})`); - } - break; - case "notifications/resources/updated": - case "notifications/resources/list_changed": - if (!this._capabilities.resources) { - throw new Error(`Server does not support notifying about resources (required for ${method})`); - } - break; - case "notifications/tools/list_changed": - if (!this._capabilities.tools) { - throw new Error(`Server does not support notifying of tool list changes (required for ${method})`); - } - break; - case "notifications/prompts/list_changed": - if (!this._capabilities.prompts) { - throw new Error(`Server does not support notifying of prompt list changes (required for ${method})`); - } - break; - case "notifications/elicitation/complete": - if (!this._clientCapabilities?.elicitation?.url) { - throw new Error(`Client does not support URL elicitation (required for ${method})`); - } - break; - case "notifications/cancelled": - break; - case "notifications/progress": - break; - } - } - assertRequestHandlerCapability(method) { - if (!this._capabilities) { - return; - } - switch (method) { - case "completion/complete": - if (!this._capabilities.completions) { - throw new Error(`Server does not support completions (required for ${method})`); - } - break; - case "logging/setLevel": - if (!this._capabilities.logging) { - throw new Error(`Server does not support logging (required for ${method})`); - } - break; - case "prompts/get": - case "prompts/list": - if (!this._capabilities.prompts) { - throw new Error(`Server does not support prompts (required for ${method})`); - } - break; - case "resources/list": - case "resources/templates/list": - case "resources/read": - if (!this._capabilities.resources) { - throw new Error(`Server does not support resources (required for ${method})`); - } - break; - case "tools/call": - case "tools/list": - if (!this._capabilities.tools) { - throw new Error(`Server does not support tools (required for ${method})`); - } - break; - case "tasks/get": - case "tasks/list": - case "tasks/result": - case "tasks/cancel": - if (!this._capabilities.tasks) { - throw new Error(`Server does not support tasks capability (required for ${method})`); - } - break; - case "ping": - case "initialize": - break; - } - } - assertTaskCapability(method) { - assertClientRequestTaskCapability(this._clientCapabilities?.tasks?.requests, method, "Client"); - } - assertTaskHandlerCapability(method) { - if (!this._capabilities) { - return; - } - assertToolsCallTaskCapability(this._capabilities.tasks?.requests, method, "Server"); - } - async _oninitialize(request) { - const requestedVersion = request.params.protocolVersion; - this._clientCapabilities = request.params.capabilities; - this._clientVersion = request.params.clientInfo; - const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion) ? requestedVersion : LATEST_PROTOCOL_VERSION; - return { - protocolVersion, - capabilities: this.getCapabilities(), - serverInfo: this._serverInfo, - ...this._instructions && { instructions: this._instructions } - }; - } - getClientCapabilities() { - return this._clientCapabilities; - } - getClientVersion() { - return this._clientVersion; - } - getCapabilities() { - return this._capabilities; - } - async ping() { - return this.request({ method: "ping" }, EmptyResultSchema); - } - async createMessage(params, options) { - if (params.tools || params.toolChoice) { - if (!this._clientCapabilities?.sampling?.tools) { - throw new Error("Client does not support sampling tools capability."); - } - } - if (params.messages.length > 0) { - const lastMessage = params.messages[params.messages.length - 1]; - const lastContent = Array.isArray(lastMessage.content) ? lastMessage.content : [lastMessage.content]; - const hasToolResults = lastContent.some((c) => c.type === "tool_result"); - const previousMessage = params.messages.length > 1 ? params.messages[params.messages.length - 2] : undefined; - const previousContent = previousMessage ? Array.isArray(previousMessage.content) ? previousMessage.content : [previousMessage.content] : []; - const hasPreviousToolUse = previousContent.some((c) => c.type === "tool_use"); - if (hasToolResults) { - if (lastContent.some((c) => c.type !== "tool_result")) { - throw new Error("The last message must contain only tool_result content if any is present"); - } - if (!hasPreviousToolUse) { - throw new Error("tool_result blocks are not matching any tool_use from the previous message"); - } - } - if (hasPreviousToolUse) { - const toolUseIds = new Set(previousContent.filter((c) => c.type === "tool_use").map((c) => c.id)); - const toolResultIds = new Set(lastContent.filter((c) => c.type === "tool_result").map((c) => c.toolUseId)); - if (toolUseIds.size !== toolResultIds.size || ![...toolUseIds].every((id) => toolResultIds.has(id))) { - throw new Error("ids of tool_result blocks and tool_use blocks from previous message do not match"); - } - } - } - if (params.tools) { - return this.request({ method: "sampling/createMessage", params }, CreateMessageResultWithToolsSchema, options); - } - return this.request({ method: "sampling/createMessage", params }, CreateMessageResultSchema, options); - } - async elicitInput(params, options) { - const mode = params.mode ?? "form"; - switch (mode) { - case "url": { - if (!this._clientCapabilities?.elicitation?.url) { - throw new Error("Client does not support url elicitation."); - } - const urlParams = params; - return this.request({ method: "elicitation/create", params: urlParams }, ElicitResultSchema, options); - } - case "form": { - if (!this._clientCapabilities?.elicitation?.form) { - throw new Error("Client does not support form elicitation."); - } - const formParams = params.mode === "form" ? params : { ...params, mode: "form" }; - const result = await this.request({ method: "elicitation/create", params: formParams }, ElicitResultSchema, options); - if (result.action === "accept" && result.content && formParams.requestedSchema) { - try { - const validator = this._jsonSchemaValidator.getValidator(formParams.requestedSchema); - const validationResult = validator(result.content); - if (!validationResult.valid) { - throw new McpError(ErrorCode.InvalidParams, `Elicitation response content does not match requested schema: ${validationResult.errorMessage}`); - } - } catch (error2) { - if (error2 instanceof McpError) { - throw error2; - } - throw new McpError(ErrorCode.InternalError, `Error validating elicitation response: ${error2 instanceof Error ? error2.message : String(error2)}`); - } - } - return result; - } - } - } - createElicitationCompletionNotifier(elicitationId, options) { - if (!this._clientCapabilities?.elicitation?.url) { - throw new Error("Client does not support URL elicitation (required for notifications/elicitation/complete)"); - } - return () => this.notification({ - method: "notifications/elicitation/complete", - params: { - elicitationId - } - }, options); - } - async listRoots(params, options) { - return this.request({ method: "roots/list", params }, ListRootsResultSchema, options); - } - async sendLoggingMessage(params, sessionId) { - if (this._capabilities.logging) { - if (!this.isMessageIgnored(params.level, sessionId)) { - return this.notification({ method: "notifications/message", params }); - } - } - } - async sendResourceUpdated(params) { - return this.notification({ - method: "notifications/resources/updated", - params - }); - } - async sendResourceListChanged() { - return this.notification({ - method: "notifications/resources/list_changed" - }); - } - async sendToolListChanged() { - return this.notification({ method: "notifications/tools/list_changed" }); - } - async sendPromptListChanged() { - return this.notification({ method: "notifications/prompts/list_changed" }); - } -} - -// node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js -import process2 from "node:process"; - -// node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js -class ReadBuffer { - append(chunk) { - this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk; - } - readMessage() { - if (!this._buffer) { - return null; - } - const index = this._buffer.indexOf(` -`); - if (index === -1) { - return null; - } - const line = this._buffer.toString("utf8", 0, index).replace(/\r$/, ""); - this._buffer = this._buffer.subarray(index + 1); - return deserializeMessage(line); - } - clear() { - this._buffer = undefined; - } -} -function deserializeMessage(line) { - return JSONRPCMessageSchema.parse(JSON.parse(line)); -} -function serializeMessage(message) { - return JSON.stringify(message) + ` -`; -} - -// node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js -class StdioServerTransport { - constructor(_stdin = process2.stdin, _stdout = process2.stdout) { - this._stdin = _stdin; - this._stdout = _stdout; - this._readBuffer = new ReadBuffer; - this._started = false; - this._ondata = (chunk) => { - this._readBuffer.append(chunk); - this.processReadBuffer(); - }; - this._onerror = (error2) => { - this.onerror?.(error2); - }; - } - async start() { - if (this._started) { - throw new Error("StdioServerTransport already started! If using Server class, note that connect() calls start() automatically."); - } - this._started = true; - this._stdin.on("data", this._ondata); - this._stdin.on("error", this._onerror); - } - processReadBuffer() { - while (true) { - try { - const message = this._readBuffer.readMessage(); - if (message === null) { - break; - } - this.onmessage?.(message); - } catch (error2) { - this.onerror?.(error2); - } - } - } - async close() { - this._stdin.off("data", this._ondata); - this._stdin.off("error", this._onerror); - const remainingDataListeners = this._stdin.listenerCount("data"); - if (remainingDataListeners === 0) { - this._stdin.pause(); - } - this._readBuffer.clear(); - this.onclose?.(); - } - send(message) { - return new Promise((resolve) => { - const json = serializeMessage(message); - if (this._stdout.write(json)) { - resolve(); - } else { - this._stdout.once("drain", resolve); - } - }); - } -} - -// packages/bridge-mcp-server/src/index.ts -import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs"; -import { join } from "node:path"; -import { homedir } from "node:os"; -function getCredentialCachePath(workspaceId, sourceSlug) { - return join(homedir(), ".craft-agent", "workspaces", workspaceId, "sources", sourceSlug, ".credential-cache.json"); -} -function readCredential(workspaceId, sourceSlug) { - const cachePath = getCredentialCachePath(workspaceId, sourceSlug); - try { - if (!existsSync(cachePath)) { - return null; - } - const content = readFileSync(cachePath, "utf-8"); - const credential = JSON.parse(content); - if (credential.expiresAt && credential.expiresAt < Date.now()) { - return null; - } - return credential.value; - } catch { - return null; - } -} -function buildAuthorizationHeader(authScheme, token) { - const scheme = authScheme ?? "Bearer"; - return scheme ? `${scheme} ${token}` : token; -} -function buildHeaders(config2, credential) { - const headers = { - "Content-Type": "application/json", - ...config2.defaultHeaders - }; - if (config2.authType === "none" || !credential) { - return headers; - } - if (config2.authType === "bearer") { - headers["Authorization"] = buildAuthorizationHeader(config2.authScheme, credential); - } else if (config2.authType === "header") { - headers[config2.headerName || "X-API-Key"] = credential; - } else if (config2.authType === "basic") { - let authString = credential; - try { - const parsed = JSON.parse(credential); - if (parsed && typeof parsed === "object" && parsed.username && parsed.password) { - authString = `${parsed.username}:${parsed.password}`; - } - } catch {} - const base642 = Buffer.from(authString).toString("base64"); - headers["Authorization"] = `Basic ${base642}`; - } - return headers; -} -function buildUrl(config2, path, method, params, credential) { - const baseUrl = config2.baseUrl.endsWith("/") ? config2.baseUrl.slice(0, -1) : config2.baseUrl; - const normalizedPath = path.startsWith("/") ? path : `/${path}`; - let url = `${baseUrl}${normalizedPath}`; - if (config2.authType === "query" && config2.queryParam && credential) { - const separator = url.includes("?") ? "&" : "?"; - url += `${separator}${config2.queryParam}=${encodeURIComponent(credential)}`; - } - if (method === "GET" && params && Object.keys(params).length > 0) { - const urlParams = new URLSearchParams; - for (const [key, value] of Object.entries(params)) { - if (value !== undefined && value !== null) { - if (typeof value === "object") { - urlParams.append(key, JSON.stringify(value)); - } else { - urlParams.append(key, String(value)); - } - } - } - const queryString = urlParams.toString(); - if (queryString) { - const separator = url.includes("?") ? "&" : "?"; - url += `${separator}${queryString}`; - } - } - return url; -} -var MAX_RESPONSE_SIZE = 60 * 1024; -var TEXT_MIME_TYPES = [ - "text/", - "application/json", - "application/xml", - "application/javascript" -]; -function isTextContentType(contentType) { - if (!contentType) - return true; - const normalized = contentType.toLowerCase().split(";")[0]?.trim() ?? ""; - if (normalized.endsWith("+json") || normalized.endsWith("+xml")) { - return true; - } - return TEXT_MIME_TYPES.some((t) => t.endsWith("/") ? normalized.startsWith(t) : normalized === t); -} -function saveBinaryResponse(sessionPath, filename, buffer, mimeType) { - try { - const downloadsDir = join(sessionPath, "downloads"); - mkdirSync(downloadsDir, { recursive: true }); - const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19); - const safeName = filename || `download_${timestamp}.bin`; - const filePath = join(downloadsDir, safeName); - writeFileSync(filePath, buffer); - return { path: filePath, size: buffer.length }; - } catch (error2) { - return { error: `Failed to save file: ${error2 instanceof Error ? error2.message : String(error2)}` }; - } -} -function saveLargeResponse(sessionPath, toolName, apiPath, content) { - try { - const responsesDir = join(sessionPath, "responses"); - mkdirSync(responsesDir, { recursive: true }); - const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 23); - const safePath = apiPath.replace(/[^a-zA-Z0-9]/g, "_").slice(0, 30); - const filename = `${timestamp}_${toolName}_${safePath}.txt`; - const filePath = join(responsesDir, filename); - writeFileSync(filePath, content, "utf-8"); - return filePath; - } catch (error2) { - return { error: `Failed to save response: ${error2 instanceof Error ? error2.message : String(error2)}` }; - } -} -var FETCH_TIMEOUT_MS = 30000; -async function executeApiTool(config2, args, sessionPath) { - const { path, method, params } = args; - const credential = readCredential(config2.workspaceId, config2.slug) || ""; - if (!credential && config2.authType !== "none") { - return { - content: [{ - type: "text", - text: `Authentication required for ${config2.name}. Please authenticate the source in Qwen Code settings.` - }], - isError: true - }; - } - const url = buildUrl(config2, path, method, params, credential); - const headers = buildHeaders(config2, credential); - const controller = new AbortController; - const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); - const fetchOptions = { - method, - headers, - signal: controller.signal - }; - if (method !== "GET" && params && Object.keys(params).length > 0) { - fetchOptions.body = JSON.stringify(params); - } - try { - const response = await fetch(url, fetchOptions); - clearTimeout(timeoutId); - const contentType = response.headers.get("content-type"); - if (contentType && !isTextContentType(contentType) && sessionPath) { - const buffer = Buffer.from(await response.arrayBuffer()); - const result = saveBinaryResponse(sessionPath, "", buffer, contentType); - if ("error" in result) { - return { - content: [{ type: "text", text: result.error }], - isError: true - }; - } - return { - content: [{ - type: "text", - text: JSON.stringify({ - type: "file_download", - path: result.path, - size: result.size, - mimeType: contentType - }, null, 2) - }] - }; - } - const text = await response.text(); - if (!response.ok) { - return { - content: [{ - type: "text", - text: `API Error ${response.status}: ${text}` - }], - isError: true - }; - } - if (text.length > MAX_RESPONSE_SIZE && sessionPath) { - const saveResult = saveLargeResponse(sessionPath, config2.slug, path, text); - if (typeof saveResult === "object" && "error" in saveResult) { - const preview2 = text.slice(0, 8000); - return { - content: [{ - type: "text", - text: `Response too large (${Math.round(text.length / 1024)}KB) and could not be saved: ${saveResult.error} - -Truncated response: -${preview2}...` - }] - }; - } - const preview = text.slice(0, 2000); - return { - content: [{ - type: "text", - text: `Response too large (${Math.round(text.length / 1024)}KB). Full response saved to: ${saveResult} - -Preview: -${preview}...` - }] - }; - } - return { - content: [{ - type: "text", - text - }] - }; - } catch (error2) { - clearTimeout(timeoutId); - if (error2 instanceof Error && error2.name === "AbortError") { - return { - content: [{ - type: "text", - text: `Request timed out after ${FETCH_TIMEOUT_MS / 1000} seconds. The API may be slow or unresponsive.` - }], - isError: true - }; - } - return { - content: [{ - type: "text", - text: `Request failed: ${error2 instanceof Error ? error2.message : String(error2)}` - }], - isError: true - }; - } -} -function buildToolDescription(config2) { - let desc = `Make authenticated requests to ${config2.name} API (${config2.baseUrl}) - -`; - desc += `Authentication is handled automatically. - -`; - if (config2.guideRaw) { - desc += config2.guideRaw.slice(0, 2000); - if (config2.guideRaw.length > 2000) { - desc += ` - -[Guide truncated - see source guide.md for full documentation]`; - } - } - return desc; -} -function createTools(sources) { - return sources.map((source) => ({ - name: `api_${source.slug}`, - description: buildToolDescription(source), - inputSchema: { - type: "object", - properties: { - path: { - type: "string", - description: 'API endpoint path, e.g., "/search" or "/v1/completions"' - }, - method: { - type: "string", - enum: ["GET", "POST", "PUT", "DELETE", "PATCH"], - description: "HTTP method" - }, - params: { - type: "object", - description: "Request body (POST/PUT/PATCH) or query parameters (GET)", - additionalProperties: true - }, - _intent: { - type: "string", - description: "Describe what you are trying to accomplish (1-2 sentences)" - } - }, - required: ["path", "method"] - } - })); -} -function setupSignalHandlers() { - const shutdown = (signal) => { - console.error(`Bridge server received ${signal}, shutting down gracefully`); - process.exit(0); - }; - process.on("SIGTERM", () => shutdown("SIGTERM")); - process.on("SIGINT", () => shutdown("SIGINT")); - process.on("unhandledRejection", (reason, promise2) => { - console.error("Unhandled promise rejection in bridge server:", reason); - }); -} -async function main() { - setupSignalHandlers(); - const args = process.argv.slice(2); - let configPath; - let sessionPath; - for (let i = 0;i < args.length; i++) { - if (args[i] === "--config" && args[i + 1]) { - configPath = args[i + 1]; - i++; - } else if (args[i] === "--session" && args[i + 1]) { - sessionPath = args[i + 1]; - i++; - } - } - if (!configPath) { - console.error("Usage: bridge-mcp-server --config [--session ]"); - process.exit(1); - } - let config2; - try { - const content = readFileSync(configPath, "utf-8"); - config2 = JSON.parse(content); - } catch (error2) { - console.error(`Failed to load config from ${configPath}:`, error2); - process.exit(1); - } - if (!config2.sources || config2.sources.length === 0) { - console.error("No sources configured"); - process.exit(1); - } - const server = new Server({ - name: "craft-agent-api-bridge", - version: "0.3.1" - }, { - capabilities: { - tools: {} - } - }); - const sourceMap = new Map; - for (const source of config2.sources) { - sourceMap.set(`api_${source.slug}`, source); - } - server.setRequestHandler(ListToolsRequestSchema, async () => ({ - tools: createTools(config2.sources) - })); - server.setRequestHandler(CallToolRequestSchema, async (request) => { - const { name, arguments: toolArgs } = request.params; - const source = sourceMap.get(name); - if (!source) { - return { - content: [{ - type: "text", - text: `Unknown tool: ${name}` - }], - isError: true - }; - } - const args2 = toolArgs; - const result = await executeApiTool(source, args2, sessionPath); - return { content: result.content, isError: result.isError }; - }); - const transport = new StdioServerTransport; - await server.connect(transport); - console.error(`Bridge MCP Server started with ${config2.sources.length} API sources`); -} -main().catch((error2) => { - console.error("Fatal error:", error2); - process.exit(1); -}); diff --git a/packages/desktop/apps/electron/resources/config-defaults.json b/packages/desktop/apps/electron/resources/config-defaults.json deleted file mode 100644 index f23d158ec12..00000000000 --- a/packages/desktop/apps/electron/resources/config-defaults.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "version": "1.0", - "description": "Default configuration values for Qwen Code", - "defaults": { - "notificationsEnabled": true, - "colorTheme": "default", - "autoCapitalisation": true, - "sendMessageKey": "enter", - "spellCheck": false, - "keepAwakeWhileRunning": false, - "richToolDescriptions": true, - "extendedPromptCache": false, - "browserToolEnabled": true - }, - "workspaceDefaults": { - "thinkingLevel": "think", - "permissionMode": "allow-all", - "cyclablePermissionModes": ["allow-all", "safe", "ask", "auto-edit"], - "localMcpServers": { - "enabled": true - } - } -} diff --git a/packages/desktop/apps/electron/resources/craft-logos/craft_app_icon.png b/packages/desktop/apps/electron/resources/craft-logos/craft_app_icon.png deleted file mode 100644 index 51f20c952d3..00000000000 Binary files a/packages/desktop/apps/electron/resources/craft-logos/craft_app_icon.png and /dev/null differ diff --git a/packages/desktop/apps/electron/resources/craft-logos/craft_app_icon_dark.png b/packages/desktop/apps/electron/resources/craft-logos/craft_app_icon_dark.png deleted file mode 100644 index 1028701bdd4..00000000000 Binary files a/packages/desktop/apps/electron/resources/craft-logos/craft_app_icon_dark.png and /dev/null differ diff --git a/packages/desktop/apps/electron/resources/craft-logos/craft_logo_black.png b/packages/desktop/apps/electron/resources/craft-logos/craft_logo_black.png deleted file mode 100644 index 3ea6f8c605d..00000000000 Binary files a/packages/desktop/apps/electron/resources/craft-logos/craft_logo_black.png and /dev/null differ diff --git a/packages/desktop/apps/electron/resources/craft-logos/craft_logo_white.png b/packages/desktop/apps/electron/resources/craft-logos/craft_logo_white.png deleted file mode 100644 index 50a6a71bc8b..00000000000 Binary files a/packages/desktop/apps/electron/resources/craft-logos/craft_logo_white.png and /dev/null differ diff --git a/packages/desktop/apps/electron/resources/dmg-background.png b/packages/desktop/apps/electron/resources/dmg-background.png deleted file mode 100644 index 769b3e0ab2f..00000000000 Binary files a/packages/desktop/apps/electron/resources/dmg-background.png and /dev/null differ diff --git a/packages/desktop/apps/electron/resources/dmg-background.tiff b/packages/desktop/apps/electron/resources/dmg-background.tiff deleted file mode 100644 index 720514e25fd..00000000000 Binary files a/packages/desktop/apps/electron/resources/dmg-background.tiff and /dev/null differ diff --git a/packages/desktop/apps/electron/resources/dmg-background@2x.png b/packages/desktop/apps/electron/resources/dmg-background@2x.png deleted file mode 100644 index 1547b75d85c..00000000000 Binary files a/packages/desktop/apps/electron/resources/dmg-background@2x.png and /dev/null differ diff --git a/packages/desktop/apps/electron/resources/docs/automations.md b/packages/desktop/apps/electron/resources/docs/automations.md deleted file mode 100644 index 99016c17cc3..00000000000 --- a/packages/desktop/apps/electron/resources/docs/automations.md +++ /dev/null @@ -1,857 +0,0 @@ -# Automations Configuration Guide - -This guide explains how to configure automations in Qwen Code to automate workflows based on events. - -> **CLI-first workflow (recommended):** Use `craft-agent automation ...` commands instead of editing JSON directly. -> - `craft-agent automation --help` -> - Canonical command reference: [craft-cli.md](./craft-cli.md) - -## What Are Automations? - -Automations allow you to trigger actions automatically when specific events occur in Qwen Code. You can: -- Send prompts to create agent sessions based on events -- Send webhook HTTP requests to external services (Slack, Discord, custom APIs, etc.) -- Execute actions on a schedule using cron expressions -- Automate workflows based on permission mode changes, flags, or session status changes - -## automations.json Location - -Automations are configured in `automations.json` at the root of your workspace: - -``` -~/.craft-agent/workspaces/{workspaceId}/automations.json -``` - -## Recommended CLI Commands - -```bash -craft-agent automation list -craft-agent automation get -craft-agent automation create --event UserPromptSubmit --prompt "..." -craft-agent automation update --json '{...}' -craft-agent automation enable -craft-agent automation disable -craft-agent automation duplicate -craft-agent automation history [] --limit 20 -craft-agent automation last-executed -craft-agent automation test --match "..." -craft-agent automation lint -craft-agent automation validate -``` - -## Basic Structure - -```json -{ - "version": 2, - "automations": { - "EventName": [ - { - "name": "Optional display name", - "matcher": "regex-pattern", - "actions": [ - { "type": "prompt", "prompt": "Check for updates and report status" } - ] - } - ] - } -} -``` - -## Supported Events - -### App Events (triggered by Qwen Code) - -| Event | Trigger | Match Value | -|-------|---------|-------------| -| `LabelAdd` | Label added to session | Label ID (e.g., `bug`, not `Bug`) | -| `LabelRemove` | Label removed from session | Label ID (e.g., `bug`, not `Bug`) | -| `LabelConfigChange` | Label configuration changed | Always matches | -| `PermissionModeChange` | Permission mode changed | New mode name | -| `FlagChange` | Session flagged/unflagged | `true` or `false` | -| `SessionStatusChange` | Session status changed | New status (e.g., `done`, `in_progress`) | -| `SchedulerTick` | Runs every minute | Uses cron matching | - -> **Note:** `TodoStateChange` is a deprecated alias for `SessionStatusChange`. Existing configs using the old name will continue to work but will show a deprecation warning during validation. - -### Agent Events - -| Event | Trigger | Match Value | -|-------|---------|-------------| -| `PreToolUse` | Before a tool executes | Tool name | -| `PostToolUse` | After a tool executes successfully | Tool name | -| `PostToolUseFailure` | After a tool execution fails | Tool name | -| `Notification` | Notification received | - | -| `UserPromptSubmit` | User submits a prompt | - | -| `SessionStart` | Session starts | - | -| `SessionEnd` | Session ends | - | -| `Stop` | Agent stops | - | -| `SubagentStart` | Subagent spawned | - | -| `SubagentStop` | Subagent completes | - | -| `PreCompact` | Before context compaction | - | -| `PermissionRequest` | Permission requested | - | -| `Setup` | Initial setup | - | - -## Action Types - -### Prompt Actions - -Send a prompt to Qwen Code (creates a new session for scheduled prompts). - -```json -{ - "type": "prompt", - "prompt": "Run the @weather skill and summarize the forecast" -} -``` - -| Property | Type | Default | Description | -|----------|------|---------|-------------| -| `type` | `"prompt"` | Required | Action type | -| `prompt` | string | Required | Prompt text to send | -| `llmConnection` | string | Workspace default | LLM connection slug (configured in AI Settings) | -| `model` | string | Workspace default | Model ID for the created session | - -**Features:** -- Use `@mentions` to reference sources or skills -- Environment variables are expanded (e.g., `$CRAFT_LABEL`) - -**LLM Connection & Model:** Optionally specify the Qwen connection and model to use for the created session. If omitted, the workspace default connection and model are used. - -```json -{ - "type": "prompt", - "prompt": "Quick code review of recent changes", - "llmConnection": "qwen-code", - "model": "qwen3-coder-flash" -} -``` - -The `llmConnection` value is the slug of an LLM connection configured in AI Settings. The `model` value is a model ID supported by Qwen Code. If either is invalid or not found, it gracefully falls back to the workspace default. Both can be used independently or together. - -### Webhook Actions - -Send an HTTP request to an external endpoint when an event fires. Useful for notifications (Slack, Discord), logging to external services, or triggering external workflows. - -```json -{ - "type": "webhook", - "url": "https://hooks.slack.com/services/${CRAFT_WH_SLACK_PATH}", - "method": "POST", - "body": { - "text": "Session ${CRAFT_SESSION_NAME} status changed to ${CRAFT_NEW_STATE}" - } -} -``` - -| Property | Type | Default | Description | -|----------|------|---------|-------------| -| `type` | `"webhook"` | Required | Action type | -| `url` | string | Required | Target URL (http or https) | -| `method` | `"GET"` \| `"POST"` \| `"PUT"` \| `"PATCH"` \| `"DELETE"` | `"POST"` | HTTP method | -| `headers` | `Record` | `{}` | HTTP headers as key-value pairs | -| `bodyFormat` | `"json"` \| `"form"` \| `"raw"` | `"json"` | Body serialization format | -| `body` | object or string | - | Request body (omitted for GET requests) | -| `auth` | object | - | Authentication shorthand (see below) | -| `captureResponse` | boolean | `false` | Capture response body in result (truncated to 4KB) | - -> **URL validation:** Literal URLs are validated at config load time. Templated URLs (containing `$VAR`) are validated at runtime after variable expansion. Both must resolve to `http://` or `https://` — other protocols are rejected. - -**Body format:** -- `json` (default) — Body is serialized as JSON. `Content-Type: application/json` is set automatically unless you override it in `headers`. -- `form` — Body object keys are URL-encoded as `application/x-www-form-urlencoded`. Useful for OAuth token endpoints, Stripe, and legacy APIs. Each value supports `$VAR` expansion. -- `raw` — Body is sent as a plain string. Set `Content-Type` in `headers` yourself. - -**Authentication:** - -Instead of manually constructing `Authorization` headers, you can use the `auth` shorthand: - -**Bearer token:** -```json -{ - "type": "webhook", - "url": "https://api.example.com/events", - "auth": { - "type": "bearer", - "token": "${CRAFT_WH_API_TOKEN}" - }, - "body": { "event": "$CRAFT_EVENT" } -} -``` - -**Basic auth (username/password):** -```json -{ - "type": "webhook", - "url": "https://legacy.example.com/webhook", - "auth": { - "type": "basic", - "username": "${CRAFT_WH_USER}", - "password": "${CRAFT_WH_PASS}" - } -} -``` - -The `auth` field is applied before custom `headers`, so you can override the generated `Authorization` header if needed. All auth field values support `$VAR` expansion. - -**Response capture:** By default, webhook response bodies are discarded after reading (to release connections). Set `captureResponse: true` to capture the response body (truncated to 4KB). The captured body is included in the execution result and recorded in automation history (truncated to 500 chars). - -```json -{ - "type": "webhook", - "url": "https://api.example.com/status", - "method": "GET", - "captureResponse": true -} -``` - -> **Note:** Response capture adds memory overhead proportional to the response size. Only enable it for endpoints where you need to inspect the response. - -**Variable expansion:** The `url`, `headers` values, `body`, and `auth` fields all support `$VAR` and `${VAR}` syntax for environment variable expansion. See [Environment Variables](#environment-variables) below. - -**Security:** Webhook actions only have access to `CRAFT_*` system variables and `CRAFT_WH_*` user-defined secrets. They do **not** have access to your full system environment (e.g., `$HOME`, `$PATH`, or other process variables). - -## Environment Variables - -Both prompt and webhook actions support variable expansion using `$VAR` or `${VAR}` syntax. - -### System Variables (CRAFT_*) - -These are automatically set by the automation system based on the triggering event: - -| Variable | Description | Available For | -|----------|-------------|---------------| -| `$CRAFT_EVENT` | Event name (e.g., `LabelAdd`) | All events | -| `$CRAFT_EVENT_DATA` | Full event payload as JSON | All events | -| `$CRAFT_SESSION_ID` | Session ID | Events with session context | -| `$CRAFT_SESSION_NAME` | Session name | Events with session context | -| `$CRAFT_WORKSPACE_ID` | Workspace ID | All events | - -**Per-event variables:** - -| Event | Variable | Description | -|-------|----------|-------------| -| `LabelAdd` / `LabelRemove` | `$CRAFT_LABEL` | The label that was added/removed | -| `PermissionModeChange` | `$CRAFT_OLD_MODE`, `$CRAFT_NEW_MODE` | Previous and new permission mode | -| `FlagChange` | `$CRAFT_IS_FLAGGED` | `true` or `false` | -| `SessionStatusChange` | `$CRAFT_OLD_STATE`, `$CRAFT_NEW_STATE` | Previous and new status | -| `SchedulerTick` | `$CRAFT_LOCAL_TIME`, `$CRAFT_LOCAL_DATE` | Current time (`14:30`) and date (`2026-03-09`) | - -### User-Defined Webhook Secrets (CRAFT_WH_*) - -For webhook actions, you can define your own secrets by setting environment variables with the `CRAFT_WH_` prefix in your shell profile (e.g., `~/.zshrc`, `~/.bashrc`): - -```bash -# In your shell profile -export CRAFT_WH_SLACK_URL="https://hooks.slack.com/services/T.../B.../xxx" -export CRAFT_WH_DISCORD_URL="https://discord.com/api/webhooks/123/abc" -export CRAFT_WH_API_TOKEN="your-secret-token" -``` - -Then reference them in `automations.json`: - -```json -{ - "type": "webhook", - "url": "${CRAFT_WH_SLACK_URL}", - "method": "POST", - "body": { "text": "Hello from Qwen Code!" } -} -``` - -```json -{ - "type": "webhook", - "url": "https://api.example.com/events", - "headers": { "Authorization": "Bearer ${CRAFT_WH_API_TOKEN}" }, - "body": { "event": "${CRAFT_EVENT}", "session": "${CRAFT_SESSION_NAME}" } -} -``` - -This keeps secrets out of `automations.json` (which may be shared or committed to version control). - -> **Note:** Only variables prefixed with `CRAFT_WH_` are injected into webhook actions. Other environment variables (like `$HOME` or `$DATABASE_URL`) are not accessible to webhooks. - -> **Note:** Environment variables are not expanded during test runs (the "Test" button in the UI). Tests send the raw URL/body as configured. - -## Matcher Configuration - -### Display Name - -Use the optional `name` field to give an automation a human-readable display name. If omitted, the name is automatically derived from the first action. - -```json -{ - "name": "Morning Weather Report", - "cron": "0 8 * * *", - "actions": [ - { "type": "prompt", "prompt": "Run the @weather skill" } - ] -} -``` - -### Regex Matching (for most events) - -Use the `matcher` field to filter which events trigger your automations: - -```json -{ - "matcher": "^urgent$", - "actions": [ - { "type": "prompt", "prompt": "An urgent label was added. Review the session and summarise the issue." } - ] -} -``` - -If `matcher` is omitted, the automation triggers for all events of that type. - -### Cron Matching (for SchedulerTick) - -For `SchedulerTick` events, use cron expressions instead of regex: - -```json -{ - "cron": "0 9 * * 1-5", - "timezone": "America/New_York", - "actions": [ - { "type": "prompt", "prompt": "Give me a morning briefing" } - ] -} -``` - -**Cron format:** `minute hour day-of-month month day-of-week` - -| Field | Values | -|-------|--------| -| Minute | 0-59 | -| Hour | 0-23 | -| Day of month | 1-31 | -| Month | 1-12 | -| Day of week | 0-6 (0 = Sunday) | - -**Examples:** -- `*/15 * * * *` - Every 15 minutes -- `0 9 * * *` - Daily at 9:00 AM -- `0 9 * * 1-5` - Weekdays at 9:00 AM -- `30 14 1 * *` - 1st of each month at 2:30 PM - -**Timezone:** Use IANA timezone names (e.g., `Europe/Budapest`, `America/New_York`). Defaults to system timezone if not specified. - -## Conditions - -Conditions are optional filters that run **after** the matcher/cron matches but **before** actions fire. All conditions in the array must pass (implicit AND). If the array is empty or omitted, actions fire unconditionally. - -```json -{ - "cron": "0 9 * * *", - "timezone": "Europe/Budapest", - "conditions": [ - { - "condition": "time", - "weekday": ["mon", "tue", "wed", "thu", "fri"] - } - ], - "actions": [ - { "type": "prompt", "prompt": "Good morning! Here's your daily briefing." } - ] -} -``` - -### Time Conditions - -Check time-of-day and day-of-week in a given timezone. - -```json -{ - "condition": "time", - "after": "09:00", - "before": "17:00", - "weekday": ["mon", "tue", "wed", "thu", "fri"], - "timezone": "Europe/Budapest" -} -``` - -| Property | Type | Description | -|----------|------|-------------| -| `after` | `"HH:MM"` | Start of time window (inclusive) | -| `before` | `"HH:MM"` | End of time window (exclusive) | -| `weekday` | `string[]` | Allowed days: `mon`, `tue`, `wed`, `thu`, `fri`, `sat`, `sun` | -| `timezone` | string | IANA timezone. Falls back to matcher timezone, then system local | - -**Overnight ranges:** If `after` is later than `before` (e.g., `"after": "22:00", "before": "06:00"`), the range wraps across midnight. - -### State Conditions - -Check fields from the event payload. Useful for filtering on specific transitions or values. - -```json -{ - "condition": "state", - "field": "permissionMode", - "from": "safe", - "to": "allow-all" -} -``` - -| Property | Type | Description | -|----------|------|-------------| -| `field` | string | Payload field name (e.g., `permissionMode`, `sessionStatus`, `labels`, `isFlagged`) | -| `value` | any | Exact match | -| `from` | any | Previous value (for transition events) | -| `to` | any | New value (for transition events) | -| `contains` | string | Array membership check (e.g., check if a label is present) | -| `not_value` | any | Matches anything except this value | - -**Transition fields:** For `permissionMode` and `sessionStatus`, `from`/`to` automatically resolve to the correct payload keys (`oldMode`/`newMode`, `oldState`/`newState`). - -### Logical Composition - -Combine conditions with `and`, `or`, and `not`: - -```json -{ - "condition": "and", - "conditions": [ - { "condition": "time", "weekday": ["mon", "tue", "wed", "thu", "fri"] }, - { "condition": "time", "after": "09:00", "before": "17:00" } - ] -} -``` - -```json -{ - "condition": "or", - "conditions": [ - { "condition": "state", "field": "permissionMode", "value": "allow-all" }, - { "condition": "state", "field": "isFlagged", "value": true } - ] -} -``` - -```json -{ - "condition": "not", - "conditions": [ - { "condition": "time", "weekday": ["sat", "sun"] } - ] -} -``` - -| Type | Behaviour | -|------|-----------| -| `and` | All sub-conditions must pass | -| `or` | At least one sub-condition must pass | -| `not` | None of the sub-conditions may pass | - -**Nesting depth:** Conditions can be nested up to 8 levels deep. A simplification warning is emitted at depth 4. Unknown condition types fail closed (evaluate to false). - -## Permission Mode - -The `permissionMode` field controls the permission level of sessions created by prompt actions. - -```json -{ - "cron": "*/10 * * * *", - "permissionMode": "allow-all", - "actions": [ - { "type": "prompt", "prompt": "Check system health and log the results" } - ] -} -``` - -**Permission modes:** -- `safe` - Session runs in Explore mode (default) -- `ask` - Session prompts for approval before write operations -- `allow-all` - Session auto-approves all operations - -## Labels for Prompt Actions - -Prompt actions can specify labels that will be applied to the session they create: - -```json -{ - "cron": "0 9 * * *", - "labels": ["Scheduled", "morning-briefing"], - "actions": [ - { "type": "prompt", "prompt": "Give me today's priorities" } - ] -} -``` - -This creates a session with the "Scheduled" and "morning-briefing" labels applied automatically. - -## Complete Examples - -### Daily Weather Report - -```json -{ - "version": 2, - "automations": { - "SchedulerTick": [ - { - "name": "Daily Weather Report", - "cron": "0 8 * * *", - "timezone": "Europe/Budapest", - "labels": ["Scheduled", "weather"], - "actions": [ - { "type": "prompt", "prompt": "Run the @weather skill and give me today's forecast" } - ] - } - ] - } -} -``` - -### Weekday-Only AI News (with Conditions) - -Use a `time` condition to restrict a daily schedule to weekdays only: - -```json -{ - "version": 2, - "automations": { - "SchedulerTick": [ - { - "name": "Morning AI news", - "cron": "0 9 * * *", - "timezone": "Europe/Budapest", - "conditions": [ - { - "condition": "time", - "weekday": ["mon", "tue", "wed", "thu", "fri"], - "timezone": "Europe/Budapest" - } - ], - "labels": ["Scheduled", "ai-news"], - "actions": [ - { "type": "prompt", "prompt": "Run the @ai-news skill and summarize today's AI developments" } - ] - } - ] - } -} -``` - -### Permission Mode Gate (with Conditions) - -Only notify when permission mode changes specifically from `safe` to `allow-all`: - -```json -{ - "version": 2, - "automations": { - "PermissionModeChange": [ - { - "conditions": [ - { - "condition": "state", - "field": "permissionMode", - "from": "safe", - "to": "allow-all" - } - ], - "actions": [ - { - "type": "webhook", - "url": "${CRAFT_WH_SLACK_URL}", - "method": "POST", - "body": { "text": ":warning: Permission escalated from safe to allow-all in *${CRAFT_SESSION_NAME}*" } - } - ] - } - ] - } -} -``` - -### Log Label Changes - -```json -{ - "version": 2, - "automations": { - "LabelAdd": [ - { - "actions": [ - { "type": "prompt", "prompt": "The label $CRAFT_LABEL was added. Log this change with a timestamp." } - ] - } - ], - "LabelRemove": [ - { - "actions": [ - { "type": "prompt", "prompt": "The label $CRAFT_LABEL was removed. Log this change with a timestamp." } - ] - } - ] - } -} -``` - -### Urgent Label Notification - -```json -{ - "version": 2, - "automations": { - "LabelAdd": [ - { - "matcher": "^urgent$", - "actions": [ - { "type": "prompt", "prompt": "An urgent label was added to this session. Triage the session and summarise what needs immediate attention." } - ] - } - ] - } -} -``` - -### Permission Mode Change Notification - -```json -{ - "version": 2, - "automations": { - "PermissionModeChange": [ - { - "matcher": "allow-all", - "actions": [ - { "type": "prompt", "prompt": "The permission mode was changed to allow-all. Log the change and note any security implications." } - ] - } - ] - } -} -``` - -### Slack Notification on Status Change - -Sends a Slack message when a session is marked as done. Requires `CRAFT_WH_SLACK_URL` in your shell profile. - -```json -{ - "version": 2, - "automations": { - "SessionStatusChange": [ - { - "name": "Notify Slack on Done", - "matcher": "^done$", - "actions": [ - { - "type": "webhook", - "url": "${CRAFT_WH_SLACK_URL}", - "method": "POST", - "body": { - "text": ":white_check_mark: Session *${CRAFT_SESSION_NAME}* marked as done" - } - } - ] - } - ] - } -} -``` - -### Mixed Actions (Prompt + Webhook) - -A single automation can have both prompt and webhook actions. They execute in order. - -```json -{ - "version": 2, - "automations": { - "LabelAdd": [ - { - "name": "Urgent: Notify and Triage", - "matcher": "^urgent$", - "actions": [ - { - "type": "webhook", - "url": "${CRAFT_WH_SLACK_URL}", - "method": "POST", - "body": { "text": ":rotating_light: Urgent label added to *${CRAFT_SESSION_NAME}*" } - }, - { - "type": "prompt", - "prompt": "An urgent label was added. Triage the session and summarise what needs immediate attention." - } - ] - } - ] - } -} -``` - -### Form-Encoded Request (OAuth / Stripe) - -```json -{ - "version": 2, - "automations": { - "SchedulerTick": [ - { - "name": "Refresh API Token", - "cron": "0 */6 * * *", - "actions": [ - { - "type": "webhook", - "url": "https://auth.example.com/oauth/token", - "method": "POST", - "bodyFormat": "form", - "body": { - "grant_type": "client_credentials", - "client_id": "${CRAFT_WH_CLIENT_ID}", - "client_secret": "${CRAFT_WH_CLIENT_SECRET}" - } - } - ] - } - ] - } -} -``` - -### Webhook with Custom Headers - -```json -{ - "version": 2, - "automations": { - "SessionStatusChange": [ - { - "name": "Log to External API", - "actions": [ - { - "type": "webhook", - "url": "https://api.example.com/craft-events", - "method": "POST", - "headers": { - "Authorization": "Bearer ${CRAFT_WH_API_TOKEN}", - "X-Source": "craft-agent" - }, - "body": { - "event": "${CRAFT_EVENT}", - "session_id": "${CRAFT_SESSION_ID}", - "old_status": "${CRAFT_OLD_STATE}", - "new_status": "${CRAFT_NEW_STATE}" - } - } - ] - } - ] - } -} -``` - -## Validation - -Automations are validated when: -1. The workspace is loaded -2. You edit automations.json (via PreToolUse hook) -3. You run `config_validate` with target `automations` or `all` - -**Using config_validate:** - -Ask Qwen Code to validate your automations configuration: - -``` -Validate my automations configuration -``` - -Or use the `config_validate` tool directly with `target: "automations"`. - -**Common validation errors:** -- Invalid JSON syntax -- Unknown event names -- Empty actions array -- Invalid cron expression -- Invalid timezone -- Invalid regex pattern -- Potentially unsafe regex patterns (nested quantifiers) - -**To validate manually:** - -```bash -# Check automations.json syntax -cat automations.json | jq . -``` - -## Retry Behavior - -Webhook actions have two levels of automatic retry: - -### Immediate retry (transient failures) - -When a webhook fails with a server error (5xx), timeout, or connection error, it is automatically retried up to **2 times** with exponential backoff (1s → 2s → 4s). Client errors (4xx) are not retried — they indicate a configuration problem. - -### Deferred retry (extended outages) - -If all immediate retries fail, the webhook is added to a **persistent retry queue**. The queue retries at increasing intervals: - -| Attempt | Delay | Cumulative | -|---------|-------|------------| -| 1st deferred | 5 minutes | 5 min | -| 2nd deferred | 30 minutes | 35 min | -| 3rd deferred | 1 hour | ~1.5 hours | - -After the final deferred attempt fails, the webhook is marked as permanently failed in the history. Deferred retries survive app restarts. - -> **Note:** Only transient failures (5xx, timeouts, connection errors) are retried. Client errors (4xx) indicate a configuration problem and should be fixed in `automations.json`. - -> **Retry and rate limiting:** Retried webhook requests count toward the per-endpoint rate limit (30/min per origin). If a retry would exceed the limit, it is deferred to the next retry window. - -## Rate Limits - -To protect against runaway automations (e.g., an automation that indirectly triggers itself in a loop), the event bus enforces per-event-type rate limits: - -| Event | Max fires / minute | -|-------|--------------------| -| `SchedulerTick` | 60 (1/sec) | -| All others (`LabelAdd`, `FlagChange`, `PreToolUse`, etc.) | 10 | - -When a limit is hit, further events of that type are **silently dropped** for the remainder of the 60-second window. A warning is logged. The window resets automatically. - -**Example:** If you have a `LabelAdd` task that triggers a prompt which adds a label back to a session, it will fire at most 10 times before being rate-limited — preventing infinite session creation. - -## Troubleshooting - -### Automation not firing - -1. **Check event name** - Must be exact (e.g., `LabelAdd` not `labeladd`) -2. **Check matcher** - Regex must match the event value -3. **Check cron** - For SchedulerTick, verify cron expression with an online tool -4. **Check logs** - Look for `[automations]` or `[Scheduler]` in the logs - -### Prompt not creating session - -1. Check that the prompt is not empty -2. Verify @mentions reference valid sources/skills - -### Webhook not working - -1. **Check URL** — Must be a valid `http://` or `https://` URL. Other protocols (ftp, ws, etc.) are rejected at runtime with a clear error. -2. **Check env vars** — Ensure `CRAFT_WH_*` variables are set in your shell profile and Qwen Code was restarted after adding them. URLs using `$VAR` templates are validated after variable expansion — if the variable is empty or unset, the URL will be invalid. -3. **Use the Test button** — Tests connectivity to the URL (note: env vars are not expanded during test) -4. **Check method** — Some endpoints require specific HTTP methods (POST, PUT, etc.) -5. **Check response** — The automation history shows HTTP status codes for webhook executions - -### Retrying failed webhooks - -When a webhook execution fails (shown with a red indicator in the timeline), you can retry it: - -1. Open the automation's detail page -2. In the "Recent Activity" timeline, failed webhook entries show a **Retry** button -3. Click "Retry" to re-execute the webhook actions immediately -4. The retry result is recorded as a new history entry - -> **Note:** Retries execute the webhook actions as currently configured. If you've changed the URL or headers since the original failure, the retry uses the updated configuration. Environment variables are not expanded during replay (same as the Test button). - -## Best Practices - -1. **Start simple** - Test with a basic prompt before building complex workflows -2. **Use labels** - Tag scheduled sessions for easy filtering -3. **Be specific** - Use matchers to avoid triggering on every event -4. **Test cron** - Use [crontab.guru](https://crontab.guru/) to verify expressions -5. **Keep secrets out of config** - Use `CRAFT_WH_*` env vars for webhook URLs and tokens instead of hardcoding them in automations.json -6. **Combine actions** - Use both webhook and prompt actions in a single automation for notification + AI response workflows diff --git a/packages/desktop/apps/electron/resources/docs/browser-tools.md b/packages/desktop/apps/electron/resources/docs/browser-tools.md deleted file mode 100644 index 78d0ea15902..00000000000 --- a/packages/desktop/apps/electron/resources/docs/browser-tools.md +++ /dev/null @@ -1,289 +0,0 @@ -# Browser Tools - -Use `browser_tool` to control built-in browser windows (Chromium) inside Qwen Code. - -> **Quick start:** Run `browser_tool --help` to see all available commands and usage examples. - -## Browser usage paths - -1. **Primary and only in-session tool surface:** `browser_tool` -2. **Secondary helper CLI:** `bun run browser-tool --help` for command discovery/templates, and `bun run browser-tool parse-url ` for safe URL diagnostics outside agent turns - ---- - -## Browser as an Alternative to Source Setup - -Use browser workflows when creating a source would add unnecessary overhead for the current task. - -**Good fit for browser-first:** -- One-off tasks that don’t need reusable integration -- UI-only workflows where API/MCP coverage is poor -- Fragile source setup/auth cases where user needs results now - -**Still prefer sources when:** -- Work is repeatable and automation/reporting is needed -- Team-wide reuse and stable tooling matter - ---- - -## Core workflow - -If you're unsure which window to use, run: - -```text -browser_tool({ command: "windows" }) -``` - -Recommended flow: -1. `open` — ensure browser window exists (background by default) -2. `navigate ` — load a URL -3. `snapshot` — inspect accessible elements and get refs (`@e1`, `@e2`, ...) -4. `find ` — quickly narrow to matching refs by keyword -5. `click` / `fill` / `select` — interact using refs -6. `screenshot --annotated` (or `screenshot-region`) — visual verification when needed - ---- - -## `browser_tool` command examples - -```text -browser_tool({ command: "--help" }) -browser_tool({ command: "open" }) -browser_tool({ command: "open --foreground" }) -browser_tool({ command: "navigate https://example.com" }) -browser_tool({ command: "snapshot" }) -browser_tool({ command: "find login button" }) -browser_tool({ command: "click @e12" }) -browser_tool({ command: "click-at 350 200" }) -browser_tool({ command: "drag 100 200 300 400" }) -browser_tool({ command: "fill @e5 user@example.com" }) -browser_tool({ command: "type Hello World" }) -browser_tool({ command: "select @e3 optionValue" }) -browser_tool({ command: "select @e75 CNAME --assert-text Target --timeout 3000" }) -browser_tool({ command: "upload @e3 /absolute/path/to/file.pdf" }) -browser_tool({ command: "set-clipboard Name\tAge\nAlice\t30" }) -browser_tool({ command: "get-clipboard" }) -browser_tool({ command: "paste Name\tAge\nAlice\t30" }) -browser_tool({ command: "scroll down 800" }) -browser_tool({ command: "evaluate document.title" }) -browser_tool({ command: "console 50 warn" }) -browser_tool({ command: "screenshot" }) -browser_tool({ command: "screenshot --annotated" }) -browser_tool({ command: "screenshot-region --ref @e12 --padding 8" }) -browser_tool({ command: "window-resize 1280 720" }) -browser_tool({ command: "network 50 failed" }) -browser_tool({ command: "wait network-idle 8000" }) -browser_tool({ command: "key Enter" }) -browser_tool({ command: "downloads wait 15000" }) -browser_tool({ command: "focus" }) -browser_tool({ command: "windows" }) -browser_tool({ command: "release" }) -browser_tool({ command: "hide" }) -browser_tool({ command: "close" }) -``` - -The wrapper validates commands and returns actionable errors when arguments are missing or invalid. - -It also returns rich execution feedback for most commands, including before/after state where available (scroll positions, active element, URL/title transitions, resize clamping, request/error summaries, and window ownership/visibility details). - -You can batch commands with semicolons, for example: -`fill @e1 user@example.com; fill @e2 password123; click @e3` - -Batches run left-to-right and stop automatically after navigation commands (`navigate`, `click`, `back`, `forward`) so refs don’t go stale silently. - -### Quoting and escaping - -`browser_tool` supports quoted arguments: -- Double quotes: `fill @e5 "Hello world"` -- Single quotes: `wait text 'welcome back' 5000` - -Semicolons inside quotes are treated as literal text (not batch separators): -- `fill @e1 "a;b;c"; click @e2` -- `screenshot-region --selector "div[data-x='a;b']" --padding 8` - -Use backslash escaping when needed: -- `\;` for a literal semicolon outside quotes -- `\"` for a literal `"` inside double-quoted text - ---- - -## Key commands - -### `open [--foreground|-f]` -Create or reuse the session browser window. -- Default: opens in background -- `--foreground` / `-f`: focuses in foreground - -### `snapshot` -Returns an accessibility tree with refs and element metadata. - -### `find ` -Performs keyword search over the snapshot accessibility nodes (`role`, `name`, `value`, `description`) and returns matching refs. - -### `click [waitFor] [timeoutMs]` -Click an element ref from `snapshot`. Optional wait modes: `none`, `navigation`, `network-idle`. - -### `click-at ` -Click at raw pixel coordinates. Use this for **canvas-based UIs** (e.g., Google Sheets cells, map elements, chart data points) where `snapshot` can't produce element refs. Get coordinates from `screenshot` or `screenshot-region`. - -### `drag ` -Drag from pixel coordinates (x1, y1) to (x2, y2). Performs mousedown, interpolated mousemove events, and mouseup. Use this for: -- Moving charts or objects in canvas-based UIs (e.g., Google Sheets charts) -- Reordering items via drag-and-drop -- Resizing elements by dragging handles -- Drawing or selecting regions - -Get coordinates from `screenshot` or `screenshot --annotated`. - -### `fill ` / `select [--assert-text ] [--assert-value ] [--timeout ]` -Fill text inputs or select dropdown values. Requires an element ref from `snapshot`. - -For modern React/portal combobox UIs, `select` now performs additional verification and may return a warning when interaction succeeds but form state does not appear to mutate. - -Useful flags: -- `--assert-text `: verify downstream UI mutation (for example field label changes to `Target`) -- `--assert-value `: verify selected control reflects expected value -- `--timeout `: verification timeout (default 2000ms) - -### `upload [path2...]` -Attach local file(s) to a file input (``) using a ref from `snapshot`. - -Notes: -- Use absolute file paths. -- Multiple files are supported: `upload @e3 /path/a.pdf /path/b.jpg` -- Files must exist and pass safety validation (sensitive paths are blocked). - -### `type ` -Type text character-by-character into the **currently focused element** without needing a ref. Use this when: -- The target is a canvas-based input (no DOM ref available) -- You've already focused an element via `click` or `click-at` -- The application uses a custom input mechanism - -Difference from `fill`: `fill` focuses a ref and replaces its value. `type` sends keystrokes to whatever is currently focused. - -### `set-clipboard ` / `get-clipboard` -Read or write the page clipboard programmatically. -- `set-clipboard` writes text and interprets common escape sequences: - - `\t` → tab - - `\n` → newline - - `\r` → carriage return - - `\\` → literal backslash -- Unknown escapes are preserved literally (example: `\\x` stays `\\x`) -- `get-clipboard` reads the current clipboard text content as raw text (tabs/newlines are returned as actual characters) - -### `paste ` -Convenience command: writes text to clipboard then triggers Ctrl+V (or Cmd+V on Mac). Equivalent to `set-clipboard ` followed by `key v meta`/`key v control`. Escape handling is identical to `set-clipboard`, which makes TSV-style bulk data entry reliable. - -### `screenshot` / `screenshot --annotated` / `screenshot-region ...` -Capture full-window or targeted screenshots. `--annotated` overlays `@eN` labels on interactive elements for easier ref debugging. - -### `console`, `network`, `wait`, `downloads` -Debug runtime issues, requests, synchronization points, and download progress. - -`downloads` output includes the resolved local `savePath` when available so you can reference the downloaded file directly. - -### `focus [windowId]` / `windows` -Manage and inspect browser window ownership and visibility. - -### Lifecycle commands -- `release` — dismiss agent overlay, keep window visible for user -- `hide` — hide window but preserve session state -- `close` — close and destroy window - ---- - -## Common validation errors - -- `Missing command...` → pass a command string (try `--help`) -- `Unknown browser_tool command ...` → typo/unsupported verb; check help -- `...requires ...` → required argument is missing for that command -- `...must be numbers` → numeric argument parse failed - ---- - -## Secondary helper: `browser-tool parse-url` - -Use this for safe URL debugging in Explore mode without running a generic interpreter snippet: - -```bash -bun run browser-tool parse-url https://example.com/path?q=1#hash -bun run browser-tool parse-url file:///Users/me/Desktop/report.html -``` - -Output is deterministic JSON (`href`, `protocol`, `host`, `hostname`, `pathname`, `search`, `hash`, `origin`, plus `basename` for `file://` URLs). - ---- - -## Behavior notes - -- Browser tools are allowed in **Explore/Safe mode** by default. -- Before first browser tool usage, the agent must read this guide (`~/.craft-agent/docs/browser-tools.md`). -- Closing browser UI via OS controls may hide the window; use `browser_tool close` for explicit teardown. - ---- - -## Recipe: Canvas-based UIs (Google Sheets, etc.) - -Canvas-based web apps (Google Sheets, Google Docs, some map/chart UIs) render content as pixels on `` — individual cells or elements are not DOM nodes and won't appear in `snapshot`. Use these patterns instead: - -### Google Sheets workflow - -```text -# 1. Navigate and wait for load -navigate https://docs.google.com/spreadsheets/d/{id}/edit -wait selector [aria-label="Name Box"] 10000 - -# 2. Navigate to a cell via Name Box (a DOM element — snapshot finds it) -snapshot -click @nameBoxRef -type A1 -key Enter - -# 3. Edit a cell -key F2 -type Hello World -key Enter - -# 4. Bulk write via TSV clipboard paste -snapshot -click @nameBoxRef -type A1 -key Enter -paste Name\tAge\tCity\nAlice\t30\tNYC\nBob\t25\tLA - -# 5. Read data via clipboard -key a meta # Select all (Cmd+A) -key c meta # Copy (Cmd+C) -get-clipboard # Returns TSV string - -# 6. Click a canvas cell by coordinates (from screenshot) -click-at 350 200 - -# 7. Move a chart by dragging (coordinates from screenshot) -drag 400 300 100 50 - -# 8. Read data via export URL (no editing needed) -navigate https://docs.google.com/spreadsheets/d/{id}/export?format=csv&gid=0 -``` - -### Key principles for canvas UIs -- **Name Box and formula bar are DOM elements** — `snapshot` can find them -- **Cells are canvas pixels** — use `click-at` or keyboard navigation, not `click` -- **Charts and objects are moveable** — use `drag` to reposition elements on the canvas -- **Keyboard shortcuts are more reliable than clicking** — use `key` for navigation -- **Clipboard TSV is the fastest bulk data path** — `paste` with tab-separated values -- **Export URLs work with session cookies** — no API key needed for reads - ---- - -## Troubleshooting - -### "Browser window controls are not available" -The desktop browser manager isn’t wired for this runtime/session. Ensure you’re in the Electron desktop app and session is initialized. - -### "Element @eX not found" -Refs are stale. Re-run `snapshot` and use fresh refs. - -### Interaction feels flaky -Wait for page readiness and retry using: -`open` → `snapshot` → interaction diff --git a/packages/desktop/apps/electron/resources/docs/craft-cli.md b/packages/desktop/apps/electron/resources/docs/craft-cli.md deleted file mode 100644 index 7c4bc193876..00000000000 --- a/packages/desktop/apps/electron/resources/docs/craft-cli.md +++ /dev/null @@ -1,395 +0,0 @@ -# Qwen Code CLI Guide - -`craft-agent` is the preferred interface for managing workspace config domains such as labels, sources, skills, and automations. - -## Usage - -```bash -craft-agent [args] [--flags] [--json ''] [--stdin] -``` - -### Global flags -- `craft-agent --help` -- `craft-agent --version` -- `craft-agent --discover` - -### Input modes -- Flat flags for simple values -- `--json` for structured inputs -- `--stdin` for piped JSON object input - ---- - - -## Label - -Manage workspace labels stored under `labels/`. - -### Commands -- `craft-agent label list` -- `craft-agent label get ` -- `craft-agent label create --name "" [--color ""] [--parent-id ] [--value-type string|number|date]` -- `craft-agent label update [--name ""] [--color ""] [--value-type string|number|date|none] [--clear-value-type]` -- `craft-agent label delete ` -- `craft-agent label move --parent ` -- `craft-agent label reorder [--parent ] ...` -- `craft-agent label auto-rule-list ` -- `craft-agent label auto-rule-add --pattern "" [--flags "gi"] [--value-template "$1"] [--description "..."]` -- `craft-agent label auto-rule-remove --index ` -- `craft-agent label auto-rule-clear ` -- `craft-agent label auto-rule-validate ` - -### Examples - -```bash -craft-agent label list -craft-agent label get bug -craft-agent label create --name "Bug" --color "accent" -craft-agent label create --name "Priority" --value-type number -craft-agent label update bug --json '{"name":"Bug Report","color":"destructive"}' -craft-agent label update priority --value-type none -craft-agent label move bug --parent root -craft-agent label reorder --parent root development content bug -craft-agent label auto-rule-add linear-issue --pattern "\\b([A-Z]{2,5}-\\d+)\\b" --value-template "$1" -craft-agent label auto-rule-list linear-issue -craft-agent label auto-rule-validate linear-issue -``` - -### Notes -- Use `--json` / `--stdin` for nested or bulk updates. -- IDs are stable slugs generated from name on create. -- Use `--value-type none` or `--clear-value-type` to remove a label value type. - - ---- - - -## Source - -Manage workspace sources stored under `sources/{slug}/`. - -### Commands -- `craft-agent source list [--include-builtins true|false]` -- `craft-agent source get ` -- `craft-agent source create` (see flags below) -- `craft-agent source update --json '{...}'` -- `craft-agent source delete ` -- `craft-agent source validate ` -- `craft-agent source test ` -- `craft-agent source init-guide [--template generic|mcp|api|local]` -- `craft-agent source init-permissions [--mode read-only]` -- `craft-agent source auth-help ` - -### Flags for `source create` - -| Flag | Description | -|------|-------------| -| `--name ""` | **(required)** Source display name | -| `--provider ""` | **(required)** Provider identifier (e.g., `linear`, `github`) | -| `--type mcp\|api\|local` | **(required)** Source type | -| `--enabled true\|false` | Enable/disable source (default: `true`) | -| `--icon ""` | Icon URL (auto-downloaded) or emoji | -| **MCP-specific** | | -| `--url ""` | MCP server URL | -| `--transport http\|stdio` | MCP transport type | -| `--auth-type oauth\|bearer\|none` | MCP authentication type | -| **API-specific** | | -| `--base-url ""` | **(required for api)** API base URL (must have trailing slash) | -| `--auth-type bearer\|header\|query\|basic\|none` | **(required for api)** API auth type | -| **Local-specific** | | -| `--path ""` | **(required for local)** Filesystem path | - -### Examples - -```bash -craft-agent source list -craft-agent source get linear -# MCP source with flat flags -craft-agent source create --name "Linear" --provider "linear" --type mcp --url "https://mcp.linear.app/sse" --auth-type oauth -# MCP source with --json for nested config -craft-agent source create --name "Linear" --provider "linear" --type mcp --json '{"mcp":{"transport":"http","url":"https://mcp.linear.app/sse","authType":"oauth"}}' -# API source -craft-agent source create --name "Exa" --provider "exa" --type api --base-url "https://api.exa.ai/" --auth-type header -# Local source -craft-agent source create --name "Docs Folder" --provider "filesystem" --type local --path "~/Documents" -craft-agent source update linear --json '{"enabled":false}' -craft-agent source validate linear -craft-agent source test linear -craft-agent source init-guide linear --template mcp -craft-agent source init-permissions linear --mode read-only -craft-agent source auth-help linear -``` - -### Notes -- Use flat flags for simple values or `--json` for type-specific nested config fields (`mcp`, `api`, `local`). -- `init-guide` scaffolds a practical `guide.md` based on source type. -- `init-permissions` scaffolds read-only `permissions.json` patterns for Explore mode. -- `auth-help` returns the recommended in-session auth tool and mode. -- `test` is lightweight CLI validation; for full in-session auth/connection probing use `source_test` MCP tool. - - ---- - - -## Skill - -Manage workspace skills stored under `skills/{slug}/SKILL.md`. - -### Commands -- `craft-agent skill list [--workspace-only] [--project-root ]` -- `craft-agent skill get [--project-root ]` -- `craft-agent skill where [--project-root ]` -- `craft-agent skill create` (see flags below) -- `craft-agent skill update --json '{...}' [--project-root ]` -- `craft-agent skill delete ` -- `craft-agent skill validate [--source workspace|project|global] [--project-root ]` - -### Flags for `skill create` - -| Flag | Description | -|------|-------------| -| `--name ""` | **(required)** Skill display name | -| `--description ""` | **(required)** Brief description (1-2 sentences) | -| `--slug ""` | Custom slug (auto-generated from name if omitted) | -| `--body "..."` | Skill content/instructions (markdown body) | -| `--icon ""` | Icon URL (auto-downloaded to `icon.*`) | -| `--globs "*.ts,*.tsx"` | Comma-separated glob patterns for auto-suggestion | -| `--always-allow "Bash,Write"` | Comma-separated tool names to always allow | -| `--required-sources "linear,github"` | Comma-separated source slugs to auto-enable | - -### Examples - -```bash -craft-agent skill list -craft-agent skill list --workspace-only -craft-agent skill where commit-helper -craft-agent skill create --name "Commit Helper" --description "Generate conventional commits" --slug commit-helper -craft-agent skill create --name "Code Review" --description "Review PRs" --globs "*.ts,*.tsx" --always-allow "Bash" --required-sources "github" -craft-agent skill update commit-helper --json '{"requiredSources":["github"],"body":"Use concise, imperative commit messages."}' -craft-agent skill validate commit-helper -craft-agent skill validate commit-helper --source global -craft-agent skill delete commit-helper -``` - -### Notes -- `create` / `update` write `SKILL.md` frontmatter and content body. -- Use `where` to inspect project/workspace/global resolution precedence. -- `--project-root` scopes resolution to a project directory (defaults to cwd). - - ---- - - -## Automation - -Manage workspace automations stored in `automations.json`. - -### Commands -- `craft-agent automation list` -- `craft-agent automation get ` -- `craft-agent automation create` (see flags below) -- `craft-agent automation update ` (same flags as create, all optional) -- `craft-agent automation delete ` -- `craft-agent automation enable ` -- `craft-agent automation disable ` -- `craft-agent automation duplicate ` -- `craft-agent automation history [] [--limit ]` -- `craft-agent automation last-executed ` -- `craft-agent automation test [--match "..."]` -- `craft-agent automation lint` -- `craft-agent automation validate` - -### Flags for `automation create` / `update` - -| Flag | Description | -|------|-------------| -| `--event ` | **(required for create)** Event trigger (e.g., `UserPromptSubmit`, `SchedulerTick`, `LabelAdd`) | -| `--name ""` | Display name for the automation | -| `--matcher ""` | Regex pattern for event matching | -| `--cron ""` | Cron expression (for `SchedulerTick` events) | -| `--timezone ""` | IANA timezone (e.g., `Europe/Budapest`) | -| `--permission-mode safe\|ask\|allow-all` | Permission level for created sessions | -| `--enabled true\|false` | Enable/disable the automation | -| `--labels "label1,label2"` | Comma-separated labels for created sessions | -| `--prompt "..."` | Prompt text (creates a prompt action automatically) | -| `--llm-connection ""` | LLM connection slug for the created session | -| `--model ""` | Model ID for the created session | - -### Examples - -```bash -craft-agent automation list -craft-agent automation validate -# Simple prompt automation with flat flags -craft-agent automation create --event UserPromptSubmit --prompt "Summarize this prompt" -# Scheduled automation with flat flags -craft-agent automation create --event SchedulerTick --cron "0 9 * * 1-5" --timezone "Europe/Budapest" --prompt "Give me a morning briefing" --labels "Scheduled" --permission-mode safe -# Complex automation with --json -craft-agent automation create --event SchedulerTick --json '{"cron":"0 9 * * 1-5","actions":[{"type":"prompt","prompt":"Daily summary"}]}' -craft-agent automation update abc123 --name "Morning Report" --prompt "Updated prompt" -craft-agent automation update abc123 --enabled false -craft-agent automation enable abc123 -craft-agent automation duplicate abc123 -craft-agent automation history abc123 --limit 10 -craft-agent automation last-executed abc123 -craft-agent automation test abc123 --match "UserPromptSubmit" -craft-agent automation lint -craft-agent automation delete abc123 -``` - -### Notes -- Use flat flags for simple automations or `--json` for complex matchers with multiple `actions`. -- `--prompt` is a shortcut that auto-wraps the text as a prompt action. Use `--json` with `actions` for multi-action automations. -- `lint` provides quick matcher/action hygiene checks (regex validity, missing actions, oversized prompt mention sets). -- `history` and `last-executed` read from `automations-history.jsonl` when present. -- `validate` runs full schema and semantic checks. - - ---- - - -## Permission - -Manage Explore mode permissions stored in `permissions.json` (workspace-level and per-source). - -### Commands -- `craft-agent permission list` -- `craft-agent permission get [--source ]` -- `craft-agent permission set [--source ] --json '{...}'` -- `craft-agent permission add-mcp-pattern "" [--comment "..."] [--source ]` -- `craft-agent permission add-api-endpoint --method GET|POST|... --path "" [--comment "..."] [--source ]` -- `craft-agent permission add-bash-pattern "" [--comment "..."] [--source ]` -- `craft-agent permission add-write-path "" [--source ]` -- `craft-agent permission remove --type mcp|api|bash|write-path|blocked [--source ]` -- `craft-agent permission validate [--source ]` -- `craft-agent permission reset [--source ]` - -### Scope - -Without `--source`: operates on workspace-level `permissions.json` (global rules). -With `--source `: operates on that source's `permissions.json` (auto-scoped). - -### Examples - -```bash -# List all permissions files (workspace + sources) -craft-agent permission list -# Get workspace permissions -craft-agent permission get -# Get source-specific permissions -craft-agent permission get --source linear -# Add read-only MCP patterns for a source -craft-agent permission add-mcp-pattern "list" --comment "List operations" --source linear -craft-agent permission add-mcp-pattern "get" --comment "Get operations" --source linear -craft-agent permission add-mcp-pattern "search" --comment "Search operations" --source linear -# Add API endpoint rules -craft-agent permission add-api-endpoint --method GET --path ".*" --comment "All GET requests" --source stripe -# Add bash patterns -craft-agent permission add-bash-pattern "^ls\\s" --comment "Allow ls" -# Add write path globs -craft-agent permission add-write-path "/tmp/**" -# Remove a rule by index and type -craft-agent permission remove 1 --type mcp --source linear -# Replace entire config -craft-agent permission set --source github --json '{"allowedMcpPatterns":[{"pattern":"list","comment":"List ops"}]}' -# Validate all permissions -craft-agent permission validate -# Validate source-specific -craft-agent permission validate --source linear -# Delete permissions file (revert to defaults) -craft-agent permission reset --source linear -``` - -### Notes -- Source-level MCP patterns are auto-scoped at runtime (e.g., `list` becomes `mcp____.*list`). -- `remove` uses 0-based index within the specified rule type array. Use `get` to see indices. -- `validate` runs schema + regex validation. Without `--source`, validates workspace + all sources. -- `reset` deletes the permissions file, reverting to defaults. - - ---- - - -## Theme - -Manage app-level and workspace-level theme settings. - -### Commands -- `craft-agent theme get` -- `craft-agent theme validate [--preset ]` -- `craft-agent theme list-presets` -- `craft-agent theme get-preset ` -- `craft-agent theme set-color-theme ` -- `craft-agent theme set-workspace-color-theme ` -- `craft-agent theme set-override --json '{...}'` -- `craft-agent theme reset-override` - -### Examples - -```bash -# Inspect current theme state -craft-agent theme get - -# Validate app override file -craft-agent theme validate - -# Validate one preset file -craft-agent theme validate --preset nord - -# List available presets -craft-agent theme list-presets - -# Inspect a specific preset -craft-agent theme get-preset dracula - -# Set app default preset -craft-agent theme set-color-theme nord - -# Set workspace override -craft-agent theme set-workspace-color-theme dracula - -# Clear workspace override (inherit app default) -craft-agent theme set-workspace-color-theme default - -# Replace app-level theme.json override -craft-agent theme set-override --json '{"accent":"oklch(0.62 0.21 293)","dark":{"accent":"oklch(0.68 0.21 293)"}}' - -# Remove app-level override file -craft-agent theme reset-override -``` - -### Notes -- `set-color-theme` and `set-workspace-color-theme` require an existing preset ID (`default` is always valid). -- `set-override` validates `theme.json` shape before writing. -- Workspace override is stored in `workspace/config.json` under `defaults.colorTheme`. -- App override is stored in `~/.craft-agent/theme.json`. - - ---- - -## Output contract - -All commands return a single JSON envelope on stdout. - -### Success -```json -{ "ok": true, "data": {}, "warnings": [] } -``` - -### Error -```json -{ - "ok": false, - "error": { - "code": "USAGE_ERROR", - "message": "...", - "suggestion": "..." - }, - "warnings": [] -} -``` - -Exit codes: -- `0` success -- `1` execution/internal failure -- `2` usage/validation/input failure diff --git a/packages/desktop/apps/electron/resources/docs/data-tables.md b/packages/desktop/apps/electron/resources/docs/data-tables.md deleted file mode 100644 index 7c3ca465bff..00000000000 --- a/packages/desktop/apps/electron/resources/docs/data-tables.md +++ /dev/null @@ -1,401 +0,0 @@ -# Data Tables Guide - -This guide covers how to present structured data using datatable and spreadsheet blocks, and how to use the `transform_data` tool for large datasets. - -## Overview - -Qwen Code supports three ways to display tabular data: - -| Format | Best For | Interactivity | -|--------|----------|---------------| -| **Markdown table** | Small, simple data (3-4 rows) | None | -| **`datatable` block** | Query results, comparisons, any data users may sort/filter | Sort, filter, group-by, search | -| **`spreadsheet` block** | Financial reports, exports, data users may download as .xlsx | Sort, export to Excel/CSV | - -**Key principle:** For datasets with 20+ rows, use the `transform_data` tool to write data to a JSON file and reference it via `"src"` instead of inlining all rows. This dramatically reduces token usage and cost. - -## Inline Tables (Small Datasets) - -For datasets under 20 rows, inline the data directly in the markdown block: - -### Datatable - -```` -```datatable -{ - "title": "Top Users", - "columns": [ - { "key": "name", "label": "Name", "type": "text" }, - { "key": "revenue", "label": "Revenue", "type": "currency" }, - { "key": "growth", "label": "Growth", "type": "percent" }, - { "key": "active", "label": "Active", "type": "boolean" }, - { "key": "tier", "label": "Tier", "type": "badge" } - ], - "rows": [ - { "name": "Acme Corp", "revenue": 4200000, "growth": 0.152, "active": true, "tier": "Enterprise" }, - { "name": "StartupCo", "revenue": 85000, "growth": -0.03, "active": true, "tier": "Starter" } - ] -} -``` -```` - -### Spreadsheet - -```` -```spreadsheet -{ - "filename": "q4-revenue.xlsx", - "sheetName": "Revenue", - "columns": [ - { "key": "month", "label": "Month", "type": "text" }, - { "key": "revenue", "label": "Revenue", "type": "currency" } - ], - "rows": [ - { "month": "October", "revenue": 125000 }, - { "month": "November", "revenue": 142000 } - ] -} -``` -```` - -## Column Types Reference - -| Type | Input Format | Rendered As | Example Input | Example Output | -|------|-------------|-------------|---------------|----------------| -| `text` | Any string | Plain text | `"John Doe"` | John Doe | -| `number` | Number | Formatted number | `1500000` | 1,500,000 | -| `currency` | Raw number (not formatted) | Dollar amount | `4200000` | $4,200,000 | -| `percent` | Decimal (0-1 range) | Percentage with color | `0.152` | +15.2% (green) | -| `boolean` | `true`/`false` | Yes/No | `true` | Yes | -| `date` | Date string | Formatted date | `"2025-01-15"` | Jan 15, 2025 | -| `badge` | String | Colored status pill | `"Active"` | Active (badge) | - -**Important notes:** -- `currency` — Pass the raw number, NOT a formatted string. `4200000` renders as `$4,200,000`. -- `percent` — Pass as decimal. `0.152` renders as `+15.2%`. Positive values are green, negative are red. -- `boolean` — Use actual `true`/`false`, not strings. - -## File-Backed Tables (Large Datasets) - -### When to Use - -Use the `transform_data` tool + `"src"` field when: -- Dataset has **20+ rows** — inlining costs ~$1+ in tokens for 100 rows -- Data comes from a **large API response** or tool result -- You need to **filter, reshape, or aggregate** raw data before display -- Data is in **CSV, TSV, or unstructured text** that needs parsing -- You want to **join data from multiple sources** - -### The transform_data Tool - -`transform_data` runs a script in an isolated subprocess that reads input files and writes structured JSON output. - -**Parameters:** - -| Parameter | Type | Description | -|-----------|------|-------------| -| `language` | `"python3"` \| `"node"` \| `"bun"` | Script runtime | -| `script` | string | Transform script source code | -| `inputFiles` | string[] | Input file paths relative to session dir | -| `outputFile` | string | Output file name (written to session `data/` dir) | - -**Path conventions:** -- **Input files** are relative to the session directory. Common locations: - - `long_responses/tool_result_abc.txt` — saved tool results - - `data/previous_output.json` — output from a prior transform - - `attachments/data.csv` — user-attached files -- **Output file** is relative to the session `data/` directory. Just provide the filename (e.g., `"transactions.json"`) - -**Script argument conventions:** -- Input file paths are passed as positional command-line arguments -- The **last argument** is always the output file path -- Python: `sys.argv[1:-1]` = input files, `sys.argv[-1]` = output path -- Node/Bun: `process.argv.slice(2, -1)` = input files, `process.argv.at(-1)` = output path - -### Output JSON Schema - -The output file should contain valid JSON in one of these formats: - -**Full format (recommended):** -```json -{ - "title": "Recent Transactions", - "columns": [ - { "key": "date", "label": "Date", "type": "date" }, - { "key": "amount", "label": "Amount", "type": "currency" }, - { "key": "status", "label": "Status", "type": "badge" } - ], - "rows": [ - { "date": "2025-01-15", "amount": 250.00, "status": "Completed" } - ] -} -``` - -**Rows-only format:** -```json -{ - "rows": [ - { "date": "2025-01-15", "amount": 250.00, "status": "Completed" } - ] -} -``` - -Or just a bare array: -```json -[ - { "date": "2025-01-15", "amount": 250.00, "status": "Completed" } -] -``` - -**Merge semantics:** When using `"src"`, inline `columns` and `title` in the markdown block take precedence over values in the file. This lets you define column types in the block while pulling rows from the file. - -### Referencing the Output - -After `transform_data` succeeds, it returns the **absolute path** to the output file. Use that exact path as the `"src"` value in your datatable or spreadsheet block: - -```` -```datatable -{ - "src": "/absolute/path/returned/by/transform_data", - "title": "Recent Transactions", - "columns": [ - { "key": "date", "label": "Date", "type": "date" }, - { "key": "amount", "label": "Amount", "type": "currency" }, - { "key": "status", "label": "Status", "type": "badge" } - ] -} -``` -```` - -**Important:** Always use the absolute path from the `transform_data` tool result. Do not construct relative paths manually. - -### Complete Workflow Example - -User asks: "Show me all Stripe transactions from last month" - -**Step 1:** Call the Stripe API via MCP tool — get large JSON response - -**Step 2:** Call `transform_data` to extract and structure the data: -``` -transform_data({ - language: "python3", - script: "import json, sys\nwith open(sys.argv[1]) as f:\n data = json.load(f)\nrows = [{\n 'id': t['id'],\n 'date': t['created'],\n 'amount': t['amount'] / 100,\n 'status': t['status'].title(),\n 'customer': t.get('customer_email', 'N/A')\n} for t in data.get('data', data.get('transactions', []))]\nwith open(sys.argv[-1], 'w') as f:\n json.dump({'rows': rows}, f)", - inputFiles: ["long_responses/stripe_result.txt"], - outputFile: "transactions.json" -}) -``` - -**Step 3:** Output the datatable block using the absolute path from `transform_data` result: -```` -```datatable -{ - "src": "/absolute/path/from/transform_data/result", - "title": "Stripe Transactions — Last Month", - "columns": [ - { "key": "id", "label": "ID", "type": "text" }, - { "key": "date", "label": "Date", "type": "date" }, - { "key": "amount", "label": "Amount", "type": "currency" }, - { "key": "status", "label": "Status", "type": "badge" }, - { "key": "customer", "label": "Customer", "type": "text" } - ] -} -``` -```` - -## Common Patterns & Recipes - -### JSON API Response → Datatable - -Most common pattern. Extract fields from a JSON API response: - -**Python:** -```python -import json, sys - -with open(sys.argv[1]) as f: - data = json.load(f) - -# Handle common API response shapes -items = data.get('data', data.get('items', data.get('results', data))) -if not isinstance(items, list): - items = [items] - -rows = [{ - 'id': item['id'], - 'name': item.get('name', ''), - 'created': item.get('created_at', ''), -} for item in items] - -with open(sys.argv[-1], 'w') as f: - json.dump({'rows': rows}, f) -``` - -### CSV/TSV → Spreadsheet - -Parse CSV data into a spreadsheet for export: - -**Python:** -```python -import csv, json, sys - -with open(sys.argv[1]) as f: - reader = csv.DictReader(f) - rows = list(reader) - -# Auto-detect columns from CSV headers -columns = [{'key': k, 'label': k.replace('_', ' ').title(), 'type': 'text'} for k in rows[0].keys()] if rows else [] - -with open(sys.argv[-1], 'w') as f: - json.dump({'columns': columns, 'rows': rows}, f) -``` - -### Multi-Source Join - -Combine data from multiple tool results: - -**Python:** -```python -import json, sys - -# sys.argv[1:-1] are input files, sys.argv[-1] is output -with open(sys.argv[1]) as f: - users = {u['id']: u for u in json.load(f)['data']} -with open(sys.argv[2]) as f: - orders = json.load(f)['data'] - -rows = [{ - 'order_id': o['id'], - 'customer': users.get(o['user_id'], {}).get('name', 'Unknown'), - 'amount': o['total'], - 'status': o['status'], -} for o in orders] - -with open(sys.argv[-1], 'w') as f: - json.dump({'rows': rows}, f) -``` - -Call with: -``` -transform_data({ - language: "python3", - script: "...", - inputFiles: ["long_responses/users.txt", "long_responses/orders.txt"], - outputFile: "orders-with-customers.json" -}) -``` - -### Filtering & Aggregation - -Summarize data before display: - -**Python:** -```python -import json, sys -from collections import defaultdict - -with open(sys.argv[1]) as f: - data = json.load(f) - -# Group by category and sum -totals = defaultdict(lambda: {'count': 0, 'total': 0}) -for item in data['transactions']: - cat = item.get('category', 'Other') - totals[cat]['count'] += 1 - totals[cat]['total'] += item['amount'] - -rows = [{'category': k, 'count': v['count'], 'total': v['total']} - for k, v in sorted(totals.items(), key=lambda x: -x[1]['total'])] - -with open(sys.argv[-1], 'w') as f: - json.dump({'rows': rows}, f) -``` - -### Node.js Alternative - -When Python isn't available or you prefer JavaScript: - -**Node:** -```javascript -const fs = require('fs'); -const data = JSON.parse(fs.readFileSync(process.argv[2], 'utf-8')); - -const rows = data.items.map(item => ({ - id: item.id, - title: item.title, - status: item.state, - created: item.created_at, -})); - -fs.writeFileSync(process.argv.at(-1), JSON.stringify({ rows })); -``` - -## Security & Constraints - -- **Isolated subprocess:** Scripts run in a child process with no access to API keys, credentials, or sensitive environment variables -- **30-second timeout:** Scripts that exceed 30 seconds are killed -- **Path sandboxing:** Input files must be within the session directory. Output files must be within the session `data/` directory. Path traversal attempts (e.g., `../`) are blocked. -- **No network access:** Scripts inherit the process environment (minus secrets) but should not make network calls — use MCP tools for data fetching, then transform locally -- **Blocked env vars:** `AWS_*`, `GITHUB_TOKEN`, `GOOGLE_API_KEY`, `STRIPE_SECRET_KEY`, `NPM_TOKEN`, and other configured secret variables - -## Best Practices - -### Decision Tree - -``` -Is the data < 20 rows? - → YES: Inline it directly in the datatable/spreadsheet block - → NO: Use transform_data + "src" field - -Is the data already structured JSON? - → YES: Write a simple extraction script - → NO: Use Python's csv, json, or string parsing to structure it - -Does the user need to export/download? - → YES: Use spreadsheet block (supports .xlsx export) - → NO: Use datatable block (better sort/filter/group UX) -``` - -### Naming Conventions - -- Output files: descriptive, kebab-case — `stripe-transactions.json`, `monthly-revenue.json` -- Match the context — if user asked about "Q4 sales", name it `q4-sales.json` - -### Error Handling in Scripts - -- Always validate input data exists before processing -- Use `try/except` (Python) or `try/catch` (Node) for JSON parsing -- Write partial results if possible — some data is better than an error -- Keep scripts concise — complex logic is harder to debug in the 30s timeout - -### Script Tips - -- Prefer Python for data transformation — it's the most reliable runtime for JSON/CSV processing -- Keep scripts self-contained — no `pip install` or external dependencies -- Use `json.dump` with default serialization — don't try to format numbers in the script; let column types handle rendering -- For dates, output ISO format strings (`YYYY-MM-DD`) — the `date` column type handles formatting - -## Troubleshooting - -### "Script failed (exit code 1)" -- Check the error output for syntax errors or missing imports -- Verify input files exist at the specified paths -- Make sure the script reads from `sys.argv` / `process.argv` correctly - -### "Output file was not created" -- Ensure the script writes to `sys.argv[-1]` / `process.argv.at(-1)` (the last argument) -- Check that `json.dump` / `fs.writeFileSync` completed successfully -- Verify the output is valid JSON - -### "Input file not found" -- Input paths are relative to the session directory -- Check the exact path from the tool result that produced the file -- Use `long_responses/` prefix for saved tool results, `attachments/` for user-uploaded files - -### Empty or missing rows in table -- Verify the JSON structure: must have `"rows"` key with an array, or be a bare array -- Check that row keys match the column `"key"` fields exactly (case-sensitive) -- Ensure values match expected types (numbers for `currency`/`percent`, not strings) - -### Table shows "Loading..." indefinitely -- The `"src"` path must be the **absolute path** returned by `transform_data` — do not use relative paths -- Verify the file was actually created by `transform_data` (check the tool result message) diff --git a/packages/desktop/apps/electron/resources/docs/html-preview.md b/packages/desktop/apps/electron/resources/docs/html-preview.md deleted file mode 100644 index e228d7e7421..00000000000 --- a/packages/desktop/apps/electron/resources/docs/html-preview.md +++ /dev/null @@ -1,406 +0,0 @@ -# HTML Preview Guide - -This guide covers how to render rich HTML content inline using `html-preview` code blocks, and how to use `transform_data` to prepare HTML files from various sources. - -## Overview - -The `html-preview` block renders HTML files in sandboxed iframes — perfect for emails, newsletters, HTML reports, and any content where markdown conversion would lose formatting. - -| Format | Best For | Rendering | -|--------|----------|-----------| -| **Markdown** | Text-heavy content, code, lists | Native markdown rendering | -| **`html-preview` block** | Emails, newsletters, styled reports, rich HTML | Sandboxed iframe with full CSS | - -**Key principle:** HTML content is always **file-backed** (referenced via `src`) to avoid inlining large HTML payloads as tokens. A typical email HTML body is 50-150KB — never inline this directly. - -## When to Use - -Use `html-preview` when: -- **Email HTML bodies** — Gmail, Outlook, or any email API returns HTML content -- **Newsletters** — Substack, Mailchimp, etc. have complex CSS layouts that markdown can't replicate -- **HTML reports** — API responses containing pre-formatted HTML (analytics dashboards, generated reports) -- **Rich documents** — Any content with complex CSS, table layouts, background images, or custom fonts -- **Web content** — HTML snapshots or previews where layout fidelity matters - -Do NOT use `html-preview` when: -- Content is simple text — just output it as markdown -- Content is structured data — use `datatable` or `spreadsheet` instead -- Content is a code snippet — use regular code blocks with syntax highlighting -- The HTML is tiny (< 1KB) — summarize it in markdown instead - -## Basic Usage - -### Single Item - -```` -```html-preview -{ - "src": "/absolute/path/to/file.html", - "title": "My HTML Content" -} -``` -```` - -### Multiple Items (Tabs) - -When you have multiple related HTML files (e.g., an email thread, multiple reports), use the `items` array. A tab bar appears below the header for switching between items. - -```` -```html-preview -{ - "title": "Email Thread", - "items": [ - { "src": "/path/to/original.html", "label": "Original" }, - { "src": "/path/to/reply.html", "label": "Reply" }, - { "src": "/path/to/forward.html", "label": "Forward" } - ] -} -``` -```` - -Content loads lazily on tab switch and is cached once loaded. - -### Config Fields - -| Field | Required | Type | Description | -|-------|----------|------|-------------| -| `src` | Yes* | string | Absolute path to the HTML file on disk (single item mode) | -| `title` | No | string | Display title shown in the header bar (defaults to "HTML Preview") | -| `items` | Yes* | array | Array of items with `src` and optional `label` (multi-item mode) | -| `items[].src` | Yes | string | Absolute path to the HTML file | -| `items[].label` | No | string | Tab label (defaults to "Item 1", "Item 2", etc.) | - -*Either `src` (single) or `items` (multiple) is required. If both are present, `items` takes precedence. - -**Important:** The `src` path must be an **absolute path** — use the exact path returned by `transform_data` or construct one using the session data folder path. - -## Preparing HTML Content - -### Using transform_data - -The `transform_data` tool is the primary way to extract and write HTML files. It runs a script that reads input files and writes output. - -**Key difference from datatable usage:** For `html-preview`, the output file is `.html` (not `.json`). The script writes raw HTML content, not JSON. - -**Parameters:** - -| Parameter | Type | Description | -|-----------|------|-------------| -| `language` | `"python3"` \| `"node"` \| `"bun"` | Script runtime | -| `script` | string | Transform script source code | -| `inputFiles` | string[] | Input file paths relative to session dir | -| `outputFile` | string | Output file name ending in `.html` (written to session `data/` dir) | - -**Path conventions:** -- **Input files** are relative to the session directory. Common locations: - - `long_responses/tool_result_abc.txt` — saved tool results (Gmail API responses, etc.) - - `data/previous_output.html` — output from a prior transform -- **Output file** is relative to the session `data/` directory. Just provide the filename (e.g., `"email.html"`) - -### Using Write Tool - -For smaller HTML content (generated reports, simple HTML), you can use the `Write` tool directly to write an `.html` file to the session data folder, then reference it. - -## Common Patterns & Recipes - -### Gmail Email Rendering - -Gmail API returns email bodies as base64url-encoded strings. The HTML body is typically in `payload.parts[1].body.data` for multipart emails. - -**Robust pattern (handles all MIME structures):** - -```python -import base64, json, sys - -with open(sys.argv[1]) as f: - msg = json.load(f) - -# Recursively find text/html part in MIME structure -def find_html_part(payload): - if payload.get('mimeType') == 'text/html': - return payload.get('body', {}).get('data') - for part in payload.get('parts', []): - result = find_html_part(part) - if result: - return result - return None - -html_b64 = find_html_part(msg['payload']) -if not html_b64: - # Fallback: body itself may be HTML (non-multipart emails) - html_b64 = msg['payload'].get('body', {}).get('data', '') - -# Gmail uses URL-safe base64 -html = base64.urlsafe_b64decode(html_b64).decode('utf-8') - -with open(sys.argv[-1], 'w') as f: - f.write(html) -``` - -Call with: -``` -transform_data({ - language: "python3", - script: "...", - inputFiles: ["long_responses/gmail_message.txt"], - outputFile: "email.html" -}) -``` - -**Simple shortcut (when you know the structure):** - -```python -import base64, json, sys -data = json.load(open(sys.argv[1])) -html = base64.urlsafe_b64decode(data['payload']['parts'][1]['body']['data']).decode('utf-8') -open(sys.argv[-1], 'w').write(html) -``` - -### Microsoft Outlook Email - -Outlook / Microsoft Graph API returns email bodies differently: - -```python -import json, sys - -with open(sys.argv[1]) as f: - msg = json.load(f) - -# Microsoft Graph returns HTML in body.content -html = msg.get('body', {}).get('content', '') - -with open(sys.argv[-1], 'w') as f: - f.write(html) -``` - -### HTML from API Responses - -Many APIs return HTML content in a JSON field: - -```python -import json, sys - -with open(sys.argv[1]) as f: - data = json.load(f) - -# Adapt field name to your API -html = data.get('html_content', data.get('body_html', data.get('html', ''))) - -with open(sys.argv[-1], 'w') as f: - f.write(html) -``` - -### Generated HTML Report - -Build an HTML report from structured data: - -```python -import json, sys - -with open(sys.argv[1]) as f: - data = json.load(f) - -items = data.get('items', data.get('data', [])) - -rows_html = ''.join( - f'{item["name"]}${item["amount"]:,.2f}' - for item in items -) - -html = f""" - - - - - -

Report

- - -{rows_html} -
NameAmount
- -""" - -with open(sys.argv[-1], 'w') as f: - f.write(html) -``` - -### Node.js Alternative - -```javascript -const fs = require('fs'); -const data = JSON.parse(fs.readFileSync(process.argv[2], 'utf-8')); - -// Extract HTML from Gmail email -const html = Buffer.from(data.payload.parts[1].body.data, 'base64url').toString('utf-8'); - -fs.writeFileSync(process.argv.at(-1), html); -``` - -## Complete Workflow Example - -User asks: "Show me that newsletter from Scott Belsky" - -**Step 1:** Search Gmail for the email: -``` -GET gmail/v1/users/me/messages?q=from:scott belsky subject:implications -``` - -**Step 2:** Fetch the full message: -``` -GET gmail/v1/users/me/messages/{id}?format=full -``` - -**Step 3:** Call `transform_data` to decode the HTML body: -``` -transform_data({ - language: "python3", - script: "import base64, json, sys\nwith open(sys.argv[1]) as f:\n msg = json.load(f)\ndef find_html(p):\n if p.get('mimeType')=='text/html': return p['body']['data']\n for part in p.get('parts',[]): \n r=find_html(part)\n if r: return r\nhtml=base64.urlsafe_b64decode(find_html(msg['payload'])).decode('utf-8')\nopen(sys.argv[-1],'w').write(html)", - inputFiles: ["long_responses/gmail_result.txt"], - outputFile: "newsletter.html" -}) -``` - -**Step 4:** Output the html-preview block with the absolute path from `transform_data` result: -```` -```html-preview -{ - "src": "/absolute/path/from/transform_data/newsletter.html", - "title": "Implications #40 — Exponential Code, Network Effects In AI" -} -``` -```` - -## Rendering Behavior - -### Inline Preview -- Fixed **max-height of 400px** with bottom fade gradient indicating more content below -- **Expand button** (top-right corner, visible on hover) opens fullscreen view -- **Header bar** shows Globe icon and title - -### Fullscreen Overlay -- Click expand button for **full-height rendering** with scrollable content -- **Copy HTML** button copies the raw HTML source to clipboard -- **"HTML" badge** in header identifies the content type - -### Visual Details -- **White background** — iframes render with white background (standard for HTML emails/documents) -- **External images** — load from their original URLs (`https://` supported by CSP) -- **CSS styling** — all inline and embedded styles work (no external stylesheet restrictions) -- **Responsive layouts** — if the HTML has responsive CSS, it adapts to the iframe width - -## Email-Specific Tips - -### Finding the HTML Part - -Email MIME structures vary. Common patterns: - -| Structure | HTML Location | -|-----------|--------------| -| `multipart/alternative` | `payload.parts[1].body.data` (index 1 is usually HTML) | -| `multipart/mixed` → `multipart/alternative` | `payload.parts[0].parts[1].body.data` | -| Single-part HTML | `payload.body.data` (no parts array) | -| Text-only email | No HTML part — use markdown instead | - -**Always use the recursive `find_html_part()` pattern** from the Gmail recipe above — it handles all structures reliably. - -### Gmail Base64 Encoding - -Gmail uses **URL-safe base64** (RFC 4648 §5): -- Uses `-` and `_` instead of `+` and `/` -- No padding (`=`) -- Python: `base64.urlsafe_b64decode()` handles this -- Node: `Buffer.from(data, 'base64url')` - -**Do NOT use** standard `base64.b64decode()` — it will fail on URL-safe encoded content. - -### Large Emails - -Some newsletter HTML bodies are 100KB+. This is fine: -- `transform_data` writes to disk (no token cost) -- The iframe loads the file directly -- The 400px inline preview shows just the top portion - -## Security - -HTML renders in a **sandboxed iframe** with these restrictions: - -| Feature | Status | Details | -|---------|--------|---------| -| JavaScript execution | **Blocked** | `sandbox` attr without `allow-scripts` | -| Form submission | **Blocked** | No `allow-forms` | -| Link navigation | **Blocked** | Sandbox prevents all navigation | -| Popups / new windows | **Blocked** | No `allow-popups` | -| CSS styling | **Allowed** | Inline, embedded, and ` - - -
-
-
Agent is working…
-
- -` - - try { - await instance.nativeOverlayView.webContents.loadURL(`data:text/html;charset=UTF-8,${encodeURIComponent(html)}`) - instance.nativeOverlayReady = true - mainLog.info(`[browser-pane] native overlay ready id=${instance.id} platform=${liveFxPlatform} corners=${cornerRadii.bottomLeft}/${cornerRadii.bottomRight}`) - this.updateNativeOverlayState(instance) - } catch (error) { - instance.nativeOverlayReady = false - mainLog.warn(`[browser-pane] native overlay load failed id=${instance.id}: ${error instanceof Error ? error.message : String(error)}`) - } - } - - private getToolbarEffectiveHeight(instance: BrowserInstance): number { - if (instance.presentation === 'docked') return 0 - if (!instance.toolbarMenuOpen) return TOOLBAR_HEIGHT - - const frame = this.getLayoutFrame(instance) - return Math.max(TOOLBAR_HEIGHT, frame?.height ?? TOOLBAR_HEIGHT) - } - - private getLayoutFrame(instance: BrowserInstance): BrowserPaneDockBounds | null { - if (instance.presentation === 'docked') { - if (!instance.isVisible || !instance.dockBounds) return null - return instance.dockBounds - } - - if (instance.window.isDestroyed()) return null - - const [width, height] = instance.window.getContentSize() - return { x: 0, y: 0, width, height } - } - - private getViewHostWindow(instance: BrowserInstance): BrowserWindow | null { - return instance.viewHostWindow.isDestroyed() ? null : instance.viewHostWindow - } - - private removeViewsFromWindow(window: BrowserWindow, instance: BrowserInstance): void { - if (window.isDestroyed()) return - - try { - window.contentView.removeChildView(instance.containerView) - } catch { - // Electron throws if a view is not attached to this host. - } - } - - private attachViewsToHost(instance: BrowserInstance, hostWindow: BrowserWindow): void { - if (hostWindow.isDestroyed()) return - if (instance.viewHostWindow === hostWindow && !hostWindow.isDestroyed()) return - - this.removeViewsFromWindow(instance.viewHostWindow, instance) - hostWindow.contentView.addChildView(instance.containerView) - instance.viewHostWindow = hostWindow - } - - private hideHostedViews(instance: BrowserInstance): void { - instance.containerView.setBounds({ x: 0, y: 0, width: 0, height: 0 }) - instance.toolbarView.setBounds({ x: 0, y: 0, width: 0, height: 0 }) - instance.pageView.setBounds({ x: 0, y: 0, width: 0, height: 0 }) - instance.nativeOverlayView.setBounds({ x: 0, y: 0, width: 0, height: 0 }) - } - - private raiseToolbarView(instance: BrowserInstance): void { - instance.containerView.addChildView(instance.toolbarView) - } - - private resetDockedPageClip(instance: BrowserInstance): void { - instance.dockClipCssKey = null - instance.dockClipCssPending = false - instance.dockClipGeneration += 1 - } - - private applyDockedPageClip(instance: BrowserInstance): void { - if ( - instance.presentation !== 'docked' - || instance.dockClipCssKey - || instance.dockClipCssPending - ) { - return - } - - const webContents = instance.pageView.webContents - if (webContents.isDestroyed()) return - - const generation = instance.dockClipGeneration - instance.dockClipCssPending = true - void webContents - .insertCSS(DOCK_PAGE_CLIP_CSS, { cssOrigin: 'user' }) - .then((key) => { - if ( - instance.presentation === 'docked' - && instance.dockClipGeneration === generation - && !webContents.isDestroyed() - ) { - instance.dockClipCssKey = key - return - } - - if (!webContents.isDestroyed()) { - void webContents.removeInsertedCSS(key).catch(() => {}) - } - }) - .catch((error) => { - mainLog.warn(`[browser-pane] dock page clip failed id=${instance.id}: ${error instanceof Error ? error.message : String(error)}`) - }) - .finally(() => { - if (instance.dockClipGeneration === generation) { - instance.dockClipCssPending = false - } - }) - } - - private removeDockedPageClip(instance: BrowserInstance): void { - const key = instance.dockClipCssKey - instance.dockClipCssKey = null - instance.dockClipCssPending = false - instance.dockClipGeneration += 1 - if (!key || instance.pageView.webContents.isDestroyed()) return - - void instance.pageView.webContents.removeInsertedCSS(key).catch(() => {}) - } - - private layoutContainerView(instance: BrowserInstance): BrowserPaneDockBounds | null { - const frame = this.getLayoutFrame(instance) - if (!frame) { - this.hideHostedViews(instance) - return null - } - - const isDocked = instance.presentation === 'docked' - instance.containerView.setBounds(frame) - instance.containerView.setBorderRadius(0) - instance.toolbarView.setBorderRadius(0) - instance.pageView.setBorderRadius(0) - instance.nativeOverlayView.setBorderRadius(0) - instance.pageView.setBackgroundColor(isDocked ? '#00000000' : getBrowserViewBackgroundColor()) - - if (isDocked) { - this.applyDockedPageClip(instance) - } else { - this.removeDockedPageClip(instance) - } - - return frame - } - - private layoutToolbarView(instance: BrowserInstance): void { - const frame = this.layoutContainerView(instance) - if (!frame) { - return - } - - const toolbarHeight = this.getToolbarEffectiveHeight(instance) - - instance.toolbarView.setBounds({ - x: 0, - y: 0, - width: frame.width, - height: toolbarHeight, - }) - } - - private updateNativeOverlayState(instance: BrowserInstance): void { - const control = instance.agentControl - const agentActive = !!control?.active - const menuActive = !!instance.toolbarMenuOverlayActive - const shouldShow = agentActive || menuActive - - const hostWindow = this.getViewHostWindow(instance) - const frame = this.getLayoutFrame(instance) - - if (!shouldShow || !instance.nativeOverlayReady || !hostWindow || !frame) { - instance.nativeOverlayView.setBounds({ x: 0, y: 0, width: 0, height: 0 }) - this.raiseToolbarView(instance) - return - } - - const toolbarHeight = this.getToolbarEffectiveHeight(instance) - const overlayHeight = Math.max(100, frame.height - toolbarHeight) - instance.nativeOverlayView.setBounds({ - x: 0, - y: toolbarHeight, - width: frame.width, - height: overlayHeight, - }) - this.raiseToolbarView(instance) - - const dockedOverlayRadius = instance.presentation === 'docked' ? `${DOCK_CONTAINER_RADIUS}px` : '' - const dockedOverlaySquareRadius = instance.presentation === 'docked' ? '0px' : '' - const dockedOverlayClip = instance.presentation === 'docked' - ? `inset(0 0 0 ${DOCK_PAGE_CLIP_LEFT_INSET}px round 0 0 ${DOCK_CONTAINER_RADIUS}px 0)` - : '' - - if (agentActive) { - const label = this.getAgentControlLabel(control) - const accent = this.getResolvedAccentColor() - - void instance.nativeOverlayView.webContents.executeJavaScript(`(() => { - const overlay = document.getElementById('overlay'); - const chip = document.getElementById('chip'); - const shield = document.getElementById('shield'); - if (!overlay || !chip || !shield) return; - - overlay.style.borderTopLeftRadius = ${JSON.stringify(dockedOverlaySquareRadius)}; - overlay.style.borderTopRightRadius = ${JSON.stringify(dockedOverlaySquareRadius)}; - overlay.style.borderBottomLeftRadius = ${JSON.stringify(dockedOverlaySquareRadius)}; - overlay.style.borderBottomRightRadius = ${JSON.stringify(dockedOverlayRadius)}; - overlay.style.clipPath = ${JSON.stringify(dockedOverlayClip)}; - overlay.style.borderColor = ${JSON.stringify(accent)}; - overlay.style.boxShadow = 'inset 0 0 0 1px color-mix(in oklab, ' + ${JSON.stringify(accent)} + ' 45%, transparent), inset 0 0 24px color-mix(in oklab, ' + ${JSON.stringify(accent)} + ' 28%, transparent)'; - chip.textContent = ${JSON.stringify(label)}; - chip.style.display = 'inline-flex'; - shield.style.pointerEvents = 'auto'; - shield.style.cursor = 'not-allowed'; - shield.style.background = 'rgba(2, 6, 23, 0.03)'; - })()`).catch(() => {}) - return - } - - // Menu mode: transparent full-page tap-catcher, no visuals - void instance.nativeOverlayView.webContents.executeJavaScript(`(() => { - const overlay = document.getElementById('overlay'); - const chip = document.getElementById('chip'); - const shield = document.getElementById('shield'); - if (!overlay || !chip || !shield) return; - - overlay.style.borderTopLeftRadius = ${JSON.stringify(dockedOverlaySquareRadius)}; - overlay.style.borderTopRightRadius = ${JSON.stringify(dockedOverlaySquareRadius)}; - overlay.style.borderBottomLeftRadius = ${JSON.stringify(dockedOverlaySquareRadius)}; - overlay.style.borderBottomRightRadius = ${JSON.stringify(dockedOverlayRadius)}; - overlay.style.clipPath = ${JSON.stringify(dockedOverlayClip)}; - overlay.style.borderColor = 'transparent'; - overlay.style.boxShadow = 'none'; - chip.style.display = 'none'; - shield.style.pointerEvents = 'auto'; - shield.style.cursor = 'default'; - shield.style.background = 'rgba(0, 0, 0, 0.001)'; - })()`).catch(() => {}) - } - - private getWindowResizable(window: BrowserWindow): boolean { - return typeof window.isResizable === 'function' ? window.isResizable() : true - } - - private setWindowResizable(window: BrowserWindow, value: boolean): void { - if (typeof window.setResizable === 'function') { - window.setResizable(value) - } - } - - private applyAgentControlLock(instance: BrowserInstance, active: boolean): void { - const wantsLock = active && !!instance.agentControl?.active - - if (wantsLock && !instance.lockState.active) { - instance.lockState.previousResizable = this.getWindowResizable(instance.window) - this.setWindowResizable(instance.window, false) - instance.lockState.active = true - mainLog.info(`[browser-pane] interaction lock enabled id=${instance.id}`) - return - } - - if (!wantsLock && instance.lockState.active) { - this.setWindowResizable(instance.window, instance.lockState.previousResizable) - instance.lockState.active = false - mainLog.info(`[browser-pane] interaction lock released id=${instance.id}`) - } - } - - destroyAll(): void { - for (const id of [...this.instances.keys()]) { - this.destroyInstance(id) - } - } - - private finalizeDestroyedInstance(instance: BrowserInstance, source: 'destroy' | 'closed'): void { - if (!this.instances.has(instance.id)) { - return - } - - this.destroyingIds.delete(instance.id) - this.closePopupsForParent(instance.id, 'parent_destroy') - this.applyAgentControlLock(instance, false) - this.updateNativeOverlayState(instance) - instance.cdp.detach() - this.instances.delete(instance.id) - this.removedCallback?.(instance.id) - mainLog.info(`[browser-pane] Destroyed instance: ${instance.id} (${source})`) - } - - private layoutPageView(instance: BrowserInstance): void { - const frame = this.layoutContainerView(instance) - if (!frame) { - this.updateNativeOverlayState(instance) - return - } - - const toolbarHeight = this.getToolbarEffectiveHeight(instance) - instance.pageView.setBounds({ - x: 0, - y: toolbarHeight, - width: frame.width, - height: Math.max(100, frame.height - toolbarHeight), - }) - this.updateNativeOverlayState(instance) - } - - private layoutAllViews(instance: BrowserInstance): void { - this.layoutToolbarView(instance) - this.layoutPageView(instance) - this.raiseToolbarView(instance) - } - - private forceCloseToolbarMenu(instance: BrowserInstance, reason: string): void { - if (!instance.toolbarMenuOpen && instance.toolbarMenuHeight === 0 && !instance.toolbarMenuOverlayActive) { - return - } - - instance.toolbarMenuOpen = false - instance.toolbarMenuHeight = 0 - instance.toolbarMenuOverlayActive = false - this.layoutAllViews(instance) - - if (!instance.window.isDestroyed() && !instance.toolbarView.webContents.isDestroyed()) { - instance.toolbarView.webContents.send(TOOLBAR_CHANNELS.FORCE_CLOSE_MENU, { reason }) - } - } - - private isBrowserEmptyStateUrl(url: string): boolean { - if (!url) return false - return url.includes(`/${BROWSER_EMPTY_STATE_PAGE}`) || url.includes(`\\${BROWSER_EMPTY_STATE_PAGE}`) - } - - private normalizePageState(url: string, title: string): { url: string; title: string } { - if (this.isBrowserEmptyStateUrl(url)) { - return { url: 'about:blank', title: 'New Tab' } - } - return { url, title } - } - - private async loadEmptyStatePage(instance: BrowserInstance): Promise { - if (VITE_DEV_SERVER_URL) { - await instance.pageView.webContents.loadURL(`${VITE_DEV_SERVER_URL}/${BROWSER_EMPTY_STATE_PAGE}`) - return - } - - await instance.pageView.webContents.loadFile(join(__dirname, `renderer/${BROWSER_EMPTY_STATE_PAGE}`)) - } - - private async handleDeepLinkUrl(url: string): Promise { - if (!url.startsWith(CRAFT_DEEPLINK_SCHEME_PREFIX)) return - - try { - if (!this.windowManager) { - mainLog.warn('[browser-pane] window manager unavailable for deep-link handling, falling back to shell.openExternal') - await shell.openExternal(url) - return - } - - const { handleDeepLink } = await import('./deep-link') - const sink = this.windowManager.getRpcEventSink() ?? undefined - const resolver = (wcId: number) => this.windowManager?.getClientIdForWindow(wcId) - const result = await handleDeepLink(url, this.windowManager, sink, resolver) - if (!result.success) { - mainLog.warn(`[browser-pane] deep-link handling failed: ${result.error ?? 'unknown error'} url=${url}`) - } - } catch (error) { - mainLog.warn(`[browser-pane] deep-link handling threw, falling back to shell.openExternal: ${error instanceof Error ? error.message : String(error)}`) - await shell.openExternal(url) - } - } - - private async maybeHandleEmptyStateLaunch(instance: BrowserInstance, url: string): Promise { - if (!this.isBrowserEmptyStateUrl(url) || !url.includes('#launch=')) { - return false - } - - let parsed: URL - try { - parsed = new URL(url) - } catch { - return false - } - - const hash = parsed.hash.startsWith('#') ? parsed.hash.slice(1) : parsed.hash - const launchPayload = hash.startsWith('launch=') ? hash.slice('launch='.length) : hash - if (!launchPayload) return false - - const params = new URLSearchParams(launchPayload) - const route = params.get('route') - const token = params.get('ts') ?? route ?? null - - if (!route) { - mainLog.warn(`[browser-pane] empty-state launch missing route id=${instance.id}`) - return false - } - - const handled = await this.triggerEmptyStateRouteLaunch(instance, route, token, 'hash') - - try { - await instance.pageView.webContents.executeJavaScript( - "if (window.location.hash.includes('launch=')) history.replaceState(null, '', window.location.pathname + window.location.search);", - ) - } catch { - // Best effort cleanup only - } - - return handled - } - - private async loadToolbarPage(instance: BrowserInstance): Promise { - const query = `instanceId=${encodeURIComponent(instance.id)}` - let lastError: unknown = null - - for (let attempt = 0; attempt <= TOOLBAR_LOAD_MAX_RETRIES; attempt++) { - try { - if (VITE_DEV_SERVER_URL) { - await instance.toolbarView.webContents.loadURL(`${VITE_DEV_SERVER_URL}/browser-toolbar.html?${query}`) - } else { - await instance.toolbarView.webContents.loadFile( - join(__dirname, 'renderer/browser-toolbar.html'), - { query: { instanceId: instance.id } }, - ) - } - - if (attempt > 0) { - mainLog.info(`[browser-pane] toolbar load recovered id=${instance.id} attempt=${attempt + 1}`) - } - return - } catch (error) { - lastError = error - const retrying = attempt < TOOLBAR_LOAD_MAX_RETRIES - mainLog.warn( - `[browser-pane] toolbar load failed id=${instance.id} attempt=${attempt + 1}/${TOOLBAR_LOAD_MAX_RETRIES + 1}: ${error instanceof Error ? error.message : String(error)}${retrying ? ' (retrying)' : ''}`, - ) - - if (retrying) { - await this.sleep(TOOLBAR_LOAD_RETRY_DELAY_MS) - } - } - } - - const errorText = lastError instanceof Error ? lastError.message : String(lastError ?? 'unknown error') - await this.loadToolbarFallback(instance, errorText) - } - - private async loadToolbarFallback(instance: BrowserInstance, reason: string): Promise { - const safeReason = reason.replace(/[<>&]/g, (ch) => ({ '<': '<', '>': '>', '&': '&' }[ch] || ch)) - const html = ` - - - - - Browser Toolbar Error - - - -
-
-
Browser toolbar failed to load
-
The page area still works, but toolbar UI is unavailable. Try reopening the browser window.
-
Reason: ${safeReason}
-
-
- -` - - try { - await instance.toolbarView.webContents.loadURL(`data:text/html;charset=UTF-8,${encodeURIComponent(html)}`) - mainLog.warn(`[browser-pane] Loaded toolbar fallback id=${instance.id}`) - } catch (error) { - mainLog.error(`[browser-pane] Failed to load toolbar fallback id=${instance.id}: ${error instanceof Error ? error.message : String(error)}`) - } - } - - private sleep(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)) - } - - private pushToolbarState(instance: BrowserInstance): void { - if (instance.window.isDestroyed() || instance.toolbarView.webContents.isDestroyed()) return - const state = { - url: instance.currentUrl, - title: instance.title, - isLoading: instance.isLoading, - canGoBack: instance.canGoBack, - canGoForward: instance.canGoForward, - themeColor: instance.themeColor, - presentation: instance.presentation, - dockExpanded: instance.dockExpanded, - } - instance.toolbarView.webContents.send(TOOLBAR_CHANNELS.STATE_UPDATE, state) - } - - /** Register IPC handlers for toolbar actions. Call once at app startup. */ - registerToolbarIpc(): void { - const findInstance = (instanceId: string): BrowserInstance | undefined => { - return this.instances.get(instanceId) - } - - ipcMain.handle(TOOLBAR_CHANNELS.NAVIGATE, async (_event, instanceId: string, url: string) => { - const inst = findInstance(instanceId) - if (inst) await this.navigate(inst.id, url) - }) - - ipcMain.handle(TOOLBAR_CHANNELS.GO_BACK, async (_event, instanceId: string) => { - const inst = findInstance(instanceId) - if (inst) await this.goBack(inst.id) - }) - - ipcMain.handle(TOOLBAR_CHANNELS.GO_FORWARD, async (_event, instanceId: string) => { - const inst = findInstance(instanceId) - if (inst) await this.goForward(inst.id) - }) - - ipcMain.handle(TOOLBAR_CHANNELS.RELOAD, async (_event, instanceId: string) => { - const inst = findInstance(instanceId) - if (inst) this.reload(inst.id) - }) - - ipcMain.handle(TOOLBAR_CHANNELS.STOP, async (_event, instanceId: string) => { - const inst = findInstance(instanceId) - if (inst) this.stop(inst.id) - }) - - ipcMain.handle(TOOLBAR_CHANNELS.MENU_GEOMETRY, async (_event, instanceId: string, open: boolean, height?: number) => { - const inst = findInstance(instanceId) - if (!inst) return - - const normalizedOpen = !!open - const normalizedHeight = Math.max(0, Math.ceil(Number(height ?? 0))) - - if (!normalizedOpen) { - this.forceCloseToolbarMenu(inst, 'renderer-close') - return - } - - const changed = !inst.toolbarMenuOpen - || inst.toolbarMenuHeight !== normalizedHeight - || !inst.toolbarMenuOverlayActive - - if (!changed) return - - inst.toolbarMenuOpen = true - inst.toolbarMenuHeight = normalizedHeight - inst.toolbarMenuOverlayActive = true - this.layoutAllViews(inst) - }) - - ipcMain.handle(TOOLBAR_CHANNELS.TOGGLE_DOCK_EXPANDED, async (_event, instanceId: string) => { - this.toggleDockExpanded(instanceId) - }) - - ipcMain.handle(TOOLBAR_CHANNELS.HIDE, async (_event, instanceId: string) => { - const inst = findInstance(instanceId) - mainLog.info(`[browser-pane] toolbar ipc hide requested instanceId=${instanceId} resolved=${inst?.id ?? 'none'}`) - if (inst) this.hide(inst.id) - }) - - ipcMain.handle(TOOLBAR_CHANNELS.DESTROY, async (_event, instanceId: string) => { - const inst = findInstance(instanceId) - mainLog.info(`[browser-pane] toolbar ipc destroy requested instanceId=${instanceId} resolved=${inst?.id ?? 'none'}`) - if (inst) this.destroyInstance(inst.id) - }) - - mainLog.info('[browser-pane] Toolbar IPC handlers registered') - } - - private markToolbarReady(instance: BrowserInstance, reason: string): void { - if (instance.toolbarReady || instance.window.isDestroyed()) return - - instance.toolbarReady = true - mainLog.info(`[browser-pane] toolbar ready id=${instance.id} reason=${reason}`) - - const shouldShowNow = instance.showOnCreate || instance.pendingShowOnReady - if (!shouldShowNow) return - - const tokenAtReady = instance.pendingShowToken - instance.pendingShowOnReady = false - - if (instance.window.isDestroyed()) return - if (instance.pendingShowToken !== tokenAtReady) return - - if (instance.presentation === 'docked') { - instance.isVisible = true - this.emitStateChange(instance) - return - } - - instance.window.show() - instance.window.focus() - instance.isVisible = true - this.emitStateChange(instance) - - } - - // --------------------------------------------------------------------------- - // Agent Control — persistent overlay while agent is using the browser - // --------------------------------------------------------------------------- - - /** - * Activate or update the agent control overlay on the browser instance - * bound to the given session. Called from sessions.ts on browser_* tool_start events. - */ - setAgentControl(sessionId: string, meta: { displayName?: string; intent?: string }): void { - for (const instance of this.instances.values()) { - if (instance.boundSessionId === sessionId) { - instance.agentControl = { - active: true, - sessionId, - displayName: meta.displayName, - intent: meta.intent, - } - - const label = this.getAgentControlLabel(instance.agentControl) - - this.reapplyAgentControlVisual(instance) - this.emitStateChange(instance) - - mainLog.info(`[browser-pane] agent control activated session=${sessionId} label=${label}`) - return - } - } - } - - /** - * Clear the agent control overlay for the given session. - * Called on explicit browser_tool release and session/window teardown. - */ - clearAgentControl(sessionId: string): void { - for (const instance of this.instances.values()) { - if (instance.boundSessionId === sessionId && instance.agentControl?.active) { - instance.agentControl = null - this.applyAgentControlLock(instance, false) - this.updateNativeOverlayState(instance) - this.emitStateChange(instance) - mainLog.info(`[browser-pane] agent control released session=${sessionId}`) - } - } - } - - clearAgentControlForInstance(instanceId: string, sessionId?: string): { released: boolean; reason?: string } { - const instance = this.instances.get(instanceId) - if (!instance) { - return { released: false, reason: `Browser window "${instanceId}" not found.` } - } - - if (sessionId) { - if (instance.boundSessionId && instance.boundSessionId !== sessionId) { - return { released: false, reason: `Browser window "${instanceId}" is locked to session ${instance.boundSessionId}.` } - } - - if (!instance.boundSessionId && instance.ownerSessionId && instance.ownerSessionId !== sessionId) { - return { released: false, reason: `Browser window "${instanceId}" is currently owned by session ${instance.ownerSessionId}.` } - } - } - - if (!instance.agentControl?.active) { - return { released: false, reason: 'No active agent overlay on the target window.' } - } - - instance.agentControl = null - this.applyAgentControlLock(instance, false) - this.updateNativeOverlayState(instance) - this.emitStateChange(instance) - mainLog.info(`[browser-pane] agent control released instance=${instanceId}${sessionId ? ` session=${sessionId}` : ''}`) - - return { released: true } - } - - /** - * Extract a theme color from the page using Safari 26-style heuristics. - * Priority: media-aware theme-color meta → elementsFromPoint (fixed/sticky headers) → body/html bg. - * All colors pass through (including white/black) — contrast is handled by the renderer. - * Guards against stale extraction (URL change during async executeJavaScript). - */ - private async extractThemeColor(instance: BrowserInstance): Promise { - if (instance.themeColor) return // already set by did-change-theme-color or observer - const urlAtStart = instance.currentUrl - try { - const color = await instance.pageView.webContents.executeJavaScript(`(${THEME_COLOR_EXTRACTOR_FN})()`) - // Guard: if user navigated away during extraction, discard stale result - if (instance.currentUrl !== urlAtStart) return - if (typeof color === 'string' && color.length > 0) { - this.applyThemeColor(instance, color) - } - } catch { - // page destroyed or JS error — ignore - } - } - - private applyThemeColor(instance: BrowserInstance, color: string | null): void { - if (instance.themeColor === color) return - instance.themeColor = color - if (!instance.window.isDestroyed() && !instance.toolbarView.webContents.isDestroyed()) { - instance.toolbarView.webContents.send(TOOLBAR_CHANNELS.THEME_COLOR, color) - } - this.emitStateChange(instance) - } - - private installThemeObserver(instance: BrowserInstance, allowRetry = true): void { - const token = `${Date.now()}-${Math.random().toString(36).slice(2)}` - const urlAtInstall = instance.currentUrl - instance.themeObserverToken = token - - void instance.pageView.webContents.executeJavaScript(` - (() => { - const token = ${JSON.stringify(token)}; - const prefix = ${JSON.stringify(THEME_COLOR_SIGNAL_PREFIX)} + token + ':'; - const nullSentinel = ${JSON.stringify(THEME_COLOR_NULL_SENTINEL)}; - const extractThemeColor = ${THEME_COLOR_EXTRACTOR_FN}; - - const w = window; - const previousCleanup = w.__CRAFT_THEME_OBSERVER_CLEANUP__; - if (typeof previousCleanup === 'function') { - try { previousCleanup(); } catch {} - } - - let lastColor = '__unset__'; - let rafId = 0; - let timerId = 0; - let lastRunAt = 0; - const minIntervalMs = ${THEME_OBSERVER_MIN_INTERVAL_MS}; - - const clearScheduled = () => { - if (timerId) { - clearTimeout(timerId); - timerId = 0; - } - if (rafId) { - cancelAnimationFrame(rafId); - rafId = 0; - } - }; - - const emit = (color) => { - const normalized = typeof color === 'string' && color.length > 0 ? color : null; - if (normalized === lastColor) return; - lastColor = normalized; - console.info(prefix + (normalized ?? nullSentinel)); - }; - - const run = () => { - rafId = 0; - lastRunAt = Date.now(); - try { - emit(extractThemeColor()); - } catch {} - }; - - const schedule = () => { - if (rafId || timerId) return; - const waitMs = Math.max(0, minIntervalMs - (Date.now() - lastRunAt)); - if (waitMs > 0) { - timerId = setTimeout(() => { - timerId = 0; - rafId = requestAnimationFrame(run); - }, waitMs); - return; - } - rafId = requestAnimationFrame(run); - }; - - const onScroll = () => schedule(); - const onResize = () => schedule(); - const onMutation = () => schedule(); - - const headObserver = new MutationObserver(onMutation); - if (document.head) { - headObserver.observe(document.head, { - subtree: true, - childList: true, - attributes: true, - attributeFilter: ['name', 'content', 'media'], - }); - } - - const rootObserver = new MutationObserver(onMutation); - if (document.documentElement) { - rootObserver.observe(document.documentElement, { - attributes: true, - attributeFilter: ['class', 'style'], - }); - } - if (document.body) { - rootObserver.observe(document.body, { - attributes: true, - attributeFilter: ['class', 'style'], - }); - } - - w.addEventListener('scroll', onScroll, { passive: true }); - w.addEventListener('resize', onResize, { passive: true }); - - const mql = w.matchMedia('(prefers-color-scheme: dark)'); - const onSchemeChange = () => schedule(); - if (typeof mql.addEventListener === 'function') mql.addEventListener('change', onSchemeChange); - else if (typeof mql.addListener === 'function') mql.addListener(onSchemeChange); - - w.__CRAFT_THEME_OBSERVER_CLEANUP__ = () => { - headObserver.disconnect(); - rootObserver.disconnect(); - w.removeEventListener('scroll', onScroll); - w.removeEventListener('resize', onResize); - if (typeof mql.removeEventListener === 'function') mql.removeEventListener('change', onSchemeChange); - else if (typeof mql.removeListener === 'function') mql.removeListener(onSchemeChange); - clearScheduled(); - }; - - // Fast first color for initial toolbar paint and after SPA route changes - schedule(); - })() - `).catch(() => { - if (!allowRetry) return - setTimeout(() => { - if (!this.instances.has(instance.id)) return - if (instance.currentUrl !== urlAtInstall) return - if (instance.themeObserverToken !== token) return - this.installThemeObserver(instance, false) - }, 120) - }) - } - - private scheduleEarlyThemeExtraction(instance: BrowserInstance, urlAtSchedule: string): void { - setTimeout(() => { - if (!this.instances.has(instance.id)) return - if (instance.currentUrl !== urlAtSchedule) return - void this.extractThemeColor(instance) - }, EARLY_THEME_EXTRACTION_DELAY_MS) - } - - private getInstanceByWebContentsId(webContentsId: number): BrowserInstance | undefined { - for (const instance of this.instances.values()) { - if (instance.pageView.webContents.id === webContentsId) return instance - } - return undefined - } - - private registerPopupWindow(parentInstance: BrowserInstance, popupWindow: BrowserWindow, sourceUrl?: string): void { - const popupWcId = popupWindow.webContents.id - const existingParent = this.popupParentByWebContentsId.get(popupWcId) - if (existingParent && existingParent !== parentInstance.id) { - this.unregisterPopupWindow(popupWindow, 'reparented') - } - - let popups = this.popupWindowsByParentInstanceId.get(parentInstance.id) - if (!popups) { - popups = new Set() - this.popupWindowsByParentInstanceId.set(parentInstance.id, popups) - } - - popups.add(popupWindow) - this.popupParentByWebContentsId.set(popupWcId, parentInstance.id) - - const initialUrl = sourceUrl || popupWindow.webContents.getURL?.() || 'about:blank' - mainLog.info(`[browser-pane] popup created parent=${parentInstance.id} popupWebContentsId=${popupWcId} url=${initialUrl}`) - - popupWindow.webContents.on('did-navigate', (_event, urlFromEvent) => { - const popupUrl = typeof popupWindow.webContents.getURL === 'function' - ? popupWindow.webContents.getURL() - : (urlFromEvent || initialUrl) - mainLog.info(`[browser-pane] popup did-navigate parent=${parentInstance.id} popupWebContentsId=${popupWcId} url=${popupUrl}`) - }) - - popupWindow.webContents.on('did-redirect-navigation', (_event, popupUrl, isInPlace, isMainFrame) => { - mainLog.info( - `[browser-pane] popup redirect parent=${parentInstance.id} popupWebContentsId=${popupWcId} url=${popupUrl} inPlace=${isInPlace} mainFrame=${isMainFrame}`, - ) - }) - - popupWindow.webContents.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL, isMainFrame) => { - if (!isMainFrame) return - mainLog.warn( - `[browser-pane] popup did-fail-load parent=${parentInstance.id} popupWebContentsId=${popupWcId} code=${errorCode} url=${validatedURL} error=${errorDescription}`, - ) - }) - - popupWindow.on('closed', () => { - this.unregisterPopupWindow(popupWindow, 'closed') - }) - } - - private unregisterPopupWindow(popupWindow: BrowserWindow, reason: 'closed' | 'parent_destroy' | 'reparented'): void { - const popupWcId = popupWindow.webContents.id - const parentId = this.popupParentByWebContentsId.get(popupWcId) - if (!parentId) return - - this.popupParentByWebContentsId.delete(popupWcId) - - const popups = this.popupWindowsByParentInstanceId.get(parentId) - if (popups) { - popups.delete(popupWindow) - if (popups.size === 0) { - this.popupWindowsByParentInstanceId.delete(parentId) - } - } - - mainLog.info(`[browser-pane] popup closed parent=${parentId} popupWebContentsId=${popupWcId} reason=${reason}`) - } - - private closePopupsForParent(parentId: string, reason: 'parent_destroy'): void { - const popups = this.popupWindowsByParentInstanceId.get(parentId) - if (!popups || popups.size === 0) return - - for (const popupWindow of Array.from(popups)) { - const popupWcId = popupWindow.webContents.id - this.unregisterPopupWindow(popupWindow, reason) - try { - if (!popupWindow.isDestroyed()) { - popupWindow.destroy() - } - } catch (error) { - mainLog.warn( - `[browser-pane] popup destroy failed parent=${parentId} popupWebContentsId=${popupWcId} reason=${reason} error=${error instanceof Error ? error.message : String(error)}`, - ) - } - } - } - - private pushNetworkLog(instance: BrowserInstance, entry: BrowserNetworkEntry): void { - instance.networkLogs.push(entry) - if (instance.networkLogs.length > MAX_NETWORK_LOG_ENTRIES) { - instance.networkLogs.splice(0, instance.networkLogs.length - MAX_NETWORK_LOG_ENTRIES) - } - } - - private pushDownloadLog(instance: BrowserInstance, entry: BrowserDownloadEntry): void { - instance.downloads.push(entry) - if (instance.downloads.length > MAX_DOWNLOAD_LOG_ENTRIES) { - instance.downloads.splice(0, instance.downloads.length - MAX_DOWNLOAD_LOG_ENTRIES) - } - } - - private resolveDownloadsDir(instance: BrowserInstance): string { - const sessionId = instance.boundSessionId ?? instance.ownerSessionId - if (sessionId && this.sessionPathResolver) { - const sessionPath = this.sessionPathResolver(sessionId) - if (sessionPath) { - const dir = join(sessionPath, 'downloads') - mkdirSync(dir, { recursive: true }) - return dir - } - } - // Fallback: OS downloads folder for manual/unbound windows - return app.getPath('downloads') - } - - private uniqueFilename(dir: string, filename: string): string { - if (!existsSync(join(dir, filename))) return filename - const { name, ext } = parsePath(filename) - let counter = 1 - while (existsSync(join(dir, `${name}_${counter}${ext}`))) { - counter++ - } - return `${name}_${counter}${ext}` - } - - private setupSessionObservers(ses: ElectronSession): void { - if (this.partitionObserversInitialized) return - this.partitionObserversInitialized = true - - ses.webRequest.onBeforeRequest((details, callback) => { - const wcId = details.webContentsId - if (typeof wcId === 'number' && wcId > 0) { - const current = this.inFlightRequestsByWebContentsId.get(wcId) ?? 0 - this.inFlightRequestsByWebContentsId.set(wcId, current + 1) - this.lastNetworkActivityByWebContentsId.set(wcId, Date.now()) - } - callback({}) - }) - - ses.webRequest.onCompleted((details) => { - const wcId = details.webContentsId - if (typeof wcId !== 'number' || wcId <= 0) return - - const current = this.inFlightRequestsByWebContentsId.get(wcId) ?? 0 - this.inFlightRequestsByWebContentsId.set(wcId, Math.max(0, current - 1)) - this.lastNetworkActivityByWebContentsId.set(wcId, Date.now()) - - const instance = this.getInstanceByWebContentsId(wcId) - if (!instance) return - - this.pushNetworkLog(instance, { - timestamp: Date.now(), - method: details.method ?? 'GET', - url: details.url ?? '', - status: details.statusCode ?? 0, - resourceType: String(details.resourceType ?? 'unknown'), - ok: (details.statusCode ?? 0) >= 200 && (details.statusCode ?? 0) < 400, - }) - }) - - ses.webRequest.onErrorOccurred((details) => { - const wcId = details.webContentsId - if (typeof wcId !== 'number' || wcId <= 0) return - - const current = this.inFlightRequestsByWebContentsId.get(wcId) ?? 0 - this.inFlightRequestsByWebContentsId.set(wcId, Math.max(0, current - 1)) - this.lastNetworkActivityByWebContentsId.set(wcId, Date.now()) - - const instance = this.getInstanceByWebContentsId(wcId) - if (!instance) return - - this.pushNetworkLog(instance, { - timestamp: Date.now(), - method: details.method ?? 'GET', - url: details.url ?? '', - status: 0, - resourceType: String(details.resourceType ?? 'unknown'), - ok: false, - }) - }) - - ses.on('will-download', (_event, item, webContents) => { - const wcId = webContents?.id - if (typeof wcId !== 'number') return - const instance = this.getInstanceByWebContentsId(wcId) - if (!instance) return - - // Auto-save: set a deterministic path so Electron doesn't show a native dialog - const downloadsDir = this.resolveDownloadsDir(instance) - const filename = this.uniqueFilename(downloadsDir, item.getFilename()) - const savePath = join(downloadsDir, filename) - item.setSavePath(savePath) - - const downloadId = `dl-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` - const started: BrowserDownloadEntry = { - id: downloadId, - timestamp: Date.now(), - url: item.getURL(), - filename, - state: 'started', - bytesReceived: item.getReceivedBytes(), - totalBytes: item.getTotalBytes(), - mimeType: item.getMimeType() || 'application/octet-stream', - savePath, - } - this.pushDownloadLog(instance, started) - - const onUpdated = (_e: Electron.Event, state: string) => { - const latest = instance.downloads.find((d) => d.id === downloadId) - if (!latest) return - latest.bytesReceived = item.getReceivedBytes() - latest.totalBytes = item.getTotalBytes() - if (state === 'interrupted') latest.state = 'interrupted' - } - - item.on('updated', onUpdated) - - item.once('done', (_e, state) => { - item.removeListener('updated', onUpdated) - const latest = instance.downloads.find((d) => d.id === downloadId) - if (!latest) return - latest.bytesReceived = item.getReceivedBytes() - latest.totalBytes = item.getTotalBytes() - latest.savePath = item.getSavePath() - latest.state = state === 'completed' ? 'completed' : state === 'cancelled' ? 'cancelled' : 'interrupted' - }) - }) - } - - private logPermissionDecision(kind: 'check' | 'request', permission: string, origin: string): void { - const isNonBlockingNoise = permission === 'background-sync' - const suffix = isNonBlockingNoise ? ' (non-blocking)' : '' - const message = `[browser-pane] permission denied (${kind}): ${permission} origin=${origin}${suffix}` - if (isNonBlockingNoise) { - mainLog.info(message) - return - } - mainLog.warn(message) - } - - private setupSessionPermissions(ses: ElectronSession): void { - if (this.partitionPermissionsInitialized) return - this.partitionPermissionsInitialized = true - - const allow = new Set([ - 'fullscreen', - 'pointerLock', - 'window-management', - 'notifications', - 'geolocation', - 'media', - 'clipboard-read', - 'clipboard-sanitized-write', - 'idle-detection', - ]) - - if (typeof ses.setPermissionCheckHandler === 'function') { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ses.setPermissionCheckHandler((_webContents, permission: string, requestingOrigin: string, _details: any) => { - const allowed = allow.has(permission) - if (!allowed) { - this.logPermissionDecision('check', permission, requestingOrigin) - } - return allowed - }) - } - - if (typeof ses.setPermissionRequestHandler === 'function') { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ses.setPermissionRequestHandler((_webContents, permission: string, callback: (allow: boolean) => void, details: any) => { - const allowed = allow.has(permission) - if (!allowed) { - this.logPermissionDecision('request', permission, details?.requestingOrigin ?? 'unknown') - } - callback(allowed) - }) - } - } - - private isToolbarUiDocumentUrl(url: string): boolean { - if (!url) return false - if (url.startsWith('data:text/html')) return true - - try { - const parsed = new URL(url) - return parsed.pathname.toLowerCase().endsWith('/browser-toolbar.html') - } catch { - return /browser-toolbar\.html(?:$|[?#])/i.test(url) - } - } - - private setupWindowListeners(instance: BrowserInstance): void { - const pageWc = instance.pageView.webContents - const toolbarWc = instance.toolbarView.webContents - const overlayWc = instance.nativeOverlayView.webContents - - instance.window.on('close', (event) => { - const explicitDestroy = this.destroyingIds.has(instance.id) - const interceptToHide = !explicitDestroy && instance.keepAliveOnWindowClose - mainLog.info(`[browser-pane] window close requested id=${instance.id} explicitDestroy=${explicitDestroy} keepAlive=${instance.keepAliveOnWindowClose} interceptToHide=${interceptToHide}`) - - if (interceptToHide) { - event.preventDefault() - this.hide(instance.id) - } - }) - - instance.window.on('resize', () => { - this.layoutAllViews(instance) - }) - - toolbarWc.on('did-finish-load', () => { - const loadedUrl = typeof toolbarWc.getURL === 'function' ? toolbarWc.getURL() : '' - if (!this.isToolbarUiDocumentUrl(loadedUrl)) { - mainLog.info(`[browser-pane] toolbar did-finish-load ignored id=${instance.id} url=${loadedUrl || 'unknown'}`) - this.pushToolbarState(instance) - return - } - - this.markToolbarReady(instance, 'did-finish-load') - this.pushToolbarState(instance) - }) - - toolbarWc.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL, isMainFrame) => { - if (!isMainFrame) return - mainLog.warn(`[browser-pane] toolbar did-fail-load id=${instance.id} code=${errorCode} url=${validatedURL} error=${errorDescription}`) - }) - - pageWc.on('did-start-loading', () => { - instance.isLoading = true - this.resetDockedPageClip(instance) - this.emitStateChange(instance) - void this.pushToolbarState(instance) - }) - - pageWc.on('did-stop-loading', () => { - instance.isLoading = false - instance.canGoBack = pageWc.canGoBack() - instance.canGoForward = pageWc.canGoForward() - // Drain in-flight count — all pending requests are settled once loading stops - this.inFlightRequestsByWebContentsId.set(pageWc.id, 0) - this.lastNetworkActivityByWebContentsId.set(pageWc.id, Date.now()) - this.emitStateChange(instance) - void this.pushToolbarState(instance) - void this.extractThemeColor(instance) - this.reapplyAgentControlVisual(instance) - }) - - pageWc.on('dom-ready', () => { - this.applyDockedPageClip(instance) - this.installThemeObserver(instance) - void this.extractThemeColor(instance) - }) - - pageWc.on('before-input-event', (_event, _input) => { - if (instance.lockState.active) { - _event.preventDefault() - } - }) - - toolbarWc.on('before-input-event', (event) => { - if (instance.lockState.active) { - event.preventDefault() - } - }) - - overlayWc.on('before-input-event', (event, input) => { - if (!instance.toolbarMenuOverlayActive) return - - const inputType = input.type || '' - if (inputType === 'mouseDown' || inputType === 'touchStart' || inputType === 'pointerDown') { - event.preventDefault() - this.forceCloseToolbarMenu(instance, 'overlay-tap') - } - }) - - pageWc.on('did-navigate', (_event, urlFromEvent) => { - const url = typeof pageWc.getURL === 'function' ? pageWc.getURL() : (urlFromEvent || instance.currentUrl) - const previousUrl = instance.currentUrl - if (instance.inPageThemeTimer) { - clearTimeout(instance.inPageThemeTimer) - instance.inPageThemeTimer = null - } - instance.themeObserverToken = null - instance.themeColor = null // reset for new page (batched with state push below) - const normalized = this.normalizePageState(url, pageWc.getTitle()) - instance.currentUrl = normalized.url - instance.title = normalized.title - mainLog.info(`[browser-pane] did-navigate id=${instance.id} from=${previousUrl} to=${instance.currentUrl}`) - instance.canGoBack = pageWc.canGoBack() - instance.canGoForward = pageWc.canGoForward() - // Drain in-flight count — prior page's requests are cancelled on navigation - this.inFlightRequestsByWebContentsId.set(pageWc.id, 0) - this.lastNetworkActivityByWebContentsId.set(pageWc.id, Date.now()) - this.emitStateChange(instance) - void this.pushToolbarState(instance) - this.scheduleEarlyThemeExtraction(instance, url) - this.reapplyAgentControlVisual(instance) - }) - - pageWc.on('did-redirect-navigation', (_event, url, isInPlace, isMainFrame) => { - if (!isMainFrame) return - mainLog.info(`[browser-pane] did-redirect-navigation id=${instance.id} url=${url} inPlace=${isInPlace}`) - }) - - pageWc.on('did-navigate-in-page', (_event, urlFromEvent) => { - const url = typeof pageWc.getURL === 'function' ? pageWc.getURL() : (urlFromEvent || instance.currentUrl) - const normalized = this.normalizePageState(url, instance.title) - instance.currentUrl = normalized.url - instance.title = normalized.title - instance.canGoBack = pageWc.canGoBack() - instance.canGoForward = pageWc.canGoForward() - - void this.maybeHandleEmptyStateLaunch(instance, url).then((handled) => { - if (handled) { - this.emitStateChange(instance) - void this.pushToolbarState(instance) - return - } - - // SPA route change — re-extract theme color (debounced) - if (instance.inPageThemeTimer) clearTimeout(instance.inPageThemeTimer) - instance.themeObserverToken = null - instance.themeColor = null - this.emitStateChange(instance) - void this.pushToolbarState(instance) - this.installThemeObserver(instance) - instance.inPageThemeTimer = setTimeout(() => { void this.extractThemeColor(instance) }, 300) - this.reapplyAgentControlVisual(instance) - }).catch((error) => { - mainLog.warn(`[browser-pane] empty-state launch handling failed id=${instance.id}: ${error instanceof Error ? error.message : String(error)}`) - }) - }) - - pageWc.on('page-title-updated', (_event, title) => { - const normalized = this.normalizePageState(pageWc.getURL(), title) - instance.title = normalized.title - this.emitStateChange(instance) - void this.pushToolbarState(instance) - }) - - pageWc.on('page-favicon-updated', (_event, favicons) => { - instance.favicon = favicons[0] || null - this.emitStateChange(instance) - }) - - pageWc.on('did-change-theme-color', (_event, color) => { - this.applyThemeColor(instance, color ?? null) - }) - - pageWc.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL) => { - mainLog.warn(`[browser-pane] did-fail-load id=${instance.id} code=${errorCode} url=${validatedURL} error=${errorDescription}`) - }) - - pageWc.on('console-message', (_event, level, message) => { - if (message.startsWith(THEME_COLOR_SIGNAL_PREFIX)) { - const payload = message.slice(THEME_COLOR_SIGNAL_PREFIX.length) - const delimiterIdx = payload.indexOf(':') - if (delimiterIdx > 0) { - const token = payload.slice(0, delimiterIdx) - const value = payload.slice(delimiterIdx + 1).trim() - if (token === instance.themeObserverToken) { - if (value === THEME_COLOR_NULL_SENTINEL) { - this.applyThemeColor(instance, null) - } else if (value.length > 0) { - this.applyThemeColor(instance, value) - } - } - } - return - } - - const mappedLevel: BrowserConsoleEntry['level'] = level >= 3 ? 'error' : level === 2 ? 'warn' : level === 1 ? 'info' : 'log' - instance.consoleLogs.push({ - timestamp: Date.now(), - level: mappedLevel, - message, - }) - if (instance.consoleLogs.length > MAX_CONSOLE_LOG_ENTRIES) { - instance.consoleLogs.splice(0, instance.consoleLogs.length - MAX_CONSOLE_LOG_ENTRIES) - } - - if (level >= 2) { - mainLog.warn(`[browser-pane] console id=${instance.id} level=${level}: ${message}`) - } - }) - - pageWc.on('will-navigate', (event, url) => { - if (url.startsWith(CRAFT_DEEPLINK_SCHEME_PREFIX)) { - event.preventDefault() - void this.handleDeepLinkUrl(url) - } - }) - - pageWc.on('did-create-window', (popupWindow, details) => { - const popupUrl = details?.url || popupWindow.webContents.getURL?.() || 'about:blank' - this.registerPopupWindow(instance, popupWindow, popupUrl) - }) - - pageWc.setWindowOpenHandler((details) => { - mainLog.info( - `[browser-pane] window-open requested id=${instance.id} url=${details.url} disposition=${details.disposition ?? 'unknown'} frameName=${details.frameName || 'none'}`, - ) - - if (details.url.startsWith(CRAFT_DEEPLINK_SCHEME_PREFIX)) { - void this.handleDeepLinkUrl(details.url) - return { action: 'deny' } - } - - let parsed: URL - try { - parsed = new URL(details.url) - } catch { - mainLog.warn(`[browser-pane] window-open denied id=${instance.id} reason=invalid_url url=${details.url}`) - return { action: 'deny' } - } - - if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { - mainLog.warn(`[browser-pane] window-open denied id=${instance.id} reason=unsupported_protocol protocol=${parsed.protocol} url=${details.url}`) - return { action: 'deny' } - } - - return { - action: 'allow', - overrideBrowserWindowOptions: { - width: 520, - height: 720, - minWidth: 420, - minHeight: 520, - show: true, - autoHideMenuBar: true, - parent: instance.window, - modal: false, - webPreferences: { - partition: SESSION_PARTITION, - session: pageWc.session, - contextIsolation: true, - nodeIntegration: false, - sandbox: true, - }, - }, - } - }) - - pageWc.on('focus', () => { - this.interactedCallback?.(instance.id) - }) - - instance.window.on('focus', () => { - this.interactedCallback?.(instance.id) - }) - - instance.window.on('show', () => { - instance.isVisible = true - this.emitStateChange(instance) - this.reapplyAgentControlVisual(instance) - this.pushToolbarState(instance) - this.updateNativeOverlayState(instance) - if (!instance.themeColor) { - void this.extractThemeColor(instance) - } - }) - - instance.window.on('hide', () => { - if (instance.presentation === 'docked') return - instance.isVisible = false - this.emitStateChange(instance) - this.updateNativeOverlayState(instance) - }) - - instance.window.on('closed', () => { - this.finalizeDestroyedInstance(instance, 'closed') - }) - } - - private toInfo(instance: BrowserInstance): BrowserInstanceInfo { - return { - id: instance.id, - url: instance.currentUrl, - title: instance.title, - favicon: instance.favicon, - isLoading: instance.isLoading, - canGoBack: instance.canGoBack, - canGoForward: instance.canGoForward, - boundSessionId: instance.boundSessionId, - ownerType: instance.ownerType, - ownerSessionId: instance.ownerSessionId, - isVisible: instance.isVisible, - agentControlActive: !!instance.agentControl?.active, - themeColor: instance.themeColor, - presentation: instance.presentation, - dockExpanded: instance.dockExpanded, - } - } - - private emitStateChange(instance: BrowserInstance): void { - if (!this.instances.has(instance.id)) { - return - } - this.stateChangeCallback?.(this.toInfo(instance)) - } -} diff --git a/packages/desktop/apps/electron/src/main/chunked-rpc.ts b/packages/desktop/apps/electron/src/main/chunked-rpc.ts deleted file mode 100644 index 921a3352e69..00000000000 --- a/packages/desktop/apps/electron/src/main/chunked-rpc.ts +++ /dev/null @@ -1,144 +0,0 @@ -/** - * Chunked RPC — send large payloads over WebSocket in small pieces. - * - * Splits a single large RPC argument into base64 chunks (~2.7MB each), - * sends them via the transfer:start/chunk/commit protocol, and the - * remote server reassembles and executes the original RPC handler. - * - * Each chunk is retried up to 3 times on failure to handle transient - * connection issues through proxies/tunnels. - */ - -import { createHash } from 'node:crypto' -import { RPC_CHANNELS } from '@craft-agent/shared/protocol' -import type { WsRpcClient } from '../transport/client' - -/** - * 2MB raw → ~2.7MB after base64 encoding. - * Larger chunks = fewer round trips (a 250MB payload = ~125 chunks instead of 651). - * Still well under common per-message proxy limits. - */ -export const CHUNK_SIZE = 2 * 1024 * 1024 - -/** Threshold above which we switch from direct RPC to chunked transfer. */ -export const CHUNKED_TRANSFER_THRESHOLD = 5 * 1024 * 1024 - -/** Max retries per chunk before giving up. */ -const MAX_CHUNK_RETRIES = 3 - -/** Delay between chunk retries (ms). */ -const CHUNK_RETRY_DELAY = 1000 - -export interface PreparedChunkedPayload { - bytes: Buffer - checksum: string - chunkCount: number -} - -export function getChunkCount(totalBytes: number): number { - return Math.ceil(totalBytes / CHUNK_SIZE) -} - -export function prepareChunkedPayload(value: unknown): PreparedChunkedPayload { - const json = JSON.stringify(value) - const bytes = Buffer.from(json, 'utf-8') - return { - bytes, - checksum: createHash('sha256').update(bytes).digest('hex'), - chunkCount: getChunkCount(bytes.length), - } -} - -/** - * Send a large RPC call in chunks over the existing WebSocket connection. - * - * @param client Connected WsRpcClient to the remote server - * @param channel The original RPC channel (e.g. 'sessions:import') - * @param args The original arguments array - * @param largeArgIndex Which argument is the large payload (will be chunked) - * @param onProgress Optional callback with (sentChunks, totalChunks) for UI progress - * @param prepared Optional pre-serialized payload so callers can inspect size without re-serializing - * @returns The result from the remote handler (same as a direct invoke) - */ -export async function invokeChunked( - client: WsRpcClient, - channel: string, - args: any[], - largeArgIndex: number, - onProgress?: (sent: number, total: number) => void, - prepared?: PreparedChunkedPayload, -): Promise { - const payload = prepared ?? prepareChunkedPayload(args[largeArgIndex]) - - // Build deferred args (replace large arg with null placeholder) - const deferredArgs = [...args] - deferredArgs[largeArgIndex] = null - - const payloadMB = (payload.bytes.length / (1024 * 1024)).toFixed(1) - console.log(`[ChunkedRPC] Starting transfer: ${payload.chunkCount} chunks, ${payloadMB}MB, sha256: ${payload.checksum.slice(0, 12)}..., channel: ${channel}`) - - let transferId: string | null = null - try { - const startResult = await client.invoke(RPC_CHANNELS.transfer.START, { - totalBytes: payload.bytes.length, - chunkCount: payload.chunkCount, - channel, - args: deferredArgs, - largeArgIndex, - checksum: payload.checksum, - }) as { transferId: string } - - transferId = startResult.transferId - console.log(`[ChunkedRPC] Transfer started: ${transferId}`) - - for (let i = 0; i < payload.chunkCount; i++) { - const start = i * CHUNK_SIZE - const end = Math.min(start + CHUNK_SIZE, payload.bytes.length) - const data = payload.bytes.subarray(start, end).toString('base64') - - let lastError: Error | null = null - for (let attempt = 1; attempt <= MAX_CHUNK_RETRIES; attempt++) { - try { - await client.invoke(RPC_CHANNELS.transfer.CHUNK, { - transferId, - index: i, - data, - }) - lastError = null - break - } catch (err) { - lastError = err instanceof Error ? err : new Error(String(err)) - if (attempt < MAX_CHUNK_RETRIES) { - console.warn(`[ChunkedRPC] Chunk ${i + 1}/${payload.chunkCount} failed (attempt ${attempt}/${MAX_CHUNK_RETRIES}): ${lastError.message}. Retrying in ${CHUNK_RETRY_DELAY}ms...`) - await new Promise(r => setTimeout(r, CHUNK_RETRY_DELAY)) - } - } - } - - if (lastError) { - throw new Error(`Chunk ${i + 1}/${payload.chunkCount} failed after ${MAX_CHUNK_RETRIES} attempts: ${lastError.message}`) - } - - onProgress?.(i + 1, payload.chunkCount) - - if ((i + 1) % 10 === 0 || i === payload.chunkCount - 1) { - console.log(`[ChunkedRPC] Sent chunk ${i + 1}/${payload.chunkCount}`) - } - } - - console.log('[ChunkedRPC] All chunks sent, committing...') - const result = await client.invoke(RPC_CHANNELS.transfer.COMMIT, { transferId }) - console.log('[ChunkedRPC] Transfer committed successfully') - transferId = null - return result - } catch (error) { - if (transferId) { - try { - await client.invoke(RPC_CHANNELS.transfer.ABORT, { transferId }) - } catch { - // Best effort cleanup — the server may already have cleaned up. - } - } - throw error - } -} diff --git a/packages/desktop/apps/electron/src/main/deep-link.ts b/packages/desktop/apps/electron/src/main/deep-link.ts deleted file mode 100644 index 62aad9c2876..00000000000 --- a/packages/desktop/apps/electron/src/main/deep-link.ts +++ /dev/null @@ -1,366 +0,0 @@ -/** - * Deep Link Handler - * - * Parses craftagents:// URLs and routes to appropriate actions. - * - * URL Formats (workspace is optional - uses active window if omitted): - * - * Compound format (hierarchical navigation): - * craftagents://allSessions[/session/{sessionId}] - Session list (all sessions) - * craftagents://flagged[/session/{sessionId}] - Session list (flagged filter) - * craftagents://state/{stateId}[/session/{sessionId}] - Session list (state filter) - * craftagents://sources[/source/{sourceSlug}] - Sources list - * craftagents://settings[/{subpage}] - Settings (general, shortcuts, preferences) - * - * Action format: - * craftagents://action/{actionName}[/{id}][?params] - * craftagents://workspace/{workspaceId}/action/{actionName}[?params] - * - * Actions: - * new-chat - Create new chat, optional ?input=text&name=name&send=true - * If send=true is provided with input, immediately sends the message - * resume-sdk-session/{id} - Resume backend session by SDK session ID - * delete-session/{id} - Delete session - * flag-session/{id} - Flag session - * unflag-session/{id} - Unflag session - * - * Examples: - * craftagents://allSessions (all sessions view) - * craftagents://allSessions/session/abc123 (specific session) - * craftagents://settings/shortcuts (shortcuts page) - * craftagents://sources/source/github (github source info) - * craftagents://action/new-chat (uses active window) - * craftagents://action/resume-sdk-session/{sdkId} (resume backend session) - * craftagents://workspace/ws123/allSessions/session/abc123 (targets specific workspace) - */ - -import type { BrowserWindow } from 'electron' -import { mainLog } from './logger' -import type { WindowManager } from './window-manager' -import { RPC_CHANNELS } from '../shared/types' -import type { EventSink } from '@craft-agent/server-core/transport' - -export interface DeepLinkTarget { - /** Workspace ID - undefined means use active window */ - workspaceId?: string - /** Compound route format (e.g., 'allSessions/session/abc123', 'settings/shortcuts') */ - view?: string - /** Action route (e.g., 'new-chat', 'delete-session') */ - action?: string - actionParams?: Record - /** Window mode - if set, opens in a new window instead of navigating in existing */ - windowMode?: 'focused' | 'full' - /** Right sidebar param (e.g., 'files/path/to/file', 'history') */ - rightSidebar?: string -} - -export interface DeepLinkResult { - success: boolean - error?: string - windowId?: number -} - -/** - * Navigation payload sent to renderer via IPC - */ -export interface DeepLinkNavigation { - /** Compound route format (e.g., 'allSessions/session/abc123', 'settings/shortcuts') */ - view?: string - /** Action route (e.g., 'new-chat', 'delete-session') */ - action?: string - actionParams?: Record -} - -/** - * Parse window mode from URL search params - */ -function parseWindowMode(parsed: URL): 'focused' | 'full' | undefined { - const windowParam = parsed.searchParams.get('window') - if (windowParam === 'focused' || windowParam === 'full') { - return windowParam - } - return undefined -} - -/** - * Parse right sidebar param from URL search params - */ -function parseRightSidebar(parsed: URL): string | undefined { - return parsed.searchParams.get('sidebar') || undefined -} - -/** - * Parse a deep link URL into structured target - */ -export function parseDeepLink(url: string): DeepLinkTarget | null { - try { - const parsed = new URL(url) - - if (parsed.protocol !== 'craftagents:') { - return null - } - - // For custom protocols, the hostname contains the first path segment - // e.g., craftagents://workspace/ws123 → hostname='workspace', pathname='/ws123' - // e.g., craftagents://allSessions/chat/abc → hostname='allSessions', pathname='/chat/abc' - const host = parsed.hostname - const pathParts = parsed.pathname.split('/').filter(Boolean) - const windowMode = parseWindowMode(parsed) - const rightSidebar = parseRightSidebar(parsed) - - // craftagents://auth-callback?... (OAuth callbacks - return null to let existing handler process) - if (host === 'auth-callback') { - return null - } - - // Compound route prefixes - const COMPOUND_ROUTE_PREFIXES = [ - 'allSessions', - 'flagged', - 'state', - 'sources', - 'settings', - 'skills', - 'skillMarketplace', - ] - - // craftagents://allSessions/..., craftagents://settings/..., etc. (compound routes) - if (COMPOUND_ROUTE_PREFIXES.includes(host)) { - // Reconstruct the full compound route from host + pathname - const viewRoute = - pathParts.length > 0 ? `${host}/${pathParts.join('/')}` : host - return { - workspaceId: undefined, - view: viewRoute, - windowMode, - rightSidebar, - } - } - - // craftagents://workspace/{workspaceId}/... (with workspace targeting) - if (host === 'workspace') { - const workspaceId = pathParts[0] - if (!workspaceId) return null - - const result: DeepLinkTarget = { workspaceId, windowMode, rightSidebar } - - // Check what type of route follows the workspace ID - const routeType = pathParts[1] - - // Parse compound routes: /workspace/{id}/{compoundRoute} - // e.g., /workspace/ws123/allSessions/session/abc123 - if (routeType && COMPOUND_ROUTE_PREFIXES.includes(routeType)) { - const viewRoute = pathParts.slice(1).join('/') - result.view = viewRoute - return result - } - - // Parse /action/{actionName}/... - if (routeType === 'action') { - result.action = pathParts[2] - result.actionParams = {} - // Handle path-based ID (e.g., /action/delete-session/{sessionId}) - if (pathParts[3]) { - result.actionParams.id = pathParts[3] - } - parsed.searchParams.forEach((value, key) => { - // Skip the window and sidebar params - they're handled separately - if (key !== 'window' && key !== 'sidebar') { - result.actionParams![key] = value - } - }) - return result - } - - return result - } - - // craftagents://action/... (no workspace - uses active window) - if (host === 'action') { - const result: DeepLinkTarget = { - workspaceId: undefined, - action: pathParts[0], - actionParams: {}, - windowMode, - rightSidebar, - } - - if (pathParts[1]) { - result.actionParams!.id = pathParts[1] - } - - parsed.searchParams.forEach((value, key) => { - // Skip the window and sidebar params - they're handled separately - if (key !== 'window' && key !== 'sidebar') { - result.actionParams![key] = value - } - }) - - return result - } - - return null - } catch (error) { - mainLog.error('[DeepLink] Failed to parse URL:', url, error) - return null - } -} - -/** - * Wait for window's renderer to signal ready - */ -function waitForWindowReady(window: BrowserWindow): Promise { - return new Promise((resolve) => { - if (window.webContents.isLoading()) { - window.webContents.once('did-finish-load', () => { - // TIMING NOTE: This 100ms delay allows React to mount and register - // IPC listeners before we send the deep link. `did-finish-load` fires - // when the HTML is loaded, but React's useEffect hooks haven't run yet. - // A proper handshake (renderer signals "ready") would be cleaner but - // adds complexity for minimal gain - this delay is sufficient for all - // practical cases and only affects reload scenarios. - setTimeout(resolve, 100) - }) - } else { - resolve() - } - }) -} - -/** - * Build a deep link URL without the window query parameter - */ -function buildDeepLinkWithoutWindowParam(url: string): string { - const parsed = new URL(url) - parsed.searchParams.delete('window') - return parsed.toString() -} - -/** - * Handle a deep link by navigating to the target - */ -export async function handleDeepLink( - url: string, - windowManager: WindowManager, - sink?: EventSink, - resolveClientId?: (webContentsId: number) => string | undefined, - preferredClientId?: string, -): Promise { - const target = parseDeepLink(url) - - if (!target) { - // Return success for null targets (like auth-callback) - they're handled elsewhere - if (url.includes('auth-callback')) { - return { success: true } - } - return { success: false, error: 'Invalid deep link URL' } - } - - mainLog.info('[DeepLink] Handling:', target) - - // If windowMode is set, create a new window instead of navigating in existing - if (target.windowMode) { - mainLog.info('[DeepLink] windowMode detected:', target.windowMode) - // Get workspaceId from target or from current window - let wsId = target.workspaceId - if (!wsId) { - const focusedWindow = windowManager.getFocusedWindow() - mainLog.info('[DeepLink] focusedWindow:', focusedWindow?.id) - if (focusedWindow) { - wsId = - windowManager.getWorkspaceForWindow(focusedWindow.webContents.id) ?? - undefined - mainLog.info('[DeepLink] wsId from focused window:', wsId) - } - if (!wsId) { - const allWindows = windowManager.getAllWindows() - mainLog.info('[DeepLink] allWindows count:', allWindows.length) - if (allWindows.length > 0) { - wsId = allWindows[0].workspaceId - mainLog.info('[DeepLink] wsId from first window:', wsId) - } - } - } - - if (!wsId) { - mainLog.error('[DeepLink] No workspace available for new window') - return { success: false, error: 'No workspace available for new window' } - } - - // Build URL without window param for navigation inside the new window - const navUrl = buildDeepLinkWithoutWindowParam(url) - mainLog.info('[DeepLink] Creating new window with navUrl:', navUrl) - - const window = windowManager.createWindow({ - workspaceId: wsId, - focused: target.windowMode === 'focused', - initialDeepLink: navUrl, - }) - mainLog.info('[DeepLink] Window created:', window.webContents.id) - - return { success: true, windowId: window.webContents.id } - } - - // 1. Get target window (existing behavior for non-window-mode links) - let window: BrowserWindow | null = null - - if (target.workspaceId) { - // Workspace specified - focus or create window for that workspace - window = windowManager.focusOrCreateWindow(target.workspaceId) - } else { - // No workspace - use focused window or last active - window = - windowManager.getFocusedWindow() ?? windowManager.getLastActiveWindow() - - if (!window) { - // No windows at all - can't navigate without a workspace - return { success: false, error: 'No active window to navigate' } - } - - // Focus the window - if (window.isMinimized()) { - window.restore() - } - window.focus() - } - - // 2. Wait for window to be ready (renderer loaded) - await waitForWindowReady(window) - - // 3. Send navigation command to renderer - if (target.view || target.action) { - const navigation: DeepLinkNavigation = { - view: target.view, - action: target.action, - actionParams: target.actionParams, - } - const wsId = - target.workspaceId ?? - windowManager.getWorkspaceForWindow(window.webContents.id) - const resolvedClientId = resolveClientId?.(window.webContents.id) - - // Prefer the resolved target window client. Only use preferredClientId as - // fallback when no resolver was provided (legacy call sites). - const clientId = - resolvedClientId ?? (!resolveClientId ? preferredClientId : undefined) - - if (sink && clientId) { - sink( - RPC_CHANNELS.deeplink.NAVIGATE, - { to: 'client', clientId }, - navigation, - ) - } else if (sink && wsId) { - sink( - RPC_CHANNELS.deeplink.NAVIGATE, - { to: 'workspace', workspaceId: wsId }, - navigation, - ) - } - } - - return { - success: true, - windowId: window.isDestroyed() ? -1 : window.webContents.id, - } -} diff --git a/packages/desktop/apps/electron/src/main/handlers/__tests__/registration-profiles.test.ts b/packages/desktop/apps/electron/src/main/handlers/__tests__/registration-profiles.test.ts deleted file mode 100644 index 397dbad44ec..00000000000 --- a/packages/desktop/apps/electron/src/main/handlers/__tests__/registration-profiles.test.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { beforeEach, describe, expect, it, mock } from 'bun:test'; -import type { RpcServer } from '@craft-agent/server-core/transport'; -import type { HandlerDeps } from '../handler-deps'; - -const registeredChannels: string[] = []; - -mock.module('electron', () => ({ - ipcMain: { - handle: () => {}, - on: () => {}, - }, - app: { - isPackaged: false, - getAppPath: () => '/', - quit: () => {}, - dock: { setIcon: () => {}, setBadge: () => {} }, - }, - nativeTheme: { shouldUseDarkColors: false }, - nativeImage: { - createFromPath: () => ({ isEmpty: () => true }), - createFromDataURL: () => ({}), - }, - dialog: { - showOpenDialog: async () => ({ canceled: true, filePaths: [] }), - showMessageBox: async () => ({ response: 0 }), - }, - shell: { - openExternal: async () => {}, - openPath: async () => '', - showItemInFolder: () => {}, - }, - BrowserWindow: { - fromWebContents: () => null, - getFocusedWindow: () => null, - getAllWindows: () => [], - }, - BrowserView: class {}, - Menu: { - buildFromTemplate: () => ({ popup: () => {} }), - }, - session: {}, -})); - -function createMockServer(): RpcServer { - return { - handle(channel: string, _handler: unknown) { - registeredChannels.push(channel); - }, - push() {}, - async invokeClient() {}, - }; -} - -function createMockDeps(): HandlerDeps { - return { - sessionManager: {} as HandlerDeps['sessionManager'], - platform: { - appRootPath: '', - resourcesPath: '', - isPackaged: false, - appVersion: '0.0.0-test', - isDebugMode: true, - logger: console, - imageProcessor: { - getMetadata: async () => null, - process: async () => Buffer.from(''), - }, - }, - windowManager: {} as HandlerDeps['windowManager'], - browserPaneManager: { - onStateChange: () => {}, - onRemoved: () => {}, - onInteracted: () => {}, - } as unknown as NonNullable, - oauthFlowStore: { - store: () => {}, - getByState: () => null, - remove: () => {}, - cleanup: () => {}, - dispose: () => {}, - size: 0, - } as unknown as HandlerDeps['oauthFlowStore'], - }; -} - -async function getExpectedCoreChannels(): Promise> { - // Core handler channels (now in server-core) - const [ - auth, - automations, - files, - labels, - llm, - oauth, - sessions, - settings, - skills, - sources, - statuses, - system, - workspace, - onboarding, - ] = await Promise.all([ - import('@craft-agent/server-core/handlers/rpc/auth'), - import('@craft-agent/server-core/handlers/rpc/automations'), - import('@craft-agent/server-core/handlers/rpc/files'), - import('@craft-agent/server-core/handlers/rpc/labels'), - import('@craft-agent/server-core/handlers/rpc/llm-connections'), - import('@craft-agent/server-core/handlers/rpc/oauth'), - import('@craft-agent/server-core/handlers/rpc/sessions'), - import('@craft-agent/server-core/handlers/rpc/settings'), - import('@craft-agent/server-core/handlers/rpc/skills'), - import('@craft-agent/server-core/handlers/rpc/sources'), - import('@craft-agent/server-core/handlers/rpc/statuses'), - import('@craft-agent/server-core/handlers/rpc/system'), - import('@craft-agent/server-core/handlers/rpc/workspace'), - import('@craft-agent/server-core/handlers/rpc/onboarding'), - ]); - - return new Set([ - ...auth.HANDLED_CHANNELS, - ...automations.HANDLED_CHANNELS, - ...files.HANDLED_CHANNELS, - ...labels.HANDLED_CHANNELS, - ...llm.HANDLED_CHANNELS, - ...oauth.HANDLED_CHANNELS, - ...sessions.HANDLED_CHANNELS, - ...settings.HANDLED_CHANNELS, - ...skills.HANDLED_CHANNELS, - ...sources.HANDLED_CHANNELS, - ...statuses.HANDLED_CHANNELS, - ...system.CORE_HANDLED_CHANNELS, - ...workspace.CORE_HANDLED_CHANNELS, - ...onboarding.HANDLED_CHANNELS, - ]); -} - -async function getExpectedGuiChannels(): Promise> { - const [browser, system, workspace, settings, windowDrag] = await Promise.all([ - import('../browser'), - import('../system'), - import('../workspace'), - import('../settings'), - import('../window-drag'), - ]); - - return new Set([ - ...browser.HANDLED_CHANNELS, - ...system.GUI_HANDLED_CHANNELS, - ...workspace.GUI_HANDLED_CHANNELS, - ...settings.GUI_HANDLED_CHANNELS, - ...windowDrag.GUI_HANDLED_CHANNELS, - ]); -} - -describe('RPC handler profile registration', () => { - beforeEach(() => { - registeredChannels.length = 0; - }); - - it('registerCoreRpcHandlers registers only core channels', async () => { - const expected = await getExpectedCoreChannels(); - const { registerCoreRpcHandlers } = await import('../index'); - - registerCoreRpcHandlers(createMockServer(), createMockDeps()); - - const actual = new Set(registeredChannels.filter((ch) => ch.includes(':'))); - expect([...expected].filter((ch) => !actual.has(ch))).toEqual([]); - expect([...actual].filter((ch) => !expected.has(ch))).toEqual([]); - }); - - it('registerGuiRpcHandlers registers only gui channels', async () => { - const expected = await getExpectedGuiChannels(); - const { registerGuiRpcHandlers } = await import('../index'); - - registerGuiRpcHandlers(createMockServer(), createMockDeps()); - - const actual = new Set(registeredChannels.filter((ch) => ch.includes(':'))); - expect([...expected].filter((ch) => !actual.has(ch))).toEqual([]); - expect([...actual].filter((ch) => !expected.has(ch))).toEqual([]); - }); -}); diff --git a/packages/desktop/apps/electron/src/main/handlers/__tests__/registration.test.ts b/packages/desktop/apps/electron/src/main/handlers/__tests__/registration.test.ts deleted file mode 100644 index 1b32f36504a..00000000000 --- a/packages/desktop/apps/electron/src/main/handlers/__tests__/registration.test.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { beforeEach, describe, expect, it, mock } from 'bun:test'; -import { RPC_CHANNELS } from '@craft-agent/shared/protocol'; -import type { RpcServer } from '@craft-agent/server-core/transport'; -import type { HandlerDeps } from '../handler-deps'; - -const registeredChannels: string[] = []; - -mock.module('electron', () => ({ - ipcMain: { - handle: () => {}, - on: () => {}, - }, - // Minimal stubs for symbols imported by IPC domain modules - app: { - isPackaged: false, - getAppPath: () => '/', - quit: () => {}, - dock: { setIcon: () => {}, setBadge: () => {} }, - }, - nativeTheme: { shouldUseDarkColors: false }, - nativeImage: { - createFromPath: () => ({ isEmpty: () => true }), - createFromDataURL: () => ({}), - }, - dialog: { - showOpenDialog: async () => ({ canceled: true, filePaths: [] }), - showMessageBox: async () => ({ response: 0 }), - }, - shell: { - openExternal: async () => {}, - openPath: async () => '', - showItemInFolder: () => {}, - }, - BrowserWindow: { - fromWebContents: () => null, - getFocusedWindow: () => null, - getAllWindows: () => [], - }, - BrowserView: class {}, - Menu: { - buildFromTemplate: () => ({ popup: () => {} }), - }, - session: {}, -})); - -function createMockServer(): RpcServer { - return { - handle(channel: string, _handler: unknown) { - registeredChannels.push(channel); - }, - push() {}, - async invokeClient() {}, - }; -} - -function createMockDeps(): HandlerDeps { - return { - sessionManager: {} as HandlerDeps['sessionManager'], - platform: { - appRootPath: '', - resourcesPath: '', - isPackaged: false, - appVersion: '0.0.0-test', - isDebugMode: true, - logger: console, - imageProcessor: { - getMetadata: async () => null, - process: async () => Buffer.from(''), - }, - }, - windowManager: {} as HandlerDeps['windowManager'], - browserPaneManager: { - onStateChange: () => {}, - onRemoved: () => {}, - onInteracted: () => {}, - } as unknown as NonNullable, - oauthFlowStore: { - store: () => {}, - getByState: () => null, - remove: () => {}, - cleanup: () => {}, - dispose: () => {}, - size: 0, - } as unknown as HandlerDeps['oauthFlowStore'], - }; -} - -async function getExpectedChannels(): Promise> { - // Core handler channels (now in server-core) - const [ - auth, - automations, - files, - labels, - llm, - oauth, - sessions, - coreSettings, - skills, - sources, - statuses, - coreSystem, - coreWorkspace, - onboarding, - resources, - ] = await Promise.all([ - import('@craft-agent/server-core/handlers/rpc/auth'), - import('@craft-agent/server-core/handlers/rpc/automations'), - import('@craft-agent/server-core/handlers/rpc/files'), - import('@craft-agent/server-core/handlers/rpc/labels'), - import('@craft-agent/server-core/handlers/rpc/llm-connections'), - import('@craft-agent/server-core/handlers/rpc/oauth'), - import('@craft-agent/server-core/handlers/rpc/sessions'), - import('@craft-agent/server-core/handlers/rpc/settings'), - import('@craft-agent/server-core/handlers/rpc/skills'), - import('@craft-agent/server-core/handlers/rpc/sources'), - import('@craft-agent/server-core/handlers/rpc/statuses'), - import('@craft-agent/server-core/handlers/rpc/system'), - import('@craft-agent/server-core/handlers/rpc/workspace'), - import('@craft-agent/server-core/handlers/rpc/onboarding'), - import('@craft-agent/server-core/handlers/rpc/resources'), - ]); - - // GUI handler channels (remain in electron) - const [ - browser, - guiSystem, - guiWorkspace, - guiSettings, - guiWindowDrag, - guiPetWindow, - ] = await Promise.all([ - import('../browser'), - import('../system'), - import('../workspace'), - import('../settings'), - import('../window-drag'), - import('../pet-window'), - ]); - - return new Set([ - ...auth.HANDLED_CHANNELS, - ...automations.HANDLED_CHANNELS, - ...files.HANDLED_CHANNELS, - ...labels.HANDLED_CHANNELS, - ...llm.HANDLED_CHANNELS, - ...oauth.HANDLED_CHANNELS, - ...sessions.HANDLED_CHANNELS, - ...coreSettings.HANDLED_CHANNELS, - ...skills.HANDLED_CHANNELS, - ...sources.HANDLED_CHANNELS, - ...statuses.HANDLED_CHANNELS, - RPC_CHANNELS.transfer.START, - RPC_CHANNELS.transfer.CHUNK, - RPC_CHANNELS.transfer.COMMIT, - RPC_CHANNELS.transfer.ABORT, - ...coreSystem.CORE_HANDLED_CHANNELS, - ...coreWorkspace.CORE_HANDLED_CHANNELS, - ...onboarding.HANDLED_CHANNELS, - ...resources.HANDLED_CHANNELS, - ...browser.HANDLED_CHANNELS, - ...guiSystem.GUI_HANDLED_CHANNELS, - ...guiWorkspace.GUI_HANDLED_CHANNELS, - ...guiSettings.GUI_HANDLED_CHANNELS, - ...guiWindowDrag.GUI_HANDLED_CHANNELS, - ...guiPetWindow.GUI_HANDLED_CHANNELS, - ]); -} - -describe('RPC handler registration', () => { - beforeEach(() => { - registeredChannels.length = 0; - }); - - it('registers all declared handled channels exactly once', async () => { - const expected = await getExpectedChannels(); - const { registerAllRpcHandlers } = await import('../index'); - - registerAllRpcHandlers(createMockServer(), createMockDeps()); - - const appChannels = registeredChannels.filter((ch) => ch.includes(':')); - const actual = new Set(appChannels); - - const missing = [...expected].filter((ch) => !actual.has(ch)).sort(); - const unexpected = [...actual].filter((ch) => !expected.has(ch)).sort(); - - expect(missing).toEqual([]); - expect(unexpected).toEqual([]); - - // Check for duplicates - const counts = new Map(); - for (const ch of appChannels) { - counts.set(ch, (counts.get(ch) ?? 0) + 1); - } - const duplicates = [...counts.entries()] - .filter(([, count]) => count > 1) - .map(([channel, count]) => `${channel} (${count}x)`) - .sort(); - - expect(duplicates).toEqual([]); - }); - - it('keeps onboarding channels in registration coverage', async () => { - const { HANDLED_CHANNELS } = await import( - '@craft-agent/server-core/handlers/rpc/onboarding' - ); - const { registerAllRpcHandlers } = await import('../index'); - - registerAllRpcHandlers(createMockServer(), createMockDeps()); - - const actual = new Set(registeredChannels); - const missingOnboarding = HANDLED_CHANNELS.filter((ch) => !actual.has(ch)); - - expect(missingOnboarding).toEqual([]); - }); -}); diff --git a/packages/desktop/apps/electron/src/main/handlers/__tests__/session-watcher.test.ts b/packages/desktop/apps/electron/src/main/handlers/__tests__/session-watcher.test.ts deleted file mode 100644 index c8f3eca2594..00000000000 --- a/packages/desktop/apps/electron/src/main/handlers/__tests__/session-watcher.test.ts +++ /dev/null @@ -1,222 +0,0 @@ -/** - * Session file watcher isolation tests. - * - * Verifies per-client watcher lifecycle: creation, cleanup, disconnect, - * and that concurrent clients don't interfere with each other. - * - * Uses real temp directories + real fs.watch to avoid mocking fs - * (which breaks transitive imports that need real fs exports). - */ - -import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test' -import { mkdtempSync, writeFileSync, rmSync } from 'fs' -import { join } from 'path' -import { tmpdir } from 'os' -import type { RpcServer, RequestContext } from '@craft-agent/server-core/transport' -import type { HandlerDeps } from '../handler-deps' -import { RPC_CHANNELS } from '../../../shared/types' - -// --------------------------------------------------------------------------- -// Electron mock (needed by transitive imports) -// --------------------------------------------------------------------------- - -mock.module('electron', () => ({ - app: { isPackaged: false, getAppPath: () => '/', quit: () => {}, dock: { setIcon: () => {}, setBadge: () => {} } }, - nativeTheme: { shouldUseDarkColors: false }, - nativeImage: { createFromPath: () => ({ isEmpty: () => true }), createFromDataURL: () => ({}) }, - dialog: { showOpenDialog: async () => ({ canceled: true, filePaths: [] }), showMessageBox: async () => ({ response: 0 }) }, - shell: { openExternal: async () => {}, openPath: async () => '', showItemInFolder: () => {} }, - BrowserWindow: { fromWebContents: () => null, getFocusedWindow: () => null, getAllWindows: () => [] }, - Menu: { buildFromTemplate: () => ({ popup: () => {} }) }, - session: {}, -})) - -// --------------------------------------------------------------------------- -// Test helpers -// --------------------------------------------------------------------------- - -interface PushCall { - channel: string - target: any - args: any[] -} - -let tempDirs: string[] = [] - -function makeTempSessionDir(): string { - const dir = mkdtempSync(join(tmpdir(), 'watcher-test-')) - tempDirs.push(dir) - return dir -} - -function createTestHarness(sessionPaths: Map) { - const handlers = new Map() - const pushCalls: PushCall[] = [] - - const server: RpcServer = { - handle(channel: string, handler: Function) { - handlers.set(channel, handler as any) - }, - push(channel: string, target: any, ...args: any[]) { - pushCalls.push({ channel, target, args }) - }, - async invokeClient() {}, - } - - const deps: HandlerDeps = { - sessionManager: { - getSessionPath: (sessionId: string) => sessionPaths.get(sessionId) ?? null, - waitForInit: async () => {}, - getSessions: () => [], - } as unknown as HandlerDeps['sessionManager'], - platform: { - appRootPath: '', - resourcesPath: '', - isPackaged: false, - appVersion: '0.0.0-test', - isDebugMode: true, - logger: { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} }, - imageProcessor: { getMetadata: async () => null, process: async () => Buffer.from('') }, - } as unknown as HandlerDeps['platform'], - oauthFlowStore: { - store: () => {}, getByState: () => null, remove: () => {}, cleanup: () => {}, dispose: () => {}, size: 0, - } as unknown as HandlerDeps['oauthFlowStore'], - } - - return { server, deps, handlers, pushCalls } -} - -function makeCtx(clientId: string, workspaceId = 'ws-1'): RequestContext { - return { clientId, workspaceId, webContentsId: null } -} - -// --------------------------------------------------------------------------- -// Tests -// --------------------------------------------------------------------------- - -describe('session file watcher isolation', () => { - afterEach(() => { - for (const dir of tempDirs) { - try { rmSync(dir, { recursive: true, force: true }) } catch {} - } - tempDirs = [] - }) - - it('creates independent watchers per client and cleans up on disconnect', async () => { - const dir1 = makeTempSessionDir() - const dir2 = makeTempSessionDir() - const sessionPaths = new Map([['s1', dir1], ['s2', dir2]]) - const { server, deps, handlers, pushCalls } = createTestHarness(sessionPaths) - - const { registerSessionsHandlers, cleanupSessionFileWatchForClient } = await import('@craft-agent/server-core/handlers/rpc') - registerSessionsHandlers(server, deps) - - const watchHandler = handlers.get(RPC_CHANNELS.sessions.WATCH_FILES)! - const unwatchHandler = handlers.get(RPC_CHANNELS.sessions.UNWATCH_FILES)! - - // Client A watches session s1, Client B watches session s2 - await watchHandler(makeCtx('client-a'), 's1') - await watchHandler(makeCtx('client-b'), 's2') - - // Trigger a change in s1 - writeFileSync(join(dir1, 'output.txt'), 'hello') - - // Wait for debounce + fs.watch delay - await new Promise(r => setTimeout(r, 300)) - - // Only client-a should have received the notification - const clientAPushes = pushCalls.filter(p => p.target?.clientId === 'client-a') - const clientBPushes = pushCalls.filter(p => p.target?.clientId === 'client-b') - expect(clientAPushes.length).toBeGreaterThanOrEqual(1) - expect(clientBPushes.length).toBe(0) - - // Verify push target is client-specific, not broadcast - expect(clientAPushes[0].channel).toBe(RPC_CHANNELS.sessions.FILES_CHANGED) - expect(clientAPushes[0].target).toEqual({ to: 'client', clientId: 'client-a' }) - - // Unwatch client A — should not affect client B - await unwatchHandler(makeCtx('client-a')) - - // Clear push history - pushCalls.length = 0 - - // Trigger a change in s2 - writeFileSync(join(dir2, 'data.json'), '{}') - await new Promise(r => setTimeout(r, 300)) - - // Client B should still receive notifications - const clientBAfter = pushCalls.filter(p => p.target?.clientId === 'client-b') - expect(clientBAfter.length).toBeGreaterThanOrEqual(1) - - // Disconnect cleanup for client B - cleanupSessionFileWatchForClient('client-b') - - // Double cleanup is a no-op (doesn't throw) - cleanupSessionFileWatchForClient('client-b') - }) - - it('cleans up previous watcher when same client watches a different session', async () => { - const dir1 = makeTempSessionDir() - const dir2 = makeTempSessionDir() - const sessionPaths = new Map([['s1', dir1], ['s2', dir2]]) - const { server, deps, handlers, pushCalls } = createTestHarness(sessionPaths) - - const { registerSessionsHandlers, cleanupSessionFileWatchForClient } = await import('@craft-agent/server-core/handlers/rpc') - registerSessionsHandlers(server, deps) - - const watchHandler = handlers.get(RPC_CHANNELS.sessions.WATCH_FILES)! - - // Client A watches s1 - await watchHandler(makeCtx('client-a'), 's1') - - // Client A switches to s2 — old watcher should be cleaned up - await watchHandler(makeCtx('client-a'), 's2') - - // Write to s1 — should NOT trigger notification (old watcher closed) - writeFileSync(join(dir1, 'old.txt'), 'stale') - await new Promise(r => setTimeout(r, 300)) - - const s1Pushes = pushCalls.filter(p => - p.args[0] === 's1' && p.channel === RPC_CHANNELS.sessions.FILES_CHANGED - ) - expect(s1Pushes.length).toBe(0) - - // Write to s2 — should trigger notification - writeFileSync(join(dir2, 'new.txt'), 'fresh') - await new Promise(r => setTimeout(r, 300)) - - const s2Pushes = pushCalls.filter(p => - p.args[0] === 's2' && p.channel === RPC_CHANNELS.sessions.FILES_CHANGED - ) - expect(s2Pushes.length).toBeGreaterThanOrEqual(1) - - cleanupSessionFileWatchForClient('client-a') - }) - - it('ignores internal session.jsonl and hidden files', async () => { - const dir = makeTempSessionDir() - const sessionPaths = new Map([['s1', dir]]) - const { server, deps, handlers, pushCalls } = createTestHarness(sessionPaths) - - const { registerSessionsHandlers, cleanupSessionFileWatchForClient } = await import('@craft-agent/server-core/handlers/rpc') - registerSessionsHandlers(server, deps) - - const watchHandler = handlers.get(RPC_CHANNELS.sessions.WATCH_FILES)! - await watchHandler(makeCtx('client-a'), 's1') - - // Write internal files — should be ignored - writeFileSync(join(dir, 'session.jsonl'), 'log entry') - writeFileSync(join(dir, '.hidden'), 'secret') - await new Promise(r => setTimeout(r, 300)) - - expect(pushCalls.length).toBe(0) - - // Write a normal file — should trigger notification - writeFileSync(join(dir, 'result.txt'), 'output') - await new Promise(r => setTimeout(r, 300)) - - expect(pushCalls.length).toBeGreaterThanOrEqual(1) - - cleanupSessionFileWatchForClient('client-a') - }) -}) diff --git a/packages/desktop/apps/electron/src/main/handlers/__tests__/sessions-watchers.test.ts b/packages/desktop/apps/electron/src/main/handlers/__tests__/sessions-watchers.test.ts deleted file mode 100644 index 566df045e84..00000000000 --- a/packages/desktop/apps/electron/src/main/handlers/__tests__/sessions-watchers.test.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it } from 'bun:test' -import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'fs' -import { join } from 'path' -import { tmpdir } from 'os' -import { RPC_CHANNELS } from '../../../shared/types' -import { registerSessionsHandlers, cleanupSessionFileWatchForClient } from '@craft-agent/server-core/handlers/rpc' -import type { RpcServer } from '@craft-agent/server-core/transport' -import type { HandlerDeps } from '../handler-deps' - -type HandlerFn = (ctx: { clientId: string }, ...args: any[]) => Promise | any - -function wait(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)) -} - -describe('sessions file watchers', () => { - const handlers = new Map() - const pushed: Array<{ channel: string; target: any; args: any[] }> = [] - - let tempRoot = '' - let sessionDirA = '' - let sessionDirB = '' - - beforeEach(() => { - handlers.clear() - pushed.length = 0 - - tempRoot = mkdtempSync(join(tmpdir(), 'craft-session-watchers-')) - sessionDirA = join(tempRoot, 'session-a') - sessionDirB = join(tempRoot, 'session-b') - mkdirSync(sessionDirA, { recursive: true }) - mkdirSync(sessionDirB, { recursive: true }) - - const server: RpcServer = { - handle(channel, handler) { - handlers.set(channel, handler as HandlerFn) - }, - push(channel, target, ...args) { - pushed.push({ channel, target, args }) - }, - async invokeClient() { - return null - }, - } - - const deps: HandlerDeps = { - sessionManager: { - getSessionPath: (sessionId: string) => { - if (sessionId === 'session-a') return sessionDirA - if (sessionId === 'session-b') return sessionDirB - return null - }, - } as unknown as HandlerDeps['sessionManager'], - platform: { - appRootPath: '', - resourcesPath: '', - isPackaged: false, - appVersion: '0.0.0-test', - isDebugMode: true, - imageProcessor: { - getMetadata: async () => null, - process: async () => Buffer.from(''), - }, - logger: { - info: () => {}, - warn: () => {}, - error: () => {}, - debug: () => {}, - }, - }, - oauthFlowStore: { - store: () => {}, - getByState: () => null, - remove: () => {}, - cleanup: () => {}, - dispose: () => {}, - get size() { return 0 }, - } as unknown as HandlerDeps['oauthFlowStore'], - } - - registerSessionsHandlers(server, deps) - }) - - afterEach(() => { - cleanupSessionFileWatchForClient('client-a') - cleanupSessionFileWatchForClient('client-b') - if (tempRoot) { - rmSync(tempRoot, { recursive: true, force: true }) - } - }) - - it('isolates file change notifications per client watcher', async () => { - const watch = handlers.get(RPC_CHANNELS.sessions.WATCH_FILES) - const unwatch = handlers.get(RPC_CHANNELS.sessions.UNWATCH_FILES) - expect(watch).toBeTruthy() - expect(unwatch).toBeTruthy() - - await watch!({ clientId: 'client-a' }, 'session-a') - await watch!({ clientId: 'client-b' }, 'session-b') - await wait(50) - - writeFileSync(join(sessionDirA, 'a.txt'), `a-${Date.now()}`) - writeFileSync(join(sessionDirB, 'b.txt'), `b-${Date.now()}`) - await wait(300) - - const aEvents = pushed.filter((evt) => evt.target?.to === 'client' && evt.target?.clientId === 'client-a') - const bEvents = pushed.filter((evt) => evt.target?.to === 'client' && evt.target?.clientId === 'client-b') - - expect(aEvents.some((evt) => evt.channel === RPC_CHANNELS.sessions.FILES_CHANGED && evt.args[0] === 'session-a')).toBe(true) - expect(bEvents.some((evt) => evt.channel === RPC_CHANNELS.sessions.FILES_CHANGED && evt.args[0] === 'session-b')).toBe(true) - - pushed.length = 0 - await unwatch!({ clientId: 'client-a' }) - - writeFileSync(join(sessionDirA, 'a.txt'), `a2-${Date.now()}`) - writeFileSync(join(sessionDirB, 'b.txt'), `b2-${Date.now()}`) - await wait(300) - - const aEventsAfter = pushed.filter((evt) => evt.target?.clientId === 'client-a') - const bEventsAfter = pushed.filter((evt) => evt.target?.clientId === 'client-b') - - expect(aEventsAfter.length).toBe(0) - expect(bEventsAfter.some((evt) => evt.channel === RPC_CHANNELS.sessions.FILES_CHANGED && evt.args[0] === 'session-b')).toBe(true) - }) - - it('disconnect cleanup removes watcher and prevents further events', async () => { - const watch = handlers.get(RPC_CHANNELS.sessions.WATCH_FILES) - expect(watch).toBeTruthy() - - await watch!({ clientId: 'client-a' }, 'session-a') - await wait(50) - - cleanupSessionFileWatchForClient('client-a') - pushed.length = 0 - - writeFileSync(join(sessionDirA, 'after-cleanup.txt'), `x-${Date.now()}`) - await wait(300) - - expect(pushed.length).toBe(0) - }) -}) diff --git a/packages/desktop/apps/electron/src/main/handlers/__tests__/settings-default-thinking.test.ts b/packages/desktop/apps/electron/src/main/handlers/__tests__/settings-default-thinking.test.ts deleted file mode 100644 index fc3c7b19b3d..00000000000 --- a/packages/desktop/apps/electron/src/main/handlers/__tests__/settings-default-thinking.test.ts +++ /dev/null @@ -1,327 +0,0 @@ -import { beforeEach, describe, expect, it, mock } from 'bun:test'; -import { RPC_CHANNELS } from '../../../shared/types'; -import type { HandlerFn, RpcServer } from '@craft-agent/server-core/transport'; -import type { HandlerDeps } from '../handler-deps'; - -const requestContext = { - clientId: 'client-1', - workspaceId: null, - webContentsId: null, -}; - -const getDefaultThinkingLevelMock = mock(() => 'think'); -const setDefaultThinkingLevelMock = mock((_level: string) => true); -const setVoiceModelMock = mock((_model: string) => {}); -let mockedWorkspace: Record | null = null; -let mockedWorkspaceConfig: Record | null = null; -const getWorkspaceByNameOrIdMock = mock( - (_workspaceId: string) => mockedWorkspace, -); -const loadWorkspaceConfigMock = mock( - (_rootPath: string) => mockedWorkspaceConfig, -); -const getQwenCoreSettingsViaAcpMock = mock(async () => ({ - user: { - path: '', - values: { 'tools.approvalMode': 'yolo' }, - mcpServers: [], - hooks: [], - }, - workspace: { path: '', values: {}, mcpServers: [], hooks: [] }, - merged: { - values: {}, - mcpServers: [], - hooks: [], - extensions: [], - }, - workspaceTrusted: true, -})); -const setQwenCoreSettingViaAcpMock = mock(async () => ({ - user: { path: '', values: {}, mcpServers: [], hooks: [] }, - workspace: { path: '', values: {}, mcpServers: [], hooks: [] }, - merged: { - values: { 'tools.approvalMode': 'yolo' }, - mcpServers: [], - hooks: [], - extensions: [], - }, - workspaceTrusted: true, -})); -const applyGlobalPermissionModeMock = mock(async (_mode: string) => {}); -const getQwenMemoryPathsViaAcpMock = mock(async () => ({ - userMemoryFile: '/tmp/QWEN.md', - projectMemoryFile: '/tmp/project/AGENTS.md', - autoMemoryDir: '/tmp/project/memory', -})); -const getQwenMemorySettingsViaAcpMock = mock(async () => ({})); -const setQwenMemorySettingsViaAcpMock = mock(async () => ({})); - -mock.module('@craft-agent/shared/config', () => ({ - getPreferencesPath: () => '/tmp/preferences.json', - getSessionDraft: () => null, - setSessionDraft: () => {}, - deleteSessionDraft: () => {}, - getAllSessionDrafts: () => ({}), - getWorkspaceByNameOrId: getWorkspaceByNameOrIdMock, - getDefaultThinkingLevel: getDefaultThinkingLevelMock, - setDefaultThinkingLevel: setDefaultThinkingLevelMock, - setVoiceModel: setVoiceModelMock, - isProtectedWorkspace: () => false, -})); - -mock.module('@craft-agent/shared/config/storage', () => ({ - setVoiceModel: setVoiceModelMock, -})); - -mock.module('@craft-agent/shared/workspaces', () => ({ - loadWorkspaceConfig: loadWorkspaceConfigMock, -})); - -mock.module('@craft-agent/shared/agent', () => ({ - getQwenCoreSettingsViaAcp: getQwenCoreSettingsViaAcpMock, - setQwenCoreSettingViaAcp: setQwenCoreSettingViaAcpMock, - setQwenMcpServerViaAcp: mock(async () => ({})), - removeQwenMcpServerViaAcp: mock(async () => ({})), - setQwenHookViaAcp: mock(async () => ({})), - removeQwenHookViaAcp: mock(async () => ({})), - setQwenExtensionSettingViaAcp: mock(async () => ({})), - getQwenPermissionSettingsViaAcp: mock(async () => ({})), - setQwenPermissionRulesViaAcp: mock(async () => ({})), - getQwenMemorySettingsViaAcp: getQwenMemorySettingsViaAcpMock, - setQwenMemorySettingsViaAcp: setQwenMemorySettingsViaAcpMock, - getQwenSettingsPathViaAcp: mock(async () => ''), - getQwenMemoryPathsViaAcp: getQwenMemoryPathsViaAcpMock, -})); - -describe('settings default thinking RPC handlers', () => { - const handlers = new Map(); - - beforeEach(async () => { - handlers.clear(); - getDefaultThinkingLevelMock.mockClear(); - setDefaultThinkingLevelMock.mockClear(); - setVoiceModelMock.mockClear(); - mockedWorkspace = null; - mockedWorkspaceConfig = null; - getWorkspaceByNameOrIdMock.mockClear(); - loadWorkspaceConfigMock.mockClear(); - getQwenCoreSettingsViaAcpMock.mockClear(); - setQwenCoreSettingViaAcpMock.mockClear(); - applyGlobalPermissionModeMock.mockClear(); - getQwenMemorySettingsViaAcpMock.mockClear(); - setQwenMemorySettingsViaAcpMock.mockClear(); - getQwenMemoryPathsViaAcpMock.mockClear(); - - const server: RpcServer = { - handle(channel, handler) { - handlers.set(channel, handler as HandlerFn); - }, - push() {}, - async invokeClient() { - return null; - }, - }; - - const deps: HandlerDeps = { - sessionManager: { - applyGlobalPermissionMode: applyGlobalPermissionModeMock, - } as unknown as HandlerDeps['sessionManager'], - platform: { - appRootPath: '', - resourcesPath: '', - isPackaged: false, - appVersion: '0.0.0-test', - isDebugMode: true, - logger: { - info: () => {}, - warn: () => {}, - error: () => {}, - debug: () => {}, - }, - imageProcessor: { - getMetadata: async () => null, - process: async () => Buffer.from(''), - }, - }, - oauthFlowStore: { - store: () => {}, - getByState: () => null, - remove: () => {}, - cleanup: () => {}, - dispose: () => {}, - get size() { - return 0; - }, - } as unknown as HandlerDeps['oauthFlowStore'], - }; - - const { registerSettingsHandlers } = await import( - '@craft-agent/server-core/handlers/rpc/settings' - ); - registerSettingsHandlers(server, deps); - }); - - it('returns persisted default thinking level', async () => { - const getHandler = handlers.get( - RPC_CHANNELS.settings.GET_DEFAULT_THINKING_LEVEL, - ); - expect(getHandler).toBeTruthy(); - - const result = await getHandler!(requestContext); - expect(result).toBe('think'); - expect(getDefaultThinkingLevelMock).toHaveBeenCalledTimes(1); - }); - - it('persists valid thinking level values', async () => { - const setHandler = handlers.get( - RPC_CHANNELS.settings.SET_DEFAULT_THINKING_LEVEL, - ); - expect(setHandler).toBeTruthy(); - - const result = await setHandler!(requestContext, 'max'); - expect(result).toEqual({ success: true }); - expect(setDefaultThinkingLevelMock).toHaveBeenCalledWith('max'); - expect(setDefaultThinkingLevelMock).toHaveBeenCalledTimes(1); - }); - - it('rejects invalid thinking level values before persistence', async () => { - const setHandler = handlers.get( - RPC_CHANNELS.settings.SET_DEFAULT_THINKING_LEVEL, - ); - expect(setHandler).toBeTruthy(); - - await expect(setHandler!(requestContext, 'ultra')).rejects.toThrow( - 'Invalid thinking level', - ); - expect(setDefaultThinkingLevelMock).not.toHaveBeenCalled(); - }); - - it('accepts dated voice model variants supported by the transport resolver', async () => { - const setHandler = handlers.get(RPC_CHANNELS.input.SET_VOICE_MODEL); - expect(setHandler).toBeTruthy(); - - await setHandler!(requestContext, 'qwen3-asr-flash-2025-06-01'); - - expect(setVoiceModelMock).toHaveBeenCalledWith( - 'qwen3-asr-flash-2025-06-01', - ); - }); - - it('returns global permission mode through Qwen ACP', async () => { - const getHandler = handlers.get( - RPC_CHANNELS.settings.GET_GLOBAL_PERMISSION_MODE, - ); - expect(getHandler).toBeTruthy(); - - const result = await getHandler!(requestContext); - expect(result).toBe('allow-all'); - expect(getQwenCoreSettingsViaAcpMock).toHaveBeenCalledTimes(1); - expect(applyGlobalPermissionModeMock).toHaveBeenCalledWith('allow-all', { - changedBy: 'restore', - }); - }); - - it('persists global permission mode through Qwen ACP', async () => { - const setHandler = handlers.get( - RPC_CHANNELS.settings.SET_GLOBAL_PERMISSION_MODE, - ); - expect(setHandler).toBeTruthy(); - - const result = await setHandler!(requestContext, 'yolo'); - expect(result).toEqual({ success: true }); - const call = setQwenCoreSettingViaAcpMock.mock.calls[0] as unknown[]; - expect(call.slice(1)).toEqual(['user', 'tools.approvalMode', 'yolo']); - expect(applyGlobalPermissionModeMock).toHaveBeenCalledWith('allow-all'); - }); - - it('syncs global permission mode when approval mode is saved as a Qwen core setting', async () => { - const setHandler = handlers.get( - RPC_CHANNELS.settings.SET_QWEN_CORE_SETTING, - ); - expect(setHandler).toBeTruthy(); - - await setHandler!(requestContext, 'user', 'tools.approvalMode', 'yolo'); - - expect(applyGlobalPermissionModeMock).toHaveBeenCalledWith('allow-all'); - }); - - it('uses the workspace working directory as the Qwen memory project root', async () => { - mockedWorkspace = { - id: 'ws-1', - name: 'qwen-code', - slug: 'qwen-code', - rootPath: '/Users/dragon/.craft-agent/workspaces/qwen-code', - }; - mockedWorkspaceConfig = { - defaults: { - workingDirectory: '/Users/dragon/Documents/qwen-code', - }, - }; - - const getHandler = handlers.get(RPC_CHANNELS.memory.GET_PATHS); - expect(getHandler).toBeTruthy(); - - await getHandler!(requestContext, 'ws-1'); - - expect(getQwenMemoryPathsViaAcpMock).toHaveBeenCalledTimes(1); - expect(getQwenMemoryPathsViaAcpMock.mock.calls[0]?.[0]).toMatchObject({ - cwd: '/Users/dragon/Documents/qwen-code', - processCwd: '/Users/dragon/.craft-agent/workspaces/qwen-code', - projectRoot: '/Users/dragon/Documents/qwen-code', - }); - }); - - it('loads memory settings through the workspace Qwen ACP process', async () => { - mockedWorkspace = { - id: 'ws-1', - name: 'qwen-code', - slug: 'qwen-code', - rootPath: '/Users/dragon/.craft-agent/workspaces/qwen-code', - }; - mockedWorkspaceConfig = { - defaults: { - workingDirectory: '/Users/dragon/Documents/qwen-code', - }, - }; - - const getHandler = handlers.get(RPC_CHANNELS.memory.GET_SETTINGS); - expect(getHandler).toBeTruthy(); - - await getHandler!(requestContext, 'ws-1'); - - expect(getQwenMemorySettingsViaAcpMock).toHaveBeenCalledTimes(1); - expect(getQwenMemorySettingsViaAcpMock.mock.calls[0]?.[0]).toMatchObject({ - cwd: '/Users/dragon/Documents/qwen-code', - processCwd: '/Users/dragon/.craft-agent/workspaces/qwen-code', - projectRoot: '/Users/dragon/Documents/qwen-code', - }); - }); - - it('saves memory settings through the workspace Qwen ACP process', async () => { - mockedWorkspace = { - id: 'ws-1', - name: 'qwen-code', - slug: 'qwen-code', - rootPath: '/Users/dragon/.craft-agent/workspaces/qwen-code', - }; - mockedWorkspaceConfig = { - defaults: { - workingDirectory: '/Users/dragon/Documents/qwen-code', - }, - }; - - const setHandler = handlers.get(RPC_CHANNELS.memory.SET_SETTINGS); - expect(setHandler).toBeTruthy(); - - const updates = { enableManagedAutoDream: true }; - await setHandler!(requestContext, updates, 'ws-1'); - - expect(setQwenMemorySettingsViaAcpMock).toHaveBeenCalledTimes(1); - expect(setQwenMemorySettingsViaAcpMock.mock.calls[0]?.[0]).toMatchObject({ - cwd: '/Users/dragon/Documents/qwen-code', - processCwd: '/Users/dragon/.craft-agent/workspaces/qwen-code', - projectRoot: '/Users/dragon/Documents/qwen-code', - }); - expect(setQwenMemorySettingsViaAcpMock.mock.calls[0]?.[1]).toBe(updates); - }); -}); diff --git a/packages/desktop/apps/electron/src/main/handlers/browser.ts b/packages/desktop/apps/electron/src/main/handlers/browser.ts deleted file mode 100644 index 74c840f485b..00000000000 --- a/packages/desktop/apps/electron/src/main/handlers/browser.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { RPC_CHANNELS, type BrowserPaneCreateOptions, type BrowserEmptyStateLaunchPayload, type BrowserPaneDockBounds } from '../../shared/types' -import type { BrowserScreenshotOptions } from '../browser-pane-manager' -import { pushTyped, type RpcServer } from '@craft-agent/server-core/transport' -import type { HandlerDeps } from './handler-deps' - -export const HANDLED_CHANNELS = [ - RPC_CHANNELS.browserPane.CREATE, - RPC_CHANNELS.browserPane.DESTROY, - RPC_CHANNELS.browserPane.LIST, - RPC_CHANNELS.browserPane.NAVIGATE, - RPC_CHANNELS.browserPane.GO_BACK, - RPC_CHANNELS.browserPane.GO_FORWARD, - RPC_CHANNELS.browserPane.RELOAD, - RPC_CHANNELS.browserPane.STOP, - RPC_CHANNELS.browserPane.FOCUS, - RPC_CHANNELS.browserPane.HIDE, - RPC_CHANNELS.browserPane.DOCK, - RPC_CHANNELS.browserPane.TOGGLE_DOCK_EXPANDED, - RPC_CHANNELS.browserPane.LAUNCH, - RPC_CHANNELS.browserPane.SNAPSHOT, - RPC_CHANNELS.browserPane.CLICK, - RPC_CHANNELS.browserPane.FILL, - RPC_CHANNELS.browserPane.SELECT, - RPC_CHANNELS.browserPane.SCREENSHOT, - RPC_CHANNELS.browserPane.EVALUATE, - RPC_CHANNELS.browserPane.SCROLL, -] as const - -export function registerBrowserHandlers(server: RpcServer, deps: HandlerDeps): void { - const { browserPaneManager, platform, windowManager } = deps - if (!browserPaneManager) return - - server.handle(RPC_CHANNELS.browserPane.CREATE, (_ctx, input?: string | BrowserPaneCreateOptions) => { - if (typeof input === 'string') { - return browserPaneManager.createInstance(input) - } - - if (input?.bindToSessionId) { - return browserPaneManager.createForSession(input.bindToSessionId, { show: input.show ?? false }) - } - - return browserPaneManager.createInstance(input?.id, { - show: input?.show, - presentation: input?.presentation, - }) - }) - - server.handle(RPC_CHANNELS.browserPane.DESTROY, (_ctx, id: string) => { - browserPaneManager.destroyInstance(id) - }) - - server.handle(RPC_CHANNELS.browserPane.LIST, () => { - return browserPaneManager.listInstances() - }) - - server.handle(RPC_CHANNELS.browserPane.NAVIGATE, async (_ctx, id: string, url: string) => { - try { - return await browserPaneManager.navigate(id, url) - } catch (err) { - platform.logger.error(`[browser-pane] navigate failed for ${id}:`, err) - throw err - } - }) - - server.handle(RPC_CHANNELS.browserPane.GO_BACK, async (_ctx, id: string) => { - try { - return await browserPaneManager.goBack(id) - } catch (err) { - platform.logger.error(`[browser-pane] goBack failed for ${id}:`, err) - throw err - } - }) - - server.handle(RPC_CHANNELS.browserPane.GO_FORWARD, async (_ctx, id: string) => { - try { - return await browserPaneManager.goForward(id) - } catch (err) { - platform.logger.error(`[browser-pane] goForward failed for ${id}:`, err) - throw err - } - }) - - server.handle(RPC_CHANNELS.browserPane.RELOAD, (_ctx, id: string) => { - browserPaneManager.reload(id) - }) - - server.handle(RPC_CHANNELS.browserPane.STOP, (_ctx, id: string) => { - browserPaneManager.stop(id) - }) - - server.handle(RPC_CHANNELS.browserPane.FOCUS, (_ctx, id: string) => { - browserPaneManager.focus(id) - }) - - server.handle(RPC_CHANNELS.browserPane.HIDE, (_ctx, id: string) => { - browserPaneManager.hide(id) - }) - - server.handle(RPC_CHANNELS.browserPane.DOCK, (ctx, id: string, bounds: BrowserPaneDockBounds) => { - const hostWindow = ctx.webContentsId && windowManager - ? windowManager.getWindowByWebContentsId(ctx.webContentsId) - : null - if (!hostWindow) { - platform.logger.warn(`[browser-pane] dock ignored for ${id}: host window unavailable`) - return - } - - browserPaneManager.dock(id, hostWindow, bounds) - }) - - server.handle(RPC_CHANNELS.browserPane.TOGGLE_DOCK_EXPANDED, (_ctx, id: string) => { - browserPaneManager.toggleDockExpanded(id) - }) - - server.handle(RPC_CHANNELS.browserPane.LAUNCH, async (ctx, payload: BrowserEmptyStateLaunchPayload) => { - try { - return await browserPaneManager.handleEmptyStateLaunchFromRenderer(ctx.webContentsId!, payload) - } catch (err) { - platform.logger.error('[browser-pane] empty-state launch IPC failed:', err) - throw err - } - }) - - server.handle(RPC_CHANNELS.browserPane.SNAPSHOT, async (_ctx, id: string) => { - try { - return await browserPaneManager.getAccessibilitySnapshot(id) - } catch (err) { - platform.logger.error(`[browser-pane] snapshot failed for ${id}:`, err) - throw err - } - }) - - server.handle(RPC_CHANNELS.browserPane.CLICK, async (_ctx, id: string, ref: string) => { - try { - return await browserPaneManager.clickElement(id, ref) - } catch (err) { - platform.logger.error(`[browser-pane] click failed for ${id} ref=${ref}:`, err) - throw err - } - }) - - server.handle(RPC_CHANNELS.browserPane.FILL, async (_ctx, id: string, ref: string, value: string) => { - try { - return await browserPaneManager.fillElement(id, ref, value) - } catch (err) { - platform.logger.error(`[browser-pane] fill failed for ${id} ref=${ref}:`, err) - throw err - } - }) - - server.handle(RPC_CHANNELS.browserPane.SELECT, async (_ctx, id: string, ref: string, value: string) => { - try { - return await browserPaneManager.selectOption(id, ref, value) - } catch (err) { - platform.logger.error(`[browser-pane] select failed for ${id} ref=${ref}:`, err) - throw err - } - }) - - server.handle(RPC_CHANNELS.browserPane.SCREENSHOT, async (_ctx, id: string, options?: BrowserScreenshotOptions) => { - try { - const result = await browserPaneManager.screenshot(id, options) - return { - base64: result.imageBuffer.toString('base64'), - imageFormat: result.imageFormat, - metadata: result.metadata, - } - } catch (err) { - platform.logger.error(`[browser-pane] screenshot failed for ${id}:`, err) - throw err - } - }) - - server.handle(RPC_CHANNELS.browserPane.EVALUATE, async (_ctx, id: string, expression: string) => { - try { - return await browserPaneManager.evaluate(id, expression) - } catch (err) { - platform.logger.error(`[browser-pane] evaluate failed for ${id}:`, err) - throw err - } - }) - - server.handle(RPC_CHANNELS.browserPane.SCROLL, async (_ctx, id: string, direction: string, amount?: number) => { - const validDirections = ['up', 'down', 'left', 'right'] - if (!validDirections.includes(direction)) { - throw new Error(`Invalid scroll direction: ${direction}`) - } - try { - return await browserPaneManager.scroll(id, direction as 'up' | 'down' | 'left' | 'right', amount) - } catch (err) { - platform.logger.error(`[browser-pane] scroll failed for ${id}:`, err) - throw err - } - }) - - // Forward browser state changes to all windows - browserPaneManager.onStateChange((info) => { - pushTyped(server, RPC_CHANNELS.browserPane.STATE_CHANGED, { to: 'all' }, info) - }) - - // Forward browser removals so renderer can immediately drop stale tabs - browserPaneManager.onRemoved((id) => { - pushTyped(server, RPC_CHANNELS.browserPane.REMOVED, { to: 'all' }, id) - }) - - // Forward browser interaction/focus events so renderer can align panel focus. - browserPaneManager.onInteracted((id) => { - pushTyped(server, RPC_CHANNELS.browserPane.INTERACTED, { to: 'all' }, id) - }) -} diff --git a/packages/desktop/apps/electron/src/main/handlers/handler-deps.ts b/packages/desktop/apps/electron/src/main/handlers/handler-deps.ts deleted file mode 100644 index b984c23a12b..00000000000 --- a/packages/desktop/apps/electron/src/main/handlers/handler-deps.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * HandlerDeps — dependency bag for all IPC handlers. - * - * Concrete Electron specialization of the generic server-core handler deps. - */ - -import type { HandlerDeps as BaseHandlerDeps } from '@craft-agent/server-core/handlers' -import type { SessionManager } from '@craft-agent/server-core/sessions' -import type { WindowManager } from '../window-manager' -import type { BrowserPaneManager } from '../browser-pane-manager' -import type { OAuthFlowStore } from '@craft-agent/shared/auth' - -export type HandlerDeps = BaseHandlerDeps< - SessionManager, - OAuthFlowStore, - WindowManager, - BrowserPaneManager -> diff --git a/packages/desktop/apps/electron/src/main/handlers/index.ts b/packages/desktop/apps/electron/src/main/handlers/index.ts deleted file mode 100644 index 82eec142873..00000000000 --- a/packages/desktop/apps/electron/src/main/handlers/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { HandlerDeps } from './handler-deps'; -import type { RpcServer } from '@craft-agent/server-core/transport'; -import { - registerCoreRpcHandlers, - type ServerHandlerContext, -} from '@craft-agent/server-core/handlers/rpc'; -export { registerCoreRpcHandlers }; - -// GUI-only handlers remain local (Electron-specific imports) -import { registerSystemGuiHandlers } from './system'; -import { registerWorkspaceGuiHandlers } from './workspace'; -import { registerBrowserHandlers } from './browser'; -import { registerSettingsGuiHandlers } from './settings'; -import { registerWindowDragGuiHandlers } from './window-drag'; -import { registerPetWindowGuiHandlers } from './pet-window'; - -export function registerGuiRpcHandlers( - server: RpcServer, - deps: HandlerDeps, -): void { - registerSystemGuiHandlers(server, deps); - registerWorkspaceGuiHandlers(server, deps); - registerBrowserHandlers(server, deps); - registerSettingsGuiHandlers(server, deps); - registerWindowDragGuiHandlers(server, deps); - registerPetWindowGuiHandlers(server, deps); -} - -export function registerAllRpcHandlers( - server: RpcServer, - deps: HandlerDeps, - serverCtx?: ServerHandlerContext, -): void { - registerCoreRpcHandlers(server, deps, serverCtx); - registerGuiRpcHandlers(server, deps); -} diff --git a/packages/desktop/apps/electron/src/main/handlers/pet-window.ts b/packages/desktop/apps/electron/src/main/handlers/pet-window.ts deleted file mode 100644 index c7c14b2f2cf..00000000000 --- a/packages/desktop/apps/electron/src/main/handlers/pet-window.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { RPC_CHANNELS } from '@craft-agent/shared/protocol'; -import type { RpcServer } from '@craft-agent/server-core/transport'; -import type { HandlerDeps } from './handler-deps'; - -export const GUI_HANDLED_CHANNELS = [ - RPC_CHANNELS.window.PET_SET_ENABLED, - RPC_CHANNELS.window.PET_SET_IGNORE_MOUSE, - RPC_CHANNELS.window.PET_FOCUS_SESSION, -] as const; - -/** - * GUI handlers for the floating desktop-pet window. The renderer that hosts the - * main UI toggles the window on/off (and reloads it on pet change); the pet - * window itself toggles click-through as the cursor enters/leaves the pet. - */ -export function registerPetWindowGuiHandlers( - server: RpcServer, - deps: HandlerDeps, -): void { - server.handle( - RPC_CHANNELS.window.PET_SET_ENABLED, - (ctx, enabled: boolean) => { - const wm = deps.windowManager; - if (!wm) return; - const workspaceId = - ctx.webContentsId != null - ? (wm.getWorkspaceForWindow(ctx.webContentsId) ?? '') - : ''; - wm.setPetWindowEnabled(Boolean(enabled), workspaceId); - }, - ); - - server.handle( - RPC_CHANNELS.window.PET_SET_IGNORE_MOUSE, - (_ctx, ignore: boolean) => { - deps.windowManager?.setPetWindowIgnoreMouse(Boolean(ignore)); - }, - ); - - // Clicking a pet notification card focuses the main window and navigates to - // the originating session (reuses the OS-notification click path). - server.handle( - RPC_CHANNELS.window.PET_FOCUS_SESSION, - async (ctx, sessionId: string) => { - const wm = deps.windowManager; - if (!wm || !sessionId || ctx.webContentsId == null) return; - const workspaceId = wm.getWorkspaceForWindow(ctx.webContentsId); - if (!workspaceId) return; - const { handleNotificationClick } = await import('../notifications'); - handleNotificationClick(workspaceId, sessionId); - }, - ); -} diff --git a/packages/desktop/apps/electron/src/main/handlers/settings.ts b/packages/desktop/apps/electron/src/main/handlers/settings.ts deleted file mode 100644 index 1e1f9bc87f0..00000000000 --- a/packages/desktop/apps/electron/src/main/handlers/settings.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { RPC_CHANNELS } from '@craft-agent/shared/protocol' -import type { RpcServer } from '@craft-agent/server-core/transport' -import type { HandlerDeps } from './handler-deps' - -export const GUI_HANDLED_CHANNELS = [ - RPC_CHANNELS.power.SET_KEEP_AWAKE, - RPC_CHANNELS.settings.SET_NETWORK_PROXY, -] as const - -// ============================================================ -// GUI-only settings (require Electron-specific APIs) -// ============================================================ - -export function registerSettingsGuiHandlers(server: RpcServer, _deps: HandlerDeps): void { - // Set keep awake while running setting (requires Electron power-manager) - server.handle(RPC_CHANNELS.power.SET_KEEP_AWAKE, async (_ctx, enabled: boolean) => { - const { setKeepAwakeWhileRunning } = await import('@craft-agent/shared/config/storage') - const { setKeepAwakeSetting } = await import('../power-manager') - // Save to config - setKeepAwakeWhileRunning(enabled) - // Update the power manager's cached value and power state - setKeepAwakeSetting(enabled) - }) - - // Set network proxy settings (requires Electron session proxy) - server.handle(RPC_CHANNELS.settings.SET_NETWORK_PROXY, async (_ctx, settings: import('@craft-agent/shared/config/types').NetworkProxySettings) => { - const { updateConfiguredProxySettings } = await import('../network-proxy') - await updateConfiguredProxySettings(settings) - }) -} diff --git a/packages/desktop/apps/electron/src/main/handlers/system.ts b/packages/desktop/apps/electron/src/main/handlers/system.ts deleted file mode 100644 index a6adb61dfe9..00000000000 --- a/packages/desktop/apps/electron/src/main/handlers/system.ts +++ /dev/null @@ -1,440 +0,0 @@ -import { resolve } from 'path' -import { join } from 'path' -import { homedir } from 'os' -import { execSync } from 'child_process' -import { RPC_CHANNELS } from '@craft-agent/shared/protocol' -import { getGitBashPath, setGitBashPath, clearGitBashPath } from '@craft-agent/shared/config' -import { isSafeExternalUrl } from '@craft-agent/shared/utils/url-safety' -import { isUsableGitBashPath, validateGitBashPath } from '@craft-agent/server-core/services' -import { validateFilePath, getWorkspaceAllowedDirs } from '@craft-agent/server-core/handlers' -import type { RpcServer } from '@craft-agent/server-core/transport' -import type { HandlerDeps } from './handler-deps' -import { - requestClientOpenExternal, - requestClientOpenPath, - requestClientShowInFolder, - requestClientOpenFileDialog, -} from '@craft-agent/server-core/transport' - -export const CORE_HANDLED_CHANNELS = [ - RPC_CHANNELS.theme.GET_SYSTEM_PREFERENCE, - RPC_CHANNELS.system.VERSIONS, - RPC_CHANNELS.system.HOME_DIR, - RPC_CHANNELS.system.IS_DEBUG_MODE, - RPC_CHANNELS.debug.LOG, - RPC_CHANNELS.shell.OPEN_URL, - RPC_CHANNELS.shell.OPEN_FILE, - RPC_CHANNELS.shell.SHOW_IN_FOLDER, - RPC_CHANNELS.releaseNotes.GET, - RPC_CHANNELS.releaseNotes.GET_LATEST_VERSION, - RPC_CHANNELS.git.GET_BRANCH, - RPC_CHANNELS.gitbash.CHECK, - RPC_CHANNELS.gitbash.BROWSE, - RPC_CHANNELS.gitbash.SET_PATH, -] as const - -export const GUI_HANDLED_CHANNELS = [ - RPC_CHANNELS.update.CHECK, - RPC_CHANNELS.update.GET_INFO, - RPC_CHANNELS.update.INSTALL, - RPC_CHANNELS.update.DISMISS, - RPC_CHANNELS.update.GET_DISMISSED, - RPC_CHANNELS.badge.REFRESH, - RPC_CHANNELS.badge.SET_ICON, - RPC_CHANNELS.window.GET_FOCUS_STATE, - RPC_CHANNELS.notification.SHOW, - RPC_CHANNELS.notification.GET_ENABLED, - RPC_CHANNELS.notification.SET_ENABLED, - RPC_CHANNELS.menu.QUIT, - RPC_CHANNELS.menu.NEW_WINDOW, - RPC_CHANNELS.menu.MINIMIZE, - RPC_CHANNELS.menu.MAXIMIZE, - RPC_CHANNELS.menu.ZOOM_IN, - RPC_CHANNELS.menu.ZOOM_OUT, - RPC_CHANNELS.menu.ZOOM_RESET, - RPC_CHANNELS.menu.TOGGLE_DEV_TOOLS, - RPC_CHANNELS.menu.UNDO, - RPC_CHANNELS.menu.REDO, - RPC_CHANNELS.menu.CUT, - RPC_CHANNELS.menu.COPY, - RPC_CHANNELS.menu.PASTE, - RPC_CHANNELS.menu.SELECT_ALL, -] as const - -export const HANDLED_CHANNELS = [ - ...CORE_HANDLED_CHANNELS, - ...GUI_HANDLED_CHANNELS, -] as const - -export function registerSystemCoreHandlers(server: RpcServer, deps: HandlerDeps): void { - const windowManager = deps.windowManager - - // Get system theme preference (dark = true, light = false) - server.handle(RPC_CHANNELS.theme.GET_SYSTEM_PREFERENCE, async () => { - return deps.platform.systemDarkMode?.() ?? false - }) - - // Get runtime versions (previously handled locally in preload via process.versions) - server.handle(RPC_CHANNELS.system.VERSIONS, async () => { - return { - node: process.versions.node, - chrome: process.versions.chrome, - electron: process.versions.electron, - } - }) - - // Get user's home directory - server.handle(RPC_CHANNELS.system.HOME_DIR, async () => { - return homedir() - }) - - // Check if running in debug mode (from source) - server.handle(RPC_CHANNELS.system.IS_DEBUG_MODE, async () => { - return !deps.platform.isPackaged - }) - - // Release notes - server.handle(RPC_CHANNELS.releaseNotes.GET, async () => { - const { getCombinedReleaseNotes } = require('@craft-agent/shared/release-notes') as typeof import('@craft-agent/shared/release-notes') - return getCombinedReleaseNotes() - }) - - server.handle(RPC_CHANNELS.releaseNotes.GET_LATEST_VERSION, async () => { - const { getLatestReleaseVersion } = require('@craft-agent/shared/release-notes') as typeof import('@craft-agent/shared/release-notes') - return getLatestReleaseVersion() - }) - - // Get git branch for a directory (returns null if not a git repo or git unavailable) - server.handle(RPC_CHANNELS.git.GET_BRANCH, async (_ctx, dirPath: string) => { - try { - const branch = execSync('git rev-parse --abbrev-ref HEAD', { - cwd: dirPath, - encoding: 'utf-8', - stdio: ['pipe', 'pipe', 'pipe'], - timeout: 5000, - }).trim() - return branch || null - } catch { - return null - } - }) - - // Git Bash detection and configuration (Windows only) - server.handle(RPC_CHANNELS.gitbash.CHECK, async () => { - const platform = process.platform as 'win32' | 'darwin' | 'linux' - - if (platform !== 'win32') { - return { found: true, path: null, platform } - } - - const commonPaths = [ - 'C:\\Program Files\\Git\\bin\\bash.exe', - 'C:\\Program Files (x86)\\Git\\bin\\bash.exe', - join(process.env.LOCALAPPDATA || '', 'Programs', 'Git', 'bin', 'bash.exe'), - join(process.env.PROGRAMFILES || '', 'Git', 'bin', 'bash.exe'), - ] - - const persistedPath = getGitBashPath() - if (persistedPath) { - if (await isUsableGitBashPath(persistedPath)) { - process.env.QWEN_CODE_GIT_BASH_PATH = persistedPath.trim() - return { found: true, path: persistedPath, platform } - } - clearGitBashPath() - } - - for (const bashPath of commonPaths) { - if (await isUsableGitBashPath(bashPath)) { - process.env.QWEN_CODE_GIT_BASH_PATH = bashPath - setGitBashPath(bashPath) - return { found: true, path: bashPath, platform } - } - } - - try { - const result = execSync('where bash', { - encoding: 'utf-8', - stdio: ['pipe', 'pipe', 'pipe'], - timeout: 5000, - }).trim() - const firstPath = result.split('\n')[0]?.trim() - if (firstPath && firstPath.toLowerCase().includes('git') && await isUsableGitBashPath(firstPath)) { - process.env.QWEN_CODE_GIT_BASH_PATH = firstPath - setGitBashPath(firstPath) - return { found: true, path: firstPath, platform } - } - } catch { - // where command failed - } - - delete process.env.QWEN_CODE_GIT_BASH_PATH - return { found: false, path: null, platform } - }) - - server.handle(RPC_CHANNELS.gitbash.BROWSE, async (ctx) => { - const result = await requestClientOpenFileDialog(server, ctx.clientId, { - title: 'Select bash.exe', - filters: [{ name: 'Executable', extensions: ['exe'] }], - properties: ['openFile'], - defaultPath: 'C:\\Program Files\\Git\\bin', - }) - - if (result.canceled || result.filePaths.length === 0) { - return null - } - - return result.filePaths[0] - }) - - server.handle(RPC_CHANNELS.gitbash.SET_PATH, async (_ctx, bashPath: string) => { - const validation = await validateGitBashPath(bashPath) - if (!validation.valid) { - return { success: false, error: validation.error } - } - - setGitBashPath(validation.path) - process.env.QWEN_CODE_GIT_BASH_PATH = validation.path - return { success: true } - }) - - // Debug logging from renderer -> main log file (fire-and-forget, no response) - server.handle(RPC_CHANNELS.debug.LOG, async (_ctx, ...args: unknown[]) => { - deps.platform.logger.info('[renderer]', ...args) - }) - - // Shell operations - open URL in external browser (or handle craftagents:// internally) - server.handle(RPC_CHANNELS.shell.OPEN_URL, async (ctx, url: string) => { - deps.platform.logger.info('[OPEN_URL] Received request:', url) - try { - const parsed = new URL(url) - - // Handle craftagents:// URLs internally via deep link handler (GUI only) - if (parsed.protocol === 'craftagents:') { - if (!windowManager) return - deps.platform.logger.info('[OPEN_URL] Handling as deep link') - const { handleDeepLink } = await import('../deep-link') - const resolver = (wcId: number) => windowManager.getClientIdForWindow(wcId) - const result = await handleDeepLink(url, windowManager, server.push.bind(server), resolver, ctx.clientId) - deps.platform.logger.info('[OPEN_URL] Deep link result:', result) - return - } - - if (!isSafeExternalUrl(url)) { - throw new Error(`Refused to open URL with blocked scheme: ${parsed.protocol}`) - } - - const result = await requestClientOpenExternal(server, ctx.clientId, url) - if (!result.opened) { - deps.platform.logger.error(`[OPEN_URL] Client capability failed: ${result.error}`) - throw new Error(`Cannot open URL on client: ${result.error}`) - } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - deps.platform.logger.error('openUrl error:', message) - throw new Error(`Failed to open URL: ${message}`) - } - }) - - server.handle(RPC_CHANNELS.shell.OPEN_FILE, async (ctx, path: string) => { - try { - const expanded = path.startsWith('~') ? path.replace(/^~/, homedir()) : path - const absolutePath = resolve(expanded) - const workspaceId = ctx.workspaceId ?? deps.windowManager?.getWorkspaceForWindow(ctx.webContentsId!) - const safePath = await validateFilePath(absolutePath, getWorkspaceAllowedDirs(workspaceId)) - const result = await requestClientOpenPath(server, ctx.clientId, safePath) - if (result.error) throw new Error(result.error) - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - deps.platform.logger.error('openFile error:', message) - throw new Error(`Failed to open file: ${message}`) - } - }) - - server.handle(RPC_CHANNELS.shell.SHOW_IN_FOLDER, async (ctx, path: string) => { - try { - const expanded = path.startsWith('~') ? path.replace(/^~/, homedir()) : path - const absolutePath = resolve(expanded) - const workspaceId = ctx.workspaceId ?? deps.windowManager?.getWorkspaceForWindow(ctx.webContentsId!) - const safePath = await validateFilePath(absolutePath, getWorkspaceAllowedDirs(workspaceId)) - await requestClientShowInFolder(server, ctx.clientId, safePath) - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - deps.platform.logger.error('showInFolder error:', message) - throw new Error(`Failed to show in folder: ${message}`) - } - }) -} - -export function registerSystemGuiHandlers(server: RpcServer, deps: HandlerDeps): void { - const { sessionManager } = deps - const windowManager = deps.windowManager - - // Auto-update handlers - server.handle(RPC_CHANNELS.update.CHECK, async () => { - const { checkForUpdates } = await import('../auto-update') - return checkForUpdates({ autoDownload: true }) - }) - - server.handle(RPC_CHANNELS.update.GET_INFO, async () => { - const { getUpdateInfo } = await import('../auto-update') - return getUpdateInfo() - }) - - server.handle(RPC_CHANNELS.update.INSTALL, async () => { - const { installUpdate } = await import('../auto-update') - return installUpdate() - }) - - server.handle(RPC_CHANNELS.update.DISMISS, async (_ctx, version: string) => { - const { setDismissedUpdateVersion } = await import('@craft-agent/shared/config') - setDismissedUpdateVersion(version) - }) - - server.handle(RPC_CHANNELS.update.GET_DISMISSED, async () => { - const { getDismissedUpdateVersion } = await import('@craft-agent/shared/config') - return getDismissedUpdateVersion() - }) - - // Menu actions from renderer (for unified Craft menu) - server.handle(RPC_CHANNELS.menu.QUIT, async () => { - deps.platform.quit?.() - }) - - server.handle(RPC_CHANNELS.menu.NEW_WINDOW, async (ctx) => { - if (!windowManager) return - const workspaceId = ctx.workspaceId ?? windowManager.getWorkspaceForWindow(ctx.webContentsId!) - if (workspaceId) { - windowManager.createWindow({ workspaceId }) - } - }) - - server.handle(RPC_CHANNELS.menu.MINIMIZE, async (ctx) => { - if (!windowManager) return - const win = windowManager.getWindowByWebContentsId(ctx.webContentsId!) - win?.minimize() - }) - - server.handle(RPC_CHANNELS.menu.MAXIMIZE, async (ctx) => { - if (!windowManager) return - const win = windowManager.getWindowByWebContentsId(ctx.webContentsId!) - if (win) { - if (win.isMaximized()) { - win.unmaximize() - } else { - win.maximize() - } - } - }) - - server.handle(RPC_CHANNELS.menu.ZOOM_IN, async (ctx) => { - if (!windowManager) return - const win = windowManager.getWindowByWebContentsId(ctx.webContentsId!) - if (win) { - const currentZoom = win.webContents.getZoomFactor() - win.webContents.setZoomFactor(Math.min(currentZoom + 0.1, 3.0)) - } - }) - - server.handle(RPC_CHANNELS.menu.ZOOM_OUT, async (ctx) => { - if (!windowManager) return - const win = windowManager.getWindowByWebContentsId(ctx.webContentsId!) - if (win) { - const currentZoom = win.webContents.getZoomFactor() - win.webContents.setZoomFactor(Math.max(currentZoom - 0.1, 0.5)) - } - }) - - server.handle(RPC_CHANNELS.menu.ZOOM_RESET, async (ctx) => { - if (!windowManager) return - const win = windowManager.getWindowByWebContentsId(ctx.webContentsId!) - win?.webContents.setZoomFactor(1.0) - }) - - server.handle(RPC_CHANNELS.menu.TOGGLE_DEV_TOOLS, async (ctx) => { - if (!windowManager) return - const win = windowManager.getWindowByWebContentsId(ctx.webContentsId!) - win?.webContents.toggleDevTools() - }) - - server.handle(RPC_CHANNELS.menu.UNDO, async (ctx) => { - if (!windowManager) return - const win = windowManager.getWindowByWebContentsId(ctx.webContentsId!) - win?.webContents.undo() - }) - - server.handle(RPC_CHANNELS.menu.REDO, async (ctx) => { - if (!windowManager) return - const win = windowManager.getWindowByWebContentsId(ctx.webContentsId!) - win?.webContents.redo() - }) - - server.handle(RPC_CHANNELS.menu.CUT, async (ctx) => { - if (!windowManager) return - const win = windowManager.getWindowByWebContentsId(ctx.webContentsId!) - win?.webContents.cut() - }) - - server.handle(RPC_CHANNELS.menu.COPY, async (ctx) => { - if (!windowManager) return - const win = windowManager.getWindowByWebContentsId(ctx.webContentsId!) - win?.webContents.copy() - }) - - server.handle(RPC_CHANNELS.menu.PASTE, async (ctx) => { - if (!windowManager) return - const win = windowManager.getWindowByWebContentsId(ctx.webContentsId!) - win?.webContents.paste() - }) - - server.handle(RPC_CHANNELS.menu.SELECT_ALL, async (ctx) => { - if (!windowManager) return - const win = windowManager.getWindowByWebContentsId(ctx.webContentsId!) - win?.webContents.selectAll() - }) - - // Notifications - server.handle(RPC_CHANNELS.notification.SHOW, async (_ctx, title: string, body: string, workspaceId: string, sessionId: string) => { - const { showNotification } = await import('../notifications') - showNotification(title, body, workspaceId, sessionId) - }) - - server.handle(RPC_CHANNELS.notification.GET_ENABLED, async () => { - const { getNotificationsEnabled } = await import('@craft-agent/shared/config/storage') - return getNotificationsEnabled() - }) - - server.handle(RPC_CHANNELS.notification.SET_ENABLED, async (_ctx, enabled: boolean) => { - const { setNotificationsEnabled } = await import('@craft-agent/shared/config/storage') - setNotificationsEnabled(enabled) - - if (enabled) { - const { showNotification } = await import('../notifications') - showNotification('Notifications enabled', 'You will be notified when tasks complete.', '', '') - } - }) - - // Badge and window focus - server.handle(RPC_CHANNELS.badge.REFRESH, async () => { - try { - await sessionManager.waitForInit() - } catch { - // continue - } - sessionManager.refreshBadge() - }) - - server.handle(RPC_CHANNELS.badge.SET_ICON, async (_ctx, dataUrl: string) => { - const { setDockIconWithBadge } = await import('../notifications') - setDockIconWithBadge(dataUrl) - }) - - server.handle(RPC_CHANNELS.window.GET_FOCUS_STATE, async () => { - const { isAnyWindowFocused } = require('../notifications') - return isAnyWindowFocused() - }) -} - -export function registerSystemHandlers(server: RpcServer, deps: HandlerDeps): void { - registerSystemCoreHandlers(server, deps) - registerSystemGuiHandlers(server, deps) -} diff --git a/packages/desktop/apps/electron/src/main/handlers/window-drag.ts b/packages/desktop/apps/electron/src/main/handlers/window-drag.ts deleted file mode 100644 index d8ef91bdcc8..00000000000 --- a/packages/desktop/apps/electron/src/main/handlers/window-drag.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { RPC_CHANNELS } from '@craft-agent/shared/protocol'; -import type { RpcServer } from '@craft-agent/server-core/transport'; -import type { HandlerDeps } from './handler-deps'; - -type WindowDragState = { - startScreenX: number; - startScreenY: number; - startWindowX: number; - startWindowY: number; -}; - -export const GUI_HANDLED_CHANNELS = [ - RPC_CHANNELS.window.BEGIN_DRAG, - RPC_CHANNELS.window.MOVE_DRAG, - RPC_CHANNELS.window.END_DRAG, -] as const; - -const dragStates = new Map(); - -function isFinitePoint(screenX: number, screenY: number): boolean { - return Number.isFinite(screenX) && Number.isFinite(screenY); -} - -export function registerWindowDragGuiHandlers( - server: RpcServer, - deps: HandlerDeps, -): void { - server.handle( - RPC_CHANNELS.window.BEGIN_DRAG, - (ctx, screenX: number, screenY: number) => { - const webContentsId = ctx.webContentsId; - if (webContentsId == null || !isFinitePoint(screenX, screenY)) return; - - const window = - deps.windowManager?.getWindowByWebContentsId(webContentsId); - if (!window || window.isDestroyed()) return; - - const [startWindowX, startWindowY] = window.getPosition(); - dragStates.set(webContentsId, { - startScreenX: screenX, - startScreenY: screenY, - startWindowX, - startWindowY, - }); - }, - ); - - server.handle( - RPC_CHANNELS.window.MOVE_DRAG, - (ctx, screenX: number, screenY: number) => { - const webContentsId = ctx.webContentsId; - if (webContentsId == null || !isFinitePoint(screenX, screenY)) return; - - const window = - deps.windowManager?.getWindowByWebContentsId(webContentsId); - const state = dragStates.get(webContentsId); - if (!window || !state || window.isDestroyed()) return; - - window.setPosition( - Math.round(state.startWindowX + screenX - state.startScreenX), - Math.round(state.startWindowY + screenY - state.startScreenY), - ); - }, - ); - - server.handle(RPC_CHANNELS.window.END_DRAG, (ctx) => { - if (ctx.webContentsId != null) { - dragStates.delete(ctx.webContentsId); - } - }); -} diff --git a/packages/desktop/apps/electron/src/main/handlers/workspace.ts b/packages/desktop/apps/electron/src/main/handlers/workspace.ts deleted file mode 100644 index 0be5c9284fc..00000000000 --- a/packages/desktop/apps/electron/src/main/handlers/workspace.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { RPC_CHANNELS } from '@craft-agent/shared/protocol' -import type { RpcServer } from '@craft-agent/server-core/transport' -import type { HandlerDeps } from './handler-deps' - -export const GUI_HANDLED_CHANNELS = [ - RPC_CHANNELS.remote.TEST_CONNECTION, - RPC_CHANNELS.window.OPEN_WORKSPACE, - RPC_CHANNELS.window.OPEN_SESSION_IN_NEW_WINDOW, - RPC_CHANNELS.window.CLOSE, - RPC_CHANNELS.window.CONFIRM_CLOSE, - RPC_CHANNELS.window.CANCEL_CLOSE, - RPC_CHANNELS.window.SET_TRAFFIC_LIGHTS, -] as const - -/** - * Connect to a remote server and wait for handshake. - * When workspaceId is provided, the handshake is scoped to that workspace so - * workspace-context RPC handlers (for example sessions:export) can resolve it. - * Returns the connected client or null + error message. - */ -export async function connectToRemote(url: string, token: string, workspaceId?: string) { - const { WsRpcClient } = await import('../../transport/client') - const client = new WsRpcClient(url, { - token, - workspaceId, - autoReconnect: false, - tlsRejectUnauthorized: false, - }) - - const connected = await new Promise((resolve) => { - const timeout = setTimeout(() => resolve(false), 10_000) - const unsub = client.onConnectionStateChanged((state) => { - if (state.status === 'connected') { - clearTimeout(timeout) - unsub() - resolve(true) - } else if (state.status === 'failed') { - clearTimeout(timeout) - unsub() - resolve(false) - } - }) - client.connect() - }) - - if (!connected) { - const error = client.getConnectionState().lastError?.message ?? 'Connection failed' - client.destroy() - return { client: null, error } - } - - return { client, error: null } -} - -export function registerWorkspaceGuiHandlers(server: RpcServer, deps: HandlerDeps): void { - const windowManager = deps.windowManager - - // Test connection to a remote Qwen Code Server. - // Pure discovery — returns list of existing workspaces or needsWorkspace flag. - // Workspace creation is handled separately via invokeOnServer → server:createWorkspace. - server.handle(RPC_CHANNELS.remote.TEST_CONNECTION, async (_ctx, url: string, token: string) => { - const { client, error } = await connectToRemote(url, token) - if (!client) return { ok: false, error } - - // Read server version from handshake_ack (null for old servers) - const serverVersion = client.getServerVersion() ?? undefined - - try { - console.log(`[TEST_CONNECTION] invoking ${RPC_CHANNELS.server.GET_WORKSPACES} on remote server...`) - const workspaces = await client.invoke(RPC_CHANNELS.server.GET_WORKSPACES) as Array<{ id: string; name: string }> - console.log(`[TEST_CONNECTION] remote returned ${workspaces?.length ?? 'null'} workspaces:`, JSON.stringify(workspaces?.map(w => ({ id: w.id, name: w.name })))) - - if (workspaces.length === 0) { - console.log('[TEST_CONNECTION] → returning needsWorkspace=true') - return { ok: true, needsWorkspace: true, serverVersion } - } - - const result = { - ok: true, - serverVersion, - remoteWorkspaces: workspaces, - // Convenience: auto-select if exactly one - remoteWorkspaceId: workspaces.length === 1 ? workspaces[0].id : undefined, - remoteWorkspaceName: workspaces.length === 1 ? workspaces[0].name : undefined, - } - console.log(`[TEST_CONNECTION] → returning ${workspaces.length} workspaces`) - return result - } catch (err) { - console.error('[TEST_CONNECTION] error:', err) - return { ok: false, error: err instanceof Error ? err.message : 'Unknown error' } - } finally { - client.destroy() - } - }) - - // Open workspace in new window (or focus existing) - server.handle(RPC_CHANNELS.window.OPEN_WORKSPACE, async (_ctx, workspaceId: string) => { - if (!windowManager) return - windowManager.focusOrCreateWindow(workspaceId) - }) - - // Open a session in a new window - server.handle(RPC_CHANNELS.window.OPEN_SESSION_IN_NEW_WINDOW, async (_ctx, workspaceId: string, sessionId: string) => { - if (!windowManager) return - const deepLink = `craftagents://allSessions/session/${sessionId}` - windowManager.createWindow({ - workspaceId, - focused: true, - initialDeepLink: deepLink, - }) - }) - - // Close the calling window (triggers close event which may be intercepted) - server.handle(RPC_CHANNELS.window.CLOSE, (ctx) => { - if (!windowManager) return - windowManager.closeWindow(ctx.webContentsId!) - }) - - // Confirm close - force close the window (bypasses interception). - server.handle(RPC_CHANNELS.window.CONFIRM_CLOSE, (ctx) => { - if (!windowManager) return - windowManager.forceCloseWindow(ctx.webContentsId!) - }) - - // Cancel close - renderer handled the request (closed a modal/panel). - server.handle(RPC_CHANNELS.window.CANCEL_CLOSE, (ctx) => { - if (!windowManager) return - windowManager.cancelPendingClose(ctx.webContentsId!) - }) - - // Show/hide macOS traffic light buttons (for fullscreen overlays) - server.handle(RPC_CHANNELS.window.SET_TRAFFIC_LIGHTS, (ctx, visible: boolean) => { - if (!windowManager) return - windowManager.setTrafficLightsVisible(ctx.webContentsId!, visible) - }) -} diff --git a/packages/desktop/apps/electron/src/main/index.ts b/packages/desktop/apps/electron/src/main/index.ts deleted file mode 100644 index c26d913a8d6..00000000000 --- a/packages/desktop/apps/electron/src/main/index.ts +++ /dev/null @@ -1,1348 +0,0 @@ -// Load user's shell environment first (before other imports that may use env) -// This ensures tools like Homebrew, nvm, etc. are available to the agent -import { loadShellEnv } from './shell-env' -loadShellEnv() - -import { app, BrowserWindow, dialog, ipcMain, nativeImage, nativeTheme, session, shell } from 'electron' -import { createHash, randomBytes, randomUUID } from 'crypto' -import { hostname, homedir } from 'os' -import { mkdirSync } from 'fs' -import * as Sentry from '@sentry/electron/main' - -// Multi-worktree dev instances need separate Electron userData directories. -// This isolates Chromium's single-instance lock, cache, cookies, and local storage. -if (process.env.CRAFT_USER_DATA_DIR) { - try { - mkdirSync(process.env.CRAFT_USER_DATA_DIR, { recursive: true }) - app.setPath('userData', process.env.CRAFT_USER_DATA_DIR) - } catch (error) { - console.warn('[main] Failed to set CRAFT_USER_DATA_DIR:', error) - } -} - -// Initialize Sentry error tracking as early as possible after app import. -// Only enabled in production (packaged) builds to avoid noise during development. -// DSN is baked in at build time via esbuild --define (same pattern as OAuth secrets). -// -// NOTE: Source map upload is intentionally disabled. Stack traces in Sentry will show -// bundled/minified code. To enable source map upload in the future: -// 1. Add SENTRY_AUTH_TOKEN, SENTRY_ORG, SENTRY_PROJECT to CI secrets -// 2. Re-enable the @sentry/vite-plugin in vite.config.ts (handles renderer maps) -// 3. Add @sentry/esbuild-plugin to scripts/electron-build-main.ts (handles main process maps) -Sentry.init({ - dsn: process.env.SENTRY_ELECTRON_INGEST_URL, - environment: app.isPackaged ? 'production' : 'development', - release: app.getVersion(), - // Enabled whenever the ingest URL is available — works in both production (baked via CI) - // and development (injected via .env / 1Password). Filter by environment in Sentry dashboard. - enabled: !!process.env.SENTRY_ELECTRON_INGEST_URL, - - // Scrub sensitive data before sending to Sentry. - // Removes authorization headers, API keys/tokens, and credential-like values. - beforeSend(event) { - // Scrub request headers (authorization, cookies) - if (event.request?.headers) { - const sensitiveHeaders = ['authorization', 'cookie', 'x-api-key'] - for (const header of sensitiveHeaders) { - if (event.request.headers[header]) { - event.request.headers[header] = '[REDACTED]' - } - } - } - - // Scrub breadcrumb data that may contain sensitive values - if (event.breadcrumbs) { - for (const breadcrumb of event.breadcrumbs) { - if (breadcrumb.data) { - for (const key of Object.keys(breadcrumb.data)) { - const lowerKey = key.toLowerCase() - if (lowerKey.includes('token') || lowerKey.includes('key') || - lowerKey.includes('secret') || lowerKey.includes('password') || - lowerKey.includes('credential') || lowerKey.includes('auth')) { - breadcrumb.data[key] = '[REDACTED]' - } - } - } - } - } - - return event - }, -}) - -// Initialize i18n for main process (menus, dialogs, etc.) -import { setupI18n, i18n } from '@craft-agent/shared/i18n' -setupI18n() - -// Set anonymous machine ID for Sentry user tracking (no PII — just a hash). -// Uses hostname + homedir to produce a stable per-machine identifier. -const machineId = createHash('sha256').update(hostname() + homedir()).digest('hex').slice(0, 16) -Sentry.setUser({ id: machineId }) - -import { join, delimiter } from 'path' -import { existsSync, readFileSync } from 'fs' -import { RPC_CHANNELS } from '@craft-agent/shared/protocol' -import { SessionManager, setSessionPlatform, setSessionRuntimeHooks } from '@craft-agent/server-core/sessions' -import { registerAllRpcHandlers } from './handlers/index' -import { registerCoreRpcHandlers, cleanupSessionFileWatchForClient } from '@craft-agent/server-core/handlers/rpc' -import type { PlatformServices } from '../runtime/platform' -import { createElectronPlatform } from './platform' -import type { HandlerDeps } from './handlers/handler-deps' -import { bootstrapServer, releaseServerLock, parseServerPort } from '@craft-agent/server-core/bootstrap' -import { startVoiceServer, resolveDesktopVoiceConfig, type VoiceServer } from '@craft-agent/server-core/voice' -import { createMessagingBootstrap, type MessagingBootstrapHandle } from '@craft-agent/messaging-gateway' -import { getCredentialManager } from '@craft-agent/shared/credentials' -import { initModelRefreshService, getModelRefreshService, setFetcherPlatform } from '@craft-agent/server-core/model-fetchers' -import { setSearchPlatform, setImageProcessor } from '@craft-agent/server-core/services' -import { createApplicationMenu } from './menu' -import { WindowManager } from './window-manager' -import { loadWindowState, saveWindowState } from './window-state' -import { ensureDefaultConversationWorkspace, getVoiceEnabled, getWorkspaces, getWorkspaceByNameOrId, isProtectedWorkspace } from '@craft-agent/shared/config' -import { initializeDocs } from '@craft-agent/shared/docs' -import { initializeReleaseNotes } from '@craft-agent/shared/release-notes' -import { ensureDefaultPermissions } from '@craft-agent/shared/agent/permissions-config' -import { ensureToolIcons, ensurePresetThemes } from '@craft-agent/shared/config' -import { setBundledAssetsRoot } from '@craft-agent/shared/utils' -import { initializeBackendHostRuntime } from '@craft-agent/shared/agent/backend' -import { setPowerShellValidatorRoot } from '@craft-agent/shared/agent' -import { handleDeepLink } from './deep-link' -import { getRendererDevOrigin as deriveRendererDevOrigin, isTrustedRendererFrameUrl } from './voice/frame-trust' -import { BrowserPaneManager } from './browser-pane-manager' -import { OAuthFlowStore } from '@craft-agent/shared/auth' -import { registerThumbnailScheme, registerThumbnailHandler } from './thumbnail-protocol' -import log, { isDebugMode, mainLog, getLogFilePath, getMessagingGatewayLogFilePath, messagingGatewayLog } from './logger' -import { setPerfEnabled, enableDebug } from '@craft-agent/shared/utils' -import { initNotificationService, initBadgeIcon, initInstanceBadge, updateBadgeCount } from './notifications' -import { checkForUpdatesOnLaunch, setAutoUpdateEventSink, isUpdating } from './auto-update' -import type { EventSink } from '@craft-agent/server-core/transport' -import { validateGitBashPath, checkVCRedistInstalled } from '@craft-agent/server-core/services' - -// Initialize electron-log for renderer process support -log.initialize() - -// Enable debug/perf in dev mode (running from source) -if (isDebugMode) { - process.env.CRAFT_DEBUG = '1' - enableDebug() - setPerfEnabled(true) -} - -// Bundle CLI tools: resolve platform-specific uv binary and wrapper scripts. -// These are available to all agent Bash sessions via CRAFT_UV, CRAFT_SCRIPTS env vars -// and PATH prepend. uv auto-downloads Python 3.12 on first use (~5s, then cached). -{ - // In packaged app: resources are at process.resourcesPath/app/resources/ - // In dev: resources are at __dirname/../resources/ (sibling of dist/) - const resourcesBase = app.isPackaged - ? join(process.resourcesPath, 'app') - : join(__dirname, '..') - const platformKey = `${process.platform}-${process.arch}` - const uvPlatformDir = join(resourcesBase, 'resources', 'bin', platformKey) - const uvBinary = join(uvPlatformDir, process.platform === 'win32' ? 'uv.exe' : 'uv') - const binDir = join(resourcesBase, 'resources', 'bin') - const scriptsDir = join(resourcesBase, 'resources', 'scripts') - - const bundledUvExists = existsSync(uvBinary) - const fallbackUv = bundledUvExists ? null : 'uv' - - // Runtime resolver hints for shared session tools - process.env.CRAFT_IS_PACKAGED = app.isPackaged ? '1' : '0' - process.env.CRAFT_RESOURCES_BASE = resourcesBase - process.env.CRAFT_APP_ROOT = app.isPackaged ? app.getAppPath() : process.cwd() - - process.env.CRAFT_UV = bundledUvExists ? uvBinary : (fallbackUv ?? uvBinary) - - // Bun runtime (packaged builds should prefer bundled runtime over PATH) - const bunBinary = join(resourcesBase, 'vendor', 'bun', process.platform === 'win32' ? 'bun.exe' : 'bun') - if (existsSync(bunBinary)) { - process.env.CRAFT_BUN = bunBinary - } - - process.env.CRAFT_SCRIPTS = scriptsDir - process.env.CRAFT_COMMANDS_ENTRY = app.isPackaged - ? join(app.getAppPath(), 'packages', 'craft-agents-commands', 'src', 'main.ts') - : join(process.cwd(), 'packages', 'craft-agents-commands', 'src', 'main.ts') - process.env.CRAFT_CLI_ENTRY = app.isPackaged - ? join(app.getAppPath(), 'packages', 'craft-cli', 'src', 'cli.ts') - : join(process.cwd(), 'packages', 'craft-cli', 'src', 'cli.ts') - process.env.CRAFT_COMMANDS_DOC_PATH = app.isPackaged - ? join(resourcesBase, 'resources', 'docs', 'craft-cli.md') - : join(process.cwd(), 'apps', 'electron', 'resources', 'docs', 'craft-cli.md') - process.env.CRAFT_CLI_DOC_PATH = process.env.CRAFT_COMMANDS_DOC_PATH - process.env.CRAFT_AGENT_VERSION = app.getVersion() - // Prepend both generic wrappers dir and platform uv dir: - // - binDir exposes wrapper commands (pdf-tool, docx-tool, ...) - // - uvPlatformDir exposes raw `uv` for direct shell usage / debugging - process.env.PATH = `${binDir}${delimiter}${uvPlatformDir}${delimiter}${process.env.PATH}` - - if (!bundledUvExists) { - mainLog.warn('Bundled uv binary missing, CLI document tools may fail unless uv is available on PATH.', { - expectedUvPath: uvBinary, - usingCraftUv: process.env.CRAFT_UV, - }) - } - - if (isDebugMode) { - mainLog.info('CLI tools configured:', { uvBinary: process.env.CRAFT_UV, binDir, scriptsDir, bundledUvExists }) - } -} - -// Custom URL scheme for deeplinks (e.g., craftagents://auth-complete) -// Supports multi-instance dev: CRAFT_DEEPLINK_SCHEME env var (craftagents1, craftagents2, etc.) -const DEEPLINK_SCHEME = process.env.CRAFT_DEEPLINK_SCHEME || 'craftagents' - -let windowManager: WindowManager | null = null -let sessionManager: SessionManager | null = null -let browserPaneManager: BrowserPaneManager | null = null -let oauthFlowStore: OAuthFlowStore | null = null -let moduleSink: EventSink | null = null -let moduleClientResolver: ((webContentsId: number) => string | undefined) | null = null -let voiceServer: VoiceServer | null = null -let voiceStreamUrl: string | null = null - -// The renderer dev origin and frame-trust gate are extracted to ./voice/frame-trust -// (pure, Electron-free) so the gate guarding the voice token is unit-testable. -// Wrap them here to keep the existing call sites reading process.env directly. -function getRendererDevOrigin(): string | undefined { - return deriveRendererDevOrigin(process.env.VITE_DEV_SERVER_URL) -} - -// Messaging gateway: the bootstrap handle is created once sessionManager is -// available (inside createHandlerDeps) and populated with the WS publisher -// after bootstrapServer resolves. Both hosts (Electron + standalone) wire -// through createMessagingBootstrap — do not construct MessagingGatewayRegistry -// directly. -let messagingHandle: MessagingBootstrapHandle | null = null - -// Store pending deep link if app not ready yet (cold start) -let pendingDeepLink: string | null = null - -// Set app name early (before app.whenReady) to ensure correct macOS menu bar title -// Supports multi-instance dev: CRAFT_APP_NAME env var (e.g., "Qwen Code [1]") -import { BRAND } from '@craft-agent/shared/branding' -app.setName(process.env.CRAFT_APP_NAME || BRAND.appName) - -// Register as default protocol client for craftagents:// URLs -// This must be done before app.whenReady() on some platforms -if (process.defaultApp) { - // Development mode: need to pass the app path - if (process.argv.length >= 2) { - app.setAsDefaultProtocolClient(DEEPLINK_SCHEME, process.execPath, [process.argv[1]]) - } -} else { - // Production mode - app.setAsDefaultProtocolClient(DEEPLINK_SCHEME) -} - -// Apply network proxy settings early (Node-level only — Electron sessions require app.whenReady) -import { applyConfiguredProxySettings } from './network-proxy' -void applyConfiguredProxySettings() - -// Accept self-signed / untrusted certificates when connecting to a user-configured remote server. -// Only bypasses cert validation for the exact CRAFT_SERVER_URL origin — all other connections -// use standard certificate verification. Without this, wss:// to self-signed servers fails with -// ERR_CERT_AUTHORITY_INVALID because Chromium's WebSocket rejects untrusted certs. -// -// Electron's certificate-error always reports URLs with https:// scheme, so we normalize -// wss:// → https:// (and ws:// → http://) to ensure origins compare correctly. -function normalizeOriginForCert(urlStr: string): string { - const u = new URL(urlStr) - if (u.protocol === 'wss:') u.protocol = 'https:' - else if (u.protocol === 'ws:') u.protocol = 'http:' - return u.origin -} - -if (process.env.CRAFT_SERVER_URL) { - let serverOrigin: string | undefined - try { - serverOrigin = normalizeOriginForCert(process.env.CRAFT_SERVER_URL) - } catch { - // Invalid URL — will fail later during connection, no need to handle here - } - if (serverOrigin) { - app.on('certificate-error', (event, _webContents, url, _error, _certificate, callback) => { - try { - if (normalizeOriginForCert(url) === serverOrigin) { - event.preventDefault() - callback(true) - return - } - } catch { - // URL parse failure — fall through to default rejection - } - callback(false) - }) - } -} - -// Register thumbnail:// custom protocol for file preview thumbnails in the sidebar. -// Must happen before app.whenReady() — Electron requires early scheme registration. -registerThumbnailScheme() - -// Handle deeplink on macOS (when app is already running) -app.on('open-url', (event, url) => { - event.preventDefault() - mainLog.info('Received deeplink:', url) - - if (windowManager) { - handleDeepLink(url, windowManager, moduleSink ?? undefined, moduleClientResolver ?? undefined).catch(err => { - mainLog.error('Failed to handle deep link:', err) - }) - } else { - // App not ready - store for later - pendingDeepLink = url - } -}) - -// Handle deeplink on Windows/Linux (single instance check) -const gotTheLock = app.requestSingleInstanceLock() -if (!gotTheLock) { - mainLog.warn('Another Qwen Code instance already owns the Electron single-instance lock; quitting.', { - userData: app.getPath('userData'), - appName: app.getName(), - }) - app.quit() -} else { - app.on('second-instance', (_event, commandLine, _workingDirectory) => { - // Someone tried to run a second instance, we should focus our window. - // On Windows/Linux, the deeplink is in commandLine - const url = commandLine.find(arg => arg.startsWith(`${DEEPLINK_SCHEME}://`)) - if (url && windowManager) { - mainLog.info('Received deeplink from second instance:', url) - handleDeepLink(url, windowManager, moduleSink ?? undefined, moduleClientResolver ?? undefined).catch(err => { - mainLog.error('Failed to handle deep link:', err) - }) - } else if (windowManager) { - // No deep link - just focus the first window - const windows = windowManager.getAllWindows() - if (windows.length > 0) { - const win = windows[0].window - if (win.isMinimized()) win.restore() - win.focus() - } - } - }) -} - -// Helper to create initial windows on startup -async function createInitialWindows(options: { createDefaultWorkspace: boolean }): Promise { - if (!windowManager) return - - // Load saved window state - const savedState = loadWindowState() - let workspaces = getWorkspaces() - - // If no workspaces exist, create the protected default conversation entry on first run. - // Thin clients intentionally skip this so the renderer can show the remote picker. - if (workspaces.length === 0) { - if (!options.createDefaultWorkspace) { - windowManager.createWindow({ workspaceId: '' }) - mainLog.info('Created workspace-picker window without a local workspace') - return - } - - ensureDefaultConversationWorkspace() - workspaces = getWorkspaces() // Refresh after creation - mainLog.info('Created default conversation workspace on first run') - } - - const validWorkspaceIds = workspaces.map(ws => ws.id) - - const startupWorkspaceId = [ - savedState?.lastFocusedWorkspaceId, - ...(savedState?.windows.map(saved => saved.workspaceId) ?? []), - ].find(id => !!id && validWorkspaceIds.includes(id)) - - // Default: open one fresh window in the last focused workspace. - const startupWorkspace = workspaces.find(ws => ws.id === startupWorkspaceId) ?? workspaces[0] - if (!startupWorkspace) { - windowManager.createWindow({ workspaceId: '' }) - mainLog.info('Created workspace-picker window because no workspaces are available') - return - } - - windowManager.createWindow({ workspaceId: startupWorkspace.id }) - mainLog.info(`Created startup window for workspace: ${startupWorkspace.name}`) -} - -app.whenReady().then(async () => { - // Export packaged state as env var so logger.ts (and headless Bun) don't need 'electron' - process.env.CRAFT_IS_PACKAGED = app.isPackaged ? 'true' : 'false' - - // Set About panel info (shown via "About " on macOS) - // Detailed credits live in the custom AboutDialog (Help → About) - app.setAboutPanelOptions({ - applicationName: BRAND.appName, - applicationVersion: app.getVersion(), - copyright: BRAND.copyright, - ...(BRAND.creditsShort ? { credits: BRAND.creditsShort } : {}), - }) - - // Register bundled assets root so all seeding functions can find their files - // (docs, permissions, themes, tool-icons resolve via getBundledAssetsDir) - setBundledAssetsRoot(__dirname) - - // Initialize backend runtime bootstrapping. - initializeBackendHostRuntime({ - hostRuntime: { - appRootPath: app.isPackaged ? app.getAppPath() : process.cwd(), - resourcesPath: process.resourcesPath, - isPackaged: app.isPackaged, - }, - }) - - // Register PowerShell validator root so it can find the bundled parser script - // (Windows only: validates PowerShell commands in Plan mode using AST analysis) - setPowerShellValidatorRoot(join(__dirname, 'resources')) - - // Initialize bundled docs - initializeDocs() - - // Initialize bundled release notes - initializeReleaseNotes() - - // Ensure default permissions file exists (copies bundled default.json on first run) - ensureDefaultPermissions() - - // Seed tool icons to ~/.craft-agent/tool-icons/ (copies bundled SVGs on first run) - ensureToolIcons() - - // Seed preset themes to ~/.craft-agent/themes/ (copies bundled theme JSONs on first run) - ensurePresetThemes() - - // Register thumbnail:// protocol handler (scheme was registered earlier, before app.whenReady) - registerThumbnailHandler() - - // Grant microphone access for the trusted main-window UI (voice dictation). - // The main window uses the default session (browser panes have their own - // partition + handler), so without this getUserMedia is blocked. Scope the - // grant to mic/media only — do NOT broaden the default session to every - // permission (geolocation, HID, serial, …). - const VOICE_PERMISSIONS = new Set(['media', 'audioCapture']) - const getMediaTypes = (details: unknown): string[] => { - if (!details || typeof details !== 'object') return [] - const mediaDetails = details as { - mediaTypes?: unknown - mediaType?: unknown - } - if (Array.isArray(mediaDetails.mediaTypes)) { - return mediaDetails.mediaTypes.filter( - (mediaType): mediaType is string => typeof mediaType === 'string', - ) - } - return typeof mediaDetails.mediaType === 'string' - ? [mediaDetails.mediaType] - : [] - } - const isAudioOnlyMediaRequest = ( - permission: string, - details?: unknown, - ) => { - if (permission === 'audioCapture') return true - if (permission !== 'media') return false - const mediaTypes = getMediaTypes(details) - return ( - mediaTypes.length > 0 && - mediaTypes.every((mediaType) => mediaType === 'audio') - ) - } - const canUseVoicePermission = ( - wc: { id: number } | null | undefined, - permission: string, - details?: unknown, - ) => Boolean( - wc && - getVoiceEnabled() && - VOICE_PERMISSIONS.has(permission) && - isAudioOnlyMediaRequest(permission, details) && - windowManager?.getWorkspaceForWindow(wc.id) != null, - ) - session.defaultSession.setPermissionRequestHandler( - (wc, permission, callback, details) => { - if (!VOICE_PERMISSIONS.has(permission)) { - mainLog.debug(`defaultSession: denied non-voice permission '${permission}'`) - callback(false) - return - } - const allowed = canUseVoicePermission(wc, permission, details) - if (!allowed) { - mainLog.debug(`defaultSession: denied permission '${permission}'`) - } - callback(allowed) - }, - ) - session.defaultSession.setPermissionCheckHandler((wc, permission, _origin, details) => { - if (!VOICE_PERMISSIONS.has(permission)) { - mainLog.debug(`defaultSession: denied non-voice permission check '${permission}'`) - return false - } - const allowed = canUseVoicePermission(wc, permission, details) - if (!allowed) { - mainLog.debug(`defaultSession: denied permission check '${permission}'`) - } - return allowed - }) - - // Re-apply proxy settings now that Electron sessions are available - // (first call before app.whenReady only configured Node-level proxy) - await applyConfiguredProxySettings() - - // Note: electron-updater handles pending updates internally via autoInstallOnAppQuit - - // Application menu is created after windowManager initialization (see below) - - // Set dock icon on macOS in dev mode; packaged apps use Info.plist/.icns. - if (process.platform === 'darwin' && app.dock && !app.isPackaged) { - // In dev, resources are at ../resources/ (sibling of dist/) - const brandIconRelPath = BRAND.assets.devDockIcon - if (brandIconRelPath) { - const dockIconPath = [ - join(__dirname, brandIconRelPath), - join(__dirname, '..', brandIconRelPath), - ].find(p => existsSync(p)) - - if (dockIconPath) { - const dockIcon = nativeImage.createFromPath(dockIconPath) - if (!dockIcon.isEmpty()) { - app.dock.setIcon(dockIcon) - // Initialize badge icon for canvas-based badge overlay - initBadgeIcon(dockIconPath) - } - } - } - - // Multi-instance dev: show instance number badge on dock icon - // CRAFT_INSTANCE_NUMBER is set by detect-instance.sh for numbered folders - const instanceNum = process.env.CRAFT_INSTANCE_NUMBER - if (instanceNum) { - const num = parseInt(instanceNum, 10) - if (!isNaN(num) && num > 0) { - initInstanceBadge(num) - } - } - } - - try { - // Initialize window manager - windowManager = new WindowManager() - - // Create the application menu (needs windowManager for New Window action) - createApplicationMenu(windowManager) - - // When CRAFT_SERVER_URL is set, this Electron instance is a thin client — - // it only creates windows whose preload connects to the remote server. - // Skip server-side initialization (SessionManager, model refresh, platform injection). - const isClientOnly = !!process.env.CRAFT_SERVER_URL - const isHeadless = !!process.env.CRAFT_HEADLESS - - if (isClientOnly) { - mainLog.info(`Client-only mode: CRAFT_SERVER_URL=${process.env.CRAFT_SERVER_URL} (server initialization skipped)`) - } - - // Initialize notification service (always — triggered by server push events) - initNotificationService(windowManager) - - // Initialize browser pane manager (always — even in headless, for deps wiring) - browserPaneManager = new BrowserPaneManager() - browserPaneManager.setWindowManager(windowManager) - browserPaneManager.registerToolbarIpc() - - // Build real PlatformServices from Electron APIs - const platform: PlatformServices = createElectronPlatform({ - app, - nativeImage, - shell, - nativeTheme, - logger: log, - isDebugMode, - getLogFilePath, - captureError: (err) => Sentry.captureException(err), - }) - - // Bootstrap IPC handlers — preload uses sendSync for window-local details - ipcMain.on('__get-web-contents-id', (e) => { - e.returnValue = e.sender.id - }) - ipcMain.on('__get-workspace-id', (e) => { - e.returnValue = windowManager?.getWorkspaceForWindow(e.sender.id) ?? '' - }) - - // Transport diagnostics bridge — preload reports remote WS connection state changes - // so failures are visible in terminal/main.log (not only renderer console). - ipcMain.on('__transport:status', (_event, payload: unknown) => { - if (!payload || typeof payload !== 'object') return - const p = payload as { - level?: 'info' | 'warn' | 'error' - message?: string - status?: string - attempt?: number - nextRetryInMs?: number - error?: unknown - close?: unknown - url?: string - } - - const level = p.level ?? 'info' - const message = p.message ?? '[transport] status update' - const context = { - status: p.status, - attempt: p.attempt, - nextRetryInMs: p.nextRetryInMs, - error: p.error, - close: p.close, - url: p.url, - } - - if (level === 'error') { - mainLog.error(message, context) - } else if (level === 'warn') { - mainLog.warn(message, context) - } else { - mainLog.info(message, context) - } - }) - - // Dialog bridge — preload capability handlers use ipcRenderer.invoke to - // call main-process-only dialog APIs (dialog, BrowserWindow). - ipcMain.handle('__dialog:showMessageBox', async (event, spec) => { - const win = BrowserWindow.fromWebContents(event.sender) - || BrowserWindow.getFocusedWindow() - || BrowserWindow.getAllWindows()[0] - const result = await dialog.showMessageBox(win, spec) - return { response: result.response } - }) - ipcMain.handle('__dialog:showOpenDialog', async (event, spec) => { - const win = BrowserWindow.fromWebContents(event.sender) - || BrowserWindow.getFocusedWindow() - || BrowserWindow.getAllWindows()[0] - const result = await dialog.showOpenDialog(win, spec) - return { canceled: result.canceled, filePaths: result.filePaths } - }) - - if (!isClientOnly) { - // Restore persisted Git Bash path on Windows (must happen before any SDK subprocess spawn) - if (process.platform === 'win32') { - const { getGitBashPath, clearGitBashPath } = await import('@craft-agent/shared/config') - const gitBashPath = getGitBashPath() - if (gitBashPath) { - const validation = await validateGitBashPath(gitBashPath) - if (validation.valid) { - process.env.QWEN_CODE_GIT_BASH_PATH = validation.path - } else { - clearGitBashPath() - delete process.env.QWEN_CODE_GIT_BASH_PATH - mainLog.warn(`Cleared invalid persisted Git Bash path: ${gitBashPath}`) - } - } - } - - // Check for VC++ Redistributable on Windows (required by onnxruntime / markitdown). - // Without it, document conversion tools (PDF, PPTX, DOCX, XLSX) crash with DLL errors. - // Sets env var so renderer can show an actionable toast with install button. - if (process.platform === 'win32') { - const vcCheck = checkVCRedistInstalled() - if (!vcCheck.installed) { - mainLog.warn('[vcredist]', vcCheck.message) - process.env.CRAFT_VCREDIST_MISSING = '1' - if (vcCheck.downloadUrl) { - process.env.CRAFT_VCREDIST_URL = vcCheck.downloadUrl - } - } else if (isDebugMode) { - mainLog.info('[vcredist]', vcCheck.message) - } - } - - // Pre-import power manager (async import needed for applyPlatformToSubsystems) - const { onSessionStarted, onSessionStopped } = await import('./power-manager') - - // Client ID tracking for Electron IPC bridge (webContentsId → clientId) - const clientMap = new Map() - const resolveClientId = (wcId: number) => clientMap.get(wcId) - - // Read embedded server config (Server settings page) - const { getServerConfig } = await import('@craft-agent/shared/config') - const embeddedServerConfig = getServerConfig() - const serverModeEnabled = embeddedServerConfig.enabled && !isClientOnly - - // Derive host/port/token from server config (or env overrides) - const serverToken = serverModeEnabled && embeddedServerConfig.token - ? embeddedServerConfig.token - : randomUUID() - const rpcHost = process.env.CRAFT_RPC_HOST - ?? (serverModeEnabled ? '0.0.0.0' : '127.0.0.1') - const envRpcPort = process.env.CRAFT_RPC_PORT - const rpcPort = envRpcPort - ? parseServerPort('CRAFT_RPC_PORT', envRpcPort, 9100) - : (serverModeEnabled ? embeddedServerConfig.port : 0) - - // Load TLS certificates if configured - let tls: import('@craft-agent/server-core/transport').WsRpcTlsOptions | undefined - if (serverModeEnabled && embeddedServerConfig.tlsCertPath && embeddedServerConfig.tlsKeyPath) { - try { - tls = { - cert: readFileSync(embeddedServerConfig.tlsCertPath), - key: readFileSync(embeddedServerConfig.tlsKeyPath), - } - mainLog.info('[server-mode] TLS enabled') - } catch (err) { - mainLog.error('[server-mode] Failed to load TLS certificates:', err) - } - } - - if (serverModeEnabled) { - mainLog.info(`[server-mode] Enabled — binding ${rpcHost}:${rpcPort}${tls ? ' (TLS)' : ''}`) - } - - // Bootstrap the WS RPC server via shared bootstrap function. - const instance = await bootstrapServer({ - serverToken, - rpcHost, - rpcPort, - tls, - bundledAssetsRoot: __dirname, - serverId: 'local', - serverVersion: app.getVersion(), - platformFactory: () => platform, - applyPlatformToSubsystems: (p) => { - setFetcherPlatform(p) - setSessionPlatform(p) - setSessionRuntimeHooks({ - updateBadgeCount, - onSessionStarted, - onSessionStopped, - captureException: (error, context) => { - Sentry.captureException(error instanceof Error ? error : new Error(String(error)), { - tags: { - ...(context?.errorSource ? { errorSource: context.errorSource } : {}), - ...(context?.sessionId ? { sessionId: context.sessionId } : {}), - }, - }) - }, - }) - setSearchPlatform(p) - setImageProcessor(p.imageProcessor) - }, - createSessionManager: () => { - const sm = new SessionManager() - sm.setBrowserPaneManager(browserPaneManager!) - return sm - }, - createHandlerDeps: ({ sessionManager: sm, platform: p, oauthFlowStore: ofs }) => { - // The messaging handle is built here because it needs sessionManager. - // The WS publisher is attached after bootstrapServer resolves (via - // handle.setPublisher) because wsServer isn't available yet. - messagingHandle = createMessagingBootstrap({ - sessionManager: sm, - credentialManager: getCredentialManager(), - getMessagingDir: (wsId: string) => - join(homedir(), '.craft-agent', 'workspaces', wsId, 'messaging'), - getLegacyMessagingDir: (wsId: string) => { - const ws = getWorkspaces().find((w) => w.id === wsId) - return ws ? join(ws.rootPath, 'messaging') : undefined - }, - // Route messaging diagnostics through the dedicated messaging log - // at ~/.craft-agent/logs/messaging-gateway.log. - logger: messagingGatewayLog, - // WhatsApp worker runs under Electron's embedded Node via - // ELECTRON_RUN_AS_NODE (WhatsAppAdapter defaults nodeBin to - // process.execPath). In dev we resolve worker.cjs from the - // monorepo; in packaged builds it's shipped via extraResources - // (see apps/electron/electron-builder.yml). - whatsapp: { - workerEntry: app.isPackaged - ? join(process.resourcesPath, 'messaging-whatsapp-worker', 'worker.cjs') - : join(process.cwd(), 'packages', 'messaging-whatsapp-worker', 'dist', 'worker.cjs'), - pairingMode: 'qr', - }, - }) - return { - sessionManager: sm, - platform: p, - windowManager: windowManager ?? undefined, - browserPaneManager: browserPaneManager ?? undefined, - oauthFlowStore: ofs, - messagingRegistry: messagingHandle.registry, - } - }, - // Headless: register only core handlers (no GUI handlers for browser, settings, etc.) - // GUI: register all handlers (core + GUI) - registerAllRpcHandlers: isHeadless - ? (server, deps, serverCtx) => registerCoreRpcHandlers(server, deps, serverCtx) - : registerAllRpcHandlers, - setSessionEventSink: (sm, sink) => sm.setEventSink(sink), - initializeSessionManager: (sm) => sm.initialize(), - initModelRefreshService: () => initModelRefreshService(async (slug: string) => { - const { getCredentialManager } = await import('@craft-agent/shared/credentials') - const manager = getCredentialManager() - const [apiKey, oauth] = await Promise.all([ - manager.getLlmApiKey(slug).catch(() => null), - manager.getLlmOAuth(slug).catch(() => null), - ]) - return { - apiKey: apiKey ?? undefined, - oauthAccessToken: oauth?.accessToken, - oauthRefreshToken: oauth?.refreshToken, - oauthIdToken: oauth?.idToken, - } - }), - onClientConnected: ({ clientId, webContentsId }) => { - if (webContentsId != null) clientMap.set(webContentsId, clientId) - }, - cleanupClientResources: (clientId) => { - for (const [wcId, cId] of clientMap) { - if (cId === clientId) { clientMap.delete(wcId); break } - } - cleanupSessionFileWatchForClient(clientId) - }, - }) - - // Capture module-level references for before-quit cleanup and deep-link handlers - sessionManager = instance.sessionManager - oauthFlowStore = instance.oauthFlowStore - moduleSink = instance.wsServer.push.bind(instance.wsServer) - moduleClientResolver = resolveClientId - - // Voice dictation: a separate loopback WS server (raw PCM, no RPC envelope) - // with a voice-scoped token that transcribes via the qwen credentials. - try { - const voiceToken = randomBytes(32).toString('hex') - voiceServer = await startVoiceServer({ - token: voiceToken, - resolveConfig: resolveDesktopVoiceConfig, - allowedOrigins: [getRendererDevOrigin()].filter( - (origin): origin is string => Boolean(origin), - ), - isEnabled: getVoiceEnabled, - logger: platform.logger, - }) - voiceStreamUrl = `${voiceServer.url}?token=${encodeURIComponent(voiceToken)}` - } catch (error) { - mainLog.error('Failed to start voice stream server:', error) - } - - // ----------------------------------------------------------------------- - // Messaging Gateway — attach the WS publisher, init local workspaces, - // install the fan-out event sink. The handle was created inside - // createHandlerDeps so the registry could be wired into HandlerDeps. - // ----------------------------------------------------------------------- - try { - if (!messagingHandle) { - throw new Error('Messaging handle was not constructed in createHandlerDeps') - } - - messagingHandle.setPublisher(instance.wsServer.push.bind(instance.wsServer)) - - // Skip remote-owned workspaces — messaging runs on the remote server. - const localWorkspaceIds = getWorkspaces() - .filter((ws) => !ws.remoteServer) - .map((ws) => ws.id) - await messagingHandle.initializeWorkspaces(localWorkspaceIds) - - // Compose fan-out event sink: RPC push + messaging gateway dispatch. - // Always install — this lets workspaces enable messaging at runtime - // without a process restart. - const baseSink = instance.wsServer.push.bind(instance.wsServer) - instance.sessionManager.setEventSink(messagingHandle.wrapSink(baseSink)) - if (messagingHandle.registry.size > 0) { - mainLog.info(`[messaging] Fan-out sink active for ${messagingHandle.registry.size} workspace(s)`) - } - } catch (err) { - mainLog.error('[messaging] Gateway initialization failed:', err) - } - - // IPC handlers — preload uses sendSync to get WS connection details - - // Remove workspace from config (cleanup stale entries) - ipcMain.handle('workspace:remove', async (_event, workspaceId: string) => { - const { getWorkspaceByNameOrId, isProtectedWorkspace, removeWorkspace: remove } = await import('@craft-agent/shared/config') - const workspace = getWorkspaceByNameOrId(workspaceId) - if (isProtectedWorkspace(workspace)) return false - return remove(workspaceId) - }) - - // Persist local project-list pinning even when the active workspace is remote. - ipcMain.handle('workspace:pinned:set', async (_event, workspaceId: string, pinned: boolean) => { - const { getWorkspaceByNameOrId } = await import('@craft-agent/shared/config') - const { loadWorkspaceConfig, saveWorkspaceConfig } = await import('@craft-agent/shared/workspaces') - const workspace = getWorkspaceByNameOrId(workspaceId) - if (!workspace) return false - if (isProtectedWorkspace(workspace)) return false - - const config = loadWorkspaceConfig(workspace.rootPath) - if (!config) return false - - config.pinned = Boolean(pinned) - saveWorkspaceConfig(workspace.rootPath, config) - return true - }) - - // Persist local project-list ordering even when the active workspace is remote. - ipcMain.handle('workspace:reorder', async (_event, orderedIds: string[]) => { - const { reorderWorkspaces } = await import('@craft-agent/shared/config') - return reorderWorkspaces(orderedIds) - }) - - // Cross-server RPC — invoke a channel on an arbitrary remote server - ipcMain.handle('server:invokeOnServer', async (_event, url: string, token: string, channel: string, ...args: unknown[]) => { - const { connectToRemote } = await import('./handlers/workspace') - const { client, error } = await connectToRemote(url, token) - if (!client) throw new Error(error ?? 'Connection failed') - try { - return await client.invoke(channel, ...args) - } finally { - client.destroy() - } - }) - - // Transfer session to another workspace — orchestrated in main process - // so large bundles can be moved directly between owning servers. - ipcMain.handle('session:transferToRemoteWorkspace', async (_event, sessionId: string, targetWorkspaceId: string, sessionIndex?: number, sessionCount?: number) => { - const idx = sessionIndex ?? 0 - const count = sessionCount ?? 1 - const { getWorkspaceByNameOrId } = await import('@craft-agent/shared/config') - const { connectToRemote } = await import('./handlers/workspace') - const { CHUNKED_TRANSFER_THRESHOLD, getChunkCount, invokeChunked, prepareChunkedPayload } = await import('./chunked-rpc') - - const targetWorkspace = getWorkspaceByNameOrId(targetWorkspaceId) - if (!targetWorkspace?.remoteServer) throw new Error(`Workspace ${targetWorkspaceId} has no remote server`) - if (!sessionManager) throw new Error('Session manager not initialized') - - const sourceWorkspaceLocalId = windowManager?.getWorkspaceForWindow(_event.sender.id) - if (!sourceWorkspaceLocalId) throw new Error('Unable to resolve source workspace for transfer') - - const sourceWorkspace = getWorkspaceByNameOrId(sourceWorkspaceLocalId) - if (!sourceWorkspace) throw new Error(`Source workspace ${sourceWorkspaceLocalId} not found`) - - let bundle: any = null - - if (sourceWorkspace.remoteServer) { - const { url: sourceUrl, token: sourceToken, remoteWorkspaceId: sourceRemoteWorkspaceId } = sourceWorkspace.remoteServer - console.log(`[Transfer] Exporting remote-owned session ${sessionId} from workspace ${sourceRemoteWorkspaceId}...`) - const { client: sourceClient, error: sourceError } = await connectToRemote(sourceUrl, sourceToken, sourceRemoteWorkspaceId) - if (!sourceClient) throw new Error(sourceError ?? 'Connection failed to source remote server') - - try { - bundle = await sourceClient.invoke('sessions:export', sessionId) - if (!bundle) throw new Error(`Failed to export session ${sessionId}`) - - try { - console.log('[Transfer] Generating conversation summary on source server...') - const transferPayload = await sourceClient.invoke('sessions:exportRemoteTransfer', sessionId) - if (transferPayload?.summary && bundle.session?.header) { - ;(bundle.session.header as any).transferredSessionSummary = transferPayload.summary - ;(bundle.session.header as any).transferredSessionSummaryApplied = false - console.log(`[Transfer] Summary generated: ${transferPayload.summary.length} chars`) - } - } catch (err) { - console.warn('[Transfer] Source-server summary generation failed:', err) - } - } finally { - sourceClient.destroy() - } - } else { - console.log(`[Transfer] Exporting local-owned session ${sessionId} from workspace ${sourceWorkspace.id}...`) - bundle = await sessionManager.exportSession(sessionId, sourceWorkspace.id) - if (!bundle) throw new Error(`Failed to export session ${sessionId}`) - - try { - console.log('[Transfer] Generating conversation summary...') - const transferPayload = await sessionManager.exportRemoteSessionTransfer(sessionId, sourceWorkspace.id) - if (transferPayload?.summary && bundle.session?.header) { - ;(bundle.session.header as any).transferredSessionSummary = transferPayload.summary - ;(bundle.session.header as any).transferredSessionSummaryApplied = false - console.log(`[Transfer] Summary generated: ${transferPayload.summary.length} chars`) - } - } catch (err) { - console.warn('[Transfer] Summary generation failed:', err) - } - } - - console.log(`[Transfer] Export complete: ${bundle.session?.messages?.length ?? 0} messages, ${bundle.files?.length ?? 0} files`) - - const { url, token, remoteWorkspaceId } = targetWorkspace.remoteServer - console.log(`[Transfer] Connecting to target remote server: ${url}`) - const { client, error } = await connectToRemote(url, token, remoteWorkspaceId) - if (!client) throw new Error(error ?? 'Connection failed to target remote server') - console.log('[Transfer] Connected to target remote server') - - try { - const preparedBundle = prepareChunkedPayload(bundle) - const payloadSize = preparedBundle.bytes.length - const payloadMB = (payloadSize / (1024 * 1024)).toFixed(1) - - const emitProgress = (chunkSent: number, chunkTotal: number) => { - try { _event.sender.send('transfer:progress', { sessionIndex: idx, sessionCount: count, chunkSent, chunkTotal }) } catch { /* renderer may be gone */ } - } - - if (payloadSize < CHUNKED_TRANSFER_THRESHOLD) { - console.log(`[Transfer] Bundle size: ${payloadMB}MB (< 5MB threshold) → using direct RPC`) - emitProgress(0, 1) - const result = await client.invoke('sessions:import', remoteWorkspaceId, bundle, 'fork') - emitProgress(1, 1) - return result - } - - const chunkCount = getChunkCount(payloadSize) - console.log(`[Transfer] Bundle size: ${payloadMB}MB (>= 5MB threshold) → using chunked transfer (${chunkCount} chunks)`) - return await invokeChunked( - client, - 'sessions:import', - [remoteWorkspaceId, bundle, 'fork'], - 1, - emitProgress, - preparedBundle, - ) - } finally { - client.destroy() - } - }) - - // App relaunch (for server config changes — NOT an update install) - ipcMain.handle('app:relaunch', () => { - app.relaunch() - app.exit(0) - }) - - // Language change: sync from renderer to main process and rebuild native menu - ipcMain.handle('i18n:changeLanguage', async (_event, lang: string) => { - i18n.changeLanguage(lang) - const { rebuildMenu } = await import('./menu') - await rebuildMenu() - }) - - ipcMain.on('__get-ws-port', (e) => { - e.returnValue = instance.port - }) - ipcMain.on('__get-ws-token', (e) => { - e.returnValue = instance.token - }) - ipcMain.on('__get-voice-stream-url', (e) => { - // The voice WS URL embeds the loopback auth token, so only hand it to - // the app's own top-level renderer frame. Reject sub-frames (injected / - // cross-origin iframes) and any frame not loaded from our renderer - // origin so the token can't be exfiltrated. - const frame = e.senderFrame - if ( - !frame || - frame !== e.sender.mainFrame || - !isTrustedRendererFrameUrl(frame.url, process.env.VITE_DEV_SERVER_URL) - ) { - e.returnValue = null - return - } - e.returnValue = getVoiceEnabled() ? voiceStreamUrl : null - }) - ipcMain.on('__get-workspace-remote-config', (e) => { - const wsId = windowManager?.getWorkspaceForWindow(e.sender.id) - if (!wsId) { e.returnValue = null; return } - const ws = getWorkspaceByNameOrId(wsId) - e.returnValue = ws?.remoteServer ?? null - }) - - // Server config RPC handlers (LOCAL_ONLY — Electron-specific) - const runningServerState = { - host: rpcHost, - port: instance.port, - tls: !!tls, - token: serverToken, - enabled: serverModeEnabled, - } - - instance.wsServer.handle(RPC_CHANNELS.settings.GET_SERVER_CONFIG, async () => { - const { getServerConfig: getConfig } = await import('@craft-agent/shared/config') - return getConfig() - }) - - instance.wsServer.handle(RPC_CHANNELS.settings.SET_SERVER_CONFIG, async (_ctx: unknown, config: unknown) => { - const { setServerConfig: setConfig } = await import('@craft-agent/shared/config') - const cfg = config as import('@craft-agent/shared/config/server-config').ServerConfig - // Validate port range - if (cfg.port < 1024 || cfg.port > 65535) { - throw new Error(`Port must be between 1024 and 65535, got ${cfg.port}`) - } - // Validate cert/key files exist if provided - if (cfg.tlsCertPath && !existsSync(cfg.tlsCertPath)) { - throw new Error(`Certificate file not found: ${cfg.tlsCertPath}`) - } - if (cfg.tlsKeyPath && !existsSync(cfg.tlsKeyPath)) { - throw new Error(`Private key file not found: ${cfg.tlsKeyPath}`) - } - setConfig(cfg) - }) - - instance.wsServer.handle(RPC_CHANNELS.settings.GET_SERVER_STATUS, async () => { - const { getServerConfig: getConfig } = await import('@craft-agent/shared/config') - const saved = getConfig() - const protocol = runningServerState.tls ? 'wss' : 'ws' - - // Determine display host (LAN IP if bound to 0.0.0.0) - let displayHost = runningServerState.host - if (displayHost === '0.0.0.0' || displayHost === '::') { - const os = await import('os') - const nets = os.networkInterfaces() - for (const name of Object.keys(nets)) { - for (const net of nets[name] ?? []) { - if (net.family === 'IPv4' && !net.internal) { - displayHost = net.address - break - } - } - if (displayHost !== '0.0.0.0' && displayHost !== '::') break - } - } - - // Only compare port/tls/token when at least one side has server mode enabled. - // When both are disabled, the running port is random — comparing it to the - // saved default (9100) would always produce a false "restart required" banner. - const needsRestart = saved.enabled !== runningServerState.enabled - || ((saved.enabled || runningServerState.enabled) && ( - saved.port !== runningServerState.port - || (!!saved.tlsCertPath) !== runningServerState.tls - || (saved.token ?? '') !== runningServerState.token - )) - - return { - running: true, - host: runningServerState.host, - port: runningServerState.port, - tls: runningServerState.tls, - url: `${protocol}://${displayHost}:${runningServerState.port}`, - token: runningServerState.token, - needsRestart, - insecureWarning: isInsecureBind, - } - }) - - // TLS enforcement — warn when server mode binds to a network address without TLS - // Mirrors the hard guard in packages/server/src/index.ts but warns instead of blocking, - // since the user explicitly enabled server mode via UI (may be on a trusted LAN). - const isInsecureBind = serverModeEnabled && !tls - && !['127.0.0.1', 'localhost', '::1'].includes(rpcHost) - if (isInsecureBind) { - mainLog.warn( - '[server-mode] WARNING: Listening on a network address without TLS. ' + - 'Auth tokens will be sent in cleartext. ' + - 'Configure TLS certificates in Settings > Server.' - ) - } - - // Wire EventSink to Electron-specific services - // Must happen BEFORE createInitialWindows() so event handlers use WS from the start - windowManager.setRpcEventSink(moduleSink!, resolveClientId) - const { setMenuEventSink } = await import('./menu') - setMenuEventSink(moduleSink!, resolveClientId) - const { setNotificationEventSink } = await import('./notifications') - setNotificationEventSink(moduleSink!, resolveClientId) - - // Headless: print connection details - if (isHeadless) { - console.log(`CRAFT_SERVER_URL=${instance.protocol}://${instance.host}:${instance.port}`) - console.log(`CRAFT_SERVER_TOKEN=${instance.token}`) - } - } - - // Create initial windows (restores from saved state or opens first workspace) - // In headless mode the server runs without any UI — skip window creation. - if (!isHeadless) { - await createInitialWindows({ createDefaultWorkspace: !isClientOnly }) - } - - // Run credential health check at startup to detect issues early - // (corruption, machine migration, missing credentials for default connection) - // Skip in thin-client mode — credentials are managed by the remote server. - if (!isClientOnly) { - try { - const { getCredentialManager } = await import('@craft-agent/shared/credentials') - const credentialManager = getCredentialManager() - const health = await credentialManager.checkHealth() - if (!health.healthy) { - mainLog.warn('Credential health check failed:', health.issues) - // Issues will be displayed in Settings → AI when user navigates there - } - } catch (err) { - mainLog.error('Credential health check error:', err) - } - } - - // Initialize power manager (loads setting, must happen after config is available) - // Non-critical — powerSaveBlocker may not work on headless/xvfb setups - try { - const { initPowerManager } = await import('./power-manager') - await initPowerManager() - } catch (err) { - mainLog.warn('[power] Power manager init failed (non-critical):', err instanceof Error ? err.message : err) - } - - // Set Sentry context tags for error grouping (no PII — just config classification). - // Runs after init so config and auth state are available. - // Derives values from the default LLM connection instead of legacy config fields. - try { - const { getLlmConnection, getDefaultLlmConnection } = await import('@craft-agent/shared/config') - const workspaces = getWorkspaces() - const defaultConnSlug = getDefaultLlmConnection() - const defaultConn = defaultConnSlug ? getLlmConnection(defaultConnSlug) : null - Sentry.setTag('authType', defaultConn?.authType ?? 'unknown') - Sentry.setTag('providerType', defaultConn?.providerType ?? 'unknown') - Sentry.setTag('hasCustomEndpoint', 'false') - Sentry.setTag('model', defaultConn?.defaultModel ?? 'default') - Sentry.setTag('workspaceCount', String(workspaces.length)) - } catch (err) { - mainLog.warn('Failed to set Sentry context tags:', err) - } - - // Initialize auto-update (check immediately on launch) - // Skip in dev mode to avoid replacing /Applications app and launching it instead - if (moduleSink) setAutoUpdateEventSink(moduleSink) - if (app.isPackaged) { - checkForUpdatesOnLaunch().catch(err => { - mainLog.error('[auto-update] Launch check failed:', err) - }) - } else { - mainLog.info('[auto-update] Skipping auto-update in dev mode') - } - - // Process pending deep link from cold start - if (pendingDeepLink) { - mainLog.info('Processing pending deep link:', pendingDeepLink) - await handleDeepLink(pendingDeepLink, windowManager, moduleSink ?? undefined, moduleClientResolver ?? undefined) - pendingDeepLink = null - } - - mainLog.info('App initialized successfully') - if (isDebugMode) { - mainLog.info('Debug mode enabled') - } - mainLog.info('Application log path:', getLogFilePath()) - mainLog.info('Messaging gateway log path:', getMessagingGatewayLogFilePath()) - } catch (error) { - mainLog.error('Failed to initialize app:', error instanceof Error ? error.message : error, (error as any)?.stack) - // Continue anyway - the app will show errors in the UI - } - - // macOS: Re-create window when dock icon is clicked - app.on('activate', () => { - if (BrowserWindow.getAllWindows().length === 0 && windowManager) { - // Open first workspace or last focused - const workspaces = getWorkspaces() - if (workspaces.length > 0) { - const savedState = loadWindowState() - const wsId = savedState?.lastFocusedWorkspaceId || workspaces[0].id - // Verify workspace still exists - if (workspaces.some(ws => ws.id === wsId)) { - windowManager.createWindow({ workspaceId: wsId }) - } else { - windowManager.createWindow({ workspaceId: workspaces[0].id }) - } - } - } - }) -}) - -app.on('window-all-closed', () => { - if (process.env.CRAFT_HEADLESS) return // headless server stays alive - // On macOS, apps typically stay active until explicitly quit - if (process.platform !== 'darwin') { - app.quit() - } -}) - -// Track if we're in the process of quitting (to avoid re-entry) -let isQuitting = false - -// Save window state and clean up resources before quitting -app.on('before-quit', async (event) => { - // Avoid re-entry when we call app.exit() - if (isQuitting) return - isQuitting = true - - // Ensure Cmd+Q/app quit bypasses layered window close interception (Cmd+W behavior). - windowManager?.setAppQuitting(true) - - if (windowManager) { - // Get full window states (includes bounds, type, and query) - const windows = windowManager.getWindowStates() - // Get the focused window's workspace as last focused - const focusedWindow = BrowserWindow.getFocusedWindow() - let lastFocusedWorkspaceId: string | undefined - if (focusedWindow) { - lastFocusedWorkspaceId = windowManager.getWorkspaceForWindow(focusedWindow.webContents.id) ?? undefined - } - - saveWindowState({ - windows, - lastFocusedWorkspaceId, - }) - mainLog.info('Saved window state:', windows.length, 'windows') - } - - // Flush all pending session writes before quitting - if (sessionManager) { - // Prevent quit until sessions are flushed - event.preventDefault() - try { - await sessionManager.flushAllSessions() - mainLog.info('Flushed all pending session writes') - } catch (error) { - mainLog.error('Failed to flush sessions:', error) - } - // Clean up SessionManager resources (file watchers, timers, etc.) - sessionManager.cleanup() - - // Clean up browser pane instances - if (browserPaneManager) { - browserPaneManager.destroyAll() - } - - // Clean up OAuth flow store (stop periodic cleanup timer) - if (oauthFlowStore) { - oauthFlowStore.dispose() - } - - // Stop all model refresh timers - getModelRefreshService().stopAll() - - // Stop the voice stream server (terminates clients so it closes promptly). - await voiceServer?.close() - voiceServer = null - - // Stop messaging gateways so the WhatsApp worker subprocess exits cleanly. - if (messagingHandle) { - try { - await messagingHandle.dispose() - } catch (err) { - mainLog.error('[messaging] dispose failed:', err) - } - } - - // Clean up power manager (release power blocker) - const { cleanup: cleanupPowerManager } = await import('./power-manager') - cleanupPowerManager() - - // Release the server lock file so the next launch doesn't see a stale PID. - // This must happen regardless of the exit path (normal quit or update quit). - releaseServerLock() - - // If update is in progress, let electron-updater handle the quit flow - // Force exit breaks the NSIS installer on Windows - if (isUpdating()) { - mainLog.info('Update in progress, letting electron-updater handle quit') - app.quit() - return - } - - // Now actually quit - app.exit(0) - } -}) - -// Handle uncaught exceptions — forward to Sentry explicitly since registering -// a custom handler can interfere with @sentry/electron's automatic capture. -process.on('uncaughtException', (error) => { - mainLog.error('Uncaught exception:', error) - Sentry.captureException(error) -}) - -process.on('unhandledRejection', (reason, promise) => { - mainLog.error('Unhandled rejection at:', promise, 'reason:', reason) - Sentry.captureException(reason instanceof Error ? reason : new Error(String(reason))) -}) diff --git a/packages/desktop/apps/electron/src/main/logger.ts b/packages/desktop/apps/electron/src/main/logger.ts deleted file mode 100644 index 33847ac30aa..00000000000 --- a/packages/desktop/apps/electron/src/main/logger.ts +++ /dev/null @@ -1,251 +0,0 @@ -import log from 'electron-log/main' -import { appendFileSync, existsSync, mkdirSync, renameSync, rmSync, statSync } from 'node:fs' -import { dirname, join } from 'node:path' -import { homedir } from 'node:os' -import type { - MessagingLogContext, - MessagingLogMeta, - MessagingLogger, -} from '@craft-agent/messaging-gateway' - -const ANSI_RESET = '\x1b[0m' -const ANSI_DIM = '\x1b[2m' -const ANSI_CYAN = '\x1b[36m' -const ANSI_GREEN = '\x1b[32m' -const ANSI_YELLOW = '\x1b[33m' -const ANSI_RED = '\x1b[31m' -const ANSI_MAGENTA = '\x1b[35m' - -function shouldColorConsole(): boolean { - if (process.env.NO_COLOR) return false - if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== '0') return true - return process.stdout.isTTY === true || process.stderr.isTTY === true -} - -const colorConsole = shouldColorConsole() - -function colorize(value: string, color: string): string { - return colorConsole ? `${color}${value}${ANSI_RESET}` : value -} - -function colorizeLevel(level: string): string { - switch (level.trim().toLowerCase()) { - case 'error': - return colorize(level, ANSI_RED) - case 'warn': - return colorize(level, ANSI_YELLOW) - case 'debug': - return colorize(level, ANSI_MAGENTA) - case 'info': - return colorize(level, ANSI_GREEN) - default: - return colorize(level, ANSI_CYAN) - } -} - -/** - * Resolve debug mode deterministically across runtimes. - * - * Priority: - * 1) --debug flag always enables debug mode - * 2) CRAFT_IS_PACKAGED env (when explicitly set) - * 3) Electron runtime heuristic (defaultApp => dev, otherwise packaged) - * 4) Non-Electron runtimes default to debug mode (headless Bun / node --check) - */ -function resolveDebugMode(): boolean { - if (process.argv.includes('--debug')) return true - - const packagedEnv = process.env.CRAFT_IS_PACKAGED - if (packagedEnv === 'true') return false - if (packagedEnv === 'false') return true - - const isElectronRuntime = typeof process.versions?.electron === 'string' - if (isElectronRuntime) { - if (process.defaultApp) return true - return false - } - - return true -} - -export const isDebugMode = resolveDebugMode() - -// Always keep a local file log for support/debugging. Capture every -// electron-log level so packaged builds retain the same diagnostic detail as -// dev/debug runs. -log.transports.file.format = ({ message }) => [ - JSON.stringify({ - timestamp: message.date.toISOString(), - level: message.level, - scope: message.scope, - message: message.data, - }), -] -log.transports.file.maxSize = 5 * 1024 * 1024 // 5MB -log.transports.file.level = 'silly' - -// Console output is useful in dev/debug mode. Packaged production keeps the -// terminal clean and relies on the file log above. -if (isDebugMode) { - // Note: format must return an array - electron-log's transformStyles calls .reduce() on it - log.transports.console.format = ({ message }) => { - const timestamp = colorize(message.date.toISOString(), ANSI_DIM) - const scope = message.scope ? colorize(`[${message.scope}]`, ANSI_CYAN) : '' - const level = colorizeLevel(message.level.toUpperCase().padEnd(5)) - const data = message.data - .map((d: unknown) => (typeof d === 'object' ? JSON.stringify(d) : String(d))) - .join(' ') - return [`${timestamp} ${level} ${scope} ${data}`] - } - log.transports.console.level = 'debug' -} else { - log.transports.console.level = false -} - -// Export scoped loggers for different modules -export const mainLog = log.scope('main') -export const sessionLog = log.scope('session') -export const handlerLog = log.scope('handler') -export const windowLog = log.scope('window') -export const agentLog = log.scope('agent') -export const searchLog = log.scope('search') - -/** - * Dedicated messaging gateway log. - * - * Kept outside the Electron-managed logs folder so messaging issues can be - * inspected independently at a stable path across debug and production builds. - */ -export const messagingGatewayLogPath = join(homedir(), '.craft-agent', 'logs', 'messaging-gateway.log') -const messagingGatewayBackupPath = `${messagingGatewayLogPath}.1` -const MESSAGING_LOG_MAX_BYTES = 5 * 1024 * 1024 // 5MB - -function ensureMessagingLogDir(): void { - mkdirSync(dirname(messagingGatewayLogPath), { recursive: true }) -} - -function rotateMessagingLogIfNeeded(nextLineBytes: number): void { - if (!existsSync(messagingGatewayLogPath)) return - try { - const currentSize = statSync(messagingGatewayLogPath).size - if (currentSize + nextLineBytes <= MESSAGING_LOG_MAX_BYTES) return - if (existsSync(messagingGatewayBackupPath)) { - rmSync(messagingGatewayBackupPath, { force: true }) - } - renameSync(messagingGatewayLogPath, messagingGatewayBackupPath) - } catch (error) { - mainLog.warn('[messaging-gateway] failed to rotate dedicated log file', normalizeLogValue(error)) - } -} - -function normalizeLogValue(value: unknown, depth = 0): unknown { - if (depth > 4) return '[truncated]' - if (value instanceof Error) { - const out: Record = { - name: value.name, - message: value.message, - } - const code = (value as { code?: unknown }).code - if (code !== undefined) out.code = code - const cause = (value as { cause?: unknown }).cause - if (cause !== undefined) out.cause = normalizeLogValue(cause, depth + 1) - if (value.stack) out.stack = value.stack - return out - } - if (Array.isArray(value)) { - return value.map((item) => normalizeLogValue(item, depth + 1)) - } - if (value && typeof value === 'object') { - const out: Record = {} - for (const [key, inner] of Object.entries(value)) { - out[key] = normalizeLogValue(inner, depth + 1) - } - return out - } - return value -} - -function normalizeMeta(meta?: MessagingLogMeta): Record { - if (!meta) return {} - const normalized = normalizeLogValue(meta) - return normalized && typeof normalized === 'object' && !Array.isArray(normalized) - ? normalized as Record - : { meta: normalized } -} - -function writeMessagingGatewayLog( - level: 'info' | 'warn' | 'error', - context: MessagingLogContext, - message: string, - meta?: MessagingLogMeta, -): void { - const entry = { - timestamp: new Date().toISOString(), - level, - scope: 'messaging-gateway', - ...context, - ...normalizeMeta(meta), - message, - } - - const line = JSON.stringify(entry) + '\n' - try { - ensureMessagingLogDir() - rotateMessagingLogIfNeeded(Buffer.byteLength(line)) - appendFileSync(messagingGatewayLogPath, line, 'utf8') - } catch (error) { - mainLog.warn('[messaging-gateway] failed to write dedicated log entry', { - error: normalizeLogValue(error), - attemptedEntry: entry, - }) - } - - if (level === 'error') { - mainLog.error('[messaging-gateway]', message, entry) - } else if (level === 'warn') { - mainLog.warn('[messaging-gateway]', message, entry) - } else if (isDebugMode) { - mainLog.info('[messaging-gateway]', message, entry) - } -} - -class StructuredMessagingGatewayLogger implements MessagingLogger { - constructor(private readonly context: MessagingLogContext = {}) {} - - child(context: MessagingLogContext): MessagingLogger { - return new StructuredMessagingGatewayLogger({ - ...this.context, - ...context, - }) - } - - info(message: string, meta?: MessagingLogMeta): void { - writeMessagingGatewayLog('info', this.context, message, meta) - } - - warn(message: string, meta?: MessagingLogMeta): void { - writeMessagingGatewayLog('warn', this.context, message, meta) - } - - error(message: string, meta?: MessagingLogMeta): void { - writeMessagingGatewayLog('error', this.context, message, meta) - } -} - -export const messagingGatewayLog: MessagingLogger = new StructuredMessagingGatewayLogger({ - component: 'root', -}) - -/** - * Get the path to the current Electron main log file. - * Returns undefined if file logging is disabled. - */ -export function getLogFilePath(): string | undefined { - return log.transports.file.getFile()?.path -} - -export function getMessagingGatewayLogFilePath(): string { - return messagingGatewayLogPath -} - -export default log diff --git a/packages/desktop/apps/electron/src/main/menu.ts b/packages/desktop/apps/electron/src/main/menu.ts deleted file mode 100644 index e9bf01489a8..00000000000 --- a/packages/desktop/apps/electron/src/main/menu.ts +++ /dev/null @@ -1,256 +0,0 @@ -import { Menu, app, shell, BrowserWindow } from 'electron' -import { i18n } from '@craft-agent/shared/i18n' -import { BRAND } from '@craft-agent/shared/branding' -import { RPC_CHANNELS, type BroadcastEventMap } from '../shared/types' -import { EDIT_MENU, VIEW_MENU, WINDOW_MENU } from '../shared/menu-schema' -import type { MenuItem } from '../shared/menu-schema' -import type { WindowManager } from './window-manager' -import type { EventSink } from '@craft-agent/server-core/transport' -import { isDebugMode } from './logger' - -type ClientResolver = (webContentsId: number) => string | undefined - -// Store references for rebuilding menu -let cachedWindowManager: WindowManager | null = null -let cachedEventSink: EventSink | null = null -let cachedClientResolver: ClientResolver | null = null - -/** - * Creates and sets the application menu for macOS. - * Includes only relevant items for the Qwen Code app. - * - * Call rebuildMenu() when shared menu state changes. - */ -export function createApplicationMenu(windowManager: WindowManager, sink?: EventSink, resolver?: ClientResolver): void { - cachedWindowManager = windowManager - cachedEventSink = sink ?? null - cachedClientResolver = resolver ?? null - rebuildMenu() -} - -/** - * Set the event sink and client resolver after server creation. - * Called separately from createApplicationMenu since the server may not exist at menu init time. - */ -export function setMenuEventSink(sink: EventSink, resolver: ClientResolver): void { - cachedEventSink = sink - cachedClientResolver = resolver -} - -/** - * Rebuilds the application menu. - * - * On Windows/Linux: Menu is hidden - all functionality is in the Craft logo menu. - * On macOS: Native menu is required by Apple guidelines, so we keep it synced. - */ -export async function rebuildMenu(): Promise { - if (!cachedWindowManager) return - - const windowManager = cachedWindowManager - const isMac = process.platform === 'darwin' - const helpMenuLinks: Electron.MenuItemConstructorOptions[] = - BRAND.helpMenuLinks.map((link) => ({ - label: i18n.t(link.labelKey), - click: () => shell.openExternal(link.url), - })) - - // On Windows/Linux, hide the native menu entirely - // Users access menu via the Craft logo dropdown in the app - if (!isMac) { - Menu.setApplicationMenu(null) - return - } - - const template: Electron.MenuItemConstructorOptions[] = [ - // App menu (macOS only) - ...(isMac ? [{ - label: BRAND.appName, - submenu: [ - { role: 'about' as const, label: i18n.t('menu.aboutCraftAgents') }, - { type: 'separator' as const }, - { - label: i18n.t("menu.settings"), - accelerator: 'CmdOrCtrl+,', - registerAccelerator: false, // Action registry handles the keyboard shortcut - click: () => sendToRenderer(RPC_CHANNELS.menu.OPEN_SETTINGS) - }, - { type: 'separator' as const }, - { role: 'hide' as const, label: i18n.t('menu.hideCraftAgents') }, - { role: 'hideOthers' as const }, - { role: 'unhide' as const }, - { type: 'separator' as const }, - { role: 'quit' as const, label: i18n.t('menu.quitCraftAgents') } - ] - }] : []), - - // File menu - { - label: i18n.t("menu.file"), - submenu: [ - { - label: i18n.t("menu.newChat"), - accelerator: 'CmdOrCtrl+N', - registerAccelerator: false, // Action registry handles the keyboard shortcut - click: () => sendToRenderer(RPC_CHANNELS.menu.NEW_CHAT) - }, - { - label: i18n.t("menu.newWindow"), - accelerator: 'CmdOrCtrl+Shift+N', - registerAccelerator: false, // Action registry handles the keyboard shortcut - click: () => { - const focused = BrowserWindow.getFocusedWindow() - if (focused) { - const workspaceId = windowManager.getWorkspaceForWindow(focused.webContents.id) - if (workspaceId) { - windowManager.createWindow({ workspaceId }) - } - } - } - }, - { type: 'separator' as const }, - isMac ? { role: 'close' as const } : { role: 'quit' as const } - ] - }, - - // Edit menu (from shared schema) - { - label: i18n.t(EDIT_MENU.labelKey), - submenu: EDIT_MENU.items.map(toElectronMenuItem), - }, - - // View menu (from shared schema + dev-only items) - { - label: i18n.t(VIEW_MENU.labelKey), - submenu: [ - ...VIEW_MENU.items.map(toElectronMenuItem), - // Dev tools — available in dev mode or when started with --debug - ...(!app.isPackaged || isDebugMode ? [ - { type: 'separator' as const }, - ...(!app.isPackaged ? [ - { - label: i18n.t("menu.reload"), - accelerator: 'CmdOrCtrl+R', - click: (_menuItem: Electron.MenuItem, window: Electron.BaseWindow | undefined) => { - const browserWindow = window instanceof BrowserWindow ? window : BrowserWindow.getFocusedWindow() - if (!browserWindow) return - const views = browserWindow.getBrowserViews() - if (views.length > 0) { - views[0].webContents.reload() - } else { - browserWindow.webContents.reload() - } - } - }, - { - label: i18n.t("menu.forceReload"), - accelerator: 'CmdOrCtrl+Shift+R', - click: (_menuItem: Electron.MenuItem, window: Electron.BaseWindow | undefined) => { - const browserWindow = window instanceof BrowserWindow ? window : BrowserWindow.getFocusedWindow() - if (!browserWindow) return - const views = browserWindow.getBrowserViews() - if (views.length > 0) { - views[0].webContents.reloadIgnoringCache() - } else { - browserWindow.webContents.reloadIgnoringCache() - } - } - }, - ] : []), - { role: 'toggleDevTools' as const }, - ] : []) - ] - }, - - // Window menu (from shared schema + macOS-specific items) - { - label: i18n.t(WINDOW_MENU.labelKey), - submenu: [ - ...WINDOW_MENU.items.map(toElectronMenuItem), - ...(isMac ? [ - { type: 'separator' as const }, - { role: 'front' as const } - ] : []) - ] - }, - - // Debug menu (development only) - ...(!app.isPackaged ? [{ - label: i18n.t("menu.debug"), - submenu: [ - { - label: i18n.t("menu.resetToDefaults"), - click: async () => { - const { dialog } = await import('electron') - await dialog.showMessageBox({ - type: 'info', - message: i18n.t("menu.resetToDefaultsTitle"), - detail: i18n.t("menu.resetToDefaultsDetail"), - buttons: [i18n.t("common.ok")] - }) - } - } - ] - }] : []), - - // Help menu - { - label: i18n.t("menu.help"), - submenu: [ - ...helpMenuLinks, - ...(helpMenuLinks.length > 0 ? [{ type: 'separator' as const }] : []), - { - label: i18n.t("menu.keyboardShortcuts"), - accelerator: 'CmdOrCtrl+/', - registerAccelerator: false, // Action registry handles the keyboard shortcut - click: () => sendToRenderer(RPC_CHANNELS.menu.KEYBOARD_SHORTCUTS) - } - ] - } - ] - - const menu = Menu.buildFromTemplate(template) - Menu.setApplicationMenu(menu) -} - -/** Menu channels that are main→renderer push events in BroadcastEventMap */ -type MenuBroadcastChannel = Extract - -/** - * Sends an event to the focused renderer window via the RPC event sink. - */ -function sendToRenderer(channel: MenuBroadcastChannel): void { - if (!cachedEventSink || !cachedClientResolver) return - const win = BrowserWindow.getFocusedWindow() - if (win && !win.isDestroyed() && !win.webContents.isDestroyed()) { - const clientId = cachedClientResolver(win.webContents.id) - if (clientId) { - cachedEventSink(channel, { to: 'client', clientId }) - } - } -} - -/** - * Converts a MenuItem from the shared schema to Electron MenuItemConstructorOptions. - */ -function toElectronMenuItem(item: MenuItem): Electron.MenuItemConstructorOptions { - if (item.type === 'separator') { - return { type: 'separator' } - } - - if (item.type === 'role') { - // Use Electron's built-in role - it handles accelerators automatically - return { role: item.role as Electron.MenuItemConstructorOptions['role'] } - } - - if (item.type === 'action') { - return { - label: i18n.t(item.labelKey), - accelerator: item.shortcut, - registerAccelerator: false, // Action registry handles the keyboard shortcut - click: () => sendToRenderer(item.ipcChannel as MenuBroadcastChannel), - } - } - - // Should never reach here - return { type: 'separator' } -} diff --git a/packages/desktop/apps/electron/src/main/network-proxy-utils.ts b/packages/desktop/apps/electron/src/main/network-proxy-utils.ts deleted file mode 100644 index 974c4fa6de6..00000000000 --- a/packages/desktop/apps/electron/src/main/network-proxy-utils.ts +++ /dev/null @@ -1,125 +0,0 @@ -/** - * Network proxy utility functions (pure — no Electron deps). - * - * Parses NO_PROXY rules and determines whether a given URL should bypass the proxy. - */ - -/** Split a comma-separated string into trimmed, non-empty entries. */ -export function splitCommaSeparated(str: string | undefined): string[] { - if (!str) return []; - return str - .split(',') - .map((s) => s.trim()) - .filter(Boolean); -} - -export interface NoProxyRule { - /** Exact hostname or domain suffix (without leading dot). */ - host: string; - /** Optional port restriction. */ - port?: number; - /** If true, matches any hostname (wildcard `*`). */ - wildcard: boolean; -} - -function parsePort(raw: string): number | undefined { - if (!/^\d+$/.test(raw)) return undefined; - const port = Number(raw); - return Number.isInteger(port) && port >= 0 && port <= 65535 - ? port - : undefined; -} - -/** - * Parse a comma-separated NO_PROXY string into structured rules. - * - * Supported formats per entry: - * - `*` → wildcard, bypass everything - * - `example.com` → exact host match - * - `.example.com` → suffix match (subdomain) - * - `example.com:8080` → host + port - * - `192.168.1.1` → exact IP literal - */ -export function parseNoProxyRules(noProxy: string | undefined): NoProxyRule[] { - if (!noProxy) return []; - - return splitCommaSeparated(noProxy) - .map((entry) => entry.toLowerCase()) - .map((entry) => { - if (entry === '*') { - return { host: '*', wildcard: true }; - } - - // Strip leading dot (treated as suffix match — same result as without dot) - let cleaned = entry.startsWith('.') ? entry.slice(1) : entry; - - // Handle IPv6: strip brackets, optionally extract trailing port ([::1]:8080) - if (cleaned.startsWith('[')) { - const closeBracket = cleaned.indexOf(']'); - if (closeBracket > 0) { - const ipv6Host = cleaned.slice(1, closeBracket); - const afterBracket = cleaned.slice(closeBracket + 1); - if (afterBracket.startsWith(':')) { - const port = parsePort(afterBracket.slice(1)); - if (port !== undefined) { - return { host: ipv6Host, port, wildcard: false }; - } - } - if (afterBracket === '') { - return { host: ipv6Host, wildcard: false }; - } - return { host: cleaned, wildcard: false }; - } - } - - // Check for port (non-IPv6) - const lastColon = cleaned.lastIndexOf(':'); - if (lastColon > 0) { - const host = cleaned.slice(0, lastColon); - const port = parsePort(cleaned.slice(lastColon + 1)); - if (port !== undefined) { - return { host, port, wildcard: false }; - } - } - - return { host: cleaned, wildcard: false }; - }); -} - -/** - * Determine whether a URL should bypass the proxy based on NO_PROXY rules. - */ -/** Default ports by protocol, used when URL omits an explicit port. */ -const DEFAULT_PORTS: Record = { 'http:': 80, 'https:': 443 }; - -export function shouldBypassProxy( - url: string | URL, - rules: NoProxyRule[], -): boolean { - if (rules.length === 0) return false; - - const parsed = typeof url === 'string' ? new URL(url) : url; - const hostname = parsed.hostname.toLowerCase(); - // Strip brackets from IPv6 - const host = hostname.startsWith('[') ? hostname.slice(1, -1) : hostname; - const port = parsed.port - ? parseInt(parsed.port, 10) - : DEFAULT_PORTS[parsed.protocol]; - - for (const rule of rules) { - if (rule.wildcard) return true; - - // Port-scoped rule: only match when port matches - if (rule.port !== undefined && rule.port !== port) { - continue; - } - - // Exact match - if (host === rule.host) return true; - - // Suffix match (subdomain): host ends with .rule.host - if (host.endsWith(`.${rule.host}`)) return true; - } - - return false; -} diff --git a/packages/desktop/apps/electron/src/main/network-proxy.ts b/packages/desktop/apps/electron/src/main/network-proxy.ts deleted file mode 100644 index f19a6287373..00000000000 --- a/packages/desktop/apps/electron/src/main/network-proxy.ts +++ /dev/null @@ -1,194 +0,0 @@ -/** - * Network proxy manager — configures both Node.js (undici) and Electron session proxies. - * - * - Node side: replaces the global undici dispatcher with a ProtocolProxyDispatcher - * that routes HTTP/HTTPS through different ProxyAgent instances and respects NO_PROXY. - * - Electron side: calls session.setProxy() on default + browser-pane sessions. - */ - -import { app, session } from 'electron'; -import { Agent, Dispatcher, ProxyAgent, setGlobalDispatcher } from 'undici'; -import { - parseNoProxyRules, - shouldBypassProxy, - splitCommaSeparated, - type NoProxyRule, -} from './network-proxy-utils'; -import { - getNetworkProxySettings, - setNetworkProxySettings, -} from '@craft-agent/shared/config/storage'; -import type { NetworkProxySettings } from '@craft-agent/shared/config/types'; -import { BROWSER_PANE_SESSION_PARTITION } from './browser-pane-manager'; -import log from './logger'; - -// Track the current dispatcher so we can close it when reconfiguring -let currentProxyDispatcher: Dispatcher | null = null; - -/** - * Custom undici Dispatcher that routes requests through proxy agents based on protocol, - * bypasses proxied destinations listed in NO_PROXY rules, and falls back to a direct Agent. - */ -class ProtocolProxyDispatcher extends Dispatcher { - private httpProxy: ProxyAgent | null; - private httpsProxy: ProxyAgent | null; - private direct: Agent; - private rules: NoProxyRule[]; - - constructor(opts: { - httpProxy?: string; - httpsProxy?: string; - noProxy?: string; - }) { - super(); - this.httpProxy = opts.httpProxy ? new ProxyAgent(opts.httpProxy) : null; - this.httpsProxy = opts.httpsProxy ? new ProxyAgent(opts.httpsProxy) : null; - this.direct = new Agent(); - this.rules = parseNoProxyRules(opts.noProxy); - } - - dispatch( - opts: Dispatcher.DispatchOptions, - handler: Dispatcher.DispatchHandlers, - ): boolean { - const url = - typeof opts.origin === 'string' ? opts.origin : opts.origin?.toString(); - - // If URL matches bypass rules, go direct - if (url && shouldBypassProxy(url, this.rules)) { - return this.direct.dispatch(opts, handler); - } - - // Route based on protocol - const isHttps = url?.startsWith('https:'); - const proxy = isHttps - ? (this.httpsProxy ?? this.httpProxy) - : this.httpProxy; - - if (proxy) { - return proxy.dispatch(opts, handler); - } - - return this.direct.dispatch(opts, handler); - } - - async close(): Promise { - await Promise.all([ - this.httpProxy?.close(), - this.httpsProxy?.close(), - this.direct.close(), - ]); - } - - async destroy(): Promise { - await Promise.all([ - this.httpProxy?.destroy(), - this.httpsProxy?.destroy(), - this.direct.destroy(), - ]); - } -} - -/** - * Configure the Node.js global undici dispatcher for proxy routing. - */ -function configureNodeProxy(settings: NetworkProxySettings | undefined): void { - // Close previous dispatcher (proxy or direct — both are tracked) - if (currentProxyDispatcher) { - currentProxyDispatcher.close().catch(() => {}); - currentProxyDispatcher = null; - } - - if (!settings?.enabled || (!settings.httpProxy && !settings.httpsProxy)) { - // Restore a direct dispatcher and track it so next reconfigure can close it - const direct = new Agent(); - setGlobalDispatcher(direct); - currentProxyDispatcher = direct; - return; - } - - const dispatcher = new ProtocolProxyDispatcher({ - httpProxy: settings.httpProxy, - httpsProxy: settings.httpsProxy, - noProxy: settings.noProxy, - }); - - setGlobalDispatcher(dispatcher); - currentProxyDispatcher = dispatcher; -} - -/** - * Configure Electron session proxies (default session + browser-pane partition). - * Requires app to be ready. - */ -async function configureElectronProxy( - settings: NetworkProxySettings | undefined, -): Promise { - if (!app.isReady()) return; - - const proxyConfig = settings?.enabled - ? buildElectronProxyConfig(settings) - : { mode: 'direct' as const }; - - const sessions = [ - session.defaultSession, - session.fromPartition(BROWSER_PANE_SESSION_PARTITION), - ]; - - await Promise.all(sessions.map((ses) => ses.setProxy(proxyConfig))); -} - -function buildElectronProxyConfig( - settings: NetworkProxySettings, -): Electron.ProxyConfig { - const rules: string[] = []; - - if (settings.httpsProxy) { - rules.push(`https=${settings.httpsProxy}`); - } - if (settings.httpProxy) { - rules.push(`http=${settings.httpProxy}`); - } - - if (rules.length === 0) { - return { mode: 'direct' }; - } - - return { - mode: 'fixed_servers', - proxyRules: rules.join(';'), - proxyBypassRules: settings.noProxy - ? splitCommaSeparated(settings.noProxy).join(',') - : undefined, - }; -} - -/** - * Read persisted proxy settings and apply to both Node and Electron. - * Safe to call before app.whenReady() — Electron session setup is skipped until ready. - */ -export async function applyConfiguredProxySettings(): Promise { - const settings = getNetworkProxySettings(); - - const hasHttpProxy = !!settings?.httpProxy; - const hasNoProxy = !!settings?.noProxy; - log.info('[proxy] Applying proxy settings:', { - enabled: settings?.enabled ?? false, - hasHttpProxy, - hasHttpsProxy: !!settings?.httpsProxy, - hasNoProxy, - }); - - configureNodeProxy(settings); - await configureElectronProxy(settings); -} - -/** - * Persist new proxy settings and apply immediately. - */ -export async function updateConfiguredProxySettings( - settings: NetworkProxySettings, -): Promise { - setNetworkProxySettings(settings); - await applyConfiguredProxySettings(); -} diff --git a/packages/desktop/apps/electron/src/main/notifications.ts b/packages/desktop/apps/electron/src/main/notifications.ts deleted file mode 100644 index 3128e8d8bac..00000000000 --- a/packages/desktop/apps/electron/src/main/notifications.ts +++ /dev/null @@ -1,303 +0,0 @@ -/** - * Notification Service - * - * Handles native OS notifications and app badge count. - * - Shows notifications when new messages arrive (when app is not focused) - * - Updates dock badge count with total unread messages - * - Clicking notification navigates to the relevant session - */ - -import { Notification, app, BrowserWindow, nativeImage } from 'electron' -import { join } from 'path' -import { mainLog } from './logger' -import { RPC_CHANNELS } from '../shared/types' -import type { WindowManager } from './window-manager' -import type { EventSink } from '@craft-agent/server-core/transport' - -type ClientResolver = (webContentsId: number) => string | undefined - -let windowManager: WindowManager | null = null -let eventSink: EventSink | null = null -let clientResolver: ClientResolver | null = null -let baseIconPath: string | null = null -let baseIconDataUrl: string | null = null -let currentBadgeCount: number = 0 -let instanceNumber: number | null = null // Multi-instance dev: instance number for dock badge - -/** - * Initialize the notification service with window manager reference - */ -export function initNotificationService(wm: WindowManager): void { - windowManager = wm -} - -/** - * Set the event sink for notification broadcasts (called after server creation). - * - * When a resolver is provided we can route session navigation events to a - * single client instead of broadcasting to every window in the workspace. - */ -export function setNotificationEventSink(sink: EventSink, resolver?: ClientResolver): void { - eventSink = sink - clientResolver = resolver ?? null -} - -/** - * Show a native notification for a new message - * - * @param title - Notification title (e.g., session name) - * @param body - Notification body (e.g., message preview) - * @param workspaceId - Workspace ID for navigation - * @param sessionId - Session ID for navigation - */ -export function showNotification( - title: string, - body: string, - workspaceId: string, - sessionId: string -): void { - if (!Notification.isSupported()) { - mainLog.info('Notifications not supported on this platform') - return - } - - const notification = new Notification({ - title, - body, - // macOS-specific options - silent: false, - // Use the app icon - icon: undefined, // Will use app icon by default on macOS - }) - - notification.on('click', () => { - mainLog.info('Notification clicked:', { workspaceId, sessionId }) - handleNotificationClick(workspaceId, sessionId) - }) - - notification.show() - mainLog.info('Notification shown:', { title, sessionId }) -} - -/** - * Handle notification click - focus window and navigate to session - */ -export function handleNotificationClick( - workspaceId: string, - sessionId: string, -): void { - if (!windowManager) { - mainLog.error('WindowManager not initialized for notification click') - return - } - - // Find or create window for this workspace - let window = windowManager.getWindowByWorkspace(workspaceId) - - if (!window) { - // Create a new window for this workspace - windowManager.createWindow({ workspaceId }) - window = windowManager.getWindowByWorkspace(workspaceId) - } - - if (window && !window.isDestroyed() && !window.webContents.isDestroyed()) { - // Focus the window - if (window.isMinimized()) { - window.restore() - } - window.focus() - - // Send navigation event to renderer to open the session. - // Prefer a single-client target to avoid cross-window navigation side effects. - if (eventSink) { - const clientId = clientResolver?.(window.webContents.id) - if (clientId) { - eventSink(RPC_CHANNELS.notification.NAVIGATE, { to: 'client', clientId }, { - workspaceId, - sessionId, - }) - } else { - eventSink(RPC_CHANNELS.notification.NAVIGATE, { to: 'workspace', workspaceId }, { - workspaceId, - sessionId, - }) - } - } - } -} - -/** - * Initialize the base icon for badge overlay - * Call this during app startup - */ -export function initBadgeIcon(iconPath: string): void { - try { - baseIconPath = iconPath - const icon = nativeImage.createFromPath(iconPath) - if (icon.isEmpty()) { - baseIconDataUrl = null - mainLog.warn('Badge icon could not be loaded:', iconPath) - return - } - - const iconBuffer = icon.toPNG() - baseIconDataUrl = `data:image/png;base64,${iconBuffer.toString('base64')}` - mainLog.info('Badge icon initialized:', iconPath) - } catch (error) { - mainLog.error('Failed to initialize badge icon:', error) - } -} - -/** - * Update the app badge count (cross-platform) - * - * - macOS: Uses a canvas-based approach to draw the badge directly onto the dock icon. - * - Windows: Uses taskbar overlay icon for badge display. - * - Linux: Uses app.setBadgeCount() where supported (Unity, KDE). - * - * @param count - Number to show on badge (0 to clear) - */ -export function updateBadgeCount(count: number): void { - // Skip if count hasn't changed - if (count === currentBadgeCount) { - return - } - - currentBadgeCount = count - - if (process.platform === 'darwin') { - updateBadgeCountMacOS(count) - } else if (process.platform === 'win32') { - updateBadgeCountWindows(count) - } else if (process.platform === 'linux') { - updateBadgeCountLinux(count) - } -} - -/** - * Update badge count on macOS using dock icon overlay - */ -function updateBadgeCountMacOS(count: number): void { - try { - if (count > 0) { - // Draw badge onto icon using the renderer process (Canvas API) - if (eventSink && baseIconDataUrl) { - eventSink(RPC_CHANNELS.badge.DRAW, { to: 'all' }, { count, iconDataUrl: baseIconDataUrl }) - } - } else { - // Reset to original icon (no badge) - if (baseIconPath) { - const originalIcon = nativeImage.createFromPath(baseIconPath) - app.dock?.setIcon(originalIcon) - } - } - mainLog.info('Badge count updated (macOS):', count) - } catch (error) { - mainLog.error('Failed to update badge count (macOS):', error) - } -} - -/** - * Update badge count on Windows using taskbar overlay icon - */ -function updateBadgeCountWindows(count: number): void { - try { - if (count > 0) { - // Draw overlay icon using the renderer process (Canvas API) - if (eventSink) { - eventSink(RPC_CHANNELS.badge.DRAW_WINDOWS, { to: 'all' }, { count }) - } - } else { - // Clear the overlay on all windows - const windows = BrowserWindow.getAllWindows() - for (const window of windows) { - if (!window.isDestroyed()) { - window.setOverlayIcon(null, '') - } - } - } - mainLog.info('Badge count updated (Windows):', count) - } catch (error) { - mainLog.error('Failed to update badge count (Windows):', error) - } -} - -/** - * Update badge count on Linux using app.setBadgeCount (Unity/KDE) - */ -function updateBadgeCountLinux(count: number): void { - try { - // Electron's setBadgeCount works on Linux with Unity launcher and KDE - app.setBadgeCount(count) - mainLog.info('Badge count updated (Linux):', count) - } catch (error) { - mainLog.error('Failed to update badge count (Linux):', error) - } -} - -/** - * Set the dock/taskbar icon with a pre-rendered badge image (cross-platform) - * Called from IPC when renderer has drawn the badge - */ -export function setDockIconWithBadge(dataUrl: string): void { - try { - const icon = nativeImage.createFromDataURL(dataUrl) - - if (process.platform === 'darwin') { - app.dock?.setIcon(icon) - mainLog.info('Dock icon updated with badge (macOS)') - } else if (process.platform === 'win32') { - // On Windows, set the taskbar overlay icon - const windows = BrowserWindow.getAllWindows() - const window = windows[0] - if (window && !window.isDestroyed()) { - window.setOverlayIcon(icon, `${currentBadgeCount} notifications`) - mainLog.info('Taskbar overlay updated with badge (Windows)') - } - } - } catch (error) { - mainLog.error('Failed to set dock/taskbar icon with badge:', error) - } -} - -/** - * Clear the app dock badge - */ -export function clearBadgeCount(): void { - updateBadgeCount(0) -} - -/** - * Check if any window is currently focused - */ -export function isAnyWindowFocused(): boolean { - const focusedWindow = BrowserWindow.getFocusedWindow() - return focusedWindow !== null && !focusedWindow.isDestroyed() -} - -/** - * Initialize instance badge for multi-instance development. - * - * When running from a numbered folder (e.g., craft-tui-agent-1), this shows - * a permanent badge on the dock icon to distinguish between instances. - * Uses macOS dock.setBadge() for text-based badge display. - * - * @param number - Instance number (1, 2, etc.) or null for default instance - */ -export function initInstanceBadge(number: number): void { - if (process.platform !== 'darwin') { - // Instance badge only supported on macOS for now - return - } - - instanceNumber = number - - try { - // Use dock.setBadge() for simple text badge - // This shows the number in a red badge on the dock icon - app.dock?.setBadge(String(number)) - mainLog.info(`Instance badge set: ${number}`) - } catch (error) { - mainLog.error('Failed to set instance badge:', error) - } -} diff --git a/packages/desktop/apps/electron/src/main/onboarding.ts b/packages/desktop/apps/electron/src/main/onboarding.ts deleted file mode 100644 index c998132bccf..00000000000 --- a/packages/desktop/apps/electron/src/main/onboarding.ts +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Onboarding IPC handlers for Electron main process - * - * Handles workspace setup and configuration persistence. - */ -import { getAuthState, getSetupNeeds } from '@craft-agent/shared/auth' -import { isSetupDeferred, setSetupDeferred } from '@craft-agent/shared/config/storage' -import { prepareMcpOAuth } from '@craft-agent/shared/auth' -import { validateMcpConnection } from '@craft-agent/shared/mcp' -import { RPC_CHANNELS } from '@craft-agent/shared/protocol' -import type { RpcServer } from '@craft-agent/server-core/transport' -import type { HandlerDeps } from './handlers/handler-deps' - -// ============================================ -// IPC Handlers -// ============================================ - -export const HANDLED_CHANNELS = [ - RPC_CHANNELS.onboarding.GET_AUTH_STATE, - RPC_CHANNELS.onboarding.VALIDATE_MCP, - RPC_CHANNELS.onboarding.START_MCP_OAUTH, - RPC_CHANNELS.onboarding.DEFER_SETUP, -] as const - -export function registerOnboardingHandlers(server: RpcServer, deps: HandlerDeps): void { - const log = deps.platform.logger - - // Get current auth state - server.handle(RPC_CHANNELS.onboarding.GET_AUTH_STATE, async () => { - const authState = await getAuthState() - const setupNeeds = getSetupNeeds(authState, isSetupDeferred()) - // Redact raw credentials — renderer only needs boolean flags (hasCredentials, setupNeeds) - return { - authState: { - ...authState, - billing: { - ...authState.billing, - apiKey: authState.billing.apiKey ? '••••' : null, - }, - }, - setupNeeds, - } - }) - - // Validate MCP connection - server.handle(RPC_CHANNELS.onboarding.VALIDATE_MCP, async (_ctx, mcpUrl: string, accessToken?: string) => { - try { - const result = await validateMcpConnection({ - mcpUrl, - mcpAccessToken: accessToken, - }) - return result - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - return { success: false, error: message } - } - }) - - // Prepare MCP server OAuth (server-side only — no browser open). - // Returns authUrl for the client to open locally. - // NOTE: Currently unused in renderer. If re-enabled, needs client-side - // orchestration (callback server + browser open) like performOAuth(). - server.handle(RPC_CHANNELS.onboarding.START_MCP_OAUTH, async (_ctx, mcpUrl: string, callbackPort?: number) => { - log.info('[Onboarding:Main] ONBOARDING_START_MCP_OAUTH received') - try { - if (!callbackPort) { - throw new Error('callbackPort is required — client must run a local callback server') - } - const prepared = await prepareMcpOAuth(mcpUrl, { callbackPort }) - log.info('[Onboarding:Main] MCP OAuth prepared, returning authUrl to client') - - return { - success: true, - authUrl: prepared.authUrl, - state: prepared.state, - codeVerifier: prepared.codeVerifier, - tokenEndpoint: prepared.tokenEndpoint, - clientId: prepared.clientId, - redirectUri: prepared.redirectUri, - } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - log.error('[Onboarding:Main] MCP OAuth prepare failed:', message) - return { success: false, error: message } - } - }) - - // User chose "Setup later" — persist so onboarding doesn't re-show on next launch. - // Cleared automatically when user configures a provider from Settings. - server.handle(RPC_CHANNELS.onboarding.DEFER_SETUP, async () => { - setSetupDeferred(true) - log.info('[Onboarding] User deferred setup') - return { success: true } - }) -} diff --git a/packages/desktop/apps/electron/src/main/platform.ts b/packages/desktop/apps/electron/src/main/platform.ts deleted file mode 100644 index 429b6d1f461..00000000000 --- a/packages/desktop/apps/electron/src/main/platform.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Electron platform factory — creates PlatformServices from Electron APIs. - * - * Extracted from main/index.ts so it can be injected into bootstrapServer() - * without duplicating construction logic. - */ - -import type { PlatformServices } from '../runtime/platform' - -export interface ElectronPlatformOptions { - app: Electron.App - nativeImage: typeof import('electron').nativeImage - shell: typeof import('electron').shell - nativeTheme: typeof import('electron').nativeTheme - logger: PlatformServices['logger'] - isDebugMode: boolean - getLogFilePath?: () => string | undefined - captureError?: (error: Error) => void -} - -export function createElectronPlatform(opts: ElectronPlatformOptions): PlatformServices { - const { app, nativeImage, shell, nativeTheme, logger } = opts - - return { - appRootPath: app.isPackaged ? app.getAppPath() : process.cwd(), - resourcesPath: process.resourcesPath, - isPackaged: app.isPackaged, - appVersion: app.getVersion(), - openExternal: (url) => shell.openExternal(url), - openPath: (p) => shell.openPath(p).then(() => {}), - showItemInFolder: (p) => shell.showItemInFolder(p), - quit: () => app.quit(), - systemDarkMode: () => nativeTheme.shouldUseDarkColors, - imageProcessor: { - async getMetadata(buffer) { - const img = nativeImage.createFromBuffer(buffer) - if (img.isEmpty()) return null - const { width, height } = img.getSize() - return (width && height) ? { width, height } : null - }, - async process(input, processOpts = {}) { - const img = typeof input === 'string' - ? nativeImage.createFromPath(input) - : nativeImage.createFromBuffer(input) - if (img.isEmpty()) throw new Error('Invalid image input') - - let result = img - if (processOpts.resize) { - const { width: tw, height: th } = processOpts.resize - const fit = processOpts.fit ?? 'inside' - if (fit === 'inside') { - const { width: sw, height: sh } = result.getSize() - const scale = Math.min(tw / sw, th / sh, 1) - result = result.resize({ - width: Math.round(sw * scale), - height: Math.round(sh * scale), - }) - } else { - result = result.resize({ width: tw, height: th }) - } - } - return (processOpts.format === 'jpeg') - ? result.toJPEG(processOpts.quality ?? 90) - : result.toPNG() - }, - }, - logger, - isDebugMode: opts.isDebugMode, - getLogFilePath: opts.getLogFilePath, - captureError: opts.captureError, - } -} diff --git a/packages/desktop/apps/electron/src/main/power-manager.ts b/packages/desktop/apps/electron/src/main/power-manager.ts deleted file mode 100644 index b502135a8ec..00000000000 --- a/packages/desktop/apps/electron/src/main/power-manager.ts +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Power Manager - Prevents screen sleep while sessions are running - * - * Uses Electron's powerSaveBlocker API to prevent the display from sleeping - * when the "Keep screen awake" setting is enabled and at least one session - * is actively processing. - */ - -import { powerSaveBlocker } from 'electron' -import { mainLog } from './logger' - -// Track the current power blocker ID (null when not blocking) -let powerBlockerId: number | null = null - -// Track the number of active (processing) sessions -let activeSessionCount = 0 - -// Cache the setting value to avoid repeated config reads -let settingEnabled = false - -/** - * Initialize the power manager by loading the current setting. - * Call this on app startup. - */ -export async function initPowerManager(): Promise { - const { getKeepAwakeWhileRunning } = await import('@craft-agent/shared/config/storage') - settingEnabled = getKeepAwakeWhileRunning() - mainLog.info('[power] Power manager initialized', { settingEnabled }) -} - -/** - * Update the power state based on active sessions and setting. - * Called when: - * - A session starts or stops processing - * - The setting is toggled - */ -function updatePowerState(): void { - const shouldBlock = settingEnabled && activeSessionCount > 0 - - if (shouldBlock && powerBlockerId === null) { - // Start blocking display sleep - powerBlockerId = powerSaveBlocker.start('prevent-display-sleep') - mainLog.info('[power] Started power save blocker', { blockerId: powerBlockerId, activeSessionCount }) - } else if (!shouldBlock && powerBlockerId !== null) { - // Stop blocking - powerSaveBlocker.stop(powerBlockerId) - mainLog.info('[power] Stopped power save blocker', { blockerId: powerBlockerId }) - powerBlockerId = null - } -} - -/** - * Called when a session starts processing. - */ -export function onSessionStarted(): void { - activeSessionCount++ - mainLog.debug('[power] Session started processing', { activeSessionCount }) - updatePowerState() -} - -/** - * Called when a session stops processing (complete, error, or cancelled). - */ -export function onSessionStopped(): void { - if (activeSessionCount > 0) { - activeSessionCount-- - } - mainLog.debug('[power] Session stopped processing', { activeSessionCount }) - updatePowerState() -} - -/** - * Update the keep awake setting. - * Called from IPC handler when user toggles the setting. - */ -export function setKeepAwakeSetting(enabled: boolean): void { - settingEnabled = enabled - mainLog.info('[power] Keep awake setting changed', { enabled, activeSessionCount }) - updatePowerState() -} - -/** - * Get the current keep awake setting value. - */ -export function getKeepAwakeSetting(): boolean { - return settingEnabled -} - -/** - * Check if power blocker is currently active. - * Useful for debugging. - */ -export function isPowerBlockerActive(): boolean { - return powerBlockerId !== null && powerSaveBlocker.isStarted(powerBlockerId) -} - -/** - * Clean up power blocker on app quit. - * Note: Electron automatically releases blockers on quit, but this is explicit. - */ -export function cleanup(): void { - if (powerBlockerId !== null) { - powerSaveBlocker.stop(powerBlockerId) - mainLog.info('[power] Cleaned up power save blocker on shutdown') - powerBlockerId = null - } - activeSessionCount = 0 -} diff --git a/packages/desktop/apps/electron/src/main/shell-env.ts b/packages/desktop/apps/electron/src/main/shell-env.ts deleted file mode 100644 index aba5f7ba51e..00000000000 --- a/packages/desktop/apps/electron/src/main/shell-env.ts +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Shell Environment Loader - * - * When Electron apps are launched from Finder/Dock on macOS, they inherit - * a minimal launchd environment with PATH=/usr/bin:/bin:/usr/sbin:/sbin. - * - * This module loads the user's full shell environment by spawning their - * login shell and extracting environment variables. This ensures tools - * like Homebrew (gh, brew), nvm, pyenv, etc. are available to the agent. - */ - -import { execSync } from 'child_process' -import { mainLog } from './logger' - -// Environment variables that should NOT be imported from the shell -// VITE_* vars from dev mode would make packaged app try to load from localhost -const shouldSkipEnvVar = (key: string): boolean => { - return key.startsWith('VITE_') -} - -/** - * Load the user's shell environment and merge it into process.env - * - * This should be called early in app startup, before creating any agents. - * It spawns the user's login shell to get the full environment including - * PATH modifications from .zshrc, .bashrc, .zprofile, etc. - */ -export function loadShellEnv(): void { - // Only needed on macOS where GUI apps have minimal environment - if (process.platform !== 'darwin') { - return - } - - // Skip in dev mode - terminal launches already have full environment - if (process.env.VITE_DEV_SERVER_URL) { - mainLog.info('[shell-env] Skipping in dev mode (already have shell environment)') - return - } - - const shell = process.env.SHELL || '/bin/zsh' - mainLog.info(`[shell-env] Loading environment from ${shell}`) - - try { - // Run login shell to get full environment - // -l = login shell (sources profile files like .zprofile) - // -i = interactive shell (sources rc files like .zshrc) - // We use a marker to separate shell startup output from env output - const output = execSync(`${shell} -l -i -c 'echo __ENV_START__ && env'`, { - encoding: 'utf-8', - timeout: 5000, - env: { - HOME: process.env.HOME, - USER: process.env.USER, - SHELL: shell, - TERM: 'xterm-256color', - TMPDIR: process.env.TMPDIR, - // Prevent macOS from showing "Install Command Line Developer Tools" dialog - // when the shell hits the /usr/bin/git shim on systems without Xcode CLT - APPLE_SUPPRESS_DEVELOPER_TOOL_POPUP: '1', - GIT_TERMINAL_PROMPT: '0', - }, - stdio: ['pipe', 'pipe', 'pipe'], - }) - - // Parse environment after marker and set variables (excluding blocked ones) - const envSection = output.split('__ENV_START__')[1] || '' - let count = 0 - for (const line of envSection.trim().split('\n')) { - const eq = line.indexOf('=') - if (eq > 0) { - const key = line.substring(0, eq) - if (shouldSkipEnvVar(key)) continue - const value = line.substring(eq + 1) - process.env[key] = value - count++ - } - } - - mainLog.info(`[shell-env] Loaded ${count} environment variables`) - - // Log PATH for debugging - if (process.env.PATH) { - const pathCount = process.env.PATH.split(':').length - mainLog.info(`[shell-env] PATH has ${pathCount} entries`) - } - } catch (error) { - // Don't fail app startup if shell env loading fails - mainLog.warn(`[shell-env] Failed to load shell environment: ${error}`) - mainLog.warn('[shell-env] Adding common paths as fallback') - - // Fallback: add common paths that are likely to be needed - const fallbackPaths = [ - '/opt/homebrew/bin', - '/opt/homebrew/sbin', - '/usr/local/bin', - '/usr/local/sbin', - `${process.env.HOME}/.local/bin`, - `${process.env.HOME}/.bun/bin`, - `${process.env.HOME}/.cargo/bin`, - ] - - const currentPath = process.env.PATH || '/usr/bin:/bin:/usr/sbin:/sbin' - const newPath = [...fallbackPaths, ...currentPath.split(':')] - .filter((p, i, arr) => arr.indexOf(p) === i) // dedupe - .join(':') - - process.env.PATH = newPath - } -} diff --git a/packages/desktop/apps/electron/src/main/shims/abort-controller.cjs b/packages/desktop/apps/electron/src/main/shims/abort-controller.cjs deleted file mode 100644 index 243d737d087..00000000000 --- a/packages/desktop/apps/electron/src/main/shims/abort-controller.cjs +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Shim: replaces the bundled `abort-controller@3` polyfill with Node's native - * `AbortController` / `AbortSignal` globals. Paired with `node-fetch.cjs` to - * eliminate the signal-class realm mismatch (see comments there). - * - * Wired in via esbuild's `--alias:abort-controller=...` flag. - */ -module.exports = { - AbortController: globalThis.AbortController, - AbortSignal: globalThis.AbortSignal, -} -module.exports.default = module.exports diff --git a/packages/desktop/apps/electron/src/main/shims/node-fetch.cjs b/packages/desktop/apps/electron/src/main/shims/node-fetch.cjs deleted file mode 100644 index 3e8616264ac..00000000000 --- a/packages/desktop/apps/electron/src/main/shims/node-fetch.cjs +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Shim: replaces the bundled `node-fetch@2` with Electron/Node 18+ native `fetch`. - * - * Why: grammY's `shim.node.js` imports `node-fetch` and `abort-controller`. - * When esbuild bundles `abort-controller`'s `class AbortSignal`, it renames it - * to `_AbortSignal` to avoid collision with the global, which breaks - * `node-fetch@2`'s check `signal.constructor.name === 'AbortSignal'`. - * - * Native `fetch` (undici) accepts the global `AbortSignal` natively and is - * faster, so we sidestep both polyfills. This file is wired in via esbuild's - * `--alias:node-fetch=...` flag in package.json's build:main script. - */ -module.exports = globalThis.fetch.bind(globalThis) -module.exports.default = globalThis.fetch.bind(globalThis) -module.exports.Headers = globalThis.Headers -module.exports.Request = globalThis.Request -module.exports.Response = globalThis.Response diff --git a/packages/desktop/apps/electron/src/main/thumbnail-protocol.ts b/packages/desktop/apps/electron/src/main/thumbnail-protocol.ts deleted file mode 100644 index 6e1cf02863c..00000000000 --- a/packages/desktop/apps/electron/src/main/thumbnail-protocol.ts +++ /dev/null @@ -1,199 +0,0 @@ -/** - * Thumbnail Protocol Handler - * - * Registers a custom `thumbnail://` protocol that serves thumbnail images - * for files in the session sidebar. The browser handles all async loading - * natively via . - * - * Thumbnail generation strategy (cross-platform): - * - macOS/Windows: nativeImage.createThumbnailFromPath() — uses OS-level - * thumbnail cache (Quick Look / Shell API). Fast (~5ms cached), handles - * images, PDFs, Office docs automatically. - * - Linux: nativeImage.createFromPath() + resize() — uses Chromium's Skia - * engine. Works for images only. No PDF/Office support. - * - * Caching: - * - In-memory LRU map keyed on `path + mtime`. Cache miss triggers generation. - * - Entries auto-invalidate when file mtime changes (e.g. after file watcher fires). - * - Capped at MAX_CACHE_ENTRIES to bound memory usage. - */ - -import { protocol, nativeImage } from 'electron' -import { stat } from 'fs/promises' -import { isAbsolute } from 'path' -import { mainLog } from './logger' - -/** Thumbnail output size in pixels (width and height) */ -const THUMBNAIL_SIZE = 64 - -/** Maximum entries in the in-memory LRU cache */ -const MAX_CACHE_ENTRIES = 200 - -/** File extensions that support thumbnail generation */ -const IMAGE_EXTENSIONS = new Set([ - 'png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'tiff', 'tif', 'ico', 'heic', 'heif', -]) - -/** Extensions that only work via OS thumbnail API (macOS/Windows) */ -const OS_THUMBNAIL_EXTENSIONS = new Set([ - 'pdf', 'svg', 'psd', 'ai', -]) - -/** All extensions we can potentially thumbnail */ -const ALL_PREVIEWABLE = new Set([...IMAGE_EXTENSIONS, ...OS_THUMBNAIL_EXTENSIONS]) - -// In-memory LRU cache: path -> { mtime, data } -const cache = new Map() - -/** - * Evict oldest entries when cache exceeds max size. - * Map iterates in insertion order, so first entries are oldest. - */ -function evictIfNeeded(): void { - while (cache.size > MAX_CACHE_ENTRIES) { - const oldestKey = cache.keys().next().value - if (oldestKey) cache.delete(oldestKey) - } -} - -/** - * Check if the current platform supports OS-level thumbnail generation. - * nativeImage.createThumbnailFromPath() is only available on macOS and Windows. - */ -const supportsOSThumbnails = process.platform === 'darwin' || process.platform === 'win32' - -/** - * Generate a thumbnail buffer for the given file path. - * Returns a PNG buffer or null if generation fails/unsupported. - */ -async function generateThumbnail(filePath: string, ext: string): Promise { - // Strategy 1: OS-level thumbnail (macOS/Windows) — handles images + PDFs + more - if (supportsOSThumbnails) { - try { - const thumbnail = await nativeImage.createThumbnailFromPath(filePath, { - width: THUMBNAIL_SIZE, - height: THUMBNAIL_SIZE, - }) - if (!thumbnail.isEmpty()) { - return thumbnail.toPNG() - } - } catch { - // OS thumbnail failed — fall through to Skia-based fallback for images - } - } - - // Strategy 2: Skia-based resize (all platforms) — images only - if (IMAGE_EXTENSIONS.has(ext)) { - try { - const img = nativeImage.createFromPath(filePath) - if (img.isEmpty()) return null - const resized = img.resize({ width: THUMBNAIL_SIZE, height: THUMBNAIL_SIZE }) - return resized.toPNG() - } catch { - return null - } - } - - // Unsupported file type on this platform - return null -} - -/** - * Register the thumbnail:// custom protocol scheme. - * MUST be called before app.whenReady() — Electron requires scheme - * registration during the earliest phase of app initialization. - */ -export function registerThumbnailScheme(): void { - protocol.registerSchemesAsPrivileged([ - { - scheme: 'thumbnail', - privileges: { - // Allow the renderer to fetch from this scheme - supportFetchAPI: true, - // Standard scheme allows normal URL parsing (host, path, etc.) - standard: true, - // Allow cross-origin access from the renderer - corsEnabled: true, - // Stream support for efficient response delivery - stream: true, - }, - }, - ]) -} - -/** - * Register the thumbnail:// protocol handler. - * Must be called after app.whenReady() — the handler processes - * incoming requests and returns thumbnail image responses. - * - * URL format: thumbnail://thumb/ - * Examples: - * macOS: thumbnail://thumb/%2FUsers%2Ffoo%2Fimage.png - * Windows: thumbnail://thumb/C%3A%5CUsers%5Cfoo%5Cimage.png - */ -export function registerThumbnailHandler(): void { - protocol.handle('thumbnail', async (request) => { - try { - // Parse the file path from the URL - // Format: thumbnail://thumb/ - // URL.pathname includes a leading /, so we strip it before decoding - const url = new URL(request.url) - const filePath = decodeURIComponent(url.pathname.slice(1)) - - // Basic validation: must be an absolute path (works on all platforms) - if (!filePath || !isAbsolute(filePath)) { - return new Response(null, { status: 400 }) - } - - // Check file extension is previewable - const ext = filePath.split('.').pop()?.toLowerCase() || '' - if (!ALL_PREVIEWABLE.has(ext)) { - return new Response(null, { status: 404 }) - } - - // Get file mtime for cache validation - let mtime: number - try { - const fileStat = await stat(filePath) - mtime = fileStat.mtimeMs - } catch { - // File doesn't exist or is inaccessible - return new Response(null, { status: 404 }) - } - - // Check cache — hit if path matches AND mtime hasn't changed - const cached = cache.get(filePath) - if (cached && cached.mtime === mtime) { - return new Response(new Uint8Array(cached.data), { - headers: { - 'Content-Type': 'image/png', - 'Cache-Control': 'max-age=3600', - }, - }) - } - - // Cache miss — generate thumbnail - const data = await generateThumbnail(filePath, ext) - if (!data) { - return new Response(null, { status: 404 }) - } - - // Store in cache (move to end for LRU behavior by delete+set) - cache.delete(filePath) - cache.set(filePath, { mtime, data }) - evictIfNeeded() - - return new Response(new Uint8Array(data), { - headers: { - 'Content-Type': 'image/png', - 'Cache-Control': 'max-age=3600', - }, - }) - } catch (error) { - mainLog.error('Thumbnail protocol error:', error) - return new Response(null, { status: 500 }) - } - }) - - mainLog.info('Registered thumbnail:// protocol handler') -} diff --git a/packages/desktop/apps/electron/src/main/voice/__tests__/frame-trust.test.ts b/packages/desktop/apps/electron/src/main/voice/__tests__/frame-trust.test.ts deleted file mode 100644 index c7d1a956a17..00000000000 --- a/packages/desktop/apps/electron/src/main/voice/__tests__/frame-trust.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Tests for the voice-token frame-trust gate. This is the SOLE guard deciding - * whether a frame receives the voice stream URL (which embeds the RPC server - * token), so a regression here leaks ASR credentials to a malicious iframe. - */ -import { describe, it, expect } from 'bun:test'; -import { - getRendererDevOrigin, - isTrustedRendererFrameUrl, -} from '../frame-trust'; - -const DEV_URL = 'http://localhost:5173'; - -describe('getRendererDevOrigin', () => { - it('returns undefined in production (no dev server url)', () => { - expect(getRendererDevOrigin(undefined)).toBeUndefined(); - expect(getRendererDevOrigin('')).toBeUndefined(); - }); - - it('derives the origin from a valid dev server url', () => { - expect(getRendererDevOrigin('http://localhost:5173/')).toBe(DEV_URL); - expect(getRendererDevOrigin('http://localhost:5173/index.html?x=1')).toBe( - DEV_URL, - ); - }); - - it('returns undefined for a malformed dev server url (no throw)', () => { - expect(getRendererDevOrigin('not-a-url')).toBeUndefined(); - }); -}); - -describe('isTrustedRendererFrameUrl', () => { - it('(a) trusts a file:// frame in production', () => { - expect(isTrustedRendererFrameUrl('file:///app/index.html', undefined)).toBe( - true, - ); - }); - - it('(b) trusts a frame whose origin matches the dev server', () => { - expect( - isTrustedRendererFrameUrl('http://localhost:5173/index.html', DEV_URL), - ).toBe(true); - // Origin match only — a different path on the same origin is still trusted. - expect( - isTrustedRendererFrameUrl( - 'http://localhost:5173/deep/path?q=1#h', - DEV_URL, - ), - ).toBe(true); - }); - - it('(c) does NOT trust a cross-origin frame', () => { - expect( - isTrustedRendererFrameUrl('https://evil.example.com/x', DEV_URL), - ).toBe(false); - // Same host, different port/scheme is still a different origin. - expect( - isTrustedRendererFrameUrl('http://localhost:6006/index.html', DEV_URL), - ).toBe(false); - expect( - isTrustedRendererFrameUrl('https://localhost:5173/index.html', DEV_URL), - ).toBe(false); - }); - - it('(d) does NOT trust an undefined frame url', () => { - expect(isTrustedRendererFrameUrl(undefined, DEV_URL)).toBe(false); - expect(isTrustedRendererFrameUrl('', DEV_URL)).toBe(false); - }); - - it('(e) does NOT trust a malformed frame url (no throw)', () => { - expect(isTrustedRendererFrameUrl('http://[bad', DEV_URL)).toBe(false); - expect(isTrustedRendererFrameUrl('::::', DEV_URL)).toBe(false); - }); - - it('(f) flows VITE_DEV_SERVER_URL through: a dev-origin frame is trusted only when the dev url is set', () => { - // With the dev url set, the dev-origin frame is trusted. - expect( - isTrustedRendererFrameUrl('http://localhost:5173/index.html', DEV_URL), - ).toBe(true); - // In production (dev url undefined) the same frame is NOT trusted. - expect( - isTrustedRendererFrameUrl('http://localhost:5173/index.html', undefined), - ).toBe(false); - // A malformed dev url falls back to no trusted dev origin. - expect( - isTrustedRendererFrameUrl('http://localhost:5173/index.html', 'not-a-url'), - ).toBe(false); - }); -}); diff --git a/packages/desktop/apps/electron/src/main/voice/frame-trust.ts b/packages/desktop/apps/electron/src/main/voice/frame-trust.ts deleted file mode 100644 index 61615e9d567..00000000000 --- a/packages/desktop/apps/electron/src/main/voice/frame-trust.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Trust gate for the voice stream URL, which embeds the loopback RPC server - * token. Extracted from main/index.ts (pure — no Electron deps) so the sole - * guard deciding whether a frame receives the token is unit-testable without - * booting Electron. - */ - -/** - * Derive the renderer dev-server origin from a `VITE_DEV_SERVER_URL` value. - * Returns undefined when unset or unparseable (production builds have no dev - * server, so there is no dev origin to trust). - */ -export function getRendererDevOrigin( - devServerUrl: string | undefined, -): string | undefined { - if (!devServerUrl) return undefined; - try { - return new URL(devServerUrl).origin; - } catch { - return undefined; - } -} - -/** - * The app's own renderer is loaded from `file://` (packaged, via loadFile) or - * the Vite dev server (development). Anything else — an injected/cross-origin - * frame or a stray webview — is untrusted. Mirrors the will-navigate origin - * trust check in window-manager. - * - * @param url The frame URL to vet (undefined when the frame has none). - * @param devServerUrl The `VITE_DEV_SERVER_URL` env value (undefined in prod). - */ -export function isTrustedRendererFrameUrl( - url: string | undefined, - devServerUrl: string | undefined, -): boolean { - if (!url) return false; - if (url.startsWith('file://')) return true; - const devOrigin = getRendererDevOrigin(devServerUrl); - if (!devOrigin) return false; - try { - return new URL(url).origin === devOrigin; - } catch { - return false; - } -} diff --git a/packages/desktop/apps/electron/src/main/window-manager.ts b/packages/desktop/apps/electron/src/main/window-manager.ts deleted file mode 100644 index 86f64a057d9..00000000000 --- a/packages/desktop/apps/electron/src/main/window-manager.ts +++ /dev/null @@ -1,788 +0,0 @@ -import { BrowserWindow, shell, nativeTheme, Menu, app, screen } from 'electron' -import { windowLog } from './logger' -import { join } from 'path' -import { existsSync } from 'fs' -import { release } from 'os' -import { RPC_CHANNELS, type WindowCloseRequestSource } from '../shared/types' -import { BRAND } from '@craft-agent/shared/branding' -import type { SavedWindow } from './window-state' -import { - getPetWindowBounds, - setPetWindowBounds, -} from '@craft-agent/shared/config/storage' - -// Vite dev server URL for hot reload -const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL - -/** - * Get the appropriate background material for Windows transparency effects - * - Windows 11 (build 22000+): Mica effect - * - Windows 10 1809+ (build 17763+): Acrylic effect - * - Older versions: No transparency - */ -function getWindowsBackgroundMaterial(): 'mica' | 'acrylic' | undefined { - if (process.platform !== 'win32') return undefined - - // os.release() returns "10.0.xxxxx" where xxxxx is the build number - const buildNumber = parseInt(release().split('.')[2] || '0', 10) - - if (buildNumber >= 22000) { - windowLog.info('Windows 11 detected (build ' + buildNumber + '), using Mica') - return 'mica' - } else if (buildNumber >= 17763) { - windowLog.info('Windows 10 1809+ detected (build ' + buildNumber + '), using Acrylic') - return 'acrylic' - } - - windowLog.info('Older Windows detected (build ' + buildNumber + '), no transparency') - return undefined -} - - -interface ManagedWindow { - window: BrowserWindow - workspaceId: string -} - -export interface CreateWindowOptions { - /** The workspace to open (empty string for onboarding) */ - workspaceId: string - /** Whether to open in focused mode (smaller window, no sidebars) */ - focused?: boolean - /** Deep link URL to navigate to after window loads (without ?window= param) */ - initialDeepLink?: string - /** Full URL to restore from saved state (preserves route/query params) */ - restoreUrl?: string -} - -export class WindowManager { - private windows: Map = new Map() // webContents.id → ManagedWindow - private petWindow: BrowserWindow | null = null // floating desktop-pet window (not a managed workspace window) - private petWorkspaceId = '' // workspace the pet window subscribes to for activity - private focusedModeWindows: Set = new Set() // webContents.id of windows in focused mode - private pendingCloseTimeouts: Map = new Map() // Fallback timeouts for window close - private eventSink: ((channel: string, target: import('@craft-agent/shared/protocol').PushTarget, ...args: any[]) => void) | null = null - private clientResolver: ((wcId: number) => string | undefined) | null = null - private keyboardCloseIntents: Set = new Set() // webContents.id flagged by Cmd/Ctrl+W before close - private keyboardCloseIntentTimeouts: Map = new Map() // Auto-clear stale keyboard-close intents - private isAppQuitting = false // Skip layered close interception during app quit - - /** - * Set the event sink and client resolver for pushing events via the RPC server - * instead of webContents.send. Called after server creation. - */ - setRpcEventSink( - sink: (channel: string, target: import('@craft-agent/shared/protocol').PushTarget, ...args: any[]) => void, - resolver: (wcId: number) => string | undefined - ): void { - this.eventSink = sink - this.clientResolver = resolver - } - - /** Return current RPC event sink, if transport has been initialized. */ - getRpcEventSink(): ((channel: string, target: import('@craft-agent/shared/protocol').PushTarget, ...args: any[]) => void) | null { - return this.eventSink - } - - /** Resolve a window's current clientId from transport handshake state. */ - getClientIdForWindow(webContentsId: number): string | undefined { - return this.clientResolver?.(webContentsId) - } - - /** Push an event to a specific window via the RPC event sink. Falls back to webContents.send. */ - private pushToWindow(window: BrowserWindow, channel: string, ...args: any[]): void { - if (this.eventSink && this.clientResolver) { - const clientId = this.clientResolver(window.webContents.id) - if (clientId) { - this.eventSink(channel, { to: 'client', clientId }, ...args) - return - } - } - // Fallback: direct webContents.send (used before WS handshake completes) - if (!window.isDestroyed() && !window.webContents.isDestroyed() && window.webContents.mainFrame) { - window.webContents.send(channel, ...args) - } - } - - /** - * Create a new window for a workspace - * @param options - Window creation options - */ - createWindow(options: CreateWindowOptions): BrowserWindow { - const { workspaceId, focused = false, initialDeepLink, restoreUrl } = options - - // Load platform-specific app icon - // In packaged app, resources are at dist/resources/ (same level as __dirname) - // In dev, resources are at ../resources/ (sibling of dist/) - const getIconPath = () => { - const iconPath = process.platform === 'darwin' ? BRAND.assets.macIcon - : process.platform === 'win32' ? BRAND.assets.winIcon - : BRAND.assets.linuxIcon - return [ - join(__dirname, iconPath), - join(__dirname, '..', iconPath), - ].find(p => existsSync(p)) ?? join(__dirname, '..', iconPath) - } - - const iconPath = getIconPath() - const iconExists = existsSync(iconPath) - - if (!iconExists) { - windowLog.warn('App icon not found at:', iconPath) - } - - // Use smaller window size for focused mode (single session view) - const windowWidth = focused ? 900 : 1400 - const windowHeight = focused ? 700 : 900 - - // Platform-specific window options - const isMac = process.platform === 'darwin' - const isWindows = process.platform === 'win32' - const windowsBackgroundMaterial = getWindowsBackgroundMaterial() - - const window = new BrowserWindow({ - width: windowWidth, - height: windowHeight, - minWidth: 800, - minHeight: 600, - show: false, // Don't show until ready-to-show event (faster perceived startup) - title: '', - icon: iconExists ? iconPath : undefined, - // macOS-specific: hidden title bar with inset traffic lights - ...(isMac && { - titleBarStyle: 'hiddenInset', - trafficLightPosition: { x: 18, y: 16 }, - vibrancy: 'under-window', - visualEffectState: 'active', - }), - // Windows: use native frame with Mica/Acrylic transparency (Windows 10/11) - ...(isWindows && { - frame: true, // Keep native frame for better UX - autoHideMenuBar: true, // Menu is null on Windows, this is just for safety - // Note: Don't use transparent:true with backgroundMaterial - it hides the window frame - ...(windowsBackgroundMaterial && { - backgroundMaterial: windowsBackgroundMaterial, - }), - }), - // Linux: use native frame - ...(!isMac && !isWindows && { - frame: true, - autoHideMenuBar: true, - }), - webPreferences: { - preload: join(__dirname, 'bootstrap-preload.cjs'), - contextIsolation: true, - nodeIntegration: false, - sandbox: false, - webviewTag: false // Browser integration uses WebContentsView, not - } - }) - - // Show window when first paint is ready (faster perceived startup) - window.once('ready-to-show', () => { - window.show() - }) - - // Open external links in default browser - window.webContents.setWindowOpenHandler((details) => { - shell.openExternal(details.url) - return { action: 'deny' } - }) - - // Handle external navigation attempts from renderer WebContents - window.webContents.on('will-navigate', (event, url) => { - // Allow navigation within the app (file:// in prod, localhost dev server) - const isInternalUrl = url.startsWith('file://') || - (VITE_DEV_SERVER_URL && url.startsWith(VITE_DEV_SERVER_URL)) - - if (!isInternalUrl) { - event.preventDefault() - shell.openExternal(url) - } - }) - - // Enable right-click context menu in development - if (!app.isPackaged) { - window.webContents.on('context-menu', (_event, params) => { - Menu.buildFromTemplate([ - { label: 'Inspect Element', click: () => window.webContents.inspectElement(params.x, params.y) }, - { type: 'separator' }, - { label: 'Cut', role: 'cut', enabled: params.editFlags.canCut }, - { label: 'Copy', role: 'copy', enabled: params.editFlags.canCopy }, - { label: 'Paste', role: 'paste', enabled: params.editFlags.canPaste }, - ]).popup() - }) - } - - // Store the window mapping BEFORE loadURL — bootstrap preload uses - // __get-workspace-id (via sendSync) which reads this map during eval. - const webContentsId = window.webContents.id - this.windows.set(webContentsId, { window, workspaceId }) - - // Track focused mode state for persistence - if (focused) { - this.focusedModeWindows.add(webContentsId) - } - - // Load the renderer - use restoreUrl if provided, otherwise build from options - if (restoreUrl) { - // Restore from saved URL - need to adapt for dev vs prod - if (VITE_DEV_SERVER_URL) { - // In dev mode, replace the base URL but keep the path and query - try { - const savedUrl = new URL(restoreUrl) - const devUrl = new URL(VITE_DEV_SERVER_URL) - // Preserve pathname and search from saved URL, use dev server host - devUrl.pathname = savedUrl.pathname - devUrl.search = savedUrl.search - window.loadURL(devUrl.toString()) - } catch { - // Fallback if URL parsing fails - windowLog.warn('Failed to parse restoreUrl, using default:', restoreUrl) - const params = new URLSearchParams({ workspaceId, ...(focused && { focused: 'true' }) }).toString() - window.loadURL(`${VITE_DEV_SERVER_URL}?${params}`) - } - } else { - // In prod, always extract query params and load from current __dirname. - // Never load file:// URLs directly — the path may be stale (e.g. Linux AppImage - // mounts to a different /tmp dir on each launch). See #13. - try { - const savedUrl = new URL(restoreUrl) - const query: Record = {} - savedUrl.searchParams.forEach((value, key) => { query[key] = value }) - window.loadFile(join(__dirname, 'renderer/index.html'), { query }) - } catch { - window.loadFile(join(__dirname, 'renderer/index.html'), { query: { workspaceId } }) - } - } - } else { - // Build URL from options - const query: Record = { workspaceId } - if (focused) { - query.focused = 'true' // Open in focused mode (no sidebars) - } - - if (VITE_DEV_SERVER_URL) { - const params = new URLSearchParams(query).toString() - window.loadURL(`${VITE_DEV_SERVER_URL}?${params}`) - } else { - window.loadFile(join(__dirname, 'renderer/index.html'), { query }) - } - } - - // Fallback: if the renderer fails to load (e.g. stale path, disk error), - // recover gracefully by loading the default state instead of showing a white screen. See #13. - // In dev mode, retry the Vite dev server (it may not be ready yet) instead of falling back - // to file:// which doesn't exist during development. - let failLoadRetries = 0 - window.webContents.on('did-fail-load', (_event, errorCode, errorDescription) => { - windowLog.warn('Failed to load renderer:', errorCode, errorDescription) - if (VITE_DEV_SERVER_URL && failLoadRetries < 5) { - failLoadRetries++ - windowLog.info(`Retrying Vite dev server (attempt ${failLoadRetries}/5)...`) - setTimeout(() => { - const params = new URLSearchParams({ workspaceId }).toString() - window.loadURL(`${VITE_DEV_SERVER_URL}?${params}`) - }, 1000) - } else { - window.loadFile(join(__dirname, 'renderer/index.html'), { query: { workspaceId } }) - } - }) - - // If an initial deep link was provided, navigate to it after the window is ready - if (initialDeepLink) { - window.once('ready-to-show', () => { - // Import parseDeepLink dynamically to avoid circular dependency - import('./deep-link').then(({ parseDeepLink }) => { - const target = parseDeepLink(initialDeepLink) - if (target && (target.view || target.action)) { - // Wait a bit for React to mount and register IPC listeners - setTimeout(() => { - this.pushToWindow(window, RPC_CHANNELS.deeplink.NAVIGATE, { - view: target.view, - action: target.action, - actionParams: target.actionParams, - }) - }, 100) - } - }) - }) - } - - // Listen for system theme changes and notify this window's renderer - const themeHandler = () => { - this.pushToWindow(window, RPC_CHANNELS.theme.SYSTEM_CHANGED, nativeTheme.shouldUseDarkColors) - } - nativeTheme.on('updated', themeHandler) - - // Handle focus/blur to broadcast window focus state - window.on('focus', () => { - this.pushToWindow(window, RPC_CHANNELS.window.FOCUS_STATE, true) - }) - window.on('blur', () => { - this.pushToWindow(window, RPC_CHANNELS.window.FOCUS_STATE, false) - }) - - // Detect Cmd/Ctrl+W before close events so renderer can distinguish close source. - // Intent is short-lived to avoid stale classification. - window.webContents.on('before-input-event', (_event, input) => { - if (!input || input.type !== 'keyDown') return - const key = input.key?.toLowerCase?.() - if (key !== 'w') return - - const isCloseShortcut = process.platform === 'darwin' - ? !!input.meta - : !!input.control - - if (!isCloseShortcut) return - - const wcId = window.webContents.id - this.keyboardCloseIntents.add(wcId) - const existingTimeout = this.keyboardCloseIntentTimeouts.get(wcId) - if (existingTimeout) clearTimeout(existingTimeout) - - this.keyboardCloseIntentTimeouts.set(wcId, setTimeout(() => { - this.keyboardCloseIntentTimeouts.delete(wcId) - this.keyboardCloseIntents.delete(wcId) - }, 500)) - }) - - // Handle window close request (traffic-light button, menu close, Cmd/Ctrl+W) - // and send source metadata so renderer can decide layered dismiss vs direct close. - window.on('close', (event) => { - // During app quit, bypass layered close behavior and allow native close flow. - // This preserves expected Cmd+Q semantics (quit app instead of closing overlays/panels first). - if (this.isAppQuitting) { - return - } - - // Check if renderer is ready (mainFrame exists) - if not, allow close directly - if (!window.webContents.isDestroyed() && window.webContents.mainFrame) { - event.preventDefault() - const wcId = window.webContents.id - let source: WindowCloseRequestSource = 'window-button' - if (this.keyboardCloseIntents.has(wcId)) { - source = 'keyboard-shortcut' - this.keyboardCloseIntents.delete(wcId) - const keyboardIntentTimeout = this.keyboardCloseIntentTimeouts.get(wcId) - if (keyboardIntentTimeout) { - clearTimeout(keyboardIntentTimeout) - this.keyboardCloseIntentTimeouts.delete(wcId) - } - } - - // Send close request to renderer - it will either close a modal/panel or confirm close. - this.pushToWindow(window, RPC_CHANNELS.window.CLOSE_REQUESTED, { source }) - - // Fallback timeout: if IPC fails (e.g., on Hyprland/Wayland), force close after 3s. - // Reset timeout on each attempt so active users closing modals aren't interrupted. - const existingTimeout = this.pendingCloseTimeouts.get(wcId) - if (existingTimeout) clearTimeout(existingTimeout) - - this.pendingCloseTimeouts.set(wcId, setTimeout(() => { - this.pendingCloseTimeouts.delete(wcId) - if (!window.isDestroyed()) window.destroy() - }, 3000)) - } - // If renderer not ready, allow default close behavior - }) - - // Handle window closed - clean up theme listener and internal state - window.on('closed', () => { - // Clean up any pending close timeout to prevent memory leaks - const timeout = this.pendingCloseTimeouts.get(webContentsId) - if (timeout) { - clearTimeout(timeout) - this.pendingCloseTimeouts.delete(webContentsId) - } - - // Clean up short-lived keyboard-close intent tracking. - const keyboardIntentTimeout = this.keyboardCloseIntentTimeouts.get(webContentsId) - if (keyboardIntentTimeout) { - clearTimeout(keyboardIntentTimeout) - this.keyboardCloseIntentTimeouts.delete(webContentsId) - } - this.keyboardCloseIntents.delete(webContentsId) - - nativeTheme.removeListener('updated', themeHandler) - this.windows.delete(webContentsId) - this.focusedModeWindows.delete(webContentsId) - windowLog.info(`Window closed for workspace ${workspaceId}`) - }) - - windowLog.info(`Created window for workspace ${workspaceId} (focused: ${focused})`) - return window - } - - /** - * Get window by webContents.id (used by IPC handlers instead of BrowserWindow.fromId) - */ - getWindowByWebContentsId(wcId: number): BrowserWindow | null { - if ( - this.petWindow && - !this.petWindow.isDestroyed() && - this.petWindow.webContents.id === wcId - ) { - return this.petWindow - } - const managed = this.windows.get(wcId) - return managed?.window ?? null - } - - /** - * Get window by workspace ID (returns first match - for backwards compatibility) - */ - getWindowByWorkspace(workspaceId: string): BrowserWindow | null { - for (const managed of this.windows.values()) { - if (managed.workspaceId === workspaceId && !managed.window.isDestroyed()) { - return managed.window - } - } - return null - } - - /** - * Get ALL windows for a workspace (main window + tab content windows) - * Used for broadcasting events to all windows showing the same workspace - */ - getAllWindowsForWorkspace(workspaceId: string): BrowserWindow[] { - const windows: BrowserWindow[] = [] - for (const managed of this.windows.values()) { - if (managed.workspaceId === workspaceId && !managed.window.isDestroyed()) { - windows.push(managed.window) - } - } - // Debug: log registered workspaces when lookup fails - if (windows.length === 0 && this.windows.size > 0) { - const registered = Array.from(this.windows.values()).map(m => m.workspaceId) - windowLog.warn(`No windows for workspace '${workspaceId}', have: [${registered.join(', ')}]`) - } - return windows - } - - /** - * Get workspace ID for a window (by webContents.id) - */ - getWorkspaceForWindow(webContentsId: number): string | null { - if ( - this.petWindow && - !this.petWindow.isDestroyed() && - this.petWindow.webContents.id === webContentsId - ) { - return this.petWorkspaceId - } - const managed = this.windows.get(webContentsId) - return managed?.workspaceId ?? null - } - - /** - * Mark whether the app is in quit flow. - * When true, window close events bypass layered close interception. - */ - setAppQuitting(isQuitting: boolean): void { - this.isAppQuitting = isQuitting - } - - /** - * Close window by webContents.id (triggers close event which may be intercepted) - */ - closeWindow(webContentsId: number): void { - const managed = this.windows.get(webContentsId) - if (managed && !managed.window.isDestroyed()) { - managed.window.close() - } - } - - /** - * Force close window by webContents.id (bypasses close event interception). - * Used when renderer confirms the close action (no modals to close). - */ - forceCloseWindow(webContentsId: number): void { - // Clear any pending close timeout since renderer confirmed - const timeout = this.pendingCloseTimeouts.get(webContentsId) - if (timeout) { - clearTimeout(timeout) - this.pendingCloseTimeouts.delete(webContentsId) - } - - const managed = this.windows.get(webContentsId) - if (managed && !managed.window.isDestroyed()) { - // Remove close listener temporarily to avoid infinite loop, - // then destroy the window directly - managed.window.destroy() - } - } - - /** - * Cancel a pending close request (renderer handled it by closing a modal/panel). - * Clears the fallback timeout so the window stays open. - */ - cancelPendingClose(webContentsId: number): void { - const timeout = this.pendingCloseTimeouts.get(webContentsId) - if (timeout) { - clearTimeout(timeout) - this.pendingCloseTimeouts.delete(webContentsId) - } - } - - /** - * Close window for a specific workspace - */ - closeWindowForWorkspace(workspaceId: string): void { - const window = this.getWindowByWorkspace(workspaceId) - if (window && !window.isDestroyed()) { - window.close() - } - } - - /** - * Update the workspace ID for an existing window (for in-window switching) - * @param webContentsId - The webContents.id of the window - * @param workspaceId - The new workspace ID - * @returns true if window was found and updated, false otherwise - */ - updateWindowWorkspace(webContentsId: number, workspaceId: string): boolean { - const managed = this.windows.get(webContentsId) - if (managed) { - const oldWorkspaceId = managed.workspaceId - managed.workspaceId = workspaceId - windowLog.info(`Updated window ${webContentsId} from workspace ${oldWorkspaceId} to ${workspaceId}`) - return true - } - // Window not found - log for debugging - windowLog.warn(`Cannot update workspace for unknown window ${webContentsId}, registered: [${Array.from(this.windows.keys()).join(', ')}]`) - return false - } - - /** - * Register an existing window with a workspace ID - * Used for re-registration when window mapping is lost (e.g., after refresh) - * @param window - The BrowserWindow to register - * @param workspaceId - The workspace ID to associate with - */ - registerWindow(window: BrowserWindow, workspaceId: string): void { - const webContentsId = window.webContents.id - this.windows.set(webContentsId, { window, workspaceId }) - windowLog.info(`Registered window ${webContentsId} for workspace ${workspaceId}`) - } - - // ---- Floating desktop-pet window ------------------------------------- - - /** The live pet window, or null. */ - getPetWindow(): BrowserWindow | null { - return this.petWindow && !this.petWindow.isDestroyed() - ? this.petWindow - : null - } - - /** Toggle click-through on the pet window (called as the cursor enters/leaves the pet). */ - setPetWindowIgnoreMouse(ignore: boolean): void { - this.getPetWindow()?.setIgnoreMouseEvents(ignore, { forward: true }) - } - - /** - * Show/hide the floating pet window. When already shown, reloads it so a - * newly-selected pet takes effect. The pet window is intentionally NOT a - * managed workspace window (excluded from state persistence + quit logic). - */ - setPetWindowEnabled(enabled: boolean, workspaceId: string): void { - if (!enabled) { - if (this.petWindow && !this.petWindow.isDestroyed()) { - this.petWindow.destroy() - } - this.petWindow = null - return - } - this.petWorkspaceId = workspaceId - const existing = this.getPetWindow() - if (existing) { - this.loadPetWindow(existing, workspaceId) - existing.showInactive() - return - } - this.createPetWindow(workspaceId) - } - - private loadPetWindow(window: BrowserWindow, workspaceId: string): void { - if (VITE_DEV_SERVER_URL) { - const params = new URLSearchParams({ workspaceId }).toString() - void window.loadURL(`${VITE_DEV_SERVER_URL}/pet.html?${params}`) - } else { - void window.loadFile(join(__dirname, 'renderer/pet.html'), { - query: { workspaceId }, - }) - } - } - - private defaultPetPosition( - width: number, - height: number, - ): { x: number; y: number } { - const area = screen.getPrimaryDisplay().workArea - return { - x: Math.round(area.x + area.width - width - 24), - y: Math.round(area.y + area.height - height - 24), - } - } - - private createPetWindow(workspaceId: string): void { - // Tall/wide enough to stack notification cards above the pet; the window is - // transparent + click-through so the empty area is invisible and inert. - const width = 380 - const height = 540 - const saved = getPetWindowBounds() - const { x, y } = saved ?? this.defaultPetPosition(width, height) - - const window = new BrowserWindow({ - width, - height, - x, - y, - show: false, - frame: false, - transparent: true, - resizable: false, - maximizable: false, - minimizable: false, - fullscreenable: false, - skipTaskbar: true, - hasShadow: false, - alwaysOnTop: true, - title: 'Qwen Pet', - webPreferences: { - preload: join(__dirname, 'bootstrap-preload.cjs'), - contextIsolation: true, - nodeIntegration: false, - sandbox: false, - webviewTag: false, - }, - }) - - window.setAlwaysOnTop(true, 'floating') - if (process.platform === 'darwin') { - // skipTransformProcessType: without it, setVisibleOnAllWorkspaces flips the - // whole app to NSApplicationActivationPolicyAccessory, which removes the - // main Dock icon a moment after the pet window shows. Keep the process type. - window.setVisibleOnAllWorkspaces(true, { - visibleOnFullScreen: true, - skipTransformProcessType: true, - }) - } - - // Register BEFORE load so the bootstrap preload's __get-workspace-id resolves. - this.petWindow = window - this.petWorkspaceId = workspaceId - - window.once('ready-to-show', () => window.showInactive()) - window.on('moved', () => { - if (window.isDestroyed()) return - const [px, py] = window.getPosition() - setPetWindowBounds({ x: px, y: py }) - }) - window.on('closed', () => { - if (this.petWindow === window) this.petWindow = null - }) - - this.loadPetWindow(window, workspaceId) - windowLog.info(`Created pet window for workspace ${workspaceId}`) - } - - /** - * Get all managed windows - */ - getAllWindows(): ManagedWindow[] { - return Array.from(this.windows.values()).filter(m => !m.window.isDestroyed()) - } - - /** - * Focus existing window for workspace or create new one - */ - focusOrCreateWindow(workspaceId: string): BrowserWindow { - const existing = this.getWindowByWorkspace(workspaceId) - if (existing) { - if (existing.isMinimized()) { - existing.restore() - } - existing.focus() - return existing - } - return this.createWindow({ workspaceId }) - } - - /** - * Get window states for persistence (includes bounds and focused mode) - * Used by window-state.ts to save/restore windows - */ - getWindowStates(): SavedWindow[] { - return this.getAllWindows().map(managed => { - const webContentsId = managed.window.webContents.id - const isFocused = this.focusedModeWindows.has(webContentsId) - const url = managed.window.webContents.getURL() - return { - type: 'main' as const, - workspaceId: managed.workspaceId, - bounds: managed.window.getBounds(), - ...(isFocused && { focused: true }), - ...(url && { url }), - } - }) - } - - /** - * Check if any windows are open - */ - hasWindows(): boolean { - return this.getAllWindows().length > 0 - } - - /** - * Get the currently focused window - */ - getFocusedWindow(): BrowserWindow | null { - const focused = BrowserWindow.getFocusedWindow() - if (focused && !focused.isDestroyed()) { - return focused - } - return null - } - - /** - * Get the last active window (most recently used) - * Falls back to any available window if none focused - */ - getLastActiveWindow(): BrowserWindow | null { - // First try focused window - const focused = this.getFocusedWindow() - if (focused) { - return focused - } - - // Fall back to any available window - const allWindows = this.getAllWindows() - if (allWindows.length > 0) { - return allWindows[0].window - } - - return null - } - - /** - * Show or hide macOS traffic light buttons (close/minimize/maximize). - * Used to hide them when fullscreen overlays are open to prevent accidental clicks. - * No-op on non-macOS platforms. - */ - setTrafficLightsVisible(webContentsId: number, visible: boolean): void { - if (process.platform !== 'darwin') return - - const managed = this.windows.get(webContentsId) - if (managed && !managed.window.isDestroyed()) { - managed.window.setWindowButtonVisibility(visible) - // Re-apply custom traffic light position after showing buttons - // setWindowButtonVisibility can reset position to default, so we need - // to restore the custom position using the modern setWindowButtonPosition API - if (visible) { - managed.window.setWindowButtonPosition({ x: 18, y: 19 }) - } - } - } -} diff --git a/packages/desktop/apps/electron/src/main/window-state.ts b/packages/desktop/apps/electron/src/main/window-state.ts deleted file mode 100644 index 745e16a847d..00000000000 --- a/packages/desktop/apps/electron/src/main/window-state.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { writeFileSync, existsSync, mkdirSync } from 'fs' -import { readJsonFileSync } from '@craft-agent/shared/utils/files' -import { mainLog } from './logger' -import { join } from 'path' -import { homedir } from 'os' - -export interface WindowBounds { - x: number - y: number - width: number - height: number -} - -export interface SavedWindow { - type: 'main' - workspaceId: string - bounds: WindowBounds - focused?: boolean - // Full URL captured from webContents.getURL() at quit time. - // May be localhost (dev) or file:// (prod) — both are safe to store because - // createWindow() never loads this URL directly. It extracts query params - // (workspaceId, route, focused, etc.) and rebuilds the URL from __dirname - // (prod) or the current dev server (dev). See window-manager.ts restoreUrl. - url?: string -} - -export interface WindowState { - windows: SavedWindow[] - lastFocusedWorkspaceId?: string -} - -const STATE_DIR = process.env.CRAFT_USER_DATA_DIR || process.env.CRAFT_CONFIG_DIR || join(homedir(), '.craft-agent') -const WINDOW_STATE_FILE = join(STATE_DIR, 'window-state.json') - -/** - * Save the current window state (windows with bounds and type) - */ -export function saveWindowState(state: WindowState): void { - try { - // Ensure config directory exists - if (!existsSync(STATE_DIR)) { - mkdirSync(STATE_DIR, { recursive: true }) - } - - writeFileSync(WINDOW_STATE_FILE, JSON.stringify(state, null, 2), 'utf-8') - mainLog.info('[WindowState] Saved window state:', state.windows.length, 'windows') - } catch (error) { - mainLog.error('[WindowState] Failed to save window state:', error) - } -} - -/** - * Load the saved window state - */ -export function loadWindowState(): WindowState | null { - try { - if (!existsSync(WINDOW_STATE_FILE)) { - return null - } - - const raw = readJsonFileSync(WINDOW_STATE_FILE) - - // Validate format - const state = raw as WindowState - if (!Array.isArray(state.windows)) { - mainLog.warn('[WindowState] Invalid window state file, ignoring') - return null - } - - mainLog.info('[WindowState] Loaded window state:', state.windows.length, 'windows') - return state - } catch (error) { - mainLog.error('[WindowState] Failed to load window state:', error) - return null - } -} - -/** - * Clear the saved window state - */ -export function clearWindowState(): void { - try { - if (existsSync(WINDOW_STATE_FILE)) { - writeFileSync(WINDOW_STATE_FILE, JSON.stringify({ windows: [] }, null, 2), 'utf-8') - mainLog.info('[WindowState] Cleared window state') - } - } catch (error) { - mainLog.error('[WindowState] Failed to clear window state:', error) - } -} diff --git a/packages/desktop/apps/electron/src/preload/bootstrap.ts b/packages/desktop/apps/electron/src/preload/bootstrap.ts deleted file mode 100644 index fe8184b32d9..00000000000 --- a/packages/desktop/apps/electron/src/preload/bootstrap.ts +++ /dev/null @@ -1,366 +0,0 @@ -/** - * WS-mode preload — replaces the full IPC preload (index.ts). - * - * Normal mode (local server): - * Creates a RoutedClient that routes LOCAL_ONLY channels to the local - * Electron server and REMOTE_ELIGIBLE channels to whichever server owns - * the active workspace (local or remote). Workspace switches swap the - * workspace client transparently. - * - * Thin-client mode (CRAFT_SERVER_URL): - * Creates a single WsRpcClient connected to the remote server. - * All channels go to the remote server. - * - * On localhost the WS handshake completes in <1ms. The React app takes >100ms - * to initialise, so by the time any component calls an API method, the - * connection is established. - */ - -import '@sentry/electron/preload' -import { contextBridge, ipcRenderer, shell, webUtils } from 'electron' -import { WsRpcClient, type TransportConnectionState } from '../transport/client' -import { RoutedClient } from '../transport/routed-client' -import { buildClientApi } from '../transport/build-api' -import { CHANNEL_MAP } from '../transport/channel-map' -import { createCallbackServer } from '@craft-agent/shared/auth/callback-server' -import { - CLIENT_OPEN_EXTERNAL, - CLIENT_OPEN_PATH, - CLIENT_SHOW_IN_FOLDER, - CLIENT_CONFIRM_DIALOG, - CLIENT_OPEN_FILE_DIALOG, - LOCAL_CLIENT_CAPABILITIES, -} from '@craft-agent/server-core/transport' -import type { ConfirmDialogSpec, FileDialogSpec } from '@craft-agent/server-core/transport' -import type { RpcClient } from '@craft-agent/server-core/transport' -import type { RemoteServerConfig } from '@craft-agent/core/types' -import type { ElectronAPI } from '../shared/types' - -// --------------------------------------------------------------------------- -// Client interface — common surface for both RoutedClient and WsRpcClient -// --------------------------------------------------------------------------- - -interface TransportClient extends RpcClient { - isChannelAvailable(channel: string): boolean - getConnectionState(): TransportConnectionState - onConnectionStateChanged(callback: (state: TransportConnectionState) => void): () => void - reconnectNow(): void -} - -// --------------------------------------------------------------------------- -// Connection setup -// --------------------------------------------------------------------------- - -const webContentsId: number = ipcRenderer.sendSync('__get-web-contents-id') -const isClientOnly = !!process.env.CRAFT_SERVER_URL -const DESKTOP_RPC_REQUEST_TIMEOUT_MS = 150_000 - -let client: TransportClient - -if (isClientOnly) { - // ── Thin-client mode ─────────────────────────────────────────────────── - // Single WsRpcClient connected directly to the remote server. - // No local server, no routing — all channels go to remote. - - const wsUrl = process.env.CRAFT_SERVER_URL! - const wsToken = process.env.CRAFT_SERVER_TOKEN ?? '' - - // Block unencrypted ws:// to non-localhost servers — tokens would be sent in cleartext - const parsed = new URL(wsUrl) - const isLocalhost = parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1' || parsed.hostname === '::1' - if (parsed.protocol === 'ws:' && !isLocalhost) { - throw new Error( - `Refusing to connect to remote server over unencrypted ws://. ` + - `Use wss:// (TLS) for non-localhost connections. ` + - `Set CRAFT_RPC_TLS_CERT/KEY on the server to enable TLS.` - ) - } - - // Workspace ID is optional — if missing, renderer shows a workspace picker - const workspaceId = process.env.CRAFT_WORKSPACE_ID || ipcRenderer.sendSync('__get-workspace-id') || undefined - - const wsClient = new WsRpcClient(wsUrl, { - token: wsToken, - workspaceId, - webContentsId, - autoReconnect: true, - mode: 'remote', - requestTimeout: DESKTOP_RPC_REQUEST_TIMEOUT_MS, - clientCapabilities: [...LOCAL_CLIENT_CAPABILITIES], - }) - wsClient.connect() - client = wsClient - -} else { - // ── Normal mode ──────────────────────────────────────────────────────── - // RoutedClient routes LOCAL_ONLY to local server, REMOTE_ELIGIBLE to - // whichever server owns the workspace (local or remote). - - const wsPort: number = ipcRenderer.sendSync('__get-ws-port') - const wsToken: string = ipcRenderer.sendSync('__get-ws-token') - const workspaceId: string = ipcRenderer.sendSync('__get-workspace-id') - - const localClient = new WsRpcClient(`ws://127.0.0.1:${wsPort}`, { - token: wsToken, - workspaceId, - webContentsId, - autoReconnect: true, - mode: 'local', - requestTimeout: DESKTOP_RPC_REQUEST_TIMEOUT_MS, - clientCapabilities: [...LOCAL_CLIENT_CAPABILITIES], - }) - - // Check if the current workspace is remote (synchronous IPC during preload eval) - const remoteConfig: RemoteServerConfig | null = ipcRenderer.sendSync('__get-workspace-remote-config') - - let initialWorkspaceClient: WsRpcClient - if (remoteConfig && typeof remoteConfig.url === 'string') { - // Workspace is remote — create a direct connection to the remote server - initialWorkspaceClient = new WsRpcClient(remoteConfig.url, { - token: remoteConfig.token, - workspaceId: remoteConfig.remoteWorkspaceId, - webContentsId, - autoReconnect: true, - mode: 'remote', - requestTimeout: DESKTOP_RPC_REQUEST_TIMEOUT_MS, - clientCapabilities: [...LOCAL_CLIENT_CAPABILITIES], - tlsRejectUnauthorized: false, - }) - initialWorkspaceClient.connect() - } else { - // Workspace is local — workspace client IS the local client - initialWorkspaceClient = localClient - } - - const routedClient = new RoutedClient(localClient, initialWorkspaceClient) - - // Set workspace ID mapping if initial workspace is remote - if (remoteConfig) { - routedClient.setWorkspaceMapping(workspaceId, remoteConfig.remoteWorkspaceId) - } - - // Factory for creating remote workspace clients on switch - routedClient.setClientFactory((remoteServer: RemoteServerConfig) => { - return new WsRpcClient(remoteServer.url, { - token: remoteServer.token, - workspaceId: remoteServer.remoteWorkspaceId, - webContentsId, - autoReconnect: true, - mode: 'remote', - requestTimeout: DESKTOP_RPC_REQUEST_TIMEOUT_MS, - clientCapabilities: [...LOCAL_CLIENT_CAPABILITIES], - tlsRejectUnauthorized: false, - }) - }) - - localClient.connect() - client = routedClient -} - -// --------------------------------------------------------------------------- -// Register client-side capability handlers (server can invoke these) -// --------------------------------------------------------------------------- - -client.handleCapability(CLIENT_OPEN_EXTERNAL, (url: string) => shell.openExternal(url)) - -client.handleCapability(CLIENT_OPEN_PATH, async (path: string) => { - const error = await shell.openPath(path) - return { error: error || undefined } -}) - -client.handleCapability(CLIENT_SHOW_IN_FOLDER, (path: string) => { - shell.showItemInFolder(path) -}) - -client.handleCapability(CLIENT_CONFIRM_DIALOG, async (spec: ConfirmDialogSpec) => { - return await ipcRenderer.invoke('__dialog:showMessageBox', spec) -}) - -client.handleCapability(CLIENT_OPEN_FILE_DIALOG, async (spec: FileDialogSpec) => { - return await ipcRenderer.invoke('__dialog:showOpenDialog', spec) -}) - -// --------------------------------------------------------------------------- -// Build ElectronAPI proxy -// --------------------------------------------------------------------------- - -const api = buildClientApi(client, CHANNEL_MAP, (ch) => client.isChannelAvailable(ch)) - -;(api as any).getRuntimeEnvironment = (): 'electron' | 'web' => 'electron' - -// --------------------------------------------------------------------------- -// Transport connection state logging (for remote connections) -// --------------------------------------------------------------------------- - -function formatTransportReason(state: TransportConnectionState): string { - const err = state.lastError - if (err) { - const codePart = err.code ? ` [${err.code}]` : '' - return `${err.kind}${codePart}: ${err.message}` - } - - if (state.lastClose?.code != null) { - const reason = state.lastClose.reason ? ` (${state.lastClose.reason})` : '' - return `close ${state.lastClose.code}${reason}` - } - - return 'no additional details' -} - -// Log remote connection state changes to main process (visible in terminal + main.log). -// Activates whenever the workspace connection is remote (thin client or remote workspace). -client.onConnectionStateChanged((state) => { - if (state.mode !== 'remote') return - - const emitToMain = (level: 'info' | 'warn' | 'error', message: string) => { - ipcRenderer.send('__transport:status', { - level, - message, - status: state.status, - attempt: state.attempt, - nextRetryInMs: state.nextRetryInMs, - error: state.lastError, - close: state.lastClose, - url: state.url, - }) - } - - if (state.status === 'connected') { - const message = `[transport] connected to ${state.url}` - console.info(message) - emitToMain('info', message) - return - } - - if (state.status === 'reconnecting') { - const retry = state.nextRetryInMs != null ? ` retry in ${state.nextRetryInMs}ms` : '' - const message = `[transport] reconnecting (attempt ${state.attempt})${retry} — ${formatTransportReason(state)}` - console.warn(message) - emitToMain('warn', message) - return - } - - if (state.status === 'failed' || state.status === 'disconnected') { - const message = `[transport] ${state.status} — ${formatTransportReason(state)}` - console.error(message) - emitToMain('error', message) - } -}) - -// --------------------------------------------------------------------------- -// Transport state API (exposed to renderer) -// --------------------------------------------------------------------------- - -;(api as any).getTransportConnectionState = async () => client.getConnectionState() -;(api as any).onTransportConnectionStateChanged = (callback: (state: TransportConnectionState) => void) => { - return client.onConnectionStateChanged(callback) -} -;(api as any).reconnectTransport = async () => { - client.reconnectNow() -} -// Voice dictation: the loopback voice WS url (with token). Read lazily each call -// so it reflects the server once it has started. -;(api as any).getVoiceStreamUrl = (): string | null => - ipcRenderer.sendSync('__get-voice-stream-url') - -// ── performOAuth ───────────────────────────────────────────────────────── -// Multi-step orchestration: callback server (local) → oauth:start (server) → -// open browser → wait for callback → oauth:complete (server). -// Runs client-side because the callback server must receive the redirect. -;(api as any).performOAuth = async (args: { - sourceSlug: string - sessionId?: string - authRequestId?: string -}): Promise<{ success: boolean; error?: string; email?: string }> => { - let callbackServer: Awaited> | null = null - let flowId: string | undefined - let state: string | undefined - - try { - // 1. Start local callback server to receive OAuth redirect - callbackServer = await createCallbackServer({ appType: 'electron' }) - const callbackUrl = `${callbackServer.url}/callback` - - // 2. Ask server to prepare the flow (PKCE, auth URL, store in flow store) - const startResult = await client.invoke('oauth:start', { - sourceSlug: args.sourceSlug, - callbackUrl, - sessionId: args.sessionId, - authRequestId: args.authRequestId, - }) - flowId = startResult.flowId - state = startResult.state - - // 3. Open browser for user consent (local — must open on the user's machine, not remote server) - await shell.openExternal(startResult.authUrl) - - // 4. Wait for OAuth provider to redirect to our callback server - const callback = await callbackServer.promise - - // 5. Check for errors from the provider - if (callback.query.error) { - const error = callback.query.error_description || callback.query.error - await client.invoke('oauth:cancel', { flowId, state }) - return { success: false, error } - } - - const code = callback.query.code - if (!code) { - await client.invoke('oauth:cancel', { flowId, state }) - return { success: false, error: 'No authorization code received' } - } - - // 6. Send code to server for token exchange + credential storage - const result = await client.invoke('oauth:complete', { flowId, code, state }) - return { success: result.success, error: result.error, email: result.email } - } catch (err) { - // Clean up server-side flow on error - if (flowId && state) { - client.invoke('oauth:cancel', { flowId, state }).catch(() => {}) - } - return { - success: false, - error: err instanceof Error ? err.message : 'OAuth flow failed', - } - } finally { - callbackServer?.close() - } -} - -// App lifecycle — direct IPC (not WS RPC) since it restarts the server itself -;(api as ElectronAPI).relaunchApp = () => ipcRenderer.invoke('app:relaunch') -;(api as ElectronAPI).removeWorkspace = (workspaceId: string) => ipcRenderer.invoke('workspace:remove', workspaceId) -;(api as ElectronAPI).setWorkspacePinned = (workspaceId: string, pinned: boolean) => ipcRenderer.invoke('workspace:pinned:set', workspaceId, pinned) -;(api as ElectronAPI).reorderWorkspaces = (orderedIds: string[]) => ipcRenderer.invoke('workspace:reorder', orderedIds) -;(api as ElectronAPI).invokeOnServer = (url: string, token: string, channel: string, ...args: any[]) => - ipcRenderer.invoke('server:invokeOnServer', url, token, channel, ...args) -;(api as ElectronAPI).transferSessionToWorkspace = (sessionId: string, targetWorkspaceId: string, sessionIndex?: number, sessionCount?: number) => - ipcRenderer.invoke('session:transferToRemoteWorkspace', sessionId, targetWorkspaceId, sessionIndex, sessionCount) -;(api as ElectronAPI).onTransferProgress = (cb: (progress: { sessionIndex: number; sessionCount: number; chunkSent: number; chunkTotal: number }) => void) => { - const handler = (_e: any, progress: { sessionIndex: number; sessionCount: number; chunkSent: number; chunkTotal: number }) => cb(progress) - ipcRenderer.on('transfer:progress', handler) - return () => { ipcRenderer.removeListener('transfer:progress', handler) } -} - -// System warnings — expose env-based flags set during main process startup -// (preload-only: reads env var directly, no IPC round-trip needed) -;(api as ElectronAPI).getSystemWarnings = async () => ({ - vcredistMissing: process.env.CRAFT_VCREDIST_MISSING === '1', - downloadUrl: process.env.CRAFT_VCREDIST_URL, -}) - -// i18n: sync language changes to main process (for native menus/dialogs) -;(api as ElectronAPI).changeLanguage = (lang: string) => ipcRenderer.invoke('i18n:changeLanguage', lang) - -// webUtils.getPathForFile: returns the absolute OS path of a File object obtained -// from or OS drag-drop. Returns null for Files fabricated from -// Blobs (clipboard paste, web-drag) — those are content-only, no filesystem path. -;(api as ElectronAPI).getFilePath = (file: File) => { - try { - return webUtils.getPathForFile(file) || null - } catch { - return null - } -} - -contextBridge.exposeInMainWorld('electronAPI', api) diff --git a/packages/desktop/apps/electron/src/preload/browser-toolbar.ts b/packages/desktop/apps/electron/src/preload/browser-toolbar.ts deleted file mode 100644 index f170e5c744c..00000000000 --- a/packages/desktop/apps/electron/src/preload/browser-toolbar.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Preload script for browser toolbar windows. - * - * Exposes a minimal API for the React BrowserControls component - * to send navigation actions and receive state updates from the - * main process BrowserPaneManager. - */ - -import { contextBridge, ipcRenderer } from 'electron' - -const CHANNELS = { - NAVIGATE: 'browser-toolbar:navigate', - GO_BACK: 'browser-toolbar:go-back', - GO_FORWARD: 'browser-toolbar:go-forward', - RELOAD: 'browser-toolbar:reload', - STOP: 'browser-toolbar:stop', - MENU_GEOMETRY: 'browser-toolbar:menu-geometry', - FORCE_CLOSE_MENU: 'browser-toolbar:force-close-menu', - TOGGLE_DOCK_EXPANDED: 'browser-toolbar:toggle-dock-expanded', - HIDE: 'browser-toolbar:hide', - DESTROY: 'browser-toolbar:destroy', - STATE_UPDATE: 'browser-toolbar:state-update', - THEME_COLOR: 'browser-toolbar:theme-color', -} as const - -// Instance ID is passed via query parameter by BrowserPaneManager -const instanceId = new URLSearchParams(location.search).get('instanceId') || '' - -contextBridge.exposeInMainWorld('browserToolbar', { - instanceId, - navigate: (url: string) => ipcRenderer.invoke(CHANNELS.NAVIGATE, instanceId, url), - goBack: () => ipcRenderer.invoke(CHANNELS.GO_BACK, instanceId), - goForward: () => ipcRenderer.invoke(CHANNELS.GO_FORWARD, instanceId), - reload: () => ipcRenderer.invoke(CHANNELS.RELOAD, instanceId), - stop: () => ipcRenderer.invoke(CHANNELS.STOP, instanceId), - setMenuGeometry: (open: boolean, height = 0) => ipcRenderer.invoke(CHANNELS.MENU_GEOMETRY, instanceId, open, height), - toggleDockExpanded: () => ipcRenderer.invoke(CHANNELS.TOGGLE_DOCK_EXPANDED, instanceId), - hideWindow: () => ipcRenderer.invoke(CHANNELS.HIDE, instanceId), - closeWindowEntirely: () => ipcRenderer.invoke(CHANNELS.DESTROY, instanceId), - onStateUpdate: (callback: (state: unknown) => void) => { - const handler = (_event: Electron.IpcRendererEvent, state: unknown) => callback(state) - ipcRenderer.on(CHANNELS.STATE_UPDATE, handler) - return () => { ipcRenderer.removeListener(CHANNELS.STATE_UPDATE, handler) } - }, - onThemeColor: (callback: (color: string | null) => void) => { - const handler = (_event: Electron.IpcRendererEvent, color: string | null) => callback(color) - ipcRenderer.on(CHANNELS.THEME_COLOR, handler) - return () => { ipcRenderer.removeListener(CHANNELS.THEME_COLOR, handler) } - }, - onForceCloseMenu: (callback: (payload: { reason?: string }) => void) => { - const handler = (_event: Electron.IpcRendererEvent, payload: { reason?: string }) => callback(payload) - ipcRenderer.on(CHANNELS.FORCE_CLOSE_MENU, handler) - return () => { ipcRenderer.removeListener(CHANNELS.FORCE_CLOSE_MENU, handler) } - }, -}) diff --git a/packages/desktop/apps/electron/src/renderer/App.tsx b/packages/desktop/apps/electron/src/renderer/App.tsx deleted file mode 100644 index b476d170705..00000000000 --- a/packages/desktop/apps/electron/src/renderer/App.tsx +++ /dev/null @@ -1,2678 +0,0 @@ -import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react' -import { flushSync } from 'react-dom' -import { useTranslation } from 'react-i18next' -import { useTheme } from '@/hooks/useTheme' -import type { ThemeOverrides } from '@config/theme' -import { useSetAtom, useStore, useAtomValue, useAtom } from 'jotai' -import type { Session, Workspace, SessionEvent, Message, FileAttachment, StoredAttachment, PermissionRequest, CredentialRequest, CredentialResponse, SetupNeeds, SessionStatus, NewChatActionParams, ContentBadge, LlmConnectionWithStatus, PermissionMode, PermissionModeState, LoadedSkill } from '../shared/types' -import type { SessionDraft, DraftAttachmentRef } from '@craft-agent/shared/config' -import type { SessionOptions, SessionOptionUpdates } from './hooks/useSessionOptions' -import { defaultSessionOptions, mergeSessionOptions } from './hooks/useSessionOptions' -import { generateMessageId } from '../shared/types' -import { useEventProcessor } from './event-processor' -import type { AgentEvent, Effect } from './event-processor' -import { AppShell } from '@/components/app-shell/AppShell' -import { FilePreviewPanel } from '@/components/app-shell/FilePreviewPanel' -import { openUrlInBuiltInBrowser } from '@/components/app-shell/open-url-in-built-in-browser' -import type { AppShellContextType } from '@/context/AppShellContext' -import { OnboardingWizard, ReauthScreen } from '@/components/onboarding' -import { WorkspacePicker } from '@/components/workspace' -import { ResetConfirmationDialog } from '@/components/ResetConfirmationDialog' -import { SplashScreen } from '@/components/SplashScreen' -import { TooltipProvider } from '@craft-agent/ui' -import { FocusProvider } from '@/context/FocusContext' -import { ModalProvider } from '@/context/ModalContext' -import { DismissibleLayerProvider } from '@/context/DismissibleLayerContext' -import { useWindowCloseHandler } from '@/hooks/useWindowCloseHandler' -import { useOnboarding } from '@/hooks/useOnboarding' -import { useNotifications } from '@/hooks/useNotifications' -import { useSession } from '@/hooks/useSession' -import { NavigationProvider } from '@/contexts/NavigationContext' -import { navigate, routes } from './lib/navigate' -import type { ViewRoute } from '../shared/routes' -import { attachmentFromContentRef, toDraftRef } from './lib/drafts' -import { getSessionDeleteNavigationRoute } from './lib/session-delete-navigation' -import { stripMarkdown } from './utils/text' -import { getSessionTitle } from './utils/session' -import { coerceInputText } from './lib/input-text' -import { getSessionsToRefreshAfterStaleReconnect } from './lib/reconnect-recovery' -import { - formatSessionLoadFailure, - mergeSessionRefreshResult, - shouldTreatSessionLoadFailureAsTransportFallback, -} from './lib/session-load' -import { extractWorkspaceSlugFromPath } from '@craft-agent/shared/utils/workspace-slug' -import { DEFAULT_THINKING_LEVEL } from '@craft-agent/shared/agent/thinking-levels' -import { initRendererPerf } from './lib/perf' -import { - initializeSessionsAtom, - initializeWorkspaceSessionsAtom, - addSessionAtom, - removeSessionAtom, - updateSessionAtom, - refreshSessionsMetadataAtom, - sessionAtomFamily, - sessionMetaMapAtom, - loadedSessionsAtom, - forceSessionMessagesReloadAtom, - backgroundTasksAtomFamily, - extractSessionMeta, - getWorkspaceSessionMetas, - mergeStableSessionMetaList, - sessionFromMeta, - workspaceSessionMetaCacheAtom, - workspaceSessionsAtom, - windowWorkspaceIdAtom, - type SessionMeta, -} from '@/atoms/sessions' -import { sourcesAtom } from '@/atoms/sources' -import { skillsAtom } from '@/atoms/skills' -import { extractBadges } from '@/lib/mentions' -import { extractCommandBadges } from '@/lib/slash-command-badges' -import { contentBadgesToTextElements } from '@craft-agent/core/utils' -import { getDefaultStore } from 'jotai' -import { - ShikiThemeProvider, - PlatformProvider, - ImagePreviewOverlay, - PDFPreviewOverlay, - CodePreviewOverlay, - DocumentFormattedMarkdownOverlay, - JSONPreviewOverlay, -} from '@craft-agent/ui' -import { useLinkInterceptor, type FilePreviewState } from '@/hooks/useLinkInterceptor' -import { useTransportConnectionState } from '@/hooks/useTransportConnectionState' -import { useStaleSessionRecovery } from '@/hooks/useStaleSessionRecovery' -import { TransportConnectionBanner, shouldShowTransportConnectionBanner } from '@/components/app-shell/TransportConnectionBanner' -import { getFileManagerName } from '@/lib/platform' -import { ActionRegistryProvider } from '@/actions' -import { toast } from 'sonner' - -type AppState = 'loading' | 'onboarding' | 'reauth' | 'workspace-picker' | 'ready' - -/** Type for the Jotai store returned by useStore() */ -type JotaiStore = ReturnType - -function skillsForBadgeExtraction(skills: LoadedSkill[], skillSlugs?: string[]): LoadedSkill[] { - if (!skillSlugs?.length) return skills - - const seen = new Set(skills.map(skill => skill.slug)) - const syntheticSkills = skillSlugs - .filter(slug => { - if (seen.has(slug)) return false - seen.add(slug) - return true - }) - .map((slug): LoadedSkill => ({ - slug, - metadata: { - name: slug, - description: '', - }, - content: '', - path: '', - source: 'provider', - })) - - return syntheticSkills.length > 0 ? [...skills, ...syntheticSkills] : skills -} - -/** - * Helper to handle background task events from the agent. - * Updates the backgroundTasksAtomFamily based on event type. - * Extracted to avoid code duplication between streaming and non-streaming paths. - */ -function handleBackgroundTaskEvent( - store: JotaiStore, - sessionId: string, - event: { type: string }, - agentEvent: unknown -): void { - // Type guard for accessing properties - const evt = agentEvent as Record - const backgroundTasksAtom = backgroundTasksAtomFamily(sessionId) - - if (event.type === 'task_backgrounded' && 'taskId' in evt && 'toolUseId' in evt) { - const currentTasks = store.get(backgroundTasksAtom) - const exists = currentTasks.some(t => t.toolUseId === evt.toolUseId) - if (!exists) { - store.set(backgroundTasksAtom, [ - ...currentTasks, - { - id: evt.taskId as string, - type: 'agent' as const, - toolUseId: evt.toolUseId as string, - startTime: Date.now(), - elapsedSeconds: 0, - intent: evt.intent as string | undefined, - }, - ]) - } - } else if (event.type === 'shell_backgrounded' && 'shellId' in evt && 'toolUseId' in evt) { - const currentTasks = store.get(backgroundTasksAtom) - const exists = currentTasks.some(t => t.toolUseId === evt.toolUseId) - if (!exists) { - store.set(backgroundTasksAtom, [ - ...currentTasks, - { - id: evt.shellId as string, - type: 'shell' as const, - toolUseId: evt.toolUseId as string, - startTime: Date.now(), - elapsedSeconds: 0, - intent: evt.intent as string | undefined, - }, - ]) - } - } else if (event.type === 'task_progress' && 'toolUseId' in evt && 'elapsedSeconds' in evt) { - const currentTasks = store.get(backgroundTasksAtom) - store.set(backgroundTasksAtom, currentTasks.map(t => - t.toolUseId === evt.toolUseId - ? { ...t, elapsedSeconds: evt.elapsedSeconds as number } - : t - )) - } else if (event.type === 'task_completed' && 'taskId' in evt) { - // Remove task when background task completes - const currentTasks = store.get(backgroundTasksAtom) - store.set(backgroundTasksAtom, currentTasks.filter(t => t.id !== evt.taskId)) - } else if (event.type === 'shell_killed' && 'shellId' in evt) { - // Remove shell task when KillShell succeeds - const currentTasks = store.get(backgroundTasksAtom) - store.set(backgroundTasksAtom, currentTasks.filter(t => t.id !== evt.shellId)) - } else if (event.type === 'tool_result' && 'toolUseId' in evt) { - // Remove task when it completes - but NOT if this is the initial backgrounding result - // Background tasks return immediately with agentId/shell_id/backgroundTaskId, - // we should only remove when the task actually completes - const result = typeof evt.result === 'string' ? evt.result : JSON.stringify(evt.result) - const isBackgroundingResult = result && ( - /agentId:\s*[a-zA-Z0-9_-]+/.test(result) || - /shell_id:\s*[a-zA-Z0-9_-]+/.test(result) || - /"backgroundTaskId":\s*"[a-zA-Z0-9_-]+"/.test(result) - ) - if (!isBackgroundingResult) { - const currentTasks = store.get(backgroundTasksAtom) - store.set(backgroundTasksAtom, currentTasks.filter(t => t.toolUseId !== evt.toolUseId)) - } - } - // Note: We do NOT clear background tasks on complete/error/interrupted - // Background tasks should persist and keep running after the turn ends - // They are only removed when: - // 1. task_completed event arrives (background task finished) - // 2. Their tool_result comes back (foreground task finished) - // 3. KillShell succeeds (shell_killed event) -} - -function SessionLoadErrorScreen({ - message, - onRetry, -}: { - message: string - onRetry: () => void -}) { - const { t } = useTranslation() - - return ( -
-
-

{t("errors.failedToLoadSessions")}

-

- {t("errors.failedToLoadSessionsDesc")} -

-

- {message} -

- -
-
- ) -} - -export default function App() { - const { t } = useTranslation() - - // Initialize renderer perf tracking early (debug mode = running from source) - // Uses useEffect with empty deps to run once on mount before any session switches - useEffect(() => { - window.electronAPI.isDebugMode().then((isDebug) => { - initRendererPerf(isDebug) - }) - }, []) - - // App state: loading -> check auth -> onboarding or ready - const [appState, setAppState] = useState('loading') - const [setupNeeds, setSetupNeeds] = useState(null) - - // Per-session Jotai atom setters for isolated updates - // NOTE: No sessionsAtom - we don't store a Session[] array anywhere to prevent memory leaks - // Instead we use: - // - sessionMetaMapAtom for lightweight listing - // - sessionAtomFamily(id) for individual session data - const initializeSessions = useSetAtom(initializeSessionsAtom) - const initializeWorkspaceSessions = useSetAtom(initializeWorkspaceSessionsAtom) - const addSession = useSetAtom(addSessionAtom) - const removeSession = useSetAtom(removeSessionAtom) - const updateSessionDirect = useSetAtom(updateSessionAtom) - const store = useStore() - - // Helper to update a session by ID with partial fields - // Uses per-session atom directly instead of updating an array - const updateSessionById = useCallback(( - sessionId: string, - updates: Partial | ((session: Session) => Partial) - ) => { - updateSessionDirect(sessionId, (prev) => { - if (!prev) return prev - const partialUpdates = typeof updates === 'function' ? updates(prev) : updates - return { ...prev, ...partialUpdates } - }) - }, [updateSessionDirect]) - - const [workspaces, setWorkspaces] = useState([]) - const workspacesRef = useRef(workspaces) - // Window's workspace ID — shared atom so Root/ThemeProvider stays in sync on switch - const [windowWorkspaceId, setWindowWorkspaceId] = useAtom(windowWorkspaceIdAtom) - const windowWorkspaceIdRef = useRef(windowWorkspaceId) - const sessionListRequestSeqRef = useRef(0) - const workspaceSwitchSeqRef = useRef(0) - const workspaceSwitchChainRef = useRef>(Promise.resolve()) - const pendingWorkspaceSwitchRouteRef = useRef<{ workspaceId: string; route: ViewRoute } | null>(null) - - useEffect(() => { - windowWorkspaceIdRef.current = windowWorkspaceId - }, [windowWorkspaceId]) - - useEffect(() => { - workspacesRef.current = workspaces - }, [workspaces]) - - const consumePendingWorkspaceSwitchRoute = useCallback((workspaceId: string): ViewRoute | null => { - const pending = pendingWorkspaceSwitchRouteRef.current - if (!pending || pending.workspaceId !== workspaceId) return null - pendingWorkspaceSwitchRouteRef.current = null - return pending.route - }, []) - - // Derive workspace slug for SDK skill qualification - const windowWorkspaceSlug = useMemo(() => { - if (!windowWorkspaceId) return null - const workspace = workspaces.find(w => w.id === windowWorkspaceId) - return workspace?.slug ?? windowWorkspaceId - }, [windowWorkspaceId, workspaces]) - - // Get initial sessionId and focused mode from URL params (for "Open in New Window" feature) - const { initialSessionId, isFocusedMode } = useMemo(() => { - const params = new URLSearchParams(window.location.search) - return { - initialSessionId: params.get('sessionId'), - isFocusedMode: params.get('focused') === 'true', - } - }, []) - - // Derive remote workspace ID for session matching in NavigationContext - const windowRemoteWorkspaceId = useMemo(() => { - if (!windowWorkspaceId) return null - const workspace = workspaces.find(w => w.id === windowWorkspaceId) - return workspace?.remoteServer?.remoteWorkspaceId ?? null - }, [windowWorkspaceId, workspaces]) - - // LLM connections with authentication status (for provider selection) - const [llmConnections, setLlmConnections] = useState([]) - // Workspace default LLM connection (for new sessions) - const [workspaceDefaultLlmConnection, setWorkspaceDefaultLlmConnection] = useState() - // Global default LLM connection slug (from app config) - const [defaultLlmConnectionSlug, setDefaultLlmConnectionSlug] = useState() - - // Derive connection default model override from the default LLM connection - const defaultConnection = useMemo(() => { - return llmConnections.find(c => c.slug === defaultLlmConnectionSlug) ?? null - }, [llmConnections, defaultLlmConnectionSlug]) - - const [menuNewChatTrigger, setMenuNewChatTrigger] = useState(0) - // Permission requests per session (queue to handle multiple concurrent requests) - const [pendingPermissions, setPendingPermissions] = useState>(new Map()) - // Credential requests per session (queue to handle multiple concurrent requests) - const [pendingCredentials, setPendingCredentials] = useState>(new Map()) - // Draft composer state per session (text + attachment refs), preserved across mode - // switches, conversation changes, and app restarts. Using a ref avoids re-renders - // during typing; attachments are stored as lightweight refs (path + name) and - // hydrated via readFileAttachment() on session switch. - const sessionDraftsRef = useRef>(new Map()) - // Unified session options for all session-scoped settings - const [sessionOptions, setSessionOptions] = useState>(new Map()) - const [globalPermissionMode, setGlobalPermissionMode] = useState( - defaultSessionOptions.permissionMode, - ) - - // Theme state (app-level only) - const [appTheme, setAppTheme] = useState(null) - // Reset confirmation dialog - const [showResetDialog, setShowResetDialog] = useState(false) - - // Splash screen state - tracks when app is fully ready (all data loaded) - const [sessionsLoaded, setSessionsLoaded] = useState(false) - const [sessionListLoading, setSessionListLoading] = useState(false) - const [sessionListRefreshWorkspaceIds, setSessionListRefreshWorkspaceIds] = useState>(new Set()) - const [projectSessionSnapshotsReady, setProjectSessionSnapshotsReady] = useState(false) - const [sessionLoadError, setSessionLoadError] = useState(null) - const [splashExiting, setSplashExiting] = useState(false) - const [splashHidden, setSplashHidden] = useState(false) - - // Notifications enabled state (from app settings) - const [notificationsEnabled, setNotificationsEnabled] = useState(true) - - // Sources and skills for badge extraction - const sources = useAtomValue(sourcesAtom) - const skills = useAtomValue(skillsAtom) - - // Compute if app is fully ready (all data loaded) - const isFullyReady = appState === 'ready' && sessionsLoaded && (sessionLoadError || projectSessionSnapshotsReady) - - // Trigger splash exit animation when fully ready - useEffect(() => { - if (isFullyReady && !splashExiting) { - setSplashExiting(true) - } - }, [isFullyReady, splashExiting]) - - // Handler for when splash exit animation completes - const handleSplashExitComplete = useCallback(() => { - setSplashHidden(true) - }, []) - - // Apply theme via hook (injects CSS variables) - // shikiTheme is passed to ShikiThemeProvider to ensure correct syntax highlighting - // theme for dark-only themes in light system mode - const { shikiTheme, isDark } = useTheme({ appTheme }) - - // Ref for sessionOptions to access current value in event handlers without re-registering - const sessionOptionsRef = useRef(sessionOptions) - // Keep ref in sync with state - useEffect(() => { - sessionOptionsRef.current = sessionOptions - }, [sessionOptions]) - - useEffect(() => { - setSessionOptions(prev => { - if (prev.size === 0) return prev - const next = new Map(prev) - for (const [sessionId, options] of next) { - next.set(sessionId, { ...options, permissionMode: globalPermissionMode }) - } - return next - }) - }, [globalPermissionMode]) - - useEffect(() => { - let cancelled = false - window.electronAPI.getGlobalPermissionMode() - .then((mode) => { - if (!cancelled) setGlobalPermissionMode(mode) - }) - .catch((error) => { - console.warn('[App] Failed to load global permission mode:', error) - }) - return () => { - cancelled = true - } - }, []) - - const applyPermissionModeState = useCallback((sessionId: string, state: PermissionModeState, source: 'event' | 'reconcile') => { - setSessionOptions(prev => { - const next = new Map(prev) - const current = { - ...defaultSessionOptions, - permissionMode: state.permissionMode, - ...next.get(sessionId), - } - const currentVersion = current.permissionModeVersion ?? -1 - - if (state.modeVersion < currentVersion) { - window.electronAPI.debugLog( - '[ModeSync] Ignoring stale permission mode update', - { sessionId, source, incoming: state.modeVersion, current: currentVersion } - ) - return prev - } - - if ( - state.modeVersion === currentVersion && - current.permissionMode !== state.permissionMode - ) { - window.electronAPI.debugLog( - '[ModeSync] Equal modeVersion with differing mode detected, applying and requesting reconciliation', - { - sessionId, - source, - modeVersion: state.modeVersion, - currentMode: current.permissionMode, - incomingMode: state.permissionMode, - } - ) - } - - setGlobalPermissionMode(state.permissionMode) - next.set(sessionId, { - ...current, - permissionMode: state.permissionMode, - permissionModeVersion: state.modeVersion, - }) - return next - }) - }, []) - - const reconcilePermissionModeState = useCallback(async (sessionId: string) => { - try { - const state = await window.electronAPI.getSessionPermissionModeState(sessionId) - if (!state) return - applyPermissionModeState(sessionId, state, 'reconcile') - } catch (error) { - window.electronAPI.debugLog('[ModeSync] Failed to reconcile permission mode', { - sessionId, - error: error instanceof Error ? error.message : String(error), - }) - } - }, [applyPermissionModeState]) - - // Event processor hook - handles all agent events through pure functions - const { processAgentEvent, clearStreamingState } = useEventProcessor() - - const syncSessionOptionsFromSession = useCallback((session: Session) => { - setSessionOptions(prev => { - const next = new Map(prev) - const current = next.get(session.id) - const merged = { - ...defaultSessionOptions, - ...current, - permissionMode: globalPermissionMode, - thinkingLevel: session.thinkingLevel ?? DEFAULT_THINKING_LEVEL, - } - - const hasNonDefaultThinking = merged.thinkingLevel !== DEFAULT_THINKING_LEVEL - - if (!hasNonDefaultThinking && merged.permissionModeVersion == null) { - next.delete(session.id) - } else { - next.set(session.id, merged) - } - - return next - }) - }, [globalPermissionMode]) - - const refreshSessionFromServer = useCallback(async (sessionId: string): Promise<'refreshed' | 'preserved_stale_messages' | 'failed'> => { - try { - const fresh = await window.electronAPI.getSessionMessages(sessionId) - if (!fresh) return 'failed' - - const prevSession = store.get(sessionAtomFamily(sessionId)) - const { - session: nextSession, - preservedExistingMessages, - } = mergeSessionRefreshResult(prevSession, fresh) - - clearStreamingState(sessionId) - updateSessionDirect(sessionId, () => nextSession) - syncSessionOptionsFromSession(nextSession) - void reconcilePermissionModeState(sessionId) - return preservedExistingMessages ? 'preserved_stale_messages' : 'refreshed' - } catch (err) { - console.error(`[App] Failed to refresh session ${sessionId}:`, err) - return 'failed' - } - }, [clearStreamingState, updateSessionDirect, syncSessionOptionsFromSession, reconcilePermissionModeState, store]) - - const cacheWorkspaceSessionMetas = useCallback((workspaceId: string, sessions: Session[]) => { - const metas = sessions.map(extractSessionMeta) - const next = new Map(store.get(workspaceSessionMetaCacheAtom)) - next.set(workspaceId, mergeStableSessionMetaList(next.get(workspaceId), metas)) - store.set(workspaceSessionMetaCacheAtom, next) - }, [store]) - - const applyLoadedSessions = useCallback(( - loadedSessions: Session[], - workspaceId: string | null, - remoteWorkspaceId?: string | null, - ) => { - // Initialize per-session atoms and metadata map. - // NOTE: No sessionsAtom used - sessions are only in per-session atoms. - if (workspaceId) { - initializeWorkspaceSessions({ - workspaceIds: [workspaceId, remoteWorkspaceId ?? ''].filter(Boolean), - sessions: loadedSessions, - }) - cacheWorkspaceSessionMetas(workspaceId, loadedSessions) - } else { - initializeSessions(loadedSessions) - } - - // Initialize unified sessionOptions from session data. - const optionsMap = new Map() - for (const s of loadedSessions) { - const hasNonDefaultThinking = s.thinkingLevel && s.thinkingLevel !== DEFAULT_THINKING_LEVEL - if (hasNonDefaultThinking) { - optionsMap.set(s.id, { - permissionMode: globalPermissionMode, - thinkingLevel: s.thinkingLevel ?? DEFAULT_THINKING_LEVEL, - }) - } - } - setSessionOptions(optionsMap) - }, [cacheWorkspaceSessionMetas, globalPermissionMode, initializeSessions, initializeWorkspaceSessions]) - - const reconcileLoadedSessionPermissionModes = useCallback((loadedSessions: Session[]) => { - return Promise.allSettled( - loadedSessions.map((s) => reconcilePermissionModeState(s.id)) - ) - }, [reconcilePermissionModeState]) - - const applyFastLocalSessionSnapshot = useCallback(async ( - workspaceId: string | null, - requestSeq: number, - ): Promise => { - if (!workspaceId) return - - let knownWorkspaces = workspacesRef.current - if (knownWorkspaces.length === 0) { - try { - knownWorkspaces = await window.electronAPI.getWorkspaces() - if (requestSeq !== sessionListRequestSeqRef.current || workspaceId !== windowWorkspaceIdRef.current) { - return - } - workspacesRef.current = knownWorkspaces - setWorkspaces(knownWorkspaces) - } catch (error) { - console.warn('[App] Failed to load workspaces for fast session snapshot:', error) - return - } - } - - const targetWorkspace = knownWorkspaces.find(workspace => workspace.id === workspaceId) - if (!targetWorkspace || targetWorkspace.remoteServer) return - - try { - const fastSessions = await window.electronAPI.getSessionsForWorkspace(workspaceId, { refreshExternal: false }) - if (requestSeq !== sessionListRequestSeqRef.current || workspaceId !== windowWorkspaceIdRef.current) { - return - } - - applyLoadedSessions(fastSessions, workspaceId, undefined) - } catch (error) { - console.warn(`[App] Failed to load fast session snapshot for workspace ${workspaceId}:`, error) - } - }, [applyLoadedSessions]) - - const loadSessionsFromServer = useCallback(async () => { - const requestWorkspaceId = windowWorkspaceIdRef.current - const requestSeq = ++sessionListRequestSeqRef.current - setSessionLoadError(null) - setSessionListLoading(true) - - const fullSessionsPromise: Promise< - | { ok: true; sessions: Session[] } - | { ok: false; error: unknown } - > = window.electronAPI.getSessions().then( - (sessions) => ({ ok: true as const, sessions }), - (error) => ({ ok: false as const, error }), - ) - - try { - await applyFastLocalSessionSnapshot(requestWorkspaceId, requestSeq) - - const fullSessionsResult = await fullSessionsPromise - if (!fullSessionsResult.ok) { - throw fullSessionsResult.error - } - const loadedSessions = fullSessionsResult.sessions - - if (requestSeq !== sessionListRequestSeqRef.current || requestWorkspaceId !== windowWorkspaceIdRef.current) { - console.info('[App] Ignoring stale session list response', { - requestWorkspaceId, - currentWorkspaceId: windowWorkspaceIdRef.current, - }) - return - } - - applyLoadedSessions(loadedSessions, requestWorkspaceId, windowRemoteWorkspaceId) - setSessionsLoaded(true) - setSessionListLoading(false) - void reconcileLoadedSessionPermissionModes(loadedSessions) - - if (initialSessionId && windowWorkspaceId) { - const session = loadedSessions.find(s => s.id === initialSessionId) - if (session) { - navigate(routes.view.allSessions(session.id)) - } - } - } catch (err) { - if (requestSeq !== sessionListRequestSeqRef.current || requestWorkspaceId !== windowWorkspaceIdRef.current) { - console.info('[App] Ignoring stale session list error', { - requestWorkspaceId, - currentWorkspaceId: windowWorkspaceIdRef.current, - }) - return - } - - console.error('[App] Failed to load sessions:', err) - const transportState = await window.electronAPI.getTransportConnectionState().catch(() => null) - - if (shouldTreatSessionLoadFailureAsTransportFallback(transportState)) { - console.error('[App] Treating session load failure as transport fallback:', transportState) - setSessionsLoaded(true) - setSessionLoadError(null) - return - } - - setSessionLoadError(formatSessionLoadFailure(err)) - setSessionsLoaded(true) - } finally { - if (requestSeq === sessionListRequestSeqRef.current && requestWorkspaceId === windowWorkspaceIdRef.current) { - setSessionListLoading(false) - } - } - }, [applyFastLocalSessionSnapshot, applyLoadedSessions, initialSessionId, reconcileLoadedSessionPermissionModes, windowRemoteWorkspaceId, windowWorkspaceId]) - - const refreshSessionListMetadataFromServer = useCallback(async (): Promise | null> => { - const requestWorkspaceId = windowWorkspaceIdRef.current - try { - const sessions = await window.electronAPI.getSessions() - if (requestWorkspaceId !== windowWorkspaceIdRef.current) { - console.info('[App] Ignoring stale reconnect session metadata response', { - requestWorkspaceId, - currentWorkspaceId: windowWorkspaceIdRef.current, - }) - return null - } - console.info(`[App] getSessions returned ${sessions.length} session(s) for reconnect refresh`) - const loadedSessionIds = store.get(loadedSessionsAtom) - - // Single transactional atom write — all cross-atom mutations happen - // inside one Jotai write function so React subscribers see one - // consistent update instead of intermediate states. - const nextMetaMap = store.set(refreshSessionsMetadataAtom, { - sessions, - loadedSessionIds, - workspaceIds: [requestWorkspaceId ?? '', windowRemoteWorkspaceId ?? ''].filter(Boolean), - }) - if (requestWorkspaceId) { - cacheWorkspaceSessionMetas(requestWorkspaceId, sessions) - } - - // Sync app-level state (React hooks / non-atom concerns) after the atom transaction - for (const session of sessions) { - syncSessionOptionsFromSession(session) - } - await Promise.allSettled(sessions.map(s => reconcilePermissionModeState(s.id))) - - return nextMetaMap - } catch (err) { - if (requestWorkspaceId !== windowWorkspaceIdRef.current) { - console.info('[App] Ignoring stale reconnect session metadata error', { - requestWorkspaceId, - currentWorkspaceId: windowWorkspaceIdRef.current, - }) - return null - } - console.error('[App] Failed to refresh session list metadata after reconnect:', err) - return null - } - }, [cacheWorkspaceSessionMetas, store, syncSessionOptionsFromSession, reconcilePermissionModeState, windowRemoteWorkspaceId]) - - const refreshChangedWorkspaceSessions = useCallback(async (workspaceId: string) => { - try { - const sessions = await window.electronAPI.getSessionsForWorkspace(workspaceId, { refreshExternal: false }) - cacheWorkspaceSessionMetas(workspaceId, sessions) - setSessionListRefreshWorkspaceIds(prev => { - if (!prev.has(workspaceId)) return prev - const next = new Set(prev) - next.delete(workspaceId) - return next - }) - - if (workspaceId !== windowWorkspaceIdRef.current) return - - applyLoadedSessions(sessions, workspaceId, windowRemoteWorkspaceId) - setSessionsLoaded(true) - setSessionLoadError(null) - setSessionListLoading(false) - void reconcileLoadedSessionPermissionModes(sessions) - } catch (err) { - console.error(`[App] Failed to refresh changed session list for workspace ${workspaceId}:`, err) - } - }, [ - applyLoadedSessions, - cacheWorkspaceSessionMetas, - reconcileLoadedSessionPermissionModes, - windowRemoteWorkspaceId, - ]) - - useEffect(() => { - const cleanup = window.electronAPI.onSessionsChanged((workspaceId) => { - void refreshChangedWorkspaceSessions(workspaceId) - }) - - return cleanup - }, [refreshChangedWorkspaceSessions]) - - useEffect(() => { - const cleanup = window.electronAPI.onSessionListRefreshStateChanged((workspaceId, isRefreshing) => { - setSessionListRefreshWorkspaceIds(prev => { - const hasWorkspace = prev.has(workspaceId) - if (isRefreshing === hasWorkspace) return prev - - const next = new Set(prev) - if (isRefreshing) next.add(workspaceId) - else next.delete(workspaceId) - return next - }) - }) - - return cleanup - }, []) - - // Stale session watchdog — catches stuck sessions that the reconnect protocol misses - const { trackSessionActivity } = useStaleSessionRecovery({ - store, - refreshSessionFromServer, - }) - - const DRAFT_SAVE_DEBOUNCE_MS = 500 - const qwenModelRefreshInFlightRef = useRef | null>(null) - const qwenModelRefreshAttemptedRef = useRef>(new Set()) - const qwenModelRefreshCompletedRef = useRef>(new Set()) - - const resolveDefaultConnectionSlug = useCallback((connections: LlmConnectionWithStatus[]) => { - return connections.find(c => c.isDefault)?.slug ?? connections[0]?.slug - }, []) - - const handleOptimisticDefaultModelChange = useCallback((model: string, connectionSlug?: string) => { - setLlmConnections(previousConnections => { - const targetSlug = connectionSlug ?? resolveDefaultConnectionSlug(previousConnections) - if (!targetSlug) return previousConnections - - let changed = false - const nextConnections = previousConnections.map(connection => { - if (connection.slug !== targetSlug) return connection - if (connection.defaultModel === model) return connection - - changed = true - return { ...connection, defaultModel: model } - }) - - return changed ? nextConnections : previousConnections - }) - }, [resolveDefaultConnectionSlug]) - - // Refresh LLM connections from config (called on workspace change and after connection updates) - const refreshLlmConnections = useCallback(async () => { - const connections = await window.electronAPI.listLlmConnectionsWithStatus() - const visibleConnections = connections.map(connection => { - if (connection.providerType !== 'qwen') return connection - if (connection.models?.length) { - qwenModelRefreshCompletedRef.current.add(connection.slug) - } - return qwenModelRefreshCompletedRef.current.has(connection.slug) - ? connection - : { ...connection, models: [], defaultModel: '' } - }) - setLlmConnections(visibleConnections) - setDefaultLlmConnectionSlug(resolveDefaultConnectionSlug(connections)) - // Also refresh workspace default - if (windowWorkspaceId) { - const settings = await window.electronAPI.getWorkspaceSettings(windowWorkspaceId) - setWorkspaceDefaultLlmConnection(settings?.defaultLlmConnection) - } - - const qwenConnectionToRefresh = connections.find(connection => - connection.providerType === 'qwen' - && !connection.models?.length - && !qwenModelRefreshAttemptedRef.current.has(connection.slug) - ) - if (qwenConnectionToRefresh && !qwenModelRefreshInFlightRef.current) { - qwenModelRefreshAttemptedRef.current.add(qwenConnectionToRefresh.slug) - qwenModelRefreshInFlightRef.current = (async () => { - try { - const result = await window.electronAPI.refreshLlmConnectionModels(qwenConnectionToRefresh.slug) - if (!result.success) { - console.warn('[App] Qwen model refresh failed:', result.error) - qwenModelRefreshAttemptedRef.current.delete(qwenConnectionToRefresh.slug) - return - } - - qwenModelRefreshCompletedRef.current.add(qwenConnectionToRefresh.slug) - const refreshedConnections = await window.electronAPI.listLlmConnectionsWithStatus() - setLlmConnections(refreshedConnections) - setDefaultLlmConnectionSlug(resolveDefaultConnectionSlug(refreshedConnections)) - } catch (error) { - console.warn('[App] Qwen model refresh failed:', error) - } finally { - qwenModelRefreshInFlightRef.current = null - } - })() - } - }, [resolveDefaultConnectionSlug, windowWorkspaceId]) - - // Handle onboarding completion - const handleOnboardingComplete = useCallback(async () => { - try { - // Reload workspaces after onboarding - const ws = await window.electronAPI.getWorkspaces() - if (ws.length > 0) { - // Switch to workspace in-place (no window close/reopen) - await window.electronAPI.switchWorkspace(ws[0].id) - windowWorkspaceIdRef.current = ws[0].id - setWindowWorkspaceId(ws[0].id) - setWorkspaces(ws) - } else { - setWorkspaces(ws) - } - } catch (error) { - console.error('[App] Failed to load workspaces after onboarding:', error) - // Still transition to ready — the app can recover via reconnect - } - setAppState('ready') - }, []) - - // Onboarding hook — onConfigSaved fires immediately when billing is saved, - // ensuring connection state updates before the wizard closes. - const onboarding = useOnboarding({ - onComplete: handleOnboardingComplete, - onConfigSaved: refreshLlmConnections, - initialSetupNeeds: setupNeeds || undefined, - }) - - // Reauth login handler - placeholder (reauth is not currently used) - const handleReauthLogin = useCallback(async () => { - // Re-check setup needs - const needs = await window.electronAPI.getSetupNeeds() - if (needs.isFullyConfigured) { - setAppState('ready') - } else { - setSetupNeeds(needs) - setAppState('onboarding') - } - }, []) - - // Reauth reset handler - open reset confirmation dialog - const handleReauthReset = useCallback(() => { - setShowResetDialog(true) - }, []) - - // Check auth state and get window's workspace ID on mount - useEffect(() => { - const initialize = async () => { - try { - // Get this window's workspace ID (passed via URL query param from main process) - const wsId = await window.electronAPI.getWindowWorkspace() - windowWorkspaceIdRef.current = wsId - setWindowWorkspaceId(wsId) - - const needs = await window.electronAPI.getSetupNeeds() - setSetupNeeds(needs) - - if (needs.isFullyConfigured) { - // If no workspace is selected (thin client without CRAFT_WORKSPACE_ID), - // show workspace picker before entering the main app - if (!wsId) { - setAppState('workspace-picker') - } else { - setAppState('ready') - } - } else { - // New user or needs setup - show onboarding - setAppState('onboarding') - } - } catch (error) { - console.error('Failed to check auth state:', error) - // If check fails, show onboarding to be safe - setAppState('onboarding') - } - } - - initialize() - }, []) - - // Session selection state - const [sessionSelection, setSession] = useSession() - - // Notification system - shows native OS notifications and badge count - const handleNavigateToSession = useCallback((sessionId: string) => { - // Navigate to the session via central routing (uses allSessions filter) - navigate(routes.view.allSessions(sessionId)) - }, []) - - const { isWindowFocused, showSessionNotification } = useNotifications({ - workspaceId: windowWorkspaceId, - // NOTE: sessions removed - hook now uses sessionMetaMapAtom internally - // to prevent closures from retaining full message arrays - onNavigateToSession: handleNavigateToSession, - enabled: notificationsEnabled, - }) - - // Load workspaces, sessions, model, notifications setting, and drafts when app is ready - useEffect(() => { - if (appState !== 'ready') return - - window.electronAPI.getWorkspaces().then(setWorkspaces) - window.electronAPI.getNotificationsEnabled().then(setNotificationsEnabled).catch(() => {}) - - // Show actionable toast for missing system dependencies (Windows only) - window.electronAPI.getSystemWarnings().then((warnings) => { - if (warnings.vcredistMissing) { - toast.warning(t('toast.vcRedistNotFound'), { - description: t('toast.vcRedistNotFoundDesc'), - duration: Infinity, - action: { - label: 'Install', - onClick: () => window.electronAPI.openUrl(warnings.downloadUrl ?? 'https://aka.ms/vs/17/release/vc_redist.x64.exe'), - }, - }) - } - }).catch(() => { /* non-fatal startup check */ }) - void loadSessionsFromServer() - // Load LLM connections with authentication status - void refreshLlmConnections() - // Load persisted input drafts into ref (no re-render needed). - // Attachment files are not read here — hydration happens lazily when the session - // is opened so app startup isn't delayed by reading potentially large files. - window.electronAPI.getAllDrafts().then((drafts) => { - if (Object.keys(drafts).length > 0) { - sessionDraftsRef.current = new Map(Object.entries(drafts)) - } - }) - // Load app-level theme - window.electronAPI.getAppTheme().then(setAppTheme) - }, [appState, loadSessionsFromServer, refreshLlmConnections]) - - // Subscribe to theme change events (live updates when theme.json changes) - useEffect(() => { - const cleanupApp = window.electronAPI.onAppThemeChange((theme) => { - setAppTheme(theme) - }) - return () => { - cleanupApp() - } - }, []) - - // Subscribe to LLM connections change events (live updates when models are fetched) - useEffect(() => { - const cleanup = window.electronAPI.onLlmConnectionsChanged(() => { - refreshLlmConnections() - }) - return () => { cleanup() } - }, [refreshLlmConnections]) - - // Refresh LLM connections and workspace default when workspace changes - useEffect(() => { - if (windowWorkspaceId) { - refreshLlmConnections() - } - }, [windowWorkspaceId, refreshLlmConnections]) - - // Listen for session events - uses centralized event processor for consistent state transitions - // - // SOURCE OF TRUTH LOGIC: - // - During streaming (atom.isProcessing = true): Atom is source of truth - // All events read from and write to atom. This preserves streaming data. - // - When not streaming: React state is source of truth - // Events read/write React state, which syncs to atoms via useEffect. - // - Handoff events (complete, error, etc.): End streaming, sync atom → React state - // - // This is simpler and more robust than checking event types - we just ask - // "is this session currently streaming?" and route accordingly. - useEffect(() => { - // Handoff events signal end of streaming - need to sync back to React state - // Also includes todo_state_changed so status updates immediately reflect in sidebar - // async_operation included so shimmer effect on session titles updates in real-time - const handoffEventTypes = new Set(['complete', 'error', 'interrupted', 'typed_error', 'session_status_changed', 'session_flagged', 'session_unflagged', 'name_changed', 'labels_changed', 'title_generated', 'async_operation']) - - // Helper to handle side effects (same logic for both paths) - const handleEffects = (effects: Effect[], sessionId: string, eventType: string) => { - for (const effect of effects) { - switch (effect.type) { - case 'permission_request': { - setPendingPermissions(prevPerms => { - const next = new Map(prevPerms) - const existingQueue = next.get(sessionId) || [] - next.set(sessionId, [...existingQueue, effect.request]) - return next - }) - - // Native notification for approval-required pauses (same gating as completion notifications) - const notifySession = store.get(sessionAtomFamily(sessionId)) - if (notifySession && !notifySession.hidden) { - const isAdminPrompt = effect.request.type === 'admin_approval' - const isQuestionPrompt = effect.request.type === 'ask_user_question' - const promptBody = isQuestionPrompt - ? 'The agent has a question for you' - : isAdminPrompt - ? `Admin approval required: ${effect.request.appName || effect.request.toolName}` - : `Permission required: ${effect.request.toolName}` - showSessionNotification(notifySession, promptBody) - } - break - } - case 'permission_mode_changed': { - if (typeof effect.modeVersion === 'number' && effect.changedAt && effect.changedBy) { - applyPermissionModeState(effect.sessionId, { - permissionMode: effect.permissionMode, - modeVersion: effect.modeVersion, - changedAt: effect.changedAt, - changedBy: effect.changedBy, - }, 'event') - } else { - // Backward compatibility: apply mode optimistically then reconcile authoritative state. - setGlobalPermissionMode(effect.permissionMode) - setSessionOptions(prevOpts => { - const next = new Map(prevOpts) - const current = next.get(effect.sessionId) ?? defaultSessionOptions - next.set(effect.sessionId, { ...current, permissionMode: effect.permissionMode }) - return next - }) - void reconcilePermissionModeState(effect.sessionId) - } - break - } - case 'credential_request': { - setPendingCredentials(prevCreds => { - const next = new Map(prevCreds) - const existingQueue = next.get(sessionId) || [] - next.set(sessionId, [...existingQueue, effect.request]) - return next - }) - break - } - case 'auto_retry': { - // A source was auto-activated, automatically re-send the original message - // Add suffix to indicate the source was activated - const messageWithSuffix = `${effect.originalMessage}\n\n[${effect.sourceSlug} activated]` - // Use setTimeout to ensure the previous turn has fully completed - setTimeout(() => { - window.electronAPI.sendMessage(effect.sessionId, messageWithSuffix) - }, 100) - break - } - case 'restore_input': { - // Queued messages were removed from chat on abort — restore their text to the input field. - // Append to existing draft (user may have started typing) rather than overwrite. - const existingDraft = sessionDraftsRef.current.get(sessionId) - const existingText = coerceInputText(existingDraft?.text) - const restoredText = coerceInputText(effect.text) - const restored = existingText - ? `${existingText}\n\n${restoredText}` - : restoredText - handleInputChange(sessionId, restored) - // handleInputChange updates the ref but ChatPage has local state. - // Dispatch a custom event so ChatPage re-reads the draft. - window.dispatchEvent(new CustomEvent('craft:restore-input', { - detail: { sessionId, text: restored }, - })) - break - } - case 'queued_input_add': { - window.dispatchEvent(new CustomEvent('craft:queued-input-add', { - detail: { - sessionId, - message: effect.message, - optimisticMessageId: effect.optimisticMessageId, - }, - })) - break - } - case 'queued_input_remove': { - window.dispatchEvent(new CustomEvent('craft:queued-input-remove', { - detail: { - sessionId, - messageId: effect.messageId, - optimisticMessageId: effect.optimisticMessageId, - }, - })) - break - } - case 'toast_error': { - toast.error(effect.message, { duration: 5000 }) - break - } - } - } - - // Clear pending permissions and credentials on complete - if (eventType === 'complete') { - setPendingPermissions(prevPerms => { - if (prevPerms.has(sessionId)) { - const next = new Map(prevPerms) - next.delete(sessionId) - return next - } - return prevPerms - }) - setPendingCredentials(prevCreds => { - if (prevCreds.has(sessionId)) { - const next = new Map(prevCreds) - next.delete(sessionId) - return next - } - return prevCreds - }) - } - } - - const cleanup = window.electronAPI.onSessionEvent((event: SessionEvent) => { - if (!('sessionId' in event)) return - - const sessionId = event.sessionId - const workspaceId = windowWorkspaceId ?? '' - - // Session lifecycle events are handled explicitly (not by the agent event processor). - if (event.type === 'session_created') { - window.electronAPI.getSessionMessages(sessionId) - .then((createdSession: Session | null) => { - if (createdSession) { - const existingMeta = store.get(sessionMetaMapAtom).has(sessionId) - if (existingMeta) { - updateSessionDirect(sessionId, () => createdSession) - } else { - addSession(createdSession) - } - if (workspaceId) { - cacheWorkspaceSessionMetas( - workspaceId, - Array.from(store.get(sessionMetaMapAtom).values()) - .filter(meta => meta.workspaceId === workspaceId || (windowRemoteWorkspaceId && meta.workspaceId === windowRemoteWorkspaceId)) - .map(meta => sessionFromMeta(meta, createdSession.workspaceName)), - ) - } - syncSessionOptionsFromSession(createdSession) - return - } - - const requestWorkspaceId = windowWorkspaceIdRef.current - return window.electronAPI.getSessions().then((sessions) => { - if (requestWorkspaceId !== windowWorkspaceIdRef.current) return - initializeWorkspaceSessions({ - workspaceIds: [requestWorkspaceId ?? '', windowRemoteWorkspaceId ?? ''].filter(Boolean), - sessions, - }) - if (requestWorkspaceId) cacheWorkspaceSessionMetas(requestWorkspaceId, sessions) - }) - }) - .catch((error: unknown) => console.error('Failed to handle session_created event:', error)) - return - } - - if (event.type === 'session_deleted') { - removeSession(sessionId) - if (sessionSelection.selected === sessionId) { - setSession({ selected: null }) - } - return - } - - // In-place id rename (e.g. Qwen managed session adopting the ACP session - // id after its first turn). This is NOT a delete+create: migrate the - // active selection and per-session state from previousId -> sessionId so - // the open chat keeps working (otherwise the next session-scoped action, - // e.g. switching approval mode, hits a dead id -> "session not found"). - if (event.type === 'session_id_changed') { - const previousId = event.previousId - const nextId = event.sessionId - if (previousId === nextId) return - - const wasSelected = sessionSelection.selected === previousId - - // Migrate per-session UI state keyed by the old id (synchronous). - setSessionOptions(prev => { - if (!prev.has(previousId)) return prev - const next = new Map(prev) - const opts = next.get(previousId) - next.delete(previousId) - if (opts && !next.has(nextId)) next.set(nextId, opts) - return next - }) - const draft = sessionDraftsRef.current.get(previousId) - if (draft !== undefined && !sessionDraftsRef.current.has(nextId)) { - sessionDraftsRef.current.set(nextId, draft) - } - sessionDraftsRef.current.delete(previousId) - - window.electronAPI.getSessionMessages(nextId) - .then((renamed: Session | null) => { - if (renamed) { - const existingMeta = store.get(sessionMetaMapAtom).has(nextId) - if (existingMeta) { - updateSessionDirect(nextId, () => renamed) - } else { - addSession(renamed) - } - syncSessionOptionsFromSession(renamed) - if (workspaceId) { - cacheWorkspaceSessionMetas( - workspaceId, - Array.from(store.get(sessionMetaMapAtom).values()) - .filter(meta => meta.workspaceId === workspaceId || (windowRemoteWorkspaceId && meta.workspaceId === windowRemoteWorkspaceId)) - .map(meta => sessionFromMeta(meta, renamed.workspaceName)), - ) - } - } - // Drop the stale id only after the canonical session is in place, - // then follow the rename with the active selection AND the route. - // ChatPage derives its sessionId from navigation state (not the - // selection atom), so without re-navigating the open chat keeps - // rendering the dead previousId -> "此会话已不存在". - removeSession(previousId) - if (wasSelected) { - setSession({ selected: nextId }) - navigate(routes.view.allSessions(nextId)) - } - }) - .catch((error: unknown) => console.error('Failed to handle session_id_changed event:', error)) - return - } - - const agentEvent = event as unknown as AgentEvent - - // Track activity for stale session watchdog - trackSessionActivity(sessionId) - - // Dispatch window event when compaction completes - // This allows FreeFormInput to sequence the plan execution message after compaction - // Note: markCompactionComplete is called on the backend (sessions.ts) to ensure - // it happens even if CMD+R occurs during compaction - if (event.type === 'info' && event.statusType === 'compaction_complete') { - window.dispatchEvent(new CustomEvent('craft:compaction-complete', { - detail: { sessionId } - })) - } - - // Check if session is currently streaming (atom is source of truth) - const atomSession = store.get(sessionAtomFamily(sessionId)) - const metaSession = store.get(sessionMetaMapAtom).get(sessionId) - const eventWorkspaceId = - event.workspaceId ?? atomSession?.workspaceId ?? metaSession?.workspaceId ?? workspaceId - const isStreaming = atomSession?.isProcessing === true - const isHandoff = handoffEventTypes.has(event.type) - - // During streaming OR for handoff events: use atom as source of truth - // This ensures all events during streaming see the complete state - if (isStreaming || isHandoff) { - const currentSession = atomSession ?? null - - // Process the event - const { session: updatedSession, effects } = processAgentEvent( - agentEvent, - currentSession, - eventWorkspaceId - ) - - // Update atom directly (UI sees update immediately) - updateSessionDirect(sessionId, () => updatedSession) - - // Handle side effects - handleEffects(effects, sessionId, event.type) - - // Handle background task events - handleBackgroundTaskEvent(store, sessionId, event, agentEvent) - - // For handoff events, update metadata map for list display - // NOTE: No sessionsAtom to sync - atom and metadata are the source of truth - if (isHandoff) { - // Show notification on complete (when window is not focused) - // Skip hidden sessions (mini-agent sessions) - they shouldn't trigger notifications - if (event.type === 'complete' && !updatedSession.hidden) { - // Get the last assistant/plan message as preview - const lastMessage = updatedSession.messages.findLast( - m => (m.role === 'assistant' || m.role === 'plan') && !m.isIntermediate - ) - // Strip markdown so OS notifications display clean plain text - const rawPreview = lastMessage?.content?.substring(0, 200) || undefined - const preview = rawPreview ? stripMarkdown(rawPreview).substring(0, 100) || undefined : undefined - showSessionNotification(updatedSession, preview) - } - } - - return - } - - // Not streaming: use per-session atoms directly (no sessionsAtom) - const currentSession = store.get(sessionAtomFamily(sessionId)) - - const { session: updatedSession, effects } = processAgentEvent( - agentEvent, - currentSession, - eventWorkspaceId - ) - - // Handle side effects - handleEffects(effects, sessionId, event.type) - - // Handle background task events - handleBackgroundTaskEvent(store, sessionId, event, agentEvent) - - // Update per-session atom - updateSessionDirect(sessionId, () => updatedSession) - }) - - return cleanup - }, [ - processAgentEvent, - trackSessionActivity, - windowWorkspaceId, - store, - updateSessionDirect, - showSessionNotification, - initializeSessions, - initializeWorkspaceSessions, - addSession, - removeSession, - cacheWorkspaceSessionMetas, - syncSessionOptionsFromSession, - applyPermissionModeState, - reconcilePermissionModeState, - windowRemoteWorkspaceId, - ]) - - // Transport reconnect recovery — refresh session metadata plus active/processing - // session content after stale reconnects. - useEffect(() => { - const cleanup = window.electronAPI.onReconnected(async (isStale: boolean) => { - if (!isStale) { - // Server replayed buffered events — we're caught up, nothing to do - console.info('[App] Reconnected with event replay — no refresh needed') - return - } - - console.warn('[App] Stale reconnect — refreshing session metadata and active/processing sessions') - - const refreshedMetaMap = await refreshSessionListMetadataFromServer() - const metaMap = refreshedMetaMap ?? store.get(sessionMetaMapAtom) - const refreshIds = getSessionsToRefreshAfterStaleReconnect(metaMap, sessionSelection.selected) - - console.info(`[App] Stale reconnect — refreshing ${refreshIds.length} session(s):`, refreshIds) - - // Refresh full message content only for the active session plus any - // session still marked processing after the metadata refresh. - for (const sessionId of refreshIds) { - let refreshResult = await refreshSessionFromServer(sessionId) - if (refreshResult !== 'refreshed') { - // Server may need time to restart session subprocess after reconnect, - // or it may still be lazily loading session messages. - for (const delay of [2000, 4000]) { - console.warn(`[App] Retrying session refresh for ${sessionId} after ${delay}ms (${refreshResult})`) - await new Promise(r => setTimeout(r, delay)) - refreshResult = await refreshSessionFromServer(sessionId) - if (refreshResult === 'refreshed') break - } - } - } - - // Final fallback: if the active session is still empty, force a reload - // even when the session is already marked loaded. - if (sessionSelection.selected) { - const session = store.get(sessionAtomFamily(sessionSelection.selected)) - if (session && (!session.messages || session.messages.length === 0)) { - console.warn('[App] Active session still has no messages after stale reconnect refresh — forcing message reload') - await store.set(forceSessionMessagesReloadAtom, sessionSelection.selected) - } else if (session) { - console.info(`[App] Stale reconnect recovery complete — active session has ${session.messages?.length ?? 0} messages`) - } - } - - }) - - return cleanup - }, [store, sessionSelection.selected, setSession, refreshSessionFromServer, refreshSessionListMetadataFromServer]) - - // Listen for menu bar events - useEffect(() => { - const unsubNewChat = window.electronAPI.onMenuNewChat(() => { - setMenuNewChatTrigger(n => n + 1) - }) - const unsubSettings = window.electronAPI.onMenuOpenSettings(() => { - handleOpenSettings() - }) - const unsubShortcuts = window.electronAPI.onMenuKeyboardShortcuts(() => { - navigate(routes.view.settings('shortcuts')) - }) - return () => { - unsubNewChat() - unsubSettings() - unsubShortcuts() - } - }, []) - - const handleCreateSession = useCallback(async (workspaceId: string, options?: import('../shared/types').CreateSessionOptions): Promise => { - const session = await window.electronAPI.createSession(workspaceId, options) - // Add to per-session atom and metadata map (no sessionsAtom) - addSession(session) - syncSessionOptionsFromSession(session) - - return session - }, [addSession, syncSessionOptionsFromSession]) - - // Deep link navigation is initialized later after handleInputChange is defined - - const handleDeleteSession = useCallback(async (sessionId: string, skipConfirmation = false, displayTitle?: string): Promise => { - if (!skipConfirmation) { - const metaMap = store.get(sessionMetaMapAtom) - const meta = metaMap.get(sessionId) - const confirmed = await window.electronAPI.showDeleteSessionConfirmation( - displayTitle || (meta ? getSessionTitle(meta) : 'Untitled'), - ) - if (!confirmed) return false - } - - await window.electronAPI.deleteSession(sessionId) - // Remove from per-session atom and metadata map (no sessionsAtom) - removeSession(sessionId) - const route = getSessionDeleteNavigationRoute({ - deleted: true, - deletedSessionId: sessionId, - selectedSessionId: sessionSelection.selected, - }) - if (route) { - navigate(route) - } - return true - }, [store, removeSession, sessionSelection.selected]) - - // Auto-delete handler for empty sessions (fire-and-forget, no confirmation) - const handleAutoDeleteEmptySession = useCallback((sessionId: string) => { - if (sessionSelection.selected === sessionId) { - setSession({ selected: null }) - } - window.electronAPI.deleteSession(sessionId) - removeSession(sessionId) - }, [removeSession, sessionSelection.selected, setSession]) - - const handleFlagSession = useCallback((sessionId: string) => { - updateSessionById(sessionId, { isFlagged: true }) - window.electronAPI.sessionCommand(sessionId, { type: 'flag' }) - }, [updateSessionById]) - - const handleUnflagSession = useCallback((sessionId: string) => { - updateSessionById(sessionId, { isFlagged: false }) - window.electronAPI.sessionCommand(sessionId, { type: 'unflag' }) - }, [updateSessionById]) - - const handleArchiveSession = useCallback((sessionId: string) => { - updateSessionById(sessionId, { isArchived: true, archivedAt: Date.now() }) - window.electronAPI.sessionCommand(sessionId, { type: 'archive' }) - }, [updateSessionById]) - - const handleUnarchiveSession = useCallback((sessionId: string) => { - updateSessionById(sessionId, { isArchived: false, archivedAt: undefined }) - window.electronAPI.sessionCommand(sessionId, { type: 'unarchive' }) - }, [updateSessionById]) - - /** - * Set which session user is actively viewing (for unread state machine). - * Called when user navigates to a session. Main process uses this to determine - * whether to mark new assistant messages as unread. - */ - const handleSetActiveViewingSession = useCallback((sessionId: string) => { - // Optimistic UI update: clear hasUnread immediately - updateSessionById(sessionId, { hasUnread: false }) - // Tell main process user is viewing this session - window.electronAPI.sessionCommand(sessionId, { type: 'setActiveViewing', workspaceId: windowWorkspaceId ?? '' }) - }, [updateSessionById, windowWorkspaceId]) - - const handleMarkSessionRead = useCallback((sessionId: string) => { - // Update hasUnread flag (primary source of truth for NEW badge) - // Also update lastReadMessageId for backwards compatibility - updateSessionById(sessionId, (s) => { - const lastFinalId = s.messages.findLast( - m => (m.role === 'assistant' || m.role === 'plan') && !m.isIntermediate - )?.id - return { - hasUnread: false, - ...(lastFinalId ? { lastReadMessageId: lastFinalId } : {}), - } - }) - window.electronAPI.sessionCommand(sessionId, { type: 'markRead' }) - }, [updateSessionById]) - - const handleMarkSessionUnread = useCallback((sessionId: string) => { - // Set hasUnread flag (primary source of truth for NEW badge) - updateSessionById(sessionId, { hasUnread: true, lastReadMessageId: undefined }) - window.electronAPI.sessionCommand(sessionId, { type: 'markUnread' }) - }, [updateSessionById]) - - const handleSessionStatusChange = useCallback((sessionId: string, state: SessionStatus) => { - updateSessionById(sessionId, { sessionStatus: state }) - window.electronAPI.sessionCommand(sessionId, { type: 'setSessionStatus', state }) - }, [updateSessionById]) - - const handleRenameSession = useCallback((sessionId: string, name: string) => { - updateSessionById(sessionId, { name }) - window.electronAPI.sessionCommand(sessionId, { type: 'rename', name }) - }, [updateSessionById]) - - const handleSendMessage = useCallback(async (sessionId: string, message: string, attachments?: FileAttachment[], skillSlugs?: string[], externalBadges?: ContentBadge[]) => { - let optimisticUserMessage: Message | undefined - let queuedOptimisticMessage = false - try { - // Step 1: Store attachments and get persistent metadata - let storedAttachments: StoredAttachment[] | undefined - let processedAttachments: FileAttachment[] | undefined - - if (attachments?.length) { - // Store each attachment to disk (generates thumbnails, converts Office→markdown) - // Use allSettled so one failure doesn't kill all attachments - const storeResults = await Promise.allSettled( - attachments.map(a => window.electronAPI.storeAttachment(sessionId, a)) - ) - - // Filter successful stores, warn about failures - storedAttachments = [] - const successfulAttachments: FileAttachment[] = [] - storeResults.forEach((result, i) => { - if (result.status === 'fulfilled') { - storedAttachments!.push(result.value) - successfulAttachments.push(attachments[i]) - } else { - console.warn(`Failed to store attachment "${attachments[i].name}":`, result.reason) - } - }) - - // Notify user about failed attachments - const failedCount = storeResults.filter(r => r.status === 'rejected').length - if (failedCount > 0) { - console.warn(`${failedCount} attachment(s) failed to store`) - // Add warning message to session so user knows some attachments weren't included - const failedNames = attachments - .filter((_, i) => storeResults[i].status === 'rejected') - .map(a => a.name) - .join(', ') - updateSessionById(sessionId, (s) => ({ - messages: [...s.messages, { - id: generateMessageId(), - role: 'warning' as const, - content: `⚠️ ${failedCount} attachment(s) could not be stored and will not be sent: ${failedNames}`, - timestamp: Date.now() - }] - })) - } - - // Step 2: Create processed attachments for backend input - // - Office files: Convert to text with markdown content - // - Others: Use original FileAttachment - // - All: Include storedPath so agent knows where files are stored - // - Resized images: Use resizedBase64 instead of original large base64 - processedAttachments = await Promise.all( - successfulAttachments.map(async (att, i) => { - const stored = storedAttachments?.[i] - if (!stored) { - console.error(`Missing stored attachment at index ${i}`) - return att // Fall back to original - } - // Include storedPath and markdownPath for all attachment types - // Agent will use Read tool to access text/office files via these paths - // If image was resized, use the resized base64 for backend input - return { - ...att, - storedPath: stored.storedPath, - markdownPath: stored.markdownPath, - // Use resized base64 if available (for images that exceeded size limits) - base64: stored.resizedBase64 ?? att.base64, - } - }) - ) - } - - // Step 3: Extract inline metadata from mentions, commands, and contextual badges. - // ContentBadge is only an input-side helper; messages persist/render textElements. - // Merge with any externally provided badges (e.g., from EditPopover context badges). - // Use workspace slug (not UUID) for skill qualification - SDK expects "workspaceSlug:skillSlug" - const badgeSkills = skillsForBadgeExtraction(skills, skillSlugs) - const mentionBadges: ContentBadge[] = windowWorkspaceSlug - ? extractBadges(message, badgeSkills, sources, windowWorkspaceSlug) - : [] - const badges: ContentBadge[] = [...(externalBadges || []), ...mentionBadges] - - // Step 4.1: Detect slash commands and create command badges. - // The command text itself is preserved for the agent; the badge only affects display. - badges.unshift(...extractCommandBadges(message)) - - // Step 4.2: Detect plan execution messages and create file badges - // Pattern: "Read the plan at and execute it." - // This is sent after compaction when accepting a plan, displays as clickable file badge - // Only the file path is replaced with a badge - surrounding text remains visible - const planExecuteMatch = message.match(/^(Read the plan at )(.+?)( and execute it\.?)$/i) - if (planExecuteMatch) { - const prefix = planExecuteMatch[1] // "Read the plan at " - const filePath = planExecuteMatch[2] // the actual path - const fileName = filePath.split('/').pop() || 'plan.md' - badges.push({ - type: 'file', - label: fileName, - rawText: filePath, - filePath: filePath, - start: prefix.length, - end: prefix.length + filePath.length, - }) - } - const textElements = contentBadgesToTextElements(message, badges) - - // Step 5: Create user message with StoredAttachments (for UI display) - // Mark as isPending for optimistic UI - will be confirmed by user_message event - const userMessage: Message = { - id: generateMessageId(), - role: 'user', - content: message, - timestamp: Date.now(), - attachments: storedAttachments, - textElements, - isPending: true, // Optimistic - will be confirmed by backend - } - optimisticUserMessage = userMessage - - const currentSession = store.get(sessionAtomFamily(sessionId)) - const shouldQueueInInput = currentSession?.isProcessing === true - - if (shouldQueueInInput) { - queuedOptimisticMessage = true - window.dispatchEvent(new CustomEvent('craft:queued-input-add', { - detail: { - sessionId, - message: { - ...userMessage, - isPending: false, - isQueued: true, - }, - optimisticMessageId: userMessage.id, - }, - })) - updateSessionById(sessionId, { - isProcessing: true, - lastMessageAt: Date.now(), - lastMessageRole: 'user', - }) - } else { - // Optimistic UI update - add user message and set processing state - updateSessionById(sessionId, (s) => ({ - messages: [...s.messages, userMessage], - isProcessing: true, - lastMessageAt: Date.now() - })) - } - - // Step 6: Send with processed attachments + stored attachments for persistence - await window.electronAPI.sendMessage(sessionId, message, processedAttachments, storedAttachments, { - skillSlugs, - textElements, - optimisticMessageId: userMessage.id, - }) - } catch (error) { - console.error('Failed to send message:', error) - if (queuedOptimisticMessage && optimisticUserMessage) { - window.dispatchEvent(new CustomEvent('craft:queued-input-remove', { - detail: { - sessionId, - messageId: optimisticUserMessage.id, - optimisticMessageId: optimisticUserMessage.id, - }, - })) - } - updateSessionById(sessionId, (s) => ({ - isProcessing: false, - messages: [ - ...s.messages, - { - id: generateMessageId(), - role: 'error' as const, - content: `Failed to send message: ${error instanceof Error ? error.message : 'Unknown error'}`, - timestamp: Date.now() - } - ] - })) - } - }, [sessionOptions, updateSessionById, store, skills, sources, windowWorkspaceSlug]) - - /** - * Unified handler for all session option changes. - * Handles persistence and backend sync for each option type. - */ - const handleSessionOptionsChange = useCallback((sessionId: string, updates: SessionOptionUpdates) => { - setSessionOptions(prev => { - const next = new Map(prev) - const current = { - ...defaultSessionOptions, - ...next.get(sessionId), - permissionMode: globalPermissionMode, - } - if (updates.permissionMode !== undefined) { - for (const [id, options] of next) { - next.set(id, { ...options, permissionMode: updates.permissionMode }) - } - } - next.set(sessionId, mergeSessionOptions(current, updates)) - return next - }) - - // Handle persistence/backend for specific options - if (updates.permissionMode !== undefined) { - setGlobalPermissionMode(updates.permissionMode) - window.electronAPI.setGlobalPermissionMode(updates.permissionMode).catch((error) => { - console.error('[App] Failed to persist global permission mode:', error) - void reconcilePermissionModeState(sessionId) - }) - } - if (updates.thinkingLevel !== undefined) { - // Sync thinking level change with backend (session-level, persisted) - window.electronAPI.sessionCommand(sessionId, { type: 'setThinkingLevel', level: updates.thinkingLevel }) - } - }, [globalPermissionMode, reconcilePermissionModeState]) - - // Handle input draft changes per session with debounced persistence - const draftSaveTimeoutRef = useRef>>(new Map()) - - // Cleanup draft save timers on unmount to prevent memory leaks - useEffect(() => { - return () => { - draftSaveTimeoutRef.current.forEach(clearTimeout) - draftSaveTimeoutRef.current.clear() - } - }, []) - - // Getter for draft text - reads from ref without triggering re-renders - const getDraft = useCallback((sessionId: string): string => { - const draft = sessionDraftsRef.current.get(sessionId) as unknown - const text = draft && typeof draft === 'object' - ? (draft as { text?: unknown }).text - : draft - return coerceInputText(text) - }, []) - - // Getter for persisted attachment refs (path + name only — not hydrated files). - // Consumers that need FileAttachment objects should call hydrateDraftAttachments. - const getDraftAttachmentRefs = useCallback((sessionId: string): DraftAttachmentRef[] => { - const attachments = sessionDraftsRef.current.get(sessionId)?.attachments - return Array.isArray(attachments) ? attachments : [] - }, []) - - // Hydrate persisted attachment refs into full FileAttachment objects. - // - Track C (ref.content set): reconstruct directly from the inlined bytes. - // - Track P (path-only): re-read from disk via the readUserAttachment RPC. - // Missing/moved files on Track P are silently dropped with a console warn — same - // UX as any other editor draft restore when the backing file is gone. - const hydrateDraftAttachments = useCallback(async (sessionId: string): Promise => { - const attachments = sessionDraftsRef.current.get(sessionId)?.attachments - const refs = Array.isArray(attachments) ? attachments : [] - if (refs.length === 0) return [] - const results = await Promise.all( - refs.map(async (ref) => { - if (ref.content) { - return attachmentFromContentRef(ref) - } - try { - const attachment = await window.electronAPI.readUserAttachment(ref.path) - if (!attachment) { - console.warn('[drafts] Attachment missing on restore, dropping:', ref.path) - return null - } - return attachment - } catch (err) { - console.warn('[drafts] Failed to restore attachment, dropping:', ref.path, err) - return null - } - }) - ) - return results.filter((a): a is FileAttachment => a !== null) - }, []) - - // Write a debounced snapshot of the current ref entry to disk. - const schedulePersistDraft = useCallback((sessionId: string) => { - const existingTimeout = draftSaveTimeoutRef.current.get(sessionId) - if (existingTimeout) { - clearTimeout(existingTimeout) - } - const timeout = setTimeout(() => { - const draft = sessionDraftsRef.current.get(sessionId) ?? { text: '' } - window.electronAPI.setDraft(sessionId, draft) - draftSaveTimeoutRef.current.delete(sessionId) - }, DRAFT_SAVE_DEBOUNCE_MS) - draftSaveTimeoutRef.current.set(sessionId, timeout) - }, []) - - const handleInputChange = useCallback((sessionId: string, value: string) => { - const text = coerceInputText(value) - const existing = sessionDraftsRef.current.get(sessionId) - const existingAttachments = Array.isArray(existing?.attachments) ? existing.attachments : [] - const nextDraft: SessionDraft = { - text, - ...(existingAttachments.length > 0 - ? { attachments: existingAttachments } - : {}), - } - const isEmpty = !nextDraft.text && (!nextDraft.attachments || nextDraft.attachments.length === 0) - if (isEmpty) { - sessionDraftsRef.current.delete(sessionId) - } else { - sessionDraftsRef.current.set(sessionId, nextDraft) - } - schedulePersistDraft(sessionId) - }, [schedulePersistDraft]) - - const handleAttachmentsChange = useCallback((sessionId: string, attachments: FileAttachment[]) => { - const existing = sessionDraftsRef.current.get(sessionId) - const refs: DraftAttachmentRef[] = [] - for (const a of attachments) { - const ref = toDraftRef(a) - if (ref) { - refs.push(ref) - } else { - console.warn('[drafts] attachment exceeds per-draft size cap, not persisted:', a.name, a.size) - } - } - const nextDraft: SessionDraft = { - text: coerceInputText(existing?.text), - ...(refs.length > 0 ? { attachments: refs } : {}), - } - const isEmpty = !nextDraft.text && (!nextDraft.attachments || nextDraft.attachments.length === 0) - if (isEmpty) { - sessionDraftsRef.current.delete(sessionId) - } else { - sessionDraftsRef.current.set(sessionId, nextDraft) - } - schedulePersistDraft(sessionId) - }, [schedulePersistDraft]) - - // Open new chat as a draft. The backing session is created on first send. - // Used by components via AppShellContext and for programmatic navigation - const openNewChat = useCallback(async (params: NewChatActionParams = {}) => { - if (!windowWorkspaceId) { - console.warn('[App] Cannot open new chat: no workspace ID') - return - } - - navigate(routes.action.newSession( - params.input || params.name - ? { - ...(params.input ? { input: params.input } : {}), - ...(params.name ? { name: params.name } : {}), - } - : undefined - )) - }, [windowWorkspaceId]) - - const handleRespondToPermission = useCallback(async ( - sessionId: string, - requestId: string, - allowed: boolean, - alwaysAllow: boolean, - options?: import('../shared/types').PermissionResponseOptions, - ) => { - const success = await window.electronAPI.respondToPermission(sessionId, requestId, allowed, alwaysAllow, options) - - if (success) { - // Remove only the first permission from the queue (the one we just responded to) - setPendingPermissions(prev => { - const next = new Map(prev) - const queue = next.get(sessionId) || [] - const remainingQueue = queue.slice(1) // Remove first item - if (remainingQueue.length === 0) { - next.delete(sessionId) - } else { - next.set(sessionId, remainingQueue) - } - return next - }) - // Note: No need to force session refresh - per-session atoms update automatically - } else { - // Response failed (agent/session gone) - clear the permission anyway - // to avoid UI being stuck with stale permission - setPendingPermissions(prev => { - const next = new Map(prev) - const queue = next.get(sessionId) || [] - const remainingQueue = queue.slice(1) - if (remainingQueue.length === 0) { - next.delete(sessionId) - } else { - next.set(sessionId, remainingQueue) - } - return next - }) - } - }, []) - - const handleRespondToCredential = useCallback(async (sessionId: string, requestId: string, response: CredentialResponse) => { - const success = await window.electronAPI.respondToCredential(sessionId, requestId, response) - - if (success) { - // Remove only the first credential from the queue (the one we just responded to) - setPendingCredentials(prev => { - const next = new Map(prev) - const queue = next.get(sessionId) || [] - const remainingQueue = queue.slice(1) // Remove first item - if (remainingQueue.length === 0) { - next.delete(sessionId) - } else { - next.set(sessionId, remainingQueue) - } - return next - }) - // Note: No need to force session refresh - per-session atoms update automatically - } else { - // Response failed (agent/session gone) - clear the credential anyway - // to avoid UI being stuck with stale credential request - setPendingCredentials(prev => { - const next = new Map(prev) - const queue = next.get(sessionId) || [] - const remainingQueue = queue.slice(1) - if (remainingQueue.length === 0) { - next.delete(sessionId) - } else { - next.set(sessionId, remainingQueue) - } - return next - }) - } - }, []) - - // Centralized link interceptor: classifies file types and decides whether to - // show an in-app preview overlay or open externally. Replaces the old - // handleOpenFile/handleOpenUrl that always opened in external apps. - const linkInterceptor = useLinkInterceptor({ - openFileExternal: async (path) => { - try { - await window.electronAPI.openFile(path) - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - console.error('Failed to open file:', error) - toast.error(t('toast.failedToOpenFile'), { - description: message, - }) - } - }, - openUrl: async (url) => { - try { - await window.electronAPI.openUrl(url) - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - console.error('Failed to open URL:', error) - toast.error(t('toast.failedToOpenLink'), { - description: `${message}. If this is a local path, use Open File instead.`, - }) - } - }, - showInFolder: async (path) => { - try { - await window.electronAPI.showInFolder(path) - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - console.error('Failed to show in folder:', error) - toast.error(t("toast.failedToReveal", { fileManager: getFileManagerName() }), { - description: message, - }) - } - }, - readFile: (path) => window.electronAPI.readFile(path), - readFileDataUrl: (path) => window.electronAPI.readFileDataUrl(path), - readFileBinary: (path) => window.electronAPI.readFileBinary(path), - }) - - const connectionState = useTransportConnectionState() - const showTransportConnectionBanner = shouldShowTransportConnectionBanner(connectionState) - - const handleReconnectTransport = useCallback(() => { - void window.electronAPI.reconnectTransport().catch((error) => { - const message = error instanceof Error ? error.message : 'Unknown error' - toast.error(t('toast.reconnectFailed'), { description: message }) - }) - }, []) - - const handleOpenFile = linkInterceptor.handleOpenFile - const handleOpenUrlExternal = linkInterceptor.handleOpenUrl - - const handleOpenUrlInBuiltInBrowser = useCallback((url: string) => { - void openUrlInBuiltInBrowser(url, { - browserPaneApi: window.electronAPI?.browserPane, - isChannelAvailable: window.electronAPI?.isChannelAvailable, - openExternal: handleOpenUrlExternal, - }) - }, [handleOpenUrlExternal]) - - const handleOpenSettings = useCallback(() => { - navigate(routes.view.settings()) - }, []) - - const handleOpenKeyboardShortcuts = useCallback(() => { - navigate(routes.view.settings('shortcuts')) - }, []) - - const handleOpenStoredUserPreferences = useCallback(() => { - navigate(routes.view.settings('preferences')) - }, []) - - // Show reset confirmation dialog - const handleReset = useCallback(() => { - setShowResetDialog(true) - }, []) - - // Execute reset after user confirms in dialog - const executeReset = useCallback(async () => { - try { - await window.electronAPI.logout() - // Reset all state - // Clear session atoms - initialize with empty array clears all per-session atoms - initializeSessions([]) - setWorkspaces([]) - windowWorkspaceIdRef.current = null - setWindowWorkspaceId(null) - // Reset setupNeeds to force fresh onboarding start - setSetupNeeds({ - needsBillingConfig: true, - needsCredentials: true, - isFullyConfigured: false, - }) - // Reset onboarding hook state - onboarding.reset() - setAppState('onboarding') - } catch (error) { - console.error('Reset failed:', error) - } finally { - setShowResetDialog(false) - } - }, [onboarding, initializeSessions]) - - // Handle workspace selection - // - Default: switch workspace in same window (in-window switching) - // - With openInNewWindow=true: open in new window (or focus existing) - const handleSelectWorkspace = useCallback(async ( - workspaceId: string, - openInNewWindow = false, - options?: { route?: ViewRoute; suppressSessionListLoading?: boolean }, - ) => { - // If selecting current workspace, do nothing - if (workspaceId === windowWorkspaceIdRef.current) return - - if (openInNewWindow) { - // Open (or focus) the window for the selected workspace - window.electronAPI.openWorkspace(workspaceId) - return - } - - pendingWorkspaceSwitchRouteRef.current = options?.route - ? { workspaceId, route: options.route } - : null - - setSessionLoadError(null) - if (!options?.suppressSessionListLoading) { - setSessionListLoading(true) - } - - const switchSeq = ++workspaceSwitchSeqRef.current - const requestSeq = ++sessionListRequestSeqRef.current - - const runSwitch = async () => { - if (switchSeq !== workspaceSwitchSeqRef.current) return - - const targetWorkspace = workspaces.find(w => w.id === workspaceId) - let loadedSessions: Session[] | null = null - const workspaceSessions = store.get(workspaceSessionsAtom) - const cachedMetas = getWorkspaceSessionMetas(workspaceSessions, workspaceId) - if (targetWorkspace?.remoteServer && cachedMetas.length > 0) { - loadedSessions = cachedMetas.map(meta => sessionFromMeta(meta, targetWorkspace?.name)) - } - - // Local workspaces can be read before the active window context changes. - // That lets us commit the target workspace and its session list together. - if (!targetWorkspace?.remoteServer) { - try { - loadedSessions = await window.electronAPI.getSessionsForWorkspace(workspaceId, { refreshExternal: true }) - cacheWorkspaceSessionMetas(workspaceId, loadedSessions) - } catch (error) { - console.warn(`[App] Failed to preload sessions for workspace ${workspaceId}:`, error) - if (cachedMetas.length > 0) { - loadedSessions = cachedMetas.map(meta => sessionFromMeta(meta, targetWorkspace?.name)) - } - } - } - - if (switchSeq !== workspaceSwitchSeqRef.current || requestSeq !== sessionListRequestSeqRef.current) { - return - } - - // Switch workspace in current window. - await window.electronAPI.switchWorkspace(workspaceId) - - if (switchSeq !== workspaceSwitchSeqRef.current || requestSeq !== sessionListRequestSeqRef.current) { - return - } - - if (!loadedSessions) { - try { - loadedSessions = await window.electronAPI.getSessions() - } catch (error) { - if (switchSeq !== workspaceSwitchSeqRef.current || requestSeq !== sessionListRequestSeqRef.current) { - return - } - console.error(`[App] Failed to load sessions while switching to workspace ${workspaceId}:`, error) - setSessionLoadError(formatSessionLoadFailure(error)) - loadedSessions = [] - } - } - - if (switchSeq !== workspaceSwitchSeqRef.current || requestSeq !== sessionListRequestSeqRef.current) { - return - } - - sessionDraftsRef.current.clear() - windowWorkspaceIdRef.current = workspaceId - - // Keep the switch visually atomic: the active workspace and its session - // metadata land in the same paint, instead of briefly rendering empty UI. - flushSync(() => { - setWindowWorkspaceId(workspaceId) - setSession({ selected: null }) - setPendingPermissions(new Map()) - setPendingCredentials(new Map()) - applyLoadedSessions( - loadedSessions, - workspaceId, - targetWorkspace?.remoteServer?.remoteWorkspaceId, - ) - setSessionsLoaded(true) - setSessionListLoading(false) - }) - - void reconcileLoadedSessionPermissionModes(loadedSessions) - // Note: NavigationContext detects the workspaceId change and handles - // panel restoration from the stored workspace URL (or defaults to allSessions). - // Sessions and theme still refresh via windowWorkspaceId-dependent effects, - // but the first frame already has the target workspace's session metadata. - } - - const nextSwitch = workspaceSwitchChainRef.current - .catch(() => {}) - .then(runSwitch) - .catch((error) => { - if (switchSeq === workspaceSwitchSeqRef.current) { - setSessionListLoading(false) - } - console.error(`[App] Failed to switch workspace to ${workspaceId}:`, error) - }) - - workspaceSwitchChainRef.current = nextSwitch.catch(() => {}) - return nextSwitch - }, [workspaces, store, setWindowWorkspaceId, setSession, applyLoadedSessions, cacheWorkspaceSessionMetas, reconcileLoadedSessionPermissionModes]) - - // Handle workspace switch by slug (called by NavigationContext on popstate when ?ws= changes) - const handleSwitchWorkspaceBySlug = useCallback((slug: string) => { - const target = workspaces.find(w => w.slug === slug) - if (target) { - handleSelectWorkspace(target.id) - } - }, [workspaces, handleSelectWorkspace]) - - // Handle workspace refresh (e.g., after icon upload) - const handleRefreshWorkspaces = useCallback(() => { - window.electronAPI.getWorkspaces().then(setWorkspaces) - }, []) - - // Handle cancel during onboarding - const handleOnboardingCancel = useCallback(() => { - onboarding.handleCancel() - }, [onboarding]) - - // Build context value for AppShell component - // This is memoized to prevent unnecessary re-renders - // IMPORTANT: Must be before early returns to maintain consistent hook order - const appShellContextValue = useMemo(() => ({ - // Data - // NOTE: sessions is NOT included - use sessionMetaMapAtom for listing - // and useSession(id) hook for individual sessions. This prevents memory leaks. - workspaces, - activeWorkspaceId: windowWorkspaceId, - activeWorkspaceSlug: windowWorkspaceSlug, - llmConnections, - workspaceDefaultLlmConnection, - refreshLlmConnections, - onOptimisticDefaultModelChange: handleOptimisticDefaultModelChange, - pendingPermissions, - pendingCredentials, - getDraft, - getDraftAttachmentRefs, - hydrateDraftAttachments, - globalPermissionMode, - sessionOptions, - // Session callbacks - onCreateSession: handleCreateSession, - onSendMessage: handleSendMessage, - onRenameSession: handleRenameSession, - onFlagSession: handleFlagSession, - onUnflagSession: handleUnflagSession, - onArchiveSession: handleArchiveSession, - onUnarchiveSession: handleUnarchiveSession, - onMarkSessionRead: handleMarkSessionRead, - onMarkSessionUnread: handleMarkSessionUnread, - onSetActiveViewingSession: handleSetActiveViewingSession, - onSessionStatusChange: handleSessionStatusChange, - onDeleteSession: handleDeleteSession, - onRespondToPermission: handleRespondToPermission, - onRespondToCredential: handleRespondToCredential, - // File/URL handlers - onOpenFile: handleOpenFile, - onOpenUrl: handleOpenUrlInBuiltInBrowser, - // Workspace - onSelectWorkspace: handleSelectWorkspace, - onRefreshWorkspaces: handleRefreshWorkspaces, - // App actions - onOpenSettings: handleOpenSettings, - onOpenKeyboardShortcuts: handleOpenKeyboardShortcuts, - onOpenStoredUserPreferences: handleOpenStoredUserPreferences, - onReset: handleReset, - // Session options - onSessionOptionsChange: handleSessionOptionsChange, - onInputChange: handleInputChange, - onAttachmentsChange: handleAttachmentsChange, - // New chat (via deep link navigation) - openNewChat, - }), [ - // NOTE: sessions removed to prevent memory leaks - components use atoms instead - workspaces, - windowWorkspaceId, - windowWorkspaceSlug, - llmConnections, - workspaceDefaultLlmConnection, - refreshLlmConnections, - handleOptimisticDefaultModelChange, - pendingPermissions, - pendingCredentials, - getDraft, - getDraftAttachmentRefs, - hydrateDraftAttachments, - globalPermissionMode, - sessionOptions, - handleCreateSession, - handleSendMessage, - handleRenameSession, - handleFlagSession, - handleUnflagSession, - handleArchiveSession, - handleUnarchiveSession, - handleMarkSessionRead, - handleMarkSessionUnread, - handleSetActiveViewingSession, - handleSessionStatusChange, - handleDeleteSession, - handleRespondToPermission, - handleRespondToCredential, - handleOpenFile, - handleOpenUrlInBuiltInBrowser, - handleSelectWorkspace, - handleRefreshWorkspaces, - handleOpenSettings, - handleOpenKeyboardShortcuts, - handleOpenStoredUserPreferences, - handleReset, - handleSessionOptionsChange, - handleInputChange, - handleAttachmentsChange, - openNewChat, - ]) - - // Platform actions for @craft-agent/ui components (overlays, etc.) - // Memoized to prevent re-renders when these callbacks don't change - // NOTE: Must be defined before early returns to maintain consistent hook order - const platformActions = useMemo(() => ({ - onOpenFile: handleOpenFile, - onOpenUrl: handleOpenUrlInBuiltInBrowser, - onOpenUrlExternal: handleOpenUrlExternal, - // Bypass link interceptor — opens file directly in system editor. - // Used by overlay header badges (when already viewing a file, "Open" should launch editor). - onOpenFileExternal: linkInterceptor.openFileExternal, - // Read file contents as UTF-8 string (used by datatable/spreadsheet/html-preview src fields) - onReadFile: (path: string) => window.electronAPI.readFile(path), - // Read file as data URL (used by image-preview blocks) - onReadFileDataUrl: (path: string) => window.electronAPI.readFileDataUrl(path), - // Read file as binary Uint8Array (used by PDF preview blocks) - onReadFileBinary: (path: string) => window.electronAPI.readFileBinary(path), - // Reveal a file in the system file manager (Finder on macOS, Explorer on Windows, etc.) - onRevealInFinder: (path: string) => { - window.electronAPI.showInFolder(path).catch(() => {}) - }, - // Platform-specific file manager name for UI labels - fileManagerName: getFileManagerName(), - // Hide/show macOS traffic lights when fullscreen overlays are open - onSetTrafficLightsVisible: (visible: boolean) => { - window.electronAPI.setTrafficLightsVisible(visible) - }, - }), [ - handleOpenFile, - handleOpenUrlExternal, - handleOpenUrlInBuiltInBrowser, - linkInterceptor.openFileExternal, - ]) - - // Loading state - show splash screen - if (appState === 'loading') { - return - } - - // Reauth state - session expired, need to re-login - // ModalProvider + WindowCloseHandler ensures X button works on Windows - if (appState === 'reauth') { - return ( - - - - - setShowResetDialog(false)} - /> - - - ) - } - - // Onboarding state - // ModalProvider + WindowCloseHandler ensures X button works on Windows - // (without this, the close IPC message has no listener and window stays open) - if (appState === 'onboarding') { - return ( - - - - - - - ) - } - - // Workspace picker — thin client with no workspace selected - if (appState === 'workspace-picker') { - return ( - - - - { - await window.electronAPI.switchWorkspace(id) - windowWorkspaceIdRef.current = id - setWindowWorkspaceId(id) - setAppState('ready') - }} - /> - - - ) - } - - // Show splash until exit animation completes - const showSplash = !splashHidden - const isActiveSessionListLoading = - sessionListLoading || - (!!windowWorkspaceId && sessionListRefreshWorkspaceIds.has(windowWorkspaceId)) - - // Ready state - main app with splash overlay during data loading - return ( - - - - - - - - - {/* Handle window close requests (X button, Cmd+W) - close modal first if open */} - - - {/* Splash screen overlay - fades out when fully ready */} - {showSplash && ( - - )} - - {/* Main UI - always rendered, splash fades away to reveal it */} -
- {showTransportConnectionBanner && connectionState && ( - - )} - {/* Main content + docked file preview live side-by-side so opening a file keeps - the conversation and file tree visible (VS Code / Cursor style split layout). */} -
-
- {sessionLoadError ? ( - { void loadSessionsFromServer() }} - /> - ) : ( - - )} -
- - {/* File preview side panel — opened by the link interceptor when a previewable - file is clicked. Rendered as a resizable docked panel rather than fullscreen. */} - {linkInterceptor.previewState && ( - - - - )} -
- setShowResetDialog(false)} - /> -
-
-
-
-
-
-
-
-
- ) -} - -/** - * Component that handles window close requests. - * Must be inside ModalProvider to access the modal registry. - */ -function WindowCloseHandler() { - useWindowCloseHandler() - return null -} - -/** - * FilePreviewRenderer - Routes file preview state to the correct overlay component. - * - * Handles all preview types from the link interceptor: - * - image → ImagePreviewOverlay (binary, loaded via data URL) - * - pdf → PDFPreviewOverlay (binary, embedded via Chromium viewer) - * - code/text → CodePreviewOverlay (syntax highlighted) - * - markdown → DocumentFormattedMarkdownOverlay - * - json → JSONPreviewOverlay - * - * File path badges with "Open" / "Reveal in {file manager}" menus are provided - * automatically by PlatformContext — no per-overlay callback props needed. - */ -function FilePreviewRenderer({ - state, - onClose, - loadDataUrl, - loadPdfData, - isDark, - embedded = false, -}: { - state: FilePreviewState - onClose: () => void - loadDataUrl: (path: string) => Promise - loadPdfData: (path: string) => Promise - isDark: boolean - /** Render inside the docked side panel instead of a fullscreen overlay */ - embedded?: boolean -}) { - const theme = isDark ? 'dark' : 'light' as const - - switch (state.type) { - case 'image': - return ( - - ) - - case 'pdf': - return ( - - ) - - case 'code': - case 'text': - return ( - - ) - - case 'markdown': { - // Show PLAN header for .md files in plans folder (handles both absolute and relative paths) - const isPlanFile = - (state.filePath.includes('/plans/') || state.filePath.startsWith('plans/')) && - state.filePath.endsWith('.md') - return ( - - ) - } - - case 'json': { - // JSONPreviewOverlay expects parsed data, not a raw string. - // @uiw/react-json-view crashes on null value, so guard against it. - let parsedData: unknown = null - try { - if (state.content) parsedData = JSON.parse(state.content) - } catch { - // If parsing fails, fall back to showing as code - return ( - - ) - } - // If read failed and content is empty, show raw code overlay with the read error. - if ((!state.content || !state.content.trim()) && state.error) { - return ( - - ) - } - return ( - - ) - } - - default: - return null - } -} diff --git a/packages/desktop/apps/electron/src/renderer/actions/__tests__/keybinding-context.test.ts b/packages/desktop/apps/electron/src/renderer/actions/__tests__/keybinding-context.test.ts deleted file mode 100644 index f33e8f76011..00000000000 --- a/packages/desktop/apps/electron/src/renderer/actions/__tests__/keybinding-context.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { afterEach, describe, expect, it } from 'bun:test' -import { setDismissibleLayerBridge } from '../../lib/dismissible-layer-bridge' -import { getKeybindingContext } from '../keybinding-context' - -const originalDocument = globalThis.document - -afterEach(() => { - setDismissibleLayerBridge(null) - ;(globalThis as unknown as { document: Document | undefined }).document = originalDocument -}) - -describe('getKeybindingContext', () => { - it('sets menuOpen=true when dismissible stack has open layers', () => { - setDismissibleLayerBridge({ - registerLayer: () => () => {}, - hasOpenLayers: () => true, - getTopLayer: () => ({ id: 'island-1', type: 'island', priority: 200 }), - closeTop: () => true, - handleEscape: () => true, - }) - - ;(globalThis as unknown as { document: { querySelector: (_selector: string) => null } }).document = { - querySelector: () => null, - } - - const event = { - target: { tagName: 'DIV', isContentEditable: false }, - } as unknown as KeyboardEvent - - const context = getKeybindingContext(event) - expect(context.menuOpen).toBe(true) - }) - - it('sets menuOpen=true when island dialog overlay is open', () => { - ;(globalThis as unknown as { document: { querySelector: (selector: string) => object | null } }).document = { - querySelector: (selector: string) => { - if (selector.includes('[data-ca-island-dialog="true"][data-state="open"]')) { - return {} - } - - return null - }, - } - - const event = { - target: { tagName: 'DIV', isContentEditable: false }, - } as unknown as KeyboardEvent - - const context = getKeybindingContext(event) - expect(context.menuOpen).toBe(true) - }) - - it('sets menuOpen=false when no overlay is open', () => { - ;(globalThis as unknown as { document: { querySelector: (_selector: string) => null } }).document = { - querySelector: () => null, - } - - const event = { - target: { tagName: 'DIV', isContentEditable: false }, - } as unknown as KeyboardEvent - - const context = getKeybindingContext(event) - expect(context.menuOpen).toBe(false) - }) -}) diff --git a/packages/desktop/apps/electron/src/renderer/actions/definitions.ts b/packages/desktop/apps/electron/src/renderer/actions/definitions.ts deleted file mode 100644 index 1ac64e6c66a..00000000000 --- a/packages/desktop/apps/electron/src/renderer/actions/definitions.ts +++ /dev/null @@ -1,224 +0,0 @@ -import type { ActionDefinition } from './types' - -export const actions = { - // ═══════════════════════════════════════════ - // General - // ═══════════════════════════════════════════ - 'app.newChat': { - id: 'app.newChat', - label: 'New Chat', - description: 'Create a new chat session', - defaultHotkey: 'mod+n', - category: 'General', - }, - 'app.newChatInPanel': { - id: 'app.newChatInPanel', - label: 'New Chat in Panel', - description: 'Create a new chat session in a new panel', - defaultHotkey: 'mod+t', - category: 'General', - }, - 'app.settings': { - id: 'app.settings', - label: 'Settings', - description: 'Open application settings', - defaultHotkey: 'mod+,', - category: 'General', - }, - 'app.toggleTheme': { - id: 'app.toggleTheme', - label: 'Toggle Theme', - description: 'Switch between light and dark mode', - defaultHotkey: 'mod+shift+a', - category: 'General', - }, - 'app.search': { - id: 'app.search', - label: 'Search', - description: 'Open search panel', - defaultHotkey: 'mod+f', - category: 'General', - }, - 'app.keyboardShortcuts': { - id: 'app.keyboardShortcuts', - label: 'Keyboard Shortcuts', - description: 'Show keyboard shortcuts reference', - defaultHotkey: 'mod+/', - category: 'General', - }, - 'app.newWindow': { - id: 'app.newWindow', - label: 'New Window', - description: 'Open a new window', - defaultHotkey: 'mod+shift+n', - category: 'General', - }, - 'app.quit': { - id: 'app.quit', - label: 'Quit', - description: 'Quit the application', - defaultHotkey: 'mod+q', - category: 'General', - }, - - // ═══════════════════════════════════════════ - // Navigation - // ═══════════════════════════════════════════ - 'nav.focusSidebar': { - id: 'nav.focusSidebar', - label: 'Focus Sidebar', - defaultHotkey: 'mod+1', - category: 'Navigation', - }, - 'nav.focusNavigator': { - id: 'nav.focusNavigator', - label: 'Focus Navigator', - defaultHotkey: 'mod+2', - category: 'Navigation', - }, - 'nav.focusChat': { - id: 'nav.focusChat', - label: 'Focus Chat', - defaultHotkey: 'mod+3', - category: 'Navigation', - }, - 'nav.nextZone': { - id: 'nav.nextZone', - label: 'Focus Next Zone', - defaultHotkey: 'tab', - category: 'Navigation', - when: '!inputFocus', // Tab should work normally in text inputs - }, - 'nav.goBack': { - id: 'nav.goBack', - label: 'Go Back', - description: 'Navigate to previous session', - defaultHotkey: 'mod+[', - category: 'Navigation', - }, - 'nav.goForward': { - id: 'nav.goForward', - label: 'Go Forward', - description: 'Navigate to next session', - defaultHotkey: 'mod+]', - category: 'Navigation', - }, - 'nav.goBackAlt': { - id: 'nav.goBackAlt', - label: 'Go Back', - description: 'Navigate to previous session (arrow key)', - defaultHotkey: 'mod+left', - category: 'Navigation', - when: '!inputFocus', // CMD+Left = cursor to line start in text inputs - }, - 'nav.goForwardAlt': { - id: 'nav.goForwardAlt', - label: 'Go Forward', - description: 'Navigate to next session (arrow key)', - defaultHotkey: 'mod+right', - category: 'Navigation', - when: '!inputFocus', // CMD+Right = cursor to line end in text inputs - }, - - // ═══════════════════════════════════════════ - // View - // ═══════════════════════════════════════════ - 'view.toggleSidebar': { - id: 'view.toggleSidebar', - label: 'Toggle Sidebar', - defaultHotkey: 'mod+b', - category: 'View', - }, - 'view.toggleFocusMode': { - id: 'view.toggleFocusMode', - label: 'Toggle Focus Mode', - description: 'Hide both sidebars for distraction-free work', - defaultHotkey: 'mod+.', - category: 'View', - }, - - // ═══════════════════════════════════════════ - // Navigator (scoped — active entity list in middle panel) - // ═══════════════════════════════════════════ - 'navigator.selectAll': { - id: 'navigator.selectAll', - label: 'Select All', - defaultHotkey: 'mod+a', - category: 'Navigator', - scope: 'navigator', - when: 'navigatorFocus', // CMD+A = select all text when in input - }, - 'navigator.clearSelection': { - id: 'navigator.clearSelection', - label: 'Clear Selection', - defaultHotkey: 'escape', - category: 'Navigator', - scope: 'navigator', - when: 'navigatorFocus', - }, - - // ═══════════════════════════════════════════ - // Panels - // ═══════════════════════════════════════════ - 'panel.focusNext': { - id: 'panel.focusNext', - label: 'Focus Next Panel', - description: 'Move focus to the next panel', - defaultHotkey: 'mod+shift+]', - category: 'Navigation', - }, - 'panel.focusPrev': { - id: 'panel.focusPrev', - label: 'Focus Previous Panel', - description: 'Move focus to the previous panel', - defaultHotkey: 'mod+shift+[', - category: 'Navigation', - }, - - // ═══════════════════════════════════════════ - // Chat - // ═══════════════════════════════════════════ - 'chat.stopProcessing': { - id: 'chat.stopProcessing', - label: 'Stop Processing', - description: 'Cancel the current agent task (double-press)', - defaultHotkey: 'escape', - category: 'Chat', - scope: 'chat', - when: '!hasSelection', // Let browser clear selection first; overlays handled by hasOpenOverlay() in enabled callback - }, - 'chat.cyclePermissionMode': { - id: 'chat.cyclePermissionMode', - label: 'Cycle Permission Mode', - description: 'Switch between YOLO, Plan mode, Ask before edits, and Edit automatically', - defaultHotkey: 'shift+tab', - category: 'Chat', - when: '!inputFocus && !menuOpen', - }, - 'chat.nextSearchMatch': { - id: 'chat.nextSearchMatch', - label: 'Next Search Match', - defaultHotkey: 'mod+g', - category: 'Chat', - }, - 'chat.prevSearchMatch': { - id: 'chat.prevSearchMatch', - label: 'Previous Search Match', - defaultHotkey: 'mod+shift+g', - category: 'Chat', - }, - -} as const satisfies Record - -// Type-safe action IDs -export type ActionId = keyof typeof actions - -// Get all actions as array (for shortcuts page) -export const actionList = Object.values(actions) - -// Get actions by category (for organized display) -export const actionsByCategory = actionList.reduce((acc, action) => { - if (!acc[action.category]) acc[action.category] = [] - acc[action.category].push(action) - return acc -}, {} as Record) diff --git a/packages/desktop/apps/electron/src/renderer/actions/index.ts b/packages/desktop/apps/electron/src/renderer/actions/index.ts deleted file mode 100644 index 2394b9681e4..00000000000 --- a/packages/desktop/apps/electron/src/renderer/actions/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Re-export everything for convenient imports -export { ActionRegistryProvider, useActionRegistry } from './registry' -export { useAction } from './useAction' -export { useHotkeyLabel, useActionLabel } from './useHotkeyLabel' -export { actions, actionList, actionsByCategory, type ActionId } from './definitions' -export type { ActionDefinition, ActionHandler, ActionScope } from './types' diff --git a/packages/desktop/apps/electron/src/renderer/actions/keybinding-context.ts b/packages/desktop/apps/electron/src/renderer/actions/keybinding-context.ts deleted file mode 100644 index 08d910736cb..00000000000 --- a/packages/desktop/apps/electron/src/renderer/actions/keybinding-context.ts +++ /dev/null @@ -1,144 +0,0 @@ -/** - * Keybinding Context - * - * Provides context keys for when-clause evaluation in the action registry. - * Inspired by VSCode's context keys but much simpler. - * - * Context is computed at keydown time from the DOM + a module-level ref. - * No React state, no re-renders — just a synchronous snapshot. - */ - -import type { FocusZoneId } from '@/context/FocusContext' -import { hasOpenOverlay } from '@/lib/overlay-detection' - -/** - * Context keys available in when-clause expressions. - * All values are boolean — evaluated by `evaluateWhen()`. - */ -export interface KeybindingContext { - /** A text input (INPUT, TEXTAREA, contentEditable) has focus */ - inputFocus: boolean - /** Text is selected within a focused input */ - hasSelection: boolean - /** Chat focus zone is active */ - chatFocus: boolean - /** Navigator focus zone is active */ - navigatorFocus: boolean - /** Sidebar focus zone is active */ - sidebarFocus: boolean - /** A modal dialog or dropdown/popover is open */ - menuOpen: boolean -} - -// ───────────────────────────────────────────── -// Module-level zone ref -// Updated by: -// 1. FocusContext.focusZone() → setCurrentZone() (keyboard navigation: Cmd+1/2/3, Tab) -// 2. focusin listener below (click-based focus changes) -// The keyboard handler reads it synchronously via getKeybindingContext(). -// ───────────────────────────────────────────── - -let _currentZone: FocusZoneId | null = 'chat' - -export function setCurrentZone(zone: FocusZoneId | null) { - _currentZone = zone -} - -// Track zone from DOM focus events (clicks, programmatic focus). -// Zone containers are stamped with data-focus-zone by useFocusZone. -// This is a module-level variable assignment — zero React re-renders. -if (typeof document !== 'undefined') { - document.addEventListener('focusin', (e) => { - const target = e.target as HTMLElement - const zoneEl = target.closest('[data-focus-zone]') - if (zoneEl) { - _currentZone = zoneEl.getAttribute('data-focus-zone') as FocusZoneId - } - }) -} - -// ───────────────────────────────────────────── -// Context snapshot -// ───────────────────────────────────────────── - -/** - * Build a context snapshot from DOM state at event time. - * Called synchronously in the keyboard handler's capture phase. - */ -export function getKeybindingContext(e: KeyboardEvent): KeybindingContext { - const target = e.target as HTMLElement - const isInput = - target.tagName === 'INPUT' || - target.tagName === 'TEXTAREA' || - target.isContentEditable - - const hasSelection = (() => { - if (!isInput) return false - - // For contenteditable (rich text), check window selection - if (target.isContentEditable) { - const sel = window.getSelection() - return sel !== null && sel.toString().length > 0 - } - - // For INPUT/TEXTAREA, check selectionStart/End - const input = target as HTMLInputElement | HTMLTextAreaElement - if ( - typeof input.selectionStart === 'number' && - typeof input.selectionEnd === 'number' - ) { - return input.selectionStart !== input.selectionEnd - } - - return false - })() - - return { - inputFocus: isInput, - hasSelection, - chatFocus: _currentZone === 'chat', - navigatorFocus: _currentZone === 'navigator', - sidebarFocus: _currentZone === 'sidebar', - menuOpen: hasOpenOverlay(), - } -} - -// ───────────────────────────────────────────── -// When-clause evaluator -// ───────────────────────────────────────────── - -/** - * Evaluate a when-clause expression against the current context. - * - * Syntax (subset of VSCode's when-clause syntax): - * undefined → always true (action fires everywhere) - * 'inputFocus' → true when input has focus - * '!inputFocus' → true when input does NOT have focus - * 'a && b' → logical AND (all terms must be true) - * 'a || b' → logical OR (any group must be true) - * 'a && !b || c' → OR has lower precedence than AND - * - * @example evaluateWhen(undefined, ctx) // always true - * @example evaluateWhen('!inputFocus', ctx) // outside text inputs - * @example evaluateWhen('chatFocus && !hasSelection', ctx) - */ -export function evaluateWhen( - when: string | undefined, - ctx: KeybindingContext -): boolean { - if (when === undefined) return true - - // Split by || (OR groups) — any group must be true - const orGroups = when.split(/\s*\|\|\s*/) - return orGroups.some((group) => { - // Split by && (AND terms) — all terms must be true - const terms = group.split(/\s*&&\s*/) - return terms.every((term) => { - const trimmed = term.trim() - const negated = trimmed.startsWith('!') - const key = (negated ? trimmed.slice(1) : trimmed) as keyof KeybindingContext - const value = ctx[key] ?? false - return negated ? !value : value - }) - }) -} diff --git a/packages/desktop/apps/electron/src/renderer/actions/registry.tsx b/packages/desktop/apps/electron/src/renderer/actions/registry.tsx deleted file mode 100644 index 46ae5c9d797..00000000000 --- a/packages/desktop/apps/electron/src/renderer/actions/registry.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import React, { createContext, useContext, useCallback, useRef, useEffect } from 'react' -import { actions, type ActionId } from './definitions' -import type { ActionDefinition, ActionHandler } from './types' -import { isMac } from '@/lib/platform' -import { getKeybindingContext, evaluateWhen } from './keybinding-context' - -interface ActionRegistryContextType { - // Register a handler for an action - register: (handler: ActionHandler) => () => void - - // Execute an action by ID - execute: (actionId: ActionId) => void - - // Get the current hotkey for an action (respects user overrides) - getHotkey: (actionId: ActionId) => string | null - - // Get display string for UI (e.g., "⌘N" on Mac, "Ctrl+N" on Windows) - getHotkeyDisplay: (actionId: ActionId) => string | null - - // Get action definition - getAction: (actionId: ActionId) => typeof actions[ActionId] - - // User hotkey overrides (future: load from config) - userOverrides: Map -} - -const ActionRegistryContext = createContext(null) - -export function ActionRegistryProvider({ children }: { children: React.ReactNode }) { - const handlersRef = useRef>(new Map()) - const userOverrides = useRef>(new Map()) - - // Register a handler - const register = useCallback((handler: ActionHandler) => { - const handlers = handlersRef.current.get(handler.actionId) || [] - handlers.push(handler) - handlersRef.current.set(handler.actionId, handlers) - - // Return cleanup function - return () => { - const handlers = handlersRef.current.get(handler.actionId) || [] - const index = handlers.indexOf(handler) - if (index > -1) handlers.splice(index, 1) - } - }, []) - - // Execute an action - const execute = useCallback((actionId: ActionId) => { - const handlers = handlersRef.current.get(actionId) || [] - for (const handler of handlers) { - if (!handler.enabled || handler.enabled()) { - handler.handler() - break // Only execute first enabled handler - } - } - }, []) - - // Get hotkey for action - const getHotkey = useCallback((actionId: ActionId): string | null => { - // Check user overrides first - if (userOverrides.current.has(actionId)) { - return userOverrides.current.get(actionId) ?? null - } - return actions[actionId].defaultHotkey - }, []) - - // Get display string - const getHotkeyDisplay = useCallback((actionId: ActionId): string | null => { - const hotkey = getHotkey(actionId) - if (!hotkey) return null - return formatHotkeyDisplay(hotkey) - }, [getHotkey]) - - // Get action definition - const getAction = useCallback((actionId: ActionId) => { - return actions[actionId] - }, []) - - // Set up global hotkey listener - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - // Build context snapshot from DOM state at event time - const context = getKeybindingContext(e) - - // Check all actions for matching hotkey - for (const [actionId, action] of Object.entries(actions)) { - const hotkey = getHotkey(actionId as ActionId) - if (!hotkey || !matchesHotkey(e, hotkey)) continue - - // Evaluate when-clause against current context - if (!evaluateWhen((action as ActionDefinition).when, context)) continue - - const handlers = handlersRef.current.get(actionId as ActionId) || [] - for (const handler of handlers) { - if (!handler.enabled || handler.enabled()) { - e.preventDefault() - e.stopPropagation() - handler.handler() - return - } - } - } - } - - // Capture phase for reliable interception - window.addEventListener('keydown', handleKeyDown, true) - return () => window.removeEventListener('keydown', handleKeyDown, true) - }, [getHotkey]) - - const value: ActionRegistryContextType = { - register, - execute, - getHotkey, - getHotkeyDisplay, - getAction, - userOverrides: userOverrides.current, - } - - return ( - - {children} - - ) -} - -export function useActionRegistry() { - const context = useContext(ActionRegistryContext) - if (!context) { - throw new Error('useActionRegistry must be used within ActionRegistryProvider') - } - return context -} - -// ───────────────────────────────────────────── -// Utility functions -// ───────────────────────────────────────────── - -function matchesHotkey(e: KeyboardEvent, hotkey: string): boolean { - const parts = hotkey.toLowerCase().split('+') - const key = parts[parts.length - 1] - const needsMod = parts.includes('mod') - const needsShift = parts.includes('shift') - const needsAlt = parts.includes('alt') - - const modPressed = isMac ? e.metaKey : e.ctrlKey - const logicalKeyMatches = e.key.toLowerCase() === key - - // Handle special keys via physical code where logical values can vary by layout. - const specialKeys: Record = { - '[': 'BracketLeft', - ']': 'BracketRight', - ',': 'Comma', - '.': 'Period', - 'left': 'ArrowLeft', - 'right': 'ArrowRight', - 'up': 'ArrowUp', - 'down': 'ArrowDown', - 'escape': 'Escape', - 'tab': 'Tab', - } - - const specialCode = specialKeys[key] - - // Important: for text shortcuts (A-Z/0-9), match logical key only. - // Mixing in physical code (e.g. KeyQ) causes AZERTY/QWERTZ collisions such as - // Cmd+A incorrectly matching a Cmd+Q binding. - const codeMatches = specialCode - ? e.code === specialCode - : logicalKeyMatches - - // Check modifier requirements - const modCorrect = needsMod ? modPressed : !modPressed - const shiftCorrect = needsShift ? e.shiftKey : !e.shiftKey - const altCorrect = needsAlt ? e.altKey : !e.altKey - - return codeMatches && modCorrect && shiftCorrect && altCorrect -} - -function formatHotkeyDisplay(hotkey: string): string { - const parts = hotkey.toLowerCase().split('+') - - const symbols = parts.map(part => { - if (part === 'mod') return isMac ? '⌘' : 'Ctrl' - if (part === 'shift') return isMac ? '⇧' : 'Shift' - if (part === 'alt') return isMac ? '⌥' : 'Alt' - if (part === 'escape') return 'Esc' - if (part === 'tab') return 'Tab' - if (part === 'left') return '←' - if (part === 'right') return '→' - if (part === '[') return '[' - if (part === ']') return ']' - return part.toUpperCase() - }) - - return isMac ? symbols.join('') : symbols.join('+') -} diff --git a/packages/desktop/apps/electron/src/renderer/actions/types.ts b/packages/desktop/apps/electron/src/renderer/actions/types.ts deleted file mode 100644 index 5b3bb6c2e40..00000000000 --- a/packages/desktop/apps/electron/src/renderer/actions/types.ts +++ /dev/null @@ -1,25 +0,0 @@ -export type ActionScope = 'global' | 'navigator' | 'chat' | 'sidebar' - -export interface ActionDefinition { - id: string - label: string - description?: string - defaultHotkey: string | null // null = no default hotkey - category: string - scope?: ActionScope // Default: 'global' - /** When-clause expression controlling when the action fires. - * Omit = fires everywhere (default). Examples: - * - '!inputFocus' — only outside text inputs - * - 'chatFocus && !hasSelection' — chat zone, no text selected - * - 'navigatorFocus' — only when navigator is focused - * @see evaluateWhen() in keybinding-context.ts */ - when?: string -} - -export type ActionId = keyof typeof import('./definitions').actions - -export interface ActionHandler { - actionId: ActionId - handler: () => void - enabled?: () => boolean -} diff --git a/packages/desktop/apps/electron/src/renderer/actions/useAction.ts b/packages/desktop/apps/electron/src/renderer/actions/useAction.ts deleted file mode 100644 index 6da418a8a33..00000000000 --- a/packages/desktop/apps/electron/src/renderer/actions/useAction.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { useEffect, useRef } from 'react' -import { useActionRegistry } from './registry' -import type { ActionId } from './definitions' - -/** - * Register a handler for an action. - * - * @example - * useAction('app.newChat', () => handleNewChat()) - * - * @example - * // With enabled condition - * useAction('navigator.selectAll', selectAll, { - * enabled: () => zoneRef.current?.contains(document.activeElement) ?? false - * }) - */ -export function useAction( - actionId: ActionId, - handler: () => void, - options?: { enabled?: () => boolean }, - deps: unknown[] = [] -) { - const { register } = useActionRegistry() - const handlerRef = useRef(handler) - const optionsRef = useRef(options) - - // Keep refs current - useEffect(() => { - handlerRef.current = handler - optionsRef.current = options - }, [handler, options, ...deps]) - - // Register handler - useEffect(() => { - return register({ - actionId, - handler: () => handlerRef.current(), - enabled: optionsRef.current?.enabled ? () => optionsRef.current?.enabled?.() ?? false : undefined, - }) - }, [actionId, register]) -} diff --git a/packages/desktop/apps/electron/src/renderer/actions/useHotkeyLabel.ts b/packages/desktop/apps/electron/src/renderer/actions/useHotkeyLabel.ts deleted file mode 100644 index 70620367176..00000000000 --- a/packages/desktop/apps/electron/src/renderer/actions/useHotkeyLabel.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { useActionRegistry } from './registry' -import type { ActionId } from './definitions' - -/** - * Get the display string for an action's hotkey. - * - * @example - * const hotkey = useHotkeyLabel('app.newChat') // "⌘N" on Mac - * - * @example - * // In a tooltip - * - */ -export function useHotkeyLabel(actionId: ActionId): string | null { - const { getHotkeyDisplay } = useActionRegistry() - return getHotkeyDisplay(actionId) -} - -/** - * Get the action label and hotkey for display. - * - * @example - * const { label, hotkey } = useActionLabel('app.newChat') - * // label: "New Chat", hotkey: "⌘N" - */ -export function useActionLabel(actionId: ActionId) { - const { getAction, getHotkeyDisplay } = useActionRegistry() - const action = getAction(actionId) - return { - label: action.label, - description: 'description' in action ? action.description : undefined, - hotkey: getHotkeyDisplay(actionId), - } -} diff --git a/packages/desktop/apps/electron/src/renderer/assets/messaging-icons/telegram.svg b/packages/desktop/apps/electron/src/renderer/assets/messaging-icons/telegram.svg deleted file mode 100644 index 5c1ff29aa14..00000000000 --- a/packages/desktop/apps/electron/src/renderer/assets/messaging-icons/telegram.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - Telegram - - - - - diff --git a/packages/desktop/apps/electron/src/renderer/assets/messaging-icons/whatsapp.svg b/packages/desktop/apps/electron/src/renderer/assets/messaging-icons/whatsapp.svg deleted file mode 100644 index 1811148806e..00000000000 --- a/packages/desktop/apps/electron/src/renderer/assets/messaging-icons/whatsapp.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - WhatsApp - - - - - diff --git a/packages/desktop/apps/electron/src/renderer/assets/pets/qwen-spritesheet.webp b/packages/desktop/apps/electron/src/renderer/assets/pets/qwen-spritesheet.webp deleted file mode 100644 index 94d66875212..00000000000 Binary files a/packages/desktop/apps/electron/src/renderer/assets/pets/qwen-spritesheet.webp and /dev/null differ diff --git a/packages/desktop/apps/electron/src/renderer/assets/samples/sample-invoice.pdf b/packages/desktop/apps/electron/src/renderer/assets/samples/sample-invoice.pdf deleted file mode 100644 index 947f036ab74..00000000000 Binary files a/packages/desktop/apps/electron/src/renderer/assets/samples/sample-invoice.pdf and /dev/null differ diff --git a/packages/desktop/apps/electron/src/renderer/assets/samples/sample-landscape.jpg b/packages/desktop/apps/electron/src/renderer/assets/samples/sample-landscape.jpg deleted file mode 100644 index de7378e35b4..00000000000 Binary files a/packages/desktop/apps/electron/src/renderer/assets/samples/sample-landscape.jpg and /dev/null differ diff --git a/packages/desktop/apps/electron/src/renderer/assets/skill-market-hero.webp b/packages/desktop/apps/electron/src/renderer/assets/skill-market-hero.webp deleted file mode 100644 index 06a4a2ec3df..00000000000 Binary files a/packages/desktop/apps/electron/src/renderer/assets/skill-market-hero.webp and /dev/null differ diff --git a/packages/desktop/apps/electron/src/renderer/atoms/__tests__/browser-pane.test.ts b/packages/desktop/apps/electron/src/renderer/atoms/__tests__/browser-pane.test.ts deleted file mode 100644 index fa2055278b3..00000000000 --- a/packages/desktop/apps/electron/src/renderer/atoms/__tests__/browser-pane.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { describe, expect, it } from 'bun:test' -import { createStore } from 'jotai' -import type { BrowserInstanceInfo } from '../../../shared/types' -import { - browserInstancesAtom, - DEFAULT_DOCKED_BROWSER_INSTANCE_ID, - removeBrowserInstanceAtom, - setBrowserInstancesAtom, - updateBrowserInstanceAtom, -} from '../browser-pane' - -function makeInstance(id: string): BrowserInstanceInfo { - return { - id, - url: 'https://example.com', - title: 'Example', - favicon: null, - isLoading: false, - canGoBack: false, - canGoForward: false, - boundSessionId: null, - ownerType: 'manual', - ownerSessionId: null, - isVisible: true, - agentControlActive: false, - themeColor: null, - } -} - -describe('browser pane atoms', () => { - it('does not resurrect removed instance from stale update event', () => { - const store = createStore() - - store.set(updateBrowserInstanceAtom, makeInstance('browser-1')) - expect(store.get(browserInstancesAtom).map((i) => i.id)).toEqual(['browser-1']) - - store.set(removeBrowserInstanceAtom, 'browser-1') - expect(store.get(browserInstancesAtom)).toHaveLength(0) - - // Simulate late out-of-order state event arriving after removal - store.set(updateBrowserInstanceAtom, makeInstance('browser-1')) - - expect(store.get(browserInstancesAtom)).toHaveLength(0) - }) - - it('authoritative list refresh can restore an instance after prior remove', () => { - const store = createStore() - - store.set(removeBrowserInstanceAtom, 'browser-2') - expect(store.get(browserInstancesAtom)).toHaveLength(0) - - // Simulate full list() reconciliation from main process - store.set(setBrowserInstancesAtom, [makeInstance('browser-2')]) - - expect(store.get(browserInstancesAtom).map((i) => i.id)).toEqual(['browser-2']) - }) - - it('allows the fixed docked browser ID to reopen after removal', () => { - const store = createStore() - - store.set(removeBrowserInstanceAtom, DEFAULT_DOCKED_BROWSER_INSTANCE_ID) - store.set( - updateBrowserInstanceAtom, - makeInstance(DEFAULT_DOCKED_BROWSER_INSTANCE_ID), - ) - - expect(store.get(browserInstancesAtom).map((i) => i.id)).toEqual([ - DEFAULT_DOCKED_BROWSER_INSTANCE_ID, - ]) - }) -}) diff --git a/packages/desktop/apps/electron/src/renderer/atoms/__tests__/panel-stack-lanes.test.ts b/packages/desktop/apps/electron/src/renderer/atoms/__tests__/panel-stack-lanes.test.ts deleted file mode 100644 index 4f6ec93c9ea..00000000000 --- a/packages/desktop/apps/electron/src/renderer/atoms/__tests__/panel-stack-lanes.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { describe, it, expect } from 'bun:test' -import { createStore } from 'jotai' -import { - panelStackAtom, - focusedPanelIdAtom, - pushPanelAtom, - reconcilePanelStackAtom, - updateFocusedPanelRouteAtom, - type PanelStackEntry, -} from '../panel-stack' - -function getStack(store: ReturnType): PanelStackEntry[] { - return store.get(panelStackAtom) -} - -describe('panel stack single-lane behavior', () => { - it('keeps insertion order for new panels', () => { - const store = createStore() - - store.set(pushPanelAtom, { route: 'allSessions/session/s1' }) - store.set(pushPanelAtom, { route: 'sources/source/github' }) - store.set(pushPanelAtom, { route: 'settings' }) - - const stack = getStack(store) - expect(stack).toHaveLength(3) - expect(stack[0].route).toBe('allSessions/session/s1') - expect(stack[1].route).toBe('sources/source/github') - expect(stack[2].route).toBe('settings') - expect(stack.every((p) => p.laneId === 'main')).toBe(true) - }) - - it('implicit navigation updates focused panel route', () => { - const store = createStore() - - store.set(pushPanelAtom, { route: 'allSessions/session/s1' }) - store.set(pushPanelAtom, { route: 'sources/source/github' }) - - const sourcePanel = getStack(store).find((p) => p.route === 'sources/source/github') - expect(sourcePanel).toBeDefined() - store.set(focusedPanelIdAtom, sourcePanel!.id) - - store.set(updateFocusedPanelRouteAtom, 'allSessions/session/s2') - - const stack = getStack(store) - expect(stack).toHaveLength(2) - expect(stack.some((p) => p.route === 'allSessions/session/s2')).toBe(true) - expect(stack.some((p) => p.route === 'allSessions/session/s1')).toBe(true) - }) - - it('pushPanel afterIndex inserts immediately after the given panel', () => { - const store = createStore() - - store.set(pushPanelAtom, { route: 'allSessions/session/s1' }) - store.set(pushPanelAtom, { route: 'allSessions/session/s2' }) - - store.set(pushPanelAtom, { route: 'sources/source/linear', afterIndex: 0 }) - - const stack = getStack(store) - expect(stack).toHaveLength(3) - expect(stack[0].route).toBe('allSessions/session/s1') - expect(stack[1].route).toBe('sources/source/linear') - expect(stack[2].route).toBe('allSessions/session/s2') - }) - - it('reconcile focuses by focusedIndex first when duplicate routes exist', () => { - const store = createStore() - - const changed = store.set(reconcilePanelStackAtom, { - entries: [ - { route: 'allSessions/session/s1', proportion: 0.5 }, - { route: 'allSessions/session/s1', proportion: 0.5 }, - ], - focusedIndex: 1, - }) - - expect(changed).toBe(true) - - const stack = getStack(store) - expect(stack).toHaveLength(2) - const focusedId = store.get(focusedPanelIdAtom) - expect(focusedId).toBe(stack[1].id) - }) - - it('reconcile no-op keeps focused index target with duplicate routes', () => { - const store = createStore() - - store.set(reconcilePanelStackAtom, { - entries: [ - { route: 'allSessions/session/s1', proportion: 0.5 }, - { route: 'allSessions/session/s1', proportion: 0.5 }, - ], - focusedIndex: 1, - }) - - const stack = getStack(store) - const firstId = stack[0].id - const secondId = stack[1].id - expect(firstId).not.toBe(secondId) - - const changed = store.set(reconcilePanelStackAtom, { - entries: [ - { route: 'allSessions/session/s1', proportion: 0.5 }, - { route: 'allSessions/session/s1', proportion: 0.5 }, - ], - focusedIndex: 1, - }) - - expect(changed).toBe(false) - expect(store.get(focusedPanelIdAtom)).toBe(secondId) - }) -}) diff --git a/packages/desktop/apps/electron/src/renderer/atoms/__tests__/sessions.test.ts b/packages/desktop/apps/electron/src/renderer/atoms/__tests__/sessions.test.ts deleted file mode 100644 index bafe230f062..00000000000 --- a/packages/desktop/apps/electron/src/renderer/atoms/__tests__/sessions.test.ts +++ /dev/null @@ -1,707 +0,0 @@ -import { afterEach, describe, expect, it } from 'bun:test' -import { createStore } from 'jotai' -import type { Message, Session } from '../../../shared/types' -import { - sessionAtomFamily, - sessionMetaMapAtom, - sessionIdsAtom, - loadedSessionsAtom, - ensureSessionMessagesLoadedAtom, - forceSessionMessagesReloadAtom, - refreshSessionsMetadataAtom, - initializeSessionsAtom, - initializeWorkspaceSessionsAtom, - addSessionAtom, - updateSessionMetaAtom, - removeSessionAtom, - extractSessionMeta, - compareSessionsByFlaggedThenActivityDesc, - getWorkspaceSessionMetas, - mergeStableSessionMetaList, - workspaceSessionMetaCacheAtom, - workspaceSessionsAtom, -} from '../sessions' - -function msg(id: string, role: Message['role'] = 'user'): Message { - return { - id, - role, - content: `content:${id}`, - timestamp: Date.now(), - } -} - -function makeSession(overrides: Partial = {}): Session { - return { - id: overrides.id ?? 'session-1', - workspaceId: overrides.workspaceId ?? 'workspace-1', - messages: overrides.messages ?? [], - permissionMode: overrides.permissionMode ?? 'ask', - supportsBranching: overrides.supportsBranching ?? true, - ...overrides, - } as Session -} - -describe('extractSessionMeta', () => { - it('keeps messageCount unknown for metadata-only sessions without an explicit count', () => { - const meta = extractSessionMeta(makeSession({ messages: [] })) - - expect(meta.messageCount).toBeUndefined() - }) - - it('honors explicit zero messageCount for confirmed empty sessions', () => { - const meta = extractSessionMeta(makeSession({ messages: [], messageCount: 0 })) - - expect(meta.messageCount).toBe(0) - }) - - it('derives messageCount from loaded messages when no explicit count exists', () => { - const meta = extractSessionMeta(makeSession({ - messages: [msg('m1'), msg('m2', 'assistant')], - })) - - expect(meta.messageCount).toBe(2) - }) -}) - -describe('mergeStableSessionMetaList', () => { - it('refreshes metadata and orders sessions by activity time', () => { - const previous = [ - extractSessionMeta(makeSession({ id: 'old-a', lastMessageAt: 100 })), - extractSessionMeta(makeSession({ id: 'old-b', lastMessageAt: 90 })), - ] - const incoming = [ - extractSessionMeta(makeSession({ id: 'newer', lastMessageAt: 1000 })), - extractSessionMeta(makeSession({ id: 'old-b', lastMessageAt: 95, name: 'Updated B' })), - extractSessionMeta(makeSession({ id: 'old-a', lastMessageAt: 110, name: 'Updated A' })), - ] - - const merged = mergeStableSessionMetaList(previous, incoming) - - expect(merged.map(session => session.id)).toEqual(['newer', 'old-a', 'old-b']) - expect(merged[1]?.name).toBe('Updated A') - expect(merged[2]?.name).toBe('Updated B') - }) - - it('orders flagged sessions before unflagged sessions for display sorting', () => { - const sessions = [ - extractSessionMeta(makeSession({ id: 'recent', lastMessageAt: 300 })), - extractSessionMeta(makeSession({ id: 'flagged-old', lastMessageAt: 100, isFlagged: true })), - extractSessionMeta(makeSession({ id: 'flagged-new', lastMessageAt: 200, isFlagged: true })), - ] - - const sorted = [...sessions].sort(compareSessionsByFlaggedThenActivityDesc) - - expect(sorted.map(session => session.id)).toEqual(['flagged-new', 'flagged-old', 'recent']) - }) -}) - -describe('session message loading atoms', () => { - const originalWindow = globalThis.window - - afterEach(() => { - if (originalWindow) { - globalThis.window = originalWindow - } else { - // @ts-expect-error test cleanup for window shim - delete globalThis.window - } - }) - - it('forceSessionMessagesReloadAtom reloads an empty-but-loaded session', async () => { - const store = createStore() - const sessionId = 'session-1' - const calls: string[] = [] - - globalThis.window = { - electronAPI: { - getSessionMessages: async (id: string) => { - calls.push(id) - return makeSession({ - id, - messages: [msg('m1'), msg('m2', 'assistant')], - }) - }, - }, - } as unknown as typeof window - - store.set(sessionAtomFamily(sessionId), makeSession({ id: sessionId, messages: [] })) - store.set(loadedSessionsAtom, new Set([sessionId])) - - const normalResult = await store.set(ensureSessionMessagesLoadedAtom, sessionId) - expect(calls).toEqual([]) - expect(normalResult?.messages).toHaveLength(0) - - const forcedResult = await store.set(forceSessionMessagesReloadAtom, sessionId) - expect(calls).toEqual([sessionId]) - expect(forcedResult?.messages.map((message) => message.id)).toEqual(['m1', 'm2']) - expect(store.get(sessionAtomFamily(sessionId))?.messages.map((message) => message.id)).toEqual(['m1', 'm2']) - expect(store.get(loadedSessionsAtom).has(sessionId)).toBe(true) - }) - - it('does not mark stale empty-response fallback as loaded', async () => { - const store = createStore() - const sessionId = 'session-1' - const calls: string[] = [] - - globalThis.window = { - electronAPI: { - getSessionMessages: async (id: string) => { - calls.push(id) - if (calls.length === 1) { - return makeSession({ id, messages: [] }) - } - return makeSession({ - id, - messages: [msg('m1'), msg('m2', 'assistant')], - }) - }, - }, - } as unknown as typeof window - - store.set(sessionAtomFamily(sessionId), makeSession({ - id: sessionId, - messages: [msg('local-1'), msg('local-2', 'assistant')], - })) - - const firstResult = await store.set(ensureSessionMessagesLoadedAtom, sessionId) - expect(firstResult?.messages.map((message) => message.id)).toEqual(['local-1', 'local-2']) - expect(store.get(loadedSessionsAtom).has(sessionId)).toBe(false) - - const secondResult = await store.set(forceSessionMessagesReloadAtom, sessionId) - expect(calls).toEqual([sessionId, sessionId]) - expect(secondResult?.messages.map((message) => message.id)).toEqual(['m1', 'm2']) - expect(store.get(loadedSessionsAtom).has(sessionId)).toBe(true) - }) - - it('does not let a shorter processing response replace existing history', async () => { - const store = createStore() - const sessionId = 'session-1' - - globalThis.window = { - electronAPI: { - getSessionMessages: async (id: string) => makeSession({ - id, - isProcessing: true, - messages: [ - { ...msg('m3', 'assistant'), content: 'fresh:m3' }, - msg('m4', 'assistant'), - ], - }), - }, - } as unknown as typeof window - - store.set(sessionAtomFamily(sessionId), makeSession({ - id: sessionId, - isProcessing: true, - messages: [msg('m1'), msg('m2', 'assistant'), msg('m3', 'assistant')], - })) - - const result = await store.set(ensureSessionMessagesLoadedAtom, sessionId) - - expect(result?.messages.map((message) => [message.id, message.content])).toEqual([ - ['m1', 'content:m1'], - ['m2', 'content:m2'], - ['m3', 'fresh:m3'], - ['m4', 'content:m4'], - ]) - expect(store.get(loadedSessionsAtom).has(sessionId)).toBe(false) - }) - - it('throws when the backend cannot provide messages for a non-empty session', async () => { - const store = createStore() - const sessionId = 'session-1' - - globalThis.window = { - electronAPI: { - getSessionMessages: async () => null, - }, - } as unknown as typeof window - - store.set(sessionAtomFamily(sessionId), makeSession({ - id: sessionId, - messages: [], - messageCount: 2, - })) - - let error: unknown - try { - await store.set(ensureSessionMessagesLoadedAtom, sessionId) - } catch (err) { - error = err - } - - expect(error).toBeInstanceOf(Error) - expect(store.get(loadedSessionsAtom).has(sessionId)).toBe(false) - }) - - it('marks metadata-empty sessions as loaded when the backend returns no payload', async () => { - const store = createStore() - const sessionId = 'session-1' - - globalThis.window = { - electronAPI: { - getSessionMessages: async () => null, - }, - } as unknown as typeof window - - store.set(sessionAtomFamily(sessionId), makeSession({ - id: sessionId, - messages: [], - messageCount: 0, - })) - - const result = await store.set(ensureSessionMessagesLoadedAtom, sessionId) - - expect(result?.id).toBe(sessionId) - expect(store.get(loadedSessionsAtom).has(sessionId)).toBe(true) - }) - - it('does not trust a loaded flag when an existing-looking session has no messages in memory', async () => { - const store = createStore() - const sessionId = 'session-1' - const calls: string[] = [] - - globalThis.window = { - electronAPI: { - getSessionMessages: async (id: string) => { - calls.push(id) - return makeSession({ - id, - name: 'Existing session', - messageCount: 2, - messages: [msg('m1'), msg('m2', 'assistant')], - }) - }, - }, - } as unknown as typeof window - - store.set(sessionAtomFamily(sessionId), makeSession({ - id: sessionId, - name: 'Existing session', - messages: [], - messageCount: 2, - })) - store.set(sessionMetaMapAtom, new Map([[ - sessionId, - extractSessionMeta(makeSession({ - id: sessionId, - name: 'Existing session', - messages: [], - messageCount: 2, - })), - ]])) - store.set(loadedSessionsAtom, new Set([sessionId])) - - const result = await store.set(ensureSessionMessagesLoadedAtom, sessionId) - - expect(calls).toEqual([sessionId]) - expect(result?.messages.map((message) => message.id)).toEqual(['m1', 'm2']) - expect(store.get(loadedSessionsAtom).has(sessionId)).toBe(true) - }) - - it('preserves an existing metadata title when loaded messages omit the name', async () => { - const store = createStore() - const sessionId = 'session-1' - - globalThis.window = { - electronAPI: { - getSessionMessages: async (id: string) => makeSession({ - id, - messages: [msg('m1'), msg('m2', 'assistant')], - messageCount: 2, - }), - }, - } as unknown as typeof window - - store.set(sessionAtomFamily(sessionId), makeSession({ - id: sessionId, - messages: [], - messageCount: 2, - })) - store.set(sessionMetaMapAtom, new Map([[ - sessionId, - extractSessionMeta(makeSession({ - id: sessionId, - name: 'Qwen generated title', - messages: [], - messageCount: 2, - })), - ]])) - - const result = await store.set(ensureSessionMessagesLoadedAtom, sessionId) - - expect(result?.name).toBe('Qwen generated title') - expect(store.get(sessionAtomFamily(sessionId))?.name).toBe('Qwen generated title') - expect(store.get(sessionMetaMapAtom).get(sessionId)?.name).toBe('Qwen generated title') - }) -}) - -describe('initializeSessionsAtom', () => { - it('preserves already-loaded messages when reinitialized from metadata for the same workspace', () => { - const store = createStore() - const existingMessages = [msg('m1'), msg('m2', 'assistant')] - - store.set(sessionAtomFamily('s1'), makeSession({ - id: 's1', - workspaceId: 'workspace-1', - messages: existingMessages, - name: 'Old title', - })) - store.set(sessionIdsAtom, ['s1']) - store.set(loadedSessionsAtom, new Set(['s1'])) - - store.set(initializeSessionsAtom, [ - makeSession({ - id: 's1', - workspaceId: 'workspace-1', - messages: [], - messageCount: 2, - name: 'Fresh title', - }), - ]) - - const session = store.get(sessionAtomFamily('s1')) - expect(session?.messages.map(m => m.id)).toEqual(['m1', 'm2']) - expect(session?.name).toBe('Fresh title') - expect(store.get(loadedSessionsAtom).has('s1')).toBe(true) - expect(store.get(sessionMetaMapAtom).get('s1')?.name).toBe('Fresh title') - }) - - it('does not preserve messages across different workspaces', () => { - const store = createStore() - - store.set(sessionAtomFamily('s1'), makeSession({ - id: 's1', - workspaceId: 'workspace-old', - messages: [msg('old-message')], - })) - store.set(sessionIdsAtom, ['s1']) - store.set(loadedSessionsAtom, new Set(['s1'])) - - store.set(initializeSessionsAtom, [ - makeSession({ - id: 's1', - workspaceId: 'workspace-new', - messages: [], - }), - ]) - - const session = store.get(sessionAtomFamily('s1')) - expect(session?.messages).toEqual([]) - expect(session?.workspaceId).toBe('workspace-new') - expect(store.get(loadedSessionsAtom).has('s1')).toBe(false) - }) - - it('does not preserve messages when metadata confirms the session is empty', () => { - const store = createStore() - - store.set(sessionAtomFamily('s1'), makeSession({ - id: 's1', - messages: [msg('old-message')], - })) - store.set(sessionIdsAtom, ['s1']) - store.set(loadedSessionsAtom, new Set(['s1'])) - - store.set(initializeSessionsAtom, [ - makeSession({ - id: 's1', - messages: [], - messageCount: 0, - }), - ]) - - const session = store.get(sessionAtomFamily('s1')) - expect(session?.messages).toEqual([]) - expect(store.get(loadedSessionsAtom).has('s1')).toBe(false) - }) -}) - -describe('initializeWorkspaceSessionsAtom', () => { - it('keeps already-loaded sessions from other workspaces cached', () => { - const store = createStore() - const cachedMessages = [msg('cached')] - - store.set(initializeWorkspaceSessionsAtom, { - workspaceIds: ['workspace-a'], - sessions: [ - makeSession({ - id: 'session-a', - workspaceId: 'workspace-a', - messages: cachedMessages, - }), - ], - }) - - store.set(initializeWorkspaceSessionsAtom, { - workspaceIds: ['workspace-b'], - sessions: [ - makeSession({ - id: 'session-b', - workspaceId: 'workspace-b', - messages: [], - }), - ], - }) - - expect(store.get(sessionMetaMapAtom).has('session-a')).toBe(true) - expect(store.get(sessionMetaMapAtom).has('session-b')).toBe(true) - expect(store.get(sessionAtomFamily('session-a'))?.messages.map(m => m.id)).toEqual(['cached']) - expect(store.get(loadedSessionsAtom).has('session-a')).toBe(true) - }) - - it('removes stale sessions only from the refreshed workspace', () => { - const store = createStore() - - store.set(initializeWorkspaceSessionsAtom, { - workspaceIds: ['workspace-a'], - sessions: [ - makeSession({ id: 'session-a1', workspaceId: 'workspace-a' }), - makeSession({ id: 'session-a2', workspaceId: 'workspace-a' }), - ], - }) - store.set(initializeWorkspaceSessionsAtom, { - workspaceIds: ['workspace-b'], - sessions: [ - makeSession({ id: 'session-b1', workspaceId: 'workspace-b' }), - ], - }) - - store.set(initializeWorkspaceSessionsAtom, { - workspaceIds: ['workspace-a'], - sessions: [ - makeSession({ id: 'session-a1', workspaceId: 'workspace-a' }), - ], - }) - - expect(store.get(sessionMetaMapAtom).has('session-a1')).toBe(true) - expect(store.get(sessionMetaMapAtom).has('session-a2')).toBe(false) - expect(store.get(sessionMetaMapAtom).has('session-b1')).toBe(true) - }) - - it('keeps each workspace order in the workspace-scoped state', () => { - const store = createStore() - - store.set(initializeWorkspaceSessionsAtom, { - workspaceIds: ['workspace-a'], - sessions: [ - makeSession({ id: 'a-old', workspaceId: 'workspace-a', lastMessageAt: 100 }), - makeSession({ id: 'a-new', workspaceId: 'workspace-a', lastMessageAt: 200 }), - ], - }) - store.set(initializeWorkspaceSessionsAtom, { - workspaceIds: ['workspace-b'], - sessions: [ - makeSession({ id: 'b-one', workspaceId: 'workspace-b', lastMessageAt: 500 }), - ], - }) - - expect(getWorkspaceSessionMetas(store.get(workspaceSessionsAtom), 'workspace-a').map(session => session.id)) - .toEqual(['a-new', 'a-old']) - expect(getWorkspaceSessionMetas(store.get(workspaceSessionsAtom), 'workspace-b').map(session => session.id)) - .toEqual(['b-one']) - }) - - it('orders refreshed workspace metadata by activity time', () => { - const store = createStore() - - store.set(initializeWorkspaceSessionsAtom, { - workspaceIds: ['workspace-a'], - sessions: [ - makeSession({ id: 'a-old', workspaceId: 'workspace-a', lastMessageAt: 100 }), - makeSession({ id: 'a-new', workspaceId: 'workspace-a', lastMessageAt: 200 }), - ], - }) - - store.set(initializeWorkspaceSessionsAtom, { - workspaceIds: ['workspace-a'], - sessions: [ - makeSession({ id: 'a-added', workspaceId: 'workspace-a', lastMessageAt: 1000 }), - makeSession({ id: 'a-old', workspaceId: 'workspace-a', lastMessageAt: 700, name: 'Updated old' }), - makeSession({ id: 'a-new', workspaceId: 'workspace-a', lastMessageAt: 50 }), - ], - }) - - const metas = getWorkspaceSessionMetas(store.get(workspaceSessionsAtom), 'workspace-a') - expect(metas.map(session => session.id)).toEqual(['a-added', 'a-old', 'a-new']) - expect(metas[1]?.name).toBe('Updated old') - }) - - it('updates workspace-scoped metadata without reordering existing sessions', () => { - const store = createStore() - - store.set(initializeWorkspaceSessionsAtom, { - workspaceIds: ['workspace-a'], - sessions: [ - makeSession({ id: 's1', workspaceId: 'workspace-a', lastMessageAt: 200 }), - makeSession({ id: 's2', workspaceId: 'workspace-a', lastMessageAt: 100 }), - ], - }) - - store.set(updateSessionMetaAtom, 's2', { name: 'Updated S2', lastMessageAt: 999 }) - - const metas = getWorkspaceSessionMetas(store.get(workspaceSessionsAtom), 'workspace-a') - expect(metas.map(session => session.id)).toEqual(['s1', 's2']) - expect(metas[1]?.name).toBe('Updated S2') - }) - - it('pins flagged workspace sessions above unflagged sessions without changing group order', () => { - const store = createStore() - - store.set(initializeWorkspaceSessionsAtom, { - workspaceIds: ['workspace-a'], - sessions: [ - makeSession({ id: 's1', workspaceId: 'workspace-a', lastMessageAt: 300 }), - makeSession({ id: 's2', workspaceId: 'workspace-a', lastMessageAt: 200, isFlagged: true }), - makeSession({ id: 's3', workspaceId: 'workspace-a', lastMessageAt: 100, isFlagged: true }), - makeSession({ id: 's4', workspaceId: 'workspace-a', lastMessageAt: 50 }), - ], - }) - - expect(getWorkspaceSessionMetas(store.get(workspaceSessionsAtom), 'workspace-a').map(session => session.id)) - .toEqual(['s2', 's3', 's1', 's4']) - - store.set(updateSessionMetaAtom, 's4', { isFlagged: true }) - - expect(getWorkspaceSessionMetas(store.get(workspaceSessionsAtom), 'workspace-a').map(session => session.id)) - .toEqual(['s2', 's3', 's4', 's1']) - }) - - it('adds new sessions to the front and removes them from all workspace states', () => { - const store = createStore() - - store.set(initializeWorkspaceSessionsAtom, { - workspaceIds: ['workspace-a'], - sessions: [ - makeSession({ id: 's1', workspaceId: 'workspace-a', lastMessageAt: 200 }), - makeSession({ id: 's2', workspaceId: 'workspace-a', lastMessageAt: 100 }), - ], - }) - - store.set(addSessionAtom, makeSession({ id: 's3', workspaceId: 'workspace-a', lastMessageAt: 300 })) - expect(getWorkspaceSessionMetas(store.get(workspaceSessionsAtom), 'workspace-a').map(session => session.id)) - .toEqual(['s3', 's1', 's2']) - - store.set(removeSessionAtom, 's1') - expect(getWorkspaceSessionMetas(store.get(workspaceSessionsAtom), 'workspace-a').map(session => session.id)) - .toEqual(['s3', 's2']) - }) - - it('keeps the legacy workspace meta cache backed by workspaceSessionsAtom', () => { - const store = createStore() - const meta = extractSessionMeta(makeSession({ id: 'cached', workspaceId: 'workspace-a' })) - - store.set(workspaceSessionMetaCacheAtom, new Map([['workspace-a', [meta]]])) - - expect(getWorkspaceSessionMetas(store.get(workspaceSessionsAtom), 'workspace-a').map(session => session.id)) - .toEqual(['cached']) - }) -}) - -describe('refreshSessionsMetadataAtom', () => { - it('preserves messages for already-loaded sessions', () => { - const store = createStore() - const existingMessages = [msg('m1'), msg('m2', 'assistant')] - - // Pre-populate: session has messages and is marked loaded - store.set(sessionAtomFamily('s1'), makeSession({ id: 's1', messages: existingMessages })) - store.set(loadedSessionsAtom, new Set(['s1'])) - - // Refresh with metadata-only payload (empty messages, like getSessions returns) - const freshSessions = [makeSession({ id: 's1', messages: [] })] - store.set(refreshSessionsMetadataAtom, { - sessions: freshSessions, - loadedSessionIds: new Set(['s1']), - }) - - // Messages should be preserved from the existing atom - const session = store.get(sessionAtomFamily('s1')) - expect(session?.messages.map(m => m.id)).toEqual(['m1', 'm2']) - }) - - it('preserves existing messages even when loaded tracking is stale', () => { - const store = createStore() - const existingMessages = [msg('m1'), msg('m2', 'assistant')] - - store.set(sessionAtomFamily('s1'), makeSession({ id: 's1', messages: existingMessages })) - store.set(loadedSessionsAtom, new Set()) - - store.set(refreshSessionsMetadataAtom, { - sessions: [makeSession({ id: 's1', messages: [], messageCount: 2 })], - loadedSessionIds: new Set(), - }) - - const session = store.get(sessionAtomFamily('s1')) - expect(session?.messages.map(m => m.id)).toEqual(['m1', 'm2']) - expect(store.get(loadedSessionsAtom).has('s1')).toBe(false) - }) - - it('marks sessions as unloaded when atom was cleared but loadedSessionIds still tracked them', () => { - const store = createStore() - - // Session was previously loaded, but its atom was cleared (e.g., by remove + re-add) - // while loadedSessionsAtom still tracks it. The atom value is null. - store.set(loadedSessionsAtom, new Set(['s1'])) - // sessionAtomFamily('s1') defaults to null — no store.set needed - - // Refresh — s1 is in loadedSessionIds but current atom is null, - // so shouldPreserveMessages is false. Since it was in loadedSessionIds, - // it should be removed so lazy-loading re-fetches messages. - const freshSessions = [makeSession({ id: 's1', messages: [] })] - store.set(refreshSessionsMetadataAtom, { - sessions: freshSessions, - loadedSessionIds: new Set(['s1']), - }) - - expect(store.get(loadedSessionsAtom).has('s1')).toBe(false) - }) - - it('removes stale sessions from all atoms', () => { - const store = createStore() - - // Initialize with two sessions via initializeSessionsAtom - store.set(initializeSessionsAtom, [ - makeSession({ id: 's1' }), - makeSession({ id: 's2' }), - ]) - expect(store.get(sessionMetaMapAtom).size).toBe(2) - expect(store.get(sessionIdsAtom)).toContain('s2') - - // Refresh with only s1 — s2 should be removed - store.set(refreshSessionsMetadataAtom, { - sessions: [makeSession({ id: 's1' })], - loadedSessionIds: new Set(), - }) - - expect(store.get(sessionMetaMapAtom).has('s2')).toBe(false) - expect(store.get(sessionIdsAtom)).not.toContain('s2') - expect(store.get(sessionAtomFamily('s2'))).toBe(null) - }) - - it('updates metadata map and returns it', () => { - const store = createStore() - - const sessions = [ - makeSession({ id: 's1', name: 'First' }), - makeSession({ id: 's2', name: 'Second' }), - ] - - const result = store.set(refreshSessionsMetadataAtom, { - sessions, - loadedSessionIds: new Set(), - }) - - // Returned map matches store state - expect(result.size).toBe(2) - expect(result.get('s1')?.name).toBe('First') - expect(result.get('s2')?.name).toBe('Second') - - // Store is consistent - const storeMap = store.get(sessionMetaMapAtom) - expect(storeMap.size).toBe(2) - expect(storeMap.get('s1')?.name).toBe('First') - - // IDs are set - expect(store.get(sessionIdsAtom)).toHaveLength(2) - }) -}) diff --git a/packages/desktop/apps/electron/src/renderer/atoms/automations.ts b/packages/desktop/apps/electron/src/renderer/atoms/automations.ts deleted file mode 100644 index eec4c10dd34..00000000000 --- a/packages/desktop/apps/electron/src/renderer/atoms/automations.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Automations Atom - * - * Simple atom for storing parsed workspace automations. - * AppShell populates this when automations.json is loaded from the workspace root. - * MainContentPanel reads from it for automation detail display. - */ - -import { atom } from 'jotai' -import type { AutomationListItem } from '../components/automations/types' - -/** - * Atom to store the current workspace's parsed automations. - * AppShell loads automations.json, parses via parseAutomationsConfig(), and sets this atom. - */ -export const automationsAtom = atom([]) diff --git a/packages/desktop/apps/electron/src/renderer/atoms/browser-pane.ts b/packages/desktop/apps/electron/src/renderer/atoms/browser-pane.ts deleted file mode 100644 index 0d0375732c1..00000000000 --- a/packages/desktop/apps/electron/src/renderer/atoms/browser-pane.ts +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Browser Pane Atoms - * - * Jotai atoms for browser instance state in the renderer. - * Synced from the main process via BROWSER_PANE_STATE_CHANGED IPC events. - */ - -import { atom } from 'jotai' -import type { BrowserInstanceInfo } from '../../shared/types' - -export const DEFAULT_DOCKED_BROWSER_INSTANCE_ID = 'built-in-browser' - -/** Map of all browser instances by ID */ -export const browserInstancesMapAtom = atom>(new Map()) - -/** Derived: array of all browser instances (for iteration) */ -export const browserInstancesAtom = atom( - (get) => Array.from(get(browserInstancesMapAtom).values()) -) - -/** Derived: count of active browser instances */ -export const browserInstanceCountAtom = atom( - (get) => get(browserInstancesMapAtom).size -) - -/** Currently active browser instance ID (selected/focused by user interactions) */ -export const activeBrowserInstanceIdAtom = atom(null) - -/** Tombstones for instances removed from renderer state (guards against late out-of-order updates) */ -export const removedBrowserInstanceIdsAtom = atom>(new Set()) - -/** Derived: currently active browser instance info */ -export const activeBrowserInstanceAtom = atom((get) => { - const activeId = get(activeBrowserInstanceIdAtom) - if (!activeId) return null - return get(browserInstancesMapAtom).get(activeId) ?? null -}) - -/** Update a single browser instance (from IPC state change event) */ -export const updateBrowserInstanceAtom = atom( - null, - (get, set, info: BrowserInstanceInfo) => { - const removedIds = get(removedBrowserInstanceIdsAtom) - if (removedIds.has(info.id)) { - if (info.id !== DEFAULT_DOCKED_BROWSER_INSTANCE_ID) { - return - } - - const nextRemovedIds = new Set(removedIds) - nextRemovedIds.delete(info.id) - set(removedBrowserInstanceIdsAtom, nextRemovedIds) - } - - const map = new Map(get(browserInstancesMapAtom)) - map.set(info.id, info) - set(browserInstancesMapAtom, map) - } -) - -/** Remove a browser instance (when destroyed) */ -export const removeBrowserInstanceAtom = atom( - null, - (get, set, id: string) => { - const map = new Map(get(browserInstancesMapAtom)) - map.delete(id) - set(browserInstancesMapAtom, map) - - const removedIds = new Set(get(removedBrowserInstanceIdsAtom)) - removedIds.add(id) - set(removedBrowserInstanceIdsAtom, removedIds) - } -) - -/** Set all browser instances at once (from list query) */ -export const setBrowserInstancesAtom = atom( - null, - (get, set, instances: BrowserInstanceInfo[]) => { - const map = new Map() - for (const info of instances) { - map.set(info.id, info) - } - set(browserInstancesMapAtom, map) - - const removedIds = new Set(get(removedBrowserInstanceIdsAtom)) - for (const info of instances) { - removedIds.delete(info.id) - } - set(removedBrowserInstanceIdsAtom, removedIds) - } -) diff --git a/packages/desktop/apps/electron/src/renderer/atoms/messaging.ts b/packages/desktop/apps/electron/src/renderer/atoms/messaging.ts deleted file mode 100644 index 5f1703572a5..00000000000 --- a/packages/desktop/apps/electron/src/renderer/atoms/messaging.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Messaging Gateway Atoms - * - * Workspace-level state for messaging bindings. - * Populated by subscribing to messaging:bindingChanged push events. - */ - -import { atom } from 'jotai' - -export interface MessagingBinding { - id: string - workspaceId: string - sessionId: string - platform: string - channelId: string - channelName?: string - enabled: boolean - createdAt: number -} - -export const messagingBindingsAtom = atom([]) - -export const messagingBindingsBySessionAtom = atom((get) => { - const map = new Map() - for (const binding of get(messagingBindingsAtom)) { - if (!binding.enabled) continue - const list = map.get(binding.sessionId) - if (list) { - list.push(binding) - } else { - map.set(binding.sessionId, [binding]) - } - } - return map -}) - -export const setMessagingBindingsAtom = atom( - null, - (_get, set, bindings: MessagingBinding[]) => { - set(messagingBindingsAtom, bindings.filter((binding) => binding.enabled)) - }, -) - -/** - * Global messaging dialog state. - * - * Hoisted out of SessionMenu so dialogs survive context-menu / dropdown close. - * Rendered by mounted at AppShell level. - */ -export type MessagingDialogState = - | { kind: 'closed' } - | { - kind: 'pairing' - platform: 'telegram' | 'whatsapp' - sessionId: string - code: string | null - expiresAt: number | null - botUsername?: string - error?: string - } - | { - kind: 'wa_connect' - continueToPairingSessionId?: string - } - -export const messagingDialogAtom = atom({ kind: 'closed' }) diff --git a/packages/desktop/apps/electron/src/renderer/atoms/new-session-draft.ts b/packages/desktop/apps/electron/src/renderer/atoms/new-session-draft.ts deleted file mode 100644 index 1f45e95b6a3..00000000000 --- a/packages/desktop/apps/electron/src/renderer/atoms/new-session-draft.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { atom } from 'jotai' -import type { ContentBadge, CreateSessionOptions } from '../../shared/types' - -export const NEW_SESSION_DRAFT_ID = '__new_session_draft__' - -export interface NewSessionDraftState { - nonce: number - input: string - createOptions: CreateSessionOptions - badges?: ContentBadge[] -} - -export const newSessionDraftAtom = atom({ - nonce: 0, - input: '', - createOptions: {}, -}) diff --git a/packages/desktop/apps/electron/src/renderer/atoms/overlay.ts b/packages/desktop/apps/electron/src/renderer/atoms/overlay.ts deleted file mode 100644 index 016e0809bdb..00000000000 --- a/packages/desktop/apps/electron/src/renderer/atoms/overlay.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { atom } from 'jotai' - -/** - * Tracks whether a full-screen overlay is open (e.g., workspace creation). - * Used by AppShell to apply a scale-back effect on the main content. - */ -export const fullscreenOverlayOpenAtom = atom(false) diff --git a/packages/desktop/apps/electron/src/renderer/atoms/panel-stack.ts b/packages/desktop/apps/electron/src/renderer/atoms/panel-stack.ts deleted file mode 100644 index 18be1b50a5d..00000000000 --- a/packages/desktop/apps/electron/src/renderer/atoms/panel-stack.ts +++ /dev/null @@ -1,361 +0,0 @@ -/** - * Panel Stack State - * - * Single-lane panel model for side-by-side content panels. - */ - -import { atom } from 'jotai' -import { parseRouteToNavigationState } from '../../shared/route-parser' -import type { ViewRoute } from '../../shared/routes' - -let nextPanelId = 0 -function generatePanelId(): string { - return `panel-${++nextPanelId}-${Date.now()}` -} - -export type PanelType = - | 'session' - | 'source' - | 'settings' - | 'skills' - | 'skillMarketplace' - | 'other' -export type PanelLaneId = 'main' -export type OpenIntent = 'implicit' | 'explicit' - -export interface PanelLanePolicy { - id: PanelLaneId - order: number - allowedTypes: PanelType[] - locked: boolean - singleton: boolean -} - -export const PANEL_LANE_POLICIES: Record = { - main: { - id: 'main', - order: 0, - allowedTypes: [ - 'session', - 'source', - 'settings', - 'skills', - 'skillMarketplace', - 'other', - ], - locked: false, - singleton: false, - }, -} - -export interface PanelStackEntry { - id: string - route: ViewRoute - proportion: number - panelType: PanelType - laneId: PanelLaneId -} - -export const panelStackAtom = atom([]) -export const focusedPanelIdAtom = atom(null) - -export const panelCountAtom = atom((get) => get(panelStackAtom).length) - -export const focusedPanelIndexAtom = atom((get) => { - const stack = get(panelStackAtom) - const focusedId = get(focusedPanelIdAtom) - if (!focusedId) return 0 - const idx = stack.findIndex((p) => p.id === focusedId) - return idx === -1 ? 0 : idx -}) - -export const focusedPanelRouteAtom = atom((get) => { - const stack = get(panelStackAtom) - const idx = get(focusedPanelIndexAtom) - return stack[idx]?.route ?? null -}) - -export function getPanelTypeFromRoute(route: ViewRoute): PanelType { - const navState = parseRouteToNavigationState(route) - if (!navState) return 'other' - - switch (navState.navigator) { - case 'sessions': - return 'session' - case 'sources': - return 'source' - case 'settings': - return 'settings' - case 'skills': - return 'skills' - case 'skillMarketplace': - return 'skillMarketplace' - default: - return 'other' - } -} - -export function getDefaultLaneForType(_type: PanelType): PanelLaneId { - return 'main' -} - -function createEntry( - route: ViewRoute, - proportion: number, - id?: string, -): PanelStackEntry { - const panelType = getPanelTypeFromRoute(route) - return { - id: id ?? generatePanelId(), - route, - proportion, - panelType, - laneId: 'main', - } -} - -function normalizeProportions(stack: PanelStackEntry[]): PanelStackEntry[] { - if (stack.length === 0) return stack - const total = stack.reduce((sum, p) => sum + p.proportion, 0) - if (total <= 0) { - const equal = 1 / stack.length - return stack.map((p) => ({ ...p, proportion: equal })) - } - return stack.map((p) => ({ ...p, proportion: p.proportion / total })) -} - -export function parseSessionIdFromRoute(route: ViewRoute): string | null { - const segments = route.split('/') - const idx = segments.indexOf('session') - if (idx >= 0 && idx + 1 < segments.length) { - return segments[idx + 1] - } - return null -} - -export const focusedSessionIdAtom = atom((get) => { - const route = get(focusedPanelRouteAtom) - if (!route) return null - return parseSessionIdFromRoute(route) -}) - -export const pushPanelAtom = atom( - null, - ( - get, - set, - { - route, - afterIndex, - }: { - route: ViewRoute - afterIndex?: number - targetLaneId?: PanelLaneId - intent?: OpenIntent - }, - ) => { - const stack = get(panelStackAtom) - let insertAt = stack.length - if ( - afterIndex !== undefined && - afterIndex >= 0 && - afterIndex < stack.length - ) { - insertAt = afterIndex + 1 - } - - const newEntry = createEntry(route, 0) - const newStack = [ - ...stack.slice(0, insertAt), - newEntry, - ...stack.slice(insertAt), - ] - - const normalized = normalizeProportions(newStack) - set(panelStackAtom, normalized) - set(focusedPanelIdAtom, newEntry.id) - }, -) - -export const closePanelAtom = atom(null, (get, set, id: string) => { - const stack = get(panelStackAtom) - const idx = stack.findIndex((p) => p.id === id) - if (idx === -1) return - const remaining = [...stack.slice(0, idx), ...stack.slice(idx + 1)] - - set(panelStackAtom, normalizeProportions(remaining)) - - if (get(focusedPanelIdAtom) === id) { - const newIdx = Math.min(idx, remaining.length - 1) - set(focusedPanelIdAtom, remaining[newIdx]?.id ?? null) - } -}) - -export const reconcilePanelStackAtom = atom( - null, - ( - get, - set, - { - entries, - focusedIndex, - }: { - entries: { route: ViewRoute; proportion: number }[] - focusedIndex?: number - }, - ): boolean => { - if (entries.length === 0) return false - - const current = get(panelStackAtom) - const used = new Set() - - const requestedFocusIndex = Math.min(focusedIndex ?? 0, entries.length - 1) - const requestedFocusRoute = - entries[requestedFocusIndex]?.route ?? entries[0].route - - const newStack = entries.map((target, i) => { - const positional = current[i] - - if ( - positional && - positional.route === target.route && - !used.has(positional.id) - ) { - used.add(positional.id) - const updated = createEntry( - target.route, - target.proportion, - positional.id, - ) - return { ...updated, proportion: target.proportion } - } - - const any = current.find( - (c) => c.route === target.route && !used.has(c.id), - ) - if (any) { - used.add(any.id) - const updated = createEntry(target.route, target.proportion, any.id) - return { ...updated, proportion: target.proportion } - } - - if (positional && !used.has(positional.id)) { - used.add(positional.id) - const updated = createEntry( - target.route, - target.proportion, - positional.id, - ) - return { ...updated, proportion: target.proportion } - } - - return createEntry(target.route, target.proportion) - }) - - const normalized = normalizeProportions(newStack) - - if ( - normalized.length === current.length && - normalized.every( - (p, i) => - p.id === current[i].id && - p.route === current[i].route && - p.laneId === current[i].laneId && - p.panelType === current[i].panelType && - Math.abs(p.proportion - current[i].proportion) < 0.001, - ) - ) { - const targetFocusId = - normalized[Math.min(requestedFocusIndex, normalized.length - 1)]?.id ?? - normalized.find((p) => p.route === requestedFocusRoute)?.id ?? - null - if (get(focusedPanelIdAtom) !== targetFocusId) { - set(focusedPanelIdAtom, targetFocusId) - } - return false - } - - set(panelStackAtom, normalized) - - const focusId = - normalized[Math.min(requestedFocusIndex, normalized.length - 1)]?.id ?? - normalized.find((p) => p.route === requestedFocusRoute)?.id ?? - null - set(focusedPanelIdAtom, focusId) - - return true - }, -) - -export const resizePanelsAtom = atom( - null, - ( - get, - set, - { - leftIndex, - rightIndex, - leftProportion, - rightProportion, - }: { - leftIndex: number - rightIndex: number - leftProportion: number - rightProportion: number - }, - ) => { - const stack = get(panelStackAtom) - if (leftIndex < 0 || rightIndex >= stack.length) return - const newStack = stack.map((p, i) => { - if (i === leftIndex) return { ...p, proportion: leftProportion } - if (i === rightIndex) return { ...p, proportion: rightProportion } - return p - }) - set(panelStackAtom, newStack) - }, -) - -export const updateFocusedPanelRouteAtom = atom( - null, - (get, set, route: ViewRoute) => { - const stack = get(panelStackAtom) - - if (stack.length === 0) { - const newEntry = createEntry(route, 1) - set(panelStackAtom, [newEntry]) - set(focusedPanelIdAtom, newEntry.id) - return - } - - const focusedId = get(focusedPanelIdAtom) - const focused = stack.find((p) => p.id === focusedId) ?? stack[0] - - const updated = stack.map((p) => - p.id === focused.id - ? { - ...createEntry(route, p.proportion, p.id), - proportion: p.proportion, - } - : p, - ) - - set(panelStackAtom, updated) - set(focusedPanelIdAtom, focused.id) - }, -) - -export const focusNextPanelAtom = atom(null, (get, set) => { - const stack = get(panelStackAtom) - if (stack.length <= 1) return - const currentIdx = get(focusedPanelIndexAtom) - const nextIdx = (currentIdx + 1) % stack.length - set(focusedPanelIdAtom, stack[nextIdx].id) -}) - -export const focusPrevPanelAtom = atom(null, (get, set) => { - const stack = get(panelStackAtom) - if (stack.length <= 1) return - const currentIdx = get(focusedPanelIndexAtom) - const prevIdx = (currentIdx - 1 + stack.length) % stack.length - set(focusedPanelIdAtom, stack[prevIdx].id) -}) diff --git a/packages/desktop/apps/electron/src/renderer/atoms/sessions.ts b/packages/desktop/apps/electron/src/renderer/atoms/sessions.ts deleted file mode 100644 index e168cefc77f..00000000000 --- a/packages/desktop/apps/electron/src/renderer/atoms/sessions.ts +++ /dev/null @@ -1,1228 +0,0 @@ -/** - * Per-Session State Management with Jotai - * - * Uses atomFamily to create isolated atoms per session. - * Updates to one session don't trigger re-renders in other sessions. - * - * This solves the performance issue where streaming in Session A - * caused re-renders and focus loss in Session B. - */ - -import { atom } from 'jotai' -import type { Getter, Setter } from 'jotai/vanilla' -import { atomFamily } from 'jotai-family' -import type { Session, Message } from '../../shared/types' -import { hasSessionContentHint, mergeSessionRefreshResult } from '../lib/session-load' - -/** - * Session metadata for list display (lightweight, no messages) - * Used by SessionList to avoid re-rendering on message changes - */ -export interface SessionMeta { - id: string - name?: string - /** Preview of first user message (for title fallback) */ - preview?: string - workspaceId: string - /** Last time the session was opened or persisted. Used only as a fallback for legacy sessions without lastMessageAt. */ - lastUsedAt?: number - lastMessageAt?: number - isProcessing?: boolean - isFlagged?: boolean - lastReadMessageId?: string - workingDirectory?: string - enabledSourceSlugs?: string[] - /** Shared viewer URL (if shared via viewer) */ - sharedUrl?: string - /** Shared session ID in viewer (for revoke) */ - sharedId?: string - /** ID of the last final (non-intermediate) assistant message - for unread detection */ - lastFinalMessageId?: string - /** - * Explicit unread flag - single source of truth for NEW badge. - * Set to true when assistant message completes while user is NOT viewing. - * Set to false when user views the session (and not processing). - */ - hasUnread?: boolean - /** Labels for filtering (additive tags, many-per-session) */ - labels?: string[] - /** Permission mode — used by view expressions */ - permissionMode?: string - /** Session status for filtering */ - sessionStatus?: string - /** Role/type of the last message (for badge display without loading messages) */ - lastMessageRole?: 'user' | 'assistant' | 'plan' | 'tool' | 'error' - /** Whether an async operation is ongoing (sharing, updating share, revoking, title regeneration) */ - isAsyncOperationOngoing?: boolean - /** @deprecated Use isAsyncOperationOngoing instead */ - isRegeneratingTitle?: boolean - /** Model override for this session */ - model?: string - /** LLM connection slug for this session */ - llmConnection?: string - /** Token usage stats (from JSONL header, available without loading messages) */ - tokenUsage?: { - inputTokens: number - outputTokens: number - totalTokens: number - costUsd: number - contextTokens: number - } - /** When the session was created (ms timestamp) */ - createdAt?: number - /** Total number of messages in this session */ - messageCount?: number - /** When true, session is hidden from session list (e.g., mini edit sessions) */ - hidden?: boolean - /** Whether this session is archived */ - isArchived?: boolean - /** Timestamp when session was archived (for retention policy) */ - archivedAt?: number -} - -type SessionOrderFields = { - id: string - lastMessageAt?: number - lastUsedAt?: number - createdAt?: number -} - -type SessionFlagFields = { - isFlagged?: boolean -} - -export function getSessionOrderTime(session: SessionOrderFields): number { - return session.lastMessageAt ?? session.lastUsedAt ?? session.createdAt ?? 0 -} - -export function compareSessionsByActivityDesc(a: SessionOrderFields, b: SessionOrderFields): number { - const byTime = getSessionOrderTime(b) - getSessionOrderTime(a) - if (byTime !== 0) return byTime - - const byCreatedAt = (b.createdAt ?? 0) - (a.createdAt ?? 0) - if (byCreatedAt !== 0) return byCreatedAt - - return a.id.localeCompare(b.id) -} - -export function compareSessionsByFlaggedThenActivityDesc(a: T, b: T): number { - const byFlagged = Number(Boolean(b.isFlagged)) - Number(Boolean(a.isFlagged)) - if (byFlagged !== 0) return byFlagged - - return compareSessionsByActivityDesc(a, b) -} - -export function prioritizeFlaggedSessions(sessions: T[]): T[] { - const flagged: T[] = [] - const unflagged: T[] = [] - - for (const session of sessions) { - if (session.isFlagged) { - flagged.push(session) - } else { - unflagged.push(session) - } - } - - return [...flagged, ...unflagged] -} - -export function mergeStableSessionMetaList(previous: SessionMeta[] | undefined, incoming: SessionMeta[]): SessionMeta[] { - if (!previous || previous.length === 0) { - return [...incoming].sort(compareSessionsByActivityDesc) - } - - const incomingById = new Map(incoming.map(session => [session.id, session])) - const previouslySeen = previous - .map(session => incomingById.get(session.id)) - .filter((session): session is SessionMeta => !!session) - const previousIds = new Set(previous.map(session => session.id)) - const added = incoming.filter(session => !previousIds.has(session.id)) - - return [...previouslySeen, ...added].sort(compareSessionsByActivityDesc) -} - -function areSessionMetasShallowEqual(a: SessionMeta, b: SessionMeta): boolean { - if (a === b) return true - - const aKeys = Object.keys(a) as Array - const bKeys = Object.keys(b) as Array - if (aKeys.length !== bKeys.length) return false - - for (const key of aKeys) { - if (a[key] !== b[key]) return false - } - return true -} - -export function areSessionMetaListsEquivalent(a: SessionMeta[] | undefined, b: SessionMeta[]): boolean { - if (!a || a.length !== b.length) return false - - for (let index = 0; index < a.length; index += 1) { - if (!areSessionMetasShallowEqual(a[index]!, b[index]!)) return false - } - - return true -} - -export function sessionFromMeta(meta: SessionMeta, workspaceName = ''): Session { - return { - ...meta, - workspaceName, - lastMessageAt: getSessionOrderTime(meta), - messages: [], - isProcessing: meta.isProcessing ?? false, - } as Session -} - -/** - * Find the last final (non-intermediate) assistant or plan message ID - */ -function findLastFinalMessageId(messages: Message[]): string | undefined { - for (let i = messages.length - 1; i >= 0; i--) { - const msg = messages[i] - // Include plan messages as final responses (they're AI-generated content) - if ((msg.role === 'assistant' || msg.role === 'plan') && !msg.isIntermediate) { - return msg.id - } - } - return undefined -} - -function resolveMessageCount(sessionMessageCount: number | undefined, messages: Message[]): number | undefined { - if (sessionMessageCount != null) return sessionMessageCount - return messages.length > 0 ? messages.length : undefined -} - -function shouldPreserveExistingMessages(currentSession: Session | null | undefined, nextSession: Session): currentSession is Session { - return !!currentSession - && currentSession.workspaceId === nextSession.workspaceId - && (currentSession.messages?.length ?? 0) > 0 - && (nextSession.messages?.length ?? 0) === 0 - && nextSession.messageCount !== 0 -} - -function mergeSessionWithoutDroppingMessages(currentSession: Session | null | undefined, nextSession: Session): Session { - const nextSessionWithTitle = currentSession?.name && !nextSession.name - ? { ...nextSession, name: currentSession.name } - : nextSession - - if (!shouldPreserveExistingMessages(currentSession, nextSessionWithTitle)) { - return nextSessionWithTitle - } - - return { - ...nextSessionWithTitle, - messages: currentSession.messages, - } -} - -/** - * Extract metadata from a full session object - */ -export function extractSessionMeta(session: Session): SessionMeta { - const messages = session.messages || [] - - // Destructure fields that don't exist on SessionMeta or need overrides - const { - messages: _msgs, sessionFolderPath: _sf, supportsBranching: _sb, - workspaceName: _wn, thinkingLevel: _tl, currentStatus: _cs, - isAsyncOperationOngoing, isRegeneratingTitle, - messageCount, lastFinalMessageId: sessionLastFinal, - ...sessionFields - } = session - - return { - ...sessionFields, - lastFinalMessageId: sessionLastFinal ?? findLastFinalMessageId(messages), - messageCount: resolveMessageCount(messageCount, messages), - isAsyncOperationOngoing: isAsyncOperationOngoing ?? isRegeneratingTitle, - isRegeneratingTitle, - } as SessionMeta -} - -/** - * Atom family for individual session state - * Each session gets its own atom - updates are isolated - */ -export const sessionAtomFamily = atomFamily( - (_sessionId: string) => atom(null), - (a, b) => a === b -) - -/** - * Atom for session metadata map (for list display) - * Only contains lightweight data needed for SessionList - */ -export const sessionMetaMapAtom = atom>(new Map()) - -/** - * Workspace-scoped session state. This is the source used by the project tree - * and workspace switcher so each workspace keeps its own metadata and order in - * memory instead of relying on the currently selected workspace's flat list. - */ -export interface WorkspaceSessionState { - sessionMetaMap: Map - sessionOrder: string[] - loadedAt?: number - isRefreshing?: boolean - error?: string -} - -export const workspaceSessionsAtom = atom>(new Map()) - -export function getWorkspaceSessionMetas( - workspaceSessions: Map, - workspaceId: string | null | undefined, -): SessionMeta[] { - if (!workspaceId) return [] - - const state = workspaceSessions.get(workspaceId) - if (!state) return [] - - const ordered = state.sessionOrder - .map(sessionId => state.sessionMetaMap.get(sessionId)) - .filter((session): session is SessionMeta => Boolean(session)) - - const orderedIds = new Set(ordered.map(session => session.id)) - const missingOrderedSessions = Array.from(state.sessionMetaMap.values()) - .filter(session => !orderedIds.has(session.id)) - .sort(compareSessionsByActivityDesc) - - return prioritizeFlaggedSessions([...ordered, ...missingOrderedSessions]) -} - -function workspaceStateFromMetas( - previousState: WorkspaceSessionState | undefined, - incomingMetas: SessionMeta[], -): WorkspaceSessionState { - const previousMetas = previousState ? getWorkspaceSessionMetas(new Map([['workspace', previousState]]), 'workspace') : undefined - const mergedMetas = mergeStableSessionMetaList(previousMetas, incomingMetas) - - if (previousState && areSessionMetaListsEquivalent(previousMetas, mergedMetas)) { - return previousState - } - - return { - ...previousState, - sessionMetaMap: new Map(mergedMetas.map(session => [session.id, session])), - sessionOrder: mergedMetas.map(session => session.id), - loadedAt: Date.now(), - error: undefined, - } -} - -function upsertWorkspaceSessionMeta( - previousState: WorkspaceSessionState | undefined, - meta: SessionMeta, - position: 'preserve' | 'front' = 'preserve', -): WorkspaceSessionState { - const previousMap = previousState?.sessionMetaMap ?? new Map() - const previousMeta = previousMap.get(meta.id) - const nextMap = new Map(previousMap) - nextMap.set(meta.id, meta) - - const previousOrder = previousState?.sessionOrder ?? [] - let nextOrder: string[] - if (position === 'front') { - nextOrder = [meta.id, ...previousOrder.filter(id => id !== meta.id)] - } else if (previousOrder.includes(meta.id)) { - nextOrder = previousOrder - } else { - nextOrder = [...previousOrder, meta.id] - } - - const nextState: WorkspaceSessionState = { - ...previousState, - sessionMetaMap: nextMap, - sessionOrder: nextOrder, - } - - if ( - previousState && - previousMeta && - areSessionMetasShallowEqual(previousMeta, meta) && - previousOrder.length === nextOrder.length && - previousOrder.every((id, index) => id === nextOrder[index]) - ) { - return previousState - } - - return nextState -} - -function setWorkspaceState( - get: Getter, - set: Setter, - workspaceId: string | null | undefined, - incomingMetas: SessionMeta[], -): void { - if (!workspaceId) return - - const current = get(workspaceSessionsAtom) - const previousState = current.get(workspaceId) - const nextState = workspaceStateFromMetas(previousState, incomingMetas) - if (nextState === previousState) return - - const next = new Map(current) - next.set(workspaceId, nextState) - set(workspaceSessionsAtom, next) -} - -function upsertMetaInWorkspaceState( - get: Getter, - set: Setter, - meta: SessionMeta, - position: 'preserve' | 'front' = 'preserve', -): void { - if (!meta.workspaceId) return - - const current = get(workspaceSessionsAtom) - const previousState = current.get(meta.workspaceId) - const nextState = upsertWorkspaceSessionMeta(previousState, meta, position) - if (nextState === previousState) return - - const next = new Map(current) - next.set(meta.workspaceId, nextState) - set(workspaceSessionsAtom, next) -} - -function removeMetaFromWorkspaceStates(get: Getter, set: Setter, sessionId: string): void { - const current = get(workspaceSessionsAtom) - let changed = false - const next = new Map(current) - - for (const [workspaceId, state] of current) { - if (!state.sessionMetaMap.has(sessionId)) continue - - const sessionMetaMap = new Map(state.sessionMetaMap) - sessionMetaMap.delete(sessionId) - next.set(workspaceId, { - ...state, - sessionMetaMap, - sessionOrder: state.sessionOrder.filter(id => id !== sessionId), - }) - changed = true - } - - if (changed) { - set(workspaceSessionsAtom, next) - } -} - -function removeWorkspaceScopedMetas( - get: Getter, - set: Setter, - workspaceIdSet: Set, - keepSessionIds: Set, -): void { - const current = get(workspaceSessionsAtom) - let changed = false - const next = new Map(current) - - for (const [workspaceId, state] of current) { - let stateChanged = false - const sessionMetaMap = new Map(state.sessionMetaMap) - for (const [sessionId, meta] of state.sessionMetaMap) { - if (!workspaceIdSet.has(meta.workspaceId) || keepSessionIds.has(sessionId)) continue - sessionMetaMap.delete(sessionId) - stateChanged = true - } - - if (stateChanged) { - next.set(workspaceId, { - ...state, - sessionMetaMap, - sessionOrder: state.sessionOrder.filter(id => sessionMetaMap.has(id)), - }) - changed = true - } - } - - if (changed) { - set(workspaceSessionsAtom, next) - } -} - -type WorkspaceSessionMetaCacheUpdate = - | Map - | ((previous: Map) => Map) - -/** - * Backward-compatible workspace metadata view. New code should prefer - * workspaceSessionsAtom, but existing callers can keep reading/writing the - * Map shape while it is backed by the richer state. - */ -export const workspaceSessionMetaCacheAtom = atom( - (get) => { - const workspaceSessions = get(workspaceSessionsAtom) - const cache = new Map() - for (const [workspaceId] of workspaceSessions) { - cache.set(workspaceId, getWorkspaceSessionMetas(workspaceSessions, workspaceId)) - } - return cache - }, - (get, set, update: WorkspaceSessionMetaCacheUpdate) => { - const previousCache = get(workspaceSessionMetaCacheAtom) - const nextCache = typeof update === 'function' ? update(previousCache) : update - if (nextCache === previousCache) return - - const current = get(workspaceSessionsAtom) - const next = new Map(current) - let changed = false - - for (const [workspaceId, sessions] of nextCache) { - const previousState = current.get(workspaceId) - const nextState = workspaceStateFromMetas(previousState, sessions) - if (nextState !== previousState) { - next.set(workspaceId, nextState) - changed = true - } - } - - for (const workspaceId of current.keys()) { - if (!nextCache.has(workspaceId)) { - next.delete(workspaceId) - changed = true - } - } - - if (changed) { - set(workspaceSessionsAtom, next) - } - }, -) - -/** - * Derived atom: ordered list of session IDs (for list ordering) - */ -export const sessionIdsAtom = atom([]) - -/** - * Track which sessions have had their messages loaded (for lazy loading) - * Sessions are loaded with empty messages initially, messages are fetched on-demand - */ -export const loadedSessionsAtom = atom>(new Set()) - -/** - * Promise cache for deduplicating concurrent session load requests. - * Prevents race condition where multiple calls (e.g., from React re-renders) - * start loading before the first completes and marks the session as loaded. - * Module-level map since it tracks in-flight promises, not React state. - */ -const sessionLoadingPromises = new Map>() - -function markSessionMessagesLoaded(get: Getter, set: Setter, sessionId: string): void { - const newLoadedSessions = new Set(get(loadedSessionsAtom)) - newLoadedSessions.add(sessionId) - set(loadedSessionsAtom, newLoadedSessions) -} - -/** - * Currently active session ID - the session displayed in the main content area - * This replaces the tab-based session selection - */ -export const activeSessionIdAtom = atom(null) - -// NOTE: sessionsAtom REMOVED to fix memory leak -// The sessions array with messages was being retained by Jotai's internal state. -// Instead, we now use: -// - sessionMetaMapAtom for listing (lightweight metadata, no messages) -// - sessionAtomFamily(id) for individual session data -// - initializeSessionsAtom for bulk initialization -// - addSessionAtom, removeSessionAtom for individual operations - -/** - * Action atom: update a single session - * Only triggers re-render in components subscribed to this specific session - */ -export const updateSessionAtom = atom( - null, - (get, set, sessionId: string, updater: (prev: Session | null) => Session | null) => { - const sessionAtom = sessionAtomFamily(sessionId) - const currentSession = get(sessionAtom) - const newSession = updater(currentSession) - const existingMeta = get(sessionMetaMapAtom).get(sessionId) - const existingTitle = currentSession?.name ?? existingMeta?.name - const nextSession = newSession && !newSession.name && existingTitle - ? { ...newSession, name: existingTitle } - : newSession - set(sessionAtom, nextSession) - - // Also update metadata if session exists - if (nextSession) { - const metaMap = get(sessionMetaMapAtom) - const newMetaMap = new Map(metaMap) - const meta = extractSessionMeta(nextSession) - newMetaMap.set(sessionId, meta) - set(sessionMetaMapAtom, newMetaMap) - upsertMetaInWorkspaceState(get, set, meta) - } - } -) - -/** - * Action atom: update only session metadata (for list display updates) - * Doesn't affect the full session atom - */ -export const updateSessionMetaAtom = atom( - null, - (get, set, sessionId: string, updates: Partial) => { - const metaMap = get(sessionMetaMapAtom) - const existing = metaMap.get(sessionId) - if (existing) { - const nextMeta = { ...existing, ...updates } - const newMetaMap = new Map(metaMap) - newMetaMap.set(sessionId, nextMeta) - set(sessionMetaMapAtom, newMetaMap) - upsertMetaInWorkspaceState(get, set, nextMeta) - } - } -) - -/** - * Action atom: append message to session (for streaming) - * Optimized to only update the specific session - * Note: Does NOT update lastMessageAt - caller must handle timestamp updates - * to avoid session list jumping on intermediate/tool messages - */ -export const appendMessageAtom = atom( - null, - (get, set, sessionId: string, message: Message) => { - const sessionAtom = sessionAtomFamily(sessionId) - const session = get(sessionAtom) - if (session) { - set(sessionAtom, { - ...session, - messages: [...session.messages, message], - // Don't update lastMessageAt here - only user messages and final responses should update it - }) - } - } -) - -/** - * Action atom: update streaming content for a session - * For text_delta events - appends to the last streaming message - */ -export const updateStreamingContentAtom = atom( - null, - (get, set, sessionId: string, content: string, turnId?: string) => { - const sessionAtom = sessionAtomFamily(sessionId) - const session = get(sessionAtom) - if (!session) return - - const messages = [...session.messages] - const lastMsg = messages[messages.length - 1] - - // Append to existing streaming message - if (lastMsg?.role === 'assistant' && lastMsg.isStreaming && - (!turnId || lastMsg.turnId === turnId)) { - messages[messages.length - 1] = { - ...lastMsg, - content: lastMsg.content + content, - } - set(sessionAtom, { ...session, messages }) - } - } -) - -/** - * Action atom: initialize sessions from loaded data - */ -export const initializeSessionsAtom = atom( - null, - (get, set, sessions: Session[]) => { - const previousLoadedSessions = get(loadedSessionsAtom) - - // Clean up stale atom family entries from previous workspace. - // Without this, switching workspaces leaves orphaned atoms in memory - // and components subscribed to old session IDs see stale/empty data. - const oldIds = get(sessionIdsAtom) - const newIdSet = new Set(sessions.map(s => s.id)) - for (const oldId of oldIds) { - if (!newIdSet.has(oldId)) { - sessionAtomFamily.remove(oldId) - backgroundTasksAtomFamily.remove(oldId) - } - } - - const nextLoadedSessions = new Set() - - // Set individual session atoms. getSessions() returns metadata-only - // payloads, so preserve any already-loaded messages for sessions that are - // still present in the same workspace. - for (const session of sessions) { - const currentSession = get(sessionAtomFamily(session.id)) - const nextSession = mergeSessionWithoutDroppingMessages(currentSession, session) - set(sessionAtomFamily(session.id), nextSession) - - const hasMessages = (nextSession.messages?.length ?? 0) > 0 - const incomingHadMessages = (session.messages?.length ?? 0) > 0 - if (incomingHadMessages || (previousLoadedSessions.has(session.id) && hasMessages)) { - nextLoadedSessions.add(session.id) - } - } - set(loadedSessionsAtom, nextLoadedSessions) - - // Build metadata map - const metaMap = new Map() - for (const session of sessions) { - metaMap.set(session.id, extractSessionMeta(session)) - } - set(sessionMetaMapAtom, metaMap) - - const workspaceStates = new Map() - const metasByWorkspace = new Map() - for (const meta of metaMap.values()) { - if (!meta.workspaceId) continue - const workspaceMetas = metasByWorkspace.get(meta.workspaceId) ?? [] - workspaceMetas.push(meta) - metasByWorkspace.set(meta.workspaceId, workspaceMetas) - } - for (const [workspaceId, metas] of metasByWorkspace) { - workspaceStates.set(workspaceId, workspaceStateFromMetas(undefined, metas)) - } - set(workspaceSessionsAtom, workspaceStates) - - // Set ordered IDs (sorted by lastMessageAt desc) - const ids = sessions - .sort(compareSessionsByActivityDesc) - .map(s => s.id) - set(sessionIdsAtom, ids) - - // NOTE: Do NOT mark metadata-only sessions as loaded here. - // Sessions from getSessions() have empty messages: [] to save memory. - // Already-loaded sessions keep their loaded flag only when their existing - // messages were preserved above. - // This reduces initial memory usage from ~500MB to ~50MB for 300+ sessions. - } -) - -/** - * Action atom: initialize or refresh one workspace without discarding cached - * sessions/messages for other workspaces. - */ -export const initializeWorkspaceSessionsAtom = atom( - null, - ( - get, - set, - payload: { workspaceIds: string[]; sessions: Session[] } - ) => { - const { workspaceIds, sessions } = payload - const workspaceIdSet = new Set(workspaceIds.filter(Boolean)) - const nextIdSet = new Set(sessions.map(s => s.id)) - - const nextLoadedSessions = new Set(get(loadedSessionsAtom)) - const metaMap = get(sessionMetaMapAtom) - const nextMetaMap = new Map(metaMap) - const nextWorkspaceMetas: SessionMeta[] = [] - - for (const [sessionId, meta] of metaMap) { - if (!workspaceIdSet.has(meta.workspaceId) || nextIdSet.has(sessionId)) continue - - set(sessionAtomFamily(sessionId), null) - sessionAtomFamily.remove(sessionId) - backgroundTasksAtomFamily.remove(sessionId) - nextMetaMap.delete(sessionId) - nextLoadedSessions.delete(sessionId) - } - - for (const session of sessions) { - const currentSession = get(sessionAtomFamily(session.id)) - const nextSession = mergeSessionWithoutDroppingMessages(currentSession, session) - const nextMeta = extractSessionMeta(nextSession) - set(sessionAtomFamily(session.id), nextSession) - nextMetaMap.set(session.id, nextMeta) - nextWorkspaceMetas.push(nextMeta) - - const hasMessages = (nextSession.messages?.length ?? 0) > 0 - const incomingHadMessages = (session.messages?.length ?? 0) > 0 - if (incomingHadMessages || (nextLoadedSessions.has(session.id) && hasMessages)) { - nextLoadedSessions.add(session.id) - } else if (currentSession && currentSession.workspaceId !== session.workspaceId) { - nextLoadedSessions.delete(session.id) - } - } - - set(loadedSessionsAtom, nextLoadedSessions) - set(sessionMetaMapAtom, nextMetaMap) - removeWorkspaceScopedMetas(get, set, workspaceIdSet, nextIdSet) - setWorkspaceState(get, set, workspaceIds.find(Boolean), nextWorkspaceMetas) - - const ids = Array.from(nextMetaMap.values()) - .sort(compareSessionsByActivityDesc) - .map(s => s.id) - set(sessionIdsAtom, ids) - } -) - -/** - * Action atom: refresh session metadata after a stale reconnect. - * - * Unlike initializeSessionsAtom (which resets everything for workspace switches), - * this preserves messages for already-loaded sessions and only marks overwritten - * metadata-only sessions as unloaded for lazy re-fetching. - * - * All cross-atom mutations happen inside a single write transaction so that - * React subscribers see one consistent update instead of intermediate states. - */ -export const refreshSessionsMetadataAtom = atom( - null, - ( - get, - set, - payload: { sessions: Session[]; loadedSessionIds: Set; workspaceIds?: string[] } - ): Map => { - const { sessions, loadedSessionIds } = payload - const workspaceIdSet = payload.workspaceIds - ? new Set(payload.workspaceIds.filter(Boolean)) - : null - - // Remove stale sessions that no longer exist on the server - const currentIds = get(sessionIdsAtom) - const latestIds = new Set(sessions.map(s => s.id)) - for (const staleId of currentIds) { - if (!latestIds.has(staleId)) { - if (workspaceIdSet) { - const meta = get(sessionMetaMapAtom).get(staleId) - if (!meta || !workspaceIdSet.has(meta.workspaceId)) continue - } - set(removeSessionAtom, staleId) - } - } - - // Update each session atom, preserving messages for metadata refreshes. - // The loadedSessionsAtom flag can lag behind when the backend briefly - // returns empty messages during lazy-load recovery, so the atom's existing - // non-empty messages are the stronger signal here. - const unloadedIds: string[] = [] - for (const session of sessions) { - const currentSession = get(sessionAtomFamily(session.id)) - const nextSession = mergeSessionWithoutDroppingMessages(currentSession, session) - const hasMessages = (nextSession.messages?.length ?? 0) > 0 - - set(sessionAtomFamily(session.id), nextSession) - - // Track sessions that lost their messages so lazy-loading re-fetches them - if (!hasMessages && loadedSessionIds.has(session.id)) { - unloadedIds.push(session.id) - } - } - - // Remove overwritten sessions from loadedSessionsAtom - if (unloadedIds.length > 0) { - const nextLoaded = new Set(get(loadedSessionsAtom)) - for (const id of unloadedIds) nextLoaded.delete(id) - set(loadedSessionsAtom, nextLoaded) - } - - // Build and set metadata map - const nextMetaMap = workspaceIdSet - ? new Map(get(sessionMetaMapAtom)) - : new Map() - if (workspaceIdSet) { - for (const [sessionId, meta] of nextMetaMap) { - if (workspaceIdSet.has(meta.workspaceId) && !latestIds.has(sessionId)) { - nextMetaMap.delete(sessionId) - } - } - } - for (const session of sessions) { - nextMetaMap.set(session.id, extractSessionMeta(session)) - } - set(sessionMetaMapAtom, nextMetaMap) - - const refreshedMetas = sessions.map(session => nextMetaMap.get(session.id)).filter((meta): meta is SessionMeta => Boolean(meta)) - if (workspaceIdSet) { - removeWorkspaceScopedMetas(get, set, workspaceIdSet, latestIds) - setWorkspaceState(get, set, payload.workspaceIds?.find(Boolean), refreshedMetas) - } else { - const workspaceStates = new Map() - const metasByWorkspace = new Map() - for (const meta of nextMetaMap.values()) { - if (!meta.workspaceId) continue - const workspaceMetas = metasByWorkspace.get(meta.workspaceId) ?? [] - workspaceMetas.push(meta) - metasByWorkspace.set(meta.workspaceId, workspaceMetas) - } - for (const [workspaceId, metas] of metasByWorkspace) { - workspaceStates.set(workspaceId, workspaceStateFromMetas(undefined, metas)) - } - set(workspaceSessionsAtom, workspaceStates) - } - - // Set ordered IDs - const nextIds = Array.from(nextMetaMap.values()) - .sort(compareSessionsByActivityDesc) - .map(s => s.id) - set(sessionIdsAtom, nextIds) - - return nextMetaMap - } -) - -/** - * Action atom: add a new session - */ -export const addSessionAtom = atom( - null, - (get, set, session: Session) => { - // Set session atom - set(sessionAtomFamily(session.id), session) - - // Add to metadata map - const metaMap = get(sessionMetaMapAtom) - const newMetaMap = new Map(metaMap) - const meta = extractSessionMeta(session) - newMetaMap.set(session.id, meta) - set(sessionMetaMapAtom, newMetaMap) - upsertMetaInWorkspaceState(get, set, meta, 'front') - - // Add to beginning of IDs list - const ids = get(sessionIdsAtom) - set(sessionIdsAtom, [session.id, ...ids]) - - // Mark as loaded (new sessions are complete - no lazy loading needed) - const loadedSessions = get(loadedSessionsAtom) - const newLoadedSessions = new Set(loadedSessions) - newLoadedSessions.add(session.id) - set(loadedSessionsAtom, newLoadedSessions) - } -) - -/** - * Action atom: remove a session - */ -export const removeSessionAtom = atom( - null, - (get, set, sessionId: string) => { - // Clear session atom value first - set(sessionAtomFamily(sessionId), null) - // Remove atom from family cache to allow GC of the atom and its stored value - sessionAtomFamily.remove(sessionId) - - // Remove from metadata map - const metaMap = get(sessionMetaMapAtom) - const newMetaMap = new Map(metaMap) - newMetaMap.delete(sessionId) - set(sessionMetaMapAtom, newMetaMap) - removeMetaFromWorkspaceStates(get, set, sessionId) - - // Remove from IDs list - const ids = get(sessionIdsAtom) - set(sessionIdsAtom, ids.filter(id => id !== sessionId)) - - // Remove from loaded sessions tracking - const loadedSessions = get(loadedSessionsAtom) - const newLoadedSessions = new Set(loadedSessions) - newLoadedSessions.delete(sessionId) - set(loadedSessionsAtom, newLoadedSessions) - - // Clean up additional atom families to prevent memory leaks - // These store per-session UI state that should be garbage collected - backgroundTasksAtomFamily.remove(sessionId) - } -) - -/** - * Action atom: sync React state to per-session atoms - * - * This is the key to the hybrid approach: - * - React state (sessions array) remains the source of truth - * - This atom syncs changes to per-session atoms automatically - * - Components using useSession(id) get isolated updates - * - Jotai's referential equality prevents unnecessary re-renders - * - * IMPORTANT: During streaming, the atom is the source of truth. - * Streaming events (text_delta, tool_start, tool_result) update atoms directly - * and bypass React state for performance. We must NOT overwrite atoms for - * sessions that are processing, or we lose streaming data (tool calls, text). - * Once a "handoff" event (complete, error, etc.) occurs, React state catches up - * and sync works normally again. - */ -export const syncSessionsToAtomsAtom = atom( - null, - (get, set, sessions: Session[]) => { - const loadedSessions = get(loadedSessionsAtom) - - // Update each session atom - for (const session of sessions) { - const sessionAtom = sessionAtomFamily(session.id) - const atomSession = get(sessionAtom) - - // CRITICAL: If the atom's session is processing, it has streaming updates - // that React state doesn't know about yet. Don't overwrite - atom is - // source of truth during streaming. The handoff event will reconcile. - if (atomSession?.isProcessing) { - continue - } - - // CRITICAL: If session messages were lazy-loaded, atom has full messages - // but React state may have empty array. Only skip if React would lose messages. - // Allow sync when React has MORE messages (e.g., user just sent a message). - if (loadedSessions.has(session.id) && atomSession) { - const atomMessageCount = atomSession.messages?.length ?? 0 - const reactMessageCount = session.messages?.length ?? 0 - // Skip sync only if React has fewer messages (would lose data) - if (reactMessageCount < atomMessageCount) { - continue - } - } - - // Only update if the session object is different (referential check) - // This prevents unnecessary re-renders when the session hasn't changed - if (atomSession !== session) { - set(sessionAtom, session) - } - } - - // Update metadata map for list display - // Note: We still update metadata from React state, which is fine because - // metadata doesn't include messages - the streaming content we're protecting - const metaMap = new Map() - for (const session of sessions) { - const meta = extractSessionMeta(session) - // Preserve isProcessing from atom if atom is processing - // React state may have stale isProcessing: false during streaming - const atomSession = get(sessionAtomFamily(session.id)) - if (atomSession?.isProcessing) { - meta.isProcessing = true - } - metaMap.set(session.id, meta) - } - set(sessionMetaMapAtom, metaMap) - - const workspaceStates = new Map(get(workspaceSessionsAtom)) - let workspaceStatesChanged = false - const metasByWorkspace = new Map() - for (const meta of metaMap.values()) { - if (!meta.workspaceId) continue - const workspaceMetas = metasByWorkspace.get(meta.workspaceId) ?? [] - workspaceMetas.push(meta) - metasByWorkspace.set(meta.workspaceId, workspaceMetas) - } - for (const [workspaceId, metas] of metasByWorkspace) { - const previousState = workspaceStates.get(workspaceId) - const nextState = workspaceStateFromMetas(previousState, metas) - if (nextState !== previousState) { - workspaceStates.set(workspaceId, nextState) - workspaceStatesChanged = true - } - } - if (workspaceStatesChanged) { - set(workspaceSessionsAtom, workspaceStates) - } - - // Update ordered IDs (preserve order from React state) - set(sessionIdsAtom, sessions.map(s => s.id)) - } -) - -// loadedSessionsAtom moved up before sessionsAtom (needed for self-syncing) - -/** - * Action atom: Load session messages if not already loaded - * Returns the loaded session or current session if already loaded. - * Uses promise deduplication to prevent redundant IPC calls from concurrent requests. - * - * IMPORTANT: This only merges messages into the existing session atom. - * UI state fields (hasUnread, isFlagged, sessionStatus, etc.) are preserved from - * the in-memory atom, NOT overwritten with potentially stale disk data. - * This prevents a race condition where optimistic updates (e.g., clearing the - * NEW badge on session view) get clobbered by async message loading that reads - * older state from disk. - */ -async function loadSessionMessages( - get: Getter, - set: Setter, - sessionId: string, - options?: { force?: boolean }, -): Promise { - const force = options?.force ?? false - - if (force) { - const nextLoadedSessions = new Set(get(loadedSessionsAtom)) - nextLoadedSessions.delete(sessionId) - set(loadedSessionsAtom, nextLoadedSessions) - - // Clear any stale in-flight request so the caller gets a fresh fetch. - sessionLoadingPromises.delete(sessionId) - } else { - const loadedSessions = get(loadedSessionsAtom) - - if (loadedSessions.has(sessionId)) { - const existingSession = get(sessionAtomFamily(sessionId)) - const existingMeta = get(sessionMetaMapAtom).get(sessionId) - const visibleMessageCount = existingSession?.messages?.length ?? 0 - const expectedMessageCount = existingMeta?.messageCount ?? existingSession?.messageCount - const contentHint = hasSessionContentHint(existingMeta ?? existingSession) - const shouldRefetchEmptyLoadedSession = expectedMessageCount !== 0 && contentHint - - // Already loaded, return current session. If the loaded flag says "ready" - // but the atom is still empty for a session that looks non-empty, keep - // loading instead of flashing the empty-chat composer. - if (visibleMessageCount > 0 || !shouldRefetchEmptyLoadedSession) { - return existingSession - } - - const nextLoadedSessions = new Set(loadedSessions) - nextLoadedSessions.delete(sessionId) - set(loadedSessionsAtom, nextLoadedSessions) - } - } - - // Check if already loading - return existing promise to deduplicate concurrent calls - const existingPromise = sessionLoadingPromises.get(sessionId) - if (existingPromise) { - return existingPromise - } - - // Create the loading promise with all the fetch and update logic - const loadPromise = (async (): Promise => { - // Fetch messages from main process - const loadedSession = await window.electronAPI.getSessionMessages(sessionId) - if (!loadedSession) { - const existingSession = get(sessionAtomFamily(sessionId)) - const expectedMessageCount = get(sessionMetaMapAtom).get(sessionId)?.messageCount - ?? existingSession?.messageCount - - if (expectedMessageCount === 0) { - markSessionMessagesLoaded(get, set, sessionId) - return existingSession - } - - throw new Error(`Messages for session ${sessionId} are unavailable`) - } - - const existingMeta = get(sessionMetaMapAtom).get(sessionId) - if ( - (loadedSession.messages?.length ?? 0) === 0 - && hasSessionContentHint(existingMeta) - && loadedSession.messageCount !== 0 - ) { - throw new Error(`Messages for session ${sessionId} are still loading`) - } - - // Merge messages and disk-only fields into existing session, preserving in-memory UI state. - // The renderer's atom is authoritative for UI fields (hasUnread, isFlagged, etc.) - // because optimistic updates may have changed them since the disk write. - // tokenUsage and sessionFolderPath are only returned by getSession() (not getSessions()), - // so they must be explicitly merged here to be available after app restart. - const existingSession = get(sessionAtomFamily(sessionId)) - const existingTitle = existingSession?.name ?? existingMeta?.name - const candidateSession = existingSession - ? { - ...existingSession, - messages: loadedSession.messages, - availableCommands: loadedSession.availableCommands ?? existingSession.availableCommands, - availableSkills: loadedSession.availableSkills ?? existingSession.availableSkills, - availableSkillDetails: loadedSession.availableSkillDetails ?? existingSession.availableSkillDetails, - tokenUsage: loadedSession.tokenUsage ?? existingSession.tokenUsage, - sessionFolderPath: loadedSession.sessionFolderPath ?? existingSession.sessionFolderPath, - name: loadedSession.name ?? existingTitle, - } - : loadedSession.name || !existingTitle - ? loadedSession - : { ...loadedSession, name: existingTitle } - const { - session: mergedSession, - preservedExistingMessages, - } = mergeSessionRefreshResult(existingSession, candidateSession) - set(sessionAtomFamily(sessionId), mergedSession) - - // Update only lastFinalMessageId in metadata (now computable from loaded messages). - // Don't replace the full meta entry — other fields are maintained through - // optimistic updates and IPC events, and may be ahead of disk state. - const lastFinalMessageId = findLastFinalMessageId(loadedSession.messages) - if (lastFinalMessageId) { - const metaMap = get(sessionMetaMapAtom) - const existingMeta = metaMap.get(sessionId) - if (existingMeta && existingMeta.lastFinalMessageId !== lastFinalMessageId) { - const nextMeta = { ...existingMeta, lastFinalMessageId } - const newMetaMap = new Map(metaMap) - newMetaMap.set(sessionId, nextMeta) - set(sessionMetaMapAtom, newMetaMap) - upsertMetaInWorkspaceState(get, set, nextMeta) - } - } - - // Mark as loaded only when we received a fresh full payload. If we had to - // preserve existing messages because the backend returned an empty or short - // processing snapshot, keep the session reloadable. - if (!preservedExistingMessages) { - markSessionMessagesLoaded(get, set, sessionId) - } - - return mergedSession - })() - - // Cache the promise before awaiting - sessionLoadingPromises.set(sessionId, loadPromise) - - try { - return await loadPromise - } finally { - // Always clean up the cache, whether success or failure - sessionLoadingPromises.delete(sessionId) - } -} - -export const ensureSessionMessagesLoadedAtom = atom( - null, - async (get, set, sessionId: string): Promise => { - return loadSessionMessages(get, set, sessionId) - } -) - -/** - * Force-refresh session messages even if the session is currently marked as loaded. - * Used by reconnect recovery when a session atom is stuck in an empty-but-loaded state. - */ -export const forceSessionMessagesReloadAtom = atom( - null, - async (get, set, sessionId: string): Promise => { - return loadSessionMessages(get, set, sessionId, { force: true }) - } -) - -/** - * Background task for ActiveTasksBar display - */ -export interface BackgroundTask { - /** Task or shell ID */ - id: string - /** Task type */ - type: 'agent' | 'shell' - /** Tool use ID for correlation with messages */ - toolUseId: string - /** When the task started */ - startTime: number - /** Elapsed seconds (from progress events) */ - elapsedSeconds: number - /** Task intent/description */ - intent?: string -} - -/** - * Atom family for tracking active background tasks per session - * Updated on task_backgrounded, shell_backgrounded, task_progress events - * Cleared when tasks complete or are killed - */ -export const backgroundTasksAtomFamily = atomFamily( - (_sessionId: string) => atom([]), - (a, b) => a === b -) - -/** - * Window's current workspace ID — shared between Root (ThemeProvider) and App. - * Written by App on workspace switch, read by Root to keep the theme in sync. - */ -export const windowWorkspaceIdAtom = atom(null) - -/** - * State for "Send to Workspace" dialog. - * Set session IDs to open; clear to close. - */ -export const sendToWorkspaceAtom = atom([]) diff --git a/packages/desktop/apps/electron/src/renderer/atoms/skills.ts b/packages/desktop/apps/electron/src/renderer/atoms/skills.ts deleted file mode 100644 index f2e1b548022..00000000000 --- a/packages/desktop/apps/electron/src/renderer/atoms/skills.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Skills Atom - * - * Simple atom for storing workspace skills. - * Used by NavigationContext for auto-selection when navigating to skills view. - */ - -import { atom } from 'jotai' -import type { LoadedSkill } from '../../shared/types' - -/** - * Atom to store the current workspace's skills. - * AppShell populates this when skills are loaded. - * NavigationContext reads from it for auto-selection. - */ -export const skillsAtom = atom([]) diff --git a/packages/desktop/apps/electron/src/renderer/atoms/sources.ts b/packages/desktop/apps/electron/src/renderer/atoms/sources.ts deleted file mode 100644 index b481a8c8165..00000000000 --- a/packages/desktop/apps/electron/src/renderer/atoms/sources.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Sources Atom - * - * Simple atom for storing workspace sources. - * Used by NavigationContext for auto-selection when navigating to sources view. - */ - -import { atom } from 'jotai' -import type { LoadedSource } from '../../shared/types' - -/** - * Atom to store the current workspace's sources. - * AppShell populates this when sources are loaded. - * NavigationContext reads from it for auto-selection. - */ -export const sourcesAtom = atom([]) diff --git a/packages/desktop/apps/electron/src/renderer/browser-empty-state.html b/packages/desktop/apps/electron/src/renderer/browser-empty-state.html deleted file mode 100644 index e65823c0933..00000000000 --- a/packages/desktop/apps/electron/src/renderer/browser-empty-state.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - New Tab - - - - - - - -
- - - diff --git a/packages/desktop/apps/electron/src/renderer/browser-empty-state.tsx b/packages/desktop/apps/electron/src/renderer/browser-empty-state.tsx deleted file mode 100644 index 422be56904f..00000000000 --- a/packages/desktop/apps/electron/src/renderer/browser-empty-state.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React, { useCallback } from 'react' -import { useTranslation } from 'react-i18next' -import ReactDOM from 'react-dom/client' -import { BrowserEmptyStateCard } from '@craft-agent/ui' -import { routes } from '../shared/routes' -import { EMPTY_STATE_PROMPT_SAMPLES } from './components/browser/empty-state-prompts' -import './index.css' - -function BrowserEmptyStateApp() { - const { t } = useTranslation() - const handlePromptSelect = useCallback(async (fullPrompt: string) => { - const route = routes.action.newSession({ input: fullPrompt, send: true }) - const token = String(Date.now()) - - try { - if (window.electronAPI?.browserPane?.emptyStateLaunch) { - await window.electronAPI.browserPane.emptyStateLaunch({ route, token }) - return - } - } catch { - // Fallback to hash-signaling below if IPC route fails for any reason. - } - - const launchParams = new URLSearchParams({ route, ts: token }) - window.location.hash = `launch=${launchParams.toString()}` - }, []) - - return ( -
-
- handlePromptSelect(sample.full)} - /> -
-
- ) -} - -ReactDOM.createRoot(document.getElementById('root')!).render( - - - , -) diff --git a/packages/desktop/apps/electron/src/renderer/browser-toolbar.html b/packages/desktop/apps/electron/src/renderer/browser-toolbar.html deleted file mode 100644 index 2f1274b8570..00000000000 --- a/packages/desktop/apps/electron/src/renderer/browser-toolbar.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - Browser Toolbar - - - - - - - -
- - - diff --git a/packages/desktop/apps/electron/src/renderer/browser-toolbar.tsx b/packages/desktop/apps/electron/src/renderer/browser-toolbar.tsx deleted file mode 100644 index 2385eef79f2..00000000000 --- a/packages/desktop/apps/electron/src/renderer/browser-toolbar.tsx +++ /dev/null @@ -1,265 +0,0 @@ -/** - * Browser Toolbar — React entry point - * - * Renders the shared BrowserControls component inside a chromeless - * BrowserWindow. Communicates with the main process via a dedicated - * preload script (browser-toolbar preload). - */ - -import React, { useState, useEffect, useCallback, useRef } from 'react' -import ReactDOM from 'react-dom/client' -import { EyeOff, Maximize2, Minimize2, X, XCircle } from 'lucide-react' -import { BrowserControls } from '@craft-agent/ui' -import { HeaderIconButton } from '@/components/ui/HeaderIconButton' -import { - DropdownMenu, - DropdownMenuTrigger, - StyledDropdownMenuContent, - StyledDropdownMenuItem, -} from '@/components/ui/styled-dropdown' -import './index.css' - -/* ------------------------------------------------------------------ */ -/* Types */ -/* ------------------------------------------------------------------ */ - -interface ToolbarState { - url: string - title: string - isLoading: boolean - canGoBack: boolean - canGoForward: boolean - themeColor?: string | null - presentation?: 'window' | 'docked' - dockExpanded?: boolean -} - -declare global { - interface Window { - browserToolbar: { - instanceId: string - navigate: (url: string) => Promise - goBack: () => Promise - goForward: () => Promise - reload: () => Promise - stop: () => Promise - setMenuGeometry: (open: boolean, height?: number) => Promise - toggleDockExpanded: () => Promise - hideWindow: () => Promise - closeWindowEntirely: () => Promise - onStateUpdate: (callback: (state: ToolbarState) => void) => () => void - onThemeColor: (callback: (color: string | null) => void) => () => void - onForceCloseMenu: (callback: (payload: { reason?: string }) => void) => () => void - } - } -} - -/* ------------------------------------------------------------------ */ -/* App */ -/* ------------------------------------------------------------------ */ - -function BrowserToolbarApp() { - const [state, setState] = useState({ - url: 'about:blank', - title: 'New Tab', - isLoading: false, - canGoBack: false, - canGoForward: false, - }) - const [themeColor, setThemeColor] = useState(null) - const [windowMenuOpen, setWindowMenuOpen] = useState(false) - const menuContentRef = useRef(null) - - const api = window.browserToolbar - - useEffect(() => { - if (!api) return - return api.onStateUpdate((s) => { - setState(s) - // Sync theme color from full state push (initial load / reconnection) - if ('themeColor' in s) { - setThemeColor((s as ToolbarState).themeColor ?? null) - } - }) - }, [api]) - - useEffect(() => { - if (!api) return - return api.onThemeColor(setThemeColor) - }, [api]) - - useEffect(() => { - if (!api) return - return api.onForceCloseMenu(() => { - setWindowMenuOpen(false) - }) - }, [api]) - - useEffect(() => { - if (!api) return - - if (!windowMenuOpen) { - void api.setMenuGeometry(false, 0) - return - } - - // Prime expansion immediately to avoid a constrained first measurement. - void api.setMenuGeometry(true, 120) - - const sendGeometry = () => { - const height = Math.ceil(menuContentRef.current?.getBoundingClientRect().height ?? 0) - void api.setMenuGeometry(true, height) - } - - let frame = requestAnimationFrame(sendGeometry) - const observer = new ResizeObserver(() => { - sendGeometry() - }) - - if (menuContentRef.current) { - observer.observe(menuContentRef.current) - } - - return () => { - cancelAnimationFrame(frame) - observer.disconnect() - void api.setMenuGeometry(false, 0) - } - }, [api, windowMenuOpen]) - - const handleNavigate = useCallback((url: string) => { - void api?.navigate(url) - }, [api]) - - const handleGoBack = useCallback(() => { - void api?.goBack() - }, [api]) - - const handleGoForward = useCallback(() => { - void api?.goForward() - }, [api]) - - const handleReload = useCallback(() => { - void api?.reload() - }, [api]) - - const handleStop = useCallback(() => { - void api?.stop() - }, [api]) - - const handleToggleDockExpanded = useCallback(() => { - void api?.toggleDockExpanded() - }, [api]) - - const handleHideWindow = useCallback(() => { - setWindowMenuOpen(false) - void api?.hideWindow() - }, [api]) - - const handleCloseWindowEntirely = useCallback(() => { - setWindowMenuOpen(false) - void api?.closeWindowEntirely() - }, [api]) - - return ( - <> - {/* - Full-window outside-tap catcher while menu is open. - Critical for draggable titlebar windows (Windows) where outside-click - dismissal can be unreliable if events fall into app-region: drag zones. - */} - {windowMenuOpen && ( -
{ - event.preventDefault() - setWindowMenuOpen(false) - }} - /> - )} - - - {state.presentation === 'docked' && ( - <> - - ) : ( - - )} - aria-label={state.dockExpanded ? 'Restore panel width' : 'Expand panel'} - tooltip={state.dockExpanded ? 'Restore panel width' : 'Expand panel'} - onClick={handleToggleDockExpanded} - className={themeColor ? '' : 'bg-background shadow-minimal hover:bg-foreground/5'} - style={themeColor ? { color: 'var(--tb-fg)' } : undefined} - /> - } - aria-label="Close side panel" - tooltip="Close side panel" - onClick={handleHideWindow} - className={themeColor ? '' : 'bg-background shadow-minimal hover:bg-foreground/5'} - style={themeColor ? { color: 'var(--tb-fg)' } : undefined} - /> - - )} - {state.presentation !== 'docked' && ( - - - } - aria-label="Browser window options" - className={themeColor ? '' : 'bg-background shadow-minimal hover:bg-foreground/5'} - style={themeColor ? { color: 'var(--tb-fg)' } : undefined} - /> - - - - - - Hide Window - - - - Close Window Entirely - - - - )} -
- )} - themeColor={themeColor} - urlBarClassName="max-w-[600px]" - className="titlebar-drag-region bg-background" - /> - - ) -} - -/* ------------------------------------------------------------------ */ -/* Mount */ -/* ------------------------------------------------------------------ */ - -ReactDOM.createRoot(document.getElementById('root')!).render( - - - , -) diff --git a/packages/desktop/apps/electron/src/renderer/components/AboutDialog.tsx b/packages/desktop/apps/electron/src/renderer/components/AboutDialog.tsx deleted file mode 100644 index 109e3b55247..00000000000 --- a/packages/desktop/apps/electron/src/renderer/components/AboutDialog.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog" -import { CraftAgentsSymbol } from "./icons/CraftAgentsSymbol" -import { BRAND, APP_VERSION } from "@craft-agent/shared/branding" -import { ExternalLink } from "lucide-react" - -interface AboutDialogProps { - open: boolean - onOpenChange: (open: boolean) => void -} - -export function AboutDialog({ open, onOpenChange }: AboutDialogProps) { - const version = APP_VERSION - - return ( - - - - {BRAND.appName} - - - {/* Logo + Name */} -
- -

{BRAND.appName}

- {version && ( -

- Version {version} -

- )} -
- - {/* Credits */} - {BRAND.creditsEntries.length > 0 && ( -
-

- {BRAND.creditsShort} -

-
- {BRAND.creditsEntries.map((entry) => ( - -
-
{entry.name}
-
- {entry.role} -
-
- -
- ))} -
-
- )} - - {/* Copyright */} -
-

- {BRAND.copyright} -

-
-
-
- ) -} diff --git a/packages/desktop/apps/electron/src/renderer/components/AppMenu.tsx b/packages/desktop/apps/electron/src/renderer/components/AppMenu.tsx deleted file mode 100644 index fed6d9f600f..00000000000 --- a/packages/desktop/apps/electron/src/renderer/components/AppMenu.tsx +++ /dev/null @@ -1,372 +0,0 @@ -import { useEffect, useState } from "react" -import { useTranslation } from "react-i18next" -import { isMac } from "@/lib/platform" -import { useActionLabel } from "@/actions" -import { - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuShortcut, - DropdownMenuSub, - StyledDropdownMenuContent, - StyledDropdownMenuItem, - StyledDropdownMenuSeparator, - StyledDropdownMenuSubTrigger, - StyledDropdownMenuSubContent, -} from "@/components/ui/styled-dropdown" -import * as Icons from "lucide-react" -import { Tooltip, TooltipTrigger, TooltipContent } from "@craft-agent/ui" -import { CraftAgentsSymbol } from "./icons/CraftAgentsSymbol" -import { SquarePenRounded } from "./icons/SquarePenRounded" -import { TopBarButton } from "./ui/TopBarButton" -import { - EDIT_MENU, - VIEW_MENU, - WINDOW_MENU, - SETTINGS_ITEMS, - getShortcutDisplay, -} from "../../shared/menu-schema" -import type { MenuItem, MenuSection, SettingsMenuItem } from "../../shared/menu-schema" -import { SETTINGS_ICONS } from "./icons/SettingsIcons" -import { BRAND } from '@craft-agent/shared/branding' - -// Map of action handlers for menu items that need custom behavior -type MenuActionHandlers = { - toggleFocusMode?: () => void - toggleSidebar?: () => void -} - -// Map of IPC handlers for role-based menu items -const roleHandlers: Record void> = { - undo: () => window.electronAPI.menuUndo(), - redo: () => window.electronAPI.menuRedo(), - cut: () => window.electronAPI.menuCut(), - copy: () => window.electronAPI.menuCopy(), - paste: () => window.electronAPI.menuPaste(), - selectAll: () => window.electronAPI.menuSelectAll(), - zoomIn: () => window.electronAPI.menuZoomIn(), - zoomOut: () => window.electronAPI.menuZoomOut(), - resetZoom: () => window.electronAPI.menuZoomReset(), - minimize: () => window.electronAPI.menuMinimize(), - zoom: () => window.electronAPI.menuMaximize(), -} - -/** - * Get the Lucide icon component by name - */ -function getIcon(name: string): React.ComponentType<{ className?: string }> | null { - const IconComponent = Icons[name as keyof typeof Icons] as React.ComponentType<{ className?: string }> | undefined - return IconComponent ?? null -} - -/** - * Renders a single menu item from the schema - */ -function renderMenuItem( - item: MenuItem, - index: number, - actionHandlers: MenuActionHandlers, - t: (key: string) => string, -): React.ReactNode { - if (item.type === 'separator') { - return - } - - const Icon = getIcon(item.icon) - const shortcut = getShortcutDisplay(item, isMac) - - if (item.type === 'role') { - const handler = roleHandlers[item.role] - // Gracefully handle missing role handlers with console warning - const safeHandler = handler ?? (() => { - console.warn(`[AppMenu] No handler registered for role: ${item.role}`) - }) - return ( - - {Icon && } - {t(item.labelKey)} - {shortcut && {shortcut}} - - ) - } - - if (item.type === 'action') { - // Map action IDs to handlers - const handler = item.id === 'toggleFocusMode' - ? actionHandlers.toggleFocusMode - : item.id === 'toggleSidebar' - ? actionHandlers.toggleSidebar - : undefined - return ( - - {Icon && } - {t(item.labelKey)} - {shortcut && {shortcut}} - - ) - } - - return null -} - -/** - * Renders a menu section as a submenu - */ -function renderMenuSection( - section: MenuSection, - actionHandlers: MenuActionHandlers, - t: (key: string) => string, -): React.ReactNode { - const Icon = getIcon(section.icon) - return ( - - - {Icon && } - {t(section.labelKey)} - - - {section.items.map((item, index) => renderMenuItem(item, index, actionHandlers, t))} - - - ) -} - -interface AppMenuProps { - onNewChat: () => void - onNewWindow?: () => void - onOpenSettings: () => void - /** Navigate to a specific settings subpage */ - onOpenSettingsSubpage: (subpage: SettingsMenuItem['id']) => void - onOpenKeyboardShortcuts: () => void - onOpenStoredUserPreferences: () => void - onShowAbout?: () => void - onBack?: () => void - onForward?: () => void - canGoBack?: boolean - canGoForward?: boolean - onToggleSidebar?: () => void - onToggleFocusMode?: () => void -} - -/** - * AppMenu - Main application dropdown menu and top bar navigation - * - * Contains the Craft logo dropdown with all menu functionality: - * - File actions (New Chat, New Window) - * - Edit submenu (Undo, Redo, Cut, Copy, Paste, Select All) - * - View submenu (Zoom In/Out, Reset) - * - Window submenu (Minimize, Maximize) - * - Settings submenu (Settings, Stored User Preferences) - * - Help submenu (Documentation, Keyboard Shortcuts) - * - Debug submenu (dev only) - * - Quit - * - * On Windows/Linux, this is the only menu (native menu is hidden). - * On macOS, this mirrors the native menu for consistency. - */ -export function AppMenu({ - onNewChat, - onNewWindow, - onOpenSettings, - onOpenSettingsSubpage, - onOpenKeyboardShortcuts, - onOpenStoredUserPreferences, - onShowAbout, - onBack, - onForward, - canGoBack = true, - canGoForward = true, - onToggleSidebar, - onToggleFocusMode, -}: AppMenuProps) { - const { t } = useTranslation() - const [isDebugMode, setIsDebugMode] = useState(false) - const hasHelpMenuLinks = BRAND.helpMenuLinks.length > 0 - - // Get hotkey labels from centralized action registry - const newChatHotkey = useActionLabel('app.newChat').hotkey - const newWindowHotkey = useActionLabel('app.newWindow').hotkey - const settingsHotkey = useActionLabel('app.settings').hotkey - const keyboardShortcutsHotkey = useActionLabel('app.keyboardShortcuts').hotkey - const quitHotkey = useActionLabel('app.quit').hotkey - const goBackHotkey = useActionLabel('nav.goBackAlt').hotkey - const goForwardHotkey = useActionLabel('nav.goForwardAlt').hotkey - - useEffect(() => { - window.electronAPI.isDebugMode().then(setIsDebugMode) - }, []) - - // Action handlers for schema-driven menu items - const actionHandlers: MenuActionHandlers = { - toggleFocusMode: onToggleFocusMode, - toggleSidebar: onToggleSidebar, - } - - return ( -
- {/* Craft Logo Menu - interactive island */} -
- - - - - - - - {/* File actions at root level */} - - - New Chat - {newChatHotkey && {newChatHotkey}} - - {onNewWindow && ( - - - New Window - {newWindowHotkey && {newWindowHotkey}} - - )} - - - - {/* Edit, View, Window submenus from shared schema */} - {renderMenuSection(EDIT_MENU, actionHandlers, t)} - {renderMenuSection(VIEW_MENU, actionHandlers, t)} - {renderMenuSection(WINDOW_MENU, actionHandlers, t)} - - - - {/* Settings submenu - items from shared schema */} - - - - Settings - - - {/* Main settings entry with keyboard shortcut */} - - - Settings... - {settingsHotkey && {settingsHotkey}} - - - {/* All settings subpages from shared schema */} - {SETTINGS_ITEMS.map((item) => { - const Icon = SETTINGS_ICONS[item.id] - return ( - onOpenSettingsSubpage(item.id)} - > - - {t(item.labelKey)} - - ) - })} - - - - {/* Help submenu */} - - - - Help - - - {BRAND.helpMenuLinks.map((link) => { - const Icon = getIcon(link.icon) ?? Icons.ExternalLink - return ( - window.electronAPI.openUrl(link.url)} - > - - {t(link.labelKey)} - - - ) - })} - {hasHelpMenuLinks && } - - - Keyboard Shortcuts - {keyboardShortcutsHotkey && {keyboardShortcutsHotkey}} - - {onShowAbout && ( - <> - - - - {t("menu.aboutCraftAgents")} - - - )} - - - - {/* Debug submenu (dev only) */} - {isDebugMode && ( - <> - - - - Debug - - - window.electronAPI.menuToggleDevTools()}> - - Toggle DevTools - {isMac ? '⌥⌘I' : 'Ctrl+Shift+I'} - - - - - )} - - - - {/* Quit */} - window.electronAPI.menuQuit()}> - - {t("menu.quitCraftAgents")} - {quitHotkey && {quitHotkey}} - - - -
- - {/* Spacer - pointer-events-none inherited from parent, drag passes through */} -
- - {/* Nav Buttons - interactive island */} -
- {/* Back Navigation */} - - - - - - - Back {goBackHotkey} - - - {/* Forward Navigation */} - - - - - - - Forward {goForwardHotkey} - -
-
- ) -} diff --git a/packages/desktop/apps/electron/src/renderer/components/KeyboardShortcuts.tsx b/packages/desktop/apps/electron/src/renderer/components/KeyboardShortcuts.tsx deleted file mode 100644 index 1943b604a82..00000000000 --- a/packages/desktop/apps/electron/src/renderer/components/KeyboardShortcuts.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { actionsByCategory, useActionLabel, type ActionId } from '@/actions' - -export function KeyboardShortcuts() { - return ( -
- {Object.entries(actionsByCategory).map(([category, actions]) => ( -
-

- {category} -

-
- {actions.map(action => ( - - ))} -
-
- ))} -
- ) -} - -function ShortcutRow({ actionId }: { actionId: ActionId }) { - const { label, description, hotkey } = useActionLabel(actionId) - - return ( -
-
-
{label}
- {description && ( -
{description}
- )} -
- {hotkey && ( - {hotkey} - )} -
- ) -} diff --git a/packages/desktop/apps/electron/src/renderer/components/KeyboardShortcutsDialog.tsx b/packages/desktop/apps/electron/src/renderer/components/KeyboardShortcutsDialog.tsx deleted file mode 100644 index 4d8c3111b40..00000000000 --- a/packages/desktop/apps/electron/src/renderer/components/KeyboardShortcutsDialog.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { useTranslation } from "react-i18next" -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog" -import { useRegisterModal } from "@/context/ModalContext" -import { isMac } from "@/lib/platform" -import { actionsByCategory, useActionLabel, type ActionId } from "@/actions" - -interface KeyboardShortcutsDialogProps { - open: boolean - onOpenChange: (open: boolean) => void -} - -interface ShortcutItem { - keys: string[] - description: string -} - -interface ShortcutSection { - title: string - shortcuts: ShortcutItem[] -} - -// Component-specific shortcuts that aren't in the centralized registry -// These are context-sensitive behaviors, not global actions -function useComponentSpecificSections(): ShortcutSection[] { - const { t } = useTranslation() - return [ - { - title: t('shortcuts.listNavigation'), - shortcuts: [ - { keys: ['↑', '↓'], description: t('shortcuts.navigateItems') }, - { keys: ['Home'], description: t('shortcuts.goToFirst') }, - { keys: ['End'], description: t('shortcuts.goToLast') }, - ], - }, - { - title: t('shortcuts.sessionList'), - shortcuts: [ - { keys: ['Enter'], description: t('shortcuts.focusChatInput') }, - { keys: ['Delete'], description: t('shortcuts.deleteSession') }, - { keys: ['R'], description: t('shortcuts.renameSession') }, - { keys: ['Right-click'], description: t('shortcuts.openContextMenu') }, - { keys: [isMac ? '⌥' : 'Alt', 'Click'], description: t('shortcuts.addFilterExcluded') }, - ], - }, - { - title: t('shortcuts.agentTree'), - shortcuts: [ - { keys: ['←'], description: t('shortcuts.collapseFolder') }, - { keys: ['→'], description: t('shortcuts.expandFolder') }, - ], - }, - { - title: t('shortcuts.chatInput'), - shortcuts: [ - { keys: ['Enter'], description: t('shortcuts.sendMessage') }, - { keys: ['Shift', 'Enter'], description: t('shortcuts.newLine') }, - { keys: ['Esc'], description: t('shortcuts.closeDialogBlur') }, - ], - }, - ] -} - -function Kbd({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ) -} - -/** - * Renders a shortcut row for an action from the registry - */ -function ActionShortcutRow({ actionId }: { actionId: ActionId }) { - const { label, hotkey } = useActionLabel(actionId) - - if (!hotkey) return null - - // Split hotkey into individual keys for display - // Mac: symbols are concatenated (⌘⇧N) - need smart splitting - // Windows: separated by + (Ctrl+Shift+N) - split on + - const keys = isMac - ? hotkey.match(/[⌘⇧⌥←→]|Tab|Esc|./g) || [] - : hotkey.split('+') - - return ( -
- {label} -
- {keys.map((key, keyIndex) => ( - {key} - ))} -
-
- ) -} - -/** - * Renders a section of shortcuts from the registry - */ -function RegistrySection({ category, actionIds }: { category: string; actionIds: ActionId[] }) { - return ( -
-

- {category} -

-
- {actionIds.map(actionId => ( - - ))} -
-
- ) -} - -/** - * Renders a section of static shortcuts (component-specific) - */ -function StaticSection({ section }: { section: ShortcutSection }) { - return ( -
-

- {section.title} -

-
- {section.shortcuts.map((shortcut, index) => ( -
- {shortcut.description} -
- {shortcut.keys.map((key, keyIndex) => ( - {key} - ))} -
-
- ))} -
-
- ) -} - -export function KeyboardShortcutsDialog({ open, onOpenChange }: KeyboardShortcutsDialogProps) { - const { t } = useTranslation() - const componentSpecificSections = useComponentSpecificSections() - - // Register with modal context so X button / Cmd+W closes this dialog first - useRegisterModal(open, () => onOpenChange(false)) - - return ( - - - - {t("shortcuts.title")} - -
- {/* Registry-driven sections */} - {Object.entries(actionsByCategory).map(([category, actions]) => ( - a.id as ActionId)} - /> - ))} - - {/* Component-specific sections */} - {componentSpecificSections.map((section) => ( - - ))} -
-
-
- ) -} diff --git a/packages/desktop/apps/electron/src/renderer/components/ResetConfirmationDialog.tsx b/packages/desktop/apps/electron/src/renderer/components/ResetConfirmationDialog.tsx deleted file mode 100644 index 184c14700a6..00000000000 --- a/packages/desktop/apps/electron/src/renderer/components/ResetConfirmationDialog.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { useState, useMemo } from "react" -import { useTranslation, Trans } from "react-i18next" -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription, - DialogFooter, -} from "@/components/ui/dialog" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { AlertTriangle } from "lucide-react" -import { useRegisterModal } from "@/context/ModalContext" - -interface ResetConfirmationDialogProps { - open: boolean - onConfirm: () => void - onCancel: () => void -} - -/** - * ResetConfirmationDialog - Destructive action confirmation with math problem - * - * Shows a warning about data loss and requires the user to solve a random - * math problem to confirm the reset action. - */ -export function ResetConfirmationDialog({ - open, - onConfirm, - onCancel, -}: ResetConfirmationDialogProps) { - const { t } = useTranslation() - const [answer, setAnswer] = useState("") - - // Register with modal context so X button / Cmd+W closes this dialog first - useRegisterModal(open, onCancel) - - // Generate a random math problem when dialog opens - const problem = useMemo(() => { - const a = Math.floor(Math.random() * 50) + 10 - const b = Math.floor(Math.random() * 50) + 10 - return { a, b, sum: a + b } - }, [open]) // Regenerate when dialog opens - - const isCorrect = parseInt(answer) === problem.sum - - const handleConfirm = () => { - if (isCorrect) { - setAnswer("") - onConfirm() - } - } - - const handleCancel = () => { - setAnswer("") - onCancel() - } - - return ( - !isOpen && handleCancel()}> - - - - - {t("dialog.reset.title")} - - - }} /> - - - -
    -
  • {t("dialog.reset.workspaces")}
  • -
  • {t("dialog.reset.credentials")}
  • -
  • {t("dialog.reset.preferences")}
  • -
- -
- {t("dialog.reset.backupWarning")} -

- {t("dialog.reset.cannotUndo")} -

-
- -
- - setAnswer(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter" && isCorrect) { - handleConfirm() - } - }} - className="max-w-32" - /> -
- - - - - -
-
- ) -} diff --git a/packages/desktop/apps/electron/src/renderer/components/ServerDirectoryBrowser.tsx b/packages/desktop/apps/electron/src/renderer/components/ServerDirectoryBrowser.tsx deleted file mode 100644 index 09cc8505921..00000000000 --- a/packages/desktop/apps/electron/src/renderer/components/ServerDirectoryBrowser.tsx +++ /dev/null @@ -1,317 +0,0 @@ -import { useState, useEffect, useCallback, useRef } from 'react' -import { useTranslation } from 'react-i18next' -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogFooter, -} from '@/components/ui/dialog' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { useRegisterModal } from '@/context/ModalContext' -import type { DirectoryListingResult } from '../../shared/types' -import { FolderIcon, FolderSymlinkIcon, ChevronRightIcon } from 'lucide-react' -import { Spinner } from '@craft-agent/ui' - -/** - * Detect paths that are clearly from the wrong platform. - * The server directory browser runs against the server's filesystem, - * so Windows-style paths are invalid when the server is macOS/Linux and vice versa. - * We infer the server platform from the home directory path. - */ -function isWrongPlatformPath(path: string, serverHomePath: string | null): boolean { - if (!serverHomePath) return false - const serverIsUnix = serverHomePath.startsWith('/') - if (serverIsUnix) { - return /^[A-Za-z]:[/\\]/.test(path) || path.startsWith('\\\\') - } - // Server is Windows — reject Unix absolute paths - return path.startsWith('/') -} - -interface ServerDirectoryBrowserProps { - open: boolean - mode: 'browse' | 'manual' - onSelect: (path: string) => void - onCancel: () => void - initialPath?: string -} - -export function ServerDirectoryBrowser({ - open, - mode, - onSelect, - onCancel, - initialPath, -}: ServerDirectoryBrowserProps) { - useRegisterModal(open, onCancel) - const { t } = useTranslation() - - const [listing, setListing] = useState(null) - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - const [pathInput, setPathInput] = useState('') - const [selectedEntry, setSelectedEntry] = useState(null) - const [serverHomePath, setServerHomePath] = useState(null) - const inputRef = useRef(null) - - // Navigate to a directory (for browse mode) - const navigateTo = useCallback(async (dirPath: string) => { - setLoading(true) - setError(null) - setSelectedEntry(null) - try { - const result = await window.electronAPI.listServerDirectory(dirPath) - setListing(result) - setPathInput(result.currentPath) - } catch (err) { - const message = err instanceof Error ? err.message : 'Failed to list directory' - setError(message) - } finally { - setLoading(false) - } - }, []) - - // Load initial directory when opened - useEffect(() => { - if (!open) { - // Reset state when closed - setListing(null) - setError(null) - setSelectedEntry(null) - setPathInput('') - setServerHomePath(null) - return - } - - const init = async () => { - if (mode === 'browse') { - setLoading(true) - - // Resolve the start path with cascading fallback for backward compat: - // 1. initialPath (if provided and valid) - // 2. getServerHomeDir() — REMOTE_ELIGIBLE, returns server's home (new servers) - // 3. listServerDirectory('~') — server-side ~ resolution (medium-age servers) - // 4. listServerDirectory('/') — root directory (old servers) - const tryNavigate = async (path: string) => { - const result = await window.electronAPI.listServerDirectory(path) - setListing(result) - setPathInput(result.currentPath) - setServerHomePath(result.currentPath) - } - - try { - if (initialPath) { - await tryNavigate(initialPath) - } else { - // Try server home dir API first (REMOTE_ELIGIBLE — correct for remote workspaces) - try { - const serverHome = await window.electronAPI.getServerHomeDir() - await tryNavigate(serverHome) - } catch { - // Fallback: ~ resolution (server-side) - try { - await tryNavigate('~') - } catch { - // Final fallback: root directory - await tryNavigate('/') - } - } - } - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to list directory') - } finally { - setLoading(false) - } - } else { - // Manual mode — fetch home dir for platform detection - const homeDir = await window.electronAPI.getHomeDir() - setServerHomePath(homeDir) - } - } - void init() - }, [open, mode, initialPath, navigateTo]) - - // Handle path input submission (Enter key or navigate button) - const handlePathSubmit = useCallback(() => { - const trimmed = pathInput.trim() - if (!trimmed) return - - // Client-side rejection of wrong-platform paths (avoids round-trip) - if (isWrongPlatformPath(trimmed, serverHomePath)) { - setError('This looks like a path from a different OS. Enter a path that exists on the server.') - return - } - - if (mode === 'browse') { - void navigateTo(trimmed) - } else { - // Manual mode — just select the path - onSelect(trimmed) - } - }, [pathInput, mode, navigateTo, onSelect, serverHomePath]) - - // Handle selecting the current directory (or highlighted entry) - const handleSelect = useCallback(() => { - if (mode === 'manual') { - handlePathSubmit() - return - } - - if (selectedEntry) { - onSelect(selectedEntry) - } else if (listing) { - onSelect(listing.currentPath) - } else if (pathInput.trim()) { - onSelect(pathInput.trim()) - } - }, [mode, selectedEntry, listing, pathInput, onSelect, handlePathSubmit]) - - // Handle double-click on an entry to navigate into it - const handleEntryDoubleClick = useCallback((entryPath: string) => { - void navigateTo(entryPath) - }, [navigateTo]) - - // Handle single-click to select an entry - const handleEntryClick = useCallback((entryPath: string) => { - setSelectedEntry(prev => prev === entryPath ? null : entryPath) - }, []) - - // Browse mode content - const renderBrowseMode = () => ( - <> - {/* Path input */} -
- setPathInput(e.target.value)} - onKeyDown={e => { - if (e.key === 'Enter') handlePathSubmit() - }} - placeholder={t("common.enterPath")} - className="flex-1 font-mono text-xs" - /> - -
- - {/* Breadcrumbs */} - {listing && ( -
- {listing.breadcrumbs.map((crumb, i) => ( - - {i > 0 && } - - - ))} -
- )} - - {/* Directory listing */} -
-
- {loading && ( -
- - Loading... -
- )} - - {error && ( -
- {error} -
- )} - - {!loading && !error && listing?.truncated && ( -
- Showing the first {listing.entries.length} folders out of {listing.totalEntries}. Narrow the path if the folder you want is missing. -
- )} - - {!loading && !error && listing && listing.entries.length === 0 && ( -
- No subdirectories. Use the path input above to navigate. -
- )} - - {!loading && !error && listing && listing.entries.map(entry => ( - - ))} -
-
- - ) - - // Manual mode content - const renderManualMode = () => ( - <> -

- Enter the full path on the server: -

- setPathInput(e.target.value)} - onKeyDown={e => { - if (e.key === 'Enter') handleSelect() - }} - placeholder="/Users/username/projects/my-project" - className="font-mono text-xs" - autoFocus - /> - - ) - - return ( - { if (!isOpen) onCancel() }}> - - - {t("settings.server.selectDirectory")} - - -
- {mode === 'browse' ? renderBrowseMode() : renderManualMode()} -
- - - - - -
-
- ) -} diff --git a/packages/desktop/apps/electron/src/renderer/components/SplashScreen.tsx b/packages/desktop/apps/electron/src/renderer/components/SplashScreen.tsx deleted file mode 100644 index 0979461feba..00000000000 --- a/packages/desktop/apps/electron/src/renderer/components/SplashScreen.tsx +++ /dev/null @@ -1,83 +0,0 @@ -// eslint-disable-next-line -import { motion } from 'motion/react'; -import type { CSSProperties, PointerEvent as ReactPointerEvent } from 'react'; -import { CraftAgentsSymbol } from './icons/CraftAgentsSymbol'; - -interface SplashScreenProps { - isExiting: boolean; - onExitComplete?: () => void; -} - -const splashStyle: CSSProperties = { - zIndex: 'var(--z-splash)', -}; - -function ignoreWindowDragError(promise: Promise) { - void promise.catch(() => {}); -} - -function beginWindowDrag(event: ReactPointerEvent) { - if (event.button !== 0) return; - - event.currentTarget.setPointerCapture(event.pointerId); - ignoreWindowDragError( - window.electronAPI.beginWindowDrag(event.screenX, event.screenY), - ); -} - -function moveWindowDrag(event: ReactPointerEvent) { - if ((event.buttons & 1) === 0) return; - - ignoreWindowDragError( - window.electronAPI.moveWindowDrag(event.screenX, event.screenY), - ); -} - -function endWindowDrag(event: ReactPointerEvent) { - if (event.currentTarget.hasPointerCapture(event.pointerId)) { - event.currentTarget.releasePointerCapture(event.pointerId); - } - - ignoreWindowDragError(window.electronAPI.endWindowDrag()); -} - -/** - * SplashScreen - Shows Craft symbol during app initialization - * - * Displays centered symbol on app background, fades out when app is fully ready. - * On exit, the symbol scales up and fades out quickly while the background fades slower. - */ -export function SplashScreen({ isExiting, onExitComplete }: SplashScreenProps) { - return ( - { - if (isExiting && onExitComplete) { - onExitComplete(); - } - }} - > - - - - - ); -} diff --git a/packages/desktop/apps/electron/src/renderer/components/apisetup/ApiKeyInput.tsx b/packages/desktop/apps/electron/src/renderer/components/apisetup/ApiKeyInput.tsx deleted file mode 100644 index fa25795ee2b..00000000000 --- a/packages/desktop/apps/electron/src/renderer/components/apisetup/ApiKeyInput.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { SquareTerminal } from "lucide-react" -import { useTranslation } from "react-i18next" - -export type ApiKeyStatus = 'idle' | 'validating' | 'success' | 'error' -export type CustomEndpointApi = never - -export interface ApiKeySubmitData { - apiKey: string -} - -export interface ApiKeyInputProps { - status: ApiKeyStatus - errorMessage?: string - onSubmit: (data: ApiKeySubmitData) => void - formId?: string - disabled?: boolean - providerType?: 'qwen' - initialValues?: { - apiKey?: string - } -} - -export function ApiKeyInput({ - status, - errorMessage, - onSubmit, - formId = "api-key-form", - disabled, -}: ApiKeyInputProps) { - const { t } = useTranslation() - - return ( -
{ - event.preventDefault() - onSubmit({ apiKey: '' }) - }} - className="space-y-3" - > -
- -

{t("apiSetup.localAuthNotice")}

-
- {status === 'error' && errorMessage && ( -
- {errorMessage} -
- )} -
-
- ); - } - - if (!selectedProvider) { - return ( - - {showHeader && ( -
-

- {t('providerConnect.title')} -

-

- {t('providerConnect.description')} -

-
- )} - -
- {groups.map((group) => ( - - ))} -
- {activeGroup && ( -

- {activeGroup.description} -

- )} - - - {providersByGroup[selectedGroup].map((provider) => ( - - ))} - - - {onCancel && ( -
- -
- )} -
- ); - } - - const fixedBaseUrl = typeof selectedProvider.baseUrl === 'string'; - const baseUrlOptions = Array.isArray(selectedProvider.baseUrl) - ? selectedProvider.baseUrl - : []; - const showProtocol = selectedProvider.protocolOptions.length > 1; - const showBaseUrlInput = !fixedBaseUrl || baseUrlOptions.length > 0; - - return ( - -
- -
-

{selectedProvider.label}

-

- {selectedProvider.description} -

-
-
- -
- {showProtocol && ( -
- - -
- )} - - {showBaseUrlInput && ( -
- - {baseUrlOptions.length > 0 ? ( - - ) : ( - setBaseUrl(event.target.value)} - placeholder={ - selectedProvider.baseUrlPlaceholder || - 'https://api.example.com/v1' - } - disabled={submitting} - /> - )} -
- )} - -
- - setApiKey(event.target.value)} - placeholder={ - selectedProvider.apiKeyPlaceholder || - t('providerConnect.apiKeyPlaceholder') - } - disabled={submitting} - /> -
- -
- -