diff --git a/README.md b/README.md index 7b8400bc8..7aebe3f4b 100644 --- a/README.md +++ b/README.md @@ -369,7 +369,7 @@ Coverage output is written to `coverage/lcov.info`, and OpenClaude also generate ## VS Code Extension -The repo includes a VS Code extension in [`vscode-extension/openclaude-vscode`](vscode-extension/openclaude-vscode) for OpenClaude launch integration, provider-aware control-center UI, and theme support. +The repo includes a VS Code extension in [`vscode-extension/openclaude-vscode`](vscode-extension/openclaude-vscode) for OpenClaude launch integration, provider-aware Control Center, in-editor chat, theme support, and optional **Microsoft Foundry / Azure OpenAI** configuration (endpoint, API version, deployment, API key via Secret Storage) injected into launched terminals. See that folder’s [README](vscode-extension/openclaude-vscode/README.md). ## Security diff --git a/docs/advanced-setup.md b/docs/advanced-setup.md index 5b54bd573..abaeec4c9 100644 --- a/docs/advanced-setup.md +++ b/docs/advanced-setup.md @@ -256,6 +256,30 @@ export OPENAI_BASE_URL=https://your-resource.openai.azure.com/openai/deployments export OPENAI_MODEL=gpt-4o ``` +### Microsoft Foundry / Azure OpenAI (resource URL + deployment) + +When your endpoint is the **resource base URL** (not the full `.../deployments/.../v1` path), set `OPENAI_MODEL` to the **deployment name** and `AZURE_OPENAI_API_VERSION` to your API version. The OpenAI shim builds: + +`{base}/openai/deployments/{OPENAI_MODEL}/chat/completions?api-version={AZURE_OPENAI_API_VERSION}` + +and sends the key in the `api-key` header for Azure hosts. + +```bash +export CLAUDE_CODE_USE_OPENAI=1 +export OPENAI_API_KEY=your-azure-key +export OPENAI_BASE_URL=https://your-resource.openai.azure.com +export OPENAI_MODEL=your-deployment-name +export AZURE_OPENAI_API_VERSION=2024-12-01-preview +``` + +If your hostname is not detected as Azure (for example some inference endpoints), force Azure URL and header behavior: + +```bash +export OPENAI_AZURE_STYLE=1 +``` + +The **OpenClaude VS Code extension** can store the key in Secret Storage and set these variables for you when you launch from the Control Center. See `vscode-extension/openclaude-vscode/README.md`. + ## Environment Variables | Variable | Required | Description | diff --git a/docs/quick-start-windows.md b/docs/quick-start-windows.md index 58d6aa886..1dbe59f48 100644 --- a/docs/quick-start-windows.md +++ b/docs/quick-start-windows.md @@ -155,9 +155,13 @@ npm install -g @gitlawb/openclaude@latest npm uninstall -g @gitlawb/openclaude ``` + ## Need Advanced Setup? -Use: +For advanced provider setup, custom endpoints, environment variables, and enterprise launch workflows, see the advanced setup guide: -- [Advanced Setup](advanced-setup.md) - For Codex, Gemini, Mistral, LiteLLM, provider profiles, and runtime diagnostics. +- [Advanced setup](advanced-setup.md) + +For Windows helper aliases and launcher shortcuts such as `oc`, `oc-init`, `oc-local`, `oc-provider`, and `oc-check`, see: + +- [Windows aliases and launchers](windows-aliases-and-launchers.md) \ No newline at end of file diff --git a/docs/windows-aliases-and-launchers.md b/docs/windows-aliases-and-launchers.md new file mode 100644 index 000000000..0ce55647a --- /dev/null +++ b/docs/windows-aliases-and-launchers.md @@ -0,0 +1,163 @@ +# Windows aliases and launchers + +This page documents optional PowerShell helper functions for launching OpenClaude on Windows after a global npm install. + +These helpers are designed for the installed package workflow: + +~~~powershell +npm install -g @gitlawb/openclaude +~~~ + +The helpers use the installed `openclaude` CLI command. They do not require a source checkout and do not call source-only `bun run scripts/*.ts` entrypoints. + +## One-time setup + +Run this once in PowerShell: + +~~~powershell +$packageRoot = Join-Path (npm root -g) "@gitlawb/openclaude" +$aliases = Join-Path $packageRoot "scripts\windows\openclaude-aliases.ps1" + +if (-not (Test-Path $aliases)) { + throw "Alias script not found at $aliases. Update or reinstall @gitlawb/openclaude." +} + +if (-not (Test-Path $PROFILE)) { + New-Item -ItemType File -Path $PROFILE -Force | Out-Null +} + +$profileLine = ". `"$aliases`"" + +if (-not (Select-String -Path $PROFILE -Pattern ([regex]::Escape($profileLine)) -Quiet)) { + Add-Content -Path $PROFILE -Value "`n$profileLine" +} + +. $aliases +oc-help +~~~ + +Open a new PowerShell window after setup, or dot-source the profile: + +~~~powershell +. $PROFILE +~~~ + +## Daily commands + +### Launch OpenClaude using the installed CLI + +~~~powershell +oc +~~~ + +You can pass normal CLI arguments through `oc`: + +~~~powershell +oc --version +oc --help +~~~ + +### Launch with local Ollama/OpenAI-compatible environment hints + +~~~powershell +oc-local +~~~ + +By default, this uses local Ollama through the OpenAI-compatible API: + +~~~text +CLAUDE_CODE_USE_OPENAI=1 +OPENAI_BASE_URL=http://localhost:11434/v1 +OPENAI_MODEL=llama3.1:8b +~~~ + +To use a different local model for that invocation: + +~~~powershell +oc-local -Model "qwen2.5-coder:7b" +~~~ + +The environment overrides are scoped to that single `openclaude` invocation. A later plain `oc` call returns to normal installed CLI behavior and saved-provider-profile behavior. + +### Launch with low-latency local defaults + +~~~powershell +oc-fast +~~~ + +To use a different model: + +~~~powershell +oc-fast -Model "qwen2.5-coder:7b" +~~~ + +Like `oc-local`, the environment overrides are scoped to that single invocation. + +### Open the provider manager + +~~~powershell +oc-provider +~~~ + +This opens the provider manager through the installed OpenClaude CLI. + +### Check local Ollama state + +~~~powershell +oc-check +~~~ + +To check a specific model: + +~~~powershell +oc-check -Model "qwen2.5-coder:7b" +~~~ + +### Pull/check a local model, then launch local mode + +~~~powershell +oc-init +~~~ + +To choose a model: + +~~~powershell +oc-init -Model "qwen2.5-coder:7b" +~~~ + +To skip pulling the model and only check/launch: + +~~~powershell +oc-init -Model "qwen2.5-coder:7b" -SkipModelPull +~~~ + +`oc-init` does not save a provider profile. It pulls/checks the local Ollama model and then launches `oc-local`. + +### Show quick help + +~~~powershell +oc-help +~~~ + +## Command summary + +| Command | Purpose | +| --- | --- | +| `oc` | Launch OpenClaude using the installed CLI and saved/default behavior | +| `oc-local` | Launch once with local Ollama/OpenAI-compatible environment hints | +| `oc-fast` | Launch once with local Ollama/OpenAI-compatible low-latency hints | +| `oc-provider` | Open the provider manager | +| `oc-check` | Show local Ollama install/listening/model state | +| `oc-init` | Pull/check a local Ollama model, then launch local mode | +| `oc-help` | Show quick command help | + +## Notes + +These helpers are intentionally global-install oriented. They use the installed CLI instead of source-checkout development scripts. + +For advanced provider setup, use the built-in provider manager: + +~~~powershell +oc-provider +~~~ + diff --git a/package.json b/package.json index f708068f4..4b64ba5e9 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "dist/sdk.mjs", "src/entrypoints/sdk.d.ts", "src/entrypoints/sdk/coreTypes.generated.ts", + "scripts/windows/openclaude-aliases.ps1", + "docs/windows-aliases-and-launchers.md", "README.md" ], "scripts": { diff --git a/scripts/windows/openclaude-aliases.ps1 b/scripts/windows/openclaude-aliases.ps1 new file mode 100644 index 000000000..1774336f8 --- /dev/null +++ b/scripts/windows/openclaude-aliases.ps1 @@ -0,0 +1,206 @@ +Set-StrictMode -Version Latest + +function Test-OpenClaudeCommand { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Name + ) + + return [bool](Get-Command -Name $Name -ErrorAction SilentlyContinue) +} + +function Assert-OpenClaudeCommand { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Name, + [Parameter(Mandatory = $true)] + [string]$InstallHint + ) + + if (-not (Test-OpenClaudeCommand -Name $Name)) { + throw "Required command '$Name' was not found. $InstallHint" + } +} + +function Invoke-OpenClaude { + [CmdletBinding()] + param( + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$OpenClaudeArgs + ) + + Assert-OpenClaudeCommand -Name "openclaude" -InstallHint "Install with: npm install -g @gitlawb/openclaude" + + & openclaude @OpenClaudeArgs + + if ($LASTEXITCODE -ne 0) { + throw "openclaude failed with exit code $LASTEXITCODE." + } +} + +function Invoke-OpenClaudeWithEnvironment { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [hashtable]$Environment, + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$OpenClaudeArgs + ) + + $previousValues = @{} + + foreach ($name in $Environment.Keys) { + $previousValues[$name] = [Environment]::GetEnvironmentVariable($name, "Process") + Set-Item -Path "Env:$name" -Value $Environment[$name] + } + + try { + Invoke-OpenClaude @OpenClaudeArgs + } + finally { + foreach ($name in $Environment.Keys) { + if ($null -eq $previousValues[$name]) { + Remove-Item -Path "Env:$name" -ErrorAction SilentlyContinue + } + else { + Set-Item -Path "Env:$name" -Value $previousValues[$name] + } + } + } +} + +function Get-OpenClaudeQuickHelp { + [CmdletBinding()] + param() + + @( + "OpenClaude quick commands:", + " oc [args...] -> launch OpenClaude using the installed CLI", + " oc-local [args...] -> launch OpenClaude with local/Ollama OpenAI-compatible environment hints for this invocation only", + " oc-fast [args...] -> launch OpenClaude with low-latency local defaults for this invocation only", + " oc-provider -> open the provider manager in OpenClaude", + " oc-check -> show Ollama install/listening/model state", + " oc-init -> pull/check the local model, then launch local/Ollama mode", + " oc-help -> show this help" + ) -join [Environment]::NewLine +} + +function oc { + [CmdletBinding()] + param( + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$OpenClaudeArgs + ) + + Invoke-OpenClaude @OpenClaudeArgs +} + +function oc-local { + [CmdletBinding()] + param( + [string]$Model = "llama3.1:8b", + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$OpenClaudeArgs + ) + + Invoke-OpenClaudeWithEnvironment ` + -Environment @{ + CLAUDE_CODE_USE_OPENAI = "1" + OPENAI_BASE_URL = "http://localhost:11434/v1" + OPENAI_MODEL = $Model + } ` + @OpenClaudeArgs +} + +function oc-fast { + [CmdletBinding()] + param( + [string]$Model = "llama3.1:8b", + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$OpenClaudeArgs + ) + + Invoke-OpenClaudeWithEnvironment ` + -Environment @{ + CLAUDE_CODE_USE_OPENAI = "1" + OPENAI_BASE_URL = "http://localhost:11434/v1" + OPENAI_MODEL = $Model + OPENCLAUDE_FAST_MODE = "1" + } ` + @OpenClaudeArgs +} + +function oc-provider { + [CmdletBinding()] + param() + + Invoke-OpenClaude "/provider" +} + +function oc-check { + [CmdletBinding()] + param( + [string]$Model = "llama3.1:8b" + ) + + Assert-OpenClaudeCommand -Name "ollama" -InstallHint "Install Ollama from https://ollama.com/download/windows." + + $version = & ollama --version 2>$null + $modelNames = (& ollama list 2>$null | Select-Object -Skip 1 | ForEach-Object { + ($_ -split "\s+")[0] + }) | Where-Object { $_ } + + $isModelAvailable = $modelNames -contains $Model + $probeSucceeded = $false + + try { + $response = Invoke-RestMethod -Uri "http://localhost:11434/api/tags" -Method Get -TimeoutSec 3 + if ($response.models) { + $probeSucceeded = $true + } + } + catch { + $probeSucceeded = $false + } + + [PSCustomObject]@{ + OllamaInstalled = $true + OllamaVersion = $version + OllamaListening = $probeSucceeded + Model = $Model + ModelAvailable = $isModelAvailable + } +} + +function oc-init { + [CmdletBinding()] + param( + [string]$Model = "llama3.1:8b", + [switch]$SkipModelPull + ) + + Assert-OpenClaudeCommand -Name "ollama" -InstallHint "Install Ollama from https://ollama.com/download/windows." + + if (-not $SkipModelPull) { + & ollama pull $Model + if ($LASTEXITCODE -ne 0) { + throw "ollama pull $Model failed with exit code $LASTEXITCODE." + } + } + + $health = oc-check -Model $Model + if (-not $health.OllamaListening) { + Write-Warning "Ollama is installed but API probe to localhost:11434 did not succeed. Start Ollama and retry." + } + + oc-local -Model $Model +} + +function oc-help { + [CmdletBinding()] + param() + + Get-OpenClaudeQuickHelp +} diff --git a/src/services/api/openaiShim.ts b/src/services/api/openaiShim.ts index e26452fd0..6e218048b 100644 --- a/src/services/api/openaiShim.ts +++ b/src/services/api/openaiShim.ts @@ -23,6 +23,11 @@ * CLAUDE_CODE_USE_GITHUB=1 — enable GitHub inference (no need for USE_OPENAI) * GITHUB_TOKEN or GH_TOKEN — Copilot API token (mapped to Bearer auth) * OPENAI_MODEL — optional; use github:copilot or openai/gpt-4.1 style IDs + * + * Azure OpenAI / Microsoft Foundry (OpenAI-compatible chat): + * AZURE_OPENAI_API_VERSION — query param for chat/completions (default: 2024-12-01-preview) + * OPENAI_AZURE_STYLE=1 — force Azure deployment URL + api-key header when the hostname + * would not otherwise match (for example inference.ml.azure.com) */ import { APIError } from '@anthropic-ai/sdk' @@ -2441,12 +2446,20 @@ class OpenAIShimMessages { : apiKey // Detect Azure endpoints by hostname (not raw URL) to prevent bypass via // path segments like https://evil.com/cognitiveservices.azure.com/ - let isAzure = false - try { - const { hostname } = new URL(request.baseUrl) - isAzure = hostname.endsWith('.azure.com') && - (hostname.includes('cognitiveservices') || hostname.includes('openai') || hostname.includes('services.ai')) - } catch { /* malformed URL — not Azure */ } + let isAzure = isEnvTruthy(process.env.OPENAI_AZURE_STYLE) + if (!isAzure) { + try { + const { hostname } = new URL(request.baseUrl) + isAzure = + hostname.endsWith('.azure.com') && + (hostname.includes('cognitiveservices') || + hostname.includes('openai') || + hostname.includes('services.ai') || + hostname.includes('inference.ml')) + } catch { + /* malformed URL — not Azure */ + } + } let isBankr = false try { diff --git a/vscode-extension/openclaude-vscode/.vscode/launch.json b/vscode-extension/openclaude-vscode/.vscode/launch.json index b521eca09..662dd021f 100644 --- a/vscode-extension/openclaude-vscode/.vscode/launch.json +++ b/vscode-extension/openclaude-vscode/.vscode/launch.json @@ -6,8 +6,7 @@ "type": "extensionHost", "request": "launch", "args": ["--extensionDevelopmentPath=${workspaceFolder}"], - "outFiles": ["${workspaceFolder}/out/**/*.js"], - "preLaunchTask": "${defaultBuildTask}" + "outFiles": ["${workspaceFolder}/out/**/*.js"] } ] } diff --git a/vscode-extension/openclaude-vscode/README.md b/vscode-extension/openclaude-vscode/README.md index 0bc3ae07b..c7e254437 100644 --- a/vscode-extension/openclaude-vscode/README.md +++ b/vscode-extension/openclaude-vscode/README.md @@ -24,6 +24,7 @@ A practical VS Code companion for OpenClaude with a project-aware **Control Cent - Open Setup Guide - Open Command Palette - **Built-in dark theme**: `OpenClaude Terminal Black` +- **Microsoft Foundry / Azure OpenAI**: optional wizard and settings store endpoint, API version, deployment name, and API key (Secret Storage); launch injects `OPENAI_*` and `AZURE_OPENAI_API_VERSION` into the OpenClaude terminal (see `docs/advanced-setup.md` on the repo). ## Requirements @@ -38,14 +39,29 @@ A practical VS Code companion for OpenClaude with a project-aware **Control Cent - `OpenClaude: Open Repository` - `OpenClaude: Open Setup Guide` - `OpenClaude: Open Workspace Profile` +- `OpenClaude: New Chat` / `OpenClaude: Open Chat Panel` / `OpenClaude: Resume Session` / `OpenClaude: Abort Generation` +- `OpenClaude: Configure Azure / Foundry Chat (wizard)` +- `OpenClaude: Set Azure / Foundry API Key (Secret Storage)` +- `OpenClaude: Clear Azure / Foundry API Key` +- `OpenClaude: Open Azure / Foundry Settings` + +## Microsoft Foundry / Azure OpenAI (terminal chat) + +1. Command Palette → **OpenClaude: Configure Azure / Foundry Chat (wizard)** and enter endpoint, API version, deployment name, and API key; or set `openclaude.azure.*` in Settings and use **OpenClaude: Set Azure / Foundry API Key**. +2. Enable **OpenClaude: Azure: Enabled** (the wizard turns this on). +3. **OpenClaude: Launch in Terminal** — the extension merges env vars the OpenAI shim expects (`CLAUDE_CODE_USE_OPENAI`, `OPENAI_BASE_URL`, `OPENAI_API_KEY`, `OPENAI_MODEL`, `AZURE_OPENAI_API_VERSION`, and `OPENAI_AZURE_STYLE` when forced). + +If you use `.openclaude-profile.json` for the same workspace, leave Azure injection off to avoid conflicting provider configuration. ## Settings - `openclaude.launchCommand` (default: `openclaude`) - `openclaude.terminalName` (default: `OpenClaude`) - `openclaude.useOpenAIShim` (default: `false`) +- `openclaude.azure.*` — Foundry / Azure OpenAI terminal injection (see Settings UI) +- `openclaude.permissionMode` — chat permission mode -`openclaude.useOpenAIShim` only injects `CLAUDE_CODE_USE_OPENAI=1` into terminals launched by the extension. It does not guess or configure a provider by itself. +`openclaude.useOpenAIShim` only injects `CLAUDE_CODE_USE_OPENAI=1` when Azure injection did not already set it. It does not configure endpoints or keys by itself. ## Notes on Status Detection diff --git a/vscode-extension/openclaude-vscode/package.json b/vscode-extension/openclaude-vscode/package.json index f5ecba320..79194cfdc 100644 --- a/vscode-extension/openclaude-vscode/package.json +++ b/vscode-extension/openclaude-vscode/package.json @@ -1,7 +1,7 @@ { "name": "openclaude-vscode", "displayName": "OpenClaude", - "description": "Practical VS Code companion for OpenClaude with project-aware launch behavior and a real Control Center.", + "description": "Practical VS Code companion for OpenClaude with project-aware launch, Control Center, optional Microsoft Foundry / Azure OpenAI terminal env, and chat.", "version": "0.2.0", "publisher": "devnull-bootloader", "engines": { @@ -23,6 +23,10 @@ "onCommand:openclaude.openChat", "onCommand:openclaude.resumeSession", "onCommand:openclaude.abortChat", + "onCommand:openclaude.setAzureApiKey", + "onCommand:openclaude.clearAzureApiKey", + "onCommand:openclaude.configureAzureChat", + "onCommand:openclaude.openAzureSettings", "onView:openclaude.controlCenter", "onView:openclaude.chat" ], @@ -87,6 +91,26 @@ "command": "openclaude.abortChat", "title": "OpenClaude: Abort Generation", "category": "OpenClaude" + }, + { + "command": "openclaude.setAzureApiKey", + "title": "OpenClaude: Set Azure / Foundry API Key (Secret Storage)", + "category": "OpenClaude" + }, + { + "command": "openclaude.clearAzureApiKey", + "title": "OpenClaude: Clear Azure / Foundry API Key", + "category": "OpenClaude" + }, + { + "command": "openclaude.configureAzureChat", + "title": "OpenClaude: Configure Azure / Foundry Chat (wizard)", + "category": "OpenClaude" + }, + { + "command": "openclaude.openAzureSettings", + "title": "OpenClaude: Open Azure / Foundry Settings", + "category": "OpenClaude" } ], "viewsContainers": { @@ -135,7 +159,37 @@ "openclaude.useOpenAIShim": { "type": "boolean", "default": false, - "description": "Optionally set CLAUDE_CODE_USE_OPENAI=1 in launched OpenClaude terminals." + "description": "Optionally set CLAUDE_CODE_USE_OPENAI=1 in launched OpenClaude terminals when Azure injection is off or incomplete." + }, + "openclaude.azure.enabled": { + "type": "boolean", + "default": false, + "description": "When true, launch injects Microsoft Foundry / Azure OpenAI-compatible chat env (OPENAI_* + AZURE_OPENAI_API_VERSION) into the OpenClaude terminal." + }, + "openclaude.azure.endpoint": { + "type": "string", + "default": "", + "markdownDescription": "Azure resource base URL for OpenAI-compatible chat (example: `https://YOUR_RESOURCE.openai.azure.com`). Do not put `api-version` here; use **OpenClaude: Azure API Version**." + }, + "openclaude.azure.apiVersion": { + "type": "string", + "default": "2024-12-01-preview", + "description": "Azure API version for chat completions (AZURE_OPENAI_API_VERSION)." + }, + "openclaude.azure.deployment": { + "type": "string", + "default": "", + "markdownDescription": "Azure deployment name (maps to `OPENAI_MODEL` for the OpenAI shim)." + }, + "openclaude.azure.forceAzureUrlStyle": { + "type": "boolean", + "default": true, + "description": "Sets OPENAI_AZURE_STYLE=1 so deployment URLs and api-key header work on Foundry and non-standard Azure hosts." + }, + "openclaude.azure.apiKey": { + "type": "string", + "default": "", + "markdownDescription": "Optional API key in settings (not recommended). Prefer **OpenClaude: Set Azure / Foundry API Key**. Secret wins if both are set." }, "openclaude.permissionMode": { "type": "string", @@ -169,7 +223,9 @@ "terminal", "theme", "cli", - "llm" + "llm", + "azure", + "foundry" ], "repository": { "type": "git", diff --git a/vscode-extension/openclaude-vscode/src/extension.js b/vscode-extension/openclaude-vscode/src/extension.js index 2cfcf06ae..18243e7a3 100644 --- a/vscode-extension/openclaude-vscode/src/extension.js +++ b/vscode-extension/openclaude-vscode/src/extension.js @@ -19,6 +19,10 @@ const { DiffContentProvider, SCHEME: DIFF_SCHEME } = require('./chat/diffControl const OPENCLAUDE_REPO_URL = 'https://github.com/Gitlawb/openclaude'; const OPENCLAUDE_SETUP_URL = 'https://github.com/Gitlawb/openclaude/blob/main/README.md#quick-start'; const PROFILE_FILE_NAME = '.openclaude-profile.json'; +const SECRET_AZURE_API_KEY = 'openclaude.azure.apiKey'; + +/** @type {vscode.ExtensionContext | null} */ +let extensionContext = null; function escapeHtml(value) { return String(value) @@ -52,6 +56,172 @@ function getExecutableFromCommand(command) { return normalized.split(/\s+/)[0]; } +/** + * @param {string} raw + */ +function normalizeAzureEndpoint(raw) { + const t = (raw || '').trim(); + if (!t) { + return ''; + } + try { + const u = new URL(t); + const pathname = u.pathname.replace(/\/+$/, ''); + return `${u.origin}${pathname}`; + } catch { + return t.replace(/\/+$/, ''); + } +} + +/** + * @param {vscode.ExtensionContext | null} context + * @param {vscode.WorkspaceConfiguration} configured + */ +async function resolveAzureApiKey(context, configured) { + if (!context) { + return ''; + } + const fromSecret = await context.secrets.get(SECRET_AZURE_API_KEY); + if (fromSecret) { + return fromSecret; + } + return (configured.get('azure.apiKey', '') || '').trim(); +} + +/** + * @param {vscode.WorkspaceConfiguration} configured + */ +async function buildLaunchAzureEnv(configured) { + const env = {}; + const ctx = extensionContext; + if (!ctx) { + return env; + } + + const azureEnabled = configured.get('azure.enabled', false); + const endpoint = normalizeAzureEndpoint(configured.get('azure.endpoint', '')); + const apiVersion = (configured.get('azure.apiVersion', '2024-12-01-preview') || '').trim(); + const deployment = (configured.get('azure.deployment', '') || '').trim(); + const forceStyle = configured.get('azure.forceAzureUrlStyle', true); + + if (!azureEnabled) { + return env; + } + + const apiKey = await resolveAzureApiKey(ctx, configured); + if (!endpoint || !deployment) { + void vscode.window.showWarningMessage( + 'OpenClaude Azure chat is enabled but endpoint or deployment is missing. Run "OpenClaude: Configure Azure / Foundry Chat" or set openclaude.azure.* in settings.', + ); + return env; + } + if (!apiKey) { + void vscode.window.showWarningMessage( + 'OpenClaude Azure chat is enabled but no API key is set. Use "OpenClaude: Set Azure / Foundry API Key" or openclaude.azure.apiKey (not recommended).', + ); + return env; + } + + env.CLAUDE_CODE_USE_OPENAI = '1'; + env.OPENAI_BASE_URL = endpoint; + env.OPENAI_API_KEY = apiKey; + env.OPENAI_MODEL = deployment; + env.AZURE_OPENAI_API_VERSION = apiVersion || '2024-12-01-preview'; + if (forceStyle) { + env.OPENAI_AZURE_STYLE = '1'; + } + return env; +} + +/** + * @param {vscode.ExtensionContext} context + */ +async function setAzureApiKey(context) { + const key = await vscode.window.showInputBox({ + title: 'OpenClaude — Azure / Foundry API key', + prompt: 'Stored in VS Code Secret Storage (not committed to the repo).', + password: true, + ignoreFocusOut: true, + validateInput: v => (v && v.trim() ? null : 'Enter a non-empty key'), + }); + if (key == null) { + return; + } + await context.secrets.store(SECRET_AZURE_API_KEY, key.trim()); + void vscode.window.showInformationMessage('OpenClaude Azure / Foundry API key saved to Secret Storage.'); +} + +/** + * @param {vscode.ExtensionContext} context + */ +async function clearAzureApiKey(context) { + await context.secrets.delete(SECRET_AZURE_API_KEY); + void vscode.window.showInformationMessage('OpenClaude Azure / Foundry API key removed from Secret Storage.'); +} + +/** + * @param {vscode.ExtensionContext} context + */ +async function configureAzureChat(context) { + const cfg = vscode.workspace.getConfiguration('openclaude'); + const target = vscode.ConfigurationTarget.Global; + + const endpoint = await vscode.window.showInputBox({ + title: 'OpenClaude — Azure / Foundry API endpoint', + prompt: 'Resource base URL only (no api-version query). Example: https://YOUR_RESOURCE.openai.azure.com', + ignoreFocusOut: true, + value: cfg.get('azure.endpoint', ''), + validateInput: v => (v && v.trim() ? null : 'Required'), + }); + if (endpoint == null) { + return; + } + + const apiVersion = await vscode.window.showInputBox({ + title: 'OpenClaude — Azure API version', + prompt: 'Matches the api-version used by your deployment (e.g. 2024-12-01-preview).', + value: (cfg.get('azure.apiVersion', '2024-12-01-preview') || '').trim(), + ignoreFocusOut: true, + validateInput: v => (v && v.trim() ? null : 'Required'), + }); + if (apiVersion == null) { + return; + } + + const deployment = await vscode.window.showInputBox({ + title: 'OpenClaude — Azure deployment / model', + prompt: 'Deployment name in Azure (this becomes OPENAI_MODEL for the OpenAI shim).', + value: cfg.get('azure.deployment', ''), + ignoreFocusOut: true, + validateInput: v => (v && v.trim() ? null : 'Required'), + }); + if (deployment == null) { + return; + } + + const key = await vscode.window.showInputBox({ + title: 'OpenClaude — Azure / Foundry API key', + prompt: 'Stored in VS Code Secret Storage.', + password: true, + ignoreFocusOut: true, + validateInput: v => (v && v.trim() ? null : 'Required'), + }); + if (key == null) { + return; + } + + await cfg.update('azure.endpoint', normalizeAzureEndpoint(endpoint), target); + await cfg.update('azure.apiVersion', apiVersion.trim(), target); + await cfg.update('azure.deployment', deployment.trim(), target); + await cfg.update('azure.forceAzureUrlStyle', true, target); + await cfg.update('azure.enabled', true, target); + await context.secrets.store(SECRET_AZURE_API_KEY, key.trim()); + + void vscode.window.showInformationMessage( + 'OpenClaude Azure / Foundry chat saved. Launch OpenClaude from the Control Center or command palette.', + ); +} + function getWorkspacePaths() { return (vscode.workspace.workspaceFolders || []).map(folder => folder.uri.fsPath); } @@ -305,8 +475,8 @@ async function launchOpenClaude(options = {}) { return; } - const env = {}; - if (shimEnabled) { + const env = await buildLaunchAzureEnv(configured); + if (shimEnabled && !env.CLAUDE_CODE_USE_OPENAI) { env.CLAUDE_CODE_USE_OPENAI = '1'; } @@ -887,6 +1057,10 @@ function renderControlCenterHtml(status, options = {}) { Open Command Palette Access VS Code and OpenClaude commands quickly. + @@ -904,6 +1078,7 @@ function renderControlCenterHtml(status, options = {}) { document.getElementById('repo').addEventListener('click', () => vscode.postMessage({ type: 'repo' })); document.getElementById('setup').addEventListener('click', () => vscode.postMessage({ type: 'setup' })); document.getElementById('commands').addEventListener('click', () => vscode.postMessage({ type: 'commands' })); + document.getElementById('azureFoundry').addEventListener('click', () => vscode.postMessage({ type: 'azureSettings' })); document.getElementById('refresh').addEventListener('click', () => vscode.postMessage({ type: 'refresh' })); const profileButton = document.getElementById('openProfile'); @@ -950,6 +1125,9 @@ class OpenClaudeControlCenterProvider { case 'commands': await vscode.commands.executeCommand('workbench.action.showCommands'); break; + case 'azureSettings': + await vscode.commands.executeCommand('workbench.action.openSettings', 'openclaude.azure'); + break; case 'refresh': default: break; @@ -1044,6 +1222,8 @@ class OpenClaudeControlCenterProvider { * @param {vscode.ExtensionContext} context */ function activate(context) { + extensionContext = context; + // ── Control Center (existing) ── const provider = new OpenClaudeControlCenterProvider(); const refreshProvider = () => { @@ -1165,6 +1345,22 @@ function activate(context) { chatController.abort(); }); + const setAzureApiKeyCommand = vscode.commands.registerCommand('openclaude.setAzureApiKey', async () => { + await setAzureApiKey(context); + }); + + const clearAzureApiKeyCommand = vscode.commands.registerCommand('openclaude.clearAzureApiKey', async () => { + await clearAzureApiKey(context); + }); + + const configureAzureChatCommand = vscode.commands.registerCommand('openclaude.configureAzureChat', async () => { + await configureAzureChat(context); + }); + + const openAzureSettingsCommand = vscode.commands.registerCommand('openclaude.openAzureSettings', async () => { + await vscode.commands.executeCommand('workbench.action.openSettings', 'openclaude.azure'); + }); + // ── Register providers ── const controlCenterProviderReg = vscode.window.registerWebviewViewProvider( 'openclaude.controlCenter', @@ -1193,6 +1389,10 @@ function activate(context) { openChatCommand, resumeSessionCommand, abortChatCommand, + setAzureApiKeyCommand, + clearAzureApiKeyCommand, + configureAzureChatCommand, + openAzureSettingsCommand, chatViewProviderReg, diffProviderReg, statusBarItem, @@ -1221,7 +1421,9 @@ function activate(context) { ); } -function deactivate() {} +function deactivate() { + extensionContext = null; +} module.exports = { activate,