Rhodawk Bot commited on
Commit
39e315a
Β·
1 Parent(s): a411ede

feat(openclaude): vendor OpenClaude as headless gRPC daemon, eliminate aider

Browse files

Replace 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.

This view is limited to 50 files because it contains too many changes. Β  See raw diff
Files changed (50) hide show
  1. .gitignore_extra +1 -0
  2. ARCHITECTURE_ANALYSIS.md +0 -527
  3. COMPARISON_REPORT.md +0 -239
  4. Dockerfile +86 -49
  5. SYSTEM_ANALYSIS_BOOK.md +40 -37
  6. app.py +70 -81
  7. entrypoint.sh +57 -0
  8. mythos/MYTHOS_PLAN.md +0 -249
  9. openclaude_grpc/__init__.py +21 -0
  10. openclaude_grpc/client.py +348 -0
  11. requirements.txt +20 -26
  12. vendor/openclaude/.dockerignore +16 -0
  13. vendor/openclaude/.env.example +374 -0
  14. vendor/openclaude/.gitignore +14 -0
  15. vendor/openclaude/.release-please-manifest.json +3 -0
  16. vendor/openclaude/ANDROID_INSTALL.md +162 -0
  17. vendor/openclaude/CHANGELOG.md +176 -0
  18. vendor/openclaude/CODE_OF_CONDUCT.md +126 -0
  19. vendor/openclaude/CONTRIBUTING.md +119 -0
  20. vendor/openclaude/Dockerfile +46 -0
  21. vendor/openclaude/LICENSE +29 -0
  22. vendor/openclaude/PLAYBOOK.md +322 -0
  23. vendor/openclaude/README.md +345 -0
  24. vendor/openclaude/SECURITY.md +69 -0
  25. vendor/openclaude/bin/import-specifier.mjs +13 -0
  26. vendor/openclaude/bin/import-specifier.test.mjs +13 -0
  27. vendor/openclaude/bin/openclaude +32 -0
  28. vendor/openclaude/bun.lock +0 -0
  29. vendor/openclaude/docs/advanced-setup.md +273 -0
  30. vendor/openclaude/docs/hook-chains.md +333 -0
  31. vendor/openclaude/docs/litellm-setup.md +144 -0
  32. vendor/openclaude/docs/non-technical-setup.md +116 -0
  33. vendor/openclaude/docs/quick-start-mac-linux.md +143 -0
  34. vendor/openclaude/docs/quick-start-windows.md +143 -0
  35. vendor/openclaude/package.json +162 -0
  36. vendor/openclaude/python/__init__.py +1 -0
  37. vendor/openclaude/python/atomic_chat_provider.py +146 -0
  38. vendor/openclaude/python/ollama_provider.py +173 -0
  39. vendor/openclaude/python/requirements.txt +3 -0
  40. vendor/openclaude/python/smart_router.py +387 -0
  41. vendor/openclaude/python/tests/__init__.py +1 -0
  42. vendor/openclaude/python/tests/conftest.py +5 -0
  43. vendor/openclaude/python/tests/test_atomic_chat_provider.py +130 -0
  44. vendor/openclaude/python/tests/test_ollama_provider.py +192 -0
  45. vendor/openclaude/python/tests/test_smart_router.py +231 -0
  46. vendor/openclaude/release-please-config.json +11 -0
  47. vendor/openclaude/scripts/build.ts +492 -0
  48. vendor/openclaude/scripts/grpc-cli.ts +121 -0
  49. vendor/openclaude/scripts/no-telemetry-growthbook-stub.test.ts +163 -0
  50. vendor/openclaude/scripts/no-telemetry-plugin.ts +459 -0
.gitignore_extra ADDED
@@ -0,0 +1 @@
 
 
1
+ vendor/openclaude/node_modules/
ARCHITECTURE_ANALYSIS.md DELETED
@@ -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".
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
COMPARISON_REPORT.md DELETED
@@ -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
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Dockerfile CHANGED
@@ -1,4 +1,30 @@
1
- # Stage 1: Builder
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- apt-get install -y --no-install-recommends git curl ca-certificates build-essential && \
11
- rm -rf /var/lib/apt/lists/*
 
12
 
13
- # Use the official Astral image for a complete, clean uv installation
14
  COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
15
 
 
 
 
 
16
  WORKDIR /build
17
  COPY requirements.txt .
18
-
19
- # Install dependencies directly β€” avoids wheel-build failures for packages
20
- # that require special compile-time tooling (e.g. atheris/libFuzzer).
21
- # atheris has been removed from requirements.txt; Hypothesis is the fallback.
22
- RUN pip install --no-cache-dir -r requirements.txt mcp-server-fetch && \
23
- # Aider's strict pins (pillow==12.1.1, litellm==1.75.0) conflict with
24
- # gradio (pillow<12) and ship a broken litellm module surface
25
- # (missing APIConnectionError, _logging, encode, token_counter).
26
- # We install aider WITHOUT its deps and then provide a known-good
27
- # set of its actual runtime imports β€” letting gradio's pillow 11
28
- # win, and a patched litellm replace the broken 1.75.0.
29
- pip install --no-cache-dir --no-deps "aider-chat==0.86.2" && \
30
- pip install --no-cache-dir --upgrade --no-deps "litellm==1.78.5" && \
31
- pip install --no-cache-dir \
32
- "configargparse" "jsonschema" "rich" "prompt_toolkit" "pyyaml" \
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
- LABEL org.opencontainers.image.title="Rhodawk AI DevSecOps Engine"
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
- RUN apt-get update && \
54
- apt-get install -y --no-install-recommends nodejs npm && \
55
- rm -rf /var/lib/apt/lists/*
56
-
57
- RUN npm install -g --quiet @modelcontextprotocol/server-github
58
-
59
- # Hugging Face UID 1000 handling
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
- # FIX: create /data with explicit mode so uv venv can write target_venv
64
- # even before the application calls os.makedirs() at runtime.
65
- RUN mkdir -p /data /app && chmod 777 /data && chown -R rhodawk:rhodawk /app
66
 
67
- # Copy the uv executable from the official image
68
- COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
 
 
 
 
 
 
69
 
70
  WORKDIR /app
71
  USER rhodawk
72
 
73
- # Copy the source code
74
  COPY --chown=rhodawk:rhodawk . .
75
 
76
- EXPOSE 7860
 
 
 
 
 
 
 
 
 
 
77
 
78
- CMD ["python", "-u", "app.py"]
 
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"]
SYSTEM_ANALYSIS_BOOK.md CHANGED
@@ -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. `requirements.txt` does **not** mention `aider-chat`.
77
- The Dockerfile installs requirements normally, then layers in
78
- (a) `aider-chat==0.86.2 --no-deps`, (b) `litellm==1.78.5 --no-deps --upgrade`,
79
- (c) a curated set of aider's actual runtime imports (configargparse,
80
- jsonschema, rich, prompt_toolkit, pyyaml, pathspec, diskcache, networkx,
81
- scipy, beautifulsoup4, pypandoc, flake8, importlib_resources, pyperclip,
82
- pexpect, json5, psutil, watchfiles, socksio, mixpanel, posthog, tree-sitter,
83
- grep_ast, oslex, tokenizers, google-generativeai, openai, diff-match-patch,
84
- soundfile, sounddevice).
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: **aider** (the patch generator) and
98
- **Hermes** (the multi-phase research orchestrator + adversarial reviewer).
99
- Both have been wired to the same provider chain.
 
100
 
101
- ### 2.1 Aider path β€” `app.py::run_aider`
102
 
103
  ```
104
- caller passes model=None
105
- β”‚
106
- β”œβ”€β–Ί DEFAULT_MODEL (constructed at import time)
107
- β”‚ β”œβ”€β”€ DO_INFERENCE_API_KEY set? β†’ "openai/llama3.3-70b-instruct"
108
- β”‚ └── otherwise β†’ "openrouter/qwen/qwen-2.5-coder-32b-instruct:free"
109
  β”‚
110
- β”œβ”€β–Ί FALLBACK_MODELS list = ["openrouter/qwen/qwen-2.5-coder-32b-instruct:free"]
 
111
  β”‚
112
- └─► provider chain executes [primary, *fallbacks]:
113
- for each model m:
114
- if m starts with "openai/" and DO_INFERENCE_API_KEY:
115
- env OPENAI_API_KEY = DO_INFERENCE_API_KEY
116
- env OPENAI_API_BASE = https://inference.do-ai.run/v1
117
- env OPENAI_BASE_URL = https://inference.do-ai.run/v1
118
- args += --openai-api-base, --openai-api-key
119
- run aider; if exit==0 β†’ return
120
- elif m starts with "openrouter/" and OPENROUTER_API_KEY:
121
- env OPENROUTER_API_KEY = OPENROUTER_API_KEY
122
- run aider; if exit==0 β†’ return
123
- else: skip
124
- if all skipped β†’ "No inference provider configured" message
125
- if all failed β†’ return last error with provider chain printed
126
  ```
127
 
128
- The `openai/<model>` prefix is litellm's convention for "treat this as an
129
- OpenAI-API-compatible endpoint." DigitalOcean's Serverless Inference exposes
130
- the OpenAI Chat Completions schema verbatim, so litellm + aider need no
131
- DO-specific code path β€” only env vars and a base URL override.
 
 
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
 
app.py CHANGED
@@ -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
- "aider-patcher": {
445
- "command": "uvx",
446
- "args": ["mcp-server-shell", "--allow-commands", "aider"],
447
- "description": "Aider AI code editor β€” applies LLM-generated patches with diff verification and test re-run",
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
- def _aider_provider_options(model: str) -> tuple[list[str], dict[str, str]]:
620
- """
621
- Return (extra_cli_args, env_overrides) for a given aider --model string.
622
- Routes ``openai/<model>`` calls through DigitalOcean Serverless Inference,
623
- and ``openrouter/<model>`` calls through OpenRouter.
624
- """
625
- env: dict[str, str] = {}
626
- args: list[str] = []
627
- if model.startswith("openai/") and DO_INFERENCE_API_KEY:
628
- # DigitalOcean OpenAI-compatible endpoint.
629
- env["OPENAI_API_KEY"] = DO_INFERENCE_API_KEY
630
- env["OPENAI_API_BASE"] = DO_INFERENCE_BASE_URL
631
- # Some litellm versions also read these:
632
- env["OPENAI_BASE_URL"] = DO_INFERENCE_BASE_URL
633
- args += ["--openai-api-base", DO_INFERENCE_BASE_URL,
634
- "--openai-api-key", DO_INFERENCE_API_KEY]
635
- elif model.startswith("openrouter/") and OPENROUTER_API_KEY:
636
- env["OPENROUTER_API_KEY"] = OPENROUTER_API_KEY
637
- return args, env
638
-
639
-
640
- def _run_aider_once(model: str, prompt_path: str, valid_files: list[str]) -> tuple[str, int]:
641
- extra_args, env_overrides = _aider_provider_options(model)
642
- cmd = ["aider", "--model", model, "--yes", "--no-stream",
643
- "--message-file", prompt_path] + extra_args + valid_files
644
- return run_subprocess_safe(cmd, cwd=REPO_DIR, timeout=600,
645
- env_overrides=env_overrides,
646
- raise_on_error=False)
 
647
 
648
 
649
  def run_aider(mcp_config_path: str, prompt: str, context_files: list[str]) -> tuple[str, int]:
 
 
 
 
 
 
 
 
 
 
 
 
 
650
  """
651
- Run aider with DigitalOcean Serverless Inference as PRIMARY and
652
- OpenRouter as FALLBACK. If the primary call exits non-zero (rate
653
- limit, provider outage, transient crash), we automatically retry the
654
- same prompt against the OpenRouter fallback model.
655
-
656
- Note: --mcp-config is NOT a supported aider CLI flag in any released
657
- version of aider-chat (including 0.86.1). The mcp_config_path argument
658
- is kept in the function signature for forward-compatibility but is not
659
- forwarded to the subprocess until upstream adds the flag.
660
- """
661
- fd, prompt_path = tempfile.mkstemp(prefix="aider_prompt_", suffix=".txt")
662
- try:
663
- with os.fdopen(fd, "w") as f:
664
- f.write(prompt)
665
- valid = [f for f in context_files if os.path.exists(os.path.join(REPO_DIR, f))]
666
-
667
- # Build provider chain: primary first, fallback second (deduped).
668
- chain: list[str] = []
669
- for m in (MODEL, FALLBACK_MODEL):
670
- if m and m not in chain:
671
- # Skip a provider if its credentials aren't configured.
672
- if m.startswith("openai/") and not DO_INFERENCE_API_KEY:
673
- continue
674
- if m.startswith("openrouter/") and not OPENROUTER_API_KEY:
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
  # ──────────────────────────────────────────────────────────────
entrypoint.sh ADDED
@@ -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
mythos/MYTHOS_PLAN.md DELETED
@@ -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.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openclaude_grpc/__init__.py ADDED
@@ -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
+ ]
openclaude_grpc/client.py ADDED
@@ -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
requirements.txt CHANGED
@@ -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
- # aider-chat is intentionally NOT listed here β€” it hard-pins
8
- # pillow==12.1.1 which conflicts with gradio (pillow<12) and litellm==1.75.0
9
- # which has a broken module surface (missing APIConnectionError, _logging).
10
- # The Dockerfile installs aider with --no-deps after this requirements file
11
- # resolves, plus a patched litellm and aider's actual runtime deps.
 
 
 
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
- # atheris removed: requires Clang + libFuzzer at compile time which is unavailable
32
- # on HuggingFace Space Docker images. Fuzzing falls back to Hypothesis automatically.
 
 
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
- # Optional heavy bridges β€” install when moving to the paid VPS / lab rig:
56
- # playwright>=1.44.0 # browser-agent-mcp (run `playwright install chromium`)
57
- # python-can>=4.3.0 # can-bus-mcp (automotive)
58
- # scapy>=2.5.0 # network-protocol fuzzing
59
- # pymodbus>=3.6.0 # ics-scada
60
- # python-snap7>=1.3 # ics-scada (Siemens S7)
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
 
 
vendor/openclaude/.dockerignore ADDED
@@ -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
vendor/openclaude/.env.example ADDED
@@ -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)
vendor/openclaude/.gitignore ADDED
@@ -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
vendor/openclaude/.release-please-manifest.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ ".": "0.6.0"
3
+ }
vendor/openclaude/ANDROID_INSTALL.md ADDED
@@ -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.
vendor/openclaude/CHANGELOG.md ADDED
@@ -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 &lt; 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))
vendor/openclaude/CODE_OF_CONDUCT.md ADDED
@@ -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).
vendor/openclaude/CONTRIBUTING.md ADDED
@@ -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.
vendor/openclaude/Dockerfile ADDED
@@ -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"]
vendor/openclaude/LICENSE ADDED
@@ -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.
vendor/openclaude/PLAYBOOK.md ADDED
@@ -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.
vendor/openclaude/README.md ADDED
@@ -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
+ [![PR Checks](https://github.com/Gitlawb/openclaude/actions/workflows/pr-checks.yml/badge.svg?branch=main)](https://github.com/Gitlawb/openclaude/actions/workflows/pr-checks.yml)
8
+ [![Release](https://img.shields.io/github/v/tag/Gitlawb/openclaude?label=release&color=0ea5e9)](https://github.com/Gitlawb/openclaude/tags)
9
+ [![Discussions](https://img.shields.io/badge/discussions-open-7c3aed)](https://github.com/Gitlawb/openclaude/discussions)
10
+ [![Security Policy](https://img.shields.io/badge/security-policy-0f766e)](SECURITY.md)
11
+ [![License](https://img.shields.io/badge/license-MIT-2563eb)](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
+ [![Star History Chart](https://api.star-history.com/chart?repos=gitlawb/openclaude&type=date&legend=top-left)](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).
vendor/openclaude/SECURITY.md ADDED
@@ -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
vendor/openclaude/bin/import-specifier.mjs ADDED
@@ -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
+ }
vendor/openclaude/bin/import-specifier.test.mjs ADDED
@@ -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
+ })
vendor/openclaude/bin/openclaude ADDED
@@ -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
+ }
vendor/openclaude/bun.lock ADDED
The diff for this file is too large to render. See raw diff
 
vendor/openclaude/docs/advanced-setup.md ADDED
@@ -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.
vendor/openclaude/docs/hook-chains.md ADDED
@@ -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.
vendor/openclaude/docs/litellm-setup.md ADDED
@@ -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)
vendor/openclaude/docs/non-technical-setup.md ADDED
@@ -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)
vendor/openclaude/docs/quick-start-mac-linux.md ADDED
@@ -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)
vendor/openclaude/docs/quick-start-windows.md ADDED
@@ -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)
vendor/openclaude/package.json ADDED
@@ -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
+ }
vendor/openclaude/python/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Python helper package for standalone provider-side utilities.
vendor/openclaude/python/atomic_chat_provider.py ADDED
@@ -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
vendor/openclaude/python/ollama_provider.py ADDED
@@ -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
vendor/openclaude/python/requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ pytest==7.4.4
2
+ pytest-asyncio==0.23.3
3
+ httpx==0.25.2
vendor/openclaude/python/smart_router.py ADDED
@@ -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
+ ]
vendor/openclaude/python/tests/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Pytest package marker for the Python helper test suite.
vendor/openclaude/python/tests/conftest.py ADDED
@@ -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]))
vendor/openclaude/python/tests/test_atomic_chat_provider.py ADDED
@@ -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
vendor/openclaude/python/tests/test_ollama_provider.py ADDED
@@ -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"]
vendor/openclaude/python/tests/test_smart_router.py ADDED
@@ -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
vendor/openclaude/release-please-config.json ADDED
@@ -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
+ }
vendor/openclaude/scripts/build.ts ADDED
@@ -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
+ }
vendor/openclaude/scripts/grpc-cli.ts ADDED
@@ -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()
vendor/openclaude/scripts/no-telemetry-growthbook-stub.test.ts ADDED
@@ -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
+ })
vendor/openclaude/scripts/no-telemetry-plugin.ts ADDED
@@ -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
+ }