📝 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:
nirholas
2026-03-31 10:59:36 +00:00
parent c0b205208d
commit d35ea47ba7
22 changed files with 1377 additions and 29 deletions

View File

@@ -3,15 +3,61 @@
# =============================================================================
# Copy this file to .env and fill in the values you need.
# 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 ───────────────────────────────────────────────────────────
# Your Anthropic API key (required for direct API access)
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)
# 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 ──────────────────────────────────────────────────────────
# Override the default model
# ANTHROPIC_MODEL=
@@ -38,16 +84,67 @@ ANTHROPIC_API_KEY=
# Model for sub-agents / teammates
# CLAUDE_CODE_SUBAGENT_MODEL=
# ── Alternative Providers ────────────────────────────────────────────────────
# Use AWS Bedrock instead of direct Anthropic API
# ── AWS Bedrock ──────────────────────────────────────────────────────────────
# Enable Bedrock backend (uses AWS SDK default credentials: IAM, profile, env)
# CLAUDE_CODE_USE_BEDROCK=true
# Custom Bedrock endpoint 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=
# Skip Bedrock auth (for testing without real AWS credentials)
# CLAUDE_CODE_SKIP_BEDROCK_AUTH=false
# Use Google Vertex AI
# ── Google Vertex AI ─────────────────────────────────────────────────────────
# Enable Vertex AI backend
# 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 ─────────────────────────────────────────────────────
# Override shell used for BashTool (default: auto-detected)
# CLAUDE_CODE_SHELL=/bin/bash
@@ -75,7 +172,7 @@ ANTHROPIC_API_KEY=
# NODE_OPTIONS=--max-old-space-size=8192
# ── Features & Modes ────────────────────────────────────────────────────────
# Enable simplified/worker mode
# Enable simplified/worker mode (also set by --bare flag)
# CLAUDE_CODE_SIMPLE=true
# Enable coordinator (multi-agent) mode
@@ -119,10 +216,24 @@ ANTHROPIC_API_KEY=
# Environment kind (e.g. bridge)
# 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=
# 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 ────────────────────────────────────────────────────────────────
# Debug log level (error, warn, info, debug, trace)
# CLAUDE_CODE_DEBUG_LOG_LEVEL=info
@@ -150,7 +261,37 @@ ANTHROPIC_API_KEY=
# Custom SSL certificate
# 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
View File

@@ -32,6 +32,9 @@ coverage/
# Bun
bun.lockb
# MCP registry tokens
.mcpregistry_*
# Temporary files
tmp/
temp/

11
.mcp.json Normal file
View File

@@ -0,0 +1,11 @@
{
"mcpServers": {
"claude-code-explorer": {
"command": "node",
"args": ["mcp-server/dist/index.js"],
"env": {
"CLAUDE_CODE_SRC_ROOT": "./src"
}
}
}
}

View File

@@ -1,31 +1,45 @@
# ─────────────────────────────────────────────────────────────
# Claude Code CLI — Development Container
# Claude Code CLI — Production Container
# ─────────────────────────────────────────────────────────────
# This image provides a ready-to-explore environment for the
# leaked source. It does NOT produce a runnable build (the
# original build tooling was not included in the leak).
# Multi-stage build: builds a production bundle, then copies
# only the output into a minimal runtime image.
#
# 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
# Install OS-level dependencies used at runtime
RUN apk add --no-cache git ripgrep
# Copy manifests first for layer caching
COPY package.json bun.lockb* ./
# Install npm packages
# Install all dependencies (including devDependencies for build)
RUN bun install --frozen-lockfile || bun install
# Copy source
COPY . .
# Typecheck (optional — fails loudly if deps are wrong)
# RUN bun run typecheck
# Build production bundle
RUN bun run build:prod
# Default: drop into a shell for exploration
CMD ["sh"]
# Stage 2: Runtime
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
View 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
View 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
View File

@@ -0,0 +1,4 @@
.mcpregistry_*
src/
tsconfig.json
*.ts.new

View File

@@ -1,10 +1,18 @@
# 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
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
@@ -46,6 +54,30 @@ npm install
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
### Claude Desktop
@@ -108,6 +140,65 @@ Add to `~/.cursor/mcp.json`:
| Variable | Default | Description |
|----------|---------|-------------|
| `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
@@ -162,9 +253,27 @@ Registry name: `io.github.nirholas/claude-code-explorer-mcp`
```bash
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 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
```

View File

@@ -2,7 +2,7 @@
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "io.github.nirholas/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": {
"url": "https://github.com/nirholas/claude-code",
"source": "github",

View File

@@ -4,7 +4,7 @@
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"rootDir": ".",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
@@ -14,7 +14,7 @@
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"include": ["src/**/*", "api/**/*"],
"exclude": ["node_modules", "dist"]
}

138
scripts/build-bundle.ts Normal file
View 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)
})

View 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
View 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
View 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
View 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
View 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
View 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
View 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)
})

View File

@@ -8,5 +8,17 @@
"source": "github",
"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
View 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() {},
}

View File

@@ -6,6 +6,7 @@ const x402 = {
aliases: ['wallet', 'pay'],
description: 'Configure x402 crypto payments (USDC on Base)',
argumentHint: '[setup|status|enable|disable|set-limit|remove]',
supportsNonInteractive: true,
load: () => import('./x402.js'),
} satisfies Command

View File

@@ -174,7 +174,7 @@ function handleRemove(): string {
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()
let value: string