Compare commits

..

1 Commits

Author SHA1 Message Date
anandh8x
bd8d0ef0fa Remove embedded source map directives from tracked sources
Inline base64 source maps had been checked into tracked src files. This strips those comments from the repository without changing runtime behavior or adding ongoing guardrails, per the requested one-time cleanup scope.

Constraint: Keep this change limited to tracked source cleanup only
Rejected: Add CI/source verification guard | user requested one-time cleanup only
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: If these directives reappear, fix the producing transform instead of reintroducing repo-side cleanup code
Tested: rg -n "sourceMappingURL" ., bun run smoke, bun run verify:privacy, bun run test:provider, npm run test:provider-recommendation
Not-tested: bun run typecheck (repository has many pre-existing unrelated failures)
2026-04-04 13:38:49 +05:30
259 changed files with 4615 additions and 11242 deletions

View File

@@ -16,8 +16,6 @@ jobs:
steps:
- name: Check out repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
@@ -35,11 +33,6 @@ jobs:
- name: Smoke check
run: bun run smoke
- name: Full unit test suite
run: bun test --max-concurrency=1
- name: Suspicious PR intent scan
run: bun run security:pr-scan -- --base ${{ github.event.pull_request.base.sha || 'origin/main' }}
- name: Provider tests
run: bun run test:provider

4
.gitignore vendored
View File

@@ -6,7 +6,3 @@ dist/
!.env.example
.openclaude-profile.json
reports/
GEMINI.md
package-lock.json
/.claude
coverage/

211
README.md
View File

@@ -1,24 +1,33 @@
# OpenClaude
OpenClaude is an open-source coding-agent CLI for cloud and local model providers.
OpenClaude is an open-source coding-agent CLI that works with more than one model provider.
Use OpenAI-compatible APIs, Gemini, GitHub Models, Codex, Ollama, Atomic Chat, and other supported backends while keeping one terminal-first workflow: prompts, tools, agents, MCP, slash commands, and streaming output.
[![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)
[![Release](https://img.shields.io/github/v/tag/Gitlawb/openclaude?label=release&color=0ea5e9)](https://github.com/Gitlawb/openclaude/tags)
[![Discussions](https://img.shields.io/badge/discussions-open-7c3aed)](https://github.com/Gitlawb/openclaude/discussions)
[![Security Policy](https://img.shields.io/badge/security-policy-0f766e)](SECURITY.md)
[![License](https://img.shields.io/badge/license-MIT-2563eb)](LICENSE)
[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)
Use OpenAI-compatible APIs, Gemini, GitHub Models, Codex, Ollama, Atomic Chat, and other supported backends while keeping the same terminal-first workflow: prompts, tools, agents, MCP, slash commands, and streaming output.
## Why OpenClaude
- Use one CLI across cloud APIs and local model backends
- Use one CLI across cloud and local model providers
- Save provider profiles inside the app with `/provider`
- Run with OpenAI-compatible services, Gemini, GitHub Models, Codex, Ollama, Atomic Chat, and other supported providers
- Keep coding-agent workflows in one place: bash, file tools, grep, glob, agents, tasks, MCP, and web tools
- Use the bundled VS Code extension for launch integration and theme support
- Run locally with Ollama or Atomic Chat
- Keep core coding-agent workflows: bash, file tools, grep, glob, agents, tasks, MCP, and web tools
## Provenance & Legal Notice
OpenClaude is derived from Anthropic's Claude Code CLI source code, which was
inadvertently exposed in March 2026 through a packaging error in npm. The
original Claude Code source is proprietary software owned by Anthropic PBC.
This project adds multi-provider support, strips telemetry, and adapts the
codebase for open use. It is not an authorized fork or open-source release
by Anthropic.
**"Claude" and "Claude Code" are trademarks of Anthropic PBC.**
Contributors should be aware that the legal status of distributing code
derived from Anthropic's proprietary source is unresolved. See the LICENSE
file for details.
---
## Quick Start
@@ -28,7 +37,7 @@ Use OpenAI-compatible APIs, Gemini, GitHub Models, Codex, Ollama, Atomic Chat, a
npm install -g @gitlawb/openclaude
```
If the install later reports `ripgrep not found`, install ripgrep system-wide and confirm `rg --version` works in the same terminal before starting OpenClaude.
If the npm install path later reports `ripgrep not found`, install ripgrep system-wide and confirm `rg --version` works in the same terminal before starting OpenClaude.
### Start
@@ -38,8 +47,8 @@ openclaude
Inside OpenClaude:
- run `/provider` for guided provider setup and saved profiles
- run `/onboard-github` for GitHub Models onboarding
- run `/provider` for guided setup of OpenAI-compatible, Gemini, Ollama, or Codex profiles
- run `/onboard-github` for GitHub Models setup
### Fastest OpenAI setup
@@ -85,6 +94,8 @@ $env:OPENAI_MODEL="qwen2.5-coder:7b"
openclaude
```
---
## Setup Guides
Beginner-friendly guides:
@@ -98,26 +109,38 @@ Advanced and source-build guides:
- [Advanced Setup](docs/advanced-setup.md)
- [Android Install](ANDROID_INSTALL.md)
---
## Supported Providers
| Provider | Setup Path | Notes |
| --- | --- | --- |
| OpenAI-compatible | `/provider` or env vars | Works with OpenAI, OpenRouter, DeepSeek, Groq, Mistral, LM Studio, and other compatible `/v1` servers |
| Gemini | `/provider` or env vars | Supports API key, access token, or local ADC workflow on current `main` |
| OpenAI-compatible | `/provider` or env vars | Works with OpenAI, OpenRouter, DeepSeek, Groq, Mistral, LM Studio, and compatible local `/v1` servers |
| Gemini | `/provider` or env vars | Google Gemini support through the runtime provider layer |
| GitHub Models | `/onboard-github` | Interactive onboarding with saved credentials |
| Codex | `/provider` | Uses existing Codex credentials when available |
| Ollama | `/provider` or env vars | Local inference with no API key |
| Atomic Chat | advanced setup | Local Apple Silicon backend |
| Bedrock / Vertex / Foundry | env vars | Additional provider integrations for supported environments |
---
## What Works
- **Tool-driven coding workflows**: Bash, file read/write/edit, grep, glob, agents, tasks, MCP, and slash commands
- **Streaming responses**: Real-time token output and tool progress
- **Tool calling**: Multi-step tool loops with model calls, tool execution, and follow-up responses
- **Images**: URL and base64 image inputs for providers that support vision
- **Provider profiles**: Guided setup plus saved `.openclaude-profile.json` support
- **Local and remote model backends**: Cloud APIs, local servers, and Apple Silicon local inference
- Tool-driven coding workflows
Bash, file read/write/edit, grep, glob, agents, tasks, MCP, and slash commands
- Streaming responses
Real-time token output and tool progress
- Tool calling
Multi-step tool loops with model calls, tool execution, and follow-up responses
- Images
URL and base64 image inputs for providers that support vision
- Provider profiles
Guided setup plus saved `.openclaude-profile.json` support
- Local and remote model backends
Cloud APIs, local servers, and Apple Silicon local inference
---
## Provider Notes
@@ -130,9 +153,13 @@ OpenClaude supports multiple providers, but behavior is not identical across all
For best results, use models with strong tool/function calling support.
---
## Agent Routing
OpenClaude can route different agents to different models through settings-based routing. This is useful for cost optimization or splitting work by model strength.
Route different agents to different AI providers within the same session. Useful for cost optimization (cheap model for code review, powerful model for complex coding) or leveraging model strengths.
### Configuration
Add to `~/.claude/settings.json`:
@@ -158,19 +185,29 @@ Add to `~/.claude/settings.json`:
}
```
When no routing match is found, the global provider remains the fallback.
### How It Works
- **agentModels**: Maps model names to OpenAI-compatible API endpoints
- **agentRouting**: Maps agent types or team member names to model names
- **Priority**: `name` > `subagent_type` > `"default"` > global provider
- **Matching**: Case-insensitive, hyphen/underscore equivalent (`general-purpose` = `general_purpose`)
- **Teams**: Team members are routed by their `name` — no extra config needed
When no routing match is found, the global provider (env vars) is used as fallback.
> **Note:** `api_key` values in `settings.json` are stored in plaintext. Keep this file private and do not commit it to version control.
---
## Web Search and Fetch
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.
By default, `WebSearch` now 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.
> **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.
>**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.
For Anthropic-native backends and Codex responses, OpenClaude keeps the native provider web search behavior.
For Anthropic-native backends (Anthropic/Vertex/Foundry) and Codex responses, OpenClaude keeps the native provider web search behavior.
`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.
`WebFetch` works but uses basic HTTP plus HTML-to-markdown conversion. That fails on JavaScript-rendered pages (React, Next.js, Vue SPAs) and sites that block plain HTTP requests.
Set a [Firecrawl](https://firecrawl.dev) API key if you want Firecrawl-powered search/fetch behavior:
@@ -180,47 +217,14 @@ export FIRECRAWL_API_KEY=your-key-here
With Firecrawl enabled:
- `WebSearch` can use Firecrawl's search API while DuckDuckGo remains the default free path for non-Claude models
- `WebSearch` can use Firecrawl's search API (while DuckDuckGo remains the default free path for non-Claude models)
- `WebFetch` uses Firecrawl's scrape endpoint instead of raw HTTP, handling JS-rendered pages correctly
Free tier at [firecrawl.dev](https://firecrawl.dev) includes 500 credits. The key is optional.
---
## Headless gRPC Server
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.
### 1. Start the gRPC Server
Start the core engine as a gRPC service on `localhost:50051`:
```bash
npm run dev:grpc
```
#### Configuration
| Variable | Default | Description |
|-----------|-------------|------------------------------------------------|
| `GRPC_PORT` | `50051` | Port the gRPC server listens on |
| `GRPC_HOST` | `localhost` | Bind address. Use `0.0.0.0` to expose on all interfaces (not recommended without authentication) |
### 2. Run the Test CLI Client
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.
In a separate terminal, run:
```bash
npm run dev:grpc:cli
```
*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.*
---
## Source Build And Local Development
## Source Build
```bash
bun install
@@ -231,78 +235,22 @@ node dist/cli.mjs
Helpful commands:
- `bun run dev`
- `bun test`
- `bun run test:coverage`
- `bun run security:pr-scan -- --base origin/main`
- `bun run smoke`
- `bun run doctor:runtime`
- `bun run verify:privacy`
- focused `bun test ...` runs for the areas you touch
## Testing And Coverage
OpenClaude uses Bun's built-in test runner for unit tests.
Run the full unit suite:
```bash
bun test
```
Generate unit test coverage:
```bash
bun run test:coverage
```
Open the visual coverage report:
```bash
open coverage/index.html
```
If you already have `coverage/lcov.info` and only want to rebuild the UI:
```bash
bun run test:coverage:ui
```
Use focused test runs when you only touch one area:
- `bun run test:provider`
- `bun run test:provider-recommendation`
- `bun test path/to/file.test.ts`
Recommended contributor validation before opening a PR:
- `bun run build`
- `bun run smoke`
- `bun run test:coverage` for broader unit coverage when your change affects shared runtime or provider logic
- focused `bun test ...` runs for the files and flows you changed
Coverage output is written to `coverage/lcov.info`, and OpenClaude also generates a git-activity-style heatmap at `coverage/index.html`.
## Repository Structure
- `src/` - core CLI/runtime
- `scripts/` - build, verification, and maintenance scripts
- `docs/` - setup, contributor, and project documentation
- `python/` - standalone Python helpers and their tests
- `vscode-extension/openclaude-vscode/` - VS Code extension
- `.github/` - repo automation, templates, and CI configuration
- `bin/` - CLI launcher entrypoints
---
## VS Code Extension
The repo includes a VS Code extension in [`vscode-extension/openclaude-vscode`](vscode-extension/openclaude-vscode) for OpenClaude launch integration, provider-aware control-center UI, and theme support.
The repo includes a VS Code extension in [`vscode-extension/openclaude-vscode`](vscode-extension/openclaude-vscode) for OpenClaude launch integration and theme support.
---
## Security
If you believe you found a security issue, see [SECURITY.md](SECURITY.md).
## Community
- Use [GitHub Discussions](https://github.com/Gitlawb/openclaude/discussions) for Q&A, ideas, and community conversation
- Use [GitHub Issues](https://github.com/Gitlawb/openclaude/issues) for confirmed bugs and actionable feature work
---
## Contributing
@@ -311,16 +259,19 @@ Contributions are welcome.
For larger changes, open an issue first so the scope is clear before implementation. Helpful validation commands include:
- `bun run build`
- `bun run test:coverage`
- `bun run smoke`
- focused `bun test ...` runs for touched areas
---
## Disclaimer
OpenClaude is an independent community project and is not affiliated with, endorsed by, or sponsored by Anthropic.
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.
"Claude" and "Claude Code" are trademarks of Anthropic.
---
## License
See [LICENSE](LICENSE).
MIT

View File

@@ -1,13 +1,7 @@
import { join, win32 } from 'path'
import { join } from 'path'
import { pathToFileURL } from 'url'
export function getDistImportSpecifier(baseDir) {
if (/^[A-Za-z]:\\/.test(baseDir)) {
const distPath = win32.join(baseDir, '..', 'dist', 'cli.mjs')
return `file:///${distPath.replace(/\\/g, '/')}`
}
const joinImpl = join
const distPath = joinImpl(baseDir, '..', 'dist', 'cli.mjs')
const distPath = join(baseDir, '..', 'dist', 'cli.mjs')
return pathToFileURL(distPath).href
}

129
bun.lock
View File

@@ -13,8 +13,6 @@
"@anthropic-ai/vertex-sdk": "0.14.4",
"@commander-js/extra-typings": "12.1.0",
"@growthbook/growthbook": "1.6.5",
"@grpc/grpc-js": "^1.14.3",
"@grpc/proto-loader": "^0.8.0",
"@mendable/firecrawl-js": "4.18.1",
"@modelcontextprotocol/sdk": "1.29.0",
"@opentelemetry/api": "1.9.1",
@@ -38,7 +36,6 @@
"cli-highlight": "2.1.11",
"code-excerpt": "4.0.0",
"commander": "12.1.0",
"cross-spawn": "7.0.6",
"diff": "8.0.3",
"duck-duck-scrape": "^2.2.7",
"emoji-regex": "10.6.0",
@@ -53,7 +50,7 @@
"ignore": "7.0.5",
"indent-string": "5.0.0",
"jsonc-parser": "3.3.1",
"lodash-es": "4.18.1",
"lodash-es": "4.18.0",
"lru-cache": "11.2.7",
"marked": "15.0.12",
"p-map": "7.0.4",
@@ -86,14 +83,10 @@
"@types/bun": "1.3.11",
"@types/node": "25.5.0",
"@types/react": "19.2.14",
"tsx": "^4.21.0",
"typescript": "5.9.3",
},
},
},
"overrides": {
"lodash-es": "4.18.1",
},
"packages": {
"@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.3.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-p+CMKJ93HFmLkjXKlXiVGlMQEuRb6H0MokBSwUsX+S6BRX8eV5naFZpQJFfJHjRZY0Hmnqy1/r6UWl3x+19zYA=="],
@@ -187,58 +180,6 @@
"@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="],
"@growthbook/growthbook": ["@growthbook/growthbook@1.6.5", "", { "dependencies": { "dom-mutator": "^0.6.0" } }, "sha512-mUaMsgeUTpRIUOTn33EUXHRK6j7pxBjwqH4WpQyq+pukjd1AIzWlEa6w7i6bInJUcweGgP2beXZmaP6b6UPn7A=="],
"@grpc/grpc-js": ["@grpc/grpc-js@1.14.3", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA=="],
@@ -511,7 +452,7 @@
"cli-highlight": ["cli-highlight@2.1.11", "", { "dependencies": { "chalk": "^4.0.0", "highlight.js": "^10.7.1", "mz": "^2.4.0", "parse5": "^5.1.1", "parse5-htmlparser2-tree-adapter": "^6.0.0", "yargs": "^16.0.0" }, "bin": { "highlight": "bin/highlight" } }, "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg=="],
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="],
"code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="],
@@ -579,8 +520,6 @@
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
"esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
@@ -627,8 +566,6 @@
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"fuse.js": ["fuse.js@7.1.0", "", {}, "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ=="],
@@ -647,8 +584,6 @@
"get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="],
"get-tsconfig": ["get-tsconfig@4.13.7", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q=="],
"google-auth-library": ["google-auth-library@9.15.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^6.1.1", "gcp-metadata": "^6.1.0", "gtoken": "^7.0.0", "jws": "^4.0.0" } }, "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng=="],
"google-logging-utils": ["google-logging-utils@0.0.2", "", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="],
@@ -721,7 +656,7 @@
"locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
"lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="],
"lodash-es": ["lodash-es@4.18.0", "", {}, "sha512-koAgswPPA+UTaPN64Etp+PGP+WT6oqOS2NMi5yDkMaiGw9qY4VxQbQF0mtKMyr4BlTznWyzePV5UpECTJQmSUA=="],
"lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
@@ -825,8 +760,6 @@
"require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="],
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
"retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
@@ -897,8 +830,6 @@
"tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
"tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="],
"turndown": ["turndown@7.2.2", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ=="],
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
@@ -949,9 +880,9 @@
"yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="],
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
"yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="],
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="],
"yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="],
@@ -959,6 +890,8 @@
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
"@anthropic-ai/sandbox-runtime/lodash-es": ["lodash-es@4.17.23", "", {}, "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg=="],
"@aws-crypto/crc32/@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
"@aws-crypto/crc32/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
@@ -1151,6 +1084,8 @@
"@emnapi/runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@grpc/proto-loader/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
"@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/core": ["@opentelemetry/core@1.30.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ=="],
"@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.57.2", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/otlp-transformer": "0.57.2" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-XdxEzL23Urhidyebg5E6jZoaiW5ygP/mRjxLHixogbqwDy2Faduzb5N0o/Oi+XTIJu+iyxXdVORjXax+Qgfxag=="],
@@ -1369,8 +1304,6 @@
"cli-highlight/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"cli-highlight/yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="],
"cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
@@ -1425,6 +1358,12 @@
"@aws-sdk/nested-clients/@smithy/util-base64/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.2", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q=="],
"@grpc/proto-loader/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"@grpc/proto-loader/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"@grpc/proto-loader/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
"@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.57.2", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A=="],
@@ -1491,12 +1430,6 @@
"cli-highlight/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"cli-highlight/yargs/cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="],
"cli-highlight/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"cli-highlight/yargs/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="],
"cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"cliui/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
@@ -1537,6 +1470,16 @@
"@aws-sdk/nested-clients/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow=="],
"@grpc/proto-loader/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"@grpc/proto-loader/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"@grpc/proto-loader/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"@grpc/proto-loader/yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"@grpc/proto-loader/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
"@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
@@ -1557,16 +1500,6 @@
"@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@2.2.0", "", { "dependencies": { "@smithy/types": "^2.12.0", "@smithy/util-uri-escape": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-L1kSeviUWL+emq3CUVSgdogoM/D9QMFaqxL/dd0X7PCNWmPXqt+ExtrBjqT0V7HLN03Vs9SuiLrG3zy3JGnE5A=="],
"cli-highlight/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"cli-highlight/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"cli-highlight/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"cli-highlight/yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"cli-highlight/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"qrcode/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"qrcode/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
@@ -1579,16 +1512,16 @@
"yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"@grpc/proto-loader/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"@grpc/proto-loader/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"@grpc/proto-loader/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-jtmJMyt1xMD/d8OtbVJ2gFZOSKc+ueYJZPW20ULW1GOp/q/YIM0wNh+u8ZFao9UaIGz4WoPW8hC64qlWLIfoDA=="],
"@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-jtmJMyt1xMD/d8OtbVJ2gFZOSKc+ueYJZPW20ULW1GOp/q/YIM0wNh+u8ZFao9UaIGz4WoPW8hC64qlWLIfoDA=="],
"cli-highlight/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"cli-highlight/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"cli-highlight/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"qrcode/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"qrcode/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],

View File

@@ -1,144 +0,0 @@
# LiteLLM Setup
OpenClaude can connect to LiteLLM through LiteLLM's OpenAI-compatible proxy.
## Overview
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.
## Prerequisites
- LiteLLM installed (`pip install litellm[proxy]`)
- A `litellm_config.yaml` or equivalent LiteLLM configuration
- LiteLLM Proxy running on a local or remote port
## 1. Start the LiteLLM Proxy
### Basic installation
```bash
pip install litellm[proxy]
```
### Configure LiteLLM
Create a `litellm_config.yaml` with your desired model aliases:
```yaml
model_list:
- model_name: gpt-4o
litellm_params:
model: openai/gpt-4o
api_key: os.environ/OPENAI_API_KEY
- model_name: claude-sonnet-4
litellm_params:
model: anthropic/claude-sonnet-4-5-20250929
api_key: os.environ/ANTHROPIC_API_KEY
- model_name: gemini-2.5-flash
litellm_params:
model: gemini/gemini-2.5-flash
api_key: os.environ/GEMINI_API_KEY
- model_name: llama-3.3-70b
litellm_params:
model: together_ai/meta-llama/Llama-3.3-70B-Instruct-Turbo
api_key: os.environ/TOGETHER_API_KEY
```
### Run the proxy
```bash
litellm --config litellm_config.yaml --port 4000
```
The proxy will start at `http://localhost:4000` by default.
## 2. Point OpenClaude to LiteLLM
### Option A: Environment Variables
```bash
export CLAUDE_CODE_USE_OPENAI=1
export OPENAI_BASE_URL=http://localhost:4000
export OPENAI_API_KEY=<your-master-key-or-placeholder>
export OPENAI_MODEL=<your-litellm-model-alias>
openclaude
```
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`).
### Option B: Using /provider
1. Run `openclaude`
2. Type `/provider` to open the provider setup flow
3. Choose the **OpenAI-compatible** option
4. When prompted for the API key, enter the key required by your LiteLLM proxy
If your local LiteLLM setup does not enforce auth, you may still need to enter a placeholder value
- 5. When prompted for the base URL, enter `http://localhost:4000`
6. 6. When prompted for the model, enter the LiteLLM model name or alias you configured
7. 7. Save the provider configuration
## 3. Example LiteLLM Configs
### Multi-provider routing with spend tracking
```yaml
model_list:
- model_name: gpt-4o
litellm_params:
model: openai/gpt-4o
api_key: os.environ/OPENAI_API_KEY
- model_name: claude-sonnet-4
litellm_params:
model: anthropic/claude-sonnet-4-5-20250929
api_key: os.environ/ANTHROPIC_API_KEY
- model_name: deepseek-chat
litellm_params:
model: deepseek/deepseek-chat
api_key: os.environ/DEEPSEEK_API_KEY
litellm_settings:
set_verbose: false
num_retries: 3
```
### With a master key for auth
```bash
# Start proxy with a master key
litellm --config litellm_config.yaml --port 4000 --master_key sk-my-master-key
# Connect OpenClaude
export CLAUDE_CODE_USE_OPENAI=1
export OPENAI_BASE_URL=http://localhost:4000
export OPENAI_API_KEY=sk-my-master-key
export OPENAI_MODEL=gpt-4o
openclaude
```
## 4. Notes
- `OPENAI_MODEL` must match the **LiteLLM model alias** defined in your config, not the upstream raw provider model name.
- If your proxy requires authentication, use the proxy key (or `master_key`) in `OPENAI_API_KEY`.
- LiteLLM's OpenAI-compatible endpoint accepts the same request format as OpenAI, so OpenClaude works without any code changes.
- You can switch between any provider configured in LiteLLM by simply changing the `OPENAI_MODEL` value — no need to reconfigure OpenClaude.
## 5. Troubleshooting
| Issue | Likely Cause | Fix |
|-------|--------------|-----|
| 404 or Model Not Found | Model alias doesn't exist in LiteLLM config | Verify the `model_name` in `litellm_config.yaml` matches `OPENAI_MODEL` |
| Connection Refused | LiteLLM proxy isn't running | Start the proxy with `litellm --config litellm_config.yaml --port 4000` |
| Auth Failed | Missing or wrong `master_key` | Set the correct key in `OPENAI_API_KEY` |
| 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 |
| 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) |
## 6. Resources
- [LiteLLM Proxy Docs](https://docs.litellm.ai/docs/proxy/quick_start)
- [LiteLLM Provider List](https://docs.litellm.ai/docs/providers)
- [LiteLLM OpenAI-Compatible Endpoints](https://docs.litellm.ai/docs/proxy/openai_compatible_proxy)

View File

@@ -1,6 +1,6 @@
{
"name": "@gitlawb/openclaude",
"version": "0.1.8",
"version": "0.1.7",
"description": "Claude Code opened to any LLM — OpenAI, Gemini, DeepSeek, Ollama, and 200+ models",
"type": "module",
"bin": {
@@ -30,13 +30,7 @@
"profile:code": "bun run profile:init -- --provider ollama --model qwen2.5-coder:7b",
"dev:fast": "bun run profile:fast && bun run dev:ollama:fast",
"dev:code": "bun run profile:code && bun run dev:profile",
"dev:grpc": "bun run scripts/start-grpc.ts",
"dev:grpc:cli": "bun run scripts/grpc-cli.ts",
"start": "node dist/cli.mjs",
"test": "bun test",
"test:coverage": "bun test --coverage --coverage-reporter=lcov --coverage-dir=coverage --max-concurrency=1 && bun run scripts/render-coverage-heatmap.ts",
"test:coverage:ui": "bun run scripts/render-coverage-heatmap.ts",
"security:pr-scan": "bun run scripts/pr-intent-scan.ts",
"test:provider-recommendation": "bun test src/utils/providerRecommendation.test.ts src/utils/providerProfile.test.ts",
"typecheck": "tsc --noEmit",
"smoke": "bun run build && node dist/cli.mjs --version",
@@ -59,8 +53,6 @@
"@anthropic-ai/vertex-sdk": "0.14.4",
"@commander-js/extra-typings": "12.1.0",
"@growthbook/growthbook": "1.6.5",
"@grpc/grpc-js": "^1.14.3",
"@grpc/proto-loader": "^0.8.0",
"@mendable/firecrawl-js": "4.18.1",
"@modelcontextprotocol/sdk": "1.29.0",
"@opentelemetry/api": "1.9.1",
@@ -84,7 +76,6 @@
"cli-highlight": "2.1.11",
"code-excerpt": "4.0.0",
"commander": "12.1.0",
"cross-spawn": "7.0.6",
"diff": "8.0.3",
"duck-duck-scrape": "^2.2.7",
"emoji-regex": "10.6.0",
@@ -99,7 +90,7 @@
"ignore": "7.0.5",
"indent-string": "5.0.0",
"jsonc-parser": "3.3.1",
"lodash-es": "4.18.1",
"lodash-es": "4.18.0",
"lru-cache": "11.2.7",
"marked": "15.0.12",
"p-map": "7.0.4",
@@ -132,7 +123,6 @@
"@types/bun": "1.3.11",
"@types/node": "25.5.0",
"@types/react": "19.2.14",
"tsx": "^4.21.0",
"typescript": "5.9.3"
},
"engines": {
@@ -155,8 +145,5 @@
"license": "SEE LICENSE FILE",
"publishConfig": {
"access": "public"
},
"overrides": {
"lodash-es": "4.18.1"
}
}

View File

@@ -1 +0,0 @@
# Python helper package for standalone provider-side utilities.

View File

@@ -1 +0,0 @@
# Pytest package marker for the Python helper test suite.

View File

@@ -1,5 +0,0 @@
from pathlib import Path
import sys
# Make the sibling `python/` helper modules importable from this test package.
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

View File

@@ -3,7 +3,7 @@
* distributable JS file using Bun's bundler.
*
* Handles:
* - bun:bundle feature() flags for the open build
* - bun:bundle feature() flags → all false (disables internal-only features)
* - MACRO.* globals → inlined version/build-time constants
* - src/ path aliases
*/
@@ -14,9 +14,8 @@ import { noTelemetryPlugin } from './no-telemetry-plugin'
const pkg = JSON.parse(readFileSync('./package.json', 'utf-8'))
const version = pkg.version
// Feature flags for the open build.
// Most Anthropic-internal features stay off; open-build features can be
// selectively enabled here when their full source exists in the mirror.
// Feature flags — all disabled for the open build.
// These gate Anthropic-internal features (voice, proactive, kairos, etc.)
const featureFlags: Record<string, boolean> = {
VOICE_MODE: false,
PROACTIVE: false,
@@ -38,7 +37,7 @@ const featureFlags: Record<string, boolean> = {
TRANSCRIPT_CLASSIFIER: false,
WEB_BROWSER_TOOL: false,
MESSAGE_ACTIONS: false,
BUDDY: true,
BUDDY: false,
CHICAGO_MCP: false,
COWORKER_TYPE_TELEMETRY: false,
}
@@ -111,7 +110,7 @@ export async function handleBgFlag() { throw new Error("Background sessions are
build.onLoad(
{ filter: /.*/, namespace: 'bun-bundle-shim' },
() => ({
contents: `const featureFlags = ${JSON.stringify(featureFlags)};\nexport function feature(name) { return featureFlags[name] ?? false; }`,
contents: `export function feature(name) { return false; }`,
loader: 'js',
}),
)
@@ -251,103 +250,6 @@ export const SeverityNumber = {};
loader: 'js',
}),
)
// Pre-scan: find all missing modules that need stubbing
// (Bun's onResolve corrupts module graph even when returning null,
// so we use exact-match resolvers instead of catch-all patterns)
const fs = require('fs')
const pathMod = require('path')
const srcDir = pathMod.resolve(__dirname, '..', 'src')
const missingModules = new Set<string>()
const missingModuleExports = new Map<string, Set<string>>()
// Known missing external packages
for (const pkg of [
'@ant/computer-use-mcp',
'@ant/computer-use-mcp/sentinelApps',
'@ant/computer-use-mcp/types',
'@ant/computer-use-swift',
'@ant/computer-use-input',
]) {
missingModules.add(pkg)
}
// Scan source to find imports that can't resolve
function scanForMissingImports() {
function walk(dir: string) {
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
const full = pathMod.join(dir, ent.name)
if (ent.isDirectory()) { walk(full); continue }
if (!/\.(ts|tsx)$/.test(ent.name)) continue
const code: string = fs.readFileSync(full, 'utf-8')
// Collect all imports
for (const m of code.matchAll(/import\s+(?:\{([^}]*)\}|(\w+))?\s*(?:,\s*\{([^}]*)\})?\s*from\s+['"](.*?)['"]/g)) {
const specifier = m[4]
const namedPart = m[1] || m[3] || ''
const names = namedPart.split(',')
.map((s: string) => s.trim().replace(/^type\s+/, ''))
.filter((s: string) => s && !s.startsWith('type '))
// Check src/tasks/ non-relative imports
if (specifier.startsWith('src/tasks/')) {
const resolved = pathMod.resolve(__dirname, '..', specifier)
const candidates = [
resolved,
`${resolved}.ts`, `${resolved}.tsx`,
resolved.replace(/\.js$/, '.ts'), resolved.replace(/\.js$/, '.tsx'),
pathMod.join(resolved, 'index.ts'), pathMod.join(resolved, 'index.tsx'),
]
if (!candidates.some((c: string) => fs.existsSync(c))) {
missingModules.add(specifier)
}
}
// Check relative .js imports
else if (specifier.endsWith('.js') && (specifier.startsWith('./') || specifier.startsWith('../'))) {
const dir2 = pathMod.dirname(full)
const resolved = pathMod.resolve(dir2, specifier)
const tsVariant = resolved.replace(/\.js$/, '.ts')
const tsxVariant = resolved.replace(/\.js$/, '.tsx')
if (!fs.existsSync(resolved) && !fs.existsSync(tsVariant) && !fs.existsSync(tsxVariant)) {
missingModules.add(specifier)
}
}
// Track named exports for missing modules
if (names.length > 0) {
if (!missingModuleExports.has(specifier)) missingModuleExports.set(specifier, new Set())
for (const n of names) missingModuleExports.get(specifier)!.add(n)
}
}
}
}
walk(srcDir)
}
scanForMissingImports()
// Register exact-match resolvers for each missing module
for (const mod of missingModules) {
const escaped = mod.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
build.onResolve({ filter: new RegExp(`^${escaped}$`) }, () => ({
path: mod,
namespace: 'missing-module-stub',
}))
}
build.onLoad(
{ filter: /.*/, namespace: 'missing-module-stub' },
(args) => {
const names = missingModuleExports.get(args.path) ?? new Set()
const exports = [...names].map(n => `export const ${n} = noop;`).join('\n')
return {
contents: `
const noop = () => null;
export default noop;
${exports}
`,
loader: 'js',
}
},
)
},
},
],

View File

@@ -1,121 +0,0 @@
import * as grpc from '@grpc/grpc-js'
import * as protoLoader from '@grpc/proto-loader'
import path from 'path'
import * as readline from 'readline'
const PROTO_PATH = path.resolve(import.meta.dirname, '../src/proto/openclaude.proto')
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
})
const protoDescriptor = grpc.loadPackageDefinition(packageDefinition) as any
const openclaudeProto = protoDescriptor.openclaude.v1
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
function askQuestion(query: string): Promise<string> {
return new Promise(resolve => {
rl.question(query, resolve)
})
}
async function main() {
const host = process.env.GRPC_HOST || 'localhost'
const port = process.env.GRPC_PORT || '50051'
const client = new openclaudeProto.AgentService(
`${host}:${port}`,
grpc.credentials.createInsecure()
)
let call: grpc.ClientDuplexStream<any, any> | null = null
const startStream = () => {
call = client.Chat()
let textStreamed = false
call.on('data', async (serverMessage: any) => {
if (serverMessage.text_chunk) {
process.stdout.write(serverMessage.text_chunk.text)
textStreamed = true
} else if (serverMessage.tool_start) {
console.log(`\n\x1b[36m[Tool Call]\x1b[0m \x1b[1m${serverMessage.tool_start.tool_name}\x1b[0m`)
console.log(`\x1b[90m${serverMessage.tool_start.arguments_json}\x1b[0m\n`)
} else if (serverMessage.tool_result) {
console.log(`\n\x1b[32m[Tool Result]\x1b[0m \x1b[1m${serverMessage.tool_result.tool_name}\x1b[0m`)
const out = serverMessage.tool_result.output
if (out.length > 500) {
console.log(`\x1b[90m${out.substring(0, 500)}...\n(Output truncated, total length: ${out.length})\x1b[0m`)
} else {
console.log(`\x1b[90m${out}\x1b[0m`)
}
} else if (serverMessage.action_required) {
const action = serverMessage.action_required
console.log(`\n\x1b[33m[Action Required]\x1b[0m`)
const reply = await askQuestion(`\x1b[1m${action.question}\x1b[0m (y/n) > `)
call?.write({
input: {
prompt_id: action.prompt_id,
reply: reply.trim()
}
})
} else if (serverMessage.done) {
if (!textStreamed && serverMessage.done.full_text) {
process.stdout.write(serverMessage.done.full_text)
}
textStreamed = false
console.log('\n\x1b[32m[Generation Complete]\x1b[0m')
promptUser()
} else if (serverMessage.error) {
console.error(`\n\x1b[31m[Server Error]\x1b[0m ${serverMessage.error.message}`)
promptUser()
}
})
call.on('end', () => {
console.log('\n\x1b[90m[Stream closed by server]\x1b[0m')
// Don't prompt user here, let 'done' or 'error' handlers do it
})
call.on('error', (err: Error) => {
console.error('\n\x1b[31m[Stream Error]\x1b[0m', err.message)
promptUser()
})
}
const promptUser = async () => {
const message = await askQuestion('\n\x1b[35m> \x1b[0m')
if (message.trim().toLowerCase() === '/exit' || message.trim().toLowerCase() === '/quit') {
console.log('Bye!')
rl.close()
process.exit(0)
}
if (!call || call.destroyed) {
startStream()
}
call!.write({
request: {
session_id: 'cli-session-1',
message: message,
working_directory: process.cwd()
}
})
}
console.log('\x1b[32mOpenClaude gRPC CLI\x1b[0m')
console.log('\x1b[90mType /exit to quit.\x1b[0m')
promptUser()
}
main()

View File

@@ -203,60 +203,6 @@ export async function submitTranscriptShare() { return { success: false }; }
'services/internalLogging': `
export async function logPermissionContextForAnts() {}
export const getContainerId = async () => null;
`,
// ─── Deleted Anthropic-internal modules ───────────────────────────────
'services/api/dumpPrompts': `
export function createDumpPromptsFetch() { return undefined; }
export function getDumpPromptsPath() { return ''; }
export function getLastApiRequests() { return []; }
export function clearApiRequestCache() {}
export function clearDumpState() {}
export function clearAllDumpState() {}
export function addApiRequestToCache() {}
`,
'utils/undercover': `
export function isUndercover() { return false; }
export function getUndercoverInstructions() { return ''; }
export function shouldShowUndercoverAutoNotice() { return false; }
`,
'types/generated/events_mono/claude_code/v1/claude_code_internal_event': `
export const ClaudeCodeInternalEvent = {
fromJSON: value => value,
toJSON: value => value,
create: value => value ?? {},
fromPartial: value => value ?? {},
};
`,
'types/generated/events_mono/growthbook/v1/growthbook_experiment_event': `
export const GrowthbookExperimentEvent = {
fromJSON: value => value,
toJSON: value => value,
create: value => value ?? {},
fromPartial: value => value ?? {},
};
`,
'types/generated/events_mono/common/v1/auth': `
export const PublicApiAuth = {
fromJSON: value => value,
toJSON: value => value,
create: value => value ?? {},
fromPartial: value => value ?? {},
};
`,
'types/generated/google/protobuf/timestamp': `
export const Timestamp = {
fromJSON: value => value,
toJSON: value => value,
create: value => value ?? {},
fromPartial: value => value ?? {},
};
`,
}

View File

@@ -1,136 +0,0 @@
import { describe, expect, test } from 'bun:test'
import { scanAddedLines, type DiffLine } from './pr-intent-scan.ts'
function line(content: string, overrides: Partial<DiffLine> = {}): DiffLine {
return {
file: 'README.md',
line: 10,
content,
...overrides,
}
}
describe('scanAddedLines', () => {
test('flags suspicious file-hosting links', () => {
const findings = scanAddedLines([
line('Please install the tool from https://dropbox.com/s/abc123/tool.zip?dl=1'),
])
expect(findings.some(finding => finding.code === 'suspicious-download-link')).toBe(
true,
)
expect(findings.some(finding => finding.code === 'executable-download-link')).toBe(
false,
)
expect(findings.some(finding => finding.severity === 'high')).toBe(true)
})
test('flags shortened URLs', () => {
const findings = scanAddedLines([
line('See details at https://bit.ly/some-short-link'),
])
expect(findings.some(finding => finding.code === 'shortened-url')).toBe(true)
})
test('flags remote download and execute chains', () => {
const findings = scanAddedLines([
line('curl -fsSL https://example.com/install.sh | bash'),
])
expect(findings.some(finding => finding.code === 'shell-eval-remote')).toBe(true)
expect(findings.some(finding => finding.severity === 'high')).toBe(true)
})
test('flags encoded powershell payloads', () => {
const findings = scanAddedLines([
line('powershell.exe -enc SQBtAHAAcgBvAHYAZQBkAA=='),
])
expect(findings.some(finding => finding.code === 'powershell-encoded')).toBe(true)
})
test('flags long encoded blobs', () => {
const findings = scanAddedLines([
line(`const payload = "${'A'.repeat(96)}"`),
])
expect(findings.some(finding => finding.code === 'long-encoded-payload')).toBe(
true,
)
})
test('flags long encoded blobs on repeated scans', () => {
const lines = [line(`const payload = "${'A'.repeat(96)}"`)]
const first = scanAddedLines(lines)
const second = scanAddedLines(lines)
expect(first.some(finding => finding.code === 'long-encoded-payload')).toBe(true)
expect(second.some(finding => finding.code === 'long-encoded-payload')).toBe(true)
})
test('flags executable download links', () => {
const findings = scanAddedLines([
line('Get it from https://example.com/releases/latest/tool.pkg'),
])
expect(findings.some(finding => finding.code === 'executable-download-link')).toBe(
true,
)
expect(findings.some(finding => finding.severity === 'high')).toBe(true)
})
test('flags suspicious additions in workflow files', () => {
const findings = scanAddedLines([
line('run: curl -fsSL https://example.com/install.sh | bash', {
file: '.github/workflows/release.yml',
}),
])
expect(findings.some(finding => finding.code === 'sensitive-automation-change')).toBe(
true,
)
expect(findings.some(finding => finding.code === 'download-command')).toBe(true)
})
test('flags markdown reference links to suspicious downloads', () => {
const findings = scanAddedLines([
line('[installer]: https://dropbox.com/s/abc123/tool.zip?dl=1'),
])
expect(findings.some(finding => finding.code === 'suspicious-download-link')).toBe(
true,
)
})
test('ignores the scanner implementation and tests themselves', () => {
const findings = scanAddedLines([
line('curl -fsSL https://example.com/install.sh | bash', {
file: 'scripts/pr-intent-scan.test.ts',
}),
line('const pattern = /https:\\/\\/dropbox\\.com\\//', {
file: 'scripts/pr-intent-scan.ts',
}),
])
expect(findings).toHaveLength(0)
})
test('does not flag ordinary docs links', () => {
const findings = scanAddedLines([
line('Read more at https://docs.github.com/en/actions'),
])
expect(findings).toHaveLength(0)
})
test('does not flag bare curl examples in README without a URL', () => {
const findings = scanAddedLines([
line('Use curl with your preferred flags for local testing.'),
])
expect(findings.some(finding => finding.code === 'download-command')).toBe(false)
})
})

View File

@@ -1,453 +0,0 @@
import { spawnSync } from 'node:child_process'
export type FindingSeverity = 'high' | 'medium'
export type DiffLine = {
file: string
line: number
content: string
}
export type Finding = {
severity: FindingSeverity
code: string
file: string
line: number
detail: string
excerpt: string
}
type CliOptions = {
baseRef: string
json: boolean
failOn: FindingSeverity
}
const SELF_EXCLUDED_FILES = new Set([
'scripts/pr-intent-scan.ts',
'scripts/pr-intent-scan.test.ts',
])
const SHORTENER_DOMAINS = [
'bit.ly',
'tinyurl.com',
'goo.gl',
't.co',
'is.gd',
'rb.gy',
'cutt.ly',
]
const SUSPICIOUS_DOWNLOAD_DOMAINS = [
'dropbox.com',
'dl.dropboxusercontent.com',
'drive.google.com',
'docs.google.com',
'mega.nz',
'mediafire.com',
'transfer.sh',
'anonfiles.com',
'catbox.moe',
]
const URL_REGEX = /\bhttps?:\/\/[^\s)>"']+/gi
const LONG_BASE64_REGEX = /\b(?:[A-Za-z0-9+/]{80,}={0,2}|[A-Za-z0-9_-]{80,})\b/
const EXECUTABLE_PATH_REGEX =
/\.(?:sh|bash|zsh|ps1|exe|msi|pkg|deb|rpm|zip|tar|tgz|gz|xz|dmg|appimage)(?:$|[?#])/i
const SENSITIVE_PATH_REGEX =
/^(?:\.github\/workflows\/|scripts\/|bin\/|install(?:\/|\.|$)|.*(?:Dockerfile|docker-compose|compose\.ya?ml)$)/i
function parseOptions(argv: string[]): CliOptions {
const options: CliOptions = {
baseRef: 'origin/main',
json: false,
failOn: 'high',
}
for (let index = 0; index < argv.length; index++) {
const arg = argv[index]
if (arg === '--json') {
options.json = true
continue
}
if (arg === '--base') {
const next = argv[index + 1]
if (next && !next.startsWith('--')) {
options.baseRef = next
index++
}
continue
}
if (arg === '--fail-on') {
const next = argv[index + 1]
if (next === 'high' || next === 'medium') {
options.failOn = next
index++
}
}
}
return options
}
function trimExcerpt(content: string): string {
const compact = content.trim().replace(/\s+/g, ' ')
return compact.length > 140 ? `${compact.slice(0, 137)}...` : compact
}
function uniqueFindings(findings: Finding[]): Finding[] {
const seen = new Set<string>()
return findings.filter(finding => {
const key = `${finding.code}:${finding.file}:${finding.line}:${finding.detail}`
if (seen.has(key)) {
return false
}
seen.add(key)
return true
})
}
function parseAddedLines(diffText: string): DiffLine[] {
const lines = diffText.split('\n')
const added: DiffLine[] = []
let currentFile: string | null = null
let currentLine = 0
for (const rawLine of lines) {
if (rawLine.startsWith('+++ b/')) {
currentFile = rawLine.slice('+++ b/'.length)
continue
}
if (rawLine.startsWith('@@')) {
const match = /\+(\d+)(?:,(\d+))?/.exec(rawLine)
if (match) {
currentLine = Number(match[1])
}
continue
}
if (!currentFile) {
continue
}
if (rawLine.startsWith('+') && !rawLine.startsWith('+++')) {
added.push({
file: currentFile,
line: currentLine,
content: rawLine.slice(1),
})
currentLine += 1
continue
}
if (rawLine.startsWith('-') && !rawLine.startsWith('---')) {
continue
}
if (!rawLine.startsWith('\\')) {
currentLine += 1
}
}
return added
}
function tryParseUrl(value: string): URL | null {
try {
return new URL(value)
} catch {
return null
}
}
function hostMatches(hostname: string, domain: string): boolean {
return hostname === domain || hostname.endsWith(`.${domain}`)
}
function hasSuspiciousDownloadIndicators(url: URL): boolean {
const combined = `${url.pathname}${url.search}`.toLowerCase()
return (
combined.includes('dl=1') ||
combined.includes('raw=1') ||
combined.includes('export=download') ||
combined.includes('/download') ||
combined.includes('/uc?export=download')
)
}
function findUrlFindings(line: DiffLine): Finding[] {
const findings: Finding[] = []
const matches = line.content.match(URL_REGEX) ?? []
for (const match of matches) {
const parsed = tryParseUrl(match)
if (!parsed) continue
const hostname = parsed.hostname.toLowerCase()
for (const domain of SHORTENER_DOMAINS) {
if (hostMatches(hostname, domain)) {
findings.push({
severity: 'medium',
code: 'shortened-url',
file: line.file,
line: line.line,
detail: `Added shortened URL: ${hostname}`,
excerpt: trimExcerpt(line.content),
})
}
}
const isSuspiciousHost = SUSPICIOUS_DOWNLOAD_DOMAINS.some(domain =>
hostMatches(hostname, domain),
)
const isExecutableDownload = EXECUTABLE_PATH_REGEX.test(
`${parsed.pathname}${parsed.search}`,
)
if (isSuspiciousHost) {
findings.push({
severity:
hasSuspiciousDownloadIndicators(parsed) || isExecutableDownload
? 'high'
: 'medium',
code: 'suspicious-download-link',
file: line.file,
line: line.line,
detail: `Added external file-hosting link: ${hostname}`,
excerpt: trimExcerpt(line.content),
})
} else if (isExecutableDownload) {
findings.push({
severity: 'high',
code: 'executable-download-link',
file: line.file,
line: line.line,
detail: `Added direct link to executable or archive payload: ${hostname}`,
excerpt: trimExcerpt(line.content),
})
}
}
return findings
}
function findSensitivePathFindings(line: DiffLine): Finding[] {
if (!SENSITIVE_PATH_REGEX.test(line.file)) {
return []
}
const lower = line.content.toLowerCase()
if (
/\b(curl|wget|invoke-webrequest|iwr|powershell|bash|sh|chmod\s+\+x)\b/i.test(
line.content,
) ||
URL_REGEX.test(line.content) ||
lower.includes('download')
) {
return [
{
severity: 'medium',
code: 'sensitive-automation-change',
file: line.file,
line: line.line,
detail:
'Added network, execution, or download-related content in a sensitive automation file',
excerpt: trimExcerpt(line.content),
},
]
}
return []
}
function findCommandFindings(line: DiffLine): Finding[] {
const findings: Finding[] = []
const lower = line.content.toLowerCase()
const highPatterns: Array<[string, RegExp, string]> = [
[
'download-exec-chain',
/\b(curl|wget|invoke-webrequest|iwr)\b.*(\|\s*(sh|bash|zsh)|;\s*chmod\s+\+x|&&\s*\.\.?\/|>\s*\/tmp\/)/i,
'Added remote download followed by execution or staging',
],
[
'powershell-encoded',
/\bpowershell(?:\.exe)?\b.*(?:-enc|-encodedcommand)\b/i,
'Added encoded PowerShell invocation',
],
[
'shell-eval-remote',
/\b(curl|wget)\b.*\|\s*(sh|bash|zsh)\b/i,
'Added shell pipe from remote content into interpreter',
],
[
'binary-lolbin',
/\b(mshta|rundll32|regsvr32|certutil)\b/i,
'Added living-off-the-land binary often used for payload staging',
],
[
'invoke-expression',
/\b(iex|invoke-expression)\b/i,
'Added PowerShell expression execution',
],
]
const mediumPatterns: Array<[string, RegExp, string]> = [
[
'download-command',
/\b(curl|wget|invoke-webrequest|iwr)\b.*https?:\/\//i,
'Added command that downloads remote content',
],
[
'archive-extract-exec',
/\b(unzip|tar|7z)\b.*(&&|;).*\b(chmod|node|python|bash|sh)\b/i,
'Added archive extraction followed by execution',
],
[
'base64-decode',
/\b(base64\s+-d|openssl\s+base64\s+-d|python .*b64decode)\b/i,
'Added explicit payload decode step',
],
]
for (const [code, pattern, detail] of highPatterns) {
if (pattern.test(line.content)) {
findings.push({
severity: 'high',
code,
file: line.file,
line: line.line,
detail,
excerpt: trimExcerpt(line.content),
})
}
}
for (const [code, pattern, detail] of mediumPatterns) {
if (code === 'download-command' && !SENSITIVE_PATH_REGEX.test(line.file)) {
continue
}
if (pattern.test(line.content)) {
findings.push({
severity: 'medium',
code,
file: line.file,
line: line.line,
detail,
excerpt: trimExcerpt(line.content),
})
}
}
if (LONG_BASE64_REGEX.test(line.content) && !lower.includes('sha256') && !lower.includes('sha512')) {
findings.push({
severity: 'medium',
code: 'long-encoded-payload',
file: line.file,
line: line.line,
detail: 'Added long encoded blob or token-like payload',
excerpt: trimExcerpt(line.content),
})
}
return findings
}
export function scanAddedLines(lines: DiffLine[]): Finding[] {
const findings = lines
.filter(line => !SELF_EXCLUDED_FILES.has(line.file))
.flatMap(line => [
...findUrlFindings(line),
...findCommandFindings(line),
...findSensitivePathFindings(line),
])
return uniqueFindings(findings)
}
export function getGitDiff(baseRef: string): string {
const mergeBase = spawnSync('git', ['merge-base', baseRef, 'HEAD'], {
encoding: 'utf8',
})
if (mergeBase.status !== 0) {
throw new Error(
`Could not determine merge-base with ${baseRef}: ${mergeBase.stderr.trim() || mergeBase.stdout.trim()}`,
)
}
const base = mergeBase.stdout.trim()
const diff = spawnSync(
'git',
['diff', '--unified=0', '--no-ext-diff', `${base}...HEAD`],
{ encoding: 'utf8' },
)
if (diff.status !== 0) {
throw new Error(`git diff failed: ${diff.stderr.trim() || diff.stdout.trim()}`)
}
return diff.stdout
}
function shouldFail(findings: Finding[], failOn: FindingSeverity): boolean {
if (failOn === 'medium') {
return findings.length > 0
}
return findings.some(finding => finding.severity === 'high')
}
function renderText(findings: Finding[]): string {
if (findings.length === 0) {
return 'PR intent scan: no suspicious additions found.'
}
const high = findings.filter(f => f.severity === 'high')
const medium = findings.filter(f => f.severity === 'medium')
const lines = [
`PR intent scan: ${findings.length} finding(s)`,
`- high: ${high.length}`,
`- medium: ${medium.length}`,
'',
]
for (const finding of findings) {
lines.push(
`[${finding.severity.toUpperCase()}] ${finding.file}:${finding.line} ${finding.detail}`,
)
lines.push(` ${finding.excerpt}`)
}
return lines.join('\n')
}
export function run(options: CliOptions): number {
const diff = getGitDiff(options.baseRef)
const addedLines = parseAddedLines(diff)
const findings = scanAddedLines(addedLines)
if (options.json) {
process.stdout.write(
`${JSON.stringify(
{
baseRef: options.baseRef,
addedLines: addedLines.length,
findings,
},
null,
2,
)}\n`,
)
} else {
process.stdout.write(`${renderText(findings)}\n`)
}
return shouldFail(findings, options.failOn) ? 1 : 0
}
if (import.meta.main) {
const options = parseOptions(process.argv.slice(2))
process.exitCode = run(options)
}

View File

@@ -1,393 +0,0 @@
import { mkdir, readFile, writeFile } from 'fs/promises'
import { dirname, resolve } from 'path'
type FileCoverage = {
path: string
found: number
hit: number
chunks: number[]
}
type DirectoryCoverage = {
path: string
found: number
hit: number
}
const LCOV_PATH = resolve(process.cwd(), 'coverage/lcov.info')
const HTML_PATH = resolve(process.cwd(), 'coverage/index.html')
const CHUNK_COUNT = 20
function escapeHtml(value: string): string {
return value
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
}
function bucketColor(ratio: number): string {
if (ratio >= 0.9) return '#166534'
if (ratio >= 0.75) return '#15803d'
if (ratio >= 0.5) return '#65a30d'
if (ratio > 0) return '#a3a3a3'
return '#262626'
}
function coverageLabel(ratio: number): string {
return `${Math.round(ratio * 100)}%`
}
function coverageRatio(found: number, hit: number): number {
return found === 0 ? 0 : hit / found
}
function bucketGlyph(ratio: number): string {
if (ratio >= 0.9) return '█'
if (ratio >= 0.75) return '▓'
if (ratio >= 0.5) return '▒'
if (ratio > 0) return '░'
return '·'
}
function terminalBar(chunks: number[]): string {
return chunks.map(bucketGlyph).join('')
}
function summarizeDirectories(files: FileCoverage[]): DirectoryCoverage[] {
const dirs = new Map<string, DirectoryCoverage>()
for (const file of files) {
const dir =
file.path.includes('/') ? file.path.slice(0, file.path.lastIndexOf('/')) : '.'
const current = dirs.get(dir) ?? { path: dir, found: 0, hit: 0 }
current.found += file.found
current.hit += file.hit
dirs.set(dir, current)
}
return [...dirs.values()].sort((a, b) => {
const left = coverageRatio(a.found, a.hit)
const right = coverageRatio(b.found, b.hit)
if (right !== left) return right - left
return b.found - a.found
})
}
function buildTerminalReport(files: FileCoverage[]): string {
const totalFound = files.reduce((sum, file) => sum + file.found, 0)
const totalHit = files.reduce((sum, file) => sum + file.hit, 0)
const totalRatio = coverageRatio(totalFound, totalHit)
const overallChunks = new Array(CHUNK_COUNT).fill(totalRatio)
const topDirectories = summarizeDirectories(files)
.filter(dir => dir.found > 0)
.slice(0, 8)
const lowestFiles = [...files]
.filter(file => file.found >= 20)
.sort((a, b) => {
const left = coverageRatio(a.found, a.hit)
const right = coverageRatio(b.found, b.hit)
if (left !== right) return left - right
return b.found - a.found
})
.slice(0, 10)
const lines = [
'',
'Coverage Activity',
`${terminalBar(overallChunks)} ${coverageLabel(totalRatio)} ${totalHit}/${totalFound} lines ${files.length} files`,
'',
'Top Directories',
]
for (const dir of topDirectories) {
const ratio = coverageRatio(dir.found, dir.hit)
lines.push(
`${terminalBar(new Array(12).fill(ratio))} ${coverageLabel(ratio).padStart(4)} ${String(dir.hit).padStart(5)}/${String(dir.found).padEnd(5)} ${dir.path}`,
)
}
lines.push('', 'Lowest Coverage Files')
for (const file of lowestFiles) {
const ratio = coverageRatio(file.found, file.hit)
lines.push(
`${terminalBar(file.chunks).padEnd(CHUNK_COUNT)} ${coverageLabel(ratio).padStart(4)} ${String(file.hit).padStart(5)}/${String(file.found).padEnd(5)} ${file.path}`,
)
}
lines.push('', `HTML report: ${HTML_PATH}`)
return lines.join('\n')
}
function parseLcov(content: string): FileCoverage[] {
const files: FileCoverage[] = []
const sections = content.split('end_of_record')
for (const rawSection of sections) {
const section = rawSection.trim()
if (!section) continue
const lines = section.split('\n')
let filePath = ''
const lineHits = new Map<number, number>()
for (const line of lines) {
if (line.startsWith('SF:')) {
filePath = line.slice(3).trim()
} else if (line.startsWith('DA:')) {
const [lineNumberText, hitText] = line.slice(3).split(',')
const lineNumber = Number(lineNumberText)
const hits = Number(hitText)
if (Number.isFinite(lineNumber) && Number.isFinite(hits)) {
lineHits.set(lineNumber, hits)
}
}
}
if (!filePath || lineHits.size === 0) continue
const ordered = [...lineHits.entries()].sort((a, b) => a[0] - b[0])
const found = ordered.length
const hit = ordered.filter(([, hits]) => hits > 0).length
const chunkSize = Math.max(1, Math.ceil(found / CHUNK_COUNT))
const chunks: number[] = []
for (let index = 0; index < found; index += chunkSize) {
const slice = ordered.slice(index, index + chunkSize)
const covered = slice.filter(([, hits]) => hits > 0).length
chunks.push(slice.length === 0 ? 0 : covered / slice.length)
}
while (chunks.length < CHUNK_COUNT) {
chunks.push(0)
}
files.push({
path: filePath,
found,
hit,
chunks: chunks.slice(0, CHUNK_COUNT),
})
}
return files.sort((a, b) => {
const left = a.found === 0 ? 0 : a.hit / a.found
const right = b.found === 0 ? 0 : b.hit / b.found
if (right !== left) return right - left
return a.path.localeCompare(b.path)
})
}
function buildHtml(files: FileCoverage[]): string {
const totalFound = files.reduce((sum, file) => sum + file.found, 0)
const totalHit = files.reduce((sum, file) => sum + file.hit, 0)
const totalRatio = totalFound === 0 ? 0 : totalHit / totalFound
const cards = [
['Files', String(files.length)],
['Covered Lines', `${totalHit}/${totalFound}`],
['Line Coverage', coverageLabel(totalRatio)],
]
const rows = files
.map(file => {
const ratio = file.found === 0 ? 0 : file.hit / file.found
const squares = file.chunks
.map(
(chunk, index) =>
`<span class="cell" title="Chunk ${index + 1}: ${coverageLabel(chunk)}" style="background:${bucketColor(chunk)}"></span>`,
)
.join('')
return `
<tr>
<td class="file">${escapeHtml(file.path)}</td>
<td class="percent">${coverageLabel(ratio)}</td>
<td class="lines">${file.hit}/${file.found}</td>
<td class="heatmap">${squares}</td>
</tr>
`
})
.join('')
const summary = cards
.map(
([label, value]) => `
<div class="card">
<div class="card-label">${escapeHtml(label)}</div>
<div class="card-value">${escapeHtml(value)}</div>
</div>
`,
)
.join('')
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>OpenClaude Coverage</title>
<style>
:root {
color-scheme: dark;
--bg: #09090b;
--panel: #111113;
--panel-2: #18181b;
--border: #27272a;
--text: #fafafa;
--muted: #a1a1aa;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: linear-gradient(180deg, #09090b 0%, #0f0f12 100%);
color: var(--text);
font: 14px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace;
}
main {
max-width: 1440px;
margin: 0 auto;
padding: 32px 24px 48px;
}
h1 {
margin: 0 0 8px;
font-size: 32px;
letter-spacing: -0.04em;
}
p {
margin: 0;
color: var(--muted);
}
.summary {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
margin: 24px 0;
}
.card {
background: rgba(24, 24, 27, 0.92);
border: 1px solid var(--border);
border-radius: 16px;
padding: 16px 18px;
}
.card-label {
color: var(--muted);
margin-bottom: 8px;
}
.card-value {
font-size: 28px;
font-weight: 700;
}
.table-wrap {
background: rgba(17, 17, 19, 0.94);
border: 1px solid var(--border);
border-radius: 18px;
overflow: hidden;
}
table {
width: 100%;
border-collapse: collapse;
}
thead th {
text-align: left;
color: var(--muted);
font-weight: 500;
background: rgba(24, 24, 27, 0.95);
border-bottom: 1px solid var(--border);
}
th, td {
padding: 12px 16px;
vertical-align: middle;
}
tbody tr + tr td {
border-top: 1px solid rgba(39, 39, 42, 0.65);
}
.file {
width: 48%;
word-break: break-all;
}
.percent, .lines {
white-space: nowrap;
}
.heatmap {
width: 32%;
min-width: 280px;
}
.cell {
display: inline-block;
width: 12px;
height: 12px;
margin-right: 4px;
border-radius: 3px;
border: 1px solid rgba(255,255,255,0.05);
}
.legend {
display: flex;
align-items: center;
gap: 10px;
margin-top: 16px;
color: var(--muted);
}
.legend-scale {
display: flex;
gap: 4px;
}
@media (max-width: 900px) {
.summary {
grid-template-columns: 1fr;
}
.heatmap {
min-width: 220px;
}
th, td {
padding: 10px 12px;
}
}
</style>
</head>
<body>
<main>
<h1>Coverage Activity</h1>
<p>Git-style heatmap generated from coverage/lcov.info</p>
<section class="summary">${summary}</section>
<section class="table-wrap">
<table>
<thead>
<tr>
<th>File</th>
<th>Coverage</th>
<th>Lines</th>
<th>Activity</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
</section>
<div class="legend">
<span>Less</span>
<div class="legend-scale">
<span class="cell" style="background:#262626"></span>
<span class="cell" style="background:#a3a3a3"></span>
<span class="cell" style="background:#65a30d"></span>
<span class="cell" style="background:#15803d"></span>
<span class="cell" style="background:#166534"></span>
</div>
<span>More</span>
</div>
</main>
</body>
</html>`
}
async function main() {
const content = await readFile(LCOV_PATH, 'utf8')
const files = parseLcov(content)
const html = buildHtml(files)
await mkdir(dirname(HTML_PATH), { recursive: true })
await writeFile(HTML_PATH, html, 'utf8')
console.log(buildTerminalReport(files))
console.log(`coverage heatmap written to ${HTML_PATH}`)
}
await main()

View File

@@ -1,50 +0,0 @@
import { GrpcServer } from '../src/grpc/server.ts'
import { init } from '../src/entrypoints/init.ts'
// Polyfill MACRO which is normally injected by the bundler
Object.assign(globalThis, {
MACRO: {
VERSION: '0.1.7',
DISPLAY_VERSION: '0.1.7',
PACKAGE_URL: '@gitlawb/openclaude',
}
})
async function main() {
console.log('Starting OpenClaude gRPC Server...')
await init()
// Mirror CLI bootstrap: hydrate secure tokens and resolve provider profile
const { enableConfigs } = await import('../src/utils/config.js')
enableConfigs()
const { applySafeConfigEnvironmentVariables } = await import('../src/utils/managedEnv.js')
applySafeConfigEnvironmentVariables()
const { hydrateGeminiAccessTokenFromSecureStorage } = await import('../src/utils/geminiCredentials.js')
hydrateGeminiAccessTokenFromSecureStorage()
const { hydrateGithubModelsTokenFromSecureStorage } = await import('../src/utils/githubModelsCredentials.js')
hydrateGithubModelsTokenFromSecureStorage()
const { buildStartupEnvFromProfile, applyProfileEnvToProcessEnv } = await import('../src/utils/providerProfile.js')
const { getProviderValidationError, validateProviderEnvOrExit } = await import('../src/utils/providerValidation.js')
const startupEnv = await buildStartupEnvFromProfile({ processEnv: process.env })
if (startupEnv !== process.env) {
const startupProfileError = await getProviderValidationError(startupEnv)
if (startupProfileError) {
console.warn(`Warning: ignoring saved provider profile. ${startupProfileError}`)
} else {
applyProfileEnvToProcessEnv(process.env, startupEnv)
}
}
await validateProviderEnvOrExit()
const port = process.env.GRPC_PORT ? parseInt(process.env.GRPC_PORT, 10) : 50051
const host = process.env.GRPC_HOST || 'localhost'
const server = new GrpcServer()
server.start(port, host)
}
main().catch((err) => {
console.error('Fatal error starting gRPC server:', err)
process.exit(1)
})

View File

@@ -19,10 +19,6 @@ BANNED=(
"/var/run/secrets/kubernetes"
"/proc/self/mountinfo"
"tengu_internal_record_permission_context"
"anthropic-serve"
"infra.ant.dev"
"claude-code-feedback"
"C07VBSHV7EV"
)
echo "Checking $DIST for banned patterns..."

View File

@@ -9,10 +9,6 @@ const BANNED_PATTERNS = [
'/var/run/secrets/kubernetes',
'/proc/self/mountinfo',
'tengu_internal_record_permission_context',
'anthropic-serve',
'infra.ant.dev',
'claude-code-feedback',
'C07VBSHV7EV',
] as const
if (!existsSync(DIST)) {

View File

@@ -112,7 +112,7 @@ type State = {
agentColorIndex: number
// Last API request for bug reports
lastAPIRequest: Omit<BetaMessageStreamParams, 'messages'> | null
// Messages from the last API request (internal-only; reference, not clone).
// Messages from the last API request (ant-only; reference, not clone).
// Captures the exact post-compaction, CLAUDE.md-injected message set sent
// to the API so /share's serialized_conversation.json reflects reality.
lastAPIRequestMessages: BetaMessageStreamParams['messages'] | null
@@ -185,7 +185,7 @@ type State = {
agentId: string | null
}
>
// Track slow operations for dev bar display (internal-only)
// Track slow operations for dev bar display (ant-only)
slowOperations: Array<{
operation: string
durationMs: number
@@ -1756,12 +1756,3 @@ export function setPromptId(id: string | null): void {
STATE.promptId = id
}
// Stub for feature-gated REPL bridge (not available in open build)
export function isReplBridgeActive(): boolean {
return false
}
export function getReplBridgeHandle(): null {
return null
}

View File

@@ -1,11 +1,11 @@
/**
* Shared bridge auth/URL resolution. Consolidates the internal-only
* Shared bridge auth/URL resolution. Consolidates the ant-only
* CLAUDE_BRIDGE_* dev overrides that were previously copy-pasted across
* a dozen files — inboundAttachments, BriefTool/upload, bridgeMain,
* initReplBridge, remoteBridgeCore, daemon workers, /rename,
* /remote-control.
*
* Two layers: *Override() returns the internal-only env var (or undefined);
* Two layers: *Override() returns the ant-only env var (or undefined);
* the non-Override versions fall through to the real OAuth store/config.
* Callers that compose with a different auth source (e.g. daemon workers
* using IPC auth) use the Override getters directly.

View File

@@ -174,7 +174,7 @@ export function checkBridgeMinVersion(): string | null {
/**
* Default for remoteControlAtStartup when the user hasn't explicitly set it.
* When the CCR_AUTO_CONNECT build flag is present (internal-only) and the
* When the CCR_AUTO_CONNECT build flag is present (ant-only) and the
* tengu_cobalt_harbor GrowthBook gate is on, all sessions connect to CCR by
* default — the user can still opt out by setting remoteControlAtStartup=false
* in config (explicit settings always win over this default).

View File

@@ -1520,7 +1520,7 @@ export async function runBridgeLoop(
// Skip when the loop exited fatally (env expired, auth failed, give-up) —
// resume is impossible in those cases and the message would contradict the
// error already printed.
// feature('KAIROS') gate: --session-id is internal-only; without the gate,
// feature('KAIROS') gate: --session-id is ant-only; without the gate,
// revert to the pre-PR behavior (archive + deregister on every shutdown).
if (
feature('KAIROS') &&
@@ -1888,7 +1888,7 @@ export function parseArgs(args: string[]): ParsedArgs {
async function printHelp(): Promise<void> {
// Use EXTERNAL_PERMISSION_MODES for help text — internal modes (bubble)
// are internal-only and auto is feature-gated; they're still accepted by validation.
// are ant-only and auto is feature-gated; they're still accepted by validation.
const { EXTERNAL_PERMISSION_MODES } = await import('../types/permissions.js')
const modes = EXTERNAL_PERMISSION_MODES.join(', ')
const showServer = await isMultiSessionSpawnEnabled()
@@ -2356,7 +2356,7 @@ export async function bridgeMain(args: string[]): Promise<void> {
// environment_id and reuse that for registration (idempotent on the
// backend). Left undefined otherwise — the backend rejects
// client-generated UUIDs and will allocate a fresh environment.
// feature('KAIROS') gate: --session-id is internal-only; parseArgs already
// feature('KAIROS') gate: --session-id is ant-only; parseArgs already
// rejects the flag when the gate is off, so resumeSessionId is always
// undefined here in external builds — this guard is for tree-shaking.
let reuseEnvironmentId: string | undefined

View File

@@ -223,7 +223,7 @@ export function createBridgeLogger(options: {
if (process.env.USER_TYPE === 'ant' && debugLogPath) {
writeStatus(
`${chalk.yellow('[internal] Logs:')} ${chalk.dim(debugLogPath)}\n`,
`${chalk.yellow('[ANT-ONLY] Logs:')} ${chalk.dim(debugLogPath)}\n`,
)
}
writeStatus(`${indicatorColor(indicator)} ${stateText}${suffix}\n`)

View File

@@ -161,7 +161,7 @@ export async function initReplBridge(
return null
}
// When CLAUDE_BRIDGE_OAUTH_TOKEN is set (internal-only local dev), the bridge
// When CLAUDE_BRIDGE_OAUTH_TOKEN is set (ant-only local dev), the bridge
// uses that token directly via getBridgeAccessToken() — keychain state is
// irrelevant. Skip 2b/2c to preserve that decoupling: an expired keychain
// token shouldn't block a bridge connection that doesn't use it.

View File

@@ -1,4 +1,4 @@
// biome-ignore-all assist/source/organizeImports: internal-only import markers must not be reordered
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
/**
* Env-less Remote Control bridge core.
*

View File

@@ -1,4 +1,4 @@
// biome-ignore-all assist/source/organizeImports: internal-only import markers must not be reordered
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
import { randomUUID } from 'crypto'
import {
createBridgeApiClient,

View File

@@ -17,7 +17,7 @@ import { jsonStringify } from '../utils/slowOperations.js'
*
* Bridge sessions have SecurityTier=ELEVATED on the server (CCR v2).
* The server gates ConnectBridgeWorker on its own flag
* (sessions_elevated_auth_enforcement in the server-side main deployment); this CLI-side
* (sessions_elevated_auth_enforcement in Anthropic Main); this CLI-side
* flag controls whether the CLI sends X-Trusted-Device-Token at all.
* Two flags so rollout can be staged: flip CLI-side first (headers
* start flowing, server still no-ops), then flip server-side.

View File

@@ -1,4 +1,5 @@
import { c as _c } from "react-compiler-runtime";
import { feature } from 'bun:bundle';
import figures from 'figures';
import React, { useEffect, useRef, useState } from 'react';
import { useTerminalSize } from '../hooks/useTerminalSize.js';
@@ -10,7 +11,6 @@ import { getGlobalConfig } from '../utils/config.js';
import { isFullscreenActive } from '../utils/fullscreen.js';
import type { Theme } from '../utils/theme.js';
import { getCompanion } from './companion.js';
import { isBuddyEnabled } from './feature.js';
import { renderFace, renderSprite, spriteFrameCount } from './sprites.js';
import { RARITY_COLORS } from './types.js';
const TICK_MS = 500;
@@ -165,7 +165,7 @@ function spriteColWidth(nameWidth: number): number {
// Narrow terminals: 0 — REPL.tsx stacks the one-liner on its own row
// (above input in fullscreen, below in scrollback), so no reservation.
export function companionReservedColumns(terminalColumns: number, speaking: boolean): number {
if (!isBuddyEnabled()) return 0;
if (!feature('BUDDY')) return 0;
const companion = getCompanion();
if (!companion || getGlobalConfig().companionMuted) return 0;
if (terminalColumns < MIN_COLS_FOR_FULL_SPRITE) return 0;
@@ -212,7 +212,7 @@ export function CompanionSprite(): React.ReactNode {
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps -- tick intentionally captured at reaction-change, not tracked
}, [reaction, setAppState]);
if (!isBuddyEnabled()) return null;
if (!feature('BUDDY')) return null;
const companion = getCompanion();
if (!companion || getGlobalConfig().companionMuted) return null;
const color = RARITY_COLORS[companion.rarity];
@@ -337,7 +337,7 @@ export function CompanionFloatingBubble() {
t3 = $[4];
}
useEffect(t2, t3);
if (!isBuddyEnabled() || !reaction) {
if (!feature("BUDDY") || !reaction) {
return null;
}
const companion = getCompanion();

View File

@@ -1,3 +0,0 @@
export function isBuddyEnabled(): boolean {
return true
}

View File

@@ -1,65 +0,0 @@
import type { Message } from '../types/message.js'
import { getGlobalConfig } from '../utils/config.js'
import { getUserMessageText } from '../utils/messages.js'
import { getCompanion } from './companion.js'
const DIRECT_REPLIES = [
'I am observing.',
'I am helping from the corner.',
'I saw that.',
'Still here.',
'Watching closely.',
] as const
const PET_REPLIES = [
'happy chirp',
'tiny victory dance',
'quietly approves',
'wiggles with joy',
'looks pleased',
] as const
function hashString(s: string): number {
let h = 2166136261
for (let i = 0; i < s.length; i++) {
h ^= s.charCodeAt(i)
h = Math.imul(h, 16777619)
}
return h >>> 0
}
function pickDeterministic<T>(items: readonly T[], seed: string): T {
return items[hashString(seed) % items.length]!
}
export async function fireCompanionObserver(
messages: Message[],
onReaction: (reaction: string | undefined) => void,
): Promise<void> {
const companion = getCompanion()
if (!companion || getGlobalConfig().companionMuted) return
const lastUser = [...messages].reverse().find(msg => msg.type === 'user')
if (!lastUser) return
const text = getUserMessageText(lastUser)?.trim()
if (!text) return
const lower = text.toLowerCase()
const companionName = companion.name.toLowerCase()
if (lower.includes('/buddy')) {
onReaction(pickDeterministic(PET_REPLIES, text + companion.name))
return
}
if (
lower.includes(companionName) ||
lower.includes('buddy') ||
lower.includes('companion')
) {
onReaction(
`${companion.name}: ${pickDeterministic(DIRECT_REPLIES, text + companion.personality)}`,
)
}
}

View File

@@ -1,8 +1,8 @@
import { feature } from 'bun:bundle'
import type { Message } from '../types/message.js'
import type { Attachment } from '../utils/attachments.js'
import { getGlobalConfig } from '../utils/config.js'
import { getCompanion } from './companion.js'
import { isBuddyEnabled } from './feature.js'
export function companionIntroText(name: string, species: string): string {
return `# Companion
@@ -15,7 +15,7 @@ When the user addresses ${name} directly (by name), its bubble will answer. Your
export function getCompanionIntroAttachment(
messages: Message[] | undefined,
): Attachment[] {
if (!isBuddyEnabled()) return []
if (!feature('BUDDY')) return []
const companion = getCompanion()
if (!companion || getGlobalConfig().companionMuted) return []

View File

@@ -1,10 +1,10 @@
import { c as _c } from "react-compiler-runtime";
import { feature } from 'bun:bundle';
import React, { useEffect } from 'react';
import { useNotifications } from '../context/notifications.js';
import { Text } from '../ink.js';
import { getGlobalConfig } from '../utils/config.js';
import { getRainbowColor } from '../utils/thinking.js';
import { isBuddyEnabled } from './feature.js';
// Local date, not UTC — 24h rolling wave across timezones. Sustained Twitter
// buzz instead of a single UTC-midnight spike, gentler on soul-gen load.
@@ -50,7 +50,7 @@ export function useBuddyNotification() {
let t1;
if ($[0] !== addNotification || $[1] !== removeNotification) {
t0 = () => {
if (!isBuddyEnabled()) {
if (!feature("BUDDY")) {
return;
}
const config = getGlobalConfig();
@@ -80,7 +80,7 @@ export function findBuddyTriggerPositions(text: string): Array<{
start: number;
end: number;
}> {
if (!isBuddyEnabled()) return [];
if (!feature('BUDDY')) return [];
const triggers: Array<{
start: number;
end: number;

View File

@@ -1,4 +1,4 @@
// biome-ignore-all assist/source/organizeImports: internal-only import markers must not be reordered
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
import { feature } from 'bun:bundle'
import { readFile, stat } from 'fs/promises'
import { dirname } from 'path'
@@ -2829,7 +2829,7 @@ function runHeadlessStreaming(
if (message.type === 'control_request') {
if (message.request.subtype === 'interrupt') {
// Track escapes for attribution (internal-only feature)
// Track escapes for attribution (ant-only feature)
if (feature('COMMIT_ATTRIBUTION')) {
setAppState(prev => ({
...prev,
@@ -3765,7 +3765,7 @@ function runHeadlessStreaming(
...getSettingsWithSources(),
applied: {
model,
// Numeric effort (internal-only) → null; SDK schema is string-level only.
// Numeric effort (ant-only) → null; SDK schema is string-level only.
effort: typeof effort === 'string' ? effort : null,
},
})
@@ -5025,7 +5025,7 @@ async function loadInitialMessages(
}
// Handle resume in print mode (accepts session ID or URL)
// URLs are [internal-only]
// URLs are [ANT-ONLY]
if (options.resume) {
try {
logEvent('tengu_resume_print', {})

View File

@@ -30,7 +30,7 @@ import { getInitialSettings } from 'src/utils/settings/settings.js'
export async function update() {
// Block updates for third-party providers. The update mechanism downloads
// from the first-party distribution bucket, which would silently replace the
// from Anthropic's distribution bucket, which would silently replace the
// OpenClaude build (with the OpenAI shim) with the upstream Claude Code
// binary (without it).
if (getAPIProvider() !== 'firstParty') {

View File

@@ -1,4 +1,4 @@
// biome-ignore-all assist/source/organizeImports: internal-only import markers must not be reordered
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
import addDir from './commands/add-dir/index.js'
import autofixPr from './commands/autofix-pr/index.js'
import backfillSessions from './commands/backfill-sessions/index.js'
@@ -59,7 +59,6 @@ import usage from './commands/usage/index.js'
import theme from './commands/theme/index.js'
import vim from './commands/vim/index.js'
import { feature } from 'bun:bundle'
import { isBuddyEnabled } from './buddy/feature.js'
// Dead code elimination: conditional imports
/* eslint-disable @typescript-eslint/no-require-imports */
const proactive =
@@ -118,7 +117,7 @@ const forkCmd = feature('FORK_SUBAGENT')
require('./commands/fork/index.js') as typeof import('./commands/fork/index.js')
).default
: null
const buddy = isBuddyEnabled()
const buddy = feature('BUDDY')
? (
require('./commands/buddy/index.js') as typeof import('./commands/buddy/index.js')
).default

View File

@@ -1,185 +0,0 @@
import type { LocalJSXCommandContext, LocalJSXCommandOnDone } from '../../types/command.js'
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
import { companionUserId, getCompanion, rollWithSeed } from '../../buddy/companion.js'
import type { StoredCompanion } from '../../buddy/types.js'
import { COMMON_HELP_ARGS, COMMON_INFO_ARGS } from '../../constants/xml.js'
const NAME_PREFIXES = [
'Byte',
'Echo',
'Glint',
'Miso',
'Nova',
'Pixel',
'Rune',
'Static',
'Vector',
'Whisk',
] as const
const NAME_SUFFIXES = [
'bean',
'bit',
'bud',
'dot',
'ling',
'loop',
'moss',
'patch',
'puff',
'spark',
] as const
const PERSONALITIES = [
'Curious and quietly encouraging',
'A patient little watcher with strong debugging instincts',
'Playful, observant, and suspicious of flaky tests',
'Calm under pressure and fond of clean diffs',
'A tiny terminal gremlin who likes successful builds',
] as const
const PET_REACTIONS = [
'leans into the headpat',
'does a proud little bounce',
'emits a content beep',
'looks delighted',
'wiggles happily',
] as const
function hashString(s: string): number {
let h = 2166136261
for (let i = 0; i < s.length; i++) {
h ^= s.charCodeAt(i)
h = Math.imul(h, 16777619)
}
return h >>> 0
}
function pickDeterministic<T>(items: readonly T[], seed: string): T {
return items[hashString(seed) % items.length]!
}
function titleCase(s: string): string {
return s.charAt(0).toUpperCase() + s.slice(1)
}
function createStoredCompanion(): StoredCompanion {
const userId = companionUserId()
const { bones } = rollWithSeed(`${userId}:buddy`)
const prefix = pickDeterministic(NAME_PREFIXES, `${userId}:prefix`)
const suffix = pickDeterministic(NAME_SUFFIXES, `${userId}:suffix`)
const personality = pickDeterministic(PERSONALITIES, `${userId}:personality`)
return {
name: `${prefix}${suffix}`,
personality: `${personality}.`,
hatchedAt: Date.now(),
}
}
function setCompanionReaction(
context: LocalJSXCommandContext,
reaction: string | undefined,
pet = false,
): void {
context.setAppState(prev => ({
...prev,
companionReaction: reaction,
companionPetAt: pet ? Date.now() : prev.companionPetAt,
}))
}
function showHelp(onDone: LocalJSXCommandOnDone): void {
onDone(
'Usage: /buddy [status|mute|unmute]\n\nRun /buddy with no args to hatch your companion the first time, then pet it on later runs.',
{ display: 'system' },
)
}
export async function call(
onDone: LocalJSXCommandOnDone,
context: LocalJSXCommandContext,
args?: string,
): Promise<null> {
const arg = args?.trim().toLowerCase() ?? ''
if (COMMON_HELP_ARGS.includes(arg) || arg === '') {
const existing = getCompanion()
if (arg !== '' || existing) {
if (arg !== '') {
showHelp(onDone)
return null
}
}
}
if (COMMON_HELP_ARGS.includes(arg)) {
showHelp(onDone)
return null
}
if (COMMON_INFO_ARGS.includes(arg) || arg === 'status') {
const companion = getCompanion()
if (!companion) {
onDone('No buddy hatched yet. Run /buddy to hatch one.', {
display: 'system',
})
return null
}
onDone(
`${companion.name} is your ${titleCase(companion.rarity)} ${companion.species}. ${companion.personality}`,
{ display: 'system' },
)
return null
}
if (arg === 'mute' || arg === 'unmute') {
const muted = arg === 'mute'
saveGlobalConfig(current => ({
...current,
companionMuted: muted,
}))
if (muted) {
setCompanionReaction(context, undefined)
}
onDone(`Buddy ${muted ? 'muted' : 'unmuted'}.`, { display: 'system' })
return null
}
if (arg !== '') {
showHelp(onDone)
return null
}
let companion = getCompanion()
if (!companion) {
const stored = createStoredCompanion()
saveGlobalConfig(current => ({
...current,
companion: stored,
companionMuted: false,
}))
companion = {
...rollWithSeed(`${companionUserId()}:buddy`).bones,
...stored,
}
setCompanionReaction(
context,
`${companion.name} the ${companion.species} has hatched.`,
true,
)
onDone(
`${companion.name} the ${companion.species} is now your buddy. Run /buddy again to pet them.`,
{ display: 'system' },
)
return null
}
const reaction = `${companion.name} ${pickDeterministic(
PET_REACTIONS,
`${Date.now()}:${companion.name}`,
)}`
setCompanionReaction(context, reaction, true)
onDone(undefined, { display: 'skip' })
return null
}

View File

@@ -1,12 +0,0 @@
import type { Command } from '../../commands.js'
const buddy = {
type: 'local-jsx',
name: 'buddy',
description: 'Hatch, pet, and manage your Open Claude companion',
immediate: true,
argumentHint: '[status|mute|unmute|help]',
load: () => import('./buddy.js'),
} satisfies Command
export default buddy

View File

@@ -199,13 +199,13 @@ function formatContextAsMarkdownTable(data: ContextData): string {
output += `\n`
}
// System tools (internal-only)
// System tools (ant-only)
if (
systemTools &&
systemTools.length > 0 &&
process.env.USER_TYPE === 'ant'
) {
output += `### [internal] System Tools\n\n`
output += `### [ANT-ONLY] System Tools\n\n`
output += `| Tool | Tokens |\n`
output += `|------|--------|\n`
for (const tool of systemTools) {
@@ -214,13 +214,13 @@ function formatContextAsMarkdownTable(data: ContextData): string {
output += `\n`
}
// System prompt sections (internal-only)
// System prompt sections (ant-only)
if (
systemPromptSections &&
systemPromptSections.length > 0 &&
process.env.USER_TYPE === 'ant'
) {
output += `### [internal] System Prompt Sections\n\n`
output += `### [ANT-ONLY] System Prompt Sections\n\n`
output += `| Section | Tokens |\n`
output += `|---------|--------|\n`
for (const section of systemPromptSections) {
@@ -288,9 +288,9 @@ function formatContextAsMarkdownTable(data: ContextData): string {
output += `\n`
}
// Message breakdown (internal-only)
// Message breakdown (ant-only)
if (messageBreakdown && process.env.USER_TYPE === 'ant') {
output += `### [internal] Message Breakdown\n\n`
output += `### [ANT-ONLY] Message Breakdown\n\n`
output += `| Category | Tokens |\n`
output += `|----------|--------|\n`
output += `| Tool calls | ${formatTokens(messageBreakdown.toolCallTokens)} |\n`

View File

@@ -16,7 +16,7 @@ export const call: LocalCommandCall = async () => {
}
if (process.env.USER_TYPE === 'ant') {
value += `\n\n[internal-only] Showing cost anyway:\n ${formatTotalCost()}`
value += `\n\n[ANT-ONLY] Showing cost anyway:\n ${formatTotalCost()}`
}
return { type: 'text', value }
}

View File

@@ -70,7 +70,7 @@ If the user chose personal CLAUDE.local.md or both: ask about them, not the code
- Only if Phase 2 found multiple git worktrees: ask whether their worktrees are nested inside the main repo (e.g., \`.claude/worktrees/<name>/\`) or siblings/external (e.g., \`../myrepo-feature/\`). If nested, the upward file walk finds the main repo's CLAUDE.local.md automatically — no special handling needed. If sibling/external, the personal content should live in a home-directory file (e.g., \`~/.claude/<project-name>-instructions.md\`) and each worktree gets a one-line CLAUDE.local.md stub that imports it: \`@~/.claude/<project-name>-instructions.md\`. Never put this import in the project CLAUDE.md — that would check a personal reference into the team-shared file.
- Any communication preferences? (e.g., "be terse", "always explain tradeoffs", "don't summarize at the end")
**Synthesize a proposal from Phase 2 findings** — e.g., format-on-edit if a formatter exists, a project verification workflow if tests exist, a CLAUDE.md note for anything from the gap-fill answers that's a guideline rather than a workflow. For each, pick the artifact type that fits, **constrained by the Phase 1 skills+hooks choice**:
**Synthesize a proposal from Phase 2 findings** — e.g., format-on-edit if a formatter exists, a \`/verify\` skill if tests exist, a CLAUDE.md note for anything from the gap-fill answers that's a guideline rather than a workflow. For each, pick the artifact type that fits, **constrained by the Phase 1 skills+hooks choice**:
- **Hook** (stricter) — deterministic shell command on a tool event; Claude can't skip it. Fits mechanical, fast, per-edit steps: formatting, linting, running a quick test on the changed file.
- **Skill** (on-demand) — you or Claude invoke \`/skill-name\` when you want it. Fits workflows that don't belong on every edit: deep verification, session reports, deploys.
@@ -85,7 +85,7 @@ If the user chose personal CLAUDE.local.md or both: ask about them, not the code
- **Keep previews compact — the preview box truncates with no scrolling.** One line per item, no blank lines between items, no header. Example preview content:
• **Format-on-edit hook** (automatic) — \`ruff format <file>\` via PostToolUse
• **Verification workflow** (on-demand) — \`make lint && make typecheck && make test\`
• **/verify skill** (on-demand) — \`make lint && make typecheck && make test\`
• **CLAUDE.md note** (guideline) — "run lint/typecheck/test before marking done"
- Option labels stay short ("Looks good", "Drop the hook", "Drop the skill") — the tool auto-adds an "Other" free-text option, so don't add your own catch-all.
@@ -157,7 +157,7 @@ Skills add capabilities Claude can use on demand without bloating every session.
**First, consume \`skill\` entries from the Phase 3 preference queue.** Each queued skill preference becomes a SKILL.md tailored to what the user described. For each:
- Name it from the preference (e.g., "verify-deep", "session-report", "deploy-sandbox")
- Write the body using the user's own words from the interview plus whatever Phase 2 found (test commands, report format, deploy target). If the preference maps to an existing project workflow, write a project skill that captures the user's specific constraints on top.
- Write the body using the user's own words from the interview plus whatever Phase 2 found (test commands, report format, deploy target). If the preference maps to an existing bundled skill (e.g., \`/verify\`), write a project skill that adds the user's specific constraints on top — tell the user the bundled one still exists and theirs is additive.
- Ask a quick follow-up if the preference is underspecified (e.g., "which test command should verify-deep run?")
**Then suggest additional skills** beyond the queue when you find:

View File

@@ -2187,7 +2187,7 @@ function generateHtmlReport(
`
: ''
// Build Team Feedback section (collapsible, internal-only)
// Build Team Feedback section (collapsible, ant-only)
const ccImprovements =
process.env.USER_TYPE === 'ant'
? insights.cc_team_improvements?.improvements || []
@@ -2804,7 +2804,7 @@ export async function generateUsageReport(options?: {
}> {
let remoteStats: { hosts: RemoteHostInfo[]; totalCopied: number } | undefined
// Optionally collect data from remote hosts first (internal-only)
// Optionally collect data from remote hosts first (ant-only)
if (process.env.USER_TYPE === 'ant' && options?.collectRemote) {
const destDir = join(getClaudeConfigHomeDir(), 'projects')
const { hosts, totalCopied } = await collectAllRemoteHostData(destDir)
@@ -3072,6 +3072,33 @@ const usageReport: Command = {
let reportUrl = `file://${htmlPath}`
let uploadHint = ''
if (process.env.USER_TYPE === 'ant') {
// Try to upload to S3
const timestamp = new Date()
.toISOString()
.replace(/[-:]/g, '')
.replace('T', '_')
.slice(0, 15)
const username = process.env.SAFEUSER || process.env.USER || 'unknown'
const filename = `${username}_insights_${timestamp}.html`
const s3Path = `s3://anthropic-serve/atamkin/cc-user-reports/${filename}`
const s3Url = `https://s3-frontend.infra.ant.dev/anthropic-serve/atamkin/cc-user-reports/${filename}`
reportUrl = s3Url
try {
execFileSync('ff', ['cp', htmlPath, s3Path], {
timeout: 60000,
stdio: 'pipe', // Suppress output
})
} catch {
// Upload failed - fall back to local file and show upload command
reportUrl = `file://${htmlPath}`
uploadHint = `\nAutomatic upload failed. Are you on the boron namespace? Try \`use-bo\` and ensure you've run \`sso\`.
To share, run: ff cp ${htmlPath} ${s3Path}
Then access at: ${s3Url}`
}
}
// Build header with stats
const sessionLabel =
data.total_sessions_scanned &&
@@ -3085,7 +3112,7 @@ const usageReport: Command = {
`${data.git_commits} commits`,
].join(' · ')
// Build remote host info (internal-only)
// Build remote host info (ant-only)
let remoteInfo = ''
if (process.env.USER_TYPE === 'ant') {
if (remoteStats && remoteStats.totalCopied > 0) {

View File

@@ -1,42 +0,0 @@
import { afterEach, expect, mock, test } from 'bun:test'
const originalEnv = {
CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI,
OPENAI_BASE_URL: process.env.OPENAI_BASE_URL,
OPENAI_MODEL: process.env.OPENAI_MODEL,
}
afterEach(() => {
mock.restore()
process.env.CLAUDE_CODE_USE_OPENAI = originalEnv.CLAUDE_CODE_USE_OPENAI
process.env.OPENAI_BASE_URL = originalEnv.OPENAI_BASE_URL
process.env.OPENAI_MODEL = originalEnv.OPENAI_MODEL
})
test('opens the model picker without awaiting local model discovery refresh', async () => {
process.env.CLAUDE_CODE_USE_OPENAI = '1'
process.env.OPENAI_BASE_URL = 'http://127.0.0.1:8080/v1'
process.env.OPENAI_MODEL = 'qwen2.5-coder-7b-instruct'
let resolveDiscovery: (() => void) | undefined
const discoverOpenAICompatibleModelOptions = mock(
() =>
new Promise<void>(resolve => {
resolveDiscovery = resolve
}),
)
mock.module('../../utils/model/openaiModelDiscovery.js', () => ({
discoverOpenAICompatibleModelOptions,
}))
const { call } = await import(`./model.js?ts=${Date.now()}-${Math.random()}`)
const result = await Promise.race([
call(() => {}, {} as never, ''),
new Promise(resolve => setTimeout(() => resolve('timeout'), 50)),
])
resolveDiscovery?.()
expect(result).not.toBe('timeout')
})

View File

@@ -4,7 +4,6 @@ import * as React from 'react';
import type { CommandResultDisplay } from '../../commands.js';
import { ModelPicker } from '../../components/ModelPicker.js';
import { COMMON_HELP_ARGS, COMMON_INFO_ARGS } from '../../constants/xml.js';
import { fetchBootstrapData } from '../../services/api/bootstrap.js';
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js';
import { useAppState, useSetAppState } from '../../state/AppState.js';
import type { LocalJSXCommandCall } from '../../types/command.js';
@@ -13,14 +12,9 @@ import { isBilledAsExtraUsage } from '../../utils/extraUsage.js';
import { clearFastModeCooldown, isFastModeAvailable, isFastModeEnabled, isFastModeSupportedByModel } from '../../utils/fastMode.js';
import { MODEL_ALIASES } from '../../utils/model/aliases.js';
import { checkOpus1mAccess, checkSonnet1mAccess } from '../../utils/model/check1mAccess.js';
import type { ModelOption } from '../../utils/model/modelOptions.js';
import { discoverOpenAICompatibleModelOptions } from '../../utils/model/openaiModelDiscovery.js';
import { getAPIProvider } from '../../utils/model/providers.js';
import { getActiveOpenAIModelOptionsCache, setActiveOpenAIModelOptionsCache } from '../../utils/providerProfiles.js';
import { getDefaultMainLoopModelSetting, isOpus1mMergeEnabled, renderDefaultModelSetting } from '../../utils/model/model.js';
import { isModelAllowed } from '../../utils/model/modelAllowlist.js';
import { validateModel } from '../../utils/model/validateModel.js';
import { getAdditionalModelOptionsCacheScope } from '../../services/api/providerConfig.js';
function ModelPickerWrapper(t0) {
const $ = _c(17);
const {
@@ -274,33 +268,6 @@ function _temp8(s_0) {
function _temp7(s) {
return s.mainLoopModel;
}
function haveSameModelOptions(left: ModelOption[], right: ModelOption[]): boolean {
if (left.length !== right.length) {
return false;
}
return left.every((option, index) => {
const other = right[index];
return other !== undefined && option.value === other.value && option.label === other.label && option.description === other.description && option.descriptionForModel === other.descriptionForModel;
});
}
async function refreshOpenAIModelOptionsCache(): Promise<void> {
if (getAPIProvider() !== 'openai') {
return;
}
try {
const discoveredOptions = await discoverOpenAICompatibleModelOptions();
if (discoveredOptions.length === 0) {
return;
}
const currentOptions = getActiveOpenAIModelOptionsCache();
if (haveSameModelOptions(currentOptions, discoveredOptions)) {
return;
}
setActiveOpenAIModelOptionsCache(discoveredOptions);
} catch {
// Keep /model usable even if endpoint discovery fails.
}
}
export const call: LocalJSXCommandCall = async (onDone, _context, args) => {
args = args?.trim() || '';
if (COMMON_INFO_ARGS.includes(args)) {
@@ -321,9 +288,6 @@ export const call: LocalJSXCommandCall = async (onDone, _context, args) => {
});
return <SetModelAndClose args={args} onDone={onDone} />;
}
if (getAdditionalModelOptionsCacheScope()?.startsWith('openai:')) {
void refreshOpenAIModelOptionsCache();
}
return <ModelPickerWrapper onDone={onDone} />;
};
function renderModelLabel(model: string | null): string {

View File

@@ -2,7 +2,6 @@ import type { Command } from '../../commands.js'
const onboardGithub: Command = {
name: 'onboard-github',
aliases: ['onboarding-github', 'onboardgithub', 'onboardinggithub'],
description:
'Interactive setup for GitHub Models: device login or PAT, saved to secure storage',
type: 'local-jsx',

View File

@@ -1,148 +0,0 @@
import { describe, expect, test } from 'bun:test'
import {
activateGithubOnboardingMode,
applyGithubOnboardingProcessEnv,
buildGithubOnboardingSettingsEnv,
hasExistingGithubModelsLoginToken,
shouldForceGithubRelogin,
} from './onboard-github.js'
describe('shouldForceGithubRelogin', () => {
test.each(['force', '--force', 'relogin', '--relogin', 'reauth', '--reauth'])(
'treats %s as force re-login',
arg => {
expect(shouldForceGithubRelogin(arg)).toBe(true)
},
)
test('returns false for empty or unknown args', () => {
expect(shouldForceGithubRelogin('')).toBe(false)
expect(shouldForceGithubRelogin(undefined)).toBe(false)
expect(shouldForceGithubRelogin('something-else')).toBe(false)
})
test('treats force flags as present in multi-word args', () => {
expect(shouldForceGithubRelogin('--force extra')).toBe(true)
expect(shouldForceGithubRelogin('foo --relogin bar')).toBe(true)
expect(shouldForceGithubRelogin('abc reauth xyz')).toBe(true)
})
})
describe('hasExistingGithubModelsLoginToken', () => {
test('returns true when GITHUB_TOKEN is present', () => {
expect(
hasExistingGithubModelsLoginToken({ GITHUB_TOKEN: 'token' }, ''),
).toBe(true)
})
test('returns true when GH_TOKEN is present', () => {
expect(
hasExistingGithubModelsLoginToken({ GH_TOKEN: 'token' }, ''),
).toBe(true)
})
test('returns true when stored token exists', () => {
expect(hasExistingGithubModelsLoginToken({}, 'stored-token')).toBe(true)
})
test('returns false when both env and stored token are missing', () => {
expect(hasExistingGithubModelsLoginToken({}, '')).toBe(false)
})
})
describe('onboarding auth precedence cleanup', () => {
test('clears preexisting OpenAI auth when switching to GitHub', () => {
const env: NodeJS.ProcessEnv = {
CLAUDE_CODE_USE_OPENAI: '1',
OPENAI_MODEL: 'gpt-4o',
OPENAI_API_KEY: 'sk-stale-openai-key',
OPENAI_ORG: 'org-old',
OPENAI_PROJECT: 'project-old',
OPENAI_ORGANIZATION: 'org-legacy',
OPENAI_BASE_URL: 'https://api.openai.com/v1',
OPENAI_API_BASE: 'https://api.openai.com/v1',
CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED: '1',
CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID: 'profile_old',
}
applyGithubOnboardingProcessEnv('github:copilot', env)
expect(env.CLAUDE_CODE_USE_GITHUB).toBe('1')
expect(env.OPENAI_MODEL).toBe('github:copilot')
expect(env.OPENAI_API_KEY).toBeUndefined()
expect(env.OPENAI_ORG).toBeUndefined()
expect(env.OPENAI_PROJECT).toBeUndefined()
expect(env.OPENAI_ORGANIZATION).toBeUndefined()
expect(env.OPENAI_BASE_URL).toBeUndefined()
expect(env.OPENAI_API_BASE).toBeUndefined()
expect(env.CLAUDE_CODE_USE_OPENAI).toBeUndefined()
expect(env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED).toBeUndefined()
expect(env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID).toBeUndefined()
const settingsEnv = buildGithubOnboardingSettingsEnv('github:copilot')
expect(settingsEnv.CLAUDE_CODE_USE_GITHUB).toBe('1')
expect(settingsEnv.OPENAI_MODEL).toBe('github:copilot')
expect(settingsEnv.OPENAI_API_KEY).toBeUndefined()
expect(settingsEnv.OPENAI_ORG).toBeUndefined()
expect(settingsEnv.OPENAI_PROJECT).toBeUndefined()
expect(settingsEnv.OPENAI_ORGANIZATION).toBeUndefined()
})
})
describe('activateGithubOnboardingMode', () => {
test('activates settings/env/hydration in order when merge succeeds', () => {
const calls: string[] = []
const result = activateGithubOnboardingMode(' github:copilot ', {
mergeSettingsEnv: model => {
calls.push(`merge:${model}`)
return { ok: true }
},
applyProcessEnv: model => {
calls.push(`apply:${model}`)
},
hydrateToken: () => {
calls.push('hydrate')
},
onChangeAPIKey: () => {
calls.push('onChangeAPIKey')
},
})
expect(result).toEqual({ ok: true })
expect(calls).toEqual([
'merge:github:copilot',
'apply:github:copilot',
'hydrate',
'onChangeAPIKey',
])
})
test('stops activation when settings merge fails', () => {
const calls: string[] = []
const result = activateGithubOnboardingMode(DEFAULT_MODEL_FOR_TESTS, {
mergeSettingsEnv: () => {
calls.push('merge')
return { ok: false, detail: 'settings write failed' }
},
applyProcessEnv: () => {
calls.push('apply')
},
hydrateToken: () => {
calls.push('hydrate')
},
onChangeAPIKey: () => {
calls.push('onChangeAPIKey')
},
})
expect(result).toEqual({ ok: false, detail: 'settings write failed' })
expect(calls).toEqual(['merge'])
})
})
const DEFAULT_MODEL_FOR_TESTS = 'github:copilot'

View File

@@ -12,20 +12,11 @@ import {
import type { LocalJSXCommandCall } from '../../types/command.js'
import {
hydrateGithubModelsTokenFromSecureStorage,
readGithubModelsToken,
saveGithubModelsToken,
} from '../../utils/githubModelsCredentials.js'
import { updateSettingsForSource } from '../../utils/settings/settings.js'
const DEFAULT_MODEL = 'github:copilot'
const FORCE_RELOGIN_ARGS = new Set([
'force',
'--force',
'relogin',
'--relogin',
'reauth',
'--reauth',
])
type Step =
| 'menu'
@@ -33,72 +24,17 @@ type Step =
| 'pat'
| 'error'
export function shouldForceGithubRelogin(args?: string): boolean {
const normalized = (args ?? '').trim().toLowerCase()
if (!normalized) {
return false
}
return normalized.split(/\s+/).some(arg => FORCE_RELOGIN_ARGS.has(arg))
}
export function hasExistingGithubModelsLoginToken(
env: NodeJS.ProcessEnv = process.env,
storedToken?: string,
): boolean {
const envToken = env.GITHUB_TOKEN?.trim() || env.GH_TOKEN?.trim()
if (envToken) {
return true
}
const persisted = (storedToken ?? readGithubModelsToken())?.trim()
return Boolean(persisted)
}
export function buildGithubOnboardingSettingsEnv(
model: string,
): Record<string, string | undefined> {
return {
CLAUDE_CODE_USE_GITHUB: '1',
OPENAI_MODEL: model,
OPENAI_API_KEY: undefined,
OPENAI_ORG: undefined,
OPENAI_PROJECT: undefined,
OPENAI_ORGANIZATION: undefined,
OPENAI_BASE_URL: undefined,
OPENAI_API_BASE: undefined,
CLAUDE_CODE_USE_OPENAI: undefined,
CLAUDE_CODE_USE_GEMINI: undefined,
CLAUDE_CODE_USE_BEDROCK: undefined,
CLAUDE_CODE_USE_VERTEX: undefined,
CLAUDE_CODE_USE_FOUNDRY: undefined,
}
}
export function applyGithubOnboardingProcessEnv(
model: string,
env: NodeJS.ProcessEnv = process.env,
): void {
env.CLAUDE_CODE_USE_GITHUB = '1'
env.OPENAI_MODEL = model
delete env.OPENAI_API_KEY
delete env.OPENAI_ORG
delete env.OPENAI_PROJECT
delete env.OPENAI_ORGANIZATION
delete env.OPENAI_BASE_URL
delete env.OPENAI_API_BASE
delete env.CLAUDE_CODE_USE_OPENAI
delete env.CLAUDE_CODE_USE_GEMINI
delete env.CLAUDE_CODE_USE_BEDROCK
delete env.CLAUDE_CODE_USE_VERTEX
delete env.CLAUDE_CODE_USE_FOUNDRY
delete env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED
delete env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID
}
function mergeUserSettingsEnv(model: string): { ok: boolean; detail?: string } {
const { error } = updateSettingsForSource('userSettings', {
env: buildGithubOnboardingSettingsEnv(model) as any,
env: {
CLAUDE_CODE_USE_GITHUB: '1',
OPENAI_MODEL: model,
CLAUDE_CODE_USE_OPENAI: undefined as any,
CLAUDE_CODE_USE_GEMINI: undefined as any,
CLAUDE_CODE_USE_BEDROCK: undefined as any,
CLAUDE_CODE_USE_VERTEX: undefined as any,
CLAUDE_CODE_USE_FOUNDRY: undefined as any,
},
})
if (error) {
return { ok: false, detail: error.message }
@@ -106,32 +42,6 @@ function mergeUserSettingsEnv(model: string): { ok: boolean; detail?: string } {
return { ok: true }
}
export function activateGithubOnboardingMode(
model: string = DEFAULT_MODEL,
options?: {
mergeSettingsEnv?: (model: string) => { ok: boolean; detail?: string }
applyProcessEnv?: (model: string) => void
hydrateToken?: () => void
onChangeAPIKey?: () => void
},
): { ok: boolean; detail?: string } {
const normalizedModel = model.trim() || DEFAULT_MODEL
const mergeSettingsEnv = options?.mergeSettingsEnv ?? mergeUserSettingsEnv
const applyProcessEnv = options?.applyProcessEnv ?? applyGithubOnboardingProcessEnv
const hydrateToken =
options?.hydrateToken ?? hydrateGithubModelsTokenFromSecureStorage
const merged = mergeSettingsEnv(normalizedModel)
if (!merged.ok) {
return merged
}
applyProcessEnv(normalizedModel)
hydrateToken()
options?.onChangeAPIKey?.()
return { ok: true }
}
function OnboardGithub(props: {
onDone: Parameters<LocalJSXCommandCall>[0]
onChangeAPIKey: () => void
@@ -154,17 +64,19 @@ function OnboardGithub(props: {
setStep('error')
return
}
const activated = activateGithubOnboardingMode(model, {
onChangeAPIKey,
})
if (!activated.ok) {
const merged = mergeUserSettingsEnv(model.trim() || DEFAULT_MODEL)
if (!merged.ok) {
setErrorMsg(
`Token saved, but settings were not updated: ${activated.detail ?? 'unknown error'}. ` +
`Token saved, but settings were not updated: ${merged.detail ?? 'unknown error'}. ` +
`Add env CLAUDE_CODE_USE_GITHUB=1 and OPENAI_MODEL to ~/.claude/settings.json manually.`,
)
setStep('error')
return
}
process.env.CLAUDE_CODE_USE_GITHUB = '1'
process.env.OPENAI_MODEL = model.trim() || DEFAULT_MODEL
hydrateGithubModelsTokenFromSecureStorage()
onChangeAPIKey()
onDone(
'GitHub Models onboard complete. Token stored in secure storage; user settings updated. Restart if the model does not switch.',
{ display: 'user' },
@@ -235,11 +147,11 @@ function OnboardGithub(props: {
{deviceHint.verification_uri}
</Text>
<Text dimColor>
A browser window may have opened. Waiting for authorization...
A browser window may have opened. Waiting for authorization
</Text>
</>
) : (
<Text dimColor>Requesting device code from GitHub...</Text>
<Text dimColor>Requesting device code from GitHub</Text>
)}
<Spinner />
</Box>
@@ -294,7 +206,7 @@ function OnboardGithub(props: {
<Text bold>GitHub Models setup</Text>
<Text dimColor>
Stores your token in the OS credential store (macOS Keychain when available)
and enables CLAUDE_CODE_USE_GITHUB in your user settings - no export
and enables CLAUDE_CODE_USE_GITHUB in your user settings no export
GITHUB_TOKEN needed for future runs.
</Text>
<Select
@@ -315,28 +227,7 @@ function OnboardGithub(props: {
)
}
export const call: LocalJSXCommandCall = async (onDone, context, args) => {
const forceRelogin = shouldForceGithubRelogin(args)
if (hasExistingGithubModelsLoginToken() && !forceRelogin) {
const activated = activateGithubOnboardingMode(DEFAULT_MODEL, {
onChangeAPIKey: context.onChangeAPIKey,
})
if (!activated.ok) {
onDone(
`GitHub token detected, but settings activation failed: ${activated.detail ?? 'unknown error'}. ` +
'Set CLAUDE_CODE_USE_GITHUB=1 and OPENAI_MODEL=github:copilot in user settings manually.',
{ display: 'system' },
)
return null
}
onDone(
'GitHub Models already authorized. Activated GitHub Models mode using your existing token. Use /onboard-github --force to re-authenticate.',
{ display: 'user' },
)
return null
}
export const call: LocalJSXCommandCall = async (onDone, context) => {
return (
<OnboardGithub
onDone={onDone}

View File

@@ -1,10 +1,12 @@
import type { Command } from '../../commands.js'
import { shouldInferenceConfigCommandBeImmediate } from '../../utils/immediateCommand.js'
const provider = {
export default {
type: 'local-jsx',
name: 'provider',
description: 'Manage API provider profiles',
description: 'Set up and save a third-party provider profile for OpenClaude',
get immediate() {
return shouldInferenceConfigCommandBeImmediate()
},
load: () => import('./provider.js'),
} satisfies Command
export default provider

View File

@@ -197,38 +197,6 @@ test('buildProfileSaveMessage maps provider fields without echoing secrets', ()
expect(message).not.toContain('sk-secret-12345678')
})
test('buildProfileSaveMessage labels local openai-compatible profiles consistently', () => {
const message = buildProfileSaveMessage(
'openai',
{
OPENAI_MODEL: 'gpt-5.4',
OPENAI_BASE_URL: 'http://127.0.0.1:8080/v1',
},
'D:/codings/Opensource/openclaude/.openclaude-profile.json',
)
expect(message).toContain('Saved Local OpenAI-compatible profile.')
expect(message).toContain('Model: gpt-5.4')
expect(message).toContain('Endpoint: http://127.0.0.1:8080/v1')
})
test('buildProfileSaveMessage describes Gemini access token / ADC mode clearly', () => {
const message = buildProfileSaveMessage(
'gemini',
{
GEMINI_AUTH_MODE: 'access-token',
GEMINI_MODEL: 'gemini-2.5-flash',
GEMINI_BASE_URL: 'https://generativelanguage.googleapis.com/v1beta/openai',
},
'D:/codings/Opensource/openclaude/.openclaude-profile.json',
)
expect(message).toContain('Saved Google Gemini profile.')
expect(message).toContain('Model: gemini-2.5-flash')
expect(message).toContain('Credentials: access token (stored securely)')
expect(message).not.toContain('AIza')
})
test('buildCurrentProviderSummary redacts poisoned model and endpoint values', () => {
const summary = buildCurrentProviderSummary({
processEnv: {
@@ -245,51 +213,6 @@ test('buildCurrentProviderSummary redacts poisoned model and endpoint values', (
expect(summary.endpointLabel).toBe('sk-...5678')
})
test('buildCurrentProviderSummary labels generic local openai-compatible providers', () => {
const summary = buildCurrentProviderSummary({
processEnv: {
CLAUDE_CODE_USE_OPENAI: '1',
OPENAI_MODEL: 'qwen2.5-coder-7b-instruct',
OPENAI_BASE_URL: 'http://127.0.0.1:8080/v1',
},
persisted: null,
})
expect(summary.providerLabel).toBe('Local OpenAI-compatible')
expect(summary.modelLabel).toBe('qwen2.5-coder-7b-instruct')
expect(summary.endpointLabel).toBe('http://127.0.0.1:8080/v1')
})
test('buildCurrentProviderSummary does not relabel local gpt-5.4 providers as Codex', () => {
const summary = buildCurrentProviderSummary({
processEnv: {
CLAUDE_CODE_USE_OPENAI: '1',
OPENAI_MODEL: 'gpt-5.4',
OPENAI_BASE_URL: 'http://127.0.0.1:8080/v1',
},
persisted: null,
})
expect(summary.providerLabel).toBe('Local OpenAI-compatible')
expect(summary.modelLabel).toBe('gpt-5.4')
expect(summary.endpointLabel).toBe('http://127.0.0.1:8080/v1')
})
test('buildCurrentProviderSummary recognizes GitHub Models mode', () => {
const summary = buildCurrentProviderSummary({
processEnv: {
CLAUDE_CODE_USE_GITHUB: '1',
OPENAI_MODEL: 'github:copilot',
OPENAI_BASE_URL: 'https://models.github.ai/inference',
},
persisted: null,
})
expect(summary.providerLabel).toBe('GitHub Models')
expect(summary.modelLabel).toBe('github:copilot')
expect(summary.endpointLabel).toBe('https://models.github.ai/inference')
})
test('getProviderWizardDefaults ignores poisoned current provider values', () => {
const defaults = getProviderWizardDefaults({
OPENAI_API_KEY: 'sk-secret-12345678',

View File

@@ -2,7 +2,6 @@ import * as React from 'react'
import type { LocalJSXCommandCall, LocalJSXCommandOnDone } from '../../types/command.js'
import { COMMON_HELP_ARGS, COMMON_INFO_ARGS } from '../../constants/xml.js'
import { ProviderManager } from '../../components/ProviderManager.js'
import TextInput from '../../components/TextInput.js'
import {
Select,
@@ -15,7 +14,6 @@ import { Box, Text } from '../../ink.js'
import {
DEFAULT_CODEX_BASE_URL,
DEFAULT_OPENAI_BASE_URL,
isLocalProviderUrl,
resolveCodexApiCredentials,
resolveProviderRequest,
} from '../../services/api/providerConfig.js'
@@ -38,14 +36,6 @@ import {
type ProfileFile,
type ProviderProfile,
} from '../../utils/providerProfile.js'
import {
getGeminiProjectIdHint,
mayHaveGeminiAdcCredentials,
} from '../../utils/geminiAuth.js'
import {
readGeminiAccessToken,
saveGeminiAccessToken,
} from '../../utils/geminiCredentials.js'
import {
getGoalDefaultOpenAIModel,
normalizeRecommendationGoal,
@@ -53,11 +43,7 @@ import {
recommendOllamaModel,
type RecommendationGoal,
} from '../../utils/providerRecommendation.js'
import {
getLocalOpenAICompatibleProviderLabel,
hasLocalOllama,
listOllamaModels,
} from '../../utils/providerDiscovery.js'
import { hasLocalOllama, listOllamaModels } from '../../utils/providerDiscovery.js'
type ProviderChoice = 'auto' | ProviderProfile | 'clear'
@@ -74,14 +60,8 @@ type Step =
baseUrl: string | null
defaultModel: string
}
| { name: 'gemini-auth-method' }
| { name: 'gemini-key' }
| { name: 'gemini-access-token' }
| {
name: 'gemini-model'
apiKey?: string
authMode: 'api-key' | 'access-token' | 'adc'
}
| { name: 'gemini-model'; apiKey: string }
| { name: 'codex-check' }
type CurrentProviderSummary = {
@@ -178,23 +158,6 @@ export function buildCurrentProviderSummary(options?: {
}
}
if (isEnvTruthy(processEnv.CLAUDE_CODE_USE_GITHUB)) {
return {
providerLabel: 'GitHub Models',
modelLabel: getSafeDisplayValue(
processEnv.OPENAI_MODEL ?? 'github:copilot',
processEnv,
),
endpointLabel: getSafeDisplayValue(
processEnv.OPENAI_BASE_URL ??
processEnv.OPENAI_API_BASE ??
'https://models.github.ai/inference',
processEnv,
),
savedProfileLabel,
}
}
if (isEnvTruthy(processEnv.CLAUDE_CODE_USE_OPENAI)) {
const request = resolveProviderRequest({
model: processEnv.OPENAI_MODEL,
@@ -204,8 +167,10 @@ export function buildCurrentProviderSummary(options?: {
let providerLabel = 'OpenAI-compatible'
if (request.transport === 'codex_responses') {
providerLabel = 'Codex'
} else if (isLocalProviderUrl(request.baseUrl)) {
providerLabel = getLocalOpenAICompatibleProviderLabel(request.baseUrl)
} else if (request.baseUrl.includes('localhost:11434')) {
providerLabel = 'Ollama'
} else if (request.baseUrl.includes('localhost:1234')) {
providerLabel = 'LM Studio'
}
return {
@@ -251,13 +216,9 @@ function buildSavedProfileSummary(
env,
),
credentialLabel:
env.GEMINI_AUTH_MODE === 'access-token'
? 'access token (stored securely)'
: env.GEMINI_AUTH_MODE === 'adc'
? 'local ADC'
: maskSecretForDisplay(env.GEMINI_API_KEY) !== undefined
? 'configured'
: undefined,
maskSecretForDisplay(env.GEMINI_API_KEY) !== undefined
? 'configured'
: undefined,
}
case 'codex':
return {
@@ -292,20 +253,16 @@ function buildSavedProfileSummary(
),
}
case 'openai':
default: {
const baseUrl = env.OPENAI_BASE_URL ?? DEFAULT_OPENAI_BASE_URL
default:
return {
providerLabel: isLocalProviderUrl(baseUrl)
? getLocalOpenAICompatibleProviderLabel(baseUrl)
: 'OpenAI-compatible',
providerLabel: 'OpenAI-compatible',
modelLabel: getSafeDisplayValue(
env.OPENAI_MODEL ?? 'gpt-4o',
process.env,
env,
),
endpointLabel: getSafeDisplayValue(
baseUrl,
env.OPENAI_BASE_URL ?? DEFAULT_OPENAI_BASE_URL,
process.env,
env,
),
@@ -314,7 +271,6 @@ function buildSavedProfileSummary(
? 'configured'
: undefined,
}
}
}
}
@@ -471,7 +427,7 @@ function ProviderChooser({
{
label: 'Gemini',
value: 'gemini',
description: 'Use Google Gemini with API key, access token, or local ADC',
description: 'Use a Google Gemini API key',
},
{
label: 'Codex',
@@ -970,7 +926,7 @@ export function ProviderWizard({
defaultModel: defaults.openAIModel,
})
} else if (value === 'gemini') {
setStep({ name: 'gemini-auth-method' })
setStep({ name: 'gemini-key' })
} else if (value === 'clear') {
const filePath = deleteProfileFile()
onDone(`Removed saved provider profile at ${filePath}. Restart OpenClaude to go back to normal startup.`, {
@@ -1110,76 +1066,12 @@ export function ProviderWizard({
/>
)
case 'gemini-auth-method': {
const hasShellGeminiKey = Boolean(
process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY,
)
const hasShellGeminiAccessToken = Boolean(process.env.GEMINI_ACCESS_TOKEN)
const hasStoredGeminiAccessToken = Boolean(readGeminiAccessToken())
const hasAdc = mayHaveGeminiAdcCredentials(process.env)
const projectHint = getGeminiProjectIdHint(process.env)
const options: OptionWithDescription[] = [
{
label: 'API key',
value: 'api-key',
description: hasShellGeminiKey
? 'Use the current Gemini API key from this shell, or enter a new one'
: 'Use a Google Gemini API key',
},
{
label: 'Access token',
value: 'access-token',
description: hasShellGeminiAccessToken || hasStoredGeminiAccessToken
? `Use ${
hasShellGeminiAccessToken
? 'the current GEMINI_ACCESS_TOKEN'
: 'the securely stored Gemini access token'
}`
: 'Enter a Gemini access token and store it securely',
},
{
label: 'Local ADC',
value: 'adc',
description: hasAdc
? `Use local Google ADC credentials${projectHint ? ` (project: ${projectHint})` : ''}`
: 'Use local Google ADC credentials after running gcloud auth application-default login',
},
]
return (
<Dialog title="Gemini setup" onCancel={() => onDone()}>
<Box flexDirection="column" gap={1}>
<Text>Choose how this Gemini profile should authenticate.</Text>
<Select
options={options}
inlineDescriptions
visibleOptionCount={options.length}
onChange={value => {
if (value === 'api-key') {
setStep({ name: 'gemini-key' })
} else if (value === 'access-token') {
setStep({ name: 'gemini-access-token' })
} else {
setStep({
name: 'gemini-model',
authMode: 'adc',
})
}
}}
onCancel={() => setStep({ name: 'choose' })}
/>
</Box>
</Dialog>
)
}
case 'gemini-key':
return (
<TextEntryDialog
resetStateKey={step.name}
title="Gemini setup"
subtitle="Step 1 of 3"
subtitle="Step 1 of 2"
description={
process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY
? 'Enter a Gemini API key, or leave this blank to reuse the current GEMINI_API_KEY/GOOGLE_API_KEY from this session.'
@@ -1197,95 +1089,25 @@ export function ProviderWizard({
process.env.GEMINI_API_KEY ||
process.env.GOOGLE_API_KEY ||
''
setStep({ name: 'gemini-model', apiKey, authMode: 'api-key' })
setStep({ name: 'gemini-model', apiKey })
}}
onCancel={() => setStep({ name: 'gemini-auth-method' })}
onCancel={() => setStep({ name: 'choose' })}
/>
)
case 'gemini-access-token': {
const currentToken =
process.env.GEMINI_ACCESS_TOKEN || readGeminiAccessToken() || ''
return (
<TextEntryDialog
resetStateKey={step.name}
title="Gemini setup"
subtitle="Step 2 of 3"
description={
currentToken
? 'Enter a Gemini access token, or leave this blank to reuse the current token from this session or secure storage.'
: 'Enter a Gemini access token. It will be stored securely for this profile.'
}
initialValue=""
placeholder="ya29...."
mask="*"
allowEmpty={Boolean(currentToken)}
validate={value => {
const token = value.trim() || currentToken
return token ? null : 'Enter a Gemini access token or go back and choose Local ADC.'
}}
onSubmit={value => {
const token = value.trim() || currentToken
const saved = saveGeminiAccessToken(token)
if (!saved.success) {
onDone(
`Failed to save Gemini access token: ${saved.warning ?? 'unknown error'}`,
{
display: 'system',
},
)
return
}
setStep({
name: 'gemini-model',
authMode: 'access-token',
})
}}
onCancel={() => setStep({ name: 'gemini-auth-method' })}
/>
)
}
case 'gemini-model':
return (
<TextEntryDialog
resetStateKey={step.name}
title="Gemini setup"
subtitle={
step.authMode === 'api-key'
? 'Step 3 of 3'
: step.authMode === 'access-token'
? 'Step 3 of 3'
: 'Step 2 of 2'
}
description={
step.authMode === 'api-key'
? `Enter a Gemini model name. Leave blank for ${DEFAULT_GEMINI_MODEL}.`
: step.authMode === 'access-token'
? `Enter a Gemini model name. Leave blank for ${DEFAULT_GEMINI_MODEL}. This profile will use the stored Gemini access token at runtime.`
: `Enter a Gemini model name. Leave blank for ${DEFAULT_GEMINI_MODEL}. This profile will use local Google ADC credentials at runtime.`
}
subtitle="Step 2 of 2"
description={`Enter a Gemini model name. Leave blank for ${DEFAULT_GEMINI_MODEL}.`}
initialValue={defaults.geminiModel}
placeholder={DEFAULT_GEMINI_MODEL}
allowEmpty
onSubmit={value => {
if (
step.authMode === 'adc' &&
!mayHaveGeminiAdcCredentials(process.env)
) {
onDone(
'Local ADC credentials were not detected. Run `gcloud auth application-default login` first, then save the Gemini ADC profile again.',
{
display: 'system',
},
)
return
}
const env = buildGeminiProfileEnv({
apiKey: step.apiKey,
authMode: step.authMode,
model: value.trim() || DEFAULT_GEMINI_MODEL,
processEnv: {},
})
@@ -1293,13 +1115,7 @@ export function ProviderWizard({
finishProfileSave(onDone, 'gemini', env)
}
}}
onCancel={() =>
step.authMode === 'api-key'
? setStep({ name: 'gemini-key' })
: step.authMode === 'access-token'
? setStep({ name: 'gemini-access-token' })
: setStep({ name: 'gemini-auth-method' })
}
onCancel={() => setStep({ name: 'gemini-key' })}
/>
)
@@ -1315,34 +1131,22 @@ export function ProviderWizard({
}
export const call: LocalJSXCommandCall = async (onDone, _context, args) => {
const trimmedArgs = args?.trim().toLowerCase() ?? ''
const normalizedArgs = args?.trim().toLowerCase() || ''
if (
COMMON_HELP_ARGS.includes(trimmedArgs) ||
COMMON_INFO_ARGS.includes(trimmedArgs) ||
trimmedArgs === 'help' ||
trimmedArgs === '--help' ||
trimmedArgs === '-h'
) {
onDone(
'Run /provider to add, edit, delete, or activate provider profiles. The active provider controls base URL, model, and API key.',
{ display: 'system' },
)
return
if (COMMON_INFO_ARGS.includes(normalizedArgs)) {
onDone(buildUsageText(), { display: 'system' })
return null
}
return (
<ProviderManager
mode="manage"
onDone={result => {
const message =
result?.message ??
(result?.action === 'saved'
? 'Provider profile updated'
: 'Provider manager closed')
if (COMMON_HELP_ARGS.includes(normalizedArgs)) {
onDone(buildUsageText(), { display: 'system' })
return null
}
onDone(message, { display: 'system' })
}}
/>
)
if (normalizedArgs) {
onDone('Usage: /provider', { display: 'system' })
return null
}
return <ProviderWizard onDone={onDone} />
}

View File

@@ -118,7 +118,7 @@ export async function setupTerminal(theme: ThemeName): Promise<string> {
});
maybeMarkProjectOnboardingComplete();
// Install shell completions (internal-only, since the completion command is internal-only)
// Install shell completions (ant-only, since the completion command is ant-only)
if ("external" === 'ant') {
result += await setupShellCompletion(theme);
}

View File

@@ -52,7 +52,7 @@ const DEFAULT_INSTRUCTIONS: string = (typeof _rawPrompt === 'string' ? _rawPromp
// so the override path is DCE'd from external builds).
// Shell-set env only, so top-level process.env read is fine
// — settings.env never injects this.
/* eslint-disable custom-rules/no-process-env-top-level, custom-rules/no-sync-fs -- internal-only dev override; eager top-level read is the point (crash at startup, not silently inside the slash-command try/catch) */
/* eslint-disable custom-rules/no-process-env-top-level, custom-rules/no-sync-fs -- ant-only dev override; eager top-level read is the point (crash at startup, not silently inside the slash-command try/catch) */
const ULTRAPLAN_INSTRUCTIONS: string = "external" === 'ant' && process.env.ULTRAPLAN_PROMPT_FILE ? readFileSync(process.env.ULTRAPLAN_PROMPT_FILE, 'utf8').trimEnd() : DEFAULT_INSTRUCTIONS;
/* eslint-enable custom-rules/no-process-env-top-level, custom-rules/no-sync-fs */

View File

@@ -270,7 +270,7 @@ export function ContextVisualization(t0) {
} else {
t4 = $[53];
}
t5 = (systemTools && systemTools.length > 0 || hasDeferredBuiltinTools) && false && <Box flexDirection="column" marginTop={1}><Box><Text bold={true}>[internal] System tools</Text>{hasDeferredBuiltinTools && <Text dimColor={true}> (some loaded on-demand)</Text>}</Box><Box flexDirection="column" marginTop={1}><Text dimColor={true}>Loaded</Text>{systemTools?.map(_temp14)}{deferredBuiltinTools.filter(_temp15).map(_temp16)}</Box>{hasDeferredBuiltinTools && deferredBuiltinTools.some(_temp17) && <Box flexDirection="column" marginTop={1}><Text dimColor={true}>Available</Text>{deferredBuiltinTools.filter(_temp18).map(_temp19)}</Box>}</Box>;
t5 = (systemTools && systemTools.length > 0 || hasDeferredBuiltinTools) && false && <Box flexDirection="column" marginTop={1}><Box><Text bold={true}>[ANT-ONLY] System tools</Text>{hasDeferredBuiltinTools && <Text dimColor={true}> (some loaded on-demand)</Text>}</Box><Box flexDirection="column" marginTop={1}><Text dimColor={true}>Loaded</Text>{systemTools?.map(_temp14)}{deferredBuiltinTools.filter(_temp15).map(_temp16)}</Box>{hasDeferredBuiltinTools && deferredBuiltinTools.some(_temp17) && <Box flexDirection="column" marginTop={1}><Text dimColor={true}>Available</Text>{deferredBuiltinTools.filter(_temp18).map(_temp19)}</Box>}</Box>;
$[0] = categories;
$[1] = gridRows;
$[2] = mcpTools;
@@ -304,7 +304,7 @@ export function ContextVisualization(t0) {
}
let t10;
if ($[54] !== systemPromptSections) {
t10 = systemPromptSections && systemPromptSections.length > 0 && false && <Box flexDirection="column" marginTop={1}><Text bold={true}>[internal] System prompt sections</Text>{systemPromptSections.map(_temp20)}</Box>;
t10 = systemPromptSections && systemPromptSections.length > 0 && false && <Box flexDirection="column" marginTop={1}><Text bold={true}>[ANT-ONLY] System prompt sections</Text>{systemPromptSections.map(_temp20)}</Box>;
$[54] = systemPromptSections;
$[55] = t10;
} else {
@@ -336,7 +336,7 @@ export function ContextVisualization(t0) {
}
let t14;
if ($[62] !== messageBreakdown) {
t14 = messageBreakdown && false && <Box flexDirection="column" marginTop={1}><Text bold={true}>[internal] Message breakdown</Text><Box flexDirection="column" marginLeft={1}><Box><Text>Tool calls: </Text><Text dimColor={true}>{formatTokens(messageBreakdown.toolCallTokens)} tokens</Text></Box><Box><Text>Tool results: </Text><Text dimColor={true}>{formatTokens(messageBreakdown.toolResultTokens)} tokens</Text></Box><Box><Text>Attachments: </Text><Text dimColor={true}>{formatTokens(messageBreakdown.attachmentTokens)} tokens</Text></Box><Box><Text>Assistant messages (non-tool): </Text><Text dimColor={true}>{formatTokens(messageBreakdown.assistantMessageTokens)} tokens</Text></Box><Box><Text>User messages (non-tool-result): </Text><Text dimColor={true}>{formatTokens(messageBreakdown.userMessageTokens)} tokens</Text></Box></Box>{messageBreakdown.toolCallsByType.length > 0 && <Box flexDirection="column" marginTop={1}><Text bold={true}>[internal] Top tools</Text>{messageBreakdown.toolCallsByType.slice(0, 5).map(_temp26)}</Box>}{messageBreakdown.attachmentsByType.length > 0 && <Box flexDirection="column" marginTop={1}><Text bold={true}>[internal] Top attachments</Text>{messageBreakdown.attachmentsByType.slice(0, 5).map(_temp27)}</Box>}</Box>;
t14 = messageBreakdown && false && <Box flexDirection="column" marginTop={1}><Text bold={true}>[ANT-ONLY] Message breakdown</Text><Box flexDirection="column" marginLeft={1}><Box><Text>Tool calls: </Text><Text dimColor={true}>{formatTokens(messageBreakdown.toolCallTokens)} tokens</Text></Box><Box><Text>Tool results: </Text><Text dimColor={true}>{formatTokens(messageBreakdown.toolResultTokens)} tokens</Text></Box><Box><Text>Attachments: </Text><Text dimColor={true}>{formatTokens(messageBreakdown.attachmentTokens)} tokens</Text></Box><Box><Text>Assistant messages (non-tool): </Text><Text dimColor={true}>{formatTokens(messageBreakdown.assistantMessageTokens)} tokens</Text></Box><Box><Text>User messages (non-tool-result): </Text><Text dimColor={true}>{formatTokens(messageBreakdown.userMessageTokens)} tokens</Text></Box></Box>{messageBreakdown.toolCallsByType.length > 0 && <Box flexDirection="column" marginTop={1}><Text bold={true}>[ANT-ONLY] Top tools</Text>{messageBreakdown.toolCallsByType.slice(0, 5).map(_temp26)}</Box>}{messageBreakdown.attachmentsByType.length > 0 && <Box flexDirection="column" marginTop={1}><Text bold={true}>[ANT-ONLY] Top attachments</Text>{messageBreakdown.attachmentsByType.slice(0, 5).map(_temp27)}</Box>}</Box>;
$[62] = messageBreakdown;
$[63] = t14;
} else {

View File

@@ -35,7 +35,7 @@ export function DevBar() {
const recentOps = t1;
let t2;
if ($[3] !== recentOps) {
t2 = <Text wrap="truncate-end" color="warning">[internal] slow sync: {recentOps}</Text>;
t2 = <Text wrap="truncate-end" color="warning">[ANT-ONLY] slow sync: {recentOps}</Text>;
$[3] = recentOps;
$[4] = t2;
} else {

View File

@@ -114,7 +114,7 @@ export function HelpV2(t0) {
if (false && antOnlyCommands.length > 0) {
let t7;
if ($[26] !== antOnlyCommands || $[27] !== close || $[28] !== columns || $[29] !== maxHeight) {
t7 = <Tab key="internal-only" title="[internal-only]"><Commands commands={antOnlyCommands} maxHeight={maxHeight} columns={columns} title="Browse internal-only commands:" onCancel={close} /></Tab>;
t7 = <Tab key="ant-only" title="[ant-only]"><Commands commands={antOnlyCommands} maxHeight={maxHeight} columns={columns} title="Browse ant-only commands:" onCancel={close} /></Tab>;
$[26] = antOnlyCommands;
$[27] = close;
$[28] = columns;

View File

@@ -4,7 +4,7 @@ export function InterruptedByUser() {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <><Text dimColor={true}>Interrupted </Text>{false ? <Text dimColor={true}>· [internal] /issue to report a model issue</Text> : <Text dimColor={true}>· What should Claude do instead?</Text>}</>;
t0 = <><Text dimColor={true}>Interrupted </Text>{false ? <Text dimColor={true}>· [ANT-ONLY] /issue to report a model issue</Text> : <Text dimColor={true}>· What should Claude do instead?</Text>}</>;
$[0] = t0;
} else {
t0 = $[0];

View File

@@ -1,5 +1,5 @@
import { c as _c } from "react-compiler-runtime";
// biome-ignore-all assist/source/organizeImports: internal-only import markers must not be reordered
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
import * as React from 'react';
import { Box, Text, color } from '../../ink.js';
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
@@ -225,7 +225,7 @@ export function LogoV2() {
let t22;
if ($[25] === Symbol.for("react.memo_cache_sentinel")) {
t19 = false && !process.env.DEMO_VERSION && <Box paddingLeft={2} flexDirection="column"><Text dimColor={true}>Use /issue to report model behavior issues</Text></Box>;
t20 = false && !process.env.DEMO_VERSION && <Box paddingLeft={2} flexDirection="column"><Text color="warning">[internal] Logs:</Text><Text dimColor={true}>API calls: {getDisplayPath(getDumpPromptsPath())}</Text><Text dimColor={true}>Debug logs: {getDisplayPath(getDebugLogPath())}</Text>{isDetailedProfilingEnabled() && <Text dimColor={true}>Startup Perf: {getDisplayPath(getStartupPerfLogPath())}</Text>}</Box>;
t20 = false && !process.env.DEMO_VERSION && <Box paddingLeft={2} flexDirection="column"><Text color="warning">[ANT-ONLY] Logs:</Text><Text dimColor={true}>API calls: {getDisplayPath(getDumpPromptsPath())}</Text><Text dimColor={true}>Debug logs: {getDisplayPath(getDebugLogPath())}</Text>{isDetailedProfilingEnabled() && <Text dimColor={true}>Startup Perf: {getDisplayPath(getStartupPerfLogPath())}</Text>}</Box>;
t21 = false && <GateOverridesWarning />;
t22 = false && <ExperimentEnrollmentNotice />;
$[25] = t19;
@@ -502,7 +502,7 @@ export function LogoV2() {
let t40;
if ($[86] === Symbol.for("react.memo_cache_sentinel")) {
t37 = false && !process.env.DEMO_VERSION && <Box paddingLeft={2} flexDirection="column"><Text dimColor={true}>Use /issue to report model behavior issues</Text></Box>;
t38 = false && !process.env.DEMO_VERSION && <Box paddingLeft={2} flexDirection="column"><Text color="warning">[internal] Logs:</Text><Text dimColor={true}>API calls: {getDisplayPath(getDumpPromptsPath())}</Text><Text dimColor={true}>Debug logs: {getDisplayPath(getDebugLogPath())}</Text>{isDetailedProfilingEnabled() && <Text dimColor={true}>Startup Perf: {getDisplayPath(getStartupPerfLogPath())}</Text>}</Box>;
t38 = false && !process.env.DEMO_VERSION && <Box paddingLeft={2} flexDirection="column"><Text color="warning">[ANT-ONLY] Logs:</Text><Text dimColor={true}>API calls: {getDisplayPath(getDumpPromptsPath())}</Text><Text dimColor={true}>Debug logs: {getDisplayPath(getDebugLogPath())}</Text>{isDetailedProfilingEnabled() && <Text dimColor={true}>Startup Perf: {getDisplayPath(getStartupPerfLogPath())}</Text>}</Box>;
t39 = false && <GateOverridesWarning />;
t40 = false && <ExperimentEnrollmentNotice />;
$[86] = t37;

View File

@@ -41,7 +41,7 @@ export function createWhatsNewFeed(releaseNotes: string[]): FeedConfig {
});
const emptyMessage = "external" === 'ant' ? 'Unable to fetch latest claude-cli-internal commits' : 'Check /release-notes for recent updates';
return {
title: "external" === 'ant' ? "Open Claude Updates [internal-only: Latest CC commits]" : "Open Claude Updates",
title: "external" === 'ant' ? "Open Claude Updates [ANT-ONLY: Latest CC commits]" : "Open Claude Updates",
lines,
footer: lines.length > 0 ? '/release-notes for more' : undefined,
emptyMessage

View File

@@ -1,5 +1,5 @@
/**
* internal-only: Banner shown in the transcript that prompts users to report
* ANT-ONLY: Banner shown in the transcript that prompts users to report
* issues via /issue. Appears when friction is detected in the conversation.
*/
export function IssueFlagBanner() {

View File

@@ -13,7 +13,6 @@ import { getCwd } from 'src/utils/cwd.js';
import { isQueuedCommandEditable, popAllEditable } from 'src/utils/messageQueueManager.js';
import stripAnsi from 'strip-ansi';
import { companionReservedColumns } from '../../buddy/CompanionSprite.js';
import { isBuddyEnabled } from '../../buddy/feature.js';
import { findBuddyTriggerPositions, useBuddyNotification } from '../../buddy/useBuddyNotification.js';
import { FastModePicker } from '../../commands/fast/fast.js';
import { isUltrareviewEnabled } from '../../commands/review/ultrareviewEnabled.js';
@@ -67,7 +66,6 @@ import { isBilledAsExtraUsage } from '../../utils/extraUsage.js';
import { getFastModeUnavailableReason, isFastModeAvailable, isFastModeCooldown, isFastModeEnabled, isFastModeSupportedByModel } from '../../utils/fastMode.js';
import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js';
import type { PromptInputHelpers } from '../../utils/handlePromptSubmit.js';
import { extractDraggedFilePaths } from '../../utils/dragDropPaths.js';
import { getImageFromClipboard, PASTE_THRESHOLD } from '../../utils/imagePaste.js';
import type { ImageDimensions } from '../../utils/imageResizer.js';
import { cacheImagePath, storeImage } from '../../utils/imageStore.js';
@@ -295,7 +293,7 @@ function PromptInput({
// the pill returns null for implicit-and-not-reconnecting, so nav must too,
// otherwise bridge becomes an invisible selection stop.
const bridgeFooterVisible = replBridgeConnected && (replBridgeExplicit || replBridgeReconnecting);
// Tmux pill (internal-only) — visible when there's an active tungsten session
// Tmux pill (ant-only) — visible when there's an active tungsten session
const hasTungstenSession = useAppState(s => "external" === 'ant' && s.tungstenActiveSession !== undefined);
const tmuxFooterVisible = "external" === 'ant' && hasTungstenSession;
// WebBrowser pill — visible when a browser is open
@@ -311,7 +309,7 @@ function PromptInput({
const {
companion: _companion,
companionMuted
} = isBuddyEnabled() ? getGlobalConfig() : {
} = feature('BUDDY') ? getGlobalConfig() : {
companion: undefined,
companionMuted: undefined
};
@@ -1205,22 +1203,6 @@ function PromptInput({
// Clean up pasted text - strip ANSI escape codes and normalize line endings and tabs
let text = stripAnsi(rawText).replace(/\r/g, '\n').replaceAll('\t', ' ');
// Detect file paths from drag-and-drop and convert to @mentions.
// When files are dragged into the terminal, the terminal sends their
// absolute paths via bracketed paste. Image files are handled by the
// image paste handler upstream; here we handle non-image files by
// converting them to @mentions so they get attached on submit.
const draggedPaths = extractDraggedFilePaths(text);
if (draggedPaths.length > 0) {
const mentions = draggedPaths
.map(p => (p.includes(' ') || p.includes(':') ? `@"${p}"` : `@${p}`))
.join(' ');
// Ensure spacing around the mention(s) relative to existing input
const charBefore = input[cursorOffset - 1];
const prefix = charBefore && !/\s/.test(charBefore) ? ' ' : '';
text = prefix + mentions + ' ';
}
// Match typed/auto-suggest: `!cmd` pasted into empty input enters bash mode.
if (input.length === 0) {
const pastedMode = getModeFromInput(text);
@@ -1262,23 +1244,12 @@ function PromptInput({
if (isNonSpacePrintable(input, key)) return ' ' + input;
return input;
}, []);
// Ref mirrors cursorOffset for use in synchronous loops (e.g. multi-image
// paste) where React batches state updates and the closure value is stale.
const cursorOffsetRef = useRef(cursorOffset);
cursorOffsetRef.current = cursorOffset;
function insertTextAtCursor(text: string) {
// Use refs for input/cursor so back-to-back calls in the same event
// (e.g. onImagePaste loop for multiple dragged images) chain correctly
// instead of each reading the same stale closure values.
const currentInput = lastInternalInputRef.current;
const currentOffset = cursorOffsetRef.current;
pushToBuffer(currentInput, currentOffset, pastedContents);
const newInput = currentInput.slice(0, currentOffset) + text + currentInput.slice(currentOffset);
// Push current state to buffer before inserting
pushToBuffer(input, cursorOffset, pastedContents);
const newInput = input.slice(0, cursorOffset) + text + input.slice(cursorOffset);
trackAndSetInput(newInput);
const newOffset = currentOffset + text.length;
cursorOffsetRef.current = newOffset;
setCursorOffset(newOffset);
setCursorOffset(cursorOffset + text.length);
}
const doublePressEscFromEmpty = useDoublePress(() => {}, () => onShowMessageSelector());
@@ -1815,7 +1786,7 @@ function PromptInput({
}
switch (footerItemSelected) {
case 'companion':
if (isBuddyEnabled()) {
if (feature('BUDDY')) {
selectFooterItem(null);
void onSubmit('/buddy');
}
@@ -2010,7 +1981,8 @@ function PromptInput({
});
}, [effortNotificationText, addNotification, removeNotification]);
useBuddyNotification();
const companionSpeaking = isBuddyEnabled() ?
const companionSpeaking = feature('BUDDY') ?
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
useAppState(s => s.companionReaction !== undefined) : false;
const {
columns,
@@ -2181,7 +2153,7 @@ function PromptInput({
}} onCancel={() => setShowHistoryPicker(false)} />;
}
// Show loop mode menu when requested (internal-only, eliminated from external builds)
// Show loop mode menu when requested (ant-only, eliminated from external builds)
if (modelPickerElement) {
return modelPickerElement;
}

View File

@@ -1,5 +1,5 @@
import { c as _c } from "react-compiler-runtime";
// biome-ignore-all assist/source/organizeImports: internal-only import markers must not be reordered
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
import { feature } from 'bun:bundle';
// Dead code elimination: conditional import for COORDINATOR_MODE
/* eslint-disable @typescript-eslint/no-require-imports */
@@ -364,7 +364,7 @@ function ModeIndicator({
// BackgroundTaskStatus is NOT in parts — it renders as a Box sibling so
// its click-target Box isn't nested inside the <Text wrap="truncate">
// wrapper (reconciler throws on Box-in-Text).
// Tmux pill (internal-only) — appears right after tasks in nav order
// Tmux pill (ant-only) — appears right after tasks in nav order
...("external" === 'ant' && hasTmuxSession ? [<TungstenPill key="tmux" selected={tmuxSelected} />] : []), ...(isAgentSwarmsEnabled() && hasTeams ? [<TeamStatus key="teams" teamsSelected={teamsSelected} showHint={showHint && !hasBackgroundTasks} />] : []), ...(shouldShowPrStatus ? [<PrBadge key="pr-status" number={prStatus.number!} url={prStatus.url!} reviewState={prStatus.reviewState!} />] : [])];
// Check if any in-process teammates exist (for hint text cycling)

View File

@@ -123,6 +123,8 @@ const SuggestionItemRow = memo(function SuggestionItemRow({
maxColumnWidth ?? stringWidth(item.displayText) + 5,
maxNameWidth,
)
const displayTextColor = isSelected ? 'inverseText' : item.color
const shouldDim = !isSelected
let displayText = item.displayText
if (stringWidth(displayText) > displayTextWidth - 2) {
@@ -142,17 +144,21 @@ const SuggestionItemRow = memo(function SuggestionItemRow({
const truncatedDescription = item.description
? truncateToWidth(item.description.replace(/\s+/g, ' '), descriptionWidth)
: ''
const lineContent = `${paddedDisplayText}${tagText}${truncatedDescription}`
return (
<Box width="100%" opaque={true} backgroundColor={rowBackgroundColor}>
<Text
color={textColor}
dimColor={!isSelected}
bold={isSelected}
wrap="truncate"
>
{lineContent}
<Text wrap="truncate">
<Text color={displayTextColor} dimColor={shouldDim} bold={isSelected}>
{paddedDisplayText}
</Text>
{tagText ? (
<Text color={textColor} dimColor={!isSelected}>
{tagText}
</Text>
) : null}
<Text color={textColor} dimColor={!isSelected}>
{truncatedDescription}
</Text>
</Text>
</Box>
)

View File

@@ -1,35 +0,0 @@
import React from 'react'
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test'
import { renderToString } from '../../utils/staticRender.js'
describe('PromptInputQueuedCommands', () => {
beforeEach(() => {
mock.module('../../hooks/useCommandQueue.js', () => ({
useCommandQueue: () => [
{
value: 'Use another library',
mode: 'prompt',
},
],
}))
mock.module('src/state/AppState.js', () => ({
useAppState: (
selector: (state: { viewingAgentTaskId?: string; isBriefOnly: boolean }) => unknown,
) => selector({ viewingAgentTaskId: undefined, isBriefOnly: false }),
}))
})
afterEach(() => {
mock.restore()
})
it('shows a next-turn guidance banner for queued prompt messages', async () => {
const { PromptInputQueuedCommands } = await import('./PromptInputQueuedCommands.js')
const output = await renderToString(<PromptInputQueuedCommands />, 100)
expect(output).toContain('1 message queued for next turn')
expect(output).toContain('Use another library')
})
})

View File

@@ -1,14 +1,13 @@
import { feature } from 'bun:bundle';
import * as React from 'react';
import { useMemo } from 'react';
import { Box, Text } from 'src/ink.js';
import { Box } from 'src/ink.js';
import { useAppState } from 'src/state/AppState.js';
import type { AppState } from 'src/state/AppState.js';
import { STATUS_TAG, SUMMARY_TAG, TASK_NOTIFICATION_TAG } from '../../constants/xml.js';
import { QueuedMessageProvider } from '../../context/QueuedMessageContext.js';
import { useCommandQueue } from '../../hooks/useCommandQueue.js';
import type { QueuedCommand } from '../../types/textInputTypes.js';
import { isQueuedCommandEditable, isQueuedCommandVisible } from '../../utils/messageQueueManager.js';
import { isQueuedCommandVisible } from '../../utils/messageQueueManager.js';
import { createUserMessage, EMPTY_LOOKUPS, normalizeMessages } from '../../utils/messages.js';
import { jsonParse } from '../../utils/slowOperations.js';
import { Message } from '../Message.js';
@@ -71,25 +70,17 @@ function processQueuedCommands(queuedCommands: QueuedCommand[]): QueuedCommand[]
}
function PromptInputQueuedCommandsImpl(): React.ReactNode {
const queuedCommands = useCommandQueue();
const viewingAgent = useAppState((s: AppState) => !!s.viewingAgentTaskId);
const viewingAgent = useAppState(s => !!s.viewingAgentTaskId);
// Brief layout: dim queue items + skip the paddingX (brief messages
// already indent themselves). Gate mirrors the brief-spinner/message
// check elsewhere — no teammate-view override needed since this
// component early-returns when viewing a teammate.
const useBriefLayout = feature('KAIROS') || feature('KAIROS_BRIEF') ?
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
useAppState((s_0: AppState) => s_0.isBriefOnly) : false;
useAppState(s_0 => s_0.isBriefOnly) : false;
// createUserMessage mints a fresh UUID per call; without memoization, streaming
// re-renders defeat Message's areMessagePropsEqual (compares uuid) → flicker.
const queuedPromptCount = useMemo(
() =>
queuedCommands.filter(
cmd => isQueuedCommandEditable(cmd) && cmd.mode === 'prompt',
).length,
[queuedCommands],
);
const messages = useMemo(() => {
if (queuedCommands.length === 0) return null;
// task-notification is shown via useInboxNotification; most isMeta commands
@@ -117,11 +108,6 @@ function PromptInputQueuedCommandsImpl(): React.ReactNode {
return null;
}
return <Box marginTop={1} flexDirection="column">
{queuedPromptCount > 0 && <Box marginLeft={2} marginBottom={1}>
<Text dimColor>
{queuedPromptCount === 1 ? '1 message queued for next turn' : `${queuedPromptCount} messages queued for next turn`}
</Text>
</Box>}
{messages.map((message, i) => <QueuedMessageProvider key={i} isFirst={i === 0} useBriefLayout={useBriefLayout}>
<Message message={message} lookups={EMPTY_LOOKUPS} addMargin={false} tools={[]} commands={[]} verbose={false} inProgressToolUseIDs={EMPTY_SET} progressMessagesForMessage={[]} shouldAnimate={false} shouldShowDot={false} isTranscriptMode={false} isStatic={true} />
</QueuedMessageProvider>)}

View File

@@ -1,860 +0,0 @@
import figures from 'figures'
import * as React from 'react'
import { Box, Text } from '../ink.js'
import { useKeybinding } from '../keybindings/useKeybinding.js'
import type { ProviderProfile } from '../utils/config.js'
import {
addProviderProfile,
applyActiveProviderProfileFromConfig,
deleteProviderProfile,
getActiveProviderProfile,
getProviderPresetDefaults,
getProviderProfiles,
setActiveProviderProfile,
type ProviderPreset,
type ProviderProfileInput,
updateProviderProfile,
} from '../utils/providerProfiles.js'
import {
clearGithubModelsToken,
GITHUB_MODELS_HYDRATED_ENV_MARKER,
hydrateGithubModelsTokenFromSecureStorage,
readGithubModelsToken,
} from '../utils/githubModelsCredentials.js'
import { isEnvTruthy } from '../utils/envUtils.js'
import { updateSettingsForSource } from '../utils/settings/settings.js'
import { Select } from './CustomSelect/index.js'
import { Pane } from './design-system/Pane.js'
import TextInput from './TextInput.js'
export type ProviderManagerResult = {
action: 'saved' | 'cancelled'
activeProfileId?: string
message?: string
}
type Props = {
mode: 'first-run' | 'manage'
onDone: (result?: ProviderManagerResult) => void
}
type Screen =
| 'menu'
| 'select-preset'
| 'form'
| 'select-active'
| 'select-edit'
| 'select-delete'
type DraftField = 'name' | 'baseUrl' | 'model' | 'apiKey'
type ProviderDraft = Record<DraftField, string>
const FORM_STEPS: Array<{
key: DraftField
label: string
placeholder: string
helpText: string
optional?: boolean
}> = [
{
key: 'name',
label: 'Provider name',
placeholder: 'e.g. Ollama Home, OpenAI Work',
helpText: 'A short label shown in /provider and startup setup.',
},
{
key: 'baseUrl',
label: 'Base URL',
placeholder: 'e.g. http://localhost:11434/v1',
helpText: 'API base URL used for this provider profile.',
},
{
key: 'model',
label: 'Default model',
placeholder: 'e.g. llama3.1:8b',
helpText: 'Model name to use when this provider is active.',
},
{
key: 'apiKey',
label: 'API key',
placeholder: 'Leave empty if your provider does not require one',
helpText: 'Optional. Press Enter with empty value to skip.',
optional: true,
},
]
const GITHUB_PROVIDER_ID = '__github_models__'
const GITHUB_PROVIDER_LABEL = 'GitHub Models'
const GITHUB_PROVIDER_DEFAULT_MODEL = 'github:copilot'
const GITHUB_PROVIDER_DEFAULT_BASE_URL = 'https://models.github.ai/inference'
type GithubCredentialSource = 'stored' | 'env' | 'none'
function toDraft(profile: ProviderProfile): ProviderDraft {
return {
name: profile.name,
baseUrl: profile.baseUrl,
model: profile.model,
apiKey: profile.apiKey ?? '',
}
}
function presetToDraft(preset: ProviderPreset): ProviderDraft {
const defaults = getProviderPresetDefaults(preset)
return {
name: defaults.name,
baseUrl: defaults.baseUrl,
model: defaults.model,
apiKey: defaults.apiKey ?? '',
}
}
function profileSummary(profile: ProviderProfile, isActive: boolean): string {
const activeSuffix = isActive ? ' (active)' : ''
const keyInfo = profile.apiKey ? 'key set' : 'no key'
const providerKind =
profile.provider === 'anthropic' ? 'anthropic' : 'openai-compatible'
return `${providerKind} · ${profile.baseUrl} · ${profile.model} · ${keyInfo}${activeSuffix}`
}
function getGithubCredentialSource(
processEnv: NodeJS.ProcessEnv = process.env,
): GithubCredentialSource {
if (readGithubModelsToken()?.trim()) {
return 'stored'
}
if (processEnv.GITHUB_TOKEN?.trim() || processEnv.GH_TOKEN?.trim()) {
return 'env'
}
return 'none'
}
function isGithubProviderAvailable(
processEnv: NodeJS.ProcessEnv = process.env,
): boolean {
if (isEnvTruthy(processEnv.CLAUDE_CODE_USE_GITHUB)) {
return true
}
return getGithubCredentialSource(processEnv) !== 'none'
}
function getGithubProviderModel(
processEnv: NodeJS.ProcessEnv = process.env,
): string {
if (isEnvTruthy(processEnv.CLAUDE_CODE_USE_GITHUB)) {
return processEnv.OPENAI_MODEL?.trim() || GITHUB_PROVIDER_DEFAULT_MODEL
}
return GITHUB_PROVIDER_DEFAULT_MODEL
}
function getGithubProviderSummary(
isActive: boolean,
credentialSource: GithubCredentialSource,
processEnv: NodeJS.ProcessEnv = process.env,
): string {
const credentialSummary =
credentialSource === 'stored'
? 'token stored'
: credentialSource === 'env'
? 'token via env'
: 'no token found'
const activeSuffix = isActive ? ' (active)' : ''
return `github-models · ${GITHUB_PROVIDER_DEFAULT_BASE_URL} · ${getGithubProviderModel(processEnv)} · ${credentialSummary}${activeSuffix}`
}
export function ProviderManager({ mode, onDone }: Props): React.ReactNode {
const [profiles, setProfiles] = React.useState(() => getProviderProfiles())
const [activeProfileId, setActiveProfileId] = React.useState(
() => getActiveProviderProfile()?.id,
)
const [githubProviderAvailable, setGithubProviderAvailable] = React.useState(() =>
isGithubProviderAvailable(),
)
const [githubCredentialSource, setGithubCredentialSource] = React.useState<GithubCredentialSource>(
() => getGithubCredentialSource(),
)
const [isGithubActive, setIsGithubActive] = React.useState(() =>
isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB),
)
const [screen, setScreen] = React.useState<Screen>(
mode === 'first-run' ? 'select-preset' : 'menu',
)
const [editingProfileId, setEditingProfileId] = React.useState<string | null>(null)
const [draftProvider, setDraftProvider] = React.useState<ProviderProfile['provider']>(
'openai',
)
const [draft, setDraft] = React.useState<ProviderDraft>(() =>
presetToDraft('ollama'),
)
const [formStepIndex, setFormStepIndex] = React.useState(0)
const [cursorOffset, setCursorOffset] = React.useState(0)
const [statusMessage, setStatusMessage] = React.useState<string | undefined>()
const [errorMessage, setErrorMessage] = React.useState<string | undefined>()
const currentStep = FORM_STEPS[formStepIndex] ?? FORM_STEPS[0]
const currentStepKey = currentStep.key
const currentValue = draft[currentStepKey]
function refreshProfiles(): void {
const nextProfiles = getProviderProfiles()
setProfiles(nextProfiles)
setActiveProfileId(getActiveProviderProfile()?.id)
setGithubProviderAvailable(isGithubProviderAvailable())
setGithubCredentialSource(getGithubCredentialSource())
setIsGithubActive(isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB))
}
function clearStartupProviderOverrideFromUserSettings(): string | null {
const { error } = updateSettingsForSource('userSettings', {
env: {
CLAUDE_CODE_USE_OPENAI: undefined as any,
CLAUDE_CODE_USE_GEMINI: undefined as any,
CLAUDE_CODE_USE_GITHUB: undefined as any,
CLAUDE_CODE_USE_BEDROCK: undefined as any,
CLAUDE_CODE_USE_VERTEX: undefined as any,
CLAUDE_CODE_USE_FOUNDRY: undefined as any,
},
})
return error ? error.message : null
}
function closeWithCancelled(message: string): void {
onDone({ action: 'cancelled', message })
}
function activateGithubProvider(): string | null {
const { error } = updateSettingsForSource('userSettings', {
env: {
CLAUDE_CODE_USE_GITHUB: '1',
OPENAI_MODEL: GITHUB_PROVIDER_DEFAULT_MODEL,
OPENAI_API_KEY: undefined as any,
OPENAI_ORG: undefined as any,
OPENAI_PROJECT: undefined as any,
OPENAI_ORGANIZATION: undefined as any,
OPENAI_BASE_URL: undefined as any,
OPENAI_API_BASE: undefined as any,
CLAUDE_CODE_USE_OPENAI: undefined as any,
CLAUDE_CODE_USE_GEMINI: undefined as any,
CLAUDE_CODE_USE_BEDROCK: undefined as any,
CLAUDE_CODE_USE_VERTEX: undefined as any,
CLAUDE_CODE_USE_FOUNDRY: undefined as any,
},
})
if (error) {
return error.message
}
process.env.CLAUDE_CODE_USE_GITHUB = '1'
process.env.OPENAI_MODEL = GITHUB_PROVIDER_DEFAULT_MODEL
delete process.env.OPENAI_API_KEY
delete process.env.OPENAI_ORG
delete process.env.OPENAI_PROJECT
delete process.env.OPENAI_ORGANIZATION
delete process.env.OPENAI_BASE_URL
delete process.env.OPENAI_API_BASE
delete process.env.CLAUDE_CODE_USE_OPENAI
delete process.env.CLAUDE_CODE_USE_GEMINI
delete process.env.CLAUDE_CODE_USE_BEDROCK
delete process.env.CLAUDE_CODE_USE_VERTEX
delete process.env.CLAUDE_CODE_USE_FOUNDRY
delete process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED
delete process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID
delete process.env[GITHUB_MODELS_HYDRATED_ENV_MARKER]
hydrateGithubModelsTokenFromSecureStorage()
return null
}
function deleteGithubProvider(): string | null {
const storedTokenBeforeClear = readGithubModelsToken()?.trim()
const cleared = clearGithubModelsToken()
if (!cleared.success) {
return cleared.warning ?? 'Could not clear GitHub credentials.'
}
const { error } = updateSettingsForSource('userSettings', {
env: {
CLAUDE_CODE_USE_GITHUB: undefined as any,
OPENAI_MODEL: undefined as any,
OPENAI_BASE_URL: undefined as any,
OPENAI_API_BASE: undefined as any,
},
})
if (error) {
return error.message
}
const hydratedTokenInSession = process.env.GITHUB_TOKEN?.trim()
if (
process.env[GITHUB_MODELS_HYDRATED_ENV_MARKER] === '1' &&
hydratedTokenInSession &&
(!storedTokenBeforeClear || hydratedTokenInSession === storedTokenBeforeClear)
) {
delete process.env.GITHUB_TOKEN
}
delete process.env.CLAUDE_CODE_USE_GITHUB
delete process.env[GITHUB_MODELS_HYDRATED_ENV_MARKER]
delete process.env.OPENAI_MODEL
delete process.env.OPENAI_API_KEY
delete process.env.OPENAI_ORG
delete process.env.OPENAI_PROJECT
delete process.env.OPENAI_ORGANIZATION
delete process.env.OPENAI_BASE_URL
delete process.env.OPENAI_API_BASE
// Restore active provider profile immediately when one exists.
applyActiveProviderProfileFromConfig()
return null
}
function startCreateFromPreset(preset: ProviderPreset): void {
const defaults = getProviderPresetDefaults(preset)
const nextDraft = {
name: defaults.name,
baseUrl: defaults.baseUrl,
model: defaults.model,
apiKey: defaults.apiKey ?? '',
}
setEditingProfileId(null)
setDraftProvider(defaults.provider ?? 'openai')
setDraft(nextDraft)
setFormStepIndex(0)
setCursorOffset(nextDraft.name.length)
setErrorMessage(undefined)
setScreen('form')
}
function startEditProfile(profileId: string): void {
const existing = profiles.find(profile => profile.id === profileId)
if (!existing) {
return
}
const nextDraft = toDraft(existing)
setEditingProfileId(profileId)
setDraftProvider(existing.provider ?? 'openai')
setDraft(nextDraft)
setFormStepIndex(0)
setCursorOffset(nextDraft.name.length)
setErrorMessage(undefined)
setScreen('form')
}
function persistDraft(): void {
const payload: ProviderProfileInput = {
provider: draftProvider,
name: draft.name,
baseUrl: draft.baseUrl,
model: draft.model,
apiKey: draft.apiKey,
}
const saved = editingProfileId
? updateProviderProfile(editingProfileId, payload)
: addProviderProfile(payload, { makeActive: true })
if (!saved) {
setErrorMessage('Could not save provider. Fill all required fields.')
return
}
const isActiveSavedProfile = getActiveProviderProfile()?.id === saved.id
const settingsOverrideError = isActiveSavedProfile
? clearStartupProviderOverrideFromUserSettings()
: null
refreshProfiles()
const successMessage =
editingProfileId
? `Updated provider: ${saved.name}`
: `Added provider: ${saved.name} (now active)`
setStatusMessage(
settingsOverrideError
? `${successMessage}. Warning: could not clear startup provider override (${settingsOverrideError}).`
: successMessage,
)
if (mode === 'first-run') {
onDone({
action: 'saved',
activeProfileId: saved.id,
message: `Provider configured: ${saved.name}`,
})
return
}
setEditingProfileId(null)
setFormStepIndex(0)
setErrorMessage(undefined)
setScreen('menu')
}
function handleFormSubmit(value: string): void {
const trimmed = value.trim()
if (!currentStep.optional && trimmed.length === 0) {
setErrorMessage(`${currentStep.label} is required.`)
return
}
const nextDraft = {
...draft,
[currentStepKey]: trimmed,
}
setDraft(nextDraft)
setErrorMessage(undefined)
if (formStepIndex < FORM_STEPS.length - 1) {
const nextIndex = formStepIndex + 1
const nextKey = FORM_STEPS[nextIndex]?.key ?? 'name'
setFormStepIndex(nextIndex)
setCursorOffset(nextDraft[nextKey].length)
return
}
persistDraft()
}
function handleBackFromForm(): void {
setErrorMessage(undefined)
if (formStepIndex > 0) {
const nextIndex = formStepIndex - 1
const nextKey = FORM_STEPS[nextIndex]?.key ?? 'name'
setFormStepIndex(nextIndex)
setCursorOffset(draft[nextKey].length)
return
}
if (mode === 'first-run') {
setScreen('select-preset')
return
}
setScreen('menu')
}
useKeybinding('confirm:no', handleBackFromForm, {
context: 'Settings',
isActive: screen === 'form',
})
function renderPresetSelection(): React.ReactNode {
const options = [
{
value: 'anthropic',
label: 'Anthropic',
description: 'Native Claude API (x-api-key auth)',
},
{
value: 'ollama',
label: 'Ollama',
description: 'Local or remote Ollama endpoint',
},
{
value: 'openai',
label: 'OpenAI',
description: 'OpenAI API with API key',
},
{
value: 'moonshotai',
label: 'Moonshot AI',
description: 'Kimi OpenAI-compatible endpoint',
},
{
value: 'deepseek',
label: 'DeepSeek',
description: 'DeepSeek OpenAI-compatible endpoint',
},
{
value: 'gemini',
label: 'Google Gemini',
description: 'Gemini OpenAI-compatible endpoint',
},
{
value: 'together',
label: 'Together AI',
description: 'Together chat/completions endpoint',
},
{
value: 'groq',
label: 'Groq',
description: 'Groq OpenAI-compatible endpoint',
},
{
value: 'mistral',
label: 'Mistral',
description: 'Mistral OpenAI-compatible endpoint',
},
{
value: 'azure-openai',
label: 'Azure OpenAI',
description: 'Azure OpenAI endpoint (model=deployment name)',
},
{
value: 'openrouter',
label: 'OpenRouter',
description: 'OpenRouter OpenAI-compatible endpoint',
},
{
value: 'lmstudio',
label: 'LM Studio',
description: 'Local LM Studio endpoint',
},
{
value: 'custom',
label: 'Custom',
description: 'Any OpenAI-compatible provider',
},
...(mode === 'first-run'
? [
{
value: 'skip',
label: 'Skip for now',
description: 'Continue with current defaults',
},
]
: []),
]
return (
<Box flexDirection="column" gap={1}>
<Text color="remember" bold>
{mode === 'first-run' ? 'Set up provider' : 'Choose provider preset'}
</Text>
<Text dimColor>
Pick a preset, then confirm base URL, model, and API key.
</Text>
<Select
options={options}
onChange={value => {
if (value === 'skip') {
closeWithCancelled('Provider setup skipped')
return
}
startCreateFromPreset(value as ProviderPreset)
}}
onCancel={() => {
if (mode === 'first-run') {
closeWithCancelled('Provider setup skipped')
return
}
setScreen('menu')
}}
visibleOptionCount={Math.min(12, options.length)}
/>
</Box>
)
}
function renderForm(): React.ReactNode {
return (
<Box flexDirection="column" gap={1}>
<Text color="remember" bold>
{editingProfileId ? 'Edit provider profile' : 'Create provider profile'}
</Text>
<Text dimColor>{currentStep.helpText}</Text>
<Text dimColor>
Provider type:{' '}
{draftProvider === 'anthropic'
? 'Anthropic native API'
: 'OpenAI-compatible API'}
</Text>
<Text dimColor>
Step {formStepIndex + 1} of {FORM_STEPS.length}: {currentStep.label}
</Text>
<Box flexDirection="row" gap={1}>
<Text>{figures.pointer}</Text>
<TextInput
value={currentValue}
onChange={value =>
setDraft(prev => ({
...prev,
[currentStepKey]: value,
}))
}
onSubmit={handleFormSubmit}
focus={true}
showCursor={true}
placeholder={`${currentStep.placeholder}${figures.ellipsis}`}
columns={80}
cursorOffset={cursorOffset}
onChangeCursorOffset={setCursorOffset}
/>
</Box>
{errorMessage && <Text color="error">{errorMessage}</Text>}
<Text dimColor>
Press Enter to continue. Press Esc to go back.
</Text>
</Box>
)
}
function renderMenu(): React.ReactNode {
const hasProfiles = profiles.length > 0
const hasSelectableProviders = hasProfiles || githubProviderAvailable
const options = [
{
value: 'add',
label: 'Add provider',
description: 'Create a new provider profile',
},
{
value: 'activate',
label: 'Set active provider',
description: 'Switch the active provider profile',
disabled: !hasSelectableProviders,
},
{
value: 'edit',
label: 'Edit provider',
description: 'Update URL, model, or key',
disabled: !hasProfiles,
},
{
value: 'delete',
label: 'Delete provider',
description: 'Remove a provider profile',
disabled: !hasSelectableProviders,
},
{
value: 'done',
label: 'Done',
description: 'Return to chat',
},
]
return (
<Box flexDirection="column" gap={1}>
<Text color="remember" bold>
Provider manager
</Text>
<Text dimColor>
Active profile controls base URL, model, and API key used by this session.
</Text>
{statusMessage && <Text>{statusMessage}</Text>}
<Box flexDirection="column">
{profiles.length === 0 && !githubProviderAvailable ? (
<Text dimColor>No provider profiles configured yet.</Text>
) : (
<>
{profiles.map(profile => (
<Text key={profile.id} dimColor>
- {profile.name}: {profileSummary(profile, profile.id === activeProfileId)}
</Text>
))}
{githubProviderAvailable ? (
<Text dimColor>
- {GITHUB_PROVIDER_LABEL}:{' '}
{getGithubProviderSummary(
isGithubActive,
githubCredentialSource,
)}
</Text>
) : null}
</>
)}
</Box>
<Select
options={options}
onChange={value => {
setErrorMessage(undefined)
switch (value) {
case 'add':
setScreen('select-preset')
break
case 'activate':
if (hasSelectableProviders) {
setScreen('select-active')
}
break
case 'edit':
if (profiles.length > 0) {
setScreen('select-edit')
}
break
case 'delete':
if (hasSelectableProviders) {
setScreen('select-delete')
}
break
default:
closeWithCancelled('Provider manager closed')
break
}
}}
onCancel={() => closeWithCancelled('Provider manager closed')}
visibleOptionCount={options.length}
/>
</Box>
)
}
function renderProfileSelection(
title: string,
emptyMessage: string,
onSelect: (profileId: string) => void,
options?: { includeGithub?: boolean },
): React.ReactNode {
const includeGithub = options?.includeGithub ?? false
const selectOptions = profiles.map(profile => ({
value: profile.id,
label:
profile.id === activeProfileId
? `${profile.name} (active)`
: profile.name,
description: `${profile.provider === 'anthropic' ? 'anthropic' : 'openai-compatible'} · ${profile.baseUrl} · ${profile.model}`,
}))
if (includeGithub && githubProviderAvailable) {
selectOptions.push({
value: GITHUB_PROVIDER_ID,
label: isGithubActive
? `${GITHUB_PROVIDER_LABEL} (active)`
: GITHUB_PROVIDER_LABEL,
description: `github-models · ${GITHUB_PROVIDER_DEFAULT_BASE_URL} · ${getGithubProviderModel()}`,
})
}
if (selectOptions.length === 0) {
return (
<Box flexDirection="column" gap={1}>
<Text color="remember" bold>
{title}
</Text>
<Text dimColor>{emptyMessage}</Text>
<Select
options={[
{
value: 'back',
label: 'Back',
description: 'Return to provider manager',
},
]}
onChange={() => setScreen('menu')}
onCancel={() => setScreen('menu')}
visibleOptionCount={1}
/>
</Box>
)
}
return (
<Box flexDirection="column" gap={1}>
<Text color="remember" bold>
{title}
</Text>
<Select
options={selectOptions}
onChange={onSelect}
onCancel={() => setScreen('menu')}
visibleOptionCount={Math.min(10, Math.max(2, selectOptions.length))}
/>
</Box>
)
}
let content: React.ReactNode
switch (screen) {
case 'select-preset':
content = renderPresetSelection()
break
case 'form':
content = renderForm()
break
case 'select-active':
content = renderProfileSelection(
'Set active provider',
'No providers available. Add one first.',
profileId => {
if (profileId === GITHUB_PROVIDER_ID) {
const githubError = activateGithubProvider()
if (githubError) {
setErrorMessage(`Could not activate GitHub provider: ${githubError}`)
setScreen('menu')
return
}
refreshProfiles()
setStatusMessage(`Active provider: ${GITHUB_PROVIDER_LABEL}`)
setScreen('menu')
return
}
const active = setActiveProviderProfile(profileId)
if (!active) {
setErrorMessage('Could not change active provider.')
setScreen('menu')
return
}
const settingsOverrideError =
clearStartupProviderOverrideFromUserSettings()
refreshProfiles()
setStatusMessage(
settingsOverrideError
? `Active provider: ${active.name}. Warning: could not clear startup provider override (${settingsOverrideError}).`
: `Active provider: ${active.name}`,
)
setScreen('menu')
},
{ includeGithub: true },
)
break
case 'select-edit':
content = renderProfileSelection(
'Edit provider',
'No providers available. Add one first.',
profileId => {
startEditProfile(profileId)
},
)
break
case 'select-delete':
content = renderProfileSelection(
'Delete provider',
'No providers available. Add one first.',
profileId => {
if (profileId === GITHUB_PROVIDER_ID) {
const githubDeleteError = deleteGithubProvider()
if (githubDeleteError) {
setErrorMessage(`Could not delete GitHub provider: ${githubDeleteError}`)
} else {
refreshProfiles()
setStatusMessage('GitHub provider deleted')
}
setScreen('menu')
return
}
const result = deleteProviderProfile(profileId)
if (!result.removed) {
setErrorMessage('Could not delete provider.')
} else {
const settingsOverrideError = result.activeProfileId
? clearStartupProviderOverrideFromUserSettings()
: null
refreshProfiles()
setStatusMessage(
settingsOverrideError
? `Provider deleted. Warning: could not clear startup provider override (${settingsOverrideError}).`
: 'Provider deleted',
)
}
setScreen('menu')
},
{ includeGithub: true },
)
break
case 'menu':
default:
content = renderMenu()
break
}
return <Pane color="permission">{content}</Pane>
}

View File

@@ -1,5 +1,5 @@
import { c as _c } from "react-compiler-runtime";
// biome-ignore-all assist/source/organizeImports: internal-only import markers must not be reordered
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
import { feature } from 'bun:bundle';
import { Box, Text, useTheme, useThemeSetting, useTerminalFocus } from '../../ink.js';
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
@@ -342,7 +342,7 @@ export function Config({
});
}
},
// Fast mode toggle (internal-only, eliminated from external builds)
// Fast mode toggle (ant-only, eliminated from external builds)
...(isFastModeEnabled() && isFastModeAvailable() ? [{
id: 'fastMode',
label: `Fast mode (${FAST_MODE_MODEL_DISPLAY} only)`,
@@ -391,7 +391,7 @@ export function Config({
});
}
}] : []),
// Speculation toggle (internal-only)
// Speculation toggle (ant-only)
...("external" === 'ant' ? [{
id: 'speculationEnabled',
label: 'Speculative execution',

View File

@@ -1,5 +1,5 @@
import { c as _c } from "react-compiler-runtime";
// biome-ignore-all assist/source/organizeImports: internal-only import markers must not be reordered
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
import * as React from 'react';
import { Suspense, useState } from 'react';
import { useKeybinding } from '../../keybindings/useKeybinding.js';

View File

@@ -1,5 +1,5 @@
import { c as _c } from "react-compiler-runtime";
// biome-ignore-all assist/source/organizeImports: internal-only import markers must not be reordered
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
import { Box, Text } from '../ink.js';
import * as React from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
@@ -258,7 +258,7 @@ function SpinnerWithVerbInner({
const showBtwTip = tipsEnabled && elapsedSnapshot > 30_000 && !getGlobalConfig().btwUseCount;
const effectiveTip = contextTipsActive ? undefined : showClearTip && !nextTask ? 'Use /clear to start fresh when switching topics and free up context' : showBtwTip && !nextTask ? "Use /btw to ask a quick side question without interrupting Claude's current work" : spinnerTip;
// Budget text (internal-only) — shown above the tip line
// Budget text (ant-only) — shown above the tip line
let budgetText: string | null = null;
if (feature('TOKEN_BUDGET')) {
const budget = getCurrentTurnTokenBudget();

View File

@@ -9,7 +9,6 @@ import { formatDuration, formatNumber } from '../../utils/format.js';
import { toInkColor } from '../../utils/ink.js';
import type { Theme } from '../../utils/theme.js';
import { Byline } from '../design-system/Byline.js';
import FullWidthRow from '../design-system/FullWidthRow.js';
import { GlimmerMessage } from './GlimmerMessage.js';
import { SpinnerGlyph } from './SpinnerGlyph.js';
import type { SpinnerMode } from './types.js';
@@ -224,13 +223,11 @@ export function SpinnerAnimationRow({
<Byline>{parts}</Byline>
<Text dimColor>)</Text>
</> : null;
return <FullWidthRow>
<Box ref={viewportRef} flexDirection="row" flexWrap="wrap" marginTop={1}>
<SpinnerGlyph frame={frame} messageColor={messageColor} stalledIntensity={overrideColor ? 0 : stalledIntensity} reducedMotion={reducedMotion} time={time} />
<GlimmerMessage message={message} mode={mode} messageColor={messageColor} glimmerIndex={glimmerIndex} flashOpacity={flashOpacity} shimmerColor={shimmerColor} stalledIntensity={overrideColor ? 0 : stalledIntensity} />
{status}
</Box>
</FullWidthRow>;
return <Box ref={viewportRef} flexDirection="row" flexWrap="wrap" marginTop={1} width="100%">
<SpinnerGlyph frame={frame} messageColor={messageColor} stalledIntensity={overrideColor ? 0 : stalledIntensity} reducedMotion={reducedMotion} time={time} />
<GlimmerMessage message={message} mode={mode} messageColor={messageColor} glimmerIndex={glimmerIndex} flashOpacity={flashOpacity} shimmerColor={shimmerColor} stalledIntensity={overrideColor ? 0 : stalledIntensity} />
{status}
</Box>;
}
function SpinnerModeGlyph(t0) {
const $ = _c(2);

View File

@@ -5,9 +5,6 @@
* Addresses: https://github.com/Gitlawb/openclaude/issues/55
*/
import { isLocalProviderUrl } from '../services/api/providerConfig.js'
import { getLocalOpenAICompatibleProviderLabel } from '../utils/providerDiscovery.js'
declare const MACRO: { VERSION: string; DISPLAY_VERSION?: string }
const ESC = '\x1b['
@@ -102,7 +99,7 @@ function detectProvider(): { name: string; model: string; baseUrl: string; isLoc
if (useOpenAI) {
const rawModel = process.env.OPENAI_MODEL || 'gpt-4o'
const baseUrl = process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1'
const isLocal = isLocalProviderUrl(baseUrl)
const isLocal = /localhost|127\.0\.0\.1|0\.0\.0\.0/.test(baseUrl)
let name = 'OpenAI'
if (/deepseek/i.test(baseUrl) || /deepseek/i.test(rawModel)) name = 'DeepSeek'
else if (/openrouter/i.test(baseUrl)) name = 'OpenRouter'
@@ -110,8 +107,10 @@ function detectProvider(): { name: string; model: string; baseUrl: string; isLoc
else if (/groq/i.test(baseUrl)) name = 'Groq'
else if (/mistral/i.test(baseUrl) || /mistral/i.test(rawModel)) name = 'Mistral'
else if (/azure/i.test(baseUrl)) name = 'Azure OpenAI'
else if (/localhost:11434/i.test(baseUrl)) name = 'Ollama'
else if (/localhost:1234/i.test(baseUrl)) name = 'LM Studio'
else if (/llama/i.test(rawModel)) name = 'Meta Llama'
else if (isLocal) name = getLocalOpenAICompatibleProviderLabel(baseUrl)
else if (isLocal) name = 'Local'
// Resolve model alias to actual model name + reasoning effort
let displayModel = rawModel

View File

@@ -379,7 +379,7 @@ function OverviewTab({
// Calculate range days based on selected date range
const rangeDays = dateRange === '7d' ? 7 : dateRange === '30d' ? 30 : stats.totalDays;
// Compute shot stats data (internal-only, gated by feature flag)
// Compute shot stats data (ant-only, gated by feature flag)
let shotStatsData: {
avgShots: string;
buckets: {
@@ -511,7 +511,7 @@ function OverviewTab({
</Box>
</Box>
{/* Speculation time saved (internal-only) */}
{/* Speculation time saved (ant-only) */}
{"external" === 'ant' && stats.totalSpeculationTimeSavedMs > 0 && <Box flexDirection="row" gap={4}>
<Box flexDirection="column" width={28}>
<Text wrap="truncate">
@@ -523,7 +523,7 @@ function OverviewTab({
</Box>
</Box>}
{/* Shot stats (internal-only) */}
{/* Shot stats (ant-only) */}
{shotStatsData && <>
<Box marginTop={1}>
<Text>Shot distribution</Text>
@@ -1150,13 +1150,13 @@ function renderOverviewToAnsi(stats: ClaudeCodeStats): string[] {
const peakHourVal = stats.peakActivityHour !== null ? `${stats.peakActivityHour}:00-${stats.peakActivityHour + 1}:00` : 'N/A';
lines.push(row('Active days', activeDaysVal, 'Peak hour', peakHourVal));
// Speculation time saved (internal-only)
// Speculation time saved (ant-only)
if ("external" === 'ant' && stats.totalSpeculationTimeSavedMs > 0) {
const label = 'Speculation saved:'.padEnd(COL1_LABEL_WIDTH);
lines.push(label + h(formatDuration(stats.totalSpeculationTimeSavedMs)));
}
// Shot stats (internal-only)
// Shot stats (ant-only)
if (feature('SHOT_STATS') && stats.shotDistribution) {
const dist = stats.shotDistribution;
const totalWithShots = Object.values(dist).reduce((s, n) => s + n, 0);

View File

@@ -13,7 +13,6 @@ import { summarizeRecentActivities } from '../utils/collapseReadSearch.js';
import { truncateToWidth } from '../utils/format.js';
import { isTodoV2Enabled, type Task } from '../utils/tasks.js';
import type { Theme } from '../utils/theme.js';
import FullWidthRow from './design-system/FullWidthRow.js';
import ThemedText from './design-system/ThemedText.js';
type Props = {
tasks: Task[];
@@ -187,11 +186,11 @@ export function TaskListV2({
}
const content = <>
{visibleTasks.map(task_0 => <TaskItem key={task_0.id} task={task_0} ownerColor={task_0.owner ? teammateColors[task_0.owner] : undefined} openBlockers={task_0.blockedBy.filter(id_3 => unresolvedTaskIds.has(id_3))} activity={task_0.owner ? teammateActivity[task_0.owner] : undefined} ownerActive={task_0.owner ? activeTeammates.has(task_0.owner) : false} columns={columns} />)}
{maxDisplay > 0 && hiddenSummary && <FullWidthRow><Text dimColor>{hiddenSummary}</Text></FullWidthRow>}
{maxDisplay > 0 && hiddenSummary && <Text dimColor>{hiddenSummary}</Text>}
</>;
if (isStandalone) {
return <Box flexDirection="column" marginTop={1} marginLeft={2} width="100%">
<Box width="100%">
return <Box flexDirection="column" marginTop={1} marginLeft={2}>
<Box>
<Text dimColor>
<Text bold>{tasks.length}</Text>
{' tasks ('}
@@ -208,7 +207,7 @@ export function TaskListV2({
{content}
</Box>;
}
return <Box flexDirection="column" width="100%">{content}</Box>;
return <Box flexDirection="column">{content}</Box>;
}
type TaskItemProps = {
task: Task;
@@ -341,7 +340,7 @@ function TaskItem(t0) {
}
let t10;
if ($[26] !== t5 || $[27] !== t7 || $[28] !== t8 || $[29] !== t9) {
t10 = <FullWidthRow>{t5}{t7}{t8}{t9}</FullWidthRow>;
t10 = <Box>{t5}{t7}{t8}{t9}</Box>;
$[26] = t5;
$[27] = t7;
$[28] = t8;
@@ -352,7 +351,7 @@ function TaskItem(t0) {
}
let t11;
if ($[31] !== displayActivity || $[32] !== showActivity) {
t11 = showActivity && displayActivity && <FullWidthRow><Text dimColor={true}>{" "}{displayActivity}{figures.ellipsis}</Text></FullWidthRow>;
t11 = showActivity && displayActivity && <Box><Text dimColor={true}>{" "}{displayActivity}{figures.ellipsis}</Text></Box>;
$[31] = displayActivity;
$[32] = showActivity;
$[33] = t11;
@@ -361,7 +360,7 @@ function TaskItem(t0) {
}
let t12;
if ($[34] !== t10 || $[35] !== t11) {
t12 = <Box flexDirection="column" width="100%">{t10}{t11}</Box>;
t12 = <Box flexDirection="column">{t10}{t11}</Box>;
$[34] = t10;
$[35] = t11;
$[36] = t12;

View File

@@ -40,7 +40,7 @@ export default function TextInput(props: Props): React.ReactNode {
// Hoisted to mount-time — this component re-renders on every keystroke.
const accessibilityEnabled = useMemo(() => isEnvTruthy(process.env.CLAUDE_CODE_ACCESSIBILITY), []);
const settings = useSettings();
const reducedMotion = settings?.prefersReducedMotion ?? false;
const reducedMotion = settings.prefersReducedMotion ?? false;
const voiceState = feature('VOICE_MODE') ?
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
useVoiceState(s => s.voiceState) : 'idle' as const;

View File

@@ -1,113 +0,0 @@
import { describe, expect, it, mock } from 'bun:test'
// We can't fully render ThemePicker due to complex dependencies
// But we can test the theme options generation logic
describe('ThemePicker', () => {
describe('theme options', () => {
it('generates correct theme options without AUTO_THEME feature flag', () => {
// Since we can't easily mock bun:bundle, test the options structure
// The real test would require integration testing
const expectedOptions = [
{ label: "Dark mode", value: "dark" },
{ label: "Light mode", value: "light" },
{ label: "Dark mode (colorblind-friendly)", value: "dark-daltonized" },
{ label: "Light mode (colorblind-friendly)", value: "light-daltonized" },
{ label: "Dark mode (ANSI colors only)", value: "dark-ansi" },
{ label: "Light mode (ANSI colors only)", value: "light-ansi" },
]
expect(expectedOptions.length).toBe(6)
})
it('includes auto theme when AUTO_THEME feature is enabled', () => {
// Test the structure when auto is present
const optionsWithAuto = [
{ label: "Auto (match terminal)", value: "auto" },
{ label: "Dark mode", value: "dark" },
]
expect(optionsWithAuto[0].value).toBe('auto')
})
})
describe('handleRowFocus callback', () => {
it('setPreviewTheme is called with theme setting', () => {
const setPreviewTheme = mock()
const handleRowFocus = (setting: string) => setPreviewTheme(setting)
handleRowFocus('dark')
expect(setPreviewTheme).toHaveBeenCalledWith('dark')
})
})
describe('handleSelect callback', () => {
it('calls savePreview and onThemeSelect', () => {
const savePreview = mock()
const onThemeSelect = mock()
const handleSelect = (setting: string) => {
savePreview()
onThemeSelect(setting)
}
handleSelect('light')
expect(savePreview).toHaveBeenCalled()
expect(onThemeSelect).toHaveBeenCalledWith('light')
})
})
describe('handleCancel callback', () => {
it('calls cancelPreview and gracefulShutdown when not skipExitHandling', () => {
const cancelPreview = mock()
const gracefulShutdown = mock()
const handleCancel = (skipExitHandling: boolean, onCancelProp?: () => void) => {
cancelPreview()
if (skipExitHandling) {
onCancelProp?.()
} else {
gracefulShutdown(0)
}
}
handleCancel(false)
expect(cancelPreview).toHaveBeenCalled()
expect(gracefulShutdown).toHaveBeenCalledWith(0)
})
it('calls onCancelProp when skipExitHandling is true', () => {
const cancelPreview = mock()
const onCancelProp = mock()
const handleCancel = (skipExitHandling: boolean, onCancelProp?: () => void) => {
cancelPreview()
if (skipExitHandling) {
onCancelProp?.()
}
}
handleCancel(true, onCancelProp)
expect(cancelPreview).toHaveBeenCalled()
expect(onCancelProp).toHaveBeenCalled()
})
})
describe('syntax hint logic', () => {
it('shows disabled hint when syntax highlighting is disabled', () => {
const syntaxHighlightingDisabled = true
const syntaxToggleShortcut = 'Ctrl+T'
const hint = syntaxHighlightingDisabled
? `Syntax highlighting disabled (${syntaxToggleShortcut} to enable)`
: `Syntax highlighting enabled (${syntaxToggleShortcut} to disable)`
expect(hint).toContain('disabled')
})
it('shows enabled hint when syntax highlighting is active', () => {
const syntaxHighlightingDisabled = false
const syntaxToggleShortcut = 'Ctrl+T'
const hint = !syntaxHighlightingDisabled
? `Syntax highlighting enabled (${syntaxToggleShortcut} to disable)`
: `Syntax highlighting disabled (${syntaxToggleShortcut} to enable)`
expect(hint).toContain('enabled')
})
})
})

View File

@@ -1,14 +1,13 @@
import { c as _c } from "react-compiler-runtime";
import { feature } from 'bun:bundle';
import type { StructuredPatchHunk } from 'diff';
import * as React from 'react';
import { useExitOnCtrlCDWithKeybindings } from '../hooks/useExitOnCtrlCDWithKeybindings.js'
import { useExitOnCtrlCDWithKeybindings } from '../hooks/useExitOnCtrlCDWithKeybindings.js';
import { useTerminalSize } from '../hooks/useTerminalSize.js';
import { Box, Text, usePreviewTheme, useTheme, useThemeSetting } from '../ink.js';
import { useRegisterKeybindingContext } from '../keybindings/KeybindingContext.js';
import { useKeybinding } from '../keybindings/useKeybinding.js';
import { useShortcutDisplay } from '../keybindings/useShortcutDisplay.js';
import { useAppState, useSetAppState } from '../state/AppState.js';
import type { AppState } from '../state/AppStateStore.js';
import { gracefulShutdown } from '../utils/gracefulShutdown.js';
import { updateSettingsForSource } from '../utils/settings/settings.js';
import type { ThemeSetting } from '../utils/theme.js';
@@ -17,17 +16,6 @@ import { Byline } from './design-system/Byline.js';
import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js';
import { getColorModuleUnavailableReason, getSyntaxTheme } from './StructuredDiff/colorDiff.js';
import { StructuredDiff } from './StructuredDiff.js';
type StructuredDiffComponent = React.ComponentType<{
patch: StructuredPatchHunk
dim: boolean
filePath: string
firstLine: string | null
width: number
skipHighlighting?: boolean
}>
const StructuredDiffView = StructuredDiff as StructuredDiffComponent
export type ThemePickerProps = {
onThemeSelect: (setting: ThemeSetting) => void;
showIntroText?: boolean;
@@ -38,224 +26,307 @@ export type ThemePickerProps = {
skipExitHandling?: boolean;
/** Called when the user cancels (presses Escape). If skipExitHandling is true and this is provided, it will be called instead of just saving the preview. */
onCancel?: () => void;
}
const DEMO_PATCH: StructuredPatchHunk = {
oldStart: 1,
newStart: 1,
oldLines: 3,
newLines: 3,
lines: [
' function greet() {',
'- console.log("Hello, World!");',
'+ console.log("Hello, Claude!");',
' }',
],
}
/**
* Theme chooser with live preview. Implemented without react-compiler `_c` memo
* caches so preview/subtree reconciliation cannot stick on stale element refs when
* `setPreviewTheme` updates the resolved palette.
*/
export function ThemePicker({
onThemeSelect,
showIntroText = false,
helpText = '',
showHelpTextBelow = false,
hideEscToCancel = false,
skipExitHandling = false,
onCancel: onCancelProp,
}: ThemePickerProps) {
};
export function ThemePicker(t0) {
const $ = _c(59);
const {
onThemeSelect,
showIntroText: t1,
helpText: t2,
showHelpTextBelow: t3,
hideEscToCancel: t4,
skipExitHandling: t5,
onCancel: onCancelProp
} = t0;
const showIntroText = t1 === undefined ? false : t1;
const helpText = t2 === undefined ? "" : t2;
const showHelpTextBelow = t3 === undefined ? false : t3;
const hideEscToCancel = t4 === undefined ? false : t4;
const skipExitHandling = t5 === undefined ? false : t5;
const [theme] = useTheme();
const themeSetting = useThemeSetting();
const { columns } = useTerminalSize();
const colorModuleUnavailableReason = React.useMemo(
() => getColorModuleUnavailableReason(),
[],
)
const syntaxTheme =
colorModuleUnavailableReason === null ? getSyntaxTheme(theme) : null
const { setPreviewTheme, savePreview, cancelPreview } = usePreviewTheme()
const syntaxHighlightingDisabled = useAppState(
(s: AppState) => s.settings.syntaxHighlightingDisabled ?? false
);
const setAppState = useSetAppState();
useRegisterKeybindingContext("ThemePicker", true);
const syntaxToggleShortcut = useShortcutDisplay("theme:toggleSyntaxHighlighting", "ThemePicker", "ctrl+t");
const toggleSyntax = React.useCallback(() => {
if (colorModuleUnavailableReason === null) {
const newValue = !syntaxHighlightingDisabled
updateSettingsForSource("userSettings", {
syntaxHighlightingDisabled: newValue
});
setAppState(prev => ({
...prev,
settings: {
...prev.settings,
syntaxHighlightingDisabled: newValue
}
}));
}
}, [
colorModuleUnavailableReason,
syntaxHighlightingDisabled,
setAppState,
])
useKeybinding("theme:toggleSyntaxHighlighting", toggleSyntax, {
context: "ThemePicker",
})
const exitState = useExitOnCtrlCDWithKeybindings(
skipExitHandling ? () => {} : undefined,
)
const themeOptions = React.useMemo(
() => [
...(feature("AUTO_THEME")
? [{ label: "Auto (match terminal)", value: "auto" as const }]
: []), {
label: "Dark mode",
value: "dark" as const
}, {
label: "Light mode",
value: "light" as const
}, {
label: "Dark mode (colorblind-friendly)",
value: "dark-daltonized" as const,
}, {
label: "Light mode (colorblind-friendly)",
value: "light-daltonized" as const,
}, {
label: "Dark mode (ANSI colors only)",
value: "dark-ansi" as const
}, {
label: "Light mode (ANSI colors only)",
value: "light-ansi" as const
},],
[],
)
const handleRowFocus = React.useCallback(
(setting: ThemeSetting) => {
setPreviewTheme(setting)
},
[setPreviewTheme],
)
const handleSelect = React.useCallback(
(setting: ThemeSetting) => {
savePreview()
onThemeSelect(setting)
},
[savePreview, onThemeSelect],
)
const handleCancel = React.useCallback(() => {
cancelPreview()
if (skipExitHandling) {
onCancelProp?.()
} else {
void gracefulShutdown(0)
}
}, [cancelPreview, onCancelProp, skipExitHandling])
const syntaxHint =
colorModuleUnavailableReason === 'env'
? `Syntax highlighting disabled (via CLAUDE_CODE_SYNTAX_HIGHLIGHT=${process.env.CLAUDE_CODE_SYNTAX_HIGHLIGHT})`
: syntaxHighlightingDisabled
? `Syntax highlighting disabled (${syntaxToggleShortcut} to enable)`
: syntaxTheme
? `Syntax theme: ${syntaxTheme.theme}${syntaxTheme.source ? ` (from ${syntaxTheme.source})` : ''} (${syntaxToggleShortcut} to disable)`
: `Syntax highlighting enabled (${syntaxToggleShortcut} to disable)`
const header = showIntroText ? (
<Text>{"Let's get started."}</Text>
) : (
<Text bold color="permission">
Theme
</Text>
)
const introBlock = (
<Box flexDirection="column">
<Text bold>Choose the text style that looks best with your terminal</Text>
{helpText && !showHelpTextBelow ? (
<Text dimColor>{helpText}</Text>
) : null}
</Box>
)
const content = (
<Box flexDirection="column" gap={1}>
<Box flexDirection="column" gap={1}>
{header}
{introBlock}
<Select
options={themeOptions}
onFocus={handleRowFocus}
onChange={handleSelect}
onCancel={handleCancel}
visibleOptionCount={themeOptions.length}
defaultValue={themeSetting}
defaultFocusValue={themeSetting}
/>
</Box>
<Box flexDirection="column" width="100%">
<Box
key={theme}
flexDirection="column"
borderTop
borderBottom
borderLeft={false}
borderRight={false}
borderStyle="dashed"
borderColor="subtle"
>
<StructuredDiffView
patch={DEMO_PATCH}
dim={false}
filePath="demo.js"
firstLine={null}
width={columns}
/>
</Box>
<Text dimColor>
{' '}
{syntaxHint}
</Text>
</Box>
</Box>
)
if (!showIntroText) {
return (
<>
<Box flexDirection="column">{content}</Box>
{showHelpTextBelow && helpText ? (
<Box marginLeft={3}>
<Text dimColor>{helpText}</Text>
</Box>
) : null}
{!hideEscToCancel ? (
<Box marginTop={1}>
<Text dimColor italic>
{exitState.pending ? (
<>Press {exitState.keyName} again to exit</>
) : (
<Byline>
<KeyboardShortcutHint shortcut="Enter" action="select" />
<KeyboardShortcutHint shortcut="Esc" action="cancel" />
</Byline>
)}
</Text>
</Box>
) : null}
</>
)
const {
columns
} = useTerminalSize();
let t6;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t6 = getColorModuleUnavailableReason();
$[0] = t6;
} else {
t6 = $[0];
}
return content
const colorModuleUnavailableReason = t6;
let t7;
if ($[1] !== theme) {
t7 = colorModuleUnavailableReason === null ? getSyntaxTheme(theme) : null;
$[1] = theme;
$[2] = t7;
} else {
t7 = $[2];
}
const syntaxTheme = t7;
const {
setPreviewTheme,
savePreview,
cancelPreview
} = usePreviewTheme();
const syntaxHighlightingDisabled = useAppState(_temp) ?? false;
const setAppState = useSetAppState();
useRegisterKeybindingContext("ThemePicker");
const syntaxToggleShortcut = useShortcutDisplay("theme:toggleSyntaxHighlighting", "ThemePicker", "ctrl+t");
let t8;
if ($[3] !== setAppState || $[4] !== syntaxHighlightingDisabled) {
t8 = () => {
if (colorModuleUnavailableReason === null) {
const newValue = !syntaxHighlightingDisabled;
updateSettingsForSource("userSettings", {
syntaxHighlightingDisabled: newValue
});
setAppState(prev => ({
...prev,
settings: {
...prev.settings,
syntaxHighlightingDisabled: newValue
}
}));
}
};
$[3] = setAppState;
$[4] = syntaxHighlightingDisabled;
$[5] = t8;
} else {
t8 = $[5];
}
let t9;
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
t9 = {
context: "ThemePicker"
};
$[6] = t9;
} else {
t9 = $[6];
}
useKeybinding("theme:toggleSyntaxHighlighting", t8, t9);
const exitState = useExitOnCtrlCDWithKeybindings(skipExitHandling ? _temp2 : undefined);
let t10;
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
t10 = [...(feature("AUTO_THEME") ? [{
label: "Auto (match terminal)",
value: "auto" as const
}] : []), {
label: "Dark mode",
value: "dark"
}, {
label: "Light mode",
value: "light"
}, {
label: "Dark mode (colorblind-friendly)",
value: "dark-daltonized"
}, {
label: "Light mode (colorblind-friendly)",
value: "light-daltonized"
}, {
label: "Dark mode (ANSI colors only)",
value: "dark-ansi"
}, {
label: "Light mode (ANSI colors only)",
value: "light-ansi"
}];
$[7] = t10;
} else {
t10 = $[7];
}
const themeOptions = t10;
let t11;
if ($[8] !== showIntroText) {
t11 = showIntroText ? <Text>Let's get started.</Text> : <Text bold={true} color="permission">Theme</Text>;
$[8] = showIntroText;
$[9] = t11;
} else {
t11 = $[9];
}
let t12;
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
t12 = <Text bold={true}>Choose the text style that looks best with your terminal</Text>;
$[10] = t12;
} else {
t12 = $[10];
}
let t13;
if ($[11] !== helpText || $[12] !== showHelpTextBelow) {
t13 = helpText && !showHelpTextBelow && <Text dimColor={true}>{helpText}</Text>;
$[11] = helpText;
$[12] = showHelpTextBelow;
$[13] = t13;
} else {
t13 = $[13];
}
let t14;
if ($[14] !== t13) {
t14 = <Box flexDirection="column">{t12}{t13}</Box>;
$[14] = t13;
$[15] = t14;
} else {
t14 = $[15];
}
let t15;
if ($[16] !== setPreviewTheme) {
t15 = setting => {
setPreviewTheme(setting as ThemeSetting);
};
$[16] = setPreviewTheme;
$[17] = t15;
} else {
t15 = $[17];
}
let t16;
if ($[18] !== onThemeSelect || $[19] !== savePreview) {
t16 = setting_0 => {
savePreview();
onThemeSelect(setting_0 as ThemeSetting);
};
$[18] = onThemeSelect;
$[19] = savePreview;
$[20] = t16;
} else {
t16 = $[20];
}
let t17;
if ($[21] !== cancelPreview || $[22] !== onCancelProp || $[23] !== skipExitHandling) {
t17 = skipExitHandling ? () => {
cancelPreview();
onCancelProp?.();
} : async () => {
cancelPreview();
await gracefulShutdown(0);
};
$[21] = cancelPreview;
$[22] = onCancelProp;
$[23] = skipExitHandling;
$[24] = t17;
} else {
t17 = $[24];
}
let t18;
if ($[25] !== t15 || $[26] !== t16 || $[27] !== t17 || $[28] !== themeSetting) {
t18 = <Select options={themeOptions} onFocus={t15} onChange={t16} onCancel={t17} visibleOptionCount={themeOptions.length} defaultValue={themeSetting} defaultFocusValue={themeSetting} />;
$[25] = t15;
$[26] = t16;
$[27] = t17;
$[28] = themeSetting;
$[29] = t18;
} else {
t18 = $[29];
}
let t19;
if ($[30] !== t11 || $[31] !== t14 || $[32] !== t18) {
t19 = <Box flexDirection="column" gap={1}>{t11}{t14}{t18}</Box>;
$[30] = t11;
$[31] = t14;
$[32] = t18;
$[33] = t19;
} else {
t19 = $[33];
}
let t20;
if ($[34] === Symbol.for("react.memo_cache_sentinel")) {
t20 = {
oldStart: 1,
newStart: 1,
oldLines: 3,
newLines: 3,
lines: [" function greet() {", "- console.log(\"Hello, World!\");", "+ console.log(\"Hello, Claude!\");", " }"]
};
$[34] = t20;
} else {
t20 = $[34];
}
let t21;
if ($[35] !== columns) {
t21 = <Box flexDirection="column" borderTop={true} borderBottom={true} borderLeft={false} borderRight={false} borderStyle="dashed" borderColor="subtle"><StructuredDiff patch={t20} dim={false} filePath="demo.js" firstLine={null} width={columns} /></Box>;
$[35] = columns;
$[36] = t21;
} else {
t21 = $[36];
}
const t22 = colorModuleUnavailableReason === "env" ? `Syntax highlighting disabled (via CLAUDE_CODE_SYNTAX_HIGHLIGHT=${process.env.CLAUDE_CODE_SYNTAX_HIGHLIGHT})` : syntaxHighlightingDisabled ? `Syntax highlighting disabled (${syntaxToggleShortcut} to enable)` : syntaxTheme ? `Syntax theme: ${syntaxTheme.theme}${syntaxTheme.source ? ` (from ${syntaxTheme.source})` : ""} (${syntaxToggleShortcut} to disable)` : `Syntax highlighting enabled (${syntaxToggleShortcut} to disable)`;
let t23;
if ($[37] !== t22) {
t23 = <Text dimColor={true}>{" "}{t22}</Text>;
$[37] = t22;
$[38] = t23;
} else {
t23 = $[38];
}
let t24;
if ($[39] !== t21 || $[40] !== t23) {
t24 = <Box flexDirection="column" width="100%">{t21}{t23}</Box>;
$[39] = t21;
$[40] = t23;
$[41] = t24;
} else {
t24 = $[41];
}
let t25;
if ($[42] !== t19 || $[43] !== t24) {
t25 = <Box flexDirection="column" gap={1}>{t19}{t24}</Box>;
$[42] = t19;
$[43] = t24;
$[44] = t25;
} else {
t25 = $[44];
}
const content = t25;
if (!showIntroText) {
let t26;
if ($[45] !== content) {
t26 = <Box flexDirection="column">{content}</Box>;
$[45] = content;
$[46] = t26;
} else {
t26 = $[46];
}
let t27;
if ($[47] !== helpText || $[48] !== showHelpTextBelow) {
t27 = showHelpTextBelow && helpText && <Box marginLeft={3}><Text dimColor={true}>{helpText}</Text></Box>;
$[47] = helpText;
$[48] = showHelpTextBelow;
$[49] = t27;
} else {
t27 = $[49];
}
let t28;
if ($[50] !== exitState || $[51] !== hideEscToCancel) {
t28 = !hideEscToCancel && <Box><Text dimColor={true} italic={true}>{exitState.pending ? <>Press {exitState.keyName} again to exit</> : <Byline><KeyboardShortcutHint shortcut="Enter" action="select" /><KeyboardShortcutHint shortcut="Esc" action="cancel" /></Byline>}</Text></Box>;
$[50] = exitState;
$[51] = hideEscToCancel;
$[52] = t28;
} else {
t28 = $[52];
}
let t29;
if ($[53] !== t27 || $[54] !== t28) {
t29 = <Box marginTop={1}>{t27}{t28}</Box>;
$[53] = t27;
$[54] = t28;
$[55] = t29;
} else {
t29 = $[55];
}
let t30;
if ($[56] !== t26 || $[57] !== t29) {
t30 = <>{t26}{t29}</>;
$[56] = t26;
$[57] = t29;
$[58] = t30;
} else {
t30 = $[58];
}
return t30;
}
return content;
}
function _temp2() {}
function _temp(s) {
return s.settings.syntaxHighlightingDisabled;
}

View File

@@ -6,7 +6,6 @@ import { useKeybinding } from '../../keybindings/useKeybinding.js';
import type { Theme } from '../../utils/theme.js';
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
import { Byline } from './Byline.js';
import FullWidthRow from './FullWidthRow.js';
import { KeyboardShortcutHint } from './KeyboardShortcutHint.js';
import { Pane } from './Pane.js';
type DialogProps = {
@@ -103,7 +102,7 @@ export function Dialog(t0) {
}
let t9;
if ($[16] !== defaultInputGuide || $[17] !== exitState || $[18] !== hideInputGuide || $[19] !== inputGuide) {
t9 = !hideInputGuide && <Box marginTop={1}><FullWidthRow><Text dimColor={true} italic={true}>{inputGuide ? inputGuide(exitState) : defaultInputGuide}</Text></FullWidthRow></Box>;
t9 = !hideInputGuide && <Box marginTop={1}><Text dimColor={true} italic={true}>{inputGuide ? inputGuide(exitState) : defaultInputGuide}</Text></Box>;
$[16] = defaultInputGuide;
$[17] = exitState;
$[18] = hideInputGuide;

View File

@@ -1,15 +0,0 @@
import * as React from 'react';
import { Box } from '../../ink.js';
type Props = {
children: React.ReactNode;
};
export default function FullWidthRow({
children
}: Props): React.ReactNode {
return <Box flexDirection="row" width="100%">
{children}
<Box flexGrow={1} />
</Box>;
}

View File

@@ -1,5 +1,5 @@
import { c as _c } from "react-compiler-runtime";
// biome-ignore-all assist/source/organizeImports: internal-only import markers must not be reordered
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
import React, { useMemo } from 'react';
import { Ansi, Box, Text } from '../../ink.js';
import type { Attachment } from 'src/utils/attachments.js';
@@ -24,7 +24,6 @@ import { BLACK_CIRCLE } from '../../constants/figures.js';
import { TeammateMessageContent } from './UserTeammateMessage.js';
import { isShutdownApproved } from '../../utils/teammateMailbox.js';
import { CtrlOToExpand } from '../CtrlOToExpand.js';
import FullWidthRow from '../design-system/FullWidthRow.js';
import { FilePathLink } from '../FilePathLink.js';
import { feature } from 'bun:bundle';
import { useSelectedMessageBg } from '../messageActions.js';
@@ -515,7 +514,7 @@ function Line(t0) {
const bg = useSelectedMessageBg();
let t2;
if ($[0] !== children || $[1] !== color || $[2] !== dimColor) {
t2 = <MessageResponse><FullWidthRow><Text color={color} dimColor={dimColor} wrap="wrap">{children}</Text></FullWidthRow></MessageResponse>;
t2 = <MessageResponse><Text color={color} dimColor={dimColor} wrap="wrap">{children}</Text></MessageResponse>;
$[0] = children;
$[1] = color;
$[2] = dimColor;

View File

@@ -1,5 +1,5 @@
import { c as _c } from "react-compiler-runtime";
// biome-ignore-all assist/source/organizeImports: internal-only import markers must not be reordered
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
import { Box, Text, type TextProps } from '../../ink.js';
import { feature } from 'bun:bundle';
import * as React from 'react';

View File

@@ -5,7 +5,6 @@ import { NO_CONTENT_MESSAGE } from '../../constants/messages.js';
import { Box, Text } from '../../ink.js';
import { extractTag } from '../../utils/messages.js';
import { Markdown } from '../Markdown.js';
import FullWidthRow from '../design-system/FullWidthRow.js';
import { MessageResponse } from '../MessageResponse.js';
type Props = {
content: string;
@@ -78,7 +77,7 @@ function IndentedContent(t0) {
}
let t2;
if ($[3] !== children) {
t2 = <FullWidthRow>{t1}<Box flexDirection="column" flexGrow={1}><Markdown>{children}</Markdown></Box></FullWidthRow>;
t2 = <Box flexDirection="row">{t1}<Box flexDirection="column" flexGrow={1}><Markdown>{children}</Markdown></Box></Box>;
$[3] = children;
$[4] = t2;
} else {
@@ -148,7 +147,7 @@ function CloudLaunchContent(t0) {
}
let t6;
if ($[14] !== rest) {
t6 = rest && <FullWidthRow><Text dimColor={true}>{" \u23BF "}</Text><Text dimColor={true}>{rest}</Text></FullWidthRow>;
t6 = rest && <Box flexDirection="row"><Text dimColor={true}>{" \u23BF "}</Text><Text dimColor={true}>{rest}</Text></Box>;
$[14] = rest;
$[15] = t6;
} else {

View File

@@ -211,7 +211,7 @@ function BashPermissionRequestInner({
// Editable prefix — initialize synchronously with the best prefix we can
// extract without tree-sitter, then refine via tree-sitter for compound
// commands. The sync path matters because TREE_SITTER_BASH is gated
// internal-only: in external builds the async refinement below always resolves
// ant-only: in external builds the async refinement below always resolves
// to [] and this initial value is what the user sees.
//
// Lazy initializer: this runs regex + split on every render if left in

View File

@@ -39,7 +39,7 @@ export function powershellToolUseOptions({
}
// Note: No sandbox toggle for PowerShell - sandbox is not supported on Windows
// Note: No classifier-reviewed option for PowerShell (internal-only feature for Bash)
// Note: No classifier-reviewed option for PowerShell (ANT-ONLY feature for Bash)
// Only show "always allow" options when not restricted by allowManagedPermissionRulesOnly.
// Prefer the editable prefix input (static extractor + user edits) over the

View File

@@ -164,7 +164,7 @@ export function usePermissionRequestLogging(
}
}
// [internal-only] Log bash tool calls, so we can categorize
// [ANT-ONLY] Log bash tool calls, so we can categorize
// & burn down calls that should have been allowed
if (process.env.USER_TYPE === 'ant') {
const parsedInput = BashTool.inputSchema.safeParse(toolUseConfirm.input)

View File

@@ -11,7 +11,6 @@ import { getSettingSourceName, type SettingSource } from '../../utils/settings/c
import { plural } from '../../utils/stringUtils.js';
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
import { Dialog } from '../design-system/Dialog.js';
import FullWidthRow from '../design-system/FullWidthRow.js';
// Skills are always PromptCommands with CommandBase properties
type SkillCommand = CommandBase & PromptCommand;
@@ -106,14 +105,14 @@ export function SkillsMenu(t0) {
if (skills.length === 0) {
let t3;
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
t3 = <FullWidthRow><Text dimColor={true}>Create skills in .claude/skills/ or ~/.claude/skills/</Text></FullWidthRow>;
t3 = <Text dimColor={true}>Create skills in .claude/skills/ or ~/.claude/skills/</Text>;
$[6] = t3;
} else {
t3 = $[6];
}
let t4;
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
t4 = <FullWidthRow><Text dimColor={true} italic={true}><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="close" /></Text></FullWidthRow>;
t4 = <Text dimColor={true} italic={true}><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="close" /></Text>;
$[7] = t4;
} else {
t4 = $[7];
@@ -138,7 +137,7 @@ export function SkillsMenu(t0) {
}
const title = getSourceTitle(source_0);
const subtitle = getSourceSubtitle(source_0, groupSkills);
return <Box flexDirection="column" key={source_0}><FullWidthRow><Text bold={true} dimColor={true}>{title}</Text>{subtitle && <Text dimColor={true}> ({subtitle})</Text>}</FullWidthRow>{groupSkills.map(skill_1 => renderSkill(skill_1))}</Box>;
return <Box flexDirection="column" key={source_0}><Box><Text bold={true} dimColor={true}>{title}</Text>{subtitle && <Text dimColor={true}> ({subtitle})</Text>}</Box>{groupSkills.map(skill_1 => renderSkill(skill_1))}</Box>;
};
$[10] = skillsBySource;
$[11] = t3;
@@ -210,7 +209,7 @@ export function SkillsMenu(t0) {
}
let t13;
if ($[30] === Symbol.for("react.memo_cache_sentinel")) {
t13 = <FullWidthRow><Text dimColor={true} italic={true}><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="close" /></Text></FullWidthRow>;
t13 = <Text dimColor={true} italic={true}><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="close" /></Text>;
$[30] = t13;
} else {
t13 = $[30];
@@ -231,7 +230,7 @@ function _temp3(skill_0) {
const estimatedTokens = estimateSkillFrontmatterTokens(skill_0);
const tokenDisplay = `~${formatTokens(estimatedTokens)}`;
const pluginName = skill_0.source === "plugin" ? skill_0.pluginInfo?.pluginManifest.name : undefined;
return <FullWidthRow key={`${skill_0.name}-${skill_0.source}`}><Text>{getSkillListLabel(skill_0)}</Text><Text dimColor={true}>{pluginName ? ` · ${pluginName}` : ""} · {tokenDisplay} description tokens</Text></FullWidthRow>;
return <Box key={`${skill_0.name}-${skill_0.source}`}><Text>{getSkillListLabel(skill_0)}</Text><Text dimColor={true}>{pluginName ? ` · ${pluginName}` : ""} · {tokenDisplay} description tokens</Text></Box>;
}
function _temp2(a, b) {
return a.name.localeCompare(b.name);

View File

@@ -103,7 +103,7 @@ type ListItem = {
status: 'running';
};
// WORKFLOW_SCRIPTS is internal-only (build_flags.yaml). Static imports would leak
// WORKFLOW_SCRIPTS is ant-only (build_flags.yaml). Static imports would leak
// ~1.3K lines into external builds. Gate with feature() + require so the
// bundler can dead-code-eliminate the branch.
/* eslint-disable @typescript-eslint/no-require-imports */

View File

@@ -2,7 +2,7 @@ import memoize from 'lodash-es/memoize.js'
// This ensures you get the LOCAL date in ISO format
export function getLocalISODate(): string {
// Check for internal-only date override
// Check for ant-only date override
if (process.env.CLAUDE_CODE_OVERRIDE_DATE) {
return process.env.CLAUDE_CODE_OVERRIDE_DATE
}

View File

@@ -1,4 +1,4 @@
// biome-ignore-all assist/source/organizeImports: internal-only import markers must not be reordered
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
import { type as osType, version as osVersion, release as osRelease } from 'os'
import { env } from '../utils/env.js'
import { getIsGit } from '../utils/git.js'
@@ -242,7 +242,7 @@ function getSimpleDoingTasksSection(): string {
: []),
...(process.env.USER_TYPE === 'ant'
? [
`If the user reports a bug, slowness, or unexpected behavior with Claude Code itself (as opposed to asking you to fix their own code), recommend the appropriate slash command: /issue for model-related problems (odd outputs, wrong tool choices, hallucinations, refusals), or /share to upload the full session transcript for product bugs, crashes, slowness, or general issues. Only recommend these when the user is describing a problem with Claude Code.`,
`If the user reports a bug, slowness, or unexpected behavior with Claude Code itself (as opposed to asking you to fix their own code), recommend the appropriate slash command: /issue for model-related problems (odd outputs, wrong tool choices, hallucinations, refusals), or /share to upload the full session transcript for product bugs, crashes, slowness, or general issues. Only recommend these when the user is describing a problem with Claude Code. After /share produces a ccshare link, if you have a Slack MCP tool available, offer to post the link to #claude-code-feedback (channel ID C07VBSHV7EV) for the user.`,
]
: []),
`If the user asks for help or wants to give feedback inform them of the following:`,
@@ -389,7 +389,7 @@ function getSessionSpecificGuidanceSection(
: null,
hasAgentTool &&
feature('VERIFICATION_AGENT') &&
// 3P default: false — verification agent is internal-only A/B
// 3P default: false — verification agent is ant-only A/B
getFeatureValue_CACHED_MAY_BE_STALE('tengu_hive_evidence', false)
? `The contract: when non-trivial implementation happens on your turn, independent adversarial verification must happen before you report completion \u2014 regardless of who did the implementing (you directly, a fork you spawned, or a subagent). You are the one reporting to the user; you own the gate. Non-trivial means: 3+ file edits, backend/API changes, or infrastructure changes. Spawn the ${AGENT_TOOL_NAME} tool with subagent_type="${VERIFICATION_AGENT_TYPE}". Your own checks, caveats, and a fork's self-checks do NOT substitute \u2014 only the verifier assigns a verdict; you cannot self-assign PARTIAL. Pass the original user request, all files changed (by anyone), the approach, and the plan file path if applicable. Flag concerns if you have them but do NOT share test results or claim things work. On FAIL: fix, resume the verifier with its findings plus your fix, repeat until PASS. On PASS: spot-check it \u2014 re-run 2-3 commands from its report, confirm every PASS has a Command run block with output that matches your re-run. If any PASS lacks a command block or diverges, resume the verifier with the specifics. On PARTIAL (from the verifier): report what passed and what could not be verified.`
: null,

View File

@@ -1,4 +1,4 @@
// biome-ignore-all assist/source/organizeImports: internal-only import markers must not be reordered
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
import { feature } from 'bun:bundle'
import { TASK_OUTPUT_TOOL_NAME } from '../tools/TaskOutputTool/constants.js'
import { EXIT_PLAN_MODE_V2_TOOL_NAME } from '../tools/ExitPlanModeTool/constants.js'

View File

@@ -19,7 +19,7 @@ import { logError } from './utils/log.js'
const MAX_STATUS_CHARS = 2000
// System prompt injection for cache breaking (internal-only, ephemeral debugging state)
// System prompt injection for cache breaking (ant-only, ephemeral debugging state)
let systemPromptInjection: string | null = null
export function getSystemPromptInjection(): string | null {
@@ -127,7 +127,7 @@ export const getSystemContext = memoize(
? null
: await getGitStatus()
// Include system prompt injection if set (for cache breaking, internal-only)
// Include system prompt injection if set (for cache breaking, ant-only)
const injection = feature('BREAK_CACHE_COMMAND')
? getSystemPromptInjection()
: null

View File

@@ -111,7 +111,7 @@ export function getCoordinatorUserContext(
export function getCoordinatorSystemPrompt(): string {
const workerCapabilities = isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)
? 'Workers have access to Bash, Read, and Edit tools, plus MCP tools from configured MCP servers.'
: 'Workers have access to standard tools, MCP tools from configured MCP servers, and project skills via the Skill tool. Delegate skill invocations (e.g. /commit or project workflow skills) to workers.'
: 'Workers have access to standard tools, MCP tools from configured MCP servers, and project skills via the Skill tool. Delegate skill invocations (e.g. /commit, /verify) to workers.'
return `You are Claude Code, an AI assistant that orchestrates software engineering tasks across multiple workers.

View File

@@ -1,12 +1,14 @@
import { feature } from 'bun:bundle';
import {
isLocalProviderUrl,
resolveCodexApiCredentials,
resolveProviderRequest,
} from '../services/api/providerConfig.js'
import {
applyProfileEnvToProcessEnv,
buildStartupEnvFromProfile,
redactSecretValueForDisplay,
} from '../utils/providerProfile.js'
import {
getProviderValidationError,
validateProviderEnvOrExit,
} from '../utils/providerValidation.js'
// OpenClaude: disable experimental API betas by default.
// Tool search (defer_loading), global cache scope, and context management
@@ -40,6 +42,82 @@ if (feature('ABLATION_BASELINE') && process.env.CLAUDE_CODE_ABLATION_BASELINE) {
}
}
function isEnvTruthy(value: string | undefined): boolean {
if (!value) return false
const normalized = value.trim().toLowerCase()
return normalized !== '' && normalized !== '0' && normalized !== 'false' && normalized !== 'no'
}
function getProviderValidationError(
env: NodeJS.ProcessEnv = process.env,
): string | null {
const useOpenAI = isEnvTruthy(env.CLAUDE_CODE_USE_OPENAI)
const useGithub = isEnvTruthy(env.CLAUDE_CODE_USE_GITHUB)
if (isEnvTruthy(env.CLAUDE_CODE_USE_GEMINI)) {
if (!(env.GEMINI_API_KEY ?? env.GOOGLE_API_KEY)) {
return 'GEMINI_API_KEY is required when CLAUDE_CODE_USE_GEMINI=1.'
}
return null
}
if (useGithub && !useOpenAI) {
const token = (env.GITHUB_TOKEN?.trim() || env.GH_TOKEN?.trim()) ?? ''
if (!token) {
return 'GITHUB_TOKEN or GH_TOKEN is required when CLAUDE_CODE_USE_GITHUB=1.'
}
return null
}
if (!useOpenAI) {
return null
}
const request = resolveProviderRequest({
model: env.OPENAI_MODEL,
baseUrl: env.OPENAI_BASE_URL,
})
if (env.OPENAI_API_KEY === 'SUA_CHAVE') {
return 'Invalid OPENAI_API_KEY: placeholder value SUA_CHAVE detected. Set a real key or unset for local providers.'
}
if (request.transport === 'codex_responses') {
const credentials = resolveCodexApiCredentials(env)
if (!credentials.apiKey) {
const authHint = credentials.authPath
? ` or put auth.json at ${credentials.authPath}`
: ''
const safeModel =
redactSecretValueForDisplay(request.requestedModel, env) ??
'the requested model'
return `Codex auth is required for ${safeModel}. Set CODEX_API_KEY${authHint}.`
}
if (!credentials.accountId) {
return 'Codex auth is missing chatgpt_account_id. Re-login with Codex or set CHATGPT_ACCOUNT_ID/CODEX_ACCOUNT_ID.'
}
return null
}
if (!env.OPENAI_API_KEY && !isLocalProviderUrl(request.baseUrl)) {
const hasGithubToken = !!(env.GITHUB_TOKEN?.trim() || env.GH_TOKEN?.trim())
if (useGithub && hasGithubToken) {
return null
}
return 'OPENAI_API_KEY is required when CLAUDE_CODE_USE_OPENAI=1 and OPENAI_BASE_URL is not local.'
}
return null
}
function validateProviderEnvOrExit(): void {
const error = getProviderValidationError()
if (error) {
console.error(error)
process.exit(1)
}
}
/**
* Bootstrap entrypoint - checks for special flags before loading the full CLI.
* All imports are dynamic to minimize module evaluation for fast paths.
@@ -73,8 +151,6 @@ async function main(): Promise<void> {
enableConfigs()
const { applySafeConfigEnvironmentVariables } = await import('../utils/managedEnv.js')
applySafeConfigEnvironmentVariables()
const { hydrateGeminiAccessTokenFromSecureStorage } = await import('../utils/geminiCredentials.js')
hydrateGeminiAccessTokenFromSecureStorage()
const { hydrateGithubModelsTokenFromSecureStorage } = await import('../utils/githubModelsCredentials.js')
hydrateGithubModelsTokenFromSecureStorage()
}
@@ -83,7 +159,7 @@ async function main(): Promise<void> {
processEnv: process.env,
})
if (startupEnv !== process.env) {
const startupProfileError = await getProviderValidationError(startupEnv)
const startupProfileError = getProviderValidationError(startupEnv)
if (startupProfileError) {
console.error(
`Warning: ignoring saved provider profile. ${startupProfileError}`,
@@ -93,7 +169,7 @@ async function main(): Promise<void> {
}
}
await validateProviderEnvOrExit()
validateProviderEnvOrExit()
// Print the gradient startup screen before the Ink UI loads
const { printStartupScreen } = await import('../components/StartupScreen.js')

View File

@@ -505,7 +505,7 @@ export const SDKControlGetSettingsResponseSchema = lazySchema(() =>
applied: z
.object({
model: z.string(),
// String levels only — numeric effort is internal-only and the
// String levels only — numeric effort is ant-only and the
// Zod→proto generator can't emit enumnumber unions.
effort: z.enum(['low', 'medium', 'high', 'max']).nullable(),
})

View File

@@ -1,252 +0,0 @@
import * as grpc from '@grpc/grpc-js'
import * as protoLoader from '@grpc/proto-loader'
import path from 'path'
import { randomUUID } from 'crypto'
import { QueryEngine } from '../QueryEngine.js'
import { getTools } from '../tools.js'
import { getDefaultAppState } from '../state/AppStateStore.js'
import { AppState } from '../state/AppState.js'
import { FileStateCache, READ_FILE_STATE_CACHE_SIZE } from '../utils/fileStateCache.js'
const PROTO_PATH = path.resolve(import.meta.dirname, '../proto/openclaude.proto')
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
})
const protoDescriptor = grpc.loadPackageDefinition(packageDefinition) as any
const openclaudeProto = protoDescriptor.openclaude.v1
const MAX_SESSIONS = 1000
export class GrpcServer {
private server: grpc.Server
private sessions: Map<string, any[]> = new Map()
constructor() {
this.server = new grpc.Server()
this.server.addService(openclaudeProto.AgentService.service, {
Chat: this.handleChat.bind(this),
})
}
start(port: number = 50051, host: string = 'localhost') {
this.server.bindAsync(
`${host}:${port}`,
grpc.ServerCredentials.createInsecure(),
(error, boundPort) => {
if (error) {
console.error('Failed to start gRPC server', error)
return
}
console.log(`gRPC Server running at ${host}:${boundPort}`)
}
)
}
private handleChat(call: grpc.ServerDuplexStream<any, any>) {
let engine: QueryEngine | null = null
let appState: AppState = getDefaultAppState()
const fileCache: FileStateCache = new FileStateCache(READ_FILE_STATE_CACHE_SIZE, 25 * 1024 * 1024)
// To handle ActionRequired (ask user for permission)
const pendingRequests = new Map<string, (reply: string) => void>()
// Accumulated messages from previous turns for multi-turn context
let previousMessages: any[] = []
let sessionId = ''
let interrupted = false
call.on('data', async (clientMessage) => {
try {
if (clientMessage.request) {
if (engine) {
call.write({
error: {
message: 'A request is already in progress on this stream',
code: 'ALREADY_EXISTS'
}
})
return
}
interrupted = false
const req = clientMessage.request
sessionId = req.session_id || ''
previousMessages = []
// Load previous messages from session store (cross-stream persistence)
if (sessionId && this.sessions.has(sessionId)) {
previousMessages = [...this.sessions.get(sessionId)!]
}
const toolNameById = new Map<string, string>()
engine = new QueryEngine({
cwd: req.working_directory || process.cwd(),
tools: getTools(appState.toolPermissionContext), // Gets all available tools
commands: [], // Slash commands
mcpClients: [],
agents: [],
...(previousMessages.length > 0 ? { initialMessages: previousMessages } : {}),
includePartialMessages: true,
canUseTool: async (tool, input, context, assistantMsg, toolUseID) => {
if (toolUseID) {
toolNameById.set(toolUseID, tool.name)
}
// Notify client of the tool call first
call.write({
tool_start: {
tool_name: tool.name,
arguments_json: JSON.stringify(input),
tool_use_id: toolUseID
}
})
// Ask user for permission
const promptId = randomUUID()
const question = `Approve ${tool.name}?`
call.write({
action_required: {
prompt_id: promptId,
question,
type: 'CONFIRM_COMMAND'
}
})
return new Promise((resolve) => {
pendingRequests.set(promptId, (reply) => {
if (reply.toLowerCase() === 'yes' || reply.toLowerCase() === 'y') {
resolve({ behavior: 'allow' })
} else {
resolve({ behavior: 'deny', reason: 'User denied via gRPC' })
}
})
})
},
getAppState: () => appState,
setAppState: (updater) => { appState = updater(appState) },
readFileCache: fileCache,
userSpecifiedModel: req.model,
fallbackModel: req.model,
})
// Track accumulated response data for FinalResponse
let fullText = ''
let promptTokens = 0
let completionTokens = 0
const generator = engine.submitMessage(req.message)
for await (const msg of generator) {
if (msg.type === 'stream_event') {
if (msg.event.type === 'content_block_delta' && msg.event.delta.type === 'text_delta') {
call.write({
text_chunk: {
text: msg.event.delta.text
}
})
fullText += msg.event.delta.text
}
} else if (msg.type === 'user') {
// Extract tool results
const content = msg.message.content
if (Array.isArray(content)) {
for (const block of content) {
if (block.type === 'tool_result') {
let outputStr = ''
if (typeof block.content === 'string') {
outputStr = block.content
} else if (Array.isArray(block.content)) {
outputStr = block.content.map(c => c.type === 'text' ? c.text : '').join('\n')
}
call.write({
tool_result: {
tool_name: toolNameById.get(block.tool_use_id) ?? block.tool_use_id,
tool_use_id: block.tool_use_id,
output: outputStr,
is_error: block.is_error || false
}
})
}
}
}
} else if (msg.type === 'result') {
// Extract real token counts and final text from the result
if (msg.subtype === 'success') {
if (msg.result) {
fullText = msg.result
}
promptTokens = msg.usage?.input_tokens ?? 0
completionTokens = msg.usage?.output_tokens ?? 0
}
}
}
if (!interrupted) {
// Save messages for multi-turn context in subsequent requests
previousMessages = [...engine.getMessages()]
// Persist to session store for cross-stream resumption
if (sessionId) {
if (!this.sessions.has(sessionId) && this.sessions.size >= MAX_SESSIONS) {
// Evict oldest session (Map preserves insertion order)
this.sessions.delete(this.sessions.keys().next().value)
}
this.sessions.set(sessionId, previousMessages)
}
call.write({
done: {
full_text: fullText,
prompt_tokens: promptTokens,
completion_tokens: completionTokens
}
})
}
engine = null
} else if (clientMessage.input) {
const promptId = clientMessage.input.prompt_id
const reply = clientMessage.input.reply
if (pendingRequests.has(promptId)) {
pendingRequests.get(promptId)!(reply)
pendingRequests.delete(promptId)
}
} else if (clientMessage.cancel) {
interrupted = true
if (engine) {
engine.interrupt()
}
call.end()
}
} catch (err: any) {
console.error("Error processing stream:", err)
call.write({
error: {
message: err.message || "Internal server error",
code: "INTERNAL"
}
})
call.end()
}
})
call.on('end', () => {
interrupted = true
// Unblock any pending permission prompts so canUseTool can return
for (const resolve of pendingRequests.values()) {
resolve('no')
}
if (engine) {
engine.interrupt()
}
engine = null
pendingRequests.clear()
})
}
}

View File

@@ -1,4 +1,4 @@
// biome-ignore-all assist/source/organizeImports: internal-only import markers must not be reordered
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
import { useMemo } from 'react'
import type { Tools, ToolPermissionContext } from '../Tool.js'
import { assembleToolPool } from '../tools.js'

View File

@@ -1,147 +0,0 @@
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
import { join } from 'node:path'
const originalEnv = { ...process.env }
const originalPlatform = process.platform
const mockedClipboardPath = join(process.cwd(), 'openclaude-clipboard.txt')
const generateTempFilePathMock = mock(() => mockedClipboardPath)
const execFileNoThrowMock = mock(
async () => ({ code: 0, stdout: '', stderr: '' }),
)
mock.module('../../utils/execFileNoThrow.js', () => ({
execFileNoThrow: execFileNoThrowMock,
}))
mock.module('../../utils/tempfile.js', () => ({
generateTempFilePath: generateTempFilePathMock,
}))
async function importFreshOscModule() {
return import(`./osc.ts?ts=${Date.now()}-${Math.random()}`)
}
async function flushClipboardCopy(): Promise<void> {
await new Promise(resolve => setTimeout(resolve, 0))
}
async function waitForExecCall(
command: string,
attempts = 20,
): Promise<(typeof execFileNoThrowMock.mock.calls)[number] | undefined> {
for (let attempt = 0; attempt < attempts; attempt++) {
const call = execFileNoThrowMock.mock.calls.find(([cmd]) => cmd === command)
if (call) {
return call
}
await flushClipboardCopy()
}
return undefined
}
describe('Windows clipboard fallback', () => {
beforeEach(() => {
execFileNoThrowMock.mockClear()
generateTempFilePathMock.mockClear()
process.env = { ...originalEnv }
delete process.env['SSH_CONNECTION']
delete process.env['TMUX']
Object.defineProperty(process, 'platform', { value: 'win32' })
})
afterEach(() => {
process.env = { ...originalEnv }
Object.defineProperty(process, 'platform', { value: originalPlatform })
})
test('uses PowerShell instead of clip.exe for local Windows copy', async () => {
const { setClipboard } = await importFreshOscModule()
await setClipboard('Привет мир')
await flushClipboardCopy()
expect(execFileNoThrowMock.mock.calls.some(([cmd]) => cmd === 'clip')).toBe(
false,
)
expect(
execFileNoThrowMock.mock.calls.some(([cmd]) => cmd === 'powershell'),
).toBe(true)
})
test('passes Windows clipboard text through a UTF-8 temp file instead of stdin', async () => {
const { setClipboard } = await importFreshOscModule()
await setClipboard('Привет мир')
await flushClipboardCopy()
const windowsCall = await waitForExecCall('powershell')
expect(windowsCall?.[2]).toMatchObject({
stdin: 'ignore',
})
expect(windowsCall?.[2]).not.toMatchObject({ input: 'Привет мир' })
expect(windowsCall?.[2]).not.toMatchObject({
env: expect.objectContaining({
OPENCLAUDE_CLIPBOARD_TEXT_B64: expect.any(String),
}),
})
expect(windowsCall?.[1]).toContain(
`$text = [System.IO.File]::ReadAllText('${mockedClipboardPath.replace(/'/g, "''")}', [System.Text.Encoding]::UTF8); Set-Clipboard -Value $text`,
)
})
})
describe('clipboard path behavior remains stable', () => {
beforeEach(() => {
execFileNoThrowMock.mockClear()
process.env = { ...originalEnv }
delete process.env['SSH_CONNECTION']
delete process.env['TMUX']
})
afterEach(() => {
process.env = { ...originalEnv }
Object.defineProperty(process, 'platform', { value: originalPlatform })
})
test('getClipboardPath stays native on local macOS', async () => {
Object.defineProperty(process, 'platform', { value: 'darwin' })
const { getClipboardPath } = await importFreshOscModule()
expect(getClipboardPath()).toBe('native')
})
test('getClipboardPath stays tmux-buffer when TMUX is set', async () => {
Object.defineProperty(process, 'platform', { value: 'linux' })
process.env['TMUX'] = '/tmp/tmux-1000/default,123,0'
const { getClipboardPath } = await importFreshOscModule()
expect(getClipboardPath()).toBe('tmux-buffer')
})
test('Windows clipboard fallback is skipped over SSH', async () => {
Object.defineProperty(process, 'platform', { value: 'win32' })
process.env['SSH_CONNECTION'] = '1 2 3 4'
const { setClipboard } = await importFreshOscModule()
await setClipboard('Привет мир')
expect(execFileNoThrowMock.mock.calls.some(([cmd]) => cmd === 'powershell')).toBe(
false,
)
})
test('local macOS clipboard fallback still uses pbcopy', async () => {
Object.defineProperty(process, 'platform', { value: 'darwin' })
const { setClipboard } = await importFreshOscModule()
await setClipboard('hello')
expect(execFileNoThrowMock.mock.calls.some(([cmd]) => cmd === 'pbcopy')).toBe(
true,
)
})
})

View File

@@ -3,10 +3,8 @@
*/
import { Buffer } from 'buffer'
import { unlink, writeFile } from 'node:fs/promises'
import { env } from '../../utils/env.js'
import { execFileNoThrow } from '../../utils/execFileNoThrow.js'
import { generateTempFilePath } from '../../utils/tempfile.js'
import { BEL, ESC, ESC_TYPE, SEP } from './ansi.js'
import type { Action, Color, TabStatusAction } from './types.js'
@@ -131,7 +129,7 @@ export async function tmuxLoadBuffer(text: string): Promise<boolean> {
* Local (no SSH_CONNECTION): also shell out to a native clipboard utility.
* OSC 52 and tmux -w both depend on terminal settings — iTerm2 disables
* OSC 52 by default, VS Code shows a permission prompt on first use. Native
* utilities (pbcopy/wl-copy/xclip/xsel/PowerShell Set-Clipboard) always work locally. Over
* utilities (pbcopy/wl-copy/xclip/xsel/clip.exe) always work locally. Over
* SSH these would write to the remote clipboard — OSC 52 is the right path there.
*
* Returns the sequence for the caller to write to stdout (raw OSC 52
@@ -213,32 +211,9 @@ function copyNative(text: string): void {
return
}
case 'win32':
// Avoid piping non-ASCII text through the Windows stdin/codepage
// boundary. Write UTF-8 text to a temp file and let PowerShell read it
// directly as UTF-8 before calling Set-Clipboard.
void (async () => {
const tempPath = generateTempFilePath('openclaude-clipboard', '.txt')
const escapedTempPath = tempPath.replace(/'/g, "''")
try {
await writeFile(tempPath, text, { encoding: 'utf8' })
await execFileNoThrow(
'powershell',
[
'-NoProfile',
'-NonInteractive',
'-Command',
`$text = [System.IO.File]::ReadAllText('${escapedTempPath}', [System.Text.Encoding]::UTF8); Set-Clipboard -Value $text`,
],
{
useCwd: false,
timeout: opts.timeout,
stdin: 'ignore',
},
)
} finally {
await unlink(tempPath).catch(() => {})
}
})().catch(() => {})
// clip.exe is always available on Windows. Unicode handling is
// imperfect (system locale encoding) but good enough for a fallback.
void execFileNoThrow('clip', [], opts)
return
}
}

View File

@@ -306,7 +306,7 @@ export const DEFAULT_BINDINGS: KeybindingBlock[] = [
// Note: diff:back is handled by left arrow in detail mode
},
},
// Model picker effort cycling (internal-only)
// Model picker effort cycling (ant-only)
{
context: 'ModelPicker',
bindings: {

Some files were not shown because too many files have changed in this diff Show More