diff --git a/.env.example b/.env.example index 6293848c..4c7643a8 100644 --- a/.env.example +++ b/.env.example @@ -31,8 +31,3 @@ VITE_POSTHOG_HOST=https://us.i.posthog.com # API URL (optional - defaults to https://21st.dev) # Only change this if you're running the web app locally # MAIN_VITE_API_URL=http://localhost:3000 - -# Voice Input (optional - uses OpenAI Whisper API) -# Set this to enable voice-to-text for users without a paid subscription -# Get your API key at https://platform.openai.com/api-keys -# MAIN_VITE_OPENAI_API_KEY=sk-... diff --git a/.gitignore b/.gitignore index 54d8ce1a..b35dfec5 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,6 @@ electron.vite.config.*.mjs # Claude binary (downloaded at build time) resources/bin/ + +# Claude Code local config +.claude/ diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 06696994..00000000 --- a/AGENTS.md +++ /dev/null @@ -1,18 +0,0 @@ - -# OpenSpec Instructions - -These instructions are for AI assistants working in this project. - -Always open `@/openspec/AGENTS.md` when the request: -- Mentions planning or proposals (words like proposal, spec, change, plan) -- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work -- Sounds ambiguous and you need the authoritative spec before coding - -Use `@/openspec/AGENTS.md` to learn: -- How to create and apply change proposals -- Spec format and conventions -- Project structure and guidelines - -Keep this managed block so 'openspec update' can refresh the instructions. - - \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index d41a54ea..30b1de5d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,22 +1,3 @@ - -# OpenSpec Instructions - -These instructions are for AI assistants working in this project. - -Always open `@/openspec/AGENTS.md` when the request: -- Mentions planning or proposals (words like proposal, spec, change, plan) -- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work -- Sounds ambiguous and you need the authoritative spec before coding - -Use `@/openspec/AGENTS.md` to learn: -- How to create and apply change proposals -- Spec format and conventions -- Project structure and guidelines - -Keep this managed block so 'openspec update' can refresh the instructions. - - - # CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. diff --git a/README.md b/README.md index 0a25e820..1e884203 100644 --- a/README.md +++ b/README.md @@ -8,27 +8,6 @@ By [21st.dev](https://21st.dev) team > **Platforms:** macOS, Linux, and Windows. Windows support improved thanks to community contributions from [@jesus-mgtc](https://github.com/jesus-mgtc) and [@evgyur](https://github.com/evgyur). -## 1Code vs Claude Code - -| Feature | 1Code | Claude Code | -|---------|-------|-------------| -| **Visual UI** | ✅ Cursor-like desktop app | ✅ | -| **Git Worktree Isolation** | ✅ Each chat runs in isolated worktree | ✅ | -| **Background Execution** | ✅ Run multiple agents in parallel | ✅ | -| **Built-in Git Client** | ✅ Visual staging, commits, branches | ❌ CLI git commands only | -| **Integrated Terminal** | ✅ | ❌ | -| **Plan Mode** | ✅ | ✅ | -| **MCP Support** | ✅ | ✅ | -| **Memory (CLAUDE.md)** | ✅ | ✅ | -| **Skills & Slash Commands** | ✅ | ✅ | -| **Custom Subagents** | ✅ | ✅ | -| **Subscription & API Key Support** | ✅ | ✅ | -| **Custom Models & Providers (BYOK)** | ✅ | ✅ | -| **Voice Input** | ✅ Hold-to-talk dictation | ❌ | -| **Checkpointing** | 🚧 Beta | ✅ | -| **Tool Approve** | 📋 Backlog | ✅ | -| **Hooks** | ❌ | ✅ | - ## Features ### Run Claude agents the right way diff --git a/build/entitlements.mac.plist b/build/entitlements.mac.plist index 63e02c31..50e8b4e2 100644 --- a/build/entitlements.mac.plist +++ b/build/entitlements.mac.plist @@ -12,8 +12,6 @@ com.apple.security.network.server - com.apple.security.device.audio-input - diff --git a/bun.lock b/bun.lock index db583128..e89145a0 100644 --- a/bun.lock +++ b/bun.lock @@ -5,19 +5,17 @@ "name": "21st-desktop", "dependencies": { "@ai-sdk/react": "^3.0.14", - "@anthropic-ai/claude-agent-sdk": "0.2.25", + "@anthropic-ai/claude-agent-sdk": "^0.2.5", "@git-diff-view/react": "^0.0.35", "@git-diff-view/shiki": "^0.0.36", - "@modelcontextprotocol/sdk": "^1.25.3", - "@monaco-editor/react": "^4.7.0", - "@radix-ui/react-accordion": "^1.2.12", - "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-accordion": "^1.2.11", + "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-collapsible": "^1.1.12", "@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-hover-card": "^1.1.15", + "@radix-ui/react-hover-card": "^1.1.14", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-popover": "^1.1.15", @@ -41,7 +39,6 @@ "@xterm/addon-web-links": "^0.12.0", "@xterm/addon-webgl": "^0.19.0", "ai": "^6.0.14", - "async-mutex": "^0.5.0", "better-sqlite3": "^11.8.1", "chokidar": "^5.0.0", "class-variance-authority": "^0.7.1", @@ -52,10 +49,7 @@ "electron-updater": "^6.7.3", "gray-matter": "^4.0.3", "jotai": "^2.11.1", - "jsonc-parser": "^3.3.1", "lucide-react": "^0.468.0", - "mermaid": "^11.12.2", - "monaco-editor": "^0.55.1", "motion": "^11.15.0", "next-themes": "^0.4.4", "node-pty": "^1.1.0", @@ -66,9 +60,7 @@ "react-dom": "19.2.1", "react-hotkeys-hook": "^4.6.1", "react-icons": "^5.5.0", - "react-zoom-pan-pinch": "^3.7.0", - "remark-breaks": "^4.0.0", - "remark-gfm": "^4.0.1", + "react-syntax-highlighter": "^16.1.0", "shiki": "^1.24.4", "simple-git": "^3.28.0", "sonner": "^1.7.1", @@ -89,8 +81,8 @@ "@types/node": "^20.17.50", "@types/react": "^19.0.7", "@types/react-dom": "^19.0.3", + "@types/react-syntax-highlighter": "^15.5.13", "@vitejs/plugin-react": "^4.3.4", - "@welldone-software/why-did-you-render": "^10.0.1", "autoprefixer": "^10.4.20", "drizzle-kit": "^0.31.8", "electron": "33.4.5", @@ -107,41 +99,39 @@ "packages": { "7zip-bin": ["7zip-bin@5.2.0", "", {}, "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A=="], - "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.4", "@ai-sdk/provider-utils": "4.0.8", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-WOHGHclkcDN3VYJ3xBafa38uhmW+xRV8Hbe9LtEJKej3+qU3Hy1SUcptxsqfVACo1H1Nt5xRX7rEvoCHlwC8tw=="], + "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.9", "", { "dependencies": { "@ai-sdk/provider": "3.0.2", "@ai-sdk/provider-utils": "4.0.4", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EA5dZIukimwoJ9HIPuuREotAqaTItpdc/yImzVF0XGNg7B0YRJmYI8Uq3aCMr87vjr1YB1cWUfnrTt6OJ9eHiQ=="], - "@ai-sdk/provider": ["@ai-sdk/provider@3.0.4", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5KXyBOSEX+l67elrEa+wqo/LSsSTtrPj9Uoh3zMbe/ceQX4ucHI3b9nUEfNkGF3Ry1svv90widAt+aiKdIJasQ=="], + "@ai-sdk/provider": ["@ai-sdk/provider@3.0.2", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-HrEmNt/BH/hkQ7zpi2o6N3k1ZR1QTb7z85WYhYygiTxOQuaml4CMtHCWRbric5WPU+RNsYI7r1EpyVQMKO1pYw=="], - "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.8", "", { "dependencies": { "@ai-sdk/provider": "3.0.4", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ns9gN7MmpI8vTRandzgz+KK/zNMLzhrriiKECMt4euLtQFSBgNfydtagPOX4j4pS1/3KvHF6RivhT3gNQgBZsg=="], + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.4", "", { "dependencies": { "@ai-sdk/provider": "3.0.2", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-VxhX0B/dWGbpNHxrKCWUAJKXIXV015J4e7qYjdIU9lLWeptk0KMLGcqkB4wFxff5Njqur8dt8wRi1MN9lZtDqg=="], - "@ai-sdk/react": ["@ai-sdk/react@3.0.47", "", { "dependencies": { "@ai-sdk/provider-utils": "4.0.8", "ai": "6.0.45", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1" } }, "sha512-H+RKDWrJCybang8RxC4WkEzdgaXD/KrlmRKq6gfJ5fNv0pmcGSjIHi3PJpEPa5zOymblesDWxh8KM0u/ToMo7w=="], + "@ai-sdk/react": ["@ai-sdk/react@3.0.14", "", { "dependencies": { "@ai-sdk/provider-utils": "4.0.4", "ai": "6.0.14", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1" } }, "sha512-3Z4SCdR06kMh+AQ0yJePXhL3UnRTchHT6RIjA6R7yWF6ONkbKsy5hHkyg2KXnkO+/XCyrz+yOepLCW5W5k/Q6A=="], "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], - "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], - - "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.25", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.33.5", "@img/sharp-darwin-x64": "^0.33.5", "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", "@img/sharp-linuxmusl-arm64": "^0.33.5", "@img/sharp-linuxmusl-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-YIP3I40+XSkC3zE1Z8KRQY02VA7UfofFamF1cFrLe7FbtCnjpslyDl9coGBh2DAi9xj2yQcKZZf751jEWpB+dQ=="], + "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.9", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.33.5", "@img/sharp-darwin-x64": "^0.33.5", "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", "@img/sharp-linuxmusl-arm64": "^0.33.5", "@img/sharp-linuxmusl-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-b4JD6ZKCZeVDqpWBnb+zJISWi3HzlweNlV7Oy/uo5G2XAfUV2M5AJ/tomKZCvZsvmr1fYbmmfyde3GL2h0pksA=="], "@apm-js-collab/code-transformer": ["@apm-js-collab/code-transformer@0.8.2", "", {}, "sha512-YRjJjNq5KFSjDUoqu5pFUWrrsvGOxl6c3bu+uMFc9HNNptZ2rNU/TI2nLw4jnhQNtka972Ee2m3uqbvDQtPeCA=="], "@apm-js-collab/tracing-hooks": ["@apm-js-collab/tracing-hooks@0.3.1", "", { "dependencies": { "@apm-js-collab/code-transformer": "^0.8.0", "debug": "^4.4.1", "module-details-from-path": "^1.0.4" } }, "sha512-Vu1CbmPURlN5fTboVuKMoJjbO5qcq9fA5YXpskx3dXe/zTBvjODFoerw+69rVBlRLrJpwPqSDqEuJDEKIrTldw=="], - "@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], - "@babel/compat-data": ["@babel/compat-data@7.28.6", "", {}, "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg=="], + "@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="], - "@babel/core": ["@babel/core@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw=="], + "@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="], - "@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], + "@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], - "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="], "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], - "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], - "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="], "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], @@ -149,9 +139,9 @@ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], - "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], + "@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="], - "@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + "@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], "@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA=="], @@ -159,23 +149,13 @@ "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], - "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], - - "@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], - - "@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], - - "@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.1", "", {}, "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw=="], + "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], - "@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@11.0.3", "", { "dependencies": { "@chevrotain/gast": "11.0.3", "@chevrotain/types": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ=="], + "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], - "@chevrotain/gast": ["@chevrotain/gast@11.0.3", "", { "dependencies": { "@chevrotain/types": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q=="], + "@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], - "@chevrotain/regexp-to-ast": ["@chevrotain/regexp-to-ast@11.0.3", "", {}, "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA=="], - - "@chevrotain/types": ["@chevrotain/types@11.0.3", "", {}, "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ=="], - - "@chevrotain/utils": ["@chevrotain/utils@11.0.3", "", {}, "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ=="], + "@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], "@develar/schema-utils": ["@develar/schema-utils@2.6.5", "", { "dependencies": { "ajv": "^6.12.0", "ajv-keywords": "^3.4.1" } }, "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig=="], @@ -271,12 +251,6 @@ "@git-diff-view/shiki": ["@git-diff-view/shiki@0.0.36", "", { "dependencies": { "@types/hast": "^3.0.0", "shiki": "^3.9.2" } }, "sha512-t6LGKISEvE0q7u2AR1rq2RLgCcccRttuKE617D1MiHecO0Xd/xvsMhqxW4PgDMkMadhbzWnt6IhBbrdgqBKPwQ=="], - "@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="], - - "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], - - "@iconify/utils": ["@iconify/utils@3.1.0", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "mlly": "^1.8.0" } }, "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw=="], - "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="], @@ -331,14 +305,6 @@ "@malept/flatpak-bundler": ["@malept/flatpak-bundler@0.4.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.0", "lodash": "^4.17.15", "tmp-promise": "^3.0.2" } }, "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q=="], - "@mermaid-js/parser": ["@mermaid-js/parser@0.6.3", "", { "dependencies": { "langium": "3.3.1" } }, "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA=="], - - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], - - "@monaco-editor/loader": ["@monaco-editor/loader@1.7.0", "", { "dependencies": { "state-local": "^1.0.6" } }, "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA=="], - - "@monaco-editor/react": ["@monaco-editor/react@4.7.0", "", { "dependencies": { "@monaco-editor/loader": "^1.5.0" }, "peerDependencies": { "monaco-editor": ">= 0.25.0 < 1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA=="], - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], @@ -353,11 +319,9 @@ "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg=="], - "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.5.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-uOXpVX0ZjO7heSVjhheW2XEPrhQAWr2BScDPoZ9UDycl5iuHG+Usyc3AIfG6kZeC1GyLpMInpQ6X5+9n69yOFw=="], - - "@opentelemetry/core": ["@opentelemetry/core@2.5.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ=="], + "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.3.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-hGcsT0qDP7Il1L+qT3JFpiGl1dCjF794Bb4yCRCYdr7XC0NwHtOF3ngF86Gk6TUnsakbyQsDQ0E/S4CU0F4d4g=="], - "@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.208.0", "@opentelemetry/otlp-transformer": "0.208.0", "@opentelemetry/sdk-logs": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg=="], + "@opentelemetry/core": ["@opentelemetry/core@2.3.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-PcmxJQzs31cfD0R2dE91YGFcLxOSN4Bxz7gez5UwSUjCai8BwH/GI5HchfVshHkWdTkUs0qcaPJgVHKXUp7I3A=="], "@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Eju0L4qWcQS+oXxi6pgh7zvE2byogAkcsVv0OjHF/97iOz1N/aKE6etSGowYkie+YA1uo6DNwdSxaaNnLvcRlA=="], @@ -405,51 +369,37 @@ "@opentelemetry/instrumentation-undici": ["@opentelemetry/instrumentation-undici@0.19.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.208.0", "@opentelemetry/semantic-conventions": "^1.24.0" }, "peerDependencies": { "@opentelemetry/api": "^1.7.0" } }, "sha512-Pst/RhR61A2OoZQZkn6OLpdVpXp6qn3Y92wXa6umfJe9rV640r4bc6SWvw4pPN6DiQqPu2c8gnSSZPDtC6JlpQ=="], - "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.208.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA=="], - - "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.208.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ=="], - "@opentelemetry/redis-common": ["@opentelemetry/redis-common@0.38.2", "", {}, "sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA=="], - "@opentelemetry/resources": ["@opentelemetry/resources@2.5.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g=="], - - "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA=="], + "@opentelemetry/resources": ["@opentelemetry/resources@2.3.0", "", { "dependencies": { "@opentelemetry/core": "2.3.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-shlr2l5g+87J8wqYlsLyaUsgKVRO7RtX70Ckd5CtDOWtImZgaUDmf4Z2ozuSKQLM2wPDR0TE/3bPVBNJtRm/cQ=="], - "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], + "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.3.0", "", { "dependencies": { "@opentelemetry/core": "2.3.0", "@opentelemetry/resources": "2.3.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-B0TQ2e9h0ETjpI+eGmCz8Ojb+lnYms0SE3jFwEKrN/PK4aSVHU28AAmnOoBmfub+I3jfgPwvDJgomBA5a7QehQ=="], - "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.5.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ=="], - - "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.39.0", "", {}, "sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg=="], + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.38.0", "", {}, "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg=="], "@opentelemetry/sql-common": ["@opentelemetry/sql-common@0.41.2", "", { "dependencies": { "@opentelemetry/core": "^2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0" } }, "sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ=="], "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], - "@posthog/core": ["@posthog/core@1.13.0", "", { "dependencies": { "cross-spawn": "^7.0.6" } }, "sha512-knjncrk7qRmssFRbGzBl1Tunt21GRpe0Wv+uVelyL0Rh7PdQUsgguulzXFTps8hA6wPwTU4kq85qnbAJ3eH6Wg=="], - - "@posthog/types": ["@posthog/types@1.333.0", "", {}, "sha512-9Wg/2ez+EZh6NmtOjhtYSkBHz/yIq8WMS0QSIizUoggh35hHVg4BTMXl3rz/tPearJNKU/8oRjEyuZ0OYTEDOA=="], - - "@prisma/instrumentation": ["@prisma/instrumentation@6.19.0", "", { "dependencies": { "@opentelemetry/instrumentation": ">=0.52.0 <1" }, "peerDependencies": { "@opentelemetry/api": "^1.8" } }, "sha512-QcuYy25pkXM8BJ37wVFBO7Zh34nyRV1GOb2n3lPkkbRYfl4hWl3PTcImP41P0KrzVXfa/45p6eVCos27x3exIg=="], - - "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + "@posthog/core": ["@posthog/core@1.9.1", "", { "dependencies": { "cross-spawn": "^7.0.6" } }, "sha512-kRb1ch2dhQjsAapZmu6V66551IF2LnCbc1rnrQqnR7ArooVyJN9KOPXre16AJ3ObJz2eTfuP7x25BMyS2Y5Exw=="], - "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + "@posthog/types": ["@posthog/types@1.318.1", "", {}, "sha512-FYjHp4wlYvt4xc7MM+zIjfYICY/+lvjby/nOib29psTuUMT3nJXwqWz65QBox6XldaJdeId5F+UW6IOtJ1Q9iA=="], - "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], + "@prisma/client": ["@prisma/client@6.19.1", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-4SXj4Oo6HyQkLUWT8Ke5R0PTAfVOKip5Roo+6+b2EDTkFg5be0FnBWiuRJc0BC0sRQIWGMLKW1XguhVfW/z3/A=="], - "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], + "@prisma/config": ["@prisma/config@6.19.1", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.18.4", "empathic": "2.0.0" } }, "sha512-bUL/aYkGXLwxVGhJmQMtslLT7KPEfUqmRa919fKI4wQFX4bIFUKiY8Jmio/2waAjjPYrtuDHa7EsNCnJTXxiOw=="], - "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], + "@prisma/debug": ["@prisma/debug@6.19.1", "", {}, "sha512-h1JImhlAd/s5nhY/e9qkAzausWldbeT+e4nZF7A4zjDYBF4BZmKDt4y0jK7EZapqOm1kW7V0e9agV/iFDy3fWw=="], - "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + "@prisma/engines": ["@prisma/engines@6.19.1", "", { "dependencies": { "@prisma/debug": "6.19.1", "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", "@prisma/fetch-engine": "6.19.1", "@prisma/get-platform": "6.19.1" } }, "sha512-xy95dNJ7DiPf9IJ3oaVfX785nbFl7oNDzclUF+DIiJw6WdWCvPl0LPU0YqQLsrwv8N64uOQkH391ujo3wSo+Nw=="], - "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], + "@prisma/engines-version": ["@prisma/engines-version@7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", "", {}, "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA=="], - "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + "@prisma/fetch-engine": ["@prisma/fetch-engine@6.19.1", "", { "dependencies": { "@prisma/debug": "6.19.1", "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", "@prisma/get-platform": "6.19.1" } }, "sha512-mmgcotdaq4VtAHO6keov3db+hqlBzQS6X7tR7dFCbvXjLVTxBYdSJFRWz+dq7F9p6dvWyy1X0v8BlfRixyQK6g=="], - "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + "@prisma/get-platform": ["@prisma/get-platform@6.19.1", "", { "dependencies": { "@prisma/debug": "6.19.1" } }, "sha512-zsg44QUiQAnFUyh6Fbt7c9HjMXHwFTqtrgcX7DAZmRgnkPyYT7Sh8Mn8D5PuuDYNtMOYcpLGg576MLfIORsBYw=="], - "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], + "@prisma/instrumentation": ["@prisma/instrumentation@6.19.0", "", { "dependencies": { "@opentelemetry/instrumentation": ">=0.52.0 <1" }, "peerDependencies": { "@opentelemetry/api": "^1.8" } }, "sha512-QcuYy25pkXM8BJ37wVFBO7Zh34nyRV1GOb2n3lPkkbRYfl4hWl3PTcImP41P0KrzVXfa/45p6eVCos27x3exIg=="], "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], @@ -541,75 +491,75 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.56.0", "", { "os": "android", "cpu": "arm" }, "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.55.1", "", { "os": "android", "cpu": "arm" }, "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.56.0", "", { "os": "android", "cpu": "arm64" }, "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.55.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.56.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.55.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.56.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.55.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.56.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.55.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.56.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.55.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.55.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.55.1", "", { "os": "linux", "cpu": "arm" }, "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.55.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.55.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA=="], - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg=="], + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g=="], - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA=="], + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.56.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.55.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw=="], - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.56.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg=="], + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.55.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.56.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.55.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w=="], - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.56.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA=="], + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.55.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg=="], - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.56.0", "", { "os": "none", "cpu": "arm64" }, "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ=="], + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.55.1", "", { "os": "none", "cpu": "arm64" }, "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.56.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.55.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.56.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.55.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA=="], - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ=="], + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.55.1", "", { "os": "win32", "cpu": "x64" }, "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.55.1", "", { "os": "win32", "cpu": "x64" }, "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw=="], - "@sentry-internal/browser-utils": ["@sentry-internal/browser-utils@10.34.0", "", { "dependencies": { "@sentry/core": "10.34.0" } }, "sha512-0YNr60rGHyedmwkO0lbDBjNx2KAmT3kWamjaqu7Aw+jsESoPLgt+fzaTVvUBvkftBDui2PeTSzXm/nqzssctYg=="], + "@sentry-internal/browser-utils": ["@sentry-internal/browser-utils@10.29.0", "", { "dependencies": { "@sentry/core": "10.29.0" } }, "sha512-M3kycMY6f3KY9a8jDYac+yG0E3ZgWVWSxlOEC5MhYyX+g7mqxkwrb3LFQyuxSm/m+CCgMTCaPOOaB2twXP6EQg=="], - "@sentry-internal/feedback": ["@sentry-internal/feedback@10.34.0", "", { "dependencies": { "@sentry/core": "10.34.0" } }, "sha512-wgGnq+iNxsFSOe9WX/FOvtoItSTjgLJJ4dQkVYtcVM6WGBVIg4wgNYfECCnRNztUTPzpZHLjC9r+4Pym451DDQ=="], + "@sentry-internal/feedback": ["@sentry-internal/feedback@10.29.0", "", { "dependencies": { "@sentry/core": "10.29.0" } }, "sha512-Y7IRsNeS99cEONu1mZWZc3HvbjNnu59Hgymm0swFFKbdgbCgdT6l85kn2oLsuq4Ew8Dw/pL/Sgpwsl9UgYFpUg=="], - "@sentry-internal/replay": ["@sentry-internal/replay@10.34.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.34.0", "@sentry/core": "10.34.0" } }, "sha512-Vmea0GcOg57z/S1bVSj3saFcRvDqdLzdy4wd9fQMpMgy5OCbTlo7lxVUndKzbcZnanma6zF6VxwnWER1WuN9RA=="], + "@sentry-internal/replay": ["@sentry-internal/replay@10.29.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.29.0", "@sentry/core": "10.29.0" } }, "sha512-45NVw9PwB9TQ8z+xJ6G6Za+wmQ1RTA35heBSzR6U4bknj8LmA04k2iwnobvxCBEQXeLfcJEO1vFgagMoqMZMBw=="], - "@sentry-internal/replay-canvas": ["@sentry-internal/replay-canvas@10.34.0", "", { "dependencies": { "@sentry-internal/replay": "10.34.0", "@sentry/core": "10.34.0" } }, "sha512-XWH/9njtgMD+LLWjc4KKgBpb+dTCkoUEIFDxcvzG/87d+jirmzf0+r8EfpLwKG+GrqNiiGRV39zIqu0SfPl+cw=="], + "@sentry-internal/replay-canvas": ["@sentry-internal/replay-canvas@10.29.0", "", { "dependencies": { "@sentry-internal/replay": "10.29.0", "@sentry/core": "10.29.0" } }, "sha512-typY4JrpAQQGPuSyd/BD8+nNCbvTV2UVvKzr+iKgI0m1qc4Dz8tHZ4Nfais2Z8eYn/pL1kqVQN5ERTmJoYFdIw=="], - "@sentry/browser": ["@sentry/browser@10.34.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.34.0", "@sentry-internal/feedback": "10.34.0", "@sentry-internal/replay": "10.34.0", "@sentry-internal/replay-canvas": "10.34.0", "@sentry/core": "10.34.0" } }, "sha512-8WCsAXli5Z+eIN8dMY8KGQjrS3XgUp1np/pjdeWNrVPVR8q8XpS34qc+f+y/LFrYQC9bs2Of5aIBwRtDCIvRsg=="], + "@sentry/browser": ["@sentry/browser@10.29.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.29.0", "@sentry-internal/feedback": "10.29.0", "@sentry-internal/replay": "10.29.0", "@sentry-internal/replay-canvas": "10.29.0", "@sentry/core": "10.29.0" } }, "sha512-XdbyIR6F4qoR9Z1JCWTgunVcTJjS9p2Th+v4wYs4ME+ZdLC4tuKKmRgYg3YdSIWCn1CBfIgdI6wqETSf7H6Njw=="], - "@sentry/core": ["@sentry/core@10.34.0", "", {}, "sha512-4FFpYBMf0VFdPcsr4grDYDOR87mRu6oCfb51oQjU/Pndmty7UgYo0Bst3LEC/8v0SpytBtzXq+Wx/fkwulBesg=="], + "@sentry/core": ["@sentry/core@10.29.0", "", {}, "sha512-olQ2DU9dA/Bwsz3PtA9KNXRMqBWRQSkPw+MxwWEoU1K1qtiM9L0j6lbEFb5iSY3d7WYD5MB+1d5COugjSBrHtw=="], - "@sentry/electron": ["@sentry/electron@7.6.0", "", { "dependencies": { "@sentry/browser": "10.34.0", "@sentry/core": "10.34.0", "@sentry/node": "10.34.0" }, "peerDependencies": { "@sentry/node-native": "10.34.0" }, "optionalPeers": ["@sentry/node-native"] }, "sha512-ueW3Coa0BtOQFPaf+QaI3mBHMi/t7CkZnuzZ6PNoVpHe6CgYfCtNdE7H1BpMsCpG1FhEAgCLBJtpaMKyQBFdzQ=="], + "@sentry/electron": ["@sentry/electron@7.5.0", "", { "dependencies": { "@sentry/browser": "10.29.0", "@sentry/core": "10.29.0", "@sentry/node": "10.29.0" }, "peerDependencies": { "@sentry/node-native": "10.29.0" }, "optionalPeers": ["@sentry/node-native"] }, "sha512-88t/YsB5iO75faKdd7lIuJkwp9FGKgFlkDuaSJhsJiVcjlywkn8CwUbctAbS0gu6Suc0raHCF4ULvGyksKAoww=="], - "@sentry/node": ["@sentry/node@10.34.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^2.2.0", "@opentelemetry/core": "^2.2.0", "@opentelemetry/instrumentation": "^0.208.0", "@opentelemetry/instrumentation-amqplib": "0.55.0", "@opentelemetry/instrumentation-connect": "0.52.0", "@opentelemetry/instrumentation-dataloader": "0.26.0", "@opentelemetry/instrumentation-express": "0.57.0", "@opentelemetry/instrumentation-fs": "0.28.0", "@opentelemetry/instrumentation-generic-pool": "0.52.0", "@opentelemetry/instrumentation-graphql": "0.56.0", "@opentelemetry/instrumentation-hapi": "0.55.0", "@opentelemetry/instrumentation-http": "0.208.0", "@opentelemetry/instrumentation-ioredis": "0.56.0", "@opentelemetry/instrumentation-kafkajs": "0.18.0", "@opentelemetry/instrumentation-knex": "0.53.0", "@opentelemetry/instrumentation-koa": "0.57.0", "@opentelemetry/instrumentation-lru-memoizer": "0.53.0", "@opentelemetry/instrumentation-mongodb": "0.61.0", "@opentelemetry/instrumentation-mongoose": "0.55.0", "@opentelemetry/instrumentation-mysql": "0.54.0", "@opentelemetry/instrumentation-mysql2": "0.55.0", "@opentelemetry/instrumentation-pg": "0.61.0", "@opentelemetry/instrumentation-redis": "0.57.0", "@opentelemetry/instrumentation-tedious": "0.27.0", "@opentelemetry/instrumentation-undici": "0.19.0", "@opentelemetry/resources": "^2.2.0", "@opentelemetry/sdk-trace-base": "^2.2.0", "@opentelemetry/semantic-conventions": "^1.37.0", "@prisma/instrumentation": "6.19.0", "@sentry/core": "10.34.0", "@sentry/node-core": "10.34.0", "@sentry/opentelemetry": "10.34.0", "import-in-the-middle": "^2.0.1", "minimatch": "^9.0.0" } }, "sha512-bEOyH97HuVtWZYAZ5mp0NhYNc+n6QCfiKuLee2P75n2kt4cIPTGvLOSdUwwjllf795uOdKZJuM1IUN0W+YMcVg=="], + "@sentry/node": ["@sentry/node@10.29.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^2.2.0", "@opentelemetry/core": "^2.2.0", "@opentelemetry/instrumentation": "^0.208.0", "@opentelemetry/instrumentation-amqplib": "0.55.0", "@opentelemetry/instrumentation-connect": "0.52.0", "@opentelemetry/instrumentation-dataloader": "0.26.0", "@opentelemetry/instrumentation-express": "0.57.0", "@opentelemetry/instrumentation-fs": "0.28.0", "@opentelemetry/instrumentation-generic-pool": "0.52.0", "@opentelemetry/instrumentation-graphql": "0.56.0", "@opentelemetry/instrumentation-hapi": "0.55.0", "@opentelemetry/instrumentation-http": "0.208.0", "@opentelemetry/instrumentation-ioredis": "0.56.0", "@opentelemetry/instrumentation-kafkajs": "0.18.0", "@opentelemetry/instrumentation-knex": "0.53.0", "@opentelemetry/instrumentation-koa": "0.57.0", "@opentelemetry/instrumentation-lru-memoizer": "0.53.0", "@opentelemetry/instrumentation-mongodb": "0.61.0", "@opentelemetry/instrumentation-mongoose": "0.55.0", "@opentelemetry/instrumentation-mysql": "0.54.0", "@opentelemetry/instrumentation-mysql2": "0.55.0", "@opentelemetry/instrumentation-pg": "0.61.0", "@opentelemetry/instrumentation-redis": "0.57.0", "@opentelemetry/instrumentation-tedious": "0.27.0", "@opentelemetry/instrumentation-undici": "0.19.0", "@opentelemetry/resources": "^2.2.0", "@opentelemetry/sdk-trace-base": "^2.2.0", "@opentelemetry/semantic-conventions": "^1.37.0", "@prisma/instrumentation": "6.19.0", "@sentry/core": "10.29.0", "@sentry/node-core": "10.29.0", "@sentry/opentelemetry": "10.29.0", "import-in-the-middle": "^2", "minimatch": "^9.0.0" } }, "sha512-9j8VzV06VCj+H8tlxpfa7BNN4HzH5exv68WOufdMTXzzWLOXnzrdNDoYplm1G2S3LMvWsc1SVI3a8A0yBY7oWg=="], - "@sentry/node-core": ["@sentry/node-core@10.34.0", "", { "dependencies": { "@apm-js-collab/tracing-hooks": "^0.3.1", "@sentry/core": "10.34.0", "@sentry/opentelemetry": "10.34.0", "import-in-the-middle": "^2.0.1" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0 || ^2.2.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0 || ^2.2.0", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/resources": "^1.30.1 || ^2.1.0 || ^2.2.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0 || ^2.2.0", "@opentelemetry/semantic-conventions": "^1.37.0" } }, "sha512-FrGfC8GzD1cnZDO3zwQ4cjyoY1ZwNHvZbXSvXRYxpjhXidZhvaPurjgLRSB0xGaFgoemmOp1ufsx/w6fQOGA6Q=="], + "@sentry/node-core": ["@sentry/node-core@10.29.0", "", { "dependencies": { "@apm-js-collab/tracing-hooks": "^0.3.1", "@sentry/core": "10.29.0", "@sentry/opentelemetry": "10.29.0", "import-in-the-middle": "^2" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0 || ^2.2.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0 || ^2.2.0", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/resources": "^1.30.1 || ^2.1.0 || ^2.2.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0 || ^2.2.0", "@opentelemetry/semantic-conventions": "^1.37.0" } }, "sha512-f/Y0okHhPPb5HnYNBqCivJ2YuXtSadvcIx16dzU5mHQxZhgGednUCPEX7rsvPcd4HneQz12HKLqxbAmNu+b3FA=="], - "@sentry/opentelemetry": ["@sentry/opentelemetry@10.34.0", "", { "dependencies": { "@sentry/core": "10.34.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0 || ^2.2.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0 || ^2.2.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0 || ^2.2.0", "@opentelemetry/semantic-conventions": "^1.37.0" } }, "sha512-uKuULBOmdVu3bYdD8doMLqKgN0PP3WWtI7Shu11P9PVrhSNT4U9yM9Z6v1aFlQcbrgyg3LynZuXs8lyjt90UbA=="], + "@sentry/opentelemetry": ["@sentry/opentelemetry@10.29.0", "", { "dependencies": { "@sentry/core": "10.29.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0 || ^2.2.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0 || ^2.2.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0 || ^2.2.0", "@opentelemetry/semantic-conventions": "^1.37.0" } }, "sha512-5QvtAwS73HlI/+OTF1poAFELzsc0se+PHmMsXGGrOeNBvjCr3ZE8qvke09aeMn7uRImf3Nc9J6i2KtSHJnbKPA=="], "@shikijs/core": ["@shikijs/core@1.29.2", "", { "dependencies": { "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.4" } }, "sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ=="], @@ -633,9 +583,9 @@ "@tailwindcss/typography": ["@tailwindcss/typography@0.5.19", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg=="], - "@tanstack/query-core": ["@tanstack/query-core@5.90.19", "", {}, "sha512-GLW5sjPVIvH491VV1ufddnfldyVB+teCnpPIvweEfkpRx7CfUmUGhoh9cdcUKBh/KwVxk22aNEDxeTsvmyB/WA=="], + "@tanstack/query-core": ["@tanstack/query-core@5.90.16", "", {}, "sha512-MvtWckSVufs/ja463/K4PyJeqT+HMlJWtw6PrCpywznd2NSgO3m4KwO9RqbFqGg6iDE8vVMFWMeQI4Io3eEYww=="], - "@tanstack/react-query": ["@tanstack/react-query@5.90.19", "", { "dependencies": { "@tanstack/query-core": "5.90.19" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-qTZRZ4QyTzQc+M0IzrbKHxSeISUmRB3RPGmao5bT+sI6ayxSRhn0FXEnT5Hg3as8SBFcRosrXXRFB+yAcxVxJQ=="], + "@tanstack/react-query": ["@tanstack/react-query@5.90.16", "", { "dependencies": { "@tanstack/query-core": "5.90.16" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-bpMGOmV4OPmif7TNMteU/Ehf/hoC0Kf98PDc0F4BZkFrEapRMEqI/V6YS0lyzwSV6PQpY1y4xxArUIfBW5LVxQ=="], "@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.18", "", { "dependencies": { "@tanstack/virtual-core": "3.13.18" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dZkhyfahpvlaV0rIKnvQiVoWPyURppl6w4m9IwMDpuIjcJ1sD9YGWrt0wISvgU7ewACXx2Ct46WPgI6qAD4v6A=="], @@ -663,68 +613,6 @@ "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], - "@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="], - - "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], - - "@types/d3-axis": ["@types/d3-axis@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="], - - "@types/d3-brush": ["@types/d3-brush@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="], - - "@types/d3-chord": ["@types/d3-chord@3.0.6", "", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="], - - "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], - - "@types/d3-contour": ["@types/d3-contour@3.0.6", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="], - - "@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="], - - "@types/d3-dispatch": ["@types/d3-dispatch@3.0.7", "", {}, "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="], - - "@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="], - - "@types/d3-dsv": ["@types/d3-dsv@3.0.7", "", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="], - - "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], - - "@types/d3-fetch": ["@types/d3-fetch@3.0.7", "", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="], - - "@types/d3-force": ["@types/d3-force@3.0.10", "", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="], - - "@types/d3-format": ["@types/d3-format@3.0.4", "", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="], - - "@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="], - - "@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="], - - "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], - - "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], - - "@types/d3-polygon": ["@types/d3-polygon@3.0.2", "", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="], - - "@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="], - - "@types/d3-random": ["@types/d3-random@3.0.3", "", {}, "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ=="], - - "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], - - "@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="], - - "@types/d3-selection": ["@types/d3-selection@3.0.11", "", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="], - - "@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="], - - "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], - - "@types/d3-time-format": ["@types/d3-time-format@4.0.3", "", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="], - - "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], - - "@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="], - - "@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="], - "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], @@ -733,8 +621,6 @@ "@types/fs-extra": ["@types/fs-extra@9.0.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA=="], - "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], - "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], "@types/http-cache-semantics": ["@types/http-cache-semantics@4.0.4", "", {}, "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA=="], @@ -747,7 +633,7 @@ "@types/mysql": ["@types/mysql@2.15.27", "", { "dependencies": { "@types/node": "*" } }, "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA=="], - "@types/node": ["@types/node@20.19.30", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g=="], + "@types/node": ["@types/node@20.19.27", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug=="], "@types/pg": ["@types/pg@8.15.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ=="], @@ -755,10 +641,14 @@ "@types/plist": ["@types/plist@3.0.5", "", { "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" } }, "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA=="], - "@types/react": ["@types/react@19.2.9", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA=="], + "@types/prismjs": ["@types/prismjs@1.26.5", "", {}, "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ=="], + + "@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + "@types/react-syntax-highlighter": ["@types/react-syntax-highlighter@15.5.13", "", { "dependencies": { "@types/react": "*" } }, "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA=="], + "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="], "@types/tedious": ["@types/tedious@4.0.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw=="], @@ -777,11 +667,9 @@ "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], - "@vue/reactivity": ["@vue/reactivity@3.5.27", "", { "dependencies": { "@vue/shared": "3.5.27" } }, "sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ=="], + "@vue/reactivity": ["@vue/reactivity@3.5.26", "", { "dependencies": { "@vue/shared": "3.5.26" } }, "sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ=="], - "@vue/shared": ["@vue/shared@3.5.27", "", {}, "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ=="], - - "@welldone-software/why-did-you-render": ["@welldone-software/why-did-you-render@10.0.1", "", { "dependencies": { "lodash": "^4" }, "peerDependencies": { "react": "^19" } }, "sha512-tMgGkt30iVYeLMUKExNmtm019QgyjLtA7lwB0QAizYNEuihlCG2eoAWBBaz/bDeI7LeqAJ9msC6hY3vX+JB97g=="], + "@vue/shared": ["@vue/shared@3.5.26", "", {}, "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A=="], "@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="], @@ -801,8 +689,6 @@ "abbrev": ["abbrev@1.1.1", "", {}, "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="], - "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], @@ -813,11 +699,9 @@ "aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="], - "ai": ["ai@6.0.45", "", { "dependencies": { "@ai-sdk/gateway": "3.0.19", "@ai-sdk/provider": "3.0.4", "@ai-sdk/provider-utils": "4.0.8", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8QFU1cFY25Z7M72gpw4pzkvyEXK4X1oO3ZRUQYofU4hs742rt+NMAmgNn2uZo/cMtEH5Dt1Vlu/fMQXmLnRoZQ=="], + "ai": ["ai@6.0.14", "", { "dependencies": { "@ai-sdk/gateway": "3.0.9", "@ai-sdk/provider": "3.0.2", "@ai-sdk/provider-utils": "4.0.4", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OaEJFeQ3gb45eZtC/lSNKqAxmsrqWxC8wLmIVXFYAMvPXE3lb96zIdS3swYArR4uXOVt6N7H/XZSyQz/Dl+HTw=="], - "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - - "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], "ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], @@ -855,8 +739,6 @@ "async-exit-hook": ["async-exit-hook@2.0.1", "", {}, "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw=="], - "async-mutex": ["async-mutex@0.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA=="], - "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], "at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="], @@ -869,7 +751,7 @@ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.9.17", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-agD0MgJFUP/4nvjqzIB29zRPUuCF7Ge6mEv9s8dHrtYD7QWXRcx75rOADE/d5ah1NI+0vkDl0yorDd5U852IQQ=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.9.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ=="], "better-sqlite3": ["better-sqlite3@11.10.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ=="], @@ -883,8 +765,6 @@ "bluebird-lst": ["bluebird-lst@1.0.9", "", { "dependencies": { "bluebird": "^3.5.5" } }, "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw=="], - "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], - "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="], "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], @@ -903,7 +783,7 @@ "builder-util-runtime": ["builder-util-runtime@9.2.10", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw=="], - "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + "c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="], "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], @@ -915,11 +795,9 @@ "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], - "caniuse-lite": ["caniuse-lite@1.0.30001765", "", {}, "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ=="], + "caniuse-lite": ["caniuse-lite@1.0.30001762", "", {}, "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], @@ -933,10 +811,6 @@ "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], - "chevrotain": ["chevrotain@11.0.3", "", { "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", "@chevrotain/regexp-to-ast": "11.0.3", "@chevrotain/types": "11.0.3", "@chevrotain/utils": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw=="], - - "chevrotain-allstar": ["chevrotain-allstar@0.3.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^11.0.0" } }, "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw=="], - "chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], "chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], @@ -945,7 +819,9 @@ "ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], - "cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], + "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], + + "cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="], "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], @@ -983,32 +859,22 @@ "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + "confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="], "config-file-ts": ["config-file-ts@0.2.8-rc1", "", { "dependencies": { "glob": "^10.3.12", "typescript": "^5.4.3" } }, "sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg=="], - "console-control-strings": ["console-control-strings@1.1.0", "", {}, "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="], - - "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], + "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], - "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + "console-control-strings": ["console-control-strings@1.1.0", "", {}, "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - - "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - "copy-anything": ["copy-anything@4.0.5", "", { "dependencies": { "is-what": "^5.2.0" } }, "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA=="], - "core-js": ["core-js@3.48.0", "", {}, "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ=="], + "core-js": ["core-js@3.47.0", "", {}, "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg=="], "core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], - "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], - - "cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="], - "crc": ["crc@3.8.0", "", { "dependencies": { "buffer": "^5.1.0" } }, "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ=="], "crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="], @@ -1021,90 +887,18 @@ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - "cytoscape": ["cytoscape@3.33.1", "", {}, "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ=="], - - "cytoscape-cose-bilkent": ["cytoscape-cose-bilkent@4.1.0", "", { "dependencies": { "cose-base": "^1.0.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ=="], - - "cytoscape-fcose": ["cytoscape-fcose@2.2.0", "", { "dependencies": { "cose-base": "^2.2.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ=="], - - "d3": ["d3@7.9.0", "", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="], - - "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], - - "d3-axis": ["d3-axis@3.0.0", "", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="], - - "d3-brush": ["d3-brush@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "3", "d3-transition": "3" } }, "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ=="], - - "d3-chord": ["d3-chord@3.0.1", "", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="], - - "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], - - "d3-contour": ["d3-contour@4.0.2", "", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="], - - "d3-delaunay": ["d3-delaunay@6.0.4", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="], - - "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="], - - "d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="], - - "d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="], - - "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], - - "d3-fetch": ["d3-fetch@3.0.1", "", { "dependencies": { "d3-dsv": "1 - 3" } }, "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw=="], - - "d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="], - - "d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], - - "d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="], - - "d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="], - - "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], - - "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], - - "d3-polygon": ["d3-polygon@3.0.1", "", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="], - - "d3-quadtree": ["d3-quadtree@3.0.1", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="], - - "d3-random": ["d3-random@3.0.1", "", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="], - - "d3-sankey": ["d3-sankey@0.12.3", "", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="], - - "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], - - "d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="], - - "d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="], - - "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], - - "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], - - "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], - - "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], - - "d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="], - - "d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="], - - "dagre-d3-es": ["dagre-d3-es@7.0.13", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q=="], - "date-fns": ["date-fns@3.6.0", "", {}, "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww=="], - "dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="], - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + "decode-named-character-reference": ["decode-named-character-reference@1.2.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q=="], "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + "deepmerge-ts": ["deepmerge-ts@7.1.5", "", {}, "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw=="], + "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], "defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="], @@ -1113,16 +907,16 @@ "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], - "delaunator": ["delaunator@5.0.1", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw=="], + "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], "delegates": ["delegates@1.0.0", "", {}, "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="], - "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="], @@ -1155,7 +949,7 @@ "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], - "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + "effect": ["effect@3.18.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA=="], "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], @@ -1171,7 +965,7 @@ "electron-rebuild": ["electron-rebuild@3.2.9", "", { "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", "chalk": "^4.0.0", "debug": "^4.1.1", "detect-libc": "^2.0.1", "fs-extra": "^10.0.0", "got": "^11.7.0", "lzma-native": "^8.0.5", "node-abi": "^3.0.0", "node-api-version": "^0.1.4", "node-gyp": "^9.0.0", "ora": "^5.1.0", "semver": "^7.3.5", "tar": "^6.0.5", "yargs": "^17.0.1" }, "bin": { "electron-rebuild": "lib/src/cli.js" } }, "sha512-FkEZNFViUem3P0RLYbZkUjC8LUFIK+wKq09GHoOITSJjfDAVQv964hwaNseTTWt58sITQX3/5fHNYcTefqaCWw=="], - "electron-to-chromium": ["electron-to-chromium@1.5.277", "", {}, "sha512-wKXFZw4erWmmOz5N/grBoJ2XrNJGDFMu2+W5ACHza5rHtvsqrK4gb6rnLC7XxKB9WlJ+RmyQatuEXmtm86xbnw=="], + "electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="], "electron-updater": ["electron-updater@6.7.3", "", { "dependencies": { "builder-util-runtime": "9.5.1", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", "lodash.escaperegexp": "^4.1.2", "lodash.isequal": "^4.5.0", "semver": "~7.7.3", "tiny-typed-emitter": "^2.1.0" } }, "sha512-EgkT8Z9noqXKbwc3u5FkJA+r48jwZ5DTUiOkJMOTEEH//n5Am6wfQGz7nvSFEA2oIAMv9jRzn5JKTyWeSKOPgg=="], @@ -1181,7 +975,7 @@ "emoji-regex-xs": ["emoji-regex-xs@1.0.0", "", {}, "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg=="], - "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], "encoding": ["encoding@0.1.13", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A=="], @@ -1209,27 +1003,19 @@ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], - - "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], - "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], - - "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], - "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], - "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - - "express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="], + "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], @@ -1239,6 +1025,8 @@ "extsprintf": ["extsprintf@1.4.1", "", {}, "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA=="], + "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="], @@ -1247,10 +1035,10 @@ "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], - "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], - "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + "fault": ["fault@1.0.4", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA=="], + "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], @@ -1263,13 +1051,11 @@ "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], - "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], - "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + "format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="], "forwarded-parse": ["forwarded-parse@2.1.2", "", {}, "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw=="], @@ -1277,8 +1063,6 @@ "framer-motion": ["framer-motion@11.18.2", "", { "dependencies": { "motion-dom": "^11.18.1", "motion-utils": "^11.18.1", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w=="], - "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], "fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], @@ -1307,6 +1091,8 @@ "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], + "giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], + "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], @@ -1325,8 +1111,6 @@ "gray-matter": ["gray-matter@4.0.3", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="], - "hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="], - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], @@ -1359,7 +1143,7 @@ "highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="], - "hono": ["hono@4.11.5", "", {}, "sha512-WemPi9/WfyMwZs+ZUXdiwcCh9Y+m7L+8vki9MzDw3jJ+W9Lc+12HGsd368Qc1vZi1xwW8BWMMsnK5efYKPdt4g=="], + "highlightjs-vue": ["highlightjs-vue@1.0.0", "", {}, "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA=="], "hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], @@ -1369,8 +1153,6 @@ "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], - "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], - "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], "http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="], @@ -1385,7 +1167,7 @@ "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - "import-in-the-middle": ["import-in-the-middle@2.0.5", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-0InH9/4oDCBRzWXhpOqusspLBrVfK1vPvbn9Wxl8DAQ8yyx5fWJRETICSwkiAMaYntjJAMBP1R4B6cQnEUYVEA=="], + "import-in-the-middle": ["import-in-the-middle@2.0.1", "", { "dependencies": { "acorn": "^8.14.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^1.2.2", "module-details-from-path": "^1.0.3" } }, "sha512-bruMpJ7xz+9jwGzrwEhWgvRrlKRYCRDBrfU+ur3FcasYXLJDxTruJ//8g2Noj+QFyRBeqbpj8Bhn4Fbw6HjvhA=="], "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], @@ -1401,12 +1183,8 @@ "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], - "internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], - "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], - "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], - "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], @@ -1437,8 +1215,6 @@ "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], - "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], - "is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], "is-what": ["is-what@5.5.0", "", {}, "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw=="], @@ -1455,9 +1231,7 @@ "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], - "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], - - "jotai": ["jotai@2.16.2", "", { "peerDependencies": { "@babel/core": ">=7.0.0", "@babel/template": ">=7.0.0", "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@babel/core", "@babel/template", "@types/react", "react"] }, "sha512-DH0lBiTXvewsxtqqwjDW6Hg9JPTDnq9LcOsXSFWCAUEt+qj5ohl9iRVX9zQXPPHKLXCdH+5mGvM28fsXMl17/g=="], + "jotai": ["jotai@2.16.1", "", { "peerDependencies": { "@babel/core": ">=7.0.0", "@babel/template": ">=7.0.0", "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@babel/core", "@babel/template", "@types/react", "react"] }, "sha512-vrHcAbo3P7Br37C8Bv6JshMtlKMPqqmx0DDREtTjT4nf3QChDrYdbH+4ik/9V0cXA57dK28RkJ5dctYvavcIlg=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], @@ -1469,30 +1243,18 @@ "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], - "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - - "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], - "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], - "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], - "katex": ["katex@0.16.27", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw=="], - "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], - "khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="], - "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], - "langium": ["langium@3.3.1", "", { "dependencies": { "chevrotain": "~11.0.3", "chevrotain-allstar": "~0.3.0", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", "vscode-uri": "~3.0.8" } }, "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w=="], - - "layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="], - "lazy-val": ["lazy-val@1.0.5", "", {}, "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q=="], "lazystream": ["lazystream@1.0.1", "", { "dependencies": { "readable-stream": "^2.0.5" } }, "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw=="], @@ -1501,9 +1263,7 @@ "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], - "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], - - "lodash-es": ["lodash-es@4.17.23", "", {}, "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg=="], + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], "lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="], @@ -1521,8 +1281,6 @@ "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], - "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], - "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="], @@ -1541,7 +1299,7 @@ "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], - "marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="], + "marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="], "matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="], @@ -1569,8 +1327,6 @@ "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="], - "mdast-util-newline-to-break": ["mdast-util-newline-to-break@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-find-and-replace": "^3.0.0" } }, "sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog=="], - "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], @@ -1579,14 +1335,8 @@ "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], - "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], - - "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], - "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], - "mermaid": ["mermaid@11.12.2", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.1", "@mermaid-js/parser": "^0.6.3", "@types/d3": "^7.4.3", "cytoscape": "^3.29.3", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.13", "dayjs": "^1.11.18", "dompurify": "^3.2.5", "katex": "^0.16.22", "khroma": "^2.1.0", "lodash-es": "^4.17.21", "marked": "^16.2.1", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w=="], - "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], @@ -1647,9 +1397,9 @@ "mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="], - "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], @@ -1677,12 +1427,8 @@ "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], - "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], - "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], - "monaco-editor": ["monaco-editor@0.55.1", "", { "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" } }, "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A=="], - "motion": ["motion@11.18.2", "", { "dependencies": { "framer-motion": "^11.18.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-JLjvFDuFr42NFtcVoMAyC2sEjnpA8xpy6qWPyzQvCloznAyQ8FIXioxWfHiLtgYhoVpfUqSWpn1h9++skj9+Wg=="], "motion-dom": ["motion-dom@11.18.1", "", { "dependencies": { "motion-utils": "^11.18.1" } }, "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw=="], @@ -1701,12 +1447,14 @@ "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], - "node-abi": ["node-abi@3.87.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ=="], + "node-abi": ["node-abi@3.85.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg=="], "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], "node-api-version": ["node-api-version@0.1.4", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-KGXihXdUChwJAOHO53bv9/vXcLmdUsZ6jIptbvYvkpKfth+r7jw44JkVxQFA3kX5nQjzjmGu1uAu/xNNLNlI5g=="], + "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], + "node-gyp": ["node-gyp@9.4.1", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "glob": "^7.1.4", "graceful-fs": "^4.2.6", "make-fetch-happen": "^10.0.3", "nopt": "^6.0.0", "npmlog": "^6.0.0", "rimraf": "^3.0.2", "semver": "^7.3.5", "tar": "^6.1.2", "which": "^2.0.2" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ=="], "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], @@ -1723,15 +1471,15 @@ "npmlog": ["npmlog@6.0.2", "", { "dependencies": { "are-we-there-yet": "^3.0.0", "console-control-strings": "^1.1.0", "gauge": "^4.0.3", "set-blocking": "^2.0.0" } }, "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg=="], + "nypm": ["nypm@0.6.2", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], - "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], - "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], @@ -1751,16 +1499,10 @@ "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], - "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], - "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], - "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - - "path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="], - "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], @@ -1769,17 +1511,17 @@ "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "pe-library": ["pe-library@0.4.1", "", {}, "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw=="], "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], + "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], + "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], - "pg-protocol": ["pg-protocol@1.11.0", "", {}, "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g=="], + "pg-protocol": ["pg-protocol@1.10.3", "", {}, "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="], "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], @@ -1793,16 +1535,10 @@ "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], - "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], - - "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], "plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="], - "points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="], - - "points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="], - "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], @@ -1825,14 +1561,18 @@ "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], - "posthog-js": ["posthog-js@1.333.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.208.0", "@opentelemetry/exporter-logs-otlp-http": "^0.208.0", "@opentelemetry/resources": "^2.2.0", "@opentelemetry/sdk-logs": "^0.208.0", "@posthog/core": "1.13.0", "@posthog/types": "1.333.0", "core-js": "^3.38.1", "dompurify": "^3.3.1", "fflate": "^0.4.8", "preact": "^10.28.0", "query-selector-shadow-dom": "^1.0.1", "web-vitals": "^4.2.4" } }, "sha512-c7vquERMedjuGE2GnaDDJW/V1BIMMQG7BlYKrH0z8O7fc3WpEsQ/IyQ+9aD9+DLxlDCFpzrwgoxVDWi9K37mdA=="], + "posthog-js": ["posthog-js@1.318.1", "", { "dependencies": { "@posthog/core": "1.9.1", "@posthog/types": "1.318.1", "core-js": "^3.38.1", "dompurify": "^3.3.1", "fflate": "^0.4.8", "preact": "^10.19.3", "query-selector-shadow-dom": "^1.0.1", "web-vitals": "^4.2.4" } }, "sha512-8zG02pcTYILB8At/9P2B2MJjuGq+DY3BeE01I4IPi6bnWNh3hka6+ZQhn9GfgtCQs7Ygk6qMaOxpUFvsDGQaQQ=="], - "posthog-node": ["posthog-node@5.24.1", "", { "dependencies": { "@posthog/core": "1.13.0" } }, "sha512-1+wsosb5fjuor9zpp3h2uq0xKYY7rDz8gpw/10Scz8Ob/uVNrsHSwGy76D9rgt4cfyaEgpJwyYv+hPi2+YjWtw=="], + "posthog-node": ["posthog-node@5.20.0", "", { "dependencies": { "@posthog/core": "1.9.1" } }, "sha512-LkR5KfrvEQTnUtNKN97VxFB00KcYG1Iz8iKg8r0e/i7f1eQhg1WSZO+Jp1B4bvtHCmdpIE4HwYbvCCzFoCyjVg=="], "preact": ["preact@10.28.2", "", {}, "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA=="], "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], + "prisma": ["prisma@6.19.1", "", { "dependencies": { "@prisma/config": "6.19.1", "@prisma/engines": "6.19.1" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-XRfmGzh6gtkc/Vq3LqZJcS2884dQQW3UhPo6jNRoiTW95FFQkXFg8vkYEy6og+Pyv0aY7zRQ7Wn1Cvr56XjhQQ=="], + + "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], + "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], @@ -1843,15 +1583,11 @@ "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], - "protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], - - "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], - "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="], + "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], "query-selector-shadow-dom": ["query-selector-shadow-dom@1.0.1", "", {}, "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw=="], @@ -1859,12 +1595,10 @@ "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], - "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - - "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], - "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], + "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], + "react": ["react@19.2.1", "", {}, "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw=="], "react-dom": ["react-dom@19.2.1", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.1" } }, "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg=="], @@ -1881,7 +1615,7 @@ "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], - "react-zoom-pan-pinch": ["react-zoom-pan-pinch@3.7.0", "", { "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-UmReVZ0TxlKzxSbYiAj+LeGRW8s8LraAFTXRAxzMYnNRgGPsxCudwZKVkjvGmjtx7SW/hZamt69NUmGf4xrkXA=="], + "react-syntax-highlighter": ["react-syntax-highlighter@16.1.0", "", { "dependencies": { "@babel/runtime": "^7.28.4", "highlight.js": "^10.4.1", "highlightjs-vue": "^1.0.0", "lowlight": "^1.17.0", "prismjs": "^1.30.0", "refractor": "^5.0.0" }, "peerDependencies": { "react": ">= 0.14.0" } }, "sha512-E40/hBiP5rCNwkeBN1vRP+xow1X0pndinO+z3h7HLsHyjztbyjfzNWNKuAsJj+7DLam9iT4AaaOZnueCU+Nplg=="], "reactivity-store": ["reactivity-store@0.3.12", "", { "dependencies": { "@vue/reactivity": "~3.5.22", "@vue/shared": "~3.5.22", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Idz9EL4dFUtQbHySZQzckWOTUfqjdYpUtNW0iOysC32mG7IjiUGB77QrsyR5eAWBkRiS9JscF6A3fuQAIy+LrQ=="], @@ -1895,6 +1629,8 @@ "readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + "refractor": ["refractor@5.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/prismjs": "^1.0.0", "hastscript": "^9.0.0", "parse-entities": "^4.0.0" } }, "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw=="], + "regex": ["regex@5.1.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw=="], "regex-recursion": ["regex-recursion@5.1.1", "", { "dependencies": { "regex": "^5.1.1", "regex-utilities": "^2.3.0" } }, "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w=="], @@ -1907,8 +1643,6 @@ "rehype-sanitize": ["rehype-sanitize@6.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-sanitize": "^5.0.0" } }, "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg=="], - "remark-breaks": ["remark-breaks@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-newline-to-break": "^2.0.0", "unified": "^11.0.0" } }, "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ=="], - "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], @@ -1921,8 +1655,6 @@ "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - "require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="], "resedit": ["resedit@1.7.2", "", { "dependencies": { "pe-library": "^0.4.1" } }, "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA=="], @@ -1945,25 +1677,17 @@ "roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="], - "robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="], - - "rollup": ["rollup@4.56.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.56.0", "@rollup/rollup-android-arm64": "4.56.0", "@rollup/rollup-darwin-arm64": "4.56.0", "@rollup/rollup-darwin-x64": "4.56.0", "@rollup/rollup-freebsd-arm64": "4.56.0", "@rollup/rollup-freebsd-x64": "4.56.0", "@rollup/rollup-linux-arm-gnueabihf": "4.56.0", "@rollup/rollup-linux-arm-musleabihf": "4.56.0", "@rollup/rollup-linux-arm64-gnu": "4.56.0", "@rollup/rollup-linux-arm64-musl": "4.56.0", "@rollup/rollup-linux-loong64-gnu": "4.56.0", "@rollup/rollup-linux-loong64-musl": "4.56.0", "@rollup/rollup-linux-ppc64-gnu": "4.56.0", "@rollup/rollup-linux-ppc64-musl": "4.56.0", "@rollup/rollup-linux-riscv64-gnu": "4.56.0", "@rollup/rollup-linux-riscv64-musl": "4.56.0", "@rollup/rollup-linux-s390x-gnu": "4.56.0", "@rollup/rollup-linux-x64-gnu": "4.56.0", "@rollup/rollup-linux-x64-musl": "4.56.0", "@rollup/rollup-openbsd-x64": "4.56.0", "@rollup/rollup-openharmony-arm64": "4.56.0", "@rollup/rollup-win32-arm64-msvc": "4.56.0", "@rollup/rollup-win32-ia32-msvc": "4.56.0", "@rollup/rollup-win32-x64-gnu": "4.56.0", "@rollup/rollup-win32-x64-msvc": "4.56.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg=="], - - "roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="], - - "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + "rollup": ["rollup@4.55.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.55.1", "@rollup/rollup-android-arm64": "4.55.1", "@rollup/rollup-darwin-arm64": "4.55.1", "@rollup/rollup-darwin-x64": "4.55.1", "@rollup/rollup-freebsd-arm64": "4.55.1", "@rollup/rollup-freebsd-x64": "4.55.1", "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", "@rollup/rollup-linux-arm-musleabihf": "4.55.1", "@rollup/rollup-linux-arm64-gnu": "4.55.1", "@rollup/rollup-linux-arm64-musl": "4.55.1", "@rollup/rollup-linux-loong64-gnu": "4.55.1", "@rollup/rollup-linux-loong64-musl": "4.55.1", "@rollup/rollup-linux-ppc64-gnu": "4.55.1", "@rollup/rollup-linux-ppc64-musl": "4.55.1", "@rollup/rollup-linux-riscv64-gnu": "4.55.1", "@rollup/rollup-linux-riscv64-musl": "4.55.1", "@rollup/rollup-linux-s390x-gnu": "4.55.1", "@rollup/rollup-linux-x64-gnu": "4.55.1", "@rollup/rollup-linux-x64-musl": "4.55.1", "@rollup/rollup-openbsd-x64": "4.55.1", "@rollup/rollup-openharmony-arm64": "4.55.1", "@rollup/rollup-win32-arm64-msvc": "4.55.1", "@rollup/rollup-win32-ia32-msvc": "4.55.1", "@rollup/rollup-win32-x64-gnu": "4.55.1", "@rollup/rollup-win32-x64-msvc": "4.55.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], - "rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="], - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], "sanitize-filename": ["sanitize-filename@1.6.3", "", { "dependencies": { "truncate-utf8-bytes": "^1.0.0" } }, "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg=="], - "sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="], + "sax": ["sax@1.4.3", "", {}, "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], @@ -1973,30 +1697,16 @@ "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], - "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], - "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], - "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], - "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], - "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], "shiki": ["shiki@1.29.2", "", { "dependencies": { "@shikijs/core": "1.29.2", "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/langs": "1.29.2", "@shikijs/themes": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg=="], - "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], - - "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], - - "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], - - "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], @@ -2031,10 +1741,6 @@ "stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="], - "state-local": ["state-local@1.0.7", "", {}, "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="], - - "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], - "streamdown": ["streamdown@2.1.0", "", { "dependencies": { "clsx": "^2.1.1", "hast-util-to-jsx-runtime": "^2.3.6", "html-url-attributes": "^3.0.1", "marked": "^17.0.1", "rehype-harden": "^1.1.7", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remend": "1.1.0", "tailwind-merge": "^3.4.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-u9gWd0AmjKg1d+74P44XaPlGrMeC21oDOSIhjGNEYMAttDMzCzlJO6lpTyJ9JkSinQQF65YcK4eOd3q9iTvULw=="], "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -2057,8 +1763,6 @@ "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], - "stylis": ["stylis@4.3.6", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="], - "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], "sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="], @@ -2103,8 +1807,6 @@ "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], @@ -2113,8 +1815,6 @@ "truncate-utf8-bytes": ["truncate-utf8-bytes@1.0.2", "", { "dependencies": { "utf8-byte-length": "^1.0.1" } }, "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ=="], - "ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="], - "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -2123,12 +1823,8 @@ "type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], - "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], - "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], @@ -2151,8 +1847,6 @@ "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], @@ -2167,10 +1861,6 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], - - "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - "verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="], "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], @@ -2181,18 +1871,6 @@ "vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], - "vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="], - - "vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="], - - "vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="], - - "vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="], - - "vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="], - - "vscode-uri": ["vscode-uri@3.0.8", "", {}, "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw=="], - "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], @@ -2231,9 +1909,7 @@ "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], - - "zustand": ["zustand@5.0.10", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg=="], + "zustand": ["zustand@5.0.9", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], @@ -2241,12 +1917,6 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@chevrotain/cst-dts-gen/lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], - - "@chevrotain/gast/lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], - - "@develar/schema-utils/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], - "@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], "@electron/asar/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], @@ -2277,26 +1947,8 @@ "@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], - "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], - "@opentelemetry/instrumentation-http/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], - "@opentelemetry/otlp-exporter-base/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], - - "@opentelemetry/otlp-transformer/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], - - "@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], - - "@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], - - "@opentelemetry/sdk-logs/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], - - "@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], - - "@opentelemetry/sdk-metrics/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], - - "@opentelemetry/sdk-metrics/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], - "@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], @@ -2323,15 +1975,13 @@ "@tailwindcss/typography/postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="], - "accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - - "ajv-keywords/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], - "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "archiver-utils/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], - "body-parser/iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + "c12/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "c12/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], "cacache/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="], @@ -2339,24 +1989,12 @@ "cacache/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], - "chevrotain/lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], - "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], "config-file-ts/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], - "cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="], - - "d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], - - "d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="], - - "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], - "dir-compare/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "dmg-license/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], - "electron-updater/builder-util-runtime": ["builder-util-runtime@9.5.1", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -2365,8 +2003,6 @@ "foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], "glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], @@ -2377,8 +2013,6 @@ "iconv-corefoundation/node-addon-api": ["node-addon-api@1.7.2", "", {}, "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="], - "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], - "lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], @@ -2393,7 +2027,7 @@ "make-fetch-happen/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], - "matcher/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], @@ -2409,17 +2043,15 @@ "minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], - "monaco-editor/dompurify": ["dompurify@3.2.7", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw=="], - - "monaco-editor/marked": ["marked@14.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ=="], - "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "path-scurry/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], - "raw-body/iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + "react-syntax-highlighter/highlight.js": ["highlight.js@10.7.3", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="], + + "react-syntax-highlighter/lowlight": ["lowlight@1.20.0", "", { "dependencies": { "fault": "^1.0.0", "highlight.js": "~10.7.0" } }, "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw=="], "readdir-glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], @@ -2429,8 +2061,6 @@ "ssri/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], - "streamdown/marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="], - "streamdown/tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], "tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], @@ -2439,8 +2069,6 @@ "zip-stream/archiver-utils": ["archiver-utils@3.0.4", "", { "dependencies": { "glob": "^7.2.3", "graceful-fs": "^4.2.0", "lazystream": "^1.0.0", "lodash.defaults": "^4.2.0", "lodash.difference": "^4.5.0", "lodash.flatten": "^4.4.0", "lodash.isplainobject": "^4.0.6", "lodash.union": "^4.6.0", "normalize-path": "^3.0.0", "readable-stream": "^3.6.0" } }, "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw=="], - "@develar/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - "@electron/asar/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], @@ -2509,28 +2137,20 @@ "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - "ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - "archiver-utils/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "archiver-utils/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "c12/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + "cacache/glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], "config-file-ts/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "config-file-ts/glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], - "cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="], - - "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], - "dir-compare/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - "dmg-license/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - - "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "gray-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], diff --git a/bun.lockb b/bun.lockb index 6601c57c..2f64528e 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/drizzle/0005_add_subchat_stats.sql b/drizzle/0005_add_subchat_stats.sql new file mode 100644 index 00000000..ee7b112f --- /dev/null +++ b/drizzle/0005_add_subchat_stats.sql @@ -0,0 +1,3 @@ +ALTER TABLE `sub_chats` ADD `additions` integer DEFAULT 0;--> statement-breakpoint +ALTER TABLE `sub_chats` ADD `deletions` integer DEFAULT 0;--> statement-breakpoint +ALTER TABLE `sub_chats` ADD `file_count` integer DEFAULT 0; diff --git a/drizzle/0005_marvelous_master_chief.sql b/drizzle/0005_marvelous_master_chief.sql deleted file mode 100644 index 45531cbf..00000000 --- a/drizzle/0005_marvelous_master_chief.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE INDEX `chats_worktree_path_idx` ON `chats` (`worktree_path`); diff --git a/drizzle/0006_anthropic_multi_account.sql b/drizzle/0006_anthropic_multi_account.sql deleted file mode 100644 index 0a7bd95d..00000000 --- a/drizzle/0006_anthropic_multi_account.sql +++ /dev/null @@ -1,33 +0,0 @@ --- Create anthropic_accounts table for multi-account support -CREATE TABLE IF NOT EXISTS `anthropic_accounts` ( - `id` text PRIMARY KEY NOT NULL, - `email` text, - `display_name` text, - `oauth_token` text NOT NULL, - `connected_at` integer, - `last_used_at` integer, - `desktop_user_id` text -); ---> statement-breakpoint --- Create anthropic_settings table to track active account -CREATE TABLE IF NOT EXISTS `anthropic_settings` ( - `id` text PRIMARY KEY DEFAULT 'singleton' NOT NULL, - `active_account_id` text, - `updated_at` integer -); ---> statement-breakpoint --- Migrate existing credential from claude_code_credentials to anthropic_accounts (skip if already migrated) -INSERT OR IGNORE INTO `anthropic_accounts` (`id`, `oauth_token`, `connected_at`, `desktop_user_id`, `display_name`) -SELECT - 'migrated-default', - `oauth_token`, - `connected_at`, - `user_id`, - 'Anthropic Account' -FROM `claude_code_credentials` -WHERE `id` = 'default' AND `oauth_token` IS NOT NULL; ---> statement-breakpoint --- Set migrated account as active (only if migration inserted a row and settings don't exist) -INSERT OR IGNORE INTO `anthropic_settings` (`id`, `active_account_id`, `updated_at`) -SELECT 'singleton', 'migrated-default', strftime('%s', 'now') * 1000 -WHERE EXISTS (SELECT 1 FROM `anthropic_accounts` WHERE `id` = 'migrated-default'); diff --git a/drizzle/0007_clammy_grim_reaper.sql b/drizzle/0007_clammy_grim_reaper.sql deleted file mode 100644 index 381e02c4..00000000 --- a/drizzle/0007_clammy_grim_reaper.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE `projects` ADD `icon_path` text; \ No newline at end of file diff --git a/drizzle/meta/0005_snapshot.json b/drizzle/meta/0005_snapshot.json index bb91ff88..66fadc7b 100644 --- a/drizzle/meta/0005_snapshot.json +++ b/drizzle/meta/0005_snapshot.json @@ -1,8 +1,8 @@ { "version": "6", "dialect": "sqlite", - "id": "0284f691-83e5-43d8-8e73-0918cc3435e8", - "prevId": "a5b2c3d4-e5f6-7890-abcd-ef1234567890", + "id": "a5b2c3d4-e5f6-7890-abcd-ef1234567890", + "prevId": "1c211023-1270-4cdd-934d-4396e72557e9", "tables": { "chats": { "name": "chats", @@ -85,15 +85,7 @@ "autoincrement": false } }, - "indexes": { - "chats_worktree_path_idx": { - "name": "chats_worktree_path_idx", - "columns": [ - "worktree_path" - ], - "isUnique": false - } - }, + "indexes": {}, "foreignKeys": { "chats_project_id_projects_id_fk": { "name": "chats_project_id_projects_id_fk", @@ -287,6 +279,30 @@ "autoincrement": false, "default": "'[]'" }, + "additions": { + "name": "additions", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "deletions": { + "name": "deletions", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "file_count": { + "name": "file_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, "created_at": { "name": "created_at", "type": "integer", @@ -333,4 +349,4 @@ "internal": { "indexes": {} } -} \ No newline at end of file +} diff --git a/drizzle/meta/0006_snapshot.json b/drizzle/meta/0006_snapshot.json deleted file mode 100644 index c11ebe3f..00000000 --- a/drizzle/meta/0006_snapshot.json +++ /dev/null @@ -1,427 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "b1c2d3e4-f5a6-7890-bcde-fa1234567890", - "prevId": "0284f691-83e5-43d8-8e73-0918cc3435e8", - "tables": { - "anthropic_accounts": { - "name": "anthropic_accounts", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "oauth_token": { - "name": "oauth_token", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "connected_at": { - "name": "connected_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "last_used_at": { - "name": "last_used_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "desktop_user_id": { - "name": "desktop_user_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "anthropic_settings": { - "name": "anthropic_settings", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false, - "default": "'singleton'" - }, - "active_account_id": { - "name": "active_account_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "chats": { - "name": "chats", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "archived_at": { - "name": "archived_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "worktree_path": { - "name": "worktree_path", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "branch": { - "name": "branch", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "base_branch": { - "name": "base_branch", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "pr_url": { - "name": "pr_url", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "pr_number": { - "name": "pr_number", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "chats_worktree_path_idx": { - "name": "chats_worktree_path_idx", - "columns": [ - "worktree_path" - ], - "isUnique": false - } - }, - "foreignKeys": { - "chats_project_id_projects_id_fk": { - "name": "chats_project_id_projects_id_fk", - "tableFrom": "chats", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "claude_code_credentials": { - "name": "claude_code_credentials", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false, - "default": "'default'" - }, - "oauth_token": { - "name": "oauth_token", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "connected_at": { - "name": "connected_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "projects": { - "name": "projects", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "path": { - "name": "path", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "git_remote_url": { - "name": "git_remote_url", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "git_provider": { - "name": "git_provider", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "git_owner": { - "name": "git_owner", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "git_repo": { - "name": "git_repo", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "projects_path_unique": { - "name": "projects_path_unique", - "columns": [ - "path" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "sub_chats": { - "name": "sub_chats", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "chat_id": { - "name": "chat_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "session_id": { - "name": "session_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "stream_id": { - "name": "stream_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "mode": { - "name": "mode", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'agent'" - }, - "messages": { - "name": "messages", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'[]'" - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "sub_chats_chat_id_chats_id_fk": { - "name": "sub_chats_chat_id_chats_id_fk", - "tableFrom": "sub_chats", - "tableTo": "chats", - "columnsFrom": [ - "chat_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} diff --git a/drizzle/meta/0007_snapshot.json b/drizzle/meta/0007_snapshot.json deleted file mode 100644 index a6aa7862..00000000 --- a/drizzle/meta/0007_snapshot.json +++ /dev/null @@ -1,434 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "b2d2d602-5de1-43b1-ada8-c9ed3edde22d", - "prevId": "b1c2d3e4-f5a6-7890-bcde-fa1234567890", - "tables": { - "anthropic_accounts": { - "name": "anthropic_accounts", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "oauth_token": { - "name": "oauth_token", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "connected_at": { - "name": "connected_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "last_used_at": { - "name": "last_used_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "desktop_user_id": { - "name": "desktop_user_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "anthropic_settings": { - "name": "anthropic_settings", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false, - "default": "'singleton'" - }, - "active_account_id": { - "name": "active_account_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "chats": { - "name": "chats", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "archived_at": { - "name": "archived_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "worktree_path": { - "name": "worktree_path", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "branch": { - "name": "branch", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "base_branch": { - "name": "base_branch", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "pr_url": { - "name": "pr_url", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "pr_number": { - "name": "pr_number", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "chats_worktree_path_idx": { - "name": "chats_worktree_path_idx", - "columns": [ - "worktree_path" - ], - "isUnique": false - } - }, - "foreignKeys": { - "chats_project_id_projects_id_fk": { - "name": "chats_project_id_projects_id_fk", - "tableFrom": "chats", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "claude_code_credentials": { - "name": "claude_code_credentials", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false, - "default": "'default'" - }, - "oauth_token": { - "name": "oauth_token", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "connected_at": { - "name": "connected_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "projects": { - "name": "projects", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "path": { - "name": "path", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "git_remote_url": { - "name": "git_remote_url", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "git_provider": { - "name": "git_provider", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "git_owner": { - "name": "git_owner", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "git_repo": { - "name": "git_repo", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "icon_path": { - "name": "icon_path", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "projects_path_unique": { - "name": "projects_path_unique", - "columns": [ - "path" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "sub_chats": { - "name": "sub_chats", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "chat_id": { - "name": "chat_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "session_id": { - "name": "session_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "stream_id": { - "name": "stream_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "mode": { - "name": "mode", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'agent'" - }, - "messages": { - "name": "messages", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'[]'" - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "sub_chats_chat_id_chats_id_fk": { - "name": "sub_chats_chat_id_chats_id_fk", - "tableFrom": "sub_chats", - "tableTo": "chats", - "columnsFrom": [ - "chat_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 88a3e0a6..ab91e108 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -36,27 +36,6 @@ "when": 1768199613729, "tag": "0004_melted_prism", "breakpoints": true - }, - { - "idx": 5, - "version": "6", - "when": 1769310092745, - "tag": "0005_marvelous_master_chief", - "breakpoints": true - }, - { - "idx": 6, - "version": "6", - "when": 1769480000000, - "tag": "0006_anthropic_multi_account", - "breakpoints": true - }, - { - "idx": 7, - "version": "6", - "when": 1769810815497, - "tag": "0007_clammy_grim_reaper", - "breakpoints": true } ] } \ No newline at end of file diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 3595f6c2..832050e5 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -4,14 +4,12 @@ import react from "@vitejs/plugin-react" import tailwindcss from "tailwindcss" import autoprefixer from "autoprefixer" -const isDev = process.env.NODE_ENV !== "production" - export default defineConfig({ main: { plugins: [ externalizeDepsPlugin({ // Don't externalize these - bundle them instead - exclude: ["superjson", "trpc-electron", "gray-matter", "async-mutex"], + exclude: ["superjson", "trpc-electron", "gray-matter"], }), ], build: { @@ -50,14 +48,7 @@ export default defineConfig({ }, }, renderer: { - plugins: [ - react({ - // In dev mode, use WDYR as JSX import source to track ALL component re-renders - jsxImportSource: isDev - ? "@welldone-software/why-did-you-render" - : undefined, - }), - ], + plugins: [react()], resolve: { alias: { "@": resolve(__dirname, "src/renderer"), diff --git a/openspec/AGENTS.md b/openspec/AGENTS.md deleted file mode 100644 index 6c1703ee..00000000 --- a/openspec/AGENTS.md +++ /dev/null @@ -1,456 +0,0 @@ -# OpenSpec Instructions - -Instructions for AI coding assistants using OpenSpec for spec-driven development. - -## TL;DR Quick Checklist - -- Search existing work: `openspec spec list --long`, `openspec list` (use `rg` only for full-text search) -- Decide scope: new capability vs modify existing capability -- Pick a unique `change-id`: kebab-case, verb-led (`add-`, `update-`, `remove-`, `refactor-`) -- Scaffold: `proposal.md`, `tasks.md`, `design.md` (only if needed), and delta specs per affected capability -- Write deltas: use `## ADDED|MODIFIED|REMOVED|RENAMED Requirements`; include at least one `#### Scenario:` per requirement -- Validate: `openspec validate [change-id] --strict --no-interactive` and fix issues -- Request approval: Do not start implementation until proposal is approved - -## Three-Stage Workflow - -### Stage 1: Creating Changes -Create proposal when you need to: -- Add features or functionality -- Make breaking changes (API, schema) -- Change architecture or patterns -- Optimize performance (changes behavior) -- Update security patterns - -Triggers (examples): -- "Help me create a change proposal" -- "Help me plan a change" -- "Help me create a proposal" -- "I want to create a spec proposal" -- "I want to create a spec" - -Loose matching guidance: -- Contains one of: `proposal`, `change`, `spec` -- With one of: `create`, `plan`, `make`, `start`, `help` - -Skip proposal for: -- Bug fixes (restore intended behavior) -- Typos, formatting, comments -- Dependency updates (non-breaking) -- Configuration changes -- Tests for existing behavior - -**Workflow** -1. Review `openspec/project.md`, `openspec list`, and `openspec list --specs` to understand current context. -2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, optional `design.md`, and spec deltas under `openspec/changes//`. -3. Draft spec deltas using `## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement. -4. Run `openspec validate --strict --no-interactive` and resolve any issues before sharing the proposal. - -### Stage 2: Implementing Changes -Track these steps as TODOs and complete them one by one. -1. **Read proposal.md** - Understand what's being built -2. **Read design.md** (if exists) - Review technical decisions -3. **Read tasks.md** - Get implementation checklist -4. **Implement tasks sequentially** - Complete in order -5. **Confirm completion** - Ensure every item in `tasks.md` is finished before updating statuses -6. **Update checklist** - After all work is done, set every task to `- [x]` so the list reflects reality -7. **Approval gate** - Do not start implementation until the proposal is reviewed and approved - -### Stage 3: Archiving Changes -After deployment, create separate PR to: -- Move `changes/[name]/` → `changes/archive/YYYY-MM-DD-[name]/` -- Update `specs/` if capabilities changed -- Use `openspec archive --skip-specs --yes` for tooling-only changes (always pass the change ID explicitly) -- Run `openspec validate --strict --no-interactive` to confirm the archived change passes checks - -## Before Any Task - -**Context Checklist:** -- [ ] Read relevant specs in `specs/[capability]/spec.md` -- [ ] Check pending changes in `changes/` for conflicts -- [ ] Read `openspec/project.md` for conventions -- [ ] Run `openspec list` to see active changes -- [ ] Run `openspec list --specs` to see existing capabilities - -**Before Creating Specs:** -- Always check if capability already exists -- Prefer modifying existing specs over creating duplicates -- Use `openspec show [spec]` to review current state -- If request is ambiguous, ask 1–2 clarifying questions before scaffolding - -### Search Guidance -- Enumerate specs: `openspec spec list --long` (or `--json` for scripts) -- Enumerate changes: `openspec list` (or `openspec change list --json` - deprecated but available) -- Show details: - - Spec: `openspec show --type spec` (use `--json` for filters) - - Change: `openspec show --json --deltas-only` -- Full-text search (use ripgrep): `rg -n "Requirement:|Scenario:" openspec/specs` - -## Quick Start - -### CLI Commands - -```bash -# Essential commands -openspec list # List active changes -openspec list --specs # List specifications -openspec show [item] # Display change or spec -openspec validate [item] # Validate changes or specs -openspec archive [--yes|-y] # Archive after deployment (add --yes for non-interactive runs) - -# Project management -openspec init [path] # Initialize OpenSpec -openspec update [path] # Update instruction files - -# Interactive mode -openspec show # Prompts for selection -openspec validate # Bulk validation mode - -# Debugging -openspec show [change] --json --deltas-only -openspec validate [change] --strict --no-interactive -``` - -### Command Flags - -- `--json` - Machine-readable output -- `--type change|spec` - Disambiguate items -- `--strict` - Comprehensive validation -- `--no-interactive` - Disable prompts -- `--skip-specs` - Archive without spec updates -- `--yes`/`-y` - Skip confirmation prompts (non-interactive archive) - -## Directory Structure - -``` -openspec/ -├── project.md # Project conventions -├── specs/ # Current truth - what IS built -│ └── [capability]/ # Single focused capability -│ ├── spec.md # Requirements and scenarios -│ └── design.md # Technical patterns -├── changes/ # Proposals - what SHOULD change -│ ├── [change-name]/ -│ │ ├── proposal.md # Why, what, impact -│ │ ├── tasks.md # Implementation checklist -│ │ ├── design.md # Technical decisions (optional; see criteria) -│ │ └── specs/ # Delta changes -│ │ └── [capability]/ -│ │ └── spec.md # ADDED/MODIFIED/REMOVED -│ └── archive/ # Completed changes -``` - -## Creating Change Proposals - -### Decision Tree - -``` -New request? -├─ Bug fix restoring spec behavior? → Fix directly -├─ Typo/format/comment? → Fix directly -├─ New feature/capability? → Create proposal -├─ Breaking change? → Create proposal -├─ Architecture change? → Create proposal -└─ Unclear? → Create proposal (safer) -``` - -### Proposal Structure - -1. **Create directory:** `changes/[change-id]/` (kebab-case, verb-led, unique) - -2. **Write proposal.md:** -```markdown -# Change: [Brief description of change] - -## Why -[1-2 sentences on problem/opportunity] - -## What Changes -- [Bullet list of changes] -- [Mark breaking changes with **BREAKING**] - -## Impact -- Affected specs: [list capabilities] -- Affected code: [key files/systems] -``` - -3. **Create spec deltas:** `specs/[capability]/spec.md` -```markdown -## ADDED Requirements -### Requirement: New Feature -The system SHALL provide... - -#### Scenario: Success case -- **WHEN** user performs action -- **THEN** expected result - -## MODIFIED Requirements -### Requirement: Existing Feature -[Complete modified requirement] - -## REMOVED Requirements -### Requirement: Old Feature -**Reason**: [Why removing] -**Migration**: [How to handle] -``` -If multiple capabilities are affected, create multiple delta files under `changes/[change-id]/specs//spec.md`—one per capability. - -4. **Create tasks.md:** -```markdown -## 1. Implementation -- [ ] 1.1 Create database schema -- [ ] 1.2 Implement API endpoint -- [ ] 1.3 Add frontend component -- [ ] 1.4 Write tests -``` - -5. **Create design.md when needed:** -Create `design.md` if any of the following apply; otherwise omit it: -- Cross-cutting change (multiple services/modules) or a new architectural pattern -- New external dependency or significant data model changes -- Security, performance, or migration complexity -- Ambiguity that benefits from technical decisions before coding - -Minimal `design.md` skeleton: -```markdown -## Context -[Background, constraints, stakeholders] - -## Goals / Non-Goals -- Goals: [...] -- Non-Goals: [...] - -## Decisions -- Decision: [What and why] -- Alternatives considered: [Options + rationale] - -## Risks / Trade-offs -- [Risk] → Mitigation - -## Migration Plan -[Steps, rollback] - -## Open Questions -- [...] -``` - -## Spec File Format - -### Critical: Scenario Formatting - -**CORRECT** (use #### headers): -```markdown -#### Scenario: User login success -- **WHEN** valid credentials provided -- **THEN** return JWT token -``` - -**WRONG** (don't use bullets or bold): -```markdown -- **Scenario: User login** ❌ -**Scenario**: User login ❌ -### Scenario: User login ❌ -``` - -Every requirement MUST have at least one scenario. - -### Requirement Wording -- Use SHALL/MUST for normative requirements (avoid should/may unless intentionally non-normative) - -### Delta Operations - -- `## ADDED Requirements` - New capabilities -- `## MODIFIED Requirements` - Changed behavior -- `## REMOVED Requirements` - Deprecated features -- `## RENAMED Requirements` - Name changes - -Headers matched with `trim(header)` - whitespace ignored. - -#### When to use ADDED vs MODIFIED -- ADDED: Introduces a new capability or sub-capability that can stand alone as a requirement. Prefer ADDED when the change is orthogonal (e.g., adding "Slash Command Configuration") rather than altering the semantics of an existing requirement. -- MODIFIED: Changes the behavior, scope, or acceptance criteria of an existing requirement. Always paste the full, updated requirement content (header + all scenarios). The archiver will replace the entire requirement with what you provide here; partial deltas will drop previous details. -- RENAMED: Use when only the name changes. If you also change behavior, use RENAMED (name) plus MODIFIED (content) referencing the new name. - -Common pitfall: Using MODIFIED to add a new concern without including the previous text. This causes loss of detail at archive time. If you aren’t explicitly changing the existing requirement, add a new requirement under ADDED instead. - -Authoring a MODIFIED requirement correctly: -1) Locate the existing requirement in `openspec/specs//spec.md`. -2) Copy the entire requirement block (from `### Requirement: ...` through its scenarios). -3) Paste it under `## MODIFIED Requirements` and edit to reflect the new behavior. -4) Ensure the header text matches exactly (whitespace-insensitive) and keep at least one `#### Scenario:`. - -Example for RENAMED: -```markdown -## RENAMED Requirements -- FROM: `### Requirement: Login` -- TO: `### Requirement: User Authentication` -``` - -## Troubleshooting - -### Common Errors - -**"Change must have at least one delta"** -- Check `changes/[name]/specs/` exists with .md files -- Verify files have operation prefixes (## ADDED Requirements) - -**"Requirement must have at least one scenario"** -- Check scenarios use `#### Scenario:` format (4 hashtags) -- Don't use bullet points or bold for scenario headers - -**Silent scenario parsing failures** -- Exact format required: `#### Scenario: Name` -- Debug with: `openspec show [change] --json --deltas-only` - -### Validation Tips - -```bash -# Always use strict mode for comprehensive checks -openspec validate [change] --strict --no-interactive - -# Debug delta parsing -openspec show [change] --json | jq '.deltas' - -# Check specific requirement -openspec show [spec] --json -r 1 -``` - -## Happy Path Script - -```bash -# 1) Explore current state -openspec spec list --long -openspec list -# Optional full-text search: -# rg -n "Requirement:|Scenario:" openspec/specs -# rg -n "^#|Requirement:" openspec/changes - -# 2) Choose change id and scaffold -CHANGE=add-two-factor-auth -mkdir -p openspec/changes/$CHANGE/{specs/auth} -printf "## Why\n...\n\n## What Changes\n- ...\n\n## Impact\n- ...\n" > openspec/changes/$CHANGE/proposal.md -printf "## 1. Implementation\n- [ ] 1.1 ...\n" > openspec/changes/$CHANGE/tasks.md - -# 3) Add deltas (example) -cat > openspec/changes/$CHANGE/specs/auth/spec.md << 'EOF' -## ADDED Requirements -### Requirement: Two-Factor Authentication -Users MUST provide a second factor during login. - -#### Scenario: OTP required -- **WHEN** valid credentials are provided -- **THEN** an OTP challenge is required -EOF - -# 4) Validate -openspec validate $CHANGE --strict --no-interactive -``` - -## Multi-Capability Example - -``` -openspec/changes/add-2fa-notify/ -├── proposal.md -├── tasks.md -└── specs/ - ├── auth/ - │ └── spec.md # ADDED: Two-Factor Authentication - └── notifications/ - └── spec.md # ADDED: OTP email notification -``` - -auth/spec.md -```markdown -## ADDED Requirements -### Requirement: Two-Factor Authentication -... -``` - -notifications/spec.md -```markdown -## ADDED Requirements -### Requirement: OTP Email Notification -... -``` - -## Best Practices - -### Simplicity First -- Default to <100 lines of new code -- Single-file implementations until proven insufficient -- Avoid frameworks without clear justification -- Choose boring, proven patterns - -### Complexity Triggers -Only add complexity with: -- Performance data showing current solution too slow -- Concrete scale requirements (>1000 users, >100MB data) -- Multiple proven use cases requiring abstraction - -### Clear References -- Use `file.ts:42` format for code locations -- Reference specs as `specs/auth/spec.md` -- Link related changes and PRs - -### Capability Naming -- Use verb-noun: `user-auth`, `payment-capture` -- Single purpose per capability -- 10-minute understandability rule -- Split if description needs "AND" - -### Change ID Naming -- Use kebab-case, short and descriptive: `add-two-factor-auth` -- Prefer verb-led prefixes: `add-`, `update-`, `remove-`, `refactor-` -- Ensure uniqueness; if taken, append `-2`, `-3`, etc. - -## Tool Selection Guide - -| Task | Tool | Why | -|------|------|-----| -| Find files by pattern | Glob | Fast pattern matching | -| Search code content | Grep | Optimized regex search | -| Read specific files | Read | Direct file access | -| Explore unknown scope | Task | Multi-step investigation | - -## Error Recovery - -### Change Conflicts -1. Run `openspec list` to see active changes -2. Check for overlapping specs -3. Coordinate with change owners -4. Consider combining proposals - -### Validation Failures -1. Run with `--strict` flag -2. Check JSON output for details -3. Verify spec file format -4. Ensure scenarios properly formatted - -### Missing Context -1. Read project.md first -2. Check related specs -3. Review recent archives -4. Ask for clarification - -## Quick Reference - -### Stage Indicators -- `changes/` - Proposed, not yet built -- `specs/` - Built and deployed -- `archive/` - Completed changes - -### File Purposes -- `proposal.md` - Why and what -- `tasks.md` - Implementation steps -- `design.md` - Technical decisions -- `spec.md` - Requirements and behavior - -### CLI Essentials -```bash -openspec list # What's in progress? -openspec show [item] # View details -openspec validate --strict --no-interactive # Is it correct? -openspec archive [--yes|-y] # Mark complete (add --yes for automation) -``` - -Remember: Specs are truth. Changes are proposals. Keep them in sync. diff --git a/openspec/project.md b/openspec/project.md deleted file mode 100644 index 9fa38c62..00000000 --- a/openspec/project.md +++ /dev/null @@ -1,58 +0,0 @@ -# Project Context - -## Purpose -**21st Agents** - A local-first Electron desktop app for AI-powered code assistance. Users create chat sessions linked to local project folders, interact with Claude in Plan or Agent mode, and see real-time tool execution (bash, file edits, web search, etc.). - -## Tech Stack -| Layer | Tech | -|-------|------| -| Desktop | Electron 33.4.5, electron-vite, electron-builder | -| UI | React 19, TypeScript 5.4.5, Tailwind CSS | -| Components | Radix UI, Lucide icons, Motion, Sonner | -| State | Jotai, Zustand, React Query | -| Backend | tRPC, Drizzle ORM, better-sqlite3 | -| AI | @anthropic-ai/claude-code | -| Package Manager | bun | - -## Project Conventions - -### Code Style -- Components: PascalCase (`ActiveChat.tsx`, `AgentsSidebar.tsx`) -- Utilities/hooks: camelCase (`useFileUpload.ts`, `formatters.ts`) -- Stores: kebab-case (`sub-chat-store.ts`, `agent-chat-store.ts`) -- Atoms: camelCase with `Atom` suffix (`selectedAgentChatIdAtom`) -- Simplicity over complexity - don't overcomplicate things - -### Architecture Patterns -- **IPC Communication**: tRPC with `trpc-electron` for type-safe main↔renderer communication -- **State Management**: - - Jotai: UI state (selected chat, sidebar open, preview settings) - - Zustand: Sub-chat tabs and pinned state (persisted to localStorage) - - React Query: Server state via tRPC (auto-caching, refetch) -- **Database**: Drizzle ORM with SQLite, auto-migration on app startup -- **Claude Integration**: Dynamic import of `@anthropic-ai/claude-code` SDK with two modes: "plan" (read-only) and "agent" (full permissions) - -### Testing Strategy -[Testing approach not yet established - to be defined] - -### Git Workflow -- Main branch: `main` -- Feature branches for development -- PRs for code review - -## Domain Context -- **Chat Sessions**: Users create chats linked to local project folders -- **Sub-chats**: Sessions within a chat that can have different modes (plan/agent) -- **Tool Execution**: Real-time display of Claude's tool execution (bash, file edits, web search) -- **Session Resume**: Sessions can be resumed via `sessionId` stored in SubChat - -## Important Constraints -- Local-first: All data stored locally in SQLite (`{userData}/data/agents.db`) -- Auth via OAuth with encrypted credential storage (safeStorage) -- macOS notarization required for releases -- Dev vs Production use separate userData paths and protocols - -## External Dependencies -- **Claude Code SDK**: `@anthropic-ai/claude-code` for AI interactions -- **21st.dev CDN**: Auto-update manifests and releases at `cdn.21st.dev` -- **OAuth Provider**: Authentication flow diff --git a/package.json b/package.json index 48e649c6..672d2fb5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "21st-desktop", - "version": "0.0.54", + "version": "0.0.29", "private": true, "description": "1Code - UI for parallel work with AI agents", "author": { @@ -19,10 +19,9 @@ "dist": "electron-builder", "dist:manifest": "node scripts/generate-update-manifest.mjs", "dist:upload": "node scripts/upload-release.mjs", - "claude:download": "node scripts/download-claude-binary.mjs --version=2.1.25", - "claude:download:all": "node scripts/download-claude-binary.mjs --version=2.1.25 --all", - "release": "rm -rf release && bun i && bun run claude:download && bun run build && bun run package:mac && bun run dist:manifest && ./scripts/upload-release-wrangler.sh", - "release:dev": "rm -rf release && bun run claude:download && bun run build && bun run package:mac && rm -rf node_modules && bun i", + "claude:download": "node scripts/download-claude-binary.mjs", + "claude:download:all": "node scripts/download-claude-binary.mjs --all", + "release": "rm -rf release && bun run claude:download && bun run build && bun run package:mac && bun run dist:manifest && ./scripts/upload-release-wrangler.sh", "sync:public": "./scripts/sync-to-public.sh", "icon:generate": "node scripts/generate-icon.mjs", "db:generate": "drizzle-kit generate", @@ -33,20 +32,17 @@ }, "dependencies": { "@ai-sdk/react": "^3.0.14", - "@anthropic-ai/claude-agent-sdk": "0.2.25", + "@anthropic-ai/claude-agent-sdk": "^0.2.12", "@git-diff-view/react": "^0.0.35", "@git-diff-view/shiki": "^0.0.36", - "@modelcontextprotocol/sdk": "^1.25.3", - "@monaco-editor/react": "^4.7.0", - "@pierre/diffs": "^1.0.10", - "@radix-ui/react-accordion": "^1.2.12", - "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-accordion": "^1.2.11", + "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-collapsible": "^1.1.12", "@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-hover-card": "^1.1.15", + "@radix-ui/react-hover-card": "^1.1.14", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-popover": "^1.1.15", @@ -70,22 +66,17 @@ "@xterm/addon-web-links": "^0.12.0", "@xterm/addon-webgl": "^0.19.0", "ai": "^6.0.14", - "async-mutex": "^0.5.0", - "better-sqlite3": "^12.6.2", + "better-sqlite3": "^11.8.1", "chokidar": "^5.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^3.6.0", - "diff": "^8.0.3", "drizzle-orm": "^0.45.1", "electron-log": "^5.4.3", "electron-updater": "^6.7.3", "gray-matter": "^4.0.3", "jotai": "^2.11.1", - "jsonc-parser": "^3.3.1", "lucide-react": "^0.468.0", - "mermaid": "^11.12.2", - "monaco-editor": "^0.55.1", "motion": "^11.15.0", "next-themes": "^0.4.4", "node-pty": "^1.1.0", @@ -96,9 +87,8 @@ "react-dom": "19.2.1", "react-hotkeys-hook": "^4.6.1", "react-icons": "^5.5.0", - "react-zoom-pan-pinch": "^3.7.0", + "react-syntax-highlighter": "^16.1.0", "remark-breaks": "^4.0.0", - "remark-gfm": "^4.0.1", "shiki": "^1.24.4", "simple-git": "^3.28.0", "sonner": "^1.7.1", @@ -116,17 +106,16 @@ "@electron-toolkit/preload": "^3.0.1", "@electron-toolkit/utils": "^4.0.0", "@types/better-sqlite3": "^7.6.13", - "@types/diff": "^8.0.0", "@types/node": "^20.17.50", "@types/react": "^19.0.7", "@types/react-dom": "^19.0.3", + "@types/react-syntax-highlighter": "^15.5.13", "@vitejs/plugin-react": "^4.3.4", - "@welldone-software/why-did-you-render": "^10.0.1", "autoprefixer": "^10.4.20", "drizzle-kit": "^0.31.8", - "electron": "~39.4.0", + "electron": "33.4.5", "electron-builder": "^25.1.8", - "@electron/rebuild": "^4.0.3", + "electron-rebuild": "^3.2.9", "electron-vite": "^3.0.0", "postcss": "^8.5.1", "tailwindcss": "^3.4.17", @@ -197,10 +186,7 @@ "hardenedRuntime": true, "gatekeeperAssess": false, "entitlements": "build/entitlements.mac.plist", - "entitlementsInherit": "build/entitlements.mac.plist", - "extendInfo": { - "NSMicrophoneUsageDescription": "1Code needs microphone access for voice dictation" - } + "entitlementsInherit": "build/entitlements.mac.plist" }, "dmg": { "window": { @@ -247,10 +233,5 @@ "provider": "generic", "url": "https://cdn.21st.dev/releases/desktop" } - }, - "pnpm": { - "overrides": { - "source-map-support>source-map": "0.7.4" - } } } diff --git a/resources/cli/1code b/resources/cli/1code deleted file mode 100755 index ef4898e2..00000000 --- a/resources/cli/1code +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash -# 1code CLI launcher -# Opens 1Code app with the specified directory - -# Resolve the directory argument (default to current directory) -DIR="${1:-.}" - -# Convert to absolute path -if [[ "$DIR" != /* ]]; then - DIR="$(cd "$DIR" 2>/dev/null && pwd)" -fi - -if [ -z "$DIR" ] || [ ! -d "$DIR" ]; then - echo "Error: Invalid directory" - exit 1 -fi - -# Open 1Code app with the directory argument -open -a "1Code" --args "$DIR" diff --git a/scripts/download-claude-binary.mjs b/scripts/download-claude-binary.mjs index d2fbb283..c6ba1752 100644 --- a/scripts/download-claude-binary.mjs +++ b/scripts/download-claude-binary.mjs @@ -127,24 +127,27 @@ function calculateSha256(filePath) { } /** - * Get latest version from GCS bucket + * Get latest version from manifest */ async function getLatestVersion() { + // Try to fetch version list or use known latest + // For now, we'll fetch the manifest for a known version console.log("Fetching latest Claude Code version...") try { - // Fetch from the same endpoint that install.sh uses - const response = await fetch("https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/latest") - if (response.ok) { - const version = await response.text() - return version.trim() + // The install script endpoint returns version info + const response = await fetch("https://claude.ai/install.sh") + const script = await response.text() + const versionMatch = script.match(/CLAUDE_CODE_VERSION="([^"]+)"/) + if (versionMatch) { + return versionMatch[1] } - } catch (error) { - console.warn(`Failed to fetch latest version: ${error.message}`) + } catch { + // Fallback } - // Fallback to known version (should be updated periodically) - return "2.1.17" + // Fallback to known version + return "2.1.5" } /** diff --git a/scripts/generate-update-manifest.mjs b/scripts/generate-update-manifest.mjs index 386d9373..764f7e77 100644 --- a/scripts/generate-update-manifest.mjs +++ b/scripts/generate-update-manifest.mjs @@ -24,17 +24,6 @@ import { fileURLToPath } from "url" const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) -// Parse --channel argument (default: "latest") -const channelArgIndex = process.argv.indexOf("--channel") -const channel = channelArgIndex !== -1 && process.argv[channelArgIndex + 1] - ? process.argv[channelArgIndex + 1] - : "latest" - -if (channel !== "latest" && channel !== "beta") { - console.error(`Invalid channel: "${channel}". Must be "latest" or "beta".`) - process.exit(1) -} - // Get version from package.json const packageJson = JSON.parse( readFileSync(join(__dirname, "../package.json"), "utf-8") @@ -108,11 +97,10 @@ function generateManifest(arch) { } // Manifest file names expected by electron-updater: - // For stable (latest): latest-mac.yml / latest-mac-x64.yml - // For beta: beta-mac.yml / beta-mac-x64.yml - const prefix = channel === "beta" ? "beta" : "latest" + // arm64: latest-mac.yml (primary) + // x64: latest-mac-x64.yml const manifestFileName = - arch === "arm64" ? `${prefix}-mac.yml` : `${prefix}-mac-x64.yml` + arch === "arm64" ? "latest-mac.yml" : "latest-mac-x64.yml" const manifestPath = join(releaseDir, manifestFileName) // Convert to YAML format (simple implementation) @@ -179,7 +167,6 @@ console.log("=".repeat(50)) console.log("Generating electron-updater manifests") console.log("=".repeat(50)) console.log(`Version: ${version}`) -console.log(`Channel: ${channel}`) console.log(`Release dir: ${releaseDir}`) console.log() @@ -195,16 +182,15 @@ if (!arm64Manifest && !x64Manifest) { console.log("=".repeat(50)) console.log("Manifest generation complete!") console.log() -const prefix = channel === "beta" ? "beta" : "latest" console.log("Next steps:") console.log("1. Upload the following files to cdn.21st.dev/releases/desktop/:") if (arm64Manifest) { - console.log(` - ${prefix}-mac.yml`) + console.log(` - latest-mac.yml`) console.log(` - Agents-${version}-arm64-mac.zip`) console.log(` - Agents-${version}-arm64.dmg (for manual download)`) } if (x64Manifest) { - console.log(` - ${prefix}-mac-x64.yml`) + console.log(` - latest-mac-x64.yml`) console.log(` - Agents-${version}-mac.zip`) console.log(` - Agents-${version}.dmg (for manual download)`) } diff --git a/scripts/sync-to-public.sh b/scripts/sync-to-public.sh index 9acfa639..4c58f108 100755 --- a/scripts/sync-to-public.sh +++ b/scripts/sync-to-public.sh @@ -78,9 +78,6 @@ test-electron.js # Exclude internal release docs (contains credentials, CDN URLs) RELEASE.md scripts/upload-release-wrangler.sh - -# Exclude wrangler local state (large R2 blobs) -.wrangler EOF # Commit and push diff --git a/src/main/auth-manager.ts b/src/main/auth-manager.ts index e31b7bc1..82533fe6 100644 --- a/src/main/auth-manager.ts +++ b/src/main/auth-manager.ts @@ -1,6 +1,5 @@ import { AuthStore, AuthData, AuthUser } from "./auth-store" import { app, BrowserWindow } from "electron" -import { AUTH_SERVER_PORT } from "./constants" // Get API URL - in packaged app always use production, in dev allow override function getApiBaseUrl(): string { @@ -211,10 +210,10 @@ export class AuthManager { let authUrl = `${this.getApiUrl()}/auth/desktop?auto=true` - // In dev mode, use localhost callback (we run HTTP server on AUTH_SERVER_PORT) + // In dev mode, use localhost callback (we run HTTP server on port 21321) // Also pass the protocol so web knows which deep link to use as fallback if (this.isDev) { - authUrl += `&callback=${encodeURIComponent(`http://localhost:${AUTH_SERVER_PORT}/auth/callback`)}` + authUrl += `&callback=${encodeURIComponent("http://localhost:21321/auth/callback")}` // Pass dev protocol so production web can use correct deep link if callback fails authUrl += `&protocol=twentyfirst-agents-dev` } @@ -251,51 +250,4 @@ export class AuthManager { // Update locally return this.store.updateUser({ name: updates.name ?? null }) } - - /** - * Fetch user's subscription plan from web backend - * Used for PostHog analytics enrichment - */ - async fetchUserPlan(): Promise<{ email: string; plan: string; status: string | null } | null> { - const token = await this.getValidToken() - if (!token) return null - - try { - const response = await fetch(`${this.getApiUrl()}/api/desktop/user/plan`, { - headers: { "X-Desktop-Token": token }, - }) - - if (!response.ok) { - console.error("[AuthManager] Failed to fetch user plan:", response.status) - return null - } - - return response.json() - } catch (error) { - console.error("[AuthManager] Failed to fetch user plan:", error) - return null - } - } -} - -// Global singleton instance -let authManagerInstance: AuthManager | null = null - -/** - * Initialize the global auth manager instance - * Must be called once from main process initialization - */ -export function initAuthManager(isDev: boolean = false): AuthManager { - if (!authManagerInstance) { - authManagerInstance = new AuthManager(isDev) - } - return authManagerInstance -} - -/** - * Get the global auth manager instance - * Returns null if not initialized - */ -export function getAuthManager(): AuthManager | null { - return authManagerInstance } diff --git a/src/main/constants.ts b/src/main/constants.ts deleted file mode 100644 index 088f2435..00000000 --- a/src/main/constants.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Dev mode detection -export const IS_DEV = !!process.env.ELECTRON_RENDERER_URL - -// Auth server port - use different port in dev to allow running alongside production -export const AUTH_SERVER_PORT = IS_DEV ? 21322 : 21321 diff --git a/src/main/index.ts b/src/main/index.ts index 32bf51eb..74ed6b77 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,42 +1,28 @@ -import * as Sentry from "@sentry/electron/main" -import { app, BrowserWindow, Menu, session } from "electron" -import { existsSync, readFileSync, readlinkSync, unlinkSync } from "fs" -import { createServer } from "http" +import { app, BrowserWindow, session, Menu } from "electron" import { join } from "path" -import { AuthManager, initAuthManager, getAuthManager as getAuthManagerFromModule } from "./auth-manager" +import { createServer } from "http" +import { readFileSync, existsSync, unlinkSync, readlinkSync } from "fs" +import * as Sentry from "@sentry/electron/main" +import { initDatabase, closeDatabase } from "./lib/db" +import { createMainWindow, getWindow, showLoginPage } from "./windows/main" +import { AuthManager } from "./auth-manager" import { - identify, initAnalytics, - setSubscriptionPlan, - shutdown as shutdownAnalytics, + identify, trackAppOpened, trackAuthCompleted, + shutdown as shutdownAnalytics, } from "./lib/analytics" import { + initAutoUpdater, checkForUpdates, downloadUpdate, - initAutoUpdater, setupFocusUpdateCheck, } from "./lib/auto-updater" -import { closeDatabase, initDatabase } from "./lib/db" -import { - getLaunchDirectory, - isCliInstalled, - installCli, - uninstallCli, - parseLaunchDirectory, -} from "./lib/cli" import { cleanupGitWatchers } from "./lib/git/watcher" -import { cancelAllPendingOAuth, handleMcpOAuthCallback } from "./lib/mcp-auth" -import { - createMainWindow, - createWindow, - getWindow, - getAllWindows, -} from "./windows/main" -import { windowManager } from "./windows/window-manager" -import { IS_DEV, AUTH_SERVER_PORT } from "./constants" +// Dev mode detection +const IS_DEV = !!process.env.ELECTRON_RENDERER_URL // Deep link protocol (must match package.json build.protocols.schemes) // Use different protocol in dev to avoid conflicts with production app @@ -84,12 +70,11 @@ export function getAppUrl(): string { return process.env.ELECTRON_RENDERER_URL || "https://21st.dev/agents" } -// Auth manager singleton (use the one from auth-manager module) +// Auth manager singleton let authManager: AuthManager export function getAuthManager(): AuthManager { - // First try to get from module, fallback to local variable for backwards compat - return getAuthManagerFromModule() || authManager + return authManager } // Handle auth code from deep link (exported for IPC handlers) @@ -103,16 +88,6 @@ export async function handleAuthCode(code: string): Promise { // Track successful authentication trackAuthCompleted(authData.user.id, authData.user.email) - // Fetch and set subscription plan for analytics - try { - const planData = await authManager.fetchUserPlan() - if (planData) { - setSubscriptionPlan(planData.plan) - } - } catch (e) { - console.warn("[Auth] Failed to fetch user plan for analytics:", e) - } - // Set desktop token cookie using persist:main partition const ses = session.fromPartition("persist:main") try { @@ -135,46 +110,20 @@ export async function handleAuthCode(code: string): Promise { console.warn("[Auth] Cookie set failed (non-critical):", cookieError) } - // Notify all windows and reload them to show app - const windows = getAllWindows() - for (const win of windows) { - try { - if (win.isDestroyed()) continue - win.webContents.send("auth:success", authData.user) - - // Use stable window ID (main, window-2, etc.) instead of Electron's numeric ID - const stableId = windowManager.getStableId(win) - - if (process.env.ELECTRON_RENDERER_URL) { - // Pass window ID via query param for dev mode - const url = new URL(process.env.ELECTRON_RENDERER_URL) - url.searchParams.set("windowId", stableId) - win.loadURL(url.toString()) - } else { - // Pass window ID via hash for production - win.loadFile(join(__dirname, "../renderer/index.html"), { - hash: `windowId=${stableId}`, - }) - } - } catch (error) { - // Window may have been destroyed during iteration - console.warn("[Auth] Failed to reload window:", error) - } + // Notify renderer + const win = getWindow() + win?.webContents.send("auth:success", authData.user) + + // Reload window to show app + if (process.env.ELECTRON_RENDERER_URL) { + win?.loadURL(process.env.ELECTRON_RENDERER_URL) + } else { + win?.loadFile(join(__dirname, "../renderer/index.html")) } - // Focus the first window - windows[0]?.focus() + win?.focus() } catch (error) { console.error("[Auth] Exchange failed:", error) - // Broadcast auth error to all windows (not just focused) - for (const win of getAllWindows()) { - try { - if (!win.isDestroyed()) { - win.webContents.send("auth:error", (error as Error).message) - } - } catch { - // Window destroyed during iteration - } - } + getWindow()?.webContents.send("auth:error", (error as Error).message) } } @@ -185,7 +134,7 @@ function handleDeepLink(url: string): void { try { const parsed = new URL(url) - // Handle auth callback: twentyfirst-agents://auth?code=xxx + // Handle auth callback: twentyfirstdev://auth?code=xxx if (parsed.pathname === "/auth" || parsed.host === "auth") { const code = parsed.searchParams.get("code") if (code) { @@ -193,16 +142,6 @@ function handleDeepLink(url: string): void { return } } - - // Handle MCP OAuth callback: twentyfirst-agents://mcp-oauth?code=xxx&state=yyy - if (parsed.pathname === "/mcp-oauth" || parsed.host === "mcp-oauth") { - const code = parsed.searchParams.get("code") - const state = parsed.searchParams.get("state") - if (code && state) { - handleMcpOAuthCallback(code, state) - return - } - } } catch (e) { console.error("[DeepLink] Failed to parse:", e) } @@ -280,10 +219,11 @@ console.log("[Protocol] =============================================") const FAVICON_SVG = `` const FAVICON_DATA_URI = `data:image/svg+xml,${encodeURIComponent(FAVICON_SVG)}` -// Start local HTTP server for auth callbacks -// This catches http://localhost:{AUTH_SERVER_PORT}/auth/callback?code=xxx and /callback (for MCP OAuth) -const server = createServer((req, res) => { - const url = new URL(req.url || "", `http://localhost:${AUTH_SERVER_PORT}`) +// Dev mode: Start local HTTP server for auth callback +// This catches http://localhost:21321/auth/callback?code=xxx +if (process.env.ELECTRON_RENDERER_URL) { + const server = createServer((req, res) => { + const url = new URL(req.url || "", "http://localhost:21321") // Serve favicon if (url.pathname === "/favicon.ico" || url.pathname === "/favicon.svg") { @@ -372,99 +312,16 @@ const server = createServer((req, res) => { res.writeHead(400, { "Content-Type": "text/plain" }) res.end("Missing code parameter") } - } else if (url.pathname === "/callback") { - // Handle MCP OAuth callback - const code = url.searchParams.get("code") - const state = url.searchParams.get("state") - console.log( - "[Auth Server] Received MCP OAuth callback with code:", - code?.slice(0, 8) + "...", - "state:", - state?.slice(0, 8) + "...", - ) - - if (code && state) { - // Handle the MCP OAuth callback - handleMcpOAuthCallback(code, state) - - // Send success response and close the browser tab - res.writeHead(200, { "Content-Type": "text/html" }) - res.end(` - - - - - 1Code - MCP Authentication - - - -
- -

MCP Server authenticated

-

You can close this tab

-
- - -`) - } else { - res.writeHead(400, { "Content-Type": "text/plain" }) - res.end("Missing code or state parameter") - } } else { res.writeHead(404, { "Content-Type": "text/plain" }) res.end("Not found") } }) -server.listen(AUTH_SERVER_PORT, () => { - console.log(`[Auth Server] Listening on http://localhost:${AUTH_SERVER_PORT}`) -}) + server.listen(21321, () => { + console.log("[Auth Server] Listening on http://localhost:21321") + }) +} // Clean up stale lock files from crashed instances // Returns true if locks were cleaned, false otherwise @@ -532,15 +389,10 @@ if (gotTheLock) { handleDeepLink(url) } - // Focus on the first available window - const windows = getAllWindows() - if (windows.length > 0) { - const window = windows[0]! + const window = getWindow() + if (window) { if (window.isMinimized()) window.restore() window.focus() - } else { - // No windows open, create a new one - createMainWindow() } }) @@ -599,13 +451,9 @@ if (gotTheLock) { // Track update availability for menu let updateAvailable = false let availableVersion: string | null = null - // Track devtools unlock state (hidden feature - 5 clicks on Beta tab) - let devToolsUnlocked = false // Function to build and set application menu const buildMenu = () => { - // Show devtools menu item only in dev mode or when unlocked - const showDevTools = !app.isPackaged || devToolsUnlocked const template: Electron.MenuItemConstructorOptions[] = [ { label: app.name, @@ -630,41 +478,6 @@ if (gotTheLock) { }, }, { type: "separator" }, - { - label: isCliInstalled() - ? "Uninstall '1code' Command..." - : "Install '1code' Command in PATH...", - click: async () => { - const { dialog } = await import("electron") - if (isCliInstalled()) { - const result = await uninstallCli() - if (result.success) { - dialog.showMessageBox({ - type: "info", - message: "CLI command uninstalled", - detail: "The '1code' command has been removed from your PATH.", - }) - buildMenu() - } else { - dialog.showErrorBox("Uninstallation Failed", result.error || "Unknown error") - } - } else { - const result = await installCli() - if (result.success) { - dialog.showMessageBox({ - type: "info", - message: "CLI command installed", - detail: - "You can now use '1code .' in any terminal to open 1Code in that directory.", - }) - buildMenu() - } else { - dialog.showErrorBox("Installation Failed", result.error || "Unknown error") - } - } - }, - }, - { type: "separator" }, { role: "services" }, { type: "separator" }, { role: "hide" }, @@ -691,25 +504,6 @@ if (gotTheLock) { } }, }, - { - label: "New Window", - accelerator: "CmdOrCtrl+Shift+N", - click: () => { - console.log("[Menu] New Window clicked (Cmd+Shift+N)") - createWindow() - }, - }, - { type: "separator" }, - { - label: "Close Window", - accelerator: "CmdOrCtrl+W", - click: () => { - const win = getWindow() - if (win) { - win.close() - } - }, - }, ], }, { @@ -727,11 +521,9 @@ if (gotTheLock) { { label: "View", submenu: [ - // Cmd+R is disabled to prevent accidental page refresh - // Use Cmd+Shift+R (Force Reload) for intentional reloads + { role: "reload" }, { role: "forceReload" }, - // Only show DevTools in dev mode or when unlocked via hidden feature - ...(showDevTools ? [{ role: "toggleDevTools" as const }] : []), + { role: "toggleDevTools" }, { type: "separator" }, { role: "resetZoom" }, { role: "zoomIn" }, @@ -765,20 +557,6 @@ if (gotTheLock) { Menu.setApplicationMenu(Menu.buildFromTemplate(template)) } - // macOS: Set dock menu (right-click on dock icon) - if (process.platform === "darwin") { - const dockMenu = Menu.buildFromTemplate([ - { - label: "New Window", - click: () => { - console.log("[Dock] New Window clicked") - createWindow() - }, - }, - ]) - app.dock.setMenu(dockMenu) - } - // Set update state and rebuild menu const setUpdateAvailable = (available: boolean, version?: string) => { updateAvailable = available @@ -786,25 +564,14 @@ if (gotTheLock) { buildMenu() } - // Unlock devtools and rebuild menu (called from renderer via IPC) - const unlockDevTools = () => { - if (!devToolsUnlocked) { - devToolsUnlocked = true - console.log("[App] DevTools unlocked via hidden feature") - buildMenu() - } - } - // Expose setUpdateAvailable globally for auto-updater ;(global as any).__setUpdateAvailable = setUpdateAvailable - // Expose unlockDevTools globally for IPC handler - ;(global as any).__unlockDevTools = unlockDevTools // Build initial menu buildMenu() - // Initialize auth manager (uses singleton from auth-manager module) - authManager = initAuthManager(!!process.env.ELECTRON_RENDERER_URL) + // Initialize auth manager + authManager = new AuthManager(!!process.env.ELECTRON_RENDERER_URL) console.log("[App] Auth manager initialized") // Initialize analytics after auth manager so we can identify user @@ -857,9 +624,9 @@ if (gotTheLock) { // Initialize auto-updater (production only) if (app.isPackaged) { - await initAutoUpdater(getAllWindows) + await initAutoUpdater(getWindow) // Setup update check on window focus (instead of periodic interval) - setupFocusUpdateCheck(getAllWindows) + setupFocusUpdateCheck(getWindow) // Check for updates 5 seconds after startup (force to bypass interval check) setTimeout(() => { checkForUpdates(true) @@ -870,16 +637,13 @@ if (gotTheLock) { // This populates the cache so all future sessions can use filtered MCP servers setTimeout(async () => { try { - const { getAllMcpConfigHandler } = await import("./lib/trpc/routers/claude") - await getAllMcpConfigHandler() + const { warmupMcpCache } = await import("./lib/trpc/routers/claude") + await warmupMcpCache() } catch (error) { console.error("[App] MCP warmup failed:", error) } }, 3000) - // Handle directory argument from CLI (e.g., `1code /path/to/project`) - parseLaunchDirectory() - // Handle deep link from app launch (Windows/Linux) const deepLinkUrl = process.argv.find((arg) => arg.startsWith(`${PROTOCOL}://`), @@ -906,7 +670,6 @@ if (gotTheLock) { // Cleanup before quit app.on("before-quit", async () => { console.log("[App] Shutting down...") - cancelAllPendingOAuth() await cleanupGitWatchers() await shutdownAnalytics() await closeDatabase() diff --git a/src/main/lib/analytics.ts b/src/main/lib/analytics.ts index bfbdd0a2..3cba8d93 100644 --- a/src/main/lib/analytics.ts +++ b/src/main/lib/analytics.ts @@ -5,56 +5,15 @@ import { PostHog } from "posthog-node" import { app } from "electron" -import * as fs from "fs" -import * as path from "path" -// PostHog configuration - hardcoded key for opensource users, env var override for internal builds -// This enables analytics for all users including those building from source -const POSTHOG_DESKTOP_KEY = import.meta.env.MAIN_VITE_POSTHOG_KEY || "phc_wM7gbrJhOLTvynyhnhPkrVGDc5mKRSXsLGQHqM3T3vq" +// PostHog configuration from environment +const POSTHOG_DESKTOP_KEY = import.meta.env.MAIN_VITE_POSTHOG_KEY const POSTHOG_HOST = import.meta.env.MAIN_VITE_POSTHOG_HOST || "https://us.i.posthog.com" let posthog: PostHog | null = null let currentUserId: string | null = null let userOptedOut = false // Synced from renderer -// track first launch using a marker file -const FIRST_LAUNCH_MARKER = ".first_launch_tracked" - -function getFirstLaunchMarkerPath(): string { - try { - return path.join(app.getPath("userData"), FIRST_LAUNCH_MARKER) - } catch { - // app not ready yet - return "" - } -} - -function isFirstLaunch(): boolean { - const markerPath = getFirstLaunchMarkerPath() - if (!markerPath) return false - - try { - return !fs.existsSync(markerPath) - } catch { - return false - } -} - -function markFirstLaunchTracked(): void { - const markerPath = getFirstLaunchMarkerPath() - if (!markerPath) return - - try { - fs.writeFileSync(markerPath, new Date().toISOString()) - } catch { - // ignore errors writing marker - } -} - -// Cached user properties for analytics enrichment -let cachedSubscriptionPlan: string | null = null -let cachedConnectionMethod: string | null = null - // Check if we're in development mode // Set FORCE_ANALYTICS=true to test analytics in development // Use a function to check lazily after app is ready @@ -72,15 +31,12 @@ function isDev(): boolean { */ function getCommonProperties() { return { - source: "desktop", // Unified source for desktop vs web analytics + source: "desktop_main", app_version: app.getVersion(), platform: process.platform, arch: process.arch, electron_version: process.versions.electron, node_version: process.versions.node, - // Analytics enrichment properties - subscription_plan: cachedSubscriptionPlan, - connection_method: cachedConnectionMethod, } } @@ -91,21 +47,6 @@ export function setOptOut(optedOut: boolean) { userOptedOut = optedOut } -/** - * Set subscription plan (called after fetching from API) - */ -export function setSubscriptionPlan(plan: string) { - cachedSubscriptionPlan = plan -} - -/** - * Set connection method (called from renderer via IPC) - * Values: "claude-subscription" | "api-key" | "custom-model" - */ -export function setConnectionMethod(method: string) { - cachedConnectionMethod = method -} - /** * Initialize PostHog for main process */ @@ -194,9 +135,6 @@ export function getCurrentUserId(): string | null { */ export function reset() { currentUserId = null - // Reset cached analytics properties - cachedSubscriptionPlan = null - cachedConnectionMethod = null // PostHog Node.js SDK doesn't have a reset method // Events will be sent as anonymous until next identify } @@ -219,22 +157,9 @@ export async function shutdown() { * Track app opened event */ export function trackAppOpened() { - const firstLaunch = isFirstLaunch() - capture("desktop_opened", { - first_launch: firstLaunch, + first_launch: false, // TODO: track first launch }) - - if (firstLaunch) { - // mark as tracked so subsequent opens don't count as first launch - markFirstLaunchTracked() - - // also fire a separate first_launch event for funnel analysis - capture("first_launch", { - app_version: app.getVersion(), - platform: process.platform, - }) - } } /** @@ -267,13 +192,11 @@ export function trackWorkspaceCreated(workspace: { id: string projectId: string useWorktree: boolean - repository?: string }) { capture("workspace_created", { workspace_id: workspace.id, project_id: workspace.projectId, use_worktree: workspace.useWorktree, - repository: workspace.repository, }) } @@ -300,12 +223,12 @@ export function trackWorkspaceDeleted(workspaceId: string) { */ export function trackMessageSent(data: { workspaceId: string - subChatId?: string + messageLength: number mode: "plan" | "agent" }) { capture("message_sent", { workspace_id: data.workspaceId, - sub_chat_id: data.subChatId, + message_length: data.messageLength, mode: data.mode, }) } @@ -316,41 +239,9 @@ export function trackMessageSent(data: { export function trackPRCreated(data: { workspaceId: string prNumber: number - repository?: string - mode?: "worktree" | "local" }) { capture("pr_created", { workspace_id: data.workspaceId, pr_number: data.prNumber, - repository: data.repository, - mode: data.mode, - }) -} - -/** - * Track commit created - */ -export function trackCommitCreated(data: { - workspaceId: string - filesChanged: number - mode: "worktree" | "local" -}) { - capture("commit_created", { - workspace_id: data.workspaceId, - files_changed: data.filesChanged, - mode: data.mode, - }) -} - -/** - * Track sub-chat created - */ -export function trackSubChatCreated(data: { - workspaceId: string - subChatId: string -}) { - capture("sub_chat_created", { - workspace_id: data.workspaceId, - sub_chat_id: data.subChatId, }) } diff --git a/src/main/lib/auto-updater.ts b/src/main/lib/auto-updater.ts index 24fe39f0..152a4bf7 100644 --- a/src/main/lib/auto-updater.ts +++ b/src/main/lib/auto-updater.ts @@ -1,8 +1,6 @@ import { BrowserWindow, ipcMain, app } from "electron" import log from "electron-log" import { autoUpdater, type UpdateInfo, type ProgressInfo } from "electron-updater" -import { readFileSync, writeFileSync, existsSync } from "fs" -import { join } from "path" /** * IMPORTANT: Do NOT use lazy/dynamic imports for electron-updater! @@ -32,88 +30,37 @@ const CDN_BASE = "https://cdn.21st.dev/releases/desktop" const MIN_CHECK_INTERVAL = 60 * 1000 // 1 minute let lastCheckTime = 0 -// Update channel preference file -const CHANNEL_PREF_FILE = "update-channel.json" - -type UpdateChannel = "latest" | "beta" - -function getChannelPrefPath(): string { - return join(app.getPath("userData"), CHANNEL_PREF_FILE) -} - -function getSavedChannel(): UpdateChannel { - try { - const prefPath = getChannelPrefPath() - if (existsSync(prefPath)) { - const data = JSON.parse(readFileSync(prefPath, "utf-8")) - if (data.channel === "beta" || data.channel === "latest") { - return data.channel - } - } - } catch { - // Ignore read errors, fall back to default - } - return "latest" -} - -function saveChannel(channel: UpdateChannel): void { - try { - writeFileSync(getChannelPrefPath(), JSON.stringify({ channel }), "utf-8") - } catch (error) { - log.error("[AutoUpdater] Failed to save channel preference:", error) - } -} - -let getAllWindows: (() => BrowserWindow[]) | null = null +let mainWindow: (() => BrowserWindow | null) | null = null /** - * Send update event to all renderer windows - * Update events are app-wide and should be visible in all windows + * Send update event to renderer process */ -function sendToAllRenderers(channel: string, data?: unknown) { - const windows = getAllWindows?.() ?? BrowserWindow.getAllWindows() - for (const win of windows) { - try { - if (win && !win.isDestroyed()) { - win.webContents.send(channel, data) - } - } catch { - // Window may have been destroyed between check and send - } +function sendToRenderer(channel: string, data?: unknown) { + const win = mainWindow?.() + if (win && !win.isDestroyed()) { + win.webContents.send(channel, data) } } /** * Initialize the auto-updater with event handlers and IPC */ -export async function initAutoUpdater(getWindows: () => BrowserWindow[]) { - getAllWindows = getWindows +export async function initAutoUpdater(getWindow: () => BrowserWindow | null) { + mainWindow = getWindow // Initialize config initAutoUpdaterConfig() - // Set update channel from saved preference - const savedChannel = getSavedChannel() - autoUpdater.channel = savedChannel - log.info(`[AutoUpdater] Using update channel: ${savedChannel}`) - // Configure feed URL to point to R2 CDN - // Note: We use a custom request headers to bypass CDN cache autoUpdater.setFeedURL({ provider: "generic", url: CDN_BASE, }) - // Add cache-busting to update requests - autoUpdater.requestHeaders = { - "Cache-Control": "no-cache, no-store, must-revalidate", - "Pragma": "no-cache", - } - // Event: Checking for updates autoUpdater.on("checking-for-update", () => { log.info("[AutoUpdater] Checking for updates...") - sendToAllRenderers("update:checking") + sendToRenderer("update:checking") }) // Event: Update available @@ -124,7 +71,7 @@ export async function initAutoUpdater(getWindows: () => BrowserWindow[]) { if (setUpdateAvailable) { setUpdateAvailable(true, info.version) } - sendToAllRenderers("update:available", { + sendToRenderer("update:available", { version: info.version, releaseDate: info.releaseDate, releaseNotes: info.releaseNotes, @@ -134,7 +81,7 @@ export async function initAutoUpdater(getWindows: () => BrowserWindow[]) { // Event: No update available autoUpdater.on("update-not-available", (info: UpdateInfo) => { log.info(`[AutoUpdater] App is up to date (v${info.version})`) - sendToAllRenderers("update:not-available", { + sendToRenderer("update:not-available", { version: info.version, }) }) @@ -145,7 +92,7 @@ export async function initAutoUpdater(getWindows: () => BrowserWindow[]) { `[AutoUpdater] Download progress: ${progress.percent.toFixed(1)}% ` + `(${formatBytes(progress.transferred)}/${formatBytes(progress.total)})`, ) - sendToAllRenderers("update:progress", { + sendToRenderer("update:progress", { percent: progress.percent, bytesPerSecond: progress.bytesPerSecond, transferred: progress.transferred, @@ -161,7 +108,7 @@ export async function initAutoUpdater(getWindows: () => BrowserWindow[]) { if (setUpdateAvailable) { setUpdateAvailable(false) } - sendToAllRenderers("update:downloaded", { + sendToRenderer("update:downloaded", { version: info.version, releaseDate: info.releaseDate, releaseNotes: info.releaseNotes, @@ -171,7 +118,7 @@ export async function initAutoUpdater(getWindows: () => BrowserWindow[]) { // Event: Error autoUpdater.on("error", (error: Error) => { log.error("[AutoUpdater] Error:", error.message) - sendToAllRenderers("update:error", error.message) + sendToRenderer("update:error", error.message) }) // Register IPC handlers @@ -185,29 +132,13 @@ export async function initAutoUpdater(getWindows: () => BrowserWindow[]) { */ function registerIpcHandlers() { // Check for updates - ipcMain.handle("update:check", async (_event, force?: boolean) => { + ipcMain.handle("update:check", async () => { if (!app.isPackaged) { log.info("[AutoUpdater] Skipping update check in dev mode") return null } try { - // If force is true, add cache-busting timestamp to URL - if (force) { - const cacheBuster = `?t=${Date.now()}` - autoUpdater.setFeedURL({ - provider: "generic", - url: `${CDN_BASE}${cacheBuster}`, - }) - log.info("[AutoUpdater] Force check with cache-busting:", `${CDN_BASE}${cacheBuster}`) - } const result = await autoUpdater.checkForUpdates() - // Reset feed URL back to normal after force check - if (force) { - autoUpdater.setFeedURL({ - provider: "generic", - url: CDN_BASE, - }) - } return result?.updateInfo || null } catch (error) { log.error("[AutoUpdater] Check failed:", error) @@ -241,31 +172,6 @@ function registerIpcHandlers() { currentVersion: app.getVersion(), } }) - - // Set update channel (latest = stable only, beta = stable + beta) - ipcMain.handle("update:set-channel", async (_event, channel: string) => { - if (channel !== "latest" && channel !== "beta") { - log.warn(`[AutoUpdater] Invalid channel: ${channel}`) - return false - } - log.info(`[AutoUpdater] Switching update channel to: ${channel}`) - autoUpdater.channel = channel - saveChannel(channel) - // Check for updates immediately with new channel - if (app.isPackaged) { - try { - await autoUpdater.checkForUpdates() - } catch (error) { - log.error("[AutoUpdater] Post-channel-switch check failed:", error) - } - } - return true - }) - - // Get current update channel - ipcMain.handle("update:get-channel", () => { - return getSavedChannel() - }) } /** @@ -314,7 +220,7 @@ export async function downloadUpdate() { * Check for updates when window gains focus * This is more natural than checking on an interval */ -export function setupFocusUpdateCheck(_getWindows: () => BrowserWindow[]) { +export function setupFocusUpdateCheck(getWindow: () => BrowserWindow | null) { // Listen for window focus events app.on("browser-window-focus", () => { log.info("[AutoUpdater] Window focused - checking for updates") diff --git a/src/main/lib/claude-config.ts b/src/main/lib/claude-config.ts deleted file mode 100644 index 1f907522..00000000 --- a/src/main/lib/claude-config.ts +++ /dev/null @@ -1,291 +0,0 @@ -/** - * Helpers for reading and writing ~/.claude.json configuration - */ -import { Mutex } from "async-mutex" -import { eq } from "drizzle-orm" -import { existsSync, readFileSync, writeFileSync } from "fs" -import * as fs from "fs/promises" -import * as os from "os" -import * as path from "path" -import { getDatabase } from "./db" -import { chats, projects } from "./db/schema" - -/** - * Mutex for protecting read-modify-write operations on ~/.claude.json - * This prevents race conditions when multiple concurrent operations - * (e.g., token refreshes for different MCP servers) try to update the config. - */ -const configMutex = new Mutex() - -export const CLAUDE_CONFIG_PATH = path.join(os.homedir(), ".claude.json") - -export interface McpServerConfig { - command?: string - args?: string[] - url?: string - authType?: "oauth" | "bearer" | "none" - _oauth?: { - accessToken: string - refreshToken?: string - clientId?: string - expiresAt?: number - } - [key: string]: unknown -} - -export interface ProjectConfig { - mcpServers?: Record - [key: string]: unknown -} - -export interface ClaudeConfig { - mcpServers?: Record // User-scope (global) MCP servers - projects?: Record - [key: string]: unknown -} - -/** - * Read ~/.claude.json asynchronously - * Returns empty config if file doesn't exist or is invalid - */ -export async function readClaudeConfig(): Promise { - try { - const content = await fs.readFile(CLAUDE_CONFIG_PATH, "utf-8") - return JSON.parse(content) - } catch { - return {} - } -} - -/** - * Read ~/.claude.json synchronously - * Returns empty config if file doesn't exist or is invalid - */ -export function readClaudeConfigSync(): ClaudeConfig { - try { - const content = readFileSync(CLAUDE_CONFIG_PATH, "utf-8") - return JSON.parse(content) - } catch { - return {} - } -} - -/** - * Write ~/.claude.json asynchronously - */ -export async function writeClaudeConfig(config: ClaudeConfig): Promise { - await fs.writeFile(CLAUDE_CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8") -} - -/** - * Write ~/.claude.json synchronously - */ -export function writeClaudeConfigSync(config: ClaudeConfig): void { - writeFileSync(CLAUDE_CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8") -} - -/** - * Execute a read-modify-write operation on ~/.claude.json atomically. - * This is the ONLY safe way to update the config when concurrent writes are possible. - * - * Uses a mutex to ensure that only one read-modify-write cycle happens at a time, - * preventing race conditions where concurrent token refreshes could overwrite - * each other's updates. - * - * @param updater Function that receives current config and returns updated config - * @returns The updated config - */ -export async function updateClaudeConfigAtomic( - updater: (config: ClaudeConfig) => ClaudeConfig | Promise -): Promise { - return configMutex.runExclusive(async () => { - const config = await readClaudeConfig() - const updatedConfig = await updater(config) - await writeClaudeConfig(updatedConfig) - return updatedConfig - }) -} - -/** - * Check if ~/.claude.json exists - */ -export function claudeConfigExists(): boolean { - return existsSync(CLAUDE_CONFIG_PATH) -} - -/** - * Get MCP servers config for a specific project - * Automatically resolves worktree paths to original project paths - */ -export function getProjectMcpServers( - config: ClaudeConfig, - projectPath: string -): Record | undefined { - const resolvedPath = resolveProjectPathFromWorktree(projectPath) || projectPath - return config.projects?.[resolvedPath]?.mcpServers -} - -// Special marker for global MCP servers (not tied to a project) -export const GLOBAL_MCP_PATH = "__global__" - -/** - * Get a specific MCP server config - * Use projectPath = GLOBAL_MCP_PATH (or null) for global MCP servers - * Automatically resolves worktree paths to original project paths - */ -export function getMcpServerConfig( - config: ClaudeConfig, - projectPath: string | null, - serverName: string -): McpServerConfig | undefined { - // Global MCP servers (root level mcpServers in ~/.claude.json) - if (!projectPath || projectPath === GLOBAL_MCP_PATH) { - return config.mcpServers?.[serverName] - } - // Project-specific MCP servers (resolve worktree paths) - const resolvedPath = resolveProjectPathFromWorktree(projectPath) || projectPath - return config.projects?.[resolvedPath]?.mcpServers?.[serverName] -} - -/** - * Update MCP server config (creates path if needed) - * Use projectPath = GLOBAL_MCP_PATH (or null) for global MCP servers - * Automatically resolves worktree paths to original project paths - */ -export function updateMcpServerConfig( - config: ClaudeConfig, - projectPath: string | null, - serverName: string, - update: Partial -): ClaudeConfig { - // Global MCP servers (root level mcpServers in ~/.claude.json) - if (!projectPath || projectPath === GLOBAL_MCP_PATH) { - config.mcpServers = config.mcpServers || {} - config.mcpServers[serverName] = { - ...config.mcpServers[serverName], - ...update, - } - return config - } - // Project-specific MCP servers (resolve worktree paths) - const resolvedPath = resolveProjectPathFromWorktree(projectPath) || projectPath - config.projects = config.projects || {} - config.projects[resolvedPath] = config.projects[resolvedPath] || {} - config.projects[resolvedPath].mcpServers = config.projects[resolvedPath].mcpServers || {} - config.projects[resolvedPath].mcpServers[serverName] = { - ...config.projects[resolvedPath].mcpServers[serverName], - ...update, - } - return config -} - -/** - * Remove an MCP server from config - * Use projectPath = GLOBAL_MCP_PATH (or null) for global MCP servers - * Automatically resolves worktree paths to original project paths - */ -export function removeMcpServerConfig( - config: ClaudeConfig, - projectPath: string | null, - serverName: string -): ClaudeConfig { - // Global MCP servers - if (!projectPath || projectPath === GLOBAL_MCP_PATH) { - if (config.mcpServers?.[serverName]) { - delete config.mcpServers[serverName] - } - return config - } - // Project-specific MCP servers - const resolvedPath = resolveProjectPathFromWorktree(projectPath) || projectPath - if (config.projects?.[resolvedPath]?.mcpServers?.[serverName]) { - delete config.projects[resolvedPath].mcpServers[serverName] - // Clean up empty objects - if (Object.keys(config.projects[resolvedPath].mcpServers).length === 0) { - delete config.projects[resolvedPath].mcpServers - } - if (Object.keys(config.projects[resolvedPath]).length === 0) { - delete config.projects[resolvedPath] - } - } - return config -} - -/** - * Resolve original project path from a worktree path. - * Supports legacy (~/.21st/worktrees/{projectId}/{chatId}/) and - * new format (~/.21st/worktrees/{projectName}/{worktreeFolder}/). - * - * @param pathToResolve - Either a worktree path or regular project path - * @returns The original project path, or the input if not a worktree, or null if resolution fails - */ -export function resolveProjectPathFromWorktree( - pathToResolve: string -): string | null { - const worktreeMarker = path.join(".21st", "worktrees") - - // Normalize for cross-platform (handle both / and \ separators) - const normalizedPath = pathToResolve.replace(/\\/g, "/") - const normalizedMarker = worktreeMarker.replace(/\\/g, "/") - - if (!normalizedPath.includes(normalizedMarker)) { - // Not a worktree path, return as-is - return pathToResolve - } - - try { - // Extract segments from path structure - // Path format: /Users/.../.21st/worktrees/{projectSlug}/{worktreeFolder} - const worktreeBase = path.join(os.homedir(), ".21st", "worktrees") - const normalizedBase = worktreeBase.replace(/\\/g, "/") - const relativePath = normalizedPath - .replace(normalizedBase, "") - .replace(/^\//, "") - - const parts = relativePath.split("/") - if (parts.length < 1 || !parts[0]) { - return null - } - - const db = getDatabase() - - // Strategy 1: Legacy lookup - folder name is a projectId - const projectById = db - .select({ path: projects.path }) - .from(projects) - .where(eq(projects.id, parts[0])) - .get() - - if (projectById) { - return projectById.path - } - - // Strategy 2: New format - folder name is the project name. - // Look up via chats.worktreePath which stores the full path. - if (parts.length >= 2) { - const expectedWorktreePath = path.join(worktreeBase, parts[0], parts[1]) - const chat = db - .select({ projectId: chats.projectId }) - .from(chats) - .where(eq(chats.worktreePath, expectedWorktreePath)) - .get() - - if (chat) { - const project = db - .select({ path: projects.path }) - .from(projects) - .where(eq(projects.id, chat.projectId)) - .get() - - if (project) { - return project.path - } - } - } - - return null - } catch (error) { - console.error("[worktree-utils] Failed to resolve project path:", error) - return null - } -} \ No newline at end of file diff --git a/src/main/lib/claude-token.ts b/src/main/lib/claude-token.ts index 204c45c5..2e21a5e4 100644 --- a/src/main/lib/claude-token.ts +++ b/src/main/lib/claude-token.ts @@ -2,7 +2,6 @@ import { execSync, spawn } from "node:child_process"; import { existsSync, readFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; -import { buildExtendedPath, isWindows } from "./platform"; interface ClaudeCredentials { claudeAiOauth?: { @@ -243,14 +242,23 @@ export function isTokenExpired(expiresAt?: number): boolean { /** * Build extended PATH with common installation locations - * This is necessary because when running from Finder/Dock (macOS) or - * Start Menu (Windows), the PATH may not include directories where - * claude CLI is installed - * - * Delegates to platform provider for cross-platform support. + * This is necessary because when running from Finder/Dock, the PATH + * may not include directories where claude CLI is installed */ function getExtendedPath(): string { - return buildExtendedPath(process.env.PATH); + const home = homedir(); + const extendedPaths = [ + '/opt/homebrew/bin', + '/usr/local/bin', + `${home}/.local/bin`, + `${home}/.bun/bin`, + `${home}/.cargo/bin`, + '/opt/local/bin', + `${home}/.nvm/versions/node/*/bin`, // Common Node.js installations + ].filter(Boolean); + + const currentPath = process.env.PATH || ''; + return [...extendedPaths, ...currentPath.split(':')].join(':'); } /** @@ -260,7 +268,7 @@ function getExtendedPath(): string { export function isClaudeCliInstalled(): boolean { try { // Use 'where' on Windows, 'which' on Unix-like systems - const command = isWindows() ? 'where claude' : 'which claude'; + const command = process.platform === 'win32' ? 'where claude' : 'which claude'; const fullPath = getExtendedPath(); execSync(command, { diff --git a/src/main/lib/claude/env.ts b/src/main/lib/claude/env.ts index e181e789..9ce6acec 100644 --- a/src/main/lib/claude/env.ts +++ b/src/main/lib/claude/env.ts @@ -1,14 +1,9 @@ -import { app } from "electron" import { execSync } from "node:child_process" import fs from "node:fs" -import os from "node:os" import path from "node:path" +import os from "node:os" +import { app } from "electron" import { stripVTControlCharacters } from "node:util" -import { - getDefaultShell, - isWindows, - platform -} from "../platform" // Cache the shell environment let cachedShellEnv: Record | null = null @@ -16,11 +11,9 @@ let cachedShellEnv: Record | null = null // Delimiter for parsing env output const DELIMITER = "_CLAUDE_ENV_DELIMITER_" -// Keys to strip (prevent interference from unrelated providers) -// NOTE: We intentionally keep ANTHROPIC_API_KEY and ANTHROPIC_BASE_URL -// so users can use their existing Claude Code CLI configuration (API proxy, etc.) -// Based on PR #29 by @sa4hnd +// Keys to strip (prevent auth interference) const STRIPPED_ENV_KEYS = [ + "ANTHROPIC_API_KEY", "OPENAI_API_KEY", "CLAUDE_CODE_USE_BEDROCK", "CLAUDE_CODE_USE_VERTEX", @@ -42,14 +35,14 @@ export function getBundledClaudeBinaryPath(): string { } const isDev = !app.isPackaged - const currentPlatform = process.platform + const platform = process.platform const arch = process.arch // Only log verbose info on first call if (process.env.DEBUG_CLAUDE_BINARY) { console.log("[claude-binary] ========== BUNDLED BINARY PATH ==========") console.log("[claude-binary] isDev:", isDev) - console.log("[claude-binary] platform:", currentPlatform) + console.log("[claude-binary] platform:", platform) console.log("[claude-binary] arch:", arch) console.log("[claude-binary] appPath:", app.getAppPath()) } @@ -57,18 +50,14 @@ export function getBundledClaudeBinaryPath(): string { // In dev: apps/desktop/resources/bin/{platform}-{arch}/claude // In production: {resourcesPath}/bin/claude const resourcesPath = isDev - ? path.join( - app.getAppPath(), - "resources/bin", - `${currentPlatform}-${arch}` - ) + ? path.join(app.getAppPath(), "resources/bin", `${platform}-${arch}`) : path.join(process.resourcesPath, "bin") if (process.env.DEBUG_CLAUDE_BINARY) { console.log("[claude-binary] resourcesPath:", resourcesPath) } - const binaryName = currentPlatform === "win32" ? "claude.exe" : "claude" + const binaryName = platform === "win32" ? "claude.exe" : "claude" const binaryPath = path.join(resourcesPath, binaryName) if (process.env.DEBUG_CLAUDE_BINARY) { @@ -80,13 +69,8 @@ export function getBundledClaudeBinaryPath(): string { // Always log if binary doesn't exist (critical error) if (!exists) { - console.error( - "[claude-binary] WARNING: Binary not found at path:", - binaryPath - ) - console.error( - "[claude-binary] Run 'bun run claude:download' to download it" - ) + console.error("[claude-binary] WARNING: Binary not found at path:", binaryPath) + console.error("[claude-binary] Run 'bun run claude:download' to download it") } else if (process.env.DEBUG_CLAUDE_BINARY) { const stats = fs.statSync(binaryPath) const sizeMB = (stats.size / 1024 / 1024).toFixed(1) @@ -126,21 +110,8 @@ function parseEnvOutput(output: string): Record { } /** - * Strip sensitive keys from environment - */ -function stripSensitiveKeys(env: Record): void { - for (const key of STRIPPED_ENV_KEYS) { - if (key in env) { - console.log(`[claude-env] Stripped ${key} from shell environment`) - delete env[key] - } - } -} - -/** - * Load full shell environment. - * - Windows: Derives PATH from process.env + common install locations (no shell spawn) - * - macOS/Linux: Spawns interactive login shell to capture PATH from shell profiles + * Load full shell environment using interactive login shell. + * This captures PATH, HOME, and all shell profile configurations. * Results are cached for the lifetime of the process. */ export function getClaudeShellEnvironment(): Record { @@ -148,27 +119,7 @@ export function getClaudeShellEnvironment(): Record { return { ...cachedShellEnv } } - // Windows: use platform provider to build environment - if (isWindows()) { - console.log( - "[claude-env] Windows detected, deriving PATH without shell invocation" - ) - - // Use platform provider to build environment - const env = platform.buildEnvironment() - - // Strip sensitive keys - stripSensitiveKeys(env) - - console.log( - `[claude-env] Built Windows environment with ${Object.keys(env).length} vars` - ) - cachedShellEnv = env - return { ...env } - } - - // macOS/Linux: spawn interactive login shell to get full environment - const shell = getDefaultShell() + const shell = process.env.SHELL || "/bin/zsh" const command = `echo -n "${DELIMITER}"; env; echo -n "${DELIMITER}"; exit` try { @@ -186,23 +137,46 @@ export function getClaudeShellEnvironment(): Record { }) const env = parseEnvOutput(output) - stripSensitiveKeys(env) + + // Strip keys that could interfere with Claude's auth resolution + for (const key of STRIPPED_ENV_KEYS) { + if (key in env) { + console.log(`[claude-env] Stripped ${key} from shell environment`) + delete env[key] + } + } console.log( - `[claude-env] Loaded ${Object.keys(env).length} environment variables from shell` + `[claude-env] Loaded ${Object.keys(env).length} environment variables from shell`, ) cachedShellEnv = env return { ...env } } catch (error) { console.error("[claude-env] Failed to load shell environment:", error) - // Fallback: use platform provider - const env = platform.buildEnvironment() - stripSensitiveKeys(env) + // Fallback: return minimal required env + const home = os.homedir() + const fallbackPath = [ + `${home}/.local/bin`, + "/opt/homebrew/bin", + "/usr/local/bin", + "/usr/bin", + "/bin", + "/usr/sbin", + "/sbin", + ].join(":") + + const fallback: Record = { + HOME: home, + USER: os.userInfo().username, + PATH: fallbackPath, + SHELL: process.env.SHELL || "/bin/zsh", + TERM: "xterm-256color", + } - console.log("[claude-env] Using fallback environment from platform provider") - cachedShellEnv = env - return { ...env } + console.log("[claude-env] Using fallback environment") + cachedShellEnv = fallback + return { ...fallback } } } @@ -213,7 +187,6 @@ export function getClaudeShellEnvironment(): Record { export function buildClaudeEnv(options?: { ghToken?: string customEnv?: Record - enableTasks?: boolean }): Record { const env: Record = {} @@ -237,17 +210,11 @@ export function buildClaudeEnv(options?: { env.PATH = shellPath } - // 3. Ensure critical vars are present using platform provider - const platformEnv = platform.buildEnvironment() - if (!env.HOME) env.HOME = platformEnv.HOME - if (!env.USER) env.USER = platformEnv.USER + // 3. Ensure critical vars are present + if (!env.HOME) env.HOME = os.homedir() + if (!env.USER) env.USER = os.userInfo().username + if (!env.SHELL) env.SHELL = "/bin/zsh" if (!env.TERM) env.TERM = "xterm-256color" - if (!env.SHELL) env.SHELL = getDefaultShell() - - // Windows-specific: ensure USERPROFILE is set - if (isWindows() && !env.USERPROFILE) { - env.USERPROFILE = os.homedir() - } // 4. Add custom overrides if (options?.ghToken) { @@ -265,8 +232,6 @@ export function buildClaudeEnv(options?: { // 5. Mark as SDK entry env.CLAUDE_CODE_ENTRYPOINT = "sdk-ts" - // Enable/disable task management tools based on user preference (default: enabled) - env.CLAUDE_CODE_ENABLE_TASKS = options?.enableTasks !== false ? "true" : "false" return env } @@ -283,17 +248,17 @@ export function clearClaudeEnvCache(): void { */ export function logClaudeEnv( env: Record, - prefix: string = "" + prefix: string = "", ): void { console.log(`${prefix}[claude-env] HOME: ${env.HOME}`) console.log(`${prefix}[claude-env] USER: ${env.USER}`) console.log( - `${prefix}[claude-env] PATH includes homebrew: ${env.PATH?.includes("/opt/homebrew")}` + `${prefix}[claude-env] PATH includes homebrew: ${env.PATH?.includes("/opt/homebrew")}`, ) console.log( - `${prefix}[claude-env] PATH includes /usr/local/bin: ${env.PATH?.includes("/usr/local/bin")}` + `${prefix}[claude-env] PATH includes /usr/local/bin: ${env.PATH?.includes("/usr/local/bin")}`, ) console.log( - `${prefix}[claude-env] ANTHROPIC_AUTH_TOKEN: ${env.ANTHROPIC_AUTH_TOKEN ? "set" : "not set"}` + `${prefix}[claude-env] ANTHROPIC_AUTH_TOKEN: ${env.ANTHROPIC_AUTH_TOKEN ? "set" : "not set"}`, ) } diff --git a/src/main/lib/claude/offline-handler.ts b/src/main/lib/claude/offline-handler.ts index 292279ac..45c7e393 100644 --- a/src/main/lib/claude/offline-handler.ts +++ b/src/main/lib/claude/offline-handler.ts @@ -20,19 +20,12 @@ export type OfflineCheckResult = { * Check if we should use Ollama as fallback * Priority: * 1. If customConfig provided → use it - * 2. If offline mode enabled AND no internet → use Ollama + * 2. If OFFLINE → use Ollama (ignore auth token) * 3. If online + auth → use Claude API - * - * @param customConfig - Custom config from user settings - * @param claudeCodeToken - Claude Code auth token - * @param selectedOllamaModel - User-selected Ollama model (optional) - * @param offlineModeEnabled - Whether offline mode is enabled in settings */ export async function checkOfflineFallback( customConfig: CustomClaudeConfig | undefined, claudeCodeToken: string | null, - selectedOllamaModel?: string | null, - offlineModeEnabled: boolean = false, ): Promise { // If custom config is provided, use it (highest priority) if (customConfig) { @@ -43,15 +36,6 @@ export async function checkOfflineFallback( } } - // If offline mode is disabled in settings, skip all Ollama checks - // and just use Claude API (will fail with auth error if no token) - if (!offlineModeEnabled) { - return { - config: undefined, - isUsingOllama: false, - } - } - // Check internet FIRST - if offline, use Ollama regardless of auth console.log('[Offline] Checking internet connectivity...') const hasInternet = await checkInternetConnection() @@ -79,12 +63,10 @@ export async function checkOfflineFallback( } } - // Use Ollama with selected model or recommended model - console.log(`[Offline] selectedOllamaModel param: ${selectedOllamaModel || "(null/undefined)"}, recommendedModel: ${ollamaStatus.recommendedModel}`) - const modelToUse = selectedOllamaModel || ollamaStatus.recommendedModel - const config = getOllamaConfig(modelToUse) + // Use Ollama! + const config = getOllamaConfig(ollamaStatus.recommendedModel) - console.log(`[Offline] Switching to Ollama (model: ${modelToUse})`) + console.log(`[Offline] Switching to Ollama (model: ${ollamaStatus.recommendedModel})`) return { config, diff --git a/src/main/lib/claude/transform.ts b/src/main/lib/claude/transform.ts index 93e0a353..b12730c0 100644 --- a/src/main/lib/claude/transform.ts +++ b/src/main/lib/claude/transform.ts @@ -60,25 +60,13 @@ export function createTransformer(options?: { emitSdkMessageUuid?: boolean; isUs if (currentToolCallId) { // Track this tool ID to avoid duplicates from assistant message emittedToolIds.add(currentToolCallId) - - let parsedInput = {} - if (accumulatedToolInput) { - try { - parsedInput = JSON.parse(accumulatedToolInput) - } catch (e) { - // Stream may have been interrupted mid-JSON (e.g. network error, abort) - // resulting in incomplete JSON like '{"prompt":"write co' - console.error("[transform] Failed to parse tool input JSON:", (e as Error).message, "partial:", accumulatedToolInput.slice(0, 120)) - parsedInput = { _raw: accumulatedToolInput, _parseError: true } - } - } - + // Emit complete tool call with accumulated input yield { type: "tool-input-available", toolCallId: currentToolCallId, toolName: currentToolName || "unknown", - input: parsedInput, + input: accumulatedToolInput ? JSON.parse(accumulatedToolInput) : {}, } currentToolCallId = null currentToolName = null @@ -87,6 +75,14 @@ export function createTransformer(options?: { emitSdkMessageUuid?: boolean; isUs } return function* transform(msg: any): Generator { + // Emit UUID early for rollback support (before abort can happen) + // This ensures frontend has sdkMessageUuid even if streaming is interrupted + if (emitSdkMessageUuid && msg.type === "assistant" && msg.uuid) { + yield { + type: "message-metadata", + messageMetadata: { sdkMessageUuid: msg.uuid } + } + } // Debug: log ALL message types to understand what SDK sends if (isUsingOllama) { @@ -356,7 +352,7 @@ export function createTransformer(options?: { emitSdkMessageUuid?: boolean; isUs } // ===== USER MESSAGE (tool results) ===== - if (msg.type === "user" && msg.message?.content && Array.isArray(msg.message.content)) { + if (msg.type === "user" && msg.message?.content) { // DEBUG: Log the message structure to understand tool_use_result console.log("[Transform DEBUG] User message:", { tool_use_result: msg.tool_use_result, @@ -426,7 +422,7 @@ export function createTransformer(options?: { emitSdkMessageUuid?: boolean; isUs }) // Map MCP servers with validated status type and additional info const mcpServers: MCPServer[] = (msg.mcp_servers || []).map( - (s: { name: string; status: string; serverInfo?: { name: string; version: string; icons?: { src: string; mimeType?: string; sizes?: string[]; theme?: "light" | "dark" }[] }; error?: string }) => ({ + (s: { name: string; status: string; serverInfo?: { name: string; version: string }; error?: string }) => ({ name: s.name, status: (["connected", "failed", "pending", "needs-auth"].includes( s.status, @@ -470,31 +466,14 @@ export function createTransformer(options?: { emitSdkMessageUuid?: boolean; isUs // ===== RESULT (final) ===== if (msg.type === "result") { + console.log("[transform] RESULT message, textStarted:", textStarted, "lastTextId:", lastTextId) yield* endTextBlock() yield* endToolInput() const inputTokens = msg.usage?.input_tokens const outputTokens = msg.usage?.output_tokens - - // Extract per-model usage from SDK (if available) - const modelUsage = msg.modelUsage - ? Object.fromEntries( - Object.entries(msg.modelUsage).map(([model, usage]: [string, any]) => [ - model, - { - inputTokens: usage.inputTokens || 0, - outputTokens: usage.outputTokens || 0, - cacheReadInputTokens: usage.cacheReadInputTokens || 0, - cacheCreationInputTokens: usage.cacheCreationInputTokens || 0, - costUSD: usage.costUSD || 0, - }, - ]) - ) - : undefined - const metadata: MessageMetadata = { sessionId: msg.session_id, - sdkMessageUuid: emitSdkMessageUuid ? msg.uuid : undefined, inputTokens, outputTokens, totalTokens: inputTokens && outputTokens ? inputTokens + outputTokens : undefined, @@ -503,11 +482,10 @@ export function createTransformer(options?: { emitSdkMessageUuid?: boolean; isUs resultSubtype: msg.subtype || "success", // Include finalTextId for collapsing tools when there's a final response finalTextId: lastTextId || undefined, - // Per-model usage breakdown - modelUsage, } yield { type: "message-metadata", messageMetadata: metadata } yield { type: "finish-step" } + console.log("[transform] YIELDING FINISH from result message") yield { type: "finish", messageMetadata: metadata } } } diff --git a/src/main/lib/claude/types.ts b/src/main/lib/claude/types.ts index 89ab312c..9ad956b7 100644 --- a/src/main/lib/claude/types.ts +++ b/src/main/lib/claude/types.ts @@ -55,32 +55,16 @@ export type UIMessageChunk = export type MCPServerStatus = "connected" | "failed" | "pending" | "needs-auth" -export type MCPServerIcon = { - src: string - mimeType?: string - sizes?: string[] - theme?: "light" | "dark" -} - export type MCPServer = { name: string status: MCPServerStatus serverInfo?: { name: string version: string - icons?: MCPServerIcon[] } error?: string } -export type ModelUsageEntry = { - inputTokens: number - outputTokens: number - cacheReadInputTokens: number - cacheCreationInputTokens: number - costUSD: number -} - export type MessageMetadata = { sessionId?: string sdkMessageUuid?: string // SDK's message UUID for resumeSessionAt (rollback support) @@ -91,6 +75,4 @@ export type MessageMetadata = { durationMs?: number resultSubtype?: string finalTextId?: string - // Per-model usage breakdown from SDK (model name -> usage) - modelUsage?: Record } diff --git a/src/main/lib/cli.ts b/src/main/lib/cli.ts deleted file mode 100644 index 4265dcdd..00000000 --- a/src/main/lib/cli.ts +++ /dev/null @@ -1,93 +0,0 @@ -/** - * CLI command support for 1code - * Allows users to open 1code from terminal with: 1code . or 1code /path/to/project - * - * Based on PR #16 by @caffeinum (Aleksey Bykhun) - * https://github.com/21st-dev/1code/pull/16 - */ - -import { app } from "electron" -import { join } from "path" -import { existsSync, lstatSync } from "fs" -import { platform } from "./platform" - -// Launch directory from CLI (e.g., `1code /path/to/project`) -let launchDirectory: string | null = null - -/** - * Get the launch directory passed via CLI args (consumed once) - */ -export function getLaunchDirectory(): string | null { - const dir = launchDirectory - launchDirectory = null // consume once - return dir -} - -/** - * Parse CLI arguments to find a directory argument - * Called on app startup to handle `1code .` or `1code /path/to/project` - */ -export function parseLaunchDirectory(): void { - // Look for a directory argument in argv - // Skip electron executable and script path - const args = process.argv.slice(process.defaultApp ? 2 : 1) - - for (const arg of args) { - // Skip flags and protocol URLs - if (arg.startsWith("-") || arg.includes("://")) continue - - // Check if it's a valid directory - if (existsSync(arg)) { - try { - const stat = lstatSync(arg) - if (stat.isDirectory()) { - console.log("[CLI] Launch directory:", arg) - launchDirectory = arg - return - } - } catch { - // ignore - } - } - } -} - -/** - * Get the CLI source path (where the CLI script is bundled) - */ -function getCliSourcePath(): string { - const cliName = platform.getCliConfig().scriptName - if (app.isPackaged) { - return join(process.resourcesPath, "cli", cliName) - } - return join(__dirname, "..", "..", "resources", "cli", cliName) -} - -/** - * Check if the CLI command is installed - */ -export function isCliInstalled(): boolean { - return platform.isCliInstalled(getCliSourcePath()) -} - -/** - * Install the CLI command - * Platform-specific behavior is handled by the platform provider - */ -export async function installCli(): Promise<{ - success: boolean - error?: string -}> { - return platform.installCli(getCliSourcePath()) -} - -/** - * Uninstall the CLI command - * Platform-specific behavior is handled by the platform provider - */ -export async function uninstallCli(): Promise<{ - success: boolean - error?: string -}> { - return platform.uninstallCli() -} diff --git a/src/main/lib/credential-manager.ts b/src/main/lib/credential-manager.ts deleted file mode 100644 index 150ef3d8..00000000 --- a/src/main/lib/credential-manager.ts +++ /dev/null @@ -1,821 +0,0 @@ -/** - * SourceCredentialManager - * - * Unified credential management for sources. Consolidates credential CRUD, - * credential ID resolution, expiry checking, and OAuth flows. - * - * This replaces scattered credential logic across: - * - SourceService.getSourceToken() - * - SourceService.getApiCredential() - * - SourceService.getCredentialId() - * - session-scoped-tools OAuth triggers - * - IPC handlers for credential storage - */ - -import { - inferGoogleServiceFromUrl, - inferSlackServiceFromUrl, - inferMicrosoftServiceFromUrl, - isApiOAuthProvider, - type LoadedSource, - type GoogleService, - type SlackService, - type MicrosoftService, -} from './types.ts'; -import type { CredentialId, StoredCredential } from '../credentials/types.ts'; -import { getCredentialManager } from '../credentials/index.ts'; -import { CraftOAuth, getMcpBaseUrl, type OAuthCallbacks, type OAuthTokens } from '../auth/oauth.ts'; -import { - startGoogleOAuth, - refreshGoogleToken, - type GoogleOAuthResult, - type GoogleOAuthOptions, -} from '../auth/google-oauth.ts'; -import { - startSlackOAuth, - refreshSlackToken, - type SlackOAuthResult, - type SlackOAuthOptions, -} from '../auth/slack-oauth.ts'; -import { - startMicrosoftOAuth, - refreshMicrosoftToken, - type MicrosoftOAuthResult, - type MicrosoftOAuthOptions, -} from '../auth/microsoft-oauth.ts'; -import { debug } from '../utils/debug.ts'; -import { markSourceAuthenticated, loadSourceConfig, saveSourceConfig } from './storage.ts'; - -/** - * Result of authentication attempt - */ -export interface AuthResult { - success: boolean; - error?: string; - /** For Gmail OAuth, includes user's email */ - email?: string; -} - -/** - * API credential types (string for simple auth, object for basic auth) - */ -export type ApiCredential = string | BasicAuthCredential; - -export interface BasicAuthCredential { - username: string; - password: string; -} - -/** - * SourceCredentialManager - unified credential operations for sources - * - * Usage: - * ```typescript - * const credManager = new SourceCredentialManager(); - * - * // Save credentials - * await credManager.save(source, { value: 'token123' }); - * - * // Load credentials - * const cred = await credManager.load(source); - * - * // Run OAuth flow - * const result = await credManager.authenticate(source, { - * onStatus: (msg) => console.log(msg), - * onError: (err) => console.error(err), - * }); - * ``` - */ -export class SourceCredentialManager { - // ============================================================ - // Core CRUD Operations - // ============================================================ - - /** - * Save credential for a source - */ - async save(source: LoadedSource, credential: StoredCredential): Promise { - const credentialId = this.getCredentialId(source); - const manager = getCredentialManager(); - await manager.set(credentialId, credential); - debug(`[SourceCredentialManager] Saved ${credentialId.type} for ${source.config.slug}`); - } - - /** - * Load credential for a source - * - * For MCP sources, tries both OAuth and bearer credentials as fallback - * (credentials may have been stored via different auth modes) - */ - async load(source: LoadedSource): Promise { - const manager = getCredentialManager(); - - // For MCP sources, try both OAuth and bearer credentials - // (stdio transport doesn't need credentials) - if (source.config.type === 'mcp' && source.config.mcp?.transport !== 'stdio' && source.config.mcp?.authType !== 'none') { - return this.loadMcpCredential(source); - } - - // For other sources, use the credential ID based on authType - const credentialId = this.getCredentialId(source); - const cred = await manager.get(credentialId); - - if (cred) { - debug(`[SourceCredentialManager] Found ${credentialId.type} for ${source.config.slug}`); - } - - return cred; - } - - /** - * Load MCP credential with fallback (OAuth -> bearer) - */ - private async loadMcpCredential(source: LoadedSource): Promise { - const manager = getCredentialManager(); - const baseId = { - workspaceId: source.workspaceId, - sourceId: source.config.slug, - }; - - // Try OAuth first - const oauthCreds = await manager.get({ type: 'source_oauth', ...baseId }); - if (oauthCreds?.value) { - debug(`[SourceCredentialManager] Found source_oauth for ${source.config.slug}`); - return oauthCreds; - } - - // Fall back to bearer - const bearerCreds = await manager.get({ type: 'source_bearer', ...baseId }); - if (bearerCreds?.value) { - debug(`[SourceCredentialManager] Found source_bearer for ${source.config.slug}`); - return bearerCreds; - } - - debug(`[SourceCredentialManager] No credential found for MCP source ${source.config.slug}`); - return null; - } - - /** - * Delete credential for a source - */ - async delete(source: LoadedSource): Promise { - const credentialId = this.getCredentialId(source); - const manager = getCredentialManager(); - const deleted = await manager.delete(credentialId); - if (deleted) { - debug(`[SourceCredentialManager] Deleted ${credentialId.type} for ${source.config.slug}`); - } - return deleted; - } - - /** - * Get token value for a source (convenience method) - * Returns null if no credential exists or if expired - */ - async getToken(source: LoadedSource): Promise { - const cred = await this.load(source); - if (!cred?.value) return null; - - // Check expiry - if (this.isExpired(cred)) { - debug(`[SourceCredentialManager] Token expired for ${source.config.slug}`); - return null; - } - - return cred.value; - } - - /** - * Get API credential for a source (handles basic auth JSON parsing) - */ - async getApiCredential(source: LoadedSource): Promise { - const cred = await this.load(source); - if (!cred?.value) return null; - - // Check for basic auth (JSON with username/password) - if (source.config.api?.authType === 'basic') { - try { - const parsed = JSON.parse(cred.value); - if (parsed.username && parsed.password) { - return parsed as BasicAuthCredential; - } - } catch { - // Not JSON, treat as regular credential - } - } - - return cred.value; - } - - // ============================================================ - // Credential ID Resolution - // ============================================================ - - /** - * Get the credential ID for a source - * - * Determines the correct credential type based on: - * - Source type (mcp, api, local) - * - Auth type (oauth, bearer, header, etc.) - */ - getCredentialId(source: LoadedSource): CredentialId { - const mcp = source.config.mcp; - const api = source.config.api; - - let type: CredentialId['type']; - - if (source.config.type === 'mcp') { - type = mcp?.authType === 'bearer' ? 'source_bearer' : 'source_oauth'; - } else if (source.config.type === 'api') { - // OAuth providers (Google/Slack/Microsoft) store credentials as source_oauth. - // This separates HOW we get credentials (OAuth flow) from HOW we send them (Bearer header). - if (isApiOAuthProvider(source.config.provider)) { - type = 'source_oauth'; - } else if (api?.authType === 'bearer') { - type = 'source_bearer'; - } else if (api?.authType === 'basic') { - type = 'source_basic'; - } else { - // header, query, or other → stored as apikey - type = 'source_apikey'; - } - } else { - type = 'source_oauth'; - } - - return { - type, - workspaceId: source.workspaceId, - sourceId: source.config.slug, - }; - } - - // ============================================================ - // Expiry Checking - // ============================================================ - - /** - * Check if a credential is expired - */ - isExpired(credential: StoredCredential): boolean { - if (!credential.expiresAt) return false; - return Date.now() > credential.expiresAt; - } - - /** - * Check if a credential needs refresh (within 5 min of expiry) - */ - needsRefresh(credential: StoredCredential): boolean { - if (!credential.expiresAt) return false; - const fiveMinutes = 5 * 60 * 1000; - return Date.now() > credential.expiresAt - fiveMinutes; - } - - /** - * Mark a source as needing re-authentication. - * Called when token is missing/expired or token refresh fails. - * Updates config.json so the UI shows "needs auth" and the agent gets proper context. - */ - markSourceNeedsReauth(source: LoadedSource, errorMessage: string): void { - try { - const config = loadSourceConfig(source.workspaceRootPath, source.config.slug); - if (config) { - config.isAuthenticated = false; - config.connectionStatus = 'needs_auth'; - config.connectionError = errorMessage; - saveSourceConfig(source.workspaceRootPath, config); - debug(`[SourceCredentialManager] Marked ${source.config.slug} as needing re-auth: ${errorMessage}`); - } - } catch (error) { - debug(`[SourceCredentialManager] Failed to mark ${source.config.slug} as needing re-auth:`, error); - } - } - - /** - * Check if source has valid (non-expired) credentials - */ - async hasValidCredentials(source: LoadedSource): Promise { - const token = await this.getToken(source); - return token !== null; - } - - // ============================================================ - // OAuth Authentication - // ============================================================ - - /** - * Authenticate source via OAuth - * - * Handles both MCP OAuth and Gmail OAuth flows. - * On success, credentials are automatically saved. - */ - async authenticate( - source: LoadedSource, - callbacks?: OAuthCallbacks - ): Promise { - const defaultCallbacks: OAuthCallbacks = { - onStatus: (msg) => debug(`[SourceCredentialManager] ${msg}`), - onError: (err) => debug(`[SourceCredentialManager] Error: ${err}`), - }; - const cb = callbacks || defaultCallbacks; - - // Google APIs use Google OAuth - if (source.config.provider === 'google') { - return this.authenticateGoogle(source, cb); - } - - // Slack APIs use Slack OAuth - if (source.config.provider === 'slack') { - return this.authenticateSlack(source, cb); - } - - // Microsoft APIs use Microsoft OAuth - if (source.config.provider === 'microsoft') { - return this.authenticateMicrosoft(source, cb); - } - - // MCP OAuth flow - if (source.config.type === 'mcp' && source.config.mcp?.authType === 'oauth') { - return this.authenticateMcp(source, cb); - } - - return { - success: false, - error: `Source ${source.config.slug} does not use OAuth authentication`, - }; - } - - /** - * Authenticate MCP source via OAuth - */ - private async authenticateMcp( - source: LoadedSource, - callbacks: OAuthCallbacks - ): Promise { - if (!source.config.mcp?.url) { - return { success: false, error: 'MCP URL not configured' }; - } - - try { - const oauth = new CraftOAuth( - { mcpBaseUrl: getMcpBaseUrl(source.config.mcp.url) }, - callbacks - ); - - const { tokens, clientId } = await oauth.authenticate(); - - // Save the credentials - await this.save(source, { - value: tokens.accessToken, - refreshToken: tokens.refreshToken, - expiresAt: tokens.expiresAt, - clientId, - tokenType: tokens.tokenType, - }); - - // Mark source as authenticated in config.json - markSourceAuthenticated(source.workspaceRootPath, source.config.slug); - - return { success: true }; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - callbacks.onError(message); - return { success: false, error: message }; - } - } - - /** - * Authenticate Google API source via Google OAuth - * - * Supports multiple Google services (Gmail, Calendar, Drive) via: - * - provider: "google" with googleService field - * - provider: "google" with custom googleScopes - * - Inferred from baseUrl (e.g., gmail.googleapis.com → gmail) - */ - private async authenticateGoogle( - source: LoadedSource, - callbacks: OAuthCallbacks - ): Promise { - try { - // Determine service/scopes from config - const api = source.config.api; - let service: GoogleService | undefined; - let scopes: string[] | undefined; - - if (api?.googleScopes && api.googleScopes.length > 0) { - // Custom scopes take precedence - scopes = api.googleScopes; - } else if (api?.googleService) { - // Use predefined service scopes - service = api.googleService; - } else { - // Infer from baseUrl - service = inferGoogleServiceFromUrl(api?.baseUrl); - if (!service) { - return { - success: false, - error: `Cannot determine Google service for source '${source.config.slug}'. Set googleService ('gmail', 'calendar', or 'drive') in api config.`, - }; - } - } - - const serviceName = service || 'Google API'; - callbacks.onStatus(`Starting ${serviceName} OAuth flow...`); - - const options: GoogleOAuthOptions = { - service, - scopes, - appType: 'electron', - }; - - const result: GoogleOAuthResult = await startGoogleOAuth(options); - - if (!result.success) { - return { success: false, error: result.error || 'Google OAuth failed' }; - } - - // Save the credentials - await this.save(source, { - value: result.accessToken!, - refreshToken: result.refreshToken, - expiresAt: result.expiresAt, - }); - - // Mark source as authenticated in config.json - markSourceAuthenticated(source.workspaceRootPath, source.config.slug); - - callbacks.onStatus(`${serviceName} authentication successful`); - return { success: true, email: result.email }; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - callbacks.onError(message); - return { success: false, error: message }; - } - } - - /** - * Authenticate Slack API source via Slack OAuth - * - * Supports multiple Slack services via: - * - provider: "slack" with slackService field - * - provider: "slack" with custom slackBotScopes/slackUserScopes - * - Inferred from baseUrl (slack.com → full) - */ - private async authenticateSlack( - source: LoadedSource, - callbacks: OAuthCallbacks - ): Promise { - try { - // Determine service/scopes from config - const api = source.config.api; - let service: SlackService | undefined; - let userScopes: string[] | undefined; - - if (api?.slackUserScopes && api.slackUserScopes.length > 0) { - // Custom scopes take precedence - userScopes = api.slackUserScopes; - } else if (api?.slackService) { - // Use predefined service scopes - service = api.slackService; - } else { - // Infer from baseUrl (defaults to 'full') - service = inferSlackServiceFromUrl(api?.baseUrl) || 'full'; - } - - const serviceName = service ? `Slack ${service}` : 'Slack'; - callbacks.onStatus(`Starting ${serviceName} OAuth flow...`); - - const options: SlackOAuthOptions = { - service, - userScopes, - appType: 'electron', - }; - - const result: SlackOAuthResult = await startSlackOAuth(options); - - if (!result.success) { - return { success: false, error: result.error || 'Slack OAuth failed' }; - } - - // Save the credentials - await this.save(source, { - value: result.accessToken!, - refreshToken: result.refreshToken, - expiresAt: result.expiresAt, - }); - - // Mark source as authenticated in config.json - markSourceAuthenticated(source.workspaceRootPath, source.config.slug); - - callbacks.onStatus(`${serviceName} authentication successful`); - // Use teamName as the identifier (similar to email for Google) - return { success: true, email: result.teamName }; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - callbacks.onError(message); - return { success: false, error: message }; - } - } - - /** - * Authenticate Microsoft API source via Microsoft OAuth - * - * Supports multiple Microsoft services (Outlook, OneDrive, Calendar, Teams) via: - * - provider: "microsoft" with microsoftService field - * - provider: "microsoft" with custom microsoftScopes - * - Inferred from baseUrl (e.g., graph.microsoft.com → outlook) - */ - private async authenticateMicrosoft( - source: LoadedSource, - callbacks: OAuthCallbacks - ): Promise { - try { - // Determine service/scopes from config - const api = source.config.api; - let service: MicrosoftService | undefined; - let scopes: string[] | undefined; - - if (api?.microsoftScopes && api.microsoftScopes.length > 0) { - // Custom scopes take precedence - scopes = api.microsoftScopes; - } else if (api?.microsoftService) { - // Use predefined service scopes - service = api.microsoftService; - } else { - // Infer from baseUrl - service = inferMicrosoftServiceFromUrl(api?.baseUrl); - if (!service) { - return { - success: false, - error: `Cannot determine Microsoft service for source '${source.config.slug}'. Set microsoftService ('outlook', 'calendar', 'onedrive', 'teams', or 'sharepoint') in api config.`, - }; - } - } - - const serviceName = service || 'Microsoft API'; - callbacks.onStatus(`Starting ${serviceName} OAuth flow...`); - - const options: MicrosoftOAuthOptions = { - service, - scopes, - appType: 'electron', - }; - - const result: MicrosoftOAuthResult = await startMicrosoftOAuth(options); - - if (!result.success) { - return { success: false, error: result.error || 'Microsoft OAuth failed' }; - } - - // Save the credentials - await this.save(source, { - value: result.accessToken!, - refreshToken: result.refreshToken, - expiresAt: result.expiresAt, - }); - - // Mark source as authenticated in config.json - markSourceAuthenticated(source.workspaceRootPath, source.config.slug); - - callbacks.onStatus(`${serviceName} authentication successful`); - return { success: true, email: result.email }; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - callbacks.onError(message); - return { success: false, error: message }; - } - } - - /** - * Refresh token for a source - * - * Returns the new access token, or null if refresh fails. - * On success, credentials are automatically updated. - */ - async refresh(source: LoadedSource): Promise { - const cred = await this.load(source); - if (!cred?.refreshToken) { - debug(`[SourceCredentialManager] No refresh token for ${source.config.slug}`); - return null; - } - - // Google API refresh - if (source.config.provider === 'google') { - return this.refreshGoogle(source, cred); - } - - // Slack API refresh - if (source.config.provider === 'slack') { - return this.refreshSlack(source, cred); - } - - // Microsoft API refresh - if (source.config.provider === 'microsoft') { - return this.refreshMicrosoft(source, cred); - } - - // MCP refresh - if (source.config.type === 'mcp' && source.config.mcp?.url) { - return this.refreshMcp(source, cred); - } - - return null; - } - - /** - * Refresh Google OAuth token - */ - private async refreshGoogle( - source: LoadedSource, - cred: StoredCredential - ): Promise { - try { - const result = await refreshGoogleToken(cred.refreshToken!); - - // Update stored credentials - await this.save(source, { - ...cred, - value: result.accessToken, - expiresAt: result.expiresAt, - }); - - debug(`[SourceCredentialManager] Refreshed Google token for ${source.config.slug}`); - return result.accessToken; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - debug(`[SourceCredentialManager] Google token refresh failed:`, error); - this.markSourceNeedsReauth(source, `Token refresh failed: ${errorMsg}`); - return null; - } - } - - /** - * Refresh Slack OAuth token - */ - private async refreshSlack( - source: LoadedSource, - cred: StoredCredential - ): Promise { - try { - const result = await refreshSlackToken(cred.refreshToken!, cred.clientId); - - // Update stored credentials - await this.save(source, { - ...cred, - value: result.accessToken, - expiresAt: result.expiresAt, - }); - - debug(`[SourceCredentialManager] Refreshed Slack token for ${source.config.slug}`); - return result.accessToken; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - debug(`[SourceCredentialManager] Slack token refresh failed:`, error); - this.markSourceNeedsReauth(source, `Token refresh failed: ${errorMsg}`); - return null; - } - } - - /** - * Refresh Microsoft OAuth token - */ - private async refreshMicrosoft( - source: LoadedSource, - cred: StoredCredential - ): Promise { - try { - const result = await refreshMicrosoftToken(cred.refreshToken!); - - // Update stored credentials (Microsoft may rotate refresh tokens) - await this.save(source, { - ...cred, - value: result.accessToken, - refreshToken: result.refreshToken || cred.refreshToken, - expiresAt: result.expiresAt, - }); - - debug(`[SourceCredentialManager] Refreshed Microsoft token for ${source.config.slug}`); - return result.accessToken; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - debug(`[SourceCredentialManager] Microsoft token refresh failed:`, error); - this.markSourceNeedsReauth(source, `Token refresh failed: ${errorMsg}`); - return null; - } - } - - /** - * Refresh MCP OAuth token - */ - private async refreshMcp( - source: LoadedSource, - cred: StoredCredential - ): Promise { - if (!cred.clientId) { - debug(`[SourceCredentialManager] No clientId for MCP token refresh`); - this.markSourceNeedsReauth(source, 'Missing clientId for token refresh'); - return null; - } - - try { - // Only HTTP/SSE transport can refresh tokens - stdio doesn't use OAuth - if (!source.config.mcp?.url) { - // This is expected for stdio transport - not an error - debug(`[SourceCredentialManager] No URL for MCP token refresh (stdio transport)`); - return null; - } - - const oauth = new CraftOAuth( - { mcpBaseUrl: getMcpBaseUrl(source.config.mcp.url) }, - { - onStatus: () => {}, - onError: () => {}, - } - ); - - const tokens = await oauth.refreshAccessToken(cred.refreshToken!, cred.clientId); - - // Update stored credentials - await this.save(source, { - ...cred, - value: tokens.accessToken, - refreshToken: tokens.refreshToken || cred.refreshToken, - expiresAt: tokens.expiresAt, - }); - - debug(`[SourceCredentialManager] Refreshed MCP token for ${source.config.slug}`); - return tokens.accessToken; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - debug(`[SourceCredentialManager] MCP token refresh failed:`, error); - this.markSourceNeedsReauth(source, `Token refresh failed: ${errorMsg}`); - return null; - } - } -} - -// ============================================================ -// Helper Functions -// ============================================================ - -/** - * Check if a single source needs authentication. - * Returns true if the source requires auth but isn't yet authenticated. - * - * This correctly handles: - * - MCP sources with authType: "none" → never needs auth - * - MCP sources with stdio transport → never needs auth (runs locally) - * - MCP sources with oauth/bearer → needs auth if not authenticated - * - API sources with authType: "none" → never needs auth - * - API sources with bearer/basic/header/query auth → needs auth if not authenticated - */ -export function sourceNeedsAuthentication(source: LoadedSource): boolean { - const mcp = source.config.mcp; - const api = source.config.api; - - // MCP sources with oauth/bearer auth (stdio transport never needs auth) - if (source.config.type === 'mcp' && mcp) { - if (mcp.transport === 'stdio') { - // Stdio sources run locally and don't need authentication - return false; - } - // Only require auth if authType is explicitly set to 'oauth' or 'bearer' - // Undefined or 'none' means no authentication required - if (mcp.authType && mcp.authType !== 'none' && !source.config.isAuthenticated) { - return true; - } - } - - // API sources with auth requirements - if (source.config.type === 'api' && api) { - if (api.authType !== 'none' && api.authType !== undefined && !source.config.isAuthenticated) { - return true; - } - } - - return false; -} - -/** - * Get sources that need authentication - * Returns enabled sources that require auth but aren't yet authenticated - */ -export function getSourcesNeedingAuth(sources: LoadedSource[]): LoadedSource[] { - return sources.filter((source) => { - if (!source.config.enabled) return false; - return sourceNeedsAuthentication(source); - }); -} - -// Singleton instance -let instance: SourceCredentialManager | null = null; - -/** - * Get shared SourceCredentialManager instance - */ -export function getSourceCredentialManager(): SourceCredentialManager { - if (!instance) { - instance = new SourceCredentialManager(); - } - return instance; -} diff --git a/src/main/lib/db/schema/index.ts b/src/main/lib/db/schema/index.ts index fe6aa349..5643794e 100644 --- a/src/main/lib/db/schema/index.ts +++ b/src/main/lib/db/schema/index.ts @@ -1,4 +1,4 @@ -import { index, sqliteTable, text, integer } from "drizzle-orm/sqlite-core" +import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core" import { relations } from "drizzle-orm" import { createId } from "../utils" @@ -20,8 +20,6 @@ export const projects = sqliteTable("projects", { gitProvider: text("git_provider"), // "github" | "gitlab" | "bitbucket" | null gitOwner: text("git_owner"), gitRepo: text("git_repo"), - // Custom project icon (absolute path to local image file) - iconPath: text("icon_path"), }) export const projectsRelations = relations(projects, ({ many }) => ({ @@ -51,9 +49,7 @@ export const chats = sqliteTable("chats", { // PR tracking fields prUrl: text("pr_url"), prNumber: integer("pr_number"), -}, (table) => [ - index("chats_worktree_path_idx").on(table.worktreePath), -]) +}) export const chatsRelations = relations(chats, ({ one, many }) => ({ project: one(projects, { @@ -93,7 +89,6 @@ export const subChatsRelations = relations(subChats, ({ one }) => ({ // ============ CLAUDE CODE CREDENTIALS ============ // Stores encrypted OAuth token for Claude Code integration -// DEPRECATED: Use anthropicAccounts for multi-account support export const claudeCodeCredentials = sqliteTable("claude_code_credentials", { id: text("id").primaryKey().default("default"), // Single row, always "default" oauthToken: text("oauth_token").notNull(), // Encrypted with safeStorage @@ -103,31 +98,6 @@ export const claudeCodeCredentials = sqliteTable("claude_code_credentials", { userId: text("user_id"), // Desktop auth user ID (for reference) }) -// ============ ANTHROPIC ACCOUNTS (Multi-account support) ============ -// Stores multiple Anthropic OAuth accounts for quick switching -export const anthropicAccounts = sqliteTable("anthropic_accounts", { - id: text("id") - .primaryKey() - .$defaultFn(() => createId()), - email: text("email"), // User's email from OAuth (if available) - displayName: text("display_name"), // User-editable label - oauthToken: text("oauth_token").notNull(), // Encrypted with safeStorage - connectedAt: integer("connected_at", { mode: "timestamp" }).$defaultFn( - () => new Date(), - ), - lastUsedAt: integer("last_used_at", { mode: "timestamp" }), - desktopUserId: text("desktop_user_id"), // Reference to 21st.dev user -}) - -// Tracks which Anthropic account is currently active -export const anthropicSettings = sqliteTable("anthropic_settings", { - id: text("id").primaryKey().default("singleton"), // Single row - activeAccountId: text("active_account_id"), // References anthropicAccounts.id - updatedAt: integer("updated_at", { mode: "timestamp" }).$defaultFn( - () => new Date(), - ), -}) - // ============ TYPE EXPORTS ============ export type Project = typeof projects.$inferSelect export type NewProject = typeof projects.$inferInsert @@ -137,6 +107,3 @@ export type SubChat = typeof subChats.$inferSelect export type NewSubChat = typeof subChats.$inferInsert export type ClaudeCodeCredential = typeof claudeCodeCredentials.$inferSelect export type NewClaudeCodeCredential = typeof claudeCodeCredentials.$inferInsert -export type AnthropicAccount = typeof anthropicAccounts.$inferSelect -export type NewAnthropicAccount = typeof anthropicAccounts.$inferInsert -export type AnthropicSettings = typeof anthropicSettings.$inferSelect diff --git a/src/main/lib/git/branches.ts b/src/main/lib/git/branches.ts index b43c3389..4701f9c0 100644 --- a/src/main/lib/git/branches.ts +++ b/src/main/lib/git/branches.ts @@ -252,17 +252,6 @@ export const createBranchesRouter = () => { return { orphanedBranches, deleted }; }), - - fetchRemote: publicProcedure - .input(z.object({ worktreePath: z.string() })) - .mutation(async ({ input }): Promise<{ success: boolean }> => { - assertRegisteredWorktree(input.worktreePath); - return withGitLock(input.worktreePath, async () => { - const git = createGitForNetwork(input.worktreePath); - await git.fetch(["--prune", "--all"]); - return { success: true }; - }); - }), }); }; diff --git a/src/main/lib/git/dictionaries/landscapes.ts b/src/main/lib/git/dictionaries/landscapes.ts deleted file mode 100644 index f3ccba2a..00000000 --- a/src/main/lib/git/dictionaries/landscapes.ts +++ /dev/null @@ -1,147 +0,0 @@ -/** - * Nature/landscape themed nouns for human-readable worktree folder names. - * Used with adjectives from unique-names-generator to produce names like - * "golden-meadow", "quiet-ridge", "misty-canyon". - * - * ~120 words organized by category for easy maintenance. - */ -export const landscapes: string[] = [ - // Water features - "brook", - "creek", - "delta", - "fjord", - "lagoon", - "marsh", - "pond", - "rapids", - "reef", - "river", - "shoal", - "spring", - "strait", - "stream", - "tide", - "bay", - - // Elevated terrain - "bluff", - "butte", - "cliff", - "crag", - "crest", - "dune", - "hill", - "knoll", - "mesa", - "peak", - "ridge", - "summit", - "tor", - - // Low terrain / depressions - "basin", - "canyon", - "cave", - "cove", - "dale", - "dell", - "glade", - "glen", - "gorge", - "grotto", - "gulch", - "hollow", - "ravine", - "vale", - "valley", - - // Flat / open terrain - "field", - "heath", - "moor", - "plain", - "prairie", - "savanna", - "steppe", - "tundra", - - // Forested / vegetated - "copse", - "forest", - "grove", - "jungle", - "meadow", - "orchard", - "thicket", - "woods", - - // Geological - "arch", - "boulder", - "cairn", - "crater", - "ledge", - "quarry", - "shelf", - "spire", - - // Coastal / island - "atoll", - "beach", - "harbor", - "inlet", - "isle", - "shore", - - // Sky / weather / atmospheric - "aurora", - "breeze", - "cloud", - "dawn", - "dusk", - "ember", - "frost", - "glacier", - "haze", - "horizon", - "mist", - "shadow", - "snow", - "storm", - "sunset", - "twilight", - "wind", - - // Other natural features - "cascade", - "clearing", - "crossing", - "falls", - "oasis", - "passage", - "plateau", - "terrace", - "trail", - "woodland", - - // Extra for variety - "anchor", - "beacon", - "canopy", - "drift", - "echo", - "fern", - "haven", - "mirage", - "nectar", - "orbit", - "pebble", - "quartz", - "ripple", - "solstice", - "stone", - "timber", - "vertex", - "zenith", -]; diff --git a/src/main/lib/git/diff-parser.ts b/src/main/lib/git/diff-parser.ts index 98cb7c2e..8c3b270e 100644 --- a/src/main/lib/git/diff-parser.ts +++ b/src/main/lib/git/diff-parser.ts @@ -175,16 +175,6 @@ export function splitUnifiedDiffByFile(diffText: string): ParsedDiffFile[] { let deletions = 0 for (const line of blockLines) { - if (line.startsWith("diff --git ")) { - // Fallback: parse paths from "diff --git a/path b/path" - // Needed for binary files that don't have ---/+++ lines - const match = line.match(/^diff --git a\/(.+) b\/(.+)$/) - if (match) { - if (!oldPath) oldPath = match[1]! - if (!newPath) newPath = match[2]! - } - } - if (line.startsWith("Binary files ") && line.endsWith(" differ")) { isBinary = true } diff --git a/src/main/lib/git/index.ts b/src/main/lib/git/index.ts index eca95f67..849131de 100644 --- a/src/main/lib/git/index.ts +++ b/src/main/lib/git/index.ts @@ -15,7 +15,6 @@ const execAsync = promisify(exec); // Re-export worktree utilities export * from "./worktree"; -export * from "./worktree-naming"; // Re-export GitHub utilities export * from "./github"; diff --git a/src/main/lib/git/sandbox-import.ts b/src/main/lib/git/sandbox-import.ts deleted file mode 100644 index 13ae1872..00000000 --- a/src/main/lib/git/sandbox-import.ts +++ /dev/null @@ -1,426 +0,0 @@ -import { execFile } from "node:child_process"; -import { mkdir, writeFile, unlink } from "node:fs/promises"; -import { join, dirname } from "node:path"; -import { promisify } from "node:util"; -import { tmpdir } from "node:os"; -import simpleGit from "simple-git"; - -const execFileAsync = promisify(execFile); - -/** - * Types for sandbox export NDJSON stream - */ -export interface ExportMeta { - type: "meta"; - branch: string; - baseCommit: string; - headCommit: string; - baseRef: string; - isFullExport?: boolean; - remoteUrl?: string; -} - -export interface ExportBundle { - type: "bundle"; - data: string | null; // base64 encoded -} - -export interface ExportPatch { - type: "staged_patch" | "unstaged_patch"; - data: string | null; -} - -export interface ExportUntracked { - type: "untracked"; - path: string; - data: string; // base64 encoded -} - -export interface ExportDone { - type: "done"; -} - -export interface ExportError { - type: "error"; - error: string; -} - -export interface ExportClaudeSession { - type: "claude_session"; - sessionId: string; - data: string; // Raw JSONL content - metadata: { - firstPrompt: string; - messageCount: number; - created: string; - modified: string; - gitBranch: string; - }; -} - -export type ExportChunk = - | ExportMeta - | ExportBundle - | ExportPatch - | ExportUntracked - | ExportClaudeSession - | ExportDone - | ExportError; - -/** - * Parsed export data from sandbox - */ -export interface SandboxExportData { - meta: ExportMeta; - bundle: Buffer | null; - stagedPatch: string | null; - unstagedPatch: string | null; - untrackedFiles: Array<{ path: string; content: Buffer }>; - claudeSessions: ExportClaudeSession[]; -} - -/** - * Parse NDJSON export stream into structured data - */ -export async function parseExportStream( - stream: ReadableStream, -): Promise { - console.log(`[sandbox-import] parseExportStream starting...`); - const reader = stream.getReader(); - const decoder = new TextDecoder(); - - let buffer = ""; - let chunkCount = 0; - const result: SandboxExportData = { - meta: null as unknown as ExportMeta, - bundle: null, - stagedPatch: null, - unstagedPatch: null, - untrackedFiles: [], - claudeSessions: [], - }; - - while (true) { - const { done, value } = await reader.read(); - - if (done) { - console.log(`[sandbox-import] Stream finished, processed ${chunkCount} chunks`); - break; - } - - buffer += decoder.decode(value, { stream: true }); - - // Process complete lines - const lines = buffer.split("\n"); - buffer = lines.pop() || ""; // Keep incomplete line in buffer - - for (const line of lines) { - if (!line.trim()) continue; - - const chunk = JSON.parse(line) as ExportChunk; - chunkCount++; - console.log(`[sandbox-import] Received chunk ${chunkCount}: type=${chunk.type}`); - - switch (chunk.type) { - case "meta": - result.meta = chunk; - console.log(`[sandbox-import] Meta chunk:`, { - branch: chunk.branch, - baseCommit: chunk.baseCommit, - headCommit: chunk.headCommit, - isFullExport: chunk.isFullExport, - remoteUrl: chunk.remoteUrl, - }); - break; - case "bundle": - if (chunk.data) { - result.bundle = Buffer.from(chunk.data, "base64"); - console.log(`[sandbox-import] Bundle chunk: ${result.bundle.length} bytes`); - } else { - console.log(`[sandbox-import] Bundle chunk: null data`); - } - break; - case "staged_patch": - result.stagedPatch = chunk.data; - console.log(`[sandbox-import] Staged patch chunk: ${chunk.data?.length || 0} chars`); - break; - case "unstaged_patch": - result.unstagedPatch = chunk.data; - console.log(`[sandbox-import] Unstaged patch chunk: ${chunk.data?.length || 0} chars`); - break; - case "untracked": - result.untrackedFiles.push({ - path: chunk.path, - content: Buffer.from(chunk.data, "base64"), - }); - console.log(`[sandbox-import] Untracked file chunk: ${chunk.path}`); - break; - case "claude_session": - result.claudeSessions.push(chunk); - console.log(`[sandbox-import] Claude session chunk: ${chunk.sessionId.slice(0, 8)}... (${chunk.data.length} chars)`); - break; - case "error": - console.error(`[sandbox-import] Error chunk received: ${chunk.error}`); - throw new Error(`Export failed: ${chunk.error}`); - case "done": - console.log(`[sandbox-import] Done chunk received`); - break; - } - } - } - - if (!result.meta) { - console.error(`[sandbox-import] No meta chunk received!`); - throw new Error("Export stream missing metadata"); - } - - console.log(`[sandbox-import] parseExportStream completed:`, { - hasMeta: !!result.meta, - hasBundle: !!result.bundle, - hasStagedPatch: !!result.stagedPatch, - hasUnstagedPatch: !!result.unstagedPatch, - untrackedFilesCount: result.untrackedFiles.length, - claudeSessionsCount: result.claudeSessions.length, - }); - - return result; -} - -/** - * Apply sandbox git state to a worktree - */ -export async function applySandboxGitState( - worktreePath: string, - exportData: SandboxExportData, -): Promise<{ success: boolean; error?: string }> { - console.log(`[sandbox-import] applySandboxGitState starting...`); - console.log(`[sandbox-import] Worktree path: ${worktreePath}`); - - const git = simpleGit(worktreePath); - const isFullExport = exportData.meta.isFullExport ?? false; - console.log(`[sandbox-import] Is full export: ${isFullExport}`); - - try { - // For full exports (cloning to empty repo), set up remote first - if (isFullExport && exportData.meta.remoteUrl) { - console.log(`[sandbox-import] Setting up remote origin: ${exportData.meta.remoteUrl}`); - try { - await git.addRemote("origin", exportData.meta.remoteUrl); - console.log(`[sandbox-import] Added remote origin successfully`); - } catch (err) { - // Remote might already exist - console.log(`[sandbox-import] Remote origin already exists or failed to add:`, err); - } - } - - // 1. Verify base commit exists locally (skip for full exports) - if (!isFullExport) { - const baseCommit = exportData.meta.baseCommit; - try { - await git.raw(["cat-file", "-e", baseCommit]); - } catch { - // Base commit doesn't exist locally, try to fetch - console.log(`[sandbox-import] Base commit ${baseCommit} not found locally, fetching...`); - try { - await git.fetch("origin"); - } catch (fetchError) { - console.warn(`[sandbox-import] Fetch failed: ${fetchError}`); - } - } - } - - // 2. Apply git bundle (if there are new commits) - if (exportData.bundle) { - console.log(`[sandbox-import] Step 2: Applying git bundle (${exportData.bundle.length} bytes)...`); - const bundlePath = join(tmpdir(), `sandbox-import-${Date.now()}.bundle`); - console.log(`[sandbox-import] Bundle temp path: ${bundlePath}`); - try { - await writeFile(bundlePath, exportData.bundle); - console.log(`[sandbox-import] Bundle written to temp file`); - - // Verify the bundle is valid - console.log(`[sandbox-import] Verifying bundle...`); - const verifyResult = await execFileAsync("git", ["-C", worktreePath, "bundle", "verify", bundlePath], { - timeout: 30_000, - }); - console.log(`[sandbox-import] Bundle verify output:`, verifyResult.stdout); - - if (isFullExport) { - // Full export: fetch all refs from bundle, then checkout the branch - // Use --update-head-ok to allow fetching into the currently checked out branch - console.log(`[sandbox-import] Full export: fetching all refs from bundle...`); - const fetchResult = await execFileAsync( - "git", - ["-C", worktreePath, "fetch", "--update-head-ok", bundlePath, "refs/heads/*:refs/heads/*"], - { timeout: 120_000 }, - ); - console.log(`[sandbox-import] Fetch output:`, fetchResult.stdout, fetchResult.stderr); - - // Checkout the branch that was active in the sandbox (with force to update working tree) - const targetBranch = exportData.meta.branch; - console.log(`[sandbox-import] Checking out branch: ${targetBranch}`); - await git.checkout(["-f", targetBranch]); - console.log(`[sandbox-import] Checked out branch successfully: ${targetBranch}`); - } else { - // Delta export: fetch HEAD to temp branch, then reset - await execFileAsync( - "git", - ["-C", worktreePath, "fetch", bundlePath, "HEAD:sandbox-import-temp"], - { timeout: 60_000 }, - ); - - // Reset to the fetched commits - await git.reset(["--hard", "sandbox-import-temp"]); - - // Clean up temp branch - await git.branch(["-D", "sandbox-import-temp"]).catch(() => {}); - } - } finally { - await unlink(bundlePath).catch(() => {}); - } - } - - // 3. Apply staged patch (stage the changes) - if (exportData.stagedPatch) { - console.log(`[sandbox-import] Step 3: Applying staged patch (${exportData.stagedPatch.length} chars)...`); - const stagedPatchPath = join(tmpdir(), `sandbox-staged-${Date.now()}.patch`); - try { - await writeFile(stagedPatchPath, exportData.stagedPatch); - - // Apply and stage - await execFileAsync( - "git", - ["-C", worktreePath, "apply", "--cached", stagedPatchPath], - { timeout: 60_000 }, - ); - console.log(`[sandbox-import] Staged patch applied successfully`); - - // Also apply to working directory - await execFileAsync("git", ["-C", worktreePath, "checkout", "--", "."], { - timeout: 30_000, - }).catch(() => {}); - } catch (error) { - console.warn(`[sandbox-import] Failed to apply staged patch: ${error}`); - } finally { - await unlink(stagedPatchPath).catch(() => {}); - } - } else { - console.log(`[sandbox-import] Step 3: No staged patch to apply`); - } - - // 4. Apply unstaged patch (don't stage) - if (exportData.unstagedPatch) { - console.log(`[sandbox-import] Step 4: Applying unstaged patch (${exportData.unstagedPatch.length} chars)...`); - const unstagedPatchPath = join(tmpdir(), `sandbox-unstaged-${Date.now()}.patch`); - try { - await writeFile(unstagedPatchPath, exportData.unstagedPatch); - - await execFileAsync("git", ["-C", worktreePath, "apply", unstagedPatchPath], { - timeout: 60_000, - }); - console.log(`[sandbox-import] Unstaged patch applied successfully`); - } catch (error) { - console.warn(`[sandbox-import] Failed to apply unstaged patch: ${error}`); - } finally { - await unlink(unstagedPatchPath).catch(() => {}); - } - } else { - console.log(`[sandbox-import] Step 4: No unstaged patch to apply`); - } - - // 5. Write untracked files - console.log(`[sandbox-import] Step 5: Writing ${exportData.untrackedFiles.length} untracked files...`); - for (const file of exportData.untrackedFiles) { - const fullPath = join(worktreePath, file.path); - try { - await mkdir(dirname(fullPath), { recursive: true }); - await writeFile(fullPath, file.content); - console.log(`[sandbox-import] Wrote untracked file: ${file.path}`); - } catch (error) { - console.warn(`[sandbox-import] Failed to write untracked file ${file.path}: ${error}`); - } - } - - console.log(`[sandbox-import] applySandboxGitState completed successfully!`); - return { success: true }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - console.error(`[sandbox-import] applySandboxGitState FAILED: ${errorMessage}`); - console.error(`[sandbox-import] Stack:`, error); - return { success: false, error: errorMessage }; - } -} - -/** - * Fetch and apply sandbox export to a worktree - * @param fullExport - If true, exports entire repo (for cloning to empty local repo) - * @param sessionId - If provided, only export this specific Claude session (for subchat import) - */ -export async function importSandboxToWorktree( - worktreePath: string, - apiUrl: string, - sandboxId: string, - token: string, - fullExport: boolean = false, - sessionId?: string, -): Promise<{ success: boolean; error?: string; claudeSessions?: ExportClaudeSession[] }> { - try { - // Fetch export stream from API - const queryParams: string[] = []; - if (fullExport) queryParams.push("full=true"); - if (sessionId) queryParams.push(`sessionId=${sessionId}`); - const queryString = queryParams.length > 0 ? `?${queryParams.join("&")}` : ""; - const exportUrl = `${apiUrl}/api/agents/sandbox/${sandboxId}/export${queryString}`; - console.log(`[OPEN-LOCALLY] Fetching sandbox export from: ${exportUrl}`); - - const response = await fetch(exportUrl, { - method: "GET", - headers: { - "X-Desktop-Token": token, - Accept: "application/x-ndjson", - }, - }); - - console.log(`[sandbox-import] Export response status: ${response.status} ${response.statusText}`); - - if (!response.ok) { - throw new Error(`Export API returned ${response.status}: ${response.statusText}`); - } - - if (!response.body) { - throw new Error("Export API returned no body"); - } - - // Parse the export stream - console.log(`[sandbox-import] Parsing export stream...`); - const exportData = await parseExportStream(response.body); - - console.log(`[sandbox-import] Export data parsed:`, { - branch: exportData.meta.branch, - baseCommit: exportData.meta.baseCommit, - headCommit: exportData.meta.headCommit, - isFullExport: exportData.meta.isFullExport, - remoteUrl: exportData.meta.remoteUrl, - hasBundle: !!exportData.bundle, - bundleSize: exportData.bundle?.length, - hasStagedPatch: !!exportData.stagedPatch, - hasUnstagedPatch: !!exportData.unstagedPatch, - untrackedFilesCount: exportData.untrackedFiles.length, - claudeSessionsCount: exportData.claudeSessions.length, - }); - - // Apply the git state - console.log(`[sandbox-import] Applying git state to worktree: ${worktreePath}`); - const gitResult = await applySandboxGitState(worktreePath, exportData); - - // Return claudeSessions along with git result - return { - ...gitResult, - claudeSessions: exportData.claudeSessions, - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - console.error(`[sandbox-import] Import failed:`, errorMessage); - return { success: false, error: errorMessage }; - } -} diff --git a/src/main/lib/git/security/git-commands.ts b/src/main/lib/git/security/git-commands.ts index 640eac53..0c555ff9 100644 --- a/src/main/lib/git/security/git-commands.ts +++ b/src/main/lib/git/security/git-commands.ts @@ -79,32 +79,6 @@ export async function gitCheckoutFile( await withLockRetry(worktreePath, () => git.checkout(["--", filePath])); } -/** - * Checkout (restore) multiple file paths, discarding local changes. - * - * Uses `git checkout -- ` to restore multiple files at once, - * avoiding multiple sequential git calls and lock conflicts. - */ -export async function gitCheckoutFiles( - worktreePath: string, - filePaths: string[], -): Promise { - assertRegisteredWorktree(worktreePath); - for (const filePath of filePaths) { - assertValidGitPath(filePath); - } - - if (filePaths.length === 0) return; - - const git = createGit(worktreePath); - - // Process in batches to avoid command line length limits - for (let i = 0; i < filePaths.length; i += BATCH_SIZE) { - const batch = filePaths.slice(i, i + BATCH_SIZE); - await withLockRetry(worktreePath, () => git.checkout(["--", ...batch])); - } -} - /** * Stage a file for commit. * diff --git a/src/main/lib/git/security/index.ts b/src/main/lib/git/security/index.ts index 0d1c9a70..bd5055ca 100644 --- a/src/main/lib/git/security/index.ts +++ b/src/main/lib/git/security/index.ts @@ -10,7 +10,6 @@ export { gitCheckoutFile, - gitCheckoutFiles, gitStageAll, gitStageFile, gitStageFiles, diff --git a/src/main/lib/git/shell-env.ts b/src/main/lib/git/shell-env.ts index 53ad9ac4..c59efa72 100644 --- a/src/main/lib/git/shell-env.ts +++ b/src/main/lib/git/shell-env.ts @@ -3,7 +3,6 @@ import { execFile, } from "node:child_process"; import os from "node:os"; -import path from "node:path"; import { promisify } from "node:util"; const execFileAsync = promisify(execFile); @@ -20,56 +19,14 @@ let pathFixAttempted = false; let pathFixSucceeded = false; /** - * Build Windows PATH by combining process.env.PATH with common install locations. - * This ensures packaged apps on Windows can find user-installed tools. - */ -function buildWindowsPath(): string { - const paths: string[] = []; - const pathSeparator = ";"; - - // Start with existing PATH from process.env - if (process.env.PATH) { - paths.push(...process.env.PATH.split(pathSeparator).filter(Boolean)); - } - - // Add Windows-specific common paths - const commonPaths = [ - // User-local installations (where tools like Claude CLI, git-lfs are often installed) - path.join(os.homedir(), ".local", "bin"), - // Git for Windows default location - "C:\\Program Files\\Git\\cmd", - "C:\\Program Files\\Git\\bin", - // System paths (usually already in PATH, but ensure they're present) - path.join(process.env.SystemRoot || "C:\\Windows", "System32"), - path.join(process.env.SystemRoot || "C:\\Windows"), - ]; - - // Add common paths that aren't already in PATH - for (const commonPath of commonPaths) { - const normalizedPath = path.normalize(commonPath); - // Case-insensitive check for Windows - const normalizedLower = normalizedPath.toLowerCase(); - const alreadyExists = paths.some( - (p) => path.normalize(p).toLowerCase() === normalizedLower, - ); - if (!alreadyExists) { - paths.push(normalizedPath); - } - } - - return paths.join(pathSeparator); -} - -/** - * Gets the full shell environment with proper PATH for all platforms. + * Gets the full shell environment by spawning a login shell. + * This captures PATH and other environment variables set in shell profiles + * which includes tools like git-lfs installed via homebrew. * - * - **Windows**: Derives PATH from process.env + common install locations (no shell spawn) - * - **macOS/Linux**: Spawns login shell to capture PATH from shell profiles + * Uses -lc (login, command) instead of -ilc to avoid interactive prompts + * and TTY issues from dotfiles expecting a terminal. * - * This captures PATH and other environment variables needed to find user-installed tools - * like git-lfs (homebrew on macOS) or Claude CLI (user-local on Windows). - * - * Results are cached for 1 minute to avoid repeated operations. + * Results are cached for 1 minute to avoid spawning shells repeatedly. */ export async function getShellEnvironment(): Promise> { const now = Date.now(); @@ -79,38 +36,6 @@ export async function getShellEnvironment(): Promise> { return { ...cachedEnv }; } - // Windows: derive PATH without shell invocation - // Git Bash PATH doesn't include Windows user paths, so we build it manually - if (process.platform === "win32") { - console.log( - "[shell-env] Windows detected, deriving PATH without shell invocation", - ); - const env: Record = { - ...process.env, - PATH: buildWindowsPath(), - HOME: os.homedir(), - USER: os.userInfo().username, - USERPROFILE: os.homedir(), - }; - - // Ensure all values are strings - const stringEnv: Record = {}; - for (const [key, value] of Object.entries(env)) { - if (typeof value === "string") { - stringEnv[key] = value; - } - } - - cachedEnv = stringEnv; - cacheTime = now; - isFallbackCache = false; - console.log( - `[shell-env] Built Windows environment with ${Object.keys(stringEnv).length} vars`, - ); - return { ...stringEnv }; - } - - // macOS/Linux: spawn login shell to get full environment const shell = process.env.SHELL || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash"); diff --git a/src/main/lib/git/staging.ts b/src/main/lib/git/staging.ts index 49c824b9..068c7115 100644 --- a/src/main/lib/git/staging.ts +++ b/src/main/lib/git/staging.ts @@ -2,7 +2,6 @@ import { z } from "zod"; import { publicProcedure, router } from "../trpc"; import { gitCheckoutFile, - gitCheckoutFiles, gitStageAll, gitStageFile, gitStageFiles, @@ -99,31 +98,5 @@ export const createStagingRouter = () => { await secureFs.delete(input.worktreePath, input.filePath); return { success: true }; }), - - discardMultipleChanges: publicProcedure - .input( - z.object({ - worktreePath: z.string(), - filePaths: z.array(z.string()), - }), - ) - .mutation(async ({ input }): Promise<{ success: boolean }> => { - await gitCheckoutFiles(input.worktreePath, input.filePaths); - return { success: true }; - }), - - deleteMultipleUntracked: publicProcedure - .input( - z.object({ - worktreePath: z.string(), - filePaths: z.array(z.string()), - }), - ) - .mutation(async ({ input }): Promise<{ success: boolean }> => { - for (const filePath of input.filePaths) { - await secureFs.delete(input.worktreePath, filePath); - } - return { success: true }; - }), }); }; diff --git a/src/main/lib/git/stash.ts b/src/main/lib/git/stash.ts index e0af6bb2..2734ad61 100644 --- a/src/main/lib/git/stash.ts +++ b/src/main/lib/git/stash.ts @@ -105,15 +105,10 @@ function parseCheckpointTrees( } } -export type RollbackResult = - | { success: true; checkpointFound: true } - | { success: true; checkpointFound: false } - | { success: false; error: string } - export async function applyRollbackStash( worktreePath: string, sdkMessageUuid: string, -): Promise { +) { try { const git = simpleGit(worktreePath) @@ -125,9 +120,7 @@ export async function applyRollbackStash( console.warn( `[claude] Rollback checkpoint not found for sdkMessageUuid=${sdkMessageUuid}`, ) - // Checkpoint not found - return success but indicate no checkpoint was applied - // The caller can decide whether to proceed with message truncation - return { success: true, checkpointFound: false } + return true // This is fine, just skip } const commitMessage = await git.raw([ @@ -141,7 +134,7 @@ export async function applyRollbackStash( console.error( `[claude] Rollback checkpoint missing tree metadata for sdkMessageUuid=${sdkMessageUuid}`, ) - return { success: false, error: "Checkpoint missing tree metadata" } + return false } let lastError: unknown @@ -151,7 +144,7 @@ export async function applyRollbackStash( await git.raw(["checkout-index", "-a", "-f"]) await git.raw(["clean", "-fd"]) await git.raw(["read-tree", indexTree]) - return { success: true, checkpointFound: true } + return true } catch (error) { lastError = error if (attempt < APPLY_RETRIES) { @@ -162,7 +155,6 @@ export async function applyRollbackStash( throw lastError } catch (e) { console.error("[claude] Failed to apply rollback checkpoint:", e) - const errorMessage = e instanceof Error ? e.message : "Unknown error" - return { success: false, error: errorMessage } + return false } } diff --git a/src/main/lib/git/watcher/ipc-bridge.ts b/src/main/lib/git/watcher/ipc-bridge.ts index f916bf37..22f13b21 100644 --- a/src/main/lib/git/watcher/ipc-bridge.ts +++ b/src/main/lib/git/watcher/ipc-bridge.ts @@ -7,19 +7,20 @@ import { gitCache } from "../cache"; * Handles subscription/unsubscription from renderer and forwards file change events. */ -// Track active subscriptions per worktree with subscribing window ID -// This ensures events are sent to the window that subscribed, not the focused window -const activeSubscriptions: Map void }> = new Map(); +// Track active subscriptions per worktree +const activeSubscriptions: Map void> = new Map(); /** * Register IPC handlers for git watcher. * Call this once during app initialization. */ -export function registerGitWatcherIPC(): void { +export function registerGitWatcherIPC( + getWindow: () => BrowserWindow | null, +): void { // Handle subscription requests from renderer ipcMain.handle( "git:subscribe-watcher", - async (event, worktreePath: string) => { + async (_event, worktreePath: string) => { if (!worktreePath) return; // Already subscribed? @@ -27,22 +28,12 @@ export function registerGitWatcherIPC(): void { return; } - // Get the window that made the subscription request - const subscribingWindow = BrowserWindow.fromWebContents(event.sender); - if (!subscribingWindow || subscribingWindow.isDestroyed()) return; - - const windowId = subscribingWindow.id; - // Subscribe to file changes (await to ensure watcher is ready) const unsubscribe = await gitWatcherRegistry.subscribe( worktreePath, - (watchEvent: GitWatchEvent) => { - // Send to the subscribing window, not the focused window - const subscription = activeSubscriptions.get(worktreePath); - if (!subscription) return; - - const targetWindow = BrowserWindow.fromId(subscription.windowId); - if (!targetWindow || targetWindow.isDestroyed()) return; + (event: GitWatchEvent) => { + const win = getWindow(); + if (!win || win.isDestroyed()) return; // We're watching .git/index and .git/HEAD, so any event means a git operation occurred. // Invalidate status and parsedDiff caches - these are always affected by git operations. @@ -51,20 +42,16 @@ export function registerGitWatcherIPC(): void { gitCache.invalidateParsedDiff(worktreePath); // Send event to renderer - try { - targetWindow.webContents.send("git:status-changed", { - worktreePath: watchEvent.worktreePath, - changes: watchEvent.changes, - }); - } catch { - // Window may have been destroyed between check and send - } + win.webContents.send("git:status-changed", { + worktreePath: event.worktreePath, + changes: event.changes, + }); }, ); - activeSubscriptions.set(worktreePath, { windowId, unsubscribe }); + activeSubscriptions.set(worktreePath, unsubscribe); console.log( - `[GitWatcher] Window ${windowId} subscribed to: ${worktreePath}`, + `[GitWatcher] Subscribed to: ${worktreePath}`, ); }, ); @@ -75,41 +62,27 @@ export function registerGitWatcherIPC(): void { async (_event, worktreePath: string) => { if (!worktreePath) return; - const subscription = activeSubscriptions.get(worktreePath); - if (subscription) { - subscription.unsubscribe(); + const unsubscribe = activeSubscriptions.get(worktreePath); + if (unsubscribe) { + unsubscribe(); activeSubscriptions.delete(worktreePath); console.log( - `[GitWatcher] Window ${subscription.windowId} unsubscribed from: ${worktreePath}`, + `[GitWatcher] Unsubscribed from: ${worktreePath}`, ); } }, ); } -/** - * Cleanup subscriptions for a specific window. - * Call this when a window is closed to prevent memory leaks. - */ -export function cleanupWindowSubscriptions(windowId: number): void { - for (const [path, subscription] of activeSubscriptions) { - if (subscription.windowId === windowId) { - subscription.unsubscribe(); - activeSubscriptions.delete(path); - console.log(`[GitWatcher] Cleaned up subscription for closed window ${windowId}: ${path}`); - } - } -} - /** * Cleanup all watchers. * Call this when the app is shutting down. */ export async function cleanupGitWatchers(): Promise { // Unsubscribe all - const subscriptions = Array.from(activeSubscriptions.values()); - for (const subscription of subscriptions) { - subscription.unsubscribe(); + const unsubscribers = Array.from(activeSubscriptions.values()); + for (const unsubscribe of unsubscribers) { + unsubscribe(); } activeSubscriptions.clear(); diff --git a/src/main/lib/git/worktree-config.ts b/src/main/lib/git/worktree-config.ts index 30529a26..e766bf1e 100644 --- a/src/main/lib/git/worktree-config.ts +++ b/src/main/lib/git/worktree-config.ts @@ -1,5 +1,5 @@ import { readFile, writeFile, mkdir, access } from "node:fs/promises" -import { join, dirname, isAbsolute } from "node:path" +import { join, dirname } from "node:path" import { exec } from "node:child_process" import { promisify } from "node:util" @@ -50,7 +50,7 @@ export async function detectWorktreeConfig( ): Promise { // 1. Check custom path if provided if (customPath) { - const fullPath = isAbsolute(customPath) + const fullPath = customPath.startsWith("/") ? customPath : join(projectPath, customPath) const config = await readJsonFile(fullPath) @@ -122,7 +122,7 @@ export async function saveWorktreeConfig( targetPath = join(projectPath, ONECODE_CONFIG_PATH) } else { // Custom path - targetPath = isAbsolute(target) ? target : join(projectPath, target) + targetPath = target.startsWith("/") ? target : join(projectPath, target) } try { diff --git a/src/main/lib/git/worktree-naming.ts b/src/main/lib/git/worktree-naming.ts deleted file mode 100644 index ac8ee3c4..00000000 --- a/src/main/lib/git/worktree-naming.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { existsSync } from "node:fs"; -import { join } from "node:path"; -import { - adjectives, - uniqueNamesGenerator, -} from "unique-names-generator"; -import { landscapes } from "./dictionaries/landscapes"; - -const MAX_RETRIES = 10; - -function generateLandscapeName(): string { - return uniqueNamesGenerator({ - dictionaries: [adjectives, landscapes], - separator: "-", - length: 2, - style: "lowerCase", - }); -} - -/** - * Sanitize a project name for use as a filesystem directory name. - * Lowercases, replaces spaces/underscores with hyphens, strips special characters. - * Truncates to 50 characters to stay within filesystem path length limits. - * - * Slug collisions (e.g., "My Project" and "my_project" both becoming "my-project") - * are safe because resolveProjectPathFromWorktree() looks up the full worktree path - * via the chats table, not just the project folder name. - */ -export function sanitizeProjectName(name: string): string { - const sanitized = name - .toLowerCase() - .replace(/[\s_]+/g, "-") - .replace(/[^a-z0-9\-.]/g, "") - .replace(/-{2,}/g, "-") - .replace(/^-+|-+$/g, "") - .slice(0, 50); - - return sanitized || "project"; -} - -/** - * Generate a unique, human-readable folder name for a worktree. - * Uses adjective-landscape pattern (e.g., "golden-meadow", "quiet-ridge"). - * Checks the parent directory for existing folders to avoid collisions. - * Falls back to appending a numeric suffix if random generation keeps colliding. - * - * Note: There is a theoretical TOCTOU race between existsSync and the actual - * git worktree add. In practice this is negligible (180k combinations, single - * local user). If it occurs, git worktree add fails atomically and the error - * is caught by createWorktreeForChat(). - */ -export function generateWorktreeFolderName(parentDir: string): string { - for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { - const name = generateLandscapeName(); - if (!existsSync(join(parentDir, name))) { - return name; - } - } - - // Fallback: generate a base name and append numeric suffix - const baseName = generateLandscapeName(); - - for (let suffix = 2; suffix <= 999; suffix++) { - const name = `${baseName}-${suffix}`; - if (!existsSync(join(parentDir, name))) { - return name; - } - } - - // Absolute fallback: append timestamp - return `${baseName}-${Date.now().toString(36)}`; -} diff --git a/src/main/lib/git/worktree.ts b/src/main/lib/git/worktree.ts index c8963c2a..ff080155 100644 --- a/src/main/lib/git/worktree.ts +++ b/src/main/lib/git/worktree.ts @@ -1,7 +1,7 @@ import { execFile } from "node:child_process"; import { randomBytes } from "node:crypto"; import { mkdir, readFile, stat } from "node:fs/promises"; -import { devNull, homedir } from "node:os"; +import { homedir } from "node:os"; import { join } from "node:path"; import { promisify } from "node:util"; import simpleGit from "simple-git"; @@ -12,7 +12,6 @@ import { } from "unique-names-generator"; import { checkGitLfsAvailable, getShellEnvironment } from "./shell-env"; import { executeWorktreeSetup } from "./worktree-config"; -import { generateWorktreeFolderName } from "./worktree-naming"; const execFileAsync = promisify(execFile); @@ -151,21 +150,6 @@ export async function createWorktree( } } - // Resolve startPoint to commit hash to avoid Windows escaping issues with ^{commit} - const git = simpleGit(mainRepoPath); - let commitHash: string; - try { - commitHash = (await git.revparse([`${startPoint}^{commit}`])).trim(); - } catch { - // Fallback to local branch if origin/branch doesn't exist - const localBranch = startPoint.replace(/^origin\//, ""); - try { - commitHash = (await git.revparse([`${localBranch}^{commit}`])).trim(); - } catch { - commitHash = (await git.revparse([startPoint])).trim(); - } - } - await execFileAsync( "git", [ @@ -176,7 +160,10 @@ export async function createWorktree( worktreePath, "-b", branch, - commitHash, + // Append ^{commit} to force Git to treat the startPoint as a commit, + // not a branch ref. This prevents implicit upstream tracking when + // creating a new branch from a remote branch like origin/main. + `${startPoint}^{commit}`, ], { env, timeout: 120_000 }, ); @@ -898,16 +885,15 @@ export interface WorktreeResult { /** * Create a git worktree for a chat (wrapper for chats.ts) * @param projectPath - Path to the main repository - * @param projectSlug - Sanitized project name for worktree directory - * @param chatId - Chat ID (used for logging) + * @param projectId - Project ID for worktree directory + * @param chatId - Chat ID for worktree directory * @param selectedBaseBranch - Optional branch to base the worktree off (defaults to auto-detected default branch) */ export async function createWorktreeForChat( projectPath: string, - projectSlug: string, + projectId: string, chatId: string, selectedBaseBranch?: string, - branchType?: "local" | "remote", ): Promise { try { const git = simpleGit(projectPath); @@ -922,16 +908,9 @@ export async function createWorktreeForChat( const branch = generateBranchName(); const worktreesDir = join(homedir(), ".21st", "worktrees"); - const projectWorktreeDir = join(worktreesDir, projectSlug); - const folderName = generateWorktreeFolderName(projectWorktreeDir); - const worktreePath = join(projectWorktreeDir, folderName); + const worktreePath = join(worktreesDir, projectId, chatId); - // Determine startPoint based on branch type - // For local branches, use the local ref directly - // For remote branches or when type is not specified, use origin/{branch} - const startPoint = branchType === "local" ? baseBranch : `origin/${baseBranch}`; - - await createWorktree(projectPath, branch, worktreePath, startPoint); + await createWorktree(projectPath, branch, worktreePath, `origin/${baseBranch}`); // Run worktree setup commands in BACKGROUND (don't block chat creation) // This allows the user to start chatting immediately while deps install @@ -1007,7 +986,7 @@ export async function getWorktreeDiff( "diff", "--no-color", "--no-index", - devNull, + "/dev/null", file, ]); if (fileDiff) { @@ -1043,14 +1022,9 @@ export async function getWorktreeDiff( // All committed - diff against base branch const targetBranch = baseBranch || await getDefaultBranch(worktreePath); - // Use origin if available, fallback to local branch - const baseRef = await refExistsLocally(worktreePath, `origin/${targetBranch}`) - ? `origin/${targetBranch}` - : targetBranch; - try { const diff = await git.diff([ - `${baseRef}...HEAD`, + `origin/${targetBranch}...HEAD`, "--no-color", "--", ":!*.lock", diff --git a/src/main/lib/mcp-auth.ts b/src/main/lib/mcp-auth.ts deleted file mode 100644 index f2464686..00000000 --- a/src/main/lib/mcp-auth.ts +++ /dev/null @@ -1,508 +0,0 @@ -import { Client } from '@modelcontextprotocol/sdk/client/index.js'; -import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; -import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; -import { BrowserWindow, shell } from 'electron'; -import { - getMcpServerConfig, - GLOBAL_MCP_PATH, - readClaudeConfig, - updateClaudeConfigAtomic, - updateMcpServerConfig, -} from './claude-config'; -import { getClaudeShellEnvironment } from './claude/env'; -import { CraftOAuth, fetchOAuthMetadata, getMcpBaseUrl, type OAuthMetadata, type OAuthTokens } from './oauth'; -import { discoverPluginMcpServers } from './plugins'; -import { bringToFront } from './window'; - - -/** - * Fetch tools from an MCP server using the official MCP SDK - * @param serverUrl The MCP server URL - * @param accessToken Optional access token (not needed for public MCPs) - */ -export interface McpToolInfo { - name: string; - description?: string; -} - -export async function fetchMcpTools( - serverUrl: string, - headers?: Record -): Promise { - let client: Client | null = null; - let transport: StreamableHTTPClientTransport | null = null; - - try { - client = new Client({ - name: '21st-desktop', - version: '1.0.0', - }); - - const requestInit: RequestInit = {}; - if (headers && Object.keys(headers).length > 0) { - requestInit.headers = { ...headers }; - } - - transport = new StreamableHTTPClientTransport(new URL(serverUrl), { - requestInit, - }); - - await client.connect(transport); - - const result = await client.listTools(); - const tools = result.tools || []; - - console.log(`[MCP] Fetched ${tools.length} tools via SDK`); - return tools.map(t => ({ name: t.name, description: t.description })); - } catch (error) { - console.error('[MCP] Failed to fetch tools:', error); - return []; - } finally { - // Clean up the connection - try { - if (transport) { - await transport.close(); - } - } catch { - // Ignore close errors - } - } -} - -/** - * Sensitive env vars to filter out when spawning MCP subprocesses - */ -const BLOCKED_ENV_VARS = [ - 'ANTHROPIC_API_KEY', - 'CLAUDE_CODE_OAUTH_TOKEN', - 'AWS_ACCESS_KEY_ID', - 'AWS_SECRET_ACCESS_KEY', - 'AWS_SESSION_TOKEN', - 'GITHUB_TOKEN', - 'GH_TOKEN', - 'OPENAI_API_KEY', -]; - -/** - * Fetch tools from a stdio-based MCP server - * Uses shell environment to ensure proper PATH (homebrew, nvm, etc.) in production - */ -export async function fetchMcpToolsStdio(config: { - command: string; - args?: string[]; - env?: Record; -}): Promise { - let transport: StdioClientTransport | null = null; - - try { - const client = new Client({ - name: '21st-desktop', - version: '1.0.0', - }); - - // Get shell environment with proper PATH (includes homebrew, nvm, etc.) - // This is critical for production where Electron apps launched from Finder - // have a minimal PATH that excludes user-installed tools - const shellEnv = getClaudeShellEnvironment(); - - // Filter sensitive env vars - const safeEnv: Record = {}; - for (const [key, value] of Object.entries(shellEnv)) { - if (!BLOCKED_ENV_VARS.includes(key)) { - safeEnv[key] = value; - } - } - - transport = new StdioClientTransport({ - command: config.command, - args: config.args, - env: { ...safeEnv, ...config.env }, - }); - - await client.connect(transport); - const result = await client.listTools(); - const tools = result.tools || []; - - console.log(`[MCP] Fetched ${tools.length} tools via stdio`); - return tools.map(t => ({ name: t.name, description: t.description })); - } catch (error) { - console.error('[MCP] Failed to fetch tools via stdio:', error); - return []; - } finally { - try { - if (transport) { - await transport.close(); - } - } catch { - // Ignore close errors - } - } -} - -import { AUTH_SERVER_PORT, IS_DEV } from '../constants'; - -const OAUTH_TIMEOUT_MS = 5 * 60 * 1000; - -function getMcpOAuthRedirectUri(): string { - return IS_DEV - ? `http://localhost:${AUTH_SERVER_PORT}/callback` - : `http://127.0.0.1:${AUTH_SERVER_PORT}/callback`; -} - -interface PendingOAuth { - serverName: string; - projectPath: string; - codeVerifier: string; - tokenEndpoint: string; - clientId: string; - clientSecret?: string; - redirectUri: string; - resolve: (result: { success: boolean; error?: string }) => void; - timeoutId: NodeJS.Timeout; -} - -const pendingOAuthFlows = new Map(); - -/** - * Start MCP OAuth flow for a server - * Fetches OAuth metadata from .well-known endpoint - */ -export async function startMcpOAuth( - serverName: string, - projectPath: string -): Promise<{ success: boolean; error?: string }> { - // 1. Read server config from ~/.claude.json - const config = await readClaudeConfig(); - let serverConfig = getMcpServerConfig(config, projectPath, serverName); - - // Fallback: check plugin MCP servers if not found in ~/.claude.json - if (!serverConfig?.url) { - const pluginMcpConfigs = await discoverPluginMcpServers(); - for (const pluginConfig of pluginMcpConfigs) { - if (pluginConfig.mcpServers[serverName]) { - serverConfig = pluginConfig.mcpServers[serverName]; - // Save plugin server config to ~/.claude.json so token storage works - await updateClaudeConfigAtomic((cfg) => { - return updateMcpServerConfig(cfg, GLOBAL_MCP_PATH, serverName, { - url: serverConfig!.url, - type: serverConfig!.url?.endsWith('/sse') ? 'sse' : 'http', - authType: 'oauth', - }); - }); - break; - } - } - } - - if (!serverConfig?.url) { - return { success: false, error: `MCP server "${serverName}" URL not configured` }; - } - - // 2. Use CraftOAuth for OAuth logic - const redirectUri = getMcpOAuthRedirectUri(); - const oauth = new CraftOAuth( - { mcpBaseUrl: getMcpBaseUrl(serverConfig.url), redirectUri }, - { onStatus: (msg) => console.log(`[MCP OAuth] ${msg}`), onError: (err) => console.error(`[MCP OAuth] ${err}`) } - ); - - // 3. Start OAuth flow (fetches metadata from .well-known, then gets auth URL) - let authFlowResult; - try { - authFlowResult = await oauth.startAuthFlow(); - } catch (error) { - const msg = error instanceof Error ? error.message : String(error); - console.error(`[MCP OAuth] Failed to start auth flow: ${msg}`); - return { success: false, error: msg }; - } - - const { authUrl, state, codeVerifier, tokenEndpoint, clientId, clientSecret } = authFlowResult; - - // 4. Store pending flow and wait for callback - return new Promise((resolve) => { - const timeoutId = setTimeout(() => { - pendingOAuthFlows.delete(state); - resolve({ success: false, error: 'OAuth timeout' }); - }, OAUTH_TIMEOUT_MS); - - pendingOAuthFlows.set(state, { - serverName, - projectPath, - codeVerifier, - tokenEndpoint, - clientId, - clientSecret, - redirectUri, - resolve, - timeoutId, - }); - - // Open browser - shell.openExternal(authUrl); - }); -} - -/** - * Handle OAuth callback from deeplink - */ -export async function handleMcpOAuthCallback(code: string, state: string): Promise { - const pending = pendingOAuthFlows.get(state); - if (!pending) { - console.warn(`[MCP OAuth] No pending flow for state: ${state.slice(0, 8)}...`); - return; - } - - clearTimeout(pending.timeoutId); - pendingOAuthFlows.delete(state); - - try { - // 1. Get server URL for CraftOAuth - const config = await readClaudeConfig(); - const serverUrl = getMcpServerConfig(config, pending.projectPath, pending.serverName)?.url; - - if (!serverUrl) { - throw new Error(`Server URL not found for ${pending.serverName}`); - } - - // 2. Use CraftOAuth to exchange code for tokens - const oauth = new CraftOAuth( - { mcpBaseUrl: getMcpBaseUrl(serverUrl), redirectUri: pending.redirectUri }, - { onStatus: () => {}, onError: () => {} } - ); - - const tokens = await oauth.completeAuthFlow( - code, - pending.codeVerifier, - pending.tokenEndpoint, - pending.clientId, - pending.clientSecret - ); - - // 3. Save to ~/.claude.json - await saveTokensToClaudeJson(pending.serverName, pending.projectPath, tokens, pending.clientId); - - // 4. Notify renderer (tools will be fetched on demand via tRPC) - BrowserWindow.getAllWindows().forEach((win) => { - win.webContents.send('mcp-auth-completed', { - serverName: pending.serverName, - projectPath: pending.projectPath, - success: true, - }); - }); - - // 5. Focus the main window after OAuth callback - bringToFront(); - - pending.resolve({ success: true }); - } catch (error) { - const msg = error instanceof Error ? error.message : String(error); - pending.resolve({ success: false, error: msg }); - } -} - - -/** - * Check if MCP token needs refresh (within 5 minutes of expiry) - */ -function needsRefresh(expiresAt: number | undefined): boolean { - if (!expiresAt) return false; - const fiveMinutes = 5 * 60 * 1000; - return Date.now() > expiresAt - fiveMinutes; -} - -/** - * Refresh MCP OAuth token for a server - * Returns the new access token, or null if refresh fails - */ -export async function refreshMcpToken( - serverName: string, - projectPath: string -): Promise { - try { - const config = await readClaudeConfig(); - let serverConfig = getMcpServerConfig(config, projectPath, serverName); - let resolvedProjectPath = projectPath; - - // Fallback to global MCP servers if not found or missing URL in project scope. - if (!serverConfig?.url) { - const globalConfig = getMcpServerConfig(config, GLOBAL_MCP_PATH, serverName); - if (globalConfig?.url) { - serverConfig = globalConfig; - resolvedProjectPath = GLOBAL_MCP_PATH; - } - } - - if (!serverConfig?.url) { - console.log(`[MCP Refresh] No URL for server ${serverName}`); - return null; - } - - const oauth = serverConfig._oauth as { - accessToken?: string; - refreshToken?: string; - clientId?: string; - expiresAt?: number; - } | undefined; - - if (!oauth?.refreshToken || !oauth?.clientId) { - console.log(`[MCP Refresh] No refresh token or clientId for ${serverName}`); - return null; - } - - // Use CraftOAuth to refresh the token - const craftOAuth = new CraftOAuth( - { mcpBaseUrl: getMcpBaseUrl(serverConfig.url) }, - { onStatus: () => {}, onError: () => {} } - ); - - const tokens = await craftOAuth.refreshAccessToken(oauth.refreshToken, oauth.clientId); - - // Update ~/.claude.json with new tokens - await saveTokensToClaudeJson(serverName, resolvedProjectPath, tokens, oauth.clientId); - - console.log(`[MCP Refresh] Successfully refreshed token for ${serverName}`); - return tokens.accessToken; - } catch (error) { - console.error(`[MCP Refresh] Failed to refresh token for ${serverName}:`, error); - return null; - } -} - -/** - * Ensure MCP servers have valid tokens, refreshing if needed - * Call this before passing servers to the SDK - * Returns the servers config with updated Authorization headers - */ -export async function ensureMcpTokensFresh( - mcpServers: Record, - projectPath: string -): Promise> { - const updatedServers = { ...mcpServers }; - - for (const [serverName, serverConfig] of Object.entries(mcpServers)) { - const oauth = serverConfig._oauth as { - accessToken?: string; - refreshToken?: string; - clientId?: string; - expiresAt?: number; - } | undefined; - - // Skip servers without OAuth - if (!oauth?.accessToken) continue; - - // Check if token needs refresh (within 5 min of expiry) - if (needsRefresh(oauth.expiresAt)) { - console.log(`[MCP] Token for ${serverName} expires soon, refreshing...`); - const newToken = await refreshMcpToken(serverName, projectPath); - - if (newToken) { - // Update the server config with the new token - updatedServers[serverName] = { - ...serverConfig, - headers: { - ...(serverConfig.headers || {}), - Authorization: `Bearer ${newToken}`, - }, - _oauth: { - ...oauth, - accessToken: newToken, - }, - }; - } - } - } - - return updatedServers; -} - -/** - * Save OAuth tokens to ~/.claude.json atomically. - * Uses a mutex to prevent race conditions when multiple concurrent - * token refreshes try to update the config simultaneously. - */ -async function saveTokensToClaudeJson( - serverName: string, - projectPath: string, - tokens: OAuthTokens, - clientId?: string -): Promise { - await updateClaudeConfigAtomic((config) => { - // Get existing server config to preserve existing headers and determine type - const existingConfig = getMcpServerConfig(config, projectPath, serverName) || {}; - const serverUrl = existingConfig.url as string | undefined; - - // Determine transport type from URL (SDK expects explicit type for HTTP servers) - const serverType = serverUrl?.endsWith('/sse') ? 'sse' : 'http'; - - // Build headers with Authorization (preserve any existing headers) - const existingHeaders = (existingConfig.headers as Record) || {}; - const headers = { - ...existingHeaders, - Authorization: `Bearer ${tokens.accessToken}`, - }; - - return updateMcpServerConfig(config, projectPath, serverName, { - // SDK-required fields - type: serverType, - headers, - // Internal tracking (for token refresh, status checking) - _oauth: { - accessToken: tokens.accessToken, - refreshToken: tokens.refreshToken, - clientId, - expiresAt: tokens.expiresAt, - }, - }); - }); -} - -export function cancelAllPendingOAuth(): void { - for (const [state, pending] of pendingOAuthFlows) { - clearTimeout(pending.timeoutId); - pending.resolve({ success: false, error: 'Cancelled' }); - } - pendingOAuthFlows.clear(); -} - -/** - * Fetch OAuth metadata for MCP server if available - * Returns metadata if server supports OAuth, undefined otherwise - */ -export async function fetchMcpOAuthMetadata( - serverName: string, - projectPath: string -): Promise { - try { - const config = await readClaudeConfig(); - const serverConfig = getMcpServerConfig(config, projectPath, serverName); - - if (!serverConfig?.url) { - return undefined; - } - - const baseUrl = getMcpBaseUrl(serverConfig.url); - const metadata = await fetchOAuthMetadata(baseUrl); - return metadata ?? undefined; - } catch { - return undefined; - } -} - -/** - * Get auth status for MCP server - */ -export async function getMcpAuthStatus( - serverName: string, - projectPath: string -): Promise<{ hasTokens: boolean; isExpired?: boolean }> { - try { - const config = await readClaudeConfig(); - const oauth = getMcpServerConfig(config, projectPath, serverName)?._oauth; - - if (!oauth?.accessToken) return { hasTokens: false }; - - const isExpired = oauth.expiresAt ? Date.now() > oauth.expiresAt : false; - return { hasTokens: true, isExpired }; - } catch { - return { hasTokens: false }; - } -} diff --git a/src/main/lib/oauth.ts b/src/main/lib/oauth.ts deleted file mode 100644 index 79e68efb..00000000 --- a/src/main/lib/oauth.ts +++ /dev/null @@ -1,942 +0,0 @@ -import { createHash, randomBytes } from 'crypto'; -import { shell } from 'electron'; -import { createServer, type Server } from 'http'; -import { URL } from 'url'; - -export interface OAuthMetadata { - authorization_endpoint: string; - token_endpoint: string; - registration_endpoint?: string; -} - -/** - * Fetch OAuth metadata from server's well-known endpoint - * Returns null if server doesn't support OAuth - */ -export async function fetchOAuthMetadata(mcpBaseUrl: string): Promise { - try { - const origin = new URL(mcpBaseUrl).origin; - const metadataUrl = `${origin}/.well-known/oauth-authorization-server`; - const response = await fetch(metadataUrl); - if (response.ok) { - return await response.json() as OAuthMetadata; - } - return null; - } catch { - return null; - } -} - -export interface OAuthConfig { - mcpBaseUrl: string; // e.g., http://localhost:3000/v1/links/abc123 - redirectUri?: string; // Optional custom redirect URI for deeplinks -} - -export interface OAuthTokens { - accessToken: string; - refreshToken?: string; - expiresAt?: number; - tokenType: string; -} - -export interface OAuthCallbacks { - onStatus: (message: string) => void; - onError: (error: string) => void; -} - -const CALLBACK_PORT = 8914; -const CALLBACK_PATH = '/callback'; -// Client names for OAuth registration -// Some MCP servers (like Figma) have an allowlist - try '1code' first, fall back to 'Codex' -const CLIENT_NAME = '1code'; -const FALLBACK_CLIENT_NAME = 'Codex'; - -/** - * Generate a styled OAuth callback page with terminal emulator aesthetic - * Matches application design with Tokyo Night theme - */ -function generateOAuthPage(options: { - title: string; - message: string; - isSuccess: boolean; - autoClose?: boolean; - errorDetail?: string; -}): string { - const { title, isSuccess, autoClose = false, errorDetail } = options; - - // Terminal output line type - interface TerminalLine { - text: string; - status?: string; - statusClass?: string; - isHighlight?: boolean; - highlightColor?: 'green' | 'red'; - hasCursor?: boolean; - isError?: boolean; - } - - // Terminal output lines based on success/error - const terminalLines: TerminalLine[] = isSuccess - ? [ - { text: 'initiating handshake sequence...' }, - { text: 'verifying credentials', status: '[PROCESSING]', statusClass: 'status-wait' }, - { text: 'token exchange completed', status: '[OK]', statusClass: 'status-ok' }, - { text: 'AUTHORIZATION SUCCESSFUL', isHighlight: true, highlightColor: 'green' }, - { text: 'closing connection', hasCursor: true }, - ] - : [ - { text: 'initiating handshake sequence...' }, - { text: 'verifying credentials', status: '[PROCESSING]', statusClass: 'status-wait' }, - { text: 'token exchange failed', status: '[ERROR]', statusClass: 'status-error' }, - { text: 'AUTHORIZATION FAILED', isHighlight: true, highlightColor: 'red' }, - ...(errorDetail ? [{ text: `error: ${errorDetail}`, isError: true }] : []), - ]; - - const terminalLinesHtml = terminalLines.map((line, i) => { - let content = ''; - if (line.isHighlight) { - const color = line.highlightColor === 'green' ? 'var(--green)' : 'var(--red)'; - const glow = line.highlightColor === 'green' - ? 'rgba(158, 206, 106, 0.4)' - : 'rgba(247, 118, 142, 0.4)'; - content = `${line.text}`; - } else if (line.isError) { - content = `${line.text}`; - } else { - content = `${line.text}${line.status ? ` ${line.status}` : ''}${line.hasCursor ? ' ' : ''}`; - } - return `
- - ~ - ${content} -
`; - }).join('\n'); - - const progressSection = autoClose ? ` -
-
- Session Autokill - 3.0s -
-
-
-
-
` : ''; - - const autoCloseScript = autoClose ? ` - // Countdown Logic - setTimeout(() => { - const duration = 3000; - const start = Date.now(); - const progressFill = document.getElementById('progress-fill'); - const countdownText = document.getElementById('countdown-text'); - - const tick = () => { - const elapsed = Date.now() - start; - const remaining = Math.max(0, duration - elapsed); - const percent = Math.min(100, (elapsed / duration) * 100); - - if(progressFill) progressFill.style.width = percent + '%'; - if(countdownText) countdownText.textContent = (remaining / 1000).toFixed(1) + 's'; - - if (elapsed < duration) { - requestAnimationFrame(tick); - } else { - window.close(); - } - }; - - requestAnimationFrame(tick); - }, 2200);` : ''; - - const logoColor = isSuccess ? 'var(--blue)' : 'var(--red)'; - const logoGlow = isSuccess - ? 'rgba(122, 162, 247, 0.3)' - : 'rgba(247, 118, 142, 0.3)'; - - return ` - - - - - Craft - ${title} - - - -
-
-
-
-
-
-
-
- user@craft-auth-cli ~ -
-
-
- -
-
- Last login: ... on ttys003 -
- -
- -
- -
-${terminalLinesHtml} -
-${progressSection} -
-
- - - -`; -} - -// Generate PKCE code verifier and challenge -export function generatePKCE(): { verifier: string; challenge: string } { - const verifier = randomBytes(32).toString('base64url'); - const challenge = createHash('sha256').update(verifier).digest('base64url'); - return { verifier, challenge }; -} - -// Generate random state for CSRF protection -export function generateState(): string { - return randomBytes(16).toString('hex'); -} - -export class CraftOAuth { - private config: OAuthConfig; - private server: Server | null = null; - private callbacks: OAuthCallbacks; - - constructor(config: OAuthConfig, callbacks: OAuthCallbacks) { - this.config = config; - this.callbacks = callbacks; - } - - // Get OAuth server metadata - private async getServerMetadata(): Promise<{ - authorization_endpoint: string; - token_endpoint: string; - registration_endpoint?: string; - }> { - const metadataUrl = `${this.config.mcpBaseUrl}/.well-known/oauth-authorization-server`; - - const response = await fetch(metadataUrl); - if (!response.ok) { - throw new Error(`Failed to get OAuth metadata: ${response.status}`); - } - - return response.json() as Promise<{ - authorization_endpoint: string; - token_endpoint: string; - registration_endpoint?: string; - }>; - } - - // Register OAuth client dynamically - private async registerClient(registrationEndpoint: string, clientName: string): Promise<{ - client_id: string; - client_secret?: string; - }> { - const redirectUri = this.config.redirectUri || `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`; - - const response = await fetch(registrationEndpoint, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - client_name: clientName, - redirect_uris: [redirectUri], - grant_types: ['authorization_code', 'refresh_token'], - response_types: ['code'], - token_endpoint_auth_method: 'none', // Public client - }), - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`Failed to register OAuth client: ${error}`); - } - - return response.json() as Promise<{ - client_id: string; - client_secret?: string; - }>; - } - - // Exchange authorization code for tokens - private async exchangeCodeForTokens( - tokenEndpoint: string, - code: string, - codeVerifier: string, - clientId: string, - redirectUri?: string, - clientSecret?: string - ): Promise { - const uri = redirectUri || this.config.redirectUri || `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`; - - const params = new URLSearchParams({ - grant_type: 'authorization_code', - code, - redirect_uri: uri, - client_id: clientId, - code_verifier: codeVerifier, - }); - - // Add client_secret if provided (some servers require it) - if (clientSecret) { - params.set('client_secret', clientSecret); - } - - const response = await fetch(tokenEndpoint, { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: params.toString(), - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`Failed to exchange code for tokens: ${error}`); - } - - const data = await response.json() as { - access_token: string; - refresh_token?: string; - expires_in?: number; - token_type?: string; - }; - - return { - accessToken: data.access_token, - refreshToken: data.refresh_token, - expiresAt: data.expires_in ? Date.now() + data.expires_in * 1000 : undefined, - tokenType: data.token_type || 'Bearer', - }; - } - - // Refresh access token - async refreshAccessToken( - refreshToken: string, - clientId: string - ): Promise { - const metadata = await this.getServerMetadata(); - - const params = new URLSearchParams({ - grant_type: 'refresh_token', - refresh_token: refreshToken, - client_id: clientId, - }); - - const response = await fetch(metadata.token_endpoint, { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: params.toString(), - }); - - if (!response.ok) { - throw new Error('Failed to refresh token'); - } - - const data = await response.json() as { - access_token: string; - refresh_token?: string; - expires_in?: number; - token_type?: string; - }; - - return { - accessToken: data.access_token, - refreshToken: data.refresh_token || refreshToken, - expiresAt: data.expires_in ? Date.now() + data.expires_in * 1000 : undefined, - tokenType: data.token_type || 'Bearer', - }; - } - - // Check if the MCP server requires OAuth - async checkAuthRequired(): Promise { - const metadataUrl = `${this.config.mcpBaseUrl}/.well-known/oauth-authorization-server`; - this.callbacks.onStatus('Checking if authentication is required...'); - - try { - const response = await fetch(metadataUrl); - if (response.ok) { - this.callbacks.onStatus('OAuth required - server has OAuth metadata'); - return true; - } - // 404 or other error means no OAuth - this.callbacks.onStatus('No OAuth metadata found - server may be public'); - return false; - } catch (error) { - this.callbacks.onStatus('Could not reach OAuth metadata - assuming public'); - return false; - } - } - - // Start the OAuth flow - async authenticate(): Promise<{ tokens: OAuthTokens; clientId: string }> { - this.callbacks.onStatus('Fetching OAuth server configuration...'); - - // Get server metadata - let metadata; - try { - metadata = await this.getServerMetadata(); - this.callbacks.onStatus(`Found OAuth endpoints at ${this.config.mcpBaseUrl}`); - } catch (error) { - const msg = error instanceof Error ? error.message : 'Unknown error'; - this.callbacks.onStatus(`Failed to get OAuth metadata: ${msg}`); - throw error; - } - - // Register client if endpoint available - let clientId: string; - if (metadata.registration_endpoint) { - // Try primary client name first, fall back to alternative if rejected - this.callbacks.onStatus(`Registering client as '${CLIENT_NAME}'...`); - try { - const client = await this.registerClient(metadata.registration_endpoint, CLIENT_NAME); - clientId = client.client_id; - this.callbacks.onStatus(`Registered as client: ${clientId}`); - } catch (error) { - // Try fallback client name (some servers have allowlists) - this.callbacks.onStatus(`Registration as '${CLIENT_NAME}' failed, trying '${FALLBACK_CLIENT_NAME}'...`); - try { - const client = await this.registerClient(metadata.registration_endpoint, FALLBACK_CLIENT_NAME); - clientId = client.client_id; - this.callbacks.onStatus(`Registered as client: ${clientId}`); - } catch (fallbackError) { - const msg = fallbackError instanceof Error ? fallbackError.message : 'Unknown error'; - this.callbacks.onStatus(`Client registration failed: ${msg}`); - throw fallbackError; - } - } - } else { - // Use a default client ID for public clients - clientId = 'craft-agent'; - this.callbacks.onStatus(`Using default client ID: ${clientId}`); - } - - // Generate PKCE and state - const pkce = generatePKCE(); - const state = generateState(); - const redirectUri = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`; - this.callbacks.onStatus('Generated PKCE challenge and state'); - - // Build authorization URL - const authUrl = new URL(metadata.authorization_endpoint); - authUrl.searchParams.set('response_type', 'code'); - authUrl.searchParams.set('client_id', clientId); - authUrl.searchParams.set('redirect_uri', redirectUri); - authUrl.searchParams.set('state', state); - authUrl.searchParams.set('code_challenge', pkce.challenge); - authUrl.searchParams.set('code_challenge_method', 'S256'); - - // Start local server to receive callback - this.callbacks.onStatus(`Starting callback server on port ${CALLBACK_PORT}...`); - const codePromise = this.startCallbackServer(state); - - // Open browser for authorization - this.callbacks.onStatus('Opening browser for authorization...'); - await shell.openExternal(authUrl.toString()); - - // Wait for the authorization code - this.callbacks.onStatus('Waiting for you to authorize in browser...'); - const authCode = await codePromise; - this.callbacks.onStatus('Authorization code received!'); - - // Exchange code for tokens - this.callbacks.onStatus('Exchanging authorization code for tokens...'); - const tokens = await this.exchangeCodeForTokens( - metadata.token_endpoint, - authCode, - pkce.verifier, - clientId - ); - this.callbacks.onStatus('Tokens received successfully!'); - - return { tokens, clientId }; - } - - /** - * Start OAuth flow without waiting for callback (for deeplink-based flows) - * Returns the authorization URL and state/verifier for later token exchange - * @param preloadedMetadata - Optional pre-fetched OAuth metadata to avoid duplicate fetch - */ - async startAuthFlow(preloadedMetadata?: OAuthMetadata): Promise<{ - authUrl: string; - state: string; - codeVerifier: string; - tokenEndpoint: string; - clientId: string; - clientSecret?: string; - }> { - this.callbacks.onStatus('Fetching OAuth server configuration...'); - const metadata = preloadedMetadata || await this.getServerMetadata(); - - // Register client if endpoint available - let clientId: string; - let clientSecret: string | undefined; - if (metadata.registration_endpoint) { - // Try primary client name first, fall back to alternative if rejected - this.callbacks.onStatus(`Registering client as '${CLIENT_NAME}'...`); - try { - const client = await this.registerClient(metadata.registration_endpoint, CLIENT_NAME); - clientId = client.client_id; - clientSecret = client.client_secret; - this.callbacks.onStatus(`Registered as client: ${clientId}`); - } catch (error) { - // Try fallback client name (some servers have allowlists) - this.callbacks.onStatus(`Registration as '${CLIENT_NAME}' failed, trying '${FALLBACK_CLIENT_NAME}'...`); - const client = await this.registerClient(metadata.registration_endpoint, FALLBACK_CLIENT_NAME); - clientId = client.client_id; - clientSecret = client.client_secret; - this.callbacks.onStatus(`Registered as client: ${clientId}`); - } - } else { - // No registration endpoint - use default client ID - clientId = '1code'; - } - - const pkce = generatePKCE(); - const state = generateState(); - const redirectUri = this.config.redirectUri || `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`; - - const authUrl = new URL(metadata.authorization_endpoint); - authUrl.searchParams.set('response_type', 'code'); - authUrl.searchParams.set('client_id', clientId); - authUrl.searchParams.set('redirect_uri', redirectUri); - authUrl.searchParams.set('state', state); - authUrl.searchParams.set('code_challenge', pkce.challenge); - authUrl.searchParams.set('code_challenge_method', 'S256'); - - return { - authUrl: authUrl.toString(), - state, - codeVerifier: pkce.verifier, - tokenEndpoint: metadata.token_endpoint, - clientId, - clientSecret, - }; - } - - /** - * Complete OAuth flow by exchanging code for tokens (called after deeplink callback) - */ - async completeAuthFlow( - code: string, - codeVerifier: string, - tokenEndpoint: string, - clientId: string, - clientSecret?: string - ): Promise { - const redirectUri = this.config.redirectUri || `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`; - return this.exchangeCodeForTokens(tokenEndpoint, code, codeVerifier, clientId, redirectUri, clientSecret); - } - - // Start local HTTP server to receive OAuth callback - private startCallbackServer(expectedState: string): Promise { - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - this.stopServer(); - reject(new Error('OAuth timeout - no callback received')); - }, 300000); // 5 minute timeout - - this.server = createServer((req, res) => { - const url = new URL(req.url || '/', `http://localhost:${CALLBACK_PORT}`); - - if (url.pathname === CALLBACK_PATH) { - const code = url.searchParams.get('code'); - const state = url.searchParams.get('state'); - const error = url.searchParams.get('error'); - - if (error) { - res.writeHead(400, { 'Content-Type': 'text/html' }); - res.end(generateOAuthPage({ - title: 'Authorization Failed', - message: 'You can close this window.', - isSuccess: false, - errorDetail: error, - })); - clearTimeout(timeout); - this.stopServer(); - reject(new Error(`OAuth error: ${error}`)); - return; - } - - if (state !== expectedState) { - res.writeHead(400, { 'Content-Type': 'text/html' }); - res.end(generateOAuthPage({ - title: 'Security Error', - message: 'State mismatch - possible CSRF attack.', - isSuccess: false, - })); - clearTimeout(timeout); - this.stopServer(); - reject(new Error('OAuth state mismatch')); - return; - } - - if (!code) { - res.writeHead(400, { 'Content-Type': 'text/html' }); - res.end(generateOAuthPage({ - title: 'Authorization Failed', - message: 'No authorization code received.', - isSuccess: false, - })); - clearTimeout(timeout); - this.stopServer(); - reject(new Error('No authorization code')); - return; - } - - // Success! - res.writeHead(200, { 'Content-Type': 'text/html' }); - res.end(generateOAuthPage({ - title: 'Authorization Successful', - message: 'You can close this window and return to the terminal.', - isSuccess: true, - autoClose: true, - })); - - clearTimeout(timeout); - this.stopServer(); - resolve(code); - } else { - res.writeHead(404); - res.end('Not found'); - } - }); - - this.server.listen(CALLBACK_PORT, () => { - // Server started - }); - - this.server.on('error', (err) => { - clearTimeout(timeout); - reject(new Error(`Failed to start callback server: ${err.message}`)); - }); - }); - } - - private stopServer(): void { - if (this.server) { - this.server.close(); - this.server = null; - } - } - - // Cancel the OAuth flow - cancel(): void { - this.stopServer(); - } -} - -// Helper to extract the base MCP URL from a full MCP URL -export function getMcpBaseUrl(mcpUrl: string): string { - // Remove /mcp or /sse suffix if present - return mcpUrl.replace(/\/(mcp|sse)\/?$/, ''); -} diff --git a/src/main/lib/ollama/detector.ts b/src/main/lib/ollama/detector.ts index e6b7d681..5cc9a9b5 100644 --- a/src/main/lib/ollama/detector.ts +++ b/src/main/lib/ollama/detector.ts @@ -50,21 +50,23 @@ export async function checkOllamaStatus(): Promise { // If no exact match, try to find any qwen-coder, deepseek-coder, or codestral variant if (!recommendedModel) { - recommendedModel = models.find((m: string) => + recommendedModel = models.find(m => m.includes('qwen') && m.includes('coder') || m.includes('deepseek') && m.includes('coder') || m.includes('codestral') ) } + console.log(`[Ollama] Available: ${models.length} models found`, models) + return { available: true, models, recommendedModel: recommendedModel || models[0], // Fallback to any model version: data.version, } - } catch { - // Ollama not available - no need to log, this is expected when offline mode is disabled + } catch (error) { + console.log('[Ollama] Not available:', error) return { available: false, models: [] } } } diff --git a/src/main/lib/platform/base.ts b/src/main/lib/platform/base.ts deleted file mode 100644 index 4e859e67..00000000 --- a/src/main/lib/platform/base.ts +++ /dev/null @@ -1,164 +0,0 @@ -/** - * Base Platform Provider - * Contains shared logic for all platforms - */ - -import { execFile } from "node:child_process" -import { promisify } from "node:util" -import * as os from "node:os" -import * as path from "node:path" -import type { - PlatformProvider, - ShellConfig, - PathConfig, - CliConfig, - EnvironmentConfig, -} from "./types" - -const execFileAsync = promisify(execFile) - -export abstract class BasePlatformProvider implements PlatformProvider { - abstract readonly platform: "win32" | "darwin" | "linux" - abstract readonly displayName: string - - abstract getShellConfig(): ShellConfig - abstract getPathConfig(): PathConfig - abstract getCliConfig(): CliConfig - abstract getEnvironmentConfig(): EnvironmentConfig - - /** - * Get home directory (cross-platform) - */ - protected getHome(): string { - return os.homedir() - } - - /** - * Get username (cross-platform) - */ - protected getUsername(): string { - return os.userInfo().username - } - - buildExtendedPath(currentPath?: string): string { - const config = this.getPathConfig() - const existingPaths = currentPath - ? currentPath.split(config.separator).filter(Boolean) - : [] - - const allPaths = [ - ...config.commonPaths, - config.localBin, - ...config.packageManagerPaths, - ] - - // Add paths that aren't already present (case-insensitive on Windows) - const isWindows = this.platform === "win32" - const normalizedExisting = new Set( - existingPaths.map((p) => - isWindows ? path.normalize(p).toLowerCase() : path.normalize(p) - ) - ) - - const newPaths: string[] = [] - for (const p of allPaths) { - const normalized = isWindows - ? path.normalize(p).toLowerCase() - : path.normalize(p) - if (!normalizedExisting.has(normalized)) { - newPaths.push(path.normalize(p)) - normalizedExisting.add(normalized) - } - } - - return [...newPaths, ...existingPaths].join(config.separator) - } - - getDefaultShell(): string { - const config = this.getShellConfig() - return config.executable - } - - async detectShell(): Promise { - // Default implementation returns configured shell - // Platforms can override for more sophisticated detection - return this.getDefaultShell() - } - - async detectLocale(): Promise { - // Default: check environment or return fallback - return process.env.LANG || "en_US.UTF-8" - } - - buildEnvironment(baseEnv?: Record): Record { - const envConfig = this.getEnvironmentConfig() - const pathConfig = this.getPathConfig() - const home = this.getHome() - const user = this.getUsername() - - const env: Record = { ...baseEnv } - - // Set home directory - env[envConfig.homeVar] = home - if (!env.HOME) env.HOME = home - - // Set user - env[envConfig.userVar] = user - if (!env.USER) env.USER = user - - // Set additional platform-specific vars - for (const [key, value] of Object.entries(envConfig.additionalVars)) { - if (!env[key]) { - // Resolve special placeholders - env[key] = value - .replace("${HOME}", home) - .replace("${USER}", user) - } - } - - // Build extended PATH - env.PATH = this.buildExtendedPath(env.PATH || process.env.PATH) - - // Set TERM if not present - if (!env.TERM) { - env.TERM = "xterm-256color" - } - - // Set SHELL - if (!env.SHELL) { - env.SHELL = this.getDefaultShell() - } - - return env - } - - /** - * Execute a command safely using execFile (no shell interpolation). - * - * This is the preferred method for simple command execution as it: - * - Avoids shell injection vulnerabilities - * - Has predictable argument handling - * - * For complex shell commands (pipes, redirects, osascript with quotes), - * implementations may use exec/execSync directly as needed. - */ - async execCommand( - command: string, - args: string[], - options?: { timeout?: number; env?: Record } - ): Promise<{ stdout: string; stderr: string }> { - const { stdout, stderr } = await execFileAsync(command, args, { - timeout: options?.timeout ?? 5000, - env: options?.env as NodeJS.ProcessEnv | undefined, - encoding: "utf8", - }) - return { stdout, stderr } - } - - // Abstract methods that must be implemented by each platform - abstract installCli( - sourcePath: string - ): Promise<{ success: boolean; error?: string }> - abstract uninstallCli(): Promise<{ success: boolean; error?: string }> - abstract isCliInstalled(sourcePath: string): boolean -} diff --git a/src/main/lib/platform/darwin.ts b/src/main/lib/platform/darwin.ts deleted file mode 100644 index 84179ae3..00000000 --- a/src/main/lib/platform/darwin.ts +++ /dev/null @@ -1,208 +0,0 @@ -/** - * macOS Platform Provider - */ - -import { exec, execSync } from "node:child_process" -import { existsSync, lstatSync, readlinkSync } from "node:fs" -import * as path from "node:path" -import { promisify } from "node:util" -import { BasePlatformProvider } from "./base" -import type { - ShellConfig, - PathConfig, - CliConfig, - EnvironmentConfig, -} from "./types" - -const execAsync = promisify(exec) - -export class DarwinPlatformProvider extends BasePlatformProvider { - readonly platform = "darwin" as const - readonly displayName = "macOS" - - getShellConfig(): ShellConfig { - const shell = process.env.SHELL || "/bin/zsh" - - return { - executable: shell, - loginArgs: ["-l"], - execArgs: (command: string) => ["-c", command], - } - } - - getPathConfig(): PathConfig { - const home = this.getHome() - - return { - separator: ":", - commonPaths: [ - // Homebrew (Apple Silicon) - "/opt/homebrew/bin", - "/opt/homebrew/sbin", - // Homebrew (Intel) - "/usr/local/bin", - "/usr/local/sbin", - // System - "/usr/bin", - "/bin", - "/usr/sbin", - "/sbin", - // MacPorts - "/opt/local/bin", - "/opt/local/sbin", - ], - localBin: path.join(home, ".local", "bin"), - packageManagerPaths: [ - path.join(home, ".bun", "bin"), - path.join(home, ".cargo", "bin"), - path.join(home, ".deno", "bin"), - // NVM managed Node.js (common pattern) - path.join(home, ".nvm", "versions", "node", "*", "bin"), - ], - } - } - - getCliConfig(): CliConfig { - return { - installPath: "/usr/local/bin/1code", - scriptName: "1code", - requiresAdmin: true, // /usr/local/bin requires admin on macOS - } - } - - getEnvironmentConfig(): EnvironmentConfig { - const home = this.getHome() - - return { - homeVar: "HOME", - userVar: "USER", - additionalVars: { - TMPDIR: process.env.TMPDIR || "/tmp", - __CF_USER_TEXT_ENCODING: process.env.__CF_USER_TEXT_ENCODING || "", - }, - } - } - - override getDefaultShell(): string { - return process.env.SHELL || "/bin/zsh" - } - - override async detectShell(): Promise { - // Try SHELL env var first (most reliable) - if (process.env.SHELL) { - return process.env.SHELL - } - - // Try to get from Directory Services - try { - const { stdout } = await this.execCommand("sh", [ - "-c", - `dscl . -read /Users/$(whoami) UserShell 2>/dev/null`, - ]) - const match = stdout.match(/UserShell:\s*(.+)/) - if (match?.[1]) { - return match[1].trim() - } - } catch { - // Ignore errors - } - - return "/bin/zsh" - } - - override async detectLocale(): Promise { - // Check environment first - if (process.env.LANG?.includes("UTF-8")) { - return process.env.LANG - } - if (process.env.LC_ALL?.includes("UTF-8")) { - return process.env.LC_ALL - } - - // Try to get from locale command - try { - const { stdout } = await this.execCommand("sh", [ - "-c", - "locale 2>/dev/null | grep LANG= | cut -d= -f2", - ]) - const trimmed = stdout.trim() - if (trimmed?.includes("UTF-8")) { - return trimmed - } - } catch { - // Ignore errors - } - - return "en_US.UTF-8" - } - - async installCli( - sourcePath: string - ): Promise<{ success: boolean; error?: string }> { - const cliConfig = this.getCliConfig() - const installPath = cliConfig.installPath - - if (!existsSync(sourcePath)) { - return { success: false, error: "CLI script not found in app bundle" } - } - - try { - // Remove existing if present - if (existsSync(installPath)) { - await execAsync( - `osascript -e 'do shell script "rm -f ${installPath}" with administrator privileges'` - ) - } - - // Create symlink with admin privileges - await execAsync( - `osascript -e 'do shell script "ln -s \\"${sourcePath}\\" ${installPath}" with administrator privileges'` - ) - - console.log("[CLI] Installed 1code command to", installPath) - return { success: true } - } catch (error: unknown) { - const errorMessage = - error instanceof Error ? error.message : "Installation failed" - console.error("[CLI] Failed to install:", error) - return { success: false, error: errorMessage } - } - } - - async uninstallCli(): Promise<{ success: boolean; error?: string }> { - const cliConfig = this.getCliConfig() - const installPath = cliConfig.installPath - - try { - if (!existsSync(installPath)) { - console.log("[CLI] CLI command not installed, nothing to uninstall") - return { success: true } - } - - await execAsync( - `osascript -e 'do shell script "rm -f ${installPath}" with administrator privileges'` - ) - - console.log("[CLI] Uninstalled 1code command") - return { success: true } - } catch (error: unknown) { - const errorMessage = - error instanceof Error ? error.message : "Uninstallation failed" - console.error("[CLI] Failed to uninstall:", error) - return { success: false, error: errorMessage } - } - } - - isCliInstalled(sourcePath: string): boolean { - const cliConfig = this.getCliConfig() - try { - if (!existsSync(cliConfig.installPath)) return false - const stat = lstatSync(cliConfig.installPath) - if (!stat.isSymbolicLink()) return false - const target = readlinkSync(cliConfig.installPath) - return target === sourcePath - } catch { - return false - } - } -} diff --git a/src/main/lib/platform/index.ts b/src/main/lib/platform/index.ts deleted file mode 100644 index f0db6eb9..00000000 --- a/src/main/lib/platform/index.ts +++ /dev/null @@ -1,164 +0,0 @@ -/** - * Platform Abstraction Layer - * - * Provides unified cross-platform APIs for: - * - Shell detection and configuration - * - PATH management - * - Environment variable handling - * - CLI installation - * - Locale detection - * - * Usage: - * import { platform, getPlatformProvider } from './platform' - * - * // Use singleton (recommended) - * const shell = platform.getDefaultShell() - * const env = platform.buildEnvironment() - * - * // Or get provider for specific platform - * const winProvider = getPlatformProvider('win32') - */ - -import type { PlatformProvider } from "./types" -import { WindowsPlatformProvider } from "./windows" -import { DarwinPlatformProvider } from "./darwin" -import { LinuxPlatformProvider } from "./linux" - -// Export types -export type { - PlatformProvider, - ShellConfig, - PathConfig, - CliConfig, - EnvironmentConfig, -} from "./types" - -// Provider instances (lazy initialized) -let windowsProvider: WindowsPlatformProvider | null = null -let darwinProvider: DarwinPlatformProvider | null = null -let linuxProvider: LinuxPlatformProvider | null = null - -/** - * Get platform provider for a specific platform - */ -export function getPlatformProvider( - platformId: "win32" | "darwin" | "linux" -): PlatformProvider { - switch (platformId) { - case "win32": - if (!windowsProvider) { - windowsProvider = new WindowsPlatformProvider() - } - return windowsProvider - case "darwin": - if (!darwinProvider) { - darwinProvider = new DarwinPlatformProvider() - } - return darwinProvider - case "linux": - if (!linuxProvider) { - linuxProvider = new LinuxPlatformProvider() - } - return linuxProvider - default: - throw new Error(`Unsupported platform: ${platformId}`) - } -} - -/** - * Get platform provider for current platform - */ -export function getCurrentPlatformProvider(): PlatformProvider { - const platformId = process.platform as "win32" | "darwin" | "linux" - return getPlatformProvider(platformId) -} - -/** - * Singleton instance for current platform - * This is the recommended way to use the platform abstraction - */ -export const platform = getCurrentPlatformProvider() - -// ============================================================================ -// Convenience functions that delegate to current platform provider -// These provide a simpler API for common operations -// ============================================================================ - -/** - * Get the default shell for current platform - */ -export function getDefaultShell(): string { - return platform.getDefaultShell() -} - -/** - * Detect user's preferred shell (async) - */ -export async function detectShell(): Promise { - return platform.detectShell() -} - -/** - * Detect system locale - */ -export async function detectLocale(): Promise { - return platform.detectLocale() -} - -/** - * Build extended PATH with common tool locations - */ -export function buildExtendedPath(currentPath?: string): string { - return platform.buildExtendedPath(currentPath) -} - -/** - * Build environment for shell/process execution - */ -export function buildEnvironment( - baseEnv?: Record -): Record { - return platform.buildEnvironment(baseEnv) -} - -/** - * Get PATH separator for current platform - */ -export function getPathSeparator(): string { - return platform.getPathConfig().separator -} - -/** - * Check if current platform is Windows - */ -export function isWindows(): boolean { - return platform.platform === "win32" -} - -/** - * Check if current platform is macOS - */ -export function isMacOS(): boolean { - return platform.platform === "darwin" -} - -/** - * Check if current platform is Linux - */ -export function isLinux(): boolean { - return platform.platform === "linux" -} - -/** - * Get CLI installation path for current platform - */ -export function getCliInstallPath(): string { - return platform.getCliConfig().installPath -} - -/** - * Get CLI script name for current platform - */ -export function getCliScriptName(): string { - return platform.getCliConfig().scriptName -} diff --git a/src/main/lib/platform/linux.ts b/src/main/lib/platform/linux.ts deleted file mode 100644 index 3cbec64e..00000000 --- a/src/main/lib/platform/linux.ts +++ /dev/null @@ -1,224 +0,0 @@ -/** - * Linux Platform Provider - */ - -import { exec } from "node:child_process" -import { existsSync, lstatSync, readlinkSync } from "node:fs" -import * as path from "node:path" -import { promisify } from "node:util" -import { BasePlatformProvider } from "./base" -import type { - ShellConfig, - PathConfig, - CliConfig, - EnvironmentConfig, -} from "./types" - -const execAsync = promisify(exec) - -export class LinuxPlatformProvider extends BasePlatformProvider { - readonly platform = "linux" as const - readonly displayName = "Linux" - - getShellConfig(): ShellConfig { - const shell = process.env.SHELL || "/bin/bash" - - return { - executable: shell, - loginArgs: ["-l"], - execArgs: (command: string) => ["-c", command], - } - } - - getPathConfig(): PathConfig { - const home = this.getHome() - - return { - separator: ":", - commonPaths: [ - // System paths - "/usr/local/bin", - "/usr/local/sbin", - "/usr/bin", - "/bin", - "/usr/sbin", - "/sbin", - // Snap packages - "/snap/bin", - // Flatpak exports - "/var/lib/flatpak/exports/bin", - path.join(home, ".local", "share", "flatpak", "exports", "bin"), - ], - localBin: path.join(home, ".local", "bin"), - packageManagerPaths: [ - path.join(home, ".bun", "bin"), - path.join(home, ".cargo", "bin"), - path.join(home, ".deno", "bin"), - // NVM managed Node.js - path.join(home, ".nvm", "versions", "node", "*", "bin"), - // ASDF version manager - path.join(home, ".asdf", "shims"), - // Linuxbrew - path.join(home, ".linuxbrew", "bin"), - "/home/linuxbrew/.linuxbrew/bin", - ], - } - } - - getCliConfig(): CliConfig { - return { - installPath: "/usr/local/bin/1code", - scriptName: "1code", - requiresAdmin: true, // Usually needs sudo, but we try without first - } - } - - getEnvironmentConfig(): EnvironmentConfig { - const home = this.getHome() - - return { - homeVar: "HOME", - userVar: "USER", - additionalVars: { - XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME || path.join(home, ".config"), - XDG_DATA_HOME: process.env.XDG_DATA_HOME || path.join(home, ".local", "share"), - XDG_CACHE_HOME: process.env.XDG_CACHE_HOME || path.join(home, ".cache"), - XDG_STATE_HOME: process.env.XDG_STATE_HOME || path.join(home, ".local", "state"), - }, - } - } - - override getDefaultShell(): string { - return process.env.SHELL || "/bin/bash" - } - - override async detectShell(): Promise { - // Try SHELL env var first - if (process.env.SHELL) { - return process.env.SHELL - } - - // Try to get from /etc/passwd via getent - try { - const uid = process.getuid?.() - if (uid !== undefined) { - const { stdout } = await this.execCommand("sh", [ - "-c", - `getent passwd ${uid} 2>/dev/null`, - ]) - // getent format: user:x:uid:gid:name:home:shell - const match = stdout.match(/:([^:]+)$/) - if (match?.[1]) { - return match[1].trim() - } - } - } catch { - // Ignore errors - } - - return "/bin/bash" - } - - override async detectLocale(): Promise { - // Check environment first - if (process.env.LANG?.includes("UTF-8")) { - return process.env.LANG - } - if (process.env.LC_ALL?.includes("UTF-8")) { - return process.env.LC_ALL - } - - // Try to get from locale command - try { - const { stdout } = await this.execCommand("sh", [ - "-c", - "locale 2>/dev/null | grep LANG= | cut -d= -f2", - ]) - const trimmed = stdout.trim() - if (trimmed?.includes("UTF-8")) { - return trimmed - } - } catch { - // Ignore errors - } - - return "en_US.UTF-8" - } - - async installCli( - sourcePath: string - ): Promise<{ success: boolean; error?: string }> { - const cliConfig = this.getCliConfig() - const installPath = cliConfig.installPath - - if (!existsSync(sourcePath)) { - return { success: false, error: "CLI script not found in app bundle" } - } - - try { - // Remove existing if present - if (existsSync(installPath)) { - try { - await execAsync(`rm -f ${installPath}`) - } catch { - await execAsync(`sudo rm -f ${installPath}`) - } - } - - // Create symlink - try without sudo first - try { - await execAsync(`ln -s "${sourcePath}" ${installPath}`) - } catch { - await execAsync(`sudo ln -s "${sourcePath}" ${installPath}`) - } - - console.log("[CLI] Installed 1code command to", installPath) - return { success: true } - } catch (error: unknown) { - const errorMessage = - error instanceof Error ? error.message : "Installation failed" - console.error("[CLI] Failed to install:", error) - return { success: false, error: errorMessage } - } - } - - async uninstallCli(): Promise<{ success: boolean; error?: string }> { - const cliConfig = this.getCliConfig() - const installPath = cliConfig.installPath - - try { - if (!existsSync(installPath)) { - console.log("[CLI] CLI command not installed, nothing to uninstall") - return { success: true } - } - - // Try without sudo first - try { - await execAsync(`rm -f ${installPath}`) - } catch { - await execAsync(`sudo rm -f ${installPath}`) - } - - console.log("[CLI] Uninstalled 1code command") - return { success: true } - } catch (error: unknown) { - const errorMessage = - error instanceof Error ? error.message : "Uninstallation failed" - console.error("[CLI] Failed to uninstall:", error) - return { success: false, error: errorMessage } - } - } - - isCliInstalled(sourcePath: string): boolean { - const cliConfig = this.getCliConfig() - try { - if (!existsSync(cliConfig.installPath)) return false - const stat = lstatSync(cliConfig.installPath) - if (!stat.isSymbolicLink()) return false - const target = readlinkSync(cliConfig.installPath) - return target === sourcePath - } catch { - return false - } - } -} diff --git a/src/main/lib/platform/types.ts b/src/main/lib/platform/types.ts deleted file mode 100644 index e5259a8d..00000000 --- a/src/main/lib/platform/types.ts +++ /dev/null @@ -1,132 +0,0 @@ -/** - * Platform abstraction layer types - * Provides a unified interface for platform-specific operations - */ - -export interface ShellConfig { - /** Path to the default shell executable */ - executable: string - /** Arguments to pass for login shell */ - loginArgs: string[] - /** Arguments to pass for command execution */ - execArgs: (command: string) => string[] -} - -export interface PathConfig { - /** PATH environment variable separator */ - separator: string - /** Common paths where tools are installed */ - commonPaths: string[] - /** User-local bin directory */ - localBin: string - /** Package manager paths (npm, cargo, etc.) */ - packageManagerPaths: string[] -} - -export interface CliConfig { - /** Path where CLI command should be installed */ - installPath: string - /** CLI script filename */ - scriptName: string - /** Whether admin/elevated privileges are required */ - requiresAdmin: boolean -} - -export interface EnvironmentConfig { - /** Home directory environment variable name */ - homeVar: string - /** User name environment variable name */ - userVar: string - /** Additional platform-specific env vars to set */ - additionalVars: Record -} - -/** - * Platform Provider Interface - * Implement this for each supported platform - */ -export interface PlatformProvider { - /** Platform identifier */ - readonly platform: "win32" | "darwin" | "linux" - - /** Display name for the platform */ - readonly displayName: string - - /** Shell configuration */ - getShellConfig(): ShellConfig - - /** PATH configuration */ - getPathConfig(): PathConfig - - /** CLI installation configuration */ - getCliConfig(): CliConfig - - /** Environment configuration */ - getEnvironmentConfig(): EnvironmentConfig - - /** - * Build extended PATH with all common tool locations - * @param currentPath - Current PATH value - * @returns Extended PATH string - */ - buildExtendedPath(currentPath?: string): string - - /** - * Get the default shell for this platform - * @returns Path to default shell executable - */ - getDefaultShell(): string - - /** - * Detect user's preferred shell (async, may spawn processes) - * @returns Promise resolving to shell path - */ - detectShell(): Promise - - /** - * Detect system locale - * @returns Promise resolving to locale string (e.g., "en_US.UTF-8") - */ - detectLocale(): Promise - - /** - * Build environment variables for shell/process execution - * @param baseEnv - Base environment to extend - * @returns Environment object with platform-specific additions - */ - buildEnvironment(baseEnv?: Record): Record - - /** - * Install CLI command to system - * @param sourcePath - Path to CLI script source - * @returns Promise with success status and optional error - */ - installCli(sourcePath: string): Promise<{ success: boolean; error?: string }> - - /** - * Uninstall CLI command from system - * @returns Promise with success status and optional error - */ - uninstallCli(): Promise<{ success: boolean; error?: string }> - - /** - * Check if CLI command is installed - * @param sourcePath - Path to CLI script source (for symlink verification) - * @returns Whether CLI is properly installed - */ - isCliInstalled(sourcePath: string): boolean - - /** - * Execute a command and get output - * Used for shell detection, locale detection, etc. - * @param command - Command to execute - * @param args - Command arguments - * @param options - Execution options - * @returns Promise with stdout and stderr - */ - execCommand( - command: string, - args: string[], - options?: { timeout?: number; env?: Record } - ): Promise<{ stdout: string; stderr: string }> -} diff --git a/src/main/lib/platform/windows.ts b/src/main/lib/platform/windows.ts deleted file mode 100644 index 88119e3c..00000000 --- a/src/main/lib/platform/windows.ts +++ /dev/null @@ -1,195 +0,0 @@ -/** - * Windows Platform Provider - */ - -import { existsSync } from "node:fs" -import { copyFile, mkdir, unlink, rmdir } from "node:fs/promises" -import * as path from "node:path" -import { BasePlatformProvider } from "./base" -import type { - ShellConfig, - PathConfig, - CliConfig, - EnvironmentConfig, -} from "./types" - -export class WindowsPlatformProvider extends BasePlatformProvider { - readonly platform = "win32" as const - readonly displayName = "Windows" - - getShellConfig(): ShellConfig { - const powershellPath = - "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" - const cmdPath = process.env.COMSPEC || "C:\\Windows\\System32\\cmd.exe" - - return { - executable: process.env.COMSPEC || powershellPath, - loginArgs: [], // Windows shells don't have login mode like Unix - execArgs: (command: string) => ["/c", command], - } - } - - getPathConfig(): PathConfig { - const home = this.getHome() - const systemRoot = process.env.SystemRoot || "C:\\Windows" - - return { - separator: ";", - commonPaths: [ - // Git for Windows - "C:\\Program Files\\Git\\cmd", - "C:\\Program Files\\Git\\bin", - "C:\\Program Files\\Git\\usr\\bin", - // Node.js - "C:\\Program Files\\nodejs", - // System - path.join(systemRoot, "System32"), - systemRoot, - ], - localBin: path.join(home, ".local", "bin"), - packageManagerPaths: [ - path.join(home, "AppData", "Roaming", "npm"), - path.join(home, ".bun", "bin"), - path.join(home, ".cargo", "bin"), - path.join(home, "scoop", "shims"), - path.join(home, "AppData", "Local", "pnpm"), - ], - } - } - - getCliConfig(): CliConfig { - // Install to ~/.local/bin which is already included in buildExtendedPath() - // This avoids needing to modify the system PATH - const home = this.getHome() - - return { - installPath: path.join(home, ".local", "bin", "1code.cmd"), - scriptName: "1code.cmd", - requiresAdmin: false, // Install to user directory, no admin needed - } - } - - getEnvironmentConfig(): EnvironmentConfig { - const home = this.getHome() - - return { - homeVar: "USERPROFILE", - userVar: "USERNAME", - additionalVars: { - USERPROFILE: home, - HOME: home, - APPDATA: path.join(home, "AppData", "Roaming"), - LOCALAPPDATA: path.join(home, "AppData", "Local"), - TEMP: process.env.TEMP || path.join(home, "AppData", "Local", "Temp"), - TMP: process.env.TMP || path.join(home, "AppData", "Local", "Temp"), - }, - } - } - - override getDefaultShell(): string { - // Prefer PowerShell, fall back to cmd.exe - return ( - process.env.COMSPEC || - "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" - ) - } - - override async detectShell(): Promise { - // Windows doesn't have a per-user shell preference like Unix - // Just return the default - return this.getDefaultShell() - } - - override async detectLocale(): Promise { - // Windows uses different locale mechanism - // Try environment first - if (process.env.LANG) { - return process.env.LANG - } - - // Could query Windows locale via PowerShell, but for simplicity use default - return "en_US.UTF-8" - } - - async installCli( - sourcePath: string - ): Promise<{ success: boolean; error?: string; pathHint?: string }> { - const cliConfig = this.getCliConfig() - const installPath = cliConfig.installPath - const installDir = path.dirname(installPath) - - if (!existsSync(sourcePath)) { - return { success: false, error: "CLI script not found in app bundle" } - } - - try { - // Create directory and copy file - await mkdir(installDir, { recursive: true }) - await copyFile(sourcePath, installPath) - - // Note: We intentionally do NOT use `setx PATH` here because: - // 1. setx has a 1024 character limit that silently truncates PATH - // 2. It can corrupt the user's PATH environment variable - // Instead, the install directory is included in buildExtendedPath() - // which ensures the CLI is found when running from the app. - // - // For terminal usage, users can manually add to PATH: - // $env:Path += ";${installDir}" - - console.log("[CLI] Installed 1code command to", installPath) - console.log( - "[CLI] To use from terminal, add to PATH:", - `$env:Path += ";${installDir}"` - ) - - return { - success: true, - pathHint: `To use 1code from terminal, add to your PATH: ${installDir}`, - } - } catch (error: unknown) { - const errorMessage = - error instanceof Error ? error.message : "Installation failed" - console.error("[CLI] Failed to install:", error) - return { success: false, error: errorMessage } - } - } - - async uninstallCli(): Promise<{ success: boolean; error?: string }> { - const cliConfig = this.getCliConfig() - const installPath = cliConfig.installPath - - try { - if (!existsSync(installPath)) { - console.log("[CLI] CLI command not installed, nothing to uninstall") - return { success: true } - } - - await unlink(installPath) - - // Try to remove directory if empty - try { - await rmdir(path.dirname(installPath)) - } catch { - // Directory not empty or other error, that's okay - } - - console.log("[CLI] Uninstalled 1code command") - return { success: true } - } catch (error: unknown) { - const errorMessage = - error instanceof Error ? error.message : "Uninstallation failed" - console.error("[CLI] Failed to uninstall:", error) - return { success: false, error: errorMessage } - } - } - - isCliInstalled(sourcePath: string): boolean { - const cliConfig = this.getCliConfig() - try { - // Windows: just check if the file exists - return existsSync(cliConfig.installPath) - } catch { - return false - } - } -} diff --git a/src/main/lib/plugins/index.ts b/src/main/lib/plugins/index.ts deleted file mode 100644 index a54996c4..00000000 --- a/src/main/lib/plugins/index.ts +++ /dev/null @@ -1,203 +0,0 @@ -import * as fs from "fs/promises" -import type { Dirent } from "fs" -import * as path from "path" -import * as os from "os" -import type { McpServerConfig } from "../claude-config" - -export interface PluginInfo { - name: string - version: string - description?: string - path: string - source: string // e.g., "marketplace:plugin-name" - marketplace: string // e.g., "claude-plugins-official" - category?: string - homepage?: string - tags?: string[] -} - -interface MarketplacePlugin { - name: string - version?: string - description?: string - source: string | { source: string; url: string } - category?: string - homepage?: string - tags?: string[] -} - -interface MarketplaceJson { - name: string - plugins: MarketplacePlugin[] -} - -export interface PluginMcpConfig { - pluginSource: string // e.g., "ccsetup:ccsetup" - mcpServers: Record -} - -// Cache for plugin discovery results -let pluginCache: { plugins: PluginInfo[]; timestamp: number } | null = null -let mcpCache: { configs: PluginMcpConfig[]; timestamp: number } | null = null -const CACHE_TTL_MS = 30000 // 30 seconds - plugins don't change often during a session - -/** - * Clear plugin caches (for testing/manual invalidation) - */ -export function clearPluginCache() { - pluginCache = null - mcpCache = null -} - -/** - * Discover all installed plugins from ~/.claude/plugins/marketplaces/ - * Returns array of plugin info with paths to their component directories - * Results are cached for 30 seconds to avoid repeated filesystem scans - */ -export async function discoverInstalledPlugins(): Promise { - // Return cached result if still valid - if (pluginCache && Date.now() - pluginCache.timestamp < CACHE_TTL_MS) { - return pluginCache.plugins - } - - const plugins: PluginInfo[] = [] - const marketplacesDir = path.join(os.homedir(), ".claude", "plugins", "marketplaces") - - try { - await fs.access(marketplacesDir) - } catch { - pluginCache = { plugins, timestamp: Date.now() } - return plugins - } - - let marketplaces: Dirent[] - try { - marketplaces = await fs.readdir(marketplacesDir, { withFileTypes: true }) - } catch { - pluginCache = { plugins, timestamp: Date.now() } - return plugins - } - - for (const marketplace of marketplaces) { - if (!marketplace.isDirectory() || marketplace.name.startsWith(".")) continue - - const marketplacePath = path.join(marketplacesDir, marketplace.name) - const marketplaceJsonPath = path.join(marketplacePath, ".claude-plugin", "marketplace.json") - - try { - const content = await fs.readFile(marketplaceJsonPath, "utf-8") - - let marketplaceJson: MarketplaceJson - try { - marketplaceJson = JSON.parse(content) - } catch { - continue - } - - if (!Array.isArray(marketplaceJson.plugins)) { - continue - } - - for (const plugin of marketplaceJson.plugins) { - // Validate plugin.source exists - if (!plugin.source) continue - - // source can be a string path or an object { source: "url", url: "..." } - const sourcePath = typeof plugin.source === "string" ? plugin.source : null - if (!sourcePath) continue - - const pluginPath = path.resolve(marketplacePath, sourcePath) - try { - await fs.access(pluginPath) - plugins.push({ - name: plugin.name, - version: plugin.version || "0.0.0", - description: plugin.description, - path: pluginPath, - source: `${marketplaceJson.name}:${plugin.name}`, - marketplace: marketplaceJson.name, - category: plugin.category, - homepage: plugin.homepage, - tags: plugin.tags, - }) - } catch { - // Plugin directory not found, skip - } - } - } catch { - // No marketplace.json, skip silently (expected for non-plugin directories) - } - } - - pluginCache = { plugins, timestamp: Date.now() } - return plugins -} - -/** - * Get component paths for a plugin (commands, skills, agents directories) - */ -export function getPluginComponentPaths(plugin: PluginInfo) { - return { - commands: path.join(plugin.path, "commands"), - skills: path.join(plugin.path, "skills"), - agents: path.join(plugin.path, "agents"), - } -} - -/** - * Discover MCP server configs from all installed plugins - * Reads .mcp.json from each plugin directory - * Results are cached for 30 seconds to avoid repeated filesystem scans - */ -export async function discoverPluginMcpServers(): Promise { - // Return cached result if still valid - if (mcpCache && Date.now() - mcpCache.timestamp < CACHE_TTL_MS) { - return mcpCache.configs - } - - const plugins = await discoverInstalledPlugins() - const configs: PluginMcpConfig[] = [] - - for (const plugin of plugins) { - const mcpJsonPath = path.join(plugin.path, ".mcp.json") - try { - const content = await fs.readFile(mcpJsonPath, "utf-8") - let parsed: Record - try { - parsed = JSON.parse(content) - } catch { - continue - } - - // Support two formats: - // Format A (flat): { "server-name": { "command": "...", ... } } - // Format B (nested): { "mcpServers": { "server-name": { ... } } } - const serversObj = - parsed.mcpServers && - typeof parsed.mcpServers === "object" && - !Array.isArray(parsed.mcpServers) - ? (parsed.mcpServers as Record) - : parsed - - const validServers: Record = {} - for (const [name, config] of Object.entries(serversObj)) { - if (config && typeof config === "object" && !Array.isArray(config)) { - validServers[name] = config as McpServerConfig - } - } - - if (Object.keys(validServers).length > 0) { - configs.push({ - pluginSource: plugin.source, - mcpServers: validServers, - }) - } - } catch { - // No .mcp.json file, skip silently (this is expected for most plugins) - } - } - - // Cache the result - mcpCache = { configs, timestamp: Date.now() } - return configs -} diff --git a/src/main/lib/terminal/env.ts b/src/main/lib/terminal/env.ts index 041dce2e..44c70655 100644 --- a/src/main/lib/terminal/env.ts +++ b/src/main/lib/terminal/env.ts @@ -1,13 +1,10 @@ +import { execFile } from "node:child_process" +import { promisify } from "node:util" import os from "node:os" -import { - platform, - getDefaultShell as platformGetDefaultShell, - detectShell as platformDetectShell, - detectLocale as platformDetectLocale, -} from "../platform" - -export const FALLBACK_SHELL = - platform.platform === "win32" ? "cmd.exe" : "/bin/sh" + +const execFileAsync = promisify(execFile) + +export const FALLBACK_SHELL = os.platform() === "win32" ? "cmd.exe" : "/bin/sh" export const SHELL_CRASH_THRESHOLD_MS = 1000 // Global cache for shell detection (computed once per process lifetime) @@ -23,8 +20,14 @@ let localeDetectionPromise: Promise | null = null * For hot paths - returns cached value or fast fallback */ export function getDefaultShell(): string { + const platform = os.platform() + + if (platform === "win32") { + return process.env.COMSPEC || "powershell.exe" + } + // Use SHELL env var (most reliable on Unix) - if (platform.platform !== "win32" && process.env.SHELL) { + if (process.env.SHELL) { return process.env.SHELL } @@ -41,15 +44,34 @@ export function getDefaultShell(): string { }) } - // Return platform default as fast fallback - return platformGetDefaultShell() + // Return fast fallback - detection will update cache for next call + return "/bin/zsh" } /** * Async shell detection (used to populate cache) */ async function detectShellAsync(): Promise { - return platformDetectShell() + try { + const uid = process.getuid?.() + if (uid !== undefined) { + const { stdout: passwd } = await execFileAsync( + "sh", + ["-c", `getent passwd ${uid} 2>/dev/null || dscl . -read /Users/$(whoami) UserShell 2>/dev/null`], + { timeout: 1000 }, + ) + // getent format: user:x:uid:gid:name:home:shell + // dscl format: UserShell: /bin/zsh + const match = passwd.match(/UserShell:\s*(.+)/) || passwd.match(/:([^:]+)$/) + if (match?.[1]) { + return match[1].trim() + } + } + } catch { + // Ignore + } + + return "/bin/zsh" } /** @@ -86,7 +108,21 @@ export function getLocale(baseEnv: Record): string { * Async locale detection (used to populate cache) */ async function detectLocaleAsync(): Promise { - return platformDetectLocale() + try { + const { stdout: result } = await execFileAsync( + "sh", + ["-c", "locale 2>/dev/null | grep LANG= | cut -d= -f2"], + { timeout: 1000 }, + ) + const trimmed = result.trim() + if (trimmed?.includes("UTF-8")) { + return trimmed + } + } catch { + // Ignore - will use fallback + } + + return "en_US.UTF-8" } /** @@ -312,8 +348,8 @@ export function buildSafeEnv( env: Record, options?: { platform?: NodeJS.Platform } ): Record { - const currentPlatform = options?.platform ?? os.platform() - const isWindows = currentPlatform === "win32" + const platform = options?.platform ?? os.platform() + const isWindows = platform === "win32" const safe: Record = {} for (const [key, value] of Object.entries(env)) { diff --git a/src/main/lib/trpc/routers/agent-utils.ts b/src/main/lib/trpc/routers/agent-utils.ts index 776a7d51..c816d765 100644 --- a/src/main/lib/trpc/routers/agent-utils.ts +++ b/src/main/lib/trpc/routers/agent-utils.ts @@ -2,8 +2,6 @@ import * as fs from "fs/promises" import * as path from "path" import * as os from "os" import matter from "gray-matter" -import { discoverInstalledPlugins, getPluginComponentPaths } from "../../plugins" -import { getEnabledPlugins } from "./claude-settings" // Valid model values for agents export const VALID_AGENT_MODELS = ["sonnet", "opus", "haiku", "inherit"] as const @@ -21,8 +19,7 @@ export interface ParsedAgent { // Agent with source/path metadata export interface FileAgent extends ParsedAgent { - source: "user" | "project" | "plugin" - pluginName?: string + source: "user" | "project" path: string } @@ -44,50 +41,121 @@ export function parseAgentMd( ): Partial { try { const { data, content: body } = matter(content) + const result = extractAgentFields(data, body, filename) - // Parse tools - can be comma-separated string or array - let tools: string[] | undefined - if (typeof data.tools === "string") { - tools = data.tools - .split(",") - .map((t: string) => t.trim()) - .filter(Boolean) - } else if (Array.isArray(data.tools)) { - tools = data.tools - } - - // Parse disallowedTools - let disallowedTools: string[] | undefined - if (typeof data.disallowedTools === "string") { - disallowedTools = data.disallowedTools - .split(",") - .map((t: string) => t.trim()) - .filter(Boolean) - } else if (Array.isArray(data.disallowedTools)) { - disallowedTools = data.disallowedTools + // gray-matter can inconsistently parse complex YAML: sometimes it throws, + // sometimes it "succeeds" but silently drops fields (e.g. description becomes empty). + // If the file has a description field but gray-matter returned empty, use fallback. + if (!result.description && content.includes("description:")) { + const fallback = parseFrontmatterFallback(content, filename) + if (fallback && fallback.description) { + return fallback + } } - // Validate model - const model = - data.model && VALID_AGENT_MODELS.includes(data.model) - ? (data.model as AgentModel) - : undefined - - return { - name: - typeof data.name === "string" ? data.name : filename.replace(".md", ""), - description: typeof data.description === "string" ? data.description : "", - prompt: body.trim(), - tools, - disallowedTools, - model, - } + return result } catch (err) { + // gray-matter fails on complex YAML (e.g. unquoted colons in descriptions) + // Fall back to regex-based frontmatter parsing + const fallback = parseFrontmatterFallback(content, filename) + if (fallback) return fallback console.error("[agents] Failed to parse markdown:", err) return {} } } +/** + * Fallback parser for when gray-matter fails on complex YAML values. + * Extracts frontmatter fields line-by-line, treating the last field value + * as everything from the key to the next key or end of frontmatter. + */ +function parseFrontmatterFallback( + content: string, + filename: string +): Partial | null { + const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/) + if (!fmMatch) return null + + const frontmatterBlock = fmMatch[1] + const body = fmMatch[2] + + // Parse frontmatter line by line, handling multi-content values + // by greedily consuming until the next known key + const knownKeys = ["name", "description", "tools", "disallowedTools", "model", "color"] + const fields: Record = {} + + const lines = frontmatterBlock.split("\n") + let currentKey = "" + let currentValue = "" + + for (const line of lines) { + // Check if this line starts a new key + const keyMatch = line.match(/^(\w+):\s*(.*)$/) + if (keyMatch && knownKeys.includes(keyMatch[1])) { + // Save previous key-value + if (currentKey) { + fields[currentKey] = currentValue.trim() + } + currentKey = keyMatch[1] + currentValue = keyMatch[2] + } else if (currentKey) { + // Continuation of previous value + currentValue += "\n" + line + } + } + // Save last key-value + if (currentKey) { + fields[currentKey] = currentValue.trim() + } + + const data: Record = { ...fields } + return extractAgentFields(data, body, filename) +} + +function extractAgentFields( + data: Record, + body: string, + filename: string +): Partial { + // Parse tools - can be comma-separated string or array + let tools: string[] | undefined + if (typeof data.tools === "string") { + tools = data.tools + .split(",") + .map((t: string) => t.trim()) + .filter(Boolean) + } else if (Array.isArray(data.tools)) { + tools = data.tools + } + + // Parse disallowedTools + let disallowedTools: string[] | undefined + if (typeof data.disallowedTools === "string") { + disallowedTools = data.disallowedTools + .split(",") + .map((t: string) => t.trim()) + .filter(Boolean) + } else if (Array.isArray(data.disallowedTools)) { + disallowedTools = data.disallowedTools + } + + // Validate model + const model = + data.model && VALID_AGENT_MODELS.includes(data.model as AgentModel) + ? (data.model as AgentModel) + : undefined + + return { + name: + typeof data.name === "string" ? data.name : filename.replace(".md", ""), + description: typeof data.description === "string" ? data.description : "", + prompt: body.trim(), + tools, + disallowedTools, + model, + } +} + /** * Generate markdown content for agent file */ @@ -149,36 +217,7 @@ export async function loadAgent( } } - // Search in plugin directories - const [enabledPluginSources, installedPlugins] = await Promise.all([ - getEnabledPlugins(), - discoverInstalledPlugins(), - ]) - const enabledPlugins = installedPlugins.filter( - (p) => enabledPluginSources.includes(p.source), - ) - const pluginResults = await Promise.all( - enabledPlugins.map(async (plugin) => { - const paths = getPluginComponentPaths(plugin) - const agentPath = path.join(paths.agents, `${name}.md`) - try { - const content = await fs.readFile(agentPath, "utf-8") - const parsed = parseAgentMd(content, `${name}.md`) - if (parsed.description && parsed.prompt) { - return { - name: parsed.name || name, - description: parsed.description, - prompt: parsed.prompt, - tools: parsed.tools, - disallowedTools: parsed.disallowedTools, - model: parsed.model, - } - } - } catch {} - return null - }), - ) - return pluginResults.find((r) => r !== null) ?? null + return null } /** @@ -187,8 +226,7 @@ export async function loadAgent( */ export async function scanAgentsDirectory( dir: string, - source: "user" | "project" | "plugin", - basePath?: string // For project agents, the cwd to make paths relative to + source: "user" | "project" ): Promise { const agents: FileAgent[] = [] @@ -215,18 +253,6 @@ export async function scanAgentsDirectory( const parsed = parseAgentMd(content, entry.name) if (parsed.description && parsed.prompt) { - // For project agents, show relative path; for user agents, show ~/.claude/... path - let displayPath: string - if (source === "project" && basePath) { - displayPath = path.relative(basePath, agentPath) - } else { - // For user agents, show ~/.claude/agents/... format - const homeDir = os.homedir() - displayPath = agentPath.startsWith(homeDir) - ? "~" + agentPath.slice(homeDir.length) - : agentPath - } - agents.push({ name: parsed.name || entry.name.replace(".md", ""), description: parsed.description, @@ -235,7 +261,7 @@ export async function scanAgentsDirectory( disallowedTools: parsed.disallowedTools, model: parsed.model, source, - path: displayPath, + path: agentPath, }) } } catch (err) { diff --git a/src/main/lib/trpc/routers/agents.ts b/src/main/lib/trpc/routers/agents.ts index a2b19ced..519ed685 100644 --- a/src/main/lib/trpc/routers/agents.ts +++ b/src/main/lib/trpc/routers/agents.ts @@ -3,15 +3,56 @@ import { router, publicProcedure } from "../index" import * as fs from "fs/promises" import * as path from "path" import * as os from "os" +import { eq } from "drizzle-orm" import { parseAgentMd, generateAgentMd, scanAgentsDirectory, VALID_AGENT_MODELS, type FileAgent, + type AgentModel, } from "./agent-utils" -import { discoverInstalledPlugins, getPluginComponentPaths } from "../../plugins" -import { getEnabledPlugins } from "./claude-settings" +import { buildClaudeEnv, getBundledClaudeBinaryPath } from "../../claude" +import { getDatabase, claudeCodeCredentials } from "../../db" +import { app, safeStorage } from "electron" + +// Dynamic import for ESM module - cached to avoid re-importing +let cachedClaudeQuery: typeof import("@anthropic-ai/claude-agent-sdk").query | null = null +const getClaudeQuery = async () => { + if (cachedClaudeQuery) { + return cachedClaudeQuery + } + const sdk = await import("@anthropic-ai/claude-agent-sdk") + cachedClaudeQuery = sdk.query + return cachedClaudeQuery +} + +/** + * Get Claude Code OAuth token from local SQLite (same as claude.ts) + */ +function getClaudeCodeToken(): string | null { + try { + const db = getDatabase() + const cred = db + .select() + .from(claudeCodeCredentials) + .where(eq(claudeCodeCredentials.id, "default")) + .get() + + if (!cred?.oauthToken) { + return null + } + + if (!safeStorage.isEncryptionAvailable()) { + return Buffer.from(cred.oauthToken, "base64").toString("utf-8") + } + const buffer = Buffer.from(cred.oauthToken, "base64") + return safeStorage.decryptString(buffer) + } catch (error) { + console.error("[agents] Error getting Claude Code token:", error) + return null + } +} // Shared procedure for listing agents const listAgentsProcedure = publicProcedure @@ -29,37 +70,15 @@ const listAgentsProcedure = publicProcedure let projectAgentsPromise = Promise.resolve([]) if (input?.cwd) { const projectAgentsDir = path.join(input.cwd, ".claude", "agents") - projectAgentsPromise = scanAgentsDirectory(projectAgentsDir, "project", input.cwd) + projectAgentsPromise = scanAgentsDirectory(projectAgentsDir, "project") } - // Discover plugin agents - const [enabledPluginSources, installedPlugins] = await Promise.all([ - getEnabledPlugins(), - discoverInstalledPlugins(), + const [userAgents, projectAgents] = await Promise.all([ + userAgentsPromise, + projectAgentsPromise, ]) - const enabledPlugins = installedPlugins.filter( - (p) => enabledPluginSources.includes(p.source), - ) - const pluginAgentsPromises = enabledPlugins.map(async (plugin) => { - const paths = getPluginComponentPaths(plugin) - try { - const agents = await scanAgentsDirectory(paths.agents, "plugin") - return agents.map((agent) => ({ ...agent, pluginName: plugin.source })) - } catch { - return [] - } - }) - - // Scan all directories in parallel - const [userAgents, projectAgents, ...pluginAgentsArrays] = - await Promise.all([ - userAgentsPromise, - projectAgentsPromise, - ...pluginAgentsPromises, - ]) - const pluginAgents = pluginAgentsArrays.flat() - - return [...projectAgents, ...userAgents, ...pluginAgents] + + return [...projectAgents, ...userAgents] }) export const agentsRouter = router({ @@ -110,31 +129,6 @@ export const agentsRouter = router({ continue } } - - // Search in plugin directories - const [enabledPluginSources, installedPlugins] = await Promise.all([ - getEnabledPlugins(), - discoverInstalledPlugins(), - ]) - const enabledPlugins = installedPlugins.filter( - (p) => enabledPluginSources.includes(p.source), - ) - for (const plugin of enabledPlugins) { - const paths = getPluginComponentPaths(plugin) - const agentPath = path.join(paths.agents, `${input.name}.md`) - try { - const content = await fs.readFile(agentPath, "utf-8") - const parsed = parseAgentMd(content, `${input.name}.md`) - return { - ...parsed, - source: "plugin" as const, - pluginName: plugin.source, - path: agentPath, - } - } catch { - continue - } - } return null }), @@ -321,4 +315,219 @@ export const agentsRouter = router({ return { deleted: true } }), + + /** + * Generate an agent using Claude based on user's description + * This creates the system prompt, tools, etc. from a natural language description + * Uses the existing Claude Agent SDK to spawn a quick session + */ + generate: publicProcedure + .input( + z.object({ + name: z.string(), + description: z.string(), // User's natural language description of what the agent should do + source: z.enum(["user", "project"]), + model: z.enum(VALID_AGENT_MODELS).optional(), + cwd: z.string().optional(), + }) + ) + .mutation(async ({ input }) => { + // Validate name (kebab-case, no special chars) + const safeName = input.name.toLowerCase().replace(/[^a-z0-9-]/g, "-") + if (!safeName || safeName.includes("..")) { + throw new Error("Invalid agent name") + } + + // Determine target directory + let targetDir: string + if (input.source === "project") { + if (!input.cwd) { + throw new Error("Project path (cwd) required for project agents") + } + targetDir = path.join(input.cwd, ".claude", "agents") + } else { + targetDir = path.join(os.homedir(), ".claude", "agents") + } + + // Ensure directory exists + await fs.mkdir(targetDir, { recursive: true }) + + const agentPath = path.join(targetDir, `${safeName}.md`) + + // Check if already exists + try { + await fs.access(agentPath) + throw new Error(`Agent "${safeName}" already exists`) + } catch (err) { + if ((err as NodeJS.ErrnoException).code !== "ENOENT") { + throw err + } + } + + // Get the Claude SDK query function + const claudeQuery = await getClaudeQuery() + + const agentGenerationPrompt = `You are an expert at creating custom Claude Code agents. Generate a well-structured agent definition based on the user's description. + +Claude Code agents are specialized sub-agents that Claude can invoke via the Task tool. They have their own system prompt, tools, and model settings. + +Available tools that agents can use: +- Read: Read files from the filesystem +- Write: Write/create files +- Edit: Edit existing files with search/replace +- Glob: Find files by pattern +- Grep: Search file contents +- Bash: Execute shell commands +- WebFetch: Fetch content from URLs +- WebSearch: Search the web +- Task: Spawn sub-agents +- TodoWrite: Manage todo lists +- AskUserQuestion: Ask user for clarification +- NotebookEdit: Edit Jupyter notebooks + +Output a JSON object with these fields: +- description: A concise one-line description of what the agent does (used for Claude to decide when to invoke it) +- prompt: The full system prompt for the agent. This should be detailed, well-structured, and give the agent clear instructions on how to behave and accomplish its task. Use markdown formatting. +- tools: (optional) Array of tool names the agent should have access to. If not specified, inherits all tools. Only include if you want to RESTRICT the agent to specific tools. +- disallowedTools: (optional) Array of tool names the agent should NOT have access to. + +Guidelines for the system prompt: +1. Be specific about the agent's role and responsibilities +2. Include examples of how the agent should behave +3. Specify any constraints or best practices +4. Use second person ("You are...", "You should...") +5. Keep it focused - don't try to do too much + +Create an agent named "${safeName}" based on this description: + +${input.description} + +Output ONLY valid JSON, no markdown code blocks or explanations.` + + // Use the SDK to generate the agent definition + const isolatedConfigDir = path.join( + app.getPath("userData"), + "claude-sessions", + "agent-generation" + ) + await fs.mkdir(isolatedConfigDir, { recursive: true }) + + // Get OAuth token from DB (same as main chat flow) + const authToken = getClaudeCodeToken() + if (!authToken) { + throw new Error("Not authenticated. Please sign in to Claude first.") + } + + const claudeEnv = buildClaudeEnv() + // Pass OAuth token via the correct env var (same as claude.ts line 823) + claudeEnv.CLAUDE_CODE_OAUTH_TOKEN = authToken + const claudeBinaryPath = getBundledClaudeBinaryPath() + + let responseText = "" + let lastError: Error | null = null + + try { + const stream = claudeQuery({ + prompt: agentGenerationPrompt, + options: { + cwd: input.cwd || os.homedir(), + env: { + ...claudeEnv, + CLAUDE_CONFIG_DIR: isolatedConfigDir, + }, + permissionMode: "bypassPermissions" as const, + allowDangerouslySkipPermissions: true, + pathToClaudeCodeExecutable: claudeBinaryPath, + maxTurns: 1, + model: "claude-sonnet-4-20250514", + }, + }) + + // Collect the text response from the stream + for await (const msg of stream) { + const msgAny = msg as any + + // Check for errors + if (msgAny.type === "error" || msgAny.error) { + lastError = new Error(msgAny.error || msgAny.message || "Unknown SDK error") + break + } + + // Extract text from assistant messages + if (msgAny.type === "assistant" && msgAny.message?.content) { + for (const block of msgAny.message.content) { + if (block.type === "text" && block.text) { + responseText += block.text + } + } + } + } + } catch (streamError) { + console.error("[agents] SDK stream error:", streamError) + throw new Error(`Failed to generate agent: ${streamError instanceof Error ? streamError.message : String(streamError)}`) + } + + if (lastError) { + console.error("[agents] SDK error:", lastError) + throw new Error(`Failed to generate agent: ${lastError.message}`) + } + + if (!responseText.trim()) { + throw new Error("Failed to generate agent: no response from Claude") + } + + // Parse the JSON response + let generatedAgent: { + description: string + prompt: string + tools?: string[] + disallowedTools?: string[] + } + + try { + // Try to extract JSON from the response (handle potential markdown code blocks) + let jsonText = responseText.trim() + if (jsonText.startsWith("```json")) { + jsonText = jsonText.slice(7) + } else if (jsonText.startsWith("```")) { + jsonText = jsonText.slice(3) + } + if (jsonText.endsWith("```")) { + jsonText = jsonText.slice(0, -3) + } + jsonText = jsonText.trim() + + generatedAgent = JSON.parse(jsonText) + } catch (parseError) { + console.error("[agents] Failed to parse Claude response:", responseText) + throw new Error("Failed to parse agent definition from Claude") + } + + // Validate required fields + if (!generatedAgent.description || !generatedAgent.prompt) { + throw new Error("Generated agent is missing required fields (description, prompt)") + } + + // Generate and write the markdown file + const content = generateAgentMd({ + name: safeName, + description: generatedAgent.description, + prompt: generatedAgent.prompt, + tools: generatedAgent.tools, + disallowedTools: generatedAgent.disallowedTools, + model: input.model, + }) + + await fs.writeFile(agentPath, content, "utf-8") + + return { + name: safeName, + path: agentPath, + source: input.source, + description: generatedAgent.description, + prompt: generatedAgent.prompt, + tools: generatedAgent.tools, + disallowedTools: generatedAgent.disallowedTools, + } + }), }) diff --git a/src/main/lib/trpc/routers/anthropic-accounts.ts b/src/main/lib/trpc/routers/anthropic-accounts.ts deleted file mode 100644 index 4366cda6..00000000 --- a/src/main/lib/trpc/routers/anthropic-accounts.ts +++ /dev/null @@ -1,436 +0,0 @@ -import { eq, sql } from "drizzle-orm" -import { safeStorage } from "electron" -import { z } from "zod" -import { getAuthManager } from "../../../index" -import { anthropicAccounts, anthropicSettings, claudeCodeCredentials, getDatabase } from "../../db" -import { createId } from "../../db/utils" -import { publicProcedure, router } from "../index" - -/** - * Encrypt token using Electron's safeStorage - */ -function encryptToken(token: string): string { - if (!safeStorage.isEncryptionAvailable()) { - console.warn("[AnthropicAccounts] Encryption not available, storing as base64") - return Buffer.from(token).toString("base64") - } - return safeStorage.encryptString(token).toString("base64") -} - -/** - * Decrypt token using Electron's safeStorage - */ -function decryptToken(encrypted: string): string { - if (!safeStorage.isEncryptionAvailable()) { - return Buffer.from(encrypted, "base64").toString("utf-8") - } - const buffer = Buffer.from(encrypted, "base64") - return safeStorage.decryptString(buffer) -} - -/** - * Multi-account Anthropic management router - */ -export const anthropicAccountsRouter = router({ - /** - * List all stored Anthropic accounts - */ - list: publicProcedure.query(() => { - const db = getDatabase() - - try { - const accounts = db - .select({ - id: anthropicAccounts.id, - email: anthropicAccounts.email, - displayName: anthropicAccounts.displayName, - connectedAt: anthropicAccounts.connectedAt, - lastUsedAt: anthropicAccounts.lastUsedAt, - }) - .from(anthropicAccounts) - .orderBy(anthropicAccounts.connectedAt) - .all() - - // If we have accounts in new table, return them - if (accounts.length > 0) { - return accounts.map((acc) => ({ - ...acc, - connectedAt: acc.connectedAt?.toISOString() ?? null, - lastUsedAt: acc.lastUsedAt?.toISOString() ?? null, - })) - } - } catch { - // Table doesn't exist yet, fall through to legacy - } - - // Fallback: check legacy table and return as single account - try { - const legacyCred = db - .select() - .from(claudeCodeCredentials) - .where(eq(claudeCodeCredentials.id, "default")) - .get() - - if (legacyCred?.oauthToken) { - return [{ - id: "legacy-default", - email: null, - displayName: "Anthropic Account", - connectedAt: legacyCred.connectedAt?.toISOString() ?? null, - lastUsedAt: null, - }] - } - } catch { - // Legacy table also doesn't exist - } - - return [] - }), - - /** - * Get currently active account info - */ - getActive: publicProcedure.query(() => { - const db = getDatabase() - - try { - const settings = db - .select() - .from(anthropicSettings) - .where(eq(anthropicSettings.id, "singleton")) - .get() - - if (settings?.activeAccountId) { - const account = db - .select({ - id: anthropicAccounts.id, - email: anthropicAccounts.email, - displayName: anthropicAccounts.displayName, - connectedAt: anthropicAccounts.connectedAt, - }) - .from(anthropicAccounts) - .where(eq(anthropicAccounts.id, settings.activeAccountId)) - .get() - - if (account) { - return { - ...account, - connectedAt: account.connectedAt?.toISOString() ?? null, - } - } - } - } catch { - // Tables don't exist yet, fall through to legacy - } - - // Fallback: if legacy credential exists, treat it as active - try { - const legacyCred = db - .select() - .from(claudeCodeCredentials) - .where(eq(claudeCodeCredentials.id, "default")) - .get() - - if (legacyCred?.oauthToken) { - return { - id: "legacy-default", - email: null, - displayName: "Anthropic Account", - connectedAt: legacyCred.connectedAt?.toISOString() ?? null, - } - } - } catch { - // Legacy table also doesn't exist - } - - return null - }), - - /** - * Get decrypted OAuth token for active account - */ - getActiveToken: publicProcedure.query(() => { - const db = getDatabase() - const settings = db - .select() - .from(anthropicSettings) - .where(eq(anthropicSettings.id, "singleton")) - .get() - - if (!settings?.activeAccountId) { - return { token: null, error: "No active account" } - } - - const account = db - .select() - .from(anthropicAccounts) - .where(eq(anthropicAccounts.id, settings.activeAccountId)) - .get() - - if (!account) { - return { token: null, error: "Active account not found" } - } - - try { - const token = decryptToken(account.oauthToken) - return { token, error: null } - } catch (error) { - console.error("[AnthropicAccounts] Decrypt error:", error) - return { token: null, error: "Failed to decrypt token" } - } - }), - - /** - * Switch to a different account - */ - setActive: publicProcedure - .input(z.object({ accountId: z.string() })) - .mutation(({ input }) => { - const db = getDatabase() - - // Verify account exists - const account = db - .select() - .from(anthropicAccounts) - .where(eq(anthropicAccounts.id, input.accountId)) - .get() - - if (!account) { - throw new Error("Account not found") - } - - // Update or insert settings - db.insert(anthropicSettings) - .values({ - id: "singleton", - activeAccountId: input.accountId, - updatedAt: new Date(), - }) - .onConflictDoUpdate({ - target: anthropicSettings.id, - set: { - activeAccountId: input.accountId, - updatedAt: new Date(), - }, - }) - .run() - - // Update lastUsedAt on the account - db.update(anthropicAccounts) - .set({ lastUsedAt: new Date() }) - .where(eq(anthropicAccounts.id, input.accountId)) - .run() - - console.log(`[AnthropicAccounts] Switched to account: ${input.accountId}`) - return { success: true } - }), - - /** - * Add a new account (called after OAuth flow) - */ - add: publicProcedure - .input( - z.object({ - oauthToken: z.string().min(1), - email: z.string().optional(), - displayName: z.string().optional(), - }) - ) - .mutation(({ input }) => { - const db = getDatabase() - const authManager = getAuthManager() - const user = authManager.getUser() - - const encryptedToken = encryptToken(input.oauthToken) - const newId = createId() - - db.insert(anthropicAccounts) - .values({ - id: newId, - email: input.email ?? null, - displayName: input.displayName || input.email || "Anthropic Account", - oauthToken: encryptedToken, - connectedAt: new Date(), - desktopUserId: user?.id ?? null, - }) - .run() - - // Count accounts - const countResult = db - .select({ count: sql`count(*)` }) - .from(anthropicAccounts) - .get() - - // Automatically set as active if it's the first account - if (countResult?.count === 1) { - db.insert(anthropicSettings) - .values({ - id: "singleton", - activeAccountId: newId, - updatedAt: new Date(), - }) - .onConflictDoUpdate({ - target: anthropicSettings.id, - set: { - activeAccountId: newId, - updatedAt: new Date(), - }, - }) - .run() - } - - console.log(`[AnthropicAccounts] Added new account: ${newId}`) - return { id: newId, success: true } - }), - - /** - * Update account display name - */ - rename: publicProcedure - .input( - z.object({ - accountId: z.string(), - displayName: z.string().min(1), - }) - ) - .mutation(({ input }) => { - const db = getDatabase() - - const result = db - .update(anthropicAccounts) - .set({ displayName: input.displayName }) - .where(eq(anthropicAccounts.id, input.accountId)) - .run() - - if (result.changes === 0) { - throw new Error("Account not found") - } - - console.log(`[AnthropicAccounts] Renamed account ${input.accountId} to "${input.displayName}"`) - return { success: true } - }), - - /** - * Remove an account - */ - remove: publicProcedure - .input(z.object({ accountId: z.string() })) - .mutation(({ input }) => { - const db = getDatabase() - - // Check if this is the active account - const settings = db - .select() - .from(anthropicSettings) - .where(eq(anthropicSettings.id, "singleton")) - .get() - - // Delete the account - db.delete(anthropicAccounts) - .where(eq(anthropicAccounts.id, input.accountId)) - .run() - - // If deleted account was active, set another account as active - if (settings?.activeAccountId === input.accountId) { - const firstRemaining = db - .select() - .from(anthropicAccounts) - .limit(1) - .get() - - if (firstRemaining) { - db.update(anthropicSettings) - .set({ - activeAccountId: firstRemaining.id, - updatedAt: new Date(), - }) - .where(eq(anthropicSettings.id, "singleton")) - .run() - } else { - db.update(anthropicSettings) - .set({ - activeAccountId: null, - updatedAt: new Date(), - }) - .where(eq(anthropicSettings.id, "singleton")) - .run() - } - } - - console.log(`[AnthropicAccounts] Removed account: ${input.accountId}`) - return { success: true } - }), - - /** - * Check if any accounts are connected - */ - hasAccounts: publicProcedure.query(() => { - const db = getDatabase() - const countResult = db - .select({ count: sql`count(*)` }) - .from(anthropicAccounts) - .get() - - return { hasAccounts: (countResult?.count ?? 0) > 0 } - }), - - /** - * Migrate legacy account from claude_code_credentials to anthropic_accounts - * Called automatically if legacy account exists but no multi-accounts - */ - migrateLegacy: publicProcedure.mutation(() => { - const db = getDatabase() - - // Check if we already have accounts - const existingAccounts = db - .select({ count: sql`count(*)` }) - .from(anthropicAccounts) - .get() - - if ((existingAccounts?.count ?? 0) > 0) { - return { migrated: false, reason: "accounts_exist" } - } - - // Check for legacy credential - const legacyCred = db - .select() - .from(claudeCodeCredentials) - .where(eq(claudeCodeCredentials.id, "default")) - .get() - - if (!legacyCred?.oauthToken) { - return { migrated: false, reason: "no_legacy" } - } - - const newId = createId() - - // Insert into new table - db.insert(anthropicAccounts) - .values({ - id: newId, - oauthToken: legacyCred.oauthToken, - displayName: "Anthropic Account", - connectedAt: legacyCred.connectedAt, - desktopUserId: legacyCred.userId, - }) - .run() - - // Set as active - db.insert(anthropicSettings) - .values({ - id: "singleton", - activeAccountId: newId, - updatedAt: new Date(), - }) - .onConflictDoUpdate({ - target: anthropicSettings.id, - set: { - activeAccountId: newId, - updatedAt: new Date(), - }, - }) - .run() - - return { migrated: true, accountId: newId } - }), -}) diff --git a/src/main/lib/trpc/routers/chats.ts b/src/main/lib/trpc/routers/chats.ts index c21c793e..223c1361 100644 --- a/src/main/lib/trpc/routers/chats.ts +++ b/src/main/lib/trpc/routers/chats.ts @@ -16,13 +16,11 @@ import { fetchGitHubPRStatus, getWorktreeDiff, removeWorktree, - sanitizeProjectName, } from "../../git" import { computeContentHash, gitCache } from "../../git/cache" import { splitUnifiedDiffByFile } from "../../git/diff-parser" import { execWithShellEnv } from "../../git/shell-env" import { applyRollbackStash } from "../../git/stash" -import { checkInternetConnection, checkOllamaStatus } from "../../ollama" import { terminalManager } from "../../terminal/manager" import { publicProcedure, router } from "../index" @@ -35,149 +33,6 @@ function getFallbackName(userMessage: string): string { return trimmed.substring(0, 25) + "..." } -/** - * Generate text using local Ollama model - * Used for chat title generation in offline mode - * @param userMessage - The user message to generate a title for - * @param model - Optional model to use (if not provided, uses recommended model) - */ -async function generateChatNameWithOllama( - userMessage: string, - model?: string | null -): Promise { - try { - const ollamaStatus = await checkOllamaStatus() - if (!ollamaStatus.available) { - return null - } - - // Use provided model, or recommended, or first available - const modelToUse = model || ollamaStatus.recommendedModel || ollamaStatus.models[0] - if (!modelToUse) { - console.error("[Ollama] No model available") - return null - } - - const prompt = `Generate a very short (2-5 words) title for a coding chat that starts with this message. Only output the title, nothing else. No quotes, no explanations. - -User message: "${userMessage.slice(0, 500)}" - -Title:` - - const response = await fetch("http://localhost:11434/api/generate", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - model: modelToUse, - prompt, - stream: false, - options: { - temperature: 0.3, - num_predict: 50, - }, - }), - }) - - if (!response.ok) { - console.error("[Ollama] Generate chat name failed:", response.status) - return null - } - - const data = await response.json() - const result = data.response?.trim() - if (result) { - // Clean up the result - remove quotes, trim, limit length - const cleaned = result - .replace(/^["']|["']$/g, "") - .replace(/^title:\s*/i, "") - .trim() - .slice(0, 50) - if (cleaned.length > 0) { - return cleaned - } - } - return null - } catch (error) { - console.error("[Ollama] Generate chat name error:", error) - return null - } -} - -/** - * Generate commit message using local Ollama model - * Used for commit message generation in offline mode - * @param diff - The diff text - * @param fileCount - Number of files changed - * @param additions - Lines added - * @param deletions - Lines deleted - * @param model - Optional model to use (if not provided, uses recommended model) - */ -async function generateCommitMessageWithOllama( - diff: string, - fileCount: number, - additions: number, - deletions: number, - model?: string | null -): Promise { - try { - const ollamaStatus = await checkOllamaStatus() - if (!ollamaStatus.available) { - return null - } - - // Use provided model, or recommended, or first available - const modelToUse = model || ollamaStatus.recommendedModel || ollamaStatus.models[0] - if (!modelToUse) { - console.error("[Ollama] No model available") - return null - } - - const prompt = `Generate a conventional commit message for these changes. Use format: type: short description - -Types: feat (new feature), fix (bug fix), docs, style, refactor, test, chore - -Changes: ${fileCount} files, +${additions}/-${deletions} lines - -Diff (truncated): -${diff.slice(0, 3000)} - -Commit message:` - - const response = await fetch("http://localhost:11434/api/generate", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - model: modelToUse, - prompt, - stream: false, - options: { - temperature: 0.3, - num_predict: 50, - }, - }), - }) - - if (!response.ok) { - console.error("[Ollama] Generate commit message failed:", response.status) - return null - } - - const data = await response.json() - const result = data.response?.trim() - if (result) { - // Clean up - get just the first line - const firstLine = result.split("\n")[0]?.trim() - if (firstLine && firstLine.length > 0 && firstLine.length < 100) { - return firstLine - } - } - return null - } catch (error) { - console.error("[Ollama] Generate commit message error:", error) - return null - } -} - export const chatsRouter = router({ /** * List all non-archived chats (optionally filter by project) @@ -265,17 +120,10 @@ export const chatsRouter = router({ base64Data: z.string().optional(), }), }), - // Hidden file content - sent to agent but not displayed in UI - z.object({ - type: z.literal("file-content"), - filePath: z.string(), - content: z.string(), - }), ]), ) .optional(), baseBranch: z.string().optional(), // Branch to base the worktree off - branchType: z.enum(["local", "remote"]).optional(), // Whether baseBranch is local or remote useWorktree: z.boolean().default(true), // If false, work directly in project dir mode: z.enum(["plan", "agent"]).default("agent"), }), @@ -346,15 +194,12 @@ export const chatsRouter = router({ console.log( "[chats.create] creating worktree with baseBranch:", input.baseBranch, - "type:", - input.branchType, ) const result = await createWorktreeForChat( project.path, - sanitizeProjectName(project.name), + project.id, chat.id, input.baseBranch, - input.branchType, ) console.log("[chats.create] worktree result:", result) @@ -716,30 +561,13 @@ export const chatsRouter = router({ // 4. Rollback git state first - if this fails, abort the whole operation if (chat?.worktreePath) { const res = await applyRollbackStash(chat.worktreePath, input.sdkMessageUuid) - if (!res.success) { - return { success: false, error: `Git rollback failed: ${res.error}` } - } - // If checkpoint wasn't found, we still fail because we can't safely rollback - // without reverting the git state to match the message history - if (!res.checkpointFound) { - return { success: false, error: "Checkpoint not found - cannot rollback git state" } + if (!res) { + return { success: false, error: `Git rollback failed` } } } // 5. Truncate messages to include up to and including the target message - let truncatedMessages = messages.slice(0, targetIndex + 1) - - // 5.5. Clear any old shouldResume flags, then set on the target message - truncatedMessages = truncatedMessages.map((m: any, i: number) => { - const { shouldResume, ...restMeta } = m.metadata || {} - return { - ...m, - metadata: { - ...restMeta, - ...(i === truncatedMessages.length - 1 && { shouldResume: true }), - }, - } - }) + const truncatedMessages = messages.slice(0, targetIndex + 1) // 6. Update the sub-chat with truncated messages db.update(subChats) @@ -967,13 +795,11 @@ export const chatsRouter = router({ * Generate a commit message using AI based on the diff * @param chatId - The chat ID to get worktree path from * @param filePaths - Optional list of file paths to generate message for (if not provided, uses all changed files) - * @param ollamaModel - Optional Ollama model for offline generation */ generateCommitMessage: publicProcedure .input(z.object({ chatId: z.string(), filePaths: z.array(z.string()).optional(), - ollamaModel: z.string().nullish(), // Optional model for offline mode })) .mutation(async ({ input }) => { const db = getDatabase() @@ -1018,80 +844,53 @@ export const chatsRouter = router({ // Build filtered diff text for API (only selected files) const filteredDiff = files.map(f => f.diffText).join('\n') - const additions = files.reduce((sum, f) => sum + f.additions, 0) - const deletions = files.reduce((sum, f) => sum + f.deletions, 0) - - // Check internet first - if offline, use Ollama - const hasInternet = await checkInternetConnection() - - if (!hasInternet) { - console.log("[generateCommitMessage] Offline - trying Ollama...") - const ollamaMessage = await generateCommitMessageWithOllama( - filteredDiff, - files.length, - additions, - deletions, - input.ollamaModel - ) - if (ollamaMessage) { - console.log("[generateCommitMessage] Generated via Ollama:", ollamaMessage) - return { message: ollamaMessage } - } - console.log("[generateCommitMessage] Ollama failed, using heuristic fallback") - // Fall through to heuristic fallback below - } else { - // Online - call web API to generate commit message - let apiError: string | null = null - try { - const authManager = getAuthManager() - const token = await authManager.getValidToken() - // Use localhost in dev, production otherwise - const apiUrl = process.env.NODE_ENV === "development" ? "http://localhost:3000" : "https://21st.dev" - if (!token) { - apiError = "No auth token available" - } else { - const response = await fetch( - `${apiUrl}/api/agents/generate-commit-message`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-Desktop-Token": token, - }, - body: JSON.stringify({ - diff: filteredDiff.slice(0, 10000), // Limit diff size, use filtered diff - fileCount: files.length, - additions, - deletions, - }), + // Call web API to generate commit message + let apiError: string | null = null + try { + const authManager = getAuthManager() + const token = await authManager.getValidToken() + // Use localhost in dev, production otherwise + const apiUrl = process.env.NODE_ENV === "development" ? "http://localhost:3000" : "https://21st.dev" + + if (!token) { + apiError = "No auth token available" + } else { + const response = await fetch( + `${apiUrl}/api/agents/generate-commit-message`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Desktop-Token": token, }, - ) + body: JSON.stringify({ + diff: filteredDiff.slice(0, 10000), // Limit diff size, use filtered diff + fileCount: files.length, + additions: files.reduce((sum, f) => sum + f.additions, 0), + deletions: files.reduce((sum, f) => sum + f.deletions, 0), + }), + }, + ) - if (response.ok) { - const data = await response.json() - if (data.message) { - return { message: data.message } - } - apiError = "API returned ok but no message in response" - } else { - apiError = `API returned ${response.status}` + if (response.ok) { + const data = await response.json() + if (data.message) { + return { message: data.message } } + apiError = "API returned ok but no message in response" + } else { + apiError = `API returned ${response.status}` } - } catch (error) { - apiError = `API call failed: ${error instanceof Error ? error.message : String(error)}` - } - - if (apiError) { - console.log("[generateCommitMessage] API error:", apiError) } + } catch (error) { + apiError = `API call failed: ${error instanceof Error ? error.message : String(error)}` } // Fallback: Generate commit message with conventional commits style const fileNames = files.map((f) => { - const filePath = f.newPath !== "/dev/null" ? f.newPath : f.oldPath - // Note: Git diff paths always use forward slashes - return path.posix.basename(filePath) || filePath + const path = f.newPath !== "/dev/null" ? f.newPath : f.oldPath + return path.split("/").pop() || path }) // Detect commit type from file changes @@ -1144,39 +943,26 @@ export const chatsRouter = router({ }), /** - * Generate a name for a sub-chat using AI - * Uses Ollama when offline, otherwise calls web API + * Generate a name for a sub-chat using AI (calls web API) + * Always uses production API since it's a lightweight call */ generateSubChatName: publicProcedure - .input(z.object({ - userMessage: z.string(), - ollamaModel: z.string().nullish(), // Optional model for offline mode - })) + .input(z.object({ userMessage: z.string() })) .mutation(async ({ input }) => { try { - // Check internet first - if offline, use Ollama - const hasInternet = await checkInternetConnection() - - if (!hasInternet) { - console.log("[generateSubChatName] Offline - trying Ollama...") - const ollamaName = await generateChatNameWithOllama(input.userMessage, input.ollamaModel) - if (ollamaName) { - console.log("[generateSubChatName] Generated name via Ollama:", ollamaName) - return { name: ollamaName } - } - console.log("[generateSubChatName] Ollama failed, using fallback") - return { name: getFallbackName(input.userMessage) } - } - - // Online - use web API const authManager = getAuthManager() const token = await authManager.getValidToken() + // Always use production API for name generation const apiUrl = "https://21st.dev" console.log( - "[generateSubChatName] Online - calling API with token:", + "[generateSubChatName] Calling API with token:", token ? "present" : "missing", ) + console.log( + "[generateSubChatName] URL:", + `${apiUrl}/api/agents/sub-chat/generate-name`, + ) const response = await fetch( `${apiUrl}/api/agents/sub-chat/generate-name`, @@ -1381,50 +1167,28 @@ export const chatsRouter = router({ /** * Get file change stats for workspaces * Parses messages from specified sub-chats and aggregates Edit/Write tool calls - * Supports two modes: - * - openSubChatIds: query specific sub-chats (used by main sidebar) - * - chatIds: query all sub-chats for given chats (used by archive popover) + * REQUIRES openSubChatIds to avoid loading all sub-chats (performance optimization) */ getFileStats: publicProcedure - .input(z.object({ - openSubChatIds: z.array(z.string()).optional(), - chatIds: z.array(z.string()).optional(), - })) + .input(z.object({ openSubChatIds: z.array(z.string()) })) .query(({ input }) => { const db = getDatabase() - // Early return if nothing to check - if ((!input.openSubChatIds || input.openSubChatIds.length === 0) && - (!input.chatIds || input.chatIds.length === 0)) { + // Early return if no sub-chats to check + if (input.openSubChatIds.length === 0) { return [] } - // Query sub-chats based on input mode - let allChats: Array<{ chatId: string | null; subChatId: string; messages: string | null }> - - if (input.chatIds && input.chatIds.length > 0) { - // Archive mode: query all sub-chats for given chat IDs - allChats = db - .select({ - chatId: subChats.chatId, - subChatId: subChats.id, - messages: subChats.messages, - }) - .from(subChats) - .where(inArray(subChats.chatId, input.chatIds)) - .all() - } else { - // Main sidebar mode: query specific sub-chats - allChats = db - .select({ - chatId: subChats.chatId, - subChatId: subChats.id, - messages: subChats.messages, - }) - .from(subChats) - .where(inArray(subChats.id, input.openSubChatIds!)) - .all() - } + // Query only the specified sub-chats (VS Code style: load only what's needed) + const allChats = db + .select({ + chatId: subChats.chatId, + subChatId: subChats.id, + messages: subChats.messages, + }) + .from(subChats) + .where(inArray(subChats.id, input.openSubChatIds)) + .all() // Aggregate stats per workspace (chatId) const statsMap = new Map< @@ -1434,7 +1198,6 @@ export const chatsRouter = router({ for (const row of allChats) { if (!row.messages || !row.chatId) continue - const chatId = row.chatId // TypeScript narrowing try { const messages = JSON.parse(row.messages) as Array<{ @@ -1511,7 +1274,7 @@ export const chatsRouter = router({ } // Add to workspace total - const existing = statsMap.get(chatId) || { + const existing = statsMap.get(row.chatId) || { additions: 0, deletions: 0, fileCount: 0, @@ -1519,7 +1282,7 @@ export const chatsRouter = router({ existing.additions += subChatAdditions existing.deletions += subChatDeletions existing.fileCount += subChatFileCount - statsMap.set(chatId, existing) + statsMap.set(row.chatId, existing) } catch { // Skip invalid JSON } @@ -1534,7 +1297,7 @@ export const chatsRouter = router({ /** * Get sub-chats with pending plan approvals - * Uses mode field as source of truth: mode="plan" + completed ExitPlanMode = pending approval + * Parses messages to find ExitPlanMode tool calls without subsequent "Implement plan" user message * Logic must match active-chat.tsx hasUnapprovedPlan * REQUIRES openSubChatIds to avoid loading all sub-chats (performance optimization) */ @@ -1548,12 +1311,11 @@ export const chatsRouter = router({ return [] } - // Query only the specified sub-chats, including mode for filtering + // Query only the specified sub-chats (VS Code style: load only what's needed) const allSubChats = db .select({ chatId: subChats.chatId, subChatId: subChats.id, - mode: subChats.mode, messages: subChats.messages, }) .from(subChats) @@ -1563,13 +1325,7 @@ export const chatsRouter = router({ const pendingApprovals: Array<{ subChatId: string; chatId: string }> = [] for (const row of allSubChats) { - if (!row.subChatId || !row.chatId) continue - - // If mode is "agent", plan is already approved - skip - if (row.mode === "agent") continue - - // Only check for ExitPlanMode in plan mode sub-chats - if (!row.messages) continue + if (!row.messages || !row.subChatId || !row.chatId) continue try { const messages = JSON.parse(row.messages) as Array<{ @@ -1578,31 +1334,38 @@ export const chatsRouter = router({ parts?: Array<{ type: string text?: string - output?: unknown }> }> - // Check if there's a completed ExitPlanMode in messages - const hasCompletedExitPlanMode = (): boolean => { - for (let i = messages.length - 1; i >= 0; i--) { - const msg = messages[i] - if (!msg) continue + // Traverse messages from end to find unapproved ExitPlanMode + // Logic matches active-chat.tsx hasUnapprovedPlan + let hasUnapprovedPlan = false + + for (let i = messages.length - 1; i >= 0; i--) { + const msg = messages[i] + if (!msg) continue + + // If user message says "Build plan" or "Implement plan" (exact match), plan is already approved + if (msg.role === "user") { + const textPart = msg.parts?.find((p) => p.type === "text") + const text = textPart?.text || "" + const normalizedText = text.trim().toLowerCase() + if (normalizedText === "implement plan" || normalizedText === "build plan") { + break // Plan was approved, stop searching + } + } - // If assistant message with completed ExitPlanMode, we found an unapproved plan - if (msg.role === "assistant" && msg.parts) { - const exitPlanPart = msg.parts.find( - (p) => p.type === "tool-ExitPlanMode" - ) - // Check if ExitPlanMode is completed (has output, even if empty) - if (exitPlanPart && exitPlanPart.output !== undefined) { - return true - } + // If assistant message with ExitPlanMode, we found an unapproved plan + if (msg.role === "assistant" && msg.parts) { + const exitPlanPart = msg.parts.find((p) => p.type === "tool-ExitPlanMode") + if (exitPlanPart) { + hasUnapprovedPlan = true + break } } - return false } - if (hasCompletedExitPlanMode()) { + if (hasUnapprovedPlan) { pendingApprovals.push({ subChatId: row.subChatId, chatId: row.chatId, @@ -1649,312 +1412,4 @@ export const chatsRouter = router({ return { hasWorktree: false, uncommittedCount: 0 } } }), - - /** - * Export a chat conversation to various formats. - * Supports exporting entire workspace or a single sub-chat. - * Useful for sharing, backup, or importing into other tools. - */ - exportChat: publicProcedure - .input( - z.object({ - chatId: z.string(), - subChatId: z.string().optional(), // If provided, export only this sub-chat - format: z.enum(["json", "markdown", "text"]).default("markdown"), - }), - ) - .query(async ({ input }) => { - const db = getDatabase() - const chat = db - .select() - .from(chats) - .where(eq(chats.id, input.chatId)) - .get() - - if (!chat) { - throw new Error("Chat not found") - } - - const project = db - .select() - .from(projects) - .where(eq(projects.id, chat.projectId)) - .get() - - // Query sub-chats: either a specific one or all for the chat - let chatSubChats - if (input.subChatId) { - // Export single sub-chat - const singleSubChat = db - .select() - .from(subChats) - .where(and( - eq(subChats.id, input.subChatId), - eq(subChats.chatId, input.chatId) // Ensure sub-chat belongs to this chat - )) - .get() - - if (!singleSubChat) { - throw new Error("Sub-chat not found") - } - chatSubChats = [singleSubChat] - } else { - // Export all sub-chats - chatSubChats = db - .select() - .from(subChats) - .where(eq(subChats.chatId, input.chatId)) - .orderBy(subChats.createdAt) - .all() - } - - // parse messages from sub-chats - const allMessages: Array<{ - subChatId: string - subChatName: string | null - messages: Array<{ - id: string - role: string - parts: Array<{ type: string; text?: string; [key: string]: any }> - metadata?: any - }> - }> = [] - - for (const subChat of chatSubChats) { - try { - const messages = JSON.parse(subChat.messages || "[]") - allMessages.push({ - subChatId: subChat.id, - subChatName: subChat.name, - messages, - }) - } catch { - // skip invalid json - } - } - - // Sanitize filename - remove characters that are invalid on Windows/macOS/Linux - const sanitizeFilename = (name: string): string => { - return name - .replace(/[<>:"/\\|?*\x00-\x1F]/g, "_") // Invalid chars - .replace(/\s+/g, "_") // Replace spaces with underscores - .replace(/_+/g, "_") // Collapse multiple underscores - .replace(/^_|_$/g, "") // Trim underscores from ends - .slice(0, 100) // Limit length - || "chat" // Fallback if empty - } - - // Use sub-chat name if exporting single sub-chat, otherwise use chat name - const exportName = input.subChatId && chatSubChats[0]?.name - ? `${chat.name || "chat"}-${chatSubChats[0].name}` - : (chat.name || "chat") - const safeFilename = sanitizeFilename(exportName) - - if (input.format === "json") { - return { - format: "json" as const, - content: JSON.stringify( - { - exportedAt: new Date().toISOString(), - chat: { - id: chat.id, - name: chat.name, - createdAt: chat.createdAt, - branch: chat.branch, - baseBranch: chat.baseBranch, - prUrl: chat.prUrl, - }, - project: project - ? { - id: project.id, - name: project.name, - path: project.path, - } - : null, - conversations: allMessages, - }, - null, - 2, - ), - filename: `${safeFilename}-${chat.id.slice(0, 8)}.json`, - } - } - - if (input.format === "text") { - // plain text format - let text = `# ${chat.name || "Untitled Chat"}\n` - text += `exported: ${new Date().toISOString()}\n` - if (project) { - text += `project: ${project.name}\n` - } - text += `\n---\n\n` - - for (const subChatData of allMessages) { - if (subChatData.subChatName) { - text += `## ${subChatData.subChatName}\n\n` - } - - for (const msg of subChatData.messages) { - const role = msg.role === "user" ? "You" : "Assistant" - text += `${role}:\n` - - for (const part of msg.parts || []) { - if (part.type === "text" && part.text) { - text += `${part.text}\n` - } else if (part.type?.startsWith("tool-") && part.toolName) { - text += `[used ${part.toolName} tool]\n` - } - } - text += "\n" - } - } - - return { - format: "text" as const, - content: text, - filename: `${safeFilename}-${chat.id.slice(0, 8)}.txt`, - } - } - - // markdown format (default) - let markdown = `# ${chat.name || "Untitled Chat"}\n\n` - markdown += `**Exported:** ${new Date().toISOString()}\n\n` - if (project) { - markdown += `**Project:** ${project.name}\n\n` - } - if (chat.branch) { - markdown += `**Branch:** \`${chat.branch}\`\n\n` - } - if (chat.prUrl) { - markdown += `**PR:** [${chat.prUrl}](${chat.prUrl})\n\n` - } - markdown += `---\n\n` - - for (const subChatData of allMessages) { - if (subChatData.subChatName) { - markdown += `## ${subChatData.subChatName}\n\n` - } - - for (const msg of subChatData.messages) { - const role = msg.role === "user" ? "**You**" : "**Assistant**" - markdown += `### ${role}\n\n` - - for (const part of msg.parts || []) { - if (part.type === "text" && part.text) { - markdown += `${part.text}\n\n` - } else if (part.type?.startsWith("tool-") && part.toolName) { - const toolName = part.toolName - if (toolName === "Bash" && part.input?.command) { - markdown += `\`\`\`bash\n${part.input.command}\n\`\`\`\n\n` - } else if ( - (toolName === "Edit" || toolName === "Write") && - part.input?.file_path - ) { - markdown += `> Modified: \`${part.input.file_path}\`\n\n` - } else if (toolName === "Read" && part.input?.file_path) { - markdown += `> Read: \`${part.input.file_path}\`\n\n` - } else { - markdown += `> *Used ${toolName} tool*\n\n` - } - } - } - } - } - - return { - format: "markdown" as const, - content: markdown, - filename: `${safeFilename}-${chat.id.slice(0, 8)}.md`, - } - }), - - /** - * Get basic stats for a chat (message count, tool usage, etc.) - * Supports both full chat stats and individual sub-chat stats. - * Useful for showing chat summary in sidebar or export dialogs. - */ - getChatStats: publicProcedure - .input(z.object({ - chatId: z.string(), - subChatId: z.string().optional(), // If provided, return stats for only this sub-chat - })) - .query(({ input }) => { - const db = getDatabase() - - let chatSubChats - if (input.subChatId) { - // Get stats for a single sub-chat - const singleSubChat = db - .select() - .from(subChats) - .where(and( - eq(subChats.id, input.subChatId), - eq(subChats.chatId, input.chatId) - )) - .get() - - chatSubChats = singleSubChat ? [singleSubChat] : [] - } else { - // Get stats for all sub-chats - chatSubChats = db - .select() - .from(subChats) - .where(eq(subChats.chatId, input.chatId)) - .all() - } - - let messageCount = 0 - let userMessageCount = 0 - let assistantMessageCount = 0 - let toolCalls = 0 - const toolUsage: Record = {} - let totalInputTokens = 0 - let totalOutputTokens = 0 - - for (const subChat of chatSubChats) { - try { - const messages = JSON.parse(subChat.messages || "[]") as Array<{ - role: string - parts?: Array<{ type: string; toolName?: string }> - metadata?: { usage?: { inputTokens?: number; outputTokens?: number } } - }> - - for (const msg of messages) { - messageCount++ - if (msg.role === "user") { - userMessageCount++ - } else if (msg.role === "assistant") { - assistantMessageCount++ - - // count tool calls - for (const part of msg.parts || []) { - if (part.type?.startsWith("tool-") && part.toolName) { - toolCalls++ - toolUsage[part.toolName] = (toolUsage[part.toolName] || 0) + 1 - } - } - - // aggregate token usage - if (msg.metadata?.usage) { - totalInputTokens += msg.metadata.usage.inputTokens || 0 - totalOutputTokens += msg.metadata.usage.outputTokens || 0 - } - } - } - } catch { - // skip invalid json - } - } - - return { - messageCount, - userMessageCount, - assistantMessageCount, - toolCalls, - toolUsage, - totalInputTokens, - totalOutputTokens, - subChatCount: chatSubChats.length, - } - }), }) diff --git a/src/main/lib/trpc/routers/claude-code.ts b/src/main/lib/trpc/routers/claude-code.ts index 78766a88..019b8f1f 100644 --- a/src/main/lib/trpc/routers/claude-code.ts +++ b/src/main/lib/trpc/routers/claude-code.ts @@ -1,17 +1,10 @@ -import { eq, sql } from "drizzle-orm" +import { eq } from "drizzle-orm" import { safeStorage, shell } from "electron" import { z } from "zod" import { getAuthManager } from "../../../index" -import { getClaudeShellEnvironment } from "../../claude" import { getExistingClaudeToken } from "../../claude-token" import { getApiUrl } from "../../config" -import { - anthropicAccounts, - anthropicSettings, - claudeCodeCredentials, - getDatabase, -} from "../../db" -import { createId } from "../../db/utils" +import { claudeCodeCredentials, getDatabase } from "../../db" import { publicProcedure, router } from "../index" /** @@ -44,48 +37,13 @@ function decryptToken(encrypted: string): string { return safeStorage.decryptString(buffer) } -/** - * Store OAuth token - now uses multi-account system - * If setAsActive is true, also sets this account as active - */ -function storeOAuthToken(oauthToken: string, setAsActive = true): string { +function storeOAuthToken(oauthToken: string) { const authManager = getAuthManager() const user = authManager.getUser() const encryptedToken = encryptToken(oauthToken) const db = getDatabase() - const newId = createId() - - // Store in new multi-account table - db.insert(anthropicAccounts) - .values({ - id: newId, - oauthToken: encryptedToken, - displayName: "Anthropic Account", - connectedAt: new Date(), - desktopUserId: user?.id ?? null, - }) - .run() - if (setAsActive) { - // Set as active account - db.insert(anthropicSettings) - .values({ - id: "singleton", - activeAccountId: newId, - updatedAt: new Date(), - }) - .onConflictDoUpdate({ - target: anthropicSettings.id, - set: { - activeAccountId: newId, - updatedAt: new Date(), - }, - }) - .run() - } - - // Also update legacy table for backward compatibility db.delete(claudeCodeCredentials) .where(eq(claudeCodeCredentials.id, "default")) .run() @@ -98,8 +56,6 @@ function storeOAuthToken(oauthToken: string, setAsActive = true): string { userId: user?.id ?? null, }) .run() - - return newId } /** @@ -107,53 +63,11 @@ function storeOAuthToken(oauthToken: string, setAsActive = true): string { * Uses server only for sandbox creation, stores token locally */ export const claudeCodeRouter = router({ - /** - * Check if user has existing CLI config (API key or proxy) - * If true, user can skip OAuth onboarding - * Based on PR #29 by @sa4hnd - */ - hasExistingCliConfig: publicProcedure.query(() => { - const shellEnv = getClaudeShellEnvironment() - const hasConfig = !!(shellEnv.ANTHROPIC_API_KEY || shellEnv.ANTHROPIC_BASE_URL) - return { - hasConfig, - hasApiKey: !!shellEnv.ANTHROPIC_API_KEY, - baseUrl: shellEnv.ANTHROPIC_BASE_URL || null, - } - }), - /** * Check if user has Claude Code connected (local check) - * Now uses multi-account system - checks for active account */ getIntegration: publicProcedure.query(() => { const db = getDatabase() - - // First try multi-account system - const settings = db - .select() - .from(anthropicSettings) - .where(eq(anthropicSettings.id, "singleton")) - .get() - - if (settings?.activeAccountId) { - const account = db - .select() - .from(anthropicAccounts) - .where(eq(anthropicAccounts.id, settings.activeAccountId)) - .get() - - if (account) { - return { - isConnected: true, - connectedAt: account.connectedAt?.toISOString() ?? null, - accountId: account.id, - displayName: account.displayName, - } - } - } - - // Fallback to legacy table const cred = db .select() .from(claudeCodeCredentials) @@ -163,8 +77,6 @@ export const claudeCodeRouter = router({ return { isConnected: !!cred?.oauthToken, connectedAt: cred?.connectedAt?.toISOString() ?? null, - accountId: null, - displayName: null, } }), @@ -329,37 +241,9 @@ export const claudeCodeRouter = router({ /** * Get decrypted OAuth token (local) - * Now uses multi-account system - gets token from active account */ getToken: publicProcedure.query(() => { const db = getDatabase() - - // First try multi-account system - const settings = db - .select() - .from(anthropicSettings) - .where(eq(anthropicSettings.id, "singleton")) - .get() - - if (settings?.activeAccountId) { - const account = db - .select() - .from(anthropicAccounts) - .where(eq(anthropicAccounts.id, settings.activeAccountId)) - .get() - - if (account) { - try { - const token = decryptToken(account.oauthToken) - return { token, error: null } - } catch (error) { - console.error("[ClaudeCode] Decrypt error:", error) - return { token: null, error: "Failed to decrypt token" } - } - } - } - - // Fallback to legacy table const cred = db .select() .from(claudeCodeCredentials) @@ -380,47 +264,10 @@ export const claudeCodeRouter = router({ }), /** - * Disconnect - delete active account from multi-account system + * Disconnect - delete local credentials */ disconnect: publicProcedure.mutation(() => { const db = getDatabase() - - // Get active account - const settings = db - .select() - .from(anthropicSettings) - .where(eq(anthropicSettings.id, "singleton")) - .get() - - if (settings?.activeAccountId) { - // Remove active account - db.delete(anthropicAccounts) - .where(eq(anthropicAccounts.id, settings.activeAccountId)) - .run() - - // Try to set another account as active - const firstRemaining = db.select().from(anthropicAccounts).limit(1).get() - - if (firstRemaining) { - db.update(anthropicSettings) - .set({ - activeAccountId: firstRemaining.id, - updatedAt: new Date(), - }) - .where(eq(anthropicSettings.id, "singleton")) - .run() - } else { - db.update(anthropicSettings) - .set({ - activeAccountId: null, - updatedAt: new Date(), - }) - .where(eq(anthropicSettings.id, "singleton")) - .run() - } - } - - // Also clear legacy table db.delete(claudeCodeCredentials) .where(eq(claudeCodeCredentials.id, "default")) .run() diff --git a/src/main/lib/trpc/routers/claude-settings.ts b/src/main/lib/trpc/routers/claude-settings.ts deleted file mode 100644 index 6ab64905..00000000 --- a/src/main/lib/trpc/routers/claude-settings.ts +++ /dev/null @@ -1,276 +0,0 @@ -import * as fs from "fs/promises" -import * as path from "path" -import * as os from "os" -import { z } from "zod" -import { router, publicProcedure } from "../index" - -const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json") - -// Cache for enabled plugins to avoid repeated filesystem reads -let enabledPluginsCache: { plugins: string[]; timestamp: number } | null = null -const ENABLED_PLUGINS_CACHE_TTL_MS = 5000 // 5 seconds - -// Cache for approved plugin MCP servers -let approvedMcpCache: { servers: string[]; timestamp: number } | null = null -const APPROVED_MCP_CACHE_TTL_MS = 5000 // 5 seconds - -/** - * Invalidate the enabled plugins cache - * Call this when enabledPlugins setting changes - */ -export function invalidateEnabledPluginsCache(): void { - enabledPluginsCache = null -} - -/** - * Invalidate the approved MCP servers cache - * Call this when approvedPluginMcpServers setting changes - */ -export function invalidateApprovedMcpCache(): void { - approvedMcpCache = null -} - -/** - * Read Claude settings.json file - * Returns empty object if file doesn't exist - */ -async function readClaudeSettings(): Promise> { - try { - const content = await fs.readFile(CLAUDE_SETTINGS_PATH, "utf-8") - return JSON.parse(content) - } catch (error) { - // File doesn't exist or is invalid JSON - return {} - } -} - -/** - * Get list of enabled plugin identifiers from settings.json - * Plugins are DISABLED by default — only plugins explicitly in this list are active. - * Returns empty array if no plugins have been enabled. - * Results are cached for 5 seconds to reduce filesystem reads. - */ -export async function getEnabledPlugins(): Promise { - // Return cached result if still valid - if (enabledPluginsCache && Date.now() - enabledPluginsCache.timestamp < ENABLED_PLUGINS_CACHE_TTL_MS) { - return enabledPluginsCache.plugins - } - - const settings = await readClaudeSettings() - const plugins = Array.isArray(settings.enabledPlugins) ? settings.enabledPlugins as string[] : [] - - enabledPluginsCache = { plugins, timestamp: Date.now() } - return plugins -} - -/** - * Get list of approved plugin MCP server identifiers from settings.json - * Format: "{pluginSource}:{serverName}" e.g., "ccsetup:ccsetup:context7" - * Returns empty array if no approved servers - * Results are cached for 5 seconds to reduce filesystem reads - */ -export async function getApprovedPluginMcpServers(): Promise { - // Return cached result if still valid - if (approvedMcpCache && Date.now() - approvedMcpCache.timestamp < APPROVED_MCP_CACHE_TTL_MS) { - return approvedMcpCache.servers - } - - const settings = await readClaudeSettings() - const servers = Array.isArray(settings.approvedPluginMcpServers) - ? settings.approvedPluginMcpServers as string[] - : [] - - approvedMcpCache = { servers, timestamp: Date.now() } - return servers -} - -/** - * Check if a plugin MCP server is approved - */ -export async function isPluginMcpApproved(pluginSource: string, serverName: string): Promise { - const approved = await getApprovedPluginMcpServers() - const identifier = `${pluginSource}:${serverName}` - return approved.includes(identifier) -} - -/** - * Write Claude settings.json file - * Creates the .claude directory if it doesn't exist - */ -async function writeClaudeSettings(settings: Record): Promise { - const dir = path.dirname(CLAUDE_SETTINGS_PATH) - await fs.mkdir(dir, { recursive: true }) - await fs.writeFile(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2), "utf-8") -} - -export const claudeSettingsRouter = router({ - /** - * Get the includeCoAuthoredBy setting - * Returns true if setting is not explicitly set to false - */ - getIncludeCoAuthoredBy: publicProcedure.query(async () => { - const settings = await readClaudeSettings() - // Default is true (include co-authored-by) - // Only return false if explicitly set to false - return settings.includeCoAuthoredBy !== false - }), - - /** - * Set the includeCoAuthoredBy setting - */ - setIncludeCoAuthoredBy: publicProcedure - .input(z.object({ enabled: z.boolean() })) - .mutation(async ({ input }) => { - const settings = await readClaudeSettings() - - if (input.enabled) { - // Remove the setting to use default (true) - delete settings.includeCoAuthoredBy - } else { - // Explicitly set to false to disable - settings.includeCoAuthoredBy = false - } - - await writeClaudeSettings(settings) - return { success: true } - }), - - /** - * Get list of enabled plugins - * Plugins are disabled by default — only explicitly enabled ones are active. - */ - getEnabledPlugins: publicProcedure.query(async () => { - return await getEnabledPlugins() - }), - - /** - * Set a plugin's enabled state - * Plugins are disabled by default — adding to enabledPlugins activates them. - */ - setPluginEnabled: publicProcedure - .input( - z.object({ - pluginSource: z.string(), - enabled: z.boolean(), - }) - ) - .mutation(async ({ input }) => { - const settings = await readClaudeSettings() - const enabledPlugins = Array.isArray(settings.enabledPlugins) - ? (settings.enabledPlugins as string[]) - : [] - - if (input.enabled && !enabledPlugins.includes(input.pluginSource)) { - enabledPlugins.push(input.pluginSource) - } else if (!input.enabled) { - const index = enabledPlugins.indexOf(input.pluginSource) - if (index > -1) enabledPlugins.splice(index, 1) - } - - settings.enabledPlugins = enabledPlugins - await writeClaudeSettings(settings) - invalidateEnabledPluginsCache() - return { success: true } - }), - - /** - * Get list of approved plugin MCP servers - */ - getApprovedPluginMcpServers: publicProcedure.query(async () => { - return await getApprovedPluginMcpServers() - }), - - /** - * Approve a plugin MCP server - * Identifier format: "{pluginSource}:{serverName}" - */ - approvePluginMcpServer: publicProcedure - .input(z.object({ identifier: z.string() })) - .mutation(async ({ input }) => { - const settings = await readClaudeSettings() - const approved = Array.isArray(settings.approvedPluginMcpServers) - ? (settings.approvedPluginMcpServers as string[]) - : [] - - if (!approved.includes(input.identifier)) { - approved.push(input.identifier) - } - - settings.approvedPluginMcpServers = approved - await writeClaudeSettings(settings) - invalidateApprovedMcpCache() - return { success: true } - }), - - /** - * Revoke approval for a plugin MCP server - * Identifier format: "{pluginSource}:{serverName}" - */ - revokePluginMcpServer: publicProcedure - .input(z.object({ identifier: z.string() })) - .mutation(async ({ input }) => { - const settings = await readClaudeSettings() - const approved = Array.isArray(settings.approvedPluginMcpServers) - ? (settings.approvedPluginMcpServers as string[]) - : [] - - const index = approved.indexOf(input.identifier) - if (index > -1) { - approved.splice(index, 1) - } - - settings.approvedPluginMcpServers = approved - await writeClaudeSettings(settings) - invalidateApprovedMcpCache() - return { success: true } - }), - - /** - * Approve all MCP servers from a plugin - * Takes the pluginSource (e.g., "ccsetup:ccsetup") and list of server names - */ - approveAllPluginMcpServers: publicProcedure - .input(z.object({ - pluginSource: z.string(), - serverNames: z.array(z.string()), - })) - .mutation(async ({ input }) => { - const settings = await readClaudeSettings() - const approved = Array.isArray(settings.approvedPluginMcpServers) - ? (settings.approvedPluginMcpServers as string[]) - : [] - - for (const serverName of input.serverNames) { - const identifier = `${input.pluginSource}:${serverName}` - if (!approved.includes(identifier)) { - approved.push(identifier) - } - } - - settings.approvedPluginMcpServers = approved - await writeClaudeSettings(settings) - invalidateApprovedMcpCache() - return { success: true } - }), - - /** - * Revoke all MCP servers from a plugin - * Removes all identifiers matching "{pluginSource}:*" - */ - revokeAllPluginMcpServers: publicProcedure - .input(z.object({ - pluginSource: z.string(), - })) - .mutation(async ({ input }) => { - const settings = await readClaudeSettings() - const approved = Array.isArray(settings.approvedPluginMcpServers) - ? (settings.approvedPluginMcpServers as string[]) - : [] - - const prefix = `${input.pluginSource}:` - settings.approvedPluginMcpServers = approved.filter((id) => !id.startsWith(prefix)) - await writeClaudeSettings(settings) - invalidateApprovedMcpCache() - return { success: true } - }), -}) diff --git a/src/main/lib/trpc/routers/claude.ts b/src/main/lib/trpc/routers/claude.ts index 7be49b8b..9cd68595 100644 --- a/src/main/lib/trpc/routers/claude.ts +++ b/src/main/lib/trpc/routers/claude.ts @@ -2,38 +2,28 @@ import { observable } from "@trpc/server/observable" import { eq } from "drizzle-orm" import { app, BrowserWindow, safeStorage } from "electron" import * as fs from "fs/promises" +import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, renameSync, statSync } from "fs" import * as os from "os" -import path from "path" +import path, { dirname, join } from "path" import { z } from "zod" -import { setConnectionMethod } from "../../analytics" import { buildClaudeEnv, - checkOfflineFallback, createTransformer, getBundledClaudeBinaryPath, logClaudeEnv, logRawClaudeMessage, + checkOfflineFallback, type UIMessageChunk, } from "../../claude" -import { getProjectMcpServers, GLOBAL_MCP_PATH, readClaudeConfig, removeMcpServerConfig, resolveProjectPathFromWorktree, updateClaudeConfigAtomic, updateMcpServerConfig, writeClaudeConfig, type McpServerConfig } from "../../claude-config" -import { discoverPluginMcpServers } from "../../plugins" -import { getEnabledPlugins, getApprovedPluginMcpServers } from "./claude-settings" import { chats, claudeCodeCredentials, getDatabase, subChats } from "../../db" import { createRollbackStash } from "../../git/stash" -import { ensureMcpTokensFresh, fetchMcpTools, fetchMcpToolsStdio, getMcpAuthStatus, startMcpOAuth, type McpToolInfo } from "../../mcp-auth" -import { fetchOAuthMetadata, getMcpBaseUrl } from "../../oauth" +import { checkInternetConnection, checkOllamaStatus, getOllamaConfig } from "../../ollama" import { publicProcedure, router } from "../index" import { buildAgentsOption } from "./agent-utils" /** - * Parse @[agent:name], @[skill:name], and @[tool:servername] mentions from prompt text - * Returns the cleaned prompt and lists of mentioned agents/skills/MCP servers - * - * File mention formats: - * - @[file:local:relative/path] - file inside project (relative path) - * - @[file:external:/absolute/path] - file outside project (absolute path) - * - @[file:owner/repo:path] - legacy web format (repo:path) - * - @[folder:local:path] or @[folder:external:path] - folder mentions + * Parse @[agent:name], @[skill:name], and @[tool:name] mentions from prompt text + * Returns the cleaned prompt and lists of mentioned agents/skills/tools */ function parseMentions(prompt: string): { cleanedPrompt: string @@ -69,8 +59,9 @@ function parseMentions(prompt: string): { folderMentions.push(name) break case "tool": - // Validate: server name (alphanumeric, underscore, hyphen) or full tool id (mcp__server__tool) - if (/^[a-zA-Z0-9_-]+$/.test(name) || /^mcp__[a-zA-Z0-9_-]+__[a-zA-Z0-9_-]+$/.test(name)) { + // Validate tool name format: only alphanumeric, underscore, hyphen allowed + // This prevents prompt injection via malicious tool names + if (/^[a-zA-Z0-9_-]+$/.test(name)) { toolMentions.push(name) } break @@ -85,27 +76,11 @@ function parseMentions(prompt: string): { .replace(/@\[tool:[^\]]+\]/g, "") .trim() - // Transform file mentions to readable paths for the agent - // @[file:local:path] -> path (relative to project) - // @[file:external:/abs/path] -> /abs/path (absolute) - cleanedPrompt = cleanedPrompt - .replace(/@\[file:local:([^\]]+)\]/g, "$1") - .replace(/@\[file:external:([^\]]+)\]/g, "$1") - .replace(/@\[folder:local:([^\]]+)\]/g, "$1") - .replace(/@\[folder:external:([^\]]+)\]/g, "$1") - - // Add usage hints for mentioned MCP servers or individual tools - // Names are already validated to contain only safe characters + // Add tool usage hints if tools were mentioned + // Tool names are already validated to contain only safe characters if (toolMentions.length > 0) { const toolHints = toolMentions - .map((t) => { - if (t.startsWith("mcp__")) { - // Individual tool mention (from MCP widget): "Use the mcp__server__tool tool" - return `Use the ${t} tool for this request.` - } - // Server mention (from @ dropdown): "Use tools from the X MCP server" - return `Use tools from the ${t} MCP server for this request.` - }) + .map((t) => `Use the ${t} tool for this request.`) .join(" ") cleanedPrompt = `${toolHints}\n\n${cleanedPrompt}` } @@ -164,17 +139,6 @@ const getClaudeQuery = async () => { // Active sessions for cancellation const activeSessions = new Map() -// In-memory cache of working MCP server names (resets on app restart) -// Key: "scope::serverName" where scope is "__global__" or projectPath -// Value: true if working (has tools), false if failed -export const workingMcpServers = new Map() - -// Helper to build scoped cache key -const GLOBAL_SCOPE = "__global__" -function mcpCacheKey(scope: string | null, serverName: string): string { - return `${scope ?? GLOBAL_SCOPE}::${serverName}` -} - // Cache for symlinks (track which subChatIds have already set up symlinks) const symlinksCreated = new Set() @@ -184,6 +148,28 @@ const mcpConfigCache = new Map() +// Cache for MCP server statuses (to filter out failed/needs-auth servers) +// Maps project path -> server name -> status +const mcpServerStatusCache = new Map>() + +// Disk cache types and configuration for MCP server statuses +interface CachedMcpStatus { + status: string + cachedAt: number +} + +interface McpCacheData { + version: number + entries: Record + updatedAt: number + }> +} + +const MCP_STATUS_TTL = 5 * 60 * 1000 // 5 minutes +const MCP_CACHE_PATH = join(app.getPath("userData"), "cache", "mcp-status.json") +let diskCacheLastLoadTime = 0 // Track when disk cache was last loaded + const pendingToolApprovals = new Map< string, { @@ -219,319 +205,237 @@ const imageAttachmentSchema = z.object({ export type ImageAttachment = z.infer /** - * Clear all performance caches (for testing/debugging) + * Load MCP status cache from disk + * Reloads if cache was updated on disk since last load (for concurrent requests) */ -export function clearClaudeCaches() { - cachedClaudeQuery = null - symlinksCreated.clear() - mcpConfigCache.clear() - console.log("[claude] All caches cleared") -} - -/** - * Determine server status based on config - * - If authType is "none" -> "connected" (no auth required) - * - If has Authorization header -> "connected" (OAuth completed, SDK can use it) - * - If has _oauth but no headers -> "needs-auth" (legacy config, needs re-auth to migrate) - * - If HTTP server (has URL) with explicit authType -> "needs-auth" - * - HTTP server without authType -> "connected" (assume public) - * - Local stdio server -> "connected" - */ -function getServerStatusFromConfig(serverConfig: McpServerConfig): string { - const headers = serverConfig.headers as Record | undefined - const { _oauth: oauth, authType } = serverConfig +function loadMcpStatusFromDisk(): void { + try { + if (!existsSync(MCP_CACHE_PATH)) { + diskCacheLastLoadTime = Date.now() + return + } - // If authType is explicitly "none", no auth required - if (authType === "none") { - return "connected" - } + // Check if file was modified since last load (handles concurrent requests) + const stats = statSync(MCP_CACHE_PATH) + const fileModTime = stats.mtimeMs - // If has Authorization header, it's ready for SDK to use - if (headers?.Authorization) { - return "connected" - } + if (diskCacheLastLoadTime > 0 && fileModTime <= diskCacheLastLoadTime) { + // File hasn't changed since last load, skip + return + } - // If has _oauth but no headers, this is a legacy config that needs re-auth - // (old format that SDK can't use) - if (oauth?.accessToken && !headers?.Authorization) { - return "needs-auth" - } + const data: McpCacheData = JSON.parse(readFileSync(MCP_CACHE_PATH, "utf-8")) - // If HTTP server with explicit authType (oauth/bearer), needs auth - if (serverConfig.url && (["oauth", "bearer"].includes(authType ?? ""))) { - return "needs-auth" - } + if (data.version !== 1) { + console.warn(`[MCP Cache] Unknown version ${data.version}, ignoring`) + diskCacheLastLoadTime = Date.now() + return + } - // HTTP server without authType - assume no auth required (public endpoint) - // Local stdio server - also connected - return "connected" -} + const now = Date.now() + let loadedCount = 0 + let expiredCount = 0 -const MCP_FETCH_TIMEOUT_MS = 10_000 + for (const [projectPath, entry] of Object.entries(data.entries)) { + const serverMap = new Map() -/** - * Fetch tools from an MCP server (HTTP or stdio transport) - * Times out after 10 seconds to prevent slow MCPs from blocking the cache update - */ -async function fetchToolsForServer(serverConfig: McpServerConfig): Promise { - const timeoutPromise = new Promise((_, reject) => - setTimeout(() => reject(new Error('Timeout')), MCP_FETCH_TIMEOUT_MS) - ) - - const fetchPromise = (async () => { - // HTTP transport - if (serverConfig.url) { - const headers = serverConfig.headers as Record | undefined - try { - return await fetchMcpTools(serverConfig.url, headers) - } catch { - return [] + for (const [serverName, cached] of Object.entries(entry.servers)) { + if (now - cached.cachedAt < MCP_STATUS_TTL) { + serverMap.set(serverName, cached.status) + loadedCount++ + } else { + expiredCount++ + } } - } - // Stdio transport - const command = (serverConfig as any).command as string | undefined - if (command) { - try { - return await fetchMcpToolsStdio({ - command, - args: (serverConfig as any).args, - env: (serverConfig as any).env, - }) - } catch { - return [] + if (serverMap.size > 0) { + mcpServerStatusCache.set(projectPath, serverMap) } } - return [] - })() - - try { - return await Promise.race([fetchPromise, timeoutPromise]) - } catch { - return [] + diskCacheLastLoadTime = Date.now() + if (loadedCount > 0) { + console.log(`[MCP Cache] Loaded ${loadedCount} cached server statuses`) + } + } catch (error) { + console.warn("[MCP Cache] Failed to load from disk:", error) + diskCacheLastLoadTime = Date.now() + try { + if (existsSync(MCP_CACHE_PATH)) { + unlinkSync(MCP_CACHE_PATH) + } + } catch {} } } /** - * Handler for getAllMcpConfig - exported so it can be called on app startup + * Save MCP status cache to disk (write-through) */ -export async function getAllMcpConfigHandler() { +function saveMcpStatusToDisk(): void { try { - const totalStart = Date.now() - - // Clear cache before repopulating - workingMcpServers.clear() - - const config = await readClaudeConfig() - - const convertServers = async (servers: Record | undefined, scope: string | null) => { - if (!servers) return [] + const cacheDir = dirname(MCP_CACHE_PATH) + if (!existsSync(cacheDir)) { + mkdirSync(cacheDir, { recursive: true }) + } - const results = await Promise.all( - Object.entries(servers).map(async ([name, serverConfig]) => { - const configObj = serverConfig as Record - let status = getServerStatusFromConfig(serverConfig) - const headers = serverConfig.headers as Record | undefined + const data: McpCacheData = { + version: 1, + entries: Object.fromEntries( + Array.from(mcpServerStatusCache.entries()).map(([projectPath, serverMap]) => [ + projectPath, + { + servers: Object.fromEntries( + Array.from(serverMap.entries()).map(([name, status]) => [ + name, + { status, cachedAt: Date.now() } + ]) + ), + updatedAt: Date.now() + } + ]) + ) + } - let tools: McpToolInfo[] = [] - let needsAuth = false + const tempPath = MCP_CACHE_PATH + ".tmp" + writeFileSync(tempPath, JSON.stringify(data, null, 2), "utf-8") + renameSync(tempPath, MCP_CACHE_PATH) - try { - tools = await fetchToolsForServer(serverConfig) - } catch (error) { - console.error(`[MCP] Failed to fetch tools for ${name}:`, error) - } + const totalServers = Array.from(mcpServerStatusCache.values()) + .reduce((sum, map) => sum + map.size, 0) + console.log(`[MCP Cache] Saved ${totalServers} statuses to disk`) + } catch (error) { + console.error("[MCP Cache] Failed to save to disk:", error) + } +} - const cacheKey = mcpCacheKey(scope, name) - if (tools.length > 0) { - status = "connected" - workingMcpServers.set(cacheKey, true) - } else { - workingMcpServers.set(cacheKey, false) - if (serverConfig.url) { - try { - const baseUrl = getMcpBaseUrl(serverConfig.url) - const metadata = await fetchOAuthMetadata(baseUrl) - needsAuth = !!metadata && !!metadata.authorization_endpoint - } catch { - // If probe fails, assume no auth needed - } - } else if (serverConfig.authType === "oauth" || serverConfig.authType === "bearer") { - needsAuth = true - } +/** + * Clear all performance caches (for testing/debugging) + */ +export function clearClaudeCaches() { + cachedClaudeQuery = null + symlinksCreated.clear() + mcpConfigCache.clear() + mcpServerStatusCache.clear() + diskCacheLastLoadTime = 0 - if (needsAuth && !headers?.Authorization) { - status = "needs-auth" - } else { - // No tools and doesn't need auth - server failed to connect or has no tools - status = "failed" - } - } + // Clear disk cache + try { + if (existsSync(MCP_CACHE_PATH)) { + unlinkSync(MCP_CACHE_PATH) + console.log("[MCP Cache] Cleared disk cache") + } + } catch (error) { + console.error("[MCP Cache] Failed to clear disk cache:", error) + } - return { name, status, tools, needsAuth, config: configObj } - }) - ) + console.log("[claude] All caches cleared") +} - return results +/** + * Warm up MCP server cache by initializing servers for all configured projects + * This runs once at app startup to populate the cache, so all future sessions + * can use filtered MCP servers without delays + */ +export async function warmupMcpCache(): Promise { + try { + const warmupStart = Date.now() + + // Read ~/.claude.json to get all projects with MCP servers + const claudeJsonPath = join(os.homedir(), ".claude.json") + let config: any + try { + const configContent = readFileSync(claudeJsonPath, "utf-8") + config = JSON.parse(configContent) + } catch (err) { + console.log("[MCP Warmup] No ~/.claude.json found or failed to read - skipping warmup") + return } - // Build list of all groups to process with timing - const groupTasks: Array<{ - groupName: string - projectPath: string | null - promise: Promise<{ - mcpServers: Array<{ name: string; status: string; tools: McpToolInfo[]; needsAuth: boolean; config: Record }> - duration: number - }> - }> = [] - - // Global MCPs - if (config.mcpServers) { - groupTasks.push({ - groupName: "Global", - projectPath: null, - promise: (async () => { - const start = Date.now() - const freshServers = await ensureMcpTokensFresh(config.mcpServers!, GLOBAL_MCP_PATH) - const mcpServers = await convertServers(freshServers, null) // null = global scope - return { mcpServers, duration: Date.now() - start } - })() - }) - } else { - groupTasks.push({ - groupName: "Global", - projectPath: null, - promise: Promise.resolve({ mcpServers: [], duration: 0 }) - }) + if (!config.projects || Object.keys(config.projects).length === 0) { + console.log("[MCP Warmup] No projects configured - skipping warmup") + return } - // Project MCPs - if (config.projects) { - for (const [projectPath, projectConfig] of Object.entries(config.projects)) { - if (projectConfig.mcpServers && Object.keys(projectConfig.mcpServers).length > 0) { - const groupName = path.basename(projectPath) || projectPath - groupTasks.push({ - groupName, - projectPath, - promise: (async () => { - const start = Date.now() - const freshServers = await ensureMcpTokensFresh(projectConfig.mcpServers!, projectPath) - const mcpServers = await convertServers(freshServers, projectPath) // projectPath = scope - return { mcpServers, duration: Date.now() - start } - })() - }) + // Find projects with MCP servers (excluding worktrees) + const projectsWithMcp: Array<{ path: string; servers: Record }> = [] + for (const [projectPath, projectConfig] of Object.entries(config.projects)) { + if ((projectConfig as any)?.mcpServers) { + // Skip worktrees - they're temporary git working directories and inherit MCP from parent + if (projectPath.includes("/.21st/worktrees/") || projectPath.includes("\\.21st\\worktrees\\")) { + continue } - } - } - // Process all groups in parallel - const results = await Promise.all(groupTasks.map(t => t.promise)) - - // Build groups with timing info - const groupsWithTiming = groupTasks.map((task, i) => ({ - groupName: task.groupName, - projectPath: task.projectPath, - mcpServers: results[i].mcpServers, - duration: results[i].duration - })) - - // Log performance (sorted by duration DESC) - const totalDuration = Date.now() - totalStart - const workingCount = [...workingMcpServers.values()].filter(v => v).length - const sortedByDuration = [...groupsWithTiming].sort((a, b) => b.duration - a.duration) - - console.log(`[MCP] Cache updated in ${totalDuration}ms. Working: ${workingCount}/${workingMcpServers.size}`) - for (const g of sortedByDuration) { - if (g.mcpServers.length > 0) { - console.log(`[MCP] ${g.groupName}: ${g.duration}ms (${g.mcpServers.length} servers)`) + projectsWithMcp.push({ + path: projectPath, + servers: (projectConfig as any).mcpServers + }) } } - // Return groups without timing info - const groups = groupsWithTiming.map(({ groupName, projectPath, mcpServers }) => ({ - groupName, - projectPath, - mcpServers - })) - - // Plugin MCPs (from installed plugins) - const [enabledPluginSources, pluginMcpConfigs, approvedServers] = await Promise.all([ - getEnabledPlugins(), - discoverPluginMcpServers(), - getApprovedPluginMcpServers(), - ]) - - for (const pluginConfig of pluginMcpConfigs) { - // Only show MCP servers from enabled plugins - if (!enabledPluginSources.includes(pluginConfig.pluginSource)) continue - - const globalServerNames = config.mcpServers ? Object.keys(config.mcpServers) : [] - if (Object.keys(pluginConfig.mcpServers).length > 0) { - const pluginMcpServers = (await Promise.all( - Object.entries(pluginConfig.mcpServers).map(async ([name, serverConfig]) => { - // Skip servers that have been promoted to ~/.claude.json (e.g., after OAuth) - if (globalServerNames.includes(name)) return null - - const configObj = serverConfig as Record - const identifier = `${pluginConfig.pluginSource}:${name}` - const isApproved = approvedServers.includes(identifier) - - if (!isApproved) { - return { name, status: "pending-approval", tools: [] as McpToolInfo[], needsAuth: false, config: configObj, isApproved } - } + if (projectsWithMcp.length === 0) { + console.log("[MCP Warmup] No MCP servers configured (excluding worktrees) - skipping warmup") + return + } - // Try to get status and tools for approved servers - let status = getServerStatusFromConfig(serverConfig) - const headers = serverConfig.headers as Record | undefined - let tools: McpToolInfo[] = [] - let needsAuth = false + // Get SDK + const sdk = await import("@anthropic-ai/claude-agent-sdk") + const claudeQuery = sdk.query - try { - tools = await fetchToolsForServer(serverConfig) - } catch (error) { - console.error(`[MCP] Failed to fetch tools for plugin ${name}:`, error) - } + // Warm up each project + for (const project of projectsWithMcp) { - if (tools.length > 0) { - status = "connected" - } else { - // Same OAuth detection logic as regular MCP servers - if (serverConfig.url) { - try { - const baseUrl = getMcpBaseUrl(serverConfig.url) - const metadata = await fetchOAuthMetadata(baseUrl) - needsAuth = !!metadata && !!metadata.authorization_endpoint - } catch { - // If probe fails, assume no auth needed - } - } else if (serverConfig.authType === "oauth" || serverConfig.authType === "bearer") { - needsAuth = true - } + try { + // Create a minimal query to initialize MCP servers + const warmupQuery = claudeQuery({ + prompt: "ping", + options: { + cwd: project.path, + mcpServers: project.servers, + systemPrompt: { + type: "preset" as const, + preset: "claude_code" as const, + }, + env: buildClaudeEnv(), + permissionMode: "bypassPermissions" as const, + allowDangerouslySkipPermissions: true, + } + }) - if (needsAuth && !headers?.Authorization) { - status = "needs-auth" - } else { - status = "failed" + // Wait for init message with MCP server statuses + let gotInit = false + for await (const msg of warmupQuery) { + const msgAny = msg as any + if (msgAny.type === "system" && msgAny.subtype === "init" && msgAny.mcp_servers) { + // Cache the statuses + const statusMap = new Map() + for (const server of msgAny.mcp_servers) { + if (server.name && server.status) { + statusMap.set(server.name, server.status) } } + mcpServerStatusCache.set(project.path, statusMap) + gotInit = true + break // We only need the init message + } + } - return { name, status, tools, needsAuth, config: configObj, isApproved } - }) - )).filter((s): s is NonNullable => s !== null) - - groups.push({ - groupName: `Plugin: ${pluginConfig.pluginSource}`, - projectPath: null, - mcpServers: pluginMcpServers, - }) + if (!gotInit) { + console.warn(`[MCP Warmup] Did not receive init message for ${project.path}`) + } + } catch (err) { + console.error(`[MCP Warmup] Failed to warm up MCP for ${project.path}:`, err) } } - return { groups } + // Save all cached statuses to disk + saveMcpStatusToDisk() + + const totalServers = Array.from(mcpServerStatusCache.values()) + .reduce((sum, map) => sum + map.size, 0) + const warmupDuration = Date.now() - warmupStart + console.log(`[MCP Warmup] Initialized ${totalServers} servers across ${projectsWithMcp.length} projects in ${warmupDuration}ms`) } catch (error) { - console.error("[getAllMcpConfig] Error:", error) - return { groups: [], error: String(error) } + console.error("[MCP Warmup] Warmup failed:", error) } } @@ -560,8 +464,6 @@ export const claudeRouter = router({ maxThinkingTokens: z.number().optional(), // Enable extended thinking images: z.array(imageAttachmentSchema).optional(), // Image attachments historyEnabled: z.boolean().optional(), - offlineModeEnabled: z.boolean().optional(), // Whether offline mode (Ollama) is enabled in settings - enableTasks: z.boolean().optional(), // Enable task management tools (TodoWrite, Task agents) }), ) .subscription(({ input }) => { @@ -648,13 +550,11 @@ export const claudeRouter = router({ const existingMessages = JSON.parse(existing?.messages || "[]") const existingSessionId = existing?.sessionId || null - // Get resumeSessionAt UUID only if shouldResume flag was set (by rollbackToMessage) + // Get resumeSessionAt UUID from the last assistant message (for rollback) const lastAssistantMsg = [...existingMessages].reverse().find( (m: any) => m.role === "assistant" ) - const resumeAtUuid = lastAssistantMsg?.metadata?.shouldResume - ? (lastAssistantMsg?.metadata?.sdkMessageUuid || null) - : null + const resumeAtUuid = lastAssistantMsg?.metadata?.sdkMessageUuid || null const historyEnabled = input.historyEnabled === true // Check if last message is already this user message (avoid duplicate) @@ -689,14 +589,8 @@ export const claudeRouter = router({ } // 2.5. AUTO-FALLBACK: Check internet and switch to Ollama if offline - // Only check if offline mode is enabled in settings const claudeCodeToken = getClaudeCodeToken() - const offlineResult = await checkOfflineFallback( - input.customConfig, - claudeCodeToken, - undefined, // selectedOllamaModel - will be read from customConfig if present - input.offlineModeEnabled ?? false, // Pass offline mode setting - ) + const offlineResult = await checkOfflineFallback(input.customConfig, claudeCodeToken) if (offlineResult.error) { emitError(new Error(offlineResult.error), 'Offline mode unavailable') @@ -709,18 +603,6 @@ export const claudeRouter = router({ const finalCustomConfig = offlineResult.config || input.customConfig const isUsingOllama = offlineResult.isUsingOllama - // Track connection method for analytics - let connectionMethod = "claude-subscription" // default (Claude Code OAuth) - if (isUsingOllama) { - connectionMethod = "offline-ollama" - } else if (finalCustomConfig) { - // Has custom config = either API key or custom model - const isDefaultAnthropicUrl = !finalCustomConfig.baseUrl || - finalCustomConfig.baseUrl.includes("anthropic.com") - connectionMethod = isDefaultAnthropicUrl ? "api-key" : "custom-model" - } - setConnectionMethod(connectionMethod) - // Offline status is shown in sidebar, no need to emit message here // (emitting text-delta without text-start breaks UI text rendering) @@ -823,15 +705,16 @@ export const claudeRouter = router({ } // Build full environment for Claude SDK (includes HOME, PATH, etc.) - const claudeEnv = buildClaudeEnv({ - ...(finalCustomConfig && { - customEnv: { - ANTHROPIC_AUTH_TOKEN: finalCustomConfig.token, - ANTHROPIC_BASE_URL: finalCustomConfig.baseUrl, - }, - }), - enableTasks: input.enableTasks ?? true, - }) + const claudeEnv = buildClaudeEnv( + finalCustomConfig + ? { + customEnv: { + ANTHROPIC_AUTH_TOKEN: finalCustomConfig.token, + ANTHROPIC_BASE_URL: finalCustomConfig.baseUrl, + }, + } + : undefined, + ) // Debug logging in dev if (process.env.NODE_ENV !== "production") { @@ -903,66 +786,27 @@ export const claudeRouter = router({ const cached = mcpConfigCache.get(claudeJsonSource) const lookupPath = input.projectPath || input.cwd - // Get or refresh cached config - let claudeConfig: any + // Check if we have a valid cache entry if (cached && cached.mtime === currentMtime) { - claudeConfig = cached.config + mcpServersForSdk = cached.config?.[lookupPath] } else { - claudeConfig = JSON.parse(await fs.readFile(claudeJsonSource, "utf-8")) - mcpConfigCache.set(claudeJsonSource, { config: claudeConfig, mtime: currentMtime }) - } - - // Merge global + project servers (project overrides global) - // getProjectMcpServers resolves worktree paths internally - const globalServers = claudeConfig.mcpServers || {} - const projectServers = getProjectMcpServers(claudeConfig, lookupPath) || {} - - // Load plugin MCP servers (filtered by enabled plugins and approval) - const [enabledPluginSources, pluginMcpConfigs, approvedServers] = await Promise.all([ - getEnabledPlugins(), - discoverPluginMcpServers(), - getApprovedPluginMcpServers(), - ]) - - const pluginServers: Record = {} - for (const config of pluginMcpConfigs) { - if (enabledPluginSources.includes(config.pluginSource)) { - for (const [name, serverConfig] of Object.entries(config.mcpServers)) { - if (!globalServers[name] && !projectServers[name]) { - const identifier = `${config.pluginSource}:${name}` - if (approvedServers.includes(identifier)) { - pluginServers[name] = serverConfig - } - } + // Read and parse config + const originalConfig = JSON.parse(await fs.readFile(claudeJsonSource, "utf-8")) + + // Cache the projects config by lookup path + const projectsConfig: Record = {} + for (const [projPath, projConfig] of Object.entries(originalConfig.projects || {})) { + if ((projConfig as any)?.mcpServers) { + projectsConfig[projPath] = (projConfig as any).mcpServers } } - } - // Priority: project > global > plugin - const allServers = { ...pluginServers, ...globalServers, ...projectServers } - - // Filter to only working MCPs using scoped cache keys - if (workingMcpServers.size > 0) { - const filtered: Record = {} - // Resolve worktree path to original project path to match cache keys - const resolvedProjectPath = resolveProjectPathFromWorktree(lookupPath) || lookupPath - for (const [name, config] of Object.entries(allServers)) { - // Use resolved project scope if server is from project, otherwise global - const scope = name in projectServers ? resolvedProjectPath : null - const cacheKey = mcpCacheKey(scope, name) - // Include server if it's marked working, or if it's not in cache at all - // (plugin servers won't be in the cache yet) - if (workingMcpServers.get(cacheKey) === true || !workingMcpServers.has(cacheKey)) { - filtered[name] = config - } - } - mcpServersForSdk = filtered - const skipped = Object.keys(allServers).length - Object.keys(filtered).length - if (skipped > 0) { - console.log(`[claude] Filtered out ${skipped} non-working MCP(s)`) - } - } else { - mcpServersForSdk = allServers + mcpConfigCache.set(claudeJsonSource, { + config: projectsConfig, + mtime: currentMtime, + }) + + mcpServersForSdk = projectsConfig[lookupPath] } } } catch (configErr) { @@ -972,20 +816,10 @@ export const claudeRouter = router({ console.error(`[claude] Failed to setup isolated config dir:`, mkdirErr) } - // Check if user has existing API key or proxy configured in their shell environment - // If so, use that instead of OAuth (allows using custom API proxies) - // Based on PR #29 by @sa4hnd - const hasExistingApiConfig = !!(claudeEnv.ANTHROPIC_API_KEY || claudeEnv.ANTHROPIC_BASE_URL) - - if (hasExistingApiConfig) { - console.log(`[claude] Using existing CLI config - API_KEY: ${claudeEnv.ANTHROPIC_API_KEY ? "set" : "not set"}, BASE_URL: ${claudeEnv.ANTHROPIC_BASE_URL || "default"}`) - } - - // Build final env - only add OAuth token if we have one AND no existing API config - // Existing CLI config takes precedence over OAuth + // Build final env - only add OAuth token if we have one const finalEnv = { ...claudeEnv, - ...(claudeCodeToken && !hasExistingApiConfig && { + ...(claudeCodeToken && { CLAUDE_CODE_OAUTH_TOKEN: claudeCodeToken, }), // Re-enable CLAUDE_CONFIG_DIR now that we properly map MCP configs @@ -997,20 +831,9 @@ export const claudeRouter = router({ const resumeSessionId = input.sessionId || existingSessionId || undefined - // DEBUG: Session resume path tracing - const expectedSanitizedCwd = input.cwd.replace(/[/.]/g, "-") - const expectedSessionPath = path.join(isolatedConfigDir, "projects", expectedSanitizedCwd, `${resumeSessionId}.jsonl`) - console.log(`[claude] ========== SESSION DEBUG ==========`) - console.log(`[claude] subChatId: ${input.subChatId}`) - console.log(`[claude] cwd: ${input.cwd}`) - console.log(`[claude] sanitized cwd (expected): ${expectedSanitizedCwd}`) - console.log(`[claude] CLAUDE_CONFIG_DIR: ${isolatedConfigDir}`) - console.log(`[claude] Expected session path: ${expectedSessionPath}`) - console.log(`[claude] Session ID to resume: ${resumeSessionId}`) - console.log(`[claude] Existing sessionId from DB: ${existingSessionId}`) + console.log(`[claude] Session ID to resume: ${resumeSessionId} (Existing: ${existingSessionId})`) console.log(`[claude] Resume at UUID: ${resumeAtUuid}`) - console.log(`[claude] ========== END SESSION DEBUG ==========`) - + console.log(`[SD] Query options - cwd: ${input.cwd}, projectPath: ${input.projectPath || "(not set)"}, mcpServers: ${mcpServersForSdk ? Object.keys(mcpServersForSdk).join(", ") : "(none)"}`) if (finalCustomConfig) { const redactedConfig = { @@ -1053,21 +876,41 @@ export const claudeRouter = router({ } } - // Skip MCP servers entirely in offline mode (Ollama) - they slow down initialization by 60+ seconds - // Otherwise pass all MCP servers - the SDK will handle connection + // Filter MCP servers: skip ONLY non-working servers (failed, needs-auth) + // Pass working/unknown servers in options so Claude can see them + // OPTIMIZATION: Cache is populated at app startup via warmupMcpCache() let mcpServersFiltered: Record | undefined - if (isUsingOllama) { - console.log('[Ollama] Skipping MCP servers to speed up initialization') - mcpServersFiltered = undefined - } else { - // Ensure MCP tokens are fresh (refresh if within 5 min of expiry) - if (mcpServersForSdk && Object.keys(mcpServersForSdk).length > 0) { - const lookupPath = input.projectPath || input.cwd - mcpServersFiltered = await ensureMcpTokensFresh(mcpServersForSdk, lookupPath) + // Skip MCP servers entirely in offline mode (Ollama) - they slow down initialization by 60+ seconds + if (mcpServersForSdk && !isUsingOllama) { + const lookupPath = input.projectPath || input.cwd + + // Load cached statuses from disk if needed + if (!mcpServerStatusCache.has(lookupPath)) { + loadMcpStatusFromDisk() + } + + const cachedStatuses = mcpServerStatusCache.get(lookupPath) + const hasCachedInfo = cachedStatuses && cachedStatuses.size > 0 + + if (hasCachedInfo) { + // We have cached statuses - filter OUT only failed/needs-auth servers + mcpServersFiltered = Object.fromEntries( + Object.entries(mcpServersForSdk).filter(([name]) => { + const status = cachedStatuses.get(name) + // Unknown servers (undefined status) are included to allow discovery + if (status === undefined) return true + return status !== "failed" && status !== "needs-auth" + }) + ) } else { - mcpServersFiltered = mcpServersForSdk + // No cache yet (warmup hasn't completed or ~/.claude.json changed) + // Skip MCP servers to avoid delays - they'll be available after warmup completes + mcpServersFiltered = undefined } + } else if (isUsingOllama) { + console.log('[Ollama] Skipping MCP servers to speed up initialization') + mcpServersFiltered = undefined } // Log SDK configuration for debugging @@ -1089,151 +932,17 @@ export const claudeRouter = router({ }) } - // Read AGENTS.md from project root if it exists - let agentsMdContent: string | undefined - try { - const agentsMdPath = path.join(input.cwd, "AGENTS.md") - agentsMdContent = await fs.readFile(agentsMdPath, "utf-8") - if (agentsMdContent.trim()) { - console.log(`[claude] Found AGENTS.md at ${agentsMdPath} (${agentsMdContent.length} chars)`) - } else { - agentsMdContent = undefined - } - } catch { - // AGENTS.md doesn't exist or can't be read - that's fine - } - - // For Ollama: embed context AND history directly in prompt - // Ollama doesn't have server-side sessions, so we must include full history - let finalQueryPrompt: string | AsyncIterable = prompt - if (isUsingOllama && typeof prompt === 'string') { - // Format conversation history from existingMessages (excluding current message) - // IMPORTANT: Include tool calls info so model knows what files were read/edited - let historyText = '' - if (existingMessages.length > 0) { - const historyParts: string[] = [] - for (const msg of existingMessages) { - if (msg.role === 'user') { - // Extract text from user message parts - const textParts = msg.parts?.filter((p: any) => p.type === 'text').map((p: any) => p.text) || [] - if (textParts.length > 0) { - historyParts.push(`User: ${textParts.join('\n')}`) - } - } else if (msg.role === 'assistant') { - // Extract text AND tool calls from assistant message parts - const parts = msg.parts || [] - const textParts: string[] = [] - const toolSummaries: string[] = [] - - for (const p of parts) { - if (p.type === 'text' && p.text) { - textParts.push(p.text) - } else if (p.type === 'tool_use' || p.type === 'tool-use') { - // Include brief tool call info - this is critical for context! - const toolName = p.name || p.tool || 'unknown' - const toolInput = p.input || {} - // Extract key info based on tool type - let toolInfo = `[Used ${toolName}` - if (toolName === 'Read' && (toolInput.file_path || toolInput.file)) { - toolInfo += `: ${toolInput.file_path || toolInput.file}` - } else if (toolName === 'Edit' && toolInput.file_path) { - toolInfo += `: ${toolInput.file_path}` - } else if (toolName === 'Write' && toolInput.file_path) { - toolInfo += `: ${toolInput.file_path}` - } else if (toolName === 'Glob' && toolInput.pattern) { - toolInfo += `: ${toolInput.pattern}` - } else if (toolName === 'Grep' && toolInput.pattern) { - toolInfo += `: "${toolInput.pattern}"` - } else if (toolName === 'Bash' && toolInput.command) { - const cmd = String(toolInput.command).slice(0, 50) - toolInfo += `: ${cmd}${toolInput.command.length > 50 ? '...' : ''}` - } - toolInfo += ']' - toolSummaries.push(toolInfo) - } - } - - // Combine text and tool summaries - let assistantContent = '' - if (textParts.length > 0) { - assistantContent = textParts.join('\n') - } - if (toolSummaries.length > 0) { - if (assistantContent) { - assistantContent += '\n' + toolSummaries.join(' ') - } else { - assistantContent = toolSummaries.join(' ') - } - } - if (assistantContent) { - historyParts.push(`Assistant: ${assistantContent}`) - } - } - } - if (historyParts.length > 0) { - // Limit history to last ~10000 chars to avoid context overflow - let history = historyParts.join('\n\n') - if (history.length > 10000) { - history = '...(earlier messages truncated)...\n\n' + history.slice(-10000) - } - historyText = `[CONVERSATION HISTORY] -${history} -[/CONVERSATION HISTORY] - -` - console.log(`[Ollama] Added ${historyParts.length} messages to history (${history.length} chars)`) - } - } - - const ollamaContext = `[CONTEXT] -You are a coding assistant in OFFLINE mode (Ollama model: ${resolvedModel || 'unknown'}). -Project: ${input.projectPath || input.cwd} -Working directory: ${input.cwd} - -IMPORTANT: When using tools, use these EXACT parameter names: -- Read: use "file_path" (not "file") -- Write: use "file_path" and "content" -- Edit: use "file_path", "old_string", "new_string" -- Glob: use "pattern" (e.g. "**/*.ts") and optionally "path" -- Grep: use "pattern" and optionally "path" -- Bash: use "command" - -When asked about the project, use Glob to find files and Read to examine them. -Be concise and helpful. -[/CONTEXT]${agentsMdContent ? ` - -[AGENTS.MD] -${agentsMdContent} -[/AGENTS.MD]` : ''} - -${historyText}[CURRENT REQUEST] -${prompt} -[/CURRENT REQUEST]` - finalQueryPrompt = ollamaContext - console.log('[Ollama] Context prefix added to prompt') - } - - // System prompt config - use preset for both Claude and Ollama - // If AGENTS.md exists, append its content to the system prompt - const systemPromptConfig = agentsMdContent - ? { - type: "preset" as const, - preset: "claude_code" as const, - append: `\n\n# AGENTS.md\nThe following are the project's AGENTS.md instructions:\n\n${agentsMdContent}`, - } - : { - type: "preset" as const, - preset: "claude_code" as const, - } - const queryOptions = { - prompt: finalQueryPrompt, + prompt, options: { abortController, // Must be inside options! cwd: input.cwd, - systemPrompt: systemPromptConfig, - // Register mentioned agents with SDK via options.agents (skip for Ollama - not supported) - ...(!isUsingOllama && Object.keys(agentsOption).length > 0 && { agents: agentsOption }), + systemPrompt: { + type: "preset" as const, + preset: "claude_code" as const, + }, + // Register mentioned agents with SDK via options.agents + ...(Object.keys(agentsOption).length > 0 && { agents: agentsOption }), // Pass filtered MCP servers (only working/unknown ones, skip failed/needs-auth) ...(mcpServersFiltered && Object.keys(mcpServersFiltered).length > 0 && { mcpServers: mcpServersFiltered }), env: finalEnv, @@ -1245,68 +954,13 @@ ${prompt} allowDangerouslySkipPermissions: true, }), includePartialMessages: true, - // Load skills from project and user directories (skip for Ollama - not supported) - ...(!isUsingOllama && { settingSources: ["project" as const, "user" as const] }), + // Load skills from project and user directories (native Claude Code skills) + settingSources: ["project" as const, "user" as const], canUseTool: async ( toolName: string, toolInput: Record, options: { toolUseID: string }, ) => { - // Fix common parameter mistakes from Ollama models - // Local models often use slightly wrong parameter names - if (isUsingOllama) { - // Read: "file" -> "file_path" - if (toolName === "Read" && toolInput.file && !toolInput.file_path) { - toolInput.file_path = toolInput.file - delete toolInput.file - console.log('[Ollama] Fixed Read tool: file -> file_path') - } - // Write: "file" -> "file_path", "content" is usually correct - if (toolName === "Write" && toolInput.file && !toolInput.file_path) { - toolInput.file_path = toolInput.file - delete toolInput.file - console.log('[Ollama] Fixed Write tool: file -> file_path') - } - // Edit: "file" -> "file_path" - if (toolName === "Edit" && toolInput.file && !toolInput.file_path) { - toolInput.file_path = toolInput.file - delete toolInput.file - console.log('[Ollama] Fixed Edit tool: file -> file_path') - } - // Glob: "path" might be passed as "directory" or "dir" - if (toolName === "Glob") { - if (toolInput.directory && !toolInput.path) { - toolInput.path = toolInput.directory - delete toolInput.directory - console.log('[Ollama] Fixed Glob tool: directory -> path') - } - if (toolInput.dir && !toolInput.path) { - toolInput.path = toolInput.dir - delete toolInput.dir - console.log('[Ollama] Fixed Glob tool: dir -> path') - } - } - // Grep: "query" -> "pattern", "directory" -> "path" - if (toolName === "Grep") { - if (toolInput.query && !toolInput.pattern) { - toolInput.pattern = toolInput.query - delete toolInput.query - console.log('[Ollama] Fixed Grep tool: query -> pattern') - } - if (toolInput.directory && !toolInput.path) { - toolInput.path = toolInput.directory - delete toolInput.directory - console.log('[Ollama] Fixed Grep tool: directory -> path') - } - } - // Bash: "cmd" -> "command" - if (toolName === "Bash" && toolInput.cmd && !toolInput.command) { - toolInput.command = toolInput.cmd - delete toolInput.cmd - console.log('[Ollama] Fixed Bash tool: cmd -> command') - } - } - if (input.mode === "plan") { if (toolName === "Edit" || toolName === "Write") { const filePath = @@ -1456,16 +1110,11 @@ ${prompt} let messageCount = 0 let lastError: Error | null = null + let planCompleted = false // Flag to stop after ExitPlanMode in plan mode + let exitPlanModeToolCallId: string | null = null // Track ExitPlanMode's toolCallId let firstMessageReceived = false - // Track last assistant message UUID for rollback support - // Only assigned to metadata AFTER the stream completes (not during generation) - let lastAssistantUuid: string | null = null const streamIterationStart = Date.now() - // Plan mode: track ExitPlanMode to stop after plan is complete - let planCompleted = false - let exitPlanModeToolCallId: string | null = null - if (isUsingOllama) { console.log(`[Ollama] ===== STARTING STREAM ITERATION =====`) console.log(`[Ollama] Model: ${finalCustomConfig?.model}`) @@ -1521,75 +1170,39 @@ ${prompt} // Check for error messages from SDK (error can be embedded in message payload!) const msgAny = msg as any if (msgAny.type === "error" || msgAny.error) { - // Extract detailed error text from message content if available - // This is where the actual error description lives (e.g., "API Error: Claude Code is unable to respond...") - const messageText = msgAny.message?.content?.[0]?.text - const sdkError = messageText || msgAny.error || msgAny.message || "Unknown SDK error" + const sdkError = + msgAny.error || msgAny.message || "Unknown SDK error" lastError = new Error(sdkError) - // Detailed SDK error logging in main process - console.error(`[CLAUDE SDK ERROR] ========================================`) - console.error(`[CLAUDE SDK ERROR] Raw error: ${sdkError}`) - console.error(`[CLAUDE SDK ERROR] Message type: ${msgAny.type}`) - console.error(`[CLAUDE SDK ERROR] SubChat ID: ${input.subChatId}`) - console.error(`[CLAUDE SDK ERROR] Chat ID: ${input.chatId}`) - console.error(`[CLAUDE SDK ERROR] CWD: ${input.cwd}`) - console.error(`[CLAUDE SDK ERROR] Mode: ${input.mode}`) - console.error(`[CLAUDE SDK ERROR] Session ID: ${msgAny.session_id || 'none'}`) - console.error(`[CLAUDE SDK ERROR] Has custom config: ${!!finalCustomConfig}`) - console.error(`[CLAUDE SDK ERROR] Is using Ollama: ${isUsingOllama}`) - console.error(`[CLAUDE SDK ERROR] Model: ${resolvedModel || 'default'}`) - console.error(`[CLAUDE SDK ERROR] Has OAuth token: ${!!claudeCodeToken}`) - console.error(`[CLAUDE SDK ERROR] MCP servers: ${mcpServersFiltered ? Object.keys(mcpServersFiltered).join(', ') : 'none'}`) - console.error(`[CLAUDE SDK ERROR] Full message:`, JSON.stringify(msgAny, null, 2)) - console.error(`[CLAUDE SDK ERROR] ========================================`) - // Categorize SDK-level errors - // Use the raw error code (e.g., "invalid_request") for category matching - const rawErrorCode = msgAny.error || "" let errorCategory = "SDK_ERROR" - // Default errorContext to the full error text (which may include detailed message) - let errorContext = sdkError + let errorContext = "Claude SDK error" if ( - rawErrorCode === "authentication_failed" || + sdkError === "authentication_failed" || sdkError.includes("authentication") ) { errorCategory = "AUTH_FAILED_SDK" errorContext = "Authentication failed - not logged into Claude Code CLI" } else if ( - String(sdkError).includes("invalid_token") || - String(sdkError).includes("Invalid access token") - ) { - errorCategory = "MCP_INVALID_TOKEN" - errorContext = "Invalid access token. Update MCP settings" - } else if ( - rawErrorCode === "invalid_api_key" || + sdkError === "invalid_api_key" || sdkError.includes("api_key") ) { errorCategory = "INVALID_API_KEY_SDK" errorContext = "Invalid API key in Claude Code CLI" } else if ( - rawErrorCode === "rate_limit_exceeded" || + sdkError === "rate_limit_exceeded" || sdkError.includes("rate") ) { errorCategory = "RATE_LIMIT_SDK" errorContext = "Session limit reached" } else if ( - rawErrorCode === "overloaded" || + sdkError === "overloaded" || sdkError.includes("overload") ) { errorCategory = "OVERLOADED_SDK" errorContext = "Claude is overloaded, try again later" - } else if ( - rawErrorCode === "invalid_request" || - sdkError.includes("Usage Policy") || - sdkError.includes("violate") - ) { - // Usage Policy violation - keep the full detailed error text - errorCategory = "USAGE_POLICY_VIOLATION" - // errorContext already contains the full message from sdkError } // Emit auth-error for authentication failures, regular error otherwise @@ -1604,7 +1217,7 @@ ${prompt} errorText: errorContext, debugInfo: { category: errorCategory, - rawErrorCode, + sdkError: sdkError, sessionId: msgAny.session_id, messageId: msgAny.message?.id, }, @@ -1612,36 +1225,17 @@ ${prompt} } console.log(`[SD] M:END sub=${subId} reason=sdk_error cat=${errorCategory} n=${chunkCount}`) - console.error(`[SD] SDK Error details:`, { - errorCategory, - errorContext: errorContext.slice(0, 200), // Truncate for log readability - rawErrorCode, - sessionId: msgAny.session_id, - messageId: msgAny.message?.id, - fullMessage: JSON.stringify(msgAny, null, 2), - }) safeEmit({ type: "finish" } as UIMessageChunk) safeComplete() return } - // Track sessionId for rollback support (available on all messages) + // Track sessionId and uuid for rollback support (available on all messages) if (msgAny.session_id) { metadata.sessionId = msgAny.session_id currentSessionId = msgAny.session_id // Share with cleanup } - // Track UUID from assistant messages for resumeSessionAt - if (msgAny.type === "assistant" && msgAny.uuid) { - lastAssistantUuid = msgAny.uuid - } - - // When result arrives, assign the last assistant UUID to metadata - // It will be emitted as part of the merged message-metadata chunk below - if (msgAny.type === "result" && historyEnabled && lastAssistantUuid && !abortController.signal.aborted) { - metadata.sdkMessageUuid = lastAssistantUuid - } - // Debug: Log system messages from SDK if (msgAny.type === "system") { // Full log to see all fields including MCP errors @@ -1652,6 +1246,22 @@ ${prompt} plugins: msgAny.plugins, permissionMode: msgAny.permissionMode, }, null, 2)) + + // Cache MCP server statuses for next request + if (msgAny.subtype === "init" && msgAny.mcp_servers) { + const lookupPath = input.projectPath || input.cwd + const statusMap = new Map() + + for (const server of msgAny.mcp_servers) { + if (server.name && server.status) { + statusMap.set(server.name, server.status) + } + } + + mcpServerStatusCache.set(lookupPath, statusMap) + // Persist to disk immediately (write-through) + saveMcpStatusToDisk() + } } // Transform and emit + accumulate @@ -1659,12 +1269,6 @@ ${prompt} chunkCount++ lastChunkType = chunk.type - // For message-metadata, inject sdkMessageUuid before emitting - // so the frontend receives the full merged metadata in one chunk - if (chunk.type === "message-metadata" && metadata.sdkMessageUuid) { - chunk.messageMetadata = { ...chunk.messageMetadata, sdkMessageUuid: metadata.sdkMessageUuid } - } - // Use safeEmit to prevent throws when observer is closed if (!safeEmit(chunk)) { // Observer closed (user clicked Stop), break out of loop @@ -1699,7 +1303,6 @@ ${prompt} toolName: chunk.toolName, input: chunk.input, state: "call", - startedAt: Date.now(), }) break case "tool-output-available": @@ -1727,13 +1330,18 @@ ${prompt} } } } - - // Check if ExitPlanMode just completed - stop the stream - if (exitPlanModeToolCallId && chunk.toolCallId === exitPlanModeToolCallId) { - console.log(`[SD] M:PLAN_FINISH sub=${subId} - ExitPlanMode completed, emitting finish`) - planCompleted = true - safeEmit({ type: "finish" } as UIMessageChunk) - } + } + // Stop streaming after ExitPlanMode completes in plan mode + // Match by toolCallId since toolName is undefined in output chunks + if (input.mode === "plan" && exitPlanModeToolCallId && chunk.toolCallId === exitPlanModeToolCallId) { + console.log(`[SD] M:PLAN_STOP sub=${subId} callId=${chunk.toolCallId} n=${chunkCount} parts=${parts.length}`) + planCompleted = true + // Emit finish chunk so Chat hook properly resets its state + console.log(`[SD] M:PLAN_FINISH sub=${subId} - emitting finish chunk`) + safeEmit({ type: "finish" } as UIMessageChunk) + // NOTE: We intentionally do NOT abort here. Aborting corrupts the session state, + // which breaks follow-up messages in plan mode. The stream will complete naturally + // via the planCompleted flag breaking out of the loops below. } break case "message-metadata": @@ -1756,23 +1364,22 @@ ${prompt} } break } - // Break from chunk loop if plan is done if (planCompleted) { console.log(`[SD] M:PLAN_BREAK_CHUNK sub=${subId}`) break } } + // Break from stream loop if plan is done + if (planCompleted) { + console.log(`[SD] M:PLAN_BREAK_STREAM sub=${subId}`) + break + } // Break from stream loop if observer closed (user clicked Stop) if (!isObservableActive) { console.log(`[SD] M:OBSERVER_CLOSED_STREAM sub=${subId}`) break } - // Break from stream loop if plan completed - if (planCompleted) { - console.log(`[SD] M:PLAN_BREAK_STREAM sub=${subId}`) - break - } } // Warn if stream yielded no messages (offline mode issue) @@ -1820,20 +1427,7 @@ ${prompt} let errorContext = "Claude streaming error" let errorCategory = "UNKNOWN" - // Check for session-not-found error in stderr - const isSessionNotFound = stderrOutput?.includes("No conversation found with session ID") - - if (isSessionNotFound) { - // Clear the invalid session ID from database so next attempt starts fresh - console.log(`[claude] Session not found - clearing invalid sessionId from database`) - db.update(subChats) - .set({ sessionId: null }) - .where(eq(subChats.id, input.subChatId)) - .run() - - errorContext = "Previous session expired. Please try again." - errorCategory = "SESSION_EXPIRED" - } else if (err.message?.includes("exited with code")) { + if (err.message?.includes("exited with code")) { errorContext = "Claude Code process crashed" errorCategory = "PROCESS_CRASH" } else if (err.message?.includes("ENOENT")) { @@ -1959,15 +1553,13 @@ ${prompt} // 7. Save final messages to DB // ALWAYS save accumulated parts, even on abort (so user sees partial responses after reload) - console.log(`[SD] M:SAVE sub=${subId} aborted=${abortController.signal.aborted} parts=${parts.length}`) + console.log(`[SD] M:SAVE sub=${subId} planCompleted=${planCompleted} aborted=${abortController.signal.aborted} parts=${parts.length}`) // Flush any remaining text if (currentText.trim()) { parts.push({ type: "text", text: currentText }) } - const savedSessionId = metadata.sessionId - if (parts.length > 0) { const assistantMessage = { id: crypto.randomUUID(), @@ -1981,7 +1573,7 @@ ${prompt} db.update(subChats) .set({ messages: JSON.stringify(finalMessages), - sessionId: savedSessionId, + sessionId: metadata.sessionId, streamId: null, updatedAt: new Date(), }) @@ -1991,7 +1583,7 @@ ${prompt} // No assistant response - just clear streamId db.update(subChats) .set({ - sessionId: savedSessionId, + sessionId: metadata.sessionId, streamId: null, updatedAt: new Date(), }) @@ -2011,7 +1603,8 @@ ${prompt} } const duration = ((Date.now() - streamStart) / 1000).toFixed(1) - console.log(`[SD] M:END sub=${subId} reason=ok n=${chunkCount} last=${lastChunkType} t=${duration}s`) + const reason = planCompleted ? "plan_complete" : "ok" + console.log(`[SD] M:END sub=${subId} reason=${reason} n=${chunkCount} last=${lastChunkType} t=${duration}s`) safeComplete() } catch (error) { const duration = ((Date.now() - streamStart) / 1000).toFixed(1) @@ -2032,13 +1625,14 @@ ${prompt} activeSessions.delete(input.subChatId) clearPendingApprovals("Session ended.", input.subChatId) - // Clear streamId since we're no longer streaming. - // sessionId is NOT saved here — the save block in the async function - // handles it (saves on normal completion, clears on abort). This avoids - // a redundant DB write that the cancel mutation would then overwrite. + // Save sessionId on abort so conversation can be resumed + // Clear streamId since we're no longer streaming const db = getDatabase() db.update(subChats) - .set({ streamId: null }) + .set({ + streamId: null, + ...(currentSessionId && { sessionId: currentSessionId }) + }) .where(eq(subChats.id, input.subChatId)) .run() } @@ -2048,50 +1642,36 @@ ${prompt} /** * Get MCP servers configuration for a project * This allows showing MCP servers in UI before starting a chat session - * NOTE: Does NOT fetch OAuth metadata here - that's done lazily when user clicks Auth */ getMcpConfig: publicProcedure .input(z.object({ projectPath: z.string() })) .query(async ({ input }) => { - try { - const config = await readClaudeConfig() - const globalServers = config.mcpServers || {} - const projectMcpServers = getProjectMcpServers(config, input.projectPath) || {} - - // Merge global + project (project overrides global) - const merged = { ...globalServers, ...projectMcpServers } - - // Add plugin MCP servers (enabled + approved only) - const [enabledPluginSources, pluginMcpConfigs, approvedServers] = await Promise.all([ - getEnabledPlugins(), - discoverPluginMcpServers(), - getApprovedPluginMcpServers(), - ]) + const claudeJsonPath = path.join(os.homedir(), ".claude.json") - for (const pluginConfig of pluginMcpConfigs) { - if (!enabledPluginSources.includes(pluginConfig.pluginSource)) continue - for (const [name, serverConfig] of Object.entries(pluginConfig.mcpServers)) { - if (!merged[name]) { - const identifier = `${pluginConfig.pluginSource}:${name}` - if (approvedServers.includes(identifier)) { - merged[name] = serverConfig - } - } - } + try { + const exists = await fs.stat(claudeJsonPath).then(() => true).catch(() => false) + if (!exists) { + return { mcpServers: [], projectPath: input.projectPath } } - // Convert to array format - determine status from config (no caching) - const mcpServers = Object.entries(merged).map(([name, serverConfig]) => { - const configObj = serverConfig as Record - const status = getServerStatusFromConfig(configObj) - const hasUrl = !!configObj.url + const configContent = await fs.readFile(claudeJsonPath, "utf-8") + const config = JSON.parse(configContent) - return { - name, - status, - config: { ...configObj, _hasUrl: hasUrl }, - } - }) + // Look for project-specific MCP config + const projectConfig = config.projects?.[input.projectPath] + + if (!projectConfig?.mcpServers) { + return { mcpServers: [], projectPath: input.projectPath } + } + + // Convert to array format with names + const mcpServers = Object.entries(projectConfig.mcpServers).map(([name, serverConfig]) => ({ + name, + // Status will be "pending" until SDK actually connects + status: "pending" as const, + // Include config details for display (command, args, etc) + config: serverConfig as Record, + })) return { mcpServers, projectPath: input.projectPath } } catch (error) { @@ -2100,13 +1680,6 @@ ${prompt} } }), - /** - * Get ALL MCP servers configuration (global + all projects) - * Returns grouped data for display in settings - * Also populates the workingMcpServers cache - */ - getAllMcpConfig: publicProcedure.query(getAllMcpConfigHandler), - /** * Cancel active session */ @@ -2118,10 +1691,9 @@ ${prompt} controller.abort() activeSessions.delete(input.subChatId) clearPendingApprovals("Session cancelled.", input.subChatId) + return { cancelled: true } } - - - return { cancelled: !!controller } + return { cancelled: false } }), /** @@ -2152,268 +1724,4 @@ ${prompt} pendingToolApprovals.delete(input.toolUseId) return { ok: true } }), - - /** - * Start MCP OAuth flow for a server - * Fetches OAuth metadata internally when needed - */ - startMcpOAuth: publicProcedure - .input(z.object({ - serverName: z.string(), - projectPath: z.string(), - })) - .mutation(async ({ input }) => { - return startMcpOAuth(input.serverName, input.projectPath) - }), - - /** - * Get MCP auth status for a server - */ - getMcpAuthStatus: publicProcedure - .input(z.object({ - serverName: z.string(), - projectPath: z.string(), - })) - .query(async ({ input }) => { - return getMcpAuthStatus(input.serverName, input.projectPath) - }), - - addMcpServer: publicProcedure - .input(z.object({ - name: z.string().min(1).regex(/^[a-zA-Z0-9_-]+$/, "Name must contain only letters, numbers, underscores, and hyphens"), - scope: z.enum(["global", "project"]), - projectPath: z.string().optional(), - transport: z.enum(["stdio", "http"]), - command: z.string().optional(), - args: z.array(z.string()).optional(), - env: z.record(z.string()).optional(), - url: z.string().url().optional(), - authType: z.enum(["none", "oauth", "bearer"]).optional(), - bearerToken: z.string().optional(), - })) - .mutation(async ({ input }) => { - const serverName = input.name.trim() - - if (input.transport === "stdio" && !input.command?.trim()) { - throw new Error("Command is required for stdio servers") - } - if (input.transport === "http" && !input.url?.trim()) { - throw new Error("URL is required for HTTP servers") - } - if (input.scope === "project" && !input.projectPath) { - throw new Error("Project path required for project-scoped servers") - } - - const serverConfig: McpServerConfig = {} - if (input.transport === "stdio") { - serverConfig.command = input.command!.trim() - if (input.args && input.args.length > 0) { - serverConfig.args = input.args - } - if (input.env && Object.keys(input.env).length > 0) { - serverConfig.env = input.env - } - } else { - serverConfig.url = input.url!.trim() - if (input.authType) { - serverConfig.authType = input.authType - } - if (input.bearerToken) { - serverConfig.headers = { Authorization: `Bearer ${input.bearerToken}` } - } - } - - // Check existence before writing - const existingConfig = await readClaudeConfig() - const projectPath = input.projectPath - if (input.scope === "project" && projectPath) { - if (existingConfig.projects?.[projectPath]?.mcpServers?.[serverName]) { - throw new Error(`Server "${serverName}" already exists in this project`) - } - } else { - if (existingConfig.mcpServers?.[serverName]) { - throw new Error(`Server "${serverName}" already exists`) - } - } - - const config = updateMcpServerConfig(existingConfig, input.scope === "project" ? projectPath ?? null : null, serverName, serverConfig) - await writeClaudeConfig(config) - - return { success: true, name: serverName } - }), - - updateMcpServer: publicProcedure - .input(z.object({ - name: z.string(), - scope: z.enum(["global", "project"]), - projectPath: z.string().optional(), - newName: z.string().regex(/^[a-zA-Z0-9_-]+$/).optional(), - command: z.string().optional(), - args: z.array(z.string()).optional(), - env: z.record(z.string()).optional(), - url: z.string().url().optional(), - authType: z.enum(["none", "oauth", "bearer"]).optional(), - bearerToken: z.string().optional(), - disabled: z.boolean().optional(), - })) - .mutation(async ({ input }) => { - const config = await readClaudeConfig() - const projectPath = input.scope === "project" ? input.projectPath : undefined - - // Check server exists - let servers: Record | undefined - if (projectPath) { - servers = config.projects?.[projectPath]?.mcpServers - } else { - servers = config.mcpServers - } - if (!servers?.[input.name]) { - throw new Error(`Server "${input.name}" not found`) - } - - const existing = servers[input.name] - - // Handle rename: create new, remove old - if (input.newName && input.newName !== input.name) { - if (servers[input.newName]) { - throw new Error(`Server "${input.newName}" already exists`) - } - const updated = removeMcpServerConfig(config, projectPath ?? null, input.name) - const finalConfig = updateMcpServerConfig(updated, projectPath ?? null, input.newName, existing) - await writeClaudeConfig(finalConfig) - return { success: true, name: input.newName } - } - - // Build update object from provided fields - const update: Partial = {} - if (input.command !== undefined) update.command = input.command - if (input.args !== undefined) update.args = input.args - if (input.env !== undefined) update.env = input.env - if (input.url !== undefined) update.url = input.url - if (input.disabled !== undefined) update.disabled = input.disabled - - // Handle bearer token - if (input.bearerToken) { - update.authType = "bearer" - update.headers = { Authorization: `Bearer ${input.bearerToken}` } - } - - // Handle authType changes - if (input.authType) { - update.authType = input.authType - if (input.authType === "none") { - // Clear auth-related fields - update.headers = undefined - update._oauth = undefined - } - } - - const merged = { ...existing, ...update } - const updatedConfig = updateMcpServerConfig(config, projectPath ?? null, input.name, merged) - await writeClaudeConfig(updatedConfig) - - return { success: true, name: input.name } - }), - - removeMcpServer: publicProcedure - .input(z.object({ - name: z.string(), - scope: z.enum(["global", "project"]), - projectPath: z.string().optional(), - })) - .mutation(async ({ input }) => { - const config = await readClaudeConfig() - const projectPath = input.scope === "project" ? input.projectPath : undefined - - // Check server exists - let servers: Record | undefined - if (projectPath) { - servers = config.projects?.[projectPath]?.mcpServers - } else { - servers = config.mcpServers - } - if (!servers?.[input.name]) { - throw new Error(`Server "${input.name}" not found`) - } - - const updated = removeMcpServerConfig(config, projectPath ?? null, input.name) - await writeClaudeConfig(updated) - - return { success: true } - }), - - setMcpBearerToken: publicProcedure - .input(z.object({ - name: z.string(), - scope: z.enum(["global", "project"]), - projectPath: z.string().optional(), - token: z.string(), - })) - .mutation(async ({ input }) => { - const config = await readClaudeConfig() - const projectPath = input.scope === "project" ? input.projectPath : undefined - - // Check server exists - let servers: Record | undefined - if (projectPath) { - servers = config.projects?.[projectPath]?.mcpServers - } else { - servers = config.mcpServers - } - if (!servers?.[input.name]) { - throw new Error(`Server "${input.name}" not found`) - } - - const existing = servers[input.name] - const updated: McpServerConfig = { - ...existing, - authType: "bearer", - headers: { Authorization: `Bearer ${input.token}` }, - } - - const updatedConfig = updateMcpServerConfig(config, projectPath ?? null, input.name, updated) - await writeClaudeConfig(updatedConfig) - - return { success: true } - }), - - getPendingPluginMcpApprovals: publicProcedure - .input(z.object({ projectPath: z.string().optional() })) - .query(async ({ input }) => { - const [enabledPluginSources, pluginMcpConfigs, approvedServers] = await Promise.all([ - getEnabledPlugins(), - discoverPluginMcpServers(), - getApprovedPluginMcpServers(), - ]) - - // Read global/project servers for conflict check - const config = await readClaudeConfig() - const globalServers = config.mcpServers || {} - const projectServers = input.projectPath ? getProjectMcpServers(config, input.projectPath) || {} : {} - - const pending: Array<{ - pluginSource: string - serverName: string - identifier: string - config: Record - }> = [] - - for (const pluginConfig of pluginMcpConfigs) { - if (!enabledPluginSources.includes(pluginConfig.pluginSource)) continue - - for (const [name, serverConfig] of Object.entries(pluginConfig.mcpServers)) { - const identifier = `${pluginConfig.pluginSource}:${name}` - if (!approvedServers.includes(identifier) && !globalServers[name] && !projectServers[name]) { - pending.push({ - pluginSource: pluginConfig.pluginSource, - serverName: name, - identifier, - config: serverConfig as Record, - }) - } - } - } - - return { pending } - }), }) diff --git a/src/main/lib/trpc/routers/commands.ts b/src/main/lib/trpc/routers/commands.ts deleted file mode 100644 index e2e9d8cd..00000000 --- a/src/main/lib/trpc/routers/commands.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { z } from "zod" -import { router, publicProcedure } from "../index" -import * as fs from "fs/promises" -import * as path from "path" -import * as os from "os" -import matter from "gray-matter" -import { discoverInstalledPlugins, getPluginComponentPaths } from "../../plugins" -import { getEnabledPlugins } from "./claude-settings" - -export interface FileCommand { - name: string - description: string - argumentHint?: string - source: "user" | "project" | "plugin" - pluginName?: string - path: string -} - -/** - * Parse command .md frontmatter to extract description and argument-hint - */ -function parseCommandMd(content: string): { - description?: string - argumentHint?: string - name?: string -} { - try { - const { data } = matter(content) - return { - description: - typeof data.description === "string" ? data.description : undefined, - argumentHint: - typeof data["argument-hint"] === "string" - ? data["argument-hint"] - : undefined, - name: typeof data.name === "string" ? data.name : undefined, - } - } catch (err) { - console.error("[commands] Failed to parse frontmatter:", err) - return {} - } -} - -/** - * Validate entry name for security (prevent path traversal) - */ -function isValidEntryName(name: string): boolean { - return !name.includes("..") && !name.includes("/") && !name.includes("\\") -} - -/** - * Recursively scan a directory for .md command files - * Supports namespaces via nested folders: git/commit.md → git:commit - */ -async function scanCommandsDirectory( - dir: string, - source: "user" | "project" | "plugin", - prefix = "", -): Promise { - const commands: FileCommand[] = [] - - try { - // Check if directory exists - try { - await fs.access(dir) - } catch { - return commands - } - - const entries = await fs.readdir(dir, { withFileTypes: true }) - - for (const entry of entries) { - if (!isValidEntryName(entry.name)) { - console.warn(`[commands] Skipping invalid entry name: ${entry.name}`) - continue - } - - const fullPath = path.join(dir, entry.name) - - if (entry.isDirectory()) { - // Recursively scan nested directories - const nestedCommands = await scanCommandsDirectory( - fullPath, - source, - prefix ? `${prefix}:${entry.name}` : entry.name, - ) - commands.push(...nestedCommands) - } else if (entry.isFile() && entry.name.endsWith(".md")) { - const baseName = entry.name.replace(/\.md$/, "") - const fallbackName = prefix ? `${prefix}:${baseName}` : baseName - - try { - const content = await fs.readFile(fullPath, "utf-8") - const parsed = parseCommandMd(content) - const commandName = parsed.name || fallbackName - - commands.push({ - name: commandName, - description: parsed.description || "", - argumentHint: parsed.argumentHint, - source, - path: fullPath, - }) - } catch (err) { - console.warn(`[commands] Failed to read ${fullPath}:`, err) - } - } - } - } catch (err) { - console.error(`[commands] Failed to scan directory ${dir}:`, err) - } - - return commands -} - -export const commandsRouter = router({ - /** - * List all commands from filesystem - * - User commands: ~/.claude/commands/ - * - Project commands: .claude/commands/ (relative to projectPath) - */ - list: publicProcedure - .input( - z - .object({ - projectPath: z.string().optional(), - }) - .optional(), - ) - .query(async ({ input }) => { - const userCommandsDir = path.join(os.homedir(), ".claude", "commands") - const userCommandsPromise = scanCommandsDirectory(userCommandsDir, "user") - - let projectCommandsPromise = Promise.resolve([]) - if (input?.projectPath) { - const projectCommandsDir = path.join( - input.projectPath, - ".claude", - "commands", - ) - projectCommandsPromise = scanCommandsDirectory( - projectCommandsDir, - "project", - ) - } - - // Discover plugin commands - const [enabledPluginSources, installedPlugins] = await Promise.all([ - getEnabledPlugins(), - discoverInstalledPlugins(), - ]) - const enabledPlugins = installedPlugins.filter( - (p) => enabledPluginSources.includes(p.source), - ) - const pluginCommandsPromises = enabledPlugins.map(async (plugin) => { - const paths = getPluginComponentPaths(plugin) - try { - const commands = await scanCommandsDirectory(paths.commands, "plugin") - return commands.map((cmd) => ({ ...cmd, pluginName: plugin.source })) - } catch { - return [] - } - }) - - // Scan all directories in parallel - const [userCommands, projectCommands, ...pluginCommandsArrays] = - await Promise.all([ - userCommandsPromise, - projectCommandsPromise, - ...pluginCommandsPromises, - ]) - const pluginCommands = pluginCommandsArrays.flat() - - // Project commands first (more specific), then user commands, then plugin commands - return [...projectCommands, ...userCommands, ...pluginCommands] - }), - - /** - * Get content of a specific command file (without frontmatter) - */ - getContent: publicProcedure - .input(z.object({ path: z.string() })) - .query(async ({ input }) => { - // Security: prevent path traversal - if (input.path.includes("..")) { - throw new Error("Invalid path") - } - - try { - const content = await fs.readFile(input.path, "utf-8") - const { content: body } = matter(content) - return { content: body.trim() } - } catch (err) { - console.error(`[commands] Failed to read command content:`, err) - return { content: "" } - } - }), -}) diff --git a/src/main/lib/trpc/routers/external.ts b/src/main/lib/trpc/routers/external.ts index 3a16b33d..3a7dab70 100644 --- a/src/main/lib/trpc/routers/external.ts +++ b/src/main/lib/trpc/routers/external.ts @@ -1,46 +1,7 @@ -import { clipboard, shell } from "electron"; -import { execFileSync, spawn } from "node:child_process"; -import * as os from "node:os"; -import * as path from "node:path"; +import { shell } from "electron"; +import { spawn } from "node:child_process"; import { z } from "zod"; import { publicProcedure, router } from "../index"; -import { - APP_META, - externalAppSchema, - type ExternalApp, -} from "../../../../shared/external-apps"; - -function expandTilde(filePath: string): string { - if (filePath.startsWith("~/") || filePath === "~") { - return path.join(os.homedir(), filePath.slice(1)); - } - return filePath; -} - -function spawnAsync(command: string, args: string[]): Promise { - return new Promise((resolve, reject) => { - const child = spawn(command, args, { - detached: true, - stdio: "ignore", - }); - child.unref(); - child.on("error", reject); - // Resolve immediately — we just need to launch the app - resolve(); - }); -} - -function openPathInApp(app: ExternalApp, targetPath: string): Promise { - const expandedPath = expandTilde(targetPath); - - if (app === "finder") { - shell.showItemInFolder(expandedPath); - return Promise.resolve(); - } - - const meta = APP_META[app]; - return spawnAsync("open", ["-a", meta.macAppName, expandedPath]); -} /** * External router for shell operations (open in finder, open in editor, etc.) @@ -48,28 +9,8 @@ function openPathInApp(app: ExternalApp, targetPath: string): Promise { export const externalRouter = router({ openInFinder: publicProcedure .input(z.string()) - .mutation(async ({ input: inputPath }) => { - const expandedPath = expandTilde(inputPath); - shell.showItemInFolder(expandedPath); - return { success: true }; - }), - - openInApp: publicProcedure - .input( - z.object({ - path: z.string(), - app: externalAppSchema, - }), - ) - .mutation(async ({ input }) => { - await openPathInApp(input.app, input.path); - return { success: true }; - }), - - copyPath: publicProcedure - .input(z.string()) - .mutation(({ input: inputPath }) => { - clipboard.writeText(inputPath); + .mutation(async ({ input: path }) => { + shell.showItemInFolder(path); return { success: true }; }), @@ -81,24 +22,19 @@ export const externalRouter = router({ }), ) .mutation(async ({ input }) => { - const { cwd } = input; - const filePath = input.path.startsWith("~") - ? input.path.replace("~", os.homedir()) - : input.path; + const { path, cwd } = input; // Try common code editors in order of preference const editors = [ - { cmd: "cursor", args: [filePath] }, // Cursor - { cmd: "code", args: [filePath] }, // VS Code - { cmd: "subl", args: [filePath] }, // Sublime Text - { cmd: "atom", args: [filePath] }, // Atom - { cmd: "open", args: ["-t", filePath] }, // macOS default text editor + { cmd: "code", args: [path] }, // VS Code + { cmd: "cursor", args: [path] }, // Cursor + { cmd: "subl", args: [path] }, // Sublime Text + { cmd: "atom", args: [path] }, // Atom + { cmd: "open", args: ["-t", path] }, // macOS default text editor ]; for (const editor of editors) { try { - // Check if the command exists first - execFileSync("which", [editor.cmd], { stdio: "ignore" }); const child = spawn(editor.cmd, editor.args, { cwd: cwd || undefined, detached: true, @@ -113,7 +49,7 @@ export const externalRouter = router({ } // Fallback: use shell.openPath which opens with default app - await shell.openPath(filePath); + await shell.openPath(path); return { success: true, editor: "default" }; }), diff --git a/src/main/lib/trpc/routers/files.ts b/src/main/lib/trpc/routers/files.ts index 04763d1f..fc40757c 100644 --- a/src/main/lib/trpc/routers/files.ts +++ b/src/main/lib/trpc/routers/files.ts @@ -1,10 +1,7 @@ import { z } from "zod" import { router, publicProcedure } from "../index" -import { readdir, stat, readFile, writeFile, mkdir } from "node:fs/promises" -import { join, relative, basename, extname } from "node:path" -import { app } from "electron" -import { watch } from "node:fs" -import { observable } from "@trpc/server/observable" +import { readdir, stat } from "node:fs/promises" +import { join, relative, basename } from "node:path" // Directories to ignore when scanning const IGNORED_DIRS = new Set([ @@ -264,160 +261,4 @@ export const filesRouter = router({ fileListCache.delete(input.projectPath) return { success: true } }), - - /** - * Read file contents from filesystem - */ - readFile: publicProcedure - .input(z.object({ filePath: z.string() })) - .query(async ({ input }) => { - const { filePath } = input - - try { - const content = await readFile(filePath, "utf-8") - return content - } catch (error) { - console.error(`[files] Error reading file ${filePath}:`, error) - throw new Error(`Failed to read file: ${error instanceof Error ? error.message : "Unknown error"}`) - } - }), - - /** - * Read a text file with size/binary validation - * Returns structured result with error reasons - */ - readTextFile: publicProcedure - .input(z.object({ filePath: z.string() })) - .query(async ({ input }) => { - const { filePath } = input - const MAX_SIZE = 2 * 1024 * 1024 // 2 MB - - try { - const fileStat = await stat(filePath) - - if (fileStat.size > MAX_SIZE) { - return { ok: false as const, reason: "too-large" as const, byteLength: fileStat.size } - } - - const buffer = await readFile(filePath) - - // Check if binary by looking for null bytes in first 8KB - const sample = buffer.subarray(0, 8192) - if (sample.includes(0)) { - return { ok: false as const, reason: "binary" as const, byteLength: fileStat.size } - } - - const content = buffer.toString("utf-8") - return { ok: true as const, content, byteLength: fileStat.size } - } catch (error) { - const msg = error instanceof Error ? error.message : "Unknown error" - if (msg.includes("ENOENT") || msg.includes("no such file")) { - return { ok: false as const, reason: "not-found" as const, byteLength: 0 } - } - throw new Error(`Failed to read file: ${msg}`) - } - }), - - /** - * Read a binary file as base64 (for images) - */ - readBinaryFile: publicProcedure - .input(z.object({ filePath: z.string() })) - .query(async ({ input }) => { - const { filePath } = input - const MAX_SIZE = 20 * 1024 * 1024 // 20 MB - - try { - const fileStat = await stat(filePath) - - if (fileStat.size > MAX_SIZE) { - return { ok: false as const, reason: "too-large" as const, byteLength: fileStat.size } - } - - const buffer = await readFile(filePath) - const ext = extname(filePath).toLowerCase() - - // Determine MIME type - const mimeMap: Record = { - ".png": "image/png", - ".jpg": "image/jpeg", - ".jpeg": "image/jpeg", - ".gif": "image/gif", - ".svg": "image/svg+xml", - ".webp": "image/webp", - ".ico": "image/x-icon", - ".bmp": "image/bmp", - } - const mimeType = mimeMap[ext] || "application/octet-stream" - - return { - ok: true as const, - data: buffer.toString("base64"), - mimeType, - byteLength: fileStat.size, - } - } catch (error) { - const msg = error instanceof Error ? error.message : "Unknown error" - if (msg.includes("ENOENT") || msg.includes("no such file")) { - return { ok: false as const, reason: "not-found" as const, byteLength: 0 } - } - throw new Error(`Failed to read binary file: ${msg}`) - } - }), - - /** - * Watch for file changes in a project directory - * Emits events when files are modified - */ - watchChanges: publicProcedure - .input(z.object({ projectPath: z.string() })) - .subscription(({ input }) => { - return observable<{ filename: string; eventType: string }>((emit) => { - const watcher = watch(input.projectPath, { recursive: true }, (eventType, filename) => { - if (filename) { - emit.next({ filename, eventType }) - } - }) - - return () => { - watcher.close() - } - }) - }), - - /** - * Write pasted text to a file in the session's pasted directory - * Used for large text pastes that shouldn't be embedded inline - */ - writePastedText: publicProcedure - .input( - z.object({ - subChatId: z.string(), - text: z.string(), - filename: z.string().optional(), - }) - ) - .mutation(async ({ input }) => { - const { subChatId, text, filename } = input - - // Create pasted directory in session folder - const sessionDir = join(app.getPath("userData"), "claude-sessions", subChatId) - const pastedDir = join(sessionDir, "pasted") - await mkdir(pastedDir, { recursive: true }) - - // Generate filename with timestamp - const finalFilename = filename || `pasted_${Date.now()}.txt` - const filePath = join(pastedDir, finalFilename) - - // Write file - await writeFile(filePath, text, "utf-8") - - console.log(`[files] Wrote pasted text to ${filePath} (${text.length} bytes)`) - - return { - filePath, - filename: finalFilename, - size: text.length, - } - }), }) diff --git a/src/main/lib/trpc/routers/index.ts b/src/main/lib/trpc/routers/index.ts index 833026a9..285a54b0 100644 --- a/src/main/lib/trpc/routers/index.ts +++ b/src/main/lib/trpc/routers/index.ts @@ -3,8 +3,6 @@ import { projectsRouter } from "./projects" import { chatsRouter } from "./chats" import { claudeRouter } from "./claude" import { claudeCodeRouter } from "./claude-code" -import { claudeSettingsRouter } from "./claude-settings" -import { anthropicAccountsRouter } from "./anthropic-accounts" import { ollamaRouter } from "./ollama" import { terminalRouter } from "./terminal" import { externalRouter } from "./external" @@ -13,10 +11,6 @@ import { debugRouter } from "./debug" import { skillsRouter } from "./skills" import { agentsRouter } from "./agents" import { worktreeConfigRouter } from "./worktree-config" -import { sandboxImportRouter } from "./sandbox-import" -import { commandsRouter } from "./commands" -import { voiceRouter } from "./voice" -import { pluginsRouter } from "./plugins" import { createGitRouter } from "../../git" import { BrowserWindow } from "electron" @@ -30,8 +24,6 @@ export function createAppRouter(getWindow: () => BrowserWindow | null) { chats: chatsRouter, claude: claudeRouter, claudeCode: claudeCodeRouter, - claudeSettings: claudeSettingsRouter, - anthropicAccounts: anthropicAccountsRouter, ollama: ollamaRouter, terminal: terminalRouter, external: externalRouter, @@ -40,10 +32,6 @@ export function createAppRouter(getWindow: () => BrowserWindow | null) { skills: skillsRouter, agents: agentsRouter, worktreeConfig: worktreeConfigRouter, - sandboxImport: sandboxImportRouter, - commands: commandsRouter, - voice: voiceRouter, - plugins: pluginsRouter, // Git operations - named "changes" to match Superset API changes: createGitRouter(), }) diff --git a/src/main/lib/trpc/routers/ollama.ts b/src/main/lib/trpc/routers/ollama.ts index d0a647ab..61f5535b 100644 --- a/src/main/lib/trpc/routers/ollama.ts +++ b/src/main/lib/trpc/routers/ollama.ts @@ -7,56 +7,6 @@ import { z } from "zod" import { checkInternetConnection, checkOllamaStatus } from "../../ollama" import { publicProcedure, router } from "../index" -/** - * Generate text using local Ollama model - * Used for chat title generation and commit messages in offline mode - * @param prompt - The prompt to send to Ollama - * @param model - Optional model to use (if not provided, uses recommended or first available) - */ -async function generateWithOllama( - prompt: string, - model?: string | null -): Promise { - try { - const ollamaStatus = await checkOllamaStatus() - if (!ollamaStatus.available) { - return null - } - - // Use provided model, or recommended, or first available - const modelToUse = model || ollamaStatus.recommendedModel || ollamaStatus.models[0] - if (!modelToUse) { - console.error("[Ollama] No model available") - return null - } - - const response = await fetch("http://localhost:11434/api/generate", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - model: modelToUse, - prompt, - stream: false, - options: { - temperature: 0.3, - num_predict: 50, // Short responses for titles - }, - }), - }) - - if (!response.ok) { - console.error("[Ollama] Generate failed:", response.status) - return null - } - - const data = await response.json() - return data.response?.trim() || null - } catch (error) { - console.error("[Ollama] Generate error:", error) - return null - } -} - export const ollamaRouter = router({ /** * Get Ollama and network status @@ -86,81 +36,4 @@ export const ollamaRouter = router({ model: ollamaStatus.recommendedModel, } }), - - /** - * Get list of installed models - */ - getModels: publicProcedure.query(async () => { - const ollamaStatus = await checkOllamaStatus() - return { - available: ollamaStatus.available, - models: ollamaStatus.models, - recommendedModel: ollamaStatus.recommendedModel, - } - }), - - /** - * Generate a chat name using local Ollama model - * Used in offline mode for sub-chat title generation - */ - generateChatName: publicProcedure - .input(z.object({ userMessage: z.string(), model: z.string().optional() })) - .mutation(async ({ input }) => { - const prompt = `Generate a very short (2-5 words) title for a coding chat that starts with this message. Only output the title, nothing else. No quotes, no explanations. - -User message: "${input.userMessage.slice(0, 500)}" - -Title:` - - const result = await generateWithOllama(prompt, input.model) - if (result) { - // Clean up the result - remove quotes, trim, limit length - const cleaned = result - .replace(/^["']|["']$/g, "") - .replace(/^title:\s*/i, "") - .trim() - .slice(0, 50) - if (cleaned.length > 0) { - return { name: cleaned } - } - } - return { name: null } - }), - - /** - * Generate a commit message using local Ollama model - * Used in offline mode for commit message generation - */ - generateCommitMessage: publicProcedure - .input( - z.object({ - diff: z.string(), - fileCount: z.number(), - additions: z.number(), - deletions: z.number(), - model: z.string().optional(), - }) - ) - .mutation(async ({ input }) => { - const prompt = `Generate a conventional commit message for these changes. Use format: type: short description - -Types: feat (new feature), fix (bug fix), docs, style, refactor, test, chore - -Changes: ${input.fileCount} files, +${input.additions}/-${input.deletions} lines - -Diff (truncated): -${input.diff.slice(0, 3000)} - -Commit message:` - - const result = await generateWithOllama(prompt, input.model) - if (result) { - // Clean up - get just the first line - const firstLine = result.split("\n")[0]?.trim() - if (firstLine && firstLine.length > 0 && firstLine.length < 100) { - return { message: firstLine } - } - } - return { message: null } - }), }) diff --git a/src/main/lib/trpc/routers/plugins.ts b/src/main/lib/trpc/routers/plugins.ts deleted file mode 100644 index 3519144c..00000000 --- a/src/main/lib/trpc/routers/plugins.ts +++ /dev/null @@ -1,227 +0,0 @@ -import { router, publicProcedure } from "../index" -import * as fs from "fs/promises" -import * as path from "path" -import matter from "gray-matter" -import { - discoverInstalledPlugins, - getPluginComponentPaths, - discoverPluginMcpServers, - clearPluginCache, -} from "../../plugins" -import { getEnabledPlugins } from "./claude-settings" - -interface PluginComponent { - name: string - description?: string -} - -interface PluginWithComponents { - name: string - version: string - description?: string - path: string - source: string // e.g., "ccsetup:ccsetup" - marketplace: string - category?: string - homepage?: string - tags?: string[] - isDisabled: boolean - components: { - commands: PluginComponent[] - skills: PluginComponent[] - agents: PluginComponent[] - mcpServers: string[] - } -} - -/** - * Validate entry name for security (prevent path traversal) - */ -function isValidEntryName(name: string): boolean { - return !name.includes("..") && !name.includes("/") && !name.includes("\\") -} - -/** - * Scan commands directory and return component info - */ -async function scanPluginCommands(dir: string): Promise { - const components: PluginComponent[] = [] - - try { - await fs.access(dir) - } catch { - return components - } - - try { - const entries = await fs.readdir(dir, { withFileTypes: true }) - - for (const entry of entries) { - if (!isValidEntryName(entry.name)) continue - - const fullPath = path.join(dir, entry.name) - - if (entry.isDirectory()) { - // Recursively scan nested directories for namespaced commands - const nested = await scanPluginCommands(fullPath) - components.push(...nested) - } else if (entry.isFile() && entry.name.endsWith(".md")) { - try { - const content = await fs.readFile(fullPath, "utf-8") - const { data } = matter(content) - const baseName = entry.name.replace(/\.md$/, "") - components.push({ - name: typeof data.name === "string" ? data.name : baseName, - description: - typeof data.description === "string" ? data.description : undefined, - }) - } catch { - // Skip files that can't be read - } - } - } - } catch { - // Directory read failed - } - - return components -} - -/** - * Scan skills directory and return component info - */ -async function scanPluginSkills(dir: string): Promise { - const components: PluginComponent[] = [] - - try { - await fs.access(dir) - } catch { - return components - } - - try { - const entries = await fs.readdir(dir, { withFileTypes: true }) - - for (const entry of entries) { - if (!entry.isDirectory() || !isValidEntryName(entry.name)) continue - - const skillMdPath = path.join(dir, entry.name, "SKILL.md") - try { - const content = await fs.readFile(skillMdPath, "utf-8") - const { data } = matter(content) - components.push({ - name: typeof data.name === "string" ? data.name : entry.name, - description: - typeof data.description === "string" ? data.description : undefined, - }) - } catch { - // Skill directory doesn't have SKILL.md - skip - } - } - } catch { - // Directory read failed - } - - return components -} - -/** - * Scan agents directory and return component info - */ -async function scanPluginAgents(dir: string): Promise { - const components: PluginComponent[] = [] - - try { - await fs.access(dir) - } catch { - return components - } - - try { - const entries = await fs.readdir(dir, { withFileTypes: true }) - - for (const entry of entries) { - if (!entry.isFile() || !entry.name.endsWith(".md") || !isValidEntryName(entry.name)) - continue - - const fullPath = path.join(dir, entry.name) - try { - const content = await fs.readFile(fullPath, "utf-8") - const { data } = matter(content) - const baseName = entry.name.replace(/\.md$/, "") - components.push({ - name: typeof data.name === "string" ? data.name : baseName, - description: - typeof data.description === "string" ? data.description : undefined, - }) - } catch { - // Skip files that can't be read - } - } - } catch { - // Directory read failed - } - - return components -} - -export const pluginsRouter = router({ - /** - * List all installed plugins with their components and disabled status - */ - list: publicProcedure.query(async (): Promise => { - const [installedPlugins, enabledPlugins, mcpConfigs] = await Promise.all([ - discoverInstalledPlugins(), - getEnabledPlugins(), - discoverPluginMcpServers(), - ]) - - // Build a map of plugin source -> MCP server names - const pluginMcpMap = new Map() - for (const config of mcpConfigs) { - pluginMcpMap.set(config.pluginSource, Object.keys(config.mcpServers)) - } - - // Scan components for each plugin in parallel - const pluginsWithComponents = await Promise.all( - installedPlugins.map(async (plugin) => { - const paths = getPluginComponentPaths(plugin) - - const [commands, skills, agents] = await Promise.all([ - scanPluginCommands(paths.commands), - scanPluginSkills(paths.skills), - scanPluginAgents(paths.agents), - ]) - - return { - name: plugin.name, - version: plugin.version, - description: plugin.description, - path: plugin.path, - source: plugin.source, - marketplace: plugin.marketplace, - category: plugin.category, - homepage: plugin.homepage, - tags: plugin.tags, - isDisabled: !enabledPlugins.includes(plugin.source), - components: { - commands, - skills, - agents, - mcpServers: pluginMcpMap.get(plugin.source) || [], - }, - } - }) - ) - - return pluginsWithComponents - }), - - /** - * Clear plugin cache (forces re-scan on next list) - */ - clearCache: publicProcedure.mutation(async () => { - clearPluginCache() - return { success: true } - }), -}) diff --git a/src/main/lib/trpc/routers/projects.ts b/src/main/lib/trpc/routers/projects.ts index 106c6d36..8b6ba2ea 100644 --- a/src/main/lib/trpc/routers/projects.ts +++ b/src/main/lib/trpc/routers/projects.ts @@ -7,23 +7,13 @@ import { basename, join } from "path" import { exec } from "node:child_process" import { promisify } from "node:util" import { existsSync } from "node:fs" -import { mkdir, copyFile, unlink } from "node:fs/promises" -import { extname } from "node:path" +import { mkdir } from "node:fs/promises" import { getGitRemoteInfo } from "../../git" import { trackProjectOpened } from "../../analytics" -import { getLaunchDirectory } from "../../cli" const execAsync = promisify(exec) export const projectsRouter = router({ - /** - * Get launch directory from CLI args (consumed once) - * Based on PR #16 by @caffeinum - */ - getLaunchDirectory: publicProcedure.query(() => { - return getLaunchDirectory() - }), - /** * List all projects */ @@ -351,199 +341,4 @@ export const projectsRouter = router({ return newProject }), - - /** - * Open folder picker to locate an existing clone of a specific repo - * Validates that the selected folder matches the expected owner/repo - */ - locateAndAddProject: publicProcedure - .input( - z.object({ - expectedOwner: z.string(), - expectedRepo: z.string(), - }) - ) - .mutation(async ({ input, ctx }) => { - const window = ctx.getWindow?.() ?? BrowserWindow.getFocusedWindow() - - if (!window) { - return { success: false as const, reason: "no-window" as const } - } - - // Ensure window is focused - if (!window.isFocused()) { - window.focus() - await new Promise((resolve) => setTimeout(resolve, 100)) - } - - const result = await dialog.showOpenDialog(window, { - properties: ["openDirectory"], - title: `Locate ${input.expectedOwner}/${input.expectedRepo}`, - buttonLabel: "Select", - }) - - if (result.canceled || !result.filePaths[0]) { - return { success: false as const, reason: "canceled" as const } - } - - const folderPath = result.filePaths[0] - const gitInfo = await getGitRemoteInfo(folderPath) - - // Validate it's the correct repo - if ( - gitInfo.owner !== input.expectedOwner || - gitInfo.repo !== input.expectedRepo - ) { - return { - success: false as const, - reason: "wrong-repo" as const, - found: - gitInfo.owner && gitInfo.repo - ? `${gitInfo.owner}/${gitInfo.repo}` - : "not a git repository", - } - } - - // Create or update project - const db = getDatabase() - const existing = db - .select() - .from(projects) - .where(eq(projects.path, folderPath)) - .get() - - if (existing) { - // Update git info in case it changed - const updated = db - .update(projects) - .set({ - updatedAt: new Date(), - gitRemoteUrl: gitInfo.remoteUrl, - gitProvider: gitInfo.provider, - gitOwner: gitInfo.owner, - gitRepo: gitInfo.repo, - }) - .where(eq(projects.id, existing.id)) - .returning() - .get() - - return { success: true as const, project: updated } - } - - const project = db - .insert(projects) - .values({ - name: basename(folderPath), - path: folderPath, - gitRemoteUrl: gitInfo.remoteUrl, - gitProvider: gitInfo.provider, - gitOwner: gitInfo.owner, - gitRepo: gitInfo.repo, - }) - .returning() - .get() - - return { success: true as const, project } - }), - - /** - * Open folder picker to choose where to clone a repository - */ - pickCloneDestination: publicProcedure - .input(z.object({ suggestedName: z.string() })) - .mutation(async ({ input, ctx }) => { - const window = ctx.getWindow?.() ?? BrowserWindow.getFocusedWindow() - - if (!window) { - return { success: false as const, reason: "no-window" as const } - } - - // Ensure window is focused - if (!window.isFocused()) { - window.focus() - await new Promise((resolve) => setTimeout(resolve, 100)) - } - - // Default to ~/.21st/repos/ - const homePath = app.getPath("home") - const defaultPath = join(homePath, ".21st", "repos") - await mkdir(defaultPath, { recursive: true }) - - const result = await dialog.showOpenDialog(window, { - properties: ["openDirectory", "createDirectory"], - title: "Choose where to clone", - defaultPath, - buttonLabel: "Clone Here", - }) - - if (result.canceled || !result.filePaths[0]) { - return { success: false as const, reason: "canceled" as const } - } - - const targetPath = join(result.filePaths[0], input.suggestedName) - return { success: true as const, targetPath } - }), - - /** - * Upload a custom icon for a project (opens file picker for images) - */ - uploadIcon: publicProcedure - .input(z.object({ id: z.string() })) - .mutation(async ({ input, ctx }) => { - const window = ctx.getWindow?.() ?? BrowserWindow.getFocusedWindow() - if (!window) return null - - if (!window.isFocused()) { - window.focus() - await new Promise((resolve) => setTimeout(resolve, 100)) - } - - const result = await dialog.showOpenDialog(window, { - properties: ["openFile"], - title: "Select Project Icon", - buttonLabel: "Set Icon", - filters: [ - { name: "Images", extensions: ["png", "jpg", "jpeg", "svg", "webp", "ico"] }, - ], - }) - - if (result.canceled || !result.filePaths[0]) return null - - const sourcePath = result.filePaths[0] - const ext = extname(sourcePath) - const iconsDir = join(app.getPath("userData"), "project-icons") - await mkdir(iconsDir, { recursive: true }) - - const destPath = join(iconsDir, `${input.id}${ext}`) - await copyFile(sourcePath, destPath) - - const db = getDatabase() - return db - .update(projects) - .set({ iconPath: destPath, updatedAt: new Date() }) - .where(eq(projects.id, input.id)) - .returning() - .get() - }), - - /** - * Remove custom icon for a project - */ - removeIcon: publicProcedure - .input(z.object({ id: z.string() })) - .mutation(async ({ input }) => { - const db = getDatabase() - const project = db.select().from(projects).where(eq(projects.id, input.id)).get() - - if (project?.iconPath && existsSync(project.iconPath)) { - try { await unlink(project.iconPath) } catch {} - } - - return db - .update(projects) - .set({ iconPath: null, updatedAt: new Date() }) - .where(eq(projects.id, input.id)) - .returning() - .get() - }), }) diff --git a/src/main/lib/trpc/routers/sandbox-import.ts b/src/main/lib/trpc/routers/sandbox-import.ts deleted file mode 100644 index e8fbf4db..00000000 --- a/src/main/lib/trpc/routers/sandbox-import.ts +++ /dev/null @@ -1,672 +0,0 @@ -import { z } from "zod"; -import { router, publicProcedure } from "../index"; -import { getDatabase } from "../../db"; -import { chats, subChats, projects } from "../../db/schema"; -import { eq } from "drizzle-orm"; -import { app } from "electron"; -import { getAuthManager, getBaseUrl } from "../../../index"; -import { createWorktreeForChat } from "../../git/worktree"; -import { importSandboxToWorktree, type ExportClaudeSession } from "../../git/sandbox-import"; -import { getGitRemoteInfo } from "../../git"; -import { mkdir, writeFile } from "node:fs/promises"; -import { basename, join } from "node:path"; -import { exec } from "node:child_process"; -import { promisify } from "node:util"; - -const execAsync = promisify(exec); - -/** - * Schema for remote chat data from web API - */ -const remoteSubChatSchema = z.object({ - id: z.string(), - name: z.string(), - mode: z.string(), - messages: z.any(), // JSON messages array - createdAt: z.string(), - updatedAt: z.string(), -}); - -const remoteChatSchema = z.object({ - id: z.string(), - name: z.string(), - sandboxId: z.string().nullable(), - meta: z - .object({ - repository: z.string().optional(), - branch: z.string().nullable().optional(), - }) - .nullable(), - createdAt: z.string(), - updatedAt: z.string(), - subChats: z.array(remoteSubChatSchema), -}); - -/** - * Write Claude session files to the isolated config directory for a subChat - * This allows conversations to be resumed after importing from sandbox - */ -async function writeClaudeSession( - subChatId: string, - localProjectPath: string, - session: ExportClaudeSession, -): Promise { - // Desktop's isolated config dir for this subChat (same as claude.ts uses) - const isolatedConfigDir = join( - app.getPath("userData"), - "claude-sessions", - subChatId - ); - - // Sanitize local path (same logic as Claude SDK) - // SDK replaces both "/" and "." with "-" - // /Users/sergey/.myapp → -Users-sergey--myapp - const sanitizedPath = localProjectPath.replace(/[/.]/g, "-"); - const projectDir = join(isolatedConfigDir, "projects", sanitizedPath); - - console.log(`[writeClaudeSession] ========== DEBUG ==========`); - console.log(`[writeClaudeSession] subChatId: ${subChatId}`); - console.log(`[writeClaudeSession] localProjectPath: ${localProjectPath}`); - console.log(`[writeClaudeSession] sanitizedPath: ${sanitizedPath}`); - console.log(`[writeClaudeSession] isolatedConfigDir: ${isolatedConfigDir}`); - console.log(`[writeClaudeSession] projectDir: ${projectDir}`); - console.log(`[writeClaudeSession] sessionId: ${session.sessionId}`); - - await mkdir(projectDir, { recursive: true }); - - // Rewrite paths in session data: /home/user/repo → local path - const rewrittenData = session.data.replace(/\/home\/user\/repo/g, localProjectPath); - - // Write session JSONL file - const sessionFilePath = join(projectDir, `${session.sessionId}.jsonl`); - await writeFile(sessionFilePath, rewrittenData, "utf-8"); - - console.log(`[writeClaudeSession] Wrote session file: ${sessionFilePath}`); - - // Write sessions-index.json (with fallbacks for empty metadata) - const indexData = { - version: 1, - entries: [{ - sessionId: session.sessionId, - fullPath: sessionFilePath, - projectPath: localProjectPath, - firstPrompt: session.metadata?.firstPrompt || "", - messageCount: session.metadata?.messageCount || 0, - created: session.metadata?.created || new Date().toISOString(), - modified: session.metadata?.modified || new Date().toISOString(), - gitBranch: session.metadata?.gitBranch || "", - fileMtime: Date.now(), - isSidechain: false, - }], - }; - - const indexPath = join(projectDir, "sessions-index.json"); - await writeFile(indexPath, JSON.stringify(indexData, null, 2), "utf-8"); - - console.log(`[writeClaudeSession] Wrote index file: ${indexPath}`); - console.log(`[writeClaudeSession] ========== END DEBUG ==========`); -} - -export const sandboxImportRouter = router({ - /** - * Import a sandbox chat to a local worktree - */ - importSandboxChat: publicProcedure - .input( - z.object({ - sandboxId: z.string(), - remoteChatId: z.string(), - remoteSubChatId: z.string().optional(), - projectId: z.string(), - chatName: z.string().optional(), - }), - ) - .mutation(async ({ input }) => { - const db = getDatabase(); - const authManager = getAuthManager(); - const apiUrl = getBaseUrl(); - - console.log(`[OPEN-LOCALLY] Starting import: remoteChatId=${input.remoteChatId}, remoteSubChatId=${input.remoteSubChatId || "all"}, sandboxId=${input.sandboxId}`); - - // Verify auth - const token = await authManager.getValidToken(); - if (!token) { - throw new Error("Not authenticated"); - } - - // Verify project exists - const project = db - .select() - .from(projects) - .where(eq(projects.id, input.projectId)) - .get(); - - if (!project) { - throw new Error("Project not found"); - } - - // Fetch remote chat data (filter by subChatId if provided) - const chatExportUrl = input.remoteSubChatId - ? `${apiUrl}/api/agents/chat/${input.remoteChatId}/export?subChatId=${input.remoteSubChatId}` - : `${apiUrl}/api/agents/chat/${input.remoteChatId}/export`; - console.log(`[OPEN-LOCALLY] Fetching chat data from: ${chatExportUrl}`); - - const chatResponse = await fetch(chatExportUrl, { - method: "GET", - headers: { - "X-Desktop-Token": token, - }, - }); - - if (!chatResponse.ok) { - throw new Error(`Failed to fetch chat data: ${chatResponse.statusText}`); - } - - const remoteChatData = remoteChatSchema.parse(await chatResponse.json()); - console.log(`[OPEN-LOCALLY] Found ${remoteChatData.subChats.length} subchat(s) to import`); - - // Extract sessionId from the target subchat's messages BEFORE calling sandbox export - // This allows us to request only the specific session from the sandbox - let targetSessionId: string | undefined; - if (remoteChatData.subChats.length > 0) { - const targetSubChat = remoteChatData.subChats[0]; // First one (only one if filtered by subChatId) - const messagesArray = targetSubChat.messages || []; - const lastAssistant = [...messagesArray].reverse().find( - (m: any) => m.role === "assistant" - ); - targetSessionId = lastAssistant?.metadata?.sessionId; - console.log(`[OPEN-LOCALLY] Target sessionId from subchat messages: ${targetSessionId || "none"}`); - } - - // Create worktree for the chat - const worktreeResult = await createWorktreeForChat( - project.path, - input.projectId, - `imported-${Date.now()}`, // Unique ID for worktree directory - ); - - if (!worktreeResult.success || !worktreeResult.worktreePath) { - throw new Error(worktreeResult.error || "Failed to create worktree"); - } - - // Import sandbox git state to worktree (pass sessionId to get only that session) - const importResult = await importSandboxToWorktree( - worktreeResult.worktreePath, - apiUrl, - input.sandboxId, - token, - false, // fullExport = false - targetSessionId, // sessionId to filter - ); - console.log(`[OPEN-LOCALLY] Received ${importResult.claudeSessions?.length || 0} Claude session(s) from sandbox`); - - if (!importResult.success) { - console.warn( - `[sandbox-import] Git state import failed: ${importResult.error}`, - ); - // Continue anyway - chat history is still valuable - } - - // Create local chat record - const chat = db - .insert(chats) - .values({ - name: input.chatName || remoteChatData.name || "Imported Chat", - projectId: input.projectId, - worktreePath: worktreeResult.worktreePath, - branch: worktreeResult.branch, - baseBranch: worktreeResult.baseBranch, - }) - .returning() - .get(); - - // Import sub-chats with messages and Claude sessions - const claudeSessions = importResult.claudeSessions || []; - console.log(`[sandbox-import] Available Claude sessions: ${claudeSessions.length}`); - - for (const remoteSubChat of remoteChatData.subChats) { - const messagesArray = remoteSubChat.messages || []; - - // Find sessionId from last assistant message BEFORE creating subChat - const lastAssistant = [...messagesArray].reverse().find( - (m: any) => m.role === "assistant" - ); - const messageSessionId = lastAssistant?.metadata?.sessionId; - - // Check if we have a matching Claude session - const matchingSession = messageSessionId && claudeSessions.length > 0 - ? claudeSessions.find(s => s.sessionId === messageSessionId) - : undefined; - - // Create subChat with sessionId if we have a matching session - const createdSubChat = db.insert(subChats) - .values({ - chatId: chat.id, - name: remoteSubChat.name, - mode: remoteSubChat.mode === "plan" ? "plan" : "agent", - messages: JSON.stringify(messagesArray), - // Set sessionId if we have matching Claude session (enables resume) - ...(matchingSession && { sessionId: messageSessionId }), - }) - .returning() - .get(); - - // Write Claude session files if we have a matching one - if (matchingSession) { - try { - await writeClaudeSession( - createdSubChat.id, - worktreeResult.worktreePath!, - matchingSession, - ); - console.log(`[sandbox-import] Wrote Claude session for subChat ${createdSubChat.id} with sessionId ${messageSessionId}`); - } catch (sessionErr) { - console.error(`[sandbox-import] Failed to write Claude session:`, sessionErr); - } - } - } - - // If no sub-chats were imported, create an empty one - const importedSubChats = db - .select() - .from(subChats) - .where(eq(subChats.chatId, chat.id)) - .all(); - - if (importedSubChats.length === 0) { - db.insert(subChats) - .values({ - chatId: chat.id, - name: "Main", - mode: "agent", - messages: "[]", - }) - .run(); - } - - return { - success: true, - chatId: chat.id, - worktreePath: worktreeResult.worktreePath, - gitImportSuccess: importResult.success, - gitImportError: importResult.error, - }; - }), - - /** - * Get list of user's remote sandbox chats - */ - listRemoteSandboxChats: publicProcedure - .input( - z.object({ - teamId: z.string(), - }), - ) - .query(async ({ input }) => { - const authManager = getAuthManager(); - const apiUrl = getBaseUrl(); - - const token = await authManager.getValidToken(); - if (!token) { - throw new Error("Not authenticated"); - } - - // Call web API to get sandbox chats - // Note: This would need a corresponding endpoint on the web side - const response = await fetch( - `${apiUrl}/api/agents/chats?teamId=${input.teamId}`, - { - method: "GET", - headers: { - "X-Desktop-Token": token, - }, - }, - ); - - if (!response.ok) { - throw new Error(`Failed to fetch sandbox chats: ${response.statusText}`); - } - - return response.json(); - }), - - /** - * Clone a repository from sandbox and import the chat - * This is for cases when user doesn't have the repo locally - */ - cloneFromSandbox: publicProcedure - .input( - z.object({ - sandboxId: z.string(), - remoteChatId: z.string(), - remoteSubChatId: z.string().optional(), - chatName: z.string().optional(), - targetPath: z.string(), - }), - ) - .mutation(async ({ input }) => { - console.log(`[OPEN-LOCALLY] Starting clone process`); - console.log(`[OPEN-LOCALLY] Input:`, { - sandboxId: input.sandboxId, - remoteChatId: input.remoteChatId, - remoteSubChatId: input.remoteSubChatId || "all", - chatName: input.chatName, - targetPath: input.targetPath, - }); - - const db = getDatabase(); - const authManager = getAuthManager(); - const apiUrl = getBaseUrl(); - console.log(`[OPEN-LOCALLY] API URL: ${apiUrl}`); - - // Verify auth - console.log(`[OPEN-LOCALLY] Getting auth token...`); - const token = await authManager.getValidToken(); - if (!token) { - console.error(`[OPEN-LOCALLY] No auth token available`); - throw new Error("Not authenticated"); - } - console.log(`[OPEN-LOCALLY] Auth token obtained`); - - // Fetch remote chat data first (filter by subChatId if provided) - const chatExportUrl = input.remoteSubChatId - ? `${apiUrl}/api/agents/chat/${input.remoteChatId}/export?subChatId=${input.remoteSubChatId}` - : `${apiUrl}/api/agents/chat/${input.remoteChatId}/export`; - console.log(`[OPEN-LOCALLY] Fetching chat data from: ${chatExportUrl}`); - const chatResponse = await fetch(chatExportUrl, { - method: "GET", - headers: { - "X-Desktop-Token": token, - }, - }); - - if (!chatResponse.ok) { - console.error(`[OPEN-LOCALLY] Failed to fetch chat data: ${chatResponse.status} ${chatResponse.statusText}`); - throw new Error(`Failed to fetch chat data: ${chatResponse.statusText}`); - } - - const chatJson = await chatResponse.json(); - console.log(`[OPEN-LOCALLY] Remote chat data received:`, { - id: chatJson.id, - name: chatJson.name, - sandboxId: chatJson.sandboxId, - meta: chatJson.meta, - subChatsCount: chatJson.subChats?.length, - }); - - const remoteChatData = remoteChatSchema.parse(chatJson); - console.log(`[OPEN-LOCALLY] Found ${remoteChatData.subChats.length} subchat(s) to import`); - - // Extract sessionId from the target subchat's messages BEFORE calling sandbox export - let targetSessionId: string | undefined; - if (remoteChatData.subChats.length > 0) { - const targetSubChat = remoteChatData.subChats[0]; // First one (only one if filtered by subChatId) - const messagesArray = targetSubChat.messages || []; - const lastAssistant = [...messagesArray].reverse().find( - (m: any) => m.role === "assistant" - ); - targetSessionId = lastAssistant?.metadata?.sessionId; - console.log(`[OPEN-LOCALLY] Target sessionId from subchat messages: ${targetSessionId || "none"}`); - } - - // DEBUG: Fetch sandbox debug info to see what Claude sessions exist - try { - const debugUrl = `${apiUrl}/api/agents/sandbox/${input.sandboxId}/export/debug`; - console.log(`[OPEN-LOCALLY] Fetching debug info from: ${debugUrl}`); - const debugResponse = await fetch(debugUrl, { - method: "GET", - headers: { "X-Desktop-Token": token }, - }); - if (debugResponse.ok) { - const debugData = await debugResponse.json(); - console.log(`[OPEN-LOCALLY] ========== SANDBOX DEBUG INFO ==========`); - console.log(`[OPEN-LOCALLY] Paths:`, debugData.paths); - console.log(`[OPEN-LOCALLY] Checks:`, debugData.checks); - console.log(`[OPEN-LOCALLY] Files in .claude:`, debugData.files?.claudeHome); - console.log(`[OPEN-LOCALLY] Projects dirs:`, debugData.files?.projects); - console.log(`[OPEN-LOCALLY] Project dir contents:`, debugData.files?.projectDir); - console.log(`[OPEN-LOCALLY] Sessions index:`, debugData.sessionsIndex); - console.log(`[OPEN-LOCALLY] Session files exist:`, debugData.sessionFilesExist); - console.log(`[OPEN-LOCALLY] Errors:`, debugData.errors); - console.log(`[OPEN-LOCALLY] ========== END SANDBOX DEBUG ==========`); - } else { - console.log(`[OPEN-LOCALLY] Debug endpoint returned ${debugResponse.status}`); - } - } catch (debugErr) { - console.log(`[OPEN-LOCALLY] Debug fetch failed:`, debugErr); - } - - // Create target directory - console.log(`[OPEN-LOCALLY] Creating target directory: ${input.targetPath}`); - await mkdir(input.targetPath, { recursive: true }); - console.log(`[OPEN-LOCALLY] Target directory created`); - - // Initialize git repo - console.log(`[OPEN-LOCALLY] Initializing git repo...`); - await execAsync("git init", { cwd: input.targetPath }); - console.log(`[OPEN-LOCALLY] Git repo initialized`); - - // Import sandbox git state with FULL export (includes entire repo history) - // Pass sessionId to get only that specific session - console.log(`[OPEN-LOCALLY] Starting sandbox import with full export, sessionId: ${targetSessionId || "all"}`); - const importResult = await importSandboxToWorktree( - input.targetPath, - apiUrl, - input.sandboxId, - token, - true, // fullExport = true for cloning - targetSessionId, // sessionId to filter - ); - - console.log(`[OPEN-LOCALLY] Import result:`, { - success: importResult.success, - error: importResult.error, - claudeSessionsCount: importResult.claudeSessions?.length || 0, - }); - - if (!importResult.success) { - console.warn( - `[OPEN-LOCALLY] Git state import failed: ${importResult.error}`, - ); - // Continue anyway - we can still use the directory - } - - // Get git remote info (should have been set from the bundle) - console.log(`[OPEN-LOCALLY] Getting git remote info...`); - const gitInfo = await getGitRemoteInfo(input.targetPath); - console.log(`[OPEN-LOCALLY] Git remote info:`, gitInfo); - - // Fallback: extract owner/repo from remote chat metadata if git remote wasn't set up - // This happens when E2B export doesn't include remoteUrl in the meta - let finalOwner = gitInfo.owner; - let finalRepo = gitInfo.repo; - let finalRemoteUrl = gitInfo.remoteUrl; - let finalProvider = gitInfo.provider; - - if (!finalOwner || !finalRepo) { - const repoFromMeta = remoteChatData.meta?.repository; - if (repoFromMeta) { - const [metaOwner, metaRepo] = repoFromMeta.split("/"); - if (metaOwner && metaRepo) { - console.log(`[OPEN-LOCALLY] Git remote missing, using meta.repository: ${repoFromMeta}`); - finalOwner = metaOwner; - finalRepo = metaRepo; - finalProvider = "github"; // Assume GitHub for now - finalRemoteUrl = `https://github.com/${metaOwner}/${metaRepo}`; - - // Actually set up the git remote so repo is properly configured - try { - await execAsync(`git remote add origin ${finalRemoteUrl}`, { cwd: input.targetPath }); - console.log(`[OPEN-LOCALLY] Added origin remote: ${finalRemoteUrl}`); - } catch (err) { - // Remote might already exist, try to update it - try { - await execAsync(`git remote set-url origin ${finalRemoteUrl}`, { cwd: input.targetPath }); - console.log(`[OPEN-LOCALLY] Updated origin remote: ${finalRemoteUrl}`); - } catch { - console.warn(`[OPEN-LOCALLY] Could not set origin remote`); - } - } - } - } - } - - console.log(`[OPEN-LOCALLY] Final git info: owner="${finalOwner}", repo="${finalRepo}"`); - - // Get the actual current branch from git - console.log(`[OPEN-LOCALLY] Getting current branch from git...`); - let actualBranch = remoteChatData.meta?.branch || "main"; // fallback - try { - const { stdout: currentBranch } = await execAsync("git rev-parse --abbrev-ref HEAD", { cwd: input.targetPath }); - actualBranch = currentBranch.trim(); - console.log(`[OPEN-LOCALLY] Actual git branch: ${actualBranch}`); - } catch (err) { - console.warn(`[OPEN-LOCALLY] Could not get current branch, using fallback: ${actualBranch}`, err); - } - - // Check if project already exists (from a previous failed attempt) - console.log(`[OPEN-LOCALLY] Checking for existing project at path: ${input.targetPath}`); - const existingProject = db - .select() - .from(projects) - .where(eq(projects.path, input.targetPath)) - .get(); - - console.log(`[OPEN-LOCALLY] Existing project:`, existingProject ? { id: existingProject.id, name: existingProject.name } : null); - - // Use existing project or create new one - const project = existingProject - ? db - .update(projects) - .set({ - updatedAt: new Date(), - gitRemoteUrl: finalRemoteUrl, - gitProvider: finalProvider, - gitOwner: finalOwner, - gitRepo: finalRepo, - }) - .where(eq(projects.id, existingProject.id)) - .returning() - .get()! - : db - .insert(projects) - .values({ - name: basename(input.targetPath), - path: input.targetPath, - gitRemoteUrl: finalRemoteUrl, - gitProvider: finalProvider, - gitOwner: finalOwner, - gitRepo: finalRepo, - }) - .returning() - .get(); - - console.log(`[OPEN-LOCALLY] Project created/updated:`, { id: project.id, name: project.name }); - - // Create chat record (using the project path directly, no separate worktree needed - // since this is a fresh clone) - console.log(`[OPEN-LOCALLY] Creating chat record with branch: ${actualBranch}`); - const chat = db - .insert(chats) - .values({ - name: input.chatName || remoteChatData.name || "Imported Chat", - projectId: project.id, - worktreePath: input.targetPath, - branch: actualBranch, - baseBranch: "main", - }) - .returning() - .get(); - - console.log(`[OPEN-LOCALLY] Chat created:`, { id: chat.id, name: chat.name }); - - // Import sub-chats with messages and Claude sessions - console.log(`[OPEN-LOCALLY] Importing ${remoteChatData.subChats.length} sub-chats...`); - const claudeSessions = importResult.claudeSessions || []; - console.log(`[OPEN-LOCALLY] Available Claude sessions: ${claudeSessions.length}`); - - for (const remoteSubChat of remoteChatData.subChats) { - const messagesArray = remoteSubChat.messages || []; - const messagesCount = Array.isArray(messagesArray) ? messagesArray.length : 0; - console.log(`[OPEN-LOCALLY] Importing sub-chat: ${remoteSubChat.name} (mode: ${remoteSubChat.mode}, messages: ${messagesCount})`); - console.log(`[OPEN-LOCALLY] Messages preview:`, JSON.stringify(messagesArray).slice(0, 500)); - - // Find sessionId from last assistant message BEFORE creating subChat - const lastAssistant = [...messagesArray].reverse().find( - (m: any) => m.role === "assistant" - ); - const messageSessionId = lastAssistant?.metadata?.sessionId; - - // Check if we have a matching Claude session - const matchingSession = messageSessionId && claudeSessions.length > 0 - ? claudeSessions.find(s => s.sessionId === messageSessionId) - : undefined; - - // Create subChat with sessionId if we have a matching session - const createdSubChat = db.insert(subChats) - .values({ - chatId: chat.id, - name: remoteSubChat.name, - mode: remoteSubChat.mode === "plan" ? "plan" : "agent", - messages: JSON.stringify(messagesArray), - // Set sessionId if we have matching Claude session (enables resume) - ...(matchingSession && { sessionId: messageSessionId }), - }) - .returning() - .get(); - - // Write Claude session files if we have a matching one - if (matchingSession) { - try { - await writeClaudeSession( - createdSubChat.id, - input.targetPath, - matchingSession, - ); - console.log(`[OPEN-LOCALLY] Wrote Claude session for subChat ${createdSubChat.id} with sessionId ${messageSessionId}`); - } catch (sessionErr) { - console.error(`[OPEN-LOCALLY] Failed to write Claude session:`, sessionErr); - } - } else if (messageSessionId) { - console.log(`[OPEN-LOCALLY] No matching Claude session found for sessionId: ${messageSessionId.slice(0, 8)}...`); - } else { - console.log(`[OPEN-LOCALLY] No sessionId in messages or no sessions exported`); - } - } - - // If no sub-chats were imported, create an empty one - const importedSubChats = db - .select() - .from(subChats) - .where(eq(subChats.chatId, chat.id)) - .all(); - - if (importedSubChats.length === 0) { - console.log(`[OPEN-LOCALLY] No sub-chats imported, creating default`); - db.insert(subChats) - .values({ - chatId: chat.id, - name: "Main", - mode: "agent", - messages: "[]", - }) - .run(); - } - - console.log(`[OPEN-LOCALLY] Clone completed successfully!`); - console.log(`[OPEN-LOCALLY] Final result:`, { - projectId: project.id, - chatId: chat.id, - gitImportSuccess: importResult.success, - gitImportError: importResult.error, - }); - - return { - success: true, - projectId: project.id, - chatId: chat.id, - gitImportSuccess: importResult.success, - gitImportError: importResult.error, - }; - }), -}); diff --git a/src/main/lib/trpc/routers/skills.ts b/src/main/lib/trpc/routers/skills.ts index bddc1688..f46b8853 100644 --- a/src/main/lib/trpc/routers/skills.ts +++ b/src/main/lib/trpc/routers/skills.ts @@ -4,32 +4,27 @@ import * as fs from "fs/promises" import * as path from "path" import * as os from "os" import matter from "gray-matter" -import { discoverInstalledPlugins, getPluginComponentPaths } from "../../plugins" -import { getEnabledPlugins } from "./claude-settings" -export interface FileSkill { +interface FileSkill { name: string description: string - source: "user" | "project" | "plugin" - pluginName?: string + source: "user" | "project" path: string - content: string } /** * Parse SKILL.md frontmatter to extract name and description */ -function parseSkillMd(rawContent: string): { name?: string; description?: string; content: string } { +function parseSkillMd(content: string): { name?: string; description?: string } { try { - const { data, content } = matter(rawContent) + const { data } = matter(content) return { name: typeof data.name === "string" ? data.name : undefined, description: typeof data.description === "string" ? data.description : undefined, - content: content.trim(), } } catch (err) { console.error("[skills] Failed to parse frontmatter:", err) - return { content: rawContent.trim() } + return {} } } @@ -38,8 +33,7 @@ function parseSkillMd(rawContent: string): { name?: string; description?: string */ async function scanSkillsDirectory( dir: string, - source: "user" | "project" | "plugin", - basePath?: string, // For project skills, the cwd to make paths relative to + source: "user" | "project", ): Promise { const skills: FileSkill[] = [] @@ -54,19 +48,7 @@ async function scanSkillsDirectory( const entries = await fs.readdir(dir, { withFileTypes: true }) for (const entry of entries) { - // Check if entry is a directory or a symlink pointing to a directory - let isDir = entry.isDirectory() - if (!isDir && entry.isSymbolicLink()) { - try { - const targetPath = path.join(dir, entry.name) - const stat = await fs.stat(targetPath) // stat() follows symlinks - isDir = stat.isDirectory() - } catch { - // Symlink target doesn't exist or is inaccessible - skip it - continue - } - } - if (!isDir) continue + if (!entry.isDirectory()) continue // Validate entry name for security (prevent path traversal) if (entry.name.includes("..") || entry.name.includes("/") || entry.name.includes("\\")) { @@ -81,24 +63,11 @@ async function scanSkillsDirectory( const content = await fs.readFile(skillMdPath, "utf-8") const parsed = parseSkillMd(content) - // For project skills, show relative path; for user skills, show ~/.claude/... path - let displayPath: string - if (source === "project" && basePath) { - displayPath = path.relative(basePath, skillMdPath) - } else { - // For user skills, show ~/.claude/skills/... format - const homeDir = os.homedir() - displayPath = skillMdPath.startsWith(homeDir) - ? "~" + skillMdPath.slice(homeDir.length) - : skillMdPath - } - skills.push({ name: parsed.name || entry.name, description: parsed.description || "", source, - path: displayPath, - content: parsed.content, + path: skillMdPath, }) } catch (err) { // Skill directory doesn't have SKILL.md or read failed - skip it @@ -127,61 +96,18 @@ const listSkillsProcedure = publicProcedure let projectSkillsPromise = Promise.resolve([]) if (input?.cwd) { const projectSkillsDir = path.join(input.cwd, ".claude", "skills") - projectSkillsPromise = scanSkillsDirectory(projectSkillsDir, "project", input.cwd) + projectSkillsPromise = scanSkillsDirectory(projectSkillsDir, "project") } - // Discover plugin skills - const [enabledPluginSources, installedPlugins] = await Promise.all([ - getEnabledPlugins(), - discoverInstalledPlugins(), + // Scan both directories in parallel + const [userSkills, projectSkills] = await Promise.all([ + userSkillsPromise, + projectSkillsPromise, ]) - const enabledPlugins = installedPlugins.filter( - (p) => enabledPluginSources.includes(p.source), - ) - const pluginSkillsPromises = enabledPlugins.map(async (plugin) => { - const paths = getPluginComponentPaths(plugin) - try { - const skills = await scanSkillsDirectory(paths.skills, "plugin") - return skills.map((skill) => ({ ...skill, pluginName: plugin.source })) - } catch { - return [] - } - }) - - // Scan all directories in parallel - const [userSkills, projectSkills, ...pluginSkillsArrays] = - await Promise.all([ - userSkillsPromise, - projectSkillsPromise, - ...pluginSkillsPromises, - ]) - const pluginSkills = pluginSkillsArrays.flat() - return [...projectSkills, ...userSkills, ...pluginSkills] + return [...projectSkills, ...userSkills] }) -/** - * Generate SKILL.md content from name, description, and body - */ -function generateSkillMd(skill: { name: string; description: string; content: string }): string { - const frontmatter: string[] = [] - frontmatter.push(`name: ${skill.name}`) - if (skill.description) { - frontmatter.push(`description: ${skill.description}`) - } - return `---\n${frontmatter.join("\n")}\n---\n\n${skill.content}` -} - -/** - * Resolve the absolute filesystem path of a skill given its display path - */ -function resolveSkillPath(displayPath: string): string { - if (displayPath.startsWith("~")) { - return path.join(os.homedir(), displayPath.slice(1)) - } - return displayPath -} - export const skillsRouter = router({ /** * List all skills from filesystem @@ -194,96 +120,4 @@ export const skillsRouter = router({ * Alias for list - used by @ mention */ listEnabled: listSkillsProcedure, - - /** - * Create a new skill - */ - create: publicProcedure - .input( - z.object({ - name: z.string(), - description: z.string(), - content: z.string(), - source: z.enum(["user", "project"]), - cwd: z.string().optional(), - }) - ) - .mutation(async ({ input }) => { - const safeName = input.name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") - if (!safeName) { - throw new Error("Skill name must contain at least one alphanumeric character") - } - - let targetDir: string - if (input.source === "project") { - if (!input.cwd) { - throw new Error("Project path (cwd) required for project skills") - } - targetDir = path.join(input.cwd, ".claude", "skills") - } else { - targetDir = path.join(os.homedir(), ".claude", "skills") - } - - const skillDir = path.join(targetDir, safeName) - const skillMdPath = path.join(skillDir, "SKILL.md") - - // Check if already exists - try { - await fs.access(skillMdPath) - throw new Error(`Skill "${safeName}" already exists`) - } catch (err) { - if ((err as NodeJS.ErrnoException).code !== "ENOENT") { - throw err - } - } - - // Create directory and write SKILL.md - await fs.mkdir(skillDir, { recursive: true }) - - const fileContent = generateSkillMd({ - name: safeName, - description: input.description, - content: input.content, - }) - - await fs.writeFile(skillMdPath, fileContent, "utf-8") - - return { - name: safeName, - path: skillMdPath, - source: input.source, - } - }), - - /** - * Update a skill's SKILL.md content - */ - update: publicProcedure - .input( - z.object({ - path: z.string(), - name: z.string(), - description: z.string(), - content: z.string(), - cwd: z.string().optional(), - }) - ) - .mutation(async ({ input }) => { - const absolutePath = input.cwd && !input.path.startsWith("~") && !input.path.startsWith("/") - ? path.join(input.cwd, input.path) - : resolveSkillPath(input.path) - - // Verify file exists before writing - await fs.access(absolutePath) - - const fileContent = generateSkillMd({ - name: input.name, - description: input.description, - content: input.content, - }) - - await fs.writeFile(absolutePath, fileContent, "utf-8") - - return { success: true } - }), }) diff --git a/src/main/lib/trpc/routers/voice.ts b/src/main/lib/trpc/routers/voice.ts deleted file mode 100644 index af2876aa..00000000 --- a/src/main/lib/trpc/routers/voice.ts +++ /dev/null @@ -1,470 +0,0 @@ -/** - * Voice TRPC router - * Provides voice-to-text transcription using OpenAI Whisper API - * - * For authenticated users (with subscription): uses 21st.dev backend - * For open-source users: requires OPENAI_API_KEY in environment - */ - -import { execSync } from "node:child_process" -import os from "node:os" -import { z } from "zod" -import { publicProcedure, router } from "../index" -import { getApiUrl } from "../../config" -import { getAuthManager } from "../../../auth-manager" - -// Max audio size: 25MB (Whisper API limit) -const MAX_AUDIO_SIZE = 25 * 1024 * 1024 - -// API request timeout: 30 seconds -const API_TIMEOUT_MS = 30000 - -/** - * Clean up transcribed text - * - Remove leading/trailing whitespace - * - Collapse multiple spaces/newlines into single space - * - Remove any weird unicode whitespace characters - * - Remove zero-width characters and other invisible unicode - */ -function cleanTranscribedText(text: string): string { - return ( - text - // Remove zero-width and invisible characters - .replace(/[\u200B-\u200D\u2060\uFEFF\u00AD]/g, "") - // Normalize unicode whitespace to regular space - .replace(/[\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]/g, " ") - // Replace all types of newlines and line breaks with space - .replace(/[\r\n\u2028\u2029]+/g, " ") - // Replace tabs with space - .replace(/\t+/g, " ") - // Collapse multiple spaces into one - .replace(/ +/g, " ") - // Trim leading/trailing whitespace - .trim() - ) -} - -// Cache for OpenAI API key -let cachedOpenAIKey: string | null | undefined = undefined - -// User-configured OpenAI API key (from settings, set via IPC) -let userConfiguredOpenAIKey: string | null = null - -/** - * Set OpenAI API key from user settings - * Called from renderer via tRPC - */ -export function setUserOpenAIKey(key: string | null): void { - userConfiguredOpenAIKey = key?.trim() || null - // Clear env cache so next call re-evaluates - cachedOpenAIKey = undefined -} - -// Cache for user plan (to avoid repeated API calls) -let cachedUserPlan: { plan: string; status: string | null; fetchedAt: number } | null = null -const PLAN_CACHE_TTL_MS = 5 * 60 * 1000 // 5 minutes - -/** - * Fetch and cache user's subscription plan - */ -async function getUserPlan(): Promise<{ plan: string; status: string | null } | null> { - const authManager = getAuthManager() - if (!authManager?.isAuthenticated()) { - return null - } - - // Return cached plan if still fresh - if (cachedUserPlan && Date.now() - cachedUserPlan.fetchedAt < PLAN_CACHE_TTL_MS) { - return { plan: cachedUserPlan.plan, status: cachedUserPlan.status } - } - - try { - const planData = await authManager.fetchUserPlan() - if (planData) { - cachedUserPlan = { - plan: planData.plan, - status: planData.status, - fetchedAt: Date.now(), - } - return { plan: planData.plan, status: planData.status } - } - } catch (err) { - console.error("[Voice] Failed to fetch user plan:", err) - } - - return null -} - -/** - * Check if user has paid subscription (onecode_pro or onecode_max with active status) - */ -async function hasPaidSubscription(): Promise { - const planData = await getUserPlan() - if (!planData) return false - - const paidPlans = ["onecode_pro", "onecode_max"] - return paidPlans.includes(planData.plan) && planData.status === "active" -} - -/** - * Clear plan cache (for testing or when subscription changes) - */ -export function clearPlanCache(): void { - cachedUserPlan = null -} - -/** - * Get OpenAI API key from multiple sources (priority order): - * 1. User-configured key from settings - * 2. Vite env vars (.env.local files) - * 3. process.env - * 4. Shell environment - */ -function getOpenAIApiKey(): string | null { - // First check user-configured key (highest priority, not cached) - if (userConfiguredOpenAIKey && userConfiguredOpenAIKey.startsWith("sk-")) { - return userConfiguredOpenAIKey - } - - // Return cached value if already fetched from env - if (cachedOpenAIKey !== undefined) { - return cachedOpenAIKey - } - - // Check Vite env vars (works with .env.local files) - const viteKey = (import.meta.env as Record) - .MAIN_VITE_OPENAI_API_KEY - if (viteKey) { - cachedOpenAIKey = viteKey - console.log( - "[Voice] Using OPENAI_API_KEY from Vite env (MAIN_VITE_OPENAI_API_KEY)" - ) - return cachedOpenAIKey - } - - // Check process.env (works in dev mode) - if (process.env.OPENAI_API_KEY) { - cachedOpenAIKey = process.env.OPENAI_API_KEY - console.log("[Voice] Using OPENAI_API_KEY from process.env") - return cachedOpenAIKey - } - - // Try to get from shell environment (for production builds) - try { - const shell = process.env.SHELL || "/bin/zsh" - const result = execSync(`${shell} -ilc 'echo $OPENAI_API_KEY'`, { - encoding: "utf8", - timeout: 5000, - env: { - HOME: os.homedir(), - USER: os.userInfo().username, - SHELL: shell, - } as unknown as NodeJS.ProcessEnv, - }) - - const key = result.trim() - if (key && key !== "$OPENAI_API_KEY" && key.startsWith("sk-")) { - cachedOpenAIKey = key - console.log("[Voice] Using OPENAI_API_KEY from shell environment") - return cachedOpenAIKey - } - } catch (err) { - console.error("[Voice] Failed to read OPENAI_API_KEY from shell:", err) - } - - cachedOpenAIKey = null - return null -} - -/** - * Clear cached API key (for testing) - */ -export function clearOpenAIKeyCache(): void { - cachedOpenAIKey = undefined -} - -/** - * Transcribe audio using 21st.dev backend (for authenticated users) - */ -async function transcribeViaBackend( - audioBuffer: Buffer, - format: string, - language?: string -): Promise { - const authManager = getAuthManager() - if (!authManager) { - throw new Error("Auth manager not initialized") - } - const token = await authManager.getValidToken() - if (!token) { - throw new Error("Not authenticated") - } - - const apiUrl = getApiUrl() - - // Create form data for the API request - const formData = new FormData() - const uint8Array = new Uint8Array(audioBuffer) - const blob = new Blob([uint8Array], { type: `audio/${format}` }) - formData.append("file", blob, `audio.${format}`) - if (language) { - formData.append("language", language) - } - - // Create abort controller for timeout - const controller = new AbortController() - const timeoutId = setTimeout(() => controller.abort(), API_TIMEOUT_MS) - - try { - const response = await fetch(`${apiUrl}/api/voice/transcribe`, { - method: "POST", - headers: { - "X-Desktop-Token": token, - }, - body: formData, - signal: controller.signal, - }) - - if (!response.ok) { - const errorText = await response.text() - console.error("[Voice] Backend API error:", response.status, errorText) - - if (response.status === 401) { - throw new Error("Authentication expired. Please sign in again.") - } else if (response.status === 403) { - throw new Error("Voice transcription requires a paid subscription.") - } else if (response.status === 429) { - throw new Error("Rate limit exceeded. Please try again later.") - } else if (response.status >= 500) { - throw new Error("Service temporarily unavailable") - } - throw new Error(`Transcription failed (${response.status})`) - } - - const data = await response.json() - return cleanTranscribedText(data.text || "") - } catch (err) { - if (err instanceof Error && err.name === "AbortError") { - throw new Error("Transcription timed out. Please try again.") - } - throw err - } finally { - clearTimeout(timeoutId) - } -} - -/** - * Transcribe audio using OpenAI Whisper API directly (for open-source users) - */ -async function transcribeWithWhisper( - audioBuffer: Buffer, - format: string, - language?: string -): Promise { - const key = getOpenAIApiKey() - if (!key) { - throw new Error( - "OpenAI API key not configured. Set OPENAI_API_KEY environment variable." - ) - } - - // Check audio size limit - if (audioBuffer.length > MAX_AUDIO_SIZE) { - throw new Error( - `Audio too large (${Math.round(audioBuffer.length / 1024 / 1024)}MB). Maximum is 25MB.` - ) - } - - // Create form data for the API request - const formData = new FormData() - - // Convert buffer to blob (need to convert to Uint8Array for Blob constructor) - const uint8Array = new Uint8Array(audioBuffer) - const blob = new Blob([uint8Array], { type: `audio/${format}` }) - formData.append("file", blob, `audio.${format}`) - formData.append("model", "whisper-1") - formData.append("response_format", "text") - - if (language) { - formData.append("language", language) - } - - // Create abort controller for timeout - const controller = new AbortController() - const timeoutId = setTimeout(() => controller.abort(), API_TIMEOUT_MS) - - try { - const response = await fetch( - "https://api.openai.com/v1/audio/transcriptions", - { - method: "POST", - headers: { - Authorization: `Bearer ${key}`, - }, - body: formData, - signal: controller.signal, - } - ) - - if (!response.ok) { - const errorText = await response.text() - console.error("[Voice] Whisper API error:", response.status, errorText) - - // Provide user-friendly error messages - if (response.status === 401) { - throw new Error("Invalid OpenAI API key") - } else if (response.status === 429) { - throw new Error("Rate limit exceeded. Please try again later.") - } else if (response.status >= 500) { - throw new Error("OpenAI service temporarily unavailable") - } - throw new Error(`Transcription failed (${response.status})`) - } - - const text = await response.text() - return cleanTranscribedText(text) - } catch (err) { - if (err instanceof Error && err.name === "AbortError") { - throw new Error("Transcription timed out. Please try again.") - } - throw err - } finally { - clearTimeout(timeoutId) - } -} - -export const voiceRouter = router({ - /** - * Transcribe audio to text - * Priority: local OPENAI_API_KEY first, then backend for authenticated users - */ - transcribe: publicProcedure - .input( - z.object({ - audio: z.string(), // base64 encoded audio - format: z.enum(["webm", "wav", "mp3", "m4a", "ogg"]).default("webm"), - language: z.string().optional(), // ISO 639-1 code (e.g., "en", "ru") - }) - ) - .mutation(async ({ input }) => { - const audioBuffer = Buffer.from(input.audio, "base64") - - console.log( - `[Voice] Transcribing ${audioBuffer.length} bytes of ${input.format} audio` - ) - - // Check audio size limit - if (audioBuffer.length > MAX_AUDIO_SIZE) { - throw new Error( - `Audio too large (${Math.round(audioBuffer.length / 1024 / 1024)}MB). Maximum is 25MB.` - ) - } - - // If local OPENAI_API_KEY exists, use it directly (fastest, no network to backend) - const hasLocalKey = !!getOpenAIApiKey() - if (hasLocalKey) { - const text = await transcribeWithWhisper( - audioBuffer, - input.format, - input.language - ) - console.log(`[Voice] Local transcription result: "${text.slice(0, 100)}..."`) - return { text } - } - - // Otherwise, try backend if user is authenticated - const authManager = getAuthManager() - const isAuthenticated = authManager?.isAuthenticated() ?? false - if (isAuthenticated) { - const text = await transcribeViaBackend( - audioBuffer, - input.format, - input.language - ) - console.log( - `[Voice] Backend transcription result: "${text.slice(0, 100)}..."` - ) - return { text } - } - - // No local key and not authenticated - throw new Error( - "Voice input requires signing in or setting OPENAI_API_KEY environment variable" - ) - }), - - /** - * Check if voice transcription is available - * Available if: has local OPENAI_API_KEY OR user has paid subscription - */ - isAvailable: publicProcedure.query(async () => { - const hasLocalKey = !!getOpenAIApiKey() - - // Local API key always works - if (hasLocalKey) { - return { - available: true, - method: "local" as const, - reason: undefined, - } - } - - // Check if user has paid subscription - const hasPaid = await hasPaidSubscription() - if (hasPaid) { - return { - available: true, - method: "backend" as const, - reason: undefined, - } - } - - // Check if authenticated but free plan - const authManager = getAuthManager() - const isAuthenticated = authManager?.isAuthenticated() ?? false - - if (isAuthenticated) { - return { - available: false, - method: null, - reason: "Voice input requires a paid subscription or OpenAI API key", - } - } - - return { - available: false, - method: null, - reason: - "Add your OpenAI API key in Settings > Models, or sign in with a paid subscription", - } - }), - - /** - * Set OpenAI API key from user settings - * This allows users without a paid subscription to use their own API key - */ - setOpenAIKey: publicProcedure - .input(z.object({ key: z.string() })) - .mutation(({ input }) => { - const key = input.key.trim() - - // Validate key format if provided - if (key && !key.startsWith("sk-")) { - throw new Error("Invalid OpenAI API key format. Key should start with 'sk-'") - } - - setUserOpenAIKey(key || null) - - // Clear plan cache so isAvailable re-evaluates - clearPlanCache() - - return { success: true } - }), - - /** - * Check if user has configured an OpenAI API key - */ - hasOpenAIKey: publicProcedure.query(() => { - return { hasKey: !!getOpenAIApiKey() } - }), -}) diff --git a/src/main/lib/vscode-theme-scanner.ts b/src/main/lib/vscode-theme-scanner.ts deleted file mode 100644 index f2468e8f..00000000 --- a/src/main/lib/vscode-theme-scanner.ts +++ /dev/null @@ -1,295 +0,0 @@ -/** - * VS Code Theme Scanner - * - * Scans local VS Code extensions directories to discover installed themes. - * Supports VS Code, VS Code Insiders, Cursor, and Windsurf. - */ - -import * as fs from "fs/promises" -import * as path from "path" -import * as os from "os" -import { ipcMain } from "electron" -import { parse as parseJsonc } from "jsonc-parser" - -/** - * Source editor type - */ -export type EditorSource = "vscode" | "vscode-insiders" | "cursor" | "windsurf" - -/** - * Discovered theme metadata (lightweight, for listing) - */ -export interface DiscoveredTheme { - id: string - name: string - type: "light" | "dark" - extensionId: string - extensionName: string - path: string - source: EditorSource -} - -/** - * Full theme data (loaded on demand) - */ -export interface VSCodeThemeData { - id: string - name: string - type: "light" | "dark" - colors: Record - tokenColors?: any[] - semanticHighlighting?: boolean - semanticTokenColors?: Record - source: "imported" - path: string -} - -// No caching - always scan fresh to avoid issues - -/** - * Extension paths for different VS Code variants - */ -const EXTENSION_PATHS: { path: string; source: EditorSource }[] = [ - // VS Code - { path: path.join(os.homedir(), ".vscode", "extensions"), source: "vscode" }, - // VS Code Insiders - { path: path.join(os.homedir(), ".vscode-insiders", "extensions"), source: "vscode-insiders" }, - // Cursor - { path: path.join(os.homedir(), ".cursor", "extensions"), source: "cursor" }, - // Windsurf - { path: path.join(os.homedir(), ".windsurf", "extensions"), source: "windsurf" }, -] - -/** - * Check if a directory exists - */ -async function directoryExists(dirPath: string): Promise { - try { - const stat = await fs.stat(dirPath) - return stat.isDirectory() - } catch { - return false - } -} - -/** - * Detect theme type from colors - */ -function detectThemeType(colors: Record | undefined): "light" | "dark" { - if (!colors) return "dark" - - const bgColor = colors["editor.background"] || colors["editorPane.background"] || "#000000" - const hex = bgColor.replace(/^#/, "") - - // Handle shorthand hex - let r: number, g: number, b: number - if (hex.length === 3) { - r = parseInt(hex[0] + hex[0], 16) - g = parseInt(hex[1] + hex[1], 16) - b = parseInt(hex[2] + hex[2], 16) - } else if (hex.length >= 6) { - r = parseInt(hex.slice(0, 2), 16) - g = parseInt(hex.slice(2, 4), 16) - b = parseInt(hex.slice(4, 6), 16) - } else { - return "dark" - } - - // Calculate perceived brightness using ITU-R BT.709 coefficients - const brightness = r * 0.2126 + g * 0.7152 + b * 0.0722 - return brightness > 128 ? "light" : "dark" -} - -/** - * Map VS Code uiTheme to our theme type - */ -function mapUiTheme(uiTheme: string | undefined): "light" | "dark" { - if (!uiTheme) return "dark" - // VS Code uses: "vs" (light), "vs-dark" (dark), "hc-black" (high contrast dark), "hc-light" (high contrast light) - return uiTheme === "vs" || uiTheme === "hc-light" ? "light" : "dark" -} - -/** - * Scan a single extensions directory - */ -async function scanExtensionsDir(extensionsDir: string, source: EditorSource): Promise { - const themes: DiscoveredTheme[] = [] - - if (!(await directoryExists(extensionsDir))) { - return themes - } - - try { - // Always use execSync to get directory listing (fs.readdir has caching issues in Electron) - const { execSync } = require("child_process") - const lsOutput = execSync(`ls -1 "${extensionsDir}"`, { encoding: "utf-8" }) - const lsEntries = lsOutput.trim().split("\n").filter(Boolean) - - // Create Dirent-like objects from ls output - const entries_final = await Promise.all( - lsEntries.map(async (name) => { - const fullPath = path.join(extensionsDir, name) - try { - const stat = await fs.stat(fullPath) - return { - name, - isDirectory: () => stat.isDirectory(), - } - } catch { - return { name, isDirectory: () => false } - } - }) - ) - - for (const entry of entries_final) { - if (!entry.isDirectory()) continue - - const extDir = entry.name - const packageJsonPath = path.join(extensionsDir, extDir, "package.json") - - try { - const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8") - const manifest = JSON.parse(packageJsonContent) - - const themeContributions = manifest.contributes?.themes || [] - - for (const theme of themeContributions) { - if (!theme.path) continue - - const themePath = path.join(extensionsDir, extDir, theme.path) - // Verify theme file exists and read the actual theme name from the file - let actualThemeName: string | undefined - try { - const themeContent = await fs.readFile(themePath, "utf-8") - // Use proper JSONC parser (handles comments and trailing commas) - const themeData = parseJsonc(themeContent) - actualThemeName = themeData.name - } catch { - continue - } - - // Prefer: actual theme file name > label from package.json > id > file basename - const themeName = actualThemeName || theme.label || theme.id || path.basename(theme.path, ".json") - // Use file path basename in ID to ensure uniqueness - const fileBasename = path.basename(theme.path, ".json") - const themeId = `vscode-${extDir}-${fileBasename}`.replace(/[^a-zA-Z0-9-_]/g, "-") - - themes.push({ - id: themeId, - name: themeName, - type: mapUiTheme(theme.uiTheme), - extensionId: extDir, - extensionName: manifest.displayName || manifest.name || extDir, - path: themePath, - source, - }) - } - } catch { - // Skip extensions with invalid package.json - continue - } - } - } catch (error) { - console.error(`Error scanning extensions directory ${extensionsDir}:`, error) - } - - return themes -} - -/** - * Scan all VS Code extension directories for themes - */ -export async function scanVSCodeThemes(): Promise { - const allThemes: DiscoveredTheme[] = [] - const seenPaths = new Set() - - for (const { path: extensionsDir, source } of EXTENSION_PATHS) { - const themes = await scanExtensionsDir(extensionsDir, source) - for (const theme of themes) { - // Deduplicate by normalized theme path (same theme in different editors) - // Use the theme file's basename + extension ID as unique key - const uniqueKey = `${theme.extensionId}-${path.basename(theme.path)}` - if (!seenPaths.has(uniqueKey)) { - seenPaths.add(uniqueKey) - allThemes.push(theme) - } - } - } - - // Sort by extension name, then theme name - allThemes.sort((a, b) => { - const extCompare = a.extensionName.localeCompare(b.extensionName) - if (extCompare !== 0) return extCompare - return a.name.localeCompare(b.name) - }) - - return allThemes -} - -/** - * Load full theme data from a theme file path - */ -export async function loadThemeFromPath(themePath: string): Promise { - const content = await fs.readFile(themePath, "utf-8") - - // Use proper JSONC parser (handles comments and trailing commas) - const theme = parseJsonc(content) - - // Generate unique ID based on path and timestamp - const id = `imported-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` - - return { - id, - name: theme.name || path.basename(themePath, ".json"), - type: detectThemeType(theme.colors), - colors: theme.colors || {}, - tokenColors: theme.tokenColors, - semanticHighlighting: theme.semanticHighlighting, - semanticTokenColors: theme.semanticTokenColors, - source: "imported", - path: themePath, - } -} - -/** - * Register IPC handlers for theme scanning - */ -let ipcRegistered = false -export function registerThemeScannerIPC(): void { - if (ipcRegistered) { - return - } - ipcRegistered = true - - ipcMain.handle("vscode:scan-themes", async () => { - try { - const themes = await scanVSCodeThemes() - return themes - } catch (error) { - console.error("Error scanning VS Code themes:", error) - throw error - } - }) - - ipcMain.handle("vscode:load-theme", async (_, themePath: string) => { - try { - // Security: Validate path is within allowed directories - const normalizedPath = path.normalize(themePath) - const isAllowedPath = EXTENSION_PATHS.some(({ path: allowedDir }) => { - const normalizedAllowed = path.normalize(allowedDir) - // Ensure we check with path separator to avoid partial matches - return normalizedPath.startsWith(normalizedAllowed + path.sep) || - normalizedPath.startsWith(normalizedAllowed) - }) - - if (!isAllowedPath) { - throw new Error("Theme path is not within allowed directories") - } - - return await loadThemeFromPath(normalizedPath) - } catch (error) { - console.error("Error loading VS Code theme:", error) - throw error - } - }) -} diff --git a/src/main/lib/window.ts b/src/main/lib/window.ts deleted file mode 100644 index ca42b8bc..00000000 --- a/src/main/lib/window.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { app, BrowserWindow } from "electron"; - -export function bringToFront(win?: BrowserWindow | null) { - const w = win ?? BrowserWindow.getAllWindows().find(x => !x.isDestroyed()); - if (!w || w.isDestroyed()) return; - - // If you hide to tray / not visible, focus() alone won't show it - if (!w.isVisible()) w.show(); - - if (w.isMinimized()) w.restore(); - - // Helps on macOS (activates the app) - if (process.platform === "darwin") { - app.focus({ steal: true }); - } - - // Normal attempt - w.focus(); - - // Windows sometimes ignores focus; this "topmost blip" often works - if (process.platform === "win32") { - w.setAlwaysOnTop(true); - setTimeout(() => w.setAlwaysOnTop(false), 200); - } -} diff --git a/src/main/windows/main.ts b/src/main/windows/main.ts index 36c88af3..81d3bba8 100644 --- a/src/main/windows/main.ts +++ b/src/main/windows/main.ts @@ -14,22 +14,11 @@ import { createIPCHandler } from "trpc-electron/main" import { createAppRouter } from "../lib/trpc/routers" import { getAuthManager, handleAuthCode, getBaseUrl } from "../index" import { registerGitWatcherIPC } from "../lib/git/watcher" -import { registerThemeScannerIPC } from "../lib/vscode-theme-scanner" -import { windowManager } from "./window-manager" - -// Helper to get window from IPC event -function getWindowFromEvent( - event: Electron.IpcMainInvokeEvent, -): BrowserWindow | null { - const webContents = event.sender - const win = BrowserWindow.fromWebContents(webContents) - return win && !win.isDestroyed() ? win : null -} // Register IPC handlers for window operations (only once) let ipcHandlersRegistered = false -function registerIpcHandlers(): void { +function registerIpcHandlers(getWindow: () => BrowserWindow | null): void { if (ipcHandlersRegistered) return ipcHandlersRegistered = true @@ -67,8 +56,8 @@ function registerIpcHandlers(): void { }) // Note: Update checking is now handled by auto-updater module (lib/auto-updater.ts) - ipcMain.handle("app:set-badge", (event, count: number | null) => { - const win = getWindowFromEvent(event) + ipcMain.handle("app:set-badge", (_event, count: number | null) => { + const win = getWindow() if (process.platform === "darwin") { app.dock.setBadge(count ? String(count) : "") } else if (process.platform === "win32" && win) { @@ -83,8 +72,8 @@ function registerIpcHandlers(): void { }) // Windows: Badge overlay icon - ipcMain.handle("app:set-badge-icon", (event, imageData: string | null) => { - const win = getWindowFromEvent(event) + ipcMain.handle("app:set-badge-icon", (_event, imageData: string | null) => { + const win = getWindow() if (process.platform === "win32" && win) { if (imageData) { const image = nativeImage.createFromDataURL(imageData) @@ -97,7 +86,7 @@ function registerIpcHandlers(): void { ipcMain.handle( "app:show-notification", - (event, options: { title: string; body: string }) => { + (_event, options: { title: string; body: string }) => { try { const { Notification } = require("electron") const iconPath = join(__dirname, "../../../build/icon.ico") @@ -113,7 +102,7 @@ function registerIpcHandlers(): void { notification.show() notification.on("click", () => { - const win = getWindowFromEvent(event) + const win = getWindow() if (win) { if (win.isMinimized()) win.restore() win.focus() @@ -128,39 +117,37 @@ function registerIpcHandlers(): void { // API base URL for fetch requests ipcMain.handle("app:get-api-base-url", () => getBaseUrl()) - // Window controls - use event.sender to identify window - ipcMain.handle("window:minimize", (event) => { - getWindowFromEvent(event)?.minimize() - }) - ipcMain.handle("window:maximize", (event) => { - const win = getWindowFromEvent(event) + // Window controls + ipcMain.handle("window:minimize", () => getWindow()?.minimize()) + ipcMain.handle("window:maximize", () => { + const win = getWindow() if (win?.isMaximized()) { win.unmaximize() } else { win?.maximize() } }) - ipcMain.handle("window:close", (event) => { - getWindowFromEvent(event)?.close() - }) - ipcMain.handle("window:is-maximized", (event) => { - return getWindowFromEvent(event)?.isMaximized() ?? false - }) - ipcMain.handle("window:toggle-fullscreen", (event) => { - const win = getWindowFromEvent(event) + ipcMain.handle("window:close", () => getWindow()?.close()) + ipcMain.handle( + "window:is-maximized", + () => getWindow()?.isMaximized() ?? false, + ) + ipcMain.handle("window:toggle-fullscreen", () => { + const win = getWindow() if (win) { win.setFullScreen(!win.isFullScreen()) } }) - ipcMain.handle("window:is-fullscreen", (event) => { - return getWindowFromEvent(event)?.isFullScreen() ?? false - }) + ipcMain.handle( + "window:is-fullscreen", + () => getWindow()?.isFullScreen() ?? false, + ) // Traffic light visibility control (for hybrid native/custom approach) ipcMain.handle( "window:set-traffic-light-visibility", - (event, visible: boolean) => { - const win = getWindowFromEvent(event) + (_event, visible: boolean) => { + const win = getWindow() if (win && process.platform === "darwin") { // In fullscreen, always show native traffic lights (don't let React hide them) if (win.isFullScreen()) { @@ -173,61 +160,36 @@ function registerIpcHandlers(): void { ) // Zoom controls - ipcMain.handle("window:zoom-in", (event) => { - const win = getWindowFromEvent(event) + ipcMain.handle("window:zoom-in", () => { + const win = getWindow() if (win) { const zoom = win.webContents.getZoomFactor() win.webContents.setZoomFactor(Math.min(zoom + 0.1, 3)) } }) - ipcMain.handle("window:zoom-out", (event) => { - const win = getWindowFromEvent(event) + ipcMain.handle("window:zoom-out", () => { + const win = getWindow() if (win) { const zoom = win.webContents.getZoomFactor() win.webContents.setZoomFactor(Math.max(zoom - 0.1, 0.5)) } }) - ipcMain.handle("window:zoom-reset", (event) => { - getWindowFromEvent(event)?.webContents.setZoomFactor(1) - }) - ipcMain.handle("window:get-zoom", (event) => { - return getWindowFromEvent(event)?.webContents.getZoomFactor() ?? 1 - }) - - // New window - optionally open with specific chat/subchat - ipcMain.handle("window:new", (_event, options?: { chatId?: string; subChatId?: string }) => { - createWindow(options) + ipcMain.handle("window:zoom-reset", () => { + getWindow()?.webContents.setZoomFactor(1) }) + ipcMain.handle( + "window:get-zoom", + () => getWindow()?.webContents.getZoomFactor() ?? 1, + ) - // Set window title - ipcMain.handle("window:set-title", (event, title: string) => { - const win = getWindowFromEvent(event) + // DevTools + ipcMain.handle("window:toggle-devtools", () => { + const win = getWindow() if (win) { - // Show just the title, or default app name if empty - win.setTitle(title || "1Code") - } - }) - - // DevTools - only allowed in dev mode or when unlocked - ipcMain.handle("window:toggle-devtools", (event) => { - const win = getWindowFromEvent(event) - // Check if devtools are unlocked (or in dev mode) - const isUnlocked = !app.isPackaged || (global as any).__devToolsUnlocked - if (win && isUnlocked) { win.webContents.toggleDevTools() } }) - // Unlock DevTools (hidden feature - 5 clicks on Beta tab) - ipcMain.handle("window:unlock-devtools", () => { - // Mark as unlocked locally for IPC check - ;(global as any).__devToolsUnlocked = true - // Call the global function to rebuild menu - if ((global as any).__unlockDevTools) { - ;(global as any).__unlockDevTools() - } - }) - // Analytics ipcMain.handle("analytics:set-opt-out", async (_event, optedOut: boolean) => { const { setOptOut } = await import("../lib/analytics") @@ -280,25 +242,18 @@ function registerIpcHandlers(): void { } catch (err) { console.error("[Auth] Failed to clear cookie:", err) } - // Show login page in all windows - for (const win of windowManager.getAll()) { - showLoginPageInWindow(win) - } + showLoginPage() }) ipcMain.handle("auth:start-flow", (event) => { if (!validateSender(event)) return - const win = getWindowFromEvent(event) - getAuthManager().startAuthFlow(win) + getAuthManager().startAuthFlow(getWindow()) }) ipcMain.handle("auth:submit-code", async (event, code: string) => { if (!validateSender(event)) return if (!code || typeof code !== "string") { - getWindowFromEvent(event)?.webContents.send( - "auth:error", - "Invalid authorization code", - ) + getWindow()?.webContents.send("auth:error", "Invalid authorization code") return } await handleAuthCode(code) @@ -314,189 +269,41 @@ function registerIpcHandlers(): void { } }) - ipcMain.handle("auth:get-token", async (event) => { - if (!validateSender(event)) return null - return getAuthManager().getValidToken() - }) - - // Signed fetch - proxies requests through main process (no CORS) - ipcMain.handle( - "api:signed-fetch", - async ( - event, - url: string, - options?: { method?: string; body?: string; headers?: Record }, - ) => { - console.log("[SignedFetch] IPC handler called with URL:", url) - if (!validateSender(event)) { - console.log("[SignedFetch] Unauthorized sender") - return { ok: false, status: 403, data: null, error: "Unauthorized sender" } - } - console.log("[SignedFetch] Sender validated OK") - - const token = await getAuthManager().getValidToken() - console.log("[SignedFetch] Token:", token ? "present" : "missing", "URL:", url) - if (!token) { - return { ok: false, status: 401, data: null, error: "Not authenticated" } - } - - try { - const response = await fetch(url, { - method: options?.method || "GET", - body: options?.body, - headers: { - ...options?.headers, - "X-Desktop-Token": token, - "Content-Type": "application/json", - }, - }) - - const data = await response.json().catch(() => null) - console.log("[SignedFetch] Response:", response.status, response.ok ? "OK" : "FAILED") - - return { - ok: response.ok, - status: response.status, - data, - error: response.ok ? null : `Request failed: ${response.status}`, - } - } catch (error) { - console.log("[SignedFetch] Error:", error) - return { - ok: false, - status: 0, - data: null, - error: error instanceof Error ? error.message : "Network error", - } - } - }, - ) - - // Streaming fetch - for SSE responses (chat streaming) - // Uses a unique stream ID to match chunks with the right request - ipcMain.handle( - "api:stream-fetch", - async ( - event, - streamId: string, - url: string, - options?: { method?: string; body?: string; headers?: Record }, - ) => { - console.log("[StreamFetch] Starting stream:", streamId, url) - if (!validateSender(event)) { - console.log("[StreamFetch] Unauthorized sender") - return { ok: false, status: 403, error: "Unauthorized sender" } - } - - const token = await getAuthManager().getValidToken() - if (!token) { - return { ok: false, status: 401, error: "Not authenticated" } - } - - try { - const response = await fetch(url, { - method: options?.method || "POST", - body: options?.body, - headers: { - ...options?.headers, - "X-Desktop-Token": token, - "Content-Type": "application/json", - }, - }) - - console.log("[StreamFetch] Response:", response.status, response.ok) - - if (!response.ok) { - const errorText = await response.text().catch(() => "Unknown error") - return { ok: false, status: response.status, error: errorText } - } - - // Stream the response body back to renderer - const reader = response.body?.getReader() - if (!reader) { - return { ok: false, status: 500, error: "No response body" } - } - - // Send chunks asynchronously - ;(async () => { - try { - while (true) { - const { done, value } = await reader.read() - if (done) { - event.sender.send(`stream:${streamId}:done`) - break - } - // Send chunk to renderer - event.sender.send(`stream:${streamId}:chunk`, value) - } - } catch (err) { - console.error("[StreamFetch] Stream error:", err) - event.sender.send(`stream:${streamId}:error`, err instanceof Error ? err.message : "Stream error") - } - })() - - return { ok: true, status: response.status } - } catch (error) { - console.error("[StreamFetch] Fetch error:", error) - return { - ok: false, - status: 0, - error: error instanceof Error ? error.message : "Network error", - } - } - }, - ) - // Register git watcher IPC handlers - registerGitWatcherIPC() - - // Register VS Code theme scanner IPC handlers - registerThemeScannerIPC() + registerGitWatcherIPC(getWindow) } +// Current window reference +let currentWindow: BrowserWindow | null = null + /** - * Show login page in a specific window + * Show login page */ -function showLoginPageInWindow(window: BrowserWindow): void { - console.log("[Main] Showing login page in window", window.id) +export function showLoginPage(): void { + if (!currentWindow) return + console.log("[Main] Showing login page") // In dev mode, login.html is in src/renderer, not out/renderer if (process.env.ELECTRON_RENDERER_URL) { // Dev mode: load from source directory const loginPath = join(app.getAppPath(), "src/renderer/login.html") console.log("[Main] Loading login from:", loginPath) - window.loadFile(loginPath) + currentWindow.loadFile(loginPath) } else { // Production: load from built output - window.loadFile(join(__dirname, "../renderer/login.html")) + currentWindow.loadFile(join(__dirname, "../renderer/login.html")) } } -/** - * Show login page in the focused window (or first window) - */ -export function showLoginPage(): void { - const win = windowManager.getFocused() || windowManager.getAll()[0] - if (!win) return - showLoginPageInWindow(win) -} - // Singleton IPC handler (prevents duplicate handlers on macOS window recreation) let ipcHandler: ReturnType | null = null /** - * Get the focused window reference + * Get the current window reference * Used by tRPC procedures that need window access */ export function getWindow(): BrowserWindow | null { - return windowManager.getFocused() -} - -/** - * Get all windows - */ -export function getAllWindows(): BrowserWindow[] { - return windowManager.getAll() + return currentWindow } /** @@ -519,14 +326,11 @@ function getUseNativeFramePreference(): boolean { } /** - * Create a new application window - * @param options Optional settings for the new window - * @param options.chatId Open this chat in the new window - * @param options.subChatId Open this sub-chat in the new window + * Create the main application window */ -export function createWindow(options?: { chatId?: string; subChatId?: string }): BrowserWindow { - // Register IPC handlers before creating first window - registerIpcHandlers() +export function createMainWindow(): BrowserWindow { + // Register IPC handlers before creating window + registerIpcHandlers(getWindow) // Read Windows frame preference const useNativeFrame = getUseNativeFramePreference() @@ -540,7 +344,8 @@ export function createWindow(options?: { chatId?: string; subChatId?: string }): title: "1Code", backgroundColor: nativeTheme.shouldUseDarkColors ? "#09090b" : "#ffffff", // hiddenInset shows native traffic lights inset in the window - // hiddenInset hides the native title bar but keeps traffic lights visible + // Start with traffic lights off-screen (custom ones shown in normal mode) + // Native lights will be moved on-screen in fullscreen mode titleBarStyle: process.platform === "darwin" ? "hiddenInset" : "default", trafficLightPosition: process.platform === "darwin" ? { x: 15, y: 12 } : undefined, @@ -559,11 +364,8 @@ export function createWindow(options?: { chatId?: string; subChatId?: string }): }, }) - // Register window with manager and get stable ID for localStorage namespacing - const stableWindowId = windowManager.register(window) - console.log( - `[Main] Created window ${window.id} with stable ID "${stableWindowId}" (total: ${windowManager.count()})`, - ) + // Update current window reference + currentWindow = window // Setup tRPC IPC handler (singleton pattern) if (ipcHandler) { @@ -582,8 +384,8 @@ export function createWindow(options?: { chatId?: string; subChatId?: string }): // Show window when ready window.on("ready-to-show", () => { - console.log("[Main] Window", window.id, "ready to show") - // Always show native macOS traffic lights + console.log("[Main] Window ready to show") + // Ensure native traffic lights are visible by default (login page, loading states) if (process.platform === "darwin") { window.setWindowButtonVisibility(true) } @@ -599,7 +401,7 @@ export function createWindow(options?: { chatId?: string; subChatId?: string }): window.webContents.send("window:fullscreen-change", true) }) window.on("leave-full-screen", () => { - // Show native traffic lights when exiting fullscreen + // Show native traffic lights when exiting fullscreen (TrafficLights component will manage after mount) if (process.platform === "darwin") { window.setWindowButtonVisibility(true) } @@ -614,16 +416,6 @@ export function createWindow(options?: { chatId?: string; subChatId?: string }): window.webContents.send("window:focus-change", false) }) - // Disable Cmd+R / Ctrl+R to prevent accidental page refresh - // Users can still use Cmd+Shift+R / Ctrl+Shift+R for intentional reloads - window.webContents.on("before-input-event", (event, input) => { - const isMac = process.platform === "darwin" - const modifierKey = isMac ? input.meta : input.control - if (modifierKey && input.key.toLowerCase() === "r" && !input.shift) { - event.preventDefault() - } - }) - // Handle external links window.webContents.setWindowOpenHandler(({ url }) => { shell.openExternal(url) @@ -632,8 +424,7 @@ export function createWindow(options?: { chatId?: string; subChatId?: string }): // Handle window close window.on("closed", () => { - console.log(`[Main] Window ${window.id} closed`) - // windowManager handles cleanup via 'closed' event listener + currentWindow = null }) // Load the renderer - check auth first @@ -650,33 +441,11 @@ export function createWindow(options?: { chatId?: string; subChatId?: string }): if (isAuth) { console.log("[Main] ✓ User authenticated, loading app") - // Get stable window ID from manager (assigned during register) - // "main" for first window, "window-2", "window-3", etc. for additional windows - const windowId = windowManager.getStableId(window) - - // Build URL params including optional chatId/subChatId - const buildParams = (params: URLSearchParams) => { - params.set("windowId", windowId) - if (options?.chatId) params.set("chatId", options.chatId) - if (options?.subChatId) params.set("subChatId", options.subChatId) - } - if (devServerUrl) { - // Pass params via query for dev mode - const url = new URL(devServerUrl) - buildParams(url.searchParams) - window.loadURL(url.toString()) - // Only open devtools for first window in development - if (!app.isPackaged && windowId === "main") { - window.webContents.openDevTools() - } + window.loadURL(devServerUrl) + window.webContents.openDevTools() } else { - // Pass params via hash for production (file:// URLs) - const hashParams = new URLSearchParams() - buildParams(hashParams) - window.loadFile(join(__dirname, "../renderer/index.html"), { - hash: hashParams.toString(), - }) + window.loadFile(join(__dirname, "../renderer/index.html")) } } else { console.log("[Main] ✗ Not authenticated, showing login page") @@ -689,9 +458,9 @@ export function createWindow(options?: { chatId?: string; subChatId?: string }): } } - // Ensure native traffic lights are visible after page load + // Ensure traffic lights are visible after page load (covers reload/Cmd+R case) window.webContents.on("did-finish-load", () => { - console.log("[Main] Page finished loading in window", window.id) + console.log("[Main] Page finished loading") if (process.platform === "darwin") { window.setWindowButtonVisibility(true) } @@ -699,22 +468,9 @@ export function createWindow(options?: { chatId?: string; subChatId?: string }): window.webContents.on( "did-fail-load", (_event, errorCode, errorDescription) => { - console.error( - "[Main] Page failed to load in window", - window.id, - ":", - errorCode, - errorDescription, - ) + console.error("[Main] Page failed to load:", errorCode, errorDescription) }, ) return window } - -/** - * Create the main application window (alias for createWindow for backwards compatibility) - */ -export function createMainWindow(): BrowserWindow { - return createWindow() -} diff --git a/src/main/windows/window-manager.ts b/src/main/windows/window-manager.ts deleted file mode 100644 index 16a004b6..00000000 --- a/src/main/windows/window-manager.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { BrowserWindow } from "electron" -import { cleanupWindowSubscriptions } from "../lib/git/watcher/ipc-bridge" - -/** - * Manages multiple application windows - */ -class WindowManager { - private windows: Map = new Map() - private focusedWindowId: number | null = null - private mainWindowId: number | null = null // Track the "main" window - private windowIdMap: Map = new Map() // Map Electron window.id to stable ID - private nextSecondaryId = 2 // Counter for secondary windows - - /** - * Register a window with the manager and assign a stable ID - * Returns the stable window ID to use for localStorage namespacing - */ - register(window: BrowserWindow): string { - const electronId = window.id - this.windows.set(electronId, window) - - // Assign stable ID - let stableId: string - if (this.mainWindowId === null) { - // First window ever registered becomes the "main" window - this.mainWindowId = electronId - stableId = "main" - } else { - // Secondary windows get incrementing IDs - stableId = `window-${this.nextSecondaryId++}` - } - this.windowIdMap.set(electronId, stableId) - - // Track focus - window.on("focus", () => { - this.focusedWindowId = electronId - }) - - // Clean up on close - // Note: Electron automatically removes all listeners when a window is destroyed, - // so we only need to clean up our internal tracking here - window.on("closed", () => { - // Cleanup git watcher subscriptions for this window to prevent memory leaks - cleanupWindowSubscriptions(electronId) - - this.windows.delete(electronId) - this.windowIdMap.delete(electronId) - if (this.focusedWindowId === electronId) { - this.focusedWindowId = null - } - // If main window is closed, update mainWindowId for internal tracking - // but DON'T change the stable ID of remaining windows - they keep their localStorage namespace - if (this.mainWindowId === electronId) { - const remainingWindows = Array.from(this.windows.keys()) - this.mainWindowId = remainingWindows.length > 0 ? remainingWindows[0] : null - // Note: We intentionally keep the existing stable ID (e.g., "window-2") - // Changing it to "main" would orphan the window's localStorage data - } - }) - - // Set as focused if it's the only window - if (this.windows.size === 1) { - this.focusedWindowId = electronId - } - - return stableId - } - - /** - * Get the stable ID for a window - */ - getStableId(window: BrowserWindow): string { - return this.windowIdMap.get(window.id) ?? "main" - } - - /** - * Unregister a window - */ - unregister(window: BrowserWindow): void { - this.windows.delete(window.id) - if (this.focusedWindowId === window.id) { - this.focusedWindowId = null - } - } - - /** - * Get a window by ID - */ - get(id: number): BrowserWindow | undefined { - return this.windows.get(id) - } - - /** - * Get the currently focused window - */ - getFocused(): BrowserWindow | null { - if (this.focusedWindowId !== null) { - const win = this.windows.get(this.focusedWindowId) - if (win && !win.isDestroyed()) { - return win - } - } - // Fallback to BrowserWindow.getFocusedWindow() with destroyed check - const focusedWin = BrowserWindow.getFocusedWindow() - if (focusedWin && !focusedWin.isDestroyed()) { - return focusedWin - } - return null - } - - /** - * Get all windows - */ - getAll(): BrowserWindow[] { - return Array.from(this.windows.values()).filter((w) => !w.isDestroyed()) - } - - /** - * Get the number of windows - */ - count(): number { - return this.windows.size - } - - /** - * Find window by webContents ID - */ - findByWebContentsId(webContentsId: number): BrowserWindow | undefined { - for (const window of this.windows.values()) { - if (!window.isDestroyed() && window.webContents.id === webContentsId) { - return window - } - } - return undefined - } -} - -// Singleton instance -export const windowManager = new WindowManager() diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 727f272c..51a06b4c 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -25,7 +25,7 @@ export interface DesktopApi { getVersion: () => Promise // Auto-update - checkForUpdates: (force?: boolean) => Promise + checkForUpdates: () => Promise downloadUpdate: () => Promise installUpdate: () => void onUpdateChecking: (callback: () => void) => () => void diff --git a/src/preload/index.ts b/src/preload/index.ts index 5152745d..e54bf770 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -1,4 +1,4 @@ -import { contextBridge, ipcRenderer, webUtils } from "electron" +import { contextBridge, ipcRenderer } from "electron" import { exposeElectronTRPC } from "trpc-electron/main" // Only initialize Sentry in production to avoid IPC errors in dev mode @@ -11,11 +11,6 @@ if (process.env.NODE_ENV === "production") { // Expose tRPC IPC bridge for type-safe communication exposeElectronTRPC() -// Expose webUtils for file path access in drag and drop -contextBridge.exposeInMainWorld("webUtils", { - getPathForFile: (file: File) => webUtils.getPathForFile(file), -}) - // Expose analytics force flag for testing if (process.env.FORCE_ANALYTICS === "true") { contextBridge.exposeInMainWorld("__FORCE_ANALYTICS__", true) @@ -30,11 +25,9 @@ contextBridge.exposeInMainWorld("desktopApi", { isPackaged: () => ipcRenderer.invoke("app:isPackaged"), // Auto-update methods - checkForUpdates: (force?: boolean) => ipcRenderer.invoke("update:check", force), + checkForUpdates: () => ipcRenderer.invoke("update:check"), downloadUpdate: () => ipcRenderer.invoke("update:download"), installUpdate: () => ipcRenderer.invoke("update:install"), - setUpdateChannel: (channel: "latest" | "beta") => ipcRenderer.invoke("update:set-channel", channel), - getUpdateChannel: () => ipcRenderer.invoke("update:get-channel") as Promise<"latest" | "beta">, // Auto-update event listeners onUpdateChecking: (callback: () => void) => { @@ -106,13 +99,8 @@ contextBridge.exposeInMainWorld("desktopApi", { zoomReset: () => ipcRenderer.invoke("window:zoom-reset"), getZoom: () => ipcRenderer.invoke("window:get-zoom"), - // Multi-window - newWindow: (options?: { chatId?: string; subChatId?: string }) => ipcRenderer.invoke("window:new", options), - setWindowTitle: (title: string) => ipcRenderer.invoke("window:set-title", title), - // DevTools toggleDevTools: () => ipcRenderer.invoke("window:toggle-devtools"), - unlockDevTools: () => ipcRenderer.invoke("window:unlock-devtools"), // Analytics setAnalyticsOptOut: (optedOut: boolean) => ipcRenderer.invoke("analytics:set-opt-out", optedOut), @@ -138,48 +126,6 @@ contextBridge.exposeInMainWorld("desktopApi", { startAuthFlow: () => ipcRenderer.invoke("auth:start-flow"), submitAuthCode: (code: string) => ipcRenderer.invoke("auth:submit-code", code), updateUser: (updates: { name?: string }) => ipcRenderer.invoke("auth:update-user", updates), - getAuthToken: () => ipcRenderer.invoke("auth:get-token"), - - // Signed fetch - proxies through main process (no CORS issues) - signedFetch: ( - url: string, - options?: { method?: string; body?: string; headers?: Record }, - ) => - ipcRenderer.invoke("api:signed-fetch", url, options) as Promise<{ - ok: boolean - status: number - data: unknown - error: string | null - }>, - - // Streaming fetch - for SSE responses (chat streaming) - streamFetch: ( - streamId: string, - url: string, - options?: { method?: string; body?: string; headers?: Record }, - ) => - ipcRenderer.invoke("api:stream-fetch", streamId, url, options) as Promise<{ - ok: boolean - status: number - error?: string - }>, - - // Stream event listeners - onStreamChunk: (streamId: string, callback: (chunk: Uint8Array) => void) => { - const handler = (_event: unknown, chunk: Uint8Array) => callback(chunk) - ipcRenderer.on(`stream:${streamId}:chunk`, handler) - return () => ipcRenderer.removeListener(`stream:${streamId}:chunk`, handler) - }, - onStreamDone: (streamId: string, callback: () => void) => { - const handler = () => callback() - ipcRenderer.on(`stream:${streamId}:done`, handler) - return () => ipcRenderer.removeListener(`stream:${streamId}:done`, handler) - }, - onStreamError: (streamId: string, callback: (error: string) => void) => { - const handler = (_event: unknown, error: string) => callback(error) - ipcRenderer.on(`stream:${streamId}:error`, handler) - return () => ipcRenderer.removeListener(`stream:${streamId}:error`, handler) - }, // Auth events onAuthSuccess: (callback: (user: any) => void) => { @@ -217,10 +163,6 @@ contextBridge.exposeInMainWorld("desktopApi", { // Subscribe to git watcher for a worktree (from renderer) subscribeToGitWatcher: (worktreePath: string) => ipcRenderer.invoke("git:subscribe-watcher", worktreePath), unsubscribeFromGitWatcher: (worktreePath: string) => ipcRenderer.invoke("git:unsubscribe-watcher", worktreePath), - - // VS Code theme scanning - scanVSCodeThemes: () => ipcRenderer.invoke("vscode:scan-themes"), - loadVSCodeTheme: (themePath: string) => ipcRenderer.invoke("vscode:load-theme", themePath), }) // Type definitions @@ -236,41 +178,15 @@ export interface UpdateProgress { total: number } -export type EditorSource = "vscode" | "vscode-insiders" | "cursor" | "windsurf" - -export interface DiscoveredTheme { - id: string - name: string - type: "light" | "dark" - extensionId: string - extensionName: string - path: string - source: EditorSource -} - -export interface VSCodeThemeData { - id: string - name: string - type: "light" | "dark" - colors: Record - tokenColors?: any[] - semanticHighlighting?: boolean - semanticTokenColors?: Record - source: "imported" - path: string -} - export interface DesktopApi { platform: NodeJS.Platform arch: string getVersion: () => Promise isPackaged: () => Promise // Auto-update - checkForUpdates: (force?: boolean) => Promise + checkForUpdates: () => Promise downloadUpdate: () => Promise installUpdate: () => void - setUpdateChannel: (channel: "latest" | "beta") => Promise - getUpdateChannel: () => Promise<"latest" | "beta"> onUpdateChecking: (callback: () => void) => () => void onUpdateAvailable: (callback: (info: UpdateInfo) => void) => () => void onUpdateNotAvailable: (callback: () => void) => () => void @@ -295,11 +211,7 @@ export interface DesktopApi { zoomOut: () => Promise zoomReset: () => Promise getZoom: () => Promise - // Multi-window - newWindow: (options?: { chatId?: string; subChatId?: string }) => Promise - setWindowTitle: (title: string) => Promise toggleDevTools: () => Promise - unlockDevTools: () => Promise setAnalyticsOptOut: (optedOut: boolean) => Promise setBadge: (count: number | null) => Promise setBadgeIcon: (imageData: string | null) => Promise @@ -327,20 +239,6 @@ export interface DesktopApi { imageUrl: string | null username: string | null } | null> - getAuthToken: () => Promise - signedFetch: ( - url: string, - options?: { method?: string; body?: string; headers?: Record }, - ) => Promise<{ ok: boolean; status: number; data: unknown; error: string | null }> - // Streaming fetch - streamFetch: ( - streamId: string, - url: string, - options?: { method?: string; body?: string; headers?: Record }, - ) => Promise<{ ok: boolean; status: number; error?: string }> - onStreamChunk: (streamId: string, callback: (chunk: Uint8Array) => void) => () => void - onStreamDone: (streamId: string, callback: () => void) => () => void - onStreamError: (streamId: string, callback: (error: string) => void) => () => void onAuthSuccess: (callback: (user: any) => void) => () => void onAuthError: (callback: (error: string) => void) => () => void // Shortcuts @@ -351,16 +249,10 @@ export interface DesktopApi { onGitStatusChanged: (callback: (data: { worktreePath: string; changes: Array<{ path: string; type: "add" | "change" | "unlink" }> }) => void) => () => void subscribeToGitWatcher: (worktreePath: string) => Promise unsubscribeFromGitWatcher: (worktreePath: string) => Promise - // VS Code theme scanning - scanVSCodeThemes: () => Promise - loadVSCodeTheme: (themePath: string) => Promise } declare global { interface Window { desktopApi: DesktopApi - webUtils: { - getPathForFile: (file: File) => string - } } } diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 9e3c9010..27b5e8ba 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -4,9 +4,7 @@ import { useEffect, useMemo } from "react" import { Toaster } from "sonner" import { TooltipProvider } from "./components/ui/tooltip" import { TRPCProvider } from "./contexts/TRPCProvider" -import { WindowProvider, getInitialWindowParams } from "./contexts/WindowContext" -import { selectedProjectAtom, selectedAgentChatIdAtom } from "./features/agents/atoms" -import { useAgentSubChatStore } from "./features/agents/stores/sub-chat-store" +import { selectedProjectAtom } from "./features/agents/atoms" import { AgentsLayout } from "./features/layout/agents-layout" import { AnthropicOnboardingPage, @@ -47,31 +45,8 @@ function AppContent() { const anthropicOnboardingCompleted = useAtomValue( anthropicOnboardingCompletedAtom ) - const setAnthropicOnboardingCompleted = useSetAtom(anthropicOnboardingCompletedAtom) const apiKeyOnboardingCompleted = useAtomValue(apiKeyOnboardingCompletedAtom) - const setApiKeyOnboardingCompleted = useSetAtom(apiKeyOnboardingCompletedAtom) const selectedProject = useAtomValue(selectedProjectAtom) - const setSelectedChatId = useSetAtom(selectedAgentChatIdAtom) - const { setActiveSubChat, addToOpenSubChats, setChatId } = useAgentSubChatStore() - - // Apply initial window params (chatId/subChatId) when opening via "Open in new window" - useEffect(() => { - const params = getInitialWindowParams() - if (params.chatId) { - console.log("[App] Opening chat from window params:", params.chatId, params.subChatId) - setSelectedChatId(params.chatId) - setChatId(params.chatId) - if (params.subChatId) { - addToOpenSubChats(params.subChatId) - setActiveSubChat(params.subChatId) - } - } - }, [setSelectedChatId, setChatId, addToOpenSubChats, setActiveSubChat]) - - // Check if user has existing CLI config (API key or proxy) - // Based on PR #29 by @sa4hnd - const { data: cliConfig, isLoading: isLoadingCliConfig } = - trpc.claudeCode.hasExistingCliConfig.useQuery() // Migration: If user already completed Anthropic onboarding but has no billing method set, // automatically set it to "claude-subscription" (legacy users before billing method was added) @@ -81,16 +56,6 @@ function AppContent() { } }, [billingMethod, anthropicOnboardingCompleted, setBillingMethod]) - // Auto-skip onboarding if user has existing CLI config (API key or proxy) - // This allows users with ANTHROPIC_API_KEY to use the app without OAuth - useEffect(() => { - if (cliConfig?.hasConfig && !billingMethod) { - console.log("[App] Detected existing CLI config, auto-completing onboarding") - setBillingMethod("api-key") - setApiKeyOnboardingCompleted(true) - } - }, [cliConfig?.hasConfig, billingMethod, setBillingMethod, setApiKeyOnboardingCompleted]) - // Fetch projects to validate selectedProject exists const { data: projects, isLoading: isLoadingProjects } = trpc.projects.list.useQuery() @@ -171,24 +136,22 @@ export function App() { }, []) return ( - - - - - - -
- -
- -
-
-
-
-
-
+ + + + + +
+ +
+ +
+
+
+
+
) } diff --git a/src/renderer/DEBUG-WDYR.md b/src/renderer/DEBUG-WDYR.md deleted file mode 100644 index bac7c665..00000000 --- a/src/renderer/DEBUG-WDYR.md +++ /dev/null @@ -1,217 +0,0 @@ -# Why Did You Render (WDYR) - React Re-render Debugging - -This document explains how to use the WDYR library to debug infinite re-render loops and unnecessary re-renders in the desktop app. - -## Quick Start - -To enable WDYR debugging: - -1. Open `src/renderer/wdyr.ts` -2. Change `const WDYR_ENABLED = false` to `const WDYR_ENABLED = true` -3. Run `bun run dev` -4. Reproduce the issue - console will show re-render logs - -## What is WDYR? - -[Why Did You Render](https://github.com/welldone-software/why-did-you-render) is a library that patches React to notify you about avoidable re-renders. It helps identify: - -- Components re-rendering with the same props (referential equality issues) -- Infinite re-render loops -- Unnecessary state updates - -## How It Works - -### Configuration Files - -1. **`src/renderer/wdyr.ts`** - WDYR initialization with custom loop detection -2. **`electron.vite.config.ts`** - JSX import source configuration (only in dev) - -### JSX Import Source - -In `electron.vite.config.ts`, we configure WDYR as the JSX import source in dev mode: - -```typescript -react({ - jsxImportSource: isDev - ? "@welldone-software/why-did-you-render" - : undefined, -}) -``` - -This is **critical** - without it, WDYR only tracks components wrapped in `React.memo` or `PureComponent`. With the JSX import source, it tracks ALL components. - -## Reading WDYR Output - -When enabled, you'll see console logs like: - -``` -[WDYR] ComponentName render #3 { props: ['sortedSubChats'], state: false, hooks: false } -``` - -This tells you: -- **Component**: Which component re-rendered -- **Render count**: How many times in the time window (1 second) -- **props**: Which props changed (by name) -- **state**: Which state changed -- **hooks**: Which hooks changed - -### Infinite Loop Detection - -When a component renders 10+ times in 1 second, WDYR will: - -1. Log a red error: `🔴 INFINITE LOOP DETECTED: ComponentName rendered 10+ times in 1000ms` -2. Log the full diff info (props, state, hooks) -3. Trigger `debugger` - pausing execution so you can inspect the call stack - -## Common Patterns & Fixes - -### Pattern 1: `diffType: "deepEquals"` - -```json -{ - "diffType": "deepEquals", - "pathString": "sortedSubChats", - "prevValue": [{ "id": "abc", "name": "Chat" }], - "nextValue": [{ "id": "abc", "name": "Chat" }] -} -``` - -**Problem**: Arrays/objects are deeply equal but have different references. A new array is created on every render. - -**Fix**: Wrap the array/object creation in `useMemo`: - -```typescript -// BAD - creates new array every render -const items = data.map(x => transform(x)) - -// GOOD - memoized, same reference if data unchanged -const items = useMemo(() => data.map(x => transform(x)), [data]) -``` - -### Pattern 2: Inline Object in Render - -```typescript -// BAD - creates new object every render -const agentChat = condition ? { ...remoteChat, extra: value } : localChat - -// GOOD - memoized -const agentChat = useMemo(() => { - if (condition) { - return { ...remoteChat, extra: value } - } - return localChat -}, [condition, remoteChat, localChat]) -``` - -### Pattern 3: Callback Creating New References - -```typescript -// BAD - new function every render - handleSelect(item)} /> - -// GOOD - stable reference -const handleItemSelect = useCallback((item) => handleSelect(item), [handleSelect]) - -``` - -### Pattern 4: useEffect with Object Dependency - -```typescript -// BAD - object reference changes every render, effect runs infinitely -useEffect(() => { - doSomething(config) -}, [config]) // config = { a, b } created inline - -// GOOD - memoize the config or use primitive deps -const config = useMemo(() => ({ a, b }), [a, b]) -useEffect(() => { - doSomething(config) -}, [config]) -``` - -## Real Example: The Bug We Fixed - -### Symptoms -- App crashed with "Maximum update depth exceeded" when clicking on remote/sandbox chats -- `SearchHistoryPopover2` showed 10+ renders in 1 second - -### WDYR Output -``` -[WDYR] SearchHistoryPopover2 render #10 { props: ['sortedSubChats'], state: false, hooks: false } -🔴 INFINITE LOOP DETECTED: SearchHistoryPopover2 rendered 10+ times in 1000ms - -diffType: "deepEquals" -pathString: "sortedSubChats" -prevValue: [{ id: "abc", name: "Github projects access" }] -nextValue: [{ id: "abc", name: "Github projects access" }] -``` - -### Root Cause -In `active-chat.tsx`, `agentChat` was created inline: - -```typescript -const agentChat = chatSourceMode === "sandbox" ? { - ...remoteAgentChat, - // ... transforms -} : localAgentChat -``` - -This created a **new object reference** on every render because of the spread operator. - -The `useEffect` that syncs sub-chats depended on `agentChat`: - -```typescript -useEffect(() => { - // ... calls setAllSubChats(newArray) -}, [agentChat, chatId]) -``` - -**Chain reaction:** -1. Component renders → new `agentChat` reference -2. useEffect runs → calls `setAllSubChats()` with new array -3. Zustand store updates → subscribers re-render -4. Parent re-renders → back to step 1 → INFINITE LOOP - -### Fix -Wrap `agentChat` in `useMemo`: - -```typescript -const agentChat = useMemo(() => { - if (chatSourceMode === "sandbox") { - if (!remoteAgentChat) return null - return { ...remoteAgentChat, /* transforms */ } - } - return localAgentChat -}, [chatSourceMode, remoteAgentChat, localAgentChat]) -``` - -## Troubleshooting - -### WDYR Not Logging Anything - -1. Make sure `WDYR_ENABLED = true` in `wdyr.ts` -2. Make sure you restarted the dev server after changing config -3. Check that `jsxImportSource` is set in `electron.vite.config.ts` - -### Too Many Logs - -Adjust the threshold in `wdyr.ts`: - -```typescript -const THRESHOLD = 10 // Increase to reduce noise -const TIME_WINDOW = 1000 // Time window in ms -``` - -### Crash Before Logs Appear - -The crash might happen before WDYR initializes. Try: -1. Add `debugger` statement at the top of the suspected component -2. Use React DevTools Profiler to identify the looping component - -## Files Reference - -| File | Purpose | -|------|---------| -| `src/renderer/wdyr.ts` | WDYR config with loop detection | -| `electron.vite.config.ts` | JSX import source (dev only) | -| `src/renderer/main.tsx` | Imports wdyr.ts (must be first import) | diff --git a/src/renderer/assets/app-icons/appcode.svg b/src/renderer/assets/app-icons/appcode.svg deleted file mode 100644 index adc28914..00000000 --- a/src/renderer/assets/app-icons/appcode.svg +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/renderer/assets/app-icons/clion.svg b/src/renderer/assets/app-icons/clion.svg deleted file mode 100644 index c14657bc..00000000 --- a/src/renderer/assets/app-icons/clion.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/src/renderer/assets/app-icons/cursor.svg b/src/renderer/assets/app-icons/cursor.svg deleted file mode 100644 index c074bf27..00000000 --- a/src/renderer/assets/app-icons/cursor.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/renderer/assets/app-icons/datagrip.svg b/src/renderer/assets/app-icons/datagrip.svg deleted file mode 100644 index 4af9320a..00000000 --- a/src/renderer/assets/app-icons/datagrip.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/src/renderer/assets/app-icons/finder.png b/src/renderer/assets/app-icons/finder.png deleted file mode 100644 index 2cff579e..00000000 Binary files a/src/renderer/assets/app-icons/finder.png and /dev/null differ diff --git a/src/renderer/assets/app-icons/fleet.svg b/src/renderer/assets/app-icons/fleet.svg deleted file mode 100644 index 3ab51b6e..00000000 --- a/src/renderer/assets/app-icons/fleet.svg +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/renderer/assets/app-icons/ghostty.svg b/src/renderer/assets/app-icons/ghostty.svg deleted file mode 100644 index a4b0637b..00000000 --- a/src/renderer/assets/app-icons/ghostty.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/src/renderer/assets/app-icons/goland.svg b/src/renderer/assets/app-icons/goland.svg deleted file mode 100644 index c3ef8653..00000000 --- a/src/renderer/assets/app-icons/goland.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/src/renderer/assets/app-icons/intellij.svg b/src/renderer/assets/app-icons/intellij.svg deleted file mode 100644 index 46cc4abe..00000000 --- a/src/renderer/assets/app-icons/intellij.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/src/renderer/assets/app-icons/iterm.png b/src/renderer/assets/app-icons/iterm.png deleted file mode 100644 index 19cfd018..00000000 Binary files a/src/renderer/assets/app-icons/iterm.png and /dev/null differ diff --git a/src/renderer/assets/app-icons/jetbrains.svg b/src/renderer/assets/app-icons/jetbrains.svg deleted file mode 100644 index 367c4ffa..00000000 --- a/src/renderer/assets/app-icons/jetbrains.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/src/renderer/assets/app-icons/phpstorm.svg b/src/renderer/assets/app-icons/phpstorm.svg deleted file mode 100644 index 9b1e48d8..00000000 --- a/src/renderer/assets/app-icons/phpstorm.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/src/renderer/assets/app-icons/pycharm.svg b/src/renderer/assets/app-icons/pycharm.svg deleted file mode 100644 index 0092bf3c..00000000 --- a/src/renderer/assets/app-icons/pycharm.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/src/renderer/assets/app-icons/rider.svg b/src/renderer/assets/app-icons/rider.svg deleted file mode 100644 index 97060ecb..00000000 --- a/src/renderer/assets/app-icons/rider.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/src/renderer/assets/app-icons/rubymine.svg b/src/renderer/assets/app-icons/rubymine.svg deleted file mode 100644 index dcd769ec..00000000 --- a/src/renderer/assets/app-icons/rubymine.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/src/renderer/assets/app-icons/rustrover.svg b/src/renderer/assets/app-icons/rustrover.svg deleted file mode 100644 index 301a1ee6..00000000 --- a/src/renderer/assets/app-icons/rustrover.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/src/renderer/assets/app-icons/sublime.svg b/src/renderer/assets/app-icons/sublime.svg deleted file mode 100644 index e70b2473..00000000 --- a/src/renderer/assets/app-icons/sublime.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/renderer/assets/app-icons/terminal.png b/src/renderer/assets/app-icons/terminal.png deleted file mode 100644 index 5cd898dd..00000000 Binary files a/src/renderer/assets/app-icons/terminal.png and /dev/null differ diff --git a/src/renderer/assets/app-icons/trae.svg b/src/renderer/assets/app-icons/trae.svg deleted file mode 100644 index dda49308..00000000 --- a/src/renderer/assets/app-icons/trae.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/renderer/assets/app-icons/vscode-insiders.svg b/src/renderer/assets/app-icons/vscode-insiders.svg deleted file mode 100644 index 5067415c..00000000 --- a/src/renderer/assets/app-icons/vscode-insiders.svg +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/renderer/assets/app-icons/vscode.svg b/src/renderer/assets/app-icons/vscode.svg deleted file mode 100644 index 0557c2cb..00000000 --- a/src/renderer/assets/app-icons/vscode.svg +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/renderer/assets/app-icons/warp.png b/src/renderer/assets/app-icons/warp.png deleted file mode 100644 index 997c7a15..00000000 Binary files a/src/renderer/assets/app-icons/warp.png and /dev/null differ diff --git a/src/renderer/assets/app-icons/webstorm.svg b/src/renderer/assets/app-icons/webstorm.svg deleted file mode 100644 index 4c0c8044..00000000 --- a/src/renderer/assets/app-icons/webstorm.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/src/renderer/assets/app-icons/windsurf.svg b/src/renderer/assets/app-icons/windsurf.svg deleted file mode 100644 index 7d60a073..00000000 --- a/src/renderer/assets/app-icons/windsurf.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/renderer/assets/app-icons/xcode.svg b/src/renderer/assets/app-icons/xcode.svg deleted file mode 100644 index cf5ae82e..00000000 --- a/src/renderer/assets/app-icons/xcode.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/renderer/assets/app-icons/zed.png b/src/renderer/assets/app-icons/zed.png deleted file mode 100644 index 08b6d8af..00000000 Binary files a/src/renderer/assets/app-icons/zed.png and /dev/null differ diff --git a/src/renderer/components/chat-markdown-renderer.tsx b/src/renderer/components/chat-markdown-renderer.tsx index ecd89957..29eae454 100644 --- a/src/renderer/components/chat-markdown-renderer.tsx +++ b/src/renderer/components/chat-markdown-renderer.tsx @@ -2,11 +2,9 @@ import { cn } from "../lib/utils" import { memo, useState, useCallback, useEffect, useMemo } from "react" import { Streamdown, parseMarkdownIntoBlocks } from "streamdown" import remarkBreaks from "remark-breaks" -import remarkGfm from "remark-gfm" import { Copy, Check } from "lucide-react" import { useCodeTheme } from "../lib/hooks/use-code-theme" import { highlightCode } from "../lib/themes/shiki-theme-loader" -import { MermaidBlock } from "./mermaid-block" // Function to strip emojis from text (only common emojis, preserving markdown symbols) export function stripEmojis(text: string): string { @@ -247,8 +245,8 @@ const sizeStyles: Record< } // Custom code component that uses our theme system -function createCodeComponent(codeTheme: string, size: MarkdownSize, styles: typeof sizeStyles.md, isStreaming: boolean = false) { - return function CodeComponent({ className, children, node, ...props }: any) { +function createCodeComponent(codeTheme: string, size: MarkdownSize, styles: typeof sizeStyles.md) { + return function CodeComponent({ className, children, ...props }: any) { const match = /language-(\w+)/.exec(className || "") const language = match ? match[1] : undefined const codeContent = String(children) @@ -258,13 +256,6 @@ function createCodeComponent(codeTheme: string, size: MarkdownSize, styles: type const isCodeBlock = language || (codeContent.includes("\n") && codeContent.length > 100) if (isCodeBlock) { - // Route mermaid blocks to MermaidBlock component - if (language === "mermaid") { - // Pass isStreaming to MermaidBlock - // When streaming, MermaidBlock shows a placeholder instead of trying to render - return - } - return ( ), pre: ({ children }: any) => <>{children}, - code: createCodeComponent(codeTheme, size, styles, isStreaming), + code: createCodeComponent(codeTheme, size, styles), }), - [styles, codeTheme, size, isStreaming], + [styles, codeTheme, size], ) return ( @@ -445,7 +436,7 @@ export const ChatMarkdownRenderer = memo(function ChatMarkdownRenderer({ {content} diff --git a/src/renderer/components/dialogs/agents-help-popover.tsx b/src/renderer/components/dialogs/agents-help-popover.tsx new file mode 100644 index 00000000..8e1cdcd5 --- /dev/null +++ b/src/renderer/components/dialogs/agents-help-popover.tsx @@ -0,0 +1,72 @@ +import * as React from "react" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "../ui/popover" +import { DiscordIcon, KeyboardIcon, RoadmapIcon, TicketIcon } from "../../icons" + +interface AgentsHelpPopoverProps { + open: boolean + onOpenChange: (open: boolean) => void + isMobile?: boolean + children: React.ReactNode +} + +export function AgentsHelpPopover({ + open, + onOpenChange, + isMobile = false, + children, +}: AgentsHelpPopoverProps) { + const menuItems = [ + { + icon: DiscordIcon, + label: "Discord", + onClick: () => window.open("https://discord.gg/8ektTZGnj4", "_blank"), + }, + { + icon: KeyboardIcon, + label: "Shortcuts", + onClick: () => { + // Open shortcuts dialog + console.log("Open shortcuts dialog") + }, + }, + { + icon: RoadmapIcon, + label: "Roadmap", + onClick: () => window.open("https://agentsby21st.featurebase.app/roadmap", "_blank"), + }, + { + icon: TicketIcon, + label: "Feature Request", + onClick: () => window.open("https://agentsby21st.featurebase.app", "_blank"), + }, + ] + + return ( + + {children} + + {menuItems.map((item) => ( + + ))} + + + ) +} diff --git a/src/renderer/components/dialogs/agents-rename-subchat-dialog.tsx b/src/renderer/components/dialogs/agents-rename-subchat-dialog.tsx new file mode 100644 index 00000000..66509fe3 --- /dev/null +++ b/src/renderer/components/dialogs/agents-rename-subchat-dialog.tsx @@ -0,0 +1,93 @@ +import * as React from "react" +import { useState, useEffect } from "react" +import { createPortal } from "react-dom" +import { cn } from "../../lib/utils" +import { Button } from "../ui/button" +import { Input } from "../ui/input" +import { X } from "lucide-react" + +interface AgentsRenameSubChatDialogProps { + isOpen: boolean + onClose: () => void + onSave: (name: string) => void + currentName: string + isLoading?: boolean +} + +export function AgentsRenameSubChatDialog({ + isOpen, + onClose, + onSave, + currentName, + isLoading = false, +}: AgentsRenameSubChatDialogProps) { + const [name, setName] = useState(currentName) + + useEffect(() => { + setName(currentName) + }, [currentName, isOpen]) + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + onClose() + } + } + + if (isOpen) { + document.addEventListener("keydown", handleKeyDown) + return () => document.removeEventListener("keydown", handleKeyDown) + } + }, [isOpen, onClose]) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + if (name.trim()) { + onSave(name.trim()) + } + } + + if (!isOpen || typeof document === "undefined") return null + + return createPortal( +
+ {/* Backdrop */} +
+ + {/* Dialog */} +
+ + +

Rename Agent

+ +
+ setName(e.target.value)} + placeholder="Enter name..." + autoFocus + className="mb-4" + /> + +
+ + +
+
+
+
, + document.body + ) +} diff --git a/src/renderer/components/dialogs/agents-settings-dialog.tsx b/src/renderer/components/dialogs/agents-settings-dialog.tsx new file mode 100644 index 00000000..8485f607 --- /dev/null +++ b/src/renderer/components/dialogs/agents-settings-dialog.tsx @@ -0,0 +1,533 @@ +import { useAtom } from "jotai" +import { useEffect, useState, useMemo } from "react" +import { createPortal } from "react-dom" +import { AnimatePresence, motion } from "motion/react" +import { X, ChevronLeft, ChevronRight, FolderOpen } from "lucide-react" +import { cn } from "../../lib/utils" +import { agentsSettingsDialogActiveTabAtom, type SettingsTab } from "../../lib/atoms" +import { + ProfileIconFilled, + EyeOpenFilledIcon, + SlidersFilledIcon, + SettingsIcon, +} from "../../icons" +import { SkillIconFilled, CustomAgentIconFilled, OriginalMCPIcon, BrainFilledIcon, FlaskFilledIcon, BugFilledIcon } from "../ui/icons" +import { AgentsAppearanceTab } from "./settings-tabs/agents-appearance-tab" +import { AgentsProfileTab } from "./settings-tabs/agents-profile-tab" +import { AgentsPreferencesTab } from "./settings-tabs/agents-preferences-tab" +import { AgentsDebugTab } from "./settings-tabs/agents-debug-tab" +import { AgentsSkillsTab } from "./settings-tabs/agents-skills-tab" +import { AgentsCustomAgentsTab } from "./settings-tabs/agents-custom-agents-tab" +import { AgentsModelsTab } from "./settings-tabs/agents-models-tab" +import { AgentsMcpTab } from "./settings-tabs/agents-mcp-tab" +import { AgentsBetaTab } from "./settings-tabs/agents-beta-tab" +import { AgentsProjectWorktreeTab } from "./settings-tabs/agents-project-worktree-tab" +import { trpc } from "../../lib/trpc" + +// Hook to detect narrow screen +function useIsNarrowScreen(): boolean { + const [isNarrow, setIsNarrow] = useState(false) + + useEffect(() => { + const checkWidth = () => { + setIsNarrow(window.innerWidth <= 768) + } + + checkWidth() + window.addEventListener("resize", checkWidth) + return () => window.removeEventListener("resize", checkWidth) + }, []) + + return isNarrow +} + +// Check if we're in development mode +const isDevelopment = process.env.NODE_ENV === "development" + +interface AgentsSettingsDialogProps { + isOpen: boolean + onClose: () => void +} + +// Main settings tabs +const MAIN_TABS = [ + { + id: "profile" as SettingsTab, + label: "Account", + icon: ProfileIconFilled, + description: "Manage your account settings", + }, + { + id: "appearance" as SettingsTab, + label: "Appearance", + icon: EyeOpenFilledIcon, + description: "Theme settings", + }, + { + id: "preferences" as SettingsTab, + label: "Preferences", + icon: SlidersFilledIcon, + description: "Claude behavior settings", + }, + { + id: "models" as SettingsTab, + label: "Models", + icon: BrainFilledIcon, + description: "Model overrides and Claude Code auth", + }, +] + +// Advanced/experimental tabs +const ADVANCED_TABS = [ + { + id: "skills" as SettingsTab, + label: "Skills", + icon: SkillIconFilled, + description: "Custom Claude skills", + }, + { + id: "agents" as SettingsTab, + label: "Custom Agents", + icon: CustomAgentIconFilled, + description: "Manage custom Claude agents", + }, + { + id: "mcp" as SettingsTab, + label: "MCP Servers", + icon: OriginalMCPIcon, + description: "Model Context Protocol servers", + }, + { + id: "beta" as SettingsTab, + label: "Beta", + icon: FlaskFilledIcon, + description: "Experimental features", + }, + // Debug tab - always shown in desktop for development + ...(isDevelopment + ? [ + { + id: "debug" as SettingsTab, + label: "Debug", + icon: BugFilledIcon, + description: "Test first-time user experience", + }, + ] + : []), +] + +interface TabButtonProps { + tab: { + id: SettingsTab + label: string + icon: React.ComponentType<{ className?: string }> | any + description?: string + beta?: boolean + } + isActive: boolean + onClick: () => void + isNarrow?: boolean +} + +function TabButton({ tab, isActive, onClick, isNarrow }: TabButtonProps) { + const Icon = tab.icon + const isBeta = "beta" in tab && tab.beta + // Check if this is a project tab (has projectId property) + const isProjectTab = "projectId" in tab + + return ( + + ) +} + +export function AgentsSettingsDialog({ + isOpen, + onClose, +}: AgentsSettingsDialogProps) { + const [activeTab, setActiveTab] = useAtom(agentsSettingsDialogActiveTabAtom) + const [mounted, setMounted] = useState(false) + const [portalTarget, setPortalTarget] = useState(null) + const isNarrowScreen = useIsNarrowScreen() + + // Get projects list for dynamic tabs + const { data: projects } = trpc.projects.list.useQuery() + + // Generate dynamic project tabs + const projectTabs = useMemo(() => { + if (!projects || projects.length === 0) { + return [] + } + + return projects.map((project) => ({ + id: `project-${project.id}` as SettingsTab, + label: project.name, + icon: (project.gitOwner && project.gitProvider === 'github') + ? (() => { + const GitHubIcon = ({ className }: { className?: string }) => ( + {project.gitOwner + ) + return GitHubIcon + })() + : FolderOpen, + description: `Worktree setup for ${project.name}`, + projectId: project.id, + })) + }, [projects]) + + // All tabs combined for lookups + const ALL_TABS = useMemo( + () => [...MAIN_TABS, ...ADVANCED_TABS, ...projectTabs], + [projectTabs] + ) + + // Helper to get tab label from tab id + const getTabLabel = (tabId: SettingsTab): string => { + return ALL_TABS.find((t) => t.id === tabId)?.label ?? "Settings" + } + + // Narrow screen: track whether we're showing tab list or content + const [showContent, setShowContent] = useState(false) + + // Reset content view when dialog closes + useEffect(() => { + if (!isOpen) { + setShowContent(false) + } + }, [isOpen]) + + // Handle keyboard navigation + useEffect(() => { + if (!isOpen) return + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") { + event.preventDefault() + if (isNarrowScreen && showContent) { + setShowContent(false) + } else { + onClose() + } + } + } + + document.addEventListener("keydown", handleKeyDown) + return () => document.removeEventListener("keydown", handleKeyDown) + }, [isOpen, onClose, isNarrowScreen, showContent]) + + // Ensure portal target only accessed on client + useEffect(() => { + setMounted(true) + if (typeof document !== "undefined") { + setPortalTarget(document.body) + } + }, []) + + const handleTabClick = (tabId: SettingsTab) => { + setActiveTab(tabId) + if (isNarrowScreen) { + setShowContent(true) + } + } + + const renderTabContent = () => { + // Handle dynamic project tabs + if (activeTab.startsWith('project-')) { + const projectId = activeTab.replace('project-', '') + return + } + + // Handle static tabs + switch (activeTab) { + case "profile": + return + case "appearance": + return + case "preferences": + return + case "models": + return + case "skills": + return + case "agents": + return + case "mcp": + return + case "beta": + return + case "debug": + return isDevelopment ? : null + default: + return null + } + } + + const renderTabList = () => ( +
+ {/* Main tabs */} +
+ {MAIN_TABS.map((tab) => ( + handleTabClick(tab.id)} + isNarrow={isNarrowScreen} + /> + ))} +
+ + {/* Separator */} +
+ + {/* Advanced tabs */} +
+ {ADVANCED_TABS.map((tab) => ( + handleTabClick(tab.id)} + isNarrow={isNarrowScreen} + /> + ))} +
+ + {/* Project tabs */} + {projectTabs.length > 0 && ( + <> + {/* Separator */} +
+ +
+ {projectTabs.map((tab) => ( + handleTabClick(tab.id)} + isNarrow={isNarrowScreen} + /> + ))} +
+ + )} +
+ ) + + if (!mounted || !portalTarget) return null + + // Narrow screen: Full-screen overlay with two-screen navigation + if (isNarrowScreen) { + if (!isOpen) return null + + return createPortal( + <> + {/* Full-screen settings panel */} +
+ {/* Header */} +
+ {showContent && ( + + )} +

+ {showContent ? getTabLabel(activeTab) : "Settings"} +

+ +
+ + {/* Content */} +
+ {showContent ? ( +
+ {renderTabContent()} +
+ ) : ( +
+ {renderTabList()} +
+ )} +
+
+ , + portalTarget, + ) + } + + // Wide screen: Centered modal with sidebar + return createPortal( + + {isOpen && ( + <> + {/* Custom Overlay */} + + + {/* Settings Dialog */} +
+ +

+ Settings +

+ +
+ {/* Left Sidebar - Tabs */} +
+

+ Settings +

+ + {/* Main Tabs */} +
+ {MAIN_TABS.map((tab) => ( + setActiveTab(tab.id)} + /> + ))} +
+ + {/* Separator */} +
+ + {/* Advanced Tabs */} +
+ {ADVANCED_TABS.map((tab) => ( + setActiveTab(tab.id)} + /> + ))} +
+ + {/* Project Tabs */} + {projectTabs.length > 0 && ( + <> + {/* Separator */} +
+ +
+ {projectTabs.map((tab) => ( + setActiveTab(tab.id)} + /> + ))} +
+ + )} +
+ + {/* Right Content Area */} +
+
+ {renderTabContent()} +
+
+
+ + {/* Close Button */} + + +
+ + )} + , + portalTarget, + ) +} diff --git a/src/renderer/components/dialogs/agents-shortcuts-dialog.tsx b/src/renderer/components/dialogs/agents-shortcuts-dialog.tsx new file mode 100644 index 00000000..9327980d --- /dev/null +++ b/src/renderer/components/dialogs/agents-shortcuts-dialog.tsx @@ -0,0 +1,242 @@ +import { useEffect, useMemo } from "react" +import { AnimatePresence, motion } from "motion/react" +import { createPortal } from "react-dom" +import { useAtomValue } from "jotai" +import { CmdIcon } from "../../icons" +import { ctrlTabTargetAtom } from "../../lib/atoms" + +interface AgentsShortcutsDialogProps { + isOpen: boolean + onClose: () => void +} + +const EASING_CURVE = [0.55, 0.055, 0.675, 0.19] as const + +interface Shortcut { + label: string + keys: Array + altKeys?: Array +} + +function ShortcutKey({ keyName }: { keyName: string }) { + if (keyName === "cmd") { + return ( + + + + ) + } + + return ( + + {keyName === "opt" + ? "⌥" + : keyName === "shift" + ? "⇧" + : keyName === "ctrl" + ? "⌃" + : keyName} + + ) +} + +function ShortcutRow({ shortcut }: { shortcut: Shortcut }) { + return ( +
+ {shortcut.label} +
+ {shortcut.keys.map((key, index) => ( + + ))} + {shortcut.altKeys && ( + <> + or + {shortcut.altKeys.map((key, index) => ( + + ))} + + )} +
+
+ ) +} + +// Desktop app shortcuts (simplified) +const GENERAL_SHORTCUTS: Shortcut[] = [ + { label: "Show shortcuts", keys: ["?"] }, + { label: "Settings", keys: ["cmd", ","] }, + { label: "Toggle sidebar", keys: ["cmd", "\\"] }, + { label: "Undo archive", keys: ["cmd", "Z"] }, +] + +// Dynamic shortcuts based on ctrlTabTarget preference +function getWorkspaceShortcuts( + ctrlTabTarget: "workspaces" | "agents", +): Shortcut[] { + return [ + { label: "New workspace", keys: ["cmd", "N"] }, + { label: "Search workspaces", keys: ["cmd", "K"] }, + { label: "Archive current workspace", keys: ["cmd", "E"] }, + { + label: "Quick switch workspaces", + keys: + ctrlTabTarget === "workspaces" ? ["ctrl", "Tab"] : ["opt", "ctrl", "Tab"], + }, + ] +} + +function getAgentShortcuts( + ctrlTabTarget: "workspaces" | "agents", +): Shortcut[] { + return [ + // Creation & Management (mirrors Workspaces order) + { label: "Create new agent", keys: ["cmd", "T"] }, + { label: "Search chats", keys: ["/"] }, + { label: "Search text in current chat", keys: ["cmd", "F"] }, + { label: "Archive current agent", keys: ["cmd", "W"] }, + // Navigation + { + label: "Quick switch agents", + keys: + ctrlTabTarget === "workspaces" ? ["opt", "ctrl", "Tab"] : ["ctrl", "Tab"], + }, + { + label: "Previous / Next agent", + keys: ["cmd", "["], + altKeys: ["cmd", "]"], + }, + // Interaction + { label: "Focus input", keys: ["Enter"] }, + { label: "Toggle focus", keys: ["cmd", "Esc"] }, + { label: "Stop generation", keys: ["Esc"], altKeys: ["ctrl", "C"] }, + { label: "Switch model", keys: ["cmd", "/"] }, + // Tools + { label: "Toggle terminal", keys: ["cmd", "J"] }, + { label: "Open diff", keys: ["cmd", "D"] }, + { label: "Create PR", keys: ["cmd", "P"] }, + ] +} + +export function AgentsShortcutsDialog({ + isOpen, + onClose, +}: AgentsShortcutsDialogProps) { + const ctrlTabTarget = useAtomValue(ctrlTabTargetAtom) + + // Memoize shortcuts based on preference + const workspaceShortcuts = useMemo( + () => getWorkspaceShortcuts(ctrlTabTarget), + [ctrlTabTarget], + ) + const agentShortcuts = useMemo( + () => getAgentShortcuts(ctrlTabTarget), + [ctrlTabTarget], + ) + + // Handle ESC key to close dialog + useEffect(() => { + if (!isOpen) return + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + e.preventDefault() + onClose() + } + } + + window.addEventListener("keydown", handleKeyDown) + return () => window.removeEventListener("keydown", handleKeyDown) + }, [isOpen, onClose]) + + const portalTarget = typeof document !== "undefined" ? document.body : null + if (!portalTarget) return null + + return createPortal( + + {isOpen && ( + <> + {/* Overlay */} + + + {/* Main Dialog */} +
+ e.stopPropagation()} + > +
+
+

+ Keyboard Shortcuts +

+ + {/* Two-column layout: General+Workspaces | Agents */} +
+ {/* Left column: General + Workspaces */} +
+ {/* General Section */} +
+

+ General +

+
+ {GENERAL_SHORTCUTS.map((shortcut, index) => ( + + ))} +
+
+ + {/* Workspaces Section */} +
+

+ Workspaces +

+
+ {workspaceShortcuts.map((shortcut, index) => ( + + ))} +
+
+
+ + {/* Right column: Agents */} +
+

+ Agents +

+
+ {agentShortcuts.map((shortcut, index) => ( + + ))} +
+
+
+
+
+
+
+ + )} +
, + portalTarget, + ) +} diff --git a/src/renderer/components/dialogs/index.ts b/src/renderer/components/dialogs/index.ts index a8d8284a..1582f09c 100644 --- a/src/renderer/components/dialogs/index.ts +++ b/src/renderer/components/dialogs/index.ts @@ -1,5 +1,9 @@ +// Dialogs +export { AgentsSettingsDialog } from "./agents-settings-dialog" +export { AgentsShortcutsDialog } from "./agents-shortcuts-dialog" +export { AgentsHelpPopover } from "./agents-help-popover" + // Settings tabs export { AgentsAppearanceTab } from "./settings-tabs/agents-appearance-tab" export { AgentsProfileTab } from "./settings-tabs/agents-profile-tab" export { AgentsDebugTab } from "./settings-tabs/agents-debug-tab" -export { AgentsKeyboardTab } from "./settings-tabs/agents-keyboard-tab" diff --git a/src/renderer/components/dialogs/mcp-approval-dialog.tsx b/src/renderer/components/dialogs/mcp-approval-dialog.tsx deleted file mode 100644 index c6b733b4..00000000 --- a/src/renderer/components/dialogs/mcp-approval-dialog.tsx +++ /dev/null @@ -1,187 +0,0 @@ -"use client" - -import { useAtom } from "jotai" -import { Shield } from "lucide-react" -import { - AlertDialog, - AlertDialogContent, - AlertDialogHeader, - AlertDialogBody, - AlertDialogFooter, - AlertDialogTitle, - AlertDialogDescription, -} from "../ui/alert-dialog" -import { Button } from "../ui/button" -import { trpc } from "../../lib/trpc" -import { toast } from "sonner" -import { - mcpApprovalDialogOpenAtom, - pendingMcpApprovalsAtom, -} from "../../lib/atoms" - -export function McpApprovalDialog() { - const [isOpen, setIsOpen] = useAtom(mcpApprovalDialogOpenAtom) - const [pendingApprovals, setPendingApprovals] = useAtom( - pendingMcpApprovalsAtom, - ) - - const approveMutation = - trpc.claudeSettings.approvePluginMcpServer.useMutation() - const approveAllMutation = - trpc.claudeSettings.approveAllPluginMcpServers.useMutation() - - const currentApproval = pendingApprovals[0] - - const handleAllow = async () => { - if (!currentApproval) return - - try { - await approveMutation.mutateAsync({ - identifier: currentApproval.identifier, - }) - toast.success("MCP server approved", { - description: currentApproval.serverName, - }) - } catch (error) { - const message = - error instanceof Error ? error.message : "Failed to approve" - toast.error(message) - } - - advance() - } - - const handleAllowAll = async () => { - if (!currentApproval) return - - // Approve all pending from the same plugin - const samePlugin = pendingApprovals.filter( - (a) => a.pluginSource === currentApproval.pluginSource, - ) - - try { - await approveAllMutation.mutateAsync({ - pluginSource: currentApproval.pluginSource, - serverNames: samePlugin.map((a) => a.serverName), - }) - toast.success("All MCP servers approved", { - description: `${samePlugin.length} server(s) from ${currentApproval.pluginSource}`, - }) - } catch (error) { - const message = - error instanceof Error ? error.message : "Failed to approve" - toast.error(message) - } - - // Remove all from same plugin - const remaining = pendingApprovals.filter( - (a) => a.pluginSource !== currentApproval.pluginSource, - ) - setPendingApprovals(remaining) - if (remaining.length === 0) { - setIsOpen(false) - } - } - - const handleDeny = () => { - advance() - } - - const advance = () => { - const remaining = pendingApprovals.slice(1) - setPendingApprovals(remaining) - if (remaining.length === 0) { - setIsOpen(false) - } - } - - if (!currentApproval) return null - - const config = currentApproval.config - const url = config.url as string | undefined - const command = config.command as string | undefined - const args = config.args as string[] | undefined - - return ( - - - -
-
- -
-
- MCP Server Approval - - A plugin wants to connect to an MCP server - -
-
-
- - -
-
-
- - Plugin - - - {currentApproval.pluginSource} - -
-
- - Server - - - {currentApproval.serverName} - -
- {command && ( -
- - Command - - - {command} - {args && args.length > 0 ? ` ${args.join(" ")}` : ""} - -
- )} - {url && ( -
- - URL - - - {url} - -
- )} -
- - {pendingApprovals.length > 1 && ( -

- +{pendingApprovals.length - 1} more approval - {pendingApprovals.length - 1 !== 1 ? "s" : ""} pending -

- )} -
-
- - - - - - -
-
- ) -} diff --git a/src/renderer/components/dialogs/settings-tabs/agents-appearance-tab.tsx b/src/renderer/components/dialogs/settings-tabs/agents-appearance-tab.tsx index 0c930a45..7561cbec 100644 --- a/src/renderer/components/dialogs/settings-tabs/agents-appearance-tab.tsx +++ b/src/renderer/components/dialogs/settings-tabs/agents-appearance-tab.tsx @@ -10,14 +10,11 @@ import { systemLightThemeIdAtom, systemDarkThemeIdAtom, showWorkspaceIconAtom, - alwaysExpandTodoListAtom, - importedThemesAtom, type VSCodeFullTheme, } from "../../../lib/atoms" import { BUILTIN_THEMES, getBuiltinThemeById, - BUILTIN_THEME_NAMES, } from "../../../lib/themes/builtin-themes" import { generateCSSVariables, @@ -30,9 +27,7 @@ import { SelectContent, SelectItem, SelectTrigger, - SelectSeparator, - SelectLabel, - SelectGroup, + SelectValue, } from "../../../components/ui/select" import { Switch } from "../../../components/ui/switch" @@ -141,67 +136,14 @@ export function AgentsAppearanceTab() { systemDarkThemeIdAtom, ) const setFullThemeData = useSetAtom(fullThemeDataAtom) - const [importedThemes, setImportedThemes] = useAtom(importedThemesAtom) // Sidebar settings const [showWorkspaceIcon, setShowWorkspaceIcon] = useAtom(showWorkspaceIconAtom) - // To-do list preference - const [alwaysExpandTodoList, setAlwaysExpandTodoList] = useAtom(alwaysExpandTodoListAtom) - - // VS Code themes state - const [isScanning, setIsScanning] = useState(false) - useEffect(() => { setMounted(true) }, []) - // Scan and load VS Code themes on mount - useEffect(() => { - if (!mounted) return - const api = window.desktopApi - if (typeof api?.scanVSCodeThemes !== "function") return - if (typeof api?.loadVSCodeTheme !== "function") return - - const loadAllThemes = async () => { - setIsScanning(true) - try { - const discovered = await api.scanVSCodeThemes() - // Filter out themes that are already builtin - const newThemes = discovered.filter( - (t) => !BUILTIN_THEME_NAMES.has(t.name.toLowerCase()) - ) - - // Load all themes in parallel - const loadedThemes = await Promise.all( - newThemes.map(async (theme) => { - try { - const fullTheme = await api.loadVSCodeTheme(theme.path) - return { - ...fullTheme, - id: theme.id, - source: "imported" as const, - } as VSCodeFullTheme - } catch (err) { - console.error("[appearance-tab] Failed to load theme:", theme.name, err) - return null - } - }) - ) - - // Filter out failed loads and update imported themes - const validThemes = loadedThemes.filter((t): t is VSCodeFullTheme => t !== null) - setImportedThemes(validThemes) - } catch (error) { - console.error("Failed to load VS Code themes:", error) - } finally { - setIsScanning(false) - } - } - - loadAllThemes() - }, [mounted, setImportedThemes]) - // Group themes by type const darkThemes = useMemo( () => BUILTIN_THEMES.filter((t) => t.type === "dark"), @@ -220,11 +162,8 @@ export function AgentsAppearanceTab() { if (selectedThemeId === null) { return null // System mode } - // Check in both builtin and imported themes - return BUILTIN_THEMES.find((t) => t.id === selectedThemeId) || - importedThemes.find((t) => t.id === selectedThemeId) || - null - }, [selectedThemeId, importedThemes]) + return BUILTIN_THEMES.find((t) => t.id === selectedThemeId) || null + }, [selectedThemeId]) // Get theme objects for system mode selectors const systemLightTheme = useMemo( @@ -258,9 +197,7 @@ export function AgentsAppearanceTab() { return } - // Check in both builtin and imported themes - const theme = BUILTIN_THEMES.find((t) => t.id === themeId) || - importedThemes.find((t) => t.id === themeId) + const theme = BUILTIN_THEMES.find((t) => t.id === themeId) if (theme) { setFullThemeData(theme) @@ -286,7 +223,6 @@ export function AgentsAppearanceTab() { systemDarkThemeId, setFullThemeData, setNextTheme, - importedThemes, ], ) @@ -336,16 +272,6 @@ export function AgentsAppearanceTab() { [setSystemDarkThemeId, resolvedTheme, selectedThemeId], ) - // Group imported themes by type - const importedDarkThemes = useMemo( - () => importedThemes.filter((t) => t.type === "dark"), - [importedThemes], - ) - const importedLightThemes = useMemo( - () => importedThemes.filter((t) => t.type === "light"), - [importedThemes], - ) - // Re-apply theme when system preference changes useEffect(() => { if (selectedThemeId === null && mounted) { @@ -378,7 +304,7 @@ export function AgentsAppearanceTab() { } return ( -
+
{/* Header - hidden on narrow screens since it's in the navigation bar */} {!isNarrowScreen && (
@@ -429,7 +355,7 @@ export function AgentsAppearanceTab() { )}
- + {/* System preference option */}
@@ -464,37 +390,6 @@ export function AgentsAppearanceTab() {
))} - - {/* Imported themes from VS Code / Cursor / Windsurf */} - {importedThemes.length > 0 && ( - <> - - - - From editors - - {importedThemes.map((theme) => ( - -
- - {theme.name} -
-
- ))} -
- - )} - - {/* Loading indicator */} - {isScanning && ( - <> - -
- - Loading themes from editors... -
- - )}
@@ -588,8 +483,7 @@ export function AgentsAppearanceTab() {
- - {/* Display Options Section */} + {/* Sidebar Section */}
@@ -605,20 +499,6 @@ export function AgentsAppearanceTab() { onCheckedChange={setShowWorkspaceIcon} />
-
-
- - Always expand to-do list - - - Show the full to-do list instead of compact view - -
- -
) diff --git a/src/renderer/components/dialogs/settings-tabs/agents-beta-tab.tsx b/src/renderer/components/dialogs/settings-tabs/agents-beta-tab.tsx index df286431..fc7fd03e 100644 --- a/src/renderer/components/dialogs/settings-tabs/agents-beta-tab.tsx +++ b/src/renderer/components/dialogs/settings-tabs/agents-beta-tab.tsx @@ -1,29 +1,6 @@ import { useAtom } from "jotai" -import { Check, Copy, RefreshCw } from "lucide-react" -import { useEffect, useState } from "react" -import { useQuery } from "@tanstack/react-query" -import { - autoOfflineModeAtom, - betaAutomationsEnabledAtom, - betaKanbanEnabledAtom, - betaUpdatesEnabledAtom, - enableTasksAtom, - historyEnabledAtom, - selectedOllamaModelAtom, - showOfflineModeFeaturesAtom, -} from "../../../lib/atoms" -import { trpc } from "../../../lib/trpc" -import { remoteTrpc } from "../../../lib/remote-trpc" -import { cn } from "../../../lib/utils" -import { Button } from "../../ui/button" -import { ExternalLinkIcon } from "../../ui/icons" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "../../ui/select" +import { useState, useEffect } from "react" +import { historyEnabledAtom } from "../../../lib/atoms" import { Switch } from "../../ui/switch" // Hook to detect narrow screen @@ -43,78 +20,9 @@ function useIsNarrowScreen(): boolean { return isNarrow } -const MINIMUM_OLLAMA_VERSION = "0.14.2" -const RECOMMENDED_MODEL = "qwen3-coder:30b" - export function AgentsBetaTab() { const isNarrowScreen = useIsNarrowScreen() const [historyEnabled, setHistoryEnabled] = useAtom(historyEnabledAtom) - const [showOfflineFeatures, setShowOfflineFeatures] = useAtom(showOfflineModeFeaturesAtom) - const [autoOffline, setAutoOffline] = useAtom(autoOfflineModeAtom) - const [selectedOllamaModel, setSelectedOllamaModel] = useAtom(selectedOllamaModelAtom) - const [kanbanEnabled, setKanbanEnabled] = useAtom(betaKanbanEnabledAtom) - const [automationsEnabled, setAutomationsEnabled] = useAtom(betaAutomationsEnabledAtom) - const [enableTasks, setEnableTasks] = useAtom(enableTasksAtom) - const [betaUpdatesEnabled, setBetaUpdatesEnabled] = useAtom(betaUpdatesEnabledAtom) - - // Check subscription to gate automations behind paid plan - const { data: subscription } = useQuery({ - queryKey: ["agents", "subscription"], - queryFn: () => remoteTrpc.agents.getAgentsSubscription.query(), - }) - const isPaidPlan = subscription?.type !== "free" && !!subscription?.type - const isDev = process.env.NODE_ENV === "development" - const canEnableAutomations = isPaidPlan || isDev - const [copied, setCopied] = useState(false) - const [updateStatus, setUpdateStatus] = useState<"idle" | "checking" | "available" | "not-available" | "error">("idle") - const [updateVersion, setUpdateVersion] = useState(null) - const [currentVersion, setCurrentVersion] = useState(null) - - // Get current version on mount and sync update channel state - useEffect(() => { - window.desktopApi?.getVersion().then(setCurrentVersion) - window.desktopApi?.getUpdateChannel().then((ch) => { - setBetaUpdatesEnabled(ch === "beta") - }) - }, []) - - // Check for updates with force flag to bypass cache - const handleCheckForUpdates = async () => { - // Check if we're in dev mode - const isPackaged = await window.desktopApi?.isPackaged?.() - if (!isPackaged) { - setUpdateStatus("error") - console.log("Update check skipped in dev mode") - return - } - - setUpdateStatus("checking") - setUpdateVersion(null) - try { - const result = await window.desktopApi?.checkForUpdates(true) - if (result) { - setUpdateStatus("available") - setUpdateVersion(result.version) - } else { - setUpdateStatus("not-available") - } - } catch (error) { - console.error("Failed to check for updates:", error) - setUpdateStatus("error") - } - } - - // Get Ollama status - const { data: ollamaStatus } = trpc.ollama.getStatus.useQuery(undefined, { - refetchInterval: showOfflineFeatures ? 30000 : false, // Only poll when feature is enabled - enabled: showOfflineFeatures, - }) - - const handleCopy = () => { - navigator.clipboard.writeText(`ollama pull ${RECOMMENDED_MODEL}`) - setCopied(true) - setTimeout(() => setCopied(false), 2000) - } return (
@@ -130,289 +38,22 @@ export function AgentsBetaTab() { {/* Beta Features Section */}
- {/* Rollback Toggle */} -
-
- - Rollback - - - Allow rolling back to previous messages and restoring files. - -
- -
- - {/* Offline Mode Toggle */} -
-
- - Offline Mode - - - Enable offline mode UI and Ollama integration. - -
- -
- - {/* Kanban Board Toggle */} -
-
- - Kanban Board - - - View workspaces as a Kanban board organized by status. - -
- -
- - {/* Automations & Inbox Toggle */} -
-
- - Automations & Inbox - - - {canEnableAutomations - ? "Automate workflows with GitHub and Linear triggers, and manage inbox notifications." - : "Requires a paid plan. Upgrade to enable automations and inbox."} - -
- { - if (canEnableAutomations) { - setAutomationsEnabled(checked) - } - }} - disabled={!canEnableAutomations} - /> -
- - {/* Agent Tasks Toggle */} -
-
- - Agent Tasks - - - Enable Task instead of legacy Todo system. - -
- -
-
- - {/* Offline Mode Settings - only show when feature is enabled */} - {showOfflineFeatures && ( -
-
-

Offline Mode Settings

-
- -
-
- {/* Status */} -
-
- - Ollama Status - -

- {ollamaStatus?.ollama.available - ? `Running - ${ollamaStatus.ollama.models.length} model${ollamaStatus.ollama.models.length !== 1 ? 's' : ''} installed` - : 'Not running or not installed'} -

-
-
- {ollamaStatus?.ollama.available ? ( - <> - - Available - - ) : ( - <> - - Unavailable - - )} -
-
- - {/* Model selector */} - {ollamaStatus?.ollama.available && ollamaStatus.ollama.models.length > 0 && ( -
-
- - Model - -

- Select which model to use for offline mode -

-
- -
- )} - - {/* Auto-fallback toggle */} -
-
- - Auto Offline Mode - -

- Automatically use Ollama when internet is unavailable -

-
- -
- - {/* Installation instructions - always show */} -
-

Setup Instructions:

-
    -
  1. - Install Ollama {MINIMUM_OLLAMA_VERSION}+ from{" "} - - ollama.com - - -
  2. -
  3. - Pull the recommended model:{" "} - - ollama pull {RECOMMENDED_MODEL} - - -
  4. -
  5. Ollama will run automatically in the background
  6. -
-
-
-
-
- )} - - {/* Updates Section */} -
-
-

Updates

-

- Check for new versions manually (bypasses CDN cache) -

-
- -
- {/* Early Access Toggle */} -
+
+ {/* Rollback Toggle */} +
- Early Access + Rollback - Receive beta versions before they're released to everyone. Beta versions may be less stable. + Allow rolling back to previous messages and restoring files.
{ - setBetaUpdatesEnabled(checked) - window.desktopApi?.setUpdateChannel(checked ? "beta" : "latest") - }} + checked={historyEnabled} + onCheckedChange={setHistoryEnabled} />
- - {/* Version & Check */} -
-
-
- - {currentVersion ? `Current: v${currentVersion}` : "Version"} - - - {updateStatus === "checking" && "Checking for updates..."} - {updateStatus === "available" && `Update available: v${updateVersion}`} - {updateStatus === "not-available" && "You're on the latest version"} - {updateStatus === "error" && "Failed to check (dev mode?)"} - {updateStatus === "idle" && "Click to check for updates"} - -
- -
-
diff --git a/src/renderer/components/dialogs/settings-tabs/agents-custom-agents-tab.tsx b/src/renderer/components/dialogs/settings-tabs/agents-custom-agents-tab.tsx index 4e2d9df1..f9aa3dab 100644 --- a/src/renderer/components/dialogs/settings-tabs/agents-custom-agents-tab.tsx +++ b/src/renderer/components/dialogs/settings-tabs/agents-custom-agents-tab.tsx @@ -1,18 +1,26 @@ -import { useEffect, useMemo, useRef, useState, useCallback } from "react" -import { useListKeyboardNav } from "./use-list-keyboard-nav" -import { useAtomValue } from "jotai" -import { selectedProjectAtom, settingsAgentsSidebarWidthAtom } from "../../../features/agents/atoms" +import { useState, useEffect } from "react" +import { ChevronRight, Trash2 } from "lucide-react" +import { motion, AnimatePresence } from "motion/react" import { trpc } from "../../../lib/trpc" import { cn } from "../../../lib/utils" -import { Plus } from "lucide-react" -import { CustomAgentIconFilled } from "../../ui/icons" -import { Input } from "../../ui/input" -import { Label } from "../../ui/label" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../ui/select" -import { Textarea } from "../../ui/textarea" -import { Button } from "../../ui/button" -import { ResizableSidebar } from "../../ui/resizable-sidebar" -import { toast } from "sonner" +import { AgentIcon } from "../../ui/icons" + +// Hook to detect narrow screen +function useIsNarrowScreen(): boolean { + const [isNarrow, setIsNarrow] = useState(false) + + useEffect(() => { + const checkWidth = () => { + setIsNarrow(window.innerWidth <= 768) + } + + checkWidth() + window.addEventListener("resize", checkWidth) + return () => window.removeEventListener("resize", checkWidth) + }, []) + + return isNarrow +} interface FileAgent { name: string @@ -25,562 +33,261 @@ interface FileAgent { path: string } -// --- Detail Panel (Editable) --- -function AgentDetail({ - agent, - onSave, - isSaving, -}: { - agent: FileAgent - onSave: (data: { description: string; prompt: string; model?: "sonnet" | "opus" | "haiku" | "inherit" }) => void - isSaving: boolean -}) { - const [description, setDescription] = useState(agent.description) - const [prompt, setPrompt] = useState(agent.prompt) - const [model, setModel] = useState(agent.model || "inherit") - - // Reset local state when agent changes - useEffect(() => { - setDescription(agent.description) - setPrompt(agent.prompt) - setModel(agent.model || "inherit") - }, [agent.name, agent.description, agent.prompt, agent.model]) +export function AgentsCustomAgentsTab() { + const isNarrowScreen = useIsNarrowScreen() + const [expandedAgentName, setExpandedAgentName] = useState(null) - const hasChanges = - description !== agent.description || - prompt !== agent.prompt || - model !== (agent.model || "inherit") + const utils = trpc.useUtils() + const { data: agents = [], isLoading } = trpc.agents.listEnabled.useQuery( + undefined, + { staleTime: 0 } // Always refetch when settings opens + ) - const handleSave = useCallback(() => { - if ( - description !== agent.description || - prompt !== agent.prompt || - model !== (agent.model || "inherit") - ) { - onSave({ - description, - prompt, - model: model as FileAgent["model"], - }) - } - }, [description, prompt, model, agent.description, agent.prompt, agent.model, onSave]) + const openInFinderMutation = trpc.external.openInFinder.useMutation() - const handleBlur = useCallback(() => { - if ( - description !== agent.description || - prompt !== agent.prompt || - model !== (agent.model || "inherit") - ) { - onSave({ - description, - prompt, - model: model as FileAgent["model"], - }) - } - }, [description, prompt, model, agent.description, agent.prompt, agent.model, onSave]) + const deleteAgentMutation = trpc.agents.delete.useMutation({ + onSuccess: () => { + utils.agents.listEnabled.invalidate() + }, + onError: (error) => { + console.error("Failed to delete agent:", error.message) + }, + }) - const handleModelChange = useCallback((value: string) => { - setModel(value) - // Auto-save with new model value - if ( - description !== agent.description || - prompt !== agent.prompt || - value !== (agent.model || "inherit") - ) { - onSave({ - description, - prompt, - model: value as FileAgent["model"], - }) - } - }, [description, prompt, agent.description, agent.prompt, agent.model, onSave]) + const userAgents = agents.filter((a) => a.source === "user") - return ( -
-
- {/* Header */} -
-
-

{agent.name}

-

{agent.path}

-
- {hasChanges && ( - - )} -
+ const handleExpandAgent = (agentName: string) => { + setExpandedAgentName(expandedAgentName === agentName ? null : agentName) + } - {/* Description */} -
- - setDescription(e.target.value)} - onBlur={handleBlur} - placeholder="Agent description..." - /> -
+ const handleOpenInFinder = (path: string) => { + openInFinderMutation.mutate(path) + } - {/* Model */} -
- - -
+ const handleDeleteAgent = (agent: FileAgent) => { + deleteAgentMutation.mutate({ + name: agent.name, + source: agent.source, + }) + } - {/* Tools (read-only) */} - {agent.tools && agent.tools.length > 0 && ( -
- -
- {agent.tools.map((tool) => ( - - {tool} - - ))} + return ( +
+ {/* Header - hidden on narrow screens */} + {!isNarrowScreen && ( +
+
+
+

Custom Agents

+ + Beta +
+ + Documentation +
- )} +
+ )} - {/* Disallowed Tools (read-only) */} - {agent.disallowedTools && agent.disallowedTools.length > 0 && ( -
- -
- {agent.disallowedTools.map((tool) => ( - - {tool} - - ))} + {/* Agents List */} +
+ {isLoading ? ( +
+ Loading agents... +
+ ) : agents.length === 0 ? ( +
+ +

+ No custom agents yet +

+

+ Use /create-agent in chat to create one, or manually add .md files to ~/.claude/agents/ or .claude/agents/ +

+
+ ) : ( +
+
+ ~/.claude/agents/ +
+
+
+ {userAgents.map((agent) => ( + handleExpandAgent(agent.name)} + onOpenInFinder={() => handleOpenInFinder(agent.path)} + onDelete={() => handleDeleteAgent(agent)} + /> + ))} +
)} - - {/* System Prompt */} -
- -