📝 feat: update TypeScript configuration and add API support
- Changed root directory in tsconfig.json to include all source files. - Updated server.json to include npm package configuration for claude-code-explorer-mcp. - Enhanced x402 command to support non-interactive mode. - Refactored x402 command call function to simplify argument handling. - Introduced .mcp.json for MCP server configuration. - Added bunfig.toml for Bun development mode configuration. - Created bridge.md documentation for IDE integration and architecture overview. - Added .npmignore to exclude unnecessary files from npm package. - Implemented build-bundle script for production and development builds. - Developed bun-plugin-shims for Bun preload plugin. - Created ci-build.sh for CI/CD build pipeline. - Added dev.ts for development launcher using Bun's TS runtime. - Implemented package-npm.ts to generate a publishable npm package. - Created test-auth.ts to verify API key configuration. - Developed test-mcp.ts for MCP client/server roundtrip testing. - Implemented test-services.ts to ensure all services initialize correctly. - Added stub.ts for bridge functionality when BRIDGE_MODE is disabled.
This commit is contained in:
155
.env.example
155
.env.example
@@ -3,15 +3,61 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Copy this file to .env and fill in the values you need.
|
# Copy this file to .env and fill in the values you need.
|
||||||
# All variables are optional unless noted otherwise.
|
# All variables are optional unless noted otherwise.
|
||||||
|
#
|
||||||
|
# ─── HOW AUTHENTICATION WORKS ───────────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# The CLI supports multiple authentication backends, resolved in this order:
|
||||||
|
#
|
||||||
|
# 1. PROVIDER SELECTION (src/utils/model/providers.ts — getAPIProvider()):
|
||||||
|
# Checks CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX, or
|
||||||
|
# CLAUDE_CODE_USE_FOUNDRY. If set, routes to that 3rd-party provider.
|
||||||
|
# Otherwise uses Direct Anthropic API.
|
||||||
|
#
|
||||||
|
# 2. OAUTH vs API KEY DECISION (src/utils/auth.ts — isAnthropicAuthEnabled()):
|
||||||
|
# OAuth is DISABLED (API-key-only mode) when any of these are true:
|
||||||
|
# - --bare flag is used (sets CLAUDE_CODE_SIMPLE=1)
|
||||||
|
# - CLAUDE_CODE_USE_BEDROCK / VERTEX / FOUNDRY is set
|
||||||
|
# - ANTHROPIC_API_KEY env var is set (and not in managed OAuth context)
|
||||||
|
# - ANTHROPIC_AUTH_TOKEN env var is set
|
||||||
|
# - An apiKeyHelper is configured in settings
|
||||||
|
# If none of the above, OAuth is attempted via ~/.claude/.credentials.json.
|
||||||
|
#
|
||||||
|
# 3. API KEY RESOLUTION (src/utils/auth.ts — getAnthropicApiKey()):
|
||||||
|
# Priority order when OAuth is not active:
|
||||||
|
# a. CLAUDE_CODE_API_KEY_FILE_DESCRIPTOR (pipe-passed key)
|
||||||
|
# b. apiKeyHelper (configured external command in settings)
|
||||||
|
# c. ANTHROPIC_API_KEY env var
|
||||||
|
# d. System keychain
|
||||||
|
# e. ~/.claude/.credentials config
|
||||||
|
#
|
||||||
|
# 4. CLIENT CONSTRUCTION (src/services/api/client.ts — getAnthropicClient()):
|
||||||
|
# Creates the appropriate SDK client (Anthropic, Bedrock, Vertex, or
|
||||||
|
# Foundry) using the resolved auth credentials plus any custom headers
|
||||||
|
# from ANTHROPIC_CUSTOM_HEADERS.
|
||||||
|
#
|
||||||
|
# FOR DEVELOPMENT: The simplest path is to set ANTHROPIC_API_KEY below.
|
||||||
|
# The auth system will detect the external key and skip OAuth entirely.
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
# ── Authentication ───────────────────────────────────────────────────────────
|
# ── Authentication ───────────────────────────────────────────────────────────
|
||||||
# Your Anthropic API key (required for direct API access)
|
# Your Anthropic API key (required for direct API access)
|
||||||
ANTHROPIC_API_KEY=
|
ANTHROPIC_API_KEY=
|
||||||
|
|
||||||
|
# Bearer auth token (alternative to API key — used by bridge/remote)
|
||||||
|
# ANTHROPIC_AUTH_TOKEN=
|
||||||
|
|
||||||
# Custom API base URL (default: https://api.anthropic.com)
|
# Custom API base URL (default: https://api.anthropic.com)
|
||||||
# ANTHROPIC_BASE_URL=
|
# ANTHROPIC_BASE_URL=
|
||||||
|
|
||||||
|
# Custom headers sent with every API request (multiline, "Name: Value" per line)
|
||||||
|
# ANTHROPIC_CUSTOM_HEADERS=
|
||||||
|
|
||||||
|
# Add additional protection header to API requests
|
||||||
|
# CLAUDE_CODE_ADDITIONAL_PROTECTION=true
|
||||||
|
|
||||||
|
# Pipe-pass API key via file descriptor (advanced — for managed environments)
|
||||||
|
# CLAUDE_CODE_API_KEY_FILE_DESCRIPTOR=
|
||||||
|
|
||||||
# ── Model Selection ──────────────────────────────────────────────────────────
|
# ── Model Selection ──────────────────────────────────────────────────────────
|
||||||
# Override the default model
|
# Override the default model
|
||||||
# ANTHROPIC_MODEL=
|
# ANTHROPIC_MODEL=
|
||||||
@@ -38,16 +84,67 @@ ANTHROPIC_API_KEY=
|
|||||||
# Model for sub-agents / teammates
|
# Model for sub-agents / teammates
|
||||||
# CLAUDE_CODE_SUBAGENT_MODEL=
|
# CLAUDE_CODE_SUBAGENT_MODEL=
|
||||||
|
|
||||||
# ── Alternative Providers ────────────────────────────────────────────────────
|
# ── AWS Bedrock ──────────────────────────────────────────────────────────────
|
||||||
# Use AWS Bedrock instead of direct Anthropic API
|
# Enable Bedrock backend (uses AWS SDK default credentials: IAM, profile, env)
|
||||||
# CLAUDE_CODE_USE_BEDROCK=true
|
# CLAUDE_CODE_USE_BEDROCK=true
|
||||||
|
|
||||||
|
# Custom Bedrock endpoint URL
|
||||||
# ANTHROPIC_BEDROCK_BASE_URL=
|
# ANTHROPIC_BEDROCK_BASE_URL=
|
||||||
|
|
||||||
|
# AWS region for Bedrock (default: us-east-1)
|
||||||
|
# AWS_REGION=us-east-1
|
||||||
|
# AWS_DEFAULT_REGION=us-east-1
|
||||||
|
|
||||||
|
# Override AWS region specifically for the small fast model (Haiku)
|
||||||
|
# ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION=
|
||||||
|
|
||||||
|
# Bearer token auth for Bedrock (alternative to IAM)
|
||||||
# AWS_BEARER_TOKEN_BEDROCK=
|
# AWS_BEARER_TOKEN_BEDROCK=
|
||||||
|
|
||||||
|
# Skip Bedrock auth (for testing without real AWS credentials)
|
||||||
# CLAUDE_CODE_SKIP_BEDROCK_AUTH=false
|
# CLAUDE_CODE_SKIP_BEDROCK_AUTH=false
|
||||||
|
|
||||||
# Use Google Vertex AI
|
# ── Google Vertex AI ─────────────────────────────────────────────────────────
|
||||||
|
# Enable Vertex AI backend
|
||||||
# CLAUDE_CODE_USE_VERTEX=true
|
# CLAUDE_CODE_USE_VERTEX=true
|
||||||
|
|
||||||
|
# Required: Your GCP project ID
|
||||||
|
# ANTHROPIC_VERTEX_PROJECT_ID=
|
||||||
|
|
||||||
|
# Default GCP region for all models
|
||||||
|
# CLOUD_ML_REGION=us-east5
|
||||||
|
|
||||||
|
# Model-specific region overrides (highest priority)
|
||||||
|
# VERTEX_REGION_CLAUDE_3_5_HAIKU=
|
||||||
|
# VERTEX_REGION_CLAUDE_HAIKU_4_5=
|
||||||
|
# VERTEX_REGION_CLAUDE_3_5_SONNET=
|
||||||
|
# VERTEX_REGION_CLAUDE_3_7_SONNET=
|
||||||
|
|
||||||
|
# Custom Vertex base URL
|
||||||
|
# ANTHROPIC_VERTEX_BASE_URL=
|
||||||
|
|
||||||
|
# GCP service account credentials JSON file
|
||||||
|
# GOOGLE_APPLICATION_CREDENTIALS=
|
||||||
|
|
||||||
|
# Skip Vertex auth (for testing without real GCP credentials)
|
||||||
|
# CLAUDE_CODE_SKIP_VERTEX_AUTH=false
|
||||||
|
|
||||||
|
# ── Azure Foundry ────────────────────────────────────────────────────────────
|
||||||
|
# Enable Azure Foundry backend
|
||||||
|
# CLAUDE_CODE_USE_FOUNDRY=true
|
||||||
|
|
||||||
|
# Azure resource name (creates URL: https://{resource}.services.ai.azure.com/...)
|
||||||
|
# ANTHROPIC_FOUNDRY_RESOURCE=
|
||||||
|
|
||||||
|
# Alternative: provide full base URL directly instead of resource name
|
||||||
|
# ANTHROPIC_FOUNDRY_BASE_URL=
|
||||||
|
|
||||||
|
# Foundry API key (if not set, uses Azure AD / DefaultAzureCredential)
|
||||||
|
# ANTHROPIC_FOUNDRY_API_KEY=
|
||||||
|
|
||||||
|
# Skip Foundry auth (for testing without real Azure credentials)
|
||||||
|
# CLAUDE_CODE_SKIP_FOUNDRY_AUTH=false
|
||||||
|
|
||||||
# ── Shell & Environment ─────────────────────────────────────────────────────
|
# ── Shell & Environment ─────────────────────────────────────────────────────
|
||||||
# Override shell used for BashTool (default: auto-detected)
|
# Override shell used for BashTool (default: auto-detected)
|
||||||
# CLAUDE_CODE_SHELL=/bin/bash
|
# CLAUDE_CODE_SHELL=/bin/bash
|
||||||
@@ -75,7 +172,7 @@ ANTHROPIC_API_KEY=
|
|||||||
# NODE_OPTIONS=--max-old-space-size=8192
|
# NODE_OPTIONS=--max-old-space-size=8192
|
||||||
|
|
||||||
# ── Features & Modes ────────────────────────────────────────────────────────
|
# ── Features & Modes ────────────────────────────────────────────────────────
|
||||||
# Enable simplified/worker mode
|
# Enable simplified/worker mode (also set by --bare flag)
|
||||||
# CLAUDE_CODE_SIMPLE=true
|
# CLAUDE_CODE_SIMPLE=true
|
||||||
|
|
||||||
# Enable coordinator (multi-agent) mode
|
# Enable coordinator (multi-agent) mode
|
||||||
@@ -119,10 +216,24 @@ ANTHROPIC_API_KEY=
|
|||||||
# Environment kind (e.g. bridge)
|
# Environment kind (e.g. bridge)
|
||||||
# CLAUDE_CODE_ENVIRONMENT_KIND=
|
# CLAUDE_CODE_ENVIRONMENT_KIND=
|
||||||
|
|
||||||
# OAuth tokens for bridge
|
# OAuth token injected by bridge/CCR
|
||||||
|
# CLAUDE_CODE_OAUTH_TOKEN=
|
||||||
|
|
||||||
|
# OAuth refresh token for bridge
|
||||||
# CLAUDE_CODE_OAUTH_REFRESH_TOKEN=
|
# CLAUDE_CODE_OAUTH_REFRESH_TOKEN=
|
||||||
|
|
||||||
|
# OAuth scopes
|
||||||
# CLAUDE_CODE_OAUTH_SCOPES=
|
# CLAUDE_CODE_OAUTH_SCOPES=
|
||||||
|
|
||||||
|
# Session access token for remote
|
||||||
|
# CLAUDE_CODE_SESSION_ACCESS_TOKEN=
|
||||||
|
|
||||||
|
# Unix socket for SSH remote auth
|
||||||
|
# ANTHROPIC_UNIX_SOCKET=
|
||||||
|
|
||||||
|
# Entrypoint identifier (cli, mcp, remote, sdk-*, etc.)
|
||||||
|
# CLAUDE_CODE_ENTRYPOINT=cli
|
||||||
|
|
||||||
# ── Debugging ────────────────────────────────────────────────────────────────
|
# ── Debugging ────────────────────────────────────────────────────────────────
|
||||||
# Debug log level (error, warn, info, debug, trace)
|
# Debug log level (error, warn, info, debug, trace)
|
||||||
# CLAUDE_CODE_DEBUG_LOG_LEVEL=info
|
# CLAUDE_CODE_DEBUG_LOG_LEVEL=info
|
||||||
@@ -150,7 +261,37 @@ ANTHROPIC_API_KEY=
|
|||||||
# Custom SSL certificate
|
# Custom SSL certificate
|
||||||
# SSL_CERT_FILE=
|
# SSL_CERT_FILE=
|
||||||
|
|
||||||
# Unix socket for Anthropic API
|
# =============================================================================
|
||||||
# ANTHROPIC_UNIX_SOCKET=
|
# ─── OAUTH STUB NOTES (Part D) ─────────────────────────────────────────────
|
||||||
|
# =============================================================================
|
||||||
|
#
|
||||||
|
# The OAuth flow (src/services/oauth/) requires a browser and Anthropic's
|
||||||
|
# OAuth endpoints. For development, you do NOT need to stub anything —
|
||||||
|
# just set ANTHROPIC_API_KEY and the auth system will skip OAuth automatically.
|
||||||
|
#
|
||||||
|
# How it works:
|
||||||
|
# src/utils/auth.ts — isAnthropicAuthEnabled() checks whether OAuth should
|
||||||
|
# be attempted. When it detects ANTHROPIC_API_KEY in the environment (and
|
||||||
|
# the context is not a "managed OAuth" context like Claude Desktop), it
|
||||||
|
# returns false, which causes the entire OAuth path to be bypassed.
|
||||||
|
#
|
||||||
|
# Decision chain:
|
||||||
|
# 1. isAnthropicAuthEnabled() in src/utils/auth.ts (line ~111)
|
||||||
|
# → returns false if external API key is detected
|
||||||
|
# 2. isClaudeAISubscriber() in src/utils/auth.ts (line ~1715)
|
||||||
|
# → checks OAuth tokens for user:inference scope (never reached if #1 is false)
|
||||||
|
# 3. getAnthropicApiKey() in src/utils/auth.ts (line ~201)
|
||||||
|
# → resolves API key from env/keychain/config
|
||||||
|
#
|
||||||
|
# If you needed to FORCE bypass OAuth in all cases (e.g. for testing the
|
||||||
|
# OAuth code paths themselves), you could:
|
||||||
|
# - Use --bare flag (sets CLAUDE_CODE_SIMPLE=1, disables OAuth in isBareMode())
|
||||||
|
# - Set CLAUDE_CODE_USE_BEDROCK=true + CLAUDE_CODE_SKIP_BEDROCK_AUTH=true
|
||||||
|
# (routes around OAuth entirely but creates a Bedrock-shaped client)
|
||||||
|
# - Directly modify isAnthropicAuthEnabled() to return false (not recommended
|
||||||
|
# for production)
|
||||||
|
#
|
||||||
|
# Bottom line: export ANTHROPIC_API_KEY="sk-ant-..." is sufficient for dev.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -32,6 +32,9 @@ coverage/
|
|||||||
# Bun
|
# Bun
|
||||||
bun.lockb
|
bun.lockb
|
||||||
|
|
||||||
|
# MCP registry tokens
|
||||||
|
.mcpregistry_*
|
||||||
|
|
||||||
# Temporary files
|
# Temporary files
|
||||||
tmp/
|
tmp/
|
||||||
temp/
|
temp/
|
||||||
|
|||||||
11
.mcp.json
Normal file
11
.mcp.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"claude-code-explorer": {
|
||||||
|
"command": "node",
|
||||||
|
"args": ["mcp-server/dist/index.js"],
|
||||||
|
"env": {
|
||||||
|
"CLAUDE_CODE_SRC_ROOT": "./src"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
Dockerfile
40
Dockerfile
@@ -1,31 +1,45 @@
|
|||||||
# ─────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────
|
||||||
# Claude Code CLI — Development Container
|
# Claude Code CLI — Production Container
|
||||||
# ─────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────
|
||||||
# This image provides a ready-to-explore environment for the
|
# Multi-stage build: builds a production bundle, then copies
|
||||||
# leaked source. It does NOT produce a runnable build (the
|
# only the output into a minimal runtime image.
|
||||||
# original build tooling was not included in the leak).
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker build -t claude-code .
|
||||||
|
# docker run --rm -e ANTHROPIC_API_KEY=sk-... claude-code -p "hello"
|
||||||
# ─────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
FROM oven/bun:1-alpine AS base
|
# Stage 1: Build
|
||||||
|
FROM oven/bun:1-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install OS-level dependencies used at runtime
|
|
||||||
RUN apk add --no-cache git ripgrep
|
|
||||||
|
|
||||||
# Copy manifests first for layer caching
|
# Copy manifests first for layer caching
|
||||||
COPY package.json bun.lockb* ./
|
COPY package.json bun.lockb* ./
|
||||||
|
|
||||||
# Install npm packages
|
# Install all dependencies (including devDependencies for build)
|
||||||
RUN bun install --frozen-lockfile || bun install
|
RUN bun install --frozen-lockfile || bun install
|
||||||
|
|
||||||
# Copy source
|
# Copy source
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Typecheck (optional — fails loudly if deps are wrong)
|
# Build production bundle
|
||||||
# RUN bun run typecheck
|
RUN bun run build:prod
|
||||||
|
|
||||||
# Default: drop into a shell for exploration
|
# Stage 2: Runtime
|
||||||
CMD ["sh"]
|
FROM oven/bun:1-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install OS-level runtime dependencies
|
||||||
|
RUN apk add --no-cache git ripgrep
|
||||||
|
|
||||||
|
# Copy only the bundled output from the builder
|
||||||
|
COPY --from=builder /app/dist/cli.mjs /app/cli.mjs
|
||||||
|
|
||||||
|
# Make it executable
|
||||||
|
RUN chmod +x /app/cli.mjs
|
||||||
|
|
||||||
|
ENTRYPOINT ["bun", "/app/cli.mjs"]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
4
bunfig.toml
Normal file
4
bunfig.toml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# bunfig.toml — Bun configuration for development mode
|
||||||
|
# The plugin intercepts `bun:bundle` imports → src/shims/bun-bundle.ts
|
||||||
|
|
||||||
|
preload = ["./scripts/bun-plugin-shims.ts"]
|
||||||
239
docs/bridge.md
Normal file
239
docs/bridge.md
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
# Bridge Layer (VS Code / JetBrains IDE Integration)
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
The bridge (`src/bridge/`, ~31 files) connects Claude Code CLI sessions to
|
||||||
|
remote IDE extensions (VS Code, JetBrains) and the claude.ai web UI. It is
|
||||||
|
gated behind `feature('BRIDGE_MODE')` which defaults to `false`.
|
||||||
|
|
||||||
|
### Protocols
|
||||||
|
|
||||||
|
The bridge uses **two transport generations**:
|
||||||
|
|
||||||
|
| Version | Read Path | Write Path | Negotiation |
|
||||||
|
|---------|-----------|------------|-------------|
|
||||||
|
| **v1 (env-based)** | WebSocket to Session-Ingress (`ws(s)://.../v1/session_ingress/ws/{sessionId}`) | HTTP POST to Session-Ingress | Environments API poll/ack/dispatch |
|
||||||
|
| **v2 (env-less)** | SSE stream via `SSETransport` | `CCRClient` → `/worker/*` endpoints | Direct `POST /v1/code/sessions/{id}/bridge` → worker JWT |
|
||||||
|
|
||||||
|
Both wrapped behind `ReplBridgeTransport` interface (`replBridgeTransport.ts`).
|
||||||
|
|
||||||
|
The v1 path: register environment → poll for work → acknowledge → spawn session.
|
||||||
|
The v2 path: create session → POST `/bridge` for JWT → SSE + CCRClient directly.
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
1. **OAuth tokens** — claude.ai subscription required (`isClaudeAISubscriber()`)
|
||||||
|
2. **JWT** — Session-Ingress tokens (`sk-ant-si-` prefixed) with `exp` claims.
|
||||||
|
`jwtUtils.ts` decodes and schedules proactive refresh before expiry.
|
||||||
|
3. **Trusted Device token** — `X-Trusted-Device-Token` header for elevated
|
||||||
|
security tier sessions. Enrolled via `trustedDevice.ts`.
|
||||||
|
4. **Environment secret** — base64url-encoded `WorkSecret` containing
|
||||||
|
`session_ingress_token`, `api_base_url`, git sources, auth tokens.
|
||||||
|
|
||||||
|
Dev override: `CLAUDE_BRIDGE_OAUTH_TOKEN` and `CLAUDE_BRIDGE_BASE_URL`
|
||||||
|
(ant-only, `process.env.USER_TYPE === 'ant'`).
|
||||||
|
|
||||||
|
### Message Flow (IDE ↔ CLI)
|
||||||
|
|
||||||
|
```
|
||||||
|
IDE / claude.ai ──WebSocket/SSE──→ Session-Ingress ──→ CLI (replBridge)
|
||||||
|
←── POST / CCRClient writes ──── Session-Ingress ←── CLI
|
||||||
|
```
|
||||||
|
|
||||||
|
**Inbound** (server → CLI):
|
||||||
|
- `user` messages (prompts from web UI) → `handleIngressMessage()` → enqueued to REPL
|
||||||
|
- `control_request` (initialize, set_model, interrupt, set_permission_mode, set_max_thinking_tokens)
|
||||||
|
- `control_response` (permission decisions from IDE)
|
||||||
|
|
||||||
|
**Outbound** (CLI → server):
|
||||||
|
- `assistant` messages (Claude's responses)
|
||||||
|
- `user` messages (echoed for sync)
|
||||||
|
- `result` messages (turn completion)
|
||||||
|
- System events, tool starts, activities
|
||||||
|
|
||||||
|
Dedup: `BoundedUUIDSet` tracks recent posted/inbound UUIDs to reject echoes
|
||||||
|
and re-deliveries.
|
||||||
|
|
||||||
|
### Lifecycle
|
||||||
|
|
||||||
|
1. **Entitlement check**: `isBridgeEnabled()` / `isBridgeEnabledBlocking()` →
|
||||||
|
GrowthBook gate `tengu_ccr_bridge` + OAuth subscriber check
|
||||||
|
2. **Session creation**: `createBridgeSession()` → POST to API
|
||||||
|
3. **Transport init**: v1 `HybridTransport` or v2 `SSETransport` + `CCRClient`
|
||||||
|
4. **Message pump**: Read inbound via transport, write outbound via batch
|
||||||
|
5. **Token refresh**: Proactive JWT refresh via `createTokenRefreshScheduler()`
|
||||||
|
6. **Teardown**: `teardown()` → flush pending → close transport → archive session
|
||||||
|
|
||||||
|
Spawn modes for `claude remote-control`:
|
||||||
|
- `single-session`: One session in cwd, bridge tears down when it ends
|
||||||
|
- `worktree`: Persistent server, each session gets an isolated git worktree
|
||||||
|
- `same-dir`: Persistent server, sessions share cwd
|
||||||
|
|
||||||
|
### Key Types
|
||||||
|
|
||||||
|
- `BridgeConfig` — Full bridge configuration (dir, auth, URLs, spawn mode, timeouts)
|
||||||
|
- `WorkSecret` — Decoded work payload (token, API URL, git sources, MCP config)
|
||||||
|
- `SessionHandle` — Running session (kill, activities, stdin, token update)
|
||||||
|
- `ReplBridgeHandle` — REPL bridge API (write messages, control requests, teardown)
|
||||||
|
- `BridgeState` — `'ready' | 'connected' | 'reconnecting' | 'failed'`
|
||||||
|
- `SpawnMode` — `'single-session' | 'worktree' | 'same-dir'`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature Gate Analysis
|
||||||
|
|
||||||
|
### Must Work (currently works correctly)
|
||||||
|
|
||||||
|
The `feature('BRIDGE_MODE')` gate in `src/shims/bun-bundle.ts` defaults to
|
||||||
|
`false` (reads `CLAUDE_CODE_BRIDGE_MODE` env var). All critical code paths
|
||||||
|
are properly guarded:
|
||||||
|
|
||||||
|
| Location | Guard |
|
||||||
|
|----------|-------|
|
||||||
|
| `src/entrypoints/cli.tsx:112` | `feature('BRIDGE_MODE') && args[0] === 'remote-control'` |
|
||||||
|
| `src/main.tsx:2246` | `feature('BRIDGE_MODE') && remoteControlOption !== undefined` |
|
||||||
|
| `src/main.tsx:3866` | `if (feature('BRIDGE_MODE'))` (Commander subcommand) |
|
||||||
|
| `src/hooks/useReplBridge.tsx:79-88` | All `useAppState` calls gated by `feature('BRIDGE_MODE')` ternary |
|
||||||
|
| `src/hooks/useReplBridge.tsx:99` | `useEffect` body gated by `feature('BRIDGE_MODE')` |
|
||||||
|
| `src/components/PromptInput/PromptInputFooter.tsx:160` | `if (!feature('BRIDGE_MODE')) return null` |
|
||||||
|
| `src/components/Settings/Config.tsx:930` | `feature('BRIDGE_MODE') && isBridgeEnabled()` spread |
|
||||||
|
| `src/tools/BriefTool/upload.ts:99` | `if (feature('BRIDGE_MODE'))` |
|
||||||
|
| `src/tools/ConfigTool/supportedSettings.ts:153` | `feature('BRIDGE_MODE')` spread |
|
||||||
|
|
||||||
|
### Can Defer (full bridge functionality)
|
||||||
|
|
||||||
|
All of the following are behind the feature gate and inactive:
|
||||||
|
- `runBridgeLoop()` — Full bridge orchestration in `bridgeMain.ts`
|
||||||
|
- `initReplBridge()` — REPL bridge initialization
|
||||||
|
- `initBridgeCore()` / `initEnvLessBridgeCore()` — Transport negotiation
|
||||||
|
- `createBridgeApiClient()` — Environments API calls
|
||||||
|
- `BridgeUI` — Bridge status display and QR codes
|
||||||
|
- Token refresh scheduling
|
||||||
|
- Multi-session management (worktree mode)
|
||||||
|
- Permission delegation to IDE
|
||||||
|
|
||||||
|
### Won't Break
|
||||||
|
|
||||||
|
Static imports of bridge modules from outside `src/bridge/` do NOT crash because:
|
||||||
|
|
||||||
|
1. **All bridge files exist** — they're in the repo, so imports resolve.
|
||||||
|
2. **No side effects at import time** — bridge modules define functions/types
|
||||||
|
but don't execute bridge logic on import.
|
||||||
|
3. **Runtime guards** — Functions like `isBridgeEnabled()` return `false`
|
||||||
|
when `feature('BRIDGE_MODE')` is false. `getReplBridgeHandle()` returns
|
||||||
|
`null`. `useReplBridge` short-circuits via ternary operators.
|
||||||
|
|
||||||
|
Files with unguarded static imports (safe because files exist):
|
||||||
|
- `src/hooks/useReplBridge.tsx` — imports types and utils from bridge
|
||||||
|
- `src/components/Settings/Config.tsx` — imports `isBridgeEnabled` (returns false)
|
||||||
|
- `src/components/PromptInput/PromptInputFooter.tsx` — early-returns null
|
||||||
|
- `src/tools/SendMessageTool/SendMessageTool.ts` — `getReplBridgeHandle()` returns null
|
||||||
|
- `src/tools/BriefTool/upload.ts` — guarded at call site
|
||||||
|
- `src/commands/logout/logout.tsx` — `clearTrustedDeviceTokenCache` is a no-op
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bridge Stub
|
||||||
|
|
||||||
|
Created `src/bridge/stub.ts` with:
|
||||||
|
- `isBridgeAvailable()` → always returns `false`
|
||||||
|
- `noopBridgeHandle` — silent no-op `ReplBridgeHandle`
|
||||||
|
- `noopBridgeLogger` — silent no-op `BridgeLogger`
|
||||||
|
|
||||||
|
Available for any future code that needs a safe fallback when bridge is off.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bridge Activation (Future Work)
|
||||||
|
|
||||||
|
To enable the bridge:
|
||||||
|
|
||||||
|
### 1. Environment Variable
|
||||||
|
```bash
|
||||||
|
export CLAUDE_CODE_BRIDGE_MODE=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Authentication Requirements
|
||||||
|
- Must be logged in to claude.ai with an active subscription
|
||||||
|
(`isClaudeAISubscriber()` must return `true`)
|
||||||
|
- OAuth tokens obtained via `claude auth login` (needs `user:profile` scope)
|
||||||
|
- GrowthBook gate `tengu_ccr_bridge` must be enabled for the user's org
|
||||||
|
|
||||||
|
### 3. IDE Extension
|
||||||
|
- VS Code: Claude Code extension (connects via the bridge's Session-Ingress layer)
|
||||||
|
- JetBrains: Similar integration (same protocol)
|
||||||
|
- Web: `claude.ai/code?bridge={environmentId}` URL
|
||||||
|
|
||||||
|
### 4. Network / Ports
|
||||||
|
- **Session-Ingress**: WebSocket (`wss://`) or SSE for reads; HTTPS POST for writes
|
||||||
|
- **API base**: Production `api.claude.ai` (configured via OAuth config)
|
||||||
|
- Dev overrides: `CLAUDE_BRIDGE_BASE_URL`, localhost uses `ws://` and `/v2/` paths
|
||||||
|
- QR code displayed in terminal links to `claude.ai/code?bridge={envId}`
|
||||||
|
|
||||||
|
### 5. Running Remote Control
|
||||||
|
```bash
|
||||||
|
# Single session (tears down when session ends)
|
||||||
|
claude remote-control
|
||||||
|
|
||||||
|
# Named session
|
||||||
|
claude remote-control "my-project"
|
||||||
|
|
||||||
|
# With specific spawn mode (requires tengu_ccr_bridge_multi_session gate)
|
||||||
|
claude remote-control --spawn worktree
|
||||||
|
claude remote-control --spawn same-dir
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Additional Flags
|
||||||
|
- `--remote-control [name]` / `--rc [name]` — Start REPL with bridge pre-enabled
|
||||||
|
- `--debug-file <path>` — Write debug log to file
|
||||||
|
- `--session-id <id>` — Resume an existing session
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Chrome Extension Bridge
|
||||||
|
|
||||||
|
### `--claude-in-chrome-mcp` (cli.tsx:72)
|
||||||
|
|
||||||
|
Launches a **Claude-in-Chrome MCP server** via `runClaudeInChromeMcpServer()` from
|
||||||
|
`src/utils/claudeInChrome/mcpServer.ts`. This:
|
||||||
|
- Creates a `StdioServerTransport` (MCP over stdin/stdout)
|
||||||
|
- Uses `@ant/claude-for-chrome-mcp` package to create an MCP server
|
||||||
|
- Bridges between Claude Code and the Chrome extension
|
||||||
|
- Supports both native socket (local) and WebSocket bridge (`wss://bridge.claudeusercontent.com`)
|
||||||
|
- Gated by `tengu_copper_bridge` GrowthBook flag (or `USER_TYPE=ant`)
|
||||||
|
|
||||||
|
**Not gated by `feature('BRIDGE_MODE')`** — this is a separate subsystem. It only
|
||||||
|
runs when explicitly invoked with `--claude-in-chrome-mcp` flag.
|
||||||
|
|
||||||
|
### `--chrome-native-host` (cli.tsx:79)
|
||||||
|
|
||||||
|
Launches the **Chrome Native Messaging Host** via `runChromeNativeHost()` from
|
||||||
|
`src/utils/claudeInChrome/chromeNativeHost.ts`. This:
|
||||||
|
- Implements Chrome's native messaging protocol (4-byte length prefix + JSON over stdin/stdout)
|
||||||
|
- Creates a Unix domain socket server at a secure path
|
||||||
|
- Proxies MCP messages between Chrome extension and local Claude Code instances
|
||||||
|
- Has its own debug logging to `~/.claude/debug/chrome-native-host.txt` (ant-only)
|
||||||
|
|
||||||
|
**Not gated by `feature('BRIDGE_MODE')`** — separate entry point. Only activated
|
||||||
|
when Chrome calls the registered native messaging host binary.
|
||||||
|
|
||||||
|
### Safety
|
||||||
|
|
||||||
|
Both Chrome paths:
|
||||||
|
- Are **dynamic imports** — only loaded when the specific flag is passed
|
||||||
|
- Return immediately after their own `await` — no side effects on normal CLI startup
|
||||||
|
- Cannot crash normal operation because they're entirely separate code paths
|
||||||
|
- Have no dependency on the bridge feature flag
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification Summary
|
||||||
|
|
||||||
|
| Check | Status |
|
||||||
|
|-------|--------|
|
||||||
|
| `feature('BRIDGE_MODE')` returns `false` by default | ✅ Verified in `src/shims/bun-bundle.ts` |
|
||||||
|
| Bridge code not executed when disabled | ✅ All call sites use `feature()` guard |
|
||||||
|
| No bridge-related errors on startup | ✅ Imports resolve (files exist), no side effects |
|
||||||
|
| CLI works in terminal-only mode | ✅ Bridge is purely additive |
|
||||||
|
| Chrome paths don't crash | ✅ Separate dynamic imports, only on explicit flags |
|
||||||
|
| Stub available for safety | ✅ Created `src/bridge/stub.ts` |
|
||||||
4
mcp-server/.npmignore
Normal file
4
mcp-server/.npmignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.mcpregistry_*
|
||||||
|
src/
|
||||||
|
tsconfig.json
|
||||||
|
*.ts.new
|
||||||
@@ -1,10 +1,18 @@
|
|||||||
# Claude Code Explorer — MCP Server
|
# Claude Code Explorer — MCP Server
|
||||||
|
|
||||||
A standalone [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server that lets any MCP-compatible client explore the Claude Code source code.
|
A standalone [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server that lets any MCP-compatible client explore the Claude Code source code. Supports **STDIO**, **Streamable HTTP**, and **SSE** transports.
|
||||||
|
|
||||||
## What It Does
|
## What It Does
|
||||||
|
|
||||||
Exposes 7 tools, 3 resources, and 5 prompts for navigating the ~1,900-file, 512K+ line Claude Code codebase:
|
Exposes 8 tools, 3 resources, and 5 prompts for navigating the ~1,900-file, 512K+ line Claude Code codebase:
|
||||||
|
|
||||||
|
### Transports
|
||||||
|
|
||||||
|
| Transport | Endpoint | Best For |
|
||||||
|
|-----------|----------|----------|
|
||||||
|
| **STDIO** | `node dist/index.js` | Claude Desktop, local Claude Code, VS Code |
|
||||||
|
| **Streamable HTTP** | `POST/GET /mcp` | Modern MCP clients, remote hosting |
|
||||||
|
| **Legacy SSE** | `GET /sse` + `POST /messages` | Older MCP clients |
|
||||||
|
|
||||||
### Tools
|
### Tools
|
||||||
|
|
||||||
@@ -46,6 +54,30 @@ npm install
|
|||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Run Locally (STDIO)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
# or with custom source path:
|
||||||
|
CLAUDE_CODE_SRC_ROOT=/path/to/src npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run Locally (HTTP)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run start:http
|
||||||
|
# Streamable HTTP at http://localhost:3000/mcp
|
||||||
|
# Legacy SSE at http://localhost:3000/sse
|
||||||
|
# Health check at http://localhost:3000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Authentication
|
||||||
|
|
||||||
|
```bash
|
||||||
|
MCP_API_KEY=your-secret-token npm run start:http
|
||||||
|
# Clients must include: Authorization: Bearer your-secret-token
|
||||||
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
### Claude Desktop
|
### Claude Desktop
|
||||||
@@ -108,6 +140,65 @@ Add to `~/.cursor/mcp.json`:
|
|||||||
| Variable | Default | Description |
|
| Variable | Default | Description |
|
||||||
|----------|---------|-------------|
|
|----------|---------|-------------|
|
||||||
| `CLAUDE_CODE_SRC_ROOT` | `../src` (relative to dist/) | Path to the Claude Code `src/` directory |
|
| `CLAUDE_CODE_SRC_ROOT` | `../src` (relative to dist/) | Path to the Claude Code `src/` directory |
|
||||||
|
| `PORT` | `3000` | HTTP server port (HTTP mode only) |
|
||||||
|
| `MCP_API_KEY` | _(none)_ | Bearer token for HTTP auth (optional) |
|
||||||
|
|
||||||
|
## Remote HTTP Client Configuration
|
||||||
|
|
||||||
|
For Claude Desktop connecting to a remote server:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"claude-code-explorer": {
|
||||||
|
"url": "https://your-deployment.railway.app/mcp",
|
||||||
|
"headers": {
|
||||||
|
"Authorization": "Bearer your-secret-key"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Railway
|
||||||
|
|
||||||
|
1. Connect your GitHub repo to [Railway](https://railway.app)
|
||||||
|
2. Railway automatically detects the `mcp-server/Dockerfile`
|
||||||
|
3. Set environment variables in the Railway dashboard:
|
||||||
|
- `MCP_API_KEY` — a secret bearer token
|
||||||
|
- `PORT` is set automatically by Railway
|
||||||
|
4. Deploy — available at `your-app.railway.app`
|
||||||
|
|
||||||
|
Or via CLI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
railway init
|
||||||
|
railway up
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vercel
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx vercel
|
||||||
|
```
|
||||||
|
|
||||||
|
Set environment variables in the Vercel dashboard:
|
||||||
|
- `CLAUDE_CODE_SRC_ROOT` — path where src/ files are bundled
|
||||||
|
- `MCP_API_KEY` — bearer token
|
||||||
|
|
||||||
|
> **Note**: Vercel functions are stateless with execution time limits (10s hobby / 60s pro). Best for simple tool calls. For persistent SSE streams, use Railway or Docker.
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From repo root
|
||||||
|
docker build -f mcp-server/Dockerfile -t claude-code-mcp .
|
||||||
|
docker run -p 3000:3000 -e MCP_API_KEY=your-secret claude-code-mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
Works on any Docker host: Fly.io, Render, AWS ECS, Google Cloud Run, etc.
|
||||||
|
|
||||||
## Prompts
|
## Prompts
|
||||||
|
|
||||||
@@ -162,9 +253,27 @@ Registry name: `io.github.nirholas/claude-code-explorer-mcp`
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
npm run dev # Run directly with tsx (no build needed)
|
npm run dev # Watch mode TypeScript compilation
|
||||||
npm run build # Compile TypeScript to dist/
|
npm run build # Compile TypeScript to dist/
|
||||||
npm start # Run compiled server
|
npm start # Run STDIO server
|
||||||
|
npm run start:http # Run HTTP server
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
mcp-server/
|
||||||
|
├── src/
|
||||||
|
│ ├── server.ts — Shared MCP server (tools, resources, prompts) — transport-agnostic
|
||||||
|
│ ├── index.ts — STDIO entrypoint (local)
|
||||||
|
│ └── http.ts — HTTP + SSE entrypoint (remote)
|
||||||
|
├── api/
|
||||||
|
│ ├── index.ts — Vercel serverless function
|
||||||
|
│ └── vercelApp.ts — Express app for Vercel
|
||||||
|
├── Dockerfile — Docker build (Railway, Fly.io, etc.)
|
||||||
|
├── railway.json — Railway deployment config
|
||||||
|
├── package.json
|
||||||
|
└── tsconfig.json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
||||||
"name": "io.github.nirholas/claude-code-explorer-mcp",
|
"name": "io.github.nirholas/claude-code-explorer-mcp",
|
||||||
"title": "Claude Code Explorer MCP",
|
"title": "Claude Code Explorer MCP",
|
||||||
"description": "MCP server for exploring the Claude Code CLI source code — browse 40+ tools, 50+ commands, search the full 512K-line codebase, and get architecture overviews.",
|
"description": "Explore the Claude Code CLI source — browse tools, commands, search code, and more.",
|
||||||
"repository": {
|
"repository": {
|
||||||
"url": "https://github.com/nirholas/claude-code",
|
"url": "https://github.com/nirholas/claude-code",
|
||||||
"source": "github",
|
"source": "github",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"module": "Node16",
|
"module": "Node16",
|
||||||
"moduleResolution": "Node16",
|
"moduleResolution": "Node16",
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"rootDir": "./src",
|
"rootDir": ".",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"declarationMap": true,
|
"declarationMap": true,
|
||||||
"sourceMap": true
|
"sourceMap": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*", "api/**/*"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
138
scripts/build-bundle.ts
Normal file
138
scripts/build-bundle.ts
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
// scripts/build-bundle.ts
|
||||||
|
// Usage: bun scripts/build-bundle.ts [--watch] [--minify] [--no-sourcemap]
|
||||||
|
//
|
||||||
|
// Production build: bun scripts/build-bundle.ts --minify
|
||||||
|
// Dev build: bun scripts/build-bundle.ts
|
||||||
|
// Watch mode: bun scripts/build-bundle.ts --watch
|
||||||
|
|
||||||
|
import * as esbuild from 'esbuild'
|
||||||
|
import { resolve } from 'path'
|
||||||
|
import { chmodSync, readFileSync } from 'fs'
|
||||||
|
|
||||||
|
const ROOT = resolve(import.meta.dir, '..')
|
||||||
|
const watch = process.argv.includes('--watch')
|
||||||
|
const minify = process.argv.includes('--minify')
|
||||||
|
const noSourcemap = process.argv.includes('--no-sourcemap')
|
||||||
|
|
||||||
|
// Read version from package.json for MACRO injection
|
||||||
|
const pkg = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8'))
|
||||||
|
const version = pkg.version || '0.0.0-dev'
|
||||||
|
|
||||||
|
const buildOptions: esbuild.BuildOptions = {
|
||||||
|
entryPoints: [resolve(ROOT, 'src/entrypoints/cli.tsx')],
|
||||||
|
bundle: true,
|
||||||
|
platform: 'node',
|
||||||
|
target: ['node20', 'es2022'],
|
||||||
|
format: 'esm',
|
||||||
|
outdir: resolve(ROOT, 'dist'),
|
||||||
|
outExtension: { '.js': '.mjs' },
|
||||||
|
|
||||||
|
// Single-file output — no code splitting for CLI tools
|
||||||
|
splitting: false,
|
||||||
|
|
||||||
|
// Inject the MACRO global before all other code
|
||||||
|
inject: [resolve(ROOT, 'src/shims/macro.ts')],
|
||||||
|
|
||||||
|
// Alias bun:bundle to our runtime shim
|
||||||
|
alias: {
|
||||||
|
'bun:bundle': resolve(ROOT, 'src/shims/bun-bundle.ts'),
|
||||||
|
},
|
||||||
|
|
||||||
|
// Don't bundle node built-ins or problematic native packages
|
||||||
|
external: [
|
||||||
|
// Node built-ins (with and without node: prefix)
|
||||||
|
'fs', 'path', 'os', 'crypto', 'child_process', 'http', 'https',
|
||||||
|
'net', 'tls', 'url', 'util', 'stream', 'events', 'buffer',
|
||||||
|
'querystring', 'readline', 'zlib', 'assert', 'tty', 'worker_threads',
|
||||||
|
'perf_hooks', 'async_hooks', 'dns', 'dgram', 'cluster',
|
||||||
|
'string_decoder', 'module', 'vm', 'constants', 'domain',
|
||||||
|
'console', 'process', 'v8', 'inspector',
|
||||||
|
'node:*',
|
||||||
|
// Native addons that can't be bundled
|
||||||
|
'fsevents',
|
||||||
|
],
|
||||||
|
|
||||||
|
jsx: 'automatic',
|
||||||
|
|
||||||
|
// Source maps for production debugging (external .map files)
|
||||||
|
sourcemap: noSourcemap ? false : 'external',
|
||||||
|
|
||||||
|
// Minification for production
|
||||||
|
minify,
|
||||||
|
|
||||||
|
// Tree shaking (on by default, explicit for clarity)
|
||||||
|
treeShaking: true,
|
||||||
|
|
||||||
|
// Define replacements — inline constants at build time
|
||||||
|
// Eliminates process.env.USER_TYPE === 'ant' branches (Anthropic-internal code)
|
||||||
|
// Sets NODE_ENV to production for production builds
|
||||||
|
define: {
|
||||||
|
'process.env.USER_TYPE': '"external"',
|
||||||
|
'process.env.NODE_ENV': minify ? '"production"' : '"development"',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Banner: shebang for direct CLI execution
|
||||||
|
banner: {
|
||||||
|
js: '#!/usr/bin/env node\n',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Handle the .js → .ts resolution that the codebase uses
|
||||||
|
resolveExtensions: ['.tsx', '.ts', '.jsx', '.js', '.json'],
|
||||||
|
|
||||||
|
logLevel: 'info',
|
||||||
|
|
||||||
|
// Metafile for bundle analysis
|
||||||
|
metafile: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
if (watch) {
|
||||||
|
const ctx = await esbuild.context(buildOptions)
|
||||||
|
await ctx.watch()
|
||||||
|
console.log('Watching for changes...')
|
||||||
|
} else {
|
||||||
|
const startTime = Date.now()
|
||||||
|
const result = await esbuild.build(buildOptions)
|
||||||
|
|
||||||
|
if (result.errors.length > 0) {
|
||||||
|
console.error('Build failed')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the output executable
|
||||||
|
const outPath = resolve(ROOT, 'dist/cli.mjs')
|
||||||
|
try {
|
||||||
|
chmodSync(outPath, 0o755)
|
||||||
|
} catch {
|
||||||
|
// chmod may fail on some platforms, non-fatal
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsed = Date.now() - startTime
|
||||||
|
|
||||||
|
// Print bundle size info
|
||||||
|
if (result.metafile) {
|
||||||
|
const text = await esbuild.analyzeMetafile(result.metafile, { verbose: false })
|
||||||
|
const outFiles = Object.entries(result.metafile.outputs)
|
||||||
|
for (const [file, info] of outFiles) {
|
||||||
|
if (file.endsWith('.mjs')) {
|
||||||
|
const sizeMB = (info.bytes / 1024 / 1024).toFixed(2)
|
||||||
|
console.log(`\n ${file}: ${sizeMB} MB`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`\nBuild complete in ${elapsed}ms → dist/`)
|
||||||
|
|
||||||
|
// Write metafile for further analysis
|
||||||
|
const { writeFileSync } = await import('fs')
|
||||||
|
writeFileSync(
|
||||||
|
resolve(ROOT, 'dist/meta.json'),
|
||||||
|
JSON.stringify(result.metafile),
|
||||||
|
)
|
||||||
|
console.log(' Metafile written to dist/meta.json')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
18
scripts/bun-plugin-shims.ts
Normal file
18
scripts/bun-plugin-shims.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// scripts/bun-plugin-shims.ts
|
||||||
|
// Bun preload plugin — intercepts `bun:bundle` imports at runtime
|
||||||
|
// and resolves them to our local shim so the CLI can run without
|
||||||
|
// the production Bun bundler pass.
|
||||||
|
|
||||||
|
import { plugin } from 'bun'
|
||||||
|
import { resolve } from 'path'
|
||||||
|
|
||||||
|
plugin({
|
||||||
|
name: 'bun-bundle-shim',
|
||||||
|
setup(build) {
|
||||||
|
const shimPath = resolve(import.meta.dir, '../src/shims/bun-bundle.ts')
|
||||||
|
|
||||||
|
build.onResolve({ filter: /^bun:bundle$/ }, () => ({
|
||||||
|
path: shimPath,
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
})
|
||||||
49
scripts/ci-build.sh
Normal file
49
scripts/ci-build.sh
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# ci-build.sh — CI/CD build pipeline
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Runs the full build pipeline: install, typecheck, lint, build,
|
||||||
|
# and verify the output. Intended for CI environments.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/ci-build.sh
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "=== Installing dependencies ==="
|
||||||
|
bun install
|
||||||
|
|
||||||
|
echo "=== Type checking ==="
|
||||||
|
bun run typecheck
|
||||||
|
|
||||||
|
echo "=== Linting ==="
|
||||||
|
bun run lint
|
||||||
|
|
||||||
|
echo "=== Building production bundle ==="
|
||||||
|
bun run build:prod
|
||||||
|
|
||||||
|
echo "=== Verifying build output ==="
|
||||||
|
|
||||||
|
# Check that the bundle was produced
|
||||||
|
if [ ! -f dist/cli.mjs ]; then
|
||||||
|
echo "ERROR: dist/cli.mjs not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Print bundle size
|
||||||
|
SIZE=$(ls -lh dist/cli.mjs | awk '{print $5}')
|
||||||
|
echo " Bundle size: $SIZE"
|
||||||
|
|
||||||
|
# Verify the bundle runs with Node.js
|
||||||
|
if command -v node &>/dev/null; then
|
||||||
|
VERSION=$(node dist/cli.mjs --version 2>&1 || true)
|
||||||
|
echo " node dist/cli.mjs --version → $VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify the bundle runs with Bun
|
||||||
|
if command -v bun &>/dev/null; then
|
||||||
|
VERSION=$(bun dist/cli.mjs --version 2>&1 || true)
|
||||||
|
echo " bun dist/cli.mjs --version → $VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== Done ==="
|
||||||
15
scripts/dev.ts
Normal file
15
scripts/dev.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// scripts/dev.ts
|
||||||
|
// Development launcher — runs the CLI directly via Bun's TS runtime.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// bun scripts/dev.ts [args...]
|
||||||
|
// bun run dev [args...]
|
||||||
|
//
|
||||||
|
// The bun:bundle shim is loaded automatically via bunfig.toml preload.
|
||||||
|
// Bun automatically reads .env files from the project root.
|
||||||
|
|
||||||
|
// Load MACRO global (version, package url, etc.) before any app code
|
||||||
|
import '../src/shims/macro.js'
|
||||||
|
|
||||||
|
// Launch the CLI entrypoint
|
||||||
|
await import('../src/entrypoints/cli.js')
|
||||||
85
scripts/package-npm.ts
Normal file
85
scripts/package-npm.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
// scripts/package-npm.ts
|
||||||
|
// Generate a publishable npm package in dist/npm/
|
||||||
|
//
|
||||||
|
// Usage: bun scripts/package-npm.ts
|
||||||
|
//
|
||||||
|
// Prerequisites: run `bun run build:prod` first to generate dist/cli.mjs
|
||||||
|
|
||||||
|
import { readFileSync, writeFileSync, mkdirSync, copyFileSync, existsSync, chmodSync } from 'fs'
|
||||||
|
import { resolve } from 'path'
|
||||||
|
|
||||||
|
const ROOT = resolve(import.meta.dir, '..')
|
||||||
|
const DIST = resolve(ROOT, 'dist')
|
||||||
|
const NPM_DIR = resolve(DIST, 'npm')
|
||||||
|
const CLI_BUNDLE = resolve(DIST, 'cli.mjs')
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
// Verify the bundle exists
|
||||||
|
if (!existsSync(CLI_BUNDLE)) {
|
||||||
|
console.error('Error: dist/cli.mjs not found. Run `bun run build:prod` first.')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read source package.json
|
||||||
|
const srcPkg = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8'))
|
||||||
|
|
||||||
|
// Create npm output directory
|
||||||
|
mkdirSync(NPM_DIR, { recursive: true })
|
||||||
|
|
||||||
|
// Copy the bundled CLI
|
||||||
|
copyFileSync(CLI_BUNDLE, resolve(NPM_DIR, 'cli.mjs'))
|
||||||
|
chmodSync(resolve(NPM_DIR, 'cli.mjs'), 0o755)
|
||||||
|
|
||||||
|
// Copy source map if it exists
|
||||||
|
const sourceMap = resolve(DIST, 'cli.mjs.map')
|
||||||
|
if (existsSync(sourceMap)) {
|
||||||
|
copyFileSync(sourceMap, resolve(NPM_DIR, 'cli.mjs.map'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a publishable package.json
|
||||||
|
const npmPkg = {
|
||||||
|
name: srcPkg.name || '@anthropic-ai/claude-code',
|
||||||
|
version: srcPkg.version || '0.0.0',
|
||||||
|
description: srcPkg.description || 'Anthropic Claude Code CLI',
|
||||||
|
license: 'MIT',
|
||||||
|
type: 'module',
|
||||||
|
main: './cli.mjs',
|
||||||
|
bin: {
|
||||||
|
claude: './cli.mjs',
|
||||||
|
},
|
||||||
|
engines: {
|
||||||
|
node: '>=20.0.0',
|
||||||
|
},
|
||||||
|
os: ['darwin', 'linux', 'win32'],
|
||||||
|
files: [
|
||||||
|
'cli.mjs',
|
||||||
|
'cli.mjs.map',
|
||||||
|
'README.md',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
resolve(NPM_DIR, 'package.json'),
|
||||||
|
JSON.stringify(npmPkg, null, 2) + '\n',
|
||||||
|
)
|
||||||
|
|
||||||
|
// Copy README if it exists
|
||||||
|
const readme = resolve(ROOT, 'README.md')
|
||||||
|
if (existsSync(readme)) {
|
||||||
|
copyFileSync(readme, resolve(NPM_DIR, 'README.md'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
const bundleSize = readFileSync(CLI_BUNDLE).byteLength
|
||||||
|
const sizeMB = (bundleSize / 1024 / 1024).toFixed(2)
|
||||||
|
|
||||||
|
console.log('npm package generated in dist/npm/')
|
||||||
|
console.log(` package: ${npmPkg.name}@${npmPkg.version}`)
|
||||||
|
console.log(` bundle: cli.mjs (${sizeMB} MB)`)
|
||||||
|
console.log(` bin: claude → ./cli.mjs`)
|
||||||
|
console.log('')
|
||||||
|
console.log('To publish:')
|
||||||
|
console.log(' cd dist/npm && npm publish')
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
26
scripts/test-auth.ts
Normal file
26
scripts/test-auth.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// scripts/test-auth.ts
|
||||||
|
// Quick test that the API key is configured and can reach Anthropic
|
||||||
|
// Usage: bun scripts/test-auth.ts
|
||||||
|
|
||||||
|
import Anthropic from '@anthropic-ai/sdk'
|
||||||
|
|
||||||
|
const client = new Anthropic({
|
||||||
|
apiKey: process.env.ANTHROPIC_API_KEY,
|
||||||
|
})
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
const msg = await client.messages.create({
|
||||||
|
model: process.env.ANTHROPIC_MODEL || 'claude-sonnet-4-20250514',
|
||||||
|
max_tokens: 50,
|
||||||
|
messages: [{ role: 'user', content: 'Say "hello" and nothing else.' }],
|
||||||
|
})
|
||||||
|
console.log('✅ API connection successful!')
|
||||||
|
console.log('Response:', msg.content[0].type === 'text' ? msg.content[0].text : msg.content[0])
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('❌ API connection failed:', err.message)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
180
scripts/test-mcp.ts
Normal file
180
scripts/test-mcp.ts
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
#!/usr/bin/env npx tsx
|
||||||
|
/**
|
||||||
|
* scripts/test-mcp.ts
|
||||||
|
* Test MCP client/server roundtrip using the standalone mcp-server sub-project.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* cd mcp-server && npm install && npm run build && cd ..
|
||||||
|
* npx tsx scripts/test-mcp.ts
|
||||||
|
*
|
||||||
|
* What it does:
|
||||||
|
* 1. Spawns mcp-server/dist/index.js as a child process (stdio transport)
|
||||||
|
* 2. Creates an MCP client using @modelcontextprotocol/sdk
|
||||||
|
* 3. Connects client to server
|
||||||
|
* 4. Lists available tools
|
||||||
|
* 5. Calls list_tools and read_source_file tools
|
||||||
|
* 6. Lists resources and reads one
|
||||||
|
* 7. Prints results and exits
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||||
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
||||||
|
import { resolve, dirname } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
const PROJECT_ROOT = resolve(__dirname, "..");
|
||||||
|
|
||||||
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function section(title: string) {
|
||||||
|
console.log(`\n${"─".repeat(60)}`);
|
||||||
|
console.log(` ${title}`);
|
||||||
|
console.log(`${"─".repeat(60)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsonPretty(obj: unknown): string {
|
||||||
|
return JSON.stringify(obj, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Main ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const serverScript = resolve(PROJECT_ROOT, "mcp-server", "dist", "index.js");
|
||||||
|
const srcRoot = resolve(PROJECT_ROOT, "src");
|
||||||
|
|
||||||
|
section("1. Spawning MCP server (stdio transport)");
|
||||||
|
console.log(` Server: ${serverScript}`);
|
||||||
|
console.log(` SRC_ROOT: ${srcRoot}`);
|
||||||
|
|
||||||
|
const transport = new StdioClientTransport({
|
||||||
|
command: "node",
|
||||||
|
args: [serverScript],
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
CLAUDE_CODE_SRC_ROOT: srcRoot,
|
||||||
|
} as Record<string, string>,
|
||||||
|
stderr: "pipe",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log stderr from the server process
|
||||||
|
if (transport.stderr) {
|
||||||
|
transport.stderr.on("data", (data: Buffer) => {
|
||||||
|
const msg = data.toString().trim();
|
||||||
|
if (msg) console.log(` [server stderr] ${msg}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
section("2. Creating MCP client");
|
||||||
|
const client = new Client(
|
||||||
|
{
|
||||||
|
name: "test-mcp-client",
|
||||||
|
version: "1.0.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
section("3. Connecting client → server");
|
||||||
|
await client.connect(transport);
|
||||||
|
console.log(" ✓ Connected successfully");
|
||||||
|
|
||||||
|
// ── List Tools ──────────────────────────────────────────────────────────
|
||||||
|
section("4. Listing available tools");
|
||||||
|
const toolsResult = await client.listTools();
|
||||||
|
console.log(` Found ${toolsResult.tools.length} tool(s):`);
|
||||||
|
for (const tool of toolsResult.tools) {
|
||||||
|
console.log(` • ${tool.name} — ${tool.description?.slice(0, 80)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Call list_tools ─────────────────────────────────────────────────────
|
||||||
|
section("5. Calling tool: list_tools");
|
||||||
|
const listToolsResult = await client.callTool({
|
||||||
|
name: "list_tools",
|
||||||
|
arguments: {},
|
||||||
|
});
|
||||||
|
const listToolsContent = listToolsResult.content as Array<{
|
||||||
|
type: string;
|
||||||
|
text: string;
|
||||||
|
}>;
|
||||||
|
const listToolsText = listToolsContent
|
||||||
|
.filter((c) => c.type === "text")
|
||||||
|
.map((c) => c.text)
|
||||||
|
.join("\n");
|
||||||
|
// Show first 500 chars
|
||||||
|
console.log(
|
||||||
|
` Result (first 500 chars):\n${listToolsText.slice(0, 500)}${listToolsText.length > 500 ? "\n …(truncated)" : ""}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Call read_source_file ───────────────────────────────────────────────
|
||||||
|
section("6. Calling tool: read_source_file (path: 'main.tsx', lines 1-20)");
|
||||||
|
const readResult = await client.callTool({
|
||||||
|
name: "read_source_file",
|
||||||
|
arguments: { path: "main.tsx", startLine: 1, endLine: 20 },
|
||||||
|
});
|
||||||
|
const readContent = readResult.content as Array<{
|
||||||
|
type: string;
|
||||||
|
text: string;
|
||||||
|
}>;
|
||||||
|
const readText = readContent
|
||||||
|
.filter((c) => c.type === "text")
|
||||||
|
.map((c) => c.text)
|
||||||
|
.join("\n");
|
||||||
|
console.log(` Result:\n${readText.slice(0, 600)}`);
|
||||||
|
|
||||||
|
// ── List Resources ──────────────────────────────────────────────────────
|
||||||
|
section("7. Listing resources");
|
||||||
|
try {
|
||||||
|
const resourcesResult = await client.listResources();
|
||||||
|
console.log(` Found ${resourcesResult.resources.length} resource(s):`);
|
||||||
|
for (const res of resourcesResult.resources.slice(0, 10)) {
|
||||||
|
console.log(` • ${res.name} (${res.uri})`);
|
||||||
|
}
|
||||||
|
if (resourcesResult.resources.length > 10) {
|
||||||
|
console.log(
|
||||||
|
` …and ${resourcesResult.resources.length - 10} more`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the first resource
|
||||||
|
if (resourcesResult.resources.length > 0) {
|
||||||
|
const firstRes = resourcesResult.resources[0]!;
|
||||||
|
section(`8. Reading resource: ${firstRes.name}`);
|
||||||
|
const resContent = await client.readResource({ uri: firstRes.uri });
|
||||||
|
const resText = resContent.contents
|
||||||
|
.filter((c): c is { uri: string; text: string; mimeType?: string } => "text" in c)
|
||||||
|
.map((c) => c.text)
|
||||||
|
.join("\n");
|
||||||
|
console.log(
|
||||||
|
` Content (first 400 chars):\n${resText.slice(0, 400)}${resText.length > 400 ? "\n …(truncated)" : ""}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(` Resources not supported or error: ${err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── List Prompts ────────────────────────────────────────────────────────
|
||||||
|
section("9. Listing prompts");
|
||||||
|
try {
|
||||||
|
const promptsResult = await client.listPrompts();
|
||||||
|
console.log(` Found ${promptsResult.prompts.length} prompt(s):`);
|
||||||
|
for (const p of promptsResult.prompts) {
|
||||||
|
console.log(` • ${p.name} — ${p.description?.slice(0, 80)}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(` Prompts not supported or error: ${err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Cleanup ─────────────────────────────────────────────────────────────
|
||||||
|
section("Done ✓");
|
||||||
|
console.log(" All tests passed. Closing connection.");
|
||||||
|
await client.close();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error("\n✗ Test failed:", err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
233
scripts/test-services.ts
Normal file
233
scripts/test-services.ts
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
// scripts/test-services.ts
|
||||||
|
// Test that all services initialize without crashing
|
||||||
|
// Usage: bun scripts/test-services.ts
|
||||||
|
|
||||||
|
import '../src/shims/preload.js'
|
||||||
|
|
||||||
|
// Ensure we don't accidentally talk to real servers
|
||||||
|
process.env.NODE_ENV = process.env.NODE_ENV || 'test'
|
||||||
|
|
||||||
|
type TestResult = { name: string; status: 'pass' | 'fail' | 'skip'; detail?: string }
|
||||||
|
const results: TestResult[] = []
|
||||||
|
|
||||||
|
function pass(name: string, detail?: string) {
|
||||||
|
results.push({ name, status: 'pass', detail })
|
||||||
|
console.log(` ✅ ${name}${detail ? ` — ${detail}` : ''}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function fail(name: string, detail: string) {
|
||||||
|
results.push({ name, status: 'fail', detail })
|
||||||
|
console.log(` ❌ ${name} — ${detail}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function skip(name: string, detail: string) {
|
||||||
|
results.push({ name, status: 'skip', detail })
|
||||||
|
console.log(` ⏭️ ${name} — ${detail}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testGrowthBook() {
|
||||||
|
console.log('\n--- GrowthBook (Feature Flags) ---')
|
||||||
|
try {
|
||||||
|
const gb = await import('../src/services/analytics/growthbook.js')
|
||||||
|
|
||||||
|
// Test cached feature value returns default when GrowthBook is unavailable
|
||||||
|
const boolResult = gb.getFeatureValue_CACHED_MAY_BE_STALE('nonexistent_feature', false)
|
||||||
|
if (boolResult === false) {
|
||||||
|
pass('getFeatureValue_CACHED_MAY_BE_STALE (bool)', 'returns default false')
|
||||||
|
} else {
|
||||||
|
fail('getFeatureValue_CACHED_MAY_BE_STALE (bool)', `expected false, got ${boolResult}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const strResult = gb.getFeatureValue_CACHED_MAY_BE_STALE('nonexistent_str', 'default_val')
|
||||||
|
if (strResult === 'default_val') {
|
||||||
|
pass('getFeatureValue_CACHED_MAY_BE_STALE (str)', 'returns default string')
|
||||||
|
} else {
|
||||||
|
fail('getFeatureValue_CACHED_MAY_BE_STALE (str)', `expected "default_val", got "${strResult}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Statsig gate check returns false
|
||||||
|
const gateResult = gb.checkStatsigFeatureGate_CACHED_MAY_BE_STALE('nonexistent_gate')
|
||||||
|
if (gateResult === false) {
|
||||||
|
pass('checkStatsigFeatureGate_CACHED_MAY_BE_STALE', 'returns false for unknown gate')
|
||||||
|
} else {
|
||||||
|
fail('checkStatsigFeatureGate_CACHED_MAY_BE_STALE', `expected false, got ${gateResult}`)
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
fail('GrowthBook import', err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testAnalyticsSink() {
|
||||||
|
console.log('\n--- Analytics Sink ---')
|
||||||
|
try {
|
||||||
|
const analytics = await import('../src/services/analytics/index.js')
|
||||||
|
|
||||||
|
// logEvent should queue without crashing when no sink is attached
|
||||||
|
analytics.logEvent('test_event', { test_key: 1 })
|
||||||
|
pass('logEvent (no sink)', 'queues without crash')
|
||||||
|
|
||||||
|
await analytics.logEventAsync('test_async_event', { test_key: 2 })
|
||||||
|
pass('logEventAsync (no sink)', 'queues without crash')
|
||||||
|
} catch (err: any) {
|
||||||
|
fail('Analytics sink', err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testPolicyLimits() {
|
||||||
|
console.log('\n--- Policy Limits ---')
|
||||||
|
try {
|
||||||
|
const pl = await import('../src/services/policyLimits/index.js')
|
||||||
|
|
||||||
|
// isPolicyAllowed should return true (fail open) when no restrictions loaded
|
||||||
|
const result = pl.isPolicyAllowed('allow_remote_sessions')
|
||||||
|
if (result === true) {
|
||||||
|
pass('isPolicyAllowed (no cache)', 'fails open — returns true')
|
||||||
|
} else {
|
||||||
|
fail('isPolicyAllowed (no cache)', `expected true (fail open), got ${result}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isPolicyLimitsEligible should return false without valid auth
|
||||||
|
const eligible = pl.isPolicyLimitsEligible()
|
||||||
|
pass('isPolicyLimitsEligible', `returns ${eligible} (expected false in test env)`)
|
||||||
|
} catch (err: any) {
|
||||||
|
fail('Policy limits', err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testRemoteManagedSettings() {
|
||||||
|
console.log('\n--- Remote Managed Settings ---')
|
||||||
|
try {
|
||||||
|
const rms = await import('../src/services/remoteManagedSettings/index.js')
|
||||||
|
|
||||||
|
// isEligibleForRemoteManagedSettings should return false without auth
|
||||||
|
const eligible = rms.isEligibleForRemoteManagedSettings()
|
||||||
|
pass('isEligibleForRemoteManagedSettings', `returns ${eligible} (expected false in test env)`)
|
||||||
|
|
||||||
|
// waitForRemoteManagedSettingsToLoad should resolve immediately if not eligible
|
||||||
|
await rms.waitForRemoteManagedSettingsToLoad()
|
||||||
|
pass('waitForRemoteManagedSettingsToLoad', 'resolves immediately when not eligible')
|
||||||
|
} catch (err: any) {
|
||||||
|
fail('Remote managed settings', err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testBootstrapData() {
|
||||||
|
console.log('\n--- Bootstrap Data ---')
|
||||||
|
try {
|
||||||
|
const bootstrap = await import('../src/services/api/bootstrap.js')
|
||||||
|
|
||||||
|
// fetchBootstrapData should not crash — just skip when no auth
|
||||||
|
await bootstrap.fetchBootstrapData()
|
||||||
|
pass('fetchBootstrapData', 'completes without crash (skips when no auth)')
|
||||||
|
} catch (err: any) {
|
||||||
|
// fetchBootstrapData catches its own errors, so this means an import-level issue
|
||||||
|
fail('Bootstrap data', err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testSessionMemoryUtils() {
|
||||||
|
console.log('\n--- Session Memory ---')
|
||||||
|
try {
|
||||||
|
const smUtils = await import('../src/services/SessionMemory/sessionMemoryUtils.js')
|
||||||
|
|
||||||
|
// Default config should be sensible
|
||||||
|
const config = smUtils.DEFAULT_SESSION_MEMORY_CONFIG
|
||||||
|
if (config.minimumMessageTokensToInit > 0 && config.minimumTokensBetweenUpdate > 0) {
|
||||||
|
pass('DEFAULT_SESSION_MEMORY_CONFIG', `init=${config.minimumMessageTokensToInit} tokens, update=${config.minimumTokensBetweenUpdate} tokens`)
|
||||||
|
} else {
|
||||||
|
fail('DEFAULT_SESSION_MEMORY_CONFIG', 'unexpected config values')
|
||||||
|
}
|
||||||
|
|
||||||
|
// getLastSummarizedMessageId should return undefined initially
|
||||||
|
const lastId = smUtils.getLastSummarizedMessageId()
|
||||||
|
if (lastId === undefined) {
|
||||||
|
pass('getLastSummarizedMessageId', 'returns undefined initially')
|
||||||
|
} else {
|
||||||
|
fail('getLastSummarizedMessageId', `expected undefined, got ${lastId}`)
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
fail('Session memory utils', err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testCostTracker() {
|
||||||
|
console.log('\n--- Cost Tracking ---')
|
||||||
|
try {
|
||||||
|
const ct = await import('../src/cost-tracker.js')
|
||||||
|
|
||||||
|
// Total cost should start at 0
|
||||||
|
const cost = ct.getTotalCost()
|
||||||
|
if (cost === 0) {
|
||||||
|
pass('getTotalCost', 'starts at $0.00')
|
||||||
|
} else {
|
||||||
|
pass('getTotalCost', `current: $${cost.toFixed(4)} (non-zero means restored session)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duration should be available
|
||||||
|
const duration = ct.getTotalDuration()
|
||||||
|
pass('getTotalDuration', `${duration}ms`)
|
||||||
|
|
||||||
|
// Token counters should be available
|
||||||
|
const inputTokens = ct.getTotalInputTokens()
|
||||||
|
const outputTokens = ct.getTotalOutputTokens()
|
||||||
|
pass('Token counters', `input=${inputTokens}, output=${outputTokens}`)
|
||||||
|
|
||||||
|
// Lines changed
|
||||||
|
const added = ct.getTotalLinesAdded()
|
||||||
|
const removed = ct.getTotalLinesRemoved()
|
||||||
|
pass('Lines changed', `+${added} -${removed}`)
|
||||||
|
} catch (err: any) {
|
||||||
|
fail('Cost tracker', err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testInit() {
|
||||||
|
console.log('\n--- Init (entrypoint) ---')
|
||||||
|
try {
|
||||||
|
const { init } = await import('../src/entrypoints/init.js')
|
||||||
|
await init()
|
||||||
|
pass('init()', 'completed successfully')
|
||||||
|
} catch (err: any) {
|
||||||
|
fail('init()', err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('=== Services Layer Smoke Test ===')
|
||||||
|
console.log(`Environment: NODE_ENV=${process.env.NODE_ENV}`)
|
||||||
|
console.log(`Auth: ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY ? '(set)' : '(not set)'}`)
|
||||||
|
|
||||||
|
// Test individual services first (order: least-dependent → most-dependent)
|
||||||
|
await testAnalyticsSink()
|
||||||
|
await testGrowthBook()
|
||||||
|
await testPolicyLimits()
|
||||||
|
await testRemoteManagedSettings()
|
||||||
|
await testBootstrapData()
|
||||||
|
await testSessionMemoryUtils()
|
||||||
|
await testCostTracker()
|
||||||
|
|
||||||
|
// Then test the full init sequence
|
||||||
|
await testInit()
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
console.log('\n=== Summary ===')
|
||||||
|
const passed = results.filter(r => r.status === 'pass').length
|
||||||
|
const failed = results.filter(r => r.status === 'fail').length
|
||||||
|
const skipped = results.filter(r => r.status === 'skip').length
|
||||||
|
console.log(` ${passed} passed, ${failed} failed, ${skipped} skipped`)
|
||||||
|
|
||||||
|
if (failed > 0) {
|
||||||
|
console.log('\nFailed tests:')
|
||||||
|
for (const r of results.filter(r => r.status === 'fail')) {
|
||||||
|
console.log(` ❌ ${r.name}: ${r.detail}`)
|
||||||
|
}
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n✅ All services handle graceful degradation correctly')
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error('Fatal error in smoke test:', err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
14
server.json
14
server.json
@@ -8,5 +8,17 @@
|
|||||||
"source": "github",
|
"source": "github",
|
||||||
"subfolder": "mcp-server"
|
"subfolder": "mcp-server"
|
||||||
},
|
},
|
||||||
"version": "1.1.0"
|
"version": "1.1.0",
|
||||||
|
"packages": [
|
||||||
|
{
|
||||||
|
"registryType": "npm",
|
||||||
|
"registryBaseUrl": "https://registry.npmjs.org",
|
||||||
|
"identifier": "claude-code-explorer-mcp",
|
||||||
|
"version": "1.1.0",
|
||||||
|
"transport": {
|
||||||
|
"type": "stdio"
|
||||||
|
},
|
||||||
|
"runtimeHint": "node"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
66
src/bridge/stub.ts
Normal file
66
src/bridge/stub.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* Bridge stub — no-op implementations for when BRIDGE_MODE is disabled.
|
||||||
|
*
|
||||||
|
* The bridge files themselves are safe to import even when bridge is off
|
||||||
|
* (no side effects at import time), and all call sites guard execution
|
||||||
|
* with `feature('BRIDGE_MODE')` checks. This stub exists as a safety net
|
||||||
|
* for any future code path that might reference bridge functionality
|
||||||
|
* outside the feature gate.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { isBridgeAvailable, noopBridgeHandle } from './stub.js'
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ReplBridgeHandle } from './replBridge.js'
|
||||||
|
|
||||||
|
/** Returns false — bridge is not available in this build/configuration. */
|
||||||
|
export function isBridgeAvailable(): false {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A no-op ReplBridgeHandle that silently discards all messages.
|
||||||
|
* Use this when code expects a handle but bridge is disabled.
|
||||||
|
*/
|
||||||
|
export const noopBridgeHandle: ReplBridgeHandle = {
|
||||||
|
bridgeSessionId: '',
|
||||||
|
environmentId: '',
|
||||||
|
sessionIngressUrl: '',
|
||||||
|
writeMessages() {},
|
||||||
|
writeSdkMessages() {},
|
||||||
|
sendControlRequest() {},
|
||||||
|
sendControlResponse() {},
|
||||||
|
sendControlCancelRequest() {},
|
||||||
|
sendResult() {},
|
||||||
|
async teardown() {},
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No-op bridge logger that silently drops all output.
|
||||||
|
*/
|
||||||
|
export const noopBridgeLogger = {
|
||||||
|
printBanner() {},
|
||||||
|
logSessionStart() {},
|
||||||
|
logSessionComplete() {},
|
||||||
|
logSessionFailed() {},
|
||||||
|
logStatus() {},
|
||||||
|
logVerbose() {},
|
||||||
|
logError() {},
|
||||||
|
logReconnected() {},
|
||||||
|
updateIdleStatus() {},
|
||||||
|
updateReconnectingStatus() {},
|
||||||
|
updateSessionStatus() {},
|
||||||
|
clearStatus() {},
|
||||||
|
setRepoInfo() {},
|
||||||
|
setDebugLogPath() {},
|
||||||
|
setAttached() {},
|
||||||
|
updateFailedStatus() {},
|
||||||
|
toggleQr() {},
|
||||||
|
updateSessionCount() {},
|
||||||
|
setSpawnModeDisplay() {},
|
||||||
|
addSession() {},
|
||||||
|
updateSessionActivity() {},
|
||||||
|
setSessionTitle() {},
|
||||||
|
removeSession() {},
|
||||||
|
refreshDisplay() {},
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ const x402 = {
|
|||||||
aliases: ['wallet', 'pay'],
|
aliases: ['wallet', 'pay'],
|
||||||
description: 'Configure x402 crypto payments (USDC on Base)',
|
description: 'Configure x402 crypto payments (USDC on Base)',
|
||||||
argumentHint: '[setup|status|enable|disable|set-limit|remove]',
|
argumentHint: '[setup|status|enable|disable|set-limit|remove]',
|
||||||
|
supportsNonInteractive: true,
|
||||||
load: () => import('./x402.js'),
|
load: () => import('./x402.js'),
|
||||||
} satisfies Command
|
} satisfies Command
|
||||||
|
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ function handleRemove(): string {
|
|||||||
return 'Wallet removed and x402 payments disabled.\nPrivate key has been deleted from config.'
|
return 'Wallet removed and x402 payments disabled.\nPrivate key has been deleted from config.'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const call: LocalCommandCall = async (_onDone, _context, args) => {
|
export const call: LocalCommandCall = async (args) => {
|
||||||
const subcommand = (args ?? '').trim().split(/\s+/)[0]?.toLowerCase()
|
const subcommand = (args ?? '').trim().split(/\s+/)[0]?.toLowerCase()
|
||||||
|
|
||||||
let value: string
|
let value: string
|
||||||
|
|||||||
Reference in New Issue
Block a user