feat(chat): add Claude Code as local chat provider#2668
feat(chat): add Claude Code as local chat provider#2668
Conversation
🧪 BenchmarkShould we run the Virtual MCP strategy benchmark for this PR? React with 👍 to run the benchmark.
Benchmark will run on the next push after you react. |
Release OptionsShould a new version be published when this PR is merged? React with an emoji to vote on the release type:
Current version: Deployment
|
There was a problem hiding this comment.
3 issues found across 7 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/mesh/src/api/routes/decopilot/routes.ts">
<violation number="1" location="apps/mesh/src/api/routes/decopilot/routes.ts:297">
P2: Validate Claude Code availability before starting the run/saving messages; currently an unavailable provider still creates a failed run and persists the user message.</violation>
</file>
<file name="apps/mesh/src/api/routes/decopilot/claude-code-provider.ts">
<violation number="1" location="apps/mesh/src/api/routes/decopilot/claude-code-provider.ts:33">
P1: Multi-turn conversation context is lost. `messagesToPrompt` strips the `role` from all messages and concatenates their text, so prior assistant replies are indistinguishable from user messages. Add role prefixes (e.g., `User:` / `Assistant:`) or only pass the last user message as the prompt.</violation>
<violation number="2" location="apps/mesh/src/api/routes/decopilot/claude-code-provider.ts:215">
P1: Text will be duplicated in the output. The `stream_event` handler already writes incremental text deltas, but the `assistant` handler then re-emits the full completed text from `message.message.content`. Remove the text extraction from the `assistant` case since it's already delivered via stream events, or gate it so it only fires when no stream events were received.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| /** | ||
| * Convert chat messages to a prompt string for the Claude Agent SDK. | ||
| */ | ||
| function messagesToPrompt(messages: ChatMessage[]): string { |
There was a problem hiding this comment.
P1: Multi-turn conversation context is lost. messagesToPrompt strips the role from all messages and concatenates their text, so prior assistant replies are indistinguishable from user messages. Add role prefixes (e.g., User: / Assistant:) or only pass the last user message as the prompt.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/api/routes/decopilot/claude-code-provider.ts, line 33:
<comment>Multi-turn conversation context is lost. `messagesToPrompt` strips the `role` from all messages and concatenates their text, so prior assistant replies are indistinguishable from user messages. Add role prefixes (e.g., `User:` / `Assistant:`) or only pass the last user message as the prompt.</comment>
<file context>
@@ -0,0 +1,277 @@
+/**
+ * Convert chat messages to a prompt string for the Claude Agent SDK.
+ */
+function messagesToPrompt(messages: ChatMessage[]): string {
+ const parts: string[] = [];
+
</file context>
There was a problem hiding this comment.
1 issue found across 3 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/mesh/src/api/routes/decopilot/routes.ts">
<violation number="1" location="apps/mesh/src/api/routes/decopilot/routes.ts:315">
P2: Guard the Claude Code sentinel model id before forwarding it to the SDK; otherwise restored sessions can pass `"claude-code"` as a concrete model value.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
When running locally with Claude Code installed, offer it as a model option in the chat UI. Routes messages through the Claude Agent SDK subprocess instead of OpenRouter, using the user's local Claude auth. - Add `claudeCodeAvailable` to AuthConfig (detected via Bun.which) - New claude-code-provider adapter wrapping @anthropic-ai/claude-agent-sdk - Fork decopilot stream endpoint for claude-code connectionId sentinel - Add Claude Code entry in model selector with "Local" badge - Handle empty connections gracefully when only Claude Code is available Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Extract text from assistant message content blocks (SDK emits full messages, not stream_event deltas) - Add start-step/finish-step markers for proper AI SDK status tracking - Add Opus/Sonnet/Haiku model variants in the selector - Pass selected model to SDK query options - Fix selectedConnectionId initialization to skip claude-code sentinel Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add Connect Studio modal with one-click Claude Code / Cursor setup - Server endpoint generates API key and runs `claude mcp add-json` or writes Cursor config - Status endpoint checks if IDE is already connected - Auto-wire MCP endpoint when using Claude Code as local chat provider - Support mcpHeaders in Claude Agent SDK http transport Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
9b795cb to
8002f8b
Compare
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…boarding - Fix missing closing brace in model selector JSX - Fix AiProviderModel type mismatches for Claude Code synthetic models - Remove stale useAllowedModels import - Add Claude Code card with "Local" badge to the AI provider onboarding screen - Card shows connection status and one-click connect via CLI Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
2 issues found across 3 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/mesh/src/web/components/chat/select-model.tsx">
<violation number="1" location="apps/mesh/src/web/components/chat/select-model.tsx:1070">
P1: Selecting a Claude Code model never tags the request with `connectionId: "claude-code"`, so the backend will not take the local Claude Code streaming path.</violation>
</file>
<file name="apps/mesh/src/web/components/chat/no-llm-binding-empty-state.tsx">
<violation number="1" location="apps/mesh/src/web/components/chat/no-llm-binding-empty-state.tsx:57">
P2: Also guard the in-flight state here so repeated clicks do not trigger duplicate connect requests.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
apps/mesh/src/web/components/chat/no-llm-binding-empty-state.tsx
Outdated
Show resolved
Hide resolved
Remove custom Claude Code UI from model selector and onboarding. Claude Code now uses the same ConnectionModelList → groupByTier → ModelTierSection pipeline as every other provider. Models are returned from factory.ts via a static list, classified into tiers via prefixes, and server detection uses thinking.provider instead of broken connectionId field. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove Cursor support (not working), show Claude Code auth info (email, org, subscription) when connected, add disconnect button, and fix refresh icon positioning. Add timeouts to all CLI spawns. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
4 issues found across 8 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/mesh/src/api/routes/decopilot/routes.ts">
<violation number="1" location="apps/mesh/src/api/routes/decopilot/routes.ts:158">
P1: Don't trust `models.thinking.provider` to decide whether to skip model-permission checks.</violation>
<violation number="2" location="apps/mesh/src/api/routes/decopilot/routes.ts:321">
P2: This passes `credentialId` into a `connectionId` field, so Claude Code responses emit the wrong metadata shape for follow-up requests.</violation>
</file>
<file name="apps/mesh/src/web/components/settings-modal/pages/org-ai-providers.tsx">
<violation number="1" location="apps/mesh/src/web/components/settings-modal/pages/org-ai-providers.tsx:334">
P2: Gate the Claude Code connect path on actual CLI availability; right now it can create a unusable "connected" provider on servers where Claude Code is unavailable.</violation>
</file>
<file name="apps/mesh/src/ai-providers/factory.ts">
<violation number="1" location="apps/mesh/src/ai-providers/factory.ts:8">
P2: Keep the Claude Code model variants in one shared source of truth; this duplicate list can drift from the runtime SDK mapping.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| organization.id, | ||
| ctx.auth.user?.role, | ||
| ); | ||
| const isClaudeCode = models.thinking.provider === "claude-code"; |
There was a problem hiding this comment.
P1: Don't trust models.thinking.provider to decide whether to skip model-permission checks.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/api/routes/decopilot/routes.ts, line 158:
<comment>Don't trust `models.thinking.provider` to decide whether to skip model-permission checks.</comment>
<file context>
@@ -155,7 +155,7 @@ export function createDecopilotRoutes(deps: DecopilotDeps) {
}
- const isClaudeCode = models.connectionId === "claude-code";
+ const isClaudeCode = models.thinking.provider === "claude-code";
// 2. Check model permissions (skip for Claude Code — uses local auth)
</file context>
| if (isActive || isClaudeCodePending) return; | ||
| setIsClaudeCodePending(true); | ||
| try { | ||
| await client.callTool({ |
There was a problem hiding this comment.
P2: Gate the Claude Code connect path on actual CLI availability; right now it can create a unusable "connected" provider on servers where Claude Code is unavailable.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/settings-modal/pages/org-ai-providers.tsx, line 334:
<comment>Gate the Claude Code connect path on actual CLI availability; right now it can create a unusable "connected" provider on servers where Claude Code is unavailable.</comment>
<file context>
@@ -322,12 +322,42 @@ export function ProviderCard({
+ if (isActive || isClaudeCodePending) return;
+ setIsClaudeCodePending(true);
+ try {
+ await client.callTool({
+ name: "AI_PROVIDER_KEY_CREATE",
+ arguments: {
</file context>
| import { PROVIDERS } from "./registry"; | ||
|
|
||
| /** Static model list for the Claude Code local provider. */ | ||
| const CLAUDE_CODE_MODEL_LIST: ModelInfo[] = [ |
There was a problem hiding this comment.
P2: Keep the Claude Code model variants in one shared source of truth; this duplicate list can drift from the runtime SDK mapping.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/ai-providers/factory.ts, line 8:
<comment>Keep the Claude Code model variants in one shared source of truth; this duplicate list can drift from the runtime SDK mapping.</comment>
<file context>
@@ -4,6 +4,40 @@ import type { ModelListCache } from "./model-list-cache";
import { PROVIDERS } from "./registry";
+/** Static model list for the Claude Code local provider. */
+const CLAUDE_CODE_MODEL_LIST: ModelInfo[] = [
+ {
+ providerId: "claude-code",
</file context>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
2 issues found across 3 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/mesh/src/api/routes/decopilot/routes.ts">
<violation number="1" location="apps/mesh/src/api/routes/decopilot/routes.ts:875">
P2: Use `getUserId(ctx)` here instead of reading `ctx.auth.user` directly; otherwise API-key-authenticated requests are rejected by the new Connect Studio endpoints.
(Based on your team's feedback about using `getUserId` for mixed user/API-key auth.) [FEEDBACK_USED]</violation>
</file>
<file name="apps/mesh/src/web/components/connect-studio-modal.tsx">
<violation number="1" location="apps/mesh/src/web/components/connect-studio-modal.tsx:83">
P1: This disconnect flow reports success without revoking the generated full-access API key, so Claude Code access persists after “Disconnect”.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| setDisconnecting(true); | ||
| try { | ||
| const res = await fetch(`/api/${org.slug}/decopilot/connect-studio`, { | ||
| method: "DELETE", |
There was a problem hiding this comment.
P1: This disconnect flow reports success without revoking the generated full-access API key, so Claude Code access persists after “Disconnect”.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/connect-studio-modal.tsx, line 83:
<comment>This disconnect flow reports success without revoking the generated full-access API key, so Claude Code access persists after “Disconnect”.</comment>
<file context>
@@ -29,101 +35,70 @@ function useConnectStatus(org: { slug: string }) {
+ setDisconnecting(true);
+ try {
+ const res = await fetch(`/api/${org.slug}/decopilot/connect-studio`, {
+ method: "DELETE",
+ });
+ if (!res.ok) {
</file context>
|
|
||
| app.delete("/:org/decopilot/connect-studio", async (c) => { | ||
| const ctx = c.get("meshContext"); | ||
| if (!ctx.auth?.user?.id) { |
There was a problem hiding this comment.
P2: Use getUserId(ctx) here instead of reading ctx.auth.user directly; otherwise API-key-authenticated requests are rejected by the new Connect Studio endpoints.
(Based on your team's feedback about using getUserId for mixed user/API-key auth.)
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/api/routes/decopilot/routes.ts, line 875:
<comment>Use `getUserId(ctx)` here instead of reading `ctx.auth.user` directly; otherwise API-key-authenticated requests are rejected by the new Connect Studio endpoints.
(Based on your team's feedback about using `getUserId` for mixed user/API-key auth.) </comment>
<file context>
@@ -799,61 +830,80 @@ export function createDecopilotRoutes(deps: DecopilotDeps) {
+
+ app.delete("/:org/decopilot/connect-studio", async (c) => {
+ const ctx = c.get("meshContext");
+ if (!ctx.auth?.user?.id) {
+ throw new HTTPException(401, { message: "Authentication required" });
+ }
</file context>
Handle thinking_delta events from the Claude Agent SDK to stream reasoning content in real-time. Track content block types to route thinking vs text deltas correctly. Prevent duplicate text from assistant message fallback when stream events are available. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Refactor Connect Studio modal to show each connection as a self-contained card with its own connect/disconnect buttons. Add GitHub connection that uses the local `gh` CLI token to register GitHub's MCP in Claude Code. Also enable `includePartialMessages` in the Claude Agent SDK to get real-time thinking/text streaming events. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
3 issues found across 3 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/mesh/src/api/routes/decopilot/claude-code-provider.ts">
<violation number="1" location="apps/mesh/src/api/routes/decopilot/claude-code-provider.ts:372">
P2: This replays the full `thinking` content even after `thinking_delta` events were already streamed, so Claude Code reasoning can show up duplicated in the chat UI.</violation>
</file>
<file name="apps/mesh/src/api/routes/decopilot/routes.ts">
<violation number="1" location="apps/mesh/src/api/routes/decopilot/routes.ts:794">
P2: `gh auth status --json` is missing the required field list, so GitHub status detection will always fall back to disconnected.</violation>
<violation number="2" location="apps/mesh/src/api/routes/decopilot/routes.ts:843">
P1: This creates a permanent wildcard API key but the disconnect/failure paths never revoke it, leaving active internal credentials behind.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
|
|
||
| if (target === "claude-code") { | ||
| // Create API key for the MCP endpoint | ||
| const apiKey = await ctx.boundAuth.apiKey.create({ |
There was a problem hiding this comment.
P1: This creates a permanent wildcard API key but the disconnect/failure paths never revoke it, leaving active internal credentials behind.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/api/routes/decopilot/routes.ts, line 843:
<comment>This creates a permanent wildcard API key but the disconnect/failure paths never revoke it, leaving active internal credentials behind.</comment>
<file context>
@@ -811,63 +835,78 @@ export function createDecopilotRoutes(deps: DecopilotDeps) {
+
+ if (target === "claude-code") {
+ // Create API key for the MCP endpoint
+ const apiKey = await ctx.boundAuth.apiKey.create({
+ name: "studio-connect-claude-code",
+ permissions: { "*": ["*"] },
</file context>
|
|
||
| for (const block of content) { | ||
| // Stream thinking content as reasoning | ||
| if (block.type === "thinking" && block.thinking) { |
There was a problem hiding this comment.
P2: This replays the full thinking content even after thinking_delta events were already streamed, so Claude Code reasoning can show up duplicated in the chat UI.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/api/routes/decopilot/claude-code-provider.ts, line 372:
<comment>This replays the full `thinking` content even after `thinking_delta` events were already streamed, so Claude Code reasoning can show up duplicated in the chat UI.</comment>
<file context>
@@ -321,26 +353,42 @@ export async function streamClaudeCode(
+
+ for (const block of content) {
+ // Stream thinking content as reasoning
+ if (block.type === "thinking" && block.thinking) {
+ if (!reasoningPartId) {
+ reasoningPartId = generateMessageId();
</file context>
GitHub status now uses `gh auth status` (plain) + `gh api user --jq .login` instead of `gh auth status --json` which requires explicit field names. Simplify ConnectionCard to single-row layout with inline toggle button. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/mesh/src/api/routes/decopilot/routes.ts">
<violation number="1" location="apps/mesh/src/api/routes/decopilot/routes.ts:796">
P2: Don't return early on missing `gh` auth here; it hides an already-registered GitHub MCP and flips the modal back to `Connect`.
(Based on your team's feedback about checking MCP authentication state first.) [FEEDBACK_USED]</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| if (!ghOk) return { connected: false, auth: null }; | ||
|
|
||
| // Get username via API | ||
| let user: string | undefined; | ||
| try { | ||
| const { ok, stdout } = await runCli("gh", [ | ||
| "api", | ||
| "user", | ||
| "--jq", | ||
| ".login", | ||
| ]); | ||
| if (ok) user = stdout.trim(); | ||
| } catch { | ||
| // Username not available | ||
| } | ||
|
|
||
| // Check if the MCP is registered in Claude Code | ||
| const { ok: mcpRegistered } = await runCli("claude", [ | ||
| "mcp", | ||
| "get", | ||
| "github", | ||
| ]); | ||
|
|
||
| return { | ||
| connected: mcpRegistered, | ||
| auth: user ? { user } : null, | ||
| }; |
There was a problem hiding this comment.
P2: Don't return early on missing gh auth here; it hides an already-registered GitHub MCP and flips the modal back to Connect.
(Based on your team's feedback about checking MCP authentication state first.)
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/api/routes/decopilot/routes.ts, line 796:
<comment>Don't return early on missing `gh` auth here; it hides an already-registered GitHub MCP and flips the modal back to `Connect`.
(Based on your team's feedback about checking MCP authentication state first.) </comment>
<file context>
@@ -791,26 +791,35 @@ export function createDecopilotRoutes(deps: DecopilotDeps) {
- if (!ok) return { connected: false, auth: null };
+ // Check if gh CLI is authenticated
+ const { ok: ghOk } = await runCli("gh", ["auth", "status"]);
+ if (!ghOk) return { connected: false, auth: null };
+
+ // Get username via API
</file context>
| if (!ghOk) return { connected: false, auth: null }; | |
| // Get username via API | |
| let user: string | undefined; | |
| try { | |
| const { ok, stdout } = await runCli("gh", [ | |
| "api", | |
| "user", | |
| "--jq", | |
| ".login", | |
| ]); | |
| if (ok) user = stdout.trim(); | |
| } catch { | |
| // Username not available | |
| } | |
| // Check if the MCP is registered in Claude Code | |
| const { ok: mcpRegistered } = await runCli("claude", [ | |
| "mcp", | |
| "get", | |
| "github", | |
| ]); | |
| return { | |
| connected: mcpRegistered, | |
| auth: user ? { user } : null, | |
| }; | |
| const { ok: mcpRegistered } = await runCli("claude", [ | |
| "mcp", | |
| "get", | |
| "github", | |
| ]); | |
| // Get username via API when gh CLI is authenticated | |
| let user: string | undefined; | |
| if (ghOk) { | |
| const { ok, stdout } = await runCli("gh", [ | |
| "api", | |
| "user", | |
| "--jq", | |
| ".login", | |
| ]); | |
| if (ok) user = stdout.trim(); | |
| } | |
| return { | |
| connected: mcpRegistered, | |
| auth: user ? { user } : null, | |
| }; |
Summary
Bun.which("claude")and offers it as a model option in the chat UIChanges
apps/mesh/src/api/routes/auth.ts—claudeCodeAvailablein AuthConfigapps/mesh/src/api/routes/decopilot/claude-code-provider.ts— New SDK adapter (stream_event + assistant message handling)apps/mesh/src/api/routes/decopilot/routes.ts— Fork stream handler forclaude-codesentinelapps/mesh/src/api/routes/decopilot/schemas.ts— Acceptclaude-codeproviderapps/mesh/src/web/components/chat/select-model.tsx— Claude Code UI entry + guardsapps/mesh/src/web/components/chat/context.tsx— Handleclaude-codeconnectionId in model stateTest plan
which claude)🤖 Generated with Claude Code
Summary by cubic
Adds Claude Code as a local chat provider (Opus/Sonnet/Haiku) streamed via
@anthropic-ai/claude-agent-sdk, plus a Connect Studio flow with per‑connection cards for one‑click setup of Mesh tools and GitHub MCP. Auto‑wires the MCP endpoint with a short‑lived API key during Claude Code chats and skips org model permissions for this local provider.New Features
claude-code:*models in the factory/registry; server routes bymodels.thinking.provider === "claude-code";/configexposesclaudeCodeAvailable.@anthropic-ai/claude-agent-sdkwith cancel, start/finish-step markers, real‑time thinking/text deltas (includePartialMessages), assistant text extraction, and usage/cost passthrough.deco-studioMCP; modal with per‑connection cards (Claude Code and GitHub via localghtoken), auth info, connect/disconnect, and a sidebar entry.Bug Fixes
claude-codemodels; handled empty connections and replacedconnectionIdsentinel with provider detection.claude-code.gh auth status+gh api user) and compacted connection cards.Written for commit fd36ce0. Summary will update on new commits.