feat(openclaude): vendor OpenClaude as headless gRPC daemon, eliminate aider
Browse filesReplace fragile aider+litellm subprocess shell-out with vendored
OpenClaude built from source via Bun, exposed as a headless gRPC
daemon on :50051 (DigitalOcean primary) and :50052 (OpenRouter
fallback). New openclaude_grpc/ Python bridge preserves the legacy
(combined_output, exit_code) contract used by SAST gate, conviction
engine, adversarial reviewer, and red-team loops.
- Dockerfile: 3-stage build (bun bundle -> python base -> runtime
with auto protoc stub generation). Removes aider/litellm/configargparse
hacks (-30 lines).
- requirements.txt: purged aider-chat + litellm + 27 curated runtime
imports; adds grpcio + protobuf as the only new Python deps.
- entrypoint.sh: launches both daemons with provider-specific OPENAI_*
env, hot-loads /tmp/mcp_runtime.json per chat session.
- app.py: run_aider is now a thin alias to run_openclaude. Legacy
aider-patcher MCP server entry deleted.
- vendor/openclaude/src/grpc/server.ts: OPENCLAUDE_AUTO_APPROVE=1
short-circuits permission prompts; reconnectMcpServerImpl loads MCP
servers from MCP_RUNTIME_CONFIG with mtime-based caching.
- SYSTEM_ANALYSIS_BOOK.md: changelog row 7 + section 2.1 rewritten.
- .gitignore_extra +1 -0
- ARCHITECTURE_ANALYSIS.md +0 -527
- COMPARISON_REPORT.md +0 -239
- Dockerfile +86 -49
- SYSTEM_ANALYSIS_BOOK.md +40 -37
- app.py +70 -81
- entrypoint.sh +57 -0
- mythos/MYTHOS_PLAN.md +0 -249
- openclaude_grpc/__init__.py +21 -0
- openclaude_grpc/client.py +348 -0
- requirements.txt +20 -26
- vendor/openclaude/.dockerignore +16 -0
- vendor/openclaude/.env.example +374 -0
- vendor/openclaude/.gitignore +14 -0
- vendor/openclaude/.release-please-manifest.json +3 -0
- vendor/openclaude/ANDROID_INSTALL.md +162 -0
- vendor/openclaude/CHANGELOG.md +176 -0
- vendor/openclaude/CODE_OF_CONDUCT.md +126 -0
- vendor/openclaude/CONTRIBUTING.md +119 -0
- vendor/openclaude/Dockerfile +46 -0
- vendor/openclaude/LICENSE +29 -0
- vendor/openclaude/PLAYBOOK.md +322 -0
- vendor/openclaude/README.md +345 -0
- vendor/openclaude/SECURITY.md +69 -0
- vendor/openclaude/bin/import-specifier.mjs +13 -0
- vendor/openclaude/bin/import-specifier.test.mjs +13 -0
- vendor/openclaude/bin/openclaude +32 -0
- vendor/openclaude/bun.lock +0 -0
- vendor/openclaude/docs/advanced-setup.md +273 -0
- vendor/openclaude/docs/hook-chains.md +333 -0
- vendor/openclaude/docs/litellm-setup.md +144 -0
- vendor/openclaude/docs/non-technical-setup.md +116 -0
- vendor/openclaude/docs/quick-start-mac-linux.md +143 -0
- vendor/openclaude/docs/quick-start-windows.md +143 -0
- vendor/openclaude/package.json +162 -0
- vendor/openclaude/python/__init__.py +1 -0
- vendor/openclaude/python/atomic_chat_provider.py +146 -0
- vendor/openclaude/python/ollama_provider.py +173 -0
- vendor/openclaude/python/requirements.txt +3 -0
- vendor/openclaude/python/smart_router.py +387 -0
- vendor/openclaude/python/tests/__init__.py +1 -0
- vendor/openclaude/python/tests/conftest.py +5 -0
- vendor/openclaude/python/tests/test_atomic_chat_provider.py +130 -0
- vendor/openclaude/python/tests/test_ollama_provider.py +192 -0
- vendor/openclaude/python/tests/test_smart_router.py +231 -0
- vendor/openclaude/release-please-config.json +11 -0
- vendor/openclaude/scripts/build.ts +492 -0
- vendor/openclaude/scripts/grpc-cli.ts +121 -0
- vendor/openclaude/scripts/no-telemetry-growthbook-stub.test.ts +163 -0
- vendor/openclaude/scripts/no-telemetry-plugin.ts +459 -0
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
vendor/openclaude/node_modules/
|
|
@@ -1,527 +0,0 @@
|
|
| 1 |
-
# Rhodawk AI DevSecOps Engine β End-to-End Architectural Analysis
|
| 2 |
-
|
| 3 |
-
> Generated by an automated architect/security-researcher pass over commit
|
| 4 |
-
> `14b1bbe` of `Architect8999/rhodawk-ai-devops-engine` on the
|
| 5 |
-
> HuggingFace Spaces repository.
|
| 6 |
-
> Scope: every Python module in the repo root + the new `mythos/` package
|
| 7 |
-
> (~17 kLOC of Python, 30 MCP servers, 1 Dockerfile, 1 Gradio UI).
|
| 8 |
-
|
| 9 |
-
---
|
| 10 |
-
|
| 11 |
-
## Phase 1 β High-Level Reconnaissance & Topology
|
| 12 |
-
|
| 13 |
-
### 1.1 Virtual directory tree (top two levels)
|
| 14 |
-
|
| 15 |
-
```
|
| 16 |
-
rhodawk-ai-devops-engine/
|
| 17 |
-
βββ app.py # Gradio control plane + main loop (~117 KB)
|
| 18 |
-
βββ hermes_orchestrator.py # Multi-phase research brain (~30 KB)
|
| 19 |
-
βββ language_runtime.py # Polyglot runtime sandbox (~70 KB)
|
| 20 |
-
βββ red_team_fuzzer.py # CEGIS-style adversarial fuzzer (~62 KB)
|
| 21 |
-
β
|
| 22 |
-
βββ adversarial_reviewer.py # 3-model concurrent LLM verdict
|
| 23 |
-
βββ audit_logger.py # Hash-chained tamper-evident log
|
| 24 |
-
βββ bounty_gateway.py # H1 / GHSA submission pipeline
|
| 25 |
-
βββ chain_analyzer.py # Commit-history attack graphs
|
| 26 |
-
βββ commit_watcher.py # CAD β silent-patch detector
|
| 27 |
-
βββ conviction_engine.py # Multi-criterion auto-merge gate
|
| 28 |
-
βββ cve_intel.py # NVD / OSV / Exploit-DB lookups
|
| 29 |
-
βββ disclosure_vault.py # Encrypted finding storage
|
| 30 |
-
βββ embedding_memory.py # sentence-transformers + sqlite-vec
|
| 31 |
-
βββ exploit_primitives.py # ROP / heap / shellcode helpers
|
| 32 |
-
βββ formal_verifier.py # Z3 bounded verification
|
| 33 |
-
βββ fuzzing_engine.py # Hypothesis fuzzer driver
|
| 34 |
-
βββ github_app.py # GitHub App JWT / installation token
|
| 35 |
-
βββ harness_factory.py # Auto-generates fuzzing harnesses
|
| 36 |
-
βββ job_queue.py # JSON-file-backed job ledger
|
| 37 |
-
βββ lora_scheduler.py # Threshold-triggered LoRA exports
|
| 38 |
-
βββ memory_engine.py # TF-IDF retrieval (legacy)
|
| 39 |
-
βββ notifier.py # Slack / Discord / email fan-out
|
| 40 |
-
βββ public_leaderboard.py # Public stats endpoint
|
| 41 |
-
βββ repo_harvester.py # Antagonist target picker
|
| 42 |
-
βββ sast_gate.py # bandit + 16 secret patterns
|
| 43 |
-
βββ semantic_extractor.py # Function / call-graph extraction
|
| 44 |
-
βββ supply_chain.py # pip-audit + typosquat heuristics
|
| 45 |
-
βββ swebench_harness.py # SWE-bench evaluation runner
|
| 46 |
-
βββ symbolic_engine.py # angr planner + Z3 solver glue
|
| 47 |
-
βββ taint_analyzer.py # Sourceβsink taint tracking
|
| 48 |
-
βββ training_store.py # SQLite/PG attempt ledger + HF push
|
| 49 |
-
βββ verification_loop.py # Retry / prompt-build state machine
|
| 50 |
-
βββ vuln_classifier.py # CWE taxonomy + scoring
|
| 51 |
-
βββ webhook_server.py # GitHub webhook HTTPServer (port 7861)
|
| 52 |
-
βββ worker_pool.py # Process-isolated parallel audits
|
| 53 |
-
β
|
| 54 |
-
βββ mythos/ # β¬
NEW: Mythos-level upgrade package
|
| 55 |
-
β βββ MYTHOS_PLAN.md
|
| 56 |
-
β βββ __init__.py / integration.py
|
| 57 |
-
β βββ agents/ # planner / explorer / executor / orchestrator
|
| 58 |
-
β βββ reasoning/ # probabilistic + attack_graph
|
| 59 |
-
β βββ static/ # tree-sitter, joern, codeql, semgrep bridges
|
| 60 |
-
β βββ dynamic/ # aflpp, klee, qemu, frida, gdb
|
| 61 |
-
β βββ exploit/ # pwntools, ROP, heap, privesc KB
|
| 62 |
-
β βββ learning/ # rl_planner, mlflow, lora, curriculum, episodic
|
| 63 |
-
β βββ mcp/ # 5 new MCP servers
|
| 64 |
-
β βββ skills/ # agentskills.io registry
|
| 65 |
-
β βββ api/ # FastAPI productization (auth, webhooks, schemas)
|
| 66 |
-
β
|
| 67 |
-
βββ mcp_config.json # 30 MCP servers registered
|
| 68 |
-
βββ Dockerfile # python:3.12-slim + uv + node + Gradio
|
| 69 |
-
βββ requirements.txt # ~30 first-class deps + Mythos optional
|
| 70 |
-
βββ pitch_deck/, pitch-deck/ # Marketing collateral (PDF/PPTX/HTML)
|
| 71 |
-
βββ FOUNDER_PLAYBOOK.md, SECURITY_RESEARCH_PLAYBOOK.md, README.md
|
| 72 |
-
βββ .git/
|
| 73 |
-
```
|
| 74 |
-
|
| 75 |
-
### 1.2 Structural paradigm
|
| 76 |
-
|
| 77 |
-
A **flat-module monolith with a side-car package**. There is no explicit
|
| 78 |
-
layered or hexagonal partitioning at the root: every concern (UI, orchestration,
|
| 79 |
-
analysis tools, persistence, networking) lives as a peer `*.py` module
|
| 80 |
-
imported by `app.py`. Communication is **in-process function calls** plus
|
| 81 |
-
**JSON files on `/data`** for cross-process state (jobs, audit chain, memory).
|
| 82 |
-
The new `mythos/` package adds a proper Python package with sub-modules per
|
| 83 |
-
concern, intended as the migration target for a cleaner future architecture.
|
| 84 |
-
|
| 85 |
-
### 1.3 Technology stack
|
| 86 |
-
|
| 87 |
-
| Layer | Technology |
|
| 88 |
-
|---|---|
|
| 89 |
-
| Runtime | Python 3.12 (slim Docker), Node.js 20 (npm-installed for some MCP servers) |
|
| 90 |
-
| UI / Control plane | **Gradio 5.29** on port 7860 |
|
| 91 |
-
| Webhooks | Stdlib `http.server.BaseHTTPRequestHandler` on port 7861 |
|
| 92 |
-
| LLM gateway | **OpenRouter** (DeepSeek-R1 / V3 free tier, plus Qwen β₯ Gemma β₯ Mistral consensus); env-driven model tiers in Mythos |
|
| 93 |
-
| Code patching | **Aider 0.86** (driven via subprocess + MCP config) |
|
| 94 |
-
| Static / SAST | bandit, ruff, semgrep, radon, custom 16-pattern secret scanner |
|
| 95 |
-
| Symbolic / Formal | **z3-solver**, **angr**, custom symbolic engine |
|
| 96 |
-
| Fuzzing | Hypothesis (atheris removed β see Dockerfile comments) |
|
| 97 |
-
| Embeddings / Memory | sentence-transformers + sqlite-vec, optional Qdrant |
|
| 98 |
-
| Persistence | SQLite (default) / Postgres (`psycopg2-binary`) for training store |
|
| 99 |
-
| ML / Training | transformers, torch, datasets, custom LoRA scheduler |
|
| 100 |
-
| MCP | `@modelcontextprotocol/server-github` (npm), `mcp-server-fetch` (uvx), and **30** servers in `mcp_config.json` (25 base + 5 new Mythos ones) |
|
| 101 |
-
| Mythos add-ons | FastAPI + uvicorn + pydantic; optional Pyro/PyMC, MLflow, RLlib, pwntools, Frida, tree-sitter-languages |
|
| 102 |
-
| Versioning / VCS | GitPython, PyGithub, PyJWT |
|
| 103 |
-
| Container | Two-stage `python:3.12-slim` Dockerfile, non-root UID 1000, `/data` writable, `EXPOSE 7860`, `CMD ["python","-u","app.py"]` |
|
| 104 |
-
| Deployment target | HuggingFace Spaces (declared in README YAML front-matter) |
|
| 105 |
-
|
| 106 |
-
### 1.4 Headline metrics
|
| 107 |
-
|
| 108 |
-
- **Python LOC:** 16 981 across 47 root modules + 47 Mythos modules.
|
| 109 |
-
- **Top three by size:** `app.py` (β3 200 LOC), `language_runtime.py` (β70 KB),
|
| 110 |
-
`red_team_fuzzer.py` (β62 KB), `hermes_orchestrator.py` (β900 LOC).
|
| 111 |
-
- **Tests:** none in the repo root β `pytest` is invoked **on the target
|
| 112 |
-
repository** being audited, not on Rhodawk itself.
|
| 113 |
-
|
| 114 |
-
---
|
| 115 |
-
|
| 116 |
-
## Phase 2 β Entry Point & Execution Flow Mapping
|
| 117 |
-
|
| 118 |
-
### 2.1 Entry points
|
| 119 |
-
|
| 120 |
-
| Surface | Entry | Listens on | Triggered by |
|
| 121 |
-
|---|---|---|---|
|
| 122 |
-
| **Primary** | `python -u app.py` (Dockerfile `CMD`) | TCP `:7860` (Gradio) | User opens HF Space URL |
|
| 123 |
-
| **Webhook** | `webhook_server.start_webhook_server()` invoked from `app.py` `__main__` | TCP `:7861` (HTTPServer in a daemon thread) | GitHub `push` / `pull_request` events |
|
| 124 |
-
| **Mythos API** | `uvicorn mythos.api.fastapi_server:app` (manual / opt-in) | configurable | `POST /v1/analyze_target` and webhook callbacks |
|
| 125 |
-
| **MCP servers** | Spawned on demand by Aider via `mcp_config.json` (`stdio` JSON-RPC) | stdin/stdout | Aider's tool calls during patching |
|
| 126 |
-
|
| 127 |
-
### 2.2 Boot sequence (from `if __name__ == "__main__"` in `app.py`)
|
| 128 |
-
|
| 129 |
-
```
|
| 130 |
-
1. ui_log("Rhodawk AI v3.0 starting β¦")
|
| 131 |
-
2. Daemon thread β embedding_memory.pre_warm_model()
|
| 132 |
-
(downloads sentence-transformers model in background to avoid first-call latency)
|
| 133 |
-
3. start_webhook_server()
|
| 134 |
-
β HTTPServer on 0.0.0.0:7861, daemon thread,
|
| 135 |
-
registers _webhook_dispatch as the job dispatcher
|
| 136 |
-
4. demo.launch(server_name=0.0.0.0, server_port=$PORT or 7860)
|
| 137 |
-
β Gradio Blocks UI bound to enterprise_audit_loop, Hermes tabs,
|
| 138 |
-
job table, audit chain viewer, leaderboard, etc.
|
| 139 |
-
5. gr.Timer(3) ticks every 3 s β get_combined_refresh()
|
| 140 |
-
(single SSE stream β comment in app.py notes it replaced
|
| 141 |
-
three concurrent streams that were exhausting connection limits)
|
| 142 |
-
```
|
| 143 |
-
|
| 144 |
-
### 2.3 Control flow β primary "audit" loop
|
| 145 |
-
|
| 146 |
-
`enterprise_audit_loop()` in `app.py` (l. 965) is the heart. Per repo:
|
| 147 |
-
|
| 148 |
-
```
|
| 149 |
-
configure_git_credentials() # writes ~/.git-credentials from env
|
| 150 |
-
clone target β /tmp/repo
|
| 151 |
-
discover failing tests via pytest --collect-only + run
|
| 152 |
-
for each failing_test:
|
| 153 |
-
process_audit_test()
|
| 154 |
-
ββ retrieve_similar_fixes_v2() # embedding_memory (fallback: TF-IDF memory_engine)
|
| 155 |
-
ββ build_initial_prompt() # verification_loop
|
| 156 |
-
ββ write_mcp_config() β mcp_config.json on disk
|
| 157 |
-
ββ run_aider(mcp_config_path, prompt, context_files) # subprocess Aider with MCP
|
| 158 |
-
ββ re-run pytest β VerificationAttempt
|
| 159 |
-
ββ if fail and attempts < MAX_RETRIES:
|
| 160 |
-
β build_retry_prompt(failure + previous diff) β loop
|
| 161 |
-
ββ run_sast_gate() # bandit + 16 secret patterns
|
| 162 |
-
ββ run_supply_chain_gate() # pip-audit + typosquat
|
| 163 |
-
ββ run_adversarial_review() # 3 LLMs in parallel; ACTS Bayesian score
|
| 164 |
-
ββ run_formal_verification() # Z3 bounded checks
|
| 165 |
-
ββ if adversary REJECT:
|
| 166 |
-
β retry with critique * ADVERSARIAL_REJECTION_MULTIPLIER
|
| 167 |
-
ββ evaluate_conviction() # multi-criteria gate
|
| 168 |
-
ββ if conviction high β auto_merge_pr() else create_github_pr()
|
| 169 |
-
ββ record_attempt() / update_test_result() # training_store (SQLite/PG)
|
| 170 |
-
ββ record_fix_outcome() # memory_engine writes back lesson
|
| 171 |
-
ββ maybe_trigger_training() # lora_scheduler exports HF dataset
|
| 172 |
-
```
|
| 173 |
-
|
| 174 |
-
A parallel **Hermes research mode** (`hermes_orchestrator.run_hermes_research`)
|
| 175 |
-
sits beside the audit loop. It runs the six phases RECON β STATIC β DYNAMIC β
|
| 176 |
-
EXPLOIT β CONSENSUS β DISCLOSURE and produces a `HermesSession` containing
|
| 177 |
-
`VulnerabilityFinding`s with VES / TVG / ACTS / CAD / SSEC scores.
|
| 178 |
-
|
| 179 |
-
### 2.4 Data flow
|
| 180 |
-
|
| 181 |
-
```
|
| 182 |
-
GitHub repo ββcloneβββΊ /tmp/repo/<sha> (ephemeral)
|
| 183 |
-
β
|
| 184 |
-
pytest output βββββββββ€
|
| 185 |
-
βΌ
|
| 186 |
-
memory_engine βββ embedding_memory (sqlite-vec @ /data)
|
| 187 |
-
β
|
| 188 |
-
βΌ
|
| 189 |
-
MCP-equipped Aider ββ subprocess βββΊ fixed source
|
| 190 |
-
β
|
| 191 |
-
adversarial verdicts ββ€
|
| 192 |
-
βΌ
|
| 193 |
-
conviction_engine βββΊ PR / auto-merge
|
| 194 |
-
β
|
| 195 |
-
attempt row βββββββββββ΄ββββββββββββΊ training_store
|
| 196 |
-
(SQLite at /data/store.db
|
| 197 |
-
or Postgres if DATABASE_URL)
|
| 198 |
-
β
|
| 199 |
-
βΌ
|
| 200 |
-
lora_scheduler βββΊ HF dataset
|
| 201 |
-
```
|
| 202 |
-
|
| 203 |
-
All persistent state lives under `/data` (writable in HF Spaces, mode 777
|
| 204 |
-
in the Dockerfile). Job ledger files: `/data/jobs/<job_id>.json`. Hash-chained
|
| 205 |
-
audit log: `/data/audit_chain.jsonl`. Embeddings vector store: `/data/mem.db`.
|
| 206 |
-
Mythos adds `/data/mythos/{rl_state.json,episodic.sqlite,skills/}`.
|
| 207 |
-
|
| 208 |
-
---
|
| 209 |
-
|
| 210 |
-
## Phase 3 β Functional Breakdown
|
| 211 |
-
|
| 212 |
-
### 3.1 Executive summary
|
| 213 |
-
|
| 214 |
-
Rhodawk is an **autonomous DevSecOps control plane**. Point it at a GitHub
|
| 215 |
-
repo; it (1) reproduces failing tests, (2) drives an LLM coding agent (Aider,
|
| 216 |
-
through 30 MCP-exposed tools) to write a patch, (3) re-runs the tests in a
|
| 217 |
-
verification loop, (4) gates the patch through SAST + supply-chain + multi-LLM
|
| 218 |
-
adversarial review + Z3 formal verification + a conviction engine, (5) opens
|
| 219 |
-
or auto-merges a pull request, and (6) feeds every attempt into a training
|
| 220 |
-
store so a LoRA fine-tune can be scheduled. A parallel **Hermes** mode flips
|
| 221 |
-
the polarity from "fix bugs" to "find bugs": coordinated multi-phase
|
| 222 |
-
vulnerability research with custom scoring algorithms (VES/TVG/ACTS/CAD/SSEC)
|
| 223 |
-
and a HackerOne / GitHub Security Advisory submission gateway. The new
|
| 224 |
-
**Mythos package** layers a multi-agent (planner / explorer / executor)
|
| 225 |
-
framework, probabilistic Bayesian reasoning, advanced static / dynamic /
|
| 226 |
-
exploit tooling, RL-driven self-improvement, 5 additional MCP servers, and a
|
| 227 |
-
FastAPI productization surface on top of all of the above.
|
| 228 |
-
|
| 229 |
-
### 3.2 Module responsibilities (selected)
|
| 230 |
-
|
| 231 |
-
| Module | Responsibility |
|
| 232 |
-
|---|---|
|
| 233 |
-
| `app.py` | Gradio UI, audit loop, Aider subprocess driver, refresh timer, boot sequence |
|
| 234 |
-
| `hermes_orchestrator.py` | Phase state machine, custom security metrics (VES/ACTS/TVG), tool dispatcher |
|
| 235 |
-
| `verification_loop.py` | Retry policy, prompt construction, attempt accounting |
|
| 236 |
-
| `adversarial_reviewer.py` | 3-model parallel verdict β consensus, used by both audit and Hermes |
|
| 237 |
-
| `conviction_engine.py` | Boolean / weighted gate that decides auto-merge vs human review |
|
| 238 |
-
| `sast_gate.py`, `supply_chain.py`, `formal_verifier.py` | Independent gates the patch must pass |
|
| 239 |
-
| `language_runtime.py` | Polyglot sandbox factory β sets up Python venvs, Node, Java, etc. for the target repo |
|
| 240 |
-
| `red_team_fuzzer.py` | CEGIS adversarial fuzzer β counter-example guided refinement |
|
| 241 |
-
| `embedding_memory.py` / `memory_engine.py` | v2 semantic retrieval (sentence-transformers + sqlite-vec); v1 TF-IDF fallback |
|
| 242 |
-
| `training_store.py` | SQLite / Postgres attempt ledger + `export_hf_dataset` for HF push |
|
| 243 |
-
| `lora_scheduler.py` | Threshold-triggered LoRA training-data export |
|
| 244 |
-
| `audit_logger.py` | Append-only hash-chained log + `verify_chain_integrity` |
|
| 245 |
-
| `webhook_server.py` | HMAC-verified GitHub webhook receiver, IP rate-limit, dispatcher hook |
|
| 246 |
-
| `worker_pool.py` | Process-isolated parallel test handling (`MAX_WORKERS`) |
|
| 247 |
-
| `bounty_gateway.py` | Holds findings for human approval β submits to HackerOne / opens GHSA |
|
| 248 |
-
| `vuln_classifier.py` / `cve_intel.py` | CWE taxonomy + NVD/OSV/Exploit-DB lookup |
|
| 249 |
-
| `commit_watcher.py` / `chain_analyzer.py` | Silent-patch detection + per-commit attack-graph diffing |
|
| 250 |
-
| `mythos/agents/*` | Planner produces a probabilistic plan; Explorer enumerates hypotheses; Executor runs tools; Orchestrator routes |
|
| 251 |
-
| `mythos/reasoning/probabilistic.py` | Bayesian hypothesis sampling (Pyro / PyMC / NumPy fallback) |
|
| 252 |
-
| `mythos/learning/rl_planner.py` | Tool-selection policy (RLlib / SB3 / UCB1 fallback), state at `/data/mythos/rl_state.json` |
|
| 253 |
-
| `mythos/api/fastapi_server.py` | `POST /v1/analyze_target`, auth middleware, webhook callbacks |
|
| 254 |
-
| `mythos/mcp/*` | 5 new servers exposed via `python -m mythos.mcp.<name>` and registered in `mcp_config.json` |
|
| 255 |
-
|
| 256 |
-
### 3.3 Background work / scheduled tasks
|
| 257 |
-
|
| 258 |
-
- **Embedding pre-warm thread** (daemon, `app.py` `__main__`).
|
| 259 |
-
- **Webhook HTTPServer thread** (daemon).
|
| 260 |
-
- **Gradio refresh timer** (3 s SSE tick) β coalesced into one stream.
|
| 261 |
-
- **Worker-pool subprocesses** for per-test isolation (`worker_pool._run_isolated`).
|
| 262 |
-
- **LoRA scheduler** triggers on attempt-count threshold (no cron β checked at the end of each audit).
|
| 263 |
-
- **MCP server lifecycle** β Aider spawns each declared MCP server on demand and tears it down with the patch session.
|
| 264 |
-
|
| 265 |
-
There is **no Celery / RQ / APScheduler** β concurrency is purely
|
| 266 |
-
threading + subprocess + ad-hoc daemons.
|
| 267 |
-
|
| 268 |
-
---
|
| 269 |
-
|
| 270 |
-
## Phase 4 β Build & Execution Guide
|
| 271 |
-
|
| 272 |
-
### 4.1 Prerequisites
|
| 273 |
-
|
| 274 |
-
| Required | Notes |
|
| 275 |
-
|---|---|
|
| 276 |
-
| Python **3.12** | Pinned in README front-matter and Dockerfile |
|
| 277 |
-
| Node.js + npm | Only for `@modelcontextprotocol/server-github` |
|
| 278 |
-
| `uv` (Astral) | Used by `language_runtime` to materialise per-target venvs |
|
| 279 |
-
| `git` | GitPython invokes the system binary |
|
| 280 |
-
| Writable `/data` (Linux) or local equivalent | All persistent state lives here |
|
| 281 |
-
| **Env vars** | `OPENROUTER_API_KEY` (mandatory for any LLM call); `GITHUB_TOKEN` *or* GitHub App creds (`GITHUB_APP_ID`, `GITHUB_APP_PRIVATE_KEY`, `GITHUB_INSTALLATION_ID`); optional `DATABASE_URL` for Postgres training store; `HF_TOKEN` for HF dataset push; `GITHUB_WEBHOOK_SECRET` for webhook HMAC; Slack/Discord URLs for notifier; Mythos tier overrides `MYTHOS_TIER1_PRIMARY` / `MYTHOS_TIER2_PRIMARY` etc.; `RHODAWK_MYTHOS=1` to engage multi-agent loop |
|
| 282 |
-
| Optional native tools | Joern, CodeQL, AFL++, KLEE, QEMU, Frida, GDB, ROPGadget, pwntools β Mythos bridges degrade gracefully if absent |
|
| 283 |
-
|
| 284 |
-
### 4.2 Setup β local
|
| 285 |
-
|
| 286 |
-
```bash
|
| 287 |
-
# 1. Clone
|
| 288 |
-
git clone https://huggingface.co/spaces/Architect8999/rhodawk-ai-devops-engine
|
| 289 |
-
cd rhodawk-ai-devops-engine
|
| 290 |
-
|
| 291 |
-
# 2. Python deps
|
| 292 |
-
python3.12 -m venv .venv && source .venv/bin/activate
|
| 293 |
-
pip install --upgrade pip
|
| 294 |
-
pip install -r requirements.txt mcp-server-fetch
|
| 295 |
-
|
| 296 |
-
# 3. Node-based MCP server
|
| 297 |
-
npm install -g @modelcontextprotocol/server-github
|
| 298 |
-
|
| 299 |
-
# 4. (Optional) astral uv for runtime sandboxing
|
| 300 |
-
curl -LsSf https://astral.sh/uv/install.sh | sh
|
| 301 |
-
|
| 302 |
-
# 5. Persistent state directory
|
| 303 |
-
sudo mkdir -p /data && sudo chmod 777 /data
|
| 304 |
-
|
| 305 |
-
# 6. Environment
|
| 306 |
-
export OPENROUTER_API_KEY=sk-or-...
|
| 307 |
-
export GITHUB_TOKEN=ghp_... # or GITHUB_APP_* trio
|
| 308 |
-
export GITHUB_WEBHOOK_SECRET=whsec_...
|
| 309 |
-
# Optional
|
| 310 |
-
export DATABASE_URL=postgres://...
|
| 311 |
-
export HF_TOKEN=hf_...
|
| 312 |
-
export RHODAWK_MYTHOS=1
|
| 313 |
-
```
|
| 314 |
-
|
| 315 |
-
### 4.3 Run
|
| 316 |
-
|
| 317 |
-
```bash
|
| 318 |
-
# Primary control plane (Gradio on :7860, webhook on :7861)
|
| 319 |
-
PORT=7860 python -u app.py
|
| 320 |
-
|
| 321 |
-
# Or via Docker (the way HF Spaces runs it)
|
| 322 |
-
docker build -t rhodawk-ai .
|
| 323 |
-
docker run -it --rm \
|
| 324 |
-
-p 7860:7860 -p 7861:7861 \
|
| 325 |
-
-e OPENROUTER_API_KEY -e GITHUB_TOKEN -e GITHUB_WEBHOOK_SECRET \
|
| 326 |
-
-v $PWD/data:/data \
|
| 327 |
-
rhodawk-ai
|
| 328 |
-
|
| 329 |
-
# Mythos productization API (independent of the Gradio loop)
|
| 330 |
-
uvicorn mythos.api.fastapi_server:app --host 0.0.0.0 --port 8000
|
| 331 |
-
|
| 332 |
-
# Run a single Mythos MCP server manually (smoke test)
|
| 333 |
-
python -m mythos.mcp.static_analysis_mcp
|
| 334 |
-
```
|
| 335 |
-
|
| 336 |
-
### 4.4 Verification
|
| 337 |
-
|
| 338 |
-
```bash
|
| 339 |
-
# 1. Gradio UI returns HTML
|
| 340 |
-
curl -sf http://localhost:7860/ | head -n 5
|
| 341 |
-
|
| 342 |
-
# 2. Webhook server is up (expect 405 Method Not Allowed on GET)
|
| 343 |
-
curl -sv http://localhost:7861/webhook 2>&1 | grep "HTTP/1"
|
| 344 |
-
|
| 345 |
-
# 3. Send a synthetic GitHub ping (replace SECRET)
|
| 346 |
-
BODY='{"zen":"hello"}'
|
| 347 |
-
SIG="sha256=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$GITHUB_WEBHOOK_SECRET" | cut -d' ' -f2)"
|
| 348 |
-
curl -sv -X POST http://localhost:7861/webhook \
|
| 349 |
-
-H "X-GitHub-Event: ping" \
|
| 350 |
-
-H "X-Hub-Signature-256: $SIG" \
|
| 351 |
-
-H "Content-Type: application/json" \
|
| 352 |
-
-d "$BODY"
|
| 353 |
-
|
| 354 |
-
# 4. Mythos API health
|
| 355 |
-
curl -sf http://localhost:8000/healthz
|
| 356 |
-
curl -sX POST http://localhost:8000/v1/analyze_target \
|
| 357 |
-
-H "Content-Type: application/json" \
|
| 358 |
-
-d '{"target":"https://github.com/octocat/Hello-World","mode":"recon"}'
|
| 359 |
-
|
| 360 |
-
# 5. Verify the audit chain is internally consistent
|
| 361 |
-
python -c "from audit_logger import verify_chain_integrity; print(verify_chain_integrity())"
|
| 362 |
-
|
| 363 |
-
# 6. Confirm 30 MCP servers register
|
| 364 |
-
jq '.mcpServers | length' mcp_config.json # β 30
|
| 365 |
-
```
|
| 366 |
-
|
| 367 |
-
In the Gradio UI you should see live logs ticking every 3 s, the metrics
|
| 368 |
-
row populating, and the **Hermes** and **CWE Reference** tabs available.
|
| 369 |
-
|
| 370 |
-
---
|
| 371 |
-
|
| 372 |
-
## Phase 5 β Architectural Thoughts & Stability Assessment
|
| 373 |
-
|
| 374 |
-
### 5.1 Patterns in use
|
| 375 |
-
|
| 376 |
-
- **Pipeline / chain-of-responsibility** β the audit loop is a clean
|
| 377 |
-
sequence of independent gates (`sast_gate β supply_chain β adversarial β
|
| 378 |
-
formal β conviction`). Each gate exposes a single function returning a
|
| 379 |
-
decision dataclass; they are trivially composable.
|
| 380 |
-
- **Strategy + graceful-degradation wrappers** β Mythos bridges
|
| 381 |
-
(`mythos/static/joern_bridge.py`, etc.) all expose `available()` plus a
|
| 382 |
-
pure-Python fallback. The orchestrator can plan with whatever is on the
|
| 383 |
-
PATH today.
|
| 384 |
-
- **Tier-routing for LLMs** β Hermes has two model tiers (`HERMES_MODEL`
|
| 385 |
-
reasoning + `HERMES_FAST_MODEL` for cheap triage); Mythos generalises this
|
| 386 |
-
to env-driven `MYTHOS_TIER{1,2}_{PRIMARY,FALLBACK}`.
|
| 387 |
-
- **Bayesian / consensus voting** β `compute_acts()` produces a Bayesian
|
| 388 |
-
multi-model trust score that is reused by both adversarial review and
|
| 389 |
-
conviction.
|
| 390 |
-
- **Append-only hash chain** β `audit_logger` builds a Merkle-style chain
|
| 391 |
-
with `verify_chain_integrity()`, which is the right primitive for
|
| 392 |
-
compliance evidence.
|
| 393 |
-
- **MCP as the universal tool bus** β every external capability (SAST,
|
| 394 |
-
fuzzers, vuln DBs, web search, GitHub, Postgres) is exposed through
|
| 395 |
-
`mcp_config.json`; this is by far the strongest piece of architecture in
|
| 396 |
-
the codebase and is what makes Aider's tool use uniform.
|
| 397 |
-
- **Multi-agent (Mythos)** β Planner emits a probabilistic plan, Explorer
|
| 398 |
-
enumerates hypotheses, Executor invokes tools, Orchestrator routes β a
|
| 399 |
-
textbook multi-agent shape with clean message contracts in
|
| 400 |
-
`mythos/agents/base.py`.
|
| 401 |
-
|
| 402 |
-
### 5.2 Strengths
|
| 403 |
-
|
| 404 |
-
- **Sharp separation between "fix mode" (audit loop) and "find mode"
|
| 405 |
-
(Hermes)** β they share gates and the training store but never tangle.
|
| 406 |
-
- **Robust webhook surface** β HMAC verification, IP rate-limit, dispatcher
|
| 407 |
-
injection (`set_job_dispatcher`) keeps the receiver pure.
|
| 408 |
-
- **Worker-pool isolation** β `worker_pool._run_isolated` puts each test fix
|
| 409 |
-
in its own subprocess, which contains LLM/aider blow-ups well.
|
| 410 |
-
- **Mythos add-on is non-invasive** β opt-in via `RHODAWK_MYTHOS=1` and a
|
| 411 |
-
separate FastAPI surface, so the existing Gradio UX is unchanged unless
|
| 412 |
-
you want it.
|
| 413 |
-
- **Persistent learning loop** β every attempt, success or failure, lands
|
| 414 |
-
in `training_store` and feeds `lora_scheduler.maybe_trigger_training`.
|
| 415 |
-
The flywheel is real and not aspirational.
|
| 416 |
-
|
| 417 |
-
### 5.3 Bug surfaces & risks
|
| 418 |
-
|
| 419 |
-
The list below is **prioritised** β items are ordered by likely real-world
|
| 420 |
-
impact on stability or security.
|
| 421 |
-
|
| 422 |
-
#### High
|
| 423 |
-
|
| 424 |
-
1. **`app.py` is a 117 KB god-module.** Boot, UI definition, audit loop,
|
| 425 |
-
subprocess management, MCP-config writing, Hermes UI bindings, and the
|
| 426 |
-
refresh timer all live in one file. This is the biggest stability risk:
|
| 427 |
-
any change ripples broadly and there are no unit tests on Rhodawk
|
| 428 |
-
itself. Recommend extracting the UI definition into `ui/`, the audit
|
| 429 |
-
loop into `audit/`, and process management into `procctl.py`.
|
| 430 |
-
2. **No test suite for Rhodawk.** `pytest` is invoked only against target
|
| 431 |
-
repos. There is no CI guard against regressions in the orchestration
|
| 432 |
-
logic. This is the single highest-leverage fix.
|
| 433 |
-
3. **`/data` mode `777` and shared by every tenant.** The Dockerfile sets
|
| 434 |
-
`chmod 777 /data` (because of HF Spaces UID quirks). All tenants share
|
| 435 |
-
`/data/jobs/`, `/data/audit_chain.jsonl`, `/data/store.db`, etc. The
|
| 436 |
-
`tenant_id` is stamped on each job key, but a buggy import or path-
|
| 437 |
-
traversal-style filename would let one tenant's data overwrite
|
| 438 |
-
another's. Add a `pathlib` allowlist + per-tenant subdirectories.
|
| 439 |
-
4. **Shell-out via `git`, `pytest`, `aider`, `npm`** with target-controlled
|
| 440 |
-
filenames. `run_subprocess_safe` exists, but several callers stitch
|
| 441 |
-
strings before reaching it. Audit every `subprocess.run` for
|
| 442 |
-
`shell=True` and for unvalidated repo paths.
|
| 443 |
-
5. **Embedding model pre-warm runs in a daemon thread without back-off.**
|
| 444 |
-
If `sentence-transformers` fails (rate-limited HF, no disk), every
|
| 445 |
-
subsequent retrieval call falls back silently to TF-IDF β a real
|
| 446 |
-
correctness regression that is invisible to operators. Surface this
|
| 447 |
-
state on the dashboard.
|
| 448 |
-
6. **Hermes phase state is in-process.** `HermesSession` lives in module
|
| 449 |
-
memory; if `app.py` restarts mid-research the entire session is lost.
|
| 450 |
-
Persist `HermesSession.asdict()` to `/data/hermes/<session_id>.json`
|
| 451 |
-
on every phase transition.
|
| 452 |
-
|
| 453 |
-
#### Medium
|
| 454 |
-
|
| 455 |
-
7. **`memory_engine.py` (TF-IDF) and `embedding_memory.py` (vec) compete.**
|
| 456 |
-
`app.py` imports both, and the audit loop calls v2 with v1 as silent
|
| 457 |
-
fallback. Two stores will drift. Pick one as the source of truth and
|
| 458 |
-
demote the other to "legacy".
|
| 459 |
-
8. **Webhook server uses stdlib HTTPServer (single-threaded by default).**
|
| 460 |
-
`start_webhook_server()` should use `ThreadingHTTPServer` (or, ideally,
|
| 461 |
-
move the same handlers to FastAPI now that uvicorn is a dep).
|
| 462 |
-
9. **`MAX_RETRIES * ADVERSARIAL_REJECTION_MULTIPLIER`** can produce long
|
| 463 |
-
tail loops where a stubborn adversary blocks the pipeline. Add an
|
| 464 |
-
absolute wall-clock cap per audit.
|
| 465 |
-
10. **JSON job ledger is read/written without `flock`.** Two parallel
|
| 466 |
-
workers updating the same job file race. Use SQLite (which is already
|
| 467 |
-
a dependency) for the job queue too β this also fixes (3) by giving
|
| 468 |
-
you per-tenant rows instead of files.
|
| 469 |
-
11. **`audit_logger` chain integrity is only verified on demand.** If the
|
| 470 |
-
chain has been tampered with, no one notices until someone clicks the
|
| 471 |
-
button. Add a periodic background verifier that pages on a break.
|
| 472 |
-
12. **Mythos optional deps are mostly commented out** β anyone who pip-
|
| 473 |
-
installs the file gets the FastAPI surface but not Pyro/MLflow/RLlib.
|
| 474 |
-
Document the on-demand install path more loudly in `MYTHOS_PLAN.md`.
|
| 475 |
-
|
| 476 |
-
#### Low / hygiene
|
| 477 |
-
|
| 478 |
-
13. `language_runtime.py` is ~70 KB with deep `if-lang ==` ladders. Replace
|
| 479 |
-
with a `Runtime` class registry plus `entry_points` so adding Go, Rust,
|
| 480 |
-
or .NET is a one-file change.
|
| 481 |
-
14. Many modules use `time.sleep()` for backoff instead of `tenacity` even
|
| 482 |
-
though `tenacity` is already imported in `app.py`.
|
| 483 |
-
15. `red_team_fuzzer.py` (62 KB) duplicates patterns that
|
| 484 |
-
`fuzzing_engine.py` already has β consolidate.
|
| 485 |
-
16. `notifier.py` swallows exceptions silently; add a structured failure
|
| 486 |
-
counter.
|
| 487 |
-
17. `pitch_deck/` and `pitch-deck/` (hyphen vs underscore) coexist as
|
| 488 |
-
sibling directories β pick one.
|
| 489 |
-
|
| 490 |
-
### 5.4 Memory / leak surfaces
|
| 491 |
-
|
| 492 |
-
- `_hermes_logs: list[str]` is unbounded β every Hermes run appends without
|
| 493 |
-
truncation. Add a ring buffer (`collections.deque(maxlen=10_000)`).
|
| 494 |
-
- The Gradio `gr.Timer(3)` keeps building string responses even when no
|
| 495 |
-
client is connected. The single-stream coalescing helps, but the
|
| 496 |
-
`live_logs` textbox still grows unboundedly.
|
| 497 |
-
- Aider subprocesses inherit the parent file descriptors; a long-running
|
| 498 |
-
audit can exhaust FDs. Add `close_fds=True` everywhere.
|
| 499 |
-
- The embedding store grows monotonically. Add a TTL eviction in
|
| 500 |
-
`embedding_memory`.
|
| 501 |
-
|
| 502 |
-
### 5.5 Targeted stabilisation roadmap
|
| 503 |
-
|
| 504 |
-
| Priority | Change | Effort | Payoff |
|
| 505 |
-
|---|---|---|---|
|
| 506 |
-
| π₯ P0 | Add a `tests/` directory with pytest covering: webhook signature, audit-loop happy path with mocked Aider, audit-chain integrity, MCP-config render | M | Catches future breakage of every gate |
|
| 507 |
-
| π₯ P0 | Per-tenant subdirectory under `/data/<tenant>/` and a path-allowlist helper | S | Hard isolation between tenants |
|
| 508 |
-
| π₯ P0 | Migrate `job_queue` from JSON files to the existing SQLite store | S | Removes file-locking races |
|
| 509 |
-
| β‘ P1 | Carve `app.py` into `ui_blocks.py` + `audit_loop.py` + `procctl.py` (no behaviour change) | M | Drops blast radius of every future change |
|
| 510 |
-
| β‘ P1 | Switch `webhook_server` to `ThreadingHTTPServer` *or* mount it under the new FastAPI app | S | Concurrency + one fewer port |
|
| 511 |
-
| β‘ P1 | Surface "embedding model healthy?" on the dashboard | XS | Stops silent fallback to TF-IDF |
|
| 512 |
-
| β‘ P1 | Persist `HermesSession` after every phase transition | S | Crash-safe research mode |
|
| 513 |
-
| π P2 | Replace `if-lang ==` ladders in `language_runtime` with a registry | M | Future polyglot support |
|
| 514 |
-
| π P2 | Bounded ring buffers for `_hermes_logs` + Gradio `live_logs` | XS | Stops slow memory growth |
|
| 515 |
-
| π P2 | Consolidate `memory_engine` and `embedding_memory` behind one interface | M | One source of retrieval truth |
|
| 516 |
-
| π‘ P2 | Explicit security pass on every `subprocess.run` for shell injection / path traversal in target-controlled inputs | M | Hardens the polyglot runtime |
|
| 517 |
-
| π‘ P3 | Background verifier thread for the audit hash-chain that pages on break | S | Compliance evidence stays trustworthy |
|
| 518 |
-
|
| 519 |
-
### 5.6 Bottom line
|
| 520 |
-
|
| 521 |
-
The architecture is **ambitious, coherent, and unusually mature for a
|
| 522 |
-
single-Space project** β the gating pipeline, the MCP tool bus, and the
|
| 523 |
-
Mythos multi-agent / RL extension are genuinely well-thought-out. The two
|
| 524 |
-
things holding it back from production-grade are (a) the absence of a
|
| 525 |
-
self-test suite and (b) the size of `app.py`. Both are mechanical, not
|
| 526 |
-
architectural, problems. Address P0 + P1 above and Rhodawk graduates from
|
| 527 |
-
"impressive HuggingFace Space" to "shippable security platform".
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,239 +0,0 @@
|
|
| 1 |
-
# Rhodawk AI DevSecOps β RHODAWK_SUPERHUMAN_MASTERPLAN Comparison Report
|
| 2 |
-
|
| 3 |
-
*Generated: April 22, 2026*
|
| 4 |
-
|
| 5 |
-
This document compares every line item in `RHODAWK_SUPERHUMAN_MASTERPLAN.md`
|
| 6 |
-
against the actual state of the repo after this delivery cycle. Entries
|
| 7 |
-
fall into one of four buckets:
|
| 8 |
-
|
| 9 |
-
- β
**DONE** β implemented in this commit.
|
| 10 |
-
- π’ **PRE-EXISTING** β already shipped before this cycle; verified.
|
| 11 |
-
- π‘ **PARTIAL** β scaffold present, deeper work still required.
|
| 12 |
-
- π΄ **NOT YET** β out of scope for this cycle, listed for the next one.
|
| 13 |
-
|
| 14 |
-
---
|
| 15 |
-
|
| 16 |
-
## PART 1 β Model & Skill Foundation
|
| 17 |
-
|
| 18 |
-
| Item | Status | Notes |
|
| 19 |
-
|---|---|---|
|
| 20 |
-
| MiniMax M2.5-highspeed β T1 fast | β
DONE | `architect/model_router.py` rewritten β `TIER1_PRIMARY = minimax/minimax-m2.5-highspeed`, env override `TIER1_PRIMARY_MODEL`. |
|
| 21 |
-
| DeepSeek V3 β T1 deep code lane | β
DONE | `TIER1_DEEP = deepseek/deepseek-chat-v3` mapped to `static_analysis`, `patch_generation`. |
|
| 22 |
-
| Qwen3-235B-A22B β T2 reasoning | β
DONE | `TIER2_PRIMARY` mapped to `exploit_reasoning`, `chain_synthesis`, `adversarial_review_c`. |
|
| 23 |
-
| Claude Sonnet 4.6 β T4 polish | β
DONE | `TIER4_PRIMARY` mapped to `critical_cve_draft`. |
|
| 24 |
-
| DeepSeek-R1-32B-AWQ local β T5 | β
DONE | `TIER5_LOCAL`, used for `bulk_triage`, budget-exceeded fallback. |
|
| 25 |
-
| `build_skill_system_prompt(profile)` | β
DONE | New helper in `model_router.py`. |
|
| 26 |
-
| `call_with_skills(task, prompt, profile)` | β
DONE | New helper that combines routing + skill injection + LLM call. |
|
| 27 |
-
| ACTS 3-model consensus updated | β
DONE | `adversarial_review_a/b/c` now route to three distinct providers. |
|
| 28 |
-
| Hard budget guardrail | π’ PRE-EXISTING | `_BUDGET` + `record_usage()` retained. |
|
| 29 |
-
|
| 30 |
-
### 8 new domain skills (`architect/skills/`)
|
| 31 |
-
|
| 32 |
-
| Skill file | Status |
|
| 33 |
-
|---|---|
|
| 34 |
-
| `smart-contract-audit.md` | β
DONE |
|
| 35 |
-
| `ai-ml-security.md` | β
DONE |
|
| 36 |
-
| `ci-cd-pipeline-attack.md` | β
DONE |
|
| 37 |
-
| `zero-day-research.md` | β
DONE |
|
| 38 |
-
| `llm-system-prompt-injection.md` | β
DONE |
|
| 39 |
-
| `linux-kernel-exploitation.md` | β
DONE |
|
| 40 |
-
| `browser-engine-security.md` | β
DONE |
|
| 41 |
-
| `cryptographic-implementation.md` | β
DONE |
|
| 42 |
-
|
| 43 |
-
### Imported community skills
|
| 44 |
-
|
| 45 |
-
| Skill | Source | Status |
|
| 46 |
-
|---|---|---|
|
| 47 |
-
| `bb-methodology-claude.md` | `shuvonsec/claude-bug-bounty` (MIT) | β
DONE |
|
| 48 |
-
| `bug-bounty-reference-index.md` | `ngalongc/bug-bounty-reference` (CC) | β
DONE |
|
| 49 |
-
|
| 50 |
-
Total skill count after this cycle: **29** markdown skill files
|
| 51 |
-
(was 19 pre-cycle).
|
| 52 |
-
|
| 53 |
-
---
|
| 54 |
-
|
| 55 |
-
## PART 2 β OSS-Guardian Lane
|
| 56 |
-
|
| 57 |
-
| Item | Status | Notes |
|
| 58 |
-
|---|---|---|
|
| 59 |
-
| `oss_target_scorer.py` | β
DONE | Stars Γ dependents Γ language-risk Γ CVE-history Γ freshness. Pure-function, unit-test ready. |
|
| 60 |
-
| `oss_guardian.py` runner | β
DONE | End-to-end: sandbox β runtime detect β fix-vs-attack split β Hermes attack β routing to disclosure_vault / GitHub PR / embodied bridge. CLI entrypoint `python -m oss_guardian --repo β¦`. |
|
| 61 |
-
| `repo_harvester.py` | π’ PRE-EXISTING | Untouched β feeds the scorer. |
|
| 62 |
-
| `language_runtime.py` integration | β
DONE | Called inside `OSSGuardian._safe_run_tests`. |
|
| 63 |
-
| Sandbox isolation | π’ PRE-EXISTING | `architect/sandbox.open_sandbox()` reused. |
|
| 64 |
-
| GitHub PR auto-fix path | π‘ PARTIAL | Hooked to existing `run_hermes_research` in fix-mode; full PR open + signing already in `app.py`. |
|
| 65 |
-
| Disclosure-vault routing | β
DONE | `_route_disclosure()` β novel zero-day β vault, known CVE β PR. |
|
| 66 |
-
|
| 67 |
-
---
|
| 68 |
-
|
| 69 |
-
## PART 3 β Knowledge & RAG
|
| 70 |
-
|
| 71 |
-
| Item | Status | Notes |
|
| 72 |
-
|---|---|---|
|
| 73 |
-
| `knowledge_rag.py` SQLite vector store | β
DONE | 256-dim deterministic hash-bag embedder; auto-uses `embedding_memory.embed` when present. `add`, `add_many`, `ingest_text_file`, `query`, `stats`. |
|
| 74 |
-
| Source allow-list | β
DONE | `SOURCES_DEFAULT` covers HackerOne, CVEDetails, ProjectZero, PortSwigger, arXiv cs.CR, awesome-bounty repos. |
|
| 75 |
-
| Cross-session memory | π’ PRE-EXISTING | `embedding_memory.py` retained. |
|
| 76 |
-
|
| 77 |
-
---
|
| 78 |
-
|
| 79 |
-
## PART 4 β Storage / Multi-tenant Hardening
|
| 80 |
-
|
| 81 |
-
| Item | Status | Notes |
|
| 82 |
-
|---|---|---|
|
| 83 |
-
| `job_queue.py` SQLite migration | β
DONE | New WAL-mode SQLite at `/data/jobs.sqlite`. Public API preserved. Legacy `/data/jobs/*.json` files imported once on first call (`*.imported` rename). |
|
| 84 |
-
| `_hermes_logs` deque(maxlen) | π’ PRE-EXISTING | Already `_collections.deque(maxlen=_HERMES_LOG_CAP)` in `hermes_orchestrator.py:54`. |
|
| 85 |
-
| `persist_hermes_session()` | π’ PRE-EXISTING | `hermes_orchestrator.py:80`. |
|
| 86 |
-
| Per-tenant data dirs | π‘ PARTIAL | `job_queue.upsert_job(tenant_id=β¦)` already namespaced; cross-cutting per-tenant disk paths still TODO. |
|
| 87 |
-
| Audit-log SHA-256 chain | π’ PRE-EXISTING | `audit_logger.py`. |
|
| 88 |
-
|
| 89 |
-
---
|
| 90 |
-
|
| 91 |
-
## PART 5 β Night Hunter Lane
|
| 92 |
-
|
| 93 |
-
| Item | Status | Notes |
|
| 94 |
-
|---|---|---|
|
| 95 |
-
| `mythos/mcp/scope_parser_mcp.py` | π’ PRE-EXISTING | Verified β H1 / Bugcrowd / Intigriti normalisers all in place. |
|
| 96 |
-
| `mythos/mcp/subdomain_enum_mcp.py` | π’ PRE-EXISTING | subfinder / amass / dnsx + crt.sh fallback. |
|
| 97 |
-
| `nightmode.py` phase callables | π’ PRE-EXISTING | `_phase_scope_ingest`, `_phase_recon`, `_phase_hunt`, `_phase_report` all wired. |
|
| 98 |
-
| Telegram morning briefing | π’ PRE-EXISTING | `embodied_bridge.emit_status()` invoked at start + end of `run_one_cycle`. |
|
| 99 |
-
| `start_in_background()` daemon | π’ PRE-EXISTING | Opt-in via `ARCHITECT_NIGHTMODE=1`. |
|
| 100 |
-
| 5 specialist hunt agents wiring | π‘ PARTIAL | Mythos orchestrator chained in via `_phase_hunt`; the named "auth / server-side / logic / infra / api" agent split is still represented as a single multi-iteration orchestrator run. |
|
| 101 |
-
|
| 102 |
-
### MCP fleet β `mcp_config.json`
|
| 103 |
-
|
| 104 |
-
| MCP | Before | After |
|
| 105 |
-
|---|---|---|
|
| 106 |
-
| `scope-parser-mcp` | missing | β
added |
|
| 107 |
-
| `subdomain-enum-mcp` | missing | β
added |
|
| 108 |
-
| `wayback-mcp` | missing | β
added |
|
| 109 |
-
| `httpx-probe-mcp` | missing | β
added |
|
| 110 |
-
| `shodan-mcp` | missing | β
added |
|
| 111 |
-
| 14 existing MCPs (fetch-docs, github-manager, filesystem, memory, sequential-thinking, web-search, dynamic / static / exploit / web-security / vuln-db / recon / browser-agent / frida / ghidra / can-bus / sdr) | π’ PRE-EXISTING | unchanged |
|
| 112 |
-
|
| 113 |
-
Total MCP servers in template: **23** (was 18).
|
| 114 |
-
|
| 115 |
-
---
|
| 116 |
-
|
| 117 |
-
## PART 6 β EmbodiedOS / OpenClaw Bridge
|
| 118 |
-
|
| 119 |
-
| Item | Status | Notes |
|
| 120 |
-
|---|---|---|
|
| 121 |
-
| `dispatch_to_openclaw(job_type, payload)` | β
DONE | New helper in `architect/embodied_bridge.py` β supports `fuzz_afl`, `klee_symbolic`, `lora_finetune`, `differential_fuzz`, `weight_scan`. |
|
| 122 |
-
| `receive_openclaw_result(payload)` | β
DONE | Webhook handler β persists to Hermes session, emits operator status. |
|
| 123 |
-
| Telegram / Discord / Hermes fan-out | π’ PRE-EXISTING | `emit_finding` retained. |
|
| 124 |
-
| OpenClaw-side job receiver | π΄ NOT YET | Lives in the OpenClaw repo, not this codebase. |
|
| 125 |
-
| AFL++ / KLEE dispatch from `mythos/dynamic/` | π΄ NOT YET | Bridge ready; call sites still need to flip from local subprocess to `dispatch_to_openclaw`. |
|
| 126 |
-
|
| 127 |
-
---
|
| 128 |
-
|
| 129 |
-
## PART 7 β Productisation & Ops
|
| 130 |
-
|
| 131 |
-
| Item | Status | Notes |
|
| 132 |
-
|---|---|---|
|
| 133 |
-
| FastAPI hardening (auth + rate limit) | π΄ NOT YET | Tracked for the productisation cycle. |
|
| 134 |
-
| Billing + API-key management | π΄ NOT YET | Out of scope for this delivery. |
|
| 135 |
-
| `app.py` split into `audit_loop.py` + `ui_blocks.py` | π΄ NOT YET | 2,621-line monolith retained; refactor planned. |
|
| 136 |
-
| Public leaderboard | π΄ NOT YET | Stub planned. |
|
| 137 |
-
| Test suite under `tests/` | π‘ PARTIAL | The new modules (`oss_target_scorer`, `knowledge_rag`, `job_queue`, `oss_guardian`) are written to be pure-function-testable; named test files still TODO. |
|
| 138 |
-
|
| 139 |
-
---
|
| 140 |
-
|
| 141 |
-
## PART 8 β Self-Improvement Loop
|
| 142 |
-
|
| 143 |
-
| Item | Status | Notes |
|
| 144 |
-
|---|---|---|
|
| 145 |
-
| `training_store.py` recording chains | π’ PRE-EXISTING | Untouched. |
|
| 146 |
-
| `lora_scheduler.py` triggering | π’ PRE-EXISTING | Untouched. |
|
| 147 |
-
| LoRA β OpenClaw dispatch | β
DONE | New `dispatch_to_openclaw("lora_finetune", β¦)` ready to be called from `lora_scheduler.py`. |
|
| 148 |
-
| Curriculum learning | π΄ NOT YET | `mythos/learning/curriculum.py` not built. |
|
| 149 |
-
| Auto-skill creation (3+ pattern) | π΄ NOT YET | Hook point identified in the masterplan; not wired in `hermes_orchestrator.maybe_create_skill`. |
|
| 150 |
-
|
| 151 |
-
---
|
| 152 |
-
|
| 153 |
-
## PART 9 β Security & Ethics (Non-Negotiable)
|
| 154 |
-
|
| 155 |
-
All hard constraints from Β§8 of the masterplan are **PRE-EXISTING** and
|
| 156 |
-
unchanged by this cycle:
|
| 157 |
-
|
| 158 |
-
- 90-day coordinated disclosure timer in `disclosure_vault.py`.
|
| 159 |
-
- `architect/sandbox.py` β local-clone-only execution.
|
| 160 |
-
- Operator approval gate before every P1/P2 submission.
|
| 161 |
-
- `audit_logger.py` SHA-256 chain on every action.
|
| 162 |
-
- `FETCH_ALLOWED_DOMAINS` SSRF allow-list across MCPs.
|
| 163 |
-
- No automated submission anywhere in the new modules β `oss_guardian`
|
| 164 |
-
routes findings to the disclosure vault, never submits directly.
|
| 165 |
-
|
| 166 |
-
---
|
| 167 |
-
|
| 168 |
-
## Summary
|
| 169 |
-
|
| 170 |
-
| Bucket | Count |
|
| 171 |
-
|---|---|
|
| 172 |
-
| β
DONE this cycle | 26 |
|
| 173 |
-
| π’ PRE-EXISTING & verified | 16 |
|
| 174 |
-
| π‘ PARTIAL | 5 |
|
| 175 |
-
| π΄ NOT YET (next cycle) | 9 |
|
| 176 |
-
|
| 177 |
-
### What you can do *right now* with this build
|
| 178 |
-
|
| 179 |
-
1. `python -m oss_guardian --repo https://github.com/redis/redis` β
|
| 180 |
-
end-to-end OSS pipeline against a real target.
|
| 181 |
-
2. Set `ARCHITECT_NIGHTMODE=1` and the four bounty-platform tokens; run
|
| 182 |
-
`python -c "from architect.nightmode import run_one_cycle; print(run_one_cycle())"`
|
| 183 |
-
for a single dry cycle.
|
| 184 |
-
3. Use `architect.model_router.call_with_skills("static_analysis", prompt,
|
| 185 |
-
{"languages":["c"], "asset_types":["kernel"]})` to get a kernel-aware
|
| 186 |
-
prompt automatically composed with the new skill packs.
|
| 187 |
-
4. `python -c "from knowledge_rag import KnowledgeRAG; r=KnowledgeRAG(); r.ingest_text_file('attached_assets/RHODAWK_SUPERHUMAN_MASTERPLAN_*.md', source='masterplan'); print(r.stats())"`.
|
| 188 |
-
|
| 189 |
-
### Next-cycle priorities (recommended)
|
| 190 |
-
|
| 191 |
-
1. Build the explicit five-agent split inside `_phase_hunt` (auth,
|
| 192 |
-
server-side, logic, infra, api).
|
| 193 |
-
2. Flip `mythos/dynamic/` AFL++ + KLEE call sites to use
|
| 194 |
-
`dispatch_to_openclaw`.
|
| 195 |
-
3. Land `tests/test_oss_guardian_dry.py`, `tests/test_model_router.py`,
|
| 196 |
-
`tests/test_scope_parser.py` and run them in CI.
|
| 197 |
-
4. Split `app.py` and harden the FastAPI surface.
|
| 198 |
-
|
| 199 |
-
---
|
| 200 |
-
|
| 201 |
-
## Round 2 β G0DM0D3 + OpenClaw-RL + Vibe-Coded Hit-List
|
| 202 |
-
|
| 203 |
-
| # | Item | Status | Where |
|
| 204 |
-
|---|---------------------------------------------------------------------------------------------------|--------|----------------------------------------------------------------|
|
| 205 |
-
| 43| **Master Red-Team operator persona** β single source of truth system prompt for every LLM call | DONE | `architect/master_redteam_prompt.py` |
|
| 206 |
-
| 44| **Vibe-Coded App Hunter skill** β the 24-hour 20-rule hit-list, pinned into every call | DONE | `architect/skills/vibe-coded-app-hunter.md` (always pinned) |
|
| 207 |
-
| 45| **`call_with_skills` upgrade** β auto master-prompt, auto pin, auto-RL-record, mode-aware | DONE | `architect/model_router.py` (`mode`, `pin_skills`, `record_rl`) |
|
| 208 |
-
| 46| **GODMODE Consensus race** β 5-combo parallel race + composite scorer (G0DM0D3 ULTRAPLINIAN port) | DONE | `architect/godmode_consensus.py` |
|
| 209 |
-
| 47| **Parseltongue input perturbation** β 7 techniques Γ 3 tiers Γ 33 default triggers | DONE | `architect/parseltongue.py` |
|
| 210 |
-
| 48| **OpenClaw-RL local rollout collector** β async 4-component loop, binary + composite reward | DONE | `architect/rl_feedback_loop.py` |
|
| 211 |
-
| 49| **OpenClaw fleet dispatch for LoRA training** β flushes batched traces via embodied bridge | DONE | `architect/rl_feedback_loop.flush()` β `embodied_bridge.dispatch_to_openclaw("lora_finetune", β¦)` |
|
| 212 |
-
| 50| **Operator language-feedback channel** β natural-language thumbs up/down on any trace | DONE | `architect/rl_feedback_loop.submit_language_feedback()` |
|
| 213 |
-
| 51| **Vibe-coded targeting baked into prompt** β operator notes + always-pinned skill + mode==hunt | DONE | `master_redteam_prompt.OPERATOR_DIRECTIVE` + `VIBE_CODED_HIT_LIST` |
|
| 214 |
-
| 52| **Aggressive / dry-run kill switches** via env (`RHODAWK_AGGRESSIVE`, `RHODAWK_DRY_RUN`) | DONE | `master_redteam_prompt._operator_notes()` |
|
| 215 |
-
|
| 216 |
-
### How to drive it
|
| 217 |
-
```python
|
| 218 |
-
from architect.model_router import call_with_skills
|
| 219 |
-
out = call_with_skills(
|
| 220 |
-
"vuln_research",
|
| 221 |
-
"Audit https://target.example/api for the 20-rule hit-list.",
|
| 222 |
-
{"languages": ["typescript"], "frameworks": ["nextjs"], "asset_types": ["http"]},
|
| 223 |
-
mode="hunt",
|
| 224 |
-
)
|
| 225 |
-
print(out["decision"].model, "β", out["response"])
|
| 226 |
-
```
|
| 227 |
-
|
| 228 |
-
```python
|
| 229 |
-
from architect.godmode_consensus import race
|
| 230 |
-
res = race("PoC for the IDOR you just found at /api/orders/<id>",
|
| 231 |
-
profile={"asset_types": ["http"]})
|
| 232 |
-
print(res.to_dict())
|
| 233 |
-
```
|
| 234 |
-
|
| 235 |
-
```python
|
| 236 |
-
from architect import rl_feedback_loop
|
| 237 |
-
print(rl_feedback_loop.stats())
|
| 238 |
-
rl_feedback_loop.flush() # ship to OpenClaw fleet for LoRA training
|
| 239 |
-
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,4 +1,30 @@
|
|
| 1 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
FROM python:3.12-slim AS base
|
| 3 |
|
| 4 |
ENV DEBIAN_FRONTEND=noninteractive \
|
|
@@ -6,73 +32,84 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|
| 6 |
UV_COMPILE_BYTECODE=1 \
|
| 7 |
UV_LINK_MODE=copy
|
| 8 |
|
| 9 |
-
RUN apt-get update && \
|
| 10 |
-
|
| 11 |
-
|
|
|
|
| 12 |
|
| 13 |
-
#
|
| 14 |
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
WORKDIR /build
|
| 17 |
COPY requirements.txt .
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
#
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
"packaging" "pathspec" "diskcache" "networkx" "scipy" \
|
| 34 |
-
"beautifulsoup4" "pypandoc" "flake8" "importlib_resources" \
|
| 35 |
-
"pyperclip" "pexpect" "json5" "psutil" "watchfiles" "socksio" \
|
| 36 |
-
"mixpanel" "posthog" "tree-sitter" "grep_ast" "oslex" \
|
| 37 |
-
"tokenizers" "google-generativeai" "openai" "diff-match-patch" \
|
| 38 |
-
"soundfile" "sounddevice"
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
# Stage 2: Runtime β inherits installed packages from base
|
| 42 |
FROM base AS runtime
|
| 43 |
-
|
| 44 |
-
|
| 45 |
|
| 46 |
ENV GRADIO_SERVER_NAME=0.0.0.0 \
|
| 47 |
GRADIO_SERVER_PORT=7860 \
|
| 48 |
HOME=/home/rhodawk \
|
| 49 |
PATH="/home/rhodawk/.local/bin:/usr/local/bin:$PATH" \
|
| 50 |
UV_PYTHON_PREFERENCE=system \
|
| 51 |
-
UV_PYTHON=/usr/local/bin/python3
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
RUN id -u 1000 >/dev/null 2>&1 && (userdel -r $(id -un 1000) || true) || true && \
|
| 61 |
useradd -m -u 1000 -s /bin/bash rhodawk
|
| 62 |
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
|
| 67 |
-
#
|
| 68 |
-
COPY --from=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
WORKDIR /app
|
| 71 |
USER rhodawk
|
| 72 |
|
| 73 |
-
#
|
| 74 |
COPY --chown=rhodawk:rhodawk . .
|
| 75 |
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
-
|
|
|
|
| 1 |
+
# syntax=docker/dockerfile:1.7
|
| 2 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 3 |
+
# Rhodawk AI DevSecOps Engine β vendored OpenClaude architecture
|
| 4 |
+
#
|
| 5 |
+
# Stages:
|
| 6 |
+
# 1. openclaude-builder β uses Bun to compile vendor/openclaude β dist/cli.mjs
|
| 7 |
+
# 2. base β python:3.12-slim with system tooling, uv, Node, Bun
|
| 8 |
+
# 3. runtime β final image (non-root rhodawk user, EXPOSE 7860)
|
| 9 |
+
#
|
| 10 |
+
# Aider, litellm, configargparse and friends have been completely removed.
|
| 11 |
+
# Code generation is now handled by the OpenClaude headless gRPC daemon
|
| 12 |
+
# launched from entrypoint.sh.
|
| 13 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 14 |
+
|
| 15 |
+
ARG BUN_VERSION=1.1.42
|
| 16 |
+
|
| 17 |
+
# βββ Stage 1: build the vendored OpenClaude bundle ββββββββββββββββββββββ
|
| 18 |
+
FROM oven/bun:${BUN_VERSION} AS openclaude-builder
|
| 19 |
+
WORKDIR /openclaude
|
| 20 |
+
COPY vendor/openclaude/package.json vendor/openclaude/bun.lock ./
|
| 21 |
+
RUN bun install --frozen-lockfile --no-progress
|
| 22 |
+
COPY vendor/openclaude/ ./
|
| 23 |
+
RUN bun run build && \
|
| 24 |
+
test -s dist/cli.mjs && \
|
| 25 |
+
echo "[builder] OpenClaude bundle: $(wc -c < dist/cli.mjs) bytes"
|
| 26 |
+
|
| 27 |
+
# βββ Stage 2: base python+node+bun runtime ββββββββββββββββββββββββββββββ
|
| 28 |
FROM python:3.12-slim AS base
|
| 29 |
|
| 30 |
ENV DEBIAN_FRONTEND=noninteractive \
|
|
|
|
| 32 |
UV_COMPILE_BYTECODE=1 \
|
| 33 |
UV_LINK_MODE=copy
|
| 34 |
|
| 35 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 36 |
+
git curl ca-certificates build-essential unzip xz-utils \
|
| 37 |
+
nodejs npm \
|
| 38 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 39 |
|
| 40 |
+
# uv (fast Python installer used by sandboxed test runs)
|
| 41 |
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
| 42 |
|
| 43 |
+
# Bun (needed at runtime to launch the daemon: `bun run dev:grpc`)
|
| 44 |
+
COPY --from=oven/bun:1.1.42 /usr/local/bin/bun /usr/local/bin/bun
|
| 45 |
+
COPY --from=oven/bun:1.1.42 /usr/local/bin/bunx /usr/local/bin/bunx
|
| 46 |
+
|
| 47 |
WORKDIR /build
|
| 48 |
COPY requirements.txt .
|
| 49 |
+
RUN pip install --no-cache-dir -r requirements.txt mcp-server-fetch \
|
| 50 |
+
grpcio==1.66.* grpcio-tools==1.66.* protobuf==5.*
|
| 51 |
+
|
| 52 |
+
# MCP servers used by the runtime β installed globally so `npx -y β¦` is
|
| 53 |
+
# instantaneous instead of resolving on every audit.
|
| 54 |
+
RUN npm install -g --quiet \
|
| 55 |
+
@modelcontextprotocol/server-github \
|
| 56 |
+
@modelcontextprotocol/server-filesystem \
|
| 57 |
+
@modelcontextprotocol/server-memory \
|
| 58 |
+
@modelcontextprotocol/server-sequential-thinking \
|
| 59 |
+
@modelcontextprotocol/server-git \
|
| 60 |
+
@modelcontextprotocol/server-sqlite \
|
| 61 |
+
@modelcontextprotocol/server-brave-search
|
| 62 |
+
|
| 63 |
+
# βββ Stage 3: final runtime image βββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
FROM base AS runtime
|
| 65 |
+
LABEL org.opencontainers.image.title="Rhodawk AI DevSecOps Engine" \
|
| 66 |
+
org.opencontainers.image.source="https://github.com/Rhodawk-AI/Rhodawk-devops-engine"
|
| 67 |
|
| 68 |
ENV GRADIO_SERVER_NAME=0.0.0.0 \
|
| 69 |
GRADIO_SERVER_PORT=7860 \
|
| 70 |
HOME=/home/rhodawk \
|
| 71 |
PATH="/home/rhodawk/.local/bin:/usr/local/bin:$PATH" \
|
| 72 |
UV_PYTHON_PREFERENCE=system \
|
| 73 |
+
UV_PYTHON=/usr/local/bin/python3 \
|
| 74 |
+
OPENCLAUDE_AUTO_APPROVE=1 \
|
| 75 |
+
OPENCLAUDE_GRPC_HOST=127.0.0.1 \
|
| 76 |
+
OPENCLAUDE_GRPC_PORT_DO=50051 \
|
| 77 |
+
OPENCLAUDE_GRPC_PORT_OR=50052 \
|
| 78 |
+
MCP_RUNTIME_CONFIG=/tmp/mcp_runtime.json
|
| 79 |
+
|
| 80 |
+
# HuggingFace UID 1000 handling (idempotent)
|
| 81 |
+
RUN id -u 1000 >/dev/null 2>&1 && (userdel -r "$(id -un 1000)" || true) || true && \
|
|
|
|
| 82 |
useradd -m -u 1000 -s /bin/bash rhodawk
|
| 83 |
|
| 84 |
+
RUN mkdir -p /data /app /opt/openclaude && \
|
| 85 |
+
chmod 777 /data && \
|
| 86 |
+
chown -R rhodawk:rhodawk /app /opt/openclaude
|
| 87 |
|
| 88 |
+
# Bring the prebuilt OpenClaude bundle in as a vendored artifact.
|
| 89 |
+
COPY --from=openclaude-builder --chown=rhodawk:rhodawk /openclaude /opt/openclaude
|
| 90 |
+
|
| 91 |
+
# Tiny global wrappers β the orchestrator never shells out to these
|
| 92 |
+
# directly any more (gRPC bridges everything), but we keep them so admins
|
| 93 |
+
# can debug interactively from `docker exec`.
|
| 94 |
+
RUN ln -sf /opt/openclaude/bin/openclaude /usr/local/bin/openclaude && \
|
| 95 |
+
chmod +x /usr/local/bin/openclaude
|
| 96 |
|
| 97 |
WORKDIR /app
|
| 98 |
USER rhodawk
|
| 99 |
|
| 100 |
+
# Source last so application edits don't bust the heavy node/python layers.
|
| 101 |
COPY --chown=rhodawk:rhodawk . .
|
| 102 |
|
| 103 |
+
# Generate Python protobuf stubs from the vendored .proto file.
|
| 104 |
+
RUN python -m grpc_tools.protoc \
|
| 105 |
+
-I /opt/openclaude/src/proto \
|
| 106 |
+
--python_out=openclaude_grpc \
|
| 107 |
+
--grpc_python_out=openclaude_grpc \
|
| 108 |
+
/opt/openclaude/src/proto/openclaude.proto && \
|
| 109 |
+
# protoc emits absolute-path imports; rewrite for relative package layout
|
| 110 |
+
sed -i 's/^import openclaude_pb2/from . import openclaude_pb2/' \
|
| 111 |
+
openclaude_grpc/openclaude_pb2_grpc.py
|
| 112 |
+
|
| 113 |
+
EXPOSE 7860 50051 50052
|
| 114 |
|
| 115 |
+
ENTRYPOINT ["/app/entrypoint.sh"]
|
|
@@ -71,17 +71,18 @@ and build behaviour (how the container is assembled).
|
|
| 71 |
| 4 | `ad06d84` | Added `gitpython==3.1.46` to `requirements.txt`. | aider 0.86.2 hard-pins gitpython; pip's resolver was bouncing between our floor (`>=3.1.40`) and aider's pin. Pinning explicitly removes the resolver thrash. |
|
| 72 |
| 5 | `a913714` | Switched the Space SDK from `gradio` to `docker` in `README.md` front-matter and bumped `gradio>=5.49.0,<6` in `requirements.txt`. | HF was auto-injecting `gradio[oauth,mcp]==5.29.0` because the Space was registered as `sdk: gradio`. That injection conflicted with aider's `pillow==12.1.1` pin. `sdk: docker` tells HF to use the existing Dockerfile verbatim, with no auto-injection. |
|
| 73 |
| 6 | `dd4ccce` | Removed `aider-chat` from `requirements.txt` entirely. The Dockerfile now installs it with `--no-deps` plus a curated runtime-deps list. | Even with `sdk: docker`, aider's `pillow==12.1.1` pin still conflicted with gradio's `pillow<12` constraint at the resolver level. `--no-deps` lets gradio's pillow 11 win, and the curated dep list provides everything aider actually imports at runtime. |
|
|
|
|
| 74 |
|
| 75 |
**Net effect today.** The Space SDK is `docker`. The build runs the
|
| 76 |
-
Dockerfile end-to-end
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
|
| 86 |
**Provider behaviour today.** When `DO_INFERENCE_API_KEY` is present the hot
|
| 87 |
path uses DigitalOcean Serverless Inference at
|
|
@@ -94,41 +95,43 @@ When DO is absent the system silently runs OpenRouter-only.
|
|
| 94 |
## 2. Provider Routing β DigitalOcean Primary, OpenRouter Fallback
|
| 95 |
<a id="2-provider-routing"></a>
|
| 96 |
|
| 97 |
-
Two independent code paths need an LLM: **
|
| 98 |
-
**Hermes** (the multi-phase research
|
| 99 |
-
|
|
|
|
| 100 |
|
| 101 |
-
### 2.1
|
| 102 |
|
| 103 |
```
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
β
|
| 110 |
-
βββΊ
|
|
|
|
| 111 |
β
|
| 112 |
-
βββΊ
|
| 113 |
-
for each model
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
if all skipped β "No inference provider configured" message
|
| 125 |
-
if all failed β return last error with provider chain printed
|
| 126 |
```
|
| 127 |
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
|
|
|
|
|
|
| 132 |
|
| 133 |
### 2.2 Hermes path β `hermes_orchestrator.py::_hermes_llm_call`
|
| 134 |
|
|
|
|
| 71 |
| 4 | `ad06d84` | Added `gitpython==3.1.46` to `requirements.txt`. | aider 0.86.2 hard-pins gitpython; pip's resolver was bouncing between our floor (`>=3.1.40`) and aider's pin. Pinning explicitly removes the resolver thrash. |
|
| 72 |
| 5 | `a913714` | Switched the Space SDK from `gradio` to `docker` in `README.md` front-matter and bumped `gradio>=5.49.0,<6` in `requirements.txt`. | HF was auto-injecting `gradio[oauth,mcp]==5.29.0` because the Space was registered as `sdk: gradio`. That injection conflicted with aider's `pillow==12.1.1` pin. `sdk: docker` tells HF to use the existing Dockerfile verbatim, with no auto-injection. |
|
| 73 |
| 6 | `dd4ccce` | Removed `aider-chat` from `requirements.txt` entirely. The Dockerfile now installs it with `--no-deps` plus a curated runtime-deps list. | Even with `sdk: docker`, aider's `pillow==12.1.1` pin still conflicted with gradio's `pillow<12` constraint at the resolver level. `--no-deps` lets gradio's pillow 11 win, and the curated dep list provides everything aider actually imports at runtime. |
|
| 74 |
+
| 7 | *current* | **Aider eliminated entirely. Vendored OpenClaude headless gRPC daemon now drives every code-generation turn.** Two daemons (`:50051` DigitalOcean primary, `:50052` OpenRouter fallback) launched from `entrypoint.sh`. New Python bridge `openclaude_grpc/` exposes `run_openclaude(...)` with the legacy `(combined_output, exit_code)` return contract. `run_aider` is now a thin alias. | The `--no-deps + litellm 1.78.5 + curated 27-package list` workaround was a fragile patchwork; any aider/gradio/pillow upstream bump could re-break the build. OpenClaude is a single Bun-built bundle with no Python dependency surface and a stable gRPC contract, so the Dockerfile loses ~30 lines of dependency hacks. |
|
| 75 |
|
| 76 |
**Net effect today.** The Space SDK is `docker`. The build runs the
|
| 77 |
+
Dockerfile end-to-end in three stages: (1) Bun compiles
|
| 78 |
+
`vendor/openclaude/` to `dist/cli.mjs`; (2) Python 3.12-slim with system
|
| 79 |
+
tooling, uv, Node and Bun is provisioned; (3) the runtime image generates
|
| 80 |
+
Python protobuf stubs from `vendor/openclaude/src/proto/openclaude.proto`
|
| 81 |
+
into `openclaude_grpc/`. `requirements.txt` no longer mentions
|
| 82 |
+
`aider-chat`, `litellm`, `configargparse`, or any of the other 27 curated
|
| 83 |
+
aider runtime imports β they have all been deleted. `entrypoint.sh`
|
| 84 |
+
boots the OpenClaude DO daemon on `:50051` and (when an OpenRouter key is
|
| 85 |
+
present) the OpenClaude OR daemon on `:50052`, then `exec`s `app.py`.
|
| 86 |
|
| 87 |
**Provider behaviour today.** When `DO_INFERENCE_API_KEY` is present the hot
|
| 88 |
path uses DigitalOcean Serverless Inference at
|
|
|
|
| 95 |
## 2. Provider Routing β DigitalOcean Primary, OpenRouter Fallback
|
| 96 |
<a id="2-provider-routing"></a>
|
| 97 |
|
| 98 |
+
Two independent code paths need an LLM: **OpenClaude** (the patch
|
| 99 |
+
generator, replaces aider) and **Hermes** (the multi-phase research
|
| 100 |
+
orchestrator + adversarial reviewer). Both are wired to the same
|
| 101 |
+
provider chain β DigitalOcean Inference primary, OpenRouter fallback.
|
| 102 |
|
| 103 |
+
### 2.1 OpenClaude path β `app.py::run_openclaude` (alias `run_aider`)
|
| 104 |
|
| 105 |
```
|
| 106 |
+
entrypoint.sh boots two OpenClaude headless gRPC daemons:
|
| 107 |
+
ββ DO_INFERENCE_API_KEY β :50051 (PRIMARY, llama3.3-70b-instruct)
|
| 108 |
+
ββ OPENROUTER_API_KEY β :50052 (FALLBACK, qwen-2.5-coder-32b)
|
| 109 |
+
|
| 110 |
+
run_openclaude(mcp_config_path, prompt, context_files):
|
| 111 |
β
|
| 112 |
+
βββΊ writes /tmp/mcp_runtime.json (the daemon hot-reloads it per chat)
|
| 113 |
+
βββΊ forwards prompt + valid context-file list to the bridge
|
| 114 |
β
|
| 115 |
+
βββΊ openclaude_grpc.run_openclaude builds the chain [primary, fallback]:
|
| 116 |
+
for each (port, label, model) in chain:
|
| 117 |
+
client = OpenClaudeClient(host=127.0.0.1, port=port)
|
| 118 |
+
if not client.wait_ready(15s): record + continue
|
| 119 |
+
result = client.chat(message, working_directory=REPO_DIR, model=model)
|
| 120 |
+
βββ streams text_chunk β result.stdout
|
| 121 |
+
βββ streams tool_start β result.tool_calls + stdout marker
|
| 122 |
+
βββ streams tool_result β result.stdout / .stderr
|
| 123 |
+
βββ auto-replies "y" to any action_required prompt
|
| 124 |
+
βββ on done event β exit_code 0
|
| 125 |
+
if exit_code == 0: return (combined_output, 0)
|
| 126 |
+
return (last_output, last_code)
|
|
|
|
|
|
|
| 127 |
```
|
| 128 |
|
| 129 |
+
There is no litellm, no `--openai-api-base` shell argument, no per-model
|
| 130 |
+
prefix munging. Each daemon is launched with `OPENAI_API_KEY`,
|
| 131 |
+
`OPENAI_BASE_URL`, `OPENAI_MODEL` set in its own process environment, so
|
| 132 |
+
the gRPC client just speaks to whichever port matches the desired
|
| 133 |
+
provider. The legacy `run_aider` symbol is preserved as an alias for
|
| 134 |
+
backwards compatibility with the Hermes/SAST/red-team callers.
|
| 135 |
|
| 136 |
### 2.2 Hermes path β `hermes_orchestrator.py::_hermes_llm_call`
|
| 137 |
|
|
@@ -441,12 +441,10 @@ def write_mcp_config() -> str:
|
|
| 441 |
"args": ["mcp-server-shell", "--allow-commands", "ruff"],
|
| 442 |
"description": "Ruff ultra-fast Python linter β anti-patterns correlating with security bugs"
|
| 443 |
},
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
"env": {"OPENROUTER_API_KEY": _openrouter_key}
|
| 449 |
-
},
|
| 450 |
"cve-intelligence": {
|
| 451 |
"command": "uvx",
|
| 452 |
"args": ["mcp-server-fetch"],
|
|
@@ -616,85 +614,76 @@ def setup_target_venv() -> str:
|
|
| 616 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 617 |
# AIDER RUNNER
|
| 618 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
|
|
|
| 647 |
|
| 648 |
|
| 649 |
def run_aider(mcp_config_path: str, prompt: str, context_files: list[str]) -> tuple[str, int]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 650 |
"""
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
continue
|
| 676 |
-
chain.append(m)
|
| 677 |
-
if not chain:
|
| 678 |
-
return ("No inference provider configured: set DO_INFERENCE_API_KEY "
|
| 679 |
-
"or OPENROUTER_API_KEY", 1)
|
| 680 |
-
|
| 681 |
-
last_output, last_code = "", 1
|
| 682 |
-
for idx, model in enumerate(chain):
|
| 683 |
-
provider = "DigitalOcean" if model.startswith("openai/") else "OpenRouter"
|
| 684 |
-
ui_log(f"Aider attempt via {provider} ({model})", "INFO")
|
| 685 |
-
output, code = _run_aider_once(model, prompt_path, valid)
|
| 686 |
-
last_output, last_code = output, code
|
| 687 |
-
if code == 0:
|
| 688 |
-
return output, code
|
| 689 |
-
if idx < len(chain) - 1:
|
| 690 |
-
ui_log(f"{provider} failed (exit {code}) β falling back to "
|
| 691 |
-
f"{chain[idx + 1]}", "WARN")
|
| 692 |
-
return last_output, last_code
|
| 693 |
-
finally:
|
| 694 |
-
try:
|
| 695 |
-
os.unlink(prompt_path)
|
| 696 |
-
except OSError:
|
| 697 |
-
pass
|
| 698 |
|
| 699 |
|
| 700 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 441 |
"args": ["mcp-server-shell", "--allow-commands", "ruff"],
|
| 442 |
"description": "Ruff ultra-fast Python linter β anti-patterns correlating with security bugs"
|
| 443 |
},
|
| 444 |
+
# aider-patcher removed β code generation now flows through the
|
| 445 |
+
# vendored OpenClaude headless gRPC daemon (see openclaude_grpc/).
|
| 446 |
+
# The model itself talks to the rest of the MCP suite directly,
|
| 447 |
+
# so no recursive "patcher" MCP server is needed.
|
|
|
|
|
|
|
| 448 |
"cve-intelligence": {
|
| 449 |
"command": "uvx",
|
| 450 |
"args": ["mcp-server-fetch"],
|
|
|
|
| 614 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 615 |
# AIDER RUNNER
|
| 616 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 617 |
+
###############################################################################
|
| 618 |
+
# OpenClaude gRPC bridge β replaces the legacy aider subprocess shell-out.
|
| 619 |
+
#
|
| 620 |
+
# Two daemons run side-by-side inside the container (see entrypoint.sh):
|
| 621 |
+
# :50051 β DigitalOcean Inference (PRIMARY)
|
| 622 |
+
# :50052 β OpenRouter (FALLBACK)
|
| 623 |
+
# `run_openclaude` walks the chain in order, returning the first 0-exit
|
| 624 |
+
# response, or the last failure if both providers reject the prompt.
|
| 625 |
+
#
|
| 626 |
+
# The signature mirrors the old `run_aider` exactly so every caller in this
|
| 627 |
+
# module β the 15-step healing loop, conviction engine, adversarial review
|
| 628 |
+
# wrappers β keeps working without modification.
|
| 629 |
+
###############################################################################
|
| 630 |
+
from openclaude_grpc import run_openclaude as _run_openclaude_bridge
|
| 631 |
+
|
| 632 |
+
# Provider chain configuration (resolved once at import time).
|
| 633 |
+
_OPENCLAUDE_PRIMARY_PORT = (
|
| 634 |
+
int(os.getenv("OPENCLAUDE_GRPC_PORT_DO", "50051"))
|
| 635 |
+
if DO_INFERENCE_API_KEY else 0
|
| 636 |
+
)
|
| 637 |
+
_OPENCLAUDE_FALLBACK_PORT = (
|
| 638 |
+
int(os.getenv("OPENCLAUDE_GRPC_PORT_OR", "50052"))
|
| 639 |
+
if OPENROUTER_API_KEY else 0
|
| 640 |
+
)
|
| 641 |
+
_OPENCLAUDE_PRIMARY_MODEL = DO_INFERENCE_MODEL if DO_INFERENCE_API_KEY else ""
|
| 642 |
+
_OPENCLAUDE_FALLBACK_MODEL = os.getenv(
|
| 643 |
+
"OPENROUTER_MODEL",
|
| 644 |
+
"qwen/qwen-2.5-coder-32b-instruct:free",
|
| 645 |
+
)
|
| 646 |
|
| 647 |
|
| 648 |
def run_aider(mcp_config_path: str, prompt: str, context_files: list[str]) -> tuple[str, int]:
|
| 649 |
+
"""Backwards-compatible alias kept so existing call sites and tests do
|
| 650 |
+
not break. Internally delegates to the OpenClaude gRPC bridge."""
|
| 651 |
+
return run_openclaude(mcp_config_path, prompt, context_files)
|
| 652 |
+
|
| 653 |
+
|
| 654 |
+
def run_openclaude(mcp_config_path: str, prompt: str,
|
| 655 |
+
context_files: list[str]) -> tuple[str, int]:
|
| 656 |
+
"""Issue one healing turn against the OpenClaude gRPC daemons.
|
| 657 |
+
|
| 658 |
+
Returns the same ``(combined_output, exit_code)`` tuple shape that
|
| 659 |
+
aider used to return so the rest of the orchestrator (validation
|
| 660 |
+
loop, SAST gate, conviction engine, red-team checks) plugs in
|
| 661 |
+
unchanged.
|
| 662 |
"""
|
| 663 |
+
if _OPENCLAUDE_PRIMARY_PORT == 0 and _OPENCLAUDE_FALLBACK_PORT == 0:
|
| 664 |
+
return (
|
| 665 |
+
"No inference provider configured: set DO_INFERENCE_API_KEY "
|
| 666 |
+
"or OPENROUTER_API_KEY",
|
| 667 |
+
1,
|
| 668 |
+
)
|
| 669 |
+
|
| 670 |
+
valid = [f for f in context_files
|
| 671 |
+
if os.path.exists(os.path.join(REPO_DIR, f))]
|
| 672 |
+
|
| 673 |
+
return _run_openclaude_bridge(
|
| 674 |
+
mcp_config_path,
|
| 675 |
+
prompt,
|
| 676 |
+
valid,
|
| 677 |
+
repo_dir=REPO_DIR,
|
| 678 |
+
primary_port=_OPENCLAUDE_PRIMARY_PORT,
|
| 679 |
+
fallback_port=_OPENCLAUDE_FALLBACK_PORT,
|
| 680 |
+
primary_label="DigitalOcean",
|
| 681 |
+
fallback_label="OpenRouter",
|
| 682 |
+
primary_model=_OPENCLAUDE_PRIMARY_MODEL,
|
| 683 |
+
fallback_model=_OPENCLAUDE_FALLBACK_MODEL,
|
| 684 |
+
timeout=int(os.getenv("OPENCLAUDE_TIMEOUT", "600")),
|
| 685 |
+
log_fn=ui_log,
|
| 686 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 687 |
|
| 688 |
|
| 689 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 3 |
+
# Rhodawk runtime bootstrap.
|
| 4 |
+
#
|
| 5 |
+
# 1. Launch the OpenClaude headless gRPC daemon for the DigitalOcean
|
| 6 |
+
# Inference provider on :50051 (PRIMARY).
|
| 7 |
+
# 2. Launch the OpenClaude headless gRPC daemon for OpenRouter on
|
| 8 |
+
# :50052 (FALLBACK) β only if OPENROUTER_API_KEY is present.
|
| 9 |
+
# 3. Wait briefly for both to bind, then hand control to app.py which
|
| 10 |
+
# talks to them over gRPC.
|
| 11 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 12 |
+
set -eo pipefail
|
| 13 |
+
|
| 14 |
+
OC_DIR=/opt/openclaude
|
| 15 |
+
LOG_DIR="${LOG_DIR:-/tmp}"
|
| 16 |
+
mkdir -p "${LOG_DIR}"
|
| 17 |
+
|
| 18 |
+
start_daemon() {
|
| 19 |
+
local label=$1 port=$2 base_url=$3 api_key=$4 model=$5
|
| 20 |
+
if [[ -z "${api_key}" ]]; then
|
| 21 |
+
echo "[entrypoint] skipping ${label} daemon β no API key"
|
| 22 |
+
return 0
|
| 23 |
+
fi
|
| 24 |
+
echo "[entrypoint] starting OpenClaude ${label} daemon on :${port}"
|
| 25 |
+
(
|
| 26 |
+
cd "${OC_DIR}"
|
| 27 |
+
CLAUDE_CODE_USE_OPENAI=1 \
|
| 28 |
+
OPENAI_API_KEY="${api_key}" \
|
| 29 |
+
OPENAI_BASE_URL="${base_url}" \
|
| 30 |
+
OPENAI_MODEL="${model}" \
|
| 31 |
+
GRPC_PORT="${port}" \
|
| 32 |
+
GRPC_HOST=0.0.0.0 \
|
| 33 |
+
OPENCLAUDE_AUTO_APPROVE=1 \
|
| 34 |
+
MCP_RUNTIME_CONFIG="${MCP_RUNTIME_CONFIG:-/tmp/mcp_runtime.json}" \
|
| 35 |
+
bun run scripts/start-grpc.ts \
|
| 36 |
+
> "${LOG_DIR}/openclaude-${label}.log" 2>&1 &
|
| 37 |
+
echo $! > "${LOG_DIR}/openclaude-${label}.pid"
|
| 38 |
+
)
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
# DigitalOcean Inference (PRIMARY)
|
| 42 |
+
DO_BASE="${DO_INFERENCE_BASE_URL:-https://inference.do-ai.run/v1}"
|
| 43 |
+
DO_MODEL="${DO_INFERENCE_MODEL:-llama3.3-70b-instruct}"
|
| 44 |
+
start_daemon "do" 50051 "${DO_BASE}" \
|
| 45 |
+
"${DO_INFERENCE_API_KEY:-${DIGITALOCEAN_INFERENCE_KEY:-}}" \
|
| 46 |
+
"${DO_MODEL}"
|
| 47 |
+
|
| 48 |
+
# OpenRouter (FALLBACK)
|
| 49 |
+
OR_BASE="${OPENROUTER_BASE_URL:-https://openrouter.ai/api/v1}"
|
| 50 |
+
OR_MODEL="${OPENROUTER_MODEL:-qwen/qwen-2.5-coder-32b-instruct:free}"
|
| 51 |
+
start_daemon "or" 50052 "${OR_BASE}" "${OPENROUTER_API_KEY:-}" "${OR_MODEL}"
|
| 52 |
+
|
| 53 |
+
# Brief settle window so the first healing call doesn't race the binder.
|
| 54 |
+
# The Python client also has wait_ready() so this is just a friendly nudge.
|
| 55 |
+
sleep 2
|
| 56 |
+
echo "[entrypoint] launching Rhodawk orchestratorβ¦"
|
| 57 |
+
exec python -u app.py
|
|
@@ -1,249 +0,0 @@
|
|
| 1 |
-
# Rhodawk: Ascending to Mythos-Level
|
| 2 |
-
|
| 3 |
-
> **An Open-Source Blueprint for Superhuman AI Security**
|
| 4 |
-
> Living document β every section in this plan maps to one or more concrete
|
| 5 |
-
> modules under `mythos/`. This file is the canonical source-of-truth that
|
| 6 |
-
> mirrors `attached_assets/rhodawk_mythos_level_plan_*.pdf` and tracks the
|
| 7 |
-
> implementation status of every gap closure.
|
| 8 |
-
|
| 9 |
-
---
|
| 10 |
-
|
| 11 |
-
## Executive Summary
|
| 12 |
-
|
| 13 |
-
This document outlines a strategic and technical blueprint for transforming
|
| 14 |
-
the existing Rhodawk AI DevSecOps Engine into a Claude Mythos-level
|
| 15 |
-
Superhuman Agent. Leveraging the robust foundation of EmbodiedOS
|
| 16 |
-
(integrating OpenClaw and Hermes Agent), this plan details the necessary
|
| 17 |
-
architectural enhancements, open-source component integrations, and
|
| 18 |
-
strategic shifts required to achieve autonomous, frontier-level vulnerability
|
| 19 |
-
discovery and exploitation. The goal is to create a self-improving,
|
| 20 |
-
multi-agent system capable of operating with the depth of reasoning,
|
| 21 |
-
precision of execution, and iterative learning observed in Anthropic's
|
| 22 |
-
unreleased Claude Mythos project, all while adhering to a cost-effective,
|
| 23 |
-
open-source model strategy.
|
| 24 |
-
|
| 25 |
-
---
|
| 26 |
-
|
| 27 |
-
## 1. Understanding Claude Mythos: A Frontier-Level AI Security Agent
|
| 28 |
-
|
| 29 |
-
### 1.1 What Claude Mythos Is
|
| 30 |
-
|
| 31 |
-
Claude Mythos is a sophisticated, integrated AI agent designed to operate as
|
| 32 |
-
an **autonomous vulnerability research pipeline**. It moves beyond traditional
|
| 33 |
-
static analysis or human-driven penetration testing by combining advanced AI
|
| 34 |
-
reasoning with dynamic execution and iterative learning.
|
| 35 |
-
|
| 36 |
-
### 1.2 Frontier-Level Capabilities
|
| 37 |
-
|
| 38 |
-
| Capability | Description |
|
| 39 |
-
|---|---|
|
| 40 |
-
| Autonomous Vulnerability Research | Discovers novel zero-day vulnerabilities and generates working exploits with no prior knowledge. |
|
| 41 |
-
| Elite Cybersecurity Expertise | Deep understanding of memory-safety, complex logic flaws, and subtle input-handling bugs. |
|
| 42 |
-
| Sophisticated Exploit Synthesis | ROP chains, heap sprays, privilege-escalation chains, full PoC code. |
|
| 43 |
-
| Self-Improving Discovery | Closed-loop hypothesis β execute β learn β refine cycle. |
|
| 44 |
-
|
| 45 |
-
### 1.3 Architecture and Working Mechanism
|
| 46 |
-
|
| 47 |
-
1. **Static + Semantic Code Analysis** β AST/CFG/CPG parsing.
|
| 48 |
-
2. **Hypothesis Generation Engine** β probabilistic reasoning over attack vectors.
|
| 49 |
-
3. **Dynamic Execution & Instrumentation** β sandboxed fuzzing + symbolic exec.
|
| 50 |
-
4. **Exploit Synthesis Engine** β primitive identification + PoC code.
|
| 51 |
-
5. **Autonomous Iteration Loop** β CEGIS-style continuous refinement.
|
| 52 |
-
|
| 53 |
-
### 1.4 What Makes It Special
|
| 54 |
-
|
| 55 |
-
- Unprecedented reasoning depth (beyond Claude 3.5 Opus class).
|
| 56 |
-
- Agentic integration (plan β execute β observe β learn).
|
| 57 |
-
- Stub-and-overlay architecture for cybersecurity specialization.
|
| 58 |
-
- Dedicated Project Glasswing focus.
|
| 59 |
-
|
| 60 |
-
---
|
| 61 |
-
|
| 62 |
-
## 2. Rhodawk and EmbodiedOS: The Current Foundation
|
| 63 |
-
|
| 64 |
-
### 2.1 EmbodiedOS β The Unified Runtime
|
| 65 |
-
|
| 66 |
-
Persistent stateful Linux workspace, multi-tier memory (short / Skill /
|
| 67 |
-
Knowledge), tool-calling autonomy, CEGIS loop. Hosts both **OpenClaw**
|
| 68 |
-
(local gateway, 50+ integrations, browser/file/script access) and **Hermes
|
| 69 |
-
Agent** (FTS5 SQLite memory, autonomous skill creation, Atropos self-training,
|
| 70 |
-
MCP server mode, Tirith pre-execution scanner).
|
| 71 |
-
|
| 72 |
-
### 2.2 Rhodawk β The Superhuman Agent Framework
|
| 73 |
-
|
| 74 |
-
- **Hermes Orchestrator** β six-phase pipeline (RECON β STATIC β DYNAMIC β EXPLOIT β CONSENSUS β DISCLOSURE).
|
| 75 |
-
- **Red Team CEGIS Engine** β zero-day discovery + Blue Team handoff.
|
| 76 |
-
- **Data Flywheel** β Training Store, Embedding Memory (MiniLM/CodeBERT), LoRA Scheduler.
|
| 77 |
-
- **Bounty Gateway** β HackerOne / Bugcrowd submission.
|
| 78 |
-
- **Tiered models** β Tier 1: DeepSeek 3.2 / MiniMax 2.5 Β· Tier 2: Qwen 2.5 Coder 32B Β· Tier 3: Llama 3.3 70B + DeepSeek V3 + Gemma 2 27B.
|
| 79 |
-
|
| 80 |
-
---
|
| 81 |
-
|
| 82 |
-
## 3. Gap Analysis: Rhodawk vs. Claude Mythos
|
| 83 |
-
|
| 84 |
-
| Capability Area | Claude Mythos (Frontier) | Rhodawk (Current) | Gap β Closure Module |
|
| 85 |
-
|---|---|---|---|
|
| 86 |
-
| Reasoning & Planning | Probabilistic, multi-step, attack-graph-aware | Deterministic 6-phase pipeline | `mythos/reasoning/probabilistic.py`, `mythos/reasoning/attack_graph.py`, `mythos/agents/planner.py` |
|
| 87 |
-
| Static Analysis | Deep semantic CPG queries | Pattern-based taint + CWE | `mythos/static/treesitter_cpg.py`, `mythos/static/joern_bridge.py`, `mythos/static/codeql_bridge.py`, `mythos/static/semgrep_bridge.py` |
|
| 88 |
-
| Dynamic Execution | Concolic + full-system + fine-grained instrumentation | Property-based fuzzing | `mythos/dynamic/aflpp_runner.py`, `mythos/dynamic/klee_runner.py`, `mythos/dynamic/qemu_harness.py`, `mythos/dynamic/frida_instr.py`, `mythos/dynamic/gdb_automation.py` |
|
| 89 |
-
| Exploit Synthesis | ROP/heap/privesc full chains | Primitive reasoning only | `mythos/exploit/pwntools_synth.py`, `mythos/exploit/rop_chain.py`, `mythos/exploit/heap_exploit.py`, `mythos/exploit/privesc_kb.py` |
|
| 90 |
-
| Self-Improvement | RL + curriculum + episodic memory | LoRA Scheduler | `mythos/learning/rl_planner.py`, `mythos/learning/curriculum.py`, `mythos/learning/episodic_memory.py`, `mythos/learning/mlflow_tracker.py`, `mythos/learning/lora_adapters.py` |
|
| 91 |
-
| Multi-Agent Coordination | Decoupled Planner/Explorer/Executor | Single orchestrator | `mythos/agents/{planner,explorer,executor,orchestrator}.py` |
|
| 92 |
-
| MCP Surface | Specialised servers per analysis domain | Generic MCP suite | `mythos/mcp/{static,dynamic,exploit,vuln_db,web_security}_*_mcp.py` |
|
| 93 |
-
| Productization | Stable API for external consumption | Gradio UI | `mythos/api/fastapi_server.py`, `mythos/api/{auth,webhooks,schemas}.py` |
|
| 94 |
-
|
| 95 |
-
---
|
| 96 |
-
|
| 97 |
-
## 4. Open-Source Components and Models β Closing Every Gap
|
| 98 |
-
|
| 99 |
-
### 4.1 Enhanced Reasoning and Planning
|
| 100 |
-
**Models** β DeepSeek-V2 (MoE), Qwen2-72B-Instruct, Mixtral 8Γ22B.
|
| 101 |
-
**Probabilistic frameworks** β Pyro (Uber AI), PyMC.
|
| 102 |
-
β `mythos/reasoning/probabilistic.py`
|
| 103 |
-
|
| 104 |
-
### 4.2 Advanced Static & Semantic Code Analysis
|
| 105 |
-
- **Tree-sitter** β CST/AST β CFG seed.
|
| 106 |
-
- **CodeQL (open components)** β semantic queries.
|
| 107 |
-
- **Joern** β Code Property Graphs.
|
| 108 |
-
- **Semgrep** β taint + dataflow rules.
|
| 109 |
-
- **CodeHawk** β binary value analysis (inspirational).
|
| 110 |
-
β `mythos/static/*.py`
|
| 111 |
-
|
| 112 |
-
### 4.3 Enhanced Dynamic Execution & Instrumentation
|
| 113 |
-
- **AFL++**, **LibFuzzer** β coverage-guided fuzzing.
|
| 114 |
-
- **KLEE**, **Angr** β symbolic + concolic execution.
|
| 115 |
-
- **QEMU** β full-system emulation.
|
| 116 |
-
- **Frida**, **GDB+Python** β instrumentation.
|
| 117 |
-
β `mythos/dynamic/*.py`
|
| 118 |
-
|
| 119 |
-
### 4.4 Sophisticated Exploit Synthesis
|
| 120 |
-
- **Pwntools**, **ROPGadget**, **angrop** β ROP / shellcode.
|
| 121 |
-
- **GEF** β heap visualization & manipulation.
|
| 122 |
-
- **LinPEAS / WinPEAS** codified into agent skills β privesc.
|
| 123 |
-
β `mythos/exploit/*.py`
|
| 124 |
-
|
| 125 |
-
### 4.5 Autonomous Iteration & Self-Improvement
|
| 126 |
-
- **Ray RLlib**, **Stable Baselines3** β RL controllers.
|
| 127 |
-
- **MLflow** β experiment tracking.
|
| 128 |
-
- **PEFT / LoRA / QLoRA** β Tier 2 adapters.
|
| 129 |
-
- **Synthetic data generation** β curriculum-driven trajectories.
|
| 130 |
-
β `mythos/learning/*.py`
|
| 131 |
-
|
| 132 |
-
### 4.6 Multi-Agent Coordination
|
| 133 |
-
- **AutoGen**, **CrewAI** β orchestration frameworks.
|
| 134 |
-
- **MCP** β inter-agent transport.
|
| 135 |
-
β `mythos/agents/orchestrator.py`
|
| 136 |
-
|
| 137 |
-
### 4.7 Cost-Effective Tiered Model Strategy
|
| 138 |
-
| Tier | Role | Open-source models |
|
| 139 |
-
|---|---|---|
|
| 140 |
-
| 1 | Strategy & deep reasoning | DeepSeek-V2, Qwen2-72B-Instruct, Mixtral 8Γ22B |
|
| 141 |
-
| 2 | Execution & code generation | Qwen 2.5 Coder 72B, CodeLlama-70B-Instruct |
|
| 142 |
-
| 3 | Consensus & adversarial review | Llama 3.3 70B, DeepSeek V3, Gemma 2 27B |
|
| 143 |
-
|
| 144 |
-
### 4.8 New MCP Servers
|
| 145 |
-
- `static-analysis-mcp` β Joern + CodeQL + Semgrep.
|
| 146 |
-
- `dynamic-analysis-mcp` β AFL++ + KLEE + Frida + GDB.
|
| 147 |
-
- `exploit-generation-mcp` β Pwntools + ROPGadget + heap kit.
|
| 148 |
-
- `vulnerability-database-mcp` β NVD + Exploit-DB + private KB.
|
| 149 |
-
- `web-security-mcp` β OWASP ZAP + custom web fuzzers.
|
| 150 |
-
β `mythos/mcp/*.py` (and registered in `mcp_config.json`).
|
| 151 |
-
|
| 152 |
-
---
|
| 153 |
-
|
| 154 |
-
## 5. Achieving Mythos-Level Capabilities β Detailed Approach
|
| 155 |
-
|
| 156 |
-
### 5.1 Hierarchical Reasoning
|
| 157 |
-
- **Planner Agent** β strategic, problem decomposition, hypothesis generation, attack-graph synthesis, resource allocation.
|
| 158 |
-
- **Explorer Agent** β tactical static analysis.
|
| 159 |
-
- **Executor Agent** β tactical dynamic execution + exploit synthesis.
|
| 160 |
-
- **Contextual Awareness** β every agent shares a rich, structured context bag.
|
| 161 |
-
|
| 162 |
-
### 5.2 Tool Use
|
| 163 |
-
- Dynamic orchestration over MCP suite.
|
| 164 |
-
- Fine-grained tool control (GDB stepping, breakpoint injection, fuzzer parameterisation).
|
| 165 |
-
- Tool-augmented reasoning (every tool output mutates the context).
|
| 166 |
-
- Custom-tool synthesis on the fly.
|
| 167 |
-
|
| 168 |
-
### 5.3 Memory
|
| 169 |
-
- **Working Memory** β EmbodiedOS persistent workspace.
|
| 170 |
-
- **Skill Memory** β `agentskills.io` registry, autonomous additions.
|
| 171 |
-
- **Knowledge Memory** β vector store of CS literature, RFCs, exploit write-ups.
|
| 172 |
-
- **Episodic Memory** β full campaign traces (`mythos/learning/episodic_memory.py`).
|
| 173 |
-
|
| 174 |
-
### 5.4 Self-Improvement
|
| 175 |
-
- Continuous LoRA fine-tuning on (success, failure) pairs.
|
| 176 |
-
- Reinforcement Learning over the Planner via Ray RLlib.
|
| 177 |
-
- Curriculum learning β progressively harder targets.
|
| 178 |
-
|
| 179 |
-
### 5.5 Multi-Agent Coordination
|
| 180 |
-
- Orchestrator (`mythos/agents/orchestrator.py`) wraps AutoGen/CrewAI semantics.
|
| 181 |
-
- Strict typed messages between agents (Pydantic models in `mythos/api/schemas.py`).
|
| 182 |
-
- Conflict resolution via Tier 3 consensus.
|
| 183 |
-
|
| 184 |
-
### 5.6 Security & Sandboxing
|
| 185 |
-
- Container hardening (Tirith pre-exec scanner).
|
| 186 |
-
- Network segmentation between LLM, exploit, and analysis layers.
|
| 187 |
-
- Strict input validation at every API boundary.
|
| 188 |
-
|
| 189 |
-
---
|
| 190 |
-
|
| 191 |
-
## 6. Implementation Roadmap
|
| 192 |
-
|
| 193 |
-
| Phase | Months | Objective | Deliverables |
|
| 194 |
-
|---|---|---|---|
|
| 195 |
-
| 1 | 1β3 | Foundation & core agents | Multi-agent orchestrator, basic Planner, initial MCP servers, Tier 1 LLM |
|
| 196 |
-
| 2 | 4β6 | Advanced tooling + CEGIS loop | Joern/CodeQL/Semgrep, AFL++/KLEE/QEMU, Tier 2 LLMs |
|
| 197 |
-
| 3 | 7β9 | Exploit synthesis & self-improvement | exploit-generation-mcp, RL planner, LoRA adapters, MLflow |
|
| 198 |
-
| 4 | 10β12 | Productization & API | FastAPI server, OAuth2/API-keys, webhooks, observability |
|
| 199 |
-
|
| 200 |
-
Every roadmap item is implemented or stubbed with a clear `TODO(mythos)` in the
|
| 201 |
-
corresponding module so that integrators can `grep -R 'TODO(mythos)'` to find
|
| 202 |
-
the remaining engineering work.
|
| 203 |
-
|
| 204 |
-
---
|
| 205 |
-
|
| 206 |
-
## 7. Productisation β Sellable API / Service
|
| 207 |
-
|
| 208 |
-
- **Rhodawk API** β `POST /v1/analyze_target` for code/binary submission.
|
| 209 |
-
- **Managed Service** β dedicated tenancy, custom fine-tune.
|
| 210 |
-
- **Enterprise** β air-gapped on-premise with white-glove support.
|
| 211 |
-
|
| 212 |
-
Implemented in `mythos/api/fastapi_server.py` (mountable next to the existing
|
| 213 |
-
Gradio UI).
|
| 214 |
-
|
| 215 |
-
---
|
| 216 |
-
|
| 217 |
-
## 8. Cross-Reference Implementation Index
|
| 218 |
-
|
| 219 |
-
| Plan Section | Module(s) |
|
| 220 |
-
|---|---|
|
| 221 |
-
| 1.3 Static + Semantic | `mythos/static/*` |
|
| 222 |
-
| 1.3 Hypothesis Engine | `mythos/reasoning/probabilistic.py` |
|
| 223 |
-
| 1.3 Dynamic Execution | `mythos/dynamic/*` |
|
| 224 |
-
| 1.3 Exploit Synthesis | `mythos/exploit/*` |
|
| 225 |
-
| 1.3 Iteration Loop | `mythos/agents/orchestrator.py`, `mythos/learning/rl_planner.py` |
|
| 226 |
-
| 4.1β4.6 Open Source | `mythos/static/*`, `mythos/dynamic/*`, `mythos/exploit/*`, `mythos/learning/*` |
|
| 227 |
-
| 4.8 New MCP servers | `mythos/mcp/*` + `mcp_config.json` extension |
|
| 228 |
-
| 5.x Mythos-level | `mythos/agents/*` + `mythos/reasoning/*` |
|
| 229 |
-
| 6 Roadmap | tracked here + `TODO(mythos)` markers |
|
| 230 |
-
| 7 Productization | `mythos/api/*` |
|
| 231 |
-
|
| 232 |
-
---
|
| 233 |
-
|
| 234 |
-
## 9. Cross-Check Checklist (vs. PDF Source)
|
| 235 |
-
|
| 236 |
-
- [x] Executive Summary β captured (Β§ Executive Summary).
|
| 237 |
-
- [x] Mythos capabilities & architecture β Β§1.
|
| 238 |
-
- [x] Rhodawk/EmbodiedOS foundation β Β§2.
|
| 239 |
-
- [x] Gap Analysis table β Β§3.
|
| 240 |
-
- [x] Open-source closures (4.1β4.8) β Β§4 + modules under `mythos/`.
|
| 241 |
-
- [x] Mythos-level approach (5.1β5.6) β Β§5 + agent/reasoning modules.
|
| 242 |
-
- [x] Implementation roadmap (Phases 1β4) β Β§6.
|
| 243 |
-
- [x] Productization & sellable API β Β§7 + `mythos/api/*`.
|
| 244 |
-
- [x] Tiered model strategy β Β§4.7 (and `mythos/agents/planner.py` env-driven).
|
| 245 |
-
- [x] MCP suite extension β `mcp_config.json` + `mythos/mcp/*`.
|
| 246 |
-
- [x] Self-improvement (RL, MLflow, LoRA, curriculum, episodic memory) β `mythos/learning/*`.
|
| 247 |
-
|
| 248 |
-
Every checkbox above corresponds to a real file in this commit; see
|
| 249 |
-
`mythos/__init__.py` for the canonical export surface.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
openclaude_grpc β Python bridge to the vendored OpenClaude headless gRPC daemon.
|
| 3 |
+
|
| 4 |
+
Public surface:
|
| 5 |
+
OpenClaudeClient β low-level bidi-streaming client
|
| 6 |
+
run_openclaude β drop-in replacement for the legacy ``run_aider``
|
| 7 |
+
function (returns ``(combined_output, exit_code)``)
|
| 8 |
+
"""
|
| 9 |
+
from .client import (
|
| 10 |
+
OpenClaudeClient,
|
| 11 |
+
OpenClaudeError,
|
| 12 |
+
OpenClaudeResult,
|
| 13 |
+
run_openclaude,
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
__all__ = [
|
| 17 |
+
"OpenClaudeClient",
|
| 18 |
+
"OpenClaudeError",
|
| 19 |
+
"OpenClaudeResult",
|
| 20 |
+
"run_openclaude",
|
| 21 |
+
]
|
|
@@ -0,0 +1,348 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
OpenClaude gRPC client β replaces the legacy aider subprocess shell-out.
|
| 3 |
+
|
| 4 |
+
Design contract
|
| 5 |
+
---------------
|
| 6 |
+
Every callable in this module returns an :class:`OpenClaudeResult` (or the
|
| 7 |
+
``(combined_output, exit_code)`` tuple that legacy callers expect) so that the
|
| 8 |
+
existing validation, SAST gate, conviction engine, and red-team loops keep
|
| 9 |
+
plugging in unchanged.
|
| 10 |
+
|
| 11 |
+
Streaming events from the daemon (text chunks, tool start/result, action
|
| 12 |
+
required) are accumulated into a single transcript so that downstream code
|
| 13 |
+
that previously parsed aider stdout still sees a useful blob.
|
| 14 |
+
|
| 15 |
+
Operational guarantees
|
| 16 |
+
----------------------
|
| 17 |
+
* Connection drops, ``StatusCode.UNAVAILABLE`` and stream-level RPC errors
|
| 18 |
+
are converted into a non-zero exit code with the error message embedded in
|
| 19 |
+
the combined output (mirrors aider's crash semantics).
|
| 20 |
+
* Per-call wall-clock timeout (defaults to 600 s) β same as the legacy
|
| 21 |
+
aider invocation.
|
| 22 |
+
* Bidi stream auto-answers any ``ActionRequired`` prompt with ``"y"`` so that
|
| 23 |
+
headless mode never deadlocks waiting for human input. The daemon also
|
| 24 |
+
honours ``OPENCLAUDE_AUTO_APPROVE=1`` server-side as a belt-and-braces.
|
| 25 |
+
* Each call produces a fresh stream β sessions are not shared across calls
|
| 26 |
+
to keep failures isolated.
|
| 27 |
+
"""
|
| 28 |
+
from __future__ import annotations
|
| 29 |
+
|
| 30 |
+
import logging
|
| 31 |
+
import os
|
| 32 |
+
import queue
|
| 33 |
+
import threading
|
| 34 |
+
import time
|
| 35 |
+
from dataclasses import dataclass, field
|
| 36 |
+
from typing import Iterable, Optional
|
| 37 |
+
|
| 38 |
+
import grpc
|
| 39 |
+
|
| 40 |
+
from . import openclaude_pb2 as pb # type: ignore
|
| 41 |
+
from . import openclaude_pb2_grpc as pb_grpc # type: ignore
|
| 42 |
+
|
| 43 |
+
logger = logging.getLogger("openclaude_grpc")
|
| 44 |
+
|
| 45 |
+
DEFAULT_HOST = os.getenv("OPENCLAUDE_GRPC_HOST", "127.0.0.1")
|
| 46 |
+
DEFAULT_PORT_DO = int(os.getenv("OPENCLAUDE_GRPC_PORT_DO", "50051"))
|
| 47 |
+
DEFAULT_PORT_OR = int(os.getenv("OPENCLAUDE_GRPC_PORT_OR", "50052"))
|
| 48 |
+
DEFAULT_TIMEOUT = int(os.getenv("OPENCLAUDE_TIMEOUT", "600"))
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class OpenClaudeError(RuntimeError):
|
| 52 |
+
"""Raised for unrecoverable client-side errors (connect, decode, etc)."""
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
@dataclass
|
| 56 |
+
class OpenClaudeResult:
|
| 57 |
+
"""Mirror of the legacy aider return contract.
|
| 58 |
+
|
| 59 |
+
``stdout`` is the model's narrative + tool stdout, ``stderr`` is tool
|
| 60 |
+
failures and gRPC errors, ``exit_code`` is 0 on a clean ``done`` event,
|
| 61 |
+
non-zero on any error event or transport failure. ``model_used`` lets
|
| 62 |
+
downstream telemetry attribute findings to the right provider.
|
| 63 |
+
"""
|
| 64 |
+
|
| 65 |
+
stdout: str = ""
|
| 66 |
+
stderr: str = ""
|
| 67 |
+
exit_code: int = 0
|
| 68 |
+
model_used: str = ""
|
| 69 |
+
prompt_tokens: int = 0
|
| 70 |
+
completion_tokens: int = 0
|
| 71 |
+
tool_calls: list[dict] = field(default_factory=list)
|
| 72 |
+
|
| 73 |
+
@property
|
| 74 |
+
def combined_output(self) -> str:
|
| 75 |
+
if self.stderr:
|
| 76 |
+
return f"{self.stdout}\n{self.stderr}".strip()
|
| 77 |
+
return self.stdout.strip()
|
| 78 |
+
|
| 79 |
+
def as_legacy_tuple(self) -> tuple[str, int]:
|
| 80 |
+
return self.combined_output, self.exit_code
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
class OpenClaudeClient:
|
| 84 |
+
"""Bidirectional-streaming client for one OpenClaude daemon."""
|
| 85 |
+
|
| 86 |
+
def __init__(
|
| 87 |
+
self,
|
| 88 |
+
host: str = DEFAULT_HOST,
|
| 89 |
+
port: int = DEFAULT_PORT_DO,
|
| 90 |
+
timeout: int = DEFAULT_TIMEOUT,
|
| 91 |
+
max_message_mb: int = 64,
|
| 92 |
+
) -> None:
|
| 93 |
+
self.host = host
|
| 94 |
+
self.port = port
|
| 95 |
+
self.timeout = timeout
|
| 96 |
+
opts = [
|
| 97 |
+
("grpc.max_send_message_length", max_message_mb * 1024 * 1024),
|
| 98 |
+
("grpc.max_receive_message_length", max_message_mb * 1024 * 1024),
|
| 99 |
+
("grpc.keepalive_time_ms", 30_000),
|
| 100 |
+
("grpc.keepalive_timeout_ms", 10_000),
|
| 101 |
+
("grpc.keepalive_permit_without_calls", 1),
|
| 102 |
+
]
|
| 103 |
+
self._channel = grpc.insecure_channel(f"{host}:{port}", options=opts)
|
| 104 |
+
self._stub = pb_grpc.AgentServiceStub(self._channel)
|
| 105 |
+
|
| 106 |
+
# ------------------------------------------------------------------
|
| 107 |
+
# health
|
| 108 |
+
# ------------------------------------------------------------------
|
| 109 |
+
def wait_ready(self, deadline_s: float = 60.0) -> bool:
|
| 110 |
+
"""Block until the daemon's gRPC channel is READY or the deadline
|
| 111 |
+
elapses. Used by the orchestrator at boot to ensure the bun-built
|
| 112 |
+
daemon is alive before issuing the first healing call."""
|
| 113 |
+
start = time.monotonic()
|
| 114 |
+
while time.monotonic() - start < deadline_s:
|
| 115 |
+
try:
|
| 116 |
+
grpc.channel_ready_future(self._channel).result(timeout=2.0)
|
| 117 |
+
return True
|
| 118 |
+
except grpc.FutureTimeoutError:
|
| 119 |
+
continue
|
| 120 |
+
except Exception:
|
| 121 |
+
time.sleep(0.5)
|
| 122 |
+
return False
|
| 123 |
+
|
| 124 |
+
# ------------------------------------------------------------------
|
| 125 |
+
# core call
|
| 126 |
+
# ------------------------------------------------------------------
|
| 127 |
+
def chat(
|
| 128 |
+
self,
|
| 129 |
+
message: str,
|
| 130 |
+
working_directory: str,
|
| 131 |
+
model: str = "",
|
| 132 |
+
session_id: str = "",
|
| 133 |
+
timeout: Optional[int] = None,
|
| 134 |
+
) -> OpenClaudeResult:
|
| 135 |
+
"""Send one prompt, drain the bidi stream, return an aggregated
|
| 136 |
+
:class:`OpenClaudeResult`."""
|
| 137 |
+
deadline = timeout if timeout is not None else self.timeout
|
| 138 |
+
result = OpenClaudeResult(model_used=model)
|
| 139 |
+
|
| 140 |
+
outbound: "queue.Queue[Optional[pb.ClientMessage]]" = queue.Queue()
|
| 141 |
+
outbound.put(
|
| 142 |
+
pb.ClientMessage(
|
| 143 |
+
request=pb.ChatRequest(
|
| 144 |
+
message=message,
|
| 145 |
+
working_directory=working_directory,
|
| 146 |
+
model=model,
|
| 147 |
+
session_id=session_id,
|
| 148 |
+
)
|
| 149 |
+
)
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
def _send_iter() -> Iterable[pb.ClientMessage]:
|
| 153 |
+
while True:
|
| 154 |
+
item = outbound.get()
|
| 155 |
+
if item is None:
|
| 156 |
+
return
|
| 157 |
+
yield item
|
| 158 |
+
|
| 159 |
+
done = threading.Event()
|
| 160 |
+
try:
|
| 161 |
+
stream = self._stub.Chat(_send_iter(), timeout=deadline)
|
| 162 |
+
for ev in stream:
|
| 163 |
+
kind = ev.WhichOneof("event")
|
| 164 |
+
if kind == "text_chunk":
|
| 165 |
+
result.stdout += ev.text_chunk.text
|
| 166 |
+
elif kind == "tool_start":
|
| 167 |
+
result.tool_calls.append(
|
| 168 |
+
{
|
| 169 |
+
"tool": ev.tool_start.tool_name,
|
| 170 |
+
"args": ev.tool_start.arguments_json,
|
| 171 |
+
"id": ev.tool_start.tool_use_id,
|
| 172 |
+
}
|
| 173 |
+
)
|
| 174 |
+
result.stdout += (
|
| 175 |
+
f"\n[tool βΆ {ev.tool_start.tool_name}]"
|
| 176 |
+
f" {ev.tool_start.arguments_json}\n"
|
| 177 |
+
)
|
| 178 |
+
elif kind == "tool_result":
|
| 179 |
+
prefix = "[tool β]" if ev.tool_result.is_error else "[tool β]"
|
| 180 |
+
line = (
|
| 181 |
+
f"\n{prefix} {ev.tool_result.tool_name}:"
|
| 182 |
+
f" {ev.tool_result.output}\n"
|
| 183 |
+
)
|
| 184 |
+
if ev.tool_result.is_error:
|
| 185 |
+
result.stderr += line
|
| 186 |
+
else:
|
| 187 |
+
result.stdout += line
|
| 188 |
+
elif kind == "action_required":
|
| 189 |
+
# Auto-approve. Server should already be in
|
| 190 |
+
# OPENCLAUDE_AUTO_APPROVE=1, but we double-tap to make
|
| 191 |
+
# the client safe even if the daemon was started
|
| 192 |
+
# without that flag.
|
| 193 |
+
outbound.put(
|
| 194 |
+
pb.ClientMessage(
|
| 195 |
+
input=pb.UserInput(
|
| 196 |
+
prompt_id=ev.action_required.prompt_id,
|
| 197 |
+
reply="y",
|
| 198 |
+
)
|
| 199 |
+
)
|
| 200 |
+
)
|
| 201 |
+
elif kind == "done":
|
| 202 |
+
if not result.stdout and ev.done.full_text:
|
| 203 |
+
result.stdout = ev.done.full_text
|
| 204 |
+
result.prompt_tokens = ev.done.prompt_tokens
|
| 205 |
+
result.completion_tokens = ev.done.completion_tokens
|
| 206 |
+
result.exit_code = 0
|
| 207 |
+
done.set()
|
| 208 |
+
break
|
| 209 |
+
elif kind == "error":
|
| 210 |
+
result.stderr += (
|
| 211 |
+
f"\n[openclaude {ev.error.code}] {ev.error.message}"
|
| 212 |
+
)
|
| 213 |
+
result.exit_code = 1
|
| 214 |
+
done.set()
|
| 215 |
+
break
|
| 216 |
+
except grpc.RpcError as exc:
|
| 217 |
+
result.stderr += (
|
| 218 |
+
f"\n[grpc {exc.code().name}] {exc.details() or str(exc)}"
|
| 219 |
+
)
|
| 220 |
+
result.exit_code = 2
|
| 221 |
+
except Exception as exc: # noqa: BLE001
|
| 222 |
+
result.stderr += f"\n[client] {type(exc).__name__}: {exc}"
|
| 223 |
+
result.exit_code = 3
|
| 224 |
+
finally:
|
| 225 |
+
outbound.put(None)
|
| 226 |
+
try:
|
| 227 |
+
self._channel # keep alive β see close()
|
| 228 |
+
except Exception:
|
| 229 |
+
pass
|
| 230 |
+
if not done.is_set() and result.exit_code == 0:
|
| 231 |
+
# Stream ended without an explicit done event.
|
| 232 |
+
result.exit_code = 4
|
| 233 |
+
result.stderr += "\n[client] stream ended without done event"
|
| 234 |
+
|
| 235 |
+
return result
|
| 236 |
+
|
| 237 |
+
def close(self) -> None:
|
| 238 |
+
try:
|
| 239 |
+
self._channel.close()
|
| 240 |
+
except Exception:
|
| 241 |
+
pass
|
| 242 |
+
|
| 243 |
+
def __enter__(self) -> "OpenClaudeClient":
|
| 244 |
+
return self
|
| 245 |
+
|
| 246 |
+
def __exit__(self, *exc) -> None:
|
| 247 |
+
self.close()
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 251 |
+
# High-level helper β drop-in replacement for the legacy ``run_aider``
|
| 252 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 253 |
+
def _format_prompt(prompt: str, context_files: list[str]) -> str:
|
| 254 |
+
"""Aider received context files as CLI args; OpenClaude gets them
|
| 255 |
+
inline so it knows which files matter. The agent already has full
|
| 256 |
+
file-tool access via MCP/native tools, so this is just hinting."""
|
| 257 |
+
if not context_files:
|
| 258 |
+
return prompt
|
| 259 |
+
file_list = "\n".join(f"- {p}" for p in context_files)
|
| 260 |
+
return (
|
| 261 |
+
f"{prompt}\n\n"
|
| 262 |
+
"ββ Context files (focus your edits here) ββ\n"
|
| 263 |
+
f"{file_list}\n"
|
| 264 |
+
)
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def run_openclaude(
|
| 268 |
+
mcp_config_path: str,
|
| 269 |
+
prompt: str,
|
| 270 |
+
context_files: list[str],
|
| 271 |
+
*,
|
| 272 |
+
repo_dir: str,
|
| 273 |
+
primary_port: int = DEFAULT_PORT_DO,
|
| 274 |
+
fallback_port: int = DEFAULT_PORT_OR,
|
| 275 |
+
primary_label: str = "DigitalOcean",
|
| 276 |
+
fallback_label: str = "OpenRouter",
|
| 277 |
+
primary_model: str = "",
|
| 278 |
+
fallback_model: str = "",
|
| 279 |
+
timeout: int = DEFAULT_TIMEOUT,
|
| 280 |
+
log_fn=None,
|
| 281 |
+
) -> tuple[str, int]:
|
| 282 |
+
"""Drop-in replacement for ``run_aider`` β preserves the
|
| 283 |
+
``(combined_output, exit_code)`` return shape so every caller in
|
| 284 |
+
``app.py`` works unchanged.
|
| 285 |
+
|
| 286 |
+
Tries the **primary** daemon (DigitalOcean Inference) first; on any
|
| 287 |
+
non-zero exit code it falls back to the **OpenRouter** daemon so the
|
| 288 |
+
healing loop's existing 15-attempt retry semantics still apply.
|
| 289 |
+
|
| 290 |
+
``mcp_config_path`` is honoured by the daemon itself β it re-reads
|
| 291 |
+
``MCP_RUNTIME_CONFIG`` per chat session β so we just forward the
|
| 292 |
+
path via env and pass the prompt verbatim.
|
| 293 |
+
"""
|
| 294 |
+
# Make sure the daemon picks up the MCP file the orchestrator just
|
| 295 |
+
# wrote. The daemon was launched with this env var; this assignment
|
| 296 |
+
# is here only as documentation / safety net for ad-hoc test runs.
|
| 297 |
+
os.environ.setdefault("MCP_RUNTIME_CONFIG", mcp_config_path)
|
| 298 |
+
|
| 299 |
+
full_prompt = _format_prompt(prompt, context_files)
|
| 300 |
+
|
| 301 |
+
chain: list[tuple[int, str, str]] = []
|
| 302 |
+
if primary_port:
|
| 303 |
+
chain.append((primary_port, primary_label, primary_model))
|
| 304 |
+
if fallback_port and fallback_port != primary_port:
|
| 305 |
+
chain.append((fallback_port, fallback_label, fallback_model))
|
| 306 |
+
|
| 307 |
+
last_output, last_code = "", 1
|
| 308 |
+
for idx, (port, label, model) in enumerate(chain):
|
| 309 |
+
if log_fn:
|
| 310 |
+
log_fn(
|
| 311 |
+
f"OpenClaude attempt via {label} (port {port}, model={model or 'default'})",
|
| 312 |
+
"INFO",
|
| 313 |
+
)
|
| 314 |
+
try:
|
| 315 |
+
with OpenClaudeClient(
|
| 316 |
+
host=DEFAULT_HOST, port=port, timeout=timeout
|
| 317 |
+
) as client:
|
| 318 |
+
if not client.wait_ready(deadline_s=15.0):
|
| 319 |
+
msg = (
|
| 320 |
+
f"OpenClaude daemon at {DEFAULT_HOST}:{port} "
|
| 321 |
+
"not ready within 15s"
|
| 322 |
+
)
|
| 323 |
+
if log_fn:
|
| 324 |
+
log_fn(msg, "WARN")
|
| 325 |
+
last_output, last_code = msg, 5
|
| 326 |
+
continue
|
| 327 |
+
result = client.chat(
|
| 328 |
+
message=full_prompt,
|
| 329 |
+
working_directory=repo_dir,
|
| 330 |
+
model=model,
|
| 331 |
+
timeout=timeout,
|
| 332 |
+
)
|
| 333 |
+
except Exception as exc: # noqa: BLE001
|
| 334 |
+
last_output = f"[client] {type(exc).__name__}: {exc}"
|
| 335 |
+
last_code = 6
|
| 336 |
+
if log_fn:
|
| 337 |
+
log_fn(f"{label} client crash: {exc}", "FAIL")
|
| 338 |
+
continue
|
| 339 |
+
|
| 340 |
+
last_output, last_code = result.as_legacy_tuple()
|
| 341 |
+
if last_code == 0:
|
| 342 |
+
return last_output, last_code
|
| 343 |
+
if log_fn and idx < len(chain) - 1:
|
| 344 |
+
log_fn(
|
| 345 |
+
f"{label} failed (exit {last_code}) β falling back",
|
| 346 |
+
"WARN",
|
| 347 |
+
)
|
| 348 |
+
return last_output, last_code
|
|
@@ -4,11 +4,14 @@ uv>=0.7.0
|
|
| 4 |
gitpython==3.1.46
|
| 5 |
gradio>=5.49.0,<6
|
| 6 |
jinja2==3.1.6
|
| 7 |
-
|
| 8 |
-
#
|
| 9 |
-
#
|
| 10 |
-
#
|
| 11 |
-
#
|
|
|
|
|
|
|
|
|
|
| 12 |
ruff
|
| 13 |
tenacity
|
| 14 |
bandit[toml]
|
|
@@ -16,6 +19,8 @@ pip-audit
|
|
| 16 |
radon
|
| 17 |
hypothesis[cli]>=6.100.0
|
| 18 |
semgrep>=1.45.0
|
|
|
|
|
|
|
| 19 |
sentence-transformers>=2.7.0
|
| 20 |
sqlite-vec>=0.1.1
|
| 21 |
pygithub>=2.3.0
|
|
@@ -28,35 +33,24 @@ z3-solver>=4.12.0
|
|
| 28 |
qdrant-client>=1.9.0
|
| 29 |
transformers>=4.40.0
|
| 30 |
torch>=2.2.0
|
| 31 |
-
|
| 32 |
-
#
|
|
|
|
|
|
|
| 33 |
angr>=9.2.0
|
| 34 |
networkx>=3.0
|
| 35 |
defusedxml>=0.7.1
|
| 36 |
|
| 37 |
# βββ Mythos-level upgrade (see mythos/MYTHOS_PLAN.md) ββββββββββββββββββββ
|
| 38 |
-
# Required for the productization API surface.
|
| 39 |
fastapi>=0.110.0
|
| 40 |
uvicorn[standard]>=0.27.0
|
| 41 |
pydantic>=2.6.0
|
| 42 |
-
# Optional: heavier capabilities β install on demand for full Mythos parity.
|
| 43 |
-
# pyro-ppl>=1.9.0 # Β§ 4.1 probabilistic reasoning (Bayesian backend)
|
| 44 |
-
# pymc>=5.10.0 # Β§ 4.1 probabilistic reasoning (alt backend)
|
| 45 |
-
# tree-sitter-languages>=1.10.0 # Β§ 4.2 Tree-sitter CPG bridge
|
| 46 |
-
# mlflow>=2.12.0 # Β§ 4.5 experiment tracking
|
| 47 |
-
# ray[rllib]>=2.10.0 # Β§ 4.5 RL planner backend
|
| 48 |
-
# stable-baselines3>=2.3.0 # Β§ 4.5 RL planner backend (alt)
|
| 49 |
-
# pwntools>=4.12.0 # Β§ 4.4 exploit synthesis (Linux only)
|
| 50 |
-
# frida>=16.0.0 # Β§ 4.3 dynamic instrumentation
|
| 51 |
|
| 52 |
# βββ ARCHITECT masterplan dependencies (see ARCHITECT_MASTERPLAN.md) βββββ
|
| 53 |
-
# Required at runtime in light-mode (no extra binaries):
|
| 54 |
dnspython>=2.6.0
|
| 55 |
-
|
| 56 |
-
#
|
| 57 |
-
#
|
| 58 |
-
#
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
# opcua>=0.98.13 # ics-scada (OPC-UA)
|
| 62 |
-
# rtl-sdr / hackrf-host (system pkgs) for sdr-analysis-mcp
|
|
|
|
| 4 |
gitpython==3.1.46
|
| 5 |
gradio>=5.49.0,<6
|
| 6 |
jinja2==3.1.6
|
| 7 |
+
|
| 8 |
+
# βββ Code-generation layer βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 9 |
+
# Aider-chat has been removed and replaced by the vendored OpenClaude
|
| 10 |
+
# headless gRPC daemon (see vendor/openclaude/ + openclaude_grpc/).
|
| 11 |
+
# The Dockerfile installs grpcio + grpcio-tools at build time so that
|
| 12 |
+
# protobuf stub generation runs inside the build, not in this lock file.
|
| 13 |
+
|
| 14 |
+
# βββ Static analysis / SAST ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 15 |
ruff
|
| 16 |
tenacity
|
| 17 |
bandit[toml]
|
|
|
|
| 19 |
radon
|
| 20 |
hypothesis[cli]>=6.100.0
|
| 21 |
semgrep>=1.45.0
|
| 22 |
+
|
| 23 |
+
# βββ ML / embeddings / vector store ββββββββββββββββββββββββββββββββββββββ
|
| 24 |
sentence-transformers>=2.7.0
|
| 25 |
sqlite-vec>=0.1.1
|
| 26 |
pygithub>=2.3.0
|
|
|
|
| 33 |
qdrant-client>=1.9.0
|
| 34 |
transformers>=4.40.0
|
| 35 |
torch>=2.2.0
|
| 36 |
+
|
| 37 |
+
# atheris removed: requires Clang + libFuzzer at compile time which is
|
| 38 |
+
# unavailable on HuggingFace Space Docker images. Fuzzing falls back to
|
| 39 |
+
# Hypothesis automatically.
|
| 40 |
angr>=9.2.0
|
| 41 |
networkx>=3.0
|
| 42 |
defusedxml>=0.7.1
|
| 43 |
|
| 44 |
# βββ Mythos-level upgrade (see mythos/MYTHOS_PLAN.md) ββββββββββββββββββββ
|
|
|
|
| 45 |
fastapi>=0.110.0
|
| 46 |
uvicorn[standard]>=0.27.0
|
| 47 |
pydantic>=2.6.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
# βββ ARCHITECT masterplan dependencies (see ARCHITECT_MASTERPLAN.md) βββββ
|
|
|
|
| 50 |
dnspython>=2.6.0
|
| 51 |
+
|
| 52 |
+
# βββ gRPC bridge to the OpenClaude daemon ββββββββββββββββββββββββββββββββ
|
| 53 |
+
# (Pinned again here as a safety net for editable installs / `pip install -r`
|
| 54 |
+
# in environments that bypass the Dockerfile.)
|
| 55 |
+
grpcio>=1.66.0,<2.0.0
|
| 56 |
+
protobuf>=5.0.0,<6.0.0
|
|
|
|
|
|
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules
|
| 2 |
+
dist
|
| 3 |
+
.git
|
| 4 |
+
.gitignore
|
| 5 |
+
.env
|
| 6 |
+
.env.*
|
| 7 |
+
!.env.example
|
| 8 |
+
coverage
|
| 9 |
+
reports
|
| 10 |
+
vscode-extension
|
| 11 |
+
python
|
| 12 |
+
docs
|
| 13 |
+
*.md
|
| 14 |
+
!README.md
|
| 15 |
+
.github
|
| 16 |
+
.tsbuildinfo
|
|
@@ -0,0 +1,374 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# =============================================================================
|
| 2 |
+
# OpenClaude Environment Configuration
|
| 3 |
+
# =============================================================================
|
| 4 |
+
# Copy this file to .env and fill in your values:
|
| 5 |
+
# cp .env.example .env
|
| 6 |
+
#
|
| 7 |
+
# Only set the variables for the provider you want to use.
|
| 8 |
+
# All other sections can be left commented out.
|
| 9 |
+
# =============================================================================
|
| 10 |
+
|
| 11 |
+
# =============================================================================
|
| 12 |
+
# SYSTEM-WIDE SETUP (OPTIONAL)
|
| 13 |
+
# =============================================================================
|
| 14 |
+
# Instead of using a .env file per project, you can set these variables
|
| 15 |
+
# system-wide so OpenClaude works from any directory on your machine.
|
| 16 |
+
#
|
| 17 |
+
# STEP 1: Pick your provider variables from the list below.
|
| 18 |
+
# STEP 2: Set them using the method for your OS (see further down).
|
| 19 |
+
#
|
| 20 |
+
# ββ Provider variables βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 21 |
+
#
|
| 22 |
+
# Option 1 β Anthropic:
|
| 23 |
+
# ANTHROPIC_API_KEY=sk-ant-your-key-here
|
| 24 |
+
# ANTHROPIC_MODEL=claude-sonnet-4-5 (optional)
|
| 25 |
+
# ANTHROPIC_BASE_URL=https://api.anthropic.com (optional)
|
| 26 |
+
#
|
| 27 |
+
# Option 2 β OpenAI:
|
| 28 |
+
# CLAUDE_CODE_USE_OPENAI=1
|
| 29 |
+
# OPENAI_API_KEY=sk-your-key-here
|
| 30 |
+
# OPENAI_MODEL=gpt-4o
|
| 31 |
+
# OPENAI_BASE_URL=https://api.openai.com/v1 (optional)
|
| 32 |
+
#
|
| 33 |
+
# Option 3 β Google Gemini:
|
| 34 |
+
# CLAUDE_CODE_USE_GEMINI=1
|
| 35 |
+
# GEMINI_API_KEY=your-gemini-key-here
|
| 36 |
+
# GEMINI_MODEL=gemini-2.0-flash
|
| 37 |
+
# GEMINI_BASE_URL=https://generativelanguage.googleapis.com (optional)
|
| 38 |
+
#
|
| 39 |
+
# Option 4 β GitHub Models:
|
| 40 |
+
# CLAUDE_CODE_USE_GITHUB=1
|
| 41 |
+
# GITHUB_TOKEN=ghp_your-token-here
|
| 42 |
+
#
|
| 43 |
+
# Option 5 β Ollama (local):
|
| 44 |
+
# CLAUDE_CODE_USE_OPENAI=1
|
| 45 |
+
# OPENAI_BASE_URL=http://localhost:11434/v1
|
| 46 |
+
# OPENAI_API_KEY=ollama
|
| 47 |
+
# OPENAI_MODEL=llama3.2
|
| 48 |
+
#
|
| 49 |
+
# Option 6 β LM Studio (local):
|
| 50 |
+
# CLAUDE_CODE_USE_OPENAI=1
|
| 51 |
+
# OPENAI_BASE_URL=http://localhost:1234/v1
|
| 52 |
+
# OPENAI_MODEL=your-model-id-here
|
| 53 |
+
# OPENAI_API_KEY=lmstudio (optional)
|
| 54 |
+
#
|
| 55 |
+
# Option 7 β AWS Bedrock (may also need: aws configure):
|
| 56 |
+
# CLAUDE_CODE_USE_BEDROCK=1
|
| 57 |
+
# AWS_REGION=us-east-1
|
| 58 |
+
# AWS_DEFAULT_REGION=us-east-1
|
| 59 |
+
# AWS_BEARER_TOKEN_BEDROCK=your-bearer-token-here
|
| 60 |
+
# ANTHROPIC_BEDROCK_BASE_URL=https://bedrock-runtime.us-east-1.amazonaws.com
|
| 61 |
+
#
|
| 62 |
+
# Option 8 β Google Vertex AI:
|
| 63 |
+
# CLAUDE_CODE_USE_VERTEX=1
|
| 64 |
+
# ANTHROPIC_VERTEX_PROJECT_ID=your-gcp-project-id
|
| 65 |
+
# CLOUD_ML_REGION=us-east5
|
| 66 |
+
# GOOGLE_CLOUD_PROJECT=your-gcp-project-id
|
| 67 |
+
#
|
| 68 |
+
# ββ How to set variables on each OS ββββββββββββββββββββββββββββββββββ
|
| 69 |
+
#
|
| 70 |
+
# macOS (zsh):
|
| 71 |
+
# 1. Open: nano ~/.zshrc
|
| 72 |
+
# 2. Add each variable as: export VAR_NAME=value
|
| 73 |
+
# 3. Save and reload: source ~/.zshrc
|
| 74 |
+
#
|
| 75 |
+
# Linux (bash):
|
| 76 |
+
# 1. Open: nano ~/.bashrc
|
| 77 |
+
# 2. Add each variable as: export VAR_NAME=value
|
| 78 |
+
# 3. Save and reload: source ~/.bashrc
|
| 79 |
+
#
|
| 80 |
+
# Windows (PowerShell):
|
| 81 |
+
# Run for each variable:
|
| 82 |
+
# [System.Environment]::SetEnvironmentVariable('VAR_NAME', 'value', 'User')
|
| 83 |
+
# Then restart your terminal.
|
| 84 |
+
#
|
| 85 |
+
# Windows (Command Prompt):
|
| 86 |
+
# Run for each variable:
|
| 87 |
+
# setx VAR_NAME value
|
| 88 |
+
# Then restart your terminal.
|
| 89 |
+
#
|
| 90 |
+
# Windows (GUI):
|
| 91 |
+
# Settings > System > About > Advanced System Settings >
|
| 92 |
+
# Environment Variables > under "User variables" click New,
|
| 93 |
+
# then add each variable.
|
| 94 |
+
#
|
| 95 |
+
# ββ Important notes ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 96 |
+
#
|
| 97 |
+
# LOCAL SERVERS: If using LM Studio or Ollama, the server MUST be
|
| 98 |
+
# running with a model loaded before you launch OpenClaude β
|
| 99 |
+
# otherwise you'll get connection errors.
|
| 100 |
+
#
|
| 101 |
+
# SWITCHING PROVIDERS: To temporarily switch, unset the relevant
|
| 102 |
+
# variables in your current terminal session:
|
| 103 |
+
#
|
| 104 |
+
# macOS / Linux:
|
| 105 |
+
# unset VAR_NAME
|
| 106 |
+
# # e.g.: unset CLAUDE_CODE_USE_OPENAI OPENAI_BASE_URL OPENAI_MODEL
|
| 107 |
+
#
|
| 108 |
+
# Windows (PowerShell β current session only):
|
| 109 |
+
# Remove-Item Env:VAR_NAME
|
| 110 |
+
#
|
| 111 |
+
# To permanently remove a variable on Windows:
|
| 112 |
+
# [System.Environment]::SetEnvironmentVariable('VAR_NAME', $null, 'User')
|
| 113 |
+
#
|
| 114 |
+
# LOAD ORDER:
|
| 115 |
+
# Shell and system environment variables are inherited by the process.
|
| 116 |
+
# Project .env files are only used if your launcher or shell loads them
|
| 117 |
+
# before starting OpenClaude.
|
| 118 |
+
# COMPATIBILITY:
|
| 119 |
+
# System-wide variables work regardless of how you run OpenClaude:
|
| 120 |
+
# npx, global npm install, bun run, or node directly. Any process
|
| 121 |
+
# launched from your terminal inherits your shell's environment.
|
| 122 |
+
#
|
| 123 |
+
# REMINDER: Make sure .env is in your .gitignore to avoid committing secrets.
|
| 124 |
+
# =============================================================================
|
| 125 |
+
|
| 126 |
+
# =============================================================================
|
| 127 |
+
# PROVIDER SELECTION β uncomment ONE block below
|
| 128 |
+
# =============================================================================
|
| 129 |
+
|
| 130 |
+
# -----------------------------------------------------------------------------
|
| 131 |
+
# Option 1: Anthropic (default β no provider flag needed)
|
| 132 |
+
# -----------------------------------------------------------------------------
|
| 133 |
+
ANTHROPIC_API_KEY=sk-ant-your-key-here
|
| 134 |
+
|
| 135 |
+
# Override the default model (optional)
|
| 136 |
+
# ANTHROPIC_MODEL=claude-sonnet-4-5
|
| 137 |
+
|
| 138 |
+
# Use a custom Anthropic-compatible endpoint (optional)
|
| 139 |
+
# ANTHROPIC_BASE_URL=https://api.anthropic.com
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
# -----------------------------------------------------------------------------
|
| 143 |
+
# Option 2: OpenAI
|
| 144 |
+
# -----------------------------------------------------------------------------
|
| 145 |
+
# CLAUDE_CODE_USE_OPENAI=1
|
| 146 |
+
# OPENAI_API_KEY=sk-your-key-here
|
| 147 |
+
# OPENAI_MODEL=gpt-4o
|
| 148 |
+
|
| 149 |
+
# Use a custom OpenAI-compatible endpoint (optional β defaults to api.openai.com)
|
| 150 |
+
# OPENAI_BASE_URL=https://api.openai.com/v1
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
# -----------------------------------------------------------------------------
|
| 154 |
+
# Option 3: Google Gemini
|
| 155 |
+
# -----------------------------------------------------------------------------
|
| 156 |
+
# CLAUDE_CODE_USE_GEMINI=1
|
| 157 |
+
# GEMINI_API_KEY=your-gemini-key-here
|
| 158 |
+
# GEMINI_MODEL=gemini-2.0-flash
|
| 159 |
+
|
| 160 |
+
# Use a custom Gemini endpoint (optional)
|
| 161 |
+
# GEMINI_BASE_URL=https://generativelanguage.googleapis.com/v1beta/openai
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
# -----------------------------------------------------------------------------
|
| 165 |
+
# Option 4: GitHub Models
|
| 166 |
+
# -----------------------------------------------------------------------------
|
| 167 |
+
# CLAUDE_CODE_USE_GITHUB=1
|
| 168 |
+
# GITHUB_TOKEN=ghp_your-token-here
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
# -----------------------------------------------------------------------------
|
| 172 |
+
# Option 5: Ollama (local models)
|
| 173 |
+
# -----------------------------------------------------------------------------
|
| 174 |
+
# CLAUDE_CODE_USE_OPENAI=1
|
| 175 |
+
# OPENAI_BASE_URL=http://localhost:11434/v1
|
| 176 |
+
# OPENAI_API_KEY=ollama
|
| 177 |
+
# OPENAI_MODEL=llama3.2
|
| 178 |
+
|
| 179 |
+
# -----------------------------------------------------------------------------
|
| 180 |
+
# Option 6: LM Studio (local models)
|
| 181 |
+
# -----------------------------------------------------------------------------
|
| 182 |
+
# LM Studio exposes an OpenAI-compatible API, so we use the OpenAI provider.
|
| 183 |
+
# Make sure LM Studio is running with the Developer server enabled
|
| 184 |
+
# (Developer tab > toggle server ON).
|
| 185 |
+
#
|
| 186 |
+
# Steps:
|
| 187 |
+
# 1. Download and install LM Studio from https://lmstudio.ai
|
| 188 |
+
# 2. Search for and download a model (e.g. any coding or instruct model)
|
| 189 |
+
# 3. Load the model and start the Developer server
|
| 190 |
+
# 4. Set OPENAI_MODEL to the model ID shown in LM Studio's Developer tab
|
| 191 |
+
#
|
| 192 |
+
# The default server URL is http://localhost:1234 β change the port below
|
| 193 |
+
# if you've configured a different one in LM Studio.
|
| 194 |
+
#
|
| 195 |
+
# OPENAI_API_KEY is optional β LM Studio runs locally and ignores it.
|
| 196 |
+
# Some clients require a non-empty value; if you get auth errors, set it
|
| 197 |
+
# to any dummy value (e.g. "lmstudio").
|
| 198 |
+
#
|
| 199 |
+
# CLAUDE_CODE_USE_OPENAI=1
|
| 200 |
+
# OPENAI_BASE_URL=http://localhost:1234/v1
|
| 201 |
+
# OPENAI_MODEL=your-model-id-here
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
# -----------------------------------------------------------------------------
|
| 205 |
+
# Option 7: AWS Bedrock
|
| 206 |
+
# -----------------------------------------------------------------------------
|
| 207 |
+
|
| 208 |
+
# You may also need AWS CLI credentials configured (run: aws configure)
|
| 209 |
+
# or have AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY set in your
|
| 210 |
+
# environment in addition to the variables below.
|
| 211 |
+
#
|
| 212 |
+
# CLAUDE_CODE_USE_BEDROCK=1
|
| 213 |
+
# AWS_REGION=us-east-1
|
| 214 |
+
# AWS_DEFAULT_REGION=us-east-1
|
| 215 |
+
# AWS_BEARER_TOKEN_BEDROCK=your-bearer-token-here
|
| 216 |
+
# ANTHROPIC_BEDROCK_BASE_URL=https://bedrock-runtime.us-east-1.amazonaws.com
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
# -----------------------------------------------------------------------------
|
| 220 |
+
# Option 8: Google Vertex AI
|
| 221 |
+
# -----------------------------------------------------------------------------
|
| 222 |
+
# CLAUDE_CODE_USE_VERTEX=1
|
| 223 |
+
# ANTHROPIC_VERTEX_PROJECT_ID=your-gcp-project-id
|
| 224 |
+
# CLOUD_ML_REGION=us-east5
|
| 225 |
+
# GOOGLE_CLOUD_PROJECT=your-gcp-project-id
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
# -----------------------------------------------------------------------------
|
| 229 |
+
# Option 9: NVIDIA NIM
|
| 230 |
+
# -----------------------------------------------------------------------------
|
| 231 |
+
# NVIDIA NIM provides hosted inference endpoints for NVIDIA models.
|
| 232 |
+
# Get your API key from https://build.nvidia.com/
|
| 233 |
+
#
|
| 234 |
+
# CLAUDE_CODE_USE_OPENAI=1
|
| 235 |
+
# NVIDIA_API_KEY=nvapi-your-key-here
|
| 236 |
+
# OPENAI_BASE_URL=https://integrate.api.nvidia.com/v1
|
| 237 |
+
# OPENAI_MODEL=nvidia/llama-3.1-nemotron-70b-instruct
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
# -----------------------------------------------------------------------------
|
| 241 |
+
# Option 10: MiniMax
|
| 242 |
+
# -----------------------------------------------------------------------------
|
| 243 |
+
# MiniMax API provides text generation models.
|
| 244 |
+
# Get your API key from https://platform.minimax.io/
|
| 245 |
+
#
|
| 246 |
+
# CLAUDE_CODE_USE_OPENAI=1
|
| 247 |
+
# MINIMAX_API_KEY=your-minimax-key-here
|
| 248 |
+
# OPENAI_BASE_URL=https://api.minimax.io/v1
|
| 249 |
+
# OPENAI_MODEL=MiniMax-M2.5
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
# =============================================================================
|
| 253 |
+
# OPTIONAL TUNING
|
| 254 |
+
# =============================================================================
|
| 255 |
+
|
| 256 |
+
# Max number of API retries on failure (default: 10)
|
| 257 |
+
# CLAUDE_CODE_MAX_RETRIES=10
|
| 258 |
+
|
| 259 |
+
# Enable persistent retry mode for unattended/CI sessions
|
| 260 |
+
# Retries 429/529 indefinitely with smart backoff
|
| 261 |
+
# CLAUDE_CODE_UNATTENDED_RETRY=1
|
| 262 |
+
|
| 263 |
+
# Enable extended key reporting (Kitty keyboard protocol)
|
| 264 |
+
# Useful for iTerm2, WezTerm, Ghostty if modifier keys feel off
|
| 265 |
+
# OPENCLAUDE_ENABLE_EXTENDED_KEYS=1
|
| 266 |
+
|
| 267 |
+
# Disable "Co-authored-by" line in git commits made by OpenClaude
|
| 268 |
+
# OPENCLAUDE_DISABLE_CO_AUTHORED_BY=1
|
| 269 |
+
|
| 270 |
+
# Disable strict tool schema normalization for non-Gemini providers
|
| 271 |
+
# Useful when MCP tools with complex optional params (e.g. list[dict])
|
| 272 |
+
# trigger "Extra required key ... supplied" errors from OpenAI-compatible endpoints
|
| 273 |
+
# OPENCLAUDE_DISABLE_STRICT_TOOLS=1
|
| 274 |
+
|
| 275 |
+
# Disable hidden <system-reminder> messages injected into tool output
|
| 276 |
+
# Suppresses the file-read cyber-risk reminder and the todo/task tool nudges
|
| 277 |
+
# Useful for users who want full transparency over what the model sees
|
| 278 |
+
# OPENCLAUDE_DISABLE_TOOL_REMINDERS=1
|
| 279 |
+
|
| 280 |
+
# Custom timeout for API requests in milliseconds (default: varies)
|
| 281 |
+
# API_TIMEOUT_MS=60000
|
| 282 |
+
|
| 283 |
+
# Enable debug logging
|
| 284 |
+
# CLAUDE_DEBUG=1
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
# =============================================================================
|
| 288 |
+
# WEB SEARCH (OPTIONAL)
|
| 289 |
+
# =============================================================================
|
| 290 |
+
# OpenClaude includes a web search tool. By default it uses DuckDuckGo (free)
|
| 291 |
+
# or the provider's native search (Anthropic firstParty / vertex).
|
| 292 |
+
#
|
| 293 |
+
# Set one API key below to enable a provider. That's it.
|
| 294 |
+
|
| 295 |
+
# ββ Provider API keys β set ONE of these ββββββββββββββββββββββββββββ
|
| 296 |
+
|
| 297 |
+
# Tavily (AI-optimized search, recommended)
|
| 298 |
+
# TAVILY_API_KEY=tvly-your-key-here
|
| 299 |
+
|
| 300 |
+
# Exa (neural/semantic search)
|
| 301 |
+
# EXA_API_KEY=your-exa-key-here
|
| 302 |
+
|
| 303 |
+
# You.com (RAG-ready snippets)
|
| 304 |
+
# YOU_API_KEY=your-you-key-here
|
| 305 |
+
|
| 306 |
+
# Jina (s.jina.ai endpoint)
|
| 307 |
+
# JINA_API_KEY=your-jina-key-here
|
| 308 |
+
|
| 309 |
+
# Bing Web Search
|
| 310 |
+
# BING_API_KEY=your-bing-key-here
|
| 311 |
+
|
| 312 |
+
# Mojeek (privacy-focused)
|
| 313 |
+
# MOJEEK_API_KEY=your-mojeek-key-here
|
| 314 |
+
|
| 315 |
+
# Linkup
|
| 316 |
+
# LINKUP_API_KEY=your-linkup-key-here
|
| 317 |
+
|
| 318 |
+
# Firecrawl (premium, uses @mendable/firecrawl-js)
|
| 319 |
+
# FIRECRAWL_API_KEY=fc-your-key-here
|
| 320 |
+
|
| 321 |
+
# ββ Provider selection mode βββββββββββββββββββββββββββββββββββββββββ
|
| 322 |
+
#
|
| 323 |
+
# WEB_SEARCH_PROVIDER controls fallback behavior:
|
| 324 |
+
#
|
| 325 |
+
# "auto" (default) β try all configured providers, fall through on failure
|
| 326 |
+
# "custom" β custom API only, throw on failure (NOT in auto chain)
|
| 327 |
+
# "firecrawl" β firecrawl only
|
| 328 |
+
# "tavily" β tavily only
|
| 329 |
+
# "exa" β exa only
|
| 330 |
+
# "you" β you.com only
|
| 331 |
+
# "jina" β jina only
|
| 332 |
+
# "bing" β bing only
|
| 333 |
+
# "mojeek" β mojeek only
|
| 334 |
+
# "linkup" β linkup only
|
| 335 |
+
# "ddg" β duckduckgo only
|
| 336 |
+
# "native" β anthropic native / codex only
|
| 337 |
+
#
|
| 338 |
+
# Auto mode priority: firecrawl β tavily β exa β you β jina β bing β mojeek β
|
| 339 |
+
# linkup β ddg
|
| 340 |
+
# Note: "custom" is NOT in the auto chain. To use the custom API provider,
|
| 341 |
+
# you must explicitly set WEB_SEARCH_PROVIDER=custom.
|
| 342 |
+
#
|
| 343 |
+
# WEB_SEARCH_PROVIDER=auto
|
| 344 |
+
|
| 345 |
+
# ββ Built-in custom API presets βββββββββββββββββββββββββββββββββββββ
|
| 346 |
+
#
|
| 347 |
+
# Use with WEB_KEY for the API key:
|
| 348 |
+
# WEB_PROVIDER=searxng|google|brave|serpapi
|
| 349 |
+
# WEB_KEY=your-api-key-here
|
| 350 |
+
|
| 351 |
+
# ββ Custom API endpoint (advanced) ββββββββββββββββββββββββββββββββββ
|
| 352 |
+
#
|
| 353 |
+
# WEB_SEARCH_API β base URL of your search endpoint
|
| 354 |
+
# WEB_QUERY_PARAM β query parameter name (default: "q")
|
| 355 |
+
# WEB_METHOD β GET or POST (default: GET)
|
| 356 |
+
# WEB_PARAMS β extra static query params as JSON: {"lang":"en","count":"10"}
|
| 357 |
+
# WEB_URL_TEMPLATE β URL template with {query} for path embedding
|
| 358 |
+
# WEB_BODY_TEMPLATE β custom POST body with {query} placeholder
|
| 359 |
+
# WEB_AUTH_HEADER β header name for API key (default: "Authorization")
|
| 360 |
+
# WEB_AUTH_SCHEME β prefix before key (default: "Bearer")
|
| 361 |
+
# WEB_HEADERS β extra headers as "Name: value; Name2: value2"
|
| 362 |
+
# WEB_JSON_PATH β dot-path to results array in response
|
| 363 |
+
|
| 364 |
+
# ββ Custom API security guardrails ββββββββββββββββββββββββββββββββββ
|
| 365 |
+
#
|
| 366 |
+
# The custom provider enforces security guardrails by default.
|
| 367 |
+
# Override these only if you understand the risks.
|
| 368 |
+
#
|
| 369 |
+
# WEB_CUSTOM_TIMEOUT_SEC=15 β request timeout in seconds (default 15)
|
| 370 |
+
# WEB_CUSTOM_MAX_BODY_KB=300 β max POST body size in KB (default 300)
|
| 371 |
+
# WEB_CUSTOM_ALLOW_ARBITRARY_HEADERS=false β set "true" to use non-standard headers
|
| 372 |
+
# WEB_CUSTOM_ALLOW_HTTP=false β set "true" to allow http:// URLs
|
| 373 |
+
# WEB_CUSTOM_ALLOW_PRIVATE=false β set "true" to target localhost/private IPs
|
| 374 |
+
# (needed for self-hosted SearXNG)
|
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules/
|
| 2 |
+
dist/
|
| 3 |
+
*.tsbuildinfo
|
| 4 |
+
.env
|
| 5 |
+
.env.*
|
| 6 |
+
!.env.example
|
| 7 |
+
.openclaude-profile.json
|
| 8 |
+
reports/
|
| 9 |
+
GEMINI.md
|
| 10 |
+
CLAUDE.md
|
| 11 |
+
package-lock.json
|
| 12 |
+
/.claude
|
| 13 |
+
coverage/
|
| 14 |
+
agent.log
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
".": "0.6.0"
|
| 3 |
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# OpenClaude on Android (Termux)
|
| 2 |
+
|
| 3 |
+
A complete guide to running OpenClaude on Android using Termux + proot Ubuntu.
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## Prerequisites
|
| 8 |
+
|
| 9 |
+
- Android phone with ~700MB free storage
|
| 10 |
+
- [Termux](https://f-droid.org/en/packages/com.termux/) installed from **F-Droid** (not Play Store)
|
| 11 |
+
- An [OpenRouter](https://openrouter.ai) API key (free, no credit card required)
|
| 12 |
+
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
## Why This Setup?
|
| 16 |
+
|
| 17 |
+
OpenClaude requires [Bun](https://bun.sh) to build, and Bun does not support Android natively. The workaround is running a real Ubuntu environment inside Termux via `proot-distro`, where Bun's Linux binary works correctly.
|
| 18 |
+
|
| 19 |
+
---
|
| 20 |
+
|
| 21 |
+
## Installation
|
| 22 |
+
|
| 23 |
+
### Step 1 β Update Termux
|
| 24 |
+
|
| 25 |
+
```bash
|
| 26 |
+
pkg update && pkg upgrade
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
Press `N` or Enter for any config file conflict prompts.
|
| 30 |
+
|
| 31 |
+
### Step 2 β Install dependencies
|
| 32 |
+
|
| 33 |
+
```bash
|
| 34 |
+
pkg install nodejs-lts git proot-distro
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
Verify Node.js:
|
| 38 |
+
```bash
|
| 39 |
+
node --version # should be v20+
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
### Step 3 β Clone OpenClaude
|
| 43 |
+
|
| 44 |
+
```bash
|
| 45 |
+
git clone https://github.com/Gitlawb/openclaude.git
|
| 46 |
+
cd openclaude
|
| 47 |
+
npm install
|
| 48 |
+
npm link
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
### Step 4 β Install Ubuntu via proot
|
| 52 |
+
|
| 53 |
+
```bash
|
| 54 |
+
proot-distro install ubuntu
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
This downloads ~200β400MB. Wait for it to complete.
|
| 58 |
+
|
| 59 |
+
### Step 5 β Install Bun inside Ubuntu
|
| 60 |
+
|
| 61 |
+
```bash
|
| 62 |
+
proot-distro login ubuntu
|
| 63 |
+
curl -fsSL https://bun.sh/install | bash
|
| 64 |
+
source ~/.bashrc
|
| 65 |
+
bun --version # should show 1.3.11+
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
### Step 6 β Build OpenClaude
|
| 69 |
+
|
| 70 |
+
```bash
|
| 71 |
+
cd /data/data/com.termux/files/home/openclaude
|
| 72 |
+
bun run build
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
You should see:
|
| 76 |
+
```
|
| 77 |
+
β Built openclaude v0.1.6 β dist/cli.mjs
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
### Step 7 β Save env vars permanently
|
| 81 |
+
|
| 82 |
+
Still inside Ubuntu, add your OpenRouter config to `.bashrc`:
|
| 83 |
+
|
| 84 |
+
```bash
|
| 85 |
+
echo 'export CLAUDE_CODE_USE_OPENAI=1' >> ~/.bashrc
|
| 86 |
+
echo 'export OPENAI_API_KEY=your_openrouter_key_here' >> ~/.bashrc
|
| 87 |
+
echo 'export OPENAI_BASE_URL=https://openrouter.ai/api/v1' >> ~/.bashrc
|
| 88 |
+
echo 'export OPENAI_MODEL=qwen/qwen3.6-plus-preview:free' >> ~/.bashrc
|
| 89 |
+
source ~/.bashrc
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
Replace `your_openrouter_key_here` with your actual key from [openrouter.ai/keys](https://openrouter.ai/keys).
|
| 93 |
+
|
| 94 |
+
### Step 8 β Run OpenClaude
|
| 95 |
+
|
| 96 |
+
```bash
|
| 97 |
+
node dist/cli.mjs
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
Select **3** (3rd-party platform) at the login screen. Your env vars will be detected automatically.
|
| 101 |
+
|
| 102 |
+
---
|
| 103 |
+
|
| 104 |
+
## Restarting After Closing Termux
|
| 105 |
+
|
| 106 |
+
Every time you reopen Termux after killing it, run:
|
| 107 |
+
|
| 108 |
+
```bash
|
| 109 |
+
proot-distro login ubuntu
|
| 110 |
+
cd /data/data/com.termux/files/home/openclaude
|
| 111 |
+
node dist/cli.mjs
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
---
|
| 115 |
+
|
| 116 |
+
## Recommended Free Model
|
| 117 |
+
|
| 118 |
+
**`qwen/qwen3.6-plus-preview:free`** β Best free model on OpenRouter as of April 2026.
|
| 119 |
+
|
| 120 |
+
- 1M token context window
|
| 121 |
+
- Beats Claude 4.5 Opus on Terminal-Bench 2.0 agentic coding (61.6 vs 59.3)
|
| 122 |
+
- Built-in chain-of-thought reasoning
|
| 123 |
+
- Native tool use and function calling
|
| 124 |
+
- $0/M tokens (preview period)
|
| 125 |
+
|
| 126 |
+
> β οΈ Free status may change when the preview period ends. Check [openrouter.ai](https://openrouter.ai/qwen/qwen3.6-plus-preview:free) for current pricing.
|
| 127 |
+
|
| 128 |
+
---
|
| 129 |
+
|
| 130 |
+
## Alternative Free Models (OpenRouter)
|
| 131 |
+
|
| 132 |
+
| Model ID | Context | Notes |
|
| 133 |
+
|---|---|---|
|
| 134 |
+
| `qwen/qwen3-coder:free` | 262K | Best for pure coding tasks |
|
| 135 |
+
| `openai/gpt-oss-120b:free` | 131K | OpenAI open model, strong tool calling |
|
| 136 |
+
| `nvidia/nemotron-3-super-120b-a12b:free` | 262K | Hybrid MoE, good general use |
|
| 137 |
+
| `meta-llama/llama-3.3-70b-instruct:free` | 66K | Reliable, widely tested |
|
| 138 |
+
|
| 139 |
+
Switch models anytime:
|
| 140 |
+
```bash
|
| 141 |
+
export OPENAI_MODEL=qwen/qwen3-coder:free
|
| 142 |
+
node dist/cli.mjs
|
| 143 |
+
```
|
| 144 |
+
|
| 145 |
+
---
|
| 146 |
+
|
| 147 |
+
## Why Not Groq or Cerebras?
|
| 148 |
+
|
| 149 |
+
Both were tested and fail due to OpenClaude's large system prompt (~50K tokens):
|
| 150 |
+
|
| 151 |
+
- **Groq free tier**: TPM limits too low (6Kβ12K tokens/min)
|
| 152 |
+
- **Cerebras free tier**: TPM limits exceeded, even on `llama3.1-8b`
|
| 153 |
+
|
| 154 |
+
OpenRouter free models have no TPM restrictions β only 20 req/min and 200 req/day.
|
| 155 |
+
|
| 156 |
+
---
|
| 157 |
+
|
| 158 |
+
## Tips
|
| 159 |
+
|
| 160 |
+
- **Don't swipe Termux away** from recent apps mid-session β use the home button to minimize instead.
|
| 161 |
+
- The Ubuntu environment persists between Termux sessions; your build and config are saved.
|
| 162 |
+
- Run `bun run build` again only if you pull updates to the OpenClaude repo.
|
|
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Changelog
|
| 2 |
+
|
| 3 |
+
## [0.6.0](https://github.com/Gitlawb/openclaude/compare/v0.5.2...v0.6.0) (2026-04-22)
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
### Features
|
| 7 |
+
|
| 8 |
+
* add model caching and benchmarking utilities ([#671](https://github.com/Gitlawb/openclaude/issues/671)) ([2b15e16](https://github.com/Gitlawb/openclaude/commit/2b15e16421f793f954a92c53933a07094544b29d))
|
| 9 |
+
* add thinking token extraction ([#798](https://github.com/Gitlawb/openclaude/issues/798)) ([268c039](https://github.com/Gitlawb/openclaude/commit/268c0398e4bf1ab898069c61500a2b3c226a0322))
|
| 10 |
+
* **api:** compress old tool_result content for small-context providers ([#801](https://github.com/Gitlawb/openclaude/issues/801)) ([a6a3de5](https://github.com/Gitlawb/openclaude/commit/a6a3de5ac155fe9d00befbfcab98d439314effd8))
|
| 11 |
+
* **api:** improve local provider reliability with readiness and self-healing ([#738](https://github.com/Gitlawb/openclaude/issues/738)) ([4cb963e](https://github.com/Gitlawb/openclaude/commit/4cb963e660dbd6ee438c04042700db05a9d32c59))
|
| 12 |
+
* **api:** smart model routing primitive (cheap-for-simple, strong-for-hard) ([#785](https://github.com/Gitlawb/openclaude/issues/785)) ([e908864](https://github.com/Gitlawb/openclaude/commit/e908864da7e7c987a98053ac5d18d702e192db2b))
|
| 13 |
+
* enable 15 additional feature flags in open build ([#667](https://github.com/Gitlawb/openclaude/issues/667)) ([6a62e3f](https://github.com/Gitlawb/openclaude/commit/6a62e3ff76ba9ba446b8e20cf2bb139ee76a9387))
|
| 14 |
+
* native Anthropic API mode for Claude models on GitHub Copilot ([#579](https://github.com/Gitlawb/openclaude/issues/579)) ([fdef4a1](https://github.com/Gitlawb/openclaude/commit/fdef4a1b4ce218ded4937ca83b30acce7c726472))
|
| 15 |
+
* **provider:** expose Atomic Chat in /provider picker with autodetect ([#810](https://github.com/Gitlawb/openclaude/issues/810)) ([ee19159](https://github.com/Gitlawb/openclaude/commit/ee19159c17b3de3b4a8b4a4541a6569f4261d54e))
|
| 16 |
+
* **provider:** zero-config autodetection primitive ([#784](https://github.com/Gitlawb/openclaude/issues/784)) ([a5bfcbb](https://github.com/Gitlawb/openclaude/commit/a5bfcbbadf8e9a1fd42f3e103d295524b8da64b0))
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
### Bug Fixes
|
| 20 |
+
|
| 21 |
+
* **api:** ensure strict role sequence and filter empty assistant messages after interruption ([#745](https://github.com/Gitlawb/openclaude/issues/745) regression) ([#794](https://github.com/Gitlawb/openclaude/issues/794)) ([06e7684](https://github.com/Gitlawb/openclaude/commit/06e7684eb56df8e694ac784575e163641931c44c))
|
| 22 |
+
* Collapse all-text arrays to string for DeepSeek compatibility ([#806](https://github.com/Gitlawb/openclaude/issues/806)) ([761924d](https://github.com/Gitlawb/openclaude/commit/761924daa7e225fe8acf41651408c7cae639a511))
|
| 23 |
+
* **model:** codex/nvidia-nim/minimax now read OPENAI_MODEL env ([#815](https://github.com/Gitlawb/openclaude/issues/815)) ([4581208](https://github.com/Gitlawb/openclaude/commit/458120889f6ce54cc9f0b287461d5e38eae48a20))
|
| 24 |
+
* **provider:** saved profile ignored when stale CLAUDE_CODE_USE_* in shell ([#807](https://github.com/Gitlawb/openclaude/issues/807)) ([13de4e8](https://github.com/Gitlawb/openclaude/commit/13de4e85df7f5fadc8cd15a76076374dc112360b))
|
| 25 |
+
* rename .claude.json to .openclaude.json with legacy fallback ([#582](https://github.com/Gitlawb/openclaude/issues/582)) ([4d4fb28](https://github.com/Gitlawb/openclaude/commit/4d4fb2880e4d0e3a62d8715e1ec13d932e736279))
|
| 26 |
+
* replace discontinued gemini-2.5-pro-preview-03-25 with stable gemini-2.5-pro ([#802](https://github.com/Gitlawb/openclaude/issues/802)) ([64582c1](https://github.com/Gitlawb/openclaude/commit/64582c119d5d0278195271379da4a68d59a89c1f)), closes [#398](https://github.com/Gitlawb/openclaude/issues/398)
|
| 27 |
+
* **security:** harden project settings trust boundary + MCP sanitization ([#789](https://github.com/Gitlawb/openclaude/issues/789)) ([ae3b723](https://github.com/Gitlawb/openclaude/commit/ae3b723f3b297b49925cada4728f3174aee8bf12))
|
| 28 |
+
* **test:** autoCompact floor assertion is flag-sensitive ([#816](https://github.com/Gitlawb/openclaude/issues/816)) ([c13842e](https://github.com/Gitlawb/openclaude/commit/c13842e91c7227246520955de6ae0636b30def9a))
|
| 29 |
+
* **ui:** prevent provider manager lag by deferring sync I/O ([#803](https://github.com/Gitlawb/openclaude/issues/803)) ([85eab27](https://github.com/Gitlawb/openclaude/commit/85eab2751e7d351bb0ed6a3fe0e15461d241c9cb))
|
| 30 |
+
|
| 31 |
+
## [0.5.2](https://github.com/Gitlawb/openclaude/compare/v0.5.1...v0.5.2) (2026-04-20)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
### Bug Fixes
|
| 35 |
+
|
| 36 |
+
* **api:** replace phrase-based reasoning sanitizer with tag-based filter ([#779](https://github.com/Gitlawb/openclaude/issues/779)) ([336ddcc](https://github.com/Gitlawb/openclaude/commit/336ddcc50d59d79ebff50993f2673652aecb0d7d))
|
| 37 |
+
|
| 38 |
+
## [0.5.1](https://github.com/Gitlawb/openclaude/compare/v0.5.0...v0.5.1) (2026-04-20)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
### Bug Fixes
|
| 42 |
+
|
| 43 |
+
* enforce Bash path constraints after sandbox allow ([#777](https://github.com/Gitlawb/openclaude/issues/777)) ([7002cb3](https://github.com/Gitlawb/openclaude/commit/7002cb302b78ea2a19da3f26226de24e2903fa1d))
|
| 44 |
+
* enforce MCP OAuth callback state before errors ([#775](https://github.com/Gitlawb/openclaude/issues/775)) ([739b8d1](https://github.com/Gitlawb/openclaude/commit/739b8d1f40fde0e401a5cbd2b9a55d88bd5124ad))
|
| 45 |
+
* require trusted approval for sandbox override ([#778](https://github.com/Gitlawb/openclaude/issues/778)) ([aab4890](https://github.com/Gitlawb/openclaude/commit/aab489055c53dd64369414116fe93226d2656273))
|
| 46 |
+
|
| 47 |
+
## [0.5.0](https://github.com/Gitlawb/openclaude/compare/v0.4.0...v0.5.0) (2026-04-20)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
### Features
|
| 51 |
+
|
| 52 |
+
* add OPENCLAUDE_DISABLE_STRICT_TOOLS env var to opt out of strict MCP tool schema normalization ([#770](https://github.com/Gitlawb/openclaude/issues/770)) ([e6e8d9a](https://github.com/Gitlawb/openclaude/commit/e6e8d9a24897e4c9ef08b72df20fabbf8ef27f38))
|
| 53 |
+
* mask provider api key input ([#772](https://github.com/Gitlawb/openclaude/issues/772)) ([13e9f22](https://github.com/Gitlawb/openclaude/commit/13e9f22a83a2b0f85f557b1e12c9442ba61241e4))
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
### Bug Fixes
|
| 57 |
+
|
| 58 |
+
* allow provider recovery during startup ([#765](https://github.com/Gitlawb/openclaude/issues/765)) ([f828171](https://github.com/Gitlawb/openclaude/commit/f828171ef1ab94e2acf73a28a292799e4e26cc0d))
|
| 59 |
+
* **api:** drop orphan tool results to satisfy strict role sequence ([#745](https://github.com/Gitlawb/openclaude/issues/745)) ([b786b76](https://github.com/Gitlawb/openclaude/commit/b786b765f01f392652eaf28ed3579a96b7260a53))
|
| 60 |
+
* **help:** prevent /help tab crash from undefined descriptions ([#732](https://github.com/Gitlawb/openclaude/issues/732)) ([3d1979f](https://github.com/Gitlawb/openclaude/commit/3d1979ff066db32415e0c8321af916d81f5f2621))
|
| 61 |
+
* **mcp:** sync required array with properties in tool schemas ([#754](https://github.com/Gitlawb/openclaude/issues/754)) ([002a8f1](https://github.com/Gitlawb/openclaude/commit/002a8f1f6de2fcfc917165d828501d3047bad61f))
|
| 62 |
+
* remove cached mcpClient in diagnostic tracking to prevent stale references ([#727](https://github.com/Gitlawb/openclaude/issues/727)) ([2c98be7](https://github.com/Gitlawb/openclaude/commit/2c98be700274a4241963b5f43530bf3bd8f8963f))
|
| 63 |
+
* use raw context window for auto-compact percentage display ([#748](https://github.com/Gitlawb/openclaude/issues/748)) ([55c5f26](https://github.com/Gitlawb/openclaude/commit/55c5f262a9a5a8be0aa9ae8dc6c7dafc465eb2c6))
|
| 64 |
+
|
| 65 |
+
## [0.4.0](https://github.com/Gitlawb/openclaude/compare/v0.3.0...v0.4.0) (2026-04-17)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
### Features
|
| 69 |
+
|
| 70 |
+
* add Alibaba Coding Plan (DashScope) provider support ([#509](https://github.com/Gitlawb/openclaude/issues/509)) ([43ac6db](https://github.com/Gitlawb/openclaude/commit/43ac6dba75537282da1e2ad8f855082bc4e25f1e))
|
| 71 |
+
* add NVIDIA NIM and MiniMax provider support ([#552](https://github.com/Gitlawb/openclaude/issues/552)) ([51191d6](https://github.com/Gitlawb/openclaude/commit/51191d61326e1f8319d70b3a3c0d9229e185a564))
|
| 72 |
+
* add ripgrep to Dockerfile for faster file searching ([#688](https://github.com/Gitlawb/openclaude/issues/688)) ([12dd375](https://github.com/Gitlawb/openclaude/commit/12dd3755c619cc27af3b151ae8fdb9d425a7b9a2))
|
| 73 |
+
* **api:** classify openai-compatible provider failures ([#708](https://github.com/Gitlawb/openclaude/issues/708)) ([80a00ac](https://github.com/Gitlawb/openclaude/commit/80a00acc2c6dc4657a78de7366f7a9ebc920bfbb))
|
| 74 |
+
* **vscode:** add full chat interface to OpenClaude extension ([#608](https://github.com/Gitlawb/openclaude/issues/608)) ([fbcd928](https://github.com/Gitlawb/openclaude/commit/fbcd928f7f8511da795aea3ad318bddf0ab9a1a7))
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
### Bug Fixes
|
| 78 |
+
|
| 79 |
+
* focus "Done" option after completing provider manager actions ([#718](https://github.com/Gitlawb/openclaude/issues/718)) ([d6f5130](https://github.com/Gitlawb/openclaude/commit/d6f5130c204d8ffe582212466768706cd7fd6774))
|
| 80 |
+
* **models:** prevent /models crash from non-string saved model values ([#691](https://github.com/Gitlawb/openclaude/issues/691)) ([6b2121d](https://github.com/Gitlawb/openclaude/commit/6b2121da12189fa7ce1f33394d18abd24cf8a01b))
|
| 81 |
+
* prevent crash in commands tab when description is undefined ([#730](https://github.com/Gitlawb/openclaude/issues/730)) ([eed77e6](https://github.com/Gitlawb/openclaude/commit/eed77e6579866a98384dcc948a0ad6406614ede3))
|
| 82 |
+
* strip comments before scanning for missing imports ([#676](https://github.com/Gitlawb/openclaude/issues/676)) ([a00b792](https://github.com/Gitlawb/openclaude/commit/a00b7928de9662ffb7ef6abd8cd040afe6f4f122))
|
| 83 |
+
* **ui:** show correct endpoint URL in intro screen for custom Anthropic endpoints ([#735](https://github.com/Gitlawb/openclaude/issues/735)) ([3424663](https://github.com/Gitlawb/openclaude/commit/34246635fb9a09499047a52e7f96ca9b36c8a85a))
|
| 84 |
+
|
| 85 |
+
## [0.3.0](https://github.com/Gitlawb/openclaude/compare/v0.2.3...v0.3.0) (2026-04-14)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
### Features
|
| 89 |
+
|
| 90 |
+
* activate coordinator mode in open build ([#647](https://github.com/Gitlawb/openclaude/issues/647)) ([99a1714](https://github.com/Gitlawb/openclaude/commit/99a17144ee285b892a0801acb6abcc9af68879af))
|
| 91 |
+
* activate local-only team memory in open build ([#648](https://github.com/Gitlawb/openclaude/issues/648)) ([24d485f](https://github.com/Gitlawb/openclaude/commit/24d485f42f5b1405d2fab13f2f497d5edd3b5300))
|
| 92 |
+
* activate message actions in open build ([#632](https://github.com/Gitlawb/openclaude/issues/632)) ([252808b](https://github.com/Gitlawb/openclaude/commit/252808bbd0a12a6ccf97e2cb09752a0212ea3acd))
|
| 93 |
+
* add allowBypassPermissionsMode setting ([#658](https://github.com/Gitlawb/openclaude/issues/658)) ([31be66d](https://github.com/Gitlawb/openclaude/commit/31be66d7645ea3473334c9ce89ea1a5095b8df6e))
|
| 94 |
+
* add Docker image build and push to GHCR on release ([#656](https://github.com/Gitlawb/openclaude/issues/656)) ([658d076](https://github.com/Gitlawb/openclaude/commit/658d076909e14eb0459bcb98aee9aa0472118265))
|
| 95 |
+
* implement /loop command with fixed and dynamic scheduling ([#621](https://github.com/Gitlawb/openclaude/issues/621)) ([64298a6](https://github.com/Gitlawb/openclaude/commit/64298a663f1391b16aa1f5a49e8a877e1d3742f2))
|
| 96 |
+
* implement Monitor tool for streaming shell output ([#649](https://github.com/Gitlawb/openclaude/issues/649)) ([b818dd5](https://github.com/Gitlawb/openclaude/commit/b818dd5958f4e8428566ce25a1a6be5fd4fe66f8))
|
| 97 |
+
* local feature flag overrides via ~/.claude/feature-flags.json ([#639](https://github.com/Gitlawb/openclaude/issues/639)) ([0e48884](https://github.com/Gitlawb/openclaude/commit/0e48884f56c6c008f047a7926d3b2cb924170625))
|
| 98 |
+
* open useful USER_TYPE-gated features to all users ([#644](https://github.com/Gitlawb/openclaude/issues/644)) ([c1beea9](https://github.com/Gitlawb/openclaude/commit/c1beea98676a413c54152a45a6b9fbe7fb9ed028))
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
### Bug Fixes
|
| 102 |
+
|
| 103 |
+
* bump axios 1.14.0 β 1.15.0 (Dependabot [#4](https://github.com/Gitlawb/openclaude/issues/4), [#5](https://github.com/Gitlawb/openclaude/issues/5)) ([#670](https://github.com/Gitlawb/openclaude/issues/670)) ([a07e5ef](https://github.com/Gitlawb/openclaude/commit/a07e5ef990a5ed01a72e83fdbd1fcab36f515a08))
|
| 104 |
+
* extend provider guard to protect anthropic profiles from cross-terminal override ([#641](https://github.com/Gitlawb/openclaude/issues/641)) ([03e0b06](https://github.com/Gitlawb/openclaude/commit/03e0b06e0784e4ea46945b3950840b10b6e3ca49))
|
| 105 |
+
* improve fetch diagnostics for bootstrap and session requests ([#646](https://github.com/Gitlawb/openclaude/issues/646)) ([df2b9f2](https://github.com/Gitlawb/openclaude/commit/df2b9f2b7b4c661ee3d9ed5dc58b3064de0599d1))
|
| 106 |
+
* **openai-shim:** preserve tool result images and local token caps ([#659](https://github.com/Gitlawb/openclaude/issues/659)) ([30c866d](https://github.com/Gitlawb/openclaude/commit/30c866d31ad8538496460667d86ed5efbd4a8547))
|
| 107 |
+
* replace broken bun:bundle shim with source pre-processing ([#657](https://github.com/Gitlawb/openclaude/issues/657)) ([adbe391](https://github.com/Gitlawb/openclaude/commit/adbe391e63721918b5d147f4f845111c1a3143db))
|
| 108 |
+
* resolve 12 bugs across API, MCP, agent tools, web search, and context overflow ([#674](https://github.com/Gitlawb/openclaude/issues/674)) ([25ce2ca](https://github.com/Gitlawb/openclaude/commit/25ce2ca7bff8937b0b79ad7f85c6dc1c68432069))
|
| 109 |
+
* route OpenAI Codex shortcuts to correct endpoint ([#566](https://github.com/Gitlawb/openclaude/issues/566)) ([7c8bdcc](https://github.com/Gitlawb/openclaude/commit/7c8bdcc3e2ac1ecb98286c705c85671044be3d6b))
|
| 110 |
+
|
| 111 |
+
## [0.2.3](https://github.com/Gitlawb/openclaude/compare/v0.2.2...v0.2.3) (2026-04-12)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
### Bug Fixes
|
| 115 |
+
|
| 116 |
+
* prevent infinite auto-compact loop for unknown 3P models ([#635](https://github.com/Gitlawb/openclaude/issues/635)) ([#636](https://github.com/Gitlawb/openclaude/issues/636)) ([aeaa658](https://github.com/Gitlawb/openclaude/commit/aeaa658f776fb8df95721e8b8962385f8b00f66a))
|
| 117 |
+
|
| 118 |
+
## [0.2.2](https://github.com/Gitlawb/openclaude/compare/v0.2.1...v0.2.2) (2026-04-12)
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
### Bug Fixes
|
| 122 |
+
|
| 123 |
+
* **read/edit:** make compact line prefix unambiguous for tab-indented files ([#613](https://github.com/Gitlawb/openclaude/issues/613)) ([08cc6f3](https://github.com/Gitlawb/openclaude/commit/08cc6f328711cd93ce9fa53351266c29a0b0a341))
|
| 124 |
+
|
| 125 |
+
## [0.2.1](https://github.com/Gitlawb/openclaude/compare/v0.2.0...v0.2.1) (2026-04-12)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
### Bug Fixes
|
| 129 |
+
|
| 130 |
+
* **provider:** add recovery guidance for missing OpenAI API key ([#616](https://github.com/Gitlawb/openclaude/issues/616)) ([9419e8a](https://github.com/Gitlawb/openclaude/commit/9419e8a4a21b3771d9ddb10f7072e0a8c5b5b631))
|
| 131 |
+
|
| 132 |
+
## [0.2.0](https://github.com/Gitlawb/openclaude/compare/v0.1.8...v0.2.0) (2026-04-12)
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
### Features
|
| 136 |
+
|
| 137 |
+
* add /cache-probe diagnostic command ([#580](https://github.com/Gitlawb/openclaude/issues/580)) ([9ccaa7a](https://github.com/Gitlawb/openclaude/commit/9ccaa7a6759b6991f4a566b4118c06e68a2398fe)), closes [#515](https://github.com/Gitlawb/openclaude/issues/515)
|
| 138 |
+
* add auto-fix service β auto-lint and test after AI file edits ([#508](https://github.com/Gitlawb/openclaude/issues/508)) ([c385047](https://github.com/Gitlawb/openclaude/commit/c385047abba4366866f4c87bfb5e0b0bd4dcbb9d))
|
| 139 |
+
* Add Gemini support with thought_signature fix ([#404](https://github.com/Gitlawb/openclaude/issues/404)) ([5012c16](https://github.com/Gitlawb/openclaude/commit/5012c160c9a2dff9418e7ee19dc9a4d29ef2b024))
|
| 140 |
+
* add headless gRPC server for external agent integration ([#278](https://github.com/Gitlawb/openclaude/issues/278)) ([26eef92](https://github.com/Gitlawb/openclaude/commit/26eef92fe72e9c3958d61435b8d3571e12bf2b74))
|
| 141 |
+
* add wiki mvp commands ([#532](https://github.com/Gitlawb/openclaude/issues/532)) ([c328fdf](https://github.com/Gitlawb/openclaude/commit/c328fdf9e2fe59ad101b049301298ce9ff24caca))
|
| 142 |
+
* GitHub provider lifecycle and onboarding hardening ([#351](https://github.com/Gitlawb/openclaude/issues/351)) ([ff7d499](https://github.com/Gitlawb/openclaude/commit/ff7d49990de515825ddbe4099f3a39b944b61370))
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
### Bug Fixes
|
| 146 |
+
|
| 147 |
+
* add File polyfill for Node < 20 to prevent startup deadlock with proxy ([#442](https://github.com/Gitlawb/openclaude/issues/442)) ([85aa8b0](https://github.com/Gitlawb/openclaude/commit/85aa8b0985c8f3cb8801efa5141114a0ab0f6a83))
|
| 148 |
+
* add GitHub Copilot model context windows and output limits ([#576](https://github.com/Gitlawb/openclaude/issues/576)) ([a7f5982](https://github.com/Gitlawb/openclaude/commit/a7f5982f6438ab0ddc3f0daae31ea68ac7ac206c)), closes [#515](https://github.com/Gitlawb/openclaude/issues/515)
|
| 149 |
+
* add LiteLLM-style aliases for GitHub Copilot context windows ([#606](https://github.com/Gitlawb/openclaude/issues/606)) ([2e0e14d](https://github.com/Gitlawb/openclaude/commit/2e0e14d71313e0e501efaa9e55c6c56f2742fb10))
|
| 150 |
+
* add store:false to Chat Completions and /responses fallback ([#578](https://github.com/Gitlawb/openclaude/issues/578)) ([8aaa4f2](https://github.com/Gitlawb/openclaude/commit/8aaa4f22ac5b942d82aa9cad54af30d56034515a))
|
| 151 |
+
* address code scanning alerts ([#434](https://github.com/Gitlawb/openclaude/issues/434)) ([e365cb4](https://github.com/Gitlawb/openclaude/commit/e365cb4010becabacd7cbccb4c3e59ea23a41e90))
|
| 152 |
+
* avoid sync github credential reads in provider manager ([#428](https://github.com/Gitlawb/openclaude/issues/428)) ([aff2bd8](https://github.com/Gitlawb/openclaude/commit/aff2bd87e4f2821992f74fb95481c505d0ba5d5d))
|
| 153 |
+
* convert dragged file paths to [@mentions](https://github.com/mentions) for attachment ([#382](https://github.com/Gitlawb/openclaude/issues/382)) ([112df59](https://github.com/Gitlawb/openclaude/commit/112df5911791ea71ee9efbb98ea59c5ded1ea161))
|
| 154 |
+
* custom web search β WEB_URL_TEMPLATE not recognized, timeout too short, silent native fallback ([#537](https://github.com/Gitlawb/openclaude/issues/537)) ([32fbd0c](https://github.com/Gitlawb/openclaude/commit/32fbd0c7b4168b32dcb13a5b69342e2727269201))
|
| 155 |
+
* defer startup checks and suppress recommendation dialogs during startup window (issue [#363](https://github.com/Gitlawb/openclaude/issues/363)) ([#504](https://github.com/Gitlawb/openclaude/issues/504)) ([2caf2fd](https://github.com/Gitlawb/openclaude/commit/2caf2fd982af1ec845c50152ad9d28d1a597f82f))
|
| 156 |
+
* display selected model in startup screen instead of hardcoded sonnet 4.6 ([#587](https://github.com/Gitlawb/openclaude/issues/587)) ([b126e38](https://github.com/Gitlawb/openclaude/commit/b126e38b1affddd2de83fcc3ba26f2e44b42a509))
|
| 157 |
+
* handle missing skill parameter in SkillTool ([#485](https://github.com/Gitlawb/openclaude/issues/485)) ([f9ce81b](https://github.com/Gitlawb/openclaude/commit/f9ce81bfb384e909353813fb6f6760cadd508ae7))
|
| 158 |
+
* include MCP tool results in microcompact to reduce token waste ([#348](https://github.com/Gitlawb/openclaude/issues/348)) ([52d33a8](https://github.com/Gitlawb/openclaude/commit/52d33a87a047b943aedaaaf772cd48636c263509))
|
| 159 |
+
* **ink:** restore host prop updates in React 19 reconciler ([#589](https://github.com/Gitlawb/openclaude/issues/589)) ([6e94dd9](https://github.com/Gitlawb/openclaude/commit/6e94dd913688b2d6433a9abe62a245c5f031b776))
|
| 160 |
+
* let saved provider profiles win on restart ([#513](https://github.com/Gitlawb/openclaude/issues/513)) ([cb8f8b7](https://github.com/Gitlawb/openclaude/commit/cb8f8b7ac2e3e74516ee219a3a48156db7c6ed78))
|
| 161 |
+
* normalize malformed Bash tool arguments from OpenAI-compatible providers ([#385](https://github.com/Gitlawb/openclaude/issues/385)) ([b4bd95b](https://github.com/Gitlawb/openclaude/commit/b4bd95b47715c9896240d708c106777507fd26ec))
|
| 162 |
+
* preserve only originally-required properties in strict tool schemas ([#471](https://github.com/Gitlawb/openclaude/issues/471)) ([ccaa193](https://github.com/Gitlawb/openclaude/commit/ccaa193eec5761f0972ffb58eb3189a81a9244b0))
|
| 163 |
+
* preserve unicode in Windows clipboard fallback ([#388](https://github.com/Gitlawb/openclaude/issues/388)) ([c193497](https://github.com/Gitlawb/openclaude/commit/c1934974aaf64db460cc850a044bd13cc744cce7))
|
| 164 |
+
* rebrand prompt identity to openclaude ([#496](https://github.com/Gitlawb/openclaude/issues/496)) ([598651f](https://github.com/Gitlawb/openclaude/commit/598651f42389ce76311ec00e8a9c701c939ead27))
|
| 165 |
+
* replace isDeepStrictEqual with navigation-aware options comparison ([#507](https://github.com/Gitlawb/openclaude/issues/507)) ([537c469](https://github.com/Gitlawb/openclaude/commit/537c469c3a2f7cb0eed05fa2f54dca57b6bc273f)), closes [#472](https://github.com/Gitlawb/openclaude/issues/472)
|
| 166 |
+
* report cache reads in streaming and correct cost calculation ([#577](https://github.com/Gitlawb/openclaude/issues/577)) ([f4ac709](https://github.com/Gitlawb/openclaude/commit/f4ac709fa6eda732bf45204fcab625ba6c5674b9))
|
| 167 |
+
* restore default context window for unknown 3p models ([#494](https://github.com/Gitlawb/openclaude/issues/494)) ([69ea1f1](https://github.com/Gitlawb/openclaude/commit/69ea1f1e4a99e9436215d8cb391a116a64442b94))
|
| 168 |
+
* restore Grep and Glob reliability on OpenAI paths ([#461](https://github.com/Gitlawb/openclaude/issues/461)) ([600c01f](https://github.com/Gitlawb/openclaude/commit/600c01faf761a080a2c7dede872ddbe05a132f23))
|
| 169 |
+
* restore Ollama auto-detect in first-run setup ([#561](https://github.com/Gitlawb/openclaude/issues/561)) ([68c2968](https://github.com/Gitlawb/openclaude/commit/68c296833dcef54ce44cb18b24357230b5204dbc))
|
| 170 |
+
* scrub canonical Anthropic headers from 3P shim requests ([#499](https://github.com/Gitlawb/openclaude/issues/499)) ([07621a6](https://github.com/Gitlawb/openclaude/commit/07621a6f8d0918170281869a47b5dbff90e71594))
|
| 171 |
+
* strip Anthropic params from 3P resume paths ([#479](https://github.com/Gitlawb/openclaude/issues/479)) ([4975cfc](https://github.com/Gitlawb/openclaude/commit/4975cfc2e0ddbe34aa4e8e3f52ee5eba07fbe465))
|
| 172 |
+
* suppress startup dialogs when input is buffered ([#423](https://github.com/Gitlawb/openclaude/issues/423)) ([8ece290](https://github.com/Gitlawb/openclaude/commit/8ece2900872dadd157e798ef501ddf126dac66c4))
|
| 173 |
+
* **tui:** restore prompt rendering on startup ([#498](https://github.com/Gitlawb/openclaude/issues/498)) ([e30ad17](https://github.com/Gitlawb/openclaude/commit/e30ad17ae0056787273be2caafd6cf5340b6ab57))
|
| 174 |
+
* update theme preview on focus change ([#562](https://github.com/Gitlawb/openclaude/issues/562)) ([6924718](https://github.com/Gitlawb/openclaude/commit/692471850fc789ee0797190089272407f9a4d953))
|
| 175 |
+
* **web-search:** close SSRF bypasses in custom provider hostname guard ([#610](https://github.com/Gitlawb/openclaude/issues/610)) ([a02c441](https://github.com/Gitlawb/openclaude/commit/a02c44143b257fbee7f38f1b93873cc0ea68a1f9))
|
| 176 |
+
* WebSearch providers + MCPTool bugs ([#593](https://github.com/Gitlawb/openclaude/issues/593)) ([91e4cfb](https://github.com/Gitlawb/openclaude/commit/91e4cfb15b62c04615834fd3c417fe38b4feb914))
|
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Contributor Covenant Code of Conduct
|
| 2 |
+
|
| 3 |
+
## Our Pledge
|
| 4 |
+
|
| 5 |
+
We as members, contributors, and maintainers pledge to make participation in
|
| 6 |
+
our community a harassment-free experience for everyone, regardless of age,
|
| 7 |
+
body size, visible or invisible disability, ethnicity, sex characteristics,
|
| 8 |
+
gender identity and expression, level of experience, education, socio-economic
|
| 9 |
+
status, nationality, personal appearance, race, religion, or sexual identity
|
| 10 |
+
and orientation.
|
| 11 |
+
|
| 12 |
+
We pledge to act and interact in ways that contribute to an open, welcoming,
|
| 13 |
+
diverse, inclusive, and healthy community.
|
| 14 |
+
|
| 15 |
+
## Our Standards
|
| 16 |
+
|
| 17 |
+
Examples of behavior that contributes to a positive environment for our
|
| 18 |
+
community include:
|
| 19 |
+
|
| 20 |
+
- Demonstrating empathy and kindness toward other people
|
| 21 |
+
- Being respectful of differing opinions, viewpoints, and experiences
|
| 22 |
+
- Giving and gracefully accepting constructive feedback
|
| 23 |
+
- Accepting responsibility and apologizing to those affected by our mistakes,
|
| 24 |
+
and learning from the experience
|
| 25 |
+
- Focusing on what is best not just for us as individuals, but for the
|
| 26 |
+
overall community
|
| 27 |
+
|
| 28 |
+
Examples of unacceptable behavior include:
|
| 29 |
+
|
| 30 |
+
- The use of sexualized language or imagery, and sexual attention or
|
| 31 |
+
advances of any kind
|
| 32 |
+
- Trolling, insulting or derogatory comments, and personal or political attacks
|
| 33 |
+
- Public or private harassment
|
| 34 |
+
- Publishing others' private information, such as a physical or email
|
| 35 |
+
address, without their explicit permission
|
| 36 |
+
- Other conduct which could reasonably be considered inappropriate in a
|
| 37 |
+
professional setting
|
| 38 |
+
|
| 39 |
+
## Enforcement Responsibilities
|
| 40 |
+
|
| 41 |
+
Community leaders are responsible for clarifying and enforcing our standards of
|
| 42 |
+
acceptable behavior and will take appropriate and fair corrective action in
|
| 43 |
+
response to any behavior that they deem inappropriate, threatening, offensive,
|
| 44 |
+
or harmful.
|
| 45 |
+
|
| 46 |
+
Community leaders have the right and responsibility to remove, edit, or reject
|
| 47 |
+
comments, commits, code, wiki edits, issues, and other contributions that are
|
| 48 |
+
not aligned to this Code of Conduct, and will communicate reasons for
|
| 49 |
+
moderation decisions when appropriate.
|
| 50 |
+
|
| 51 |
+
## Scope
|
| 52 |
+
|
| 53 |
+
This Code of Conduct applies within all community spaces, and also applies when
|
| 54 |
+
an individual is officially representing the community in public spaces.
|
| 55 |
+
Examples of representing our community include using an official email address,
|
| 56 |
+
posting via an official social media account, or acting as an appointed
|
| 57 |
+
representative at an online or offline event.
|
| 58 |
+
|
| 59 |
+
## Enforcement
|
| 60 |
+
|
| 61 |
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
| 62 |
+
reported to the project maintainers through the repository maintainers or
|
| 63 |
+
security/community contact paths available in the repository.
|
| 64 |
+
|
| 65 |
+
All complaints will be reviewed and investigated promptly and fairly.
|
| 66 |
+
|
| 67 |
+
All community leaders are obligated to respect the privacy and security of the
|
| 68 |
+
reporter of any incident.
|
| 69 |
+
|
| 70 |
+
## Enforcement Guidelines
|
| 71 |
+
|
| 72 |
+
Community leaders will follow these Community Impact Guidelines in determining
|
| 73 |
+
the consequences for any action they deem in violation of this Code of Conduct:
|
| 74 |
+
|
| 75 |
+
### 1. Correction
|
| 76 |
+
|
| 77 |
+
**Community Impact**: Use of inappropriate language or other behavior deemed
|
| 78 |
+
unprofessional or unwelcome in the community.
|
| 79 |
+
|
| 80 |
+
**Consequence**: A private, written warning from community leaders, providing
|
| 81 |
+
clarity around the nature of the violation and an explanation of why the
|
| 82 |
+
behavior was inappropriate. A public apology may be requested.
|
| 83 |
+
|
| 84 |
+
### 2. Warning
|
| 85 |
+
|
| 86 |
+
**Community Impact**: A violation through a single incident or series
|
| 87 |
+
of actions.
|
| 88 |
+
|
| 89 |
+
**Consequence**: A warning with consequences for continued behavior. No
|
| 90 |
+
interaction with the people involved, including unsolicited interaction with
|
| 91 |
+
those enforcing the Code of Conduct, for a specified period of time. This
|
| 92 |
+
includes avoiding interactions in community spaces as well as external channels
|
| 93 |
+
like social media. Violating these terms may lead to a temporary or permanent
|
| 94 |
+
ban.
|
| 95 |
+
|
| 96 |
+
### 3. Temporary Ban
|
| 97 |
+
|
| 98 |
+
**Community Impact**: A serious violation of community standards, including
|
| 99 |
+
sustained inappropriate behavior.
|
| 100 |
+
|
| 101 |
+
**Consequence**: A temporary ban from any sort of interaction or public
|
| 102 |
+
communication with the community for a specified period of time. No public or
|
| 103 |
+
private interaction with the people involved, including unsolicited interaction
|
| 104 |
+
with those enforcing the Code of Conduct, is allowed during this period.
|
| 105 |
+
Violating these terms may lead to a permanent ban.
|
| 106 |
+
|
| 107 |
+
### 4. Permanent Ban
|
| 108 |
+
|
| 109 |
+
**Community Impact**: Demonstrating a pattern of violation of community
|
| 110 |
+
standards, including sustained inappropriate behavior, harassment of an
|
| 111 |
+
individual, or aggression toward or disparagement of classes of individuals.
|
| 112 |
+
|
| 113 |
+
**Consequence**: A permanent ban from any sort of public interaction within
|
| 114 |
+
the community.
|
| 115 |
+
|
| 116 |
+
## Attribution
|
| 117 |
+
|
| 118 |
+
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/),
|
| 119 |
+
version 2.1, available at
|
| 120 |
+
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html).
|
| 121 |
+
|
| 122 |
+
Community Impact Guidelines were inspired by
|
| 123 |
+
[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
| 124 |
+
|
| 125 |
+
For answers to common questions about this code of conduct, see the FAQ at
|
| 126 |
+
[https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq).
|
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Contributing to OpenClaude
|
| 2 |
+
|
| 3 |
+
Thanks for contributing.
|
| 4 |
+
|
| 5 |
+
OpenClaude is a fast-moving open-source coding-agent CLI with support for multiple providers, local backends, MCP, and a terminal-first workflow. The best contributions here are focused, well-tested, and easy to review.
|
| 6 |
+
|
| 7 |
+
## Before You Start
|
| 8 |
+
|
| 9 |
+
- Search existing [issues](https://github.com/Gitlawb/openclaude/issues) and [discussions](https://github.com/Gitlawb/openclaude/discussions) before opening a new thread.
|
| 10 |
+
- Use issues for confirmed bugs and actionable feature work.
|
| 11 |
+
- Use discussions for setup help, ideas, and general community conversation.
|
| 12 |
+
- For larger changes, open an issue first so the scope is clear before implementation.
|
| 13 |
+
- For security reports, follow [SECURITY.md](SECURITY.md).
|
| 14 |
+
|
| 15 |
+
## Local Setup
|
| 16 |
+
|
| 17 |
+
Install dependencies:
|
| 18 |
+
|
| 19 |
+
```bash
|
| 20 |
+
bun install
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
Build the CLI:
|
| 24 |
+
|
| 25 |
+
```bash
|
| 26 |
+
bun run build
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
Smoke test:
|
| 30 |
+
|
| 31 |
+
```bash
|
| 32 |
+
bun run smoke
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
Run the app locally:
|
| 36 |
+
|
| 37 |
+
```bash
|
| 38 |
+
bun run dev
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
If you are working on provider setup or saved profiles, useful commands include:
|
| 42 |
+
|
| 43 |
+
```bash
|
| 44 |
+
bun run profile:init
|
| 45 |
+
bun run dev:profile
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
## Development Workflow
|
| 49 |
+
|
| 50 |
+
- Keep PRs focused on one problem or feature.
|
| 51 |
+
- Avoid mixing unrelated cleanup into the same change.
|
| 52 |
+
- Preserve existing repo patterns unless the change is intentionally refactoring them.
|
| 53 |
+
- Add or update tests when the change affects behavior.
|
| 54 |
+
- Update docs when setup, commands, or user-facing behavior changes.
|
| 55 |
+
|
| 56 |
+
## Validation
|
| 57 |
+
|
| 58 |
+
At minimum, run the most relevant checks for your change.
|
| 59 |
+
|
| 60 |
+
Common checks:
|
| 61 |
+
|
| 62 |
+
```bash
|
| 63 |
+
bun run build
|
| 64 |
+
bun run smoke
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
Focused tests:
|
| 68 |
+
|
| 69 |
+
```bash
|
| 70 |
+
bun test ./path/to/test-file.test.ts
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
When working on provider/runtime setup, this can also help:
|
| 74 |
+
|
| 75 |
+
```bash
|
| 76 |
+
bun run doctor:runtime
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
## Pull Requests
|
| 80 |
+
|
| 81 |
+
Good PRs usually include:
|
| 82 |
+
|
| 83 |
+
- a short explanation of what changed
|
| 84 |
+
- why it changed
|
| 85 |
+
- the user or developer impact
|
| 86 |
+
- the exact checks you ran
|
| 87 |
+
|
| 88 |
+
If the PR touches UI, terminal presentation, or the VS Code extension, include screenshots when useful.
|
| 89 |
+
|
| 90 |
+
If the PR changes provider behavior, mention which provider path was tested.
|
| 91 |
+
|
| 92 |
+
## Code Style
|
| 93 |
+
|
| 94 |
+
- Follow the existing code style in the touched files.
|
| 95 |
+
- Prefer small, readable changes over broad rewrites.
|
| 96 |
+
- Do not reformat unrelated files just because they are nearby.
|
| 97 |
+
- Keep comments useful and concise.
|
| 98 |
+
|
| 99 |
+
## Provider Changes
|
| 100 |
+
|
| 101 |
+
OpenClaude supports multiple provider paths. If you change provider logic:
|
| 102 |
+
|
| 103 |
+
- be explicit about which providers are affected
|
| 104 |
+
- avoid breaking third-party providers while fixing first-party behavior
|
| 105 |
+
- test the exact provider/model path you changed when possible
|
| 106 |
+
- call out any limitations or follow-up work in the PR description
|
| 107 |
+
|
| 108 |
+
## Community
|
| 109 |
+
|
| 110 |
+
Please be respectful and constructive with other contributors.
|
| 111 |
+
|
| 112 |
+
Maintainers may ask for:
|
| 113 |
+
|
| 114 |
+
- narrower scope
|
| 115 |
+
- focused follow-up PRs
|
| 116 |
+
- stronger validation
|
| 117 |
+
- docs updates for behavior changes
|
| 118 |
+
|
| 119 |
+
That is normal and helps keep the project reviewable as it grows.
|
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ---- build stage ----
|
| 2 |
+
FROM node:22-slim AS build
|
| 3 |
+
|
| 4 |
+
# Install Bun
|
| 5 |
+
RUN npm install -g bun@1.3.11
|
| 6 |
+
|
| 7 |
+
WORKDIR /app
|
| 8 |
+
|
| 9 |
+
# Copy dependency manifests first for better layer caching
|
| 10 |
+
COPY package.json bun.lock ./
|
| 11 |
+
|
| 12 |
+
# Install all dependencies (including devDependencies for build)
|
| 13 |
+
RUN bun install --frozen-lockfile
|
| 14 |
+
|
| 15 |
+
# Copy source code
|
| 16 |
+
COPY src/ src/
|
| 17 |
+
COPY scripts/ scripts/
|
| 18 |
+
COPY bin/ bin/
|
| 19 |
+
COPY tsconfig.json ./
|
| 20 |
+
|
| 21 |
+
# Build the CLI bundle
|
| 22 |
+
RUN bun run build
|
| 23 |
+
|
| 24 |
+
# Prune devDependencies
|
| 25 |
+
RUN rm -rf node_modules && bun install --frozen-lockfile --production
|
| 26 |
+
|
| 27 |
+
# ---- runtime stage ----
|
| 28 |
+
FROM node:22-slim
|
| 29 |
+
|
| 30 |
+
WORKDIR /app
|
| 31 |
+
|
| 32 |
+
# Copy only what's needed to run
|
| 33 |
+
COPY --from=build /app/dist/cli.mjs dist/cli.mjs
|
| 34 |
+
COPY --from=build /app/bin/ bin/
|
| 35 |
+
COPY --from=build /app/node_modules/ node_modules/
|
| 36 |
+
COPY --from=build /app/package.json package.json
|
| 37 |
+
COPY README.md ./
|
| 38 |
+
|
| 39 |
+
# Install git and ripgrep β many CLI tool operations depend on them
|
| 40 |
+
RUN apt-get update && apt-get install -y --no-install-recommends git ripgrep \
|
| 41 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 42 |
+
|
| 43 |
+
# Run as non-root user
|
| 44 |
+
USER node
|
| 45 |
+
|
| 46 |
+
ENTRYPOINT ["node", "/app/dist/cli.mjs"]
|
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
NOTICE
|
| 2 |
+
|
| 3 |
+
This repository contains code derived from Anthropic's Claude Code CLI.
|
| 4 |
+
|
| 5 |
+
The original Claude Code source is proprietary software:
|
| 6 |
+
Copyright (c) Anthropic PBC. All rights reserved.
|
| 7 |
+
Subject to Anthropic's Commercial Terms of Service.
|
| 8 |
+
|
| 9 |
+
Modifications and additions by OpenClaude contributors are offered under
|
| 10 |
+
the MIT License where legally permissible:
|
| 11 |
+
|
| 12 |
+
MIT License
|
| 13 |
+
Copyright (c) 2026 OpenClaude contributors (modifications only)
|
| 14 |
+
|
| 15 |
+
Permission is hereby granted, free of charge, to any person obtaining
|
| 16 |
+
a copy of the modifications made by OpenClaude contributors, to deal
|
| 17 |
+
in those modifications without restriction, including without limitation
|
| 18 |
+
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
| 19 |
+
and/or sell copies, subject to the following conditions:
|
| 20 |
+
|
| 21 |
+
The above copyright notice and this permission notice shall be included
|
| 22 |
+
in all copies or substantial portions of the modifications.
|
| 23 |
+
|
| 24 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
|
| 25 |
+
|
| 26 |
+
The underlying derived code remains subject to Anthropic's copyright.
|
| 27 |
+
This project does not have Anthropic's authorization to distribute
|
| 28 |
+
their proprietary source. Users and contributors should evaluate their
|
| 29 |
+
own legal position.
|
|
@@ -0,0 +1,322 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# OpenClaude Local Agent Playbook
|
| 2 |
+
|
| 3 |
+
This playbook is a practical guide to run OpenClaude with a local model (Ollama), work safely, and get strong day-to-day results.
|
| 4 |
+
|
| 5 |
+
## 1. What You Have
|
| 6 |
+
|
| 7 |
+
- A CLI agent loop that can read/write files, run terminal commands, and help with coding workflows.
|
| 8 |
+
- A local provider profile system (`profile:init` and `dev:profile`).
|
| 9 |
+
- Runtime checks (`doctor:runtime`) and reporting (`doctor:report`).
|
| 10 |
+
- A local model profile currently set to `llama3.1:8b`.
|
| 11 |
+
|
| 12 |
+
## 2. Daily Start (Fast Path)
|
| 13 |
+
|
| 14 |
+
Run this in your project root:
|
| 15 |
+
|
| 16 |
+
```powershell
|
| 17 |
+
bun run dev:profile
|
| 18 |
+
```
|
| 19 |
+
|
| 20 |
+
For quick switches:
|
| 21 |
+
|
| 22 |
+
```powershell
|
| 23 |
+
# low latency preset
|
| 24 |
+
bun run dev:fast
|
| 25 |
+
|
| 26 |
+
# better coding quality preset
|
| 27 |
+
bun run dev:code
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
If everything is healthy, OpenClaude starts directly.
|
| 31 |
+
|
| 32 |
+
## 3. One-Time Setup (If Needed)
|
| 33 |
+
|
| 34 |
+
### 3.1 Initialize a local profile
|
| 35 |
+
|
| 36 |
+
```powershell
|
| 37 |
+
bun run profile:init -- --provider ollama --model llama3.1:8b
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
Or let OpenClaude recommend the best local model for your goal:
|
| 41 |
+
|
| 42 |
+
```powershell
|
| 43 |
+
bun run profile:init -- --provider ollama --goal coding
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
Preview recommendations before saving:
|
| 47 |
+
|
| 48 |
+
```powershell
|
| 49 |
+
bun run profile:recommend -- --goal coding --benchmark
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
### 3.2 Confirm profile file
|
| 53 |
+
|
| 54 |
+
```powershell
|
| 55 |
+
Get-Content .\.openclaude-profile.json
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
### 3.3 Validate environment
|
| 59 |
+
|
| 60 |
+
```powershell
|
| 61 |
+
bun run doctor:runtime
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
## 4. Health and Diagnostics
|
| 65 |
+
|
| 66 |
+
### 4.1 Human-readable checks
|
| 67 |
+
|
| 68 |
+
```powershell
|
| 69 |
+
bun run doctor:runtime
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
### 4.2 JSON diagnostics (automation/logging)
|
| 73 |
+
|
| 74 |
+
```powershell
|
| 75 |
+
bun run doctor:runtime:json
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
### 4.3 Persist runtime report
|
| 79 |
+
|
| 80 |
+
```powershell
|
| 81 |
+
bun run doctor:report
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
Report output:
|
| 85 |
+
|
| 86 |
+
- `reports/doctor-runtime.json`
|
| 87 |
+
|
| 88 |
+
### 4.4 Hardening checks
|
| 89 |
+
|
| 90 |
+
```powershell
|
| 91 |
+
# practical checks (smoke + runtime doctor)
|
| 92 |
+
bun run hardening:check
|
| 93 |
+
|
| 94 |
+
# strict checks (includes typecheck)
|
| 95 |
+
bun run hardening:strict
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
## 5. Provider Modes
|
| 99 |
+
|
| 100 |
+
## 5.1 Local mode (Ollama)
|
| 101 |
+
|
| 102 |
+
```powershell
|
| 103 |
+
bun run profile:init -- --provider ollama --model llama3.1:8b
|
| 104 |
+
bun run dev:profile
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
Expected behavior:
|
| 108 |
+
|
| 109 |
+
- No API key required.
|
| 110 |
+
- `OPENAI_BASE_URL` should be `http://localhost:11434/v1`.
|
| 111 |
+
|
| 112 |
+
## 5.2 OpenAI mode
|
| 113 |
+
|
| 114 |
+
```powershell
|
| 115 |
+
bun run profile:init -- --provider openai --api-key sk-... --model gpt-4o
|
| 116 |
+
bun run dev:profile
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
Expected behavior:
|
| 120 |
+
|
| 121 |
+
- Real API key required.
|
| 122 |
+
- Placeholder values fail fast.
|
| 123 |
+
|
| 124 |
+
## 6. Troubleshooting Matrix
|
| 125 |
+
|
| 126 |
+
## 6.1 `Script not found "dev"`
|
| 127 |
+
|
| 128 |
+
Cause:
|
| 129 |
+
|
| 130 |
+
- You ran command in the wrong folder.
|
| 131 |
+
|
| 132 |
+
Fix:
|
| 133 |
+
|
| 134 |
+
```powershell
|
| 135 |
+
cd C:\Users\Lucas Pedry\Documents\openclaude\openclaude
|
| 136 |
+
bun run dev:profile
|
| 137 |
+
```
|
| 138 |
+
|
| 139 |
+
## 6.2 `ollama: term not recognized`
|
| 140 |
+
|
| 141 |
+
Cause:
|
| 142 |
+
|
| 143 |
+
- Ollama not installed or PATH not loaded in this terminal.
|
| 144 |
+
|
| 145 |
+
Fix:
|
| 146 |
+
|
| 147 |
+
- Install Ollama from https://ollama.com/download/windows or `winget install Ollama.Ollama`.
|
| 148 |
+
- Open a new terminal and run:
|
| 149 |
+
|
| 150 |
+
```powershell
|
| 151 |
+
ollama --version
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
## 6.3 `Provider reachability failed` for localhost
|
| 155 |
+
|
| 156 |
+
Cause:
|
| 157 |
+
|
| 158 |
+
- Ollama service not running.
|
| 159 |
+
|
| 160 |
+
Fix:
|
| 161 |
+
|
| 162 |
+
```powershell
|
| 163 |
+
ollama serve
|
| 164 |
+
```
|
| 165 |
+
|
| 166 |
+
Then, in another terminal:
|
| 167 |
+
|
| 168 |
+
```powershell
|
| 169 |
+
bun run doctor:runtime
|
| 170 |
+
```
|
| 171 |
+
|
| 172 |
+
## 6.4 `Missing key for non-local provider URL`
|
| 173 |
+
|
| 174 |
+
Cause:
|
| 175 |
+
|
| 176 |
+
- `OPENAI_BASE_URL` points to remote endpoint without key.
|
| 177 |
+
|
| 178 |
+
Fix:
|
| 179 |
+
|
| 180 |
+
- Re-initialize profile for ollama:
|
| 181 |
+
|
| 182 |
+
```powershell
|
| 183 |
+
bun run profile:init -- --provider ollama --model llama3.1:8b
|
| 184 |
+
```
|
| 185 |
+
|
| 186 |
+
Or pick a local Ollama profile automatically by goal:
|
| 187 |
+
|
| 188 |
+
```powershell
|
| 189 |
+
bun run profile:init -- --provider ollama --goal balanced
|
| 190 |
+
```
|
| 191 |
+
|
| 192 |
+
## 6.5 Placeholder key (`SUA_CHAVE`) error
|
| 193 |
+
|
| 194 |
+
Cause:
|
| 195 |
+
|
| 196 |
+
- Placeholder was used instead of real key.
|
| 197 |
+
|
| 198 |
+
Fix:
|
| 199 |
+
|
| 200 |
+
- For OpenAI: use a real key.
|
| 201 |
+
- For Ollama: no key needed; keep localhost base URL.
|
| 202 |
+
|
| 203 |
+
## 7. Recommended Local Models
|
| 204 |
+
|
| 205 |
+
- Fast/general: `llama3.1:8b`
|
| 206 |
+
- Better coding quality (if hardware supports): `qwen2.5-coder:14b`
|
| 207 |
+
- Low-resource fallback: smaller instruct model
|
| 208 |
+
|
| 209 |
+
Switch model quickly:
|
| 210 |
+
|
| 211 |
+
```powershell
|
| 212 |
+
bun run profile:init -- --provider ollama --model qwen2.5-coder:14b
|
| 213 |
+
bun run dev:profile
|
| 214 |
+
```
|
| 215 |
+
|
| 216 |
+
Preset shortcuts already configured:
|
| 217 |
+
|
| 218 |
+
```powershell
|
| 219 |
+
bun run profile:fast # llama3.2:3b
|
| 220 |
+
bun run profile:code # qwen2.5-coder:7b
|
| 221 |
+
```
|
| 222 |
+
|
| 223 |
+
Goal-based local auto-selection:
|
| 224 |
+
|
| 225 |
+
```powershell
|
| 226 |
+
bun run profile:init -- --provider ollama --goal latency
|
| 227 |
+
bun run profile:init -- --provider ollama --goal balanced
|
| 228 |
+
bun run profile:init -- --provider ollama --goal coding
|
| 229 |
+
```
|
| 230 |
+
|
| 231 |
+
`profile:auto` is a best-available provider picker, not a local-only command. Use `--provider ollama` when you want to stay on a local model.
|
| 232 |
+
|
| 233 |
+
## 8. Practical Prompt Playbook (Copy/Paste)
|
| 234 |
+
|
| 235 |
+
## 8.1 Code understanding
|
| 236 |
+
|
| 237 |
+
- "Map this repository architecture and explain the execution flow from entrypoint to tool invocation."
|
| 238 |
+
- "Find the top 5 risky modules and explain why."
|
| 239 |
+
|
| 240 |
+
## 8.2 Refactoring
|
| 241 |
+
|
| 242 |
+
- "Refactor this module for clarity without behavior change, then run checks and summarize diff impact."
|
| 243 |
+
- "Extract shared logic from duplicated functions and add minimal tests."
|
| 244 |
+
|
| 245 |
+
## 8.3 Debugging
|
| 246 |
+
|
| 247 |
+
- "Reproduce the failure, identify root cause, implement fix, and validate with commands."
|
| 248 |
+
- "Trace this error path and list likely failure points with confidence levels."
|
| 249 |
+
|
| 250 |
+
## 8.4 Reliability
|
| 251 |
+
|
| 252 |
+
- "Add runtime guardrails and fail-fast messages for invalid provider env vars."
|
| 253 |
+
- "Create a diagnostic command that outputs JSON report for CI artifacts."
|
| 254 |
+
|
| 255 |
+
## 8.5 Review mode
|
| 256 |
+
|
| 257 |
+
- "Do a code review of unstaged changes, prioritize bugs/regressions, and suggest concrete patches."
|
| 258 |
+
|
| 259 |
+
## 9. Safe Working Rules
|
| 260 |
+
|
| 261 |
+
- Run `doctor:runtime` before debugging provider issues.
|
| 262 |
+
- Prefer `dev:profile` over manual env edits.
|
| 263 |
+
- Keep `.openclaude-profile.json` local (already gitignored).
|
| 264 |
+
- Use `doctor:report` before asking for help so you have a reproducible snapshot.
|
| 265 |
+
|
| 266 |
+
## 10. Quick Recovery Checklist
|
| 267 |
+
|
| 268 |
+
When something breaks, run in order:
|
| 269 |
+
|
| 270 |
+
```powershell
|
| 271 |
+
bun run doctor:runtime
|
| 272 |
+
bun run doctor:report
|
| 273 |
+
bun run smoke
|
| 274 |
+
```
|
| 275 |
+
|
| 276 |
+
If answers are very slow, check processor mode:
|
| 277 |
+
|
| 278 |
+
```powershell
|
| 279 |
+
ollama ps
|
| 280 |
+
```
|
| 281 |
+
|
| 282 |
+
If `PROCESSOR` shows `CPU`, your setup is valid but latency will be higher for large models.
|
| 283 |
+
|
| 284 |
+
If local model mode is failing:
|
| 285 |
+
|
| 286 |
+
```powershell
|
| 287 |
+
ollama --version
|
| 288 |
+
ollama serve
|
| 289 |
+
bun run doctor:runtime
|
| 290 |
+
bun run dev:profile
|
| 291 |
+
```
|
| 292 |
+
|
| 293 |
+
## 11. Command Reference
|
| 294 |
+
|
| 295 |
+
```powershell
|
| 296 |
+
# profile
|
| 297 |
+
bun run profile:init -- --provider ollama --model llama3.1:8b
|
| 298 |
+
bun run profile:init -- --provider openai --api-key sk-... --model gpt-4o
|
| 299 |
+
|
| 300 |
+
# launch
|
| 301 |
+
bun run dev:profile
|
| 302 |
+
bun run dev:ollama
|
| 303 |
+
bun run dev:openai
|
| 304 |
+
|
| 305 |
+
# diagnostics
|
| 306 |
+
bun run doctor:runtime
|
| 307 |
+
bun run doctor:runtime:json
|
| 308 |
+
bun run doctor:report
|
| 309 |
+
|
| 310 |
+
# quality
|
| 311 |
+
bun run smoke
|
| 312 |
+
bun run hardening:check
|
| 313 |
+
bun run hardening:strict
|
| 314 |
+
```
|
| 315 |
+
|
| 316 |
+
## 12. Success Criteria
|
| 317 |
+
|
| 318 |
+
Your setup is healthy when:
|
| 319 |
+
|
| 320 |
+
- `bun run doctor:runtime` passes provider and reachability checks.
|
| 321 |
+
- `bun run dev:profile` opens the CLI normally.
|
| 322 |
+
- Model shown in the UI matches your selected profile model.
|
|
@@ -0,0 +1,345 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# OpenClaude
|
| 2 |
+
|
| 3 |
+
OpenClaude is an open-source coding-agent CLI for cloud and local model providers.
|
| 4 |
+
|
| 5 |
+
Use OpenAI-compatible APIs, Gemini, GitHub Models, Codex OAuth, Codex, Ollama, Atomic Chat, and other supported backends while keeping one terminal-first workflow: prompts, tools, agents, MCP, slash commands, and streaming output.
|
| 6 |
+
|
| 7 |
+
[](https://github.com/Gitlawb/openclaude/actions/workflows/pr-checks.yml)
|
| 8 |
+
[](https://github.com/Gitlawb/openclaude/tags)
|
| 9 |
+
[](https://github.com/Gitlawb/openclaude/discussions)
|
| 10 |
+
[](SECURITY.md)
|
| 11 |
+
[](LICENSE)
|
| 12 |
+
|
| 13 |
+
OpenClaude is also mirrored to GitLawb:
|
| 14 |
+
[gitlawb.com/node/repos/z6MkqDnb/openclaude](https://gitlawb.com/node/repos/z6MkqDnb/openclaude)
|
| 15 |
+
|
| 16 |
+
[Quick Start](#quick-start) | [Setup Guides](#setup-guides) | [Providers](#supported-providers) | [Source Build](#source-build-and-local-development) | [VS Code Extension](#vs-code-extension) | [Community](#community)
|
| 17 |
+
|
| 18 |
+
## Star History
|
| 19 |
+
|
| 20 |
+
[](https://www.star-history.com/?repos=gitlawb%2Fopenclaude&type=date&legend=top-left)
|
| 21 |
+
|
| 22 |
+
## Why OpenClaude
|
| 23 |
+
|
| 24 |
+
- Use one CLI across cloud APIs and local model backends
|
| 25 |
+
- Save provider profiles inside the app with `/provider`
|
| 26 |
+
- Run with OpenAI-compatible services, Gemini, GitHub Models, Codex OAuth, Codex, Ollama, Atomic Chat, and other supported providers
|
| 27 |
+
- Keep coding-agent workflows in one place: bash, file tools, grep, glob, agents, tasks, MCP, and web tools
|
| 28 |
+
- Use the bundled VS Code extension for launch integration and theme support
|
| 29 |
+
|
| 30 |
+
## Quick Start
|
| 31 |
+
|
| 32 |
+
### Install
|
| 33 |
+
|
| 34 |
+
```bash
|
| 35 |
+
npm install -g @gitlawb/openclaude
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
If the install later reports `ripgrep not found`, install ripgrep system-wide and confirm `rg --version` works in the same terminal before starting OpenClaude.
|
| 39 |
+
|
| 40 |
+
### Start
|
| 41 |
+
|
| 42 |
+
```bash
|
| 43 |
+
openclaude
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
Inside OpenClaude:
|
| 47 |
+
|
| 48 |
+
- run `/provider` for guided provider setup and saved profiles
|
| 49 |
+
- run `/onboard-github` for GitHub Models onboarding
|
| 50 |
+
|
| 51 |
+
### Fastest OpenAI setup
|
| 52 |
+
|
| 53 |
+
macOS / Linux:
|
| 54 |
+
|
| 55 |
+
```bash
|
| 56 |
+
export CLAUDE_CODE_USE_OPENAI=1
|
| 57 |
+
export OPENAI_API_KEY=sk-your-key-here
|
| 58 |
+
export OPENAI_MODEL=gpt-4o
|
| 59 |
+
|
| 60 |
+
openclaude
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
Windows PowerShell:
|
| 64 |
+
|
| 65 |
+
```powershell
|
| 66 |
+
$env:CLAUDE_CODE_USE_OPENAI="1"
|
| 67 |
+
$env:OPENAI_API_KEY="sk-your-key-here"
|
| 68 |
+
$env:OPENAI_MODEL="gpt-4o"
|
| 69 |
+
|
| 70 |
+
openclaude
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
### Fastest local Ollama setup
|
| 74 |
+
|
| 75 |
+
macOS / Linux:
|
| 76 |
+
|
| 77 |
+
```bash
|
| 78 |
+
export CLAUDE_CODE_USE_OPENAI=1
|
| 79 |
+
export OPENAI_BASE_URL=http://localhost:11434/v1
|
| 80 |
+
export OPENAI_MODEL=qwen2.5-coder:7b
|
| 81 |
+
|
| 82 |
+
openclaude
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
Windows PowerShell:
|
| 86 |
+
|
| 87 |
+
```powershell
|
| 88 |
+
$env:CLAUDE_CODE_USE_OPENAI="1"
|
| 89 |
+
$env:OPENAI_BASE_URL="http://localhost:11434/v1"
|
| 90 |
+
$env:OPENAI_MODEL="qwen2.5-coder:7b"
|
| 91 |
+
|
| 92 |
+
openclaude
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
### Using Ollama's launch command
|
| 96 |
+
|
| 97 |
+
If you have [Ollama](https://ollama.com) installed, you can skip the env var setup entirely:
|
| 98 |
+
|
| 99 |
+
```bash
|
| 100 |
+
ollama launch openclaude --model qwen2.5-coder:7b
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
This automatically sets `ANTHROPIC_BASE_URL`, model routing, and auth so all API traffic goes through your local Ollama instance. Works with any model you have pulled β local or cloud.
|
| 104 |
+
|
| 105 |
+
## Setup Guides
|
| 106 |
+
|
| 107 |
+
Beginner-friendly guides:
|
| 108 |
+
|
| 109 |
+
- [Non-Technical Setup](docs/non-technical-setup.md)
|
| 110 |
+
- [Windows Quick Start](docs/quick-start-windows.md)
|
| 111 |
+
- [macOS / Linux Quick Start](docs/quick-start-mac-linux.md)
|
| 112 |
+
|
| 113 |
+
Advanced and source-build guides:
|
| 114 |
+
|
| 115 |
+
- [Advanced Setup](docs/advanced-setup.md)
|
| 116 |
+
- [Android Install](ANDROID_INSTALL.md)
|
| 117 |
+
|
| 118 |
+
## Supported Providers
|
| 119 |
+
|
| 120 |
+
| Provider | Setup Path | Notes |
|
| 121 |
+
| --- | --- | --- |
|
| 122 |
+
| OpenAI-compatible | `/provider` or env vars | Works with OpenAI, OpenRouter, DeepSeek, Groq, Mistral, LM Studio, and other compatible `/v1` servers |
|
| 123 |
+
| Gemini | `/provider` or env vars | Supports API key, access token, or local ADC workflow on current `main` |
|
| 124 |
+
| GitHub Models | `/onboard-github` | Interactive onboarding with saved credentials |
|
| 125 |
+
| Codex OAuth | `/provider` | Opens ChatGPT sign-in in your browser and stores Codex credentials securely |
|
| 126 |
+
| Codex | `/provider` | Uses existing Codex CLI auth, OpenClaude secure storage, or env credentials |
|
| 127 |
+
| Ollama | `/provider`, env vars, or `ollama launch` | Local inference with no API key |
|
| 128 |
+
| Atomic Chat | `/provider`, env vars, or `bun run dev:atomic-chat` | Local Model Provider; auto-detects loaded models |
|
| 129 |
+
| Bedrock / Vertex / Foundry | env vars | Additional provider integrations for supported environments |
|
| 130 |
+
|
| 131 |
+
## What Works
|
| 132 |
+
|
| 133 |
+
- **Tool-driven coding workflows**: Bash, file read/write/edit, grep, glob, agents, tasks, MCP, and slash commands
|
| 134 |
+
- **Streaming responses**: Real-time token output and tool progress
|
| 135 |
+
- **Tool calling**: Multi-step tool loops with model calls, tool execution, and follow-up responses
|
| 136 |
+
- **Images**: URL and base64 image inputs for providers that support vision
|
| 137 |
+
- **Provider profiles**: Guided setup plus saved `.openclaude-profile.json` support
|
| 138 |
+
- **Local and remote model backends**: Cloud APIs, local servers, and Apple Silicon local inference
|
| 139 |
+
|
| 140 |
+
## Provider Notes
|
| 141 |
+
|
| 142 |
+
OpenClaude supports multiple providers, but behavior is not identical across all of them.
|
| 143 |
+
|
| 144 |
+
- Anthropic-specific features may not exist on other providers
|
| 145 |
+
- Tool quality depends heavily on the selected model
|
| 146 |
+
- Smaller local models can struggle with long multi-step tool flows
|
| 147 |
+
- Some providers impose lower output caps than the CLI defaults, and OpenClaude adapts where possible
|
| 148 |
+
|
| 149 |
+
For best results, use models with strong tool/function calling support.
|
| 150 |
+
|
| 151 |
+
## Agent Routing
|
| 152 |
+
|
| 153 |
+
OpenClaude can route different agents to different models through settings-based routing. This is useful for cost optimization or splitting work by model strength.
|
| 154 |
+
|
| 155 |
+
Add to `~/.claude/settings.json`:
|
| 156 |
+
|
| 157 |
+
```json
|
| 158 |
+
{
|
| 159 |
+
"agentModels": {
|
| 160 |
+
"deepseek-chat": {
|
| 161 |
+
"base_url": "https://api.deepseek.com/v1",
|
| 162 |
+
"api_key": "sk-your-key"
|
| 163 |
+
},
|
| 164 |
+
"gpt-4o": {
|
| 165 |
+
"base_url": "https://api.openai.com/v1",
|
| 166 |
+
"api_key": "sk-your-key"
|
| 167 |
+
}
|
| 168 |
+
},
|
| 169 |
+
"agentRouting": {
|
| 170 |
+
"Explore": "deepseek-chat",
|
| 171 |
+
"Plan": "gpt-4o",
|
| 172 |
+
"general-purpose": "gpt-4o",
|
| 173 |
+
"frontend-dev": "deepseek-chat",
|
| 174 |
+
"default": "gpt-4o"
|
| 175 |
+
}
|
| 176 |
+
}
|
| 177 |
+
```
|
| 178 |
+
|
| 179 |
+
When no routing match is found, the global provider remains the fallback.
|
| 180 |
+
|
| 181 |
+
> **Note:** `api_key` values in `settings.json` are stored in plaintext. Keep this file private and do not commit it to version control.
|
| 182 |
+
|
| 183 |
+
## Web Search and Fetch
|
| 184 |
+
|
| 185 |
+
By default, `WebSearch` works on non-Anthropic models using DuckDuckGo. This gives GPT-4o, DeepSeek, Gemini, Ollama, and other OpenAI-compatible providers a free web search path out of the box.
|
| 186 |
+
|
| 187 |
+
> **Note:** DuckDuckGo fallback works by scraping search results and may be rate-limited, blocked, or subject to DuckDuckGo's Terms of Service. If you want a more reliable supported option, configure Firecrawl.
|
| 188 |
+
|
| 189 |
+
For Anthropic-native backends and Codex responses, OpenClaude keeps the native provider web search behavior.
|
| 190 |
+
|
| 191 |
+
`WebFetch` works, but its basic HTTP plus HTML-to-markdown path can still fail on JavaScript-rendered sites or sites that block plain HTTP requests.
|
| 192 |
+
|
| 193 |
+
Set a [Firecrawl](https://firecrawl.dev) API key if you want Firecrawl-powered search/fetch behavior:
|
| 194 |
+
|
| 195 |
+
```bash
|
| 196 |
+
export FIRECRAWL_API_KEY=your-key-here
|
| 197 |
+
```
|
| 198 |
+
|
| 199 |
+
With Firecrawl enabled:
|
| 200 |
+
|
| 201 |
+
- `WebSearch` can use Firecrawl's search API while DuckDuckGo remains the default free path for non-Claude models
|
| 202 |
+
- `WebFetch` uses Firecrawl's scrape endpoint instead of raw HTTP, handling JS-rendered pages correctly
|
| 203 |
+
|
| 204 |
+
Free tier at [firecrawl.dev](https://firecrawl.dev) includes 500 credits. The key is optional.
|
| 205 |
+
|
| 206 |
+
---
|
| 207 |
+
|
| 208 |
+
## Headless gRPC Server
|
| 209 |
+
|
| 210 |
+
OpenClaude can be run as a headless gRPC service, allowing you to integrate its agentic capabilities (tools, bash, file editing) into other applications, CI/CD pipelines, or custom user interfaces. The server uses bidirectional streaming to send real-time text chunks, tool calls, and request permissions for sensitive commands.
|
| 211 |
+
|
| 212 |
+
### 1. Start the gRPC Server
|
| 213 |
+
|
| 214 |
+
Start the core engine as a gRPC service on `localhost:50051`:
|
| 215 |
+
|
| 216 |
+
```bash
|
| 217 |
+
npm run dev:grpc
|
| 218 |
+
```
|
| 219 |
+
|
| 220 |
+
#### Configuration
|
| 221 |
+
|
| 222 |
+
| Variable | Default | Description |
|
| 223 |
+
|-----------|-------------|------------------------------------------------|
|
| 224 |
+
| `GRPC_PORT` | `50051` | Port the gRPC server listens on |
|
| 225 |
+
| `GRPC_HOST` | `localhost` | Bind address. Use `0.0.0.0` to expose on all interfaces (not recommended without authentication) |
|
| 226 |
+
|
| 227 |
+
### 2. Run the Test CLI Client
|
| 228 |
+
|
| 229 |
+
We provide a lightweight CLI client that communicates exclusively over gRPC. It acts just like the main interactive CLI, rendering colors, streaming tokens, and prompting you for tool permissions (y/n) via the gRPC `action_required` event.
|
| 230 |
+
|
| 231 |
+
In a separate terminal, run:
|
| 232 |
+
|
| 233 |
+
```bash
|
| 234 |
+
npm run dev:grpc:cli
|
| 235 |
+
```
|
| 236 |
+
|
| 237 |
+
*Note: The gRPC definitions are located in `src/proto/openclaude.proto`. You can use this file to generate clients in Python, Go, Rust, or any other language.*
|
| 238 |
+
|
| 239 |
+
---
|
| 240 |
+
|
| 241 |
+
## Source Build And Local Development
|
| 242 |
+
|
| 243 |
+
```bash
|
| 244 |
+
bun install
|
| 245 |
+
bun run build
|
| 246 |
+
node dist/cli.mjs
|
| 247 |
+
```
|
| 248 |
+
|
| 249 |
+
Helpful commands:
|
| 250 |
+
|
| 251 |
+
- `bun run dev`
|
| 252 |
+
- `bun test`
|
| 253 |
+
- `bun run test:coverage`
|
| 254 |
+
- `bun run security:pr-scan -- --base origin/main`
|
| 255 |
+
- `bun run smoke`
|
| 256 |
+
- `bun run doctor:runtime`
|
| 257 |
+
- `bun run verify:privacy`
|
| 258 |
+
- focused `bun test ...` runs for the areas you touch
|
| 259 |
+
|
| 260 |
+
## Testing And Coverage
|
| 261 |
+
|
| 262 |
+
OpenClaude uses Bun's built-in test runner for unit tests.
|
| 263 |
+
|
| 264 |
+
Run the full unit suite:
|
| 265 |
+
|
| 266 |
+
```bash
|
| 267 |
+
bun test
|
| 268 |
+
```
|
| 269 |
+
|
| 270 |
+
Generate unit test coverage:
|
| 271 |
+
|
| 272 |
+
```bash
|
| 273 |
+
bun run test:coverage
|
| 274 |
+
```
|
| 275 |
+
|
| 276 |
+
Open the visual coverage report:
|
| 277 |
+
|
| 278 |
+
```bash
|
| 279 |
+
open coverage/index.html
|
| 280 |
+
```
|
| 281 |
+
|
| 282 |
+
If you already have `coverage/lcov.info` and only want to rebuild the UI:
|
| 283 |
+
|
| 284 |
+
```bash
|
| 285 |
+
bun run test:coverage:ui
|
| 286 |
+
```
|
| 287 |
+
|
| 288 |
+
Use focused test runs when you only touch one area:
|
| 289 |
+
|
| 290 |
+
- `bun run test:provider`
|
| 291 |
+
- `bun run test:provider-recommendation`
|
| 292 |
+
- `bun test path/to/file.test.ts`
|
| 293 |
+
|
| 294 |
+
Recommended contributor validation before opening a PR:
|
| 295 |
+
|
| 296 |
+
- `bun run build`
|
| 297 |
+
- `bun run smoke`
|
| 298 |
+
- `bun run test:coverage` for broader unit coverage when your change affects shared runtime or provider logic
|
| 299 |
+
- focused `bun test ...` runs for the files and flows you changed
|
| 300 |
+
|
| 301 |
+
Coverage output is written to `coverage/lcov.info`, and OpenClaude also generates a git-activity-style heatmap at `coverage/index.html`.
|
| 302 |
+
## Repository Structure
|
| 303 |
+
|
| 304 |
+
- `src/` - core CLI/runtime
|
| 305 |
+
- `scripts/` - build, verification, and maintenance scripts
|
| 306 |
+
- `docs/` - setup, contributor, and project documentation
|
| 307 |
+
- `python/` - standalone Python helpers and their tests
|
| 308 |
+
- `vscode-extension/openclaude-vscode/` - VS Code extension
|
| 309 |
+
- `.github/` - repo automation, templates, and CI configuration
|
| 310 |
+
- `bin/` - CLI launcher entrypoints
|
| 311 |
+
|
| 312 |
+
## VS Code Extension
|
| 313 |
+
|
| 314 |
+
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.
|
| 315 |
+
|
| 316 |
+
## Security
|
| 317 |
+
|
| 318 |
+
If you believe you found a security issue, see [SECURITY.md](SECURITY.md).
|
| 319 |
+
|
| 320 |
+
## Community
|
| 321 |
+
|
| 322 |
+
- Use [GitHub Discussions](https://github.com/Gitlawb/openclaude/discussions) for Q&A, ideas, and community conversation
|
| 323 |
+
- Use [GitHub Issues](https://github.com/Gitlawb/openclaude/issues) for confirmed bugs and actionable feature work
|
| 324 |
+
|
| 325 |
+
## Contributing
|
| 326 |
+
|
| 327 |
+
Contributions are welcome.
|
| 328 |
+
|
| 329 |
+
For larger changes, open an issue first so the scope is clear before implementation. Helpful validation commands include:
|
| 330 |
+
|
| 331 |
+
- `bun run build`
|
| 332 |
+
- `bun run test:coverage`
|
| 333 |
+
- `bun run smoke`
|
| 334 |
+
- focused `bun test ...` runs for files and flows you changed
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
## Disclaimer
|
| 338 |
+
|
| 339 |
+
OpenClaude is an independent community project and is not affiliated with, endorsed by, or sponsored by Anthropic.
|
| 340 |
+
|
| 341 |
+
OpenClaude originated from the Claude Code codebase and has since been substantially modified to support multiple providers and open use. "Claude" and "Claude Code" are trademarks of Anthropic PBC. See [LICENSE](LICENSE) for details.
|
| 342 |
+
|
| 343 |
+
## License
|
| 344 |
+
|
| 345 |
+
See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Security Policy
|
| 2 |
+
|
| 3 |
+
## Supported Versions
|
| 4 |
+
|
| 5 |
+
Open Claude is currently maintained on the latest `main` branch and the latest
|
| 6 |
+
npm release only.
|
| 7 |
+
|
| 8 |
+
| Version | Supported |
|
| 9 |
+
| ------- | --------- |
|
| 10 |
+
| Latest release | :white_check_mark: |
|
| 11 |
+
| Older releases | :x: |
|
| 12 |
+
| Unreleased forks / modified builds | :x: |
|
| 13 |
+
|
| 14 |
+
Security fixes are generally released in the next patch version and may also be
|
| 15 |
+
landed directly on `main` before a package release is published.
|
| 16 |
+
|
| 17 |
+
## Reporting a Vulnerability
|
| 18 |
+
|
| 19 |
+
If you believe you have found a security vulnerability in Open Claude, please
|
| 20 |
+
report it privately.
|
| 21 |
+
|
| 22 |
+
Preferred reporting channel:
|
| 23 |
+
|
| 24 |
+
- GitHub Security Advisories / private vulnerability reporting for this
|
| 25 |
+
repository
|
| 26 |
+
|
| 27 |
+
Please include:
|
| 28 |
+
|
| 29 |
+
- a clear description of the issue
|
| 30 |
+
- affected version, commit, or environment
|
| 31 |
+
- reproduction steps or a proof of concept
|
| 32 |
+
- impact assessment
|
| 33 |
+
- any suggested remediation, if available
|
| 34 |
+
|
| 35 |
+
Please do **not** open a public issue for an unpatched vulnerability.
|
| 36 |
+
|
| 37 |
+
## Response Process
|
| 38 |
+
|
| 39 |
+
Our general goals are:
|
| 40 |
+
|
| 41 |
+
- initial triage acknowledgment within 7 days
|
| 42 |
+
- follow-up after validation when we can reproduce the issue
|
| 43 |
+
- coordinated disclosure after a fix is available
|
| 44 |
+
|
| 45 |
+
Severity, exploitability, and maintenance bandwidth may affect timelines.
|
| 46 |
+
|
| 47 |
+
## Disclosure and CVEs
|
| 48 |
+
|
| 49 |
+
Valid reports may be fixed privately first and disclosed after a patch is
|
| 50 |
+
available.
|
| 51 |
+
|
| 52 |
+
If a report is accepted and the issue is significant enough to warrant formal
|
| 53 |
+
tracking, we may publish a GitHub Security Advisory and request or assign a CVE
|
| 54 |
+
through the appropriate channel. CVE issuance is not guaranteed for every
|
| 55 |
+
report.
|
| 56 |
+
|
| 57 |
+
## Scope
|
| 58 |
+
|
| 59 |
+
This policy applies to:
|
| 60 |
+
|
| 61 |
+
- the Open Claude source code in this repository
|
| 62 |
+
- official release artifacts published from this repository
|
| 63 |
+
- the `@gitlawb/openclaude` npm package
|
| 64 |
+
|
| 65 |
+
This policy does not cover:
|
| 66 |
+
|
| 67 |
+
- third-party model providers, endpoints, or hosted services
|
| 68 |
+
- local misconfiguration on the reporter's machine
|
| 69 |
+
- vulnerabilities in unofficial forks, mirrors, or downstream repackages
|
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { join, win32 } from 'path'
|
| 2 |
+
import { pathToFileURL } from 'url'
|
| 3 |
+
|
| 4 |
+
export function getDistImportSpecifier(baseDir) {
|
| 5 |
+
if (/^[A-Za-z]:\\/.test(baseDir)) {
|
| 6 |
+
const distPath = win32.join(baseDir, '..', 'dist', 'cli.mjs')
|
| 7 |
+
return `file:///${distPath.replace(/\\/g, '/')}`
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
const joinImpl = join
|
| 11 |
+
const distPath = joinImpl(baseDir, '..', 'dist', 'cli.mjs')
|
| 12 |
+
return pathToFileURL(distPath).href
|
| 13 |
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import assert from 'node:assert/strict'
|
| 2 |
+
import test from 'node:test'
|
| 3 |
+
|
| 4 |
+
import { getDistImportSpecifier } from './import-specifier.mjs'
|
| 5 |
+
|
| 6 |
+
test('builds a file URL import specifier for dist/cli.mjs', () => {
|
| 7 |
+
const specifier = getDistImportSpecifier('C:\\repo\\bin')
|
| 8 |
+
|
| 9 |
+
assert.equal(
|
| 10 |
+
specifier,
|
| 11 |
+
'file:///C:/repo/dist/cli.mjs',
|
| 12 |
+
)
|
| 13 |
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env node
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* OpenClaude β Claude Code with any LLM
|
| 5 |
+
*
|
| 6 |
+
* If dist/cli.mjs exists (built), run that.
|
| 7 |
+
* Otherwise, tell the user to build first or use `bun run dev`.
|
| 8 |
+
*/
|
| 9 |
+
|
| 10 |
+
import { existsSync } from 'fs'
|
| 11 |
+
import { join, dirname } from 'path'
|
| 12 |
+
import { fileURLToPath, pathToFileURL } from 'url'
|
| 13 |
+
|
| 14 |
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
| 15 |
+
const distPath = join(__dirname, '..', 'dist', 'cli.mjs')
|
| 16 |
+
|
| 17 |
+
if (existsSync(distPath)) {
|
| 18 |
+
await import(pathToFileURL(distPath).href)
|
| 19 |
+
} else {
|
| 20 |
+
console.error(`
|
| 21 |
+
openclaude: dist/cli.mjs not found.
|
| 22 |
+
|
| 23 |
+
Build first:
|
| 24 |
+
bun run build
|
| 25 |
+
|
| 26 |
+
Or run directly with Bun:
|
| 27 |
+
bun run dev
|
| 28 |
+
|
| 29 |
+
See README.md for setup instructions.
|
| 30 |
+
`)
|
| 31 |
+
process.exit(1)
|
| 32 |
+
}
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# OpenClaude Advanced Setup
|
| 2 |
+
|
| 3 |
+
This guide is for users who want source builds, Bun workflows, provider profiles, diagnostics, or more control over runtime behavior.
|
| 4 |
+
|
| 5 |
+
## Install Options
|
| 6 |
+
|
| 7 |
+
### Option A: npm
|
| 8 |
+
|
| 9 |
+
```bash
|
| 10 |
+
npm install -g @gitlawb/openclaude
|
| 11 |
+
```
|
| 12 |
+
|
| 13 |
+
### Option B: From source with Bun
|
| 14 |
+
|
| 15 |
+
Use Bun `1.3.11` or newer for source builds on Windows. Older Bun versions can fail during `bun run build`.
|
| 16 |
+
|
| 17 |
+
```bash
|
| 18 |
+
git clone https://node.gitlawb.com/z6MkqDnb7Siv3Cwj7pGJq4T5EsUisECqR8KpnDLwcaZq5TPr/openclaude.git
|
| 19 |
+
cd openclaude
|
| 20 |
+
|
| 21 |
+
bun install
|
| 22 |
+
bun run build
|
| 23 |
+
npm link
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
### Option C: Run directly with Bun
|
| 27 |
+
|
| 28 |
+
```bash
|
| 29 |
+
git clone https://node.gitlawb.com/z6MkqDnb7Siv3Cwj7pGJq4T5EsUisECqR8KpnDLwcaZq5TPr/openclaude.git
|
| 30 |
+
cd openclaude
|
| 31 |
+
|
| 32 |
+
bun install
|
| 33 |
+
bun run dev
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
## Provider Examples
|
| 37 |
+
|
| 38 |
+
### OpenAI
|
| 39 |
+
|
| 40 |
+
```bash
|
| 41 |
+
export CLAUDE_CODE_USE_OPENAI=1
|
| 42 |
+
export OPENAI_API_KEY=sk-...
|
| 43 |
+
export OPENAI_MODEL=gpt-4o
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
### Codex via ChatGPT auth
|
| 47 |
+
|
| 48 |
+
`codexplan` maps to GPT-5.4 on the Codex backend with high reasoning.
|
| 49 |
+
`codexspark` maps to GPT-5.3 Codex Spark for faster loops.
|
| 50 |
+
|
| 51 |
+
If you use the in-app provider wizard, choose `Codex OAuth` to open ChatGPT sign-in in your browser and let OpenClaude store Codex credentials securely.
|
| 52 |
+
|
| 53 |
+
If you already use the Codex CLI, OpenClaude reads `~/.codex/auth.json` automatically. You can also point it elsewhere with `CODEX_AUTH_JSON_PATH` or override the token directly with `CODEX_API_KEY`.
|
| 54 |
+
|
| 55 |
+
```bash
|
| 56 |
+
export CLAUDE_CODE_USE_OPENAI=1
|
| 57 |
+
export OPENAI_MODEL=codexplan
|
| 58 |
+
|
| 59 |
+
# optional if you do not already have ~/.codex/auth.json
|
| 60 |
+
export CODEX_API_KEY=...
|
| 61 |
+
|
| 62 |
+
openclaude
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
### DeepSeek
|
| 66 |
+
|
| 67 |
+
```bash
|
| 68 |
+
export CLAUDE_CODE_USE_OPENAI=1
|
| 69 |
+
export OPENAI_API_KEY=sk-...
|
| 70 |
+
export OPENAI_BASE_URL=https://api.deepseek.com/v1
|
| 71 |
+
export OPENAI_MODEL=deepseek-chat
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
### Google Gemini via OpenRouter
|
| 75 |
+
|
| 76 |
+
```bash
|
| 77 |
+
export CLAUDE_CODE_USE_OPENAI=1
|
| 78 |
+
export OPENAI_API_KEY=sk-or-...
|
| 79 |
+
export OPENAI_BASE_URL=https://openrouter.ai/api/v1
|
| 80 |
+
export OPENAI_MODEL=google/gemini-2.0-flash-001
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
OpenRouter model availability changes over time. If a model stops working, try another current OpenRouter model before assuming the integration is broken.
|
| 84 |
+
|
| 85 |
+
### Ollama
|
| 86 |
+
|
| 87 |
+
Using `ollama launch` (recommended if you have Ollama installed):
|
| 88 |
+
|
| 89 |
+
```bash
|
| 90 |
+
ollama launch openclaude --model llama3.3:70b
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
This handles all environment setup automatically β no env vars needed. Works with any local or cloud model available in your Ollama instance.
|
| 94 |
+
|
| 95 |
+
Using environment variables manually:
|
| 96 |
+
|
| 97 |
+
```bash
|
| 98 |
+
ollama pull llama3.3:70b
|
| 99 |
+
|
| 100 |
+
export CLAUDE_CODE_USE_OPENAI=1
|
| 101 |
+
export OPENAI_BASE_URL=http://localhost:11434/v1
|
| 102 |
+
export OPENAI_MODEL=llama3.3:70b
|
| 103 |
+
```
|
| 104 |
+
|
| 105 |
+
### Atomic Chat (local, Apple Silicon)
|
| 106 |
+
|
| 107 |
+
```bash
|
| 108 |
+
export CLAUDE_CODE_USE_OPENAI=1
|
| 109 |
+
export OPENAI_BASE_URL=http://127.0.0.1:1337/v1
|
| 110 |
+
export OPENAI_MODEL=your-model-name
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
No API key is needed for Atomic Chat local models.
|
| 114 |
+
|
| 115 |
+
Or use the profile launcher:
|
| 116 |
+
|
| 117 |
+
```bash
|
| 118 |
+
bun run dev:atomic-chat
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
Download Atomic Chat from [atomic.chat](https://atomic.chat/). The app must be running with a model loaded before launching.
|
| 122 |
+
|
| 123 |
+
### LM Studio
|
| 124 |
+
|
| 125 |
+
```bash
|
| 126 |
+
export CLAUDE_CODE_USE_OPENAI=1
|
| 127 |
+
export OPENAI_BASE_URL=http://localhost:1234/v1
|
| 128 |
+
export OPENAI_MODEL=your-model-name
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
### Together AI
|
| 132 |
+
|
| 133 |
+
```bash
|
| 134 |
+
export CLAUDE_CODE_USE_OPENAI=1
|
| 135 |
+
export OPENAI_API_KEY=...
|
| 136 |
+
export OPENAI_BASE_URL=https://api.together.xyz/v1
|
| 137 |
+
export OPENAI_MODEL=meta-llama/Llama-3.3-70B-Instruct-Turbo
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
### Groq
|
| 141 |
+
|
| 142 |
+
```bash
|
| 143 |
+
export CLAUDE_CODE_USE_OPENAI=1
|
| 144 |
+
export OPENAI_API_KEY=gsk_...
|
| 145 |
+
export OPENAI_BASE_URL=https://api.groq.com/openai/v1
|
| 146 |
+
export OPENAI_MODEL=llama-3.3-70b-versatile
|
| 147 |
+
```
|
| 148 |
+
|
| 149 |
+
### Mistral
|
| 150 |
+
|
| 151 |
+
```bash
|
| 152 |
+
export CLAUDE_CODE_USE_MISTRAL=1
|
| 153 |
+
export MISTRAL_API_KEY=...
|
| 154 |
+
export MISTRAL_MODEL=mistral-large-latest
|
| 155 |
+
```
|
| 156 |
+
|
| 157 |
+
### Azure OpenAI
|
| 158 |
+
|
| 159 |
+
```bash
|
| 160 |
+
export CLAUDE_CODE_USE_OPENAI=1
|
| 161 |
+
export OPENAI_API_KEY=your-azure-key
|
| 162 |
+
export OPENAI_BASE_URL=https://your-resource.openai.azure.com/openai/deployments/your-deployment/v1
|
| 163 |
+
export OPENAI_MODEL=gpt-4o
|
| 164 |
+
```
|
| 165 |
+
|
| 166 |
+
## Environment Variables
|
| 167 |
+
|
| 168 |
+
| Variable | Required | Description |
|
| 169 |
+
|----------|----------|-------------|
|
| 170 |
+
| `CLAUDE_CODE_USE_OPENAI` | Yes | Set to `1` to enable the OpenAI provider |
|
| 171 |
+
| `OPENAI_API_KEY` | Yes* | Your API key (`*` not needed for local models like Ollama or Atomic Chat) |
|
| 172 |
+
| `OPENAI_MODEL` | Yes | Model name such as `gpt-4o`, `deepseek-chat`, or `llama3.3:70b` |
|
| 173 |
+
| `OPENAI_BASE_URL` | No | API endpoint, defaulting to `https://api.openai.com/v1` |
|
| 174 |
+
| `CODEX_API_KEY` | Codex only | Codex or ChatGPT access token override |
|
| 175 |
+
| `CODEX_AUTH_JSON_PATH` | Codex only | Path to a Codex CLI `auth.json` file |
|
| 176 |
+
| `CODEX_HOME` | Codex only | Alternative Codex home directory |
|
| 177 |
+
| `OPENCLAUDE_DISABLE_CO_AUTHORED_BY` | No | Suppress the default `Co-Authored-By` trailer in generated git commits |
|
| 178 |
+
|
| 179 |
+
You can also use `ANTHROPIC_MODEL` to override the model name. `OPENAI_MODEL` takes priority.
|
| 180 |
+
|
| 181 |
+
## Runtime Hardening
|
| 182 |
+
|
| 183 |
+
Use these commands to validate your setup and catch mistakes early:
|
| 184 |
+
|
| 185 |
+
```bash
|
| 186 |
+
# quick startup sanity check
|
| 187 |
+
bun run smoke
|
| 188 |
+
|
| 189 |
+
# validate provider env + reachability
|
| 190 |
+
bun run doctor:runtime
|
| 191 |
+
|
| 192 |
+
# print machine-readable runtime diagnostics
|
| 193 |
+
bun run doctor:runtime:json
|
| 194 |
+
|
| 195 |
+
# persist a diagnostics report to reports/doctor-runtime.json
|
| 196 |
+
bun run doctor:report
|
| 197 |
+
|
| 198 |
+
# full local hardening check (smoke + runtime doctor)
|
| 199 |
+
bun run hardening:check
|
| 200 |
+
|
| 201 |
+
# strict hardening (includes project-wide typecheck)
|
| 202 |
+
bun run hardening:strict
|
| 203 |
+
```
|
| 204 |
+
|
| 205 |
+
Notes:
|
| 206 |
+
|
| 207 |
+
- `doctor:runtime` fails fast if `CLAUDE_CODE_USE_OPENAI=1` with a placeholder key or a missing key for non-local providers.
|
| 208 |
+
- Local providers such as `http://localhost:11434/v1`, `http://10.0.0.1:11434/v1`, and `http://127.0.0.1:1337/v1` can run without `OPENAI_API_KEY`.
|
| 209 |
+
- Codex profiles validate `CODEX_API_KEY` or the Codex CLI auth file and probe `POST /responses` instead of `GET /models`.
|
| 210 |
+
|
| 211 |
+
## Provider Launch Profiles
|
| 212 |
+
|
| 213 |
+
Use profile launchers to avoid repeated environment setup:
|
| 214 |
+
|
| 215 |
+
```bash
|
| 216 |
+
# one-time profile bootstrap (prefer viable local Ollama, otherwise OpenAI)
|
| 217 |
+
bun run profile:init
|
| 218 |
+
|
| 219 |
+
# preview the best provider/model for your goal
|
| 220 |
+
bun run profile:recommend -- --goal coding --benchmark
|
| 221 |
+
|
| 222 |
+
# auto-apply the best available local/openai provider/model for your goal
|
| 223 |
+
bun run profile:auto -- --goal latency
|
| 224 |
+
|
| 225 |
+
# codex bootstrap (defaults to codexplan and ~/.codex/auth.json)
|
| 226 |
+
bun run profile:codex
|
| 227 |
+
|
| 228 |
+
# openai bootstrap with explicit key
|
| 229 |
+
bun run profile:init -- --provider openai --api-key sk-...
|
| 230 |
+
|
| 231 |
+
# ollama bootstrap with custom model
|
| 232 |
+
bun run profile:init -- --provider ollama --model llama3.1:8b
|
| 233 |
+
|
| 234 |
+
# ollama bootstrap with intelligent model auto-selection
|
| 235 |
+
bun run profile:init -- --provider ollama --goal coding
|
| 236 |
+
|
| 237 |
+
# atomic-chat bootstrap (auto-detects running model)
|
| 238 |
+
bun run profile:init -- --provider atomic-chat
|
| 239 |
+
|
| 240 |
+
# codex bootstrap with a fast model alias
|
| 241 |
+
bun run profile:init -- --provider codex --model codexspark
|
| 242 |
+
|
| 243 |
+
# launch using persisted profile (.openclaude-profile.json)
|
| 244 |
+
bun run dev:profile
|
| 245 |
+
|
| 246 |
+
# codex profile (uses CODEX_API_KEY or ~/.codex/auth.json)
|
| 247 |
+
bun run dev:codex
|
| 248 |
+
|
| 249 |
+
# OpenAI profile (requires OPENAI_API_KEY in your shell)
|
| 250 |
+
bun run dev:openai
|
| 251 |
+
|
| 252 |
+
# Ollama profile (defaults: localhost:11434, llama3.1:8b)
|
| 253 |
+
bun run dev:ollama
|
| 254 |
+
|
| 255 |
+
# Atomic Chat profile (Apple Silicon local LLMs at 127.0.0.1:1337)
|
| 256 |
+
bun run dev:atomic-chat
|
| 257 |
+
```
|
| 258 |
+
|
| 259 |
+
`profile:recommend` ranks installed Ollama models for `latency`, `balanced`, or `coding`, and `profile:auto` can persist the recommendation directly.
|
| 260 |
+
|
| 261 |
+
If no profile exists yet, `dev:profile` uses the same goal-aware defaults when picking the initial model.
|
| 262 |
+
|
| 263 |
+
Use `--provider ollama` when you want a local-only path. Auto mode falls back to OpenAI when no viable local chat model is installed.
|
| 264 |
+
|
| 265 |
+
Use `--provider atomic-chat` when you want Atomic Chat as the local Apple Silicon provider.
|
| 266 |
+
|
| 267 |
+
Use `profile:codex` or `--provider codex` when you want the ChatGPT Codex backend.
|
| 268 |
+
|
| 269 |
+
`dev:openai`, `dev:ollama`, `dev:atomic-chat`, and `dev:codex` run `doctor:runtime` first and only launch the app if checks pass.
|
| 270 |
+
|
| 271 |
+
For `dev:ollama`, make sure Ollama is running locally before launch.
|
| 272 |
+
|
| 273 |
+
For `dev:atomic-chat`, make sure Atomic Chat is running with a model loaded before launch.
|
|
@@ -0,0 +1,333 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hook Chains (Self-Healing Agent Mesh MVP)
|
| 2 |
+
|
| 3 |
+
Hook Chains provide an event-driven recovery layer for important workflow failures.
|
| 4 |
+
When a matching hook event occurs, OpenClaude evaluates declarative rules and can dispatch remediation actions such as:
|
| 5 |
+
|
| 6 |
+
- `spawn_fallback_agent`
|
| 7 |
+
- `notify_team`
|
| 8 |
+
- `warm_remote_capacity`
|
| 9 |
+
|
| 10 |
+
## Disabled-By-Default Rollout
|
| 11 |
+
|
| 12 |
+
> **Rollout recommendation:** keep Hook Chains disabled until you validate rules in your environment.
|
| 13 |
+
>
|
| 14 |
+
> - Set top-level config to `"enabled": false` initially.
|
| 15 |
+
> - Enable per environment when ready.
|
| 16 |
+
> - Dispatch is gated by `feature('HOOK_CHAINS')`.
|
| 17 |
+
> - Env gate defaults to off unless `CLAUDE_CODE_ENABLE_HOOK_CHAINS=1` is set.
|
| 18 |
+
|
| 19 |
+
This keeps existing workflows unchanged while you tune guard windows and action behavior.
|
| 20 |
+
|
| 21 |
+
## Feature Overview
|
| 22 |
+
|
| 23 |
+
Hook Chains are loaded from a deterministic config file and evaluated on dispatched hook events.
|
| 24 |
+
|
| 25 |
+
MVP runtime trigger wiring:
|
| 26 |
+
|
| 27 |
+
- `PostToolUseFailure` hooks dispatch Hook Chains with outcome `failed`.
|
| 28 |
+
- `TaskCompleted` hooks dispatch Hook Chains with outcome:
|
| 29 |
+
- `success` when completion hooks did not block.
|
| 30 |
+
- `failed` when completion hooks returned blocking errors or prevented continuation.
|
| 31 |
+
|
| 32 |
+
Default config path:
|
| 33 |
+
|
| 34 |
+
- `.openclaude/hook-chains.json`
|
| 35 |
+
|
| 36 |
+
Override path:
|
| 37 |
+
|
| 38 |
+
- `CLAUDE_CODE_HOOK_CHAINS_CONFIG_PATH=/abs/or/relative/path/to/hook-chains.json`
|
| 39 |
+
|
| 40 |
+
Global gate:
|
| 41 |
+
|
| 42 |
+
- `feature('HOOK_CHAINS')` must be enabled in the build
|
| 43 |
+
- `CLAUDE_CODE_ENABLE_HOOK_CHAINS=0|1` (defaults to disabled when unset)
|
| 44 |
+
|
| 45 |
+
## Safety Guarantees
|
| 46 |
+
|
| 47 |
+
The runtime is intentionally conservative:
|
| 48 |
+
|
| 49 |
+
- **Depth guard:** chain dispatch is blocked when `chainDepth >= maxChainDepth`.
|
| 50 |
+
- **Rule cooldown:** each rule can only re-fire after cooldown expires.
|
| 51 |
+
- **Dedup window:** identical event/action combinations are suppressed for a window.
|
| 52 |
+
- **Abort-safe behavior:** if the current signal is aborted, actions skip safely.
|
| 53 |
+
- **Policy-aware remote warm:** `warm_remote_capacity` skips when remote sessions are policy denied.
|
| 54 |
+
- **Bridge inactive no-op:** `warm_remote_capacity` safely skips when no active bridge handle exists.
|
| 55 |
+
- **Missing team context safety:** `notify_team` skips with structured reason if no team context/team file is available.
|
| 56 |
+
- **Fallback launcher safety:** `spawn_fallback_agent` fails with a structured reason when launch permissions/context are unavailable.
|
| 57 |
+
|
| 58 |
+
## Configuration Schema Reference
|
| 59 |
+
|
| 60 |
+
Top-level object:
|
| 61 |
+
|
| 62 |
+
```json
|
| 63 |
+
{
|
| 64 |
+
"version": 1,
|
| 65 |
+
"enabled": true,
|
| 66 |
+
"maxChainDepth": 2,
|
| 67 |
+
"defaultCooldownMs": 30000,
|
| 68 |
+
"defaultDedupWindowMs": 30000,
|
| 69 |
+
"rules": []
|
| 70 |
+
}
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
### Top-Level Fields
|
| 74 |
+
|
| 75 |
+
| Field | Type | Required | Notes |
|
| 76 |
+
|---|---|---:|---|
|
| 77 |
+
| `version` | `1` | No | Defaults to `1`. |
|
| 78 |
+
| `enabled` | `boolean` | No | Global feature switch for this config file. |
|
| 79 |
+
| `maxChainDepth` | `integer` | No | Global depth guard (default `2`, max `10`). |
|
| 80 |
+
| `defaultCooldownMs` | `integer` | No | Default rule cooldown in ms (default `30000`). |
|
| 81 |
+
| `defaultDedupWindowMs` | `integer` | No | Default action dedup window in ms (default `30000`). |
|
| 82 |
+
| `rules` | `HookChainRule[]` | No | Defaults to `[]`. May be omitted or empty; when no rules are present, dispatch is a no-op and returns `enabled: false`. |
|
| 83 |
+
|
| 84 |
+
> **Note:** An empty ruleset is valid and can be used to keep Hook Chains configured but effectively disabled until rules are added.
|
| 85 |
+
### Rule Object (`HookChainRule`)
|
| 86 |
+
|
| 87 |
+
```json
|
| 88 |
+
{
|
| 89 |
+
"id": "task-failure-recovery",
|
| 90 |
+
"enabled": true,
|
| 91 |
+
"trigger": {
|
| 92 |
+
"event": "TaskCompleted",
|
| 93 |
+
"outcome": "failed"
|
| 94 |
+
},
|
| 95 |
+
"condition": {
|
| 96 |
+
"toolNames": ["Edit"],
|
| 97 |
+
"taskStatuses": ["failed"],
|
| 98 |
+
"errorIncludes": ["timeout", "permission denied"],
|
| 99 |
+
"eventFieldEquals": {
|
| 100 |
+
"meta.source": "scheduler"
|
| 101 |
+
}
|
| 102 |
+
},
|
| 103 |
+
"cooldownMs": 60000,
|
| 104 |
+
"dedupWindowMs": 30000,
|
| 105 |
+
"maxDepth": 2,
|
| 106 |
+
"actions": []
|
| 107 |
+
}
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
| Field | Type | Required | Notes |
|
| 111 |
+
|---|---|---:|---|
|
| 112 |
+
| `id` | `string` | Yes | Stable identifier used in telemetry/guards. |
|
| 113 |
+
| `enabled` | `boolean` | No | Per-rule switch. |
|
| 114 |
+
| `trigger.event` | `HookEvent` | Yes | Event name to match. |
|
| 115 |
+
| `trigger.outcome` | `"success"|"failed"|"timeout"|"unknown"` | No | Single outcome matcher. |
|
| 116 |
+
| `trigger.outcomes` | `Outcome[]` | No | Multi-outcome matcher. Use either `outcome` or `outcomes`. |
|
| 117 |
+
| `condition` | `object` | No | Optional extra matching constraints. |
|
| 118 |
+
| `cooldownMs` | `integer` | No | Overrides global cooldown for this rule. |
|
| 119 |
+
| `dedupWindowMs` | `integer` | No | Overrides global dedup for this rule. |
|
| 120 |
+
| `maxDepth` | `integer` | No | Per-rule depth cap. |
|
| 121 |
+
| `actions` | `HookChainAction[]` | Yes | One or more actions to execute in order. |
|
| 122 |
+
|
| 123 |
+
### Condition Fields
|
| 124 |
+
|
| 125 |
+
| Field | Type | Notes |
|
| 126 |
+
|---|---|---|
|
| 127 |
+
| `toolNames` | `string[]` | Matches `tool_name` / `toolName` in event payload. |
|
| 128 |
+
| `taskStatuses` | `string[]` | Matches `task_status` / `taskStatus` / `status`. |
|
| 129 |
+
| `errorIncludes` | `string[]` | Case-insensitive substring match against `error` / `reason` / `message`. |
|
| 130 |
+
| `eventFieldEquals` | `Record<string, string\|number\|boolean>` | Dot-path equality against payload (example: `"meta.source": "scheduler"`). |
|
| 131 |
+
|
| 132 |
+
### Actions
|
| 133 |
+
|
| 134 |
+
#### `spawn_fallback_agent`
|
| 135 |
+
|
| 136 |
+
```json
|
| 137 |
+
{
|
| 138 |
+
"type": "spawn_fallback_agent",
|
| 139 |
+
"id": "fallback-1",
|
| 140 |
+
"enabled": true,
|
| 141 |
+
"dedupWindowMs": 30000,
|
| 142 |
+
"description": "Fallback recovery for failed task",
|
| 143 |
+
"promptTemplate": "Recover task ${TASK_SUBJECT}. Event=${EVENT_NAME}, outcome=${OUTCOME}, error=${ERROR}. Payload=${PAYLOAD_JSON}",
|
| 144 |
+
"agentType": "general-purpose",
|
| 145 |
+
"model": "sonnet"
|
| 146 |
+
}
|
| 147 |
+
```
|
| 148 |
+
|
| 149 |
+
#### `notify_team`
|
| 150 |
+
|
| 151 |
+
```json
|
| 152 |
+
{
|
| 153 |
+
"type": "notify_team",
|
| 154 |
+
"id": "notify-ops",
|
| 155 |
+
"enabled": true,
|
| 156 |
+
"dedupWindowMs": 30000,
|
| 157 |
+
"teamName": "mesh-team",
|
| 158 |
+
"recipients": ["*"],
|
| 159 |
+
"summary": "Hook chain ${RULE_ID} fired",
|
| 160 |
+
"messageTemplate": "Event=${EVENT_NAME} outcome=${OUTCOME}\nTask=${TASK_ID}\nError=${ERROR}\nPayload=${PAYLOAD_JSON}"
|
| 161 |
+
}
|
| 162 |
+
```
|
| 163 |
+
|
| 164 |
+
#### `warm_remote_capacity`
|
| 165 |
+
|
| 166 |
+
```json
|
| 167 |
+
{
|
| 168 |
+
"type": "warm_remote_capacity",
|
| 169 |
+
"id": "warm-bridge",
|
| 170 |
+
"enabled": true,
|
| 171 |
+
"dedupWindowMs": 60000,
|
| 172 |
+
"createDefaultEnvironmentIfMissing": false
|
| 173 |
+
}
|
| 174 |
+
```
|
| 175 |
+
|
| 176 |
+
## Complete Example Configs
|
| 177 |
+
|
| 178 |
+
### 1) Retry via Fallback Agent
|
| 179 |
+
|
| 180 |
+
```json
|
| 181 |
+
{
|
| 182 |
+
"version": 1,
|
| 183 |
+
"enabled": true,
|
| 184 |
+
"maxChainDepth": 2,
|
| 185 |
+
"defaultCooldownMs": 30000,
|
| 186 |
+
"defaultDedupWindowMs": 30000,
|
| 187 |
+
"rules": [
|
| 188 |
+
{
|
| 189 |
+
"id": "retry-task-via-fallback",
|
| 190 |
+
"trigger": {
|
| 191 |
+
"event": "TaskCompleted",
|
| 192 |
+
"outcome": "failed"
|
| 193 |
+
},
|
| 194 |
+
"cooldownMs": 60000,
|
| 195 |
+
"actions": [
|
| 196 |
+
{
|
| 197 |
+
"type": "spawn_fallback_agent",
|
| 198 |
+
"id": "spawn-retry-agent",
|
| 199 |
+
"description": "Retry failed task with fallback agent",
|
| 200 |
+
"promptTemplate": "A task failed. Recover it safely.\nTask=${TASK_SUBJECT}\nDescription=${TASK_DESCRIPTION}\nError=${ERROR}\nPayload=${PAYLOAD_JSON}",
|
| 201 |
+
"agentType": "general-purpose",
|
| 202 |
+
"model": "sonnet"
|
| 203 |
+
}
|
| 204 |
+
]
|
| 205 |
+
}
|
| 206 |
+
]
|
| 207 |
+
}
|
| 208 |
+
```
|
| 209 |
+
|
| 210 |
+
### 2) Notify Only
|
| 211 |
+
|
| 212 |
+
```json
|
| 213 |
+
{
|
| 214 |
+
"version": 1,
|
| 215 |
+
"enabled": true,
|
| 216 |
+
"maxChainDepth": 2,
|
| 217 |
+
"defaultCooldownMs": 30000,
|
| 218 |
+
"defaultDedupWindowMs": 30000,
|
| 219 |
+
"rules": [
|
| 220 |
+
{
|
| 221 |
+
"id": "notify-on-tool-failure",
|
| 222 |
+
"trigger": {
|
| 223 |
+
"event": "PostToolUseFailure",
|
| 224 |
+
"outcome": "failed"
|
| 225 |
+
},
|
| 226 |
+
"condition": {
|
| 227 |
+
"toolNames": ["Edit", "Write", "Bash"]
|
| 228 |
+
},
|
| 229 |
+
"actions": [
|
| 230 |
+
{
|
| 231 |
+
"type": "notify_team",
|
| 232 |
+
"id": "notify-team-failure",
|
| 233 |
+
"recipients": ["*"],
|
| 234 |
+
"summary": "Tool failure detected",
|
| 235 |
+
"messageTemplate": "Tool failure detected.\nEvent=${EVENT_NAME} outcome=${OUTCOME}\nError=${ERROR}\nPayload=${PAYLOAD_JSON}"
|
| 236 |
+
}
|
| 237 |
+
]
|
| 238 |
+
}
|
| 239 |
+
]
|
| 240 |
+
}
|
| 241 |
+
```
|
| 242 |
+
|
| 243 |
+
### 3) Combined Fallback + Notify + Bridge Warm
|
| 244 |
+
|
| 245 |
+
```json
|
| 246 |
+
{
|
| 247 |
+
"version": 1,
|
| 248 |
+
"enabled": true,
|
| 249 |
+
"maxChainDepth": 2,
|
| 250 |
+
"defaultCooldownMs": 45000,
|
| 251 |
+
"defaultDedupWindowMs": 30000,
|
| 252 |
+
"rules": [
|
| 253 |
+
{
|
| 254 |
+
"id": "full-recovery-chain",
|
| 255 |
+
"trigger": {
|
| 256 |
+
"event": "TaskCompleted",
|
| 257 |
+
"outcomes": ["failed", "timeout"]
|
| 258 |
+
},
|
| 259 |
+
"condition": {
|
| 260 |
+
"errorIncludes": ["timeout", "capacity", "connection"]
|
| 261 |
+
},
|
| 262 |
+
"cooldownMs": 90000,
|
| 263 |
+
"actions": [
|
| 264 |
+
{
|
| 265 |
+
"type": "spawn_fallback_agent",
|
| 266 |
+
"id": "fallback-agent",
|
| 267 |
+
"description": "Recover failed task execution",
|
| 268 |
+
"promptTemplate": "Recover failed task and produce a concise fix summary.\nTask=${TASK_SUBJECT}\nError=${ERROR}\nPayload=${PAYLOAD_JSON}"
|
| 269 |
+
},
|
| 270 |
+
{
|
| 271 |
+
"type": "notify_team",
|
| 272 |
+
"id": "notify-team",
|
| 273 |
+
"recipients": ["*"],
|
| 274 |
+
"summary": "Recovery chain triggered",
|
| 275 |
+
"messageTemplate": "Recovery chain ${RULE_ID} fired.\nOutcome=${OUTCOME}\nTask=${TASK_SUBJECT}\nError=${ERROR}"
|
| 276 |
+
},
|
| 277 |
+
{
|
| 278 |
+
"type": "warm_remote_capacity",
|
| 279 |
+
"id": "warm-capacity",
|
| 280 |
+
"createDefaultEnvironmentIfMissing": false
|
| 281 |
+
}
|
| 282 |
+
]
|
| 283 |
+
}
|
| 284 |
+
]
|
| 285 |
+
}
|
| 286 |
+
```
|
| 287 |
+
|
| 288 |
+
## Template Variables
|
| 289 |
+
|
| 290 |
+
The following placeholders are supported by `promptTemplate`, `summary`, and `messageTemplate`:
|
| 291 |
+
|
| 292 |
+
- `${EVENT_NAME}`
|
| 293 |
+
- `${OUTCOME}`
|
| 294 |
+
- `${RULE_ID}`
|
| 295 |
+
- `${TASK_SUBJECT}`
|
| 296 |
+
- `${TASK_DESCRIPTION}`
|
| 297 |
+
- `${TASK_ID}`
|
| 298 |
+
- `${ERROR}`
|
| 299 |
+
- `${PAYLOAD_JSON}`
|
| 300 |
+
|
| 301 |
+
## Troubleshooting
|
| 302 |
+
|
| 303 |
+
### Rule never triggers
|
| 304 |
+
|
| 305 |
+
- Verify `trigger.event` and `trigger.outcome`/`trigger.outcomes` exactly match dispatched event data.
|
| 306 |
+
- Check `condition` filters (especially `toolNames` and `eventFieldEquals` dot-path keys).
|
| 307 |
+
- Confirm the config file is valid JSON and schema-valid.
|
| 308 |
+
|
| 309 |
+
### Actions show as skipped
|
| 310 |
+
|
| 311 |
+
Common skip reasons:
|
| 312 |
+
|
| 313 |
+
- `action disabled`
|
| 314 |
+
- `rule cooldown active ...`
|
| 315 |
+
- `dedup window active ...`
|
| 316 |
+
- `max chain depth reached ...`
|
| 317 |
+
- `No team context is available ...`
|
| 318 |
+
- `Team file not found ...`
|
| 319 |
+
- `Remote sessions are blocked by policy`
|
| 320 |
+
- `Bridge is not active; warm_remote_capacity is a safe no-op`
|
| 321 |
+
- `No fallback agent launcher is registered in runtime context`
|
| 322 |
+
|
| 323 |
+
### Config changes not reflected
|
| 324 |
+
|
| 325 |
+
- Loader uses memoization by file mtime/size.
|
| 326 |
+
- Ensure your editor writes the file fully and updates mtime.
|
| 327 |
+
- If needed, force reload from the caller side with `forceReloadConfig: true`.
|
| 328 |
+
|
| 329 |
+
### Existing workflows changed unexpectedly
|
| 330 |
+
|
| 331 |
+
- Set `"enabled": false` at top-level.
|
| 332 |
+
- Or globally disable with `CLAUDE_CODE_ENABLE_HOOK_CHAINS=0`.
|
| 333 |
+
- Re-enable gradually after validating one rule at a time.
|
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# LiteLLM Setup
|
| 2 |
+
|
| 3 |
+
OpenClaude can connect to LiteLLM through LiteLLM's OpenAI-compatible proxy.
|
| 4 |
+
|
| 5 |
+
## Overview
|
| 6 |
+
|
| 7 |
+
LiteLLM is an open-source LLM gateway that provides a unified API to 100+ model providers. By running the LiteLLM Proxy, you can route OpenClaude requests through LiteLLM to access any of its supported providers β all while using OpenClaude's existing OpenAI-compatible provider path.
|
| 8 |
+
|
| 9 |
+
## Prerequisites
|
| 10 |
+
|
| 11 |
+
- LiteLLM installed (`pip install litellm[proxy]`)
|
| 12 |
+
- A `litellm_config.yaml` or equivalent LiteLLM configuration
|
| 13 |
+
- LiteLLM Proxy running on a local or remote port
|
| 14 |
+
|
| 15 |
+
## 1. Start the LiteLLM Proxy
|
| 16 |
+
|
| 17 |
+
### Basic installation
|
| 18 |
+
|
| 19 |
+
```bash
|
| 20 |
+
pip install litellm[proxy]
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
### Configure LiteLLM
|
| 24 |
+
|
| 25 |
+
Create a `litellm_config.yaml` with your desired model aliases:
|
| 26 |
+
|
| 27 |
+
```yaml
|
| 28 |
+
model_list:
|
| 29 |
+
- model_name: gpt-4o
|
| 30 |
+
litellm_params:
|
| 31 |
+
model: openai/gpt-4o
|
| 32 |
+
api_key: os.environ/OPENAI_API_KEY
|
| 33 |
+
|
| 34 |
+
- model_name: claude-sonnet-4
|
| 35 |
+
litellm_params:
|
| 36 |
+
model: anthropic/claude-sonnet-4-5-20250929
|
| 37 |
+
api_key: os.environ/ANTHROPIC_API_KEY
|
| 38 |
+
|
| 39 |
+
- model_name: gemini-2.5-flash
|
| 40 |
+
litellm_params:
|
| 41 |
+
model: gemini/gemini-2.5-flash
|
| 42 |
+
api_key: os.environ/GEMINI_API_KEY
|
| 43 |
+
|
| 44 |
+
- model_name: llama-3.3-70b
|
| 45 |
+
litellm_params:
|
| 46 |
+
model: together_ai/meta-llama/Llama-3.3-70B-Instruct-Turbo
|
| 47 |
+
api_key: os.environ/TOGETHER_API_KEY
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
### Run the proxy
|
| 51 |
+
|
| 52 |
+
```bash
|
| 53 |
+
litellm --config litellm_config.yaml --port 4000
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
The proxy will start at `http://localhost:4000` by default.
|
| 57 |
+
|
| 58 |
+
## 2. Point OpenClaude to LiteLLM
|
| 59 |
+
|
| 60 |
+
### Option A: Environment Variables
|
| 61 |
+
|
| 62 |
+
```bash
|
| 63 |
+
export CLAUDE_CODE_USE_OPENAI=1
|
| 64 |
+
export OPENAI_BASE_URL=http://localhost:4000
|
| 65 |
+
export OPENAI_API_KEY=<your-master-key-or-placeholder>
|
| 66 |
+
export OPENAI_MODEL=<your-litellm-model-alias>
|
| 67 |
+
openclaude
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
Replace `<your-litellm-model-alias>` with a model name from your `litellm_config.yaml` (e.g., `gpt-4o`, `claude-sonnet-4`, `gemini-2.5-flash`).
|
| 71 |
+
|
| 72 |
+
### Option B: Using /provider
|
| 73 |
+
|
| 74 |
+
1. Run `openclaude`
|
| 75 |
+
2. Type `/provider` to open the provider setup flow
|
| 76 |
+
3. Choose the **OpenAI-compatible** option
|
| 77 |
+
4. When prompted for the API key, enter the key required by your LiteLLM proxy
|
| 78 |
+
If your local LiteLLM setup does not enforce auth, you may still need to enter a placeholder value
|
| 79 |
+
- 5. When prompted for the base URL, enter `http://localhost:4000`
|
| 80 |
+
6. 6. When prompted for the model, enter the LiteLLM model name or alias you configured
|
| 81 |
+
7. 7. Save the provider configuration
|
| 82 |
+
|
| 83 |
+
## 3. Example LiteLLM Configs
|
| 84 |
+
|
| 85 |
+
### Multi-provider routing with spend tracking
|
| 86 |
+
|
| 87 |
+
```yaml
|
| 88 |
+
model_list:
|
| 89 |
+
- model_name: gpt-4o
|
| 90 |
+
litellm_params:
|
| 91 |
+
model: openai/gpt-4o
|
| 92 |
+
api_key: os.environ/OPENAI_API_KEY
|
| 93 |
+
|
| 94 |
+
- model_name: claude-sonnet-4
|
| 95 |
+
litellm_params:
|
| 96 |
+
model: anthropic/claude-sonnet-4-5-20250929
|
| 97 |
+
api_key: os.environ/ANTHROPIC_API_KEY
|
| 98 |
+
|
| 99 |
+
- model_name: deepseek-chat
|
| 100 |
+
litellm_params:
|
| 101 |
+
model: deepseek/deepseek-chat
|
| 102 |
+
api_key: os.environ/DEEPSEEK_API_KEY
|
| 103 |
+
|
| 104 |
+
litellm_settings:
|
| 105 |
+
set_verbose: false
|
| 106 |
+
num_retries: 3
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
### With a master key for auth
|
| 110 |
+
|
| 111 |
+
```bash
|
| 112 |
+
# Start proxy with a master key
|
| 113 |
+
litellm --config litellm_config.yaml --port 4000 --master_key sk-my-master-key
|
| 114 |
+
|
| 115 |
+
# Connect OpenClaude
|
| 116 |
+
export CLAUDE_CODE_USE_OPENAI=1
|
| 117 |
+
export OPENAI_BASE_URL=http://localhost:4000
|
| 118 |
+
export OPENAI_API_KEY=sk-my-master-key
|
| 119 |
+
export OPENAI_MODEL=gpt-4o
|
| 120 |
+
openclaude
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
## 4. Notes
|
| 124 |
+
|
| 125 |
+
- `OPENAI_MODEL` must match the **LiteLLM model alias** defined in your config, not the upstream raw provider model name.
|
| 126 |
+
- If your proxy requires authentication, use the proxy key (or `master_key`) in `OPENAI_API_KEY`.
|
| 127 |
+
- LiteLLM's OpenAI-compatible endpoint accepts the same request format as OpenAI, so OpenClaude works without any code changes.
|
| 128 |
+
- You can switch between any provider configured in LiteLLM by simply changing the `OPENAI_MODEL` value β no need to reconfigure OpenClaude.
|
| 129 |
+
|
| 130 |
+
## 5. Troubleshooting
|
| 131 |
+
|
| 132 |
+
| Issue | Likely Cause | Fix |
|
| 133 |
+
|-------|--------------|-----|
|
| 134 |
+
| 404 or Model Not Found | Model alias doesn't exist in LiteLLM config | Verify the `model_name` in `litellm_config.yaml` matches `OPENAI_MODEL` |
|
| 135 |
+
| Connection Refused | LiteLLM proxy isn't running | Start the proxy with `litellm --config litellm_config.yaml --port 4000` |
|
| 136 |
+
| Auth Failed | Missing or wrong `master_key` | Set the correct key in `OPENAI_API_KEY` |
|
| 137 |
+
| Upstream provider error | The backend provider key is missing or invalid | Ensure the upstream API key (e.g., `OPENAI_API_KEY`) is set in your LiteLLM proxy process environment |
|
| 138 |
+
| Tools fail but chat works | The selected model has weak function/tool calling support | Switch to a model with strong tool support (e.g., GPT-4o, Claude Sonnet) |
|
| 139 |
+
|
| 140 |
+
## 6. Resources
|
| 141 |
+
|
| 142 |
+
- [LiteLLM Proxy Docs](https://docs.litellm.ai/docs/proxy/quick_start)
|
| 143 |
+
- [LiteLLM Provider List](https://docs.litellm.ai/docs/providers)
|
| 144 |
+
- [LiteLLM OpenAI-Compatible Endpoints](https://docs.litellm.ai/docs/proxy/openai_compatible_proxy)
|
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# OpenClaude for Non-Technical Users
|
| 2 |
+
|
| 3 |
+
This guide is for people who want the easiest setup path.
|
| 4 |
+
|
| 5 |
+
You do not need to build from source. You do not need Bun. You do not need to understand the full codebase.
|
| 6 |
+
|
| 7 |
+
If you can copy and paste commands into a terminal, you can set this up.
|
| 8 |
+
|
| 9 |
+
## What OpenClaude Does
|
| 10 |
+
|
| 11 |
+
OpenClaude lets you use an AI coding assistant with different model providers such as:
|
| 12 |
+
|
| 13 |
+
- OpenAI
|
| 14 |
+
- DeepSeek
|
| 15 |
+
- Gemini
|
| 16 |
+
- Ollama
|
| 17 |
+
- Codex
|
| 18 |
+
|
| 19 |
+
For most first-time users, OpenAI is the easiest option.
|
| 20 |
+
|
| 21 |
+
## Before You Start
|
| 22 |
+
|
| 23 |
+
You need:
|
| 24 |
+
|
| 25 |
+
1. Node.js 20 or newer installed
|
| 26 |
+
2. A terminal window
|
| 27 |
+
3. An API key from your provider, unless you are using a local model like Ollama
|
| 28 |
+
|
| 29 |
+
## Fastest Path
|
| 30 |
+
|
| 31 |
+
1. Install OpenClaude with npm
|
| 32 |
+
2. Set 3 environment variables
|
| 33 |
+
3. Run `openclaude`
|
| 34 |
+
|
| 35 |
+
## Choose Your Operating System
|
| 36 |
+
|
| 37 |
+
- Windows: [Windows Quick Start](quick-start-windows.md)
|
| 38 |
+
- macOS / Linux: [macOS / Linux Quick Start](quick-start-mac-linux.md)
|
| 39 |
+
|
| 40 |
+
## Which Provider Should You Choose?
|
| 41 |
+
|
| 42 |
+
### OpenAI
|
| 43 |
+
|
| 44 |
+
Choose this if:
|
| 45 |
+
|
| 46 |
+
- you want the easiest setup
|
| 47 |
+
- you already have an OpenAI API key
|
| 48 |
+
|
| 49 |
+
### Ollama
|
| 50 |
+
|
| 51 |
+
Choose this if:
|
| 52 |
+
|
| 53 |
+
- you want to run models locally
|
| 54 |
+
- you do not want to depend on a cloud API for testing
|
| 55 |
+
|
| 56 |
+
### Codex
|
| 57 |
+
|
| 58 |
+
Choose this if:
|
| 59 |
+
|
| 60 |
+
- you already use the Codex CLI
|
| 61 |
+
- you already have Codex or ChatGPT auth configured
|
| 62 |
+
|
| 63 |
+
## What Success Looks Like
|
| 64 |
+
|
| 65 |
+
After you run `openclaude`, the CLI should start and wait for your prompt.
|
| 66 |
+
|
| 67 |
+
At that point, you can ask it to:
|
| 68 |
+
|
| 69 |
+
- explain code
|
| 70 |
+
- edit files
|
| 71 |
+
- run commands
|
| 72 |
+
- review changes
|
| 73 |
+
|
| 74 |
+
## Common Problems
|
| 75 |
+
|
| 76 |
+
### `openclaude` command not found
|
| 77 |
+
|
| 78 |
+
Cause:
|
| 79 |
+
|
| 80 |
+
- npm installed the package, but your terminal has not refreshed yet
|
| 81 |
+
|
| 82 |
+
Fix:
|
| 83 |
+
|
| 84 |
+
1. Close the terminal
|
| 85 |
+
2. Open a new terminal
|
| 86 |
+
3. Run `openclaude` again
|
| 87 |
+
|
| 88 |
+
### Invalid API key
|
| 89 |
+
|
| 90 |
+
Cause:
|
| 91 |
+
|
| 92 |
+
- the key is wrong, expired, or copied incorrectly
|
| 93 |
+
|
| 94 |
+
Fix:
|
| 95 |
+
|
| 96 |
+
1. Get a fresh key from your provider
|
| 97 |
+
2. Paste it again carefully
|
| 98 |
+
3. Re-run `openclaude`
|
| 99 |
+
|
| 100 |
+
### Ollama not working
|
| 101 |
+
|
| 102 |
+
Cause:
|
| 103 |
+
|
| 104 |
+
- Ollama is not installed or not running
|
| 105 |
+
|
| 106 |
+
Fix:
|
| 107 |
+
|
| 108 |
+
1. Install Ollama from `https://ollama.com/download`
|
| 109 |
+
2. Start Ollama
|
| 110 |
+
3. Try again
|
| 111 |
+
|
| 112 |
+
## Want More Control?
|
| 113 |
+
|
| 114 |
+
If you want source builds, advanced provider profiles, diagnostics, or Bun-based workflows, use:
|
| 115 |
+
|
| 116 |
+
- [Advanced Setup](advanced-setup.md)
|
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# OpenClaude Quick Start for macOS and Linux
|
| 2 |
+
|
| 3 |
+
This guide uses a standard shell such as Terminal, iTerm, bash, or zsh.
|
| 4 |
+
|
| 5 |
+
## 1. Install Node.js
|
| 6 |
+
|
| 7 |
+
Install Node.js 20 or newer from:
|
| 8 |
+
|
| 9 |
+
- `https://nodejs.org/`
|
| 10 |
+
|
| 11 |
+
Then check it:
|
| 12 |
+
|
| 13 |
+
```bash
|
| 14 |
+
node --version
|
| 15 |
+
npm --version
|
| 16 |
+
```
|
| 17 |
+
|
| 18 |
+
## 2. Install OpenClaude
|
| 19 |
+
|
| 20 |
+
```bash
|
| 21 |
+
npm install -g @gitlawb/openclaude
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
## 3. Pick One Provider
|
| 25 |
+
|
| 26 |
+
### Option A: OpenAI
|
| 27 |
+
|
| 28 |
+
Replace `sk-your-key-here` with your real key.
|
| 29 |
+
|
| 30 |
+
```bash
|
| 31 |
+
export CLAUDE_CODE_USE_OPENAI=1
|
| 32 |
+
export OPENAI_API_KEY=sk-your-key-here
|
| 33 |
+
export OPENAI_MODEL=gpt-4o
|
| 34 |
+
|
| 35 |
+
openclaude
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
### Option B: DeepSeek
|
| 39 |
+
|
| 40 |
+
```bash
|
| 41 |
+
export CLAUDE_CODE_USE_OPENAI=1
|
| 42 |
+
export OPENAI_API_KEY=sk-your-key-here
|
| 43 |
+
export OPENAI_BASE_URL=https://api.deepseek.com/v1
|
| 44 |
+
export OPENAI_MODEL=deepseek-chat
|
| 45 |
+
|
| 46 |
+
openclaude
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
### Option C: Ollama
|
| 50 |
+
|
| 51 |
+
Install Ollama first from:
|
| 52 |
+
|
| 53 |
+
- `https://ollama.com/download`
|
| 54 |
+
|
| 55 |
+
Then run:
|
| 56 |
+
|
| 57 |
+
```bash
|
| 58 |
+
ollama pull llama3.1:8b
|
| 59 |
+
|
| 60 |
+
export CLAUDE_CODE_USE_OPENAI=1
|
| 61 |
+
export OPENAI_BASE_URL=http://localhost:11434/v1
|
| 62 |
+
export OPENAI_MODEL=llama3.1:8b
|
| 63 |
+
|
| 64 |
+
openclaude
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
No API key is needed for Ollama local models.
|
| 68 |
+
|
| 69 |
+
### Option D: LM Studio
|
| 70 |
+
|
| 71 |
+
Install LM Studio first from:
|
| 72 |
+
|
| 73 |
+
- `https://lmstudio.ai/`
|
| 74 |
+
|
| 75 |
+
Then in LM Studio:
|
| 76 |
+
|
| 77 |
+
1. Download a model (e.g., Llama 3.1 8B, Mistral 7B)
|
| 78 |
+
2. Go to the "Developer" tab
|
| 79 |
+
3. Select your model and enable the server via the toggle
|
| 80 |
+
|
| 81 |
+
Then run:
|
| 82 |
+
|
| 83 |
+
```bash
|
| 84 |
+
export CLAUDE_CODE_USE_OPENAI=1
|
| 85 |
+
export OPENAI_BASE_URL=http://localhost:1234/v1
|
| 86 |
+
export OPENAI_MODEL=your-model-name
|
| 87 |
+
# export OPENAI_API_KEY=lmstudio # optional: some users need a dummy key
|
| 88 |
+
|
| 89 |
+
openclaude
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
Replace `your-model-name` with the model name shown in LM Studio.
|
| 93 |
+
|
| 94 |
+
No API key is needed for LM Studio local models (but uncomment the `OPENAI_API_KEY` line if you hit auth errors).
|
| 95 |
+
|
| 96 |
+
## 4. If `openclaude` Is Not Found
|
| 97 |
+
|
| 98 |
+
Close the terminal, open a new one, and try again:
|
| 99 |
+
|
| 100 |
+
```bash
|
| 101 |
+
openclaude
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
## 5. If Your Provider Fails
|
| 105 |
+
|
| 106 |
+
Check the basics:
|
| 107 |
+
|
| 108 |
+
### For OpenAI or DeepSeek
|
| 109 |
+
|
| 110 |
+
- make sure the key is real
|
| 111 |
+
- make sure you copied it fully
|
| 112 |
+
|
| 113 |
+
### For Ollama
|
| 114 |
+
|
| 115 |
+
- make sure Ollama is installed
|
| 116 |
+
- make sure Ollama is running
|
| 117 |
+
- make sure the model was pulled successfully
|
| 118 |
+
|
| 119 |
+
### For LM Studio
|
| 120 |
+
|
| 121 |
+
- make sure LM Studio is installed
|
| 122 |
+
- make sure LM Studio is running
|
| 123 |
+
- make sure the server is enabled (toggle on in the "Developer" tab)
|
| 124 |
+
- make sure a model is loaded in LM Studio
|
| 125 |
+
- make sure the model name matches what you set in `OPENAI_MODEL`
|
| 126 |
+
|
| 127 |
+
## 6. Updating OpenClaude
|
| 128 |
+
|
| 129 |
+
```bash
|
| 130 |
+
npm install -g @gitlawb/openclaude@latest
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
## 7. Uninstalling OpenClaude
|
| 134 |
+
|
| 135 |
+
```bash
|
| 136 |
+
npm uninstall -g @gitlawb/openclaude
|
| 137 |
+
```
|
| 138 |
+
|
| 139 |
+
## Need Advanced Setup?
|
| 140 |
+
|
| 141 |
+
Use:
|
| 142 |
+
|
| 143 |
+
- [Advanced Setup](advanced-setup.md)
|
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# OpenClaude Quick Start for Windows
|
| 2 |
+
|
| 3 |
+
This guide uses Windows PowerShell.
|
| 4 |
+
|
| 5 |
+
## 1. Install Node.js
|
| 6 |
+
|
| 7 |
+
Install Node.js 20 or newer from:
|
| 8 |
+
|
| 9 |
+
- `https://nodejs.org/`
|
| 10 |
+
|
| 11 |
+
Then open PowerShell and check it:
|
| 12 |
+
|
| 13 |
+
```powershell
|
| 14 |
+
node --version
|
| 15 |
+
npm --version
|
| 16 |
+
```
|
| 17 |
+
|
| 18 |
+
## 2. Install OpenClaude
|
| 19 |
+
|
| 20 |
+
```powershell
|
| 21 |
+
npm install -g @gitlawb/openclaude
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
## 3. Pick One Provider
|
| 25 |
+
|
| 26 |
+
### Option A: OpenAI
|
| 27 |
+
|
| 28 |
+
Replace `sk-your-key-here` with your real key.
|
| 29 |
+
|
| 30 |
+
```powershell
|
| 31 |
+
$env:CLAUDE_CODE_USE_OPENAI="1"
|
| 32 |
+
$env:OPENAI_API_KEY="sk-your-key-here"
|
| 33 |
+
$env:OPENAI_MODEL="gpt-4o"
|
| 34 |
+
|
| 35 |
+
openclaude
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
### Option B: DeepSeek
|
| 39 |
+
|
| 40 |
+
```powershell
|
| 41 |
+
$env:CLAUDE_CODE_USE_OPENAI="1"
|
| 42 |
+
$env:OPENAI_API_KEY="sk-your-key-here"
|
| 43 |
+
$env:OPENAI_BASE_URL="https://api.deepseek.com/v1"
|
| 44 |
+
$env:OPENAI_MODEL="deepseek-chat"
|
| 45 |
+
|
| 46 |
+
openclaude
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
### Option C: Ollama
|
| 50 |
+
|
| 51 |
+
Install Ollama first from:
|
| 52 |
+
|
| 53 |
+
- `https://ollama.com/download/windows`
|
| 54 |
+
|
| 55 |
+
Then run:
|
| 56 |
+
|
| 57 |
+
```powershell
|
| 58 |
+
ollama pull llama3.1:8b
|
| 59 |
+
|
| 60 |
+
$env:CLAUDE_CODE_USE_OPENAI="1"
|
| 61 |
+
$env:OPENAI_BASE_URL="http://localhost:11434/v1"
|
| 62 |
+
$env:OPENAI_MODEL="llama3.1:8b"
|
| 63 |
+
|
| 64 |
+
openclaude
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
No API key is needed for Ollama local models.
|
| 68 |
+
|
| 69 |
+
### Option D: LM Studio
|
| 70 |
+
|
| 71 |
+
Install LM Studio first from:
|
| 72 |
+
|
| 73 |
+
- `https://lmstudio.ai/`
|
| 74 |
+
|
| 75 |
+
Then in LM Studio:
|
| 76 |
+
|
| 77 |
+
1. Download a model (e.g., Llama 3.1 8B, Mistral 7B)
|
| 78 |
+
2. Go to the "Developer" tab
|
| 79 |
+
3. Select your model and enable the server via the toggle
|
| 80 |
+
|
| 81 |
+
Then run:
|
| 82 |
+
|
| 83 |
+
```powershell
|
| 84 |
+
$env:CLAUDE_CODE_USE_OPENAI="1"
|
| 85 |
+
$env:OPENAI_BASE_URL="http://localhost:1234/v1"
|
| 86 |
+
$env:OPENAI_MODEL="your-model-name"
|
| 87 |
+
# $env:OPENAI_API_KEY="lmstudio" # optional: some users need a dummy key
|
| 88 |
+
|
| 89 |
+
openclaude
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
Replace `your-model-name` with the model name shown in LM Studio.
|
| 93 |
+
|
| 94 |
+
No API key is needed for LM Studio local models (but uncomment the `OPENAI_API_KEY` line if you hit auth errors).
|
| 95 |
+
|
| 96 |
+
## 4. If `openclaude` Is Not Found
|
| 97 |
+
|
| 98 |
+
Close PowerShell, open a new one, and try again:
|
| 99 |
+
|
| 100 |
+
```powershell
|
| 101 |
+
openclaude
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
## 5. If Your Provider Fails
|
| 105 |
+
|
| 106 |
+
Check the basics:
|
| 107 |
+
|
| 108 |
+
### For OpenAI or DeepSeek
|
| 109 |
+
|
| 110 |
+
- make sure the key is real
|
| 111 |
+
- make sure you copied it fully
|
| 112 |
+
|
| 113 |
+
### For Ollama
|
| 114 |
+
|
| 115 |
+
- make sure Ollama is installed
|
| 116 |
+
- make sure Ollama is running
|
| 117 |
+
- make sure the model was pulled successfully
|
| 118 |
+
|
| 119 |
+
### For LM Studio
|
| 120 |
+
|
| 121 |
+
- make sure LM Studio is installed
|
| 122 |
+
- make sure LM Studio is running
|
| 123 |
+
- make sure the server is enabled (toggle on in the "Developer" tab)
|
| 124 |
+
- make sure a model is loaded in LM Studio
|
| 125 |
+
- make sure the model name matches what you set in `OPENAI_MODEL`
|
| 126 |
+
|
| 127 |
+
## 6. Updating OpenClaude
|
| 128 |
+
|
| 129 |
+
```powershell
|
| 130 |
+
npm install -g @gitlawb/openclaude@latest
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
## 7. Uninstalling OpenClaude
|
| 134 |
+
|
| 135 |
+
```powershell
|
| 136 |
+
npm uninstall -g @gitlawb/openclaude
|
| 137 |
+
```
|
| 138 |
+
|
| 139 |
+
## Need Advanced Setup?
|
| 140 |
+
|
| 141 |
+
Use:
|
| 142 |
+
|
| 143 |
+
- [Advanced Setup](advanced-setup.md)
|
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "@gitlawb/openclaude",
|
| 3 |
+
"version": "0.6.0",
|
| 4 |
+
"description": "Claude Code opened to any LLM β OpenAI, Gemini, DeepSeek, Ollama, and 200+ models",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"bin": {
|
| 7 |
+
"openclaude": "./bin/openclaude"
|
| 8 |
+
},
|
| 9 |
+
"files": [
|
| 10 |
+
"bin/",
|
| 11 |
+
"dist/cli.mjs",
|
| 12 |
+
"README.md"
|
| 13 |
+
],
|
| 14 |
+
"scripts": {
|
| 15 |
+
"build": "bun run scripts/build.ts",
|
| 16 |
+
"dev": "bun run build && node dist/cli.mjs",
|
| 17 |
+
"dev:profile": "bun run scripts/provider-launch.ts",
|
| 18 |
+
"dev:profile:fast": "bun run scripts/provider-launch.ts auto --fast --bare",
|
| 19 |
+
"dev:codex": "bun run scripts/provider-launch.ts codex",
|
| 20 |
+
"dev:openai": "bun run scripts/provider-launch.ts openai",
|
| 21 |
+
"dev:gemini": "bun run scripts/provider-launch.ts gemini",
|
| 22 |
+
"dev:ollama": "bun run scripts/provider-launch.ts ollama",
|
| 23 |
+
"dev:ollama:fast": "bun run scripts/provider-launch.ts ollama --fast --bare",
|
| 24 |
+
"dev:atomic-chat": "bun run scripts/provider-launch.ts atomic-chat",
|
| 25 |
+
"profile:init": "bun run scripts/provider-bootstrap.ts",
|
| 26 |
+
"profile:recommend": "bun run scripts/provider-recommend.ts",
|
| 27 |
+
"profile:auto": "bun run scripts/provider-recommend.ts --apply",
|
| 28 |
+
"profile:codex": "bun run profile:init -- --provider codex --model codexplan",
|
| 29 |
+
"profile:fast": "bun run profile:init -- --provider ollama --model llama3.2:3b",
|
| 30 |
+
"profile:code": "bun run profile:init -- --provider ollama --model qwen2.5-coder:7b",
|
| 31 |
+
"dev:fast": "bun run profile:fast && bun run dev:ollama:fast",
|
| 32 |
+
"dev:code": "bun run profile:code && bun run dev:profile",
|
| 33 |
+
"dev:grpc": "bun run scripts/start-grpc.ts",
|
| 34 |
+
"dev:grpc:cli": "bun run scripts/grpc-cli.ts",
|
| 35 |
+
"start": "node dist/cli.mjs",
|
| 36 |
+
"test": "bun test",
|
| 37 |
+
"test:coverage": "bun test --coverage --coverage-reporter=lcov --coverage-dir=coverage --max-concurrency=1 && bun run scripts/render-coverage-heatmap.ts",
|
| 38 |
+
"test:coverage:ui": "bun run scripts/render-coverage-heatmap.ts",
|
| 39 |
+
"security:pr-scan": "bun run scripts/pr-intent-scan.ts",
|
| 40 |
+
"test:provider-recommendation": "bun test src/utils/providerRecommendation.test.ts src/utils/providerProfile.test.ts",
|
| 41 |
+
"typecheck": "tsc --noEmit",
|
| 42 |
+
"smoke": "bun run build && node dist/cli.mjs --version",
|
| 43 |
+
"verify:privacy": "bun run scripts/verify-no-phone-home.ts",
|
| 44 |
+
"build:verified": "bun run build && bun run verify:privacy",
|
| 45 |
+
"test:provider": "bun test src/services/api/*.test.ts src/utils/context.test.ts",
|
| 46 |
+
"doctor:runtime": "bun run scripts/system-check.ts",
|
| 47 |
+
"doctor:runtime:json": "bun run scripts/system-check.ts --json",
|
| 48 |
+
"doctor:report": "bun run scripts/system-check.ts --out reports/doctor-runtime.json",
|
| 49 |
+
"hardening:check": "bun run smoke && bun run doctor:runtime",
|
| 50 |
+
"hardening:strict": "bun run typecheck && bun run hardening:check",
|
| 51 |
+
"prepack": "npm run build"
|
| 52 |
+
},
|
| 53 |
+
"dependencies": {
|
| 54 |
+
"@alcalzone/ansi-tokenize": "0.3.0",
|
| 55 |
+
"@anthropic-ai/bedrock-sdk": "0.26.4",
|
| 56 |
+
"@anthropic-ai/foundry-sdk": "0.2.3",
|
| 57 |
+
"@anthropic-ai/sandbox-runtime": "0.0.46",
|
| 58 |
+
"@anthropic-ai/sdk": "0.81.0",
|
| 59 |
+
"@anthropic-ai/vertex-sdk": "0.14.4",
|
| 60 |
+
"@commander-js/extra-typings": "12.1.0",
|
| 61 |
+
"@growthbook/growthbook": "1.6.5",
|
| 62 |
+
"@grpc/grpc-js": "^1.14.3",
|
| 63 |
+
"@grpc/proto-loader": "^0.8.0",
|
| 64 |
+
"@mendable/firecrawl-js": "4.18.1",
|
| 65 |
+
"@modelcontextprotocol/sdk": "1.29.0",
|
| 66 |
+
"@opentelemetry/api": "1.9.1",
|
| 67 |
+
"@opentelemetry/api-logs": "0.214.0",
|
| 68 |
+
"@opentelemetry/core": "2.6.1",
|
| 69 |
+
"@opentelemetry/exporter-logs-otlp-http": "0.214.0",
|
| 70 |
+
"@opentelemetry/exporter-trace-otlp-grpc": "0.57.2",
|
| 71 |
+
"@opentelemetry/resources": "2.6.1",
|
| 72 |
+
"@opentelemetry/sdk-logs": "0.214.0",
|
| 73 |
+
"@opentelemetry/sdk-metrics": "2.6.1",
|
| 74 |
+
"@opentelemetry/sdk-trace-base": "2.6.1",
|
| 75 |
+
"@opentelemetry/sdk-trace-node": "2.6.1",
|
| 76 |
+
"@opentelemetry/semantic-conventions": "1.40.0",
|
| 77 |
+
"ajv": "8.18.0",
|
| 78 |
+
"auto-bind": "5.0.1",
|
| 79 |
+
"axios": "1.15.0",
|
| 80 |
+
"bidi-js": "1.0.3",
|
| 81 |
+
"chalk": "5.6.2",
|
| 82 |
+
"chokidar": "4.0.3",
|
| 83 |
+
"cli-boxes": "3.0.0",
|
| 84 |
+
"cli-highlight": "2.1.11",
|
| 85 |
+
"code-excerpt": "4.0.0",
|
| 86 |
+
"commander": "12.1.0",
|
| 87 |
+
"cross-spawn": "7.0.6",
|
| 88 |
+
"diff": "8.0.3",
|
| 89 |
+
"duck-duck-scrape": "^2.2.7",
|
| 90 |
+
"emoji-regex": "10.6.0",
|
| 91 |
+
"env-paths": "3.0.0",
|
| 92 |
+
"execa": "9.6.1",
|
| 93 |
+
"fflate": "0.8.2",
|
| 94 |
+
"figures": "6.1.0",
|
| 95 |
+
"fuse.js": "7.1.0",
|
| 96 |
+
"get-east-asian-width": "1.5.0",
|
| 97 |
+
"google-auth-library": "9.15.1",
|
| 98 |
+
"https-proxy-agent": "7.0.6",
|
| 99 |
+
"ignore": "7.0.5",
|
| 100 |
+
"indent-string": "5.0.0",
|
| 101 |
+
"jsonc-parser": "3.3.1",
|
| 102 |
+
"lodash-es": "4.18.1",
|
| 103 |
+
"lru-cache": "11.2.7",
|
| 104 |
+
"marked": "15.0.12",
|
| 105 |
+
"p-map": "7.0.4",
|
| 106 |
+
"picomatch": "4.0.4",
|
| 107 |
+
"proper-lockfile": "4.1.2",
|
| 108 |
+
"qrcode": "1.5.4",
|
| 109 |
+
"react": "19.2.4",
|
| 110 |
+
"react-compiler-runtime": "1.0.0",
|
| 111 |
+
"react-reconciler": "0.33.0",
|
| 112 |
+
"semver": "7.7.4",
|
| 113 |
+
"sharp": "^0.34.5",
|
| 114 |
+
"shell-quote": "1.8.3",
|
| 115 |
+
"signal-exit": "4.1.0",
|
| 116 |
+
"stack-utils": "2.0.6",
|
| 117 |
+
"strip-ansi": "7.2.0",
|
| 118 |
+
"supports-hyperlinks": "3.2.0",
|
| 119 |
+
"tree-kill": "1.2.2",
|
| 120 |
+
"turndown": "7.2.2",
|
| 121 |
+
"type-fest": "4.41.0",
|
| 122 |
+
"undici": "7.24.6",
|
| 123 |
+
"usehooks-ts": "3.1.1",
|
| 124 |
+
"vscode-languageserver-protocol": "3.17.5",
|
| 125 |
+
"wrap-ansi": "9.0.2",
|
| 126 |
+
"ws": "8.20.0",
|
| 127 |
+
"xss": "1.0.15",
|
| 128 |
+
"yaml": "2.8.3",
|
| 129 |
+
"zod": "3.25.76"
|
| 130 |
+
},
|
| 131 |
+
"devDependencies": {
|
| 132 |
+
"@types/bun": "1.3.11",
|
| 133 |
+
"@types/node": "25.5.0",
|
| 134 |
+
"@types/react": "19.2.14",
|
| 135 |
+
"tsx": "^4.21.0",
|
| 136 |
+
"typescript": "5.9.3"
|
| 137 |
+
},
|
| 138 |
+
"engines": {
|
| 139 |
+
"node": ">=20.0.0"
|
| 140 |
+
},
|
| 141 |
+
"repository": {
|
| 142 |
+
"type": "git",
|
| 143 |
+
"url": "https://github.com/Gitlawb/openclaude.git"
|
| 144 |
+
},
|
| 145 |
+
"keywords": [
|
| 146 |
+
"claude-code",
|
| 147 |
+
"openai",
|
| 148 |
+
"llm",
|
| 149 |
+
"cli",
|
| 150 |
+
"agent",
|
| 151 |
+
"deepseek",
|
| 152 |
+
"ollama",
|
| 153 |
+
"gemini"
|
| 154 |
+
],
|
| 155 |
+
"license": "SEE LICENSE FILE",
|
| 156 |
+
"publishConfig": {
|
| 157 |
+
"access": "public"
|
| 158 |
+
},
|
| 159 |
+
"overrides": {
|
| 160 |
+
"lodash-es": "4.18.1"
|
| 161 |
+
}
|
| 162 |
+
}
|
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Python helper package for standalone provider-side utilities.
|
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
atomic_chat_provider.py
|
| 3 |
+
-----------------------
|
| 4 |
+
Adds native Atomic Chat support to openclaude.
|
| 5 |
+
Lets Claude Code route requests to any locally-running model via
|
| 6 |
+
Atomic Chat (Apple Silicon only) at 127.0.0.1:1337.
|
| 7 |
+
|
| 8 |
+
Atomic Chat exposes an OpenAI-compatible API, so messages are forwarded
|
| 9 |
+
directly without translation.
|
| 10 |
+
|
| 11 |
+
Usage (.env):
|
| 12 |
+
PREFERRED_PROVIDER=atomic-chat
|
| 13 |
+
ATOMIC_CHAT_BASE_URL=http://127.0.0.1:1337
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
import httpx
|
| 17 |
+
import json
|
| 18 |
+
import logging
|
| 19 |
+
import os
|
| 20 |
+
from typing import AsyncIterator
|
| 21 |
+
|
| 22 |
+
logger = logging.getLogger(__name__)
|
| 23 |
+
ATOMIC_CHAT_BASE_URL = os.getenv("ATOMIC_CHAT_BASE_URL", "http://127.0.0.1:1337")
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def _api_url(path: str) -> str:
|
| 27 |
+
return f"{ATOMIC_CHAT_BASE_URL}/v1{path}"
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
async def check_atomic_chat_running() -> bool:
|
| 31 |
+
try:
|
| 32 |
+
async with httpx.AsyncClient(timeout=3.0) as client:
|
| 33 |
+
resp = await client.get(_api_url("/models"))
|
| 34 |
+
return resp.status_code == 200
|
| 35 |
+
except Exception:
|
| 36 |
+
return False
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
async def list_atomic_chat_models() -> list[str]:
|
| 40 |
+
try:
|
| 41 |
+
async with httpx.AsyncClient(timeout=5.0) as client:
|
| 42 |
+
resp = await client.get(_api_url("/models"))
|
| 43 |
+
resp.raise_for_status()
|
| 44 |
+
data = resp.json()
|
| 45 |
+
return [m["id"] for m in data.get("data", [])]
|
| 46 |
+
except Exception as e:
|
| 47 |
+
logger.warning(f"Could not list Atomic Chat models: {e}")
|
| 48 |
+
return []
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
async def atomic_chat(
|
| 52 |
+
model: str,
|
| 53 |
+
messages: list[dict],
|
| 54 |
+
system: str | None = None,
|
| 55 |
+
max_tokens: int = 4096,
|
| 56 |
+
temperature: float = 1.0,
|
| 57 |
+
) -> dict:
|
| 58 |
+
chat_messages = list(messages)
|
| 59 |
+
if system:
|
| 60 |
+
chat_messages.insert(0, {"role": "system", "content": system})
|
| 61 |
+
|
| 62 |
+
payload = {
|
| 63 |
+
"model": model,
|
| 64 |
+
"messages": chat_messages,
|
| 65 |
+
"max_tokens": max_tokens,
|
| 66 |
+
"temperature": temperature,
|
| 67 |
+
"stream": False,
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
async with httpx.AsyncClient(timeout=120.0) as client:
|
| 71 |
+
resp = await client.post(_api_url("/chat/completions"), json=payload)
|
| 72 |
+
resp.raise_for_status()
|
| 73 |
+
data = resp.json()
|
| 74 |
+
|
| 75 |
+
choice = data.get("choices", [{}])[0]
|
| 76 |
+
assistant_text = choice.get("message", {}).get("content", "")
|
| 77 |
+
usage = data.get("usage", {})
|
| 78 |
+
|
| 79 |
+
return {
|
| 80 |
+
"id": data.get("id", "msg_atomic_chat"),
|
| 81 |
+
"type": "message",
|
| 82 |
+
"role": "assistant",
|
| 83 |
+
"content": [{"type": "text", "text": assistant_text}],
|
| 84 |
+
"model": model,
|
| 85 |
+
"stop_reason": "end_turn",
|
| 86 |
+
"stop_sequence": None,
|
| 87 |
+
"usage": {
|
| 88 |
+
"input_tokens": usage.get("prompt_tokens", 0),
|
| 89 |
+
"output_tokens": usage.get("completion_tokens", 0),
|
| 90 |
+
},
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
async def atomic_chat_stream(
|
| 95 |
+
model: str,
|
| 96 |
+
messages: list[dict],
|
| 97 |
+
system: str | None = None,
|
| 98 |
+
max_tokens: int = 4096,
|
| 99 |
+
temperature: float = 1.0,
|
| 100 |
+
) -> AsyncIterator[str]:
|
| 101 |
+
chat_messages = list(messages)
|
| 102 |
+
if system:
|
| 103 |
+
chat_messages.insert(0, {"role": "system", "content": system})
|
| 104 |
+
|
| 105 |
+
payload = {
|
| 106 |
+
"model": model,
|
| 107 |
+
"messages": chat_messages,
|
| 108 |
+
"max_tokens": max_tokens,
|
| 109 |
+
"temperature": temperature,
|
| 110 |
+
"stream": True,
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
yield "event: message_start\n"
|
| 114 |
+
yield f'data: {json.dumps({"type": "message_start", "message": {"id": "msg_atomic_chat_stream", "type": "message", "role": "assistant", "content": [], "model": model, "stop_reason": None, "usage": {"input_tokens": 0, "output_tokens": 0}}})}\n\n'
|
| 115 |
+
yield "event: content_block_start\n"
|
| 116 |
+
yield f'data: {json.dumps({"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}})}\n\n'
|
| 117 |
+
|
| 118 |
+
async with httpx.AsyncClient(timeout=120.0) as client:
|
| 119 |
+
async with client.stream("POST", _api_url("/chat/completions"), json=payload) as resp:
|
| 120 |
+
resp.raise_for_status()
|
| 121 |
+
async for line in resp.aiter_lines():
|
| 122 |
+
if not line or not line.startswith("data: "):
|
| 123 |
+
continue
|
| 124 |
+
raw = line[len("data: "):]
|
| 125 |
+
if raw.strip() == "[DONE]":
|
| 126 |
+
break
|
| 127 |
+
try:
|
| 128 |
+
chunk = json.loads(raw)
|
| 129 |
+
delta = chunk.get("choices", [{}])[0].get("delta", {})
|
| 130 |
+
delta_text = delta.get("content", "")
|
| 131 |
+
if delta_text:
|
| 132 |
+
yield "event: content_block_delta\n"
|
| 133 |
+
yield f'data: {json.dumps({"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": delta_text}})}\n\n'
|
| 134 |
+
|
| 135 |
+
finish_reason = chunk.get("choices", [{}])[0].get("finish_reason")
|
| 136 |
+
if finish_reason:
|
| 137 |
+
usage = chunk.get("usage", {})
|
| 138 |
+
yield "event: content_block_stop\n"
|
| 139 |
+
yield f'data: {json.dumps({"type": "content_block_stop", "index": 0})}\n\n'
|
| 140 |
+
yield "event: message_delta\n"
|
| 141 |
+
yield f'data: {json.dumps({"type": "message_delta", "delta": {"stop_reason": "end_turn", "stop_sequence": None}, "usage": {"output_tokens": usage.get("completion_tokens", 0)}})}\n\n'
|
| 142 |
+
yield "event: message_stop\n"
|
| 143 |
+
yield f'data: {json.dumps({"type": "message_stop"})}\n\n'
|
| 144 |
+
break
|
| 145 |
+
except json.JSONDecodeError:
|
| 146 |
+
continue
|
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
ollama_provider.py
|
| 3 |
+
------------------
|
| 4 |
+
Adds native Ollama support to openclaude.
|
| 5 |
+
Lets Claude Code route requests to any locally-running Ollama model
|
| 6 |
+
(llama3, mistral, codellama, phi3, qwen2, deepseek-coder, etc.)
|
| 7 |
+
without needing an API key.
|
| 8 |
+
|
| 9 |
+
Usage (.env):
|
| 10 |
+
PREFERRED_PROVIDER=ollama
|
| 11 |
+
OLLAMA_BASE_URL=http://localhost:11434
|
| 12 |
+
BIG_MODEL=codellama:34b
|
| 13 |
+
SMALL_MODEL=llama3:8b
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
import httpx
|
| 17 |
+
import logging
|
| 18 |
+
import os
|
| 19 |
+
from typing import AsyncIterator
|
| 20 |
+
|
| 21 |
+
logger = logging.getLogger(__name__)
|
| 22 |
+
OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434")
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
async def check_ollama_running() -> bool:
|
| 26 |
+
try:
|
| 27 |
+
async with httpx.AsyncClient(timeout=3.0) as client:
|
| 28 |
+
resp = await client.get(f"{OLLAMA_BASE_URL}/api/tags")
|
| 29 |
+
return resp.status_code == 200
|
| 30 |
+
except Exception:
|
| 31 |
+
return False
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
async def list_ollama_models() -> list[str]:
|
| 35 |
+
try:
|
| 36 |
+
async with httpx.AsyncClient(timeout=5.0) as client:
|
| 37 |
+
resp = await client.get(f"{OLLAMA_BASE_URL}/api/tags")
|
| 38 |
+
resp.raise_for_status()
|
| 39 |
+
data = resp.json()
|
| 40 |
+
return [m["name"] for m in data.get("models", [])]
|
| 41 |
+
except Exception as e:
|
| 42 |
+
logger.warning(f"Could not list Ollama models: {e}")
|
| 43 |
+
return []
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def normalize_ollama_model(model_name: str) -> str:
|
| 47 |
+
if model_name.startswith("ollama/"):
|
| 48 |
+
return model_name[len("ollama/"):]
|
| 49 |
+
return model_name
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def _extract_ollama_image_data(block: dict) -> str | None:
|
| 53 |
+
source = block.get("source")
|
| 54 |
+
if not isinstance(source, dict):
|
| 55 |
+
return None
|
| 56 |
+
if source.get("type") != "base64":
|
| 57 |
+
return None
|
| 58 |
+
data = source.get("data")
|
| 59 |
+
if isinstance(data, str) and data:
|
| 60 |
+
return data
|
| 61 |
+
return None
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def anthropic_to_ollama_messages(messages: list[dict]) -> list[dict]:
|
| 65 |
+
ollama_messages = []
|
| 66 |
+
for msg in messages:
|
| 67 |
+
role = msg.get("role", "user")
|
| 68 |
+
content = msg.get("content", "")
|
| 69 |
+
if isinstance(content, str):
|
| 70 |
+
ollama_messages.append({"role": role, "content": content})
|
| 71 |
+
elif isinstance(content, list):
|
| 72 |
+
text_parts = []
|
| 73 |
+
image_parts = []
|
| 74 |
+
for block in content:
|
| 75 |
+
if isinstance(block, dict):
|
| 76 |
+
if block.get("type") == "text":
|
| 77 |
+
text_parts.append(block.get("text", ""))
|
| 78 |
+
elif block.get("type") == "image":
|
| 79 |
+
image_data = _extract_ollama_image_data(block)
|
| 80 |
+
if image_data:
|
| 81 |
+
image_parts.append(image_data)
|
| 82 |
+
else:
|
| 83 |
+
text_parts.append("[image]")
|
| 84 |
+
elif isinstance(block, str):
|
| 85 |
+
text_parts.append(block)
|
| 86 |
+
ollama_message = {"role": role, "content": "\n".join(text_parts)}
|
| 87 |
+
if image_parts:
|
| 88 |
+
ollama_message["images"] = image_parts
|
| 89 |
+
ollama_messages.append(ollama_message)
|
| 90 |
+
return ollama_messages
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
async def ollama_chat(
|
| 94 |
+
model: str,
|
| 95 |
+
messages: list[dict],
|
| 96 |
+
system: str | None = None,
|
| 97 |
+
max_tokens: int = 4096,
|
| 98 |
+
temperature: float = 1.0,
|
| 99 |
+
) -> dict:
|
| 100 |
+
model = normalize_ollama_model(model)
|
| 101 |
+
ollama_messages = anthropic_to_ollama_messages(messages)
|
| 102 |
+
if system:
|
| 103 |
+
ollama_messages.insert(0, {"role": "system", "content": system})
|
| 104 |
+
payload = {
|
| 105 |
+
"model": model,
|
| 106 |
+
"messages": ollama_messages,
|
| 107 |
+
"stream": False,
|
| 108 |
+
"options": {"num_predict": max_tokens, "temperature": temperature},
|
| 109 |
+
}
|
| 110 |
+
async with httpx.AsyncClient(timeout=120.0) as client:
|
| 111 |
+
resp = await client.post(f"{OLLAMA_BASE_URL}/api/chat", json=payload)
|
| 112 |
+
resp.raise_for_status()
|
| 113 |
+
data = resp.json()
|
| 114 |
+
assistant_text = data.get("message", {}).get("content", "")
|
| 115 |
+
return {
|
| 116 |
+
"id": f"msg_ollama_{data.get('created_at', 'unknown')}",
|
| 117 |
+
"type": "message",
|
| 118 |
+
"role": "assistant",
|
| 119 |
+
"content": [{"type": "text", "text": assistant_text}],
|
| 120 |
+
"model": model,
|
| 121 |
+
"stop_reason": "end_turn",
|
| 122 |
+
"stop_sequence": None,
|
| 123 |
+
"usage": {
|
| 124 |
+
"input_tokens": data.get("prompt_eval_count", 0),
|
| 125 |
+
"output_tokens": data.get("eval_count", 0),
|
| 126 |
+
},
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
async def ollama_chat_stream(
|
| 131 |
+
model: str,
|
| 132 |
+
messages: list[dict],
|
| 133 |
+
system: str | None = None,
|
| 134 |
+
max_tokens: int = 4096,
|
| 135 |
+
temperature: float = 1.0,
|
| 136 |
+
) -> AsyncIterator[str]:
|
| 137 |
+
import json
|
| 138 |
+
model = normalize_ollama_model(model)
|
| 139 |
+
ollama_messages = anthropic_to_ollama_messages(messages)
|
| 140 |
+
if system:
|
| 141 |
+
ollama_messages.insert(0, {"role": "system", "content": system})
|
| 142 |
+
payload = {
|
| 143 |
+
"model": model,
|
| 144 |
+
"messages": ollama_messages,
|
| 145 |
+
"stream": True,
|
| 146 |
+
"options": {"num_predict": max_tokens, "temperature": temperature},
|
| 147 |
+
}
|
| 148 |
+
yield "event: message_start\n"
|
| 149 |
+
yield f'data: {json.dumps({"type": "message_start", "message": {"id": "msg_ollama_stream", "type": "message", "role": "assistant", "content": [], "model": model, "stop_reason": None, "usage": {"input_tokens": 0, "output_tokens": 0}}})}\n\n'
|
| 150 |
+
yield "event: content_block_start\n"
|
| 151 |
+
yield f'data: {json.dumps({"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}})}\n\n'
|
| 152 |
+
async with httpx.AsyncClient(timeout=120.0) as client:
|
| 153 |
+
async with client.stream("POST", f"{OLLAMA_BASE_URL}/api/chat", json=payload) as resp:
|
| 154 |
+
resp.raise_for_status()
|
| 155 |
+
async for line in resp.aiter_lines():
|
| 156 |
+
if not line:
|
| 157 |
+
continue
|
| 158 |
+
try:
|
| 159 |
+
chunk = json.loads(line)
|
| 160 |
+
delta_text = chunk.get("message", {}).get("content", "")
|
| 161 |
+
if delta_text:
|
| 162 |
+
yield "event: content_block_delta\n"
|
| 163 |
+
yield f'data: {json.dumps({"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": delta_text}})}\n\n'
|
| 164 |
+
if chunk.get("done"):
|
| 165 |
+
yield "event: content_block_stop\n"
|
| 166 |
+
yield f'data: {json.dumps({"type": "content_block_stop", "index": 0})}\n\n'
|
| 167 |
+
yield "event: message_delta\n"
|
| 168 |
+
yield f'data: {json.dumps({"type": "message_delta", "delta": {"stop_reason": "end_turn", "stop_sequence": None}, "usage": {"output_tokens": chunk.get("eval_count", 0)}})}\n\n'
|
| 169 |
+
yield "event: message_stop\n"
|
| 170 |
+
yield f'data: {json.dumps({"type": "message_stop"})}\n\n'
|
| 171 |
+
break
|
| 172 |
+
except json.JSONDecodeError:
|
| 173 |
+
continue
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
pytest==7.4.4
|
| 2 |
+
pytest-asyncio==0.23.3
|
| 3 |
+
httpx==0.25.2
|
|
@@ -0,0 +1,387 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
smart_router.py
|
| 3 |
+
---------------
|
| 4 |
+
Intelligent auto-router for openclaude.
|
| 5 |
+
|
| 6 |
+
Instead of always using one fixed provider, the smart router:
|
| 7 |
+
- Pings all configured providers on startup
|
| 8 |
+
- Scores them by latency, cost, and health
|
| 9 |
+
- Routes each request to the optimal provider
|
| 10 |
+
- Falls back automatically if a provider fails
|
| 11 |
+
- Learns from real request timings over time
|
| 12 |
+
|
| 13 |
+
Usage in server.py:
|
| 14 |
+
from smart_router import SmartRouter
|
| 15 |
+
router = SmartRouter()
|
| 16 |
+
await router.initialize()
|
| 17 |
+
result = await router.route(messages, model, stream)
|
| 18 |
+
|
| 19 |
+
.env config:
|
| 20 |
+
ROUTER_MODE=smart # or: fixed (default behaviour)
|
| 21 |
+
ROUTER_STRATEGY=latency # or: cost, balanced
|
| 22 |
+
ROUTER_FALLBACK=true # auto-retry on failure
|
| 23 |
+
|
| 24 |
+
Contribution to: https://github.com/Gitlawb/openclaude
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
import asyncio
|
| 28 |
+
import logging
|
| 29 |
+
import os
|
| 30 |
+
import time
|
| 31 |
+
from dataclasses import dataclass, field
|
| 32 |
+
from typing import Optional
|
| 33 |
+
import httpx
|
| 34 |
+
|
| 35 |
+
logger = logging.getLogger(__name__)
|
| 36 |
+
|
| 37 |
+
# ββ Provider definitions ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 38 |
+
|
| 39 |
+
@dataclass
|
| 40 |
+
class Provider:
|
| 41 |
+
name: str # e.g. "openai", "gemini", "ollama"
|
| 42 |
+
ping_url: str # URL used to check health
|
| 43 |
+
api_key_env: str # env var name for API key
|
| 44 |
+
cost_per_1k_tokens: float # estimated cost USD per 1k tokens
|
| 45 |
+
big_model: str # model for sonnet/large requests
|
| 46 |
+
small_model: str # model for haiku/small requests
|
| 47 |
+
latency_ms: float = 9999.0 # updated by benchmark
|
| 48 |
+
healthy: bool = True # updated by health checks
|
| 49 |
+
request_count: int = 0 # total requests routed here
|
| 50 |
+
error_count: int = 0 # total errors from this provider
|
| 51 |
+
avg_latency_ms: float = 9999.0 # rolling average from real requests
|
| 52 |
+
|
| 53 |
+
@property
|
| 54 |
+
def api_key(self) -> Optional[str]:
|
| 55 |
+
return os.getenv(self.api_key_env)
|
| 56 |
+
|
| 57 |
+
@property
|
| 58 |
+
def is_configured(self) -> bool:
|
| 59 |
+
"""True if the provider has an API key set."""
|
| 60 |
+
if self.name in ("ollama", "atomic-chat"):
|
| 61 |
+
return True # Local providers need no API key
|
| 62 |
+
return bool(self.api_key)
|
| 63 |
+
|
| 64 |
+
@property
|
| 65 |
+
def error_rate(self) -> float:
|
| 66 |
+
if self.request_count == 0:
|
| 67 |
+
return 0.0
|
| 68 |
+
return self.error_count / self.request_count
|
| 69 |
+
|
| 70 |
+
def score(self, strategy: str = "balanced") -> float:
|
| 71 |
+
"""
|
| 72 |
+
Lower score = better provider.
|
| 73 |
+
strategy: 'latency' | 'cost' | 'balanced'
|
| 74 |
+
"""
|
| 75 |
+
if not self.healthy or not self.is_configured:
|
| 76 |
+
return float("inf")
|
| 77 |
+
|
| 78 |
+
latency_score = self.avg_latency_ms / 1000.0 # normalize to seconds
|
| 79 |
+
cost_score = self.cost_per_1k_tokens * 100 # normalize to similar scale
|
| 80 |
+
error_penalty = self.error_rate * 500 # heavy penalty for errors
|
| 81 |
+
|
| 82 |
+
if strategy == "latency":
|
| 83 |
+
return latency_score + error_penalty
|
| 84 |
+
elif strategy == "cost":
|
| 85 |
+
return cost_score + error_penalty
|
| 86 |
+
else: # balanced
|
| 87 |
+
return (latency_score * 0.5) + (cost_score * 0.5) + error_penalty
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
# ββ Default provider catalogue ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 91 |
+
|
| 92 |
+
def build_default_providers() -> list[Provider]:
|
| 93 |
+
big = os.getenv("BIG_MODEL", "gpt-4.1")
|
| 94 |
+
small = os.getenv("SMALL_MODEL", "gpt-4.1-mini")
|
| 95 |
+
ollama_url = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434")
|
| 96 |
+
atomic_chat_url = os.getenv("ATOMIC_CHAT_BASE_URL", "http://127.0.0.1:1337")
|
| 97 |
+
|
| 98 |
+
return [
|
| 99 |
+
Provider(
|
| 100 |
+
name="openai",
|
| 101 |
+
ping_url="https://api.openai.com/v1/models",
|
| 102 |
+
api_key_env="OPENAI_API_KEY",
|
| 103 |
+
cost_per_1k_tokens=0.002,
|
| 104 |
+
big_model=big if "gpt" in big else "gpt-4.1",
|
| 105 |
+
small_model=small if "gpt" in small else "gpt-4.1-mini",
|
| 106 |
+
),
|
| 107 |
+
Provider(
|
| 108 |
+
name="gemini",
|
| 109 |
+
ping_url="https://generativelanguage.googleapis.com/v1/models",
|
| 110 |
+
api_key_env="GEMINI_API_KEY",
|
| 111 |
+
cost_per_1k_tokens=0.0005,
|
| 112 |
+
big_model=big if "gemini" in big else "gemini-2.5-pro",
|
| 113 |
+
small_model=small if "gemini" in small else "gemini-2.0-flash",
|
| 114 |
+
),
|
| 115 |
+
Provider(
|
| 116 |
+
name="mistral",
|
| 117 |
+
ping_url="",
|
| 118 |
+
api_key_env="MISTRAL_API_KEY",
|
| 119 |
+
cost_per_1k_tokens=0.0001,
|
| 120 |
+
big_model=big if "mistral" in big else "devstral-latest",
|
| 121 |
+
small_model=small if "small" in small else "ministral-3b-latest",
|
| 122 |
+
),
|
| 123 |
+
Provider(
|
| 124 |
+
name="ollama",
|
| 125 |
+
ping_url=f"{ollama_url}/api/tags",
|
| 126 |
+
api_key_env="",
|
| 127 |
+
cost_per_1k_tokens=0.0, # free β local
|
| 128 |
+
big_model=big if "gemini" not in big and "gpt" not in big else "llama3:8b",
|
| 129 |
+
small_model=small if "gemini" not in small and "gpt" not in small else "llama3:8b",
|
| 130 |
+
),
|
| 131 |
+
Provider(
|
| 132 |
+
name="atomic-chat",
|
| 133 |
+
ping_url=f"{atomic_chat_url}/v1/models",
|
| 134 |
+
api_key_env="",
|
| 135 |
+
cost_per_1k_tokens=0.0, # free β local (Apple Silicon)
|
| 136 |
+
big_model=big if "gemini" not in big and "gpt" not in big else "llama3:8b",
|
| 137 |
+
small_model=small if "gemini" not in small and "gpt" not in small else "llama3:8b",
|
| 138 |
+
),
|
| 139 |
+
]
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
# ββ Smart Router ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 143 |
+
|
| 144 |
+
class SmartRouter:
|
| 145 |
+
"""
|
| 146 |
+
Intelligently routes Claude Code API requests to the best
|
| 147 |
+
available LLM provider based on latency, cost, and health.
|
| 148 |
+
"""
|
| 149 |
+
|
| 150 |
+
def __init__(
|
| 151 |
+
self,
|
| 152 |
+
providers: Optional[list[Provider]] = None,
|
| 153 |
+
strategy: Optional[str] = None,
|
| 154 |
+
fallback_enabled: Optional[bool] = None,
|
| 155 |
+
):
|
| 156 |
+
self.providers = providers or build_default_providers()
|
| 157 |
+
self.strategy = strategy or os.getenv("ROUTER_STRATEGY", "balanced")
|
| 158 |
+
self.fallback_enabled = (
|
| 159 |
+
fallback_enabled
|
| 160 |
+
if fallback_enabled is not None
|
| 161 |
+
else os.getenv("ROUTER_FALLBACK", "true").lower() == "true"
|
| 162 |
+
)
|
| 163 |
+
self._initialized = False
|
| 164 |
+
|
| 165 |
+
# ββ Initialization ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 166 |
+
|
| 167 |
+
async def initialize(self) -> None:
|
| 168 |
+
"""Ping all providers and build initial latency scores."""
|
| 169 |
+
logger.info("SmartRouter: benchmarking providers...")
|
| 170 |
+
await asyncio.gather(
|
| 171 |
+
*[self._ping_provider(p) for p in self.providers],
|
| 172 |
+
return_exceptions=True,
|
| 173 |
+
)
|
| 174 |
+
available = [p for p in self.providers if p.healthy and p.is_configured]
|
| 175 |
+
logger.info(
|
| 176 |
+
f"SmartRouter ready. Available providers: "
|
| 177 |
+
f"{[p.name for p in available]}"
|
| 178 |
+
)
|
| 179 |
+
if not available:
|
| 180 |
+
logger.warning(
|
| 181 |
+
"SmartRouter: no providers available! "
|
| 182 |
+
"Check your API keys in .env"
|
| 183 |
+
)
|
| 184 |
+
self._initialized = True
|
| 185 |
+
|
| 186 |
+
async def _ping_provider(self, provider: Provider) -> None:
|
| 187 |
+
"""Measure latency to a provider's health endpoint."""
|
| 188 |
+
if not provider.is_configured:
|
| 189 |
+
provider.healthy = False
|
| 190 |
+
logger.debug(f"SmartRouter: {provider.name} skipped β no API key")
|
| 191 |
+
return
|
| 192 |
+
|
| 193 |
+
headers = {}
|
| 194 |
+
if provider.api_key:
|
| 195 |
+
headers["Authorization"] = f"Bearer {provider.api_key}"
|
| 196 |
+
|
| 197 |
+
start = time.monotonic()
|
| 198 |
+
try:
|
| 199 |
+
async with httpx.AsyncClient(timeout=5.0) as client:
|
| 200 |
+
resp = await client.get(provider.ping_url, headers=headers)
|
| 201 |
+
elapsed_ms = (time.monotonic() - start) * 1000
|
| 202 |
+
if resp.status_code in (200, 400, 401, 403):
|
| 203 |
+
# 400/401/403 means reachable, just possibly bad key
|
| 204 |
+
# We still mark healthy for routing purposes
|
| 205 |
+
provider.healthy = True
|
| 206 |
+
provider.latency_ms = elapsed_ms
|
| 207 |
+
provider.avg_latency_ms = elapsed_ms
|
| 208 |
+
logger.info(
|
| 209 |
+
f"SmartRouter: {provider.name} OK "
|
| 210 |
+
f"({elapsed_ms:.0f}ms, status={resp.status_code})"
|
| 211 |
+
)
|
| 212 |
+
else:
|
| 213 |
+
provider.healthy = False
|
| 214 |
+
logger.warning(
|
| 215 |
+
f"SmartRouter: {provider.name} unhealthy "
|
| 216 |
+
f"(status={resp.status_code})"
|
| 217 |
+
)
|
| 218 |
+
except Exception as e:
|
| 219 |
+
provider.healthy = False
|
| 220 |
+
logger.warning(f"SmartRouter: {provider.name} unreachable β {e}")
|
| 221 |
+
|
| 222 |
+
# ββ Routing logic βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 223 |
+
|
| 224 |
+
def select_provider(self, is_large_request: bool = False) -> Optional[Provider]:
|
| 225 |
+
"""
|
| 226 |
+
Pick the best available provider for this request.
|
| 227 |
+
Returns None if no providers are available.
|
| 228 |
+
"""
|
| 229 |
+
available = [
|
| 230 |
+
p for p in self.providers
|
| 231 |
+
if p.healthy and p.is_configured
|
| 232 |
+
]
|
| 233 |
+
if not available:
|
| 234 |
+
return None
|
| 235 |
+
|
| 236 |
+
return min(available, key=lambda p: p.score(self.strategy))
|
| 237 |
+
|
| 238 |
+
def get_model_for_provider(
|
| 239 |
+
self,
|
| 240 |
+
provider: Provider,
|
| 241 |
+
claude_model: str,
|
| 242 |
+
is_large_request: bool = False,
|
| 243 |
+
) -> str:
|
| 244 |
+
"""Map a Claude model name to the provider's actual model."""
|
| 245 |
+
if is_large_request:
|
| 246 |
+
return provider.big_model
|
| 247 |
+
is_large = any(
|
| 248 |
+
keyword in claude_model.lower()
|
| 249 |
+
for keyword in ["opus", "sonnet", "large", "big"]
|
| 250 |
+
)
|
| 251 |
+
return provider.big_model if is_large else provider.small_model
|
| 252 |
+
|
| 253 |
+
def is_large_request(self, messages: list[dict]) -> bool:
|
| 254 |
+
"""Estimate if this is a large request based on message length."""
|
| 255 |
+
total_chars = sum(
|
| 256 |
+
len(str(m.get("content", ""))) for m in messages
|
| 257 |
+
)
|
| 258 |
+
return total_chars > 2000 # >2000 chars = treat as large
|
| 259 |
+
|
| 260 |
+
def _update_latency(self, provider: Provider, duration_ms: float) -> None:
|
| 261 |
+
"""Exponential moving average update for latency tracking."""
|
| 262 |
+
alpha = 0.3 # weight for new observation
|
| 263 |
+
provider.avg_latency_ms = (
|
| 264 |
+
alpha * duration_ms + (1 - alpha) * provider.avg_latency_ms
|
| 265 |
+
)
|
| 266 |
+
|
| 267 |
+
# ββ Main routing entry point ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 268 |
+
|
| 269 |
+
async def route(
|
| 270 |
+
self,
|
| 271 |
+
messages: list[dict],
|
| 272 |
+
claude_model: str = "claude-sonnet",
|
| 273 |
+
attempt: int = 0,
|
| 274 |
+
exclude_providers: Optional[list[str]] = None,
|
| 275 |
+
) -> dict:
|
| 276 |
+
"""
|
| 277 |
+
Route a request to the best provider.
|
| 278 |
+
Returns a dict with routing decision info:
|
| 279 |
+
{
|
| 280 |
+
"provider": provider name,
|
| 281 |
+
"model": actual model to use,
|
| 282 |
+
"api_key": API key for the provider,
|
| 283 |
+
"base_url": base URL for the provider,
|
| 284 |
+
}
|
| 285 |
+
Raises RuntimeError if no providers available.
|
| 286 |
+
"""
|
| 287 |
+
if not self._initialized:
|
| 288 |
+
await self.initialize()
|
| 289 |
+
|
| 290 |
+
exclude = set(exclude_providers or [])
|
| 291 |
+
large = self.is_large_request(messages)
|
| 292 |
+
|
| 293 |
+
available = [
|
| 294 |
+
p for p in self.providers
|
| 295 |
+
if p.healthy and p.is_configured and p.name not in exclude
|
| 296 |
+
]
|
| 297 |
+
|
| 298 |
+
if not available:
|
| 299 |
+
raise RuntimeError(
|
| 300 |
+
"SmartRouter: no providers available. "
|
| 301 |
+
"Check your API keys and provider health."
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
provider = min(available, key=lambda p: p.score(self.strategy))
|
| 305 |
+
model = self.get_model_for_provider(
|
| 306 |
+
provider,
|
| 307 |
+
claude_model,
|
| 308 |
+
is_large_request=large,
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
logger.debug(
|
| 312 |
+
f"SmartRouter: routing to {provider.name}/{model} "
|
| 313 |
+
f"(strategy={self.strategy}, large={large}, attempt={attempt})"
|
| 314 |
+
)
|
| 315 |
+
|
| 316 |
+
return {
|
| 317 |
+
"provider": provider.name,
|
| 318 |
+
"model": model,
|
| 319 |
+
"api_key": provider.api_key or "none",
|
| 320 |
+
"provider_object": provider,
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
async def record_result(
|
| 324 |
+
self,
|
| 325 |
+
provider_name: str,
|
| 326 |
+
success: bool,
|
| 327 |
+
duration_ms: float,
|
| 328 |
+
) -> None:
|
| 329 |
+
"""
|
| 330 |
+
Record the outcome of a request.
|
| 331 |
+
Called after each proxied request to update provider scores.
|
| 332 |
+
"""
|
| 333 |
+
provider = next(
|
| 334 |
+
(p for p in self.providers if p.name == provider_name), None
|
| 335 |
+
)
|
| 336 |
+
if not provider:
|
| 337 |
+
return
|
| 338 |
+
|
| 339 |
+
provider.request_count += 1
|
| 340 |
+
if success:
|
| 341 |
+
self._update_latency(provider, duration_ms)
|
| 342 |
+
else:
|
| 343 |
+
provider.error_count += 1
|
| 344 |
+
# After 3 consecutive failures, mark unhealthy temporarily
|
| 345 |
+
recent_errors = provider.error_count
|
| 346 |
+
recent_total = provider.request_count
|
| 347 |
+
if recent_total >= 3 and (recent_errors / recent_total) > 0.7:
|
| 348 |
+
logger.warning(
|
| 349 |
+
f"SmartRouter: {provider_name} error rate high "
|
| 350 |
+
f"({provider.error_rate:.0%}), marking unhealthy"
|
| 351 |
+
)
|
| 352 |
+
provider.healthy = False
|
| 353 |
+
# Schedule re-check after 60s
|
| 354 |
+
asyncio.create_task(self._recheck_provider(provider, delay=60))
|
| 355 |
+
|
| 356 |
+
async def _recheck_provider(
|
| 357 |
+
self, provider: Provider, delay: float = 60
|
| 358 |
+
) -> None:
|
| 359 |
+
"""Re-ping a provider after a delay and restore if healthy."""
|
| 360 |
+
await asyncio.sleep(delay)
|
| 361 |
+
await self._ping_provider(provider)
|
| 362 |
+
if provider.healthy:
|
| 363 |
+
logger.info(
|
| 364 |
+
f"SmartRouter: {provider.name} recovered, "
|
| 365 |
+
f"re-adding to pool"
|
| 366 |
+
)
|
| 367 |
+
|
| 368 |
+
# ββ Status report βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 369 |
+
|
| 370 |
+
def status(self) -> list[dict]:
|
| 371 |
+
"""Return current provider status for monitoring."""
|
| 372 |
+
return [
|
| 373 |
+
{
|
| 374 |
+
"provider": p.name,
|
| 375 |
+
"healthy": p.healthy,
|
| 376 |
+
"configured": p.is_configured,
|
| 377 |
+
"latency_ms": round(p.avg_latency_ms, 1),
|
| 378 |
+
"cost_per_1k": p.cost_per_1k_tokens,
|
| 379 |
+
"requests": p.request_count,
|
| 380 |
+
"errors": p.error_count,
|
| 381 |
+
"error_rate": f"{p.error_rate:.1%}",
|
| 382 |
+
"score": round(p.score(self.strategy), 3)
|
| 383 |
+
if p.healthy and p.is_configured
|
| 384 |
+
else "N/A",
|
| 385 |
+
}
|
| 386 |
+
for p in self.providers
|
| 387 |
+
]
|
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Pytest package marker for the Python helper test suite.
|
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
# Make the sibling `python/` helper modules importable from this test package.
|
| 5 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
test_atomic_chat_provider.py
|
| 3 |
+
Run: pytest python/tests/test_atomic_chat_provider.py -v
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import pytest
|
| 7 |
+
from unittest.mock import AsyncMock, MagicMock, patch
|
| 8 |
+
from atomic_chat_provider import (
|
| 9 |
+
atomic_chat,
|
| 10 |
+
list_atomic_chat_models,
|
| 11 |
+
check_atomic_chat_running,
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@pytest.mark.asyncio
|
| 16 |
+
async def test_atomic_chat_running_true():
|
| 17 |
+
mock_response = MagicMock()
|
| 18 |
+
mock_response.status_code = 200
|
| 19 |
+
with patch("atomic_chat_provider.httpx.AsyncClient") as MockClient:
|
| 20 |
+
MockClient.return_value.__aenter__.return_value.get = AsyncMock(return_value=mock_response)
|
| 21 |
+
result = await check_atomic_chat_running()
|
| 22 |
+
assert result is True
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@pytest.mark.asyncio
|
| 26 |
+
async def test_atomic_chat_running_false_on_exception():
|
| 27 |
+
with patch("atomic_chat_provider.httpx.AsyncClient") as MockClient:
|
| 28 |
+
MockClient.return_value.__aenter__.return_value.get = AsyncMock(side_effect=Exception("refused"))
|
| 29 |
+
result = await check_atomic_chat_running()
|
| 30 |
+
assert result is False
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@pytest.mark.asyncio
|
| 34 |
+
async def test_list_models_returns_ids():
|
| 35 |
+
mock_response = MagicMock()
|
| 36 |
+
mock_response.status_code = 200
|
| 37 |
+
mock_response.json.return_value = {
|
| 38 |
+
"data": [{"id": "llama-3.1-8b"}, {"id": "mistral-7b"}],
|
| 39 |
+
}
|
| 40 |
+
mock_response.raise_for_status = MagicMock()
|
| 41 |
+
with patch("atomic_chat_provider.httpx.AsyncClient") as MockClient:
|
| 42 |
+
MockClient.return_value.__aenter__.return_value.get = AsyncMock(return_value=mock_response)
|
| 43 |
+
models = await list_atomic_chat_models()
|
| 44 |
+
assert "llama-3.1-8b" in models
|
| 45 |
+
assert "mistral-7b" in models
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@pytest.mark.asyncio
|
| 49 |
+
async def test_list_models_empty_on_failure():
|
| 50 |
+
with patch("atomic_chat_provider.httpx.AsyncClient") as MockClient:
|
| 51 |
+
MockClient.return_value.__aenter__.return_value.get = AsyncMock(side_effect=Exception("down"))
|
| 52 |
+
models = await list_atomic_chat_models()
|
| 53 |
+
assert models == []
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@pytest.mark.asyncio
|
| 57 |
+
async def test_atomic_chat_returns_anthropic_format():
|
| 58 |
+
mock_response = MagicMock()
|
| 59 |
+
mock_response.raise_for_status = MagicMock()
|
| 60 |
+
mock_response.json.return_value = {
|
| 61 |
+
"id": "chatcmpl-abc123",
|
| 62 |
+
"choices": [{"message": {"content": "42 is the answer."}}],
|
| 63 |
+
"usage": {"prompt_tokens": 10, "completion_tokens": 8},
|
| 64 |
+
}
|
| 65 |
+
with patch("atomic_chat_provider.httpx.AsyncClient") as MockClient:
|
| 66 |
+
MockClient.return_value.__aenter__.return_value.post = AsyncMock(return_value=mock_response)
|
| 67 |
+
result = await atomic_chat(
|
| 68 |
+
model="llama-3.1-8b",
|
| 69 |
+
messages=[{"role": "user", "content": "What is 6*7?"}],
|
| 70 |
+
)
|
| 71 |
+
assert result["type"] == "message"
|
| 72 |
+
assert result["role"] == "assistant"
|
| 73 |
+
assert "42" in result["content"][0]["text"]
|
| 74 |
+
assert result["usage"]["input_tokens"] == 10
|
| 75 |
+
assert result["usage"]["output_tokens"] == 8
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
@pytest.mark.asyncio
|
| 79 |
+
async def test_atomic_chat_prepends_system():
|
| 80 |
+
captured = {}
|
| 81 |
+
|
| 82 |
+
async def mock_post(url, json=None, **kwargs):
|
| 83 |
+
captured.update(json or {})
|
| 84 |
+
m = MagicMock()
|
| 85 |
+
m.raise_for_status = MagicMock()
|
| 86 |
+
m.json.return_value = {
|
| 87 |
+
"id": "chatcmpl-xyz",
|
| 88 |
+
"choices": [{"message": {"content": "ok"}}],
|
| 89 |
+
"usage": {"prompt_tokens": 1, "completion_tokens": 1},
|
| 90 |
+
}
|
| 91 |
+
return m
|
| 92 |
+
|
| 93 |
+
with patch("atomic_chat_provider.httpx.AsyncClient") as MockClient:
|
| 94 |
+
MockClient.return_value.__aenter__.return_value.post = mock_post
|
| 95 |
+
await atomic_chat(
|
| 96 |
+
model="llama-3.1-8b",
|
| 97 |
+
messages=[{"role": "user", "content": "Hi"}],
|
| 98 |
+
system="Be helpful.",
|
| 99 |
+
)
|
| 100 |
+
assert captured["messages"][0]["role"] == "system"
|
| 101 |
+
assert "helpful" in captured["messages"][0]["content"]
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
@pytest.mark.asyncio
|
| 105 |
+
async def test_atomic_chat_sends_correct_payload():
|
| 106 |
+
captured = {}
|
| 107 |
+
|
| 108 |
+
async def mock_post(url, json=None, **kwargs):
|
| 109 |
+
captured.update(json or {})
|
| 110 |
+
m = MagicMock()
|
| 111 |
+
m.raise_for_status = MagicMock()
|
| 112 |
+
m.json.return_value = {
|
| 113 |
+
"id": "chatcmpl-xyz",
|
| 114 |
+
"choices": [{"message": {"content": "ok"}}],
|
| 115 |
+
"usage": {"prompt_tokens": 1, "completion_tokens": 1},
|
| 116 |
+
}
|
| 117 |
+
return m
|
| 118 |
+
|
| 119 |
+
with patch("atomic_chat_provider.httpx.AsyncClient") as MockClient:
|
| 120 |
+
MockClient.return_value.__aenter__.return_value.post = mock_post
|
| 121 |
+
await atomic_chat(
|
| 122 |
+
model="test-model",
|
| 123 |
+
messages=[{"role": "user", "content": "Test"}],
|
| 124 |
+
max_tokens=2048,
|
| 125 |
+
temperature=0.5,
|
| 126 |
+
)
|
| 127 |
+
assert captured["model"] == "test-model"
|
| 128 |
+
assert captured["max_tokens"] == 2048
|
| 129 |
+
assert captured["temperature"] == 0.5
|
| 130 |
+
assert captured["stream"] is False
|
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
test_ollama_provider.py
|
| 3 |
+
Run: pytest python/tests/test_ollama_provider.py -v
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import pytest
|
| 7 |
+
from unittest.mock import AsyncMock, MagicMock, patch
|
| 8 |
+
from ollama_provider import (
|
| 9 |
+
normalize_ollama_model,
|
| 10 |
+
anthropic_to_ollama_messages,
|
| 11 |
+
ollama_chat,
|
| 12 |
+
list_ollama_models,
|
| 13 |
+
check_ollama_running,
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def test_normalize_strips_prefix():
|
| 18 |
+
assert normalize_ollama_model("ollama/llama3:8b") == "llama3:8b"
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def test_normalize_no_prefix():
|
| 22 |
+
assert normalize_ollama_model("codellama:34b") == "codellama:34b"
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def test_normalize_empty():
|
| 26 |
+
assert normalize_ollama_model("") == ""
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def test_converts_string_content():
|
| 30 |
+
messages = [{"role": "user", "content": "Hello!"}]
|
| 31 |
+
result = anthropic_to_ollama_messages(messages)
|
| 32 |
+
assert result == [{"role": "user", "content": "Hello!"}]
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def test_converts_text_block_list():
|
| 36 |
+
messages = [{"role": "user", "content": [{"type": "text", "text": "What is Python?"}]}]
|
| 37 |
+
result = anthropic_to_ollama_messages(messages)
|
| 38 |
+
assert result[0]["content"] == "What is Python?"
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def test_converts_image_block_to_placeholder():
|
| 42 |
+
messages = [{"role": "user", "content": [{"type": "image", "source": {}}, {"type": "text", "text": "Describe this"}]}]
|
| 43 |
+
result = anthropic_to_ollama_messages(messages)
|
| 44 |
+
assert "[image]" in result[0]["content"]
|
| 45 |
+
assert "Describe this" in result[0]["content"]
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def test_converts_base64_image_block_to_ollama_images():
|
| 49 |
+
messages = [{
|
| 50 |
+
"role": "user",
|
| 51 |
+
"content": [
|
| 52 |
+
{
|
| 53 |
+
"type": "image",
|
| 54 |
+
"source": {
|
| 55 |
+
"type": "base64",
|
| 56 |
+
"media_type": "image/png",
|
| 57 |
+
"data": "YWJjMTIz",
|
| 58 |
+
},
|
| 59 |
+
},
|
| 60 |
+
{"type": "text", "text": "Describe this"},
|
| 61 |
+
],
|
| 62 |
+
}]
|
| 63 |
+
result = anthropic_to_ollama_messages(messages)
|
| 64 |
+
assert result[0]["images"] == ["YWJjMTIz"]
|
| 65 |
+
assert "Describe this" in result[0]["content"]
|
| 66 |
+
|
| 67 |
+
def test_converts_multi_turn():
|
| 68 |
+
messages = [
|
| 69 |
+
{"role": "user", "content": "Hi"},
|
| 70 |
+
{"role": "assistant", "content": "Hello!"},
|
| 71 |
+
{"role": "user", "content": "How are you?"},
|
| 72 |
+
]
|
| 73 |
+
result = anthropic_to_ollama_messages(messages)
|
| 74 |
+
assert len(result) == 3
|
| 75 |
+
assert result[1]["role"] == "assistant"
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
@pytest.mark.asyncio
|
| 79 |
+
async def test_ollama_running_true():
|
| 80 |
+
mock_response = MagicMock()
|
| 81 |
+
mock_response.status_code = 200
|
| 82 |
+
with patch("ollama_provider.httpx.AsyncClient") as MockClient:
|
| 83 |
+
MockClient.return_value.__aenter__.return_value.get = AsyncMock(return_value=mock_response)
|
| 84 |
+
result = await check_ollama_running()
|
| 85 |
+
assert result is True
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
@pytest.mark.asyncio
|
| 89 |
+
async def test_ollama_running_false_on_exception():
|
| 90 |
+
with patch("ollama_provider.httpx.AsyncClient") as MockClient:
|
| 91 |
+
MockClient.return_value.__aenter__.return_value.get = AsyncMock(side_effect=Exception("refused"))
|
| 92 |
+
result = await check_ollama_running()
|
| 93 |
+
assert result is False
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
@pytest.mark.asyncio
|
| 97 |
+
async def test_list_models_returns_names():
|
| 98 |
+
mock_response = MagicMock()
|
| 99 |
+
mock_response.status_code = 200
|
| 100 |
+
mock_response.json.return_value = {"models": [{"name": "llama3:8b"}, {"name": "codellama:34b"}]}
|
| 101 |
+
mock_response.raise_for_status = MagicMock()
|
| 102 |
+
with patch("ollama_provider.httpx.AsyncClient") as MockClient:
|
| 103 |
+
MockClient.return_value.__aenter__.return_value.get = AsyncMock(return_value=mock_response)
|
| 104 |
+
models = await list_ollama_models()
|
| 105 |
+
assert "llama3:8b" in models
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
@pytest.mark.asyncio
|
| 109 |
+
async def test_ollama_chat_returns_anthropic_format():
|
| 110 |
+
mock_response = MagicMock()
|
| 111 |
+
mock_response.raise_for_status = MagicMock()
|
| 112 |
+
mock_response.json.return_value = {
|
| 113 |
+
"message": {"content": "42 is the answer."},
|
| 114 |
+
"created_at": "2026-01-01T00:00:00Z",
|
| 115 |
+
"prompt_eval_count": 10,
|
| 116 |
+
"eval_count": 8,
|
| 117 |
+
}
|
| 118 |
+
with patch("ollama_provider.httpx.AsyncClient") as MockClient:
|
| 119 |
+
MockClient.return_value.__aenter__.return_value.post = AsyncMock(return_value=mock_response)
|
| 120 |
+
result = await ollama_chat(
|
| 121 |
+
model="llama3:8b",
|
| 122 |
+
messages=[{"role": "user", "content": "What is 6*7?"}]
|
| 123 |
+
)
|
| 124 |
+
assert result["type"] == "message"
|
| 125 |
+
assert result["role"] == "assistant"
|
| 126 |
+
assert "42" in result["content"][0]["text"]
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
@pytest.mark.asyncio
|
| 130 |
+
async def test_ollama_chat_prepends_system():
|
| 131 |
+
captured = {}
|
| 132 |
+
|
| 133 |
+
async def mock_post(url, json=None, **kwargs):
|
| 134 |
+
captured.update(json or {})
|
| 135 |
+
m = MagicMock()
|
| 136 |
+
m.raise_for_status = MagicMock()
|
| 137 |
+
m.json.return_value = {
|
| 138 |
+
"message": {"content": "ok"},
|
| 139 |
+
"created_at": "",
|
| 140 |
+
"prompt_eval_count": 1,
|
| 141 |
+
"eval_count": 1
|
| 142 |
+
}
|
| 143 |
+
return m
|
| 144 |
+
with patch("ollama_provider.httpx.AsyncClient") as MockClient:
|
| 145 |
+
MockClient.return_value.__aenter__.return_value.post = mock_post
|
| 146 |
+
await ollama_chat(
|
| 147 |
+
model="llama3:8b",
|
| 148 |
+
messages=[{"role": "user", "content": "Hi"}],
|
| 149 |
+
system="Be helpful.",
|
| 150 |
+
)
|
| 151 |
+
assert captured["messages"][0]["role"] == "system"
|
| 152 |
+
assert "helpful" in captured["messages"][0]["content"]
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
@pytest.mark.asyncio
|
| 156 |
+
async def test_ollama_chat_includes_base64_images_in_payload():
|
| 157 |
+
captured = {}
|
| 158 |
+
|
| 159 |
+
async def mock_post(url, json=None, **kwargs):
|
| 160 |
+
captured.update(json or {})
|
| 161 |
+
m = MagicMock()
|
| 162 |
+
m.raise_for_status = MagicMock()
|
| 163 |
+
m.json.return_value = {
|
| 164 |
+
"message": {"content": "ok"},
|
| 165 |
+
"created_at": "",
|
| 166 |
+
"prompt_eval_count": 1,
|
| 167 |
+
"eval_count": 1,
|
| 168 |
+
}
|
| 169 |
+
return m
|
| 170 |
+
|
| 171 |
+
with patch("ollama_provider.httpx.AsyncClient") as MockClient:
|
| 172 |
+
MockClient.return_value.__aenter__.return_value.post = mock_post
|
| 173 |
+
await ollama_chat(
|
| 174 |
+
model="llama3:8b",
|
| 175 |
+
messages=[{
|
| 176 |
+
"role": "user",
|
| 177 |
+
"content": [
|
| 178 |
+
{
|
| 179 |
+
"type": "image",
|
| 180 |
+
"source": {
|
| 181 |
+
"type": "base64",
|
| 182 |
+
"media_type": "image/jpeg",
|
| 183 |
+
"data": "ZHVtbXk=",
|
| 184 |
+
},
|
| 185 |
+
},
|
| 186 |
+
{"type": "text", "text": "What is in this image?"},
|
| 187 |
+
],
|
| 188 |
+
}],
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
assert captured["messages"][0]["images"] == ["ZHVtbXk="]
|
| 192 |
+
assert "What is in this image?" in captured["messages"][0]["content"]
|
|
@@ -0,0 +1,231 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
test_smart_router.py
|
| 3 |
+
--------------------
|
| 4 |
+
Tests for the SmartRouter.
|
| 5 |
+
Run: pytest python/tests/test_smart_router.py -v
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import pytest
|
| 9 |
+
import asyncio
|
| 10 |
+
from unittest.mock import AsyncMock, MagicMock, patch
|
| 11 |
+
from smart_router import SmartRouter, Provider
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
# ββ Fixtures ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@pytest.fixture(autouse=True)
|
| 18 |
+
def fake_api_key(monkeypatch):
|
| 19 |
+
monkeypatch.setenv("FAKE_KEY", "test-key")
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def make_provider(name, healthy=True, configured=True,
|
| 23 |
+
latency=100.0, cost=0.002, errors=0, requests=0):
|
| 24 |
+
p = Provider(
|
| 25 |
+
name=name,
|
| 26 |
+
ping_url=f"https://{name}.example.com/health",
|
| 27 |
+
api_key_env="FAKE_KEY",
|
| 28 |
+
cost_per_1k_tokens=cost,
|
| 29 |
+
big_model=f"{name}-big",
|
| 30 |
+
small_model=f"{name}-small",
|
| 31 |
+
)
|
| 32 |
+
p.healthy = healthy
|
| 33 |
+
p.avg_latency_ms = latency
|
| 34 |
+
p.error_count = errors
|
| 35 |
+
p.request_count = requests
|
| 36 |
+
if not configured:
|
| 37 |
+
p.api_key_env = "" # makes is_configured False for non-local providers
|
| 38 |
+
return p
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def make_router(providers=None, strategy="balanced"):
|
| 42 |
+
r = SmartRouter(providers=providers, strategy=strategy)
|
| 43 |
+
r._initialized = True
|
| 44 |
+
return r
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
# ββ Provider.score() ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 48 |
+
|
| 49 |
+
def test_score_unhealthy_is_inf():
|
| 50 |
+
p = make_provider("openai", healthy=False)
|
| 51 |
+
assert p.score() == float("inf")
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def test_score_unconfigured_is_inf():
|
| 55 |
+
p = make_provider("openai", configured=False)
|
| 56 |
+
assert p.score() == float("inf")
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def test_score_latency_strategy_prefers_faster():
|
| 60 |
+
fast = make_provider("fast", latency=50.0, cost=0.01)
|
| 61 |
+
slow = make_provider("slow", latency=500.0, cost=0.001)
|
| 62 |
+
assert fast.score("latency") < slow.score("latency")
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def test_score_cost_strategy_prefers_cheaper():
|
| 66 |
+
cheap = make_provider("cheap", latency=500.0, cost=0.0001)
|
| 67 |
+
expensive = make_provider("expensive", latency=50.0, cost=0.05)
|
| 68 |
+
assert cheap.score("cost") < expensive.score("cost")
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def test_score_balanced_strategy_uses_both():
|
| 72 |
+
p = make_provider("test", latency=200.0, cost=0.002)
|
| 73 |
+
s = p.score("balanced")
|
| 74 |
+
assert s > 0
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def test_score_error_rate_penalty():
|
| 78 |
+
clean = make_provider("clean", errors=0, requests=10)
|
| 79 |
+
dirty = make_provider("dirty", errors=8, requests=10)
|
| 80 |
+
assert clean.score() < dirty.score()
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
# ββ SmartRouter.is_large_request() βββββββββββββββββββββββββββββββββββββββββββ
|
| 84 |
+
|
| 85 |
+
def test_is_large_request_short():
|
| 86 |
+
r = make_router()
|
| 87 |
+
msgs = [{"role": "user", "content": "Hello!"}]
|
| 88 |
+
assert r.is_large_request(msgs) is False
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def test_is_large_request_long():
|
| 92 |
+
r = make_router()
|
| 93 |
+
msgs = [{"role": "user", "content": "x" * 3000}]
|
| 94 |
+
assert r.is_large_request(msgs) is True
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
# ββ SmartRouter.select_provider() ββββββββββββββββββββββββββββββββββββββββββββ
|
| 98 |
+
|
| 99 |
+
def test_select_provider_picks_best_score():
|
| 100 |
+
p1 = make_provider("slow", latency=800.0)
|
| 101 |
+
p2 = make_provider("fast", latency=50.0)
|
| 102 |
+
r = make_router(providers=[p1, p2], strategy="latency")
|
| 103 |
+
selected = r.select_provider()
|
| 104 |
+
assert selected.name == "fast"
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def test_select_provider_skips_unhealthy():
|
| 108 |
+
p1 = make_provider("bad", healthy=False)
|
| 109 |
+
p2 = make_provider("good", healthy=True)
|
| 110 |
+
r = make_router(providers=[p1, p2])
|
| 111 |
+
selected = r.select_provider()
|
| 112 |
+
assert selected.name == "good"
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def test_select_provider_returns_none_when_all_down():
|
| 116 |
+
p1 = make_provider("a", healthy=False)
|
| 117 |
+
p2 = make_provider("b", healthy=False)
|
| 118 |
+
r = make_router(providers=[p1, p2])
|
| 119 |
+
assert r.select_provider() is None
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
# ββ SmartRouter.get_model_for_provider() βββββββββββββββββββββββββββββββββββββ
|
| 123 |
+
|
| 124 |
+
def test_get_model_large_request():
|
| 125 |
+
p = make_provider("openai")
|
| 126 |
+
r = make_router()
|
| 127 |
+
model = r.get_model_for_provider(p, "claude-sonnet")
|
| 128 |
+
assert model == "openai-big"
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def test_get_model_large_message_overrides_claude_label():
|
| 132 |
+
p = make_provider("openai")
|
| 133 |
+
r = make_router()
|
| 134 |
+
model = r.get_model_for_provider(p, "claude-haiku", is_large_request=True)
|
| 135 |
+
assert model == "openai-big"
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def test_get_model_small_request():
|
| 139 |
+
p = make_provider("openai")
|
| 140 |
+
r = make_router()
|
| 141 |
+
model = r.get_model_for_provider(p, "claude-haiku")
|
| 142 |
+
assert model == "openai-small"
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
# ββ SmartRouter.route() βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 146 |
+
|
| 147 |
+
@pytest.mark.asyncio
|
| 148 |
+
async def test_route_returns_best_provider():
|
| 149 |
+
p1 = make_provider("expensive", cost=0.05, latency=50.0)
|
| 150 |
+
p2 = make_provider("cheap", cost=0.0005, latency=200.0)
|
| 151 |
+
r = make_router(providers=[p1, p2], strategy="cost")
|
| 152 |
+
result = await r.route([{"role": "user", "content": "Hi"}], "claude-haiku")
|
| 153 |
+
assert result["provider"] == "cheap"
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
@pytest.mark.asyncio
|
| 157 |
+
async def test_route_uses_big_model_for_large_message_bodies():
|
| 158 |
+
p = make_provider("openai")
|
| 159 |
+
r = make_router(providers=[p])
|
| 160 |
+
result = await r.route([
|
| 161 |
+
{"role": "user", "content": "x" * 3001},
|
| 162 |
+
], "claude-haiku")
|
| 163 |
+
assert result["model"] == "openai-big"
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
@pytest.mark.asyncio
|
| 167 |
+
async def test_route_raises_when_no_providers():
|
| 168 |
+
p = make_provider("a", healthy=False)
|
| 169 |
+
r = make_router(providers=[p])
|
| 170 |
+
with pytest.raises(RuntimeError, match="no providers available"):
|
| 171 |
+
await r.route([{"role": "user", "content": "Hi"}])
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
@pytest.mark.asyncio
|
| 175 |
+
async def test_route_excludes_providers():
|
| 176 |
+
p1 = make_provider("openai", latency=50.0)
|
| 177 |
+
p2 = make_provider("gemini", latency=200.0)
|
| 178 |
+
r = make_router(providers=[p1, p2], strategy="latency")
|
| 179 |
+
result = await r.route(
|
| 180 |
+
[{"role": "user", "content": "Hi"}],
|
| 181 |
+
exclude_providers=["openai"]
|
| 182 |
+
)
|
| 183 |
+
assert result["provider"] == "gemini"
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
# ββ SmartRouter.record_result() ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 187 |
+
|
| 188 |
+
@pytest.mark.asyncio
|
| 189 |
+
async def test_record_result_updates_latency():
|
| 190 |
+
p = make_provider("openai", latency=200.0)
|
| 191 |
+
r = make_router(providers=[p])
|
| 192 |
+
await r.record_result("openai", success=True, duration_ms=100.0)
|
| 193 |
+
assert p.avg_latency_ms < 200.0 # should decrease toward 100
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
@pytest.mark.asyncio
|
| 197 |
+
async def test_record_result_increments_requests():
|
| 198 |
+
p = make_provider("openai")
|
| 199 |
+
r = make_router(providers=[p])
|
| 200 |
+
await r.record_result("openai", success=True, duration_ms=100.0)
|
| 201 |
+
assert p.request_count == 1
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
@pytest.mark.asyncio
|
| 205 |
+
async def test_record_result_increments_errors():
|
| 206 |
+
p = make_provider("openai")
|
| 207 |
+
r = make_router(providers=[p])
|
| 208 |
+
await r.record_result("openai", success=False, duration_ms=0)
|
| 209 |
+
assert p.error_count == 1
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
# ββ SmartRouter.status() βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 213 |
+
|
| 214 |
+
def test_status_returns_all_providers():
|
| 215 |
+
p1 = make_provider("openai")
|
| 216 |
+
p2 = make_provider("gemini")
|
| 217 |
+
r = make_router(providers=[p1, p2])
|
| 218 |
+
status = r.status()
|
| 219 |
+
assert len(status) == 2
|
| 220 |
+
names = [s["provider"] for s in status]
|
| 221 |
+
assert "openai" in names
|
| 222 |
+
assert "gemini" in names
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
def test_status_contains_required_fields():
|
| 226 |
+
p = make_provider("openai")
|
| 227 |
+
r = make_router(providers=[p])
|
| 228 |
+
status = r.status()[0]
|
| 229 |
+
for field in ["provider", "healthy", "latency_ms",
|
| 230 |
+
"cost_per_1k", "requests", "errors", "score"]:
|
| 231 |
+
assert field in status
|
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
|
| 3 |
+
"packages": {
|
| 4 |
+
".": {
|
| 5 |
+
"release-type": "node",
|
| 6 |
+
"package-name": "@gitlawb/openclaude",
|
| 7 |
+
"bump-minor-pre-major": true,
|
| 8 |
+
"include-v-in-tag": true
|
| 9 |
+
}
|
| 10 |
+
}
|
| 11 |
+
}
|
|
@@ -0,0 +1,492 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* OpenClaude build script β bundles the TypeScript source into a single
|
| 3 |
+
* distributable JS file using Bun's bundler.
|
| 4 |
+
*
|
| 5 |
+
* Handles:
|
| 6 |
+
* - bun:bundle feature() flags for the open build
|
| 7 |
+
* - MACRO.* globals β inlined version/build-time constants
|
| 8 |
+
* - src/ path aliases
|
| 9 |
+
*/
|
| 10 |
+
|
| 11 |
+
import { readFileSync, readdirSync, writeFileSync } from 'fs'
|
| 12 |
+
import { join } from 'path'
|
| 13 |
+
import { noTelemetryPlugin } from './no-telemetry-plugin'
|
| 14 |
+
|
| 15 |
+
const pkg = JSON.parse(readFileSync('./package.json', 'utf-8'))
|
| 16 |
+
const version = pkg.version
|
| 17 |
+
|
| 18 |
+
// Feature flags for the open build.
|
| 19 |
+
// Most Anthropic-internal features stay off; open-build features can be
|
| 20 |
+
// selectively enabled here when their full source exists in the mirror.
|
| 21 |
+
const featureFlags: Record<string, boolean> = {
|
| 22 |
+
// ββ Disabled: require Anthropic infrastructure or missing source βββββ
|
| 23 |
+
VOICE_MODE: false, // Push-to-talk STT via claude.ai OAuth endpoint
|
| 24 |
+
PROACTIVE: false, // Autonomous agent mode (missing proactive/ module)
|
| 25 |
+
KAIROS: false, // Persistent assistant/session mode (cloud backend)
|
| 26 |
+
BRIDGE_MODE: false, // Remote desktop bridge via CCR infrastructure
|
| 27 |
+
DAEMON: false, // Background daemon process (stubbed in open build)
|
| 28 |
+
AGENT_TRIGGERS: false, // Scheduled remote agent triggers
|
| 29 |
+
ABLATION_BASELINE: false, // A/B testing harness for eval experiments
|
| 30 |
+
CONTEXT_COLLAPSE: false, // Context collapsing optimization (stubbed)
|
| 31 |
+
COMMIT_ATTRIBUTION: false, // Co-Authored-By metadata in git commits
|
| 32 |
+
UDS_INBOX: false, // Unix Domain Socket inter-session messaging
|
| 33 |
+
BG_SESSIONS: false, // Background sessions via tmux (stubbed)
|
| 34 |
+
WEB_BROWSER_TOOL: false, // Built-in browser automation (source not mirrored)
|
| 35 |
+
CHICAGO_MCP: false, // Computer-use MCP (native Swift modules stubbed)
|
| 36 |
+
COWORKER_TYPE_TELEMETRY: false, // Telemetry for agent/coworker type classification
|
| 37 |
+
|
| 38 |
+
// ββ Enabled: upstream defaults ββββββββββββββββββββββββββββββββββββββ
|
| 39 |
+
COORDINATOR_MODE: true, // Multi-agent coordinator with worker delegation
|
| 40 |
+
BUILTIN_EXPLORE_PLAN_AGENTS: true, // Built-in Explore/Plan specialized subagents
|
| 41 |
+
BUDDY: true, // Buddy mode for paired programming
|
| 42 |
+
MONITOR_TOOL: true, // MCP server monitoring/streaming tool
|
| 43 |
+
TEAMMEM: true, // Team memory management
|
| 44 |
+
MESSAGE_ACTIONS: true, // Message action buttons in the UI
|
| 45 |
+
|
| 46 |
+
// ββ Enabled: new activations ββββββββββββββββββββββββββββββββββββββββ
|
| 47 |
+
DUMP_SYSTEM_PROMPT: true, // --dump-system-prompt CLI flag for debugging
|
| 48 |
+
CACHED_MICROCOMPACT: true, // Cache-aware tool result truncation optimization
|
| 49 |
+
AWAY_SUMMARY: true, // "While you were away" recap after 5min blur
|
| 50 |
+
TRANSCRIPT_CLASSIFIER: true, // Auto-approval classifier for safe tool uses
|
| 51 |
+
ULTRATHINK: true, // Deep thinking mode β type "ultrathink" to boost reasoning
|
| 52 |
+
TOKEN_BUDGET: true, // Token budget tracking with usage warnings
|
| 53 |
+
HISTORY_PICKER: true, // Enhanced interactive prompt history picker
|
| 54 |
+
QUICK_SEARCH: true, // Ctrl+G quick search across prompts
|
| 55 |
+
SHOT_STATS: true, // Shot distribution stats in session summary
|
| 56 |
+
EXTRACT_MEMORIES: true, // Auto-extract durable memories from conversations
|
| 57 |
+
FORK_SUBAGENT: true, // Implicit context-forking when omitting subagent_type
|
| 58 |
+
VERIFICATION_AGENT: true, // Built-in read-only agent for test/verification
|
| 59 |
+
MCP_SKILLS: true, // Discover skills dynamically from MCP server resources
|
| 60 |
+
PROMPT_CACHE_BREAK_DETECTION: true, // Detect & log unexpected prompt cache invalidations
|
| 61 |
+
HOOK_PROMPTS: true, // Allow tools to request interactive user prompts
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
// ββ Pre-process: replace feature() calls with boolean literals ββββββ
|
| 65 |
+
// Bun v1.3.9+ resolves `import { feature } from 'bun:bundle'` natively
|
| 66 |
+
// before plugins can intercept it via onResolve. The bun: namespace is
|
| 67 |
+
// handled by Bun's C++ resolver which runs before the JS plugin phase,
|
| 68 |
+
// so the previous onResolve/onLoad shim was silently ineffective β ALL
|
| 69 |
+
// feature() calls evaluated to false regardless of the featureFlags map.
|
| 70 |
+
//
|
| 71 |
+
// Fix: pre-process source files to strip the bun:bundle import and
|
| 72 |
+
// replace feature('FLAG') calls with their boolean literal. Files are
|
| 73 |
+
// modified in-place before Bun.build() and restored in a finally block.
|
| 74 |
+
|
| 75 |
+
// Match feature('FLAG') calls, including multi-line: feature(\n 'FLAG',\n)
|
| 76 |
+
const featureCallRe = /\bfeature\(\s*['"](\w+)['"][,\s]*\)/gs
|
| 77 |
+
const featureImportRe = /import\s*\{[^}]*\bfeature\b[^}]*\}\s*from\s*['"]bun:bundle['"];?\s*\n?/g
|
| 78 |
+
const modifiedFiles = new Map<string, string>() // path β original content
|
| 79 |
+
|
| 80 |
+
function preProcessFeatureFlags(dir: string) {
|
| 81 |
+
for (const ent of readdirSync(dir, { withFileTypes: true })) {
|
| 82 |
+
const full = join(dir, ent.name)
|
| 83 |
+
if (ent.isDirectory()) { preProcessFeatureFlags(full); continue }
|
| 84 |
+
if (!/\.(ts|tsx)$/.test(ent.name)) continue
|
| 85 |
+
|
| 86 |
+
const raw = readFileSync(full, 'utf-8')
|
| 87 |
+
if (!raw.includes('feature(')) continue
|
| 88 |
+
|
| 89 |
+
let contents = raw
|
| 90 |
+
contents = contents.replace(featureImportRe, '')
|
| 91 |
+
contents = contents.replace(featureCallRe, (_match, name) =>
|
| 92 |
+
String((featureFlags as Record<string, boolean>)[name] ?? false),
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
if (contents !== raw) {
|
| 96 |
+
modifiedFiles.set(full, raw)
|
| 97 |
+
writeFileSync(full, contents)
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
function restoreModifiedFiles() {
|
| 103 |
+
for (const [path, original] of modifiedFiles) {
|
| 104 |
+
writeFileSync(path, original)
|
| 105 |
+
}
|
| 106 |
+
modifiedFiles.clear()
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
preProcessFeatureFlags(join(import.meta.dir, '..', 'src'))
|
| 110 |
+
const numModified = modifiedFiles.size
|
| 111 |
+
|
| 112 |
+
// Restore source files on abrupt termination (Ctrl+C, kill, etc.)
|
| 113 |
+
for (const signal of ['SIGINT', 'SIGTERM'] as const) {
|
| 114 |
+
process.on(signal, () => {
|
| 115 |
+
restoreModifiedFiles()
|
| 116 |
+
process.exit(signal === 'SIGINT' ? 130 : 143)
|
| 117 |
+
})
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
try {
|
| 121 |
+
|
| 122 |
+
const result = await Bun.build({
|
| 123 |
+
entrypoints: ['./src/entrypoints/cli.tsx'],
|
| 124 |
+
outdir: './dist',
|
| 125 |
+
target: 'node',
|
| 126 |
+
format: 'esm',
|
| 127 |
+
splitting: false,
|
| 128 |
+
sourcemap: 'external',
|
| 129 |
+
minify: false,
|
| 130 |
+
naming: 'cli.mjs',
|
| 131 |
+
define: {
|
| 132 |
+
// MACRO.* build-time constants
|
| 133 |
+
// Keep the internal compatibility version high enough to pass
|
| 134 |
+
// first-party minimum-version guards, but expose the real package
|
| 135 |
+
// version separately in Open Claude branding.
|
| 136 |
+
'MACRO.VERSION': JSON.stringify('99.0.0'),
|
| 137 |
+
'MACRO.DISPLAY_VERSION': JSON.stringify(version),
|
| 138 |
+
'MACRO.BUILD_TIME': JSON.stringify(new Date().toISOString()),
|
| 139 |
+
'MACRO.ISSUES_EXPLAINER':
|
| 140 |
+
JSON.stringify('report the issue at https://github.com/anthropics/claude-code/issues'),
|
| 141 |
+
'MACRO.PACKAGE_URL': JSON.stringify('@gitlawb/openclaude'),
|
| 142 |
+
'MACRO.NATIVE_PACKAGE_URL': 'undefined',
|
| 143 |
+
},
|
| 144 |
+
plugins: [
|
| 145 |
+
noTelemetryPlugin,
|
| 146 |
+
{
|
| 147 |
+
name: 'bun-bundle-shim',
|
| 148 |
+
setup(build) {
|
| 149 |
+
const internalFeatureStubModules = new Map([
|
| 150 |
+
[
|
| 151 |
+
'../daemon/workerRegistry.js',
|
| 152 |
+
'export async function runDaemonWorker() { throw new Error("Daemon worker is unavailable in the open build."); }',
|
| 153 |
+
],
|
| 154 |
+
[
|
| 155 |
+
'../daemon/main.js',
|
| 156 |
+
'export async function daemonMain() { throw new Error("Daemon mode is unavailable in the open build."); }',
|
| 157 |
+
],
|
| 158 |
+
[
|
| 159 |
+
'../cli/bg.js',
|
| 160 |
+
`
|
| 161 |
+
export async function psHandler() { throw new Error("Background sessions are unavailable in the open build."); }
|
| 162 |
+
export async function logsHandler() { throw new Error("Background sessions are unavailable in the open build."); }
|
| 163 |
+
export async function attachHandler() { throw new Error("Background sessions are unavailable in the open build."); }
|
| 164 |
+
export async function killHandler() { throw new Error("Background sessions are unavailable in the open build."); }
|
| 165 |
+
export async function handleBgFlag() { throw new Error("Background sessions are unavailable in the open build."); }
|
| 166 |
+
`,
|
| 167 |
+
],
|
| 168 |
+
[
|
| 169 |
+
'../cli/handlers/templateJobs.js',
|
| 170 |
+
'export async function templatesMain() { throw new Error("Template jobs are unavailable in the open build."); }',
|
| 171 |
+
],
|
| 172 |
+
[
|
| 173 |
+
'../environment-runner/main.js',
|
| 174 |
+
'export async function environmentRunnerMain() { throw new Error("Environment runner is unavailable in the open build."); }',
|
| 175 |
+
],
|
| 176 |
+
[
|
| 177 |
+
'../self-hosted-runner/main.js',
|
| 178 |
+
'export async function selfHostedRunnerMain() { throw new Error("Self-hosted runner is unavailable in the open build."); }',
|
| 179 |
+
],
|
| 180 |
+
] as const)
|
| 181 |
+
|
| 182 |
+
// bun:bundle feature() replacement is handled by the source
|
| 183 |
+
// pre-processing step above (see preProcessFeatureFlags).
|
| 184 |
+
// The previous onResolve/onLoad shim was ineffective in Bun
|
| 185 |
+
// v1.3.9+ because the bun: namespace is resolved natively
|
| 186 |
+
// before the JS plugin phase runs.
|
| 187 |
+
|
| 188 |
+
build.onResolve(
|
| 189 |
+
{ filter: /^\.\.\/(daemon\/workerRegistry|daemon\/main|cli\/bg|cli\/handlers\/templateJobs|environment-runner\/main|self-hosted-runner\/main)\.js$/ },
|
| 190 |
+
args => {
|
| 191 |
+
if (!internalFeatureStubModules.has(args.path)) return null
|
| 192 |
+
return {
|
| 193 |
+
path: args.path,
|
| 194 |
+
namespace: 'internal-feature-stub',
|
| 195 |
+
}
|
| 196 |
+
},
|
| 197 |
+
)
|
| 198 |
+
build.onLoad(
|
| 199 |
+
{ filter: /.*/, namespace: 'internal-feature-stub' },
|
| 200 |
+
args => ({
|
| 201 |
+
contents:
|
| 202 |
+
internalFeatureStubModules.get(args.path) ??
|
| 203 |
+
'export {}',
|
| 204 |
+
loader: 'js',
|
| 205 |
+
}),
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
// Resolve react/compiler-runtime to the standalone package
|
| 209 |
+
build.onResolve({ filter: /^react\/compiler-runtime$/ }, () => ({
|
| 210 |
+
path: 'react/compiler-runtime',
|
| 211 |
+
namespace: 'react-compiler-shim',
|
| 212 |
+
}))
|
| 213 |
+
build.onLoad(
|
| 214 |
+
{ filter: /.*/, namespace: 'react-compiler-shim' },
|
| 215 |
+
() => ({
|
| 216 |
+
contents: `export function c(size) { return new Array(size).fill(Symbol.for('react.memo_cache_sentinel')); }`,
|
| 217 |
+
loader: 'js',
|
| 218 |
+
}),
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
// NOTE: @opentelemetry/* kept as external deps (too many named exports to stub)
|
| 222 |
+
|
| 223 |
+
// Resolve native addon and missing snapshot imports to stubs
|
| 224 |
+
for (const mod of [
|
| 225 |
+
'audio-capture-napi',
|
| 226 |
+
'audio-capture.node',
|
| 227 |
+
'image-processor-napi',
|
| 228 |
+
'modifiers-napi',
|
| 229 |
+
'url-handler-napi',
|
| 230 |
+
'color-diff-napi',
|
| 231 |
+
'@anthropic-ai/mcpb',
|
| 232 |
+
'@ant/claude-for-chrome-mcp',
|
| 233 |
+
'@anthropic-ai/sandbox-runtime',
|
| 234 |
+
'asciichart',
|
| 235 |
+
'plist',
|
| 236 |
+
'cacache',
|
| 237 |
+
'fuse',
|
| 238 |
+
'code-excerpt',
|
| 239 |
+
'stack-utils',
|
| 240 |
+
]) {
|
| 241 |
+
build.onResolve({ filter: new RegExp(`^${mod}$`) }, () => ({
|
| 242 |
+
path: mod,
|
| 243 |
+
namespace: 'native-stub',
|
| 244 |
+
}))
|
| 245 |
+
}
|
| 246 |
+
build.onLoad(
|
| 247 |
+
{ filter: /.*/, namespace: 'native-stub' },
|
| 248 |
+
() => ({
|
| 249 |
+
// Comprehensive stub that handles any named export via Proxy
|
| 250 |
+
contents: `
|
| 251 |
+
const noop = () => null;
|
| 252 |
+
const noopClass = class {};
|
| 253 |
+
const handler = {
|
| 254 |
+
get(_, prop) {
|
| 255 |
+
if (prop === '__esModule') return true;
|
| 256 |
+
if (prop === 'default') return new Proxy({}, handler);
|
| 257 |
+
if (prop === 'ExportResultCode') return { SUCCESS: 0, FAILED: 1 };
|
| 258 |
+
if (prop === 'resourceFromAttributes') return () => ({});
|
| 259 |
+
if (prop === 'SandboxRuntimeConfigSchema') return { parse: () => ({}) };
|
| 260 |
+
return noop;
|
| 261 |
+
}
|
| 262 |
+
};
|
| 263 |
+
const stub = new Proxy(noop, handler);
|
| 264 |
+
export default stub;
|
| 265 |
+
export const __stub = true;
|
| 266 |
+
// Named exports for all known imports
|
| 267 |
+
export const SandboxViolationStore = null;
|
| 268 |
+
export const SandboxManager = new Proxy({}, { get: () => noop });
|
| 269 |
+
export const SandboxRuntimeConfigSchema = { parse: () => ({}) };
|
| 270 |
+
export const BROWSER_TOOLS = [];
|
| 271 |
+
export const getMcpConfigForManifest = noop;
|
| 272 |
+
export const ColorDiff = null;
|
| 273 |
+
export const ColorFile = null;
|
| 274 |
+
export const getSyntaxTheme = noop;
|
| 275 |
+
export const plot = noop;
|
| 276 |
+
export const createClaudeForChromeMcpServer = noop;
|
| 277 |
+
// OpenTelemetry exports
|
| 278 |
+
export const ExportResultCode = { SUCCESS: 0, FAILED: 1 };
|
| 279 |
+
export const resourceFromAttributes = noop;
|
| 280 |
+
export const Resource = noopClass;
|
| 281 |
+
export const SimpleSpanProcessor = noopClass;
|
| 282 |
+
export const BatchSpanProcessor = noopClass;
|
| 283 |
+
export const NodeTracerProvider = noopClass;
|
| 284 |
+
export const BasicTracerProvider = noopClass;
|
| 285 |
+
export const OTLPTraceExporter = noopClass;
|
| 286 |
+
export const OTLPLogExporter = noopClass;
|
| 287 |
+
export const OTLPMetricExporter = noopClass;
|
| 288 |
+
export const PrometheusExporter = noopClass;
|
| 289 |
+
export const LoggerProvider = noopClass;
|
| 290 |
+
export const SimpleLogRecordProcessor = noopClass;
|
| 291 |
+
export const BatchLogRecordProcessor = noopClass;
|
| 292 |
+
export const MeterProvider = noopClass;
|
| 293 |
+
export const PeriodicExportingMetricReader = noopClass;
|
| 294 |
+
export const trace = { getTracer: () => ({ startSpan: () => ({ end: noop, setAttribute: noop, setStatus: noop, recordException: noop }) }) };
|
| 295 |
+
export const context = { active: noop, with: (_, fn) => fn() };
|
| 296 |
+
export const SpanStatusCode = { OK: 0, ERROR: 1, UNSET: 2 };
|
| 297 |
+
export const ATTR_SERVICE_NAME = 'service.name';
|
| 298 |
+
export const ATTR_SERVICE_VERSION = 'service.version';
|
| 299 |
+
export const SEMRESATTRS_SERVICE_NAME = 'service.name';
|
| 300 |
+
export const SEMRESATTRS_SERVICE_VERSION = 'service.version';
|
| 301 |
+
export const AggregationTemporality = { CUMULATIVE: 0, DELTA: 1 };
|
| 302 |
+
export const DataPointType = { HISTOGRAM: 0, SUM: 1, GAUGE: 2 };
|
| 303 |
+
export const InstrumentType = { COUNTER: 0, HISTOGRAM: 1, UP_DOWN_COUNTER: 2 };
|
| 304 |
+
export const PushMetricExporter = noopClass;
|
| 305 |
+
export const SeverityNumber = {};
|
| 306 |
+
`,
|
| 307 |
+
loader: 'js',
|
| 308 |
+
}),
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
// Resolve .md and .txt file imports to empty string stubs
|
| 312 |
+
build.onResolve({ filter: /\.(md|txt)$/ }, (args) => ({
|
| 313 |
+
path: args.path,
|
| 314 |
+
namespace: 'text-stub',
|
| 315 |
+
}))
|
| 316 |
+
build.onLoad(
|
| 317 |
+
{ filter: /.*/, namespace: 'text-stub' },
|
| 318 |
+
() => ({
|
| 319 |
+
contents: `export default '';`,
|
| 320 |
+
loader: 'js',
|
| 321 |
+
}),
|
| 322 |
+
)
|
| 323 |
+
|
| 324 |
+
// Pre-scan: find all missing modules that need stubbing
|
| 325 |
+
// (Bun's onResolve corrupts module graph even when returning null,
|
| 326 |
+
// so we use exact-match resolvers instead of catch-all patterns)
|
| 327 |
+
const fs = require('fs')
|
| 328 |
+
const pathMod = require('path')
|
| 329 |
+
const srcDir = pathMod.resolve(__dirname, '..', 'src')
|
| 330 |
+
const missingModules = new Set<string>()
|
| 331 |
+
const missingModuleExports = new Map<string, Set<string>>()
|
| 332 |
+
|
| 333 |
+
// Known missing external packages
|
| 334 |
+
for (const pkg of [
|
| 335 |
+
'@ant/computer-use-mcp',
|
| 336 |
+
'@ant/computer-use-mcp/sentinelApps',
|
| 337 |
+
'@ant/computer-use-mcp/types',
|
| 338 |
+
'@ant/computer-use-swift',
|
| 339 |
+
'@ant/computer-use-input',
|
| 340 |
+
]) {
|
| 341 |
+
missingModules.add(pkg)
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
// Scan source to find imports that can't resolve
|
| 345 |
+
function scanForMissingImports() {
|
| 346 |
+
function checkAndRegister(specifier: string, fileDir: string, namedPart: string) {
|
| 347 |
+
const names = namedPart.split(',')
|
| 348 |
+
.map((s: string) => s.trim().replace(/^type\s+/, ''))
|
| 349 |
+
.filter((s: string) => s && !s.startsWith('type '))
|
| 350 |
+
|
| 351 |
+
// Check src/tasks/ non-relative imports
|
| 352 |
+
if (specifier.startsWith('src/tasks/')) {
|
| 353 |
+
const resolved = pathMod.resolve(__dirname, '..', specifier)
|
| 354 |
+
const candidates = [
|
| 355 |
+
resolved,
|
| 356 |
+
`${resolved}.ts`, `${resolved}.tsx`,
|
| 357 |
+
resolved.replace(/\.js$/, '.ts'), resolved.replace(/\.js$/, '.tsx'),
|
| 358 |
+
pathMod.join(resolved, 'index.ts'), pathMod.join(resolved, 'index.tsx'),
|
| 359 |
+
]
|
| 360 |
+
if (!candidates.some((c: string) => fs.existsSync(c))) {
|
| 361 |
+
missingModules.add(specifier)
|
| 362 |
+
}
|
| 363 |
+
}
|
| 364 |
+
// Check relative .js imports
|
| 365 |
+
else if (specifier.endsWith('.js') && (specifier.startsWith('./') || specifier.startsWith('../'))) {
|
| 366 |
+
const resolved = pathMod.resolve(fileDir, specifier)
|
| 367 |
+
const tsVariant = resolved.replace(/\.js$/, '.ts')
|
| 368 |
+
const tsxVariant = resolved.replace(/\.js$/, '.tsx')
|
| 369 |
+
if (!fs.existsSync(resolved) && !fs.existsSync(tsVariant) && !fs.existsSync(tsxVariant)) {
|
| 370 |
+
missingModules.add(specifier)
|
| 371 |
+
}
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
// Track named exports for missing modules
|
| 375 |
+
if (names.length > 0) {
|
| 376 |
+
if (!missingModuleExports.has(specifier)) missingModuleExports.set(specifier, new Set())
|
| 377 |
+
for (const n of names) missingModuleExports.get(specifier)!.add(n)
|
| 378 |
+
}
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
function walk(dir: string) {
|
| 382 |
+
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
|
| 383 |
+
const full = pathMod.join(dir, ent.name)
|
| 384 |
+
if (ent.isDirectory()) { walk(full); continue }
|
| 385 |
+
if (!/\.(ts|tsx)$/.test(ent.name)) continue
|
| 386 |
+
const rawCode: string = fs.readFileSync(full, 'utf-8')
|
| 387 |
+
const fileDir = pathMod.dirname(full)
|
| 388 |
+
|
| 389 |
+
// Strip comments before scanning for imports/requires.
|
| 390 |
+
// The regex scanner matches require()/import() patterns
|
| 391 |
+
// inside JSDoc comments, causing false-positive missing
|
| 392 |
+
// module detection that breaks the build with noop stubs.
|
| 393 |
+
const code = rawCode
|
| 394 |
+
.replace(/\/\*[\s\S]*?\*\//g, '') // block comments
|
| 395 |
+
.replace(/\/\/.*$/gm, '') // line comments
|
| 396 |
+
|
| 397 |
+
// Collect static imports: import { X } from '...'
|
| 398 |
+
for (const m of code.matchAll(/import\s+(?:\{([^}]*)\}|(\w+))?\s*(?:,\s*\{([^}]*)\})?\s*from\s+['"](.*?)['"]/g)) {
|
| 399 |
+
checkAndRegister(m[4], fileDir, m[1] || m[3] || '')
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
// Collect dynamic requires: require('...') β these are used
|
| 403 |
+
// behind feature() gates and become live when flags are enabled.
|
| 404 |
+
for (const m of code.matchAll(/require\(\s*['"](\.\.?\/[^'"]+)['"]\s*\)/g)) {
|
| 405 |
+
checkAndRegister(m[1], fileDir, '')
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
// Collect dynamic imports: import('...')
|
| 409 |
+
for (const m of code.matchAll(/import\(\s*['"](\.\.?\/[^'"]+)['"]\s*\)/g)) {
|
| 410 |
+
checkAndRegister(m[1], fileDir, '')
|
| 411 |
+
}
|
| 412 |
+
}
|
| 413 |
+
}
|
| 414 |
+
walk(srcDir)
|
| 415 |
+
}
|
| 416 |
+
scanForMissingImports()
|
| 417 |
+
|
| 418 |
+
// Register exact-match resolvers for each missing module
|
| 419 |
+
for (const mod of missingModules) {
|
| 420 |
+
const escaped = mod.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
| 421 |
+
build.onResolve({ filter: new RegExp(`^${escaped}$`) }, () => ({
|
| 422 |
+
path: mod,
|
| 423 |
+
namespace: 'missing-module-stub',
|
| 424 |
+
}))
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
build.onLoad(
|
| 428 |
+
{ filter: /.*/, namespace: 'missing-module-stub' },
|
| 429 |
+
(args) => {
|
| 430 |
+
const names = missingModuleExports.get(args.path) ?? new Set()
|
| 431 |
+
const exports = [...names].map(n => `export const ${n} = noop;`).join('\n')
|
| 432 |
+
return {
|
| 433 |
+
contents: `
|
| 434 |
+
const noop = () => null;
|
| 435 |
+
export default noop;
|
| 436 |
+
${exports}
|
| 437 |
+
`,
|
| 438 |
+
loader: 'js',
|
| 439 |
+
}
|
| 440 |
+
},
|
| 441 |
+
)
|
| 442 |
+
},
|
| 443 |
+
},
|
| 444 |
+
],
|
| 445 |
+
external: [
|
| 446 |
+
// OpenTelemetry β too many named exports to stub, kept external
|
| 447 |
+
'@opentelemetry/api',
|
| 448 |
+
'@opentelemetry/api-logs',
|
| 449 |
+
'@opentelemetry/core',
|
| 450 |
+
'@opentelemetry/exporter-trace-otlp-grpc',
|
| 451 |
+
'@opentelemetry/exporter-trace-otlp-http',
|
| 452 |
+
'@opentelemetry/exporter-trace-otlp-proto',
|
| 453 |
+
'@opentelemetry/exporter-logs-otlp-http',
|
| 454 |
+
'@opentelemetry/exporter-logs-otlp-proto',
|
| 455 |
+
'@opentelemetry/exporter-logs-otlp-grpc',
|
| 456 |
+
'@opentelemetry/exporter-metrics-otlp-proto',
|
| 457 |
+
'@opentelemetry/exporter-metrics-otlp-grpc',
|
| 458 |
+
'@opentelemetry/exporter-metrics-otlp-http',
|
| 459 |
+
'@opentelemetry/exporter-prometheus',
|
| 460 |
+
'@opentelemetry/resources',
|
| 461 |
+
'@opentelemetry/sdk-trace-base',
|
| 462 |
+
'@opentelemetry/sdk-trace-node',
|
| 463 |
+
'@opentelemetry/sdk-logs',
|
| 464 |
+
'@opentelemetry/sdk-metrics',
|
| 465 |
+
'@opentelemetry/semantic-conventions',
|
| 466 |
+
// Native image processing
|
| 467 |
+
'sharp',
|
| 468 |
+
// Cloud provider SDKs
|
| 469 |
+
'@aws-sdk/client-bedrock',
|
| 470 |
+
'@aws-sdk/client-bedrock-runtime',
|
| 471 |
+
'@aws-sdk/client-sts',
|
| 472 |
+
'@aws-sdk/credential-providers',
|
| 473 |
+
'@azure/identity',
|
| 474 |
+
'google-auth-library',
|
| 475 |
+
],
|
| 476 |
+
})
|
| 477 |
+
|
| 478 |
+
if (!result.success) {
|
| 479 |
+
console.error('Build failed:')
|
| 480 |
+
for (const log of result.logs) {
|
| 481 |
+
console.error(log)
|
| 482 |
+
}
|
| 483 |
+
process.exitCode = 1
|
| 484 |
+
} else {
|
| 485 |
+
console.log(`β Built openclaude v${version} β dist/cli.mjs`)
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
} finally {
|
| 489 |
+
// Always restore source files, even if Bun.build() throws
|
| 490 |
+
restoreModifiedFiles()
|
| 491 |
+
console.log(` π feature-flags: pre-processed ${numModified} files (restored)`)
|
| 492 |
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as grpc from '@grpc/grpc-js'
|
| 2 |
+
import * as protoLoader from '@grpc/proto-loader'
|
| 3 |
+
import path from 'path'
|
| 4 |
+
import * as readline from 'readline'
|
| 5 |
+
|
| 6 |
+
const PROTO_PATH = path.resolve(import.meta.dirname, '../src/proto/openclaude.proto')
|
| 7 |
+
|
| 8 |
+
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
|
| 9 |
+
keepCase: true,
|
| 10 |
+
longs: String,
|
| 11 |
+
enums: String,
|
| 12 |
+
defaults: true,
|
| 13 |
+
oneofs: true,
|
| 14 |
+
})
|
| 15 |
+
|
| 16 |
+
const protoDescriptor = grpc.loadPackageDefinition(packageDefinition) as any
|
| 17 |
+
const openclaudeProto = protoDescriptor.openclaude.v1
|
| 18 |
+
|
| 19 |
+
const rl = readline.createInterface({
|
| 20 |
+
input: process.stdin,
|
| 21 |
+
output: process.stdout
|
| 22 |
+
})
|
| 23 |
+
|
| 24 |
+
function askQuestion(query: string): Promise<string> {
|
| 25 |
+
return new Promise(resolve => {
|
| 26 |
+
rl.question(query, resolve)
|
| 27 |
+
})
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
async function main() {
|
| 31 |
+
const host = process.env.GRPC_HOST || 'localhost'
|
| 32 |
+
const port = process.env.GRPC_PORT || '50051'
|
| 33 |
+
const client = new openclaudeProto.AgentService(
|
| 34 |
+
`${host}:${port}`,
|
| 35 |
+
grpc.credentials.createInsecure()
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
let call: grpc.ClientDuplexStream<any, any> | null = null
|
| 39 |
+
|
| 40 |
+
const startStream = () => {
|
| 41 |
+
call = client.Chat()
|
| 42 |
+
let textStreamed = false
|
| 43 |
+
|
| 44 |
+
call.on('data', async (serverMessage: any) => {
|
| 45 |
+
if (serverMessage.text_chunk) {
|
| 46 |
+
process.stdout.write(serverMessage.text_chunk.text)
|
| 47 |
+
textStreamed = true
|
| 48 |
+
} else if (serverMessage.tool_start) {
|
| 49 |
+
console.log(`\n\x1b[36m[Tool Call]\x1b[0m \x1b[1m${serverMessage.tool_start.tool_name}\x1b[0m`)
|
| 50 |
+
console.log(`\x1b[90m${serverMessage.tool_start.arguments_json}\x1b[0m\n`)
|
| 51 |
+
} else if (serverMessage.tool_result) {
|
| 52 |
+
console.log(`\n\x1b[32m[Tool Result]\x1b[0m \x1b[1m${serverMessage.tool_result.tool_name}\x1b[0m`)
|
| 53 |
+
const out = serverMessage.tool_result.output
|
| 54 |
+
if (out.length > 500) {
|
| 55 |
+
console.log(`\x1b[90m${out.substring(0, 500)}...\n(Output truncated, total length: ${out.length})\x1b[0m`)
|
| 56 |
+
} else {
|
| 57 |
+
console.log(`\x1b[90m${out}\x1b[0m`)
|
| 58 |
+
}
|
| 59 |
+
} else if (serverMessage.action_required) {
|
| 60 |
+
const action = serverMessage.action_required
|
| 61 |
+
console.log(`\n\x1b[33m[Action Required]\x1b[0m`)
|
| 62 |
+
const reply = await askQuestion(`\x1b[1m${action.question}\x1b[0m (y/n) > `)
|
| 63 |
+
|
| 64 |
+
call?.write({
|
| 65 |
+
input: {
|
| 66 |
+
prompt_id: action.prompt_id,
|
| 67 |
+
reply: reply.trim()
|
| 68 |
+
}
|
| 69 |
+
})
|
| 70 |
+
} else if (serverMessage.done) {
|
| 71 |
+
if (!textStreamed && serverMessage.done.full_text) {
|
| 72 |
+
process.stdout.write(serverMessage.done.full_text)
|
| 73 |
+
}
|
| 74 |
+
textStreamed = false
|
| 75 |
+
console.log('\n\x1b[32m[Generation Complete]\x1b[0m')
|
| 76 |
+
promptUser()
|
| 77 |
+
} else if (serverMessage.error) {
|
| 78 |
+
console.error(`\n\x1b[31m[Server Error]\x1b[0m ${serverMessage.error.message}`)
|
| 79 |
+
promptUser()
|
| 80 |
+
}
|
| 81 |
+
})
|
| 82 |
+
|
| 83 |
+
call.on('end', () => {
|
| 84 |
+
console.log('\n\x1b[90m[Stream closed by server]\x1b[0m')
|
| 85 |
+
// Don't prompt user here, let 'done' or 'error' handlers do it
|
| 86 |
+
})
|
| 87 |
+
|
| 88 |
+
call.on('error', (err: Error) => {
|
| 89 |
+
console.error('\n\x1b[31m[Stream Error]\x1b[0m', err.message)
|
| 90 |
+
promptUser()
|
| 91 |
+
})
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
const promptUser = async () => {
|
| 95 |
+
const message = await askQuestion('\n\x1b[35m> \x1b[0m')
|
| 96 |
+
|
| 97 |
+
if (message.trim().toLowerCase() === '/exit' || message.trim().toLowerCase() === '/quit') {
|
| 98 |
+
console.log('Bye!')
|
| 99 |
+
rl.close()
|
| 100 |
+
process.exit(0)
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
if (!call || call.destroyed) {
|
| 104 |
+
startStream()
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
call!.write({
|
| 108 |
+
request: {
|
| 109 |
+
session_id: 'cli-session-1',
|
| 110 |
+
message: message,
|
| 111 |
+
working_directory: process.cwd()
|
| 112 |
+
}
|
| 113 |
+
})
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
console.log('\x1b[32mOpenClaude gRPC CLI\x1b[0m')
|
| 117 |
+
console.log('\x1b[90mType /exit to quit.\x1b[0m')
|
| 118 |
+
promptUser()
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
main()
|
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { afterAll, beforeEach, describe, expect, test } from 'bun:test'
|
| 2 |
+
import { mkdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from 'node:fs'
|
| 3 |
+
import { join } from 'node:path'
|
| 4 |
+
import { tmpdir } from 'node:os'
|
| 5 |
+
|
| 6 |
+
// ---------------------------------------------------------------------------
|
| 7 |
+
// Setup: extract the growthbook stub from no-telemetry-plugin.ts, write it to
|
| 8 |
+
// a temp .mjs file, and dynamically import it so we can test the real code
|
| 9 |
+
// that gets bundled.
|
| 10 |
+
// ---------------------------------------------------------------------------
|
| 11 |
+
|
| 12 |
+
const pluginSource = readFileSync(join(__dirname, 'no-telemetry-plugin.ts'), 'utf-8')
|
| 13 |
+
const stubMatch = pluginSource.match(/'services\/analytics\/growthbook': `([\s\S]*?)`/)
|
| 14 |
+
if (!stubMatch) throw new Error('Could not extract growthbook stub from no-telemetry-plugin.ts')
|
| 15 |
+
|
| 16 |
+
const testDir = join(tmpdir(), `growthbook-stub-test-${process.pid}`)
|
| 17 |
+
const stubFile = join(testDir, 'growthbook-stub.mjs')
|
| 18 |
+
const flagsFile = join(testDir, 'test-flags.json')
|
| 19 |
+
|
| 20 |
+
mkdirSync(testDir, { recursive: true })
|
| 21 |
+
writeFileSync(stubFile, stubMatch[1])
|
| 22 |
+
|
| 23 |
+
// Point the stub at our test flags file (checked by _loadFlags on first access)
|
| 24 |
+
process.env.CLAUDE_FEATURE_FLAGS_FILE = flagsFile
|
| 25 |
+
|
| 26 |
+
const stub = await import(stubFile)
|
| 27 |
+
|
| 28 |
+
// ---------------------------------------------------------------------------
|
| 29 |
+
// Tests
|
| 30 |
+
// ---------------------------------------------------------------------------
|
| 31 |
+
|
| 32 |
+
describe('growthbook stub β local feature flag overrides', () => {
|
| 33 |
+
beforeEach(() => {
|
| 34 |
+
stub.resetGrowthBook()
|
| 35 |
+
try { unlinkSync(flagsFile) } catch { /* may not exist */ }
|
| 36 |
+
})
|
| 37 |
+
|
| 38 |
+
afterAll(() => {
|
| 39 |
+
rmSync(testDir, { recursive: true, force: true })
|
| 40 |
+
delete process.env.CLAUDE_FEATURE_FLAGS_FILE
|
| 41 |
+
})
|
| 42 |
+
|
| 43 |
+
// ββ File absent ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 44 |
+
|
| 45 |
+
test('returns defaultValue when flags file is absent', () => {
|
| 46 |
+
expect(stub.getFeatureValue_CACHED_MAY_BE_STALE('tengu_foo', 42)).toBe(42)
|
| 47 |
+
})
|
| 48 |
+
|
| 49 |
+
test('getAllGrowthBookFeatures returns {} when file is absent', () => {
|
| 50 |
+
expect(stub.getAllGrowthBookFeatures()).toEqual({})
|
| 51 |
+
})
|
| 52 |
+
|
| 53 |
+
// ββ Open-build defaults (_openBuildDefaults) ββββββββββββββββββββ
|
| 54 |
+
|
| 55 |
+
test('returns open-build default when flags file is absent', () => {
|
| 56 |
+
// tengu_passport_quail is in _openBuildDefaults as true; without a
|
| 57 |
+
// flags file the stub should return the open-build override, not
|
| 58 |
+
// the call-site defaultValue.
|
| 59 |
+
expect(stub.getFeatureValue_CACHED_MAY_BE_STALE('tengu_passport_quail', false)).toBe(true)
|
| 60 |
+
expect(stub.getFeatureValue_CACHED_MAY_BE_STALE('tengu_coral_fern', false)).toBe(true)
|
| 61 |
+
})
|
| 62 |
+
|
| 63 |
+
test('flags file overrides open-build defaults', () => {
|
| 64 |
+
// User-provided feature-flags.json takes priority over _openBuildDefaults.
|
| 65 |
+
writeFileSync(flagsFile, JSON.stringify({ tengu_passport_quail: false }))
|
| 66 |
+
|
| 67 |
+
expect(stub.getFeatureValue_CACHED_MAY_BE_STALE('tengu_passport_quail', true)).toBe(false)
|
| 68 |
+
})
|
| 69 |
+
|
| 70 |
+
// ββ Valid JSON object ββββββββββββββββββββββββββββββββββββββββββββ
|
| 71 |
+
|
| 72 |
+
test('loads and returns values from a valid JSON file', () => {
|
| 73 |
+
writeFileSync(flagsFile, JSON.stringify({ tengu_foo: true, tengu_bar: 'hello' }))
|
| 74 |
+
|
| 75 |
+
expect(stub.getFeatureValue_CACHED_MAY_BE_STALE('tengu_foo', false)).toBe(true)
|
| 76 |
+
expect(stub.getFeatureValue_CACHED_MAY_BE_STALE('tengu_bar', 'default')).toBe('hello')
|
| 77 |
+
})
|
| 78 |
+
|
| 79 |
+
test('returns defaultValue for keys not present in the file', () => {
|
| 80 |
+
writeFileSync(flagsFile, JSON.stringify({ tengu_foo: true }))
|
| 81 |
+
|
| 82 |
+
expect(stub.getFeatureValue_CACHED_MAY_BE_STALE('tengu_missing', 99)).toBe(99)
|
| 83 |
+
})
|
| 84 |
+
|
| 85 |
+
test('getAllGrowthBookFeatures returns the full flags object', () => {
|
| 86 |
+
const flags = { tengu_a: true, tengu_b: false, tengu_c: 42 }
|
| 87 |
+
writeFileSync(flagsFile, JSON.stringify(flags))
|
| 88 |
+
|
| 89 |
+
expect(stub.getAllGrowthBookFeatures()).toEqual(flags)
|
| 90 |
+
})
|
| 91 |
+
|
| 92 |
+
// ββ Malformed / non-object JSON ββββββββββββββββββββββββββββββββββ
|
| 93 |
+
|
| 94 |
+
test('falls back to defaults on malformed JSON', () => {
|
| 95 |
+
writeFileSync(flagsFile, '{not valid json!!!')
|
| 96 |
+
|
| 97 |
+
expect(stub.getFeatureValue_CACHED_MAY_BE_STALE('tengu_foo', 'fallback')).toBe('fallback')
|
| 98 |
+
})
|
| 99 |
+
|
| 100 |
+
test('falls back to defaults when JSON is a primitive (true)', () => {
|
| 101 |
+
writeFileSync(flagsFile, 'true')
|
| 102 |
+
|
| 103 |
+
expect(stub.getFeatureValue_CACHED_MAY_BE_STALE('tengu_foo', 'fallback')).toBe('fallback')
|
| 104 |
+
})
|
| 105 |
+
|
| 106 |
+
test('falls back to defaults when JSON is an array', () => {
|
| 107 |
+
writeFileSync(flagsFile, '["a", "b"]')
|
| 108 |
+
|
| 109 |
+
expect(stub.getFeatureValue_CACHED_MAY_BE_STALE('tengu_foo', 'fallback')).toBe('fallback')
|
| 110 |
+
})
|
| 111 |
+
|
| 112 |
+
// ββ Cache invalidation βββββββββββββββββββββββββββββββββββββββββββ
|
| 113 |
+
|
| 114 |
+
test('resetGrowthBook clears cache so the file is re-read', () => {
|
| 115 |
+
writeFileSync(flagsFile, JSON.stringify({ tengu_foo: 'first' }))
|
| 116 |
+
expect(stub.getFeatureValue_CACHED_MAY_BE_STALE('tengu_foo', 'x')).toBe('first')
|
| 117 |
+
|
| 118 |
+
// Update the file β cached value is still 'first'
|
| 119 |
+
writeFileSync(flagsFile, JSON.stringify({ tengu_foo: 'second' }))
|
| 120 |
+
expect(stub.getFeatureValue_CACHED_MAY_BE_STALE('tengu_foo', 'x')).toBe('first')
|
| 121 |
+
|
| 122 |
+
// After reset, the new value is picked up
|
| 123 |
+
stub.resetGrowthBook()
|
| 124 |
+
expect(stub.getFeatureValue_CACHED_MAY_BE_STALE('tengu_foo', 'x')).toBe('second')
|
| 125 |
+
})
|
| 126 |
+
|
| 127 |
+
test('refreshGrowthBookFeatures clears cache', async () => {
|
| 128 |
+
writeFileSync(flagsFile, JSON.stringify({ tengu_foo: 'v1' }))
|
| 129 |
+
expect(stub.getFeatureValue_CACHED_MAY_BE_STALE('tengu_foo', 'x')).toBe('v1')
|
| 130 |
+
|
| 131 |
+
writeFileSync(flagsFile, JSON.stringify({ tengu_foo: 'v2' }))
|
| 132 |
+
await stub.refreshGrowthBookFeatures()
|
| 133 |
+
expect(stub.getFeatureValue_CACHED_MAY_BE_STALE('tengu_foo', 'x')).toBe('v2')
|
| 134 |
+
})
|
| 135 |
+
|
| 136 |
+
// ββ Multiple getter variants βββββββββββββββββββββββββββββββββββββ
|
| 137 |
+
|
| 138 |
+
test('all getter functions read from local flags', async () => {
|
| 139 |
+
writeFileSync(flagsFile, JSON.stringify({ tengu_gate: true, tengu_config: { a: 1 } }))
|
| 140 |
+
|
| 141 |
+
expect(await stub.getFeatureValue_DEPRECATED('tengu_gate', false)).toBe(true)
|
| 142 |
+
stub.resetGrowthBook()
|
| 143 |
+
expect(stub.getFeatureValue_CACHED_WITH_REFRESH('tengu_gate', false)).toBe(true)
|
| 144 |
+
stub.resetGrowthBook()
|
| 145 |
+
expect(stub.checkStatsigFeatureGate_CACHED_MAY_BE_STALE('tengu_gate')).toBe(true)
|
| 146 |
+
stub.resetGrowthBook()
|
| 147 |
+
expect(await stub.checkGate_CACHED_OR_BLOCKING('tengu_gate')).toBe(true)
|
| 148 |
+
stub.resetGrowthBook()
|
| 149 |
+
expect(await stub.getDynamicConfig_BLOCKS_ON_INIT('tengu_config', {})).toEqual({ a: 1 })
|
| 150 |
+
stub.resetGrowthBook()
|
| 151 |
+
expect(stub.getDynamicConfig_CACHED_MAY_BE_STALE('tengu_config', {})).toEqual({ a: 1 })
|
| 152 |
+
})
|
| 153 |
+
|
| 154 |
+
// ββ Security gate ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 155 |
+
|
| 156 |
+
test('checkSecurityRestrictionGate always returns false regardless of flags', async () => {
|
| 157 |
+
writeFileSync(flagsFile, JSON.stringify({
|
| 158 |
+
tengu_disable_bypass_permissions_mode: true,
|
| 159 |
+
}))
|
| 160 |
+
|
| 161 |
+
expect(await stub.checkSecurityRestrictionGate()).toBe(false)
|
| 162 |
+
})
|
| 163 |
+
})
|
|
@@ -0,0 +1,459 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* No-Telemetry Build Plugin for OpenClaude
|
| 3 |
+
*
|
| 4 |
+
* Replaces all analytics, telemetry, and phone-home modules with no-op stubs
|
| 5 |
+
* at compile time. Zero runtime cost, zero network calls to Anthropic.
|
| 6 |
+
*
|
| 7 |
+
* This file is NOT tracked upstream β merge conflicts are impossible.
|
| 8 |
+
* Only build.ts needs a one-line import + one-line array entry.
|
| 9 |
+
*
|
| 10 |
+
* Kills:
|
| 11 |
+
* - GrowthBook remote feature flags (api.anthropic.com)
|
| 12 |
+
* - Datadog event intake
|
| 13 |
+
* - 1P event logging (api.anthropic.com/api/event_logging/batch)
|
| 14 |
+
* - BigQuery metrics exporter (api.anthropic.com/api/claude_code/metrics)
|
| 15 |
+
* - Perfetto / OpenTelemetry session tracing
|
| 16 |
+
* - Auto-updater (storage.googleapis.com, npm registry)
|
| 17 |
+
* - Plugin fetch telemetry
|
| 18 |
+
* - Transcript / feedback sharing
|
| 19 |
+
*/
|
| 20 |
+
|
| 21 |
+
import type { BunPlugin } from 'bun'
|
| 22 |
+
|
| 23 |
+
// Module path (relative to src/, without extension) β stub source
|
| 24 |
+
const stubs: Record<string, string> = {
|
| 25 |
+
|
| 26 |
+
// βββ Analytics core βββββββββββββββββββββββββββββββββββββββββββββ
|
| 27 |
+
|
| 28 |
+
'services/analytics/index': `
|
| 29 |
+
export function stripProtoFields(metadata) { return metadata; }
|
| 30 |
+
export function attachAnalyticsSink() {}
|
| 31 |
+
export function logEvent() {}
|
| 32 |
+
export async function logEventAsync() {}
|
| 33 |
+
export function _resetForTesting() {}
|
| 34 |
+
`,
|
| 35 |
+
|
| 36 |
+
'services/analytics/growthbook': `
|
| 37 |
+
import _fs from 'node:fs';
|
| 38 |
+
import _path from 'node:path';
|
| 39 |
+
import _os from 'node:os';
|
| 40 |
+
|
| 41 |
+
let _flags = undefined;
|
| 42 |
+
|
| 43 |
+
// ββ Open-build GrowthBook overrides βββββββββββββββββββββββββββββββββββ
|
| 44 |
+
// Override upstream defaultValue for runtime gates tied to build-time
|
| 45 |
+
// features. Only keys that DIFFER from upstream belong here β the
|
| 46 |
+
// catalog below is pure documentation and does NOT affect resolution.
|
| 47 |
+
//
|
| 48 |
+
// Priority: ~/.claude/feature-flags.json > _openBuildDefaults > defaultValue
|
| 49 |
+
//
|
| 50 |
+
// To override at runtime, create ~/.claude/feature-flags.json:
|
| 51 |
+
// { "tengu_some_flag": true }
|
| 52 |
+
const _openBuildDefaults = {
|
| 53 |
+
'tengu_sedge_lantern': true, // AWAY_SUMMARY β "while you were away" recap (upstream: false)
|
| 54 |
+
'tengu_hive_evidence': true, // VERIFICATION_AGENT β read-only test/verification agent (upstream: false)
|
| 55 |
+
'tengu_passport_quail': true, // EXTRACT_MEMORIES β enable memory extraction (upstream: false)
|
| 56 |
+
'tengu_coral_fern': true, // EXTRACT_MEMORIES β enable memory search in past context (upstream: false)
|
| 57 |
+
};
|
| 58 |
+
|
| 59 |
+
/* ββ Known runtime feature keys (reference only) βββββββββββββββββββββββ
|
| 60 |
+
* This catalog does NOT participate in flag resolution. It documents
|
| 61 |
+
* the known GrowthBook keys and their upstream default values, scraped
|
| 62 |
+
* from src/ call sites. It is NOT exhaustive β new keys may be added
|
| 63 |
+
* upstream between catalog updates.
|
| 64 |
+
*
|
| 65 |
+
* Some keys have different defaults at different call sites β this is
|
| 66 |
+
* intentional upstream (the server unifies the value at runtime).
|
| 67 |
+
*
|
| 68 |
+
* To activate any of these, add them to ~/.claude/feature-flags.json
|
| 69 |
+
* or to _openBuildDefaults above.
|
| 70 |
+
*
|
| 71 |
+
* ββ Reasoning & thinking ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 72 |
+
* tengu_turtle_carbon = true ULTRATHINK deep thinking runtime gate
|
| 73 |
+
* tengu_thinkback = gate /thinkback replay command
|
| 74 |
+
*
|
| 75 |
+
* ββ Agents & orchestration ββββββββββββββββββββββββββββββββββββββββββββ
|
| 76 |
+
* tengu_amber_flint = true Agent swarms coordination
|
| 77 |
+
* tengu_amber_stoat = true Built-in agent availability (Explore, Plan, etc.)
|
| 78 |
+
* tengu_agent_list_attach = true Attach file context to agent list
|
| 79 |
+
* tengu_auto_background_agents = false Auto-spawn background agents
|
| 80 |
+
* tengu_slim_subagent_claudemd = true Lighter ClaudeMD for subagents
|
| 81 |
+
* tengu_hive_evidence = false Verification agent / evidence tracking (4 call sites)
|
| 82 |
+
* tengu_ultraplan_model = model cfg ULTRAPLAN model selection (dynamic config)
|
| 83 |
+
*
|
| 84 |
+
* ββ Memory & context ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 85 |
+
* tengu_passport_quail = false EXTRACT_MEMORIES main gate (isExtractModeActive)
|
| 86 |
+
* tengu_coral_fern = false EXTRACT_MEMORIES search in past context
|
| 87 |
+
* tengu_slate_thimble = false Memory dir paths (non-interactive sessions)
|
| 88 |
+
* tengu_herring_clock = true/false Team memory paths (varies by call site)
|
| 89 |
+
* tengu_bramble_lintel = null Extract memories throttle (null β every turn)
|
| 90 |
+
* tengu_sedge_lantern = false AWAY_SUMMARY "while you were away" recap
|
| 91 |
+
* tengu_session_memory = false Session memory service
|
| 92 |
+
* tengu_sm_config = {} Session memory config (dynamic)
|
| 93 |
+
* tengu_sm_compact_config = {} Session memory compaction config (dynamic)
|
| 94 |
+
* tengu_cobalt_raccoon = false Reactive compaction (suppress auto-compact)
|
| 95 |
+
* tengu_pebble_leaf_prune = false Session storage pruning
|
| 96 |
+
*
|
| 97 |
+
* ββ Kairos & cron βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 98 |
+
* tengu_kairos_brief = false Brief layout mode (KAIROS)
|
| 99 |
+
* tengu_kairos_brief_config = {} Brief config (dynamic)
|
| 100 |
+
* tengu_kairos_cron = true Cron scheduler enable
|
| 101 |
+
* tengu_kairos_cron_durable = true Durable (disk-persistent) cron tasks
|
| 102 |
+
* tengu_kairos_cron_config = {} Cron jitter config (dynamic)
|
| 103 |
+
*
|
| 104 |
+
* ββ Bridge & remote (require Anthropic infra) βββββββββββββββββββββββββ
|
| 105 |
+
* tengu_ccr_bridge = false CCR bridge connection
|
| 106 |
+
* tengu_ccr_bridge_multi_session = gate Multi-session spawn mode
|
| 107 |
+
* tengu_ccr_mirror = false CCR session mirroring
|
| 108 |
+
* tengu_ccr_bundle_seed_enabled = gate Git bundle seeding for CCR
|
| 109 |
+
* tengu_ccr_bundle_max_bytes = null Bundle size limit (null β default)
|
| 110 |
+
* tengu_bridge_repl_v2 = false Environment-less REPL bridge v2
|
| 111 |
+
* tengu_bridge_repl_v2_cse_shim_enabled = true CSEβSession tag retag shim
|
| 112 |
+
* tengu_bridge_min_version = {min:'0'} Min CLI version for bridge (dynamic)
|
| 113 |
+
* tengu_bridge_initial_history_cap = 200 Initial history cap for bridge
|
| 114 |
+
* tengu_bridge_system_init = false Bridge system initialization
|
| 115 |
+
* tengu_cobalt_harbor = false Auto-connect CCR at startup
|
| 116 |
+
* tengu_cobalt_lantern = false Remote setup preconditions
|
| 117 |
+
* tengu_remote_backend = false Remote TUI backend
|
| 118 |
+
* tengu_surreal_dali = false Remote agent tasks / triggers
|
| 119 |
+
*
|
| 120 |
+
* ββ Prompt & API ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 121 |
+
* tengu_attribution_header = true Attribution header in API requests
|
| 122 |
+
* tengu_basalt_3kr = true MCP instructions delta
|
| 123 |
+
* tengu_slate_prism = true/false Message formatting (varies by call site)
|
| 124 |
+
* tengu_amber_prism = false Message content formatting
|
| 125 |
+
* tengu_amber_json_tools = false JSON format for tool schemas
|
| 126 |
+
* tengu_fgts = false API feature gates
|
| 127 |
+
* tengu_otk_slot_v1 = false One-time key slots for API auth
|
| 128 |
+
* tengu_cicada_nap_ms = 0 Background GrowthBook refresh throttle (ms)
|
| 129 |
+
* tengu_miraculo_the_bard = false Service initialization gate
|
| 130 |
+
* tengu_immediate_model_command = false Immediate /model command execution
|
| 131 |
+
* tengu_chomp_inflection = false Prompt suggestions after responses
|
| 132 |
+
* tengu_tool_pear = gate API betas for tool use
|
| 133 |
+
* tengu-off-switch = {act:false} Service kill switch (dynamic; uses dash)
|
| 134 |
+
*
|
| 135 |
+
* ββ Permissions & security ββββββββββββββββββββββββββββββββββββββββββββ
|
| 136 |
+
* tengu_birch_trellis = true Bash auto-mode permissions config
|
| 137 |
+
* tengu_auto_mode_config = {} Auto-mode configuration (dynamic, many call sites)
|
| 138 |
+
* tengu_iron_gate_closed = true Permission iron gate (with refresh)
|
| 139 |
+
* tengu_destructive_command_warning = false Warning for destructive bash commands
|
| 140 |
+
* tengu_disable_bypass_permissions_mode = security Security killswitch (always false in open build)
|
| 141 |
+
*
|
| 142 |
+
* ββ UI & UX βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 143 |
+
* tengu_willow_mode = 'off' REPL rendering mode
|
| 144 |
+
* tengu_terminal_panel = false Terminal panel keybinding
|
| 145 |
+
* tengu_terminal_sidebar = false Terminal sidebar in REPL/config
|
| 146 |
+
* tengu_marble_sandcastle = false Fast mode gate
|
| 147 |
+
* tengu_jade_anvil_4 = false Rate limit options UI ordering
|
| 148 |
+
* tengu_collage_kaleidoscope = true Native clipboard image paste (macOS)
|
| 149 |
+
* tengu_lapis_finch = false Plugin/hint recommendation
|
| 150 |
+
* tengu_lodestone_enabled = false Deep links claude-cli:// protocol
|
| 151 |
+
* tengu_copper_panda = false Skill improvement suggestions
|
| 152 |
+
* tengu_desktop_upsell = {} Desktop app upsell config (dynamic)
|
| 153 |
+
* tengu-top-of-feed-tip = {} Emergency tip of feed (dynamic; uses dash)
|
| 154 |
+
*
|
| 155 |
+
* ββ File operations βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 156 |
+
* tengu_quartz_lantern = false File read/write dedup optimization
|
| 157 |
+
* tengu_moth_copse = false Attachments handling (variant A)
|
| 158 |
+
* tengu_marble_fox = false Attachments handling (variant B)
|
| 159 |
+
* tengu_scratch = gate Scratchpad filesystem access / coordinator
|
| 160 |
+
*
|
| 161 |
+
* ββ MCP & plugins βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 162 |
+
* tengu_harbor = false MCP channel allowlist verification
|
| 163 |
+
* tengu_harbor_permissions = false MCP channel permissions enforcement
|
| 164 |
+
* tengu_copper_bridge = false Chrome MCP bridge
|
| 165 |
+
* tengu_chrome_auto_enable = false Auto-enable Chrome MCP on startup
|
| 166 |
+
* tengu_glacier_2xr = false Enhanced tool search / ToolSearchTool
|
| 167 |
+
* tengu_malort_pedway = {} Computer-use (Chicago) config (dynamic)
|
| 168 |
+
*
|
| 169 |
+
* ββ VSCode / IDE ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 170 |
+
* tengu_quiet_fern = false VSCode browser support
|
| 171 |
+
* tengu_vscode_cc_auth = false VSCode in-band OAuth via claude_authenticate
|
| 172 |
+
* tengu_vscode_review_upsell = gate VSCode review upsell
|
| 173 |
+
* tengu_vscode_onboarding = gate VSCode onboarding experience
|
| 174 |
+
*
|
| 175 |
+
* ββ Voice βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 176 |
+
* tengu_amber_quartz_disabled = false VOICE_MODE kill-switch (false = voice allowed)
|
| 177 |
+
*
|
| 178 |
+
* ββ Auto-updater (stubbed in open build) ββββββββββββββββββββββββββββββ
|
| 179 |
+
* tengu_version_config = {min:'0'} Min version enforcement (dynamic)
|
| 180 |
+
* tengu_max_version_config = {} Max version / deprecation config (dynamic)
|
| 181 |
+
*
|
| 182 |
+
* ββ Telemetry & tracing βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 183 |
+
* tengu_trace_lantern = false Beta session tracing
|
| 184 |
+
* tengu_chair_sermon = gate Analytics / message formatting gate
|
| 185 |
+
* tengu_strap_foyer = false Settings sync to cloud
|
| 186 |
+
*/
|
| 187 |
+
|
| 188 |
+
function _loadFlags() {
|
| 189 |
+
if (_flags !== undefined) return;
|
| 190 |
+
try {
|
| 191 |
+
const flagsPath = process.env.CLAUDE_FEATURE_FLAGS_FILE
|
| 192 |
+
|| _path.join(_os.homedir(), '.claude', 'feature-flags.json');
|
| 193 |
+
const parsed = JSON.parse(_fs.readFileSync(flagsPath, 'utf-8'));
|
| 194 |
+
_flags = (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) ? parsed : null;
|
| 195 |
+
} catch {
|
| 196 |
+
_flags = null;
|
| 197 |
+
}
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
function _getFlagValue(key, defaultValue) {
|
| 201 |
+
_loadFlags();
|
| 202 |
+
if (_flags != null && Object.hasOwn(_flags, key)) return _flags[key];
|
| 203 |
+
if (Object.hasOwn(_openBuildDefaults, key)) return _openBuildDefaults[key];
|
| 204 |
+
return defaultValue;
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
const noop = () => {};
|
| 208 |
+
export function onGrowthBookRefresh() { return noop; }
|
| 209 |
+
export function hasGrowthBookEnvOverride() { return false; }
|
| 210 |
+
export function getAllGrowthBookFeatures() { _loadFlags(); return _flags || {}; }
|
| 211 |
+
export function getGrowthBookConfigOverrides() { return {}; }
|
| 212 |
+
export function setGrowthBookConfigOverride() {}
|
| 213 |
+
export function clearGrowthBookConfigOverrides() {}
|
| 214 |
+
export function getApiBaseUrlHost() { return undefined; }
|
| 215 |
+
export const initializeGrowthBook = async () => null;
|
| 216 |
+
export async function getFeatureValue_DEPRECATED(feature, defaultValue) { return _getFlagValue(feature, defaultValue); }
|
| 217 |
+
export function getFeatureValue_CACHED_MAY_BE_STALE(feature, defaultValue) { return _getFlagValue(feature, defaultValue); }
|
| 218 |
+
export function getFeatureValue_CACHED_WITH_REFRESH(feature, defaultValue) { return _getFlagValue(feature, defaultValue); }
|
| 219 |
+
export function checkStatsigFeatureGate_CACHED_MAY_BE_STALE(gate) { return Boolean(_getFlagValue(gate, false)); }
|
| 220 |
+
// Security killswitch β always false in the open build. Anthropic uses this
|
| 221 |
+
// gate to remotely disable bypassPermissions mode; exposing it via local flags
|
| 222 |
+
// would let users accidentally lock themselves out of --dangerously-skip-permissions.
|
| 223 |
+
export async function checkSecurityRestrictionGate(gate) { return false; }
|
| 224 |
+
export async function checkGate_CACHED_OR_BLOCKING(gate) { return Boolean(_getFlagValue(gate, false)); }
|
| 225 |
+
export function refreshGrowthBookAfterAuthChange() {}
|
| 226 |
+
export function resetGrowthBook() { _flags = undefined; }
|
| 227 |
+
export async function refreshGrowthBookFeatures() { _flags = undefined; }
|
| 228 |
+
export function setupPeriodicGrowthBookRefresh() {}
|
| 229 |
+
export function stopPeriodicGrowthBookRefresh() {}
|
| 230 |
+
export async function getDynamicConfig_BLOCKS_ON_INIT(configName, defaultValue) { return _getFlagValue(configName, defaultValue); }
|
| 231 |
+
export function getDynamicConfig_CACHED_MAY_BE_STALE(configName, defaultValue) { return _getFlagValue(configName, defaultValue); }
|
| 232 |
+
`,
|
| 233 |
+
|
| 234 |
+
'services/analytics/sink': `
|
| 235 |
+
export function initializeAnalyticsGates() {}
|
| 236 |
+
export function initializeAnalyticsSink() {}
|
| 237 |
+
`,
|
| 238 |
+
|
| 239 |
+
'services/analytics/config': `
|
| 240 |
+
export function isAnalyticsDisabled() { return true; }
|
| 241 |
+
export function isFeedbackSurveyDisabled() { return true; }
|
| 242 |
+
`,
|
| 243 |
+
|
| 244 |
+
'services/analytics/datadog': `
|
| 245 |
+
export const initializeDatadog = async () => false;
|
| 246 |
+
export async function shutdownDatadog() {}
|
| 247 |
+
export async function trackDatadogEvent() {}
|
| 248 |
+
`,
|
| 249 |
+
|
| 250 |
+
'services/analytics/firstPartyEventLogger': `
|
| 251 |
+
export function getEventSamplingConfig() { return {}; }
|
| 252 |
+
export function shouldSampleEvent() { return null; }
|
| 253 |
+
export async function shutdown1PEventLogging() {}
|
| 254 |
+
export function is1PEventLoggingEnabled() { return false; }
|
| 255 |
+
export function logEventTo1P() {}
|
| 256 |
+
export function logGrowthBookExperimentTo1P() {}
|
| 257 |
+
export function initialize1PEventLogging() {}
|
| 258 |
+
export async function reinitialize1PEventLoggingIfConfigChanged() {}
|
| 259 |
+
`,
|
| 260 |
+
|
| 261 |
+
'services/analytics/firstPartyEventLoggingExporter': `
|
| 262 |
+
export class FirstPartyEventLoggingExporter {
|
| 263 |
+
constructor() {}
|
| 264 |
+
async export(logs, resultCallback) { resultCallback({ code: 0 }); }
|
| 265 |
+
async getQueuedEventCount() { return 0; }
|
| 266 |
+
async shutdown() {}
|
| 267 |
+
async forceFlush() {}
|
| 268 |
+
}
|
| 269 |
+
`,
|
| 270 |
+
|
| 271 |
+
'services/analytics/metadata': `
|
| 272 |
+
export function sanitizeToolNameForAnalytics(toolName) { return toolName; }
|
| 273 |
+
export function isToolDetailsLoggingEnabled() { return false; }
|
| 274 |
+
export function isAnalyticsToolDetailsLoggingEnabled() { return false; }
|
| 275 |
+
export function mcpToolDetailsForAnalytics() { return {}; }
|
| 276 |
+
export function extractMcpToolDetails() { return undefined; }
|
| 277 |
+
export function extractSkillName() { return undefined; }
|
| 278 |
+
export function extractToolInputForTelemetry() { return undefined; }
|
| 279 |
+
export function getFileExtensionForAnalytics() { return undefined; }
|
| 280 |
+
export function getFileExtensionsFromBashCommand() { return undefined; }
|
| 281 |
+
export async function getEventMetadata() { return {}; }
|
| 282 |
+
export function to1PEventFormat() { return {}; }
|
| 283 |
+
`,
|
| 284 |
+
|
| 285 |
+
// βββ Telemetry subsystems βββββββββββββββββββββββββββββββββββββββ
|
| 286 |
+
|
| 287 |
+
'utils/telemetry/bigqueryExporter': `
|
| 288 |
+
export class BigQueryMetricsExporter {
|
| 289 |
+
constructor() {}
|
| 290 |
+
async export(metrics, resultCallback) { resultCallback({ code: 0 }); }
|
| 291 |
+
async shutdown() {}
|
| 292 |
+
async forceFlush() {}
|
| 293 |
+
selectAggregationTemporality() { return 0; }
|
| 294 |
+
}
|
| 295 |
+
`,
|
| 296 |
+
|
| 297 |
+
'utils/telemetry/perfettoTracing': `
|
| 298 |
+
export function initializePerfettoTracing() {}
|
| 299 |
+
export function isPerfettoTracingEnabled() { return false; }
|
| 300 |
+
export function registerAgent() {}
|
| 301 |
+
export function unregisterAgent() {}
|
| 302 |
+
export function startLLMRequestPerfettoSpan() { return ''; }
|
| 303 |
+
export function endLLMRequestPerfettoSpan() {}
|
| 304 |
+
export function startToolPerfettoSpan() { return ''; }
|
| 305 |
+
export function endToolPerfettoSpan() {}
|
| 306 |
+
export function startUserInputPerfettoSpan() { return ''; }
|
| 307 |
+
export function endUserInputPerfettoSpan() {}
|
| 308 |
+
export function emitPerfettoInstant() {}
|
| 309 |
+
export function emitPerfettoCounter() {}
|
| 310 |
+
export function startInteractionPerfettoSpan() { return ''; }
|
| 311 |
+
export function endInteractionPerfettoSpan() {}
|
| 312 |
+
export function getPerfettoEvents() { return []; }
|
| 313 |
+
export function resetPerfettoTracer() {}
|
| 314 |
+
export async function triggerPeriodicWriteForTesting() {}
|
| 315 |
+
export function evictStaleSpansForTesting() {}
|
| 316 |
+
export const MAX_EVENTS_FOR_TESTING = 0;
|
| 317 |
+
export function evictOldestEventsForTesting() {}
|
| 318 |
+
`,
|
| 319 |
+
|
| 320 |
+
'utils/telemetry/sessionTracing': `
|
| 321 |
+
const noopSpan = {
|
| 322 |
+
end() {}, setAttribute() {}, setStatus() {},
|
| 323 |
+
recordException() {}, addEvent() {}, isRecording() { return false; },
|
| 324 |
+
};
|
| 325 |
+
export function isBetaTracingEnabled() { return false; }
|
| 326 |
+
export function isEnhancedTelemetryEnabled() { return false; }
|
| 327 |
+
export function startInteractionSpan() { return noopSpan; }
|
| 328 |
+
export function endInteractionSpan() {}
|
| 329 |
+
export function startLLMRequestSpan() { return noopSpan; }
|
| 330 |
+
export function endLLMRequestSpan() {}
|
| 331 |
+
export function startToolSpan() { return noopSpan; }
|
| 332 |
+
export function startToolBlockedOnUserSpan() { return noopSpan; }
|
| 333 |
+
export function endToolBlockedOnUserSpan() {}
|
| 334 |
+
export function startToolExecutionSpan() { return noopSpan; }
|
| 335 |
+
export function endToolExecutionSpan() {}
|
| 336 |
+
export function endToolSpan() {}
|
| 337 |
+
export function addToolContentEvent() {}
|
| 338 |
+
export function getCurrentSpan() { return null; }
|
| 339 |
+
export async function executeInSpan(spanName, fn) { return fn(noopSpan); }
|
| 340 |
+
export function startHookSpan() { return noopSpan; }
|
| 341 |
+
export function endHookSpan() {}
|
| 342 |
+
`,
|
| 343 |
+
|
| 344 |
+
// βββ Auto-updater (phones home to GCS + npm) ββββββββββββββββββ
|
| 345 |
+
|
| 346 |
+
'utils/autoUpdater': `
|
| 347 |
+
export async function assertMinVersion() {}
|
| 348 |
+
export async function getMaxVersion() { return undefined; }
|
| 349 |
+
export async function getMaxVersionMessage() { return undefined; }
|
| 350 |
+
export function shouldSkipVersion() { return true; }
|
| 351 |
+
export function getLockFilePath() { return '/tmp/openclaude-update.lock'; }
|
| 352 |
+
export async function checkGlobalInstallPermissions() { return { hasPermissions: false, npmPrefix: null }; }
|
| 353 |
+
export async function getLatestVersion() { return null; }
|
| 354 |
+
export async function getNpmDistTags() { return { latest: null, stable: null }; }
|
| 355 |
+
export async function getLatestVersionFromGcs() { return null; }
|
| 356 |
+
export async function getGcsDistTags() { return { latest: null, stable: null }; }
|
| 357 |
+
export async function getVersionHistory() { return []; }
|
| 358 |
+
export async function installGlobalPackage() { return 'success'; }
|
| 359 |
+
`,
|
| 360 |
+
|
| 361 |
+
// βββ Plugin fetch telemetry (not the marketplace itself) βββββββ
|
| 362 |
+
|
| 363 |
+
'utils/plugins/fetchTelemetry': `
|
| 364 |
+
export function logPluginFetch() {}
|
| 365 |
+
export function classifyFetchError() { return 'disabled'; }
|
| 366 |
+
`,
|
| 367 |
+
|
| 368 |
+
// βββ Transcript / feedback sharing βββββββββββββββββββββββββββββ
|
| 369 |
+
|
| 370 |
+
'components/FeedbackSurvey/submitTranscriptShare': `
|
| 371 |
+
export async function submitTranscriptShare() { return { success: false }; }
|
| 372 |
+
`,
|
| 373 |
+
|
| 374 |
+
// βββ Internal employee logging (not needed in the external build) βββββ
|
| 375 |
+
|
| 376 |
+
'services/internalLogging': `
|
| 377 |
+
export async function logPermissionContextForAnts() {}
|
| 378 |
+
export const getContainerId = async () => null;
|
| 379 |
+
`,
|
| 380 |
+
|
| 381 |
+
// βββ Deleted Anthropic-internal modules βββββββββββββββββββββββββββββββ
|
| 382 |
+
|
| 383 |
+
'services/api/dumpPrompts': `
|
| 384 |
+
export function createDumpPromptsFetch() { return undefined; }
|
| 385 |
+
export function getDumpPromptsPath() { return ''; }
|
| 386 |
+
export function getLastApiRequests() { return []; }
|
| 387 |
+
export function clearApiRequestCache() {}
|
| 388 |
+
export function clearDumpState() {}
|
| 389 |
+
export function clearAllDumpState() {}
|
| 390 |
+
export function addApiRequestToCache() {}
|
| 391 |
+
`,
|
| 392 |
+
|
| 393 |
+
'utils/undercover': `
|
| 394 |
+
export function isUndercover() { return false; }
|
| 395 |
+
export function getUndercoverInstructions() { return ''; }
|
| 396 |
+
export function shouldShowUndercoverAutoNotice() { return false; }
|
| 397 |
+
`,
|
| 398 |
+
|
| 399 |
+
'types/generated/events_mono/claude_code/v1/claude_code_internal_event': `
|
| 400 |
+
export const ClaudeCodeInternalEvent = {
|
| 401 |
+
fromJSON: value => value,
|
| 402 |
+
toJSON: value => value,
|
| 403 |
+
create: value => value ?? {},
|
| 404 |
+
fromPartial: value => value ?? {},
|
| 405 |
+
};
|
| 406 |
+
`,
|
| 407 |
+
|
| 408 |
+
'types/generated/events_mono/growthbook/v1/growthbook_experiment_event': `
|
| 409 |
+
export const GrowthbookExperimentEvent = {
|
| 410 |
+
fromJSON: value => value,
|
| 411 |
+
toJSON: value => value,
|
| 412 |
+
create: value => value ?? {},
|
| 413 |
+
fromPartial: value => value ?? {},
|
| 414 |
+
};
|
| 415 |
+
`,
|
| 416 |
+
|
| 417 |
+
'types/generated/events_mono/common/v1/auth': `
|
| 418 |
+
export const PublicApiAuth = {
|
| 419 |
+
fromJSON: value => value,
|
| 420 |
+
toJSON: value => value,
|
| 421 |
+
create: value => value ?? {},
|
| 422 |
+
fromPartial: value => value ?? {},
|
| 423 |
+
};
|
| 424 |
+
`,
|
| 425 |
+
|
| 426 |
+
'types/generated/google/protobuf/timestamp': `
|
| 427 |
+
export const Timestamp = {
|
| 428 |
+
fromJSON: value => value,
|
| 429 |
+
toJSON: value => value,
|
| 430 |
+
create: value => value ?? {},
|
| 431 |
+
fromPartial: value => value ?? {},
|
| 432 |
+
};
|
| 433 |
+
`,
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
function escapeForResolvedPathRegex(modulePath: string): string {
|
| 437 |
+
return modulePath
|
| 438 |
+
.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
|
| 439 |
+
.replace(/\//g, '[/\\\\]')
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
export const noTelemetryPlugin: BunPlugin = {
|
| 443 |
+
name: 'no-telemetry',
|
| 444 |
+
setup(build) {
|
| 445 |
+
for (const [modulePath, contents] of Object.entries(stubs)) {
|
| 446 |
+
// Build regex that matches the resolved file path on any OS
|
| 447 |
+
// e.g. "services/analytics/growthbook" β /services[/\\]analytics[/\\]growthbook\.(ts|js)$/
|
| 448 |
+
const escaped = escapeForResolvedPathRegex(modulePath)
|
| 449 |
+
const filter = new RegExp(`${escaped}\\.(ts|js)$`)
|
| 450 |
+
|
| 451 |
+
build.onLoad({ filter }, () => ({
|
| 452 |
+
contents,
|
| 453 |
+
loader: 'js',
|
| 454 |
+
}))
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
console.log(` π no-telemetry: stubbed ${Object.keys(stubs).length} modules`)
|
| 458 |
+
},
|
| 459 |
+
}
|