📝 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.
|
||||
# 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
3
.gitignore
vendored
@@ -32,6 +32,9 @@ coverage/
|
||||
# Bun
|
||||
bun.lockb
|
||||
|
||||
# MCP registry tokens
|
||||
.mcpregistry_*
|
||||
|
||||
# Temporary files
|
||||
tmp/
|
||||
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
|
||||
# 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
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
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
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",
|
||||
"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'],
|
||||
description: 'Configure x402 crypto payments (USDC on Base)',
|
||||
argumentHint: '[setup|status|enable|disable|set-limit|remove]',
|
||||
supportsNonInteractive: true,
|
||||
load: () => import('./x402.js'),
|
||||
} satisfies Command
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user