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
407 changed files with 5415 additions and 23567 deletions

View File

@@ -248,93 +248,3 @@ ANTHROPIC_API_KEY=sk-ant-your-key-here
# Enable debug logging # Enable debug logging
# CLAUDE_DEBUG=1 # CLAUDE_DEBUG=1
# =============================================================================
# WEB SEARCH (OPTIONAL)
# =============================================================================
# OpenClaude includes a web search tool. By default it uses DuckDuckGo (free)
# or the provider's native search (Anthropic firstParty / vertex).
#
# Set one API key below to enable a provider. That's it.
# ── Provider API keys — set ONE of these ────────────────────────────
# Tavily (AI-optimized search, recommended)
# TAVILY_API_KEY=tvly-your-key-here
# Exa (neural/semantic search)
# EXA_API_KEY=your-exa-key-here
# You.com (RAG-ready snippets)
# YOU_API_KEY=your-you-key-here
# Jina (s.jina.ai endpoint)
# JINA_API_KEY=your-jina-key-here
# Bing Web Search
# BING_API_KEY=your-bing-key-here
# Mojeek (privacy-focused)
# MOJEEK_API_KEY=your-mojeek-key-here
# Linkup
# LINKUP_API_KEY=your-linkup-key-here
# Firecrawl (premium, uses @mendable/firecrawl-js)
# FIRECRAWL_API_KEY=fc-your-key-here
# ── Provider selection mode ─────────────────────────────────────────
#
# WEB_SEARCH_PROVIDER controls fallback behavior:
#
# "auto" (default) — try all configured providers, fall through on failure
# "custom" — custom API only, throw on failure (NOT in auto chain)
# "firecrawl" — firecrawl only
# "tavily" — tavily only
# "exa" — exa only
# "you" — you.com only
# "jina" — jina only
# "bing" — bing only
# "mojeek" — mojeek only
# "linkup" — linkup only
# "ddg" — duckduckgo only
# "native" — anthropic native / codex only
#
# Auto mode priority: firecrawl → tavily → exa → you → jina → bing → mojeek →
# linkup → ddg
# Note: "custom" is NOT in the auto chain. To use the custom API provider,
# you must explicitly set WEB_SEARCH_PROVIDER=custom.
#
# WEB_SEARCH_PROVIDER=auto
# ── Built-in custom API presets ─────────────────────────────────────
#
# Use with WEB_KEY for the API key:
# WEB_PROVIDER=searxng|google|brave|serpapi
# WEB_KEY=your-api-key-here
# ── Custom API endpoint (advanced) ──────────────────────────────────
#
# WEB_SEARCH_API — base URL of your search endpoint
# WEB_QUERY_PARAM — query parameter name (default: "q")
# WEB_METHOD — GET or POST (default: GET)
# WEB_PARAMS — extra static query params as JSON: {"lang":"en","count":"10"}
# WEB_URL_TEMPLATE — URL template with {query} for path embedding
# WEB_BODY_TEMPLATE — custom POST body with {query} placeholder
# WEB_AUTH_HEADER — header name for API key (default: "Authorization")
# WEB_AUTH_SCHEME — prefix before key (default: "Bearer")
# WEB_HEADERS — extra headers as "Name: value; Name2: value2"
# WEB_JSON_PATH — dot-path to results array in response
# ── Custom API security guardrails ──────────────────────────────────
#
# The custom provider enforces security guardrails by default.
# Override these only if you understand the risks.
#
# WEB_CUSTOM_TIMEOUT_SEC=15 — request timeout in seconds (default 15)
# WEB_CUSTOM_MAX_BODY_KB=300 — max POST body size in KB (default 300)
# WEB_CUSTOM_ALLOW_ARBITRARY_HEADERS=false — set "true" to use non-standard headers
# WEB_CUSTOM_ALLOW_HTTP=false — set "true" to allow http:// URLs
# WEB_CUSTOM_ALLOW_PRIVATE=false — set "true" to target localhost/private IPs
# (needed for self-hosted SearXNG)

View File

@@ -16,8 +16,6 @@ jobs:
steps: steps:
- name: Check out repository - name: Check out repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
@@ -29,30 +27,12 @@ jobs:
with: with:
bun-version: 1.3.11 bun-version: 1.3.11
- name: Set up Python
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
with:
python-version: "3.12"
cache: "pip"
cache-dependency-path: python/requirements.txt
- name: Install dependencies - name: Install dependencies
run: bun install --frozen-lockfile run: bun install --frozen-lockfile
- name: Smoke check - name: Smoke check
run: bun run smoke run: bun run smoke
- name: Full unit test suite
run: bun test --max-concurrency=1
- name: Install Python test dependencies
run: python -m pip install -r python/requirements.txt
- name: Python unit tests
run: python -m pytest -q python/tests
- name: Suspicious PR intent scan
run: bun run security:pr-scan -- --base ${{ github.event.pull_request.base.sha || 'origin/main' }}
- name: Provider tests - name: Provider tests
run: bun run test:provider run: bun run test:provider

View File

@@ -1,88 +0,0 @@
name: Auto Release
on:
push:
branches:
- main
concurrency:
group: auto-release-${{ github.ref }}
cancel-in-progress: false
jobs:
release-please:
name: Release Please
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
version: ${{ steps.release.outputs.version }}
steps:
- name: Run release-please
id: release
uses: googleapis/release-please-action@16a9c90856f42705d54a6fda1823352bdc62cf38
with:
token: ${{ secrets.GITHUB_TOKEN }}
release-type: node
publish-npm:
name: Publish to npm
needs: release-please
if: ${{ needs.release-please.outputs.release_created == 'true' }}
runs-on: ubuntu-latest
environment: release
permissions:
contents: read
id-token: write
steps:
- name: Checkout release tag
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
ref: ${{ needs.release-please.outputs.tag_name }}
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 24
registry-url: https://registry.npmjs.org
- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
with:
bun-version: 1.3.11
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Run unit tests
run: bun test --max-concurrency=1
- name: Smoke test
run: bun run smoke
- name: Build
run: bun run build
- name: Dry-run package
run: npm pack --dry-run
- name: Clear token auth for trusted publishing
run: |
unset NODE_AUTH_TOKEN
echo "NODE_AUTH_TOKEN=" >> "$GITHUB_ENV"
- name: Publish to npm
run: npm publish --access public --provenance
- name: Release summary
run: |
{
echo "## Released ${{ needs.release-please.outputs.tag_name }}"
echo
echo "- npm: https://www.npmjs.com/package/@gitlawb/openclaude"
echo "- GitHub: https://github.com/Gitlawb/openclaude/releases/tag/${{ needs.release-please.outputs.tag_name }}"
} >> "$GITHUB_STEP_SUMMARY"

4
.gitignore vendored
View File

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

View File

@@ -1,3 +0,0 @@
{
".": "0.2.3"
}

View File

@@ -1,68 +0,0 @@
# Changelog
## [0.2.3](https://github.com/Gitlawb/openclaude/compare/v0.2.2...v0.2.3) (2026-04-12)
### Bug Fixes
* prevent infinite auto-compact loop for unknown 3P models ([#635](https://github.com/Gitlawb/openclaude/issues/635)) ([#636](https://github.com/Gitlawb/openclaude/issues/636)) ([aeaa658](https://github.com/Gitlawb/openclaude/commit/aeaa658f776fb8df95721e8b8962385f8b00f66a))
## [0.2.2](https://github.com/Gitlawb/openclaude/compare/v0.2.1...v0.2.2) (2026-04-12)
### Bug Fixes
* **read/edit:** make compact line prefix unambiguous for tab-indented files ([#613](https://github.com/Gitlawb/openclaude/issues/613)) ([08cc6f3](https://github.com/Gitlawb/openclaude/commit/08cc6f328711cd93ce9fa53351266c29a0b0a341))
## [0.2.1](https://github.com/Gitlawb/openclaude/compare/v0.2.0...v0.2.1) (2026-04-12)
### Bug Fixes
* **provider:** add recovery guidance for missing OpenAI API key ([#616](https://github.com/Gitlawb/openclaude/issues/616)) ([9419e8a](https://github.com/Gitlawb/openclaude/commit/9419e8a4a21b3771d9ddb10f7072e0a8c5b5b631))
## [0.2.0](https://github.com/Gitlawb/openclaude/compare/v0.1.8...v0.2.0) (2026-04-12)
### Features
* add /cache-probe diagnostic command ([#580](https://github.com/Gitlawb/openclaude/issues/580)) ([9ccaa7a](https://github.com/Gitlawb/openclaude/commit/9ccaa7a6759b6991f4a566b4118c06e68a2398fe)), closes [#515](https://github.com/Gitlawb/openclaude/issues/515)
* add auto-fix service — auto-lint and test after AI file edits ([#508](https://github.com/Gitlawb/openclaude/issues/508)) ([c385047](https://github.com/Gitlawb/openclaude/commit/c385047abba4366866f4c87bfb5e0b0bd4dcbb9d))
* Add Gemini support with thought_signature fix ([#404](https://github.com/Gitlawb/openclaude/issues/404)) ([5012c16](https://github.com/Gitlawb/openclaude/commit/5012c160c9a2dff9418e7ee19dc9a4d29ef2b024))
* add headless gRPC server for external agent integration ([#278](https://github.com/Gitlawb/openclaude/issues/278)) ([26eef92](https://github.com/Gitlawb/openclaude/commit/26eef92fe72e9c3958d61435b8d3571e12bf2b74))
* add wiki mvp commands ([#532](https://github.com/Gitlawb/openclaude/issues/532)) ([c328fdf](https://github.com/Gitlawb/openclaude/commit/c328fdf9e2fe59ad101b049301298ce9ff24caca))
* GitHub provider lifecycle and onboarding hardening ([#351](https://github.com/Gitlawb/openclaude/issues/351)) ([ff7d499](https://github.com/Gitlawb/openclaude/commit/ff7d49990de515825ddbe4099f3a39b944b61370))
### Bug Fixes
* add File polyfill for Node < 20 to prevent startup deadlock with proxy ([#442](https://github.com/Gitlawb/openclaude/issues/442)) ([85aa8b0](https://github.com/Gitlawb/openclaude/commit/85aa8b0985c8f3cb8801efa5141114a0ab0f6a83))
* add GitHub Copilot model context windows and output limits ([#576](https://github.com/Gitlawb/openclaude/issues/576)) ([a7f5982](https://github.com/Gitlawb/openclaude/commit/a7f5982f6438ab0ddc3f0daae31ea68ac7ac206c)), closes [#515](https://github.com/Gitlawb/openclaude/issues/515)
* add LiteLLM-style aliases for GitHub Copilot context windows ([#606](https://github.com/Gitlawb/openclaude/issues/606)) ([2e0e14d](https://github.com/Gitlawb/openclaude/commit/2e0e14d71313e0e501efaa9e55c6c56f2742fb10))
* add store:false to Chat Completions and /responses fallback ([#578](https://github.com/Gitlawb/openclaude/issues/578)) ([8aaa4f2](https://github.com/Gitlawb/openclaude/commit/8aaa4f22ac5b942d82aa9cad54af30d56034515a))
* address code scanning alerts ([#434](https://github.com/Gitlawb/openclaude/issues/434)) ([e365cb4](https://github.com/Gitlawb/openclaude/commit/e365cb4010becabacd7cbccb4c3e59ea23a41e90))
* avoid sync github credential reads in provider manager ([#428](https://github.com/Gitlawb/openclaude/issues/428)) ([aff2bd8](https://github.com/Gitlawb/openclaude/commit/aff2bd87e4f2821992f74fb95481c505d0ba5d5d))
* convert dragged file paths to [@mentions](https://github.com/mentions) for attachment ([#382](https://github.com/Gitlawb/openclaude/issues/382)) ([112df59](https://github.com/Gitlawb/openclaude/commit/112df5911791ea71ee9efbb98ea59c5ded1ea161))
* custom web search — WEB_URL_TEMPLATE not recognized, timeout too short, silent native fallback ([#537](https://github.com/Gitlawb/openclaude/issues/537)) ([32fbd0c](https://github.com/Gitlawb/openclaude/commit/32fbd0c7b4168b32dcb13a5b69342e2727269201))
* defer startup checks and suppress recommendation dialogs during startup window (issue [#363](https://github.com/Gitlawb/openclaude/issues/363)) ([#504](https://github.com/Gitlawb/openclaude/issues/504)) ([2caf2fd](https://github.com/Gitlawb/openclaude/commit/2caf2fd982af1ec845c50152ad9d28d1a597f82f))
* display selected model in startup screen instead of hardcoded sonnet 4.6 ([#587](https://github.com/Gitlawb/openclaude/issues/587)) ([b126e38](https://github.com/Gitlawb/openclaude/commit/b126e38b1affddd2de83fcc3ba26f2e44b42a509))
* handle missing skill parameter in SkillTool ([#485](https://github.com/Gitlawb/openclaude/issues/485)) ([f9ce81b](https://github.com/Gitlawb/openclaude/commit/f9ce81bfb384e909353813fb6f6760cadd508ae7))
* include MCP tool results in microcompact to reduce token waste ([#348](https://github.com/Gitlawb/openclaude/issues/348)) ([52d33a8](https://github.com/Gitlawb/openclaude/commit/52d33a87a047b943aedaaaf772cd48636c263509))
* **ink:** restore host prop updates in React 19 reconciler ([#589](https://github.com/Gitlawb/openclaude/issues/589)) ([6e94dd9](https://github.com/Gitlawb/openclaude/commit/6e94dd913688b2d6433a9abe62a245c5f031b776))
* let saved provider profiles win on restart ([#513](https://github.com/Gitlawb/openclaude/issues/513)) ([cb8f8b7](https://github.com/Gitlawb/openclaude/commit/cb8f8b7ac2e3e74516ee219a3a48156db7c6ed78))
* normalize malformed Bash tool arguments from OpenAI-compatible providers ([#385](https://github.com/Gitlawb/openclaude/issues/385)) ([b4bd95b](https://github.com/Gitlawb/openclaude/commit/b4bd95b47715c9896240d708c106777507fd26ec))
* preserve only originally-required properties in strict tool schemas ([#471](https://github.com/Gitlawb/openclaude/issues/471)) ([ccaa193](https://github.com/Gitlawb/openclaude/commit/ccaa193eec5761f0972ffb58eb3189a81a9244b0))
* preserve unicode in Windows clipboard fallback ([#388](https://github.com/Gitlawb/openclaude/issues/388)) ([c193497](https://github.com/Gitlawb/openclaude/commit/c1934974aaf64db460cc850a044bd13cc744cce7))
* rebrand prompt identity to openclaude ([#496](https://github.com/Gitlawb/openclaude/issues/496)) ([598651f](https://github.com/Gitlawb/openclaude/commit/598651f42389ce76311ec00e8a9c701c939ead27))
* replace isDeepStrictEqual with navigation-aware options comparison ([#507](https://github.com/Gitlawb/openclaude/issues/507)) ([537c469](https://github.com/Gitlawb/openclaude/commit/537c469c3a2f7cb0eed05fa2f54dca57b6bc273f)), closes [#472](https://github.com/Gitlawb/openclaude/issues/472)
* report cache reads in streaming and correct cost calculation ([#577](https://github.com/Gitlawb/openclaude/issues/577)) ([f4ac709](https://github.com/Gitlawb/openclaude/commit/f4ac709fa6eda732bf45204fcab625ba6c5674b9))
* restore default context window for unknown 3p models ([#494](https://github.com/Gitlawb/openclaude/issues/494)) ([69ea1f1](https://github.com/Gitlawb/openclaude/commit/69ea1f1e4a99e9436215d8cb391a116a64442b94))
* restore Grep and Glob reliability on OpenAI paths ([#461](https://github.com/Gitlawb/openclaude/issues/461)) ([600c01f](https://github.com/Gitlawb/openclaude/commit/600c01faf761a080a2c7dede872ddbe05a132f23))
* restore Ollama auto-detect in first-run setup ([#561](https://github.com/Gitlawb/openclaude/issues/561)) ([68c2968](https://github.com/Gitlawb/openclaude/commit/68c296833dcef54ce44cb18b24357230b5204dbc))
* scrub canonical Anthropic headers from 3P shim requests ([#499](https://github.com/Gitlawb/openclaude/issues/499)) ([07621a6](https://github.com/Gitlawb/openclaude/commit/07621a6f8d0918170281869a47b5dbff90e71594))
* strip Anthropic params from 3P resume paths ([#479](https://github.com/Gitlawb/openclaude/issues/479)) ([4975cfc](https://github.com/Gitlawb/openclaude/commit/4975cfc2e0ddbe34aa4e8e3f52ee5eba07fbe465))
* suppress startup dialogs when input is buffered ([#423](https://github.com/Gitlawb/openclaude/issues/423)) ([8ece290](https://github.com/Gitlawb/openclaude/commit/8ece2900872dadd157e798ef501ddf126dac66c4))
* **tui:** restore prompt rendering on startup ([#498](https://github.com/Gitlawb/openclaude/issues/498)) ([e30ad17](https://github.com/Gitlawb/openclaude/commit/e30ad17ae0056787273be2caafd6cf5340b6ab57))
* update theme preview on focus change ([#562](https://github.com/Gitlawb/openclaude/issues/562)) ([6924718](https://github.com/Gitlawb/openclaude/commit/692471850fc789ee0797190089272407f9a4d953))
* **web-search:** close SSRF bypasses in custom provider hostname guard ([#610](https://github.com/Gitlawb/openclaude/issues/610)) ([a02c441](https://github.com/Gitlawb/openclaude/commit/a02c44143b257fbee7f38f1b93873cc0ea68a1f9))
* WebSearch providers + MCPTool bugs ([#593](https://github.com/Gitlawb/openclaude/issues/593)) ([91e4cfb](https://github.com/Gitlawb/openclaude/commit/91e4cfb15b62c04615834fd3c417fe38b4feb914))

211
README.md
View File

@@ -1,24 +1,33 @@
# OpenClaude # 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. 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.
[![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)
## Why OpenClaude ## 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` - Save provider profiles inside the app with `/provider`
- Run with OpenAI-compatible services, Gemini, GitHub Models, Codex, Ollama, Atomic Chat, and other supported providers - Run locally with Ollama or Atomic Chat
- Keep coding-agent workflows in one place: bash, file tools, grep, glob, agents, tasks, MCP, and web tools - Keep core coding-agent workflows: bash, file tools, grep, glob, agents, tasks, MCP, and web tools
- Use the bundled VS Code extension for launch integration and theme support
## 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 ## Quick Start
@@ -28,7 +37,7 @@ Use OpenAI-compatible APIs, Gemini, GitHub Models, Codex, Ollama, Atomic Chat, a
npm install -g @gitlawb/openclaude 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 ### Start
@@ -38,8 +47,8 @@ openclaude
Inside OpenClaude: Inside OpenClaude:
- run `/provider` for guided provider setup and saved profiles - run `/provider` for guided setup of OpenAI-compatible, Gemini, Ollama, or Codex profiles
- run `/onboard-github` for GitHub Models onboarding - run `/onboard-github` for GitHub Models setup
### Fastest OpenAI setup ### Fastest OpenAI setup
@@ -85,6 +94,8 @@ $env:OPENAI_MODEL="qwen2.5-coder:7b"
openclaude openclaude
``` ```
---
## Setup Guides ## Setup Guides
Beginner-friendly guides: Beginner-friendly guides:
@@ -98,26 +109,38 @@ Advanced and source-build guides:
- [Advanced Setup](docs/advanced-setup.md) - [Advanced Setup](docs/advanced-setup.md)
- [Android Install](ANDROID_INSTALL.md) - [Android Install](ANDROID_INSTALL.md)
---
## Supported Providers ## Supported Providers
| Provider | Setup Path | Notes | | Provider | Setup Path | Notes |
| --- | --- | --- | | --- | --- | --- |
| OpenAI-compatible | `/provider` or env vars | Works with OpenAI, OpenRouter, DeepSeek, Groq, Mistral, LM Studio, and other compatible `/v1` servers | | 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 | Supports API key, access token, or local ADC workflow on current `main` | | Gemini | `/provider` or env vars | Google Gemini support through the runtime provider layer |
| GitHub Models | `/onboard-github` | Interactive onboarding with saved credentials | | GitHub Models | `/onboard-github` | Interactive onboarding with saved credentials |
| Codex | `/provider` | Uses existing Codex credentials when available | | Codex | `/provider` | Uses existing Codex credentials when available |
| Ollama | `/provider` or env vars | Local inference with no API key | | Ollama | `/provider` or env vars | Local inference with no API key |
| Atomic Chat | advanced setup | Local Apple Silicon backend | | Atomic Chat | advanced setup | Local Apple Silicon backend |
| Bedrock / Vertex / Foundry | env vars | Additional provider integrations for supported environments | | Bedrock / Vertex / Foundry | env vars | Additional provider integrations for supported environments |
---
## What Works ## What Works
- **Tool-driven coding workflows**: Bash, file read/write/edit, grep, glob, agents, tasks, MCP, and slash commands - Tool-driven coding workflows
- **Streaming responses**: Real-time token output and tool progress Bash, file read/write/edit, grep, glob, agents, tasks, MCP, and slash commands
- **Tool calling**: Multi-step tool loops with model calls, tool execution, and follow-up responses - Streaming responses
- **Images**: URL and base64 image inputs for providers that support vision Real-time token output and tool progress
- **Provider profiles**: Guided setup plus saved `.openclaude-profile.json` support - Tool calling
- **Local and remote model backends**: Cloud APIs, local servers, and Apple Silicon local inference 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 ## 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. For best results, use models with strong tool/function calling support.
---
## Agent Routing ## 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`: 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. > **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 ## 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: 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: 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 - `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. Free tier at [firecrawl.dev](https://firecrawl.dev) includes 500 credits. The key is optional.
--- ---
## Headless gRPC Server ## Source Build
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
```bash ```bash
bun install bun install
@@ -231,78 +235,22 @@ node dist/cli.mjs
Helpful commands: Helpful commands:
- `bun run dev` - `bun run dev`
- `bun test`
- `bun run test:coverage`
- `bun run security:pr-scan -- --base origin/main`
- `bun run smoke` - `bun run smoke`
- `bun run doctor:runtime` - `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 ## 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 ## Security
If you believe you found a security issue, see [SECURITY.md](SECURITY.md). 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 ## 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: For larger changes, open an issue first so the scope is clear before implementation. Helpful validation commands include:
- `bun run build` - `bun run build`
- `bun run test:coverage`
- `bun run smoke` - `bun run smoke`
- focused `bun test ...` runs for touched areas - focused `bun test ...` runs for touched areas
---
## Disclaimer ## Disclaimer
OpenClaude is an independent community project and is not affiliated with, endorsed by, or sponsored by Anthropic. 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 ## License
See [LICENSE](LICENSE). MIT

View File

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

129
bun.lock
View File

@@ -13,8 +13,6 @@
"@anthropic-ai/vertex-sdk": "0.14.4", "@anthropic-ai/vertex-sdk": "0.14.4",
"@commander-js/extra-typings": "12.1.0", "@commander-js/extra-typings": "12.1.0",
"@growthbook/growthbook": "1.6.5", "@growthbook/growthbook": "1.6.5",
"@grpc/grpc-js": "^1.14.3",
"@grpc/proto-loader": "^0.8.0",
"@mendable/firecrawl-js": "4.18.1", "@mendable/firecrawl-js": "4.18.1",
"@modelcontextprotocol/sdk": "1.29.0", "@modelcontextprotocol/sdk": "1.29.0",
"@opentelemetry/api": "1.9.1", "@opentelemetry/api": "1.9.1",
@@ -38,7 +36,6 @@
"cli-highlight": "2.1.11", "cli-highlight": "2.1.11",
"code-excerpt": "4.0.0", "code-excerpt": "4.0.0",
"commander": "12.1.0", "commander": "12.1.0",
"cross-spawn": "7.0.6",
"diff": "8.0.3", "diff": "8.0.3",
"duck-duck-scrape": "^2.2.7", "duck-duck-scrape": "^2.2.7",
"emoji-regex": "10.6.0", "emoji-regex": "10.6.0",
@@ -53,7 +50,7 @@
"ignore": "7.0.5", "ignore": "7.0.5",
"indent-string": "5.0.0", "indent-string": "5.0.0",
"jsonc-parser": "3.3.1", "jsonc-parser": "3.3.1",
"lodash-es": "4.18.1", "lodash-es": "4.18.0",
"lru-cache": "11.2.7", "lru-cache": "11.2.7",
"marked": "15.0.12", "marked": "15.0.12",
"p-map": "7.0.4", "p-map": "7.0.4",
@@ -86,14 +83,10 @@
"@types/bun": "1.3.11", "@types/bun": "1.3.11",
"@types/node": "25.5.0", "@types/node": "25.5.0",
"@types/react": "19.2.14", "@types/react": "19.2.14",
"tsx": "^4.21.0",
"typescript": "5.9.3", "typescript": "5.9.3",
}, },
}, },
}, },
"overrides": {
"lodash-es": "4.18.1",
},
"packages": { "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=="], "@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=="], "@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=="], "@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=="], "@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=="], "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=="], "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=="], "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=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], "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=="], "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=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"fuse.js": ["fuse.js@7.1.0", "", {}, "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ=="], "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-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-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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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/@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=="], "@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=="], "@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/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=="], "@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/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/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=="], "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=="], "@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/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=="], "@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/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/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=="], "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=="], "@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/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=="], "@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=="], "@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/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=="], "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=="], "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/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=="], "@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/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=="], "qrcode/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],

View File

@@ -137,9 +137,10 @@ export OPENAI_MODEL=llama-3.3-70b-versatile
### Mistral ### Mistral
```bash ```bash
export CLAUDE_CODE_USE_MISTRAL=1 export CLAUDE_CODE_USE_OPENAI=1
export MISTRAL_API_KEY=... export OPENAI_API_KEY=...
export MISTRAL_MODEL=mistral-large-latest export OPENAI_BASE_URL=https://api.mistral.ai/v1
export OPENAI_MODEL=mistral-large-latest
``` ```
### Azure OpenAI ### Azure OpenAI

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

View File

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

View File

@@ -1,3 +0,0 @@
pytest==7.4.4
pytest-asyncio==0.23.3
httpx==0.25.2

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

@@ -1,11 +0,0 @@
{
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"packages": {
".": {
"release-type": "node",
"package-name": "@gitlawb/openclaude",
"bump-minor-pre-major": true,
"include-v-in-tag": true
}
}
}

View File

@@ -3,7 +3,7 @@
* distributable JS file using Bun's bundler. * distributable JS file using Bun's bundler.
* *
* Handles: * 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 * - MACRO.* globals → inlined version/build-time constants
* - src/ path aliases * - src/ path aliases
*/ */
@@ -14,9 +14,8 @@ import { noTelemetryPlugin } from './no-telemetry-plugin'
const pkg = JSON.parse(readFileSync('./package.json', 'utf-8')) const pkg = JSON.parse(readFileSync('./package.json', 'utf-8'))
const version = pkg.version const version = pkg.version
// Feature flags for the open build. // Feature flags — all disabled for the open build.
// Most Anthropic-internal features stay off; open-build features can be // These gate Anthropic-internal features (voice, proactive, kairos, etc.)
// selectively enabled here when their full source exists in the mirror.
const featureFlags: Record<string, boolean> = { const featureFlags: Record<string, boolean> = {
VOICE_MODE: false, VOICE_MODE: false,
PROACTIVE: false, PROACTIVE: false,
@@ -38,7 +37,7 @@ const featureFlags: Record<string, boolean> = {
TRANSCRIPT_CLASSIFIER: false, TRANSCRIPT_CLASSIFIER: false,
WEB_BROWSER_TOOL: false, WEB_BROWSER_TOOL: false,
MESSAGE_ACTIONS: false, MESSAGE_ACTIONS: false,
BUDDY: true, BUDDY: false,
CHICAGO_MCP: false, CHICAGO_MCP: false,
COWORKER_TYPE_TELEMETRY: false, COWORKER_TYPE_TELEMETRY: false,
} }
@@ -111,7 +110,7 @@ export async function handleBgFlag() { throw new Error("Background sessions are
build.onLoad( build.onLoad(
{ filter: /.*/, namespace: 'bun-bundle-shim' }, { 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', loader: 'js',
}), }),
) )
@@ -251,103 +250,6 @@ export const SeverityNumber = {};
loader: 'js', 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': ` 'services/internalLogging': `
export async function logPermissionContextForAnts() {} export async function logPermissionContextForAnts() {}
export const getContainerId = async () => null; 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

@@ -11,7 +11,6 @@ import {
buildAtomicChatProfileEnv, buildAtomicChatProfileEnv,
buildCodexProfileEnv, buildCodexProfileEnv,
buildGeminiProfileEnv, buildGeminiProfileEnv,
buildMistralProfileEnv,
buildOllamaProfileEnv, buildOllamaProfileEnv,
buildOpenAIProfileEnv, buildOpenAIProfileEnv,
createProfileFile, createProfileFile,
@@ -38,7 +37,7 @@ function parseArg(name: string): string | null {
function parseProviderArg(): ProviderProfile | 'auto' { function parseProviderArg(): ProviderProfile | 'auto' {
const p = parseArg('--provider')?.toLowerCase() const p = parseArg('--provider')?.toLowerCase()
if (p === 'openai' || p === 'ollama' || p === 'codex' || p === 'gemini' || p === 'mistral' || p === 'atomic-chat') return p if (p === 'openai' || p === 'ollama' || p === 'codex' || p === 'gemini' || p === 'atomic-chat') return p
return 'auto' return 'auto'
} }
@@ -91,21 +90,6 @@ async function main(): Promise<void> {
process.exit(1) process.exit(1)
} }
env = builtEnv
} else if (selected === 'mistral') {
const builtEnv = buildMistralProfileEnv({
model: argModel || null,
baseUrl: argBaseUrl || null,
apiKey: argApiKey || null,
processEnv: process.env,
})
if (!builtEnv) {
console.error('Mistral profile requires an API key. Use --api-key or set MISTRAL_API_KEY.')
console.error('Get a free key at: https://admin.mistral.ai/organization/api-keys')
process.exit(1)
}
env = builtEnv env = builtEnv
} else if (selected === 'ollama') { } else if (selected === 'ollama') {
resolvedOllamaModel ??= await resolveOllamaModel(argModel, argBaseUrl, goal) resolvedOllamaModel ??= await resolveOllamaModel(argModel, argBaseUrl, goal)
@@ -185,7 +169,7 @@ async function main(): Promise<void> {
console.log(`Saved profile: ${selected}`) console.log(`Saved profile: ${selected}`)
console.log(`Goal: ${goal}`) console.log(`Goal: ${goal}`)
console.log(`Model: ${profile.env.GEMINI_MODEL || profile.env.MISTRAL_MODEL || profile.env.OPENAI_MODEL || getGoalDefaultOpenAIModel(goal)}`) console.log(`Model: ${profile.env.GEMINI_MODEL || profile.env.OPENAI_MODEL || getGoalDefaultOpenAIModel(goal)}`)
console.log(`Path: ${outputPath}`) console.log(`Path: ${outputPath}`)
console.log('Next: bun run dev:profile') console.log('Next: bun run dev:profile')
} }

View File

@@ -50,7 +50,7 @@ function parseLaunchOptions(argv: string[]): LaunchOptions {
continue continue
} }
if ((lower === 'auto' || lower === 'openai' || lower === 'ollama' || lower === 'codex' || lower === 'gemini' || lower ==='mistral' || lower === 'atomic-chat') && requestedProfile === 'auto') { if ((lower === 'auto' || lower === 'openai' || lower === 'ollama' || lower === 'codex' || lower === 'gemini' || lower === 'atomic-chat') && requestedProfile === 'auto') {
requestedProfile = lower as ProviderProfile | 'auto' requestedProfile = lower as ProviderProfile | 'auto'
continue continue
} }
@@ -124,8 +124,6 @@ function printSummary(profile: ProviderProfile): void {
console.log(`Launching profile: ${profile}`) console.log(`Launching profile: ${profile}`)
if (profile === 'gemini') { if (profile === 'gemini') {
console.log('Using configured Gemini provider settings.') console.log('Using configured Gemini provider settings.')
} else if (profile === 'mistral') {
console.log('Using configured Mistral provider settings.')
} else if (profile === 'codex') { } else if (profile === 'codex') {
console.log('Using configured Codex/OpenAI-compatible provider settings.') console.log('Using configured Codex/OpenAI-compatible provider settings.')
} else if (profile === 'atomic-chat') { } else if (profile === 'atomic-chat') {
@@ -141,7 +139,7 @@ async function main(): Promise<void> {
const options = parseLaunchOptions(process.argv.slice(2)) const options = parseLaunchOptions(process.argv.slice(2))
const requestedProfile = options.requestedProfile const requestedProfile = options.requestedProfile
if (!requestedProfile) { if (!requestedProfile) {
console.error('Usage: bun run scripts/provider-launch.ts [openai|ollama|codex|gemini|mistral|atomic-chat|mistral|auto] [--fast] [--goal <latency|balanced|coding>] [-- <cli args>]') console.error('Usage: bun run scripts/provider-launch.ts [openai|ollama|codex|gemini|atomic-chat|auto] [--fast] [--goal <latency|balanced|coding>] [-- <cli args>]')
process.exit(1) process.exit(1)
} }
@@ -207,11 +205,6 @@ async function main(): Promise<void> {
process.exit(1) process.exit(1)
} }
if (profile === 'mistral' && !env.MISTRAL_API_KEY) {
console.error('MISTRAL_API_KEY is required for mistral profile. Run: bun run profile:init -- --provider mistral --api-key <key>')
process.exit(1)
}
if (profile === 'openai' && (!env.OPENAI_API_KEY || env.OPENAI_API_KEY === 'SUA_CHAVE')) { if (profile === 'openai' && (!env.OPENAI_API_KEY || env.OPENAI_API_KEY === 'SUA_CHAVE')) {
console.error('OPENAI_API_KEY is required for openai profile and cannot be SUA_CHAVE. Run: bun run profile:init -- --provider openai --api-key <key>') console.error('OPENAI_API_KEY is required for openai profile and cannot be SUA_CHAVE. Run: bun run profile:init -- --provider openai --api-key <key>')
process.exit(1) process.exit(1)

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

@@ -118,18 +118,14 @@ function isLocalBaseUrl(baseUrl: string): boolean {
} }
const GEMINI_DEFAULT_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta/openai' const GEMINI_DEFAULT_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta/openai'
const MISTRAL_DEFAULT_BASE_URL = 'https://api.mistral.ai/v1' const GITHUB_MODELS_DEFAULT_BASE = 'https://models.github.ai/inference'
const GITHUB_COPILOT_BASE = 'https://api.githubcopilot.com'
function currentBaseUrl(): string { function currentBaseUrl(): string {
if (isTruthy(process.env.CLAUDE_CODE_USE_GEMINI)) { if (isTruthy(process.env.CLAUDE_CODE_USE_GEMINI)) {
return process.env.GEMINI_BASE_URL ?? GEMINI_DEFAULT_BASE_URL return process.env.GEMINI_BASE_URL ?? GEMINI_DEFAULT_BASE_URL
} }
if (isTruthy(process.env.CLAUDE_CODE_USE_MISTRAL)) {
return process.env.MISTRAL_BASE_URL ?? MISTRAL_DEFAULT_BASE_URL
}
if (isTruthy(process.env.CLAUDE_CODE_USE_GITHUB)) { if (isTruthy(process.env.CLAUDE_CODE_USE_GITHUB)) {
return process.env.OPENAI_BASE_URL ?? GITHUB_COPILOT_BASE return process.env.OPENAI_BASE_URL ?? GITHUB_MODELS_DEFAULT_BASE
} }
return process.env.OPENAI_BASE_URL ?? 'https://api.openai.com/v1' return process.env.OPENAI_BASE_URL ?? 'https://api.openai.com/v1'
} }
@@ -159,34 +155,9 @@ function checkGeminiEnv(): CheckResult[] {
return results return results
} }
function checkMistralEnv(): CheckResult[] {
const results: CheckResult[] = []
const model = process.env.MISTRAL_MODEL
const key = process.env.MISTRAL_API_KEY
const baseUrl = process.env.MISTRAL_BASE_URL ?? MISTRAL_DEFAULT_BASE_URL
results.push(pass('Provider mode', 'Mistral provider enabled.'))
if (!model) {
results.push(pass('MISTRAL_MODEL', 'Not set. Default will be used at runtime.'))
} else {
results.push(pass('MISTRAL_MODEL', model))
}
results.push(pass('MISTRAL_BASE_URL', baseUrl))
if (!key) {
results.push(fail('MISTRAL_API_KEY', 'Missing. Set MISTRAL_API_KEY.'))
} else {
results.push(pass('MISTRAL_API_KEY', 'Configured.'))
}
return results
}
function checkGithubEnv(): CheckResult[] { function checkGithubEnv(): CheckResult[] {
const results: CheckResult[] = [] const results: CheckResult[] = []
const baseUrl = process.env.OPENAI_BASE_URL ?? GITHUB_COPILOT_BASE const baseUrl = process.env.OPENAI_BASE_URL ?? GITHUB_MODELS_DEFAULT_BASE
results.push(pass('Provider mode', 'GitHub Models provider enabled.')) results.push(pass('Provider mode', 'GitHub Models provider enabled.'))
const token = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN const token = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN
@@ -215,17 +186,12 @@ function checkOpenAIEnv(): CheckResult[] {
const results: CheckResult[] = [] const results: CheckResult[] = []
const useGemini = isTruthy(process.env.CLAUDE_CODE_USE_GEMINI) const useGemini = isTruthy(process.env.CLAUDE_CODE_USE_GEMINI)
const useGithub = isTruthy(process.env.CLAUDE_CODE_USE_GITHUB) const useGithub = isTruthy(process.env.CLAUDE_CODE_USE_GITHUB)
const useMistral = isTruthy(process.env.CLAUDE_CODE_USE_MISTRAL)
const useOpenAI = isTruthy(process.env.CLAUDE_CODE_USE_OPENAI) const useOpenAI = isTruthy(process.env.CLAUDE_CODE_USE_OPENAI)
if (useGemini) { if (useGemini) {
return checkGeminiEnv() return checkGeminiEnv()
} }
if (useMistral) {
return checkMistralEnv()
}
if (useGithub && !useOpenAI) { if (useGithub && !useOpenAI) {
return checkGithubEnv() return checkGithubEnv()
} }
@@ -302,9 +268,8 @@ async function checkBaseUrlReachability(): Promise<CheckResult> {
const useGemini = isTruthy(process.env.CLAUDE_CODE_USE_GEMINI) const useGemini = isTruthy(process.env.CLAUDE_CODE_USE_GEMINI)
const useOpenAI = isTruthy(process.env.CLAUDE_CODE_USE_OPENAI) const useOpenAI = isTruthy(process.env.CLAUDE_CODE_USE_OPENAI)
const useGithub = isTruthy(process.env.CLAUDE_CODE_USE_GITHUB) const useGithub = isTruthy(process.env.CLAUDE_CODE_USE_GITHUB)
const useMistral = isTruthy(process.env.CLAUDE_CODE_USE_MISTRAL)
if (!useGemini && !useOpenAI && !useGithub && !useMistral) { if (!useGemini && !useOpenAI && !useGithub) {
return pass('Provider reachability', 'Skipped (OpenAI-compatible mode disabled).') return pass('Provider reachability', 'Skipped (OpenAI-compatible mode disabled).')
} }
@@ -361,8 +326,6 @@ async function checkBaseUrlReachability(): Promise<CheckResult> {
}) })
} else if (useGemini && (process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY)) { } else if (useGemini && (process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY)) {
headers.Authorization = `Bearer ${process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY}` headers.Authorization = `Bearer ${process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY}`
} else if (useMistral && process.env.MISTRAL_API_KEY) {
headers.Authorization = `Bearer ${process.env.MISTRAL_API_KEY}`
} else if (process.env.OPENAI_API_KEY) { } else if (process.env.OPENAI_API_KEY) {
headers.Authorization = `Bearer ${process.env.OPENAI_API_KEY}` headers.Authorization = `Bearer ${process.env.OPENAI_API_KEY}`
} }
@@ -410,8 +373,7 @@ function checkOllamaProcessorMode(): CheckResult {
if ( if (
!isTruthy(process.env.CLAUDE_CODE_USE_OPENAI) || !isTruthy(process.env.CLAUDE_CODE_USE_OPENAI) ||
isTruthy(process.env.CLAUDE_CODE_USE_GEMINI) || isTruthy(process.env.CLAUDE_CODE_USE_GEMINI) ||
isTruthy(process.env.CLAUDE_CODE_USE_GITHUB) || isTruthy(process.env.CLAUDE_CODE_USE_GITHUB)
isTruthy(process.env.CLAUDE_CODE_USE_MISTRAL)
) { ) {
return pass('Ollama processor mode', 'Skipped (OpenAI-compatible mode disabled).') return pass('Ollama processor mode', 'Skipped (OpenAI-compatible mode disabled).')
} }
@@ -463,14 +425,6 @@ function serializeSafeEnvSummary(): Record<string, string | boolean> {
GEMINI_API_KEY_SET: Boolean(process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY), GEMINI_API_KEY_SET: Boolean(process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY),
} }
} }
if (isTruthy(process.env.CLAUDE_CODE_USE_MISTRAL)) {
return {
CLAUDE_CODE_USE_MISTRAL: true,
MISTRAL_MODEL: process.env.MISTRAL_MODEL ?? '(unset, default: devstral-latest)',
MISTRAL_BASE_URL: process.env.MISTRAL_BASE_URL ?? 'https://api.mistral.ai/v1',
MISTRAL_API_KEY_SET: Boolean(process.env.MISTRAL_API_KEY),
}
}
if ( if (
isTruthy(process.env.CLAUDE_CODE_USE_GITHUB) && isTruthy(process.env.CLAUDE_CODE_USE_GITHUB) &&
!isTruthy(process.env.CLAUDE_CODE_USE_OPENAI) !isTruthy(process.env.CLAUDE_CODE_USE_OPENAI)
@@ -481,7 +435,7 @@ function serializeSafeEnvSummary(): Record<string, string | boolean> {
process.env.OPENAI_MODEL ?? process.env.OPENAI_MODEL ??
'(unset, default: github:copilot → openai/gpt-4.1)', '(unset, default: github:copilot → openai/gpt-4.1)',
OPENAI_BASE_URL: OPENAI_BASE_URL:
process.env.OPENAI_BASE_URL ?? GITHUB_COPILOT_BASE, process.env.OPENAI_BASE_URL ?? GITHUB_MODELS_DEFAULT_BASE,
GITHUB_TOKEN_SET: Boolean( GITHUB_TOKEN_SET: Boolean(
process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN, process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN,
), ),

View File

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

View File

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

View File

@@ -112,14 +112,6 @@ def build_default_providers() -> list[Provider]:
big_model=big if "gemini" in big else "gemini-2.5-pro", big_model=big if "gemini" in big else "gemini-2.5-pro",
small_model=small if "gemini" in small else "gemini-2.0-flash", small_model=small if "gemini" in small else "gemini-2.0-flash",
), ),
Provider(
name="mistral",
ping_url="",
api_key_env="MISTRAL_API_KEY",
cost_per_1k_tokens=0.0001,
big_model=big if "mistral" in big else "devstral-latest",
small_model=small if "small" in small else "ministral-3b-latest",
),
Provider( Provider(
name="ollama", name="ollama",
ping_url=f"{ollama_url}/api/tags", ping_url=f"{ollama_url}/api/tags",

View File

@@ -112,7 +112,7 @@ type State = {
agentColorIndex: number agentColorIndex: number
// Last API request for bug reports // Last API request for bug reports
lastAPIRequest: Omit<BetaMessageStreamParams, 'messages'> | null 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 // Captures the exact post-compaction, CLAUDE.md-injected message set sent
// to the API so /share's serialized_conversation.json reflects reality. // to the API so /share's serialized_conversation.json reflects reality.
lastAPIRequestMessages: BetaMessageStreamParams['messages'] | null lastAPIRequestMessages: BetaMessageStreamParams['messages'] | null
@@ -185,7 +185,7 @@ type State = {
agentId: string | null agentId: string | null
} }
> >
// Track slow operations for dev bar display (internal-only) // Track slow operations for dev bar display (ant-only)
slowOperations: Array<{ slowOperations: Array<{
operation: string operation: string
durationMs: number durationMs: number
@@ -1756,12 +1756,3 @@ export function setPromptId(id: string | null): void {
STATE.promptId = id 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 * CLAUDE_BRIDGE_* dev overrides that were previously copy-pasted across
* a dozen files — inboundAttachments, BriefTool/upload, bridgeMain, * a dozen files — inboundAttachments, BriefTool/upload, bridgeMain,
* initReplBridge, remoteBridgeCore, daemon workers, /rename, * initReplBridge, remoteBridgeCore, daemon workers, /rename,
* /remote-control. * /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. * the non-Override versions fall through to the real OAuth store/config.
* Callers that compose with a different auth source (e.g. daemon workers * Callers that compose with a different auth source (e.g. daemon workers
* using IPC auth) use the Override getters directly. * 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. * 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 * tengu_cobalt_harbor GrowthBook gate is on, all sessions connect to CCR by
* default — the user can still opt out by setting remoteControlAtStartup=false * default — the user can still opt out by setting remoteControlAtStartup=false
* in config (explicit settings always win over this default). * 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) — // Skip when the loop exited fatally (env expired, auth failed, give-up) —
// resume is impossible in those cases and the message would contradict the // resume is impossible in those cases and the message would contradict the
// error already printed. // 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). // revert to the pre-PR behavior (archive + deregister on every shutdown).
if ( if (
feature('KAIROS') && feature('KAIROS') &&
@@ -1888,7 +1888,7 @@ export function parseArgs(args: string[]): ParsedArgs {
async function printHelp(): Promise<void> { async function printHelp(): Promise<void> {
// Use EXTERNAL_PERMISSION_MODES for help text — internal modes (bubble) // 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 { EXTERNAL_PERMISSION_MODES } = await import('../types/permissions.js')
const modes = EXTERNAL_PERMISSION_MODES.join(', ') const modes = EXTERNAL_PERMISSION_MODES.join(', ')
const showServer = await isMultiSessionSpawnEnabled() 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 // environment_id and reuse that for registration (idempotent on the
// backend). Left undefined otherwise — the backend rejects // backend). Left undefined otherwise — the backend rejects
// client-generated UUIDs and will allocate a fresh environment. // 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 // rejects the flag when the gate is off, so resumeSessionId is always
// undefined here in external builds — this guard is for tree-shaking. // undefined here in external builds — this guard is for tree-shaking.
let reuseEnvironmentId: string | undefined let reuseEnvironmentId: string | undefined

View File

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

View File

@@ -161,7 +161,7 @@ export async function initReplBridge(
return null 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 // uses that token directly via getBridgeAccessToken() — keychain state is
// irrelevant. Skip 2b/2c to preserve that decoupling: an expired keychain // irrelevant. Skip 2b/2c to preserve that decoupling: an expired keychain
// token shouldn't block a bridge connection that doesn't use it. // 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. * 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 { randomUUID } from 'crypto'
import { import {
createBridgeApiClient, createBridgeApiClient,

View File

@@ -17,7 +17,7 @@ import { jsonStringify } from '../utils/slowOperations.js'
* *
* Bridge sessions have SecurityTier=ELEVATED on the server (CCR v2). * Bridge sessions have SecurityTier=ELEVATED on the server (CCR v2).
* The server gates ConnectBridgeWorker on its own flag * 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. * flag controls whether the CLI sends X-Trusted-Device-Token at all.
* Two flags so rollout can be staged: flip CLI-side first (headers * Two flags so rollout can be staged: flip CLI-side first (headers
* start flowing, server still no-ops), then flip server-side. * 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 { c as _c } from "react-compiler-runtime";
import { feature } from 'bun:bundle';
import figures from 'figures'; import figures from 'figures';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { useTerminalSize } from '../hooks/useTerminalSize.js'; import { useTerminalSize } from '../hooks/useTerminalSize.js';
@@ -10,7 +11,6 @@ import { getGlobalConfig } from '../utils/config.js';
import { isFullscreenActive } from '../utils/fullscreen.js'; import { isFullscreenActive } from '../utils/fullscreen.js';
import type { Theme } from '../utils/theme.js'; import type { Theme } from '../utils/theme.js';
import { getCompanion } from './companion.js'; import { getCompanion } from './companion.js';
import { isBuddyEnabled } from './feature.js';
import { renderFace, renderSprite, spriteFrameCount } from './sprites.js'; import { renderFace, renderSprite, spriteFrameCount } from './sprites.js';
import { RARITY_COLORS } from './types.js'; import { RARITY_COLORS } from './types.js';
const TICK_MS = 500; 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 // Narrow terminals: 0 — REPL.tsx stacks the one-liner on its own row
// (above input in fullscreen, below in scrollback), so no reservation. // (above input in fullscreen, below in scrollback), so no reservation.
export function companionReservedColumns(terminalColumns: number, speaking: boolean): number { export function companionReservedColumns(terminalColumns: number, speaking: boolean): number {
if (!isBuddyEnabled()) return 0; if (!feature('BUDDY')) return 0;
const companion = getCompanion(); const companion = getCompanion();
if (!companion || getGlobalConfig().companionMuted) return 0; if (!companion || getGlobalConfig().companionMuted) return 0;
if (terminalColumns < MIN_COLS_FOR_FULL_SPRITE) return 0; if (terminalColumns < MIN_COLS_FOR_FULL_SPRITE) return 0;
@@ -212,7 +212,7 @@ export function CompanionSprite(): React.ReactNode {
return () => clearTimeout(timer); return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps -- tick intentionally captured at reaction-change, not tracked // eslint-disable-next-line react-hooks/exhaustive-deps -- tick intentionally captured at reaction-change, not tracked
}, [reaction, setAppState]); }, [reaction, setAppState]);
if (!isBuddyEnabled()) return null; if (!feature('BUDDY')) return null;
const companion = getCompanion(); const companion = getCompanion();
if (!companion || getGlobalConfig().companionMuted) return null; if (!companion || getGlobalConfig().companionMuted) return null;
const color = RARITY_COLORS[companion.rarity]; const color = RARITY_COLORS[companion.rarity];
@@ -337,7 +337,7 @@ export function CompanionFloatingBubble() {
t3 = $[4]; t3 = $[4];
} }
useEffect(t2, t3); useEffect(t2, t3);
if (!isBuddyEnabled() || !reaction) { if (!feature("BUDDY") || !reaction) {
return null; return null;
} }
const companion = getCompanion(); 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 { Message } from '../types/message.js'
import type { Attachment } from '../utils/attachments.js' import type { Attachment } from '../utils/attachments.js'
import { getGlobalConfig } from '../utils/config.js' import { getGlobalConfig } from '../utils/config.js'
import { getCompanion } from './companion.js' import { getCompanion } from './companion.js'
import { isBuddyEnabled } from './feature.js'
export function companionIntroText(name: string, species: string): string { export function companionIntroText(name: string, species: string): string {
return `# Companion return `# Companion
@@ -15,7 +15,7 @@ When the user addresses ${name} directly (by name), its bubble will answer. Your
export function getCompanionIntroAttachment( export function getCompanionIntroAttachment(
messages: Message[] | undefined, messages: Message[] | undefined,
): Attachment[] { ): Attachment[] {
if (!isBuddyEnabled()) return [] if (!feature('BUDDY')) return []
const companion = getCompanion() const companion = getCompanion()
if (!companion || getGlobalConfig().companionMuted) return [] if (!companion || getGlobalConfig().companionMuted) return []

View File

@@ -1,10 +1,10 @@
import { c as _c } from "react-compiler-runtime"; import { c as _c } from "react-compiler-runtime";
import { feature } from 'bun:bundle';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { useNotifications } from '../context/notifications.js'; import { useNotifications } from '../context/notifications.js';
import { Text } from '../ink.js'; import { Text } from '../ink.js';
import { getGlobalConfig } from '../utils/config.js'; import { getGlobalConfig } from '../utils/config.js';
import { getRainbowColor } from '../utils/thinking.js'; import { getRainbowColor } from '../utils/thinking.js';
import { isBuddyEnabled } from './feature.js';
// Local date, not UTC — 24h rolling wave across timezones. Sustained Twitter // Local date, not UTC — 24h rolling wave across timezones. Sustained Twitter
// buzz instead of a single UTC-midnight spike, gentler on soul-gen load. // buzz instead of a single UTC-midnight spike, gentler on soul-gen load.
@@ -50,7 +50,7 @@ export function useBuddyNotification() {
let t1; let t1;
if ($[0] !== addNotification || $[1] !== removeNotification) { if ($[0] !== addNotification || $[1] !== removeNotification) {
t0 = () => { t0 = () => {
if (!isBuddyEnabled()) { if (!feature("BUDDY")) {
return; return;
} }
const config = getGlobalConfig(); const config = getGlobalConfig();
@@ -80,7 +80,7 @@ export function findBuddyTriggerPositions(text: string): Array<{
start: number; start: number;
end: number; end: number;
}> { }> {
if (!isBuddyEnabled()) return []; if (!feature('BUDDY')) return [];
const triggers: Array<{ const triggers: Array<{
start: number; start: number;
end: 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 { feature } from 'bun:bundle'
import { readFile, stat } from 'fs/promises' import { readFile, stat } from 'fs/promises'
import { dirname } from 'path' import { dirname } from 'path'
@@ -2829,7 +2829,7 @@ function runHeadlessStreaming(
if (message.type === 'control_request') { if (message.type === 'control_request') {
if (message.request.subtype === 'interrupt') { if (message.request.subtype === 'interrupt') {
// Track escapes for attribution (internal-only feature) // Track escapes for attribution (ant-only feature)
if (feature('COMMIT_ATTRIBUTION')) { if (feature('COMMIT_ATTRIBUTION')) {
setAppState(prev => ({ setAppState(prev => ({
...prev, ...prev,
@@ -3765,7 +3765,7 @@ function runHeadlessStreaming(
...getSettingsWithSources(), ...getSettingsWithSources(),
applied: { applied: {
model, 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, effort: typeof effort === 'string' ? effort : null,
}, },
}) })
@@ -5025,7 +5025,7 @@ async function loadInitialMessages(
} }
// Handle resume in print mode (accepts session ID or URL) // Handle resume in print mode (accepts session ID or URL)
// URLs are [internal-only] // URLs are [ANT-ONLY]
if (options.resume) { if (options.resume) {
try { try {
logEvent('tengu_resume_print', {}) logEvent('tengu_resume_print', {})

View File

@@ -30,7 +30,7 @@ import { getInitialSettings } from 'src/utils/settings/settings.js'
export async function update() { export async function update() {
// Block updates for third-party providers. The update mechanism downloads // 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 // OpenClaude build (with the OpenAI shim) with the upstream Claude Code
// binary (without it). // binary (without it).
if (getAPIProvider() !== 'firstParty') { if (getAPIProvider() !== 'firstParty') {
@@ -400,12 +400,12 @@ export async function update() {
if (useLocalUpdate) { if (useLocalUpdate) {
process.stderr.write('Try manually updating with:\n') process.stderr.write('Try manually updating with:\n')
process.stderr.write( process.stderr.write(
` cd ~/.openclaude/local && npm update ${MACRO.PACKAGE_URL}\n`, ` cd ~/.claude/local && npm update ${MACRO.PACKAGE_URL}\n`,
) )
} else { } else {
process.stderr.write('Try running with sudo or fix npm permissions\n') process.stderr.write('Try running with sudo or fix npm permissions\n')
process.stderr.write( process.stderr.write(
'Or consider using native installation with: openclaude install\n', 'Or consider using native installation with: claude install\n',
) )
} }
await gracefulShutdown(1) await gracefulShutdown(1)
@@ -415,11 +415,11 @@ export async function update() {
if (useLocalUpdate) { if (useLocalUpdate) {
process.stderr.write('Try manually updating with:\n') process.stderr.write('Try manually updating with:\n')
process.stderr.write( process.stderr.write(
` cd ~/.openclaude/local && npm update ${MACRO.PACKAGE_URL}\n`, ` cd ~/.claude/local && npm update ${MACRO.PACKAGE_URL}\n`,
) )
} else { } else {
process.stderr.write( process.stderr.write(
'Or consider using native installation with: openclaude install\n', 'Or consider using native installation with: claude install\n',
) )
} }
await gracefulShutdown(1) await gracefulShutdown(1)

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 addDir from './commands/add-dir/index.js'
import autofixPr from './commands/autofix-pr/index.js' import autofixPr from './commands/autofix-pr/index.js'
import backfillSessions from './commands/backfill-sessions/index.js' import backfillSessions from './commands/backfill-sessions/index.js'
@@ -32,7 +32,6 @@ import logout from './commands/logout/index.js'
import installGitHubApp from './commands/install-github-app/index.js' import installGitHubApp from './commands/install-github-app/index.js'
import installSlackApp from './commands/install-slack-app/index.js' import installSlackApp from './commands/install-slack-app/index.js'
import breakCache from './commands/break-cache/index.js' import breakCache from './commands/break-cache/index.js'
import cacheProbe from './commands/cache-probe/index.js'
import mcp from './commands/mcp/index.js' import mcp from './commands/mcp/index.js'
import mobile from './commands/mobile/index.js' import mobile from './commands/mobile/index.js'
import onboarding from './commands/onboarding/index.js' import onboarding from './commands/onboarding/index.js'
@@ -60,7 +59,6 @@ import usage from './commands/usage/index.js'
import theme from './commands/theme/index.js' import theme from './commands/theme/index.js'
import vim from './commands/vim/index.js' import vim from './commands/vim/index.js'
import { feature } from 'bun:bundle' import { feature } from 'bun:bundle'
import { isBuddyEnabled } from './buddy/feature.js'
// Dead code elimination: conditional imports // Dead code elimination: conditional imports
/* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable @typescript-eslint/no-require-imports */
const proactive = const proactive =
@@ -119,7 +117,7 @@ const forkCmd = feature('FORK_SUBAGENT')
require('./commands/fork/index.js') as typeof import('./commands/fork/index.js') require('./commands/fork/index.js') as typeof import('./commands/fork/index.js')
).default ).default
: null : null
const buddy = isBuddyEnabled() const buddy = feature('BUDDY')
? ( ? (
require('./commands/buddy/index.js') as typeof import('./commands/buddy/index.js') require('./commands/buddy/index.js') as typeof import('./commands/buddy/index.js')
).default ).default
@@ -137,7 +135,6 @@ import hooks from './commands/hooks/index.js'
import files from './commands/files/index.js' import files from './commands/files/index.js'
import branch from './commands/branch/index.js' import branch from './commands/branch/index.js'
import agents from './commands/agents/index.js' import agents from './commands/agents/index.js'
import autoFix from './commands/auto-fix.js'
import plugin from './commands/plugin/index.js' import plugin from './commands/plugin/index.js'
import reloadPlugins from './commands/reload-plugins/index.js' import reloadPlugins from './commands/reload-plugins/index.js'
import rewind from './commands/rewind/index.js' import rewind from './commands/rewind/index.js'
@@ -145,7 +142,6 @@ import heapDump from './commands/heapdump/index.js'
import mockLimits from './commands/mock-limits/index.js' import mockLimits from './commands/mock-limits/index.js'
import bridgeKick from './commands/bridge-kick.js' import bridgeKick from './commands/bridge-kick.js'
import version from './commands/version.js' import version from './commands/version.js'
import wiki from './commands/wiki/index.js'
import summary from './commands/summary/index.js' import summary from './commands/summary/index.js'
import { import {
resetLimits, resetLimits,
@@ -266,10 +262,8 @@ const COMMANDS = memoize((): Command[] => [
addDir, addDir,
advisor, advisor,
agents, agents,
autoFix,
branch, branch,
btw, btw,
cacheProbe,
chrome, chrome,
clear, clear,
color, color,
@@ -329,7 +323,6 @@ const COMMANDS = memoize((): Command[] => [
usage, usage,
usageReport, usageReport,
vim, vim,
wiki,
...(webCmd ? [webCmd] : []), ...(webCmd ? [webCmd] : []),
...(forkCmd ? [forkCmd] : []), ...(forkCmd ? [forkCmd] : []),
...(buddy ? [buddy] : []), ...(buddy ? [buddy] : []),

View File

@@ -1,25 +0,0 @@
import type { Command } from '../types/command.js'
const command: Command = {
name: 'auto-fix',
description: 'Configure auto-fix: run lint/test after AI edits',
isEnabled: () => true,
type: 'prompt',
progressMessage: 'Configuring auto-fix...',
contentLength: 0,
source: 'builtin',
async getPromptForCommand() {
return [
{
type: 'text',
text:
'The user wants to configure auto-fix settings. Auto-fix automatically runs lint and test commands after AI file edits, feeding errors back for self-repair.\n\n' +
'Current settings location: `.claude/settings.json` or `.claude/settings.local.json`\n\n' +
'Example configuration:\n```json\n{\n "autoFix": {\n "enabled": true,\n "lint": "eslint . --fix",\n "test": "bun test",\n "maxRetries": 3,\n "timeout": 30000\n }\n}\n```\n\n' +
'Ask the user what lint and test commands they use, then help them set up the configuration.',
},
]
},
}
export default command

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

@@ -1,413 +0,0 @@
import { getSessionId } from '../../bootstrap/state.js'
import { resolveProviderRequest } from '../../services/api/providerConfig.js'
import type { LocalCommandCall } from '../../types/command.js'
import { logForDebugging } from '../../utils/debug.js'
import { isEnvTruthy } from '../../utils/envUtils.js'
import { hydrateGithubModelsTokenFromSecureStorage } from '../../utils/githubModelsCredentials.js'
import { getMainLoopModel } from '../../utils/model/model.js'
const COPILOT_HEADERS: Record<string, string> = {
'User-Agent': 'GitHubCopilotChat/0.26.7',
'Editor-Version': 'vscode/1.99.3',
'Editor-Plugin-Version': 'copilot-chat/0.26.7',
'Copilot-Integration-Id': 'vscode-chat',
}
// Large system prompt (~6000 chars, ~1500 tokens) to cross the 1024-token cache threshold
const SYSTEM_PROMPT = [
'You are a coding assistant. Answer concisely.',
'CONTEXT: User is working on a TypeScript project with Bun runtime.',
...Array.from(
{ length: 80 },
(_, i) =>
`Rule ${i + 1}: Follow best practices for TypeScript including strict typing, error handling, testing, and clean code. Prefer explicit types over any. Use const assertions. Await all async operations.`,
),
].join('\n\n')
const USER_MESSAGE = 'Say "hello" and nothing else.'
const DELAY_MS = 3000
/**
* Extract model family from a versioned model string.
* e.g. "gpt-5.4-0626" → "gpt-5.4", "codex-mini-latest" → "codex-mini"
*/
function getModelFamily(model: string | undefined): string {
if (!model) return 'unknown'
return model
.replace(/-\d{4,}$/, '')
.replace(/-latest$/, '')
.replace(/-preview$/, '')
}
function getField(obj: unknown, path: string): unknown {
return path
.split('.')
.reduce((o: any, k: string) => (o != null ? o[k] : undefined), obj)
}
interface ProbeResult {
label: string
status: number
elapsed: number
headers: Record<string, string>
usage: Record<string, unknown> | null
responseId: string | null
error: string | null
}
async function sendProbe(
url: string,
headers: Record<string, string>,
body: Record<string, unknown>,
label: string,
): Promise<ProbeResult> {
const start = Date.now()
let response: Response
try {
response = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify(body),
})
} catch (err: any) {
return {
label,
status: 0,
elapsed: Date.now() - start,
headers: {},
usage: null,
responseId: null,
error: err.message,
}
}
const elapsed = Date.now() - start
const respHeaders: Record<string, string> = {}
response.headers.forEach((value, key) => {
respHeaders[key] = value
})
if (!response.ok) {
const errorBody = await response.text().catch(() => '')
return {
label,
status: response.status,
elapsed,
headers: respHeaders,
usage: null,
responseId: null,
error: errorBody,
}
}
// Parse SSE stream for usage data
const text = await response.text()
let usage: Record<string, unknown> | null = null
let responseId: string | null = null
const isResponses = url.endsWith('/responses')
for (const chunk of text.split('\n\n')) {
const lines = chunk
.split('\n')
.map((l) => l.trim())
.filter(Boolean)
if (isResponses) {
const eventLine = lines.find((l) => l.startsWith('event: '))
const dataLines = lines.filter((l) => l.startsWith('data: '))
if (!eventLine || !dataLines.length) continue
const event = eventLine.slice(7).trim()
if (
event === 'response.completed' ||
event === 'response.incomplete'
) {
try {
const data = JSON.parse(
dataLines.map((l) => l.slice(6)).join('\n'),
)
usage = (data?.response?.usage as Record<string, unknown>) ?? null
responseId = (data?.response?.id as string) ?? null
} catch {}
}
} else {
for (const line of lines) {
if (!line.startsWith('data: ')) continue
const raw = line.slice(6).trim()
if (raw === '[DONE]') continue
try {
const data = JSON.parse(raw) as Record<string, unknown>
if (data.usage) {
usage = data.usage as Record<string, unknown>
responseId = (data.id as string) ?? null
}
} catch {}
}
}
}
return { label, status: response.status, elapsed, headers: respHeaders, usage, responseId, error: null }
}
function formatResult(r: ProbeResult): string {
const lines: string[] = [`--- ${r.label} ---`]
if (r.error) {
lines.push(` ERROR (HTTP ${r.status}): ${r.error.slice(0, 200)}`)
return lines.join('\n')
}
lines.push(` HTTP ${r.status}${r.elapsed}ms`)
if (r.responseId) lines.push(` response.id: ${r.responseId}`)
if (r.usage) {
lines.push(' Usage:')
lines.push(` ${JSON.stringify(r.usage, null, 2).replace(/\n/g, '\n ')}`)
} else {
lines.push(' Usage: null')
}
// Interesting headers
for (const h of [
'openai-processing-ms',
'x-ratelimit-remaining',
'x-ratelimit-limit',
'x-ms-region',
'x-github-request-id',
'x-request-id',
]) {
if (r.headers[h]) lines.push(` ${h}: ${r.headers[h]}`)
}
return lines.join('\n')
}
export const call: LocalCommandCall = async (args) => {
const parts = (args ?? '').trim().split(/\s+/).filter(Boolean)
const noKey = parts.includes('--no-key')
const modelOverride = parts.find((p) => !p.startsWith('--')) || undefined
const modelStr = modelOverride ?? getMainLoopModel()
const request = resolveProviderRequest({ model: modelStr })
const isGithub = isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB)
// Resolve API key the same way the OpenAI shim does
let apiKey = process.env.OPENAI_API_KEY ?? ''
if (!apiKey && isGithub) {
hydrateGithubModelsTokenFromSecureStorage()
apiKey =
process.env.OPENAI_API_KEY ??
process.env.GITHUB_TOKEN ??
process.env.GH_TOKEN ??
''
}
if (!apiKey) {
return {
type: 'text',
value:
'No API key found. Make sure you are in an active OpenAI-compatible or GitHub Copilot session.\n' +
'For GitHub Copilot: run /onboard-github first.\n' +
'For OpenAI-compatible: set OPENAI_API_KEY.',
}
}
const useResponses = request.transport === 'codex_responses'
const endpoint = useResponses ? '/responses' : '/chat/completions'
const url = `${request.baseUrl}${endpoint}`
const family = getModelFamily(request.resolvedModel)
const cacheKey = `${getSessionId()}:${family}`
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
originator: 'openclaude',
}
if (isGithub) {
Object.assign(headers, COPILOT_HEADERS)
}
let body: Record<string, unknown>
if (useResponses) {
body = {
model: request.resolvedModel,
instructions: SYSTEM_PROMPT,
input: [
{
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: USER_MESSAGE }],
},
],
stream: true,
...(noKey ? {} : {
store: false,
prompt_cache_key: cacheKey,
prompt_cache_retention: '24h',
}),
}
} else {
body = {
model: request.resolvedModel,
messages: [
{ role: 'system', content: SYSTEM_PROMPT },
{ role: 'user', content: USER_MESSAGE },
],
stream: true,
stream_options: { include_usage: true },
max_tokens: 20,
...(noKey ? {} : {
store: false,
prompt_cache_key: cacheKey,
}),
}
}
// Log configuration
const config = [
`[cache-probe] Starting cache probe${noKey ? ' (--no-key: cache params OMITTED)' : ''}`,
` model: ${request.resolvedModel} (family: ${family})`,
` transport: ${request.transport}`,
` endpoint: ${url}`,
` prompt_cache_key: ${noKey ? 'NOT SENT' : cacheKey}`,
` store: ${noKey ? 'NOT SENT' : 'false'}`,
` system prompt: ~${Math.round(SYSTEM_PROMPT.length / 4)} tokens`,
` delay between calls: ${DELAY_MS}ms`,
].join('\n')
logForDebugging(config)
// Call 1 — Cold
const r1 = await sendProbe(url, headers, body, 'CALL 1 — Cold (no cache)')
logForDebugging(`[cache-probe]\n${formatResult(r1)}`)
if (r1.error) {
return {
type: 'text',
value: `Cache probe failed on first call: HTTP ${r1.status}\n${r1.error.slice(0, 300)}\n\nFull details in debug log.`,
}
}
// Wait
await new Promise((r) => setTimeout(r, DELAY_MS))
// Call 2 — Warm
const r2 = await sendProbe(url, headers, body, 'CALL 2 — Warm (cache expected)')
logForDebugging(`[cache-probe]\n${formatResult(r2)}`)
// --- Comparison ---
const fields = [
'input_tokens',
'output_tokens',
'total_tokens',
'prompt_tokens',
'completion_tokens',
'input_tokens_details.cached_tokens',
'prompt_tokens_details.cached_tokens',
'output_tokens_details.reasoning_tokens',
]
const comparison: string[] = ['[cache-probe] COMPARISON']
comparison.push(
` ${'Field'.padEnd(42)} ${'Call 1'.padStart(8)} ${'Call 2'.padStart(8)} ${'Delta'.padStart(8)}`,
)
comparison.push(` ${'-'.repeat(72)}`)
for (const f of fields) {
const v1 = getField(r1.usage, f)
const v2 = getField(r2.usage, f)
if (v1 === undefined && v2 === undefined) continue
const d =
typeof v1 === 'number' && typeof v2 === 'number' ? v2 - v1 : ''
comparison.push(
` ${f.padEnd(42)} ${String(v1 ?? '-').padStart(8)} ${String(v2 ?? '-').padStart(8)} ${String(d).padStart(8)}`,
)
}
comparison.push('')
comparison.push(
` Latency: ${r1.elapsed}ms → ${r2.elapsed}ms (${r2.elapsed - r1.elapsed > 0 ? '+' : ''}${r2.elapsed - r1.elapsed}ms)`,
)
// Header comparison
for (const h of ['openai-processing-ms', 'x-ms-region', 'x-ratelimit-remaining']) {
const v1 = r1.headers[h]
const v2 = r2.headers[h]
if (v1 || v2) {
comparison.push(` ${h}: ${v1 ?? '-'}${v2 ?? '-'}`)
}
}
// Verdict
const cached2 =
(getField(r2.usage, 'input_tokens_details.cached_tokens') as number) ??
(getField(r2.usage, 'prompt_tokens_details.cached_tokens') as number) ??
0
const input1 =
((r1.usage?.input_tokens ?? r1.usage?.prompt_tokens) as number) ?? 0
const input2 =
((r2.usage?.input_tokens ?? r2.usage?.prompt_tokens) as number) ?? 0
let verdict: string
if (cached2 > 0) {
const rate = input2 > 0 ? Math.round((cached2 / input2) * 100) : '?'
verdict = `CACHE HIT: ${cached2} cached tokens (${rate}% of input)`
} else if (input1 === 0 && input2 === 0) {
verdict = 'INCONCLUSIVE: Server returns 0 input_tokens — cannot measure'
} else if (r2.elapsed < r1.elapsed * 0.6 && input1 > 100) {
verdict = `POSSIBLE SILENT CACHING: Call 2 was ${Math.round((1 - r2.elapsed / r1.elapsed) * 100)}% faster but no cached_tokens reported`
} else {
verdict = 'NO CACHE DETECTED'
}
comparison.push(`\n Verdict: ${verdict}`)
// --- Simulate what main's shim code does with this usage ---
// codexShim.ts makeUsage() — used for Responses API (GPT-5+/Codex)
function mainMakeUsage(u: any) {
return {
input_tokens: u?.input_tokens ?? 0,
output_tokens: u?.output_tokens ?? 0,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0, // ← main hardcodes this to 0
}
}
// openaiShim.ts convertChunkUsage() — used for Chat Completions
function mainConvertChunkUsage(u: any) {
return {
input_tokens: u?.prompt_tokens ?? 0,
output_tokens: u?.completion_tokens ?? 0,
cache_creation_input_tokens: 0,
cache_read_input_tokens: u?.prompt_tokens_details?.cached_tokens ?? 0,
}
}
const shimFn = useResponses ? mainMakeUsage : mainConvertChunkUsage
const shim1 = shimFn(r1.usage)
const shim2 = shimFn(r2.usage)
comparison.push('')
comparison.push(` --- What main's shim reports (${useResponses ? 'codexShim.makeUsage' : 'openaiShim.convertChunkUsage'}) ---`)
comparison.push(` Call 1: cache_read_input_tokens=${shim1.cache_read_input_tokens}`)
comparison.push(` Call 2: cache_read_input_tokens=${shim2.cache_read_input_tokens}`)
if (useResponses && cached2 > 0) {
comparison.push(` BUG: Server returned ${cached2} cached tokens but main's makeUsage() drops it → reports 0`)
} else if (!useResponses && shim2.cache_read_input_tokens > 0) {
comparison.push(` OK: Chat Completions path on main correctly reads cached_tokens`)
}
logForDebugging(comparison.join('\n'))
// User-facing summary
const mode = noKey ? ' (NO cache key sent)' : ''
const shimLabel = useResponses ? 'codexShim.makeUsage()' : 'openaiShim.convertChunkUsage()'
const summary = [
`Cache Probe — ${request.resolvedModel} via ${useResponses ? 'Responses API' : 'Chat Completions'}${mode}`,
'',
`Call 1: ${r1.elapsed}ms, input=${input1}, cached=${(getField(r1.usage, 'input_tokens_details.cached_tokens') as number) ?? (getField(r1.usage, 'prompt_tokens_details.cached_tokens') as number) ?? 0}`,
`Call 2: ${r2.elapsed}ms, input=${input2}, cached=${cached2}`,
'',
verdict,
'',
`What main's ${shimLabel} reports:`,
` Call 2 cache_read_input_tokens = ${shim2.cache_read_input_tokens}${useResponses && cached2 > 0 ? ' ← BUG: server sent ' + cached2 + ' but main drops it' : ''}`,
'',
'Full details written to debug log.',
].join('\n')
return { type: 'text', value: summary }
}

View File

@@ -1,17 +0,0 @@
import type { Command } from '../../commands.js'
import { isEnvTruthy } from '../../utils/envUtils.js'
const cacheProbe: Command = {
type: 'local',
name: 'cache-probe',
description:
'Send identical requests to test prompt caching (results in debug log)',
argumentHint: '[model] [--no-key]',
isEnabled: () =>
isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI) ||
isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB),
supportsNonInteractive: false,
load: () => import('./cache-probe.js'),
}
export default cacheProbe

View File

@@ -45,7 +45,7 @@ function getPromptContent(
<!-- CHANGELOG:END -->` <!-- CHANGELOG:END -->`
let slackStep = ` let slackStep = `
5. After creating/updating the PR, check if the user's AGENTS.md or CLAUDE.md mentions posting to Slack channels. If it does, use ToolSearch to search for "slack send message" tools. If ToolSearch finds a Slack tool, ask the user if they'd like you to post the PR URL to the relevant Slack channel. Only post if the user confirms. If ToolSearch returns no results or errors, skip this step silently—do not mention the failure, do not attempt workarounds, and do not try alternative approaches.` 5. After creating/updating the PR, check if the user's CLAUDE.md mentions posting to Slack channels. If it does, use ToolSearch to search for "slack send message" tools. If ToolSearch finds a Slack tool, ask the user if they'd like you to post the PR URL to the relevant Slack channel. Only post if the user confirms. If ToolSearch returns no results or errors, skip this step silently—do not mention the failure, do not attempt workarounds, and do not try alternative approaches.`
if (process.env.USER_TYPE === 'ant' && isUndercover()) { if (process.env.USER_TYPE === 'ant' && isUndercover()) {
prefix = getUndercoverInstructions() + '\n' prefix = getUndercoverInstructions() + '\n'
reviewerArg = '' reviewerArg = ''

View File

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

View File

@@ -1,43 +0,0 @@
import { afterEach, expect, mock, test } from 'bun:test'
const originalClaudeCodeNewInit = process.env.CLAUDE_CODE_NEW_INIT
async function importInitCommand() {
return (await import(`./init.ts?ts=${Date.now()}-${Math.random()}`)).default
}
afterEach(() => {
mock.restore()
if (originalClaudeCodeNewInit === undefined) {
delete process.env.CLAUDE_CODE_NEW_INIT
} else {
process.env.CLAUDE_CODE_NEW_INIT = originalClaudeCodeNewInit
}
})
test('NEW_INIT prompt preserves existing root CLAUDE.md by default', async () => {
process.env.CLAUDE_CODE_NEW_INIT = '1'
mock.module('../projectOnboardingState.js', () => ({
maybeMarkProjectOnboardingComplete: () => {},
}))
mock.module('./initMode.js', () => ({
isNewInitEnabled: () => true,
}))
const command = await importInitCommand()
const blocks = await command.getPromptForCommand()
expect(blocks).toHaveLength(1)
expect(blocks[0]?.type).toBe('text')
expect(String(blocks[0]?.text)).toContain(
'checked-in root `CLAUDE.md` and does NOT already have a root `AGENTS.md`',
)
expect(String(blocks[0]?.text)).toContain(
'do NOT silently create a second root instruction file',
)
expect(String(blocks[0]?.text)).toContain(
'update the existing root `CLAUDE.md` in place by default',
)
})

View File

@@ -1,6 +1,7 @@
import { feature } from 'bun:bundle'
import type { Command } from '../commands.js' import type { Command } from '../commands.js'
import { maybeMarkProjectOnboardingComplete } from '../projectOnboardingState.js' import { maybeMarkProjectOnboardingComplete } from '../projectOnboardingState.js'
import { isNewInitEnabled } from './initMode.js' import { isEnvTruthy } from '../utils/envUtils.js'
const OLD_INIT_PROMPT = `Please analyze this codebase and create a CLAUDE.md file, which will be given to future instances of Claude Code to operate in this repository. const OLD_INIT_PROMPT = `Please analyze this codebase and create a CLAUDE.md file, which will be given to future instances of Claude Code to operate in this repository.
@@ -24,19 +25,19 @@ Usage notes:
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
\`\`\`` \`\`\``
const NEW_INIT_PROMPT = `Set up a minimal AGENTS.md (and optionally CLAUDE.local.md, skills, and hooks) for this repo. The root project instruction file is loaded into every Claude Code session, so it must be concise — only include what Claude would get wrong without it. const NEW_INIT_PROMPT = `Set up a minimal CLAUDE.md (and optionally skills and hooks) for this repo. CLAUDE.md is loaded into every Claude Code session, so it must be concise — only include what Claude would get wrong without it.
## Phase 1: Ask what to set up ## Phase 1: Ask what to set up
Use AskUserQuestion to find out what the user wants: Use AskUserQuestion to find out what the user wants:
- "Which instruction files should /init set up?" - "Which CLAUDE.md files should /init set up?"
Options: "Project AGENTS.md" | "Personal CLAUDE.local.md" | "Both project + personal" Options: "Project CLAUDE.md" | "Personal CLAUDE.local.md" | "Both project + personal"
Description for project: "Team-shared instructions checked into source control — architecture, coding standards, common workflows." Description for project: "Team-shared instructions checked into source control — architecture, coding standards, common workflows."
Description for personal: "Your private preferences for this project (gitignored, not shared) — your role, sandbox URLs, preferred test data, workflow quirks." Description for personal: "Your private preferences for this project (gitignored, not shared) — your role, sandbox URLs, preferred test data, workflow quirks."
- "Also set up skills and hooks?" - "Also set up skills and hooks?"
Options: "Skills + hooks" | "Skills only" | "Hooks only" | "Neither, just the instruction file(s)" Options: "Skills + hooks" | "Skills only" | "Hooks only" | "Neither, just CLAUDE.md"
Description for skills: "On-demand capabilities you or Claude invoke with \`/skill-name\` — good for repeatable workflows and reference knowledge." Description for skills: "On-demand capabilities you or Claude invoke with \`/skill-name\` — good for repeatable workflows and reference knowledge."
Description for hooks: "Deterministic shell commands that run on tool events (e.g., format after every edit). Claude can't skip them." Description for hooks: "Deterministic shell commands that run on tool events (e.g., format after every edit). Claude can't skip them."
@@ -58,24 +59,24 @@ Note what you could NOT figure out from code alone — these become interview qu
## Phase 3: Fill in the gaps ## Phase 3: Fill in the gaps
Use AskUserQuestion to gather what you still need to write good instruction files and skills. Ask only things the code can't answer. Use AskUserQuestion to gather what you still need to write good CLAUDE.md files and skills. Ask only things the code can't answer.
If the user chose project AGENTS.md or both: ask about codebase practices — non-obvious commands, gotchas, branch/PR conventions, required env setup, testing quirks. Skip things already in README or obvious from manifest files. Do not mark any options as "recommended" — this is about how their team works, not best practices. If the user chose project CLAUDE.md or both: ask about codebase practices — non-obvious commands, gotchas, branch/PR conventions, required env setup, testing quirks. Skip things already in README or obvious from manifest files. Do not mark any options as "recommended" — this is about how their team works, not best practices.
If the user chose personal CLAUDE.local.md or both: ask about them, not the codebase. Do not mark any options as "recommended" — this is about their personal preferences, not best practices. Examples of questions: If the user chose personal CLAUDE.local.md or both: ask about them, not the codebase. Do not mark any options as "recommended" — this is about their personal preferences, not best practices. Examples of questions:
- What's their role on the team? (e.g., "backend engineer", "data scientist", "new hire onboarding") - What's their role on the team? (e.g., "backend engineer", "data scientist", "new hire onboarding")
- How familiar are they with this codebase and its languages/frameworks? (so Claude can calibrate explanation depth) - How familiar are they with this codebase and its languages/frameworks? (so Claude can calibrate explanation depth)
- Do they have personal sandbox URLs, test accounts, API key paths, or local setup details Claude should know? - Do they have personal sandbox URLs, test accounts, API key paths, or local setup details Claude should know?
- 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 AGENTS.md — that would check a personal reference into the team-shared file. - 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") - 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, an AGENTS.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. - **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. - **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.
- **AGENTS.md note** (looser) — influences Claude's behavior but not enforced. Fits communication/thinking preferences: "plan before coding", "be terse", "explain tradeoffs". - **CLAUDE.md note** (looser) — influences Claude's behavior but not enforced. Fits communication/thinking preferences: "plan before coding", "be terse", "explain tradeoffs".
**Respect Phase 1's skills+hooks choice as a hard filter**: if the user picked "Skills only", downgrade any hook you'd suggest to a skill or an AGENTS.md note. If "Hooks only", downgrade skills to hooks (where mechanically possible) or notes. If "Neither", everything becomes an AGENTS.md note. Never propose an artifact type the user didn't opt into. **Respect Phase 1's skills+hooks choice as a hard filter**: if the user picked "Skills only", downgrade any hook you'd suggest to a skill or a CLAUDE.md note. If "Hooks only", downgrade skills to hooks (where mechanically possible) or notes. If "Neither", everything becomes a CLAUDE.md note. Never propose an artifact type the user didn't opt into.
**Show the proposal via AskUserQuestion's \`preview\` field, not as a separate text message** — the dialog overlays your output, so preceding text is hidden. The \`preview\` field renders markdown in a side-panel (like plan mode); the \`question\` field is plain-text-only. Structure it as: **Show the proposal via AskUserQuestion's \`preview\` field, not as a separate text message** — the dialog overlays your output, so preceding text is hidden. The \`preview\` field renders markdown in a side-panel (like plan mode); the \`question\` field is plain-text-only. Structure it as:
@@ -84,20 +85,18 @@ 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: - **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 • **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\`
• **AGENTS.md note** (guideline) — "run lint/typecheck/test before marking done" • **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. - 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.
**Build the preference queue** from the accepted proposal. Each entry: {type: hook|skill|note, description, target file, any Phase-2-sourced details like the actual test/format command}. Phases 4-7 consume this queue. **Build the preference queue** from the accepted proposal. Each entry: {type: hook|skill|note, description, target file, any Phase-2-sourced details like the actual test/format command}. Phases 4-7 consume this queue.
## Phase 4: Write AGENTS.md (if user chose project or both) ## Phase 4: Write CLAUDE.md (if user chose project or both)
Write a minimal AGENTS.md at the project root. Every line must pass this test: "Would removing this cause Claude to make mistakes?" If no, cut it. Write a minimal CLAUDE.md at the project root. Every line must pass this test: "Would removing this cause Claude to make mistakes?" If no, cut it.
If the repo already has a checked-in root \`CLAUDE.md\` and does NOT already have a root \`AGENTS.md\`, do NOT silently create a second root instruction file. In that case, update the existing root \`CLAUDE.md\` in place by default. Only create or migrate to root \`AGENTS.md\` if the user explicitly asks to migrate. **Consume \`note\` entries from the Phase 3 preference queue whose target is CLAUDE.md** (team-level notes) — add each as a concise line in the most relevant section. These are the behaviors the user wants Claude to follow but didn't need guaranteed (e.g., "propose a plan before implementing", "explain the tradeoffs when refactoring"). Leave personal-targeted notes for Phase 5.
**Consume \`note\` entries from the Phase 3 preference queue whose target is AGENTS.md** (team-level notes) — add each as a concise line in the most relevant section. These are the behaviors the user wants Claude to follow but didn't need guaranteed (e.g., "propose a plan before implementing", "explain the tradeoffs when refactoring"). Leave personal-targeted notes for Phase 5.
Include: Include:
- Build/test/lint commands Claude can't guess (non-standard scripts, flags, or sequences) - Build/test/lint commands Claude can't guess (non-standard scripts, flags, or sequences)
@@ -112,7 +111,7 @@ Exclude:
- File-by-file structure or component lists (Claude can discover these by reading the codebase) - File-by-file structure or component lists (Claude can discover these by reading the codebase)
- Standard language conventions Claude already knows - Standard language conventions Claude already knows
- Generic advice ("write clean code", "handle errors") - Generic advice ("write clean code", "handle errors")
- Detailed API docs or long references — use \`@path/to/import\` syntax instead (e.g., \`@docs/api-reference.md\`) to inline content on demand without bloating AGENTS.md - Detailed API docs or long references — use \`@path/to/import\` syntax instead (e.g., \`@docs/api-reference.md\`) to inline content on demand without bloating CLAUDE.md
- Information that changes frequently — reference the source with \`@path/to/import\` so Claude always reads the current version - Information that changes frequently — reference the source with \`@path/to/import\` so Claude always reads the current version
- Long tutorials or walkthroughs (move to a separate file and reference with \`@path/to/import\`, or put in a skill) - Long tutorials or walkthroughs (move to a separate file and reference with \`@path/to/import\`, or put in a skill)
- Commands obvious from manifest files (e.g., standard "npm test", "cargo test", "pytest") - Commands obvious from manifest files (e.g., standard "npm test", "cargo test", "pytest")
@@ -124,20 +123,20 @@ Do not repeat yourself and do not make up sections like "Common Development Task
Prefix the file with: Prefix the file with:
\`\`\` \`\`\`
# AGENTS.md # CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
\`\`\` \`\`\`
If AGENTS.md already exists: read it, propose specific changes as diffs, and explain why each change improves it. Do not silently overwrite. If CLAUDE.md already exists: read it, propose specific changes as diffs, and explain why each change improves it. Do not silently overwrite.
For projects with multiple concerns, suggest organizing instructions into \`.claude/rules/\` as separate focused files (e.g., \`code-style.md\`, \`testing.md\`, \`security.md\`). These are loaded automatically alongside AGENTS.md and can be scoped to specific file paths using \`paths\` frontmatter. For projects with multiple concerns, suggest organizing instructions into \`.claude/rules/\` as separate focused files (e.g., \`code-style.md\`, \`testing.md\`, \`security.md\`). These are loaded automatically alongside CLAUDE.md and can be scoped to specific file paths using \`paths\` frontmatter.
For projects with distinct subdirectories (monorepos, multi-module projects, etc.): mention that subdirectory AGENTS.md files can be added for module-specific instructions (they're loaded automatically when Claude works in those directories). Offer to create them if the user wants. For projects with distinct subdirectories (monorepos, multi-module projects, etc.): mention that subdirectory CLAUDE.md files can be added for module-specific instructions (they're loaded automatically when Claude works in those directories). Offer to create them if the user wants.
## Phase 5: Write CLAUDE.local.md (if user chose personal or both) ## Phase 5: Write CLAUDE.local.md (if user chose personal or both)
Write a minimal CLAUDE.local.md at the project root. This file is automatically loaded alongside AGENTS.md. After creating it, add \`CLAUDE.local.md\` to the project's .gitignore so it stays private. Write a minimal CLAUDE.local.md at the project root. This file is automatically loaded alongside CLAUDE.md. After creating it, add \`CLAUDE.local.md\` to the project's .gitignore so it stays private.
**Consume \`note\` entries from the Phase 3 preference queue whose target is CLAUDE.local.md** (personal-level notes) — add each as a concise line. If the user chose personal-only in Phase 1, this is the sole consumer of note entries. **Consume \`note\` entries from the Phase 3 preference queue whose target is CLAUDE.local.md** (personal-level notes) — add each as a concise line. If the user chose personal-only in Phase 1, this is the sole consumer of note entries.
@@ -148,7 +147,7 @@ Include:
Keep it short — only include what would make Claude's responses noticeably better for this user. Keep it short — only include what would make Claude's responses noticeably better for this user.
If Phase 2 found multiple git worktrees and the user confirmed they use sibling/external worktrees (not nested inside the main repo): the upward file walk won't find a single CLAUDE.local.md from all worktrees. Write the actual personal content to \`~/.claude/<project-name>-instructions.md\` and make CLAUDE.local.md a one-line stub that imports it: \`@~/.claude/<project-name>-instructions.md\`. The user can copy this one-line stub to each sibling worktree. Never put this import in the project AGENTS.md. If worktrees are nested inside the main repo (e.g., \`.claude/worktrees/\`), no special handling is needed — the main repo's CLAUDE.local.md is found automatically. If Phase 2 found multiple git worktrees and the user confirmed they use sibling/external worktrees (not nested inside the main repo): the upward file walk won't find a single CLAUDE.local.md from all worktrees. Write the actual personal content to \`~/.claude/<project-name>-instructions.md\` and make CLAUDE.local.md a one-line stub that imports it: \`@~/.claude/<project-name>-instructions.md\`. The user can copy this one-line stub to each sibling worktree. Never put this import in the project CLAUDE.md. If worktrees are nested inside the main repo (e.g., \`.claude/worktrees/\`), no special handling is needed — the main repo's CLAUDE.local.md is found automatically.
If CLAUDE.local.md already exists: read it, propose specific additions, and do not silently overwrite. If CLAUDE.local.md already exists: read it, propose specific additions, and do not silently overwrite.
@@ -158,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: **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") - 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?") - 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: **Then suggest additional skills** beyond the queue when you find:
@@ -184,7 +183,7 @@ Both the user (\`/<skill-name>\`) and Claude can invoke skills by default. For w
## Phase 7: Suggest additional optimizations ## Phase 7: Suggest additional optimizations
Tell the user you're going to suggest a few additional optimizations now that AGENTS.md and skills (if chosen) are in place. Tell the user you're going to suggest a few additional optimizations now that CLAUDE.md and skills (if chosen) are in place.
Check the environment and ask about each gap you find (use AskUserQuestion): Check the environment and ask about each gap you find (use AskUserQuestion):
@@ -196,7 +195,7 @@ Check the environment and ask about each gap you find (use AskUserQuestion):
For each hook preference (from the queue or the formatter fallback): For each hook preference (from the queue or the formatter fallback):
1. Target file: default based on the Phase 1 instruction-file choice — project → \`.claude/settings.json\` (team-shared, committed); personal → \`.claude/settings.local.json\`. Only ask if the user chose "both" in Phase 1 or the preference is ambiguous. Ask once for all hooks, not per-hook. 1. Target file: default based on the Phase 1 CLAUDE.md choice — project → \`.claude/settings.json\` (team-shared, committed); personal → \`.claude/settings.local.json\`. Only ask if the user chose "both" in Phase 1 or the preference is ambiguous. Ask once for all hooks, not per-hook.
2. Pick the event and matcher from the preference: 2. Pick the event and matcher from the preference:
- "after every edit" → \`PostToolUse\` with matcher \`Write|Edit\` - "after every edit" → \`PostToolUse\` with matcher \`Write|Edit\`
@@ -228,9 +227,11 @@ const command = {
type: 'prompt', type: 'prompt',
name: 'init', name: 'init',
get description() { get description() {
return isNewInitEnabled() return feature('NEW_INIT') &&
? 'Initialize new project instruction file(s) and optional skills/hooks with codebase documentation' (process.env.USER_TYPE === 'ant' ||
: 'Initialize a new project instruction file with codebase documentation' isEnvTruthy(process.env.CLAUDE_CODE_NEW_INIT))
? 'Initialize new CLAUDE.md file(s) and optional skills/hooks with codebase documentation'
: 'Initialize a new CLAUDE.md file with codebase documentation'
}, },
contentLength: 0, // Dynamic content contentLength: 0, // Dynamic content
progressMessage: 'analyzing your codebase', progressMessage: 'analyzing your codebase',
@@ -241,7 +242,12 @@ const command = {
return [ return [
{ {
type: 'text', type: 'text',
text: isNewInitEnabled() ? NEW_INIT_PROMPT : OLD_INIT_PROMPT, text:
feature('NEW_INIT') &&
(process.env.USER_TYPE === 'ant' ||
isEnvTruthy(process.env.CLAUDE_CODE_NEW_INIT))
? NEW_INIT_PROMPT
: OLD_INIT_PROMPT,
}, },
] ]
}, },

View File

@@ -1,13 +0,0 @@
import { feature } from 'bun:bundle'
import { isEnvTruthy } from '../utils/envUtils.js'
export function isNewInitEnabled(): boolean {
if (feature('NEW_INIT')) {
return (
process.env.USER_TYPE === 'ant' ||
isEnvTruthy(process.env.CLAUDE_CODE_NEW_INIT)
)
}
return false
}

View File

@@ -2187,7 +2187,7 @@ function generateHtmlReport(
` `
: '' : ''
// Build Team Feedback section (collapsible, internal-only) // Build Team Feedback section (collapsible, ant-only)
const ccImprovements = const ccImprovements =
process.env.USER_TYPE === 'ant' process.env.USER_TYPE === 'ant'
? insights.cc_team_improvements?.improvements || [] ? insights.cc_team_improvements?.improvements || []
@@ -2804,7 +2804,7 @@ export async function generateUsageReport(options?: {
}> { }> {
let remoteStats: { hosts: RemoteHostInfo[]; totalCopied: number } | undefined 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) { if (process.env.USER_TYPE === 'ant' && options?.collectRemote) {
const destDir = join(getClaudeConfigHomeDir(), 'projects') const destDir = join(getClaudeConfigHomeDir(), 'projects')
const { hosts, totalCopied } = await collectAllRemoteHostData(destDir) const { hosts, totalCopied } = await collectAllRemoteHostData(destDir)
@@ -3072,6 +3072,33 @@ const usageReport: Command = {
let reportUrl = `file://${htmlPath}` let reportUrl = `file://${htmlPath}`
let uploadHint = '' 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 // Build header with stats
const sessionLabel = const sessionLabel =
data.total_sessions_scanned && data.total_sessions_scanned &&
@@ -3085,7 +3112,7 @@ const usageReport: Command = {
`${data.git_commits} commits`, `${data.git_commits} commits`,
].join(' · ') ].join(' · ')
// Build remote host info (internal-only) // Build remote host info (ant-only)
let remoteInfo = '' let remoteInfo = ''
if (process.env.USER_TYPE === 'ant') { if (process.env.USER_TYPE === 'ant') {
if (remoteStats && remoteStats.totalCopied > 0) { if (remoteStats && remoteStats.totalCopied > 0) {

View File

@@ -39,16 +39,16 @@ type InstallState = {
message: string; message: string;
warnings?: string[]; warnings?: string[];
}; };
export function getInstallationPath(): string { function getInstallationPath(): string {
const isWindows = env.platform === 'win32'; const isWindows = env.platform === 'win32';
const homeDir = homedir(); const homeDir = homedir();
if (isWindows) { if (isWindows) {
// Convert to Windows-style path // Convert to Windows-style path
const windowsPath = join(homeDir, '.local', 'bin', 'openclaude.exe'); const windowsPath = join(homeDir, '.local', 'bin', 'claude.exe');
// Replace forward slashes with backslashes for Windows display // Replace forward slashes with backslashes for Windows display
return windowsPath.replace(/\//g, '\\'); return windowsPath.replace(/\//g, '\\');
} }
return '~/.local/bin/openclaude'; return '~/.local/bin/claude';
} }
function SetupNotes(t0) { function SetupNotes(t0) {
const $ = _c(5); const $ = _c(5);

View File

@@ -1,68 +0,0 @@
import { afterEach, expect, mock, test } from 'bun:test'
import { getAdditionalModelOptionsCacheScope } from '../../services/api/providerConfig.js'
import { getAPIProvider } from '../../utils/model/providers.js'
const originalEnv = {
CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI,
CLAUDE_CODE_USE_GEMINI: process.env.CLAUDE_CODE_USE_GEMINI,
CLAUDE_CODE_USE_GITHUB: process.env.CLAUDE_CODE_USE_GITHUB,
CLAUDE_CODE_USE_MISTRAL: process.env.CLAUDE_CODE_USE_MISTRAL,
CLAUDE_CODE_USE_BEDROCK: process.env.CLAUDE_CODE_USE_BEDROCK,
CLAUDE_CODE_USE_VERTEX: process.env.CLAUDE_CODE_USE_VERTEX,
CLAUDE_CODE_USE_FOUNDRY: process.env.CLAUDE_CODE_USE_FOUNDRY,
OPENAI_BASE_URL: process.env.OPENAI_BASE_URL,
OPENAI_API_BASE: process.env.OPENAI_API_BASE,
OPENAI_MODEL: process.env.OPENAI_MODEL,
}
afterEach(() => {
mock.restore()
process.env.CLAUDE_CODE_USE_OPENAI = originalEnv.CLAUDE_CODE_USE_OPENAI
process.env.CLAUDE_CODE_USE_GEMINI = originalEnv.CLAUDE_CODE_USE_GEMINI
process.env.CLAUDE_CODE_USE_GITHUB = originalEnv.CLAUDE_CODE_USE_GITHUB
process.env.CLAUDE_CODE_USE_MISTRAL = originalEnv.CLAUDE_CODE_USE_MISTRAL
process.env.CLAUDE_CODE_USE_BEDROCK = originalEnv.CLAUDE_CODE_USE_BEDROCK
process.env.CLAUDE_CODE_USE_VERTEX = originalEnv.CLAUDE_CODE_USE_VERTEX
process.env.CLAUDE_CODE_USE_FOUNDRY = originalEnv.CLAUDE_CODE_USE_FOUNDRY
process.env.OPENAI_BASE_URL = originalEnv.OPENAI_BASE_URL
process.env.OPENAI_API_BASE = originalEnv.OPENAI_API_BASE
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'
delete process.env.CLAUDE_CODE_USE_GEMINI
delete process.env.CLAUDE_CODE_USE_GITHUB
delete process.env.CLAUDE_CODE_USE_MISTRAL
delete process.env.CLAUDE_CODE_USE_BEDROCK
delete process.env.CLAUDE_CODE_USE_VERTEX
delete process.env.CLAUDE_CODE_USE_FOUNDRY
delete process.env.OPENAI_API_BASE
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,
}))
expect(getAdditionalModelOptionsCacheScope()).toBe('openai:http://127.0.0.1:8080/v1')
const { call } = await import('./model.js')
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 type { CommandResultDisplay } from '../../commands.js';
import { ModelPicker } from '../../components/ModelPicker.js'; import { ModelPicker } from '../../components/ModelPicker.js';
import { COMMON_HELP_ARGS, COMMON_INFO_ARGS } from '../../constants/xml.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 { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js';
import { useAppState, useSetAppState } from '../../state/AppState.js'; import { useAppState, useSetAppState } from '../../state/AppState.js';
import type { LocalJSXCommandCall } from '../../types/command.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 { clearFastModeCooldown, isFastModeAvailable, isFastModeEnabled, isFastModeSupportedByModel } from '../../utils/fastMode.js';
import { MODEL_ALIASES } from '../../utils/model/aliases.js'; import { MODEL_ALIASES } from '../../utils/model/aliases.js';
import { checkOpus1mAccess, checkSonnet1mAccess } from '../../utils/model/check1mAccess.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 { getDefaultMainLoopModelSetting, isOpus1mMergeEnabled, renderDefaultModelSetting } from '../../utils/model/model.js';
import { isModelAllowed } from '../../utils/model/modelAllowlist.js'; import { isModelAllowed } from '../../utils/model/modelAllowlist.js';
import { validateModel } from '../../utils/model/validateModel.js'; import { validateModel } from '../../utils/model/validateModel.js';
import { getAdditionalModelOptionsCacheScope } from '../../services/api/providerConfig.js';
function ModelPickerWrapper(t0) { function ModelPickerWrapper(t0) {
const $ = _c(17); const $ = _c(17);
const { const {
@@ -274,33 +268,6 @@ function _temp8(s_0) {
function _temp7(s) { function _temp7(s) {
return s.mainLoopModel; 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 (!getAdditionalModelOptionsCacheScope()?.startsWith('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) => { export const call: LocalJSXCommandCall = async (onDone, _context, args) => {
args = args?.trim() || ''; args = args?.trim() || '';
if (COMMON_INFO_ARGS.includes(args)) { if (COMMON_INFO_ARGS.includes(args)) {
@@ -321,9 +288,6 @@ export const call: LocalJSXCommandCall = async (onDone, _context, args) => {
}); });
return <SetModelAndClose args={args} onDone={onDone} />; return <SetModelAndClose args={args} onDone={onDone} />;
} }
if (getAdditionalModelOptionsCacheScope()?.startsWith('openai:')) {
void refreshOpenAIModelOptionsCache();
}
return <ModelPickerWrapper onDone={onDone} />; return <ModelPickerWrapper onDone={onDone} />;
}; };
function renderModelLabel(model: string | null): string { function renderModelLabel(model: string | null): string {

View File

@@ -2,9 +2,8 @@ import type { Command } from '../../commands.js'
const onboardGithub: Command = { const onboardGithub: Command = {
name: 'onboard-github', name: 'onboard-github',
aliases: ['onboarding-github', 'onboardgithub', 'onboardinggithub'],
description: description:
'Interactive setup for GitHub Copilot: OAuth device login stored in secure storage', 'Interactive setup for GitHub Models: device login or PAT, saved to secure storage',
type: 'local-jsx', type: 'local-jsx',
load: () => import('./onboard-github.js'), load: () => import('./onboard-github.js'),
} }

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

@@ -2,9 +2,9 @@ import * as React from 'react'
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { Select } from '../../components/CustomSelect/select.js' import { Select } from '../../components/CustomSelect/select.js'
import { Spinner } from '../../components/Spinner.js' import { Spinner } from '../../components/Spinner.js'
import TextInput from '../../components/TextInput.js'
import { Box, Text } from '../../ink.js' import { Box, Text } from '../../ink.js'
import { import {
exchangeForCopilotToken,
openVerificationUri, openVerificationUri,
pollAccessToken, pollAccessToken,
requestDeviceCode, requestDeviceCode,
@@ -12,134 +12,29 @@ import {
import type { LocalJSXCommandCall } from '../../types/command.js' import type { LocalJSXCommandCall } from '../../types/command.js'
import { import {
hydrateGithubModelsTokenFromSecureStorage, hydrateGithubModelsTokenFromSecureStorage,
readGithubModelsToken,
saveGithubModelsToken, saveGithubModelsToken,
} from '../../utils/githubModelsCredentials.js' } from '../../utils/githubModelsCredentials.js'
import { getSettingsForSource, updateSettingsForSource } from '../../utils/settings/settings.js' import { updateSettingsForSource } from '../../utils/settings/settings.js'
const DEFAULT_MODEL = 'github:copilot' const DEFAULT_MODEL = 'github:copilot'
const FORCE_RELOGIN_ARGS = new Set([
'force',
'--force',
'relogin',
'--relogin',
'reauth',
'--reauth',
])
type Step = 'menu' | 'device-busy' | 'error' type Step =
| 'menu'
const PROVIDER_SPECIFIC_KEYS = new Set([ | 'device-busy'
'CLAUDE_CODE_USE_OPENAI', | 'pat'
'CLAUDE_CODE_USE_GEMINI', | 'error'
'CLAUDE_CODE_USE_BEDROCK',
'CLAUDE_CODE_USE_VERTEX',
'CLAUDE_CODE_USE_FOUNDRY',
'OPENAI_BASE_URL',
'OPENAI_API_BASE',
'OPENAI_API_KEY',
'OPENAI_MODEL',
'GEMINI_API_KEY',
'GOOGLE_API_KEY',
'GEMINI_BASE_URL',
'GEMINI_MODEL',
'GEMINI_ACCESS_TOKEN',
'GEMINI_AUTH_MODE',
])
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))
}
const GITHUB_PAT_PREFIXES = ['ghp_', 'gho_','ghs_', 'ghr_', 'github_pat_']
function isGithubPat(token: string): boolean {
return GITHUB_PAT_PREFIXES.some(prefix => token.startsWith(prefix))
}
export function hasExistingGithubModelsLoginToken(
env: NodeJS.ProcessEnv = process.env,
storedToken?: string,
): boolean {
const envToken = env.GITHUB_TOKEN?.trim() || env.GH_TOKEN?.trim()
if (envToken) {
// PATs are no longer supported - require OAuth re-auth
if (isGithubPat(envToken)) {
return false
}
return true
}
const persisted = (storedToken ?? readGithubModelsToken())?.trim()
// PATs are no longer supported - require OAuth re-auth
if (persisted && isGithubPat(persisted)) {
return false
}
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 } { function mergeUserSettingsEnv(model: string): { ok: boolean; detail?: string } {
const currentSettings = getSettingsForSource('userSettings')
const currentEnv = currentSettings?.env ?? {}
const newEnv: Record<string, string> = {}
for (const [key, value] of Object.entries(currentEnv)) {
if (!PROVIDER_SPECIFIC_KEYS.has(key)) {
newEnv[key] = value
}
}
newEnv.CLAUDE_CODE_USE_GITHUB = '1'
newEnv.OPENAI_MODEL = model
const { error } = updateSettingsForSource('userSettings', { const { error } = updateSettingsForSource('userSettings', {
env: newEnv, 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) { if (error) {
return { ok: false, detail: error.message } return { ok: false, detail: error.message }
@@ -147,32 +42,6 @@ function mergeUserSettingsEnv(model: string): { ok: boolean; detail?: string } {
return { ok: true } 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: { function OnboardGithub(props: {
onDone: Parameters<LocalJSXCommandCall>[0] onDone: Parameters<LocalJSXCommandCall>[0]
onChangeAPIKey: () => void onChangeAPIKey: () => void
@@ -184,42 +53,32 @@ function OnboardGithub(props: {
user_code: string user_code: string
verification_uri: string verification_uri: string
} | null>(null) } | null>(null)
const [patDraft, setPatDraft] = useState('')
const [cursorOffset, setCursorOffset] = useState(0)
const finalize = useCallback( const finalize = useCallback(
async ( async (token: string, model: string = DEFAULT_MODEL) => {
token: string, const saved = saveGithubModelsToken(token)
model: string = DEFAULT_MODEL,
oauthToken?: string,
) => {
const saved = saveGithubModelsToken(token, oauthToken)
if (!saved.success) { if (!saved.success) {
setErrorMsg(saved.warning ?? 'Could not save token to secure storage.') setErrorMsg(saved.warning ?? 'Could not save token to secure storage.')
setStep('error') setStep('error')
return return
} }
const activated = activateGithubOnboardingMode(model, { const merged = mergeUserSettingsEnv(model.trim() || DEFAULT_MODEL)
onChangeAPIKey, if (!merged.ok) {
})
if (!activated.ok) {
setErrorMsg( 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.`, `Add env CLAUDE_CODE_USE_GITHUB=1 and OPENAI_MODEL to ~/.claude/settings.json manually.`,
) )
setStep('error') setStep('error')
return return
} }
// Clear stale provider-specific env vars from the current session
// so resolveProviderRequest() doesn't pick up a previous provider's
// base URL or key after onboarding completes.
for (const key of PROVIDER_SPECIFIC_KEYS) {
delete process.env[key]
}
process.env.CLAUDE_CODE_USE_GITHUB = '1' process.env.CLAUDE_CODE_USE_GITHUB = '1'
process.env.OPENAI_MODEL = model.trim() || DEFAULT_MODEL process.env.OPENAI_MODEL = model.trim() || DEFAULT_MODEL
hydrateGithubModelsTokenFromSecureStorage() hydrateGithubModelsTokenFromSecureStorage()
onChangeAPIKey() onChangeAPIKey()
onDone( onDone(
'GitHub Copilot onboard complete. Copilot token and OAuth token stored in secure storage (Windows/Linux: ~/.claude/.credentials.json, macOS: Keychain fallback to ~/.claude/.credentials.json); user settings updated. Restart if the model does not switch.', 'GitHub Models onboard complete. Token stored in secure storage; user settings updated. Restart if the model does not switch.',
{ display: 'user' }, { display: 'user' },
) )
}, },
@@ -237,12 +96,11 @@ function OnboardGithub(props: {
verification_uri: device.verification_uri, verification_uri: device.verification_uri,
}) })
await openVerificationUri(device.verification_uri) await openVerificationUri(device.verification_uri)
const oauthToken = await pollAccessToken(device.device_code, { const token = await pollAccessToken(device.device_code, {
initialInterval: device.interval, initialInterval: device.interval,
timeoutSeconds: device.expires_in, timeoutSeconds: device.expires_in,
}) })
const copilotToken = await exchangeForCopilotToken(oauthToken) await finalize(token, DEFAULT_MODEL)
await finalize(copilotToken.token, DEFAULT_MODEL, oauthToken)
} catch (e) { } catch (e) {
setErrorMsg(e instanceof Error ? e.message : String(e)) setErrorMsg(e instanceof Error ? e.message : String(e))
setStep('error') setStep('error')
@@ -281,7 +139,7 @@ function OnboardGithub(props: {
if (step === 'device-busy') { if (step === 'device-busy') {
return ( return (
<Box flexDirection="column" gap={1}> <Box flexDirection="column" gap={1}>
<Text>GitHub Copilot sign-in</Text> <Text>GitHub device login</Text>
{deviceHint ? ( {deviceHint ? (
<> <>
<Text> <Text>
@@ -289,22 +147,54 @@ function OnboardGithub(props: {
{deviceHint.verification_uri} {deviceHint.verification_uri}
</Text> </Text>
<Text dimColor> <Text dimColor>
A browser window may have opened. Waiting for authorization... A browser window may have opened. Waiting for authorization
</Text> </Text>
</> </>
) : ( ) : (
<Text dimColor>Requesting device code from GitHub...</Text> <Text dimColor>Requesting device code from GitHub</Text>
)} )}
<Spinner /> <Spinner />
</Box> </Box>
) )
} }
if (step === 'pat') {
return (
<Box flexDirection="column" gap={1}>
<Text>Paste a GitHub personal access token with access to GitHub Models.</Text>
<Text dimColor>Input is masked. Enter to submit; Esc to go back.</Text>
<TextInput
value={patDraft}
mask="*"
onChange={setPatDraft}
onSubmit={async (value: string) => {
const t = value.trim()
if (!t) {
return
}
await finalize(t, DEFAULT_MODEL)
}}
onExit={() => {
setStep('menu')
setPatDraft('')
}}
columns={80}
cursorOffset={cursorOffset}
onChangeCursorOffset={setCursorOffset}
/>
</Box>
)
}
const menuOptions = [ const menuOptions = [
{ {
label: 'Sign in with browser', label: 'Sign in with browser (device code)',
value: 'device' as const, value: 'device' as const,
}, },
{
label: 'Paste personal access token',
value: 'pat' as const,
},
{ {
label: 'Cancel', label: 'Cancel',
value: 'cancel' as const, value: 'cancel' as const,
@@ -313,10 +203,10 @@ function OnboardGithub(props: {
return ( return (
<Box flexDirection="column" gap={1}> <Box flexDirection="column" gap={1}>
<Text bold>GitHub Copilot setup</Text> <Text bold>GitHub Models setup</Text>
<Text dimColor> <Text dimColor>
Stores your token in the OS credential store (macOS Keychain when available) 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. GITHUB_TOKEN needed for future runs.
</Text> </Text>
<Select <Select
@@ -326,6 +216,10 @@ function OnboardGithub(props: {
onDone('GitHub onboard cancelled', { display: 'system' }) onDone('GitHub onboard cancelled', { display: 'system' })
return return
} }
if (v === 'pat') {
setStep('pat')
return
}
void runDeviceFlow() void runDeviceFlow()
}} }}
/> />
@@ -333,28 +227,7 @@ function OnboardGithub(props: {
) )
} }
export const call: LocalJSXCommandCall = async (onDone, context, args) => { export const call: LocalJSXCommandCall = async (onDone, context) => {
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
}
return ( return (
<OnboardGithub <OnboardGithub
onDone={onDone} onDone={onDone}

View File

@@ -1,10 +1,12 @@
import type { Command } from '../../commands.js' import type { Command } from '../../commands.js'
import { shouldInferenceConfigCommandBeImmediate } from '../../utils/immediateCommand.js'
const provider = { export default {
type: 'local-jsx', type: 'local-jsx',
name: 'provider', 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'), load: () => import('./provider.js'),
} satisfies Command } satisfies Command
export default provider

View File

@@ -52,11 +52,7 @@ async function renderFinalFrame(node: React.ReactNode): Promise<string> {
patchConsole: false, patchConsole: false,
}) })
// Timeout guard: if render throws before exit effect fires, don't hang await instance.waitUntilExit()
await Promise.race([
instance.waitUntilExit(),
new Promise<void>(resolve => setTimeout(resolve, 3000)),
])
return stripAnsi(extractLastFrame(getOutput())) return stripAnsi(extractLastFrame(getOutput()))
} }
@@ -201,38 +197,6 @@ test('buildProfileSaveMessage maps provider fields without echoing secrets', ()
expect(message).not.toContain('sk-secret-12345678') 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', () => { test('buildCurrentProviderSummary redacts poisoned model and endpoint values', () => {
const summary = buildCurrentProviderSummary({ const summary = buildCurrentProviderSummary({
processEnv: { processEnv: {
@@ -249,51 +213,6 @@ test('buildCurrentProviderSummary redacts poisoned model and endpoint values', (
expect(summary.endpointLabel).toBe('sk-...5678') 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', () => { test('getProviderWizardDefaults ignores poisoned current provider values', () => {
const defaults = getProviderWizardDefaults({ const defaults = getProviderWizardDefaults({
OPENAI_API_KEY: 'sk-secret-12345678', 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 type { LocalJSXCommandCall, LocalJSXCommandOnDone } from '../../types/command.js'
import { COMMON_HELP_ARGS, COMMON_INFO_ARGS } from '../../constants/xml.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 TextInput from '../../components/TextInput.js'
import { import {
Select, Select,
@@ -15,21 +14,17 @@ import { Box, Text } from '../../ink.js'
import { import {
DEFAULT_CODEX_BASE_URL, DEFAULT_CODEX_BASE_URL,
DEFAULT_OPENAI_BASE_URL, DEFAULT_OPENAI_BASE_URL,
isLocalProviderUrl,
resolveCodexApiCredentials, resolveCodexApiCredentials,
resolveProviderRequest, resolveProviderRequest,
} from '../../services/api/providerConfig.js' } from '../../services/api/providerConfig.js'
import { import {
buildCodexProfileEnv, buildCodexProfileEnv,
buildGeminiProfileEnv, buildGeminiProfileEnv,
buildMistralProfileEnv,
buildOllamaProfileEnv, buildOllamaProfileEnv,
buildOpenAIProfileEnv, buildOpenAIProfileEnv,
createProfileFile, createProfileFile,
DEFAULT_GEMINI_BASE_URL, DEFAULT_GEMINI_BASE_URL,
DEFAULT_GEMINI_MODEL, DEFAULT_GEMINI_MODEL,
DEFAULT_MISTRAL_BASE_URL,
DEFAULT_MISTRAL_MODEL,
deleteProfileFile, deleteProfileFile,
loadProfileFile, loadProfileFile,
maskSecretForDisplay, maskSecretForDisplay,
@@ -41,14 +36,6 @@ import {
type ProfileFile, type ProfileFile,
type ProviderProfile, type ProviderProfile,
} from '../../utils/providerProfile.js' } from '../../utils/providerProfile.js'
import {
getGeminiProjectIdHint,
mayHaveGeminiAdcCredentials,
} from '../../utils/geminiAuth.js'
import {
readGeminiAccessToken,
saveGeminiAccessToken,
} from '../../utils/geminiCredentials.js'
import { import {
getGoalDefaultOpenAIModel, getGoalDefaultOpenAIModel,
normalizeRecommendationGoal, normalizeRecommendationGoal,
@@ -56,11 +43,7 @@ import {
recommendOllamaModel, recommendOllamaModel,
type RecommendationGoal, type RecommendationGoal,
} from '../../utils/providerRecommendation.js' } from '../../utils/providerRecommendation.js'
import { import { hasLocalOllama, listOllamaModels } from '../../utils/providerDiscovery.js'
getLocalOpenAICompatibleProviderLabel,
hasLocalOllama,
listOllamaModels,
} from '../../utils/providerDiscovery.js'
type ProviderChoice = 'auto' | ProviderProfile | 'clear' type ProviderChoice = 'auto' | ProviderProfile | 'clear'
@@ -77,22 +60,8 @@ type Step =
baseUrl: string | null baseUrl: string | null
defaultModel: string defaultModel: string
} }
| { name: 'mistral-key'; defaultModel: string }
| { name: 'mistral-base'; apiKey: string; defaultModel: string }
| {
name: 'mistral-model'
apiKey: string
baseUrl: string | null
defaultModel: string
}
| { name: 'gemini-auth-method' }
| { name: 'gemini-key' } | { name: 'gemini-key' }
| { name: 'gemini-access-token' } | { name: 'gemini-model'; apiKey: string }
| {
name: 'gemini-model'
apiKey?: string
authMode: 'api-key' | 'access-token' | 'adc'
}
| { name: 'codex-check' } | { name: 'codex-check' }
type CurrentProviderSummary = { type CurrentProviderSummary = {
@@ -127,8 +96,6 @@ type ProviderWizardDefaults = {
openAIModel: string openAIModel: string
openAIBaseUrl: string openAIBaseUrl: string
geminiModel: string geminiModel: string
mistralModel: string
mistralBaseUrl: string
} }
function isEnvTruthy(value: string | undefined): boolean { function isEnvTruthy(value: string | undefined): boolean {
@@ -160,19 +127,11 @@ export function getProviderWizardDefaults(
const safeGeminiModel = const safeGeminiModel =
sanitizeProviderConfigValue(processEnv.GEMINI_MODEL, processEnv) || sanitizeProviderConfigValue(processEnv.GEMINI_MODEL, processEnv) ||
DEFAULT_GEMINI_MODEL DEFAULT_GEMINI_MODEL
const safeMistralModel =
sanitizeProviderConfigValue(processEnv.MISTRAL_MODEL, processEnv) ||
DEFAULT_MISTRAL_MODEL
const safeMistralBaseUrl =
sanitizeProviderConfigValue(processEnv.MISTRAL_BASE_URL, processEnv) ||
DEFAULT_MISTRAL_BASE_URL
return { return {
openAIModel: safeOpenAIModel, openAIModel: safeOpenAIModel,
openAIBaseUrl: safeOpenAIBaseUrl, openAIBaseUrl: safeOpenAIBaseUrl,
geminiModel: safeGeminiModel, geminiModel: safeGeminiModel,
mistralModel: safeMistralModel,
mistralBaseUrl: safeMistralBaseUrl,
} }
} }
@@ -199,38 +158,6 @@ export function buildCurrentProviderSummary(options?: {
} }
} }
if (isEnvTruthy(processEnv.CLAUDE_CODE_USE_MISTRAL)) {
return {
providerLabel: 'Mistral',
modelLabel: getSafeDisplayValue(
processEnv.MISTRAL_MODEL ?? DEFAULT_MISTRAL_MODEL,
processEnv
),
endpointLabel: getSafeDisplayValue(
processEnv.MISTRAL_BASE_URL ?? DEFAULT_MISTRAL_BASE_URL,
processEnv
),
savedProfileLabel,
}
}
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)) { if (isEnvTruthy(processEnv.CLAUDE_CODE_USE_OPENAI)) {
const request = resolveProviderRequest({ const request = resolveProviderRequest({
model: processEnv.OPENAI_MODEL, model: processEnv.OPENAI_MODEL,
@@ -240,8 +167,10 @@ export function buildCurrentProviderSummary(options?: {
let providerLabel = 'OpenAI-compatible' let providerLabel = 'OpenAI-compatible'
if (request.transport === 'codex_responses') { if (request.transport === 'codex_responses') {
providerLabel = 'Codex' providerLabel = 'Codex'
} else if (isLocalProviderUrl(request.baseUrl)) { } else if (request.baseUrl.includes('localhost:11434')) {
providerLabel = getLocalOpenAICompatibleProviderLabel(request.baseUrl) providerLabel = 'Ollama'
} else if (request.baseUrl.includes('localhost:1234')) {
providerLabel = 'LM Studio'
} }
return { return {
@@ -287,29 +216,7 @@ function buildSavedProfileSummary(
env, env,
), ),
credentialLabel: credentialLabel:
env.GEMINI_AUTH_MODE === 'access-token' maskSecretForDisplay(env.GEMINI_API_KEY) !== undefined
? 'access token (stored securely)'
: env.GEMINI_AUTH_MODE === 'adc'
? 'local ADC'
: maskSecretForDisplay(env.GEMINI_API_KEY) !== undefined
? 'configured'
: undefined,
}
case 'mistral':
return {
providerLabel: 'Mistral',
modelLabel: getSafeDisplayValue(
env.MISTRAL_MODEL ?? DEFAULT_MISTRAL_MODEL,
process.env,
env,
),
endpointLabel: getSafeDisplayValue(
env.MISTRAL_BASE_URL ?? DEFAULT_MISTRAL_BASE_URL,
process.env,
env,
),
credentialLabel:
maskSecretForDisplay(env.MISTRAL_API_KEY) !== undefined
? 'configured' ? 'configured'
: undefined, : undefined,
} }
@@ -346,20 +253,16 @@ function buildSavedProfileSummary(
), ),
} }
case 'openai': case 'openai':
default: { default:
const baseUrl = env.OPENAI_BASE_URL ?? DEFAULT_OPENAI_BASE_URL
return { return {
providerLabel: isLocalProviderUrl(baseUrl) providerLabel: 'OpenAI-compatible',
? getLocalOpenAICompatibleProviderLabel(baseUrl)
: 'OpenAI-compatible',
modelLabel: getSafeDisplayValue( modelLabel: getSafeDisplayValue(
env.OPENAI_MODEL ?? 'gpt-4o', env.OPENAI_MODEL ?? 'gpt-4o',
process.env, process.env,
env, env,
), ),
endpointLabel: getSafeDisplayValue( endpointLabel: getSafeDisplayValue(
baseUrl, env.OPENAI_BASE_URL ?? DEFAULT_OPENAI_BASE_URL,
process.env, process.env,
env, env,
), ),
@@ -368,7 +271,6 @@ function buildSavedProfileSummary(
? 'configured' ? 'configured'
: undefined, : undefined,
} }
}
} }
} }
@@ -525,12 +427,7 @@ function ProviderChooser({
{ {
label: 'Gemini', label: 'Gemini',
value: 'gemini', value: 'gemini',
description: 'Use Google Gemini with API key, access token, or local ADC', description: 'Use a Google Gemini API key',
},
{
label: 'Mistral',
value: 'mistral',
description: 'Use Mistral with API key'
}, },
{ {
label: 'Codex', label: 'Codex',
@@ -1029,12 +926,7 @@ export function ProviderWizard({
defaultModel: defaults.openAIModel, defaultModel: defaults.openAIModel,
}) })
} else if (value === 'gemini') { } else if (value === 'gemini') {
setStep({ name: 'gemini-auth-method' }) setStep({ name: 'gemini-key' })
} else if (value === 'mistral') {
setStep({
name: 'mistral-key',
defaultModel: defaults.mistralModel,
})
} else if (value === 'clear') { } else if (value === 'clear') {
const filePath = deleteProfileFile() const filePath = deleteProfileFile()
onDone(`Removed saved provider profile at ${filePath}. Restart OpenClaude to go back to normal startup.`, { onDone(`Removed saved provider profile at ${filePath}. Restart OpenClaude to go back to normal startup.`, {
@@ -1174,171 +1066,12 @@ export function ProviderWizard({
/> />
) )
case 'mistral-key':
return (
<TextEntryDialog
resetStateKey={step.name}
title="Mistral setup"
subtitle="Step 1 of 3"
description={
process.env.MISTRAL_API_KEY
? 'Enter an API key, or leave this blank to reuse the current MISTRAL_API_KEY from this session.'
: 'Enter the API key for your Mistral provider.'
}
initialValue=""
placeholder="..."
mask="*"
allowEmpty={Boolean(process.env.MISTRAL_API_KEY)}
validate={value => {
const candidate = value.trim() || process.env.MISTRAL_API_KEY || ''
return sanitizeApiKey(candidate)
? null
: 'Enter a real API key. Placeholder values like SUA_CHAVE are not valid.'
}}
onSubmit={value => {
const apiKey = value.trim() || process.env.MISTRAL_API_KEY || ''
setStep({
name: 'mistral-base',
apiKey,
defaultModel: step.defaultModel,
})
}}
onCancel={() => setStep({ name: 'choose' })}
/>
)
case 'mistral-base':
return (
<TextEntryDialog
resetStateKey={step.name}
title="Mistral setup"
subtitle="Step 2 of 3"
description={`Optionally enter a base URL. Leave blank for ${DEFAULT_MISTRAL_BASE_URL}.`}
initialValue={
defaults.mistralBaseUrl === DEFAULT_MISTRAL_BASE_URL
? ''
: defaults.mistralBaseUrl
}
placeholder={DEFAULT_MISTRAL_BASE_URL}
allowEmpty
onSubmit={value => {
setStep({
name: 'mistral-model',
apiKey: step.apiKey,
baseUrl: value.trim() || null,
defaultModel: step.defaultModel,
})
}}
onCancel={() =>
setStep({
name: 'mistral-key',
defaultModel: step.defaultModel,
})
}
/>
)
case 'mistral-model':
return (
<TextEntryDialog
resetStateKey={step.name}
title="Mistral setup"
subtitle="Step 3 of 3"
description={`Enter a model name. Leave blank for ${step.defaultModel}.`}
initialValue={defaults.mistralModel ?? step.defaultModel}
placeholder={step.defaultModel}
allowEmpty
onSubmit={value => {
const env = buildMistralProfileEnv({
model: value.trim() || step.defaultModel,
baseUrl: step.baseUrl,
apiKey: step.apiKey,
processEnv: process.env,
})
if (env) {
finishProfileSave(onDone, 'mistral', env)
}
}}
onCancel={() =>
setStep({
name: 'mistral-base',
apiKey: step.apiKey,
defaultModel: step.defaultModel,
})
}
/>
)
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': case 'gemini-key':
return ( return (
<TextEntryDialog <TextEntryDialog
resetStateKey={step.name} resetStateKey={step.name}
title="Gemini setup" title="Gemini setup"
subtitle="Step 1 of 3" subtitle="Step 1 of 2"
description={ description={
process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY 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.' ? 'Enter a Gemini API key, or leave this blank to reuse the current GEMINI_API_KEY/GOOGLE_API_KEY from this session.'
@@ -1356,95 +1089,25 @@ export function ProviderWizard({
process.env.GEMINI_API_KEY || process.env.GEMINI_API_KEY ||
process.env.GOOGLE_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': case 'gemini-model':
return ( return (
<TextEntryDialog <TextEntryDialog
resetStateKey={step.name} resetStateKey={step.name}
title="Gemini setup" title="Gemini setup"
subtitle={ subtitle="Step 2 of 2"
step.authMode === 'api-key' description={`Enter a Gemini model name. Leave blank for ${DEFAULT_GEMINI_MODEL}.`}
? '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.`
}
initialValue={defaults.geminiModel} initialValue={defaults.geminiModel}
placeholder={DEFAULT_GEMINI_MODEL} placeholder={DEFAULT_GEMINI_MODEL}
allowEmpty allowEmpty
onSubmit={value => { 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({ const env = buildGeminiProfileEnv({
apiKey: step.apiKey, apiKey: step.apiKey,
authMode: step.authMode,
model: value.trim() || DEFAULT_GEMINI_MODEL, model: value.trim() || DEFAULT_GEMINI_MODEL,
processEnv: {}, processEnv: {},
}) })
@@ -1452,13 +1115,7 @@ export function ProviderWizard({
finishProfileSave(onDone, 'gemini', env) finishProfileSave(onDone, 'gemini', env)
} }
}} }}
onCancel={() => onCancel={() => setStep({ name: 'gemini-key' })}
step.authMode === 'api-key'
? setStep({ name: 'gemini-key' })
: step.authMode === 'access-token'
? setStep({ name: 'gemini-access-token' })
: setStep({ name: 'gemini-auth-method' })
}
/> />
) )
@@ -1474,34 +1131,22 @@ export function ProviderWizard({
} }
export const call: LocalJSXCommandCall = async (onDone, _context, args) => { export const call: LocalJSXCommandCall = async (onDone, _context, args) => {
const trimmedArgs = args?.trim().toLowerCase() ?? '' const normalizedArgs = args?.trim().toLowerCase() || ''
if ( if (COMMON_INFO_ARGS.includes(normalizedArgs)) {
COMMON_HELP_ARGS.includes(trimmedArgs) || onDone(buildUsageText(), { display: 'system' })
COMMON_INFO_ARGS.includes(trimmedArgs) || return null
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
} }
return ( if (COMMON_HELP_ARGS.includes(normalizedArgs)) {
<ProviderManager onDone(buildUsageText(), { display: 'system' })
mode="manage" return null
onDone={result => { }
const message =
result?.message ??
(result?.action === 'saved'
? 'Provider profile updated'
: 'Provider manager closed')
onDone(message, { display: 'system' }) if (normalizedArgs) {
}} onDone('Usage: /provider', { display: 'system' })
/> return null
) }
return <ProviderWizard onDone={onDone} />
} }

View File

@@ -65,7 +65,7 @@ export async function call(onDone: (result?: string) => void, _context: unknown,
// Get the local settings path and make it relative to cwd // Get the local settings path and make it relative to cwd
const localSettingsPath = getSettingsFilePathForSource('localSettings'); const localSettingsPath = getSettingsFilePathForSource('localSettings');
const relativePath = localSettingsPath ? relative(getCwdState(), localSettingsPath) : '.openclaude/settings.local.json'; const relativePath = localSettingsPath ? relative(getCwdState(), localSettingsPath) : '.claude/settings.local.json';
const message = color('success', themeName)(`Added "${cleanPattern}" to excluded commands in ${relativePath}`); const message = color('success', themeName)(`Added "${cleanPattern}" to excluded commands in ${relativePath}`);
onDone(message); onDone(message);
return null; return null;

View File

@@ -118,7 +118,7 @@ export async function setupTerminal(theme: ThemeName): Promise<string> {
}); });
maybeMarkProjectOnboardingComplete(); 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') { if ("external" === 'ant') {
result += await setupShellCompletion(theme); 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). // so the override path is DCE'd from external builds).
// Shell-set env only, so top-level process.env read is fine // Shell-set env only, so top-level process.env read is fine
// — settings.env never injects this. // — 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; 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 */ /* eslint-enable custom-rules/no-process-env-top-level, custom-rules/no-sync-fs */

View File

@@ -1,12 +0,0 @@
import type { Command } from '../../commands.js'
const wiki = {
type: 'local-jsx',
name: 'wiki',
description: 'Initialize and inspect the OpenClaude project wiki',
argumentHint: '[init|status]',
immediate: true,
load: () => import('./wiki.js'),
} satisfies Command
export default wiki

View File

@@ -1,123 +0,0 @@
import React from 'react'
import { COMMON_HELP_ARGS, COMMON_INFO_ARGS } from '../../constants/xml.js'
import { ingestLocalWikiSource } from '../../services/wiki/ingest.js'
import { initializeWiki } from '../../services/wiki/init.js'
import { getWikiStatus } from '../../services/wiki/status.js'
import type {
LocalJSXCommandCall,
LocalJSXCommandOnDone,
} from '../../types/command.js'
import { getCwd } from '../../utils/cwd.js'
function renderHelp(): string {
return `Usage: /wiki [init|status|ingest <path>]
Manage the OpenClaude project wiki stored in .openclaude/wiki.
Commands:
/wiki init Initialize the wiki structure in the current project
/wiki status Show wiki status and page/source counts
/wiki ingest Ingest a local file into wiki sources
Examples:
/wiki init
/wiki status
/wiki ingest README.md`
}
function formatInitResult(result: Awaited<ReturnType<typeof initializeWiki>>): string {
const lines = [`Initialized OpenClaude wiki at ${result.root}`]
if (result.alreadyExisted) {
lines.push('', 'Wiki already existed. No new files were created.')
return lines.join('\n')
}
if (result.createdFiles.length > 0) {
lines.push('', 'Created files:')
for (const file of result.createdFiles) {
lines.push(`- ${file}`)
}
}
return lines.join('\n')
}
function formatStatus(status: Awaited<ReturnType<typeof getWikiStatus>>): string {
if (!status.initialized) {
return `OpenClaude wiki is not initialized in this project.\n\nRun /wiki init to create ${status.root}.`
}
return [
'OpenClaude wiki status',
'',
`Root: ${status.root}`,
`Pages: ${status.pageCount}`,
`Sources: ${status.sourceCount}`,
`Schema: ${status.hasSchema ? 'present' : 'missing'}`,
`Index: ${status.hasIndex ? 'present' : 'missing'}`,
`Log: ${status.hasLog ? 'present' : 'missing'}`,
`Last updated: ${status.lastUpdatedAt ?? 'unknown'}`,
].join('\n')
}
function formatIngestResult(
result: Awaited<ReturnType<typeof ingestLocalWikiSource>>,
): string {
return [
`Ingested ${result.sourceFile} into the OpenClaude wiki.`,
'',
`Title: ${result.title}`,
`Source note: ${result.sourceNote}`,
`Summary: ${result.summary}`,
].join('\n')
}
async function runWikiCommand(
onDone: LocalJSXCommandOnDone,
args: string,
): Promise<void> {
const cwd = getCwd()
const normalized = args.trim().toLowerCase()
if (COMMON_HELP_ARGS.includes(normalized) || COMMON_INFO_ARGS.includes(normalized)) {
onDone(renderHelp(), { display: 'system' })
return
}
if (!normalized || normalized === 'status') {
onDone(formatStatus(await getWikiStatus(cwd)), { display: 'system' })
return
}
if (normalized === 'init') {
onDone(formatInitResult(await initializeWiki(cwd)), { display: 'system' })
return
}
if (normalized.startsWith('ingest')) {
const pathArg = args.trim().slice('ingest'.length).trim()
if (!pathArg) {
onDone('Usage: /wiki ingest <local-file-path>', { display: 'system' })
return
}
onDone(formatIngestResult(await ingestLocalWikiSource(cwd, pathArg)), {
display: 'system',
})
return
}
onDone(`Unknown wiki subcommand: ${args.trim()}\n\n${renderHelp()}`, {
display: 'system',
})
}
export const call: LocalJSXCommandCall = async (
onDone,
_context,
args,
): Promise<React.ReactNode> => {
await runWikiCommand(onDone, args ?? '')
return null
}

View File

@@ -188,9 +188,9 @@ export function AutoUpdater({
Update installed · Restart to apply Update installed · Restart to apply
</Text>} </Text>}
{(autoUpdaterResult?.status === 'install_failed' || autoUpdaterResult?.status === 'no_permissions') && <Text color="error" wrap="truncate"> {(autoUpdaterResult?.status === 'install_failed' || autoUpdaterResult?.status === 'no_permissions') && <Text color="error" wrap="truncate">
Auto-update failed &middot; Try <Text bold>openclaude doctor</Text> or{' '} Auto-update failed &middot; Try <Text bold>claude doctor</Text> or{' '}
<Text bold> <Text bold>
{hasLocalInstall ? `cd ~/.openclaude/local && npm update ${MACRO.PACKAGE_URL}` : `npm i -g ${MACRO.PACKAGE_URL}`} {hasLocalInstall ? `cd ~/.claude/local && npm update ${MACRO.PACKAGE_URL}` : `npm i -g ${MACRO.PACKAGE_URL}`}
</Text> </Text>
</Text>} </Text>}
</Box>; </Box>;

View File

@@ -31,11 +31,9 @@ export function BaseTextInput(t0) {
} = t0; } = t0;
const { const {
onInput, onInput,
value,
renderedValue, renderedValue,
cursorLine, cursorLine,
cursorColumn, cursorColumn
offset,
} = inputState; } = inputState;
const t1 = Boolean(props.focus && props.showCursor && terminalFocus); const t1 = Boolean(props.focus && props.showCursor && terminalFocus);
let t2; let t2;
@@ -80,7 +78,7 @@ export function BaseTextInput(t0) {
renderedPlaceholder renderedPlaceholder
} = renderPlaceholder({ } = renderPlaceholder({
placeholder: props.placeholder, placeholder: props.placeholder,
value, value: props.value,
showCursor: props.showCursor, showCursor: props.showCursor,
focus: props.focus, focus: props.focus,
terminalFocus, terminalFocus,
@@ -90,9 +88,9 @@ export function BaseTextInput(t0) {
useInput(wrappedOnInput, { useInput(wrappedOnInput, {
isActive: props.focus isActive: props.focus
}); });
const commandWithoutArgs = value && value.trim().indexOf(" ") === -1 || value && value.endsWith(" "); const commandWithoutArgs = props.value && props.value.trim().indexOf(" ") === -1 || props.value && props.value.endsWith(" ");
const showArgumentHint = Boolean(props.argumentHint && value && commandWithoutArgs && value.startsWith("/")); const showArgumentHint = Boolean(props.argumentHint && props.value && commandWithoutArgs && props.value.startsWith("/"));
const cursorFiltered = props.showCursor && props.highlights ? props.highlights.filter(h => h.dimColor || offset < h.start || offset >= h.end) : props.highlights; const cursorFiltered = props.showCursor && props.highlights ? props.highlights.filter(h => h.dimColor || props.cursorOffset < h.start || props.cursorOffset >= h.end) : props.highlights;
const { const {
viewportCharOffset, viewportCharOffset,
viewportCharEnd viewportCharEnd
@@ -104,13 +102,13 @@ export function BaseTextInput(t0) {
})) : cursorFiltered; })) : cursorFiltered;
const hasHighlights = filteredHighlights && filteredHighlights.length > 0; const hasHighlights = filteredHighlights && filteredHighlights.length > 0;
if (hasHighlights) { if (hasHighlights) {
return <Box ref={cursorRef}><HighlightedInput text={renderedValue} highlights={filteredHighlights} />{showArgumentHint && <Text dimColor={true}>{value.endsWith(" ") ? "" : " "}{props.argumentHint}</Text>}{children}</Box>; return <Box ref={cursorRef}><HighlightedInput text={renderedValue} highlights={filteredHighlights} />{showArgumentHint && <Text dimColor={true}>{props.value?.endsWith(" ") ? "" : " "}{props.argumentHint}</Text>}{children}</Box>;
} }
const T0 = Box; const T0 = Box;
const T1 = Text; const T1 = Text;
const t4 = "truncate-end"; const t4 = "truncate-end";
const t5 = showPlaceholder && props.placeholderElement ? props.placeholderElement : showPlaceholder && renderedPlaceholder ? <Ansi>{renderedPlaceholder}</Ansi> : <Ansi>{renderedValue}</Ansi>; const t5 = showPlaceholder && props.placeholderElement ? props.placeholderElement : showPlaceholder && renderedPlaceholder ? <Ansi>{renderedPlaceholder}</Ansi> : <Ansi>{renderedValue}</Ansi>;
const t6 = showArgumentHint && <Text dimColor={true}>{value.endsWith(" ") ? "" : " "}{props.argumentHint}</Text>; const t6 = showArgumentHint && <Text dimColor={true}>{props.value?.endsWith(" ") ? "" : " "}{props.argumentHint}</Text>;
let t7; let t7;
if ($[4] !== T1 || $[5] !== children || $[6] !== props || $[7] !== t5 || $[8] !== t6) { if ($[4] !== T1 || $[5] !== children || $[6] !== props || $[7] !== t5 || $[8] !== t6) {
t7 = <T1 wrap={t4} dimColor={props.dimColor}>{t5}{t6}{children}</T1>; t7 = <T1 wrap={t4} dimColor={props.dimColor}>{t5}{t6}{children}</T1>;

View File

@@ -103,7 +103,7 @@ test('login picker shows the third-party platform option', async () => {
expect(output).toContain('3rd-party platform') expect(output).toContain('3rd-party platform')
}) })
test('third-party provider branch opens the first-run provider manager', async () => { test('third-party provider branch opens the provider wizard', async () => {
const output = await renderFrame( const output = await renderFrame(
<ConsoleOAuthFlow <ConsoleOAuthFlow
initialStatus={{ state: 'platform_setup' }} initialStatus={{ state: 'platform_setup' }}
@@ -111,9 +111,7 @@ test('third-party provider branch opens the first-run provider manager', async (
/>, />,
) )
expect(output).toContain('Set up provider') expect(output).toContain('Set up a provider profile')
expect(output).toContain('Anthropic') expect(output).toContain('OpenAI-compatible')
expect(output).toContain('OpenAI')
expect(output).toContain('Ollama') expect(output).toContain('Ollama')
expect(output).toContain('LM Studio')
}) })

View File

@@ -12,7 +12,7 @@ import { OAuthService } from '../services/oauth/index.js';
import { getOauthAccountInfo, validateForceLoginOrg } from '../utils/auth.js'; import { getOauthAccountInfo, validateForceLoginOrg } from '../utils/auth.js';
import { logError } from '../utils/log.js'; import { logError } from '../utils/log.js';
import { getSettings_DEPRECATED } from '../utils/settings/settings.js'; import { getSettings_DEPRECATED } from '../utils/settings/settings.js';
import { ProviderManager } from './ProviderManager.js'; import { ProviderWizard } from '../commands/provider/provider.js';
import { Select } from './CustomSelect/select.js'; import { Select } from './CustomSelect/select.js';
import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js'; import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js';
import { Spinner } from './Spinner.js'; import { Spinner } from './Spinner.js';
@@ -450,17 +450,16 @@ function OAuthStatusMessage({
case 'platform_setup': case 'platform_setup':
return ( return (
<ProviderManager <ProviderWizard
mode="first-run"
onDone={result => { onDone={result => {
if (!result || result.action !== 'saved' || !result.message) { if (!result) {
setOAuthStatus({ state: 'idle' }) setOAuthStatus({ state: 'idle' })
return return
} }
setOAuthStatus({ setOAuthStatus({
state: 'platform_setup_complete', state: 'platform_setup_complete',
message: result.message, message: result,
}) })
}} }}
/> />

View File

@@ -270,7 +270,7 @@ export function ContextVisualization(t0) {
} else { } else {
t4 = $[53]; 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; $[0] = categories;
$[1] = gridRows; $[1] = gridRows;
$[2] = mcpTools; $[2] = mcpTools;
@@ -304,7 +304,7 @@ export function ContextVisualization(t0) {
} }
let t10; let t10;
if ($[54] !== systemPromptSections) { 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; $[54] = systemPromptSections;
$[55] = t10; $[55] = t10;
} else { } else {
@@ -336,7 +336,7 @@ export function ContextVisualization(t0) {
} }
let t14; let t14;
if ($[62] !== messageBreakdown) { 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; $[62] = messageBreakdown;
$[63] = t14; $[63] = t14;
} else { } else {

View File

@@ -285,7 +285,7 @@ export function Select(t0) {
onChange, onChange,
onCancel, onCancel,
onFocus, onFocus,
defaultFocusValue, focusValue: defaultFocusValue
}; };
$[7] = defaultFocusValue; $[7] = defaultFocusValue;
$[8] = defaultValue; $[8] = defaultValue;

View File

@@ -1,4 +1,5 @@
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { isDeepStrictEqual } from 'util'
import { useRegisterOverlay } from '../../context/overlayContext.js' import { useRegisterOverlay } from '../../context/overlayContext.js'
import type { InputEvent } from '../../ink/events/input-event.js' import type { InputEvent } from '../../ink/events/input-event.js'
// eslint-disable-next-line custom-rules/prefer-use-keybindings -- raw space/arrow multiselect input // eslint-disable-next-line custom-rules/prefer-use-keybindings -- raw space/arrow multiselect input
@@ -8,7 +9,6 @@ import {
normalizeFullWidthSpace, normalizeFullWidthSpace,
} from '../../utils/stringUtils.js' } from '../../utils/stringUtils.js'
import type { OptionWithDescription } from './select.js' import type { OptionWithDescription } from './select.js'
import { optionsNavigateEqual } from './use-select-navigation.js'
import { useSelectNavigation } from './use-select-navigation.js' import { useSelectNavigation } from './use-select-navigation.js'
export type UseMultiSelectStateProps<T> = { export type UseMultiSelectStateProps<T> = {
@@ -174,7 +174,7 @@ export function useMultiSelectState<T>({
// and the deleted ui/useMultiSelectState.ts — without this, MCPServerDesktopImportDialog // and the deleted ui/useMultiSelectState.ts — without this, MCPServerDesktopImportDialog
// keeps colliding servers checked after getAllMcpConfigs() resolves. // keeps colliding servers checked after getAllMcpConfigs() resolves.
const [lastOptions, setLastOptions] = useState(options) const [lastOptions, setLastOptions] = useState(options)
if (options !== lastOptions && !optionsNavigateEqual(options, lastOptions)) { if (options !== lastOptions && !isDeepStrictEqual(options, lastOptions)) {
setSelectedValues(defaultValue) setSelectedValues(defaultValue)
setLastOptions(options) setLastOptions(options)
} }

View File

@@ -6,34 +6,10 @@ import {
useRef, useRef,
useState, useState,
} from 'react' } from 'react'
import { isDeepStrictEqual } from 'util'
import OptionMap from './option-map.js' import OptionMap from './option-map.js'
import type { OptionWithDescription } from './select.js' import type { OptionWithDescription } from './select.js'
/**
* Compare two option arrays for structural equality on properties that
* affect navigation behavior. ReactNode `label` and function `onChange`
* are intentionally excluded — they are identity-unstable (new reference
* each render) but don't change navigation semantics.
*/
export function optionsNavigateEqual<T>(
a: OptionWithDescription<T>[],
b: OptionWithDescription<T>[],
): boolean {
if (a.length !== b.length) return false
for (let i = 0; i < a.length; i++) {
const ao = a[i]!
const bo = b[i]!
if (
ao.value !== bo.value ||
ao.disabled !== bo.disabled ||
ao.type !== bo.type
) {
return false
}
}
return true
}
type State<T> = { type State<T> = {
/** /**
* Map where key is option's value and value is option's index. * Map where key is option's value and value is option's index.
@@ -548,7 +524,7 @@ export function useSelectNavigation<T>({
const [lastOptions, setLastOptions] = useState(options) const [lastOptions, setLastOptions] = useState(options)
if (options !== lastOptions && !optionsNavigateEqual(options, lastOptions)) { if (options !== lastOptions && !isDeepStrictEqual(options, lastOptions)) {
dispatch({ dispatch({
type: 'reset', type: 'reset',
state: createDefaultState({ state: createDefaultState({

View File

@@ -35,11 +35,6 @@ export type UseSelectStateProps<T> = {
*/ */
onFocus?: (value: T) => void onFocus?: (value: T) => void
/**
* Initial value to focus when the component mounts.
*/
defaultFocusValue?: T
/** /**
* Value to focus * Value to focus
*/ */
@@ -136,7 +131,6 @@ export function useSelectState<T>({
onChange, onChange,
onCancel, onCancel,
onFocus, onFocus,
defaultFocusValue,
focusValue, focusValue,
}: UseSelectStateProps<T>): SelectState<T> { }: UseSelectStateProps<T>): SelectState<T> {
const [value, setValue] = useState<T | undefined>(defaultValue) const [value, setValue] = useState<T | undefined>(defaultValue)
@@ -144,7 +138,7 @@ export function useSelectState<T>({
const navigation = useSelectNavigation<T>({ const navigation = useSelectNavigation<T>({
visibleOptionCount, visibleOptionCount,
options, options,
initialFocusValue: defaultFocusValue, initialFocusValue: undefined,
onFocus, onFocus,
focusValue, focusValue,
}) })

View File

@@ -35,7 +35,7 @@ export function DevBar() {
const recentOps = t1; const recentOps = t1;
let t2; let t2;
if ($[3] !== recentOps) { 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; $[3] = recentOps;
$[4] = t2; $[4] = t2;
} else { } else {

View File

@@ -112,9 +112,9 @@ export function HelpV2(t0) {
} }
tabs.push(t6); tabs.push(t6);
if (false && antOnlyCommands.length > 0) { if (false && antOnlyCommands.length > 0) {
let t7; let t7;
if ($[26] !== antOnlyCommands || $[27] !== close || $[28] !== columns || $[29] !== maxHeight) { 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; $[26] = antOnlyCommands;
$[27] = close; $[27] = close;
$[28] = columns; $[28] = columns;

View File

@@ -4,7 +4,7 @@ export function InterruptedByUser() {
const $ = _c(1); const $ = _c(1);
let t0; let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 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; $[0] = t0;
} else { } else {
t0 = $[0]; t0 = $[0];

View File

@@ -1,5 +1,5 @@
import { c as _c } from "react-compiler-runtime"; 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 * as React from 'react';
import { Box, Text, color } from '../../ink.js'; import { Box, Text, color } from '../../ink.js';
import { useTerminalSize } from '../../hooks/useTerminalSize.js'; import { useTerminalSize } from '../../hooks/useTerminalSize.js';
@@ -225,7 +225,7 @@ export function LogoV2() {
let t22; let t22;
if ($[25] === Symbol.for("react.memo_cache_sentinel")) { 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>; 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 />; t21 = false && <GateOverridesWarning />;
t22 = false && <ExperimentEnrollmentNotice />; t22 = false && <ExperimentEnrollmentNotice />;
$[25] = t19; $[25] = t19;
@@ -502,7 +502,7 @@ export function LogoV2() {
let t40; let t40;
if ($[86] === Symbol.for("react.memo_cache_sentinel")) { 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>; 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 />; t39 = false && <GateOverridesWarning />;
t40 = false && <ExperimentEnrollmentNotice />; t40 = false && <ExperimentEnrollmentNotice />;
$[86] = t37; $[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'; const emptyMessage = "external" === 'ant' ? 'Unable to fetch latest claude-cli-internal commits' : 'Check /release-notes for recent updates';
return { 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, lines,
footer: lines.length > 0 ? '/release-notes for more' : undefined, footer: lines.length > 0 ? '/release-notes for more' : undefined,
emptyMessage 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. * issues via /issue. Appears when friction is detected in the conversation.
*/ */
export function IssueFlagBanner() { 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 { isQueuedCommandEditable, popAllEditable } from 'src/utils/messageQueueManager.js';
import stripAnsi from 'strip-ansi'; import stripAnsi from 'strip-ansi';
import { companionReservedColumns } from '../../buddy/CompanionSprite.js'; import { companionReservedColumns } from '../../buddy/CompanionSprite.js';
import { isBuddyEnabled } from '../../buddy/feature.js';
import { findBuddyTriggerPositions, useBuddyNotification } from '../../buddy/useBuddyNotification.js'; import { findBuddyTriggerPositions, useBuddyNotification } from '../../buddy/useBuddyNotification.js';
import { FastModePicker } from '../../commands/fast/fast.js'; import { FastModePicker } from '../../commands/fast/fast.js';
import { isUltrareviewEnabled } from '../../commands/review/ultrareviewEnabled.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 { getFastModeUnavailableReason, isFastModeAvailable, isFastModeCooldown, isFastModeEnabled, isFastModeSupportedByModel } from '../../utils/fastMode.js';
import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js'; import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js';
import type { PromptInputHelpers } from '../../utils/handlePromptSubmit.js'; import type { PromptInputHelpers } from '../../utils/handlePromptSubmit.js';
import { extractDraggedFilePaths } from '../../utils/dragDropPaths.js';
import { getImageFromClipboard, PASTE_THRESHOLD } from '../../utils/imagePaste.js'; import { getImageFromClipboard, PASTE_THRESHOLD } from '../../utils/imagePaste.js';
import type { ImageDimensions } from '../../utils/imageResizer.js'; import type { ImageDimensions } from '../../utils/imageResizer.js';
import { cacheImagePath, storeImage } from '../../utils/imageStore.js'; import { cacheImagePath, storeImage } from '../../utils/imageStore.js';
@@ -252,24 +250,14 @@ function PromptInput({
show: false show: false
}); });
const [cursorOffset, setCursorOffset] = useState<number>(input.length); const [cursorOffset, setCursorOffset] = useState<number>(input.length);
// Track the last input value set via internal handlers so external updates // Track the last input value set via internal handlers so we can detect
// (for example speech-to-text injection) can still move the cursor to end // external input changes (e.g. speech-to-text injection) and move cursor to end.
// without clobbering a pending internal keystroke during render.
const lastInternalInputRef = React.useRef(input); const lastInternalInputRef = React.useRef(input);
const lastPropInputRef = React.useRef(input); if (input !== lastInternalInputRef.current) {
React.useLayoutEffect(() => { // Input changed externally (not through any internal handler) — move cursor to end
if (input === lastPropInputRef.current) { setCursorOffset(input.length);
return;
}
lastPropInputRef.current = input;
if (input === lastInternalInputRef.current) {
return;
}
lastInternalInputRef.current = input; lastInternalInputRef.current = input;
setCursorOffset(prev => prev === input.length ? prev : input.length); }
}, [input]);
// Wrap onInputChange to track internal changes before they trigger re-render // Wrap onInputChange to track internal changes before they trigger re-render
const trackAndSetInput = React.useCallback((value: string) => { const trackAndSetInput = React.useCallback((value: string) => {
lastInternalInputRef.current = value; lastInternalInputRef.current = value;
@@ -305,7 +293,7 @@ function PromptInput({
// the pill returns null for implicit-and-not-reconnecting, so nav must too, // the pill returns null for implicit-and-not-reconnecting, so nav must too,
// otherwise bridge becomes an invisible selection stop. // otherwise bridge becomes an invisible selection stop.
const bridgeFooterVisible = replBridgeConnected && (replBridgeExplicit || replBridgeReconnecting); 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 hasTungstenSession = useAppState(s => "external" === 'ant' && s.tungstenActiveSession !== undefined);
const tmuxFooterVisible = "external" === 'ant' && hasTungstenSession; const tmuxFooterVisible = "external" === 'ant' && hasTungstenSession;
// WebBrowser pill — visible when a browser is open // WebBrowser pill — visible when a browser is open
@@ -321,7 +309,7 @@ function PromptInput({
const { const {
companion: _companion, companion: _companion,
companionMuted companionMuted
} = isBuddyEnabled() ? getGlobalConfig() : { } = feature('BUDDY') ? getGlobalConfig() : {
companion: undefined, companion: undefined,
companionMuted: undefined companionMuted: undefined
}; };
@@ -1215,22 +1203,6 @@ function PromptInput({
// Clean up pasted text - strip ANSI escape codes and normalize line endings and tabs // Clean up pasted text - strip ANSI escape codes and normalize line endings and tabs
let text = stripAnsi(rawText).replace(/\r/g, '\n').replaceAll('\t', ' '); 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. // Match typed/auto-suggest: `!cmd` pasted into empty input enters bash mode.
if (input.length === 0) { if (input.length === 0) {
const pastedMode = getModeFromInput(text); const pastedMode = getModeFromInput(text);
@@ -1272,23 +1244,12 @@ function PromptInput({
if (isNonSpacePrintable(input, key)) return ' ' + input; if (isNonSpacePrintable(input, key)) return ' ' + input;
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) { function insertTextAtCursor(text: string) {
// Use refs for input/cursor so back-to-back calls in the same event // Push current state to buffer before inserting
// (e.g. onImagePaste loop for multiple dragged images) chain correctly pushToBuffer(input, cursorOffset, pastedContents);
// instead of each reading the same stale closure values. const newInput = input.slice(0, cursorOffset) + text + input.slice(cursorOffset);
const currentInput = lastInternalInputRef.current;
const currentOffset = cursorOffsetRef.current;
pushToBuffer(currentInput, currentOffset, pastedContents);
const newInput = currentInput.slice(0, currentOffset) + text + currentInput.slice(currentOffset);
trackAndSetInput(newInput); trackAndSetInput(newInput);
const newOffset = currentOffset + text.length; setCursorOffset(cursorOffset + text.length);
cursorOffsetRef.current = newOffset;
setCursorOffset(newOffset);
} }
const doublePressEscFromEmpty = useDoublePress(() => {}, () => onShowMessageSelector()); const doublePressEscFromEmpty = useDoublePress(() => {}, () => onShowMessageSelector());
@@ -1825,7 +1786,7 @@ function PromptInput({
} }
switch (footerItemSelected) { switch (footerItemSelected) {
case 'companion': case 'companion':
if (isBuddyEnabled()) { if (feature('BUDDY')) {
selectFooterItem(null); selectFooterItem(null);
void onSubmit('/buddy'); void onSubmit('/buddy');
} }
@@ -2020,7 +1981,8 @@ function PromptInput({
}); });
}, [effortNotificationText, addNotification, removeNotification]); }, [effortNotificationText, addNotification, removeNotification]);
useBuddyNotification(); 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; useAppState(s => s.companionReaction !== undefined) : false;
const { const {
columns, columns,
@@ -2191,7 +2153,7 @@ function PromptInput({
}} onCancel={() => setShowHistoryPicker(false)} />; }} 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) { if (modelPickerElement) {
return modelPickerElement; return modelPickerElement;
} }
@@ -2211,7 +2173,7 @@ function PromptInput({
multiline: true, multiline: true,
onSubmit, onSubmit,
onChange, onChange,
value: isSearchingHistory && historyMatch ? getValueFromInput(typeof historyMatch === 'string' ? historyMatch : historyMatch.display) : input, value: historyMatch ? getValueFromInput(typeof historyMatch === 'string' ? historyMatch : historyMatch.display) : input,
// History navigation is handled via TextInput props (onHistoryUp/onHistoryDown), // History navigation is handled via TextInput props (onHistoryUp/onHistoryDown),
// NOT via useKeybindings. This allows useTextInput's upOrHistoryUp/downOrHistoryDown // NOT via useKeybindings. This allows useTextInput's upOrHistoryUp/downOrHistoryDown
// to try cursor movement first and only fall through to history navigation when the // to try cursor movement first and only fall through to history navigation when the

View File

@@ -1,5 +1,5 @@
import { c as _c } from "react-compiler-runtime"; 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 { feature } from 'bun:bundle';
// Dead code elimination: conditional import for COORDINATOR_MODE // Dead code elimination: conditional import for COORDINATOR_MODE
/* eslint-disable @typescript-eslint/no-require-imports */ /* 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 // BackgroundTaskStatus is NOT in parts — it renders as a Box sibling so
// its click-target Box isn't nested inside the <Text wrap="truncate"> // its click-target Box isn't nested inside the <Text wrap="truncate">
// wrapper (reconciler throws on Box-in-Text). // 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!} />] : [])]; ...("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) // 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, maxColumnWidth ?? stringWidth(item.displayText) + 5,
maxNameWidth, maxNameWidth,
) )
const displayTextColor = isSelected ? 'inverseText' : item.color
const shouldDim = !isSelected
let displayText = item.displayText let displayText = item.displayText
if (stringWidth(displayText) > displayTextWidth - 2) { if (stringWidth(displayText) > displayTextWidth - 2) {
@@ -142,17 +144,21 @@ const SuggestionItemRow = memo(function SuggestionItemRow({
const truncatedDescription = item.description const truncatedDescription = item.description
? truncateToWidth(item.description.replace(/\s+/g, ' '), descriptionWidth) ? truncateToWidth(item.description.replace(/\s+/g, ' '), descriptionWidth)
: '' : ''
const lineContent = `${paddedDisplayText}${tagText}${truncatedDescription}`
return ( return (
<Box width="100%" opaque={true} backgroundColor={rowBackgroundColor}> <Box width="100%" opaque={true} backgroundColor={rowBackgroundColor}>
<Text <Text wrap="truncate">
color={textColor} <Text color={displayTextColor} dimColor={shouldDim} bold={isSelected}>
dimColor={!isSelected} {paddedDisplayText}
bold={isSelected} </Text>
wrap="truncate" {tagText ? (
> <Text color={textColor} dimColor={!isSelected}>
{lineContent} {tagText}
</Text>
) : null}
<Text color={textColor} dimColor={!isSelected}>
{truncatedDescription}
</Text>
</Text> </Text>
</Box> </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 { feature } from 'bun:bundle';
import * as React from 'react'; import * as React from 'react';
import { useMemo } 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 { 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 { STATUS_TAG, SUMMARY_TAG, TASK_NOTIFICATION_TAG } from '../../constants/xml.js';
import { QueuedMessageProvider } from '../../context/QueuedMessageContext.js'; import { QueuedMessageProvider } from '../../context/QueuedMessageContext.js';
import { useCommandQueue } from '../../hooks/useCommandQueue.js'; import { useCommandQueue } from '../../hooks/useCommandQueue.js';
import type { QueuedCommand } from '../../types/textInputTypes.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 { createUserMessage, EMPTY_LOOKUPS, normalizeMessages } from '../../utils/messages.js';
import { jsonParse } from '../../utils/slowOperations.js'; import { jsonParse } from '../../utils/slowOperations.js';
import { Message } from '../Message.js'; import { Message } from '../Message.js';
@@ -71,25 +70,17 @@ function processQueuedCommands(queuedCommands: QueuedCommand[]): QueuedCommand[]
} }
function PromptInputQueuedCommandsImpl(): React.ReactNode { function PromptInputQueuedCommandsImpl(): React.ReactNode {
const queuedCommands = useCommandQueue(); 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 // Brief layout: dim queue items + skip the paddingX (brief messages
// already indent themselves). Gate mirrors the brief-spinner/message // already indent themselves). Gate mirrors the brief-spinner/message
// check elsewhere — no teammate-view override needed since this // check elsewhere — no teammate-view override needed since this
// component early-returns when viewing a teammate. // component early-returns when viewing a teammate.
const useBriefLayout = feature('KAIROS') || feature('KAIROS_BRIEF') ? const useBriefLayout = feature('KAIROS') || feature('KAIROS_BRIEF') ?
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant // 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 // createUserMessage mints a fresh UUID per call; without memoization, streaming
// re-renders defeat Message's areMessagePropsEqual (compares uuid) → flicker. // 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(() => { const messages = useMemo(() => {
if (queuedCommands.length === 0) return null; if (queuedCommands.length === 0) return null;
// task-notification is shown via useInboxNotification; most isMeta commands // task-notification is shown via useInboxNotification; most isMeta commands
@@ -117,11 +108,6 @@ function PromptInputQueuedCommandsImpl(): React.ReactNode {
return null; return null;
} }
return <Box marginTop={1} flexDirection="column"> 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}> {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} /> <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>)} </QueuedMessageProvider>)}

View File

@@ -1,437 +0,0 @@
import { PassThrough } from 'node:stream'
import { afterEach, expect, mock, test } from 'bun:test'
import React from 'react'
import stripAnsi from 'strip-ansi'
import { createRoot } from '../ink.js'
import { AppStateProvider } from '../state/AppState.js'
import { KeybindingSetup } from '../keybindings/KeybindingProviderSetup.js'
const SYNC_START = '\x1B[?2026h'
const SYNC_END = '\x1B[?2026l'
const ORIGINAL_ENV = {
CLAUDE_CODE_USE_GITHUB: process.env.CLAUDE_CODE_USE_GITHUB,
GITHUB_TOKEN: process.env.GITHUB_TOKEN,
GH_TOKEN: process.env.GH_TOKEN,
}
function extractLastFrame(output: string): string {
let lastFrame: string | null = null
let cursor = 0
while (cursor < output.length) {
const start = output.indexOf(SYNC_START, cursor)
if (start === -1) {
break
}
const contentStart = start + SYNC_START.length
const end = output.indexOf(SYNC_END, contentStart)
if (end === -1) {
break
}
const frame = output.slice(contentStart, end)
if (frame.trim().length > 0) {
lastFrame = frame
}
cursor = end + SYNC_END.length
}
return lastFrame ?? output
}
function createTestStreams(): {
stdout: PassThrough
stdin: PassThrough & {
isTTY: boolean
setRawMode: (mode: boolean) => void
ref: () => void
unref: () => void
}
getOutput: () => string
} {
let output = ''
const stdout = new PassThrough()
const stdin = new PassThrough() as PassThrough & {
isTTY: boolean
setRawMode: (mode: boolean) => void
ref: () => void
unref: () => void
}
stdin.isTTY = true
stdin.setRawMode = () => {}
stdin.ref = () => {}
stdin.unref = () => {}
;(stdout as unknown as { columns: number }).columns = 120
stdout.on('data', chunk => {
output += chunk.toString()
})
return {
stdout,
stdin,
getOutput: () => output,
}
}
async function waitForCondition(
predicate: () => boolean,
options?: { timeoutMs?: number; intervalMs?: number },
): Promise<void> {
const timeoutMs = options?.timeoutMs ?? 2000
const intervalMs = options?.intervalMs ?? 10
const startedAt = Date.now()
while (Date.now() - startedAt < timeoutMs) {
if (predicate()) {
return
}
await Bun.sleep(intervalMs)
}
throw new Error('Timed out waiting for ProviderManager test condition')
}
function createDeferred<T>(): {
promise: Promise<T>
resolve: (value: T) => void
} {
let resolve!: (value: T) => void
const promise = new Promise<T>(r => {
resolve = r
})
return { promise, resolve }
}
function mockProviderProfilesModule(options?: {
addProviderProfile?: (...args: unknown[]) => unknown
}): void {
mock.module('../utils/providerProfiles.js', () => ({
addProviderProfile: options?.addProviderProfile ?? (() => null),
applyActiveProviderProfileFromConfig: () => {},
deleteProviderProfile: () => ({ removed: false, activeProfileId: null }),
getActiveProviderProfile: () => null,
getProviderPresetDefaults: (preset: string) =>
preset === 'ollama'
? {
provider: 'openai',
name: 'Ollama',
baseUrl: 'http://localhost:11434/v1',
model: 'llama3.1:8b',
apiKey: '',
}
: {
provider: 'openai',
name: 'Mock provider',
baseUrl: 'http://localhost:11434/v1',
model: 'mock-model',
apiKey: '',
},
getProviderProfiles: () => [],
setActiveProviderProfile: () => null,
updateProviderProfile: () => null,
}))
}
function mockProviderManagerDependencies(
syncRead: () => string | undefined,
asyncRead: () => Promise<string | undefined>,
options?: {
addProviderProfile?: (...args: unknown[]) => unknown
hasLocalOllama?: () => Promise<boolean>
listOllamaModels?: () => Promise<
Array<{
name: string
sizeBytes?: number | null
family?: string | null
families?: string[]
parameterSize?: string | null
quantizationLevel?: string | null
}>
>
},
): void {
mockProviderProfilesModule({ addProviderProfile: options?.addProviderProfile })
mock.module('../utils/providerDiscovery.js', () => ({
hasLocalOllama: options?.hasLocalOllama ?? (async () => false),
listOllamaModels: options?.listOllamaModels ?? (async () => []),
}))
mock.module('../utils/githubModelsCredentials.js', () => ({
clearGithubModelsToken: () => ({ success: true }),
GITHUB_MODELS_HYDRATED_ENV_MARKER: 'CLAUDE_CODE_GITHUB_TOKEN_HYDRATED',
hydrateGithubModelsTokenFromSecureStorage: () => {},
readGithubModelsToken: syncRead,
readGithubModelsTokenAsync: asyncRead,
}))
mock.module('../utils/settings/settings.js', () => ({
updateSettingsForSource: () => ({ error: null }),
}))
}
async function waitForFrameOutput(
getOutput: () => string,
predicate: (output: string) => boolean,
timeoutMs = 2500,
): Promise<string> {
let output = ''
await waitForCondition(() => {
output = stripAnsi(extractLastFrame(getOutput()))
return predicate(output)
}, { timeoutMs })
return output
}
async function mountProviderManager(
ProviderManager: React.ComponentType<{
mode: 'first-run' | 'manage'
onDone: (result?: unknown) => void
}>,
options?: {
mode?: 'first-run' | 'manage'
onDone?: (result?: unknown) => void
},
): Promise<{
stdin: PassThrough
getOutput: () => string
dispose: () => Promise<void>
}> {
const { stdout, stdin, getOutput } = createTestStreams()
const root = await createRoot({
stdout: stdout as unknown as NodeJS.WriteStream,
stdin: stdin as unknown as NodeJS.ReadStream,
patchConsole: false,
})
root.render(
<AppStateProvider>
<KeybindingSetup>
<ProviderManager
mode={options?.mode ?? 'manage'}
onDone={options?.onDone ?? (() => {})}
/>
</KeybindingSetup>
</AppStateProvider>,
)
return {
stdin,
getOutput,
dispose: async () => {
root.unmount()
stdin.end()
stdout.end()
await Bun.sleep(0)
},
}
}
async function renderProviderManagerFrame(
ProviderManager: React.ComponentType<{
mode: 'first-run' | 'manage'
onDone: (result?: unknown) => void
}>,
options?: {
waitForOutput?: (output: string) => boolean
timeoutMs?: number
mode?: 'first-run' | 'manage'
},
): Promise<string> {
const mounted = await mountProviderManager(ProviderManager, {
mode: options?.mode,
})
const output = await waitForFrameOutput(
mounted.getOutput,
frame => {
if (!options?.waitForOutput) {
return frame.includes('Provider manager')
}
return options.waitForOutput(frame)
},
options?.timeoutMs ?? 2500,
)
await mounted.dispose()
return output
}
afterEach(() => {
mock.restore()
for (const [key, value] of Object.entries(ORIGINAL_ENV)) {
if (value === undefined) {
delete process.env[key as keyof typeof ORIGINAL_ENV]
} else {
process.env[key as keyof typeof ORIGINAL_ENV] = value
}
}
})
test('ProviderManager resolves GitHub virtual provider from async storage without sync reads in render flow', async () => {
delete process.env.CLAUDE_CODE_USE_GITHUB
delete process.env.GITHUB_TOKEN
delete process.env.GH_TOKEN
const syncRead = mock(() => {
throw new Error('sync credential read should not run in ProviderManager render flow')
})
const asyncRead = mock(async () => 'stored-token')
mockProviderManagerDependencies(syncRead, asyncRead)
const nonce = `${Date.now()}-${Math.random()}`
const { ProviderManager } = await import(`./ProviderManager.js?ts=${nonce}`)
const output = await renderProviderManagerFrame(ProviderManager, {
waitForOutput: frame =>
frame.includes('Provider manager') &&
frame.includes('GitHub Models') &&
frame.includes('token stored'),
})
expect(output).toContain('Provider manager')
expect(output).toContain('GitHub Models')
expect(output).toContain('token stored')
expect(output).not.toContain('No provider profiles configured yet.')
expect(syncRead).not.toHaveBeenCalled()
expect(asyncRead).toHaveBeenCalled()
})
test('ProviderManager first-run Ollama preset auto-detects installed models', async () => {
delete process.env.CLAUDE_CODE_USE_GITHUB
delete process.env.GITHUB_TOKEN
delete process.env.GH_TOKEN
const onDone = mock(() => {})
const addProviderProfile = mock((payload: {
provider: string
name: string
baseUrl: string
model: string
apiKey?: string
}) => ({
id: 'provider_ollama',
provider: payload.provider,
name: payload.name,
baseUrl: payload.baseUrl,
model: payload.model,
apiKey: payload.apiKey,
}))
mockProviderManagerDependencies(
() => undefined,
async () => undefined,
{
addProviderProfile,
hasLocalOllama: async () => true,
listOllamaModels: async () => [
{
name: 'gemma4:31b-cloud',
family: 'gemma',
parameterSize: '31b',
},
{
name: 'kimi-k2.5:cloud',
family: 'kimi',
parameterSize: '2.5b',
},
],
},
)
const nonce = `${Date.now()}-${Math.random()}`
const { ProviderManager } = await import(`./ProviderManager.js?ts=${nonce}`)
const mounted = await mountProviderManager(ProviderManager, {
mode: 'first-run',
onDone,
})
await waitForFrameOutput(
mounted.getOutput,
frame => frame.includes('Set up provider') && frame.includes('Ollama'),
)
mounted.stdin.write('j')
await Bun.sleep(50)
mounted.stdin.write('\r')
const modelFrame = await waitForFrameOutput(
mounted.getOutput,
frame =>
frame.includes('Choose an Ollama model') &&
frame.includes('gemma4:31b-cloud') &&
frame.includes('kimi-k2.5:cloud'),
)
expect(modelFrame).toContain('Choose an Ollama model')
expect(modelFrame).toContain('gemma4:31b-cloud')
await Bun.sleep(25)
mounted.stdin.write('\r')
await waitForCondition(() => onDone.mock.calls.length > 0)
expect(addProviderProfile).toHaveBeenCalled()
expect(addProviderProfile.mock.calls[0]?.[0]).toMatchObject({
name: 'Ollama',
baseUrl: 'http://localhost:11434/v1',
model: 'gemma4:31b-cloud',
})
expect(onDone).toHaveBeenCalledWith(
expect.objectContaining({
action: 'saved',
message: 'Provider configured: Ollama',
}),
)
await mounted.dispose()
})
test('ProviderManager avoids first-frame false negative while stored-token lookup is pending', async () => {
delete process.env.CLAUDE_CODE_USE_GITHUB
delete process.env.GITHUB_TOKEN
delete process.env.GH_TOKEN
const syncRead = mock(() => {
throw new Error('sync credential read should not run in ProviderManager render flow')
})
const deferredStoredToken = createDeferred<string | undefined>()
const asyncRead = mock(async () => deferredStoredToken.promise)
mockProviderManagerDependencies(syncRead, asyncRead)
const nonce = `${Date.now()}-${Math.random()}`
const { ProviderManager } = await import(`./ProviderManager.js?ts=${nonce}`)
const mounted = await mountProviderManager(ProviderManager)
const firstFrame = await waitForFrameOutput(
mounted.getOutput,
frame => frame.includes('Provider manager'),
)
expect(firstFrame).toContain('Checking GitHub Models credentials...')
expect(firstFrame).not.toContain('No provider profiles configured yet.')
deferredStoredToken.resolve('stored-token')
const resolvedFrame = await waitForFrameOutput(
mounted.getOutput,
frame => frame.includes('GitHub Models') && frame.includes('token stored'),
)
expect(resolvedFrame).toContain('GitHub Models')
expect(resolvedFrame).toContain('token stored')
await mounted.dispose()
expect(syncRead).not.toHaveBeenCalled()
expect(asyncRead).toHaveBeenCalled()
})

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
import { c as _c } from "react-compiler-runtime"; 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 { feature } from 'bun:bundle';
import { Box, Text, useTheme, useThemeSetting, useTerminalFocus } from '../../ink.js'; import { Box, Text, useTheme, useThemeSetting, useTerminalFocus } from '../../ink.js';
import type { KeyboardEvent } from '../../ink/events/keyboard-event.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() ? [{ ...(isFastModeEnabled() && isFastModeAvailable() ? [{
id: 'fastMode', id: 'fastMode',
label: `Fast mode (${FAST_MODE_MODEL_DISPLAY} only)`, 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' ? [{ ...("external" === 'ant' ? [{
id: 'speculationEnabled', id: 'speculationEnabled',
label: 'Speculative execution', label: 'Speculative execution',

View File

@@ -1,5 +1,5 @@
import { c as _c } from "react-compiler-runtime"; 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 * as React from 'react';
import { Suspense, useState } from 'react'; import { Suspense, useState } from 'react';
import { useKeybinding } from '../../keybindings/useKeybinding.js'; import { useKeybinding } from '../../keybindings/useKeybinding.js';

View File

@@ -1,5 +1,5 @@
import { c as _c } from "react-compiler-runtime"; 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 { Box, Text } from '../ink.js';
import * as React from 'react'; import * as React from 'react';
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
@@ -258,7 +258,7 @@ function SpinnerWithVerbInner({
const showBtwTip = tipsEnabled && elapsedSnapshot > 30_000 && !getGlobalConfig().btwUseCount; 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; 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; let budgetText: string | null = null;
if (feature('TOKEN_BUDGET')) { if (feature('TOKEN_BUDGET')) {
const budget = getCurrentTurnTokenBudget(); const budget = getCurrentTurnTokenBudget();

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