From fc619bec0c0d1dd91536ce2ae8ba2860e042e2a5 Mon Sep 17 00:00:00 2001 From: buddhathemanoj Date: Fri, 23 Jan 2026 05:40:32 +0530 Subject: [PATCH 01/26] worked locally using azure --- .env.example | 22 +- CLAUDE.md | 77 +++--- README.md | 28 +- bun.lock | 13 +- package.json | 20 +- scripts/download-claude-binary.mjs | 15 +- src/main/auth-manager.ts | 267 ++++-------------- src/main/auth-store.ts | 141 +++------- src/main/index.ts | 144 ++-------- src/main/lib/auto-updater.ts | 2 +- src/main/lib/claude/env.ts | 16 +- src/main/lib/config.ts | 9 +- src/main/lib/trpc/routers/claude-code.ts | 307 +++++---------------- src/main/lib/trpc/routers/claude.ts | 328 +++++++++++++++++------ src/main/lib/trpc/routers/debug.ts | 2 +- src/main/windows/main.ts | 58 +--- src/renderer/index.html | 4 +- src/renderer/login.html | 2 +- 18 files changed, 542 insertions(+), 913 deletions(-) diff --git a/.env.example b/.env.example index 4c7643a80..4ce0ebe59 100644 --- a/.env.example +++ b/.env.example @@ -28,6 +28,24 @@ VITE_POSTHOG_HOST=https://us.i.posthog.com # Set this in hosted builds to use a private feedback channel # VITE_FEEDBACK_URL=https://discord.gg/your-private-invite -# API URL (optional - defaults to https://21st.dev) -# Only change this if you're running the web app locally +# API URL (optional - no longer used for auth) # MAIN_VITE_API_URL=http://localhost:3000 + +# ============================================================================= +# Microsoft Foundry Claude Configuration (REQUIRED for Claude functionality) +# ============================================================================= +# Get these from your Azure AI Foundry deployment +# See: https://code.claude.com/docs/en/microsoft-foundry + +# Enable Microsoft Foundry integration +MAIN_VITE_CLAUDE_CODE_USE_FOUNDRY=1 + +# Your Azure resource name (from the Azure portal) +# Example: if your endpoint is https://my-resource.services.ai.azure.com, use "my-resource" +MAIN_VITE_ANTHROPIC_FOUNDRY_RESOURCE=your-resource-name + +# Your Azure API key +MAIN_VITE_ANTHROPIC_FOUNDRY_API_KEY=your-api-key-here + +# Model deployment names (must match your Azure deployment names) +MAIN_VITE_ANTHROPIC_DEFAULT_OPUS_MODEL=claude-opus-4-5 diff --git a/CLAUDE.md b/CLAUDE.md index 30b1de5d3..59ac77327 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## What is this? -**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.). +**kcode** - 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.). ## Commands @@ -30,7 +30,7 @@ bun run db:push # Push schema directly (dev only) src/ ├── main/ # Electron main process │ ├── index.ts # App entry, window lifecycle -│ ├── auth-manager.ts # OAuth flow, token refresh +│ ├── auth-manager.ts # Azure credentials management │ ├── auth-store.ts # Encrypted credential storage (safeStorage) │ ├── windows/main.ts # Window creation, IPC handlers │ └── lib/ @@ -101,10 +101,29 @@ const projectChats = db.select().from(chats).where(eq(chats.projectId, id)).all( - **React Query**: Server state via tRPC (auto-caching, refetch) ### Claude Integration -- Dynamic import of `@anthropic-ai/claude-code` SDK +- Dynamic import of `@anthropic-ai/claude-agent-sdk` - Two modes: "plan" (read-only) and "agent" (full permissions) - Session resume via `sessionId` stored in SubChat - Message streaming via tRPC subscription (`claude.onMessage`) +- Uses Azure Claude API credentials stored locally + +## Authentication + +kcode uses Azure Claude API credentials instead of OAuth: + +```typescript +// AzureConfig interface (auth-store.ts) +interface AzureConfig { + endpoint: string // https://your-resource.openai.azure.com + apiKey: string // Azure API key + deploymentName: string // Claude deployment name +} +``` + +Credentials are: +- Stored encrypted using Electron's `safeStorage` API +- Managed via `AuthManager` class in `auth-manager.ts` +- Configured through settings UI in the renderer ## Tech Stack @@ -115,7 +134,7 @@ const projectChats = db.select().from(chats).where(eq(chats.projectId, id)).all( | Components | Radix UI, Lucide icons, Motion, Sonner | | State | Jotai, Zustand, React Query | | Backend | tRPC, Drizzle ORM, better-sqlite3 | -| AI | @anthropic-ai/claude-code | +| AI | @anthropic-ai/claude-agent-sdk | | Package Manager | bun | ## File Naming @@ -130,46 +149,38 @@ const projectChats = db.select().from(chats).where(eq(chats.projectId, id)).all( - `electron.vite.config.ts` - Build config (main/preload/renderer entries) - `src/main/lib/db/schema/index.ts` - Drizzle schema (source of truth) - `src/main/lib/db/index.ts` - DB initialization + auto-migrate +- `src/main/auth-manager.ts` - Azure credentials management +- `src/main/auth-store.ts` - Encrypted credential storage - `src/renderer/features/agents/atoms/index.ts` - Agent UI state atoms - `src/renderer/features/agents/main/active-chat.tsx` - Main chat component - `src/main/lib/trpc/routers/claude.ts` - Claude SDK integration ## Debugging First Install Issues -When testing auth flows or behavior for new users, you need to simulate a fresh install: +When testing behavior for new users, you need to simulate a fresh install: ```bash -# 1. Clear all app data (auth, database, settings) -rm -rf ~/Library/Application\ Support/Agents\ Dev/ +# 1. Clear all app data (config, database, settings) +rm -rf ~/Library/Application\ Support/kcode\ Dev/ # 2. Reset macOS protocol handler registration (if testing deep links) /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -kill -r -domain local -domain system -domain user # 3. Clear app preferences -defaults delete dev.21st.agents.dev # Dev mode -defaults delete dev.21st.agents # Production +defaults delete io.kosal.kcode.dev # Dev mode +defaults delete io.kosal.kcode # Production # 4. Run in dev mode with clean state -cd apps/desktop bun run dev ``` -**Common First-Install Bugs:** -- **OAuth deep link not working**: macOS Launch Services may not immediately recognize protocol handlers on first app launch. User may need to click "Sign in" again after the first attempt. -- **Folder dialog not appearing**: Window focus timing issues on first launch. Fixed by ensuring window focus before showing `dialog.showOpenDialog()`. - **Dev vs Production App:** -- Dev mode uses `twentyfirst-agents-dev://` protocol -- Dev mode uses separate userData path (`~/Library/Application Support/Agents Dev/`) +- Dev mode uses `kcode-dev://` protocol +- Dev mode uses separate userData path (`~/Library/Application Support/kcode Dev/`) - This prevents conflicts between dev and production installs ## Releasing a New Version -### Prerequisites for Notarization - -- Keychain profile: `21st-notarize` -- Create with: `xcrun notarytool store-credentials "21st-notarize" --apple-id YOUR_APPLE_ID --team-id YOUR_TEAM_ID` - ### Release Commands ```bash @@ -180,7 +191,6 @@ bun run release bun run build # Compile TypeScript bun run package:mac # Build & sign macOS app bun run dist:manifest # Generate latest-mac.yml manifests -./scripts/upload-release-wrangler.sh # Submit notarization & upload to R2 CDN ``` ### Bump Version Before Release @@ -189,29 +199,20 @@ bun run dist:manifest # Generate latest-mac.yml manifests npm version patch --no-git-tag-version # 0.0.27 → 0.0.28 ``` -### After Release Script Completes - -1. Wait for notarization (2-5 min): `xcrun notarytool history --keychain-profile "21st-notarize"` -2. Staple DMGs: `cd release && xcrun stapler staple *.dmg` -3. Re-upload stapled DMGs to R2 and GitHub (see RELEASE.md for commands) -4. Update changelog: `gh release edit v0.0.X --notes "..."` -5. **Upload manifests (triggers auto-updates!)** — see RELEASE.md -6. Sync to public: `./scripts/sync-to-public.sh` - ### Files Uploaded to CDN | File | Purpose | |------|---------| | `latest-mac.yml` | Manifest for arm64 auto-updates | | `latest-mac-x64.yml` | Manifest for Intel auto-updates | -| `1Code-{version}-arm64-mac.zip` | Auto-update payload (arm64) | -| `1Code-{version}-mac.zip` | Auto-update payload (Intel) | -| `1Code-{version}-arm64.dmg` | Manual download (arm64) | -| `1Code-{version}.dmg` | Manual download (Intel) | +| `kcode-{version}-arm64-mac.zip` | Auto-update payload (arm64) | +| `kcode-{version}-mac.zip` | Auto-update payload (Intel) | +| `kcode-{version}-arm64.dmg` | Manual download (arm64) | +| `kcode-{version}.dmg` | Manual download (Intel) | ### Auto-Update Flow -1. App checks `https://cdn.21st.dev/releases/desktop/latest-mac.yml` on startup and when window regains focus (with 1 min cooldown) +1. App checks `https://cdn.kosal.io/releases/kcode/latest-mac.yml` on startup and when window regains focus (with 1 min cooldown) 2. If version in manifest > current version, shows "Update Available" banner 3. User clicks Download → downloads ZIP in background 4. User clicks "Restart Now" → installs update and restarts @@ -222,12 +223,12 @@ npm version patch --no-git-tag-version # 0.0.27 → 0.0.28 - Drizzle ORM setup with schema (projects, chats, sub_chats) - Auto-migration on app startup - tRPC routers structure +- Azure Claude credentials integration **In Progress:** +- Settings UI for Azure credentials configuration - Replacing `mock-api.ts` with real tRPC calls in renderer -- ProjectSelector component (local folder picker) **Planned:** - Git worktree per chat (isolation) - Claude Code execution in worktree path -- Full feature parity with web app diff --git a/README.md b/README.md index ca25dbbc7..dc621d3b9 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,8 @@ -# 1Code - -[1Code.dev](https://1code.dev) +# kcode Best UI for Claude Code with local and remote agent execution. -By [21st.dev](https://21st.dev) team +By [Kosal](https://kosal.io) > **Note:** Currently tested on macOS and Linux. Windows support is experimental and may have issues. @@ -14,8 +12,6 @@ By [21st.dev](https://21st.dev) team Run agents locally, in worktrees, in background — without touching main branch. -![Worktree Demo](assets/worktree.gif) - - **Git Worktree Isolation** - Each chat session runs in its own isolated worktree - **Background Execution** - Run agents in background while you continue working - **Local-first** - All code stays on your machine, no cloud sync required @@ -27,8 +23,6 @@ Run agents locally, in worktrees, in background — without touching main branch Cursor-like UI for Claude Code with diff previews, built-in git client, and the ability to see changes before they land. -![Cursor UI Demo](assets/cursor-ui.gif) - - **Diff Previews** - See exactly what changes Claude is making in real-time - **Built-in Git Client** - Stage, commit, and manage branches without leaving the app - **Change Tracking** - Visual diffs and PR management @@ -40,8 +34,6 @@ Cursor-like UI for Claude Code with diff previews, built-in git client, and the Claude asks clarifying questions, builds structured plans, and shows clean markdown preview — all before execution. -![Plan Mode Demo](assets/plan-mode.gif) - - **Clarifying Questions** - Claude asks what it needs to know before starting - **Structured Plans** - See step-by-step breakdown of what will happen - **Clean Markdown Preview** - Review plans in readable format @@ -57,7 +49,7 @@ Claude asks clarifying questions, builds structured plans, and shows clean markd ## Installation -### Option 1: Build from source (free) +### Build from source ```bash # Prerequisites: Bun, Python, Xcode Command Line Tools (macOS) @@ -69,11 +61,15 @@ bun run package:mac # or package:win, package:linux > **Important:** The `claude:download` step downloads the Claude CLI binary which is required for the agent chat to work. If you skip this step, the app will build but agent functionality won't work. -### Option 2: Subscribe to 1code.dev (recommended) +## Configuration -Get pre-built releases + background agents support by subscribing at [1code.dev](https://1code.dev). +kcode uses Azure Claude API credentials. On first launch, you'll need to configure: -Your subscription helps us maintain and improve 1Code. +1. **Endpoint** - Your Azure OpenAI endpoint (e.g., `https://your-resource.openai.azure.com`) +2. **API Key** - Your Azure API key +3. **Deployment Name** - Your Claude model deployment name + +These credentials are stored securely using your system's keychain. ## Development @@ -83,10 +79,6 @@ bun run claude:download # First time only bun run dev ``` -## Feedback & Community - -Join our [Discord](https://discord.gg/8ektTZGnj4) for support and discussions. - ## License Apache License 2.0 - see [LICENSE](LICENSE) for details. diff --git a/bun.lock b/bun.lock index e89145a05..1cff585b5 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "name": "21st-desktop", "dependencies": { "@ai-sdk/react": "^3.0.14", - "@anthropic-ai/claude-agent-sdk": "^0.2.5", + "@anthropic-ai/claude-agent-sdk": "^0.2.12", "@git-diff-view/react": "^0.0.35", "@git-diff-view/shiki": "^0.0.36", "@radix-ui/react-accordion": "^1.2.11", @@ -61,6 +61,7 @@ "react-hotkeys-hook": "^4.6.1", "react-icons": "^5.5.0", "react-syntax-highlighter": "^16.1.0", + "remark-breaks": "^4.0.0", "shiki": "^1.24.4", "simple-git": "^3.28.0", "sonner": "^1.7.1", @@ -109,7 +110,7 @@ "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], - "@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=="], + "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.14", "", { "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-mkjWCT2N3IMMH27AWFm+PWBjreUbBrOnrXs2ffSoq3mZjRZFQk529z5ZYLzJa/NJzgpnyxWe7+cbQiN2h1BCag=="], "@apm-js-collab/code-transformer": ["@apm-js-collab/code-transformer@0.8.2", "", {}, "sha512-YRjJjNq5KFSjDUoqu5pFUWrrsvGOxl6c3bu+uMFc9HNNptZ2rNU/TI2nLw4jnhQNtka972Ee2m3uqbvDQtPeCA=="], @@ -1003,7 +1004,7 @@ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], @@ -1327,6 +1328,8 @@ "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=="], @@ -1643,6 +1646,8 @@ "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=="], @@ -2027,7 +2032,7 @@ "make-fetch-happen/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], - "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + "matcher/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], diff --git a/package.json b/package.json index ae1e992a6..94121ee6d 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { - "name": "21st-desktop", + "name": "kcode-desktop", "version": "0.0.27", "private": true, - "description": "1Code - UI for parallel work with AI agents", + "description": "kcode - UI for parallel work with AI agents", "author": { - "name": "21st.dev", - "email": "support@21st.dev" + "name": "Kosal", + "email": "info@kosal.io" }, "main": "out/main/index.js", "scripts": { @@ -123,14 +123,14 @@ "vite": "^6.3.4" }, "build": { - "appId": "dev.21st.agents", - "productName": "1Code", + "appId": "io.kosal.kcode", + "productName": "kcode", "npmRebuild": true, "protocols": [ { - "name": "1Code", + "name": "kcode", "schemes": [ - "twentyfirst-agents" + "kcode" ] } ], @@ -231,7 +231,7 @@ }, "publish": { "provider": "generic", - "url": "https://cdn.21st.dev/releases/desktop" + "url": "https://cdn.kosal.io/releases/kcode" } } -} +} \ No newline at end of file diff --git a/scripts/download-claude-binary.mjs b/scripts/download-claude-binary.mjs index c6ba1752f..bc2af8ae6 100644 --- a/scripts/download-claude-binary.mjs +++ b/scripts/download-claude-binary.mjs @@ -130,24 +130,21 @@ function calculateSha256(filePath) { * 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 { - // 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] + // Fetch the "latest" file from GCS which contains the current stable version + const response = await fetch(`${DIST_BASE}/latest`) + const version = (await response.text()).trim() + if (version && /^\d+\.\d+\.\d+$/.test(version)) { + return version } } catch { // Fallback } // Fallback to known version - return "2.1.5" + return "2.1.17" } /** diff --git a/src/main/auth-manager.ts b/src/main/auth-manager.ts index 82533fe61..0038d4cad 100644 --- a/src/main/auth-manager.ts +++ b/src/main/auth-manager.ts @@ -1,253 +1,80 @@ -import { AuthStore, AuthData, AuthUser } from "./auth-store" -import { app, BrowserWindow } from "electron" - -// Get API URL - in packaged app always use production, in dev allow override -function getApiBaseUrl(): string { - if (app.isPackaged) { - return "https://21st.dev" - } - return import.meta.env.MAIN_VITE_API_URL || "https://21st.dev" -} - +/** + * Simplified Auth Manager for Microsoft Foundry Claude credentials + * Reads configuration from environment variables (via import.meta.env for electron-vite) + * See: https://code.claude.com/docs/en/microsoft-foundry + */ export class AuthManager { - private store: AuthStore - private refreshTimer?: NodeJS.Timeout - private isDev: boolean - private onTokenRefresh?: (authData: AuthData) => void - - constructor(isDev: boolean = false) { - this.store = new AuthStore(app.getPath("userData")) - this.isDev = isDev - - // Schedule refresh if already authenticated - if (this.store.isAuthenticated()) { - this.scheduleRefresh() - } - } - - /** - * Set callback to be called when token is refreshed - * This allows the main process to update cookies when tokens change - */ - setOnTokenRefresh(callback: (authData: AuthData) => void): void { - this.onTokenRefresh = callback - } - - private getApiUrl(): string { - return getApiBaseUrl() + constructor(_isDev: boolean = false) { + // No-op - we use environment variables now } /** - * Exchange auth code for session tokens - * Called after receiving code via deep link + * Check if Foundry credentials are configured via environment variables */ - async exchangeCode(code: string): Promise { - const response = await fetch(`${this.getApiUrl()}/api/auth/desktop/exchange`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - code, - deviceInfo: this.getDeviceInfo(), - }), - }) - - if (!response.ok) { - const error = await response.json().catch(() => ({ error: "Unknown error" })) - throw new Error(error.error || `Exchange failed: ${response.status}`) - } - - const data = await response.json() - - const authData: AuthData = { - token: data.token, - refreshToken: data.refreshToken, - expiresAt: data.expiresAt, - user: data.user, - } - - this.store.save(authData) - this.scheduleRefresh() - - return authData + isAuthenticated(): boolean { + // In electron-vite, MAIN_VITE_ prefixed vars are available via import.meta.env + const useFoundry = import.meta.env.MAIN_VITE_CLAUDE_CODE_USE_FOUNDRY + const resource = import.meta.env.MAIN_VITE_ANTHROPIC_FOUNDRY_RESOURCE + const apiKey = import.meta.env.MAIN_VITE_ANTHROPIC_FOUNDRY_API_KEY + + console.log("[AuthManager] isAuthenticated() check:") + console.log("[AuthManager] MAIN_VITE_CLAUDE_CODE_USE_FOUNDRY:", useFoundry || "(not set)") + console.log("[AuthManager] MAIN_VITE_ANTHROPIC_FOUNDRY_RESOURCE:", resource || "(not set)") + console.log("[AuthManager] MAIN_VITE_ANTHROPIC_FOUNDRY_API_KEY:", apiKey ? `${apiKey.slice(0, 8)}...` : "(not set)") + + const result = !!(useFoundry && resource && apiKey) + console.log("[AuthManager] Result:", result ? "✓ Authenticated (Foundry)" : "✗ Not authenticated") + return result } /** - * Get device info for session tracking + * Get Foundry configuration from environment variables */ - private getDeviceInfo(): string { - const platform = process.platform - const arch = process.arch - const version = app.getVersion() - return `21st Desktop ${version} (${platform} ${arch})` - } + getConfig(): { resource: string; apiKey: string; model: string } | null { + const useFoundry = import.meta.env.MAIN_VITE_CLAUDE_CODE_USE_FOUNDRY + const resource = import.meta.env.MAIN_VITE_ANTHROPIC_FOUNDRY_RESOURCE + const apiKey = import.meta.env.MAIN_VITE_ANTHROPIC_FOUNDRY_API_KEY + const model = import.meta.env.MAIN_VITE_ANTHROPIC_DEFAULT_OPUS_MODEL - /** - * Get a valid token, refreshing if necessary - */ - async getValidToken(): Promise { - if (!this.store.isAuthenticated()) { + if (!useFoundry || !resource || !apiKey) { return null } - if (this.store.needsRefresh()) { - await this.refresh() - } - - return this.store.getToken() - } - - /** - * Refresh the current session - */ - async refresh(): Promise { - const refreshToken = this.store.getRefreshToken() - if (!refreshToken) { - console.warn("No refresh token available") - return false - } - - try { - const response = await fetch(`${this.getApiUrl()}/api/auth/desktop/refresh`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ refreshToken }), - }) - - if (!response.ok) { - console.error("Refresh failed:", response.status) - // If refresh fails, clear auth and require re-login - if (response.status === 401) { - this.logout() - } - return false - } - - const data = await response.json() - - const authData: AuthData = { - token: data.token, - refreshToken: data.refreshToken, - expiresAt: data.expiresAt, - user: data.user, - } - - this.store.save(authData) - this.scheduleRefresh() - - // Notify callback about token refresh (so cookie can be updated) - if (this.onTokenRefresh) { - this.onTokenRefresh(authData) - } - - return true - } catch (error) { - console.error("Refresh error:", error) - return false - } + return { resource, apiKey, model: model || "claude-opus-4-5" } } /** - * Schedule token refresh before expiration + * Get user info (stub for compatibility - returns null since we use API keys) */ - private scheduleRefresh(): void { - if (this.refreshTimer) { - clearTimeout(this.refreshTimer) - } - - const authData = this.store.load() - if (!authData) return - - const expiresAt = new Date(authData.expiresAt).getTime() - const now = Date.now() - - // Refresh 5 minutes before expiration - const refreshIn = Math.max(0, expiresAt - now - 5 * 60 * 1000) - - this.refreshTimer = setTimeout(() => { - this.refresh() - }, refreshIn) - - console.log(`Scheduled token refresh in ${Math.round(refreshIn / 1000 / 60)} minutes`) + getUser(): null { + return null } /** - * Check if user is authenticated + * Logout - no-op since we use env vars */ - isAuthenticated(): boolean { - return this.store.isAuthenticated() + logout(): void { + console.log("[Auth] Logout called but using env vars - no action needed") } - /** - * Get current user - */ - getUser(): AuthUser | null { - return this.store.getUser() + // Legacy methods for compatibility - these are no-ops now + setOnTokenRefresh(_callback: (authData: any) => void): void { + // No-op } - /** - * Get current auth data - */ - getAuth(): AuthData | null { - return this.store.load() + startAuthFlow(_mainWindow: any): void { + console.log("[Auth] startAuthFlow called but using env vars - configure in .env.local") } - /** - * Logout and clear stored credentials - */ - logout(): void { - if (this.refreshTimer) { - clearTimeout(this.refreshTimer) - this.refreshTimer = undefined - } - this.store.clear() + async getValidToken(): Promise { + return import.meta.env.MAIN_VITE_AZURE_API_KEY || null } - /** - * Start auth flow by opening browser - */ - startAuthFlow(mainWindow: BrowserWindow | null): void { - const { shell } = require("electron") - - let authUrl = `${this.getApiUrl()}/auth/desktop?auto=true` - - // 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:21321/auth/callback")}` - // Pass dev protocol so production web can use correct deep link if callback fails - authUrl += `&protocol=twentyfirst-agents-dev` - } - - shell.openExternal(authUrl) + async updateUser(_updates: { name?: string }): Promise { + return null } - /** - * Update user profile on server and locally - */ - async updateUser(updates: { name?: string }): Promise { - const token = await this.getValidToken() - if (!token) { - throw new Error("Not authenticated") - } - - // Update on server using X-Desktop-Token header - const response = await fetch(`${this.getApiUrl()}/api/user/profile`, { - method: "PATCH", - headers: { - "Content-Type": "application/json", - "X-Desktop-Token": token, - }, - body: JSON.stringify({ - display_name: updates.name, - }), - }) - - if (!response.ok) { - const error = await response.json().catch(() => ({ error: "Unknown error" })) - throw new Error(error.error || `Update failed: ${response.status}`) - } - - // Update locally - return this.store.updateUser({ name: updates.name ?? null }) + saveConfig(_config: any): void { + console.log("[Auth] saveConfig called but using env vars - configure in .env.local") } } diff --git a/src/main/auth-store.ts b/src/main/auth-store.ts index cd4a38812..58c50295f 100644 --- a/src/main/auth-store.ts +++ b/src/main/auth-store.ts @@ -2,23 +2,14 @@ import { readFileSync, writeFileSync, existsSync, unlinkSync, mkdirSync } from " import { join, dirname } from "path" import { safeStorage } from "electron" -export interface AuthUser { - id: string - email: string - name: string | null - imageUrl: string | null - username: string | null -} - -export interface AuthData { - token: string - refreshToken: string - expiresAt: string - user: AuthUser +export interface AzureConfig { + endpoint: string // https://your-resource.openai.azure.com + apiKey: string // Azure API key + deploymentName: string // Claude deployment name } /** - * Storage for desktop authentication tokens + * Storage for Azure Claude credentials * Uses Electron's safeStorage API to encrypt sensitive data using OS keychain * Falls back to plaintext only if encryption is unavailable (rare edge case) */ @@ -26,7 +17,7 @@ export class AuthStore { private filePath: string constructor(userDataPath: string) { - this.filePath = join(userDataPath, "auth.dat") // .dat for encrypted data + this.filePath = join(userDataPath, "azure-config.dat") // .dat for encrypted data } /** @@ -37,36 +28,36 @@ export class AuthStore { } /** - * Save authentication data (encrypted if possible) + * Save Azure configuration (encrypted if possible) */ - save(data: AuthData): void { + save(config: AzureConfig): void { try { const dir = dirname(this.filePath) if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }) } - const jsonData = JSON.stringify(data) - + const jsonData = JSON.stringify(config) + if (this.isEncryptionAvailable()) { // Encrypt using OS keychain (macOS Keychain, Windows DPAPI, Linux Secret Service) const encrypted = safeStorage.encryptString(jsonData) writeFileSync(this.filePath, encrypted) } else { // Fallback: store with warning (should rarely happen) - console.warn("safeStorage not available - storing auth data without encryption") + console.warn("safeStorage not available - storing config without encryption") writeFileSync(this.filePath + ".json", jsonData, "utf-8") } } catch (error) { - console.error("Failed to save auth data:", error) + console.error("Failed to save Azure config:", error) throw error } } /** - * Load authentication data (decrypts if encrypted) + * Load Azure configuration (decrypts if encrypted) */ - load(): AuthData | null { + load(): AzureConfig | null { try { // Try encrypted file first if (existsSync(this.filePath) && this.isEncryptionAvailable()) { @@ -74,45 +65,31 @@ export class AuthStore { const decrypted = safeStorage.decryptString(encrypted) return JSON.parse(decrypted) } - + // Fallback: try unencrypted file (for migration or when encryption unavailable) const fallbackPath = this.filePath + ".json" if (existsSync(fallbackPath)) { const content = readFileSync(fallbackPath, "utf-8") - const data = JSON.parse(content) - + const config = JSON.parse(content) + // Migrate to encrypted storage if now available if (this.isEncryptionAvailable()) { - this.save(data) + this.save(config) unlinkSync(fallbackPath) // Remove unencrypted file after migration } - - return data - } - - // Legacy: check for old auth.json file and migrate - const legacyPath = join(dirname(this.filePath), "auth.json") - if (existsSync(legacyPath)) { - const content = readFileSync(legacyPath, "utf-8") - const data = JSON.parse(content) - - // Migrate to encrypted storage - this.save(data) - unlinkSync(legacyPath) // Remove legacy unencrypted file - console.log("Migrated auth data from plaintext to encrypted storage") - - return data + + return config } return null } catch { - console.error("Failed to load auth data") + console.error("Failed to load Azure config") return null } } /** - * Clear all stored authentication data (both encrypted and fallback files) + * Clear stored Azure configuration */ clear(): void { try { @@ -125,78 +102,26 @@ export class AuthStore { if (existsSync(fallbackPath)) { unlinkSync(fallbackPath) } - // Remove legacy file if exists - const legacyPath = join(dirname(this.filePath), "auth.json") - if (existsSync(legacyPath)) { - unlinkSync(legacyPath) - } } catch (error) { - console.error("Failed to clear auth data:", error) + console.error("Failed to clear Azure config:", error) } } /** - * Check if user is authenticated + * Check if Azure credentials are configured */ - isAuthenticated(): boolean { - const data = this.load() - if (!data) return false - - // Check if token is expired - const expiresAt = new Date(data.expiresAt).getTime() - return expiresAt > Date.now() + isConfigured(): boolean { + const config = this.load() + return config !== null && + config.endpoint.length > 0 && + config.apiKey.length > 0 && + config.deploymentName.length > 0 } /** - * Get current user if authenticated + * Get current configuration */ - getUser(): AuthUser | null { - const data = this.load() - return data?.user ?? null - } - - /** - * Get current token if valid - */ - getToken(): string | null { - const data = this.load() - if (!data) return null - - const expiresAt = new Date(data.expiresAt).getTime() - if (expiresAt <= Date.now()) return null - - return data.token - } - - /** - * Get refresh token - */ - getRefreshToken(): string | null { - const data = this.load() - return data?.refreshToken ?? null - } - - /** - * Check if token needs refresh (expires in less than 5 minutes) - */ - needsRefresh(): boolean { - const data = this.load() - if (!data) return false - - const expiresAt = new Date(data.expiresAt).getTime() - const fiveMinutes = 5 * 60 * 1000 - return expiresAt - Date.now() < fiveMinutes - } - - /** - * Update user data (e.g., after profile update) - */ - updateUser(updates: Partial): AuthUser | null { - const data = this.load() - if (!data) return null - - data.user = { ...data.user, ...updates } - this.save(data) - return data.user + getConfig(): AzureConfig | null { + return this.load() } } diff --git a/src/main/index.ts b/src/main/index.ts index 74ed6b77b..7d080eb40 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,16 +1,14 @@ -import { app, BrowserWindow, session, Menu } from "electron" +import { app, BrowserWindow, Menu } from "electron" import { join } from "path" 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 { createMainWindow, getWindow } from "./windows/main" import { AuthManager } from "./auth-manager" import { initAnalytics, - identify, trackAppOpened, - trackAuthCompleted, shutdown as shutdownAnalytics, } from "./lib/analytics" import { @@ -26,13 +24,13 @@ 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 -const PROTOCOL = IS_DEV ? "twentyfirst-agents-dev" : "twentyfirst-agents" +const PROTOCOL = IS_DEV ? "kcode-dev" : "kcode" // Set dev mode userData path BEFORE requestSingleInstanceLock() // This ensures dev and prod have separate instance locks if (IS_DEV) { const { join } = require("path") - const devUserData = join(app.getPath("userData"), "..", "Agents Dev") + const devUserData = join(app.getPath("userData"), "..", "kcode Dev") app.setPath("userData", devUserData) console.log("[Dev] Using separate userData path:", devUserData) } @@ -57,17 +55,13 @@ if (app.isPackaged && !IS_DEV) { } // URL configuration (exported for use in other modules) -// In packaged app, ALWAYS use production URL to prevent localhost leaking into releases -// In dev mode, allow override via MAIN_VITE_API_URL env variable +// These are now deprecated as we use local Azure credentials export function getBaseUrl(): string { - if (app.isPackaged) { - return "https://21st.dev" - } - return import.meta.env.MAIN_VITE_API_URL || "https://21st.dev" + return "https://code.kosal.io" } export function getAppUrl(): string { - return process.env.ELECTRON_RENDERER_URL || "https://21st.dev/agents" + return process.env.ELECTRON_RENDERER_URL || "https://code.kosal.io" } // Auth manager singleton @@ -77,74 +71,17 @@ export function getAuthManager(): AuthManager { return authManager } -// Handle auth code from deep link (exported for IPC handlers) -export async function handleAuthCode(code: string): Promise { - console.log("[Auth] Handling auth code:", code.slice(0, 8) + "...") - - try { - const authData = await authManager.exchangeCode(code) - console.log("[Auth] Success for user:", authData.user.email) - - // Track successful authentication - trackAuthCompleted(authData.user.id, authData.user.email) - - // Set desktop token cookie using persist:main partition - const ses = session.fromPartition("persist:main") - try { - // First remove any existing cookie to avoid HttpOnly conflict - await ses.cookies.remove(getBaseUrl(), "x-desktop-token") - await ses.cookies.set({ - url: getBaseUrl(), - name: "x-desktop-token", - value: authData.token, - expirationDate: Math.floor( - new Date(authData.expiresAt).getTime() / 1000, - ), - httpOnly: false, - secure: getBaseUrl().startsWith("https"), - sameSite: "lax" as const, - }) - console.log("[Auth] Desktop token cookie set") - } catch (cookieError) { - // Cookie setting is optional - auth data is already saved to disk - console.warn("[Auth] Cookie set failed (non-critical):", cookieError) - } - - // 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")) - } - win?.focus() - } catch (error) { - console.error("[Auth] Exchange failed:", error) - getWindow()?.webContents.send("auth:error", (error as Error).message) - } +// Handle auth code from deep link - deprecated, now uses settings UI +export async function handleAuthCode(_code: string): Promise { + console.log("[Auth] handleAuthCode called but OAuth is no longer used") + console.log("[Auth] Please configure Azure credentials in Settings") } -// Handle deep link +// Handle deep link - simplified since we don't use OAuth anymore function handleDeepLink(url: string): void { console.log("[DeepLink] Received:", url) - - try { - const parsed = new URL(url) - - // Handle auth callback: twentyfirstdev://auth?code=xxx - if (parsed.pathname === "/auth" || parsed.host === "auth") { - const code = parsed.searchParams.get("code") - if (code) { - handleAuthCode(code) - return - } - } - } catch (e) { - console.error("[DeepLink] Failed to parse:", e) - } + // Deep links are no longer used for auth + // Could be extended for other purposes in the future } // Register protocol BEFORE app is ready @@ -250,7 +187,7 @@ if (process.env.ELECTRON_RENDERER_URL) { - 1Code - Authentication + kcode - Authentication