-
Notifications
You must be signed in to change notification settings - Fork 2.4k
feat(claude-code): Claude Code CLI provider — Phases 1–5 #2472
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
finedesignz
wants to merge
6
commits into
tinyhumansai:main
Choose a base branch
from
finedesignz:feat/claude-code-provider
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
6aad097
feat(claude-code): scaffold Claude Code CLI provider (Phase 1)
finedesignz 3c81e87
feat(claude-code): driver + stream parser (Phase 2)
finedesignz b6f52a4
feat(claude-code): wire MCP stdio bridge to openhuman-core mcp (Phase 3)
finedesignz e6bc3b9
feat(claude-code): settings card, ProviderRef extension, docs (Phase 4)
finedesignz bc612e6
test(claude-code): stream-json E2E integration test (Phase 5)
finedesignz 7c33199
chore(claude-code): apply prettier + rustfmt auto-fixes
finedesignz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,224 @@ | ||
| # Plan — `claude-code` Provider for OpenHuman | ||
|
|
||
| **Owner:** jamie · **Status:** Locked v1 · **Branch:** `feat/claude-code-provider` | ||
|
|
||
| ## 1. Goal | ||
|
|
||
| Add `claude-code` as a selectable LLM provider in OpenHuman that drives Anthropic's `claude` CLI (`--output-format stream-json --verbose --print --resume`) instead of calling the Anthropic HTTP API directly. Existing API providers stay. Native OpenHuman tools remain Rust-side and are exposed to the CLI over MCP so CC can call them. | ||
|
|
||
| Reference implementation: `C:\Users\artic\GitHub\opencode` — `packages/opencode/src/provider/claude-code/`. | ||
|
|
||
| ## 2. Non-goals (v1) | ||
|
|
||
| - Subscription/OAuth auth (Claude Pro/Max) — defer to v2. v1 uses `ANTHROPIC_API_KEY` and any pre-existing `~/.claude/.credentials.json`. | ||
| - Exposing **write** tools (memory mutation, channel send, etc.) via MCP — defer to v1.1 after threat model. | ||
| - Co-enabling CC's built-in tools (`Bash`/`Read`/`Edit`) — disabled in v1 via `--disallowedTools`. | ||
| - Cost accounting wired into `cost.rs` — defer to v1.1. | ||
| - Process pool / cold-spawn optimization — defer to v2 if needed. | ||
|
|
||
| ## 3. Architecture (confirmed via Backend Architect review) | ||
|
|
||
| ``` | ||
| Frontend ──invoke──> Tauri shell ──HTTP+bearer──> openhuman-core (Axum :7788) | ||
| │ | ||
| ├─ /rpc (existing JSON-RPC) | ||
| └─ /mcp (NEW — MCP server, SSE) | ||
| ▲ | ||
| │ mcp__openhuman__* | ||
| │ | ||
| ChatRequest ──Provider::chat──> ClaudeCodeProvider ──spawn──> `claude --print | ||
| --output-format stream-json | ||
| --verbose --resume <uuid> | ||
| --mcp-config <tmp.json> | ||
| --disallowedTools <CC builtins>` | ||
| ▲ │ | ||
| SSE+bearer │ stdout JSONL | ||
| ▼ | ||
| stream_parser ─→ event_mapper | ||
| │ | ||
| ▼ | ||
| ProviderDelta stream | ||
| → harness turn loop | ||
| ``` | ||
|
|
||
| **Key files (existing, do not invent):** | ||
| - `src/openhuman/inference/provider/traits.rs` — `Provider` trait, `ProviderDelta`, `ToolsPayload`, `ChatRequest`. | ||
| - `src/openhuman/inference/provider/factory.rs` — `create_chat_provider_from_string(role, provider, config)`. String-grammar dispatch. | ||
| - `src/openhuman/inference/provider/openhuman_backend.rs` — reference impl with auth. | ||
| - `src/openhuman/inference/provider/compatible.rs` — reference impl with streaming + Anthropic-style auth. | ||
| - `src/openhuman/config/schema/cloud_providers.rs` — `CloudProviderType`, `AuthStyle`. | ||
| - `src/core/` — Axum server, bearer auth middleware, existing `/rpc` route. | ||
|
|
||
| ## 4. Module layout | ||
|
|
||
| ### 4.1 Provider | ||
|
|
||
| ``` | ||
| src/openhuman/inference/provider/claude_code/ | ||
| mod.rs — pub struct ClaudeCodeProvider; impl Provider for ... | ||
| driver.rs — process spawn, stdin/stdout/stderr piping, kill-on-drop, | ||
| tokio::sync::Semaphore(4) concurrency cap | ||
| stream_parser.rs — line-buffered JSONL → ClaudeCodeEvent | ||
| event_mapper.rs — ClaudeCodeEvent → ProviderDelta + tool-call accumulator | ||
| session_store.rs — ThreadId ↔ CC session UUID, persisted under config dir | ||
| input_builder.rs — ChatRequest → CLI argv + stdin payload | ||
| mcp_config.rs — generate per-launch mcp-config JSON (bearer + url), | ||
| write to temp, delete on drop | ||
| version_check.rs — `claude --version` parse + MIN_VERSION gate | ||
| auth.rs — API key resolution: env > config > ~/.claude/.credentials.json | ||
| schemas.rs — serde types for CC's stream-json envelope | ||
| types.rs — internal types | ||
| tests/ | ||
| fixtures/ — canned JSONL transcripts pulled from opencode fork's test fixtures | ||
| parser.rs — golden tests on each fixture | ||
| mapper.rs — event→delta correctness | ||
| driver.rs — spawn happy-path + version-fail + missing-binary | ||
| ``` | ||
|
|
||
| ### 4.2 MCP server (sibling, not under provider) | ||
|
|
||
| ``` | ||
| src/openhuman/mcp_server/ | ||
| mod.rs — Axum sub-router mounted at /mcp on core HTTP | ||
| transport.rs — SSE transport (MCP HTTP server protocol) | ||
| tool_registry.rs — bridge to existing tool dispatch | ||
| schemas.rs — MCP wire types | ||
| bus.rs — EventBus subscriber for tool-result fan-out | ||
| tests/ | ||
| ``` | ||
|
|
||
| Wire mount in `src/core/all.rs` next to JSON-RPC route. Reuses existing bearer-auth middleware — **no new auth surface**. | ||
|
|
||
| ### 4.3 Config | ||
|
|
||
| Add to `src/openhuman/config/schema/cloud_providers.rs`: | ||
| - `CloudProviderType::ClaudeCode` | ||
| - Fields: `binary_path: Option<PathBuf>`, `min_version: String`, `disallowed_builtins: Vec<String>` (defaults to all of CC's built-in tool names). | ||
|
|
||
| ### 4.4 RPC additions | ||
|
|
||
| New controller methods (per AGENTS.md `RpcOutcome<T>` contract, exposed via registry): | ||
| - `openhuman.claude_code_status` → `{ installed, version, path, min_satisfied, auth_state, last_error }` | ||
| - `openhuman.claude_code_check_version` — re-probe `claude --version` | ||
| - `openhuman.claude_code_set_auth` — store API key in credentials domain | ||
| - Extend `openhuman.providers_list` to surface CC entry with `requires_external_binary: true` | ||
|
|
||
| Per layout rule, these live in `src/openhuman/inference/rpc.rs` extension (or new `inference/claude_code_rpc.rs`). | ||
|
|
||
| ### 4.5 Frontend | ||
|
|
||
| Files under `app/src/`: | ||
| - `app/src/components/settings/ProviderSettings/ClaudeCodeSection.tsx` — install status, install instructions, API key input, version display. | ||
| - `app/src/components/settings/ProviderSettings/index.tsx` — add picker entry. | ||
| - `app/src/services/api/claudeCode.ts` — thin RPC wrappers. | ||
| - `app/src/store/slices/claudeCodeSlice.ts` — status state. | ||
|
|
||
| ## 5. Provider dispatch grammar | ||
|
|
||
| `factory.rs::create_chat_provider_from_string`: | ||
| - New arm matches `"claude-code:<model>[@<temp>]"` (e.g. `claude-code:sonnet-4-5`, `claude-code:opus-4-7@0.7`). | ||
| - Model string passed verbatim to `--model`. | ||
| - Temperature → input payload (CC stream-json supports it in the input message). | ||
|
|
||
| Existing `provider_for_role` reading `chat_provider`, `agentic_provider`, etc., now resolves CC for any role. | ||
|
|
||
| ## 6. Tool exposure via MCP | ||
|
|
||
| **v1 surface (read-only safe subset)** — to be confirmed once we read the existing tool registry: | ||
| - `memory_search`, `memory_get` | ||
| - `threads_list`, `threads_get`, `threads_messages` | ||
| - `channels_list`, `channels_messages_read` | ||
| - `people_search`, `people_get` | ||
| - `webhooks_list` | ||
|
|
||
| CC auto-prefixes MCP tools → CC sees them as `mcp__openhuman__memory_search` etc. **No collision risk** with CC built-ins. | ||
|
|
||
| CC built-ins (`Bash`, `Read`, `Write`, `Edit`, `Grep`, `Glob`, `WebFetch`, `WebSearch`, `Task`, `TodoWrite`, etc.) disabled via `--disallowedTools` for v1. | ||
|
|
||
| ## 7. Auth (v1) | ||
|
|
||
| `auth.rs` resolution order: | ||
| 1. `ChatRequest`/Config explicit key (per-thread/per-agent override) | ||
| 2. `ANTHROPIC_API_KEY` env | ||
| 3. `~/.claude/.credentials.json` (read-only — never write it; if present, set `ANTHROPIC_API_KEY` in spawned process env) | ||
| 4. None → `claude_code_status.auth_state = "missing"`, provider returns clear error on `chat()` | ||
|
|
||
| API key set per-process via env var on spawn (`Command::env`), not as CLI arg (would leak in process listings). | ||
|
|
||
| ## 8. Concurrency & lifecycle | ||
|
|
||
| - One CC process per turn (`--print` exits after assistant response). Reuse session UUID across turns via `--resume`. | ||
| - Global `Semaphore(4)` in `driver.rs` to cap concurrent processes. | ||
| - `Child` wrapped in a guard that calls `kill_on_drop(true)` + waits for exit; abort on harness interrupt. | ||
| - Hard timeout: 5 min per turn (configurable). Surface as `ProviderError::Timeout`. | ||
|
|
||
| ## 9. Risks / open questions | ||
|
|
||
| | # | Risk | Mitigation | | ||
| |---|------|------------| | ||
| | R1 | CC stream-json schema drift between versions | Pin `MIN_VERSION` (initially `2.0.0`); `version_check` blocks startup with clear error. Re-test on every CC release. | | ||
| | R2 | Windows `claude.cmd` shim | `driver.rs` uses `where claude` resolution + spawns via `cmd /c` on Windows when target is `.cmd`. | | ||
| | R3 | `OPENHUMAN_CORE_TOKEN` rotates per launch | mcp-config JSON regenerated each session, written to tempfile, deleted on drop. Never cached. | | ||
| | R4 | CC built-ins re-enabled accidentally | v1 hard-codes `--disallowedTools` list; flag in config but undocumented until threat model. | | ||
| | R5 | Cost data lost (no `cost.rs` wiring) | v1.1. v1 logs `result.total_cost_usd` to debug log. | | ||
| | R6 | MCP server perf under tool spam | SSE on same Axum runtime — same backpressure story as `/rpc`. Add semaphore on tool-dispatch handler if it becomes a hotspot. | | ||
| | R7 | Subscription users without API key can't use v1 | Clear UX in settings: "v1 requires API key; subscription support coming." | | ||
|
|
||
| ## 10. Phases & checkpoints | ||
|
|
||
| ### Phase 1 — Skeleton + version check (1–2 days) | ||
| - Create branch `feat/claude-code-provider` off `upstream/main`. | ||
| - Add `CloudProviderType::ClaudeCode` config variant. | ||
| - Scaffold `claude_code/` module with `version_check.rs`, `auth.rs`, `types.rs`, `schemas.rs`, `mod.rs` (Provider impl returning `not_implemented` for `chat`). | ||
| - Add `claude_code_status` + `claude_code_check_version` RPC. | ||
| - Frontend: minimal settings panel showing install status only. | ||
| - Unit tests: version parsing, auth resolution. | ||
| - **Checkpoint**: settings panel shows `installed: true/false`, version, path on real Windows install. | ||
|
|
||
| ### Phase 2 — Driver + stream parsing (2–3 days) | ||
| - `input_builder.rs`, `driver.rs` (spawn, kill-on-drop, semaphore), `stream_parser.rs`, `event_mapper.rs`, `session_store.rs`. | ||
| - Pull JSONL fixtures from opencode `packages/opencode/test/fixtures/claude-code-stream/`. Re-license headers if needed. | ||
| - Unit tests against fixtures: every event type maps to correct `ProviderDelta`. | ||
| - **Skip MCP for now**: spawn CC with `--disallowedTools <all>` and no MCP — just verify text streaming round-trip. | ||
| - Wire into `factory.rs` grammar. | ||
| - **Checkpoint**: pick provider in dev settings → run a turn → text streams back correctly. Multi-turn `--resume` works. | ||
|
|
||
| ### Phase 3 — MCP server (2–3 days) | ||
| - `src/openhuman/mcp_server/` scaffold. Mount `/mcp` SSE route under existing auth. | ||
| - Expose v1 read-only tool subset via `tool_registry.rs`. | ||
| - `mcp_config.rs` generates per-launch JSON, driver passes `--mcp-config` + `--strict-mcp-config`. | ||
| - Integration test: spawn CC, ask "list my threads", verify tool call lands and result returns. | ||
| - **Checkpoint**: end-to-end roundtrip — CC calls `mcp__openhuman__threads_list`, gets result, continues turn. | ||
|
|
||
| ### Phase 4 — Frontend polish + docs (1 day) | ||
| - Settings UI: install instructions per-OS, API key entry, "test connection" button. | ||
| - Per-role override UI if existing provider-selection UI supports it. | ||
| - Add docs entry in `gitbooks/developing/` covering the provider. | ||
| - Update `CLAUDE.md` if anything contract-changing landed (e.g. new `/mcp` route). | ||
|
|
||
| ### Phase 5 — E2E + ship (1–2 days) | ||
| - E2E spec: configure CC provider, send a message, verify response. | ||
| - Rust integration test exercising `Provider::chat` against a mocked `claude` binary (`scripts/test-rust-with-mock.sh` harness extension). | ||
| - Coverage ≥ 80% on changed lines (merge gate). | ||
| - PR to `tinyhumansai/openhuman:main` from `senamakel:feat/claude-code-provider`. | ||
|
|
||
| **Total estimate:** 7–11 days of focused work. | ||
|
|
||
| ## 11. Testing strategy | ||
|
|
||
| - **Unit (Vitest)** — frontend slice + components. | ||
| - **Unit (cargo)** — parser, mapper, auth, version check (all against fixtures, no real CC binary). | ||
| - **Rust integration** — driver against mocked binary that emits canned JSONL on stdin → stdout. | ||
| - **E2E (WDIO)** — happy path with CC mocked at the binary level via `OPENHUMAN_CLAUDE_BINARY` env override. | ||
|
|
||
| ## 12. Rollout | ||
|
|
||
| - Behind a settings toggle (defaults to off) for first release. No auto-selection. | ||
| - Document beta status in settings panel until v1.1 (cost wiring + write tools) lands. | ||
|
|
||
| ## 13. Locked decisions | ||
|
|
||
| 1. **MIN_VERSION**: `2.0.0`. `version_check.rs` blocks startup below this. | ||
| 2. **Read-only MCP tool subset (v1)**: `memory_search`, `memory_get`, `threads_list`, `threads_get`, `threads_messages`, `channels_list`, `channels_messages_read`, `people_search`, `people_get`, `webhooks_list`. Exposed as `mcp__openhuman__<name>`. Write tools deferred to v1.1. | ||
| 3. **Per-role provider selection**: CC selectable independently for `chat`, `agentic`, `reasoning` roles via factory string grammar. No single global toggle. | ||
| 4. **UI branding**: "Claude Code CLI" in all settings copy, provider picker labels, and status panel headings. |
Submodule tauri-cef
updated
2 files
| +15 −29 | crates/tauri-bundler/src/bundle/linux/appimage/sharun_cef.rs | |
| +22 −112 | crates/tauri-runtime-cef/src/cef_impl.rs |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
119 changes: 119 additions & 0 deletions
119
app/src/components/settings/panels/ai/ClaudeCodeStatusCard.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| import { useCallback, useEffect, useState } from 'react'; | ||
|
|
||
| import { | ||
| type ClaudeCodeStatus, | ||
| openhumanClaudeCodeStatus, | ||
| } from '../../../../utils/tauriCommands/config'; | ||
|
|
||
| /** | ||
| * Status card for the Claude Code CLI provider. | ||
| * | ||
| * Probes the local `claude` binary on mount (and on a manual Refresh) and | ||
| * surfaces install / version state to the user. Read-only — does not write | ||
| * any settings. Embed inside the AI settings panel above the routing | ||
| * dropdowns once per-role selection wiring lands. | ||
| */ | ||
| export function ClaudeCodeStatusCard() { | ||
| const [status, setStatus] = useState<ClaudeCodeStatus | null>(null); | ||
| const [error, setError] = useState<string | null>(null); | ||
| const [loading, setLoading] = useState<boolean>(false); | ||
|
|
||
| const probe = useCallback(async () => { | ||
| setLoading(true); | ||
| setError(null); | ||
| try { | ||
| const resp = await openhumanClaudeCodeStatus(); | ||
| setStatus(resp.result); | ||
| } catch (err) { | ||
| setError(err instanceof Error ? err.message : String(err)); | ||
| setStatus(null); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| }, []); | ||
|
|
||
| useEffect(() => { | ||
| void probe(); | ||
| }, [probe]); | ||
|
|
||
| return ( | ||
| <section | ||
| data-testid="claude-code-status-card" | ||
| className="rounded-lg border border-neutral-200 bg-white p-4 dark:border-neutral-800 dark:bg-neutral-900"> | ||
| <header className="mb-2 flex items-center justify-between"> | ||
| <h3 className="text-sm font-semibold text-neutral-900 dark:text-neutral-100"> | ||
| Claude Code CLI | ||
| </h3> | ||
| <button | ||
| type="button" | ||
| onClick={() => { | ||
| void probe(); | ||
| }} | ||
| disabled={loading} | ||
| className="text-xs text-neutral-500 hover:text-neutral-900 disabled:opacity-50 dark:text-neutral-400 dark:hover:text-neutral-100"> | ||
| {loading ? 'Probing…' : 'Refresh'} | ||
| </button> | ||
| </header> | ||
| <StatusBody status={status} error={error} /> | ||
| <p className="mt-3 text-xs text-neutral-500 dark:text-neutral-400"> | ||
| Use the <code>claude-code:<model></code> provider string to route chat, agentic, or | ||
| reasoning workloads through your local Claude Code CLI install. | ||
| </p> | ||
| </section> | ||
| ); | ||
| } | ||
|
|
||
| function StatusBody({ status, error }: { status: ClaudeCodeStatus | null; error: string | null }) { | ||
| if (error) { | ||
| return <p className="text-xs text-rose-600 dark:text-rose-400">Failed to probe: {error}</p>; | ||
| } | ||
| if (!status) { | ||
| return <p className="text-xs text-neutral-500 dark:text-neutral-400">Probing…</p>; | ||
| } | ||
| switch (status.status) { | ||
| case 'ok': | ||
| return ( | ||
| <dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-xs"> | ||
| <dt className="text-neutral-500">Status</dt> | ||
| <dd className="text-emerald-600 dark:text-emerald-400">Installed ({status.version})</dd> | ||
| <dt className="text-neutral-500">Path</dt> | ||
| <dd className="font-mono text-neutral-700 dark:text-neutral-300">{status.path}</dd> | ||
| </dl> | ||
| ); | ||
| case 'not_installed': | ||
| return ( | ||
| <p className="text-xs text-amber-600 dark:text-amber-400"> | ||
| Claude Code CLI is not installed. Install via{' '} | ||
| <code>npm install -g @anthropic-ai/claude-code</code> or follow{' '} | ||
| <a | ||
| href="https://docs.anthropic.com/en/docs/claude-code" | ||
| target="_blank" | ||
| rel="noreferrer noopener" | ||
| className="underline hover:text-amber-700 dark:hover:text-amber-300"> | ||
| Anthropic's docs | ||
| </a> | ||
| . | ||
| </p> | ||
| ); | ||
| case 'outdated': | ||
| return ( | ||
| <dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-xs"> | ||
| <dt className="text-neutral-500">Status</dt> | ||
| <dd className="text-rose-600 dark:text-rose-400"> | ||
| Outdated — found {status.version}, need ≥ {status.min_required} | ||
| </dd> | ||
| <dt className="text-neutral-500">Path</dt> | ||
| <dd className="font-mono text-neutral-700 dark:text-neutral-300">{status.path}</dd> | ||
| </dl> | ||
| ); | ||
| case 'unusable': | ||
| return ( | ||
| <dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-xs"> | ||
| <dt className="text-neutral-500">Status</dt> | ||
| <dd className="text-rose-600 dark:text-rose-400">Unusable — {status.reason}</dd> | ||
| <dt className="text-neutral-500">Path</dt> | ||
| <dd className="font-mono text-neutral-700 dark:text-neutral-300">{status.path}</dd> | ||
| </dl> | ||
| ); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handle
claude-codeexhaustively in route display paths.Adding the new variant here is correct, but
WorkloadRowand save-bardiffSummarystill fall through to local formatting, soclaude-coderoutes are shown asOllama/local:*.💡 Suggested patch
🤖 Prompt for AI Agents