mirror of
https://github.com/777genius/claude-code-source-code.git
synced 2026-07-26 22:42:19 +00:00
Initial source import
This commit is contained in:
commit
67ef753411
2172 changed files with 569095 additions and 0 deletions
32
.dockerignore
Normal file
32
.dockerignore
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Dependencies (rebuilt in container)
|
||||
node_modules
|
||||
|
||||
# Git metadata
|
||||
.git
|
||||
.github
|
||||
.gitignore
|
||||
|
||||
# Build output (rebuilt in container)
|
||||
dist
|
||||
|
||||
# Env files — never bake secrets into the image
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# Logs and debug
|
||||
*.log
|
||||
npm-debug.log*
|
||||
bun-debug.log*
|
||||
|
||||
# Test artifacts
|
||||
coverage
|
||||
.nyc_output
|
||||
|
||||
# Editor / OS noise
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
# Docker context itself
|
||||
docker
|
||||
297
.env.example
Normal file
297
.env.example
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
# =============================================================================
|
||||
# Claude Code CLI — Environment Variables
|
||||
# =============================================================================
|
||||
# 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=
|
||||
|
||||
# Override the small/fast model used internally
|
||||
# ANTHROPIC_SMALL_FAST_MODEL=
|
||||
|
||||
# Custom model overrides (name + description are optional display labels)
|
||||
# ANTHROPIC_DEFAULT_OPUS_MODEL=
|
||||
# ANTHROPIC_DEFAULT_OPUS_MODEL_NAME=
|
||||
# ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION=
|
||||
# ANTHROPIC_DEFAULT_SONNET_MODEL=
|
||||
# ANTHROPIC_DEFAULT_SONNET_MODEL_NAME=
|
||||
# ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION=
|
||||
# ANTHROPIC_DEFAULT_HAIKU_MODEL=
|
||||
# ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME=
|
||||
# ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION=
|
||||
|
||||
# Custom model option (shown alongside built-in models)
|
||||
# ANTHROPIC_CUSTOM_MODEL_OPTION=
|
||||
# ANTHROPIC_CUSTOM_MODEL_OPTION_NAME=
|
||||
# ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION=
|
||||
|
||||
# Model for sub-agents / teammates
|
||||
# CLAUDE_CODE_SUBAGENT_MODEL=
|
||||
|
||||
# ── 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
|
||||
|
||||
# ── 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
|
||||
|
||||
# Prefix prepended to every shell command
|
||||
# CLAUDE_CODE_SHELL_PREFIX=
|
||||
|
||||
# Override temp directory
|
||||
# CLAUDE_CODE_TMPDIR=
|
||||
|
||||
# ── Performance & Limits ─────────────────────────────────────────────────────
|
||||
# Maximum output tokens per response
|
||||
# CLAUDE_CODE_MAX_OUTPUT_TOKENS=
|
||||
|
||||
# Maximum tokens when reading files
|
||||
# CLAUDE_CODE_FILE_READ_MAX_OUTPUT_TOKENS=
|
||||
|
||||
# Idle timeout (minutes) before session ends (default: 75)
|
||||
# CLAUDE_CODE_IDLE_THRESHOLD_MINUTES=75
|
||||
|
||||
# Idle token threshold (default: 100000)
|
||||
# CLAUDE_CODE_IDLE_TOKEN_THRESHOLD=100000
|
||||
|
||||
# Node.js options (e.g. heap size)
|
||||
# NODE_OPTIONS=--max-old-space-size=8192
|
||||
|
||||
# ── Features & Modes ────────────────────────────────────────────────────────
|
||||
# Enable simplified/worker mode (also set by --bare flag)
|
||||
# CLAUDE_CODE_SIMPLE=true
|
||||
|
||||
# Enable coordinator (multi-agent) mode
|
||||
# CLAUDE_CODE_COORDINATOR_MODE=true
|
||||
|
||||
# Enable proactive mode
|
||||
# CLAUDE_CODE_PROACTIVE=true
|
||||
|
||||
# Enable experimental agent teams
|
||||
# CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=true
|
||||
|
||||
# Enable task management
|
||||
# CLAUDE_CODE_ENABLE_TASKS=true
|
||||
|
||||
# Enable prompt suggestions
|
||||
# CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=true
|
||||
|
||||
# ── Accessibility & UX ──────────────────────────────────────────────────────
|
||||
# Enable accessibility mode
|
||||
# CLAUDE_CODE_ACCESSIBILITY=true
|
||||
|
||||
# Disable terminal title updates
|
||||
# CLAUDE_CODE_DISABLE_TERMINAL_TITLE=true
|
||||
|
||||
# Disable virtual scrolling
|
||||
# CLAUDE_CODE_DISABLE_VIRTUAL_SCROLL=true
|
||||
|
||||
# Streamlined output (less verbose)
|
||||
# CLAUDE_CODE_STREAMLINED_OUTPUT=true
|
||||
|
||||
# Disable syntax highlighting
|
||||
# CLAUDE_CODE_SYNTAX_HIGHLIGHT=false
|
||||
|
||||
# ── Remote / Bridge ─────────────────────────────────────────────────────────
|
||||
# Enable remote execution mode
|
||||
# CLAUDE_CODE_REMOTE=true
|
||||
|
||||
# Remote session ID
|
||||
# CLAUDE_CODE_REMOTE_SESSION_ID=
|
||||
|
||||
# Environment kind (e.g. bridge)
|
||||
# CLAUDE_CODE_ENVIRONMENT_KIND=
|
||||
|
||||
# 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
|
||||
|
||||
# Debug logs directory
|
||||
# CLAUDE_CODE_DEBUG_LOGS_DIR=
|
||||
|
||||
# Enable startup profiling
|
||||
# CLAUDE_CODE_PROFILE_STARTUP=true
|
||||
|
||||
# General debug flag
|
||||
# DEBUG=true
|
||||
|
||||
# ── Privacy & Telemetry ─────────────────────────────────────────────────────
|
||||
# Disable telemetry
|
||||
# DISABLE_TELEMETRY=true
|
||||
|
||||
# Disable non-essential network traffic
|
||||
# CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=true
|
||||
|
||||
# ── Proxy & Network ─────────────────────────────────────────────────────────
|
||||
# HTTPS proxy
|
||||
# HTTPS_PROXY=
|
||||
|
||||
# Custom SSL certificate
|
||||
# SSL_CERT_FILE=
|
||||
|
||||
# =============================================================================
|
||||
# ─── 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.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
134
.github/agents/agent.agent.md
vendored
Normal file
134
.github/agents/agent.agent.md
vendored
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
---
|
||||
description: "Use when: developing, debugging, refactoring, reviewing, exploring, or explaining code in the Claude Code CLI codebase. Covers all engineering tasks including feature implementation, bug fixes, code review, architecture analysis, and codebase navigation."
|
||||
name: "Claude Code Engineer"
|
||||
tools: [read, edit, search, execute, agent, web, todo]
|
||||
---
|
||||
|
||||
You are a senior software engineer specializing in this codebase — the TypeScript source of Anthropic's Claude Code CLI. You have deep knowledge of its architecture, conventions, and patterns.
|
||||
|
||||
## Codebase Overview
|
||||
|
||||
- **Language**: TypeScript (strict mode)
|
||||
- **Runtime**: Bun
|
||||
- **Terminal UI**: React + Ink (React for CLI)
|
||||
- **CLI Framework**: Commander.js
|
||||
- **Validation**: Zod (`zod/v4`)
|
||||
- **Module Format**: ESM with `.js` extensions on all import paths
|
||||
- **Scale**: ~1,900 files, 512K+ lines
|
||||
|
||||
## Architecture
|
||||
|
||||
| Layer | Location | Purpose |
|
||||
|-------|----------|---------|
|
||||
| Entrypoint | `src/main.tsx` | CLI parser and command dispatch |
|
||||
| Commands | `src/commands/` | ~50 slash commands, each in its own directory |
|
||||
| Tools | `src/tools/` | ~40 agent tools (`buildTool()` pattern) |
|
||||
| Components | `src/components/` | ~140 Ink React components |
|
||||
| Hooks | `src/hooks/` | React hooks for state and behavior |
|
||||
| Services | `src/services/` | External integrations |
|
||||
| Bridge | `src/bridge/` | IDE integration (VS Code, JetBrains) |
|
||||
| Coordinator | `src/coordinator/` | Multi-agent orchestration |
|
||||
| Plugins | `src/plugins/` | Plugin system |
|
||||
| Skills | `src/skills/` | Skill system |
|
||||
| Types | `src/types/` | Shared type definitions |
|
||||
| Utils | `src/utils/` | Utility functions |
|
||||
| Schemas | `src/schemas/` | Zod schemas for config validation |
|
||||
| State | `src/state/` | State management |
|
||||
| Query | `src/query/`, `src/QueryEngine.ts` | LLM query pipeline and API caller |
|
||||
| Context | `src/context/`, `src/context.ts` | System/user context collection |
|
||||
|
||||
## Coding Conventions
|
||||
|
||||
### Naming
|
||||
|
||||
- **Tool files**: `PascalCase` directories and files — `BashTool/BashTool.ts`
|
||||
- **Components**: `PascalCase.tsx` — `Spinner.tsx`, `MessageResponse.tsx`
|
||||
- **Utilities**: `camelCase.ts` — `claudemd.ts`, `gitSettings.ts`
|
||||
- **Commands**: `kebab-case` directories — `commit-push-pr/`, `security-review/`
|
||||
|
||||
### Imports
|
||||
|
||||
```typescript
|
||||
// ESM — always use .js extension, even for .ts/.tsx source files
|
||||
import { Item } from './file.js'
|
||||
import type { TypeName } from './types.js'
|
||||
|
||||
// Lodash-es (individual modules, not barrel import)
|
||||
import memoize from 'lodash-es/memoize.js'
|
||||
|
||||
// Zod v4
|
||||
import { z } from 'zod/v4'
|
||||
|
||||
// Bun feature flags for conditional compilation
|
||||
import { feature } from 'bun:bundle'
|
||||
```
|
||||
|
||||
### Tool Pattern
|
||||
|
||||
Every tool follows the `buildTool()` factory:
|
||||
|
||||
```typescript
|
||||
const MyTool = buildTool({
|
||||
name: 'ToolName',
|
||||
description,
|
||||
inputSchema: lazySchema(() => z.object({ /* ... */ })),
|
||||
outputSchema: lazySchema(() => z.object({ /* ... */ })),
|
||||
async execute(input, context) { /* ... */ },
|
||||
async checkPermissions(input, context) { /* ... */ },
|
||||
getPath?(input) { /* ... */ },
|
||||
isReadOnly() { /* ... */ },
|
||||
isConcurrencySafe() { /* ... */ },
|
||||
})
|
||||
```
|
||||
|
||||
### Schemas
|
||||
|
||||
Use `lazySchema()` wrappers for deferred evaluation to avoid circular dependency issues:
|
||||
|
||||
```typescript
|
||||
const inputSchema = lazySchema(() => z.strictObject({
|
||||
path: z.string(),
|
||||
content: z.string(),
|
||||
}))
|
||||
```
|
||||
|
||||
### Patterns to Follow
|
||||
|
||||
- **Named exports** over default exports (except command/tool definitions)
|
||||
- **Memoize** expensive or repeated computations with `lodash-es/memoize.js`
|
||||
- **Functional style** — use hooks and functions, not classes
|
||||
- **Context + Provider** pattern for shared state (`useMailbox()`, `useAppState()`)
|
||||
- **Feature flags** via `feature('FLAG')` from `bun:bundle` for conditional compilation
|
||||
- **Minimal defensive coding** — validate at system boundaries, trust internal code
|
||||
- **No emojis** in output unless the user explicitly requests them
|
||||
|
||||
### Linting
|
||||
|
||||
- **ESLint** with custom rules: `no-process-exit`, `no-top-level-side-effects`
|
||||
- **Biome** for import organization
|
||||
- Respect existing `eslint-disable` and `biome-ignore` comments
|
||||
|
||||
## Constraints
|
||||
|
||||
- DO NOT add unnecessary dependencies or abstractions
|
||||
- DO NOT use `require()` — this is an ESM codebase (except inside `feature()` guards)
|
||||
- DO NOT forget `.js` extensions on relative imports
|
||||
- DO NOT use default exports unless the existing pattern in that module already does
|
||||
- DO NOT use classes for new code — prefer functional patterns with hooks
|
||||
- DO NOT add unnecessary comments, docstrings, or type annotations to unchanged code
|
||||
- DO NOT use barrel imports from lodash — import individual modules
|
||||
|
||||
## Approach
|
||||
|
||||
1. **Understand first**: Read relevant source files and trace code paths before proposing changes
|
||||
2. **Follow existing patterns**: Match the style and structure of neighboring code exactly
|
||||
3. **Minimal changes**: Only modify what is necessary — no drive-by refactors
|
||||
4. **Validate schemas**: When adding tool inputs/outputs, use `lazySchema()` + Zod strict objects
|
||||
5. **Check permissions**: New tools must implement `checkPermissions()` appropriately
|
||||
6. **Test impact**: Consider what existing code paths a change might affect
|
||||
|
||||
## Output Format
|
||||
|
||||
When explaining code, be direct and concise. Reference specific files and line numbers. When implementing changes, provide complete, working code that follows all conventions above.
|
||||
|
||||
|
||||
75
.github/copilot-instructions.md
vendored
Normal file
75
.github/copilot-instructions.md
vendored
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# Claude Code CLI — Agent Instructions
|
||||
|
||||
## What This Is
|
||||
|
||||
This is the **leaked source code** of Anthropic's Claude Code CLI (leaked 2026-03-31). It is a read-only reference codebase — there is no build system, no tests, and no package.json included. Treat it as a study/exploration resource.
|
||||
|
||||
- **Language**: TypeScript / TSX
|
||||
- **Runtime**: Bun (not Node.js)
|
||||
- **Terminal UI**: React + Ink (React for CLI)
|
||||
- **Scale**: ~1,900 files, 512,000+ lines across `src/`
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Pipeline
|
||||
|
||||
1. **Entrypoint** — `src/main.tsx`: Commander.js CLI parser, service initialization, lazy feature loading via Bun feature flags
|
||||
2. **Query Engine** — `src/QueryEngine.ts` (~46K lines): LLM API call handler — streaming, tool loops, thinking mode, retries, token counting
|
||||
3. **Tool System** — `src/Tool.ts` (~29K lines) + `src/tools/`: ~50 tools (BashTool, FileEditTool, AgentTool, etc.), each self-contained with input schema, permissions, and execution logic
|
||||
4. **Command System** — `src/commands.ts` (~25K lines) + `src/commands/`: ~50 slash commands (`/commit`, `/review`, `/config`, etc.)
|
||||
5. **Context** — `src/context.ts`: Collects OS, shell, git, and user context for prompt construction
|
||||
|
||||
### Key Subsystems
|
||||
|
||||
| Directory | Purpose |
|
||||
|-----------|---------|
|
||||
| `src/bridge/` | IDE integration layer (VS Code, JetBrains) — bidirectional messaging, JWT auth, session management |
|
||||
| `src/coordinator/` | Multi-agent orchestration |
|
||||
| `src/services/` | External integrations — Anthropic API, MCP, OAuth, LSP, analytics, plugins |
|
||||
| `src/hooks/` | React hooks including `toolPermission/` for per-tool permission checks |
|
||||
| `src/components/` | ~140 Ink UI components |
|
||||
| `src/plugins/` | Plugin system |
|
||||
| `src/skills/` | Skill system |
|
||||
| `src/tasks/` | Task management |
|
||||
| `src/types/` | Centralized TypeScript type definitions |
|
||||
| `src/utils/` | Shared utilities |
|
||||
| `src/schemas/` | Zod-based config schemas |
|
||||
| `src/memdir/` | Persistent memory directory |
|
||||
| `src/voice/` | Voice input |
|
||||
| `src/vim/` | Vim mode |
|
||||
| `src/buddy/` | Companion sprite (Easter egg) |
|
||||
|
||||
### Feature Flags (Dead Code Elimination)
|
||||
|
||||
Bun strips inactive code at build time via `import { feature } from 'bun:bundle'`. Notable flags: `PROACTIVE`, `KAIROS`, `BRIDGE_MODE`, `DAEMON`, `VOICE_MODE`, `AGENT_TRIGGERS`, `MONITOR_TOOL`, `COORDINATOR_MODE`.
|
||||
|
||||
## Code Style & Conventions
|
||||
|
||||
- **Imports**: ESM with explicit `.js` extensions (e.g., `from './commands.js'`)
|
||||
- **Files/Dirs**: kebab-case (`commit-push-pr.ts`, `add-dir/`)
|
||||
- **Classes/Components**: PascalCase (`BashTool`, `QueryEngine`)
|
||||
- **Functions**: camelCase (`getCommands()`, `getAllTools()`)
|
||||
- **Constants**: UPPER_SNAKE_CASE (`ALLOWED_TOOLS`, `FRAME_INTERVAL_MS`)
|
||||
- **Linter**: Biome (not ESLint). Look for `biome-ignore` directives
|
||||
- **Ant-only code**: Some imports/features are gated behind `process.env.USER_TYPE === 'ant'` (Anthropic internal)
|
||||
- **Index pattern**: Features export via `index.ts` files
|
||||
|
||||
## Navigation Tips
|
||||
|
||||
- **Find a tool**: Look in `src/tools/<ToolName>/` — each tool is a directory with its implementation, UI, and permissions co-located
|
||||
- **Find a command**: Look in `src/commands/<command-name>/` or `src/commands/<command-name>.ts`
|
||||
- **Understand permissions**: Start at `src/hooks/toolPermission/`
|
||||
- **Trace an API call**: Start at `src/QueryEngine.ts` → `src/services/api/`
|
||||
- **Understand types**: Centralized in `src/types/`, tool-specific types in `src/Tool.ts`
|
||||
- **Follow the bridge**: IDE integration starts at `src/bridge/bridgeMain.ts`
|
||||
- **MCP integration**: `src/services/mcp/`
|
||||
|
||||
## Terminal Management
|
||||
|
||||
- **Always use background terminals** (`isBackground: true`) for every command so a terminal ID is returned
|
||||
- **Always kill the terminal** after the command completes, whether it succeeds or fails — never leave terminals open
|
||||
- Do not reuse foreground shell sessions — stale sessions block future terminal operations in Codespaces
|
||||
- In GitHub Codespaces, agent-spawned terminals may be hidden — they still work. Do not assume a terminal is broken if you cannot see it
|
||||
- If a terminal appears unresponsive, kill it and create a new one rather than retrying in the same terminal
|
||||
|
||||
|
||||
13
.github/workflows/ci.yml
vendored
Normal file
13
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
name: CI
|
||||
|
||||
# Отключено для автозапуска на push/PR, чтобы не падало по Typecheck & Lint
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Typecheck & Lint (disabled)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: CI temporarily disabled
|
||||
run: echo "CI disabled for push/pull_request events."
|
||||
48
.github/workflows/publish-mcp.yml
vendored
Normal file
48
.github/workflows/publish-mcp.yml
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
name: Publish to MCP Registry
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ["v*"]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish MCP Server
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
defaults:
|
||||
run:
|
||||
working-directory: mcp-server
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Publish to npm
|
||||
run: npm publish --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Install MCP Publisher CLI
|
||||
run: |
|
||||
curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
|
||||
working-directory: ${{ github.workspace }}
|
||||
|
||||
- name: Login to MCP Registry
|
||||
run: ../mcp-publisher login github-oidc
|
||||
working-directory: ${{ github.workspace }}/mcp-server
|
||||
|
||||
- name: Publish to MCP Registry
|
||||
run: ../mcp-publisher publish
|
||||
working-directory: ${{ github.workspace }}/mcp-server
|
||||
45
.gitignore
vendored
Normal file
45
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
out/
|
||||
build/
|
||||
*.tsbuildinfo
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Prompts
|
||||
prompts/
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Editor / IDE
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.vscode/settings.json
|
||||
.vscode/launch.json
|
||||
|
||||
# Debug & logs
|
||||
*.log
|
||||
debug-logs/
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
.vscode/mcp.json
vendored
Normal file
14
.vscode/mcp.json
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"servers": {
|
||||
"claude-code-explorer": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["tsx", "${workspaceFolder}/mcp-server/src/index.ts"],
|
||||
"env": {
|
||||
"CLAUDE_CODE_SRC_ROOT": "${workspaceFolder}/src"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
72
CONTRIBUTING.md
Normal file
72
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# Contributing
|
||||
|
||||
Thanks for your interest in contributing to this repository!
|
||||
|
||||
## What This Is
|
||||
|
||||
This repo archives the **leaked source code** of Anthropic's Claude Code CLI. Contributions here are about **documentation, tooling, and exploration aids** — not modifying the original Claude Code source.
|
||||
|
||||
## What You Can Contribute
|
||||
|
||||
- **Documentation** — Improve or expand the [docs/](docs/) directory
|
||||
- **MCP Server** — Enhance the exploration MCP server in [mcp-server/](mcp-server/)
|
||||
- **Analysis** — Write-ups, architecture diagrams, or annotated walkthroughs
|
||||
- **Tooling** — Scripts or tools that aid in studying the source code
|
||||
- **Bug fixes** — Fix issues in the MCP server or supporting infrastructure
|
||||
|
||||
## What Not to Change
|
||||
|
||||
- **`src/` directory** — This is the original leaked source, preserved as-is. Don't modify it.
|
||||
- The [`backup` branch](https://github.com/777genius/claude-code-source-code/tree/backup) contains the unmodified original.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Node.js** 18+ (for the MCP server)
|
||||
- **Git**
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
git clone https://github.com/777genius/claude-code-source-code.git
|
||||
cd claude-code-source-code
|
||||
```
|
||||
|
||||
### MCP Server Development
|
||||
|
||||
```bash
|
||||
cd mcp-server
|
||||
npm install
|
||||
npm run dev # Run with tsx (no build step)
|
||||
npm run build # Compile to dist/
|
||||
```
|
||||
|
||||
### Linting & Type Checking
|
||||
|
||||
```bash
|
||||
# From the repo root — checks the leaked src/
|
||||
npm run lint # Biome lint
|
||||
npm run typecheck # TypeScript type check
|
||||
```
|
||||
|
||||
## Code Style
|
||||
|
||||
For any new code (MCP server, tooling, scripts):
|
||||
|
||||
- TypeScript with strict mode
|
||||
- ES modules
|
||||
- 2-space indentation (tabs for `src/` to match Biome config)
|
||||
- Descriptive variable names, minimal comments
|
||||
|
||||
## Submitting Changes
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch (`git checkout -b my-feature`)
|
||||
3. Make your changes
|
||||
4. Commit with a clear message
|
||||
5. Push and open a pull request
|
||||
|
||||
## Questions?
|
||||
|
||||
Open an issue or reach out to [nichxbt](https://www.x.com/nichxbt).
|
||||
45
Dockerfile
Normal file
45
Dockerfile
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# ─────────────────────────────────────────────────────────────
|
||||
# Claude Code CLI — Production Container
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# 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"
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
|
||||
# Stage 1: Build
|
||||
FROM oven/bun:1-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy manifests first for layer caching
|
||||
COPY package.json bun.lockb* ./
|
||||
|
||||
# Install all dependencies (including devDependencies for build)
|
||||
RUN bun install --frozen-lockfile || bun install
|
||||
|
||||
# Copy source
|
||||
COPY . .
|
||||
|
||||
# Build production bundle
|
||||
RUN bun run build:prod
|
||||
|
||||
# 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"]
|
||||
|
||||
|
||||
11
LICENSE
Normal file
11
LICENSE
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
UNLICENSED — NOT FOR REDISTRIBUTION
|
||||
|
||||
This repository contains leaked proprietary source code belonging to Anthropic, PBC.
|
||||
It is published here strictly for educational and research purposes.
|
||||
|
||||
The original software is NOT open-source. Anthropic has not released this code
|
||||
under any permissive or copyleft license. Use at your own legal risk.
|
||||
|
||||
For the official Claude Code CLI, see: https://docs.anthropic.com/en/docs/claude-code
|
||||
|
||||
|
||||
446
README.md
Normal file
446
README.md
Normal file
|
|
@ -0,0 +1,446 @@
|
|||
<div align="center">
|
||||
|
||||
# Claude Code — Leaked Source
|
||||
|
||||
**The full source code of Anthropic's Claude Code CLI, leaked on March 31, 2026**
|
||||
|
||||
[](#tech-stack)
|
||||
[](#tech-stack)
|
||||
[](#tech-stack)
|
||||
[](#directory-structure)
|
||||
[](#-explore-with-mcp-server)
|
||||
[](https://www.npmjs.com/package/claude-code-explorer-mcp)
|
||||
|
||||
> The original unmodified leaked source is preserved in this repository's [`backup` branch](https://github.com/777genius/claude-code-source-code/tree/backup).
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [How It Leaked](#how-it-leaked)
|
||||
- [What Is Claude Code?](#what-is-claude-code)
|
||||
- [Documentation](#-documentation)
|
||||
- [Explore with MCP Server](#-explore-with-mcp-server)
|
||||
- [Directory Structure](#directory-structure)
|
||||
- [Architecture](#architecture)
|
||||
- [Tool System](#1-tool-system)
|
||||
- [Command System](#2-command-system)
|
||||
- [Service Layer](#3-service-layer)
|
||||
- [Bridge System](#4-bridge-system)
|
||||
- [Permission System](#5-permission-system)
|
||||
- [Feature Flags](#6-feature-flags)
|
||||
- [Key Files](#key-files)
|
||||
- [Tech Stack](#tech-stack)
|
||||
- [Design Patterns](#design-patterns)
|
||||
- [GitPretty Setup](#gitpretty-setup)
|
||||
- [Contributing](#contributing)
|
||||
- [Disclaimer](#disclaimer)
|
||||
|
||||
---
|
||||
|
||||
## How It Leaked
|
||||
|
||||
[Chaofan Shou (@Fried_rice)](https://x.com/Fried_rice) discovered that the published npm package for Claude Code included a `.map` file referencing the full, unobfuscated TypeScript source — downloadable as a zip from Anthropic's R2 storage bucket.
|
||||
|
||||
> **"Claude code source code has been leaked via a map file in their npm registry!"**
|
||||
>
|
||||
> — [@Fried_rice, March 31, 2026](https://x.com/Fried_rice/status/2038894956459290963)
|
||||
|
||||
---
|
||||
|
||||
## What Is Claude Code?
|
||||
|
||||
Claude Code is Anthropic's official CLI tool for interacting with Claude directly from the terminal — editing files, running commands, searching codebases, managing git workflows, and more. This repository contains the leaked `src/` directory.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Leaked** | 2026-03-31 |
|
||||
| **Language** | TypeScript (strict) |
|
||||
| **Runtime** | [Bun](https://bun.sh) |
|
||||
| **Terminal UI** | [React](https://react.dev) + [Ink](https://github.com/vadimdemedes/ink) |
|
||||
| **Scale** | ~1,900 files · 512,000+ lines of code |
|
||||
|
||||
---
|
||||
|
||||
## <20> Documentation
|
||||
|
||||
For in-depth guides, see the [`docs/`](docs/) directory:
|
||||
|
||||
| Guide | Description |
|
||||
|-------|-------------|
|
||||
| **[Architecture](docs/architecture.md)** | Core pipeline, startup sequence, state management, rendering, data flow |
|
||||
| **[Tools Reference](docs/tools.md)** | Complete catalog of all ~40 agent tools with categories and permission model |
|
||||
| **[Commands Reference](docs/commands.md)** | All ~85 slash commands organized by category |
|
||||
| **[Subsystems Guide](docs/subsystems.md)** | Deep dives into Bridge, MCP, Permissions, Plugins, Skills, Tasks, Memory, Voice |
|
||||
| **[Exploration Guide](docs/exploration-guide.md)** | How to navigate the codebase — study paths, grep patterns, key files |
|
||||
|
||||
Also see: [CONTRIBUTING.md](CONTRIBUTING.md) · [MCP Server README](mcp-server/README.md)
|
||||
|
||||
---
|
||||
|
||||
## <20>🔍 Explore with MCP Server
|
||||
|
||||
This repo ships an [MCP server](https://modelcontextprotocol.io/) that lets any MCP-compatible client (Claude Code, Claude Desktop, VS Code Copilot, Cursor) explore the full source interactively.
|
||||
|
||||
### Install from npm
|
||||
|
||||
The MCP server is published as [`claude-code-explorer-mcp`](https://www.npmjs.com/package/claude-code-explorer-mcp) on npm — no need to clone the repo:
|
||||
|
||||
```bash
|
||||
# Claude Code
|
||||
claude mcp add claude-code-explorer -- npx -y claude-code-explorer-mcp
|
||||
```
|
||||
|
||||
### One-liner setup (from source)
|
||||
|
||||
```bash
|
||||
git clone https://github.com/777genius/claude-code-source-code.git ~/claude-code-source-code \
|
||||
&& cd ~/claude-code-source-code/mcp-server \
|
||||
&& npm install && npm run build \
|
||||
&& claude mcp add claude-code-explorer -- node ~/claude-code/mcp-server/dist/index.js
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary><strong>Step-by-step setup</strong></summary>
|
||||
|
||||
```bash
|
||||
# 1. Clone the repo
|
||||
git clone https://github.com/777genius/claude-code-source-code.git
|
||||
cd claude-code-source-code/mcp-server
|
||||
|
||||
# 2. Install & build
|
||||
npm install && npm run build
|
||||
|
||||
# 3. Register with Claude Code
|
||||
claude mcp add claude-code-explorer -- node /absolute/path/to/claude-code/mcp-server/dist/index.js
|
||||
```
|
||||
|
||||
Replace `/absolute/path/to/claude-code-source-code` with your actual clone path.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>VS Code / Cursor / Claude Desktop config</strong></summary>
|
||||
|
||||
**VS Code** — add to `.vscode/mcp.json`:
|
||||
```json
|
||||
{
|
||||
"servers": {
|
||||
"claude-code-explorer": {
|
||||
"type": "stdio",
|
||||
"command": "node",
|
||||
"args": ["${workspaceFolder}/mcp-server/dist/index.js"],
|
||||
"env": { "CLAUDE_CODE_SRC_ROOT": "${workspaceFolder}/src" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Claude Desktop** — add to your config file:
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"claude-code-explorer": {
|
||||
"command": "node",
|
||||
"args": ["/absolute/path/to/claude-code/mcp-server/dist/index.js"],
|
||||
"env": { "CLAUDE_CODE_SRC_ROOT": "/absolute/path/to/claude-code/src" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Cursor** — add to `~/.cursor/mcp.json` (same format as Claude Desktop).
|
||||
|
||||
</details>
|
||||
|
||||
### Available tools & prompts
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `list_tools` | List all ~40 agent tools with source files |
|
||||
| `list_commands` | List all ~50 slash commands with source files |
|
||||
| `get_tool_source` | Read full source of any tool (e.g. BashTool, FileEditTool) |
|
||||
| `get_command_source` | Read source of any slash command (e.g. review, mcp) |
|
||||
| `read_source_file` | Read any file from `src/` by path |
|
||||
| `search_source` | Grep across the entire source tree |
|
||||
| `list_directory` | Browse `src/` directories |
|
||||
| `get_architecture` | High-level architecture overview |
|
||||
|
||||
| Prompt | Description |
|
||||
|--------|-------------|
|
||||
| `explain_tool` | Deep-dive into how a specific tool works |
|
||||
| `explain_command` | Understand a slash command's implementation |
|
||||
| `architecture_overview` | Guided tour of the full architecture |
|
||||
| `how_does_it_work` | Explain any subsystem (permissions, MCP, bridge, etc.) |
|
||||
| `compare_tools` | Side-by-side comparison of two tools |
|
||||
|
||||
**Try asking:** *"How does the BashTool work?"* · *"Search for where permissions are checked"* · *"Show me the /review command source"*
|
||||
|
||||
### Custom source path / Remove
|
||||
|
||||
```bash
|
||||
# Custom source location
|
||||
claude mcp add claude-code-explorer -e CLAUDE_CODE_SRC_ROOT=/path/to/src -- node /path/to/mcp-server/dist/index.js
|
||||
|
||||
# Remove
|
||||
claude mcp remove claude-code-explorer
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── main.tsx # Entrypoint — Commander.js CLI parser + React/Ink renderer
|
||||
├── QueryEngine.ts # Core LLM API caller (~46K lines)
|
||||
├── Tool.ts # Tool type definitions (~29K lines)
|
||||
├── commands.ts # Command registry (~25K lines)
|
||||
├── tools.ts # Tool registry
|
||||
├── context.ts # System/user context collection
|
||||
├── cost-tracker.ts # Token cost tracking
|
||||
│
|
||||
├── tools/ # Agent tool implementations (~40)
|
||||
├── commands/ # Slash command implementations (~50)
|
||||
├── components/ # Ink UI components (~140)
|
||||
├── services/ # External service integrations
|
||||
├── hooks/ # React hooks (incl. permission checks)
|
||||
├── types/ # TypeScript type definitions
|
||||
├── utils/ # Utility functions
|
||||
├── screens/ # Full-screen UIs (Doctor, REPL, Resume)
|
||||
│
|
||||
├── bridge/ # IDE integration (VS Code, JetBrains)
|
||||
├── coordinator/ # Multi-agent orchestration
|
||||
├── plugins/ # Plugin system
|
||||
├── skills/ # Skill system
|
||||
├── server/ # Server mode
|
||||
├── remote/ # Remote sessions
|
||||
├── memdir/ # Persistent memory directory
|
||||
├── tasks/ # Task management
|
||||
├── state/ # State management
|
||||
│
|
||||
├── voice/ # Voice input
|
||||
├── vim/ # Vim mode
|
||||
├── keybindings/ # Keybinding configuration
|
||||
├── schemas/ # Config schemas (Zod)
|
||||
├── migrations/ # Config migrations
|
||||
├── entrypoints/ # Initialization logic
|
||||
├── query/ # Query pipeline
|
||||
├── ink/ # Ink renderer wrapper
|
||||
├── buddy/ # Companion sprite (Easter egg 🐣)
|
||||
├── native-ts/ # Native TypeScript utils
|
||||
├── outputStyles/ # Output styling
|
||||
└── upstreamproxy/ # Proxy configuration
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### 1. Tool System
|
||||
|
||||
> `src/tools/` — Every tool Claude can invoke is a self-contained module with its own input schema, permission model, and execution logic.
|
||||
|
||||
| Tool | Description |
|
||||
|---|---|
|
||||
| **File I/O** | |
|
||||
| `FileReadTool` | Read files (images, PDFs, notebooks) |
|
||||
| `FileWriteTool` | Create / overwrite files |
|
||||
| `FileEditTool` | Partial modification (string replacement) |
|
||||
| `NotebookEditTool` | Jupyter notebook editing |
|
||||
| **Search** | |
|
||||
| `GlobTool` | File pattern matching |
|
||||
| `GrepTool` | ripgrep-based content search |
|
||||
| `WebSearchTool` | Web search |
|
||||
| `WebFetchTool` | Fetch URL content |
|
||||
| **Execution** | |
|
||||
| `BashTool` | Shell command execution |
|
||||
| `SkillTool` | Skill execution |
|
||||
| `MCPTool` | MCP server tool invocation |
|
||||
| `LSPTool` | Language Server Protocol integration |
|
||||
| **Agents & Teams** | |
|
||||
| `AgentTool` | Sub-agent spawning |
|
||||
| `SendMessageTool` | Inter-agent messaging |
|
||||
| `TeamCreateTool` / `TeamDeleteTool` | Team management |
|
||||
| `TaskCreateTool` / `TaskUpdateTool` | Task management |
|
||||
| **Mode & State** | |
|
||||
| `EnterPlanModeTool` / `ExitPlanModeTool` | Plan mode toggle |
|
||||
| `EnterWorktreeTool` / `ExitWorktreeTool` | Git worktree isolation |
|
||||
| `ToolSearchTool` | Deferred tool discovery |
|
||||
| `SleepTool` | Proactive mode wait |
|
||||
| `CronCreateTool` | Scheduled triggers |
|
||||
| `RemoteTriggerTool` | Remote trigger |
|
||||
| `SyntheticOutputTool` | Structured output generation |
|
||||
|
||||
### 2. Command System
|
||||
|
||||
> `src/commands/` — User-facing slash commands invoked with `/` in the REPL.
|
||||
|
||||
| Command | Description | | Command | Description |
|
||||
|---|---|---|---|---|
|
||||
| `/commit` | Git commit | | `/memory` | Persistent memory |
|
||||
| `/review` | Code review | | `/skills` | Skill management |
|
||||
| `/compact` | Context compression | | `/tasks` | Task management |
|
||||
| `/mcp` | MCP server management | | `/vim` | Vim mode toggle |
|
||||
| `/config` | Settings | | `/diff` | View changes |
|
||||
| `/doctor` | Environment diagnostics | | `/cost` | Check usage cost |
|
||||
| `/login` / `/logout` | Auth | | `/theme` | Change theme |
|
||||
| `/context` | Context visualization | | `/share` | Share session |
|
||||
| `/pr_comments` | PR comments | | `/resume` | Restore session |
|
||||
| `/desktop` | Desktop handoff | | `/mobile` | Mobile handoff |
|
||||
|
||||
### 3. Service Layer
|
||||
|
||||
> `src/services/` — External integrations and core infrastructure.
|
||||
|
||||
| Service | Description |
|
||||
|---|---|
|
||||
| `api/` | Anthropic API client, file API, bootstrap |
|
||||
| `mcp/` | Model Context Protocol connection & management |
|
||||
| `oauth/` | OAuth 2.0 authentication |
|
||||
| `lsp/` | Language Server Protocol manager |
|
||||
| `analytics/` | GrowthBook feature flags & analytics |
|
||||
| `plugins/` | Plugin loader |
|
||||
| `compact/` | Conversation context compression |
|
||||
| `extractMemories/` | Automatic memory extraction |
|
||||
| `teamMemorySync/` | Team memory synchronization |
|
||||
| `tokenEstimation.ts` | Token count estimation |
|
||||
| `policyLimits/` | Organization policy limits |
|
||||
| `remoteManagedSettings/` | Remote managed settings |
|
||||
|
||||
### 4. Bridge System
|
||||
|
||||
> `src/bridge/` — Bidirectional communication layer connecting IDE extensions (VS Code, JetBrains) with the CLI.
|
||||
|
||||
Key files: `bridgeMain.ts` (main loop) · `bridgeMessaging.ts` (protocol) · `bridgePermissionCallbacks.ts` (permission callbacks) · `replBridge.ts` (REPL session) · `jwtUtils.ts` (JWT auth) · `sessionRunner.ts` (session execution)
|
||||
|
||||
### 5. Permission System
|
||||
|
||||
> `src/hooks/toolPermission/` — Checks permissions on every tool invocation.
|
||||
|
||||
Prompts the user for approval/denial or auto-resolves based on the configured permission mode: `default`, `plan`, `bypassPermissions`, `auto`, etc.
|
||||
|
||||
### 6. Feature Flags
|
||||
|
||||
Dead code elimination at build time via Bun's `bun:bundle`:
|
||||
|
||||
```typescript
|
||||
import { feature } from 'bun:bundle'
|
||||
|
||||
const voiceCommand = feature('VOICE_MODE')
|
||||
? require('./commands/voice/index.js').default
|
||||
: null
|
||||
```
|
||||
|
||||
Notable flags: `PROACTIVE` · `KAIROS` · `BRIDGE_MODE` · `DAEMON` · `VOICE_MODE` · `AGENT_TRIGGERS` · `MONITOR_TOOL`
|
||||
|
||||
---
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Lines | Purpose |
|
||||
|------|------:|---------|
|
||||
| `QueryEngine.ts` | ~46K | Core LLM API engine — streaming, tool loops, thinking mode, retries, token counting |
|
||||
| `Tool.ts` | ~29K | Base types/interfaces for all tools — input schemas, permissions, progress state |
|
||||
| `commands.ts` | ~25K | Command registration & execution with conditional per-environment imports |
|
||||
| `main.tsx` | — | CLI parser + React/Ink renderer; parallelizes MDM, keychain, and GrowthBook on startup |
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Category | Technology |
|
||||
|---|---|
|
||||
| Runtime | [Bun](https://bun.sh) |
|
||||
| Language | TypeScript (strict) |
|
||||
| Terminal UI | [React](https://react.dev) + [Ink](https://github.com/vadimdemedes/ink) |
|
||||
| CLI Parsing | [Commander.js](https://github.com/tj/commander.js) (extra-typings) |
|
||||
| Schema Validation | [Zod v4](https://zod.dev) |
|
||||
| Code Search | [ripgrep](https://github.com/BurntSushi/ripgrep) (via GrepTool) |
|
||||
| Protocols | [MCP SDK](https://modelcontextprotocol.io) · LSP |
|
||||
| API | [Anthropic SDK](https://docs.anthropic.com) |
|
||||
| Telemetry | OpenTelemetry + gRPC |
|
||||
| Feature Flags | GrowthBook |
|
||||
| Auth | OAuth 2.0 · JWT · macOS Keychain |
|
||||
|
||||
---
|
||||
|
||||
## Design Patterns
|
||||
|
||||
<details>
|
||||
<summary><strong>Parallel Prefetch</strong> — Startup optimization</summary>
|
||||
|
||||
MDM settings, keychain reads, and API preconnect fire in parallel as side-effects before heavy module evaluation:
|
||||
|
||||
```typescript
|
||||
// main.tsx
|
||||
startMdmRawRead()
|
||||
startKeychainPrefetch()
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Lazy Loading</strong> — Deferred heavy modules</summary>
|
||||
|
||||
OpenTelemetry (~400KB) and gRPC (~700KB) are loaded via dynamic `import()` only when needed.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Agent Swarms</strong> — Multi-agent orchestration</summary>
|
||||
|
||||
Sub-agents spawn via `AgentTool`, with `coordinator/` handling orchestration. `TeamCreateTool` enables team-level parallel work.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Skill System</strong> — Reusable workflows</summary>
|
||||
|
||||
Defined in `skills/` and executed through `SkillTool`. Users can add custom skills.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Plugin Architecture</strong> — Extensibility</summary>
|
||||
|
||||
Built-in and third-party plugins loaded through the `plugins/` subsystem.
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## GitPretty Setup
|
||||
|
||||
<details>
|
||||
<summary>Show per-file emoji commit messages in GitHub's file UI</summary>
|
||||
|
||||
```bash
|
||||
# Apply emoji commits
|
||||
bash ./gitpretty-apply.sh .
|
||||
|
||||
# Optional: install hooks for future commits
|
||||
bash ./gitpretty-apply.sh . --hooks
|
||||
|
||||
# Push as usual
|
||||
git push origin main
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions to documentation, the MCP server, and exploration tooling are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
||||
|
||||
> **Note:** The `src/` directory is the original leaked source and should not be modified.
|
||||
|
||||
---
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This repository archives source code leaked from Anthropic's npm registry on **2026-03-31**. All original source code is the property of [Anthropic](https://www.anthropic.com). This is not an official release and is not licensed for redistribution. Contact [nichxbt](https://www.x.com/nichxbt) for any comments.
|
||||
|
||||
220
Skill.md
Normal file
220
Skill.md
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
---
|
||||
name: claude-code-skill
|
||||
description: Development conventions and architecture guide for the Claude Code CLI repository.
|
||||
---
|
||||
|
||||
# Claude Code — Repository Skill
|
||||
|
||||
## Project Overview
|
||||
|
||||
Claude Code is Anthropic's CLI tool for interacting with Claude from the terminal. It supports file editing, shell commands, git workflows, code review, multi-agent coordination, IDE integration (VS Code, JetBrains), and Model Context Protocol (MCP).
|
||||
|
||||
**Codebase:** ~1,900 files, 512,000+ lines of TypeScript under `src/`.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Component | Technology |
|
||||
|------------------|------------------------------------------------|
|
||||
| Language | TypeScript (strict mode, ES modules) |
|
||||
| Runtime | Bun (JSX support, `bun:bundle` feature flags) |
|
||||
| Terminal UI | React + Ink (React for CLI) |
|
||||
| CLI Parser | Commander.js (`@commander-js/extra-typings`) |
|
||||
| API Client | `@anthropic-ai/sdk` |
|
||||
| Validation | Zod v4 |
|
||||
| Linter/Formatter | Biome |
|
||||
| Analytics | GrowthBook (feature flags & A/B testing) |
|
||||
| Protocol | Model Context Protocol (MCP) |
|
||||
|
||||
## Architecture
|
||||
|
||||
### Directory Map (`src/`)
|
||||
|
||||
| Directory | Purpose |
|
||||
|------------------|-----------------------------------------------------------------|
|
||||
| `commands/` | ~50 slash commands (`/commit`, `/review`, `/config`, etc.) |
|
||||
| `tools/` | ~40 agent tools (Bash, FileRead, FileWrite, Glob, Grep, etc.) |
|
||||
| `components/` | ~140 Ink/React UI components for terminal rendering |
|
||||
| `services/` | External integrations (API, OAuth, MCP, LSP, analytics, plugins)|
|
||||
| `bridge/` | Bidirectional IDE communication layer |
|
||||
| `state/` | React context + custom store (AppState) |
|
||||
| `hooks/` | React hooks (permissions, keybindings, commands, settings) |
|
||||
| `types/` | TypeScript type definitions |
|
||||
| `utils/` | Utilities (shell, file ops, permissions, config, git) |
|
||||
| `screens/` | Full-screen UIs (Doctor, REPL, Resume, Compact) |
|
||||
| `skills/` | Bundled skills + skill loader system |
|
||||
| `plugins/` | Plugin system (marketplace + bundled plugins) |
|
||||
| `coordinator/` | Multi-agent coordination & supervisor logic |
|
||||
| `tasks/` | Task management (shell tasks, agent tasks, teammates) |
|
||||
| `context/` | React context providers (notifications, stats, FPS) |
|
||||
| `memdir/` | Persistent memory system (CLAUDE.md, user/project memory) |
|
||||
| `entrypoints/` | Initialization logic, Agent SDK, MCP entry |
|
||||
| `voice/` | Voice input/output (STT, keyterms) |
|
||||
| `vim/` | Vim mode keybinding support |
|
||||
| `schemas/` | Zod configuration schemas |
|
||||
| `keybindings/` | Keybinding configuration & resolver |
|
||||
| `migrations/` | Config migrations between versions |
|
||||
| `outputStyles/` | Output formatting & theming |
|
||||
| `query/` | Query pipeline & processing |
|
||||
| `server/` | Server/daemon mode |
|
||||
| `remote/` | Remote session handling |
|
||||
|
||||
### Key Files
|
||||
|
||||
| File | Role |
|
||||
|---------------------|-----------------------------------------------------|
|
||||
| `src/main.tsx` | CLI entry point (Commander parser, startup profiling)|
|
||||
| `src/QueryEngine.ts`| Core LLM API caller (streaming, tool-call loops) |
|
||||
| `src/Tool.ts` | Tool type definitions & `buildTool` factory |
|
||||
| `src/tools.ts` | Tool registry & presets |
|
||||
| `src/commands.ts` | Command registry |
|
||||
| `src/context.ts` | System/user context collection (git status, memory) |
|
||||
| `src/cost-tracker.ts`| Token cost tracking |
|
||||
|
||||
### Entry Points & Initialization Sequence
|
||||
|
||||
1. `src/main.tsx` — Commander CLI parser, startup profiling
|
||||
2. `src/entrypoints/init.ts` — Config, telemetry, OAuth, MDM
|
||||
3. `src/entrypoints/cli.tsx` — CLI session orchestration
|
||||
4. `src/entrypoints/mcp.ts` — MCP server mode
|
||||
5. `src/entrypoints/sdk/` — Agent SDK (programmatic API)
|
||||
6. `src/replLauncher.tsx` — REPL session launcher
|
||||
|
||||
Startup performs parallel initialization: MDM policy reads, Keychain prefetch, feature flag checks, then core init.
|
||||
|
||||
## Patterns & Conventions
|
||||
|
||||
### Tool Definition
|
||||
|
||||
Each tool lives in `src/tools/{ToolName}/` and uses `buildTool`:
|
||||
|
||||
```typescript
|
||||
export const MyTool = buildTool({
|
||||
name: 'MyTool',
|
||||
aliases: ['my_tool'],
|
||||
description: 'What this tool does',
|
||||
inputSchema: z.object({
|
||||
param: z.string(),
|
||||
}),
|
||||
async call(args, context, canUseTool, parentMessage, onProgress) {
|
||||
// Execute and return { data: result, newMessages?: [...] }
|
||||
},
|
||||
async checkPermissions(input, context) { /* Permission checks */ },
|
||||
isConcurrencySafe(input) { /* Can run in parallel? */ },
|
||||
isReadOnly(input) { /* Non-destructive? */ },
|
||||
prompt(options) { /* System prompt injection */ },
|
||||
renderToolUseMessage(input, options) { /* UI for invocation */ },
|
||||
renderToolResultMessage(content, progressMessages, options) { /* UI for result */ },
|
||||
})
|
||||
```
|
||||
|
||||
**Directory structure per tool:** `{ToolName}.ts` or `.tsx` (main), `UI.tsx` (rendering), `prompt.ts` (system prompt), plus utility files.
|
||||
|
||||
### Command Definition
|
||||
|
||||
Commands live in `src/commands/` and follow three types:
|
||||
|
||||
- **PromptCommand** — Sends a formatted prompt with injected tools (most commands)
|
||||
- **LocalCommand** — Runs in-process, returns text
|
||||
- **LocalJSXCommand** — Runs in-process, returns React JSX
|
||||
|
||||
```typescript
|
||||
const command = {
|
||||
type: 'prompt',
|
||||
name: 'my-command',
|
||||
description: 'What this command does',
|
||||
progressMessage: 'working...',
|
||||
allowedTools: ['Bash(git *)', 'FileRead(*)'],
|
||||
source: 'builtin',
|
||||
async getPromptForCommand(args, context) {
|
||||
return [{ type: 'text', text: '...' }]
|
||||
},
|
||||
} satisfies Command
|
||||
```
|
||||
|
||||
Commands are registered in `src/commands.ts` and invoked via `/command-name` in the REPL.
|
||||
|
||||
### Component Structure
|
||||
|
||||
- Functional React components with Ink primitives (`Box`, `Text`, `useInput()`)
|
||||
- Styled with Chalk for terminal colors
|
||||
- React Compiler for optimized re-renders
|
||||
- Design system primitives in `src/components/design-system/`
|
||||
|
||||
### State Management
|
||||
|
||||
- `AppState` via React context + custom store (`src/state/AppStateStore.ts`)
|
||||
- Mutable state object passed to tool contexts
|
||||
- Selector functions for derived state
|
||||
- Change observers in `src/state/onChangeAppState.ts`
|
||||
|
||||
### Permission System
|
||||
|
||||
- **Modes:** `default` (prompt per operation), `plan` (show plan, ask once), `bypassPermissions` (auto-approve), `auto` (ML classifier)
|
||||
- **Rules:** Wildcard patterns — `Bash(git *)`, `FileEdit(/src/*)`
|
||||
- Tools implement `checkPermissions()` returning `{ granted: boolean, reason?, prompt? }`
|
||||
|
||||
### Feature Flags & Build
|
||||
|
||||
Bun's `bun:bundle` feature flags enable dead-code elimination at build time:
|
||||
|
||||
```typescript
|
||||
import { feature } from 'bun:bundle'
|
||||
if (feature('PROACTIVE')) { /* proactive agent tools */ }
|
||||
```
|
||||
|
||||
Notable flags: `PROACTIVE`, `KAIROS`, `BRIDGE_MODE`, `VOICE_MODE`, `COORDINATOR_MODE`, `DAEMON`, `WORKFLOW_SCRIPTS`.
|
||||
|
||||
Some features are also gated via `process.env.USER_TYPE === 'ant'`.
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
| Element | Convention | Example |
|
||||
|-------------|---------------------|----------------------------------|
|
||||
| Files | PascalCase (exports) or kebab-case (commands) | `BashTool.tsx`, `commit-push-pr.ts` |
|
||||
| Components | PascalCase | `App.tsx`, `PromptInput.tsx` |
|
||||
| Types | PascalCase, suffix with Props/State/Context | `ToolUseContext` |
|
||||
| Hooks | `use` prefix | `useCanUseTool`, `useSettings` |
|
||||
| Constants | SCREAMING_SNAKE_CASE | `MAX_TOKENS`, `DEFAULT_TIMEOUT_MS`|
|
||||
|
||||
## Import Practices
|
||||
|
||||
- ES modules with `.js` extensions (Bun convention)
|
||||
- Lazy imports for circular dependency breaking: `const getModule = () => require('./heavy.js')`
|
||||
- Conditional imports via feature flags or `process.env`
|
||||
- `biome-ignore` markers for manual import ordering where needed
|
||||
|
||||
## Services
|
||||
|
||||
| Service | Path | Purpose |
|
||||
|--------------------|-------------------------------|-----------------------------------|
|
||||
| API | `services/api/` | Anthropic SDK client, file uploads|
|
||||
| MCP | `services/mcp/` | MCP client, tool/resource discovery|
|
||||
| OAuth | `services/oauth/` | OAuth 2.0 auth flow |
|
||||
| LSP | `services/lsp/` | Language Server Protocol manager |
|
||||
| Analytics | `services/analytics/` | GrowthBook, telemetry, events |
|
||||
| Plugins | `services/plugins/` | Plugin loader, marketplace |
|
||||
| Compact | `services/compact/` | Context compression |
|
||||
| Policy Limits | `services/policyLimits/` | Org rate limits, quota checking |
|
||||
| Remote Settings | `services/remoteManagedSettings/` | Managed settings sync (Enterprise) |
|
||||
| Token Estimation | `services/tokenEstimation.ts` | Token count estimation |
|
||||
|
||||
## Configuration
|
||||
|
||||
**Settings locations:**
|
||||
- **Global:** `~/.claude/config.json`, `~/.claude/settings.json`
|
||||
- **Project:** `.claude/config.json`, `.claude/settings.json`
|
||||
- **System:** macOS Keychain + MDM, Windows Registry + MDM
|
||||
- **Managed:** Remote sync for Enterprise users
|
||||
|
||||
## Guidelines
|
||||
|
||||
1. Read relevant source files before making changes — understand existing patterns first.
|
||||
2. Follow the tool/command/component patterns above when adding new ones.
|
||||
3. Keep edits minimal and focused — avoid unnecessary refactoring.
|
||||
4. Use Zod for all input validation at system boundaries.
|
||||
5. Gate experimental features behind `bun:bundle` feature flags or env checks.
|
||||
6. Respect the permission system — tools that modify state must implement `checkPermissions()`.
|
||||
7. Use lazy imports when adding dependencies that could create circular references.
|
||||
8. Update this file as project conventions evolve.
|
||||
|
||||
|
||||
34
agent.md
Normal file
34
agent.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
name: repository-agent
|
||||
description: Agent operating guide for claude-code.
|
||||
---
|
||||
|
||||
# Agent
|
||||
|
||||
## Purpose
|
||||
Define how an automated coding agent should operate in this repository.
|
||||
|
||||
## Core Rules
|
||||
- Keep changes small, targeted, and easy to review.
|
||||
- Preserve existing command behavior unless a task explicitly asks for a behavior change.
|
||||
- Favor existing patterns in `src/commands/`, `src/tools/`, and shared utility modules.
|
||||
- Avoid broad refactors while fixing localized issues.
|
||||
|
||||
## Workflow
|
||||
1. Gather context from relevant files before editing.
|
||||
2. Implement the smallest viable change.
|
||||
3. Run focused validation (type checks/tests for changed areas).
|
||||
4. Summarize what changed and any remaining risks.
|
||||
|
||||
## Code Style
|
||||
- Match existing TypeScript style and naming in nearby files.
|
||||
- Prefer explicit, readable logic over compact clever code.
|
||||
- Add brief comments only when logic is not obvious.
|
||||
|
||||
## Validation
|
||||
- Prefer targeted checks first, then broader checks if needed.
|
||||
- If validation cannot run, clearly state what was skipped and why.
|
||||
|
||||
## Notes
|
||||
- Repository conventions may evolve; update this file when team norms change.
|
||||
|
||||
49
biome.json
Normal file
49
biome.json
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"complexity": {
|
||||
"noExcessiveCognitiveComplexity": "warn"
|
||||
},
|
||||
"correctness": {
|
||||
"noUnusedImports": "warn",
|
||||
"noUnusedVariables": "warn"
|
||||
},
|
||||
"style": {
|
||||
"noNonNullAssertion": "off",
|
||||
"useImportType": "warn"
|
||||
},
|
||||
"suspicious": {
|
||||
"noExplicitAny": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "tab",
|
||||
"indentWidth": 2,
|
||||
"lineWidth": 100
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "single",
|
||||
"semicolons": "asNeeded"
|
||||
}
|
||||
},
|
||||
"json": {
|
||||
"formatter": {
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"ignore": ["node_modules", "dist", "*.d.ts"]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
635
bun.lock
Normal file
635
bun.lock
Normal file
|
|
@ -0,0 +1,635 @@
|
|||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "@anthropic-ai/claude-code",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.39.0",
|
||||
"@commander-js/extra-typings": "^13.1.0",
|
||||
"@growthbook/growthbook": "^1.4.0",
|
||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/api-logs": "^0.57.0",
|
||||
"@opentelemetry/core": "^1.30.0",
|
||||
"@opentelemetry/sdk-logs": "^0.57.0",
|
||||
"@opentelemetry/sdk-metrics": "^1.30.0",
|
||||
"@opentelemetry/sdk-trace-base": "^1.30.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-search": "^0.15.0",
|
||||
"@xterm/addon-unicode11": "^0.8.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
"@xterm/addon-webgl": "^0.18.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"auto-bind": "^5.0.1",
|
||||
"axios": "^1.7.0",
|
||||
"chalk": "^5.4.0",
|
||||
"chokidar": "^4.0.0",
|
||||
"cli-boxes": "^3.0.0",
|
||||
"code-excerpt": "^4.0.0",
|
||||
"diff": "^7.0.0",
|
||||
"execa": "^9.5.0",
|
||||
"figures": "^6.1.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"highlight.js": "^11.11.0",
|
||||
"ignore": "^6.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"marked": "^15.0.0",
|
||||
"node-pty": "^1.1.0",
|
||||
"p-map": "^7.0.0",
|
||||
"picomatch": "^4.0.0",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"qrcode": "^1.5.0",
|
||||
"react": "^19.0.0",
|
||||
"react-reconciler": "^0.31.0",
|
||||
"semver": "^7.6.0",
|
||||
"stack-utils": "^2.0.6",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"supports-hyperlinks": "^3.1.0",
|
||||
"tree-kill": "^1.2.2",
|
||||
"type-fest": "^4.30.0",
|
||||
"undici": "^7.3.0",
|
||||
"usehooks-ts": "^3.1.0",
|
||||
"wrap-ansi": "^9.0.0",
|
||||
"ws": "^8.18.0",
|
||||
"yaml": "^2.6.0",
|
||||
"zod": "^3.24.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.0",
|
||||
"@types/diff": "^7.0.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^22.10.0",
|
||||
"@types/picomatch": "^3.0.0",
|
||||
"@types/proper-lockfile": "^4.1.4",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/stack-utils": "^2.0.3",
|
||||
"@types/ws": "^8.5.0",
|
||||
"esbuild": "^0.25.0",
|
||||
"typescript": "^5.7.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.39.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg=="],
|
||||
|
||||
"@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="],
|
||||
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="],
|
||||
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="],
|
||||
|
||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="],
|
||||
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="],
|
||||
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="],
|
||||
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="],
|
||||
|
||||
"@commander-js/extra-typings": ["@commander-js/extra-typings@13.1.0", "", { "peerDependencies": { "commander": "~13.1.0" } }, "sha512-q5P52BYb1hwVWE6dtID7VvuJWrlfbCv4klj7BjUUOqMz4jbSZD4C9fJ9lRjL2jnBGTg+gDDlaXN51rkWcLk4fg=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
||||
|
||||
"@growthbook/growthbook": ["@growthbook/growthbook@1.6.5", "", { "dependencies": { "dom-mutator": "^0.6.0" } }, "sha512-mUaMsgeUTpRIUOTn33EUXHRK6j7pxBjwqH4WpQyq+pukjd1AIzWlEa6w7i6bInJUcweGgP2beXZmaP6b6UPn7A=="],
|
||||
|
||||
"@hono/node-server": ["@hono/node-server@1.19.12", "", { "peerDependencies": { "hono": "^4" } }, "sha512-txsUW4SQ1iilgE0l9/e9VQWmELXifEFvmdA1j6WFh/aFPj99hIntrSsq/if0UWyGVkmrRPKA1wCeP+UCr1B9Uw=="],
|
||||
|
||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
|
||||
|
||||
"@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.57.2", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A=="],
|
||||
|
||||
"@opentelemetry/core": ["@opentelemetry/core@1.30.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ=="],
|
||||
|
||||
"@opentelemetry/resources": ["@opentelemetry/resources@1.30.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA=="],
|
||||
|
||||
"@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.57.2", "", { "dependencies": { "@opentelemetry/api-logs": "0.57.2", "@opentelemetry/core": "1.30.1", "@opentelemetry/resources": "1.30.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-TXFHJ5c+BKggWbdEQ/inpgIzEmS2BGQowLE9UhsMd7YYlUfBQJ4uax0VF/B5NYigdM/75OoJGhAV3upEhK+3gg=="],
|
||||
|
||||
"@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@1.30.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/resources": "1.30.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-q9zcZ0Okl8jRgmy7eNW3Ku1XSgg3sDLa5evHZpCwjspw7E8Is4K/haRPDJrBcX3YSn/Y7gUvFnByNYEKQNbNog=="],
|
||||
|
||||
"@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@1.30.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/resources": "1.30.1", "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg=="],
|
||||
|
||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
|
||||
|
||||
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
|
||||
|
||||
"@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="],
|
||||
|
||||
"@types/diff": ["@types/diff@7.0.2", "", {}, "sha512-JSWRMozjFKsGlEjiiKajUjIJVKuKdE3oVy2DNtK+fUo8q82nhFZ2CPQwicAIkXrofahDXrWJ7mjelvZphMS98Q=="],
|
||||
|
||||
"@types/lodash": ["@types/lodash@4.17.24", "", {}, "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ=="],
|
||||
|
||||
"@types/lodash-es": ["@types/lodash-es@4.17.12", "", { "dependencies": { "@types/lodash": "*" } }, "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ=="],
|
||||
|
||||
"@types/node": ["@types/node@22.19.15", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg=="],
|
||||
|
||||
"@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="],
|
||||
|
||||
"@types/picomatch": ["@types/picomatch@3.0.2", "", {}, "sha512-n0i8TD3UDB7paoMMxA3Y65vUncFJXjcUf7lQY7YyKGl6031FNjfsLs6pdLFCy2GNFxItPJG8GvvpbZc2skH7WA=="],
|
||||
|
||||
"@types/proper-lockfile": ["@types/proper-lockfile@4.1.4", "", { "dependencies": { "@types/retry": "*" } }, "sha512-uo2ABllncSqg9F1D4nugVl9v93RmjxF6LJzQLMLDdPaXCUIDPeOJ21Gbqi43xNKzBi/WQ0Q0dICqufzQbMjipQ=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
|
||||
|
||||
"@types/retry": ["@types/retry@0.12.5", "", {}, "sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw=="],
|
||||
|
||||
"@types/semver": ["@types/semver@7.7.1", "", {}, "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA=="],
|
||||
|
||||
"@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="],
|
||||
|
||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||
|
||||
"@xterm/addon-fit": ["@xterm/addon-fit@0.10.0", "", { "peerDependencies": { "@xterm/xterm": "^5.0.0" } }, "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ=="],
|
||||
|
||||
"@xterm/addon-search": ["@xterm/addon-search@0.15.0", "", { "peerDependencies": { "@xterm/xterm": "^5.0.0" } }, "sha512-ZBZKLQ+EuKE83CqCmSSz5y1tx+aNOCUaA7dm6emgOX+8J9H1FWXZyrKfzjwzV+V14TV3xToz1goIeRhXBS5qjg=="],
|
||||
|
||||
"@xterm/addon-unicode11": ["@xterm/addon-unicode11@0.8.0", "", { "peerDependencies": { "@xterm/xterm": "^5.0.0" } }, "sha512-LxinXu8SC4OmVa6FhgwsVCBZbr8WoSGzBl2+vqe8WcQ6hb1r6Gj9P99qTNdPiFPh4Ceiu2pC8xukZ6+2nnh49Q=="],
|
||||
|
||||
"@xterm/addon-web-links": ["@xterm/addon-web-links@0.11.0", "", { "peerDependencies": { "@xterm/xterm": "^5.0.0" } }, "sha512-nIHQ38pQI+a5kXnRaTgwqSHnX7KE6+4SVoceompgHL26unAxdfP6IPqUTSYPQgSwM56hsElfoNrrW5V7BUED/Q=="],
|
||||
|
||||
"@xterm/addon-webgl": ["@xterm/addon-webgl@0.18.0", "", { "peerDependencies": { "@xterm/xterm": "^5.0.0" } }, "sha512-xCnfMBTI+/HKPdRnSOHaJDRqEpq2Ugy8LEj9GiY4J3zJObo3joylIFaMvzBwbYRg8zLtkO0KQaStCeSfoaI2/w=="],
|
||||
|
||||
"@xterm/xterm": ["@xterm/xterm@5.5.0", "", {}, "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A=="],
|
||||
|
||||
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
||||
|
||||
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
|
||||
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
|
||||
|
||||
"ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||
|
||||
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" }, "peerDependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||
|
||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||
|
||||
"auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="],
|
||||
|
||||
"axios": ["axios@1.14.0", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ=="],
|
||||
|
||||
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
||||
|
||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
|
||||
|
||||
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||
|
||||
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||
|
||||
"cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
|
||||
|
||||
"cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="],
|
||||
|
||||
"code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||
|
||||
"commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
|
||||
|
||||
"content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
|
||||
|
||||
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||
|
||||
"convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="],
|
||||
|
||||
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
|
||||
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||
|
||||
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="],
|
||||
|
||||
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||
|
||||
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||
|
||||
"diff": ["diff@7.0.0", "", {}, "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw=="],
|
||||
|
||||
"dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="],
|
||||
|
||||
"dom-mutator": ["dom-mutator@0.6.0", "", {}, "sha512-iCt9o0aYfXMUkz/43ZOAUFQYotjGB+GNbYJiJdz4TgXkyToXbbRy5S6FbTp72lRBtfpUMwEc1KmpFEU4CZeoNg=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
||||
|
||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||
|
||||
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": "bin/esbuild" }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
||||
|
||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="],
|
||||
|
||||
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||
|
||||
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
|
||||
|
||||
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
|
||||
|
||||
"eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
|
||||
|
||||
"execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="],
|
||||
|
||||
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
||||
|
||||
"express-rate-limit": ["express-rate-limit@8.3.2", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
||||
|
||||
"figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="],
|
||||
|
||||
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
|
||||
|
||||
"find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
|
||||
|
||||
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
|
||||
|
||||
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
|
||||
|
||||
"form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="],
|
||||
|
||||
"formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="],
|
||||
|
||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||
|
||||
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"fuse.js": ["fuse.js@7.1.0", "", {}, "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ=="],
|
||||
|
||||
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||
|
||||
"get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="],
|
||||
|
||||
"hono": ["hono@4.12.9", "", {}, "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
||||
"human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="],
|
||||
|
||||
"humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||
|
||||
"ignore": ["ignore@6.0.2", "", {}, "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||
|
||||
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
||||
|
||||
"is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
|
||||
|
||||
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
|
||||
|
||||
"locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
|
||||
|
||||
"lodash-es": ["lodash-es@4.17.23", "", {}, "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg=="],
|
||||
|
||||
"lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="],
|
||||
|
||||
"marked": ["marked@15.0.12", "", { "bin": "bin/marked.js" }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
||||
|
||||
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
||||
|
||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
|
||||
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
|
||||
|
||||
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
|
||||
|
||||
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||
|
||||
"node-pty": ["node-pty@1.1.0", "", { "dependencies": { "node-addon-api": "^7.1.0" } }, "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg=="],
|
||||
|
||||
"npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
||||
|
||||
"p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
|
||||
|
||||
"p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="],
|
||||
|
||||
"p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="],
|
||||
|
||||
"parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="],
|
||||
|
||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@8.4.1", "", {}, "sha512-fvU78fIjZ+SBM9YwCknCvKOUKkLVqtWDVctl0s7xIqfmfb38t2TT4ZU2gHm+Z8xGwgW+QWEU3oQSAzIbo89Ggw=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
||||
|
||||
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
|
||||
|
||||
"pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="],
|
||||
|
||||
"pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="],
|
||||
|
||||
"proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="],
|
||||
|
||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||
|
||||
"proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="],
|
||||
|
||||
"qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": "bin/qrcode" }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="],
|
||||
|
||||
"qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
|
||||
|
||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
|
||||
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
||||
|
||||
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
|
||||
|
||||
"react-reconciler": ["react-reconciler@0.31.0", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-7Ob7Z+URmesIsIVRjnLoDGwBEG/tVitidU0nMsqX/eeJaLY89RISO/10ERe0MqmzuKUUB1rmY+h1itMbUHg9BQ=="],
|
||||
|
||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
|
||||
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||
|
||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="],
|
||||
|
||||
"retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
|
||||
|
||||
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="],
|
||||
|
||||
"semver": ["semver@7.7.4", "", { "bin": "bin/semver.js" }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
|
||||
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
|
||||
|
||||
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
|
||||
|
||||
"set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="],
|
||||
|
||||
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||
|
||||
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
||||
|
||||
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||
|
||||
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||
|
||||
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="],
|
||||
|
||||
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
|
||||
"string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
||||
|
||||
"strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="],
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"supports-hyperlinks": ["supports-hyperlinks@3.2.0", "", { "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" } }, "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig=="],
|
||||
|
||||
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||
|
||||
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||
|
||||
"tree-kill": ["tree-kill@1.2.2", "", { "bin": "cli.js" }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
|
||||
|
||||
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
||||
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici": ["undici@7.24.6", "", {}, "sha512-Xi4agocCbRzt0yYMZGMA6ApD7gvtUFaxm4ZmeacWI4cZxaF6C+8I8QfofC20NAePiB/IcvZmzkJ7XPa471AEtA=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
"usehooks-ts": ["usehooks-ts@3.1.1", "", { "dependencies": { "lodash.debounce": "^4.0.8" }, "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-I4diPp9Cq6ieSUH2wu+fDAVQO43xwtulo+fKEidHUwZPnYImbtkTjzIJYcDcJqxgmX31GVqNFURodvcgHcW0pA=="],
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="],
|
||||
|
||||
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||
|
||||
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="],
|
||||
|
||||
"y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="],
|
||||
|
||||
"yaml": ["yaml@2.8.3", "", { "bin": "bin.mjs" }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="],
|
||||
|
||||
"yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="],
|
||||
|
||||
"yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="],
|
||||
|
||||
"yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="],
|
||||
|
||||
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
|
||||
|
||||
"@anthropic-ai/sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
|
||||
|
||||
"cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
|
||||
|
||||
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
||||
|
||||
"proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
|
||||
"yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"@anthropic-ai/sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||
|
||||
"cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
}
|
||||
}
|
||||
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"]
|
||||
35
docker/.dockerignore
Normal file
35
docker/.dockerignore
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# NOTE: Docker reads .dockerignore from the build context root.
|
||||
# The canonical copy lives at /.dockerignore — keep both in sync.
|
||||
|
||||
# Dependencies (rebuilt in container)
|
||||
node_modules
|
||||
|
||||
# Git metadata
|
||||
.git
|
||||
.github
|
||||
.gitignore
|
||||
|
||||
# Build output (rebuilt in container)
|
||||
dist
|
||||
|
||||
# Env files — never bake secrets into the image
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# Logs and debug
|
||||
*.log
|
||||
npm-debug.log*
|
||||
bun-debug.log*
|
||||
|
||||
# Test artifacts
|
||||
coverage
|
||||
.nyc_output
|
||||
|
||||
# Editor / OS noise
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
# Docker context itself
|
||||
docker
|
||||
83
docker/Dockerfile
Normal file
83
docker/Dockerfile
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# ─────────────────────────────────────────────────────────────
|
||||
# Claude Web Terminal — Production Container
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Multi-stage build: compiles node-pty native module and bundles
|
||||
# the Claude CLI, then copies artifacts into a slim runtime image.
|
||||
#
|
||||
# Usage:
|
||||
# docker build -f docker/Dockerfile -t claude-web .
|
||||
# docker run -p 3000:3000 -e ANTHROPIC_API_KEY=sk-ant-... claude-web
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
|
||||
# ── Stage 1: Build ────────────────────────────────────────────
|
||||
FROM oven/bun:1 AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Build tools required to compile node-pty's native C++ addon
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 make g++ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy manifests first for layer caching
|
||||
COPY package.json bun.lockb* ./
|
||||
|
||||
# Install all deps (triggers node-pty native compilation)
|
||||
RUN bun install --frozen-lockfile 2>/dev/null || bun install
|
||||
|
||||
# Copy source tree
|
||||
COPY . .
|
||||
|
||||
# Bundle the Claude CLI (produces dist/cli.mjs)
|
||||
RUN bun run build:prod
|
||||
|
||||
# ── Stage 2: Runtime ──────────────────────────────────────────
|
||||
FROM oven/bun:1 AS runtime
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# curl for health checks; no build tools needed at runtime
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Non-root user that PTY sessions will run under
|
||||
RUN groupadd -r claude && useradd -r -g claude -m -d /home/claude claude
|
||||
|
||||
# Compiled node_modules (includes native node-pty binary)
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
|
||||
# Bundled Claude CLI
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
# PTY server source (bun runs TypeScript natively)
|
||||
COPY --from=builder /app/src/server ./src/server
|
||||
|
||||
# TypeScript config needed for bun's module resolution
|
||||
COPY --from=builder /app/tsconfig.json ./tsconfig.json
|
||||
|
||||
# Thin wrapper so the PTY server can exec `claude` as a subprocess
|
||||
RUN printf '#!/bin/sh\nexec bun /app/dist/cli.mjs "$@"\n' \
|
||||
> /usr/local/bin/claude && chmod +x /usr/local/bin/claude
|
||||
|
||||
# Entrypoint script
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
# Allow the claude user to write its config into its home dir
|
||||
RUN chown -R claude:claude /home/claude
|
||||
|
||||
# ── Defaults ──────────────────────────────────────────────────
|
||||
ENV NODE_ENV=production \
|
||||
PORT=3000 \
|
||||
MAX_SESSIONS=5 \
|
||||
CLAUDE_BIN=claude
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
USER claude
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
|
||||
CMD curl -f http://localhost:${PORT:-3000}/health || exit 1
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
28
docker/docker-compose.yml
Normal file
28
docker/docker-compose.yml
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
services:
|
||||
claude-web:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: docker/Dockerfile
|
||||
ports:
|
||||
- "${PORT:-3000}:3000"
|
||||
environment:
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
- AUTH_TOKEN=${AUTH_TOKEN:-}
|
||||
- MAX_SESSIONS=${MAX_SESSIONS:-5}
|
||||
- ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-}
|
||||
volumes:
|
||||
# Persist Claude's config and session data across restarts
|
||||
- claude-data:/home/claude/.claude
|
||||
tmpfs:
|
||||
# PTY processes write temp files here; no persistent storage needed
|
||||
- /tmp:mode=1777
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
volumes:
|
||||
claude-data:
|
||||
28
docker/entrypoint.sh
Normal file
28
docker/entrypoint.sh
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# ── Validate required env vars ────────────────────────────────
|
||||
if [ -z "$ANTHROPIC_API_KEY" ]; then
|
||||
echo "ERROR: ANTHROPIC_API_KEY is not set." >&2
|
||||
echo "" >&2
|
||||
echo " docker run -p 3000:3000 -e ANTHROPIC_API_KEY=sk-ant-... claude-web" >&2
|
||||
echo "" >&2
|
||||
echo " Or via docker-compose with a .env file:" >&2
|
||||
echo " ANTHROPIC_API_KEY=sk-ant-... docker-compose up" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The API key is forwarded to child PTY processes via process.env,
|
||||
# so the claude CLI will pick it up automatically — no config file needed.
|
||||
|
||||
echo "Claude Web Terminal starting on port ${PORT:-3000}..."
|
||||
if [ -n "$AUTH_TOKEN" ]; then
|
||||
echo " Auth token protection: enabled"
|
||||
fi
|
||||
if [ -n "$ALLOWED_ORIGINS" ]; then
|
||||
echo " Allowed origins: $ALLOWED_ORIGINS"
|
||||
fi
|
||||
echo " Max sessions: ${MAX_SESSIONS:-5}"
|
||||
|
||||
# Hand off to the PTY WebSocket server
|
||||
exec bun /app/src/server/web/pty-server.ts
|
||||
224
docs/architecture.md
Normal file
224
docs/architecture.md
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
# Architecture
|
||||
|
||||
> Deep-dive into how Claude Code is structured internally.
|
||||
|
||||
---
|
||||
|
||||
## High-Level Overview
|
||||
|
||||
Claude Code is a terminal-native AI coding assistant built as a single-binary CLI. The architecture follows a pipeline model:
|
||||
|
||||
```
|
||||
User Input → CLI Parser → Query Engine → LLM API → Tool Execution Loop → Terminal UI
|
||||
```
|
||||
|
||||
The entire UI layer is built with **React + Ink** (React for the terminal), making it a fully reactive CLI application with components, hooks, state management, and all the patterns you'd expect in a React web app — just rendered to the terminal.
|
||||
|
||||
---
|
||||
|
||||
## Core Pipeline
|
||||
|
||||
### 1. Entrypoint (`src/main.tsx`)
|
||||
|
||||
The CLI parser is built with [Commander.js](https://github.com/tj/commander.js) (`@commander-js/extra-typings`). On startup, it:
|
||||
|
||||
- Fires parallel prefetch side-effects (MDM settings, Keychain, API preconnect) before heavy module imports
|
||||
- Parses CLI arguments and flags
|
||||
- Initializes the React/Ink renderer
|
||||
- Hands off to the REPL launcher (`src/replLauncher.tsx`)
|
||||
|
||||
### 2. Initialization (`src/entrypoints/`)
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `cli.tsx` | CLI session orchestration — the main path from launch to REPL |
|
||||
| `init.ts` | Config, telemetry, OAuth, MDM policy initialization |
|
||||
| `mcp.ts` | MCP server mode entrypoint (Claude Code as an MCP server) |
|
||||
| `sdk/` | Agent SDK — programmatic API for embedding Claude Code |
|
||||
|
||||
Startup performs parallel initialization: MDM policy reads, Keychain prefetch, feature flag checks, then core init.
|
||||
|
||||
### 3. Query Engine (`src/QueryEngine.ts`, ~46K lines)
|
||||
|
||||
The heart of Claude Code. Handles:
|
||||
|
||||
- **Streaming responses** from the Anthropic API
|
||||
- **Tool-call loops** — when the LLM requests a tool, execute it and feed the result back
|
||||
- **Thinking mode** — extended thinking with budget management
|
||||
- **Retry logic** — automatic retries with backoff for transient failures
|
||||
- **Token counting** — tracks input/output tokens and cost per turn
|
||||
- **Context management** — manages conversation history and context windows
|
||||
|
||||
### 4. Tool System (`src/Tool.ts` + `src/tools/`)
|
||||
|
||||
Every capability Claude can invoke is a **tool**. Each tool is self-contained with:
|
||||
|
||||
- **Input schema** (Zod validation)
|
||||
- **Permission model** (what needs user approval)
|
||||
- **Execution logic** (the actual implementation)
|
||||
- **UI components** (how invocation/results render in the terminal)
|
||||
|
||||
Tools are registered in `src/tools.ts` and discovered by the Query Engine during tool-call loops.
|
||||
|
||||
See [Tools Reference](tools.md) for the complete catalog.
|
||||
|
||||
### 5. Command System (`src/commands.ts` + `src/commands/`)
|
||||
|
||||
User-facing slash commands (`/commit`, `/review`, `/mcp`, etc.) that can be typed in the REPL. Three types:
|
||||
|
||||
| Type | Description | Example |
|
||||
|------|-------------|---------|
|
||||
| **PromptCommand** | Sends a formatted prompt to the LLM with injected tools | `/review`, `/commit` |
|
||||
| **LocalCommand** | Runs in-process, returns plain text | `/cost`, `/version` |
|
||||
| **LocalJSXCommand** | Runs in-process, returns React JSX | `/doctor`, `/install` |
|
||||
|
||||
Commands are registered in `src/commands.ts` and invoked via `/command-name` in the REPL.
|
||||
|
||||
See [Commands Reference](commands.md) for the complete catalog.
|
||||
|
||||
---
|
||||
|
||||
## State Management
|
||||
|
||||
Claude Code uses a **React context + custom store** pattern:
|
||||
|
||||
| Component | Location | Purpose |
|
||||
|-----------|----------|---------|
|
||||
| `AppState` | `src/state/AppStateStore.ts` | Global mutable state object |
|
||||
| Context Providers | `src/context/` | React context for notifications, stats, FPS |
|
||||
| Selectors | `src/state/` | Derived state functions |
|
||||
| Change Observers | `src/state/onChangeAppState.ts` | Side-effects on state changes |
|
||||
|
||||
The `AppState` object is passed into tool contexts, giving tools access to conversation history, settings, and runtime state.
|
||||
|
||||
---
|
||||
|
||||
## UI Layer
|
||||
|
||||
### Components (`src/components/`, ~140 components)
|
||||
|
||||
- Functional React components using Ink primitives (`Box`, `Text`, `useInput()`)
|
||||
- Styled with [Chalk](https://github.com/chalk/chalk) for terminal colors
|
||||
- React Compiler enabled for optimized re-renders
|
||||
- Design system primitives in `src/components/design-system/`
|
||||
|
||||
### Screens (`src/screens/`)
|
||||
|
||||
Full-screen UI modes:
|
||||
|
||||
| Screen | Purpose |
|
||||
|--------|---------|
|
||||
| `REPL.tsx` | Main interactive REPL (the default screen) |
|
||||
| `Doctor.tsx` | Environment diagnostics (`/doctor`) |
|
||||
| `ResumeConversation.tsx` | Session restore (`/resume`) |
|
||||
|
||||
### Hooks (`src/hooks/`, ~80 hooks)
|
||||
|
||||
Standard React hooks pattern. Notable categories:
|
||||
|
||||
- **Permission hooks** — `useCanUseTool`, `src/hooks/toolPermission/`
|
||||
- **IDE integration** — `useIDEIntegration`, `useIdeConnectionStatus`, `useDiffInIDE`
|
||||
- **Input handling** — `useTextInput`, `useVimInput`, `usePasteHandler`, `useInputBuffer`
|
||||
- **Session management** — `useSessionBackgrounding`, `useRemoteSession`, `useAssistantHistory`
|
||||
- **Plugin/skill hooks** — `useManagePlugins`, `useSkillsChange`
|
||||
- **Notification hooks** — `src/hooks/notifs/` (rate limits, deprecation warnings, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Configuration & Schemas
|
||||
|
||||
### Config Schemas (`src/schemas/`)
|
||||
|
||||
Zod v4-based schemas for all configuration:
|
||||
|
||||
- User settings
|
||||
- Project-level settings
|
||||
- Organization/enterprise policies
|
||||
- Permission rules
|
||||
|
||||
### Migrations (`src/migrations/`)
|
||||
|
||||
Handles config format changes between versions — reads old configs and transforms them to the current schema.
|
||||
|
||||
---
|
||||
|
||||
## Build System
|
||||
|
||||
### Bun Runtime
|
||||
|
||||
Claude Code runs on [Bun](https://bun.sh) (not Node.js). Key implications:
|
||||
|
||||
- Native JSX/TSX support without a transpilation step
|
||||
- `bun:bundle` feature flags for dead-code elimination
|
||||
- ES modules with `.js` extensions (Bun convention)
|
||||
|
||||
### Feature Flags (Dead Code Elimination)
|
||||
|
||||
```typescript
|
||||
import { feature } from 'bun:bundle'
|
||||
|
||||
// Code inside inactive feature flags is completely stripped at build time
|
||||
if (feature('VOICE_MODE')) {
|
||||
const voiceCommand = require('./commands/voice/index.js').default
|
||||
}
|
||||
```
|
||||
|
||||
Notable flags:
|
||||
|
||||
| Flag | Feature |
|
||||
|------|---------|
|
||||
| `PROACTIVE` | Proactive agent mode (autonomous actions) |
|
||||
| `KAIROS` | Kairos subsystem |
|
||||
| `BRIDGE_MODE` | IDE bridge integration |
|
||||
| `DAEMON` | Background daemon mode |
|
||||
| `VOICE_MODE` | Voice input/output |
|
||||
| `AGENT_TRIGGERS` | Triggered agent actions |
|
||||
| `MONITOR_TOOL` | Monitoring tool |
|
||||
| `COORDINATOR_MODE` | Multi-agent coordinator |
|
||||
| `WORKFLOW_SCRIPTS` | Workflow automation scripts |
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
Heavy modules are deferred via dynamic `import()` until first use:
|
||||
|
||||
- OpenTelemetry (~400KB)
|
||||
- gRPC (~700KB)
|
||||
- Other optional dependencies
|
||||
|
||||
---
|
||||
|
||||
## Error Handling & Telemetry
|
||||
|
||||
### Telemetry (`src/services/analytics/`)
|
||||
|
||||
- [GrowthBook](https://www.growthbook.io/) for feature flags and A/B testing
|
||||
- [OpenTelemetry](https://opentelemetry.io/) for distributed tracing and metrics
|
||||
- Custom event tracking for usage analytics
|
||||
|
||||
### Cost Tracking (`src/cost-tracker.ts`)
|
||||
|
||||
Tracks token usage and estimated cost per conversation turn. Accessible via the `/cost` command.
|
||||
|
||||
### Diagnostics (`/doctor` command)
|
||||
|
||||
The `Doctor.tsx` screen runs environment checks: API connectivity, authentication, tool availability, MCP server status, and more.
|
||||
|
||||
---
|
||||
|
||||
## Concurrency Model
|
||||
|
||||
Claude Code uses a **single-threaded event loop** (Bun/Node.js model) with:
|
||||
|
||||
- Async/await for I/O operations
|
||||
- React's concurrent rendering for UI updates
|
||||
- Web Workers or child processes for CPU-intensive tasks (gRPC, etc.)
|
||||
- Tool concurrency safety — each tool declares `isConcurrencySafe()` to indicate if it can run in parallel with other tools
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Tools Reference](tools.md) — Complete catalog of all 40 agent tools
|
||||
- [Commands Reference](commands.md) — Complete catalog of all slash commands
|
||||
- [Subsystems Guide](subsystems.md) — Bridge, MCP, permissions, skills, plugins, and more
|
||||
- [Exploration Guide](exploration-guide.md) — How to navigate this codebase
|
||||
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` |
|
||||
211
docs/commands.md
Normal file
211
docs/commands.md
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
# Commands Reference
|
||||
|
||||
> Complete catalog of all slash commands in Claude Code.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Commands are user-facing actions invoked with a `/` prefix in the REPL (e.g., `/commit`, `/review`). They live in `src/commands/` and are registered in `src/commands.ts`.
|
||||
|
||||
### Command Types
|
||||
|
||||
| Type | Description | Example |
|
||||
|------|-------------|---------|
|
||||
| **PromptCommand** | Sends a formatted prompt to the LLM with injected tools | `/review`, `/commit` |
|
||||
| **LocalCommand** | Runs in-process, returns plain text | `/cost`, `/version` |
|
||||
| **LocalJSXCommand** | Runs in-process, returns React JSX | `/install`, `/doctor` |
|
||||
|
||||
### Command Definition Pattern
|
||||
|
||||
```typescript
|
||||
const command = {
|
||||
type: 'prompt',
|
||||
name: 'my-command',
|
||||
description: 'What this command does',
|
||||
progressMessage: 'working...',
|
||||
allowedTools: ['Bash(git *)', 'FileRead(*)'],
|
||||
source: 'builtin',
|
||||
async getPromptForCommand(args, context) {
|
||||
return [{ type: 'text', text: '...' }]
|
||||
},
|
||||
} satisfies Command
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Git & Version Control
|
||||
|
||||
| Command | Source | Description |
|
||||
|---------|--------|-------------|
|
||||
| `/commit` | `commit.ts` | Create a git commit with an AI-generated message |
|
||||
| `/commit-push-pr` | `commit-push-pr.ts` | Commit, push, and create a PR in one step |
|
||||
| `/branch` | `branch/` | Create or switch git branches |
|
||||
| `/diff` | `diff/` | View file changes (staged, unstaged, or against a ref) |
|
||||
| `/pr_comments` | `pr_comments/` | View and address PR review comments |
|
||||
| `/rewind` | `rewind/` | Revert to a previous state |
|
||||
|
||||
## Code Quality
|
||||
|
||||
| Command | Source | Description |
|
||||
|---------|--------|-------------|
|
||||
| `/review` | `review.ts` | AI-powered code review of staged/unstaged changes |
|
||||
| `/security-review` | `security-review.ts` | Security-focused code review |
|
||||
| `/advisor` | `advisor.ts` | Get architectural or design advice |
|
||||
| `/bughunter` | `bughunter/` | Find potential bugs in the codebase |
|
||||
|
||||
## Session & Context
|
||||
|
||||
| Command | Source | Description |
|
||||
|---------|--------|-------------|
|
||||
| `/compact` | `compact/` | Compress conversation context to fit more history |
|
||||
| `/context` | `context/` | Visualize current context (files, memory, etc.) |
|
||||
| `/resume` | `resume/` | Restore a previous conversation session |
|
||||
| `/session` | `session/` | Manage sessions (list, switch, delete) |
|
||||
| `/share` | `share/` | Share a session via link |
|
||||
| `/export` | `export/` | Export conversation to a file |
|
||||
| `/summary` | `summary/` | Generate a summary of the current session |
|
||||
| `/clear` | `clear/` | Clear the conversation history |
|
||||
|
||||
## Configuration & Settings
|
||||
|
||||
| Command | Source | Description |
|
||||
|---------|--------|-------------|
|
||||
| `/config` | `config/` | View or modify Claude Code settings |
|
||||
| `/permissions` | `permissions/` | Manage tool permission rules |
|
||||
| `/theme` | `theme/` | Change the terminal color theme |
|
||||
| `/output-style` | `output-style/` | Change output formatting style |
|
||||
| `/color` | `color/` | Toggle color output |
|
||||
| `/keybindings` | `keybindings/` | View or customize keybindings |
|
||||
| `/vim` | `vim/` | Toggle vim mode for input |
|
||||
| `/effort` | `effort/` | Adjust response effort level |
|
||||
| `/model` | `model/` | Switch the active model |
|
||||
| `/privacy-settings` | `privacy-settings/` | Manage privacy/data settings |
|
||||
| `/fast` | `fast/` | Toggle fast mode (shorter responses) |
|
||||
| `/brief` | `brief.ts` | Toggle brief output mode |
|
||||
|
||||
## Memory & Knowledge
|
||||
|
||||
| Command | Source | Description |
|
||||
|---------|--------|-------------|
|
||||
| `/memory` | `memory/` | Manage persistent memory (CLAUDE.md files) |
|
||||
| `/add-dir` | `add-dir/` | Add a directory to the project context |
|
||||
| `/files` | `files/` | List files in the current context |
|
||||
|
||||
## MCP & Plugins
|
||||
|
||||
| Command | Source | Description |
|
||||
|---------|--------|-------------|
|
||||
| `/mcp` | `mcp/` | Manage MCP server connections |
|
||||
| `/plugin` | `plugin/` | Install, remove, or manage plugins |
|
||||
| `/reload-plugins` | `reload-plugins/` | Reload all installed plugins |
|
||||
| `/skills` | `skills/` | View and manage skills |
|
||||
|
||||
## Authentication
|
||||
|
||||
| Command | Source | Description |
|
||||
|---------|--------|-------------|
|
||||
| `/login` | `login/` | Authenticate with Anthropic |
|
||||
| `/logout` | `logout/` | Sign out |
|
||||
| `/oauth-refresh` | `oauth-refresh/` | Refresh OAuth tokens |
|
||||
|
||||
## Tasks & Agents
|
||||
|
||||
| Command | Source | Description |
|
||||
|---------|--------|-------------|
|
||||
| `/tasks` | `tasks/` | Manage background tasks |
|
||||
| `/agents` | `agents/` | Manage sub-agents |
|
||||
| `/ultraplan` | `ultraplan.tsx` | Generate a detailed execution plan |
|
||||
| `/plan` | `plan/` | Enter planning mode |
|
||||
|
||||
## Diagnostics & Status
|
||||
|
||||
| Command | Source | Description |
|
||||
|---------|--------|-------------|
|
||||
| `/doctor` | `doctor/` | Run environment diagnostics |
|
||||
| `/status` | `status/` | Show system and session status |
|
||||
| `/stats` | `stats/` | Show session statistics |
|
||||
| `/cost` | `cost/` | Display token usage and estimated cost |
|
||||
| `/version` | `version.ts` | Show Claude Code version |
|
||||
| `/usage` | `usage/` | Show detailed API usage |
|
||||
| `/extra-usage` | `extra-usage/` | Show extended usage details |
|
||||
| `/rate-limit-options` | `rate-limit-options/` | View rate limit configuration |
|
||||
|
||||
## Installation & Setup
|
||||
|
||||
| Command | Source | Description |
|
||||
|---------|--------|-------------|
|
||||
| `/install` | `install.tsx` | Install or update Claude Code |
|
||||
| `/upgrade` | `upgrade/` | Upgrade to the latest version |
|
||||
| `/init` | `init.ts` | Initialize a project (create CLAUDE.md) |
|
||||
| `/init-verifiers` | `init-verifiers.ts` | Set up verifier hooks |
|
||||
| `/onboarding` | `onboarding/` | Run the first-time setup wizard |
|
||||
| `/terminalSetup` | `terminalSetup/` | Configure terminal integration |
|
||||
|
||||
## IDE & Desktop Integration
|
||||
|
||||
| Command | Source | Description |
|
||||
|---------|--------|-------------|
|
||||
| `/bridge` | `bridge/` | Manage IDE bridge connections |
|
||||
| `/bridge-kick` | `bridge-kick.ts` | Force-restart the IDE bridge |
|
||||
| `/ide` | `ide/` | Open in IDE |
|
||||
| `/desktop` | `desktop/` | Hand off to the desktop app |
|
||||
| `/mobile` | `mobile/` | Hand off to the mobile app |
|
||||
| `/teleport` | `teleport/` | Transfer session to another device |
|
||||
|
||||
## Remote & Environment
|
||||
|
||||
| Command | Source | Description |
|
||||
|---------|--------|-------------|
|
||||
| `/remote-env` | `remote-env/` | Configure remote environment |
|
||||
| `/remote-setup` | `remote-setup/` | Set up remote session |
|
||||
| `/env` | `env/` | View environment variables |
|
||||
| `/sandbox-toggle` | `sandbox-toggle/` | Toggle sandbox mode |
|
||||
|
||||
## Misc
|
||||
|
||||
| Command | Source | Description |
|
||||
|---------|--------|-------------|
|
||||
| `/help` | `help/` | Show help and available commands |
|
||||
| `/exit` | `exit/` | Exit Claude Code |
|
||||
| `/copy` | `copy/` | Copy content to clipboard |
|
||||
| `/feedback` | `feedback/` | Send feedback to Anthropic |
|
||||
| `/release-notes` | `release-notes/` | View release notes |
|
||||
| `/rename` | `rename/` | Rename the current session |
|
||||
| `/tag` | `tag/` | Tag the current session |
|
||||
| `/insights` | `insights.ts` | Show codebase insights |
|
||||
| `/stickers` | `stickers/` | Easter egg — stickers |
|
||||
| `/good-claude` | `good-claude/` | Easter egg — praise Claude |
|
||||
| `/voice` | `voice/` | Toggle voice input mode |
|
||||
| `/chrome` | `chrome/` | Chrome extension integration |
|
||||
| `/issue` | `issue/` | File a GitHub issue |
|
||||
| `/statusline` | `statusline.tsx` | Customize the status line |
|
||||
| `/thinkback` | `thinkback/` | Replay Claude's thinking process |
|
||||
| `/thinkback-play` | `thinkback-play/` | Animated thinking replay |
|
||||
| `/passes` | `passes/` | Multi-pass execution |
|
||||
| `/x402` | `x402/` | x402 payment protocol integration |
|
||||
|
||||
## Internal / Debug Commands
|
||||
|
||||
| Command | Source | Description |
|
||||
|---------|--------|-------------|
|
||||
| `/ant-trace` | `ant-trace/` | Anthropic-internal tracing |
|
||||
| `/autofix-pr` | `autofix-pr/` | Auto-fix PR issues |
|
||||
| `/backfill-sessions` | `backfill-sessions/` | Backfill session data |
|
||||
| `/break-cache` | `break-cache/` | Invalidate caches |
|
||||
| `/btw` | `btw/` | "By the way" interjection |
|
||||
| `/ctx_viz` | `ctx_viz/` | Context visualization (debug) |
|
||||
| `/debug-tool-call` | `debug-tool-call/` | Debug a specific tool call |
|
||||
| `/heapdump` | `heapdump/` | Dump heap for memory analysis |
|
||||
| `/hooks` | `hooks/` | Manage hook scripts |
|
||||
| `/mock-limits` | `mock-limits/` | Mock rate limits for testing |
|
||||
| `/perf-issue` | `perf-issue/` | Report performance issues |
|
||||
| `/reset-limits` | `reset-limits/` | Reset rate limit counters |
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Architecture](architecture.md) — How the command system fits into the pipeline
|
||||
- [Tools Reference](tools.md) — Agent tools (different from slash commands)
|
||||
- [Exploration Guide](exploration-guide.md) — Finding command source code
|
||||
246
docs/exploration-guide.md
Normal file
246
docs/exploration-guide.md
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
# Exploration Guide
|
||||
|
||||
> How to navigate and study the Claude Code source code.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
This is a **read-only reference codebase** — there's no build system or test suite. The goal is to understand how a production AI coding assistant is built.
|
||||
|
||||
### Orientation
|
||||
|
||||
| What | Where |
|
||||
|------|-------|
|
||||
| CLI entrypoint | `src/main.tsx` |
|
||||
| Core LLM engine | `src/QueryEngine.ts` (~46K lines) |
|
||||
| Tool definitions | `src/Tool.ts` (~29K lines) |
|
||||
| Command registry | `src/commands.ts` (~25K lines) |
|
||||
| Tool registry | `src/tools.ts` |
|
||||
| Context collection | `src/context.ts` |
|
||||
| All tool implementations | `src/tools/` (40 subdirectories) |
|
||||
| All command implementations | `src/commands/` (~85 subdirectories + 15 files) |
|
||||
|
||||
---
|
||||
|
||||
## Finding Things
|
||||
|
||||
### "How does tool X work?"
|
||||
|
||||
1. Go to `src/tools/{ToolName}/`
|
||||
2. Main implementation is `{ToolName}.ts` or `.tsx`
|
||||
3. UI rendering is in `UI.tsx`
|
||||
4. System prompt contribution is in `prompt.ts`
|
||||
|
||||
Example — understanding BashTool:
|
||||
```
|
||||
src/tools/BashTool/
|
||||
├── BashTool.ts ← Core execution logic
|
||||
├── UI.tsx ← How bash output renders in terminal
|
||||
├── prompt.ts ← What the system prompt says about bash
|
||||
└── ...
|
||||
```
|
||||
|
||||
### "How does command X work?"
|
||||
|
||||
1. Check `src/commands/{command-name}/` (directory) or `src/commands/{command-name}.ts` (file)
|
||||
2. Look for the `getPromptForCommand()` function (PromptCommands) or direct implementation (LocalCommands)
|
||||
|
||||
### "How does feature X work?"
|
||||
|
||||
| Feature | Start Here |
|
||||
|---------|-----------|
|
||||
| Permissions | `src/hooks/toolPermission/` |
|
||||
| IDE bridge | `src/bridge/bridgeMain.ts` |
|
||||
| MCP client | `src/services/mcp/` |
|
||||
| Plugin system | `src/plugins/` + `src/services/plugins/` |
|
||||
| Skills | `src/skills/` |
|
||||
| Voice input | `src/voice/` + `src/services/voice.ts` |
|
||||
| Multi-agent | `src/coordinator/` |
|
||||
| Memory | `src/memdir/` |
|
||||
| Authentication | `src/services/oauth/` |
|
||||
| Config schemas | `src/schemas/` |
|
||||
| State management | `src/state/` |
|
||||
|
||||
### "How does an API call flow?"
|
||||
|
||||
Trace from user input to API response:
|
||||
|
||||
```
|
||||
src/main.tsx ← CLI parsing
|
||||
→ src/replLauncher.tsx ← REPL session start
|
||||
→ src/QueryEngine.ts ← Core engine
|
||||
→ src/services/api/ ← Anthropic SDK client
|
||||
→ (Anthropic API) ← HTTP/streaming
|
||||
← Tool use response
|
||||
→ src/tools/{ToolName}/ ← Tool execution
|
||||
← Tool result
|
||||
→ (feed back to API) ← Continue the loop
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Code Patterns to Recognize
|
||||
|
||||
### `buildTool()` — Tool Factory
|
||||
|
||||
Every tool uses this pattern:
|
||||
|
||||
```typescript
|
||||
export const MyTool = buildTool({
|
||||
name: 'MyTool',
|
||||
inputSchema: z.object({ ... }),
|
||||
async call(args, context) { ... },
|
||||
async checkPermissions(input, context) { ... },
|
||||
})
|
||||
```
|
||||
|
||||
### Feature Flag Gates
|
||||
|
||||
```typescript
|
||||
import { feature } from 'bun:bundle'
|
||||
|
||||
if (feature('VOICE_MODE')) {
|
||||
// This code is stripped at build time if VOICE_MODE is off
|
||||
}
|
||||
```
|
||||
|
||||
### Anthropic-Internal Gates
|
||||
|
||||
```typescript
|
||||
if (process.env.USER_TYPE === 'ant') {
|
||||
// Anthropic employee-only features
|
||||
}
|
||||
```
|
||||
|
||||
### Index Re-exports
|
||||
|
||||
Most directories have an `index.ts` that re-exports the public API:
|
||||
|
||||
```typescript
|
||||
// src/tools/BashTool/index.ts
|
||||
export { BashTool } from './BashTool.js'
|
||||
```
|
||||
|
||||
### Lazy Dynamic Imports
|
||||
|
||||
Heavy modules are loaded only when needed:
|
||||
|
||||
```typescript
|
||||
const { OpenTelemetry } = await import('./heavy-module.js')
|
||||
```
|
||||
|
||||
### ESM with `.js` Extensions
|
||||
|
||||
Bun convention — all imports use `.js` extensions even for `.ts` files:
|
||||
|
||||
```typescript
|
||||
import { something } from './utils.js' // Actually imports utils.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Files by Size
|
||||
|
||||
The largest files contain the most logic and are worth studying:
|
||||
|
||||
| File | Lines | What's Inside |
|
||||
|------|-------|---------------|
|
||||
| `QueryEngine.ts` | ~46K | Streaming, tool loops, retries, token counting |
|
||||
| `Tool.ts` | ~29K | Tool types, `buildTool`, permission models |
|
||||
| `commands.ts` | ~25K | Command registry, conditional loading |
|
||||
| `main.tsx` | — | CLI parser, startup optimization |
|
||||
| `context.ts` | — | OS, shell, git, user context assembly |
|
||||
|
||||
---
|
||||
|
||||
## Study Paths
|
||||
|
||||
### Path 1: "How does a tool work end-to-end?"
|
||||
|
||||
1. Read `src/Tool.ts` — understand the `buildTool` interface
|
||||
2. Pick a simple tool like `FileReadTool` in `src/tools/FileReadTool/`
|
||||
3. Trace how `QueryEngine.ts` calls tools during the tool loop
|
||||
4. See how permissions are checked in `src/hooks/toolPermission/`
|
||||
|
||||
### Path 2: "How does the UI work?"
|
||||
|
||||
1. Read `src/screens/REPL.tsx` — the main screen
|
||||
2. Explore `src/components/` — pick a few components
|
||||
3. See `src/hooks/useTextInput.ts` — how user input is captured
|
||||
4. Check `src/ink/` — the Ink renderer wrapper
|
||||
|
||||
### Path 3: "How does the IDE integration work?"
|
||||
|
||||
1. Start at `src/bridge/bridgeMain.ts`
|
||||
2. Follow `bridgeMessaging.ts` for the message protocol
|
||||
3. See `bridgePermissionCallbacks.ts` for how permissions route to the IDE
|
||||
4. Check `replBridge.ts` for REPL session bridging
|
||||
|
||||
### Path 4: "How do plugins extend Claude Code?"
|
||||
|
||||
1. Read `src/types/plugin.ts` — the plugin API surface
|
||||
2. See `src/services/plugins/` — how plugins are loaded
|
||||
3. Check `src/plugins/builtinPlugins.ts` — built-in examples
|
||||
4. Look at `src/plugins/bundled/` — bundled plugin code
|
||||
|
||||
### Path 5: "How does MCP work?"
|
||||
|
||||
1. Read `src/services/mcp/` — the MCP client
|
||||
2. See `src/tools/MCPTool/` — how MCP tools are invoked
|
||||
3. Check `src/entrypoints/mcp.ts` — Claude Code as an MCP server
|
||||
4. Look at `src/skills/mcpSkillBuilders.ts` — skills from MCP
|
||||
|
||||
---
|
||||
|
||||
## Using the MCP Server for Exploration
|
||||
|
||||
This repo includes a standalone MCP server (`mcp-server/`) that lets any MCP-compatible client explore the source code. See the [MCP Server README](../mcp-server/README.md) for setup.
|
||||
|
||||
Once connected, you can ask an AI assistant to explore the source:
|
||||
|
||||
- "How does the BashTool work?"
|
||||
- "Search for where permissions are checked"
|
||||
- "List all files in the bridge directory"
|
||||
- "Read QueryEngine.ts lines 1-100"
|
||||
|
||||
---
|
||||
|
||||
## Grep Patterns
|
||||
|
||||
Useful grep/ripgrep patterns for finding things:
|
||||
|
||||
```bash
|
||||
# Find all tool definitions
|
||||
rg "buildTool\(" src/tools/
|
||||
|
||||
# Find all command definitions
|
||||
rg "satisfies Command" src/commands/
|
||||
|
||||
# Find feature flag usage
|
||||
rg "feature\(" src/
|
||||
|
||||
# Find Anthropic-internal gates
|
||||
rg "USER_TYPE.*ant" src/
|
||||
|
||||
# Find all React hooks
|
||||
rg "^export function use" src/hooks/
|
||||
|
||||
# Find all Zod schemas
|
||||
rg "z\.object\(" src/schemas/
|
||||
|
||||
# Find all system prompt contributions
|
||||
rg "prompt\(" src/tools/*/prompt.ts
|
||||
|
||||
# Find permission rule patterns
|
||||
rg "checkPermissions" src/tools/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Architecture](architecture.md) — Overall system design
|
||||
- [Tools Reference](tools.md) — Complete tool catalog
|
||||
- [Commands Reference](commands.md) — All slash commands
|
||||
- [Subsystems Guide](subsystems.md) — Deep dives into Bridge, MCP, Permissions, etc.
|
||||
346
docs/subsystems.md
Normal file
346
docs/subsystems.md
Normal file
|
|
@ -0,0 +1,346 @@
|
|||
# Subsystems Guide
|
||||
|
||||
> Detailed documentation of Claude Code's major subsystems.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Bridge (IDE Integration)](#bridge-ide-integration)
|
||||
- [MCP (Model Context Protocol)](#mcp-model-context-protocol)
|
||||
- [Permission System](#permission-system)
|
||||
- [Plugin System](#plugin-system)
|
||||
- [Skill System](#skill-system)
|
||||
- [Task System](#task-system)
|
||||
- [Memory System](#memory-system)
|
||||
- [Coordinator (Multi-Agent)](#coordinator-multi-agent)
|
||||
- [Voice System](#voice-system)
|
||||
- [Service Layer](#service-layer)
|
||||
|
||||
---
|
||||
|
||||
## Bridge (IDE Integration)
|
||||
|
||||
**Location:** `src/bridge/`
|
||||
|
||||
The bridge is a bidirectional communication layer connecting Claude Code's CLI with IDE extensions (VS Code, JetBrains). It allows the CLI to run as a backend for IDE-based interfaces.
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
┌──────────────────┐ ┌──────────────────────┐
|
||||
│ IDE Extension │◄───────►│ Bridge Layer │
|
||||
│ (VS Code, JB) │ JWT │ (src/bridge/) │
|
||||
│ │ Auth │ │
|
||||
│ - UI rendering │ │ - Session mgmt │
|
||||
│ - File watching │ │ - Message routing │
|
||||
│ - Diff display │ │ - Permission proxy │
|
||||
└──────────────────┘ └──────────┬───────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ Claude Code Core │
|
||||
│ (QueryEngine, Tools) │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
### Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `bridgeMain.ts` | Main bridge loop — starts the bidirectional channel |
|
||||
| `bridgeMessaging.ts` | Message protocol (serialize/deserialize) |
|
||||
| `bridgePermissionCallbacks.ts` | Routes permission prompts to the IDE |
|
||||
| `bridgeApi.ts` | API surface exposed to the IDE |
|
||||
| `bridgeConfig.ts` | Bridge configuration |
|
||||
| `replBridge.ts` | Connects the REPL session to the bridge |
|
||||
| `jwtUtils.ts` | JWT-based authentication between CLI and IDE |
|
||||
| `sessionRunner.ts` | Manages bridge session execution |
|
||||
| `createSession.ts` | Creates new bridge sessions |
|
||||
| `trustedDevice.ts` | Device trust verification |
|
||||
| `workSecret.ts` | Workspace-scoped secrets |
|
||||
| `inboundMessages.ts` | Handles messages coming from the IDE |
|
||||
| `inboundAttachments.ts` | Handles file attachments from the IDE |
|
||||
| `types.ts` | TypeScript types for the bridge protocol |
|
||||
|
||||
### Feature Flag
|
||||
|
||||
The bridge is gated behind the `BRIDGE_MODE` feature flag and is stripped from non-IDE builds.
|
||||
|
||||
---
|
||||
|
||||
## MCP (Model Context Protocol)
|
||||
|
||||
**Location:** `src/services/mcp/`
|
||||
|
||||
Claude Code acts as both an **MCP client** (consuming tools/resources from MCP servers) and can run as an **MCP server** (exposing its own tools via `src/entrypoints/mcp.ts`).
|
||||
|
||||
### Client Features
|
||||
|
||||
- **Tool discovery** — Enumerates tools from connected MCP servers
|
||||
- **Resource browsing** — Lists and reads MCP-exposed resources
|
||||
- **Dynamic tool loading** — `ToolSearchTool` discovers tools at runtime
|
||||
- **Authentication** — `McpAuthTool` handles MCP server auth flows
|
||||
- **Connectivity monitoring** — `useMcpConnectivityStatus` hook tracks connection health
|
||||
|
||||
### Server Mode
|
||||
|
||||
When launched via `src/entrypoints/mcp.ts`, Claude Code exposes its own tools and resources via the MCP protocol, allowing other AI agents to use Claude Code as a tool server.
|
||||
|
||||
### Related Tools
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
| `MCPTool` | Invoke tools on connected MCP servers |
|
||||
| `ListMcpResourcesTool` | List available MCP resources |
|
||||
| `ReadMcpResourceTool` | Read a specific MCP resource |
|
||||
| `McpAuthTool` | Authenticate with an MCP server |
|
||||
| `ToolSearchTool` | Discover deferred tools from MCP servers |
|
||||
|
||||
### Configuration
|
||||
|
||||
MCP servers are configured via `/mcp` command or settings files. The server approval flow lives in `src/services/mcpServerApproval.tsx`.
|
||||
|
||||
---
|
||||
|
||||
## Permission System
|
||||
|
||||
**Location:** `src/hooks/toolPermission/`
|
||||
|
||||
Every tool invocation passes through a centralized permission check before execution.
|
||||
|
||||
### Permission Modes
|
||||
|
||||
| Mode | Behavior |
|
||||
|------|----------|
|
||||
| `default` | Prompts the user for each potentially destructive operation |
|
||||
| `plan` | Shows the full execution plan, asks once for batch approval |
|
||||
| `bypassPermissions` | Auto-approves all operations (dangerous — for trusted environments) |
|
||||
| `auto` | ML-based classifier automatically decides (experimental) |
|
||||
|
||||
### How It Works
|
||||
|
||||
1. Tool is invoked by the Query Engine
|
||||
2. `checkPermissions(input, context)` is called on the tool
|
||||
3. Permission handler checks against configured rules
|
||||
4. If not auto-approved, user is prompted via terminal or IDE
|
||||
|
||||
### Permission Rules
|
||||
|
||||
Rules use wildcard patterns to match tool invocations:
|
||||
|
||||
```
|
||||
Bash(git *) # Allow all git commands without prompt
|
||||
Bash(npm test) # Allow 'npm test' specifically
|
||||
FileEdit(/src/*) # Allow edits to anything under src/
|
||||
FileRead(*) # Allow reading any file
|
||||
```
|
||||
|
||||
### Key Files
|
||||
|
||||
| File | Path |
|
||||
|------|------|
|
||||
| Permission context | `src/hooks/toolPermission/PermissionContext.ts` |
|
||||
| Permission handlers | `src/hooks/toolPermission/handlers/` |
|
||||
| Permission logging | `src/hooks/toolPermission/permissionLogging.ts` |
|
||||
| Permission types | `src/types/permissions.ts` |
|
||||
|
||||
---
|
||||
|
||||
## Plugin System
|
||||
|
||||
**Location:** `src/plugins/`, `src/services/plugins/`
|
||||
|
||||
Claude Code supports installable plugins that can extend its capabilities.
|
||||
|
||||
### Structure
|
||||
|
||||
| Component | Location | Purpose |
|
||||
|-----------|----------|---------|
|
||||
| Plugin loader | `src/services/plugins/` | Discovers and loads plugins |
|
||||
| Built-in plugins | `src/plugins/builtinPlugins.ts` | Plugins that ship with Claude Code |
|
||||
| Bundled plugins | `src/plugins/bundled/` | Plugin code bundled into the binary |
|
||||
| Plugin types | `src/types/plugin.ts` | TypeScript types for plugin API |
|
||||
|
||||
### Plugin Lifecycle
|
||||
|
||||
1. **Discovery** — Scans plugin directories and marketplace
|
||||
2. **Installation** — Downloaded and registered (`/plugin` command)
|
||||
3. **Loading** — Initialized at startup or on-demand
|
||||
4. **Execution** — Plugins can contribute tools, commands, and prompts
|
||||
5. **Auto-update** — `usePluginAutoupdateNotification` handles updates
|
||||
|
||||
### Related Commands
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `/plugin` | Install, remove, or manage plugins |
|
||||
| `/reload-plugins` | Reload all installed plugins |
|
||||
|
||||
---
|
||||
|
||||
## Skill System
|
||||
|
||||
**Location:** `src/skills/`
|
||||
|
||||
Skills are reusable, named workflows that bundle prompts and tool configurations for specific tasks.
|
||||
|
||||
### Structure
|
||||
|
||||
| Component | Location | Purpose |
|
||||
|-----------|----------|---------|
|
||||
| Bundled skills | `src/skills/bundled/` | Skills that ship with Claude Code |
|
||||
| Skill loader | `src/skills/loadSkillsDir.ts` | Loads skills from disk |
|
||||
| MCP skill builders | `src/skills/mcpSkillBuilders.ts` | Creates skills from MCP resources |
|
||||
| Skill registry | `src/skills/bundledSkills.ts` | Registration of all bundled skills |
|
||||
|
||||
### Bundled Skills (16)
|
||||
|
||||
| Skill | Purpose |
|
||||
|-------|---------|
|
||||
| `batch` | Batch operations across multiple files |
|
||||
| `claudeApi` | Direct Anthropic API interaction |
|
||||
| `claudeInChrome` | Chrome extension integration |
|
||||
| `debug` | Debugging workflows |
|
||||
| `keybindings` | Keybinding configuration |
|
||||
| `loop` | Iterative refinement loops |
|
||||
| `loremIpsum` | Generate placeholder text |
|
||||
| `remember` | Persist information to memory |
|
||||
| `scheduleRemoteAgents` | Schedule agents for remote execution |
|
||||
| `simplify` | Simplify complex code |
|
||||
| `skillify` | Create new skills from workflows |
|
||||
| `stuck` | Get unstuck when blocked |
|
||||
| `updateConfig` | Modify configuration programmatically |
|
||||
| `verify` / `verifyContent` | Verify code correctness |
|
||||
|
||||
### Execution
|
||||
|
||||
Skills are invoked via the `SkillTool` or the `/skills` command. Users can also create custom skills.
|
||||
|
||||
---
|
||||
|
||||
## Task System
|
||||
|
||||
**Location:** `src/tasks/`
|
||||
|
||||
Manages background and parallel work items — shell tasks, agent tasks, and teammate agents.
|
||||
|
||||
### Task Types
|
||||
|
||||
| Type | Location | Purpose |
|
||||
|------|----------|---------|
|
||||
| `LocalShellTask` | `LocalShellTask/` | Background shell command execution |
|
||||
| `LocalAgentTask` | `LocalAgentTask/` | Sub-agent running locally |
|
||||
| `RemoteAgentTask` | `RemoteAgentTask/` | Agent running on a remote machine |
|
||||
| `InProcessTeammateTask` | `InProcessTeammateTask/` | Parallel teammate agent |
|
||||
| `DreamTask` | `DreamTask/` | Background "dreaming" process |
|
||||
| `LocalMainSessionTask` | `LocalMainSessionTask.ts` | Main session as a task |
|
||||
|
||||
### Task Tools
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
| `TaskCreateTool` | Create a new background task |
|
||||
| `TaskUpdateTool` | Update task status |
|
||||
| `TaskGetTool` | Retrieve task details |
|
||||
| `TaskListTool` | List all tasks |
|
||||
| `TaskOutputTool` | Get task output |
|
||||
| `TaskStopTool` | Stop a running task |
|
||||
|
||||
---
|
||||
|
||||
## Memory System
|
||||
|
||||
**Location:** `src/memdir/`
|
||||
|
||||
Claude Code's persistent memory system, based on `CLAUDE.md` files.
|
||||
|
||||
### Memory Hierarchy
|
||||
|
||||
| Scope | Location | Purpose |
|
||||
|-------|----------|---------|
|
||||
| Project memory | `CLAUDE.md` in project root | Project-specific facts, conventions |
|
||||
| User memory | `~/.claude/CLAUDE.md` | User preferences, cross-project |
|
||||
| Extracted memories | `src/services/extractMemories/` | Auto-extracted from conversations |
|
||||
| Team memory sync | `src/services/teamMemorySync/` | Shared team knowledge |
|
||||
|
||||
### Related
|
||||
|
||||
- `/memory` command for managing memories
|
||||
- `remember` skill for persisting information
|
||||
- `useMemoryUsage` hook for tracking memory size
|
||||
|
||||
---
|
||||
|
||||
## Coordinator (Multi-Agent)
|
||||
|
||||
**Location:** `src/coordinator/`
|
||||
|
||||
Orchestrates multiple agents working in parallel on different aspects of a task.
|
||||
|
||||
### How It Works
|
||||
|
||||
- `coordinatorMode.ts` manages the coordinator lifecycle
|
||||
- `TeamCreateTool` and `TeamDeleteTool` manage agent teams
|
||||
- `SendMessageTool` enables inter-agent communication
|
||||
- `AgentTool` spawns sub-agents
|
||||
|
||||
Gated behind the `COORDINATOR_MODE` feature flag.
|
||||
|
||||
---
|
||||
|
||||
## Voice System
|
||||
|
||||
**Location:** `src/voice/`
|
||||
|
||||
Voice input/output support for hands-free interaction.
|
||||
|
||||
### Components
|
||||
|
||||
| File | Location | Purpose |
|
||||
|------|----------|---------|
|
||||
| Voice service | `src/services/voice.ts` | Core voice processing |
|
||||
| STT streaming | `src/services/voiceStreamSTT.ts` | Speech-to-text streaming |
|
||||
| Key terms | `src/services/voiceKeyterms.ts` | Domain-specific vocabulary |
|
||||
| Voice hooks | `src/hooks/useVoice.ts`, `useVoiceEnabled.ts`, `useVoiceIntegration.tsx` | React hooks |
|
||||
| Voice command | `src/commands/voice/` | `/voice` slash command |
|
||||
|
||||
Gated behind the `VOICE_MODE` feature flag.
|
||||
|
||||
---
|
||||
|
||||
## Service Layer
|
||||
|
||||
**Location:** `src/services/`
|
||||
|
||||
External integrations and shared services.
|
||||
|
||||
| Service | Path | Purpose |
|
||||
|---------|------|---------|
|
||||
| **API** | `api/` | Anthropic SDK client, file uploads, bootstrap |
|
||||
| **MCP** | `mcp/` | MCP client connections and tool discovery |
|
||||
| **OAuth** | `oauth/` | OAuth 2.0 authentication flow |
|
||||
| **LSP** | `lsp/` | Language Server Protocol manager |
|
||||
| **Analytics** | `analytics/` | GrowthBook feature flags, telemetry |
|
||||
| **Plugins** | `plugins/` | Plugin loader and marketplace |
|
||||
| **Compact** | `compact/` | Conversation context compression |
|
||||
| **Policy Limits** | `policyLimits/` | Organization rate limits/quota |
|
||||
| **Remote Settings** | `remoteManagedSettings/` | Enterprise managed settings sync |
|
||||
| **Token Estimation** | `tokenEstimation.ts` | Token count estimation |
|
||||
| **Team Memory** | `teamMemorySync/` | Team knowledge synchronization |
|
||||
| **Tips** | `tips/` | Contextual usage tips |
|
||||
| **Agent Summary** | `AgentSummary/` | Agent work summaries |
|
||||
| **Prompt Suggestion** | `PromptSuggestion/` | Suggested follow-up prompts |
|
||||
| **Session Memory** | `SessionMemory/` | Session-level memory |
|
||||
| **Magic Docs** | `MagicDocs/` | Documentation generation |
|
||||
| **Auto Dream** | `autoDream/` | Background ideation |
|
||||
| **x402** | `x402/` | x402 payment protocol |
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Architecture](architecture.md) — How subsystems connect in the core pipeline
|
||||
- [Tools Reference](tools.md) — Tools related to each subsystem
|
||||
- [Commands Reference](commands.md) — Commands for managing subsystems
|
||||
- [Exploration Guide](exploration-guide.md) — Finding subsystem source code
|
||||
173
docs/tools.md
Normal file
173
docs/tools.md
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
# Tools Reference
|
||||
|
||||
> Complete catalog of all ~40 agent tools in Claude Code.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Every tool lives in `src/tools/<ToolName>/` as a self-contained module. Each tool defines:
|
||||
|
||||
- **Input schema** — Zod-validated parameters
|
||||
- **Permission model** — What requires user approval
|
||||
- **Execution logic** — The tool's implementation
|
||||
- **UI components** — Terminal rendering for invocation and results
|
||||
- **Concurrency safety** — Whether it can run in parallel
|
||||
|
||||
Tools are registered in `src/tools.ts` and invoked by the Query Engine during LLM tool-call loops.
|
||||
|
||||
### Tool Definition Pattern
|
||||
|
||||
```typescript
|
||||
export const MyTool = buildTool({
|
||||
name: 'MyTool',
|
||||
aliases: ['my_tool'],
|
||||
description: 'What this tool does',
|
||||
inputSchema: z.object({
|
||||
param: z.string(),
|
||||
}),
|
||||
async call(args, context, canUseTool, parentMessage, onProgress) {
|
||||
// Execute and return { data: result, newMessages?: [...] }
|
||||
},
|
||||
async checkPermissions(input, context) { /* Permission checks */ },
|
||||
isConcurrencySafe(input) { /* Can run in parallel? */ },
|
||||
isReadOnly(input) { /* Non-destructive? */ },
|
||||
prompt(options) { /* System prompt injection */ },
|
||||
renderToolUseMessage(input, options) { /* UI for invocation */ },
|
||||
renderToolResultMessage(content, progressMessages, options) { /* UI for result */ },
|
||||
})
|
||||
```
|
||||
|
||||
**Directory structure per tool:**
|
||||
|
||||
```
|
||||
src/tools/MyTool/
|
||||
├── MyTool.ts # Main implementation
|
||||
├── UI.tsx # Terminal rendering
|
||||
├── prompt.ts # System prompt contribution
|
||||
└── utils.ts # Tool-specific helpers
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File System Tools
|
||||
|
||||
| Tool | Description | Read-Only |
|
||||
|------|-------------|-----------|
|
||||
| **FileReadTool** | Read file contents (text, images, PDFs, notebooks). Supports line ranges | Yes |
|
||||
| **FileWriteTool** | Create or overwrite files | No |
|
||||
| **FileEditTool** | Partial file modification via string replacement | No |
|
||||
| **GlobTool** | Find files matching glob patterns (e.g. `**/*.ts`) | Yes |
|
||||
| **GrepTool** | Content search using ripgrep (regex-capable) | Yes |
|
||||
| **NotebookEditTool** | Edit Jupyter notebook cells | No |
|
||||
| **TodoWriteTool** | Write to a structured todo/task file | No |
|
||||
|
||||
## Shell & Execution Tools
|
||||
|
||||
| Tool | Description | Read-Only |
|
||||
|------|-------------|-----------|
|
||||
| **BashTool** | Execute shell commands in bash | No |
|
||||
| **PowerShellTool** | Execute PowerShell commands (Windows) | No |
|
||||
| **REPLTool** | Run code in a REPL session (Python, Node, etc.) | No |
|
||||
|
||||
## Agent & Orchestration Tools
|
||||
|
||||
| Tool | Description | Read-Only |
|
||||
|------|-------------|-----------|
|
||||
| **AgentTool** | Spawn a sub-agent for complex tasks | No |
|
||||
| **SendMessageTool** | Send messages between agents | No |
|
||||
| **TeamCreateTool** | Create a team of parallel agents | No |
|
||||
| **TeamDeleteTool** | Remove a team agent | No |
|
||||
| **EnterPlanModeTool** | Switch to planning mode (no execution) | No |
|
||||
| **ExitPlanModeTool** | Exit planning mode, resume execution | No |
|
||||
| **EnterWorktreeTool** | Isolate work in a git worktree | No |
|
||||
| **ExitWorktreeTool** | Exit worktree isolation | No |
|
||||
| **SleepTool** | Pause execution (proactive mode) | Yes |
|
||||
| **SyntheticOutputTool** | Generate structured output | Yes |
|
||||
|
||||
## Task Management Tools
|
||||
|
||||
| Tool | Description | Read-Only |
|
||||
|------|-------------|-----------|
|
||||
| **TaskCreateTool** | Create a new background task | No |
|
||||
| **TaskUpdateTool** | Update a task's status or details | No |
|
||||
| **TaskGetTool** | Get details of a specific task | Yes |
|
||||
| **TaskListTool** | List all tasks | Yes |
|
||||
| **TaskOutputTool** | Get output from a completed task | Yes |
|
||||
| **TaskStopTool** | Stop a running task | No |
|
||||
|
||||
## Web Tools
|
||||
|
||||
| Tool | Description | Read-Only |
|
||||
|------|-------------|-----------|
|
||||
| **WebFetchTool** | Fetch content from a URL | Yes |
|
||||
| **WebSearchTool** | Search the web | Yes |
|
||||
|
||||
## MCP (Model Context Protocol) Tools
|
||||
|
||||
| Tool | Description | Read-Only |
|
||||
|------|-------------|-----------|
|
||||
| **MCPTool** | Invoke tools on connected MCP servers | Varies |
|
||||
| **ListMcpResourcesTool** | List resources exposed by MCP servers | Yes |
|
||||
| **ReadMcpResourceTool** | Read a specific MCP resource | Yes |
|
||||
| **McpAuthTool** | Handle MCP server authentication | No |
|
||||
| **ToolSearchTool** | Discover deferred/dynamic tools from MCP servers | Yes |
|
||||
|
||||
## Integration Tools
|
||||
|
||||
| Tool | Description | Read-Only |
|
||||
|------|-------------|-----------|
|
||||
| **LSPTool** | Language Server Protocol operations (go-to-definition, find references, etc.) | Yes |
|
||||
| **SkillTool** | Execute a registered skill | Varies |
|
||||
|
||||
## Scheduling & Triggers
|
||||
|
||||
| Tool | Description | Read-Only |
|
||||
|------|-------------|-----------|
|
||||
| **ScheduleCronTool** | Create a scheduled cron trigger | No |
|
||||
| **RemoteTriggerTool** | Fire a remote trigger | No |
|
||||
|
||||
## Utility Tools
|
||||
|
||||
| Tool | Description | Read-Only |
|
||||
|------|-------------|-----------|
|
||||
| **AskUserQuestionTool** | Prompt the user for input during execution | Yes |
|
||||
| **BriefTool** | Generate a brief/summary | Yes |
|
||||
| **ConfigTool** | Read or modify Claude Code configuration | No |
|
||||
|
||||
---
|
||||
|
||||
## Permission Model
|
||||
|
||||
Every tool invocation passes through the permission system (`src/hooks/toolPermission/`). Permission modes:
|
||||
|
||||
| Mode | Behavior |
|
||||
|------|----------|
|
||||
| `default` | Prompt the user for each potentially destructive operation |
|
||||
| `plan` | Show the full plan, ask once |
|
||||
| `bypassPermissions` | Auto-approve everything (dangerous) |
|
||||
| `auto` | ML-based classifier decides |
|
||||
|
||||
Permission rules use wildcard patterns:
|
||||
|
||||
```
|
||||
Bash(git *) # Allow all git commands
|
||||
FileEdit(/src/*) # Allow edits to anything in src/
|
||||
FileRead(*) # Allow reading any file
|
||||
```
|
||||
|
||||
Each tool implements `checkPermissions()` returning `{ granted: boolean, reason?, prompt? }`.
|
||||
|
||||
---
|
||||
|
||||
## Tool Presets
|
||||
|
||||
Tools are grouped into presets in `src/tools.ts` for different contexts (e.g. read-only tools for code review, full toolset for development).
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Architecture](architecture.md) — How tools fit into the overall pipeline
|
||||
- [Subsystems Guide](subsystems.md) — MCP, permissions, and other tool-related subsystems
|
||||
- [Exploration Guide](exploration-guide.md) — How to read tool source code
|
||||
43
gitpretty-apply.sh
Normal file
43
gitpretty-apply.sh
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Apply gitpretty's per-file beautification so GitHub file history shows
|
||||
# readable, themed commit messages for each file.
|
||||
|
||||
REPO_PATH="${1:-.}"
|
||||
INSTALL_HOOKS="${2:-}"
|
||||
GITPRETTY_HOME="${HOME}/.gitpretty"
|
||||
|
||||
if ! command -v git >/dev/null 2>&1; then
|
||||
echo "git is required but was not found on PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "${REPO_PATH}/.git" ]; then
|
||||
echo "Target is not a git repository: ${REPO_PATH}"
|
||||
echo "Usage: $0 [repo-path] [--hooks]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "${GITPRETTY_HOME}" ]; then
|
||||
echo "Installing gitpretty into ${GITPRETTY_HOME} ..."
|
||||
git clone https://github.com/nirholas/gitpretty.git "${GITPRETTY_HOME}"
|
||||
fi
|
||||
|
||||
chmod +x "${GITPRETTY_HOME}"/*.sh "${GITPRETTY_HOME}"/scripts/*.sh
|
||||
|
||||
if [ "${INSTALL_HOOKS}" = "--hooks" ]; then
|
||||
echo "Installing gitpretty hooks in ${REPO_PATH} ..."
|
||||
(
|
||||
cd "${REPO_PATH}"
|
||||
"${GITPRETTY_HOME}"/scripts/emoji-hooks.sh install
|
||||
)
|
||||
fi
|
||||
|
||||
echo "Running per-file beautify commits in ${REPO_PATH} ..."
|
||||
"${GITPRETTY_HOME}"/emoji-file-commits.sh "${REPO_PATH}"
|
||||
|
||||
echo "Done. Review with: git -C ${REPO_PATH} log --oneline -n 20"
|
||||
|
||||
|
||||
7
mcp-server/.gitignore
vendored
Normal file
7
mcp-server/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
node_modules/
|
||||
dist/
|
||||
*.js
|
||||
*.d.ts
|
||||
*.js.map
|
||||
*.d.ts.map
|
||||
!api/
|
||||
4
mcp-server/.npmignore
Normal file
4
mcp-server/.npmignore
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
.mcpregistry_*
|
||||
src/
|
||||
tsconfig.json
|
||||
*.ts.new
|
||||
29
mcp-server/Dockerfile
Normal file
29
mcp-server/Dockerfile
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
FROM node:22-slim AS build
|
||||
WORKDIR /app
|
||||
|
||||
# Copy full repo (src/ is needed at runtime for the MCP explorer)
|
||||
COPY . .
|
||||
|
||||
# Build MCP server
|
||||
WORKDIR /app/mcp-server
|
||||
RUN npm ci && npm run build
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
FROM node:22-slim
|
||||
WORKDIR /app
|
||||
|
||||
# Copy built MCP server and source code it explores
|
||||
COPY --from=build /app/mcp-server/dist /app/mcp-server/dist
|
||||
COPY --from=build /app/mcp-server/node_modules /app/mcp-server/node_modules
|
||||
COPY --from=build /app/mcp-server/package.json /app/mcp-server/package.json
|
||||
COPY --from=build /app/src /app/src
|
||||
COPY --from=build /app/README.md /app/README.md
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV CLAUDE_CODE_SRC_ROOT=/app/src
|
||||
ENV PORT=3000
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
WORKDIR /app/mcp-server
|
||||
CMD ["node", "dist/http.js"]
|
||||
278
mcp-server/README.md
Normal file
278
mcp-server/README.md
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
# 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. Supports **STDIO**, **Streamable HTTP**, and **SSE** transports.
|
||||
|
||||
## What It Does
|
||||
|
||||
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
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `list_tools` | List all 40+ agent tools (BashTool, FileEditTool, etc.) |
|
||||
| `list_commands` | List all 50+ slash commands (/commit, /review, etc.) |
|
||||
| `get_tool_source` | Read a specific tool's implementation |
|
||||
| `get_command_source` | Read a specific command's implementation |
|
||||
| `read_source_file` | Read any file from `src/` by relative path |
|
||||
| `search_source` | Regex search across the entire source tree |
|
||||
| `list_directory` | List contents of any directory under `src/` |
|
||||
| `get_architecture` | Get a full architecture overview |
|
||||
|
||||
### Resources
|
||||
|
||||
| URI | Description |
|
||||
|-----|-------------|
|
||||
| `claude-code://architecture` | README / architecture overview |
|
||||
| `claude-code://tools` | Tool registry (JSON) |
|
||||
| `claude-code://commands` | Command registry (JSON) |
|
||||
| `claude-code://source/{path}` | Any source file (template) |
|
||||
|
||||
### Prompts
|
||||
|
||||
| Prompt | Description |
|
||||
|--------|-------------|
|
||||
| `explain_tool` | Deep-dive explanation of a specific tool's purpose, schema, permissions, and flow |
|
||||
| `explain_command` | Explanation of a specific slash command's behavior and implementation |
|
||||
| `architecture_overview` | Guided tour of the full Claude Code architecture |
|
||||
| `how_does_it_work` | Explain a feature/subsystem (permissions, MCP, bridge, etc.) |
|
||||
| `compare_tools` | Side-by-side comparison of two tools |
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
cd mcp-server
|
||||
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
|
||||
|
||||
Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"claude-code-explorer": {
|
||||
"command": "node",
|
||||
"args": ["/absolute/path/to/claude-code/mcp-server/dist/index.js"],
|
||||
"env": {
|
||||
"CLAUDE_CODE_SRC_ROOT": "/absolute/path/to/claude-code/src"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### VS Code (GitHub Copilot)
|
||||
|
||||
Add to `.vscode/mcp.json` in your workspace:
|
||||
|
||||
```json
|
||||
{
|
||||
"servers": {
|
||||
"claude-code-explorer": {
|
||||
"type": "stdio",
|
||||
"command": "node",
|
||||
"args": ["${workspaceFolder}/mcp-server/dist/index.js"],
|
||||
"env": {
|
||||
"CLAUDE_CODE_SRC_ROOT": "${workspaceFolder}/src"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Cursor
|
||||
|
||||
Add to `~/.cursor/mcp.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"claude-code-explorer": {
|
||||
"command": "node",
|
||||
"args": ["/absolute/path/to/claude-code/mcp-server/dist/index.js"],
|
||||
"env": {
|
||||
"CLAUDE_CODE_SRC_ROOT": "/absolute/path/to/claude-code/src"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| 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
|
||||
|
||||
The server also exposes prompt templates for guided exploration:
|
||||
|
||||
| Prompt | Description |
|
||||
|--------|-------------|
|
||||
| `explain_tool` | Deep-dive explanation of a specific tool (input schema, permissions, execution flow) |
|
||||
| `explain_command` | Explain how a slash command works |
|
||||
| `architecture_overview` | Guided tour of the entire Claude Code architecture |
|
||||
| `how_does_it_work` | Explain a feature or subsystem (e.g. "permission system", "MCP client", "query engine") |
|
||||
| `compare_tools` | Side-by-side comparison of two tools |
|
||||
|
||||
## Example Usage
|
||||
|
||||
Once connected, you can ask your AI assistant things like:
|
||||
|
||||
- "List all Claude Code tools"
|
||||
- "Show me the BashTool implementation"
|
||||
- "Search for how permissions are checked"
|
||||
- "What files are in the bridge directory?"
|
||||
- "Read the QueryEngine.ts file, lines 1-100"
|
||||
- "How does the MCP client connection work?"
|
||||
- Use the `explain_tool` prompt with "FileEditTool" to get a full breakdown
|
||||
- Use `how_does_it_work` with "bridge" to understand IDE integration
|
||||
|
||||
## Publishing to MCP Registry
|
||||
|
||||
This server is published to the [MCP Registry](https://registry.modelcontextprotocol.io) via GitHub Actions. On a tagged release (`v*`), the workflow:
|
||||
|
||||
1. Publishes the npm package to npmjs.org
|
||||
2. Authenticates with the MCP Registry using GitHub OIDC
|
||||
3. Publishes the `server.json` metadata to the registry
|
||||
|
||||
To publish manually:
|
||||
|
||||
```bash
|
||||
# Install the MCP Publisher CLI
|
||||
curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
|
||||
|
||||
# Authenticate (GitHub OAuth)
|
||||
./mcp-publisher login github
|
||||
|
||||
# Publish
|
||||
cd mcp-server
|
||||
../mcp-publisher publish
|
||||
```
|
||||
|
||||
Registry name: `io.github.777genius/claude-code-explorer-mcp`
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev # Watch mode TypeScript compilation
|
||||
npm run build # Compile TypeScript to dist/
|
||||
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
|
||||
```
|
||||
|
||||
21
mcp-server/api/index.ts
Normal file
21
mcp-server/api/index.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* Vercel serverless function — proxies requests to the Express HTTP server.
|
||||
*
|
||||
* This file re-exports the Express app as a Vercel serverless handler.
|
||||
* Vercel automatically routes /api/* to this function.
|
||||
*
|
||||
* Deploy:
|
||||
* cd mcp-server && npx vercel
|
||||
*
|
||||
* Environment variables (set in Vercel dashboard):
|
||||
* CLAUDE_CODE_SRC_ROOT — absolute path where src/ is deployed
|
||||
* MCP_API_KEY — optional bearer token for auth
|
||||
*
|
||||
* NOTE: Vercel serverless functions are stateless, so the Streamable HTTP
|
||||
* transport (which requires sessions) won't persist across invocations.
|
||||
* For production use with session-based MCP clients, prefer Railway/Render/VPS.
|
||||
* The legacy SSE transport and stateless tool calls work fine on Vercel.
|
||||
*/
|
||||
|
||||
export { app as default } from "./vercelApp.js";
|
||||
|
||||
96
mcp-server/api/vercelApp.ts
Normal file
96
mcp-server/api/vercelApp.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
/**
|
||||
* Express app exported for Vercel serverless.
|
||||
* Shares the same logic as http.ts but is importable as a module.
|
||||
*/
|
||||
|
||||
import express from "express";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
||||
import { createServer, SRC_ROOT } from "../src/server.js";
|
||||
|
||||
const API_KEY = process.env.MCP_API_KEY;
|
||||
|
||||
export const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// Auth
|
||||
app.use((req, res, next) => {
|
||||
if (!API_KEY || req.path === "/health" || req.path === "/api") return next();
|
||||
const auth = req.headers.authorization;
|
||||
if (!auth || auth !== `Bearer ${API_KEY}`) {
|
||||
res.status(401).json({ error: "Unauthorized" });
|
||||
return;
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// Health
|
||||
app.get("/health", (_req, res) => {
|
||||
res.json({ status: "ok", server: "claude-code-explorer", version: "1.1.0", srcRoot: SRC_ROOT });
|
||||
});
|
||||
app.get("/api", (_req, res) => {
|
||||
res.json({ status: "ok", server: "claude-code-explorer", version: "1.1.0", srcRoot: SRC_ROOT });
|
||||
});
|
||||
|
||||
// Streamable HTTP
|
||||
const transports = new Map<string, StreamableHTTPServerTransport>();
|
||||
|
||||
app.post("/mcp", async (req, res) => {
|
||||
const sessionId = (req.headers["mcp-session-id"] as string) ?? undefined;
|
||||
let transport = sessionId ? transports.get(sessionId) : undefined;
|
||||
|
||||
if (!transport) {
|
||||
const server = createServer();
|
||||
transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() });
|
||||
await server.connect(transport);
|
||||
transport.onclose = () => {
|
||||
if (transport!.sessionId) transports.delete(transport!.sessionId);
|
||||
};
|
||||
}
|
||||
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
|
||||
if (transport.sessionId && !transports.has(transport.sessionId)) {
|
||||
transports.set(transport.sessionId, transport);
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/mcp", async (req, res) => {
|
||||
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
||||
if (!sessionId || !transports.has(sessionId)) {
|
||||
res.status(400).json({ error: "Invalid or missing session ID" });
|
||||
return;
|
||||
}
|
||||
await transports.get(sessionId)!.handleRequest(req, res);
|
||||
});
|
||||
|
||||
app.delete("/mcp", async (req, res) => {
|
||||
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
||||
if (sessionId && transports.has(sessionId)) {
|
||||
await transports.get(sessionId)!.close();
|
||||
transports.delete(sessionId);
|
||||
}
|
||||
res.status(200).json({ ok: true });
|
||||
});
|
||||
|
||||
// Legacy SSE
|
||||
const sseTransports = new Map<string, SSEServerTransport>();
|
||||
|
||||
app.get("/sse", async (_req, res) => {
|
||||
const server = createServer();
|
||||
const transport = new SSEServerTransport("/messages", res);
|
||||
sseTransports.set(transport.sessionId, transport);
|
||||
transport.onclose = () => sseTransports.delete(transport.sessionId);
|
||||
await server.connect(transport);
|
||||
});
|
||||
|
||||
app.post("/messages", async (req, res) => {
|
||||
const sessionId = req.query.sessionId as string;
|
||||
const transport = sseTransports.get(sessionId);
|
||||
if (!transport) {
|
||||
res.status(400).json({ error: "Unknown session" });
|
||||
return;
|
||||
}
|
||||
await transport.handlePostMessage(req, res, req.body);
|
||||
});
|
||||
2183
mcp-server/package-lock.json
generated
Normal file
2183
mcp-server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
32
mcp-server/package.json
Normal file
32
mcp-server/package.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"name": "claude-code-explorer-mcp",
|
||||
"version": "1.1.0",
|
||||
"mcpName": "io.github.777genius/claude-code-explorer-mcp",
|
||||
"description": "MCP server for exploring the Claude Code source code — STDIO, HTTP, and SSE transports",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/777genius/claude-code-source-code.git"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
"claude-code-explorer-mcp": "dist/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"start:http": "node dist/http.js",
|
||||
"dev": "npx tsx src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||
"express": "^4.21.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^22.0.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.7.0"
|
||||
}
|
||||
}
|
||||
|
||||
13
mcp-server/railway.json
Normal file
13
mcp-server/railway.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"$schema": "https://railway.com/railway.schema.json",
|
||||
"build": {
|
||||
"builder": "DOCKERFILE",
|
||||
"dockerfilePath": "mcp-server/Dockerfile"
|
||||
},
|
||||
"deploy": {
|
||||
"startCommand": "node dist/http.js",
|
||||
"healthcheckPath": "/health",
|
||||
"restartPolicyType": "ON_FAILURE",
|
||||
"restartPolicyMaxRetries": 3
|
||||
}
|
||||
}
|
||||
24
mcp-server/server.json
Normal file
24
mcp-server/server.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
||||
"name": "io.github.777genius/claude-code-explorer-mcp",
|
||||
"title": "Claude Code Explorer MCP",
|
||||
"description": "Explore the Claude Code CLI source — browse tools, commands, search code, and more.",
|
||||
"repository": {
|
||||
"url": "https://github.com/777genius/claude-code-source-code",
|
||||
"source": "github",
|
||||
"subfolder": "mcp-server"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
172
mcp-server/src/http.ts
Normal file
172
mcp-server/src/http.ts
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* HTTP + SSE entrypoint — for remote hosting (Railway, Render, VPS, etc.)
|
||||
*
|
||||
* Supports:
|
||||
* - Streamable HTTP transport (POST /mcp for JSON-RPC, GET /mcp for SSE)
|
||||
* - Health check at GET /health
|
||||
*
|
||||
* Environment:
|
||||
* PORT — HTTP port (default: 3000)
|
||||
* CLAUDE_CODE_SRC_ROOT — Path to Claude Code src/ directory
|
||||
* MCP_API_KEY — Optional bearer token for authentication
|
||||
*/
|
||||
|
||||
import express from "express";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
||||
import { createServer, validateSrcRoot, SRC_ROOT } from "./server.js";
|
||||
|
||||
const PORT = parseInt(process.env.PORT ?? "3000", 10);
|
||||
const API_KEY = process.env.MCP_API_KEY;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Auth middleware (optional — only active when MCP_API_KEY is set)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function authMiddleware(
|
||||
req: express.Request,
|
||||
res: express.Response,
|
||||
next: express.NextFunction
|
||||
): void {
|
||||
if (!API_KEY) return next();
|
||||
// Skip auth for health check
|
||||
if (req.path === "/health") return next();
|
||||
|
||||
const auth = req.headers.authorization;
|
||||
if (!auth || auth !== `Bearer ${API_KEY}`) {
|
||||
res.status(401).json({ error: "Unauthorized" });
|
||||
return;
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Streamable HTTP transport (modern MCP protocol)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function startStreamableHTTP(app: express.Express): Promise<void> {
|
||||
// Map of session ID -> transport
|
||||
const transports = new Map<string, StreamableHTTPServerTransport>();
|
||||
|
||||
app.post("/mcp", async (req, res) => {
|
||||
const sessionId =
|
||||
(req.headers["mcp-session-id"] as string) ?? undefined;
|
||||
let transport = sessionId ? transports.get(sessionId) : undefined;
|
||||
|
||||
if (!transport) {
|
||||
// New session
|
||||
const server = createServer();
|
||||
transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => randomUUID(),
|
||||
});
|
||||
await server.connect(transport);
|
||||
|
||||
// Store session after first request so we can retrieve it later
|
||||
transport.onclose = () => {
|
||||
if (transport!.sessionId) {
|
||||
transports.delete(transport!.sessionId);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
|
||||
// After handling, store with the now-known session ID
|
||||
if (transport.sessionId && !transports.has(transport.sessionId)) {
|
||||
transports.set(transport.sessionId, transport);
|
||||
}
|
||||
});
|
||||
|
||||
// SSE stream endpoint for Streamable HTTP
|
||||
app.get("/mcp", async (req, res) => {
|
||||
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
||||
if (!sessionId || !transports.has(sessionId)) {
|
||||
res.status(400).json({ error: "Invalid or missing session ID" });
|
||||
return;
|
||||
}
|
||||
const transport = transports.get(sessionId)!;
|
||||
await transport.handleRequest(req, res);
|
||||
});
|
||||
|
||||
// DELETE — session cleanup
|
||||
app.delete("/mcp", async (req, res) => {
|
||||
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
||||
if (sessionId && transports.has(sessionId)) {
|
||||
const transport = transports.get(sessionId)!;
|
||||
await transport.close();
|
||||
transports.delete(sessionId);
|
||||
}
|
||||
res.status(200).json({ ok: true });
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Legacy SSE transport (for older clients)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function startLegacySSE(app: express.Express): Promise<void> {
|
||||
const transports = new Map<string, SSEServerTransport>();
|
||||
|
||||
app.get("/sse", async (_req, res) => {
|
||||
const server = createServer();
|
||||
const transport = new SSEServerTransport("/messages", res);
|
||||
transports.set(transport.sessionId, transport);
|
||||
transport.onclose = () => {
|
||||
transports.delete(transport.sessionId);
|
||||
};
|
||||
await server.connect(transport);
|
||||
});
|
||||
|
||||
app.post("/messages", async (req, res) => {
|
||||
const sessionId = req.query.sessionId as string;
|
||||
const transport = transports.get(sessionId);
|
||||
if (!transport) {
|
||||
res.status(400).json({ error: "Unknown session" });
|
||||
return;
|
||||
}
|
||||
await transport.handlePostMessage(req, res, req.body);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function main(): Promise<void> {
|
||||
await validateSrcRoot();
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use(authMiddleware);
|
||||
|
||||
// Health check
|
||||
app.get("/health", (_req, res) => {
|
||||
res.json({
|
||||
status: "ok",
|
||||
server: "claude-code-explorer",
|
||||
version: "1.1.0",
|
||||
srcRoot: SRC_ROOT,
|
||||
});
|
||||
});
|
||||
|
||||
// Register both transports
|
||||
await startStreamableHTTP(app);
|
||||
await startLegacySSE(app);
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Claude Code Explorer MCP (HTTP) listening on port ${PORT}`);
|
||||
console.log(` Streamable HTTP: POST/GET http://localhost:${PORT}/mcp`);
|
||||
console.log(` Legacy SSE: GET http://localhost:${PORT}/sse`);
|
||||
console.log(` Health: GET http://localhost:${PORT}/health`);
|
||||
console.log(` Source root: ${SRC_ROOT}`);
|
||||
if (API_KEY) console.log(" Auth: Bearer token required");
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("Fatal error:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
24
mcp-server/src/index.ts
Normal file
24
mcp-server/src/index.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* STDIO entrypoint — for local use with Claude Desktop, Claude Code, etc.
|
||||
*
|
||||
* Usage:
|
||||
* node dist/index.js
|
||||
* CLAUDE_CODE_SRC_ROOT=/path/to/src node dist/index.js
|
||||
*/
|
||||
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { createServer, validateSrcRoot, SRC_ROOT } from "./server.js";
|
||||
|
||||
async function main() {
|
||||
await validateSrcRoot();
|
||||
const server = createServer();
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error(`Claude Code Explorer MCP (stdio) started — src: ${SRC_ROOT}`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("Fatal error:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
24
mcp-server/src/index.ts.new
Normal file
24
mcp-server/src/index.ts.new
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* STDIO entrypoint — for local use with Claude Desktop, Claude Code, etc.
|
||||
*
|
||||
* Usage:
|
||||
* node dist/index.js
|
||||
* CLAUDE_CODE_SRC_ROOT=/path/to/src node dist/index.js
|
||||
*/
|
||||
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { createServer, validateSrcRoot, SRC_ROOT } from "./server.js";
|
||||
|
||||
async function main() {
|
||||
await validateSrcRoot();
|
||||
const server = createServer();
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error(`Claude Code Explorer MCP (stdio) started — src: ${SRC_ROOT}`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("Fatal error:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
958
mcp-server/src/server.ts
Normal file
958
mcp-server/src/server.ts
Normal file
|
|
@ -0,0 +1,958 @@
|
|||
/**
|
||||
* Shared MCP server definition — transport-agnostic.
|
||||
*
|
||||
* Exposes tools and resources for exploring the Claude Code source code.
|
||||
* This module is imported by both the STDIO and HTTP entrypoints.
|
||||
*/
|
||||
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
ListResourcesRequestSchema,
|
||||
ReadResourceRequestSchema,
|
||||
ListResourceTemplatesRequestSchema,
|
||||
ListPromptsRequestSchema,
|
||||
GetPromptRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Configuration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
export const SRC_ROOT = path.resolve(
|
||||
process.env.CLAUDE_CODE_SRC_ROOT ?? path.join(__dirname, "..", "..", "src")
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function dirExists(p: string): Promise<boolean> {
|
||||
try {
|
||||
return (await fs.stat(p)).isDirectory();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fileExists(p: string): Promise<boolean> {
|
||||
try {
|
||||
return (await fs.stat(p)).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function listDir(dir: string): Promise<string[]> {
|
||||
try {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
return entries
|
||||
.map((e) => (e.isDirectory() ? e.name + "/" : e.name))
|
||||
.sort();
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function walkFiles(root: string, rel = ""): Promise<string[]> {
|
||||
const results: string[] = [];
|
||||
let entries;
|
||||
try {
|
||||
entries = await fs.readdir(path.join(root, rel), { withFileTypes: true });
|
||||
} catch {
|
||||
return results;
|
||||
}
|
||||
for (const e of entries) {
|
||||
const child = rel ? `${rel}/${e.name}` : e.name;
|
||||
if (e.isDirectory()) {
|
||||
results.push(...(await walkFiles(root, child)));
|
||||
} else {
|
||||
results.push(child);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/** Safely resolve a user-supplied relative path under SRC_ROOT (blocks path traversal). */
|
||||
function safePath(relPath: string): string | null {
|
||||
const resolved = path.resolve(SRC_ROOT, relPath);
|
||||
if (!resolved.startsWith(SRC_ROOT)) return null;
|
||||
return resolved;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Metadata Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface ToolInfo {
|
||||
name: string;
|
||||
directory: string;
|
||||
files: string[];
|
||||
}
|
||||
|
||||
interface CommandInfo {
|
||||
name: string;
|
||||
path: string;
|
||||
isDirectory: boolean;
|
||||
files?: string[];
|
||||
}
|
||||
|
||||
async function getToolList(): Promise<ToolInfo[]> {
|
||||
const toolsDir = path.join(SRC_ROOT, "tools");
|
||||
const entries = await fs.readdir(toolsDir, { withFileTypes: true });
|
||||
const tools: ToolInfo[] = [];
|
||||
for (const e of entries) {
|
||||
if (!e.isDirectory() || e.name === "shared" || e.name === "testing")
|
||||
continue;
|
||||
const files = await listDir(path.join(toolsDir, e.name));
|
||||
tools.push({ name: e.name, directory: `tools/${e.name}`, files });
|
||||
}
|
||||
return tools.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
async function getCommandList(): Promise<CommandInfo[]> {
|
||||
const cmdsDir = path.join(SRC_ROOT, "commands");
|
||||
const entries = await fs.readdir(cmdsDir, { withFileTypes: true });
|
||||
const commands: CommandInfo[] = [];
|
||||
for (const e of entries) {
|
||||
if (e.isDirectory()) {
|
||||
const files = await listDir(path.join(cmdsDir, e.name));
|
||||
commands.push({
|
||||
name: e.name,
|
||||
path: `commands/${e.name}`,
|
||||
isDirectory: true,
|
||||
files,
|
||||
});
|
||||
} else {
|
||||
commands.push({
|
||||
name: e.name.replace(/\.(ts|tsx)$/, ""),
|
||||
path: `commands/${e.name}`,
|
||||
isDirectory: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
return commands.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Server Factory
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function createServer(): Server {
|
||||
const server = new Server(
|
||||
{ name: "claude-code-explorer", version: "1.1.0" },
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
resources: {},
|
||||
prompts: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// ---- Resources ---------------------------------------------------------
|
||||
|
||||
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
||||
resources: [
|
||||
{
|
||||
uri: "claude-code://architecture",
|
||||
name: "Architecture Overview",
|
||||
description:
|
||||
"High-level overview of the Claude Code source architecture",
|
||||
mimeType: "text/markdown",
|
||||
},
|
||||
{
|
||||
uri: "claude-code://tools",
|
||||
name: "Tool Registry",
|
||||
description: "List of all agent tools with their files",
|
||||
mimeType: "application/json",
|
||||
},
|
||||
{
|
||||
uri: "claude-code://commands",
|
||||
name: "Command Registry",
|
||||
description: "List of all slash commands",
|
||||
mimeType: "application/json",
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
server.setRequestHandler(
|
||||
ListResourceTemplatesRequestSchema,
|
||||
async () => ({
|
||||
resourceTemplates: [
|
||||
{
|
||||
uriTemplate: "claude-code://source/{path}",
|
||||
name: "Source file",
|
||||
description:
|
||||
"Read a source file from the Claude Code src/ directory",
|
||||
mimeType: "text/plain",
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
server.setRequestHandler(
|
||||
ReadResourceRequestSchema,
|
||||
async (request: { params: { uri: string } }) => {
|
||||
const { uri } = request.params;
|
||||
|
||||
if (uri === "claude-code://architecture") {
|
||||
const readmePath = path.resolve(SRC_ROOT, "..", "README.md");
|
||||
let text: string;
|
||||
try {
|
||||
text = await fs.readFile(readmePath, "utf-8");
|
||||
} catch {
|
||||
text = "README.md not found.";
|
||||
}
|
||||
return { contents: [{ uri, mimeType: "text/markdown", text }] };
|
||||
}
|
||||
|
||||
if (uri === "claude-code://tools") {
|
||||
const tools = await getToolList();
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri,
|
||||
mimeType: "application/json",
|
||||
text: JSON.stringify(tools, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (uri === "claude-code://commands") {
|
||||
const commands = await getCommandList();
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri,
|
||||
mimeType: "application/json",
|
||||
text: JSON.stringify(commands, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (uri.startsWith("claude-code://source/")) {
|
||||
const relPath = uri.slice("claude-code://source/".length);
|
||||
const abs = safePath(relPath);
|
||||
if (!abs) throw new Error("Invalid path");
|
||||
const text = await fs.readFile(abs, "utf-8");
|
||||
return { contents: [{ uri, mimeType: "text/plain", text }] };
|
||||
}
|
||||
|
||||
throw new Error(`Unknown resource: ${uri}`);
|
||||
}
|
||||
);
|
||||
|
||||
// ---- Tools -------------------------------------------------------------
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: [
|
||||
{
|
||||
name: "list_tools",
|
||||
description:
|
||||
"List all Claude Code agent tools (BashTool, FileReadTool, etc.) with their source files.",
|
||||
inputSchema: { type: "object" as const, properties: {} },
|
||||
},
|
||||
{
|
||||
name: "list_commands",
|
||||
description:
|
||||
"List all Claude Code slash commands (/commit, /review, /mcp, etc.) with their source files.",
|
||||
inputSchema: { type: "object" as const, properties: {} },
|
||||
},
|
||||
{
|
||||
name: "get_tool_source",
|
||||
description:
|
||||
"Read the full source code of a specific Claude Code tool implementation.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
toolName: {
|
||||
type: "string",
|
||||
description: "Tool directory name, e.g. 'BashTool'",
|
||||
},
|
||||
fileName: {
|
||||
type: "string",
|
||||
description:
|
||||
"Specific file within the tool directory. Omit for the main file.",
|
||||
},
|
||||
},
|
||||
required: ["toolName"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get_command_source",
|
||||
description:
|
||||
"Read the source code of a specific Claude Code slash command.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
commandName: {
|
||||
type: "string",
|
||||
description: "Command name, e.g. 'commit', 'review'",
|
||||
},
|
||||
fileName: {
|
||||
type: "string",
|
||||
description: "Specific file within the command directory.",
|
||||
},
|
||||
},
|
||||
required: ["commandName"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "read_source_file",
|
||||
description:
|
||||
"Read any source file from the Claude Code src/ directory by relative path.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
path: {
|
||||
type: "string",
|
||||
description: "Relative path from src/, e.g. 'QueryEngine.ts'",
|
||||
},
|
||||
startLine: {
|
||||
type: "number",
|
||||
description: "1-based start line.",
|
||||
},
|
||||
endLine: {
|
||||
type: "number",
|
||||
description: "1-based end line.",
|
||||
},
|
||||
},
|
||||
required: ["path"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "search_source",
|
||||
description:
|
||||
"Search for a regex pattern across the Claude Code source. Returns matching lines with paths and line numbers.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
pattern: {
|
||||
type: "string",
|
||||
description: "Search pattern (regex).",
|
||||
},
|
||||
filePattern: {
|
||||
type: "string",
|
||||
description: "File extension filter, e.g. '.ts'",
|
||||
},
|
||||
maxResults: {
|
||||
type: "number",
|
||||
description: "Max matches (default: 50).",
|
||||
},
|
||||
},
|
||||
required: ["pattern"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_directory",
|
||||
description: "List files and subdirectories under src/.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
path: {
|
||||
type: "string",
|
||||
description: "Relative path from src/, e.g. 'services'. '' for root.",
|
||||
},
|
||||
},
|
||||
required: ["path"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get_architecture",
|
||||
description:
|
||||
"Get a high-level architecture overview of Claude Code.",
|
||||
inputSchema: { type: "object" as const, properties: {} },
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
server.setRequestHandler(
|
||||
CallToolRequestSchema,
|
||||
async (request: {
|
||||
params: { name: string; arguments?: Record<string, unknown> };
|
||||
}) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
switch (name) {
|
||||
case "list_tools": {
|
||||
const tools = await getToolList();
|
||||
return {
|
||||
content: [
|
||||
{ type: "text" as const, text: JSON.stringify(tools, null, 2) },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "list_commands": {
|
||||
const commands = await getCommandList();
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: JSON.stringify(commands, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "get_tool_source": {
|
||||
const toolName = (args as Record<string, unknown>)
|
||||
?.toolName as string;
|
||||
if (!toolName) throw new Error("toolName is required");
|
||||
const toolDir = safePath(`tools/${toolName}`);
|
||||
if (!toolDir || !(await dirExists(toolDir)))
|
||||
throw new Error(`Tool not found: ${toolName}`);
|
||||
|
||||
let fileName = (args as Record<string, unknown>)?.fileName as
|
||||
| string
|
||||
| undefined;
|
||||
if (!fileName) {
|
||||
const files = await listDir(toolDir);
|
||||
const main =
|
||||
files.find(
|
||||
(f) => f === `${toolName}.ts` || f === `${toolName}.tsx`
|
||||
) ??
|
||||
files.find((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
|
||||
if (!main) throw new Error(`No source files in ${toolName}`);
|
||||
fileName = main;
|
||||
}
|
||||
|
||||
const filePath = safePath(`tools/${toolName}/${fileName}`);
|
||||
if (!filePath || !(await fileExists(filePath)))
|
||||
throw new Error(`File not found: tools/${toolName}/${fileName}`);
|
||||
const content = await fs.readFile(filePath, "utf-8");
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: `// tools/${toolName}/${fileName}\n// ${content.split("\n").length} lines\n\n${content}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "get_command_source": {
|
||||
const commandName = (args as Record<string, unknown>)
|
||||
?.commandName as string;
|
||||
if (!commandName) throw new Error("commandName is required");
|
||||
|
||||
const candidates = [
|
||||
`commands/${commandName}`,
|
||||
`commands/${commandName}.ts`,
|
||||
`commands/${commandName}.tsx`,
|
||||
];
|
||||
let found: string | null = null;
|
||||
let isDir = false;
|
||||
for (const c of candidates) {
|
||||
const abs = safePath(c);
|
||||
if (abs && (await dirExists(abs))) {
|
||||
found = abs;
|
||||
isDir = true;
|
||||
break;
|
||||
}
|
||||
if (abs && (await fileExists(abs))) {
|
||||
found = abs;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) throw new Error(`Command not found: ${commandName}`);
|
||||
|
||||
if (!isDir) {
|
||||
const content = await fs.readFile(found, "utf-8");
|
||||
return { content: [{ type: "text" as const, text: content }] };
|
||||
}
|
||||
|
||||
const reqFile = (args as Record<string, unknown>)?.fileName as
|
||||
| string
|
||||
| undefined;
|
||||
if (reqFile) {
|
||||
const filePath = safePath(`commands/${commandName}/${reqFile}`);
|
||||
if (!filePath || !(await fileExists(filePath)))
|
||||
throw new Error(
|
||||
`File not found: commands/${commandName}/${reqFile}`
|
||||
);
|
||||
const content = await fs.readFile(filePath, "utf-8");
|
||||
return { content: [{ type: "text" as const, text: content }] };
|
||||
}
|
||||
|
||||
const files = await listDir(found);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: `Command: ${commandName}\nFiles:\n${files.map((f) => ` ${f}`).join("\n")}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "read_source_file": {
|
||||
const relPath = (args as Record<string, unknown>)?.path as string;
|
||||
if (!relPath) throw new Error("path is required");
|
||||
const abs = safePath(relPath);
|
||||
if (!abs || !(await fileExists(abs)))
|
||||
throw new Error(`File not found: ${relPath}`);
|
||||
const content = await fs.readFile(abs, "utf-8");
|
||||
const lines = content.split("\n");
|
||||
const start =
|
||||
((args as Record<string, unknown>)?.startLine as number) ?? 1;
|
||||
const end =
|
||||
((args as Record<string, unknown>)?.endLine as number) ??
|
||||
lines.length;
|
||||
const slice = lines.slice(
|
||||
Math.max(0, start - 1),
|
||||
Math.min(lines.length, end)
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: slice
|
||||
.map(
|
||||
(l: string, i: number) =>
|
||||
`${(start + i).toString().padStart(5)} | ${l}`
|
||||
)
|
||||
.join("\n"),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "search_source": {
|
||||
const pattern = (args as Record<string, unknown>)
|
||||
?.pattern as string;
|
||||
if (!pattern) throw new Error("pattern is required");
|
||||
const filePattern = (args as Record<string, unknown>)
|
||||
?.filePattern as string | undefined;
|
||||
const maxResults =
|
||||
((args as Record<string, unknown>)?.maxResults as number) ?? 50;
|
||||
|
||||
let regex: RegExp;
|
||||
try {
|
||||
regex = new RegExp(pattern, "i");
|
||||
} catch {
|
||||
throw new Error(`Invalid regex pattern: ${pattern}`);
|
||||
}
|
||||
|
||||
const allFiles = await walkFiles(SRC_ROOT);
|
||||
const filtered = filePattern
|
||||
? allFiles.filter((f) => f.endsWith(filePattern))
|
||||
: allFiles;
|
||||
|
||||
const matches: string[] = [];
|
||||
for (const file of filtered) {
|
||||
if (matches.length >= maxResults) break;
|
||||
const abs = path.join(SRC_ROOT, file);
|
||||
let content: string;
|
||||
try {
|
||||
content = await fs.readFile(abs, "utf-8");
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
const lines = content.split("\n");
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (matches.length >= maxResults) break;
|
||||
if (regex.test(lines[i]!)) {
|
||||
matches.push(`${file}:${i + 1}: ${lines[i]!.trim()}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text:
|
||||
matches.length > 0
|
||||
? `Found ${matches.length} match(es):\n\n${matches.join("\n")}`
|
||||
: "No matches found.",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "list_directory": {
|
||||
const relPath =
|
||||
((args as Record<string, unknown>)?.path as string) ?? "";
|
||||
const abs = safePath(relPath);
|
||||
if (!abs || !(await dirExists(abs)))
|
||||
throw new Error(`Directory not found: ${relPath}`);
|
||||
const entries = await listDir(abs);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text:
|
||||
entries.length > 0
|
||||
? entries.join("\n")
|
||||
: "(empty directory)",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "get_architecture": {
|
||||
const topLevel = await listDir(SRC_ROOT);
|
||||
const tools = await getToolList();
|
||||
const commands = await getCommandList();
|
||||
|
||||
const overview = `# Claude Code Architecture Overview
|
||||
|
||||
## Source Root
|
||||
${SRC_ROOT}
|
||||
|
||||
## Top-Level Entries
|
||||
${topLevel.map((e) => `- ${e}`).join("\n")}
|
||||
|
||||
## Agent Tools (${tools.length})
|
||||
${tools.map((t) => `- **${t.name}** — ${t.files.length} files: ${t.files.join(", ")}`).join("\n")}
|
||||
|
||||
## Slash Commands (${commands.length})
|
||||
${commands.map((c) => `- **${c.name}** ${c.isDirectory ? "(directory)" : "(file)"}${c.files ? ": " + c.files.join(", ") : ""}`).join("\n")}
|
||||
|
||||
## Key Files
|
||||
- **main.tsx** — CLI entrypoint (Commander.js)
|
||||
- **QueryEngine.ts** — Core LLM API caller, streaming, tool loops
|
||||
- **Tool.ts** — Base tool types, schemas, permission model
|
||||
- **commands.ts** — Command registry and loader
|
||||
- **tools.ts** — Tool registry and loader
|
||||
- **context.ts** — System/user context collection
|
||||
|
||||
## Core Subsystems
|
||||
- **bridge/** — IDE integration (VS Code, JetBrains)
|
||||
- **coordinator/** — Multi-agent orchestration
|
||||
- **services/mcp/** — MCP client connections
|
||||
- **services/api/** — Anthropic API client
|
||||
- **plugins/** — Plugin system
|
||||
- **skills/** — Skill system
|
||||
- **tasks/** — Background task management
|
||||
- **server/** — Server/remote mode
|
||||
`;
|
||||
return { content: [{ type: "text" as const, text: overview }] };
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ---- Prompts -----------------------------------------------------------
|
||||
|
||||
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
||||
prompts: [
|
||||
{
|
||||
name: "explain_tool",
|
||||
description:
|
||||
"Explain how a specific Claude Code tool works, including its input schema, permissions, and execution flow.",
|
||||
arguments: [
|
||||
{
|
||||
name: "toolName",
|
||||
description: "Tool directory name, e.g. 'BashTool', 'FileEditTool'",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "explain_command",
|
||||
description: "Explain how a specific Claude Code slash command works.",
|
||||
arguments: [
|
||||
{
|
||||
name: "commandName",
|
||||
description: "Command name, e.g. 'commit', 'review', 'mcp'",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "architecture_overview",
|
||||
description:
|
||||
"Get a guided tour of the Claude Code architecture with explanations of each subsystem.",
|
||||
},
|
||||
{
|
||||
name: "how_does_it_work",
|
||||
description:
|
||||
"Explain how a specific feature or subsystem of Claude Code works.",
|
||||
arguments: [
|
||||
{
|
||||
name: "feature",
|
||||
description:
|
||||
"Feature or subsystem, e.g. 'permission system', 'MCP client', 'query engine', 'bridge'",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "compare_tools",
|
||||
description:
|
||||
"Compare two Claude Code tools side by side — purpose, inputs, permissions, implementation.",
|
||||
arguments: [
|
||||
{ name: "tool1", description: "First tool name", required: true },
|
||||
{ name: "tool2", description: "Second tool name", required: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
server.setRequestHandler(
|
||||
GetPromptRequestSchema,
|
||||
async (request: {
|
||||
params: { name: string; arguments?: Record<string, string> };
|
||||
}) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
switch (name) {
|
||||
case "explain_tool": {
|
||||
const toolName = args?.toolName;
|
||||
if (!toolName) throw new Error("toolName argument is required");
|
||||
const toolDir = safePath(`tools/${toolName}`);
|
||||
if (!toolDir || !(await dirExists(toolDir)))
|
||||
throw new Error(`Tool not found: ${toolName}`);
|
||||
const files = await listDir(toolDir);
|
||||
const mainFile =
|
||||
files.find(
|
||||
(f) => f === `${toolName}.ts` || f === `${toolName}.tsx`
|
||||
) ?? files.find((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
|
||||
let source = "";
|
||||
if (mainFile) {
|
||||
source = await fs.readFile(path.join(toolDir, mainFile), "utf-8");
|
||||
}
|
||||
return {
|
||||
description: `Explanation of the ${toolName} tool`,
|
||||
messages: [
|
||||
{
|
||||
role: "user" as const,
|
||||
content: {
|
||||
type: "text" as const,
|
||||
text: `Analyze and explain this Claude Code tool implementation. Cover:\n1. Purpose\n2. Input Schema\n3. Permissions\n4. Execution Flow\n5. Output\n6. Safety characteristics\n\nFiles in tools/${toolName}/: ${files.join(", ")}\n\nMain source (${mainFile ?? "not found"}):\n\`\`\`typescript\n${source}\n\`\`\``,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "explain_command": {
|
||||
const commandName = args?.commandName;
|
||||
if (!commandName)
|
||||
throw new Error("commandName argument is required");
|
||||
const candidates = [
|
||||
`commands/${commandName}`,
|
||||
`commands/${commandName}.ts`,
|
||||
`commands/${commandName}.tsx`,
|
||||
];
|
||||
let found: string | null = null;
|
||||
let isDir = false;
|
||||
for (const c of candidates) {
|
||||
const abs = safePath(c);
|
||||
if (abs && (await dirExists(abs))) {
|
||||
found = abs;
|
||||
isDir = true;
|
||||
break;
|
||||
}
|
||||
if (abs && (await fileExists(abs))) {
|
||||
found = abs;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) throw new Error(`Command not found: ${commandName}`);
|
||||
let source = "";
|
||||
let fileList = "";
|
||||
if (isDir) {
|
||||
const files = await listDir(found);
|
||||
fileList = files.join(", ");
|
||||
const indexFile = files.find(
|
||||
(f) => f === "index.ts" || f === "index.tsx"
|
||||
);
|
||||
if (indexFile) {
|
||||
source = await fs.readFile(
|
||||
path.join(found, indexFile),
|
||||
"utf-8"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
source = await fs.readFile(found, "utf-8");
|
||||
fileList = path.basename(found);
|
||||
}
|
||||
return {
|
||||
description: `Explanation of the /${commandName} command`,
|
||||
messages: [
|
||||
{
|
||||
role: "user" as const,
|
||||
content: {
|
||||
type: "text" as const,
|
||||
text: `Analyze and explain this Claude Code slash command. Cover:\n1. Purpose\n2. Type (prompt vs action)\n3. Allowed Tools\n4. Arguments\n5. Implementation\n\nFiles: ${fileList}\n\nSource:\n\`\`\`typescript\n${source}\n\`\`\``,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "architecture_overview": {
|
||||
const readmePath = path.resolve(SRC_ROOT, "..", "README.md");
|
||||
let readme = "";
|
||||
try {
|
||||
readme = await fs.readFile(readmePath, "utf-8");
|
||||
} catch {
|
||||
/* */
|
||||
}
|
||||
const topLevel = await listDir(SRC_ROOT);
|
||||
const tools = await getToolList();
|
||||
const commands = await getCommandList();
|
||||
return {
|
||||
description: "Architecture overview of Claude Code",
|
||||
messages: [
|
||||
{
|
||||
role: "user" as const,
|
||||
content: {
|
||||
type: "text" as const,
|
||||
text: `Give a comprehensive guided tour of the Claude Code architecture.\n\n## README\n${readme}\n\n## src/ entries\n${topLevel.join("\n")}\n\n## Tools (${tools.length})\n${tools.map((t) => `- ${t.name}: ${t.files.join(", ")}`).join("\n")}\n\n## Commands (${commands.length})\n${commands.map((c) => `- ${c.name} ${c.isDirectory ? "(dir)" : "(file)"}`).join("\n")}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "how_does_it_work": {
|
||||
const feature = args?.feature;
|
||||
if (!feature) throw new Error("feature argument is required");
|
||||
const featureMap: Record<string, string[]> = {
|
||||
"permission system": [
|
||||
"utils/permissions/",
|
||||
"hooks/toolPermission/",
|
||||
"Tool.ts",
|
||||
],
|
||||
permissions: [
|
||||
"utils/permissions/",
|
||||
"hooks/toolPermission/",
|
||||
"Tool.ts",
|
||||
],
|
||||
"mcp client": [
|
||||
"services/mcp/",
|
||||
"tools/MCPTool/",
|
||||
"tools/ListMcpResourcesTool/",
|
||||
],
|
||||
mcp: ["services/mcp/", "entrypoints/mcp.ts", "tools/MCPTool/"],
|
||||
"tool system": ["Tool.ts", "tools.ts", "tools/"],
|
||||
tools: ["Tool.ts", "tools.ts"],
|
||||
"query engine": ["QueryEngine.ts", "query/"],
|
||||
bridge: ["bridge/"],
|
||||
"ide integration": ["bridge/"],
|
||||
context: ["context.ts", "context/"],
|
||||
commands: ["commands.ts", "types/command.ts"],
|
||||
"command system": ["commands.ts", "types/command.ts", "commands/"],
|
||||
plugins: ["plugins/"],
|
||||
skills: ["skills/"],
|
||||
tasks: ["tasks.ts", "tasks/", "tools/TaskCreateTool/"],
|
||||
coordinator: ["coordinator/", "tools/AgentTool/"],
|
||||
"multi-agent": ["coordinator/", "tools/AgentTool/"],
|
||||
memory: ["memdir/", "commands/memory/"],
|
||||
voice: ["voice/"],
|
||||
server: ["server/"],
|
||||
};
|
||||
const paths = featureMap[feature.toLowerCase()] ?? [];
|
||||
let contextFiles = "";
|
||||
for (const p of paths) {
|
||||
const abs = safePath(p);
|
||||
if (!abs) continue;
|
||||
try {
|
||||
const stat = await fs.stat(abs);
|
||||
if (stat.isDirectory()) {
|
||||
const files = await listDir(abs);
|
||||
contextFiles += `\n### ${p}\nFiles: ${files.join(", ")}\n`;
|
||||
} else {
|
||||
const content = await fs.readFile(abs, "utf-8");
|
||||
const preview = content.split("\n").slice(0, 200).join("\n");
|
||||
contextFiles += `\n### ${p} (first 200 lines)\n\`\`\`typescript\n${preview}\n\`\`\`\n`;
|
||||
}
|
||||
} catch {
|
||||
/* skip */
|
||||
}
|
||||
}
|
||||
return {
|
||||
description: `How ${feature} works in Claude Code`,
|
||||
messages: [
|
||||
{
|
||||
role: "user" as const,
|
||||
content: {
|
||||
type: "text" as const,
|
||||
text: `Explain how "${feature}" works in the Claude Code CLI.\n${contextFiles || "(No specific files mapped — use search_source and read_source_file to find relevant code.)"}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "compare_tools": {
|
||||
const tool1 = args?.tool1;
|
||||
const tool2 = args?.tool2;
|
||||
if (!tool1 || !tool2)
|
||||
throw new Error("Both tool1 and tool2 arguments are required");
|
||||
const sources: string[] = [];
|
||||
for (const toolName of [tool1, tool2]) {
|
||||
const toolDir = safePath(`tools/${toolName}`);
|
||||
if (!toolDir || !(await dirExists(toolDir))) {
|
||||
sources.push(`// Tool not found: ${toolName}`);
|
||||
continue;
|
||||
}
|
||||
const files = await listDir(toolDir);
|
||||
const mainFile =
|
||||
files.find(
|
||||
(f) => f === `${toolName}.ts` || f === `${toolName}.tsx`
|
||||
) ?? files.find((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
|
||||
if (mainFile) {
|
||||
const content = await fs.readFile(
|
||||
path.join(toolDir, mainFile),
|
||||
"utf-8"
|
||||
);
|
||||
sources.push(`// tools/${toolName}/${mainFile}\n${content}`);
|
||||
} else {
|
||||
sources.push(`// No main source found for ${toolName}`);
|
||||
}
|
||||
}
|
||||
return {
|
||||
description: `Comparison of ${tool1} vs ${tool2}`,
|
||||
messages: [
|
||||
{
|
||||
role: "user" as const,
|
||||
content: {
|
||||
type: "text" as const,
|
||||
text: `Compare these two Claude Code tools:\n\n## ${tool1}\n\`\`\`typescript\n${sources[0]}\n\`\`\`\n\n## ${tool2}\n\`\`\`typescript\n${sources[1]}\n\`\`\``,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown prompt: ${name}`);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
/** Validate that SRC_ROOT exists. Exits with error if not. */
|
||||
export async function validateSrcRoot(): Promise<void> {
|
||||
if (!(await dirExists(SRC_ROOT))) {
|
||||
console.error(
|
||||
`Error: Claude Code src/ directory not found at ${SRC_ROOT}`
|
||||
);
|
||||
console.error(
|
||||
"Set CLAUDE_CODE_SRC_ROOT environment variable to the src/ directory path."
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
21
mcp-server/tsconfig.json
Normal file
21
mcp-server/tsconfig.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/**/*", "api/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
|
||||
3545
package-lock.json
generated
Normal file
3545
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
94
package.json
Normal file
94
package.json
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
{
|
||||
"name": "@anthropic-ai/claude-code",
|
||||
"version": "0.0.0-leaked",
|
||||
"description": "Anthropic Claude Code CLI — leaked source (2026-03-31). Not an official release.",
|
||||
"license": "UNLICENSED",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/entrypoints/cli.tsx",
|
||||
"bin": {
|
||||
"claude": "src/entrypoints/cli.tsx"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "bun scripts/build-bundle.ts",
|
||||
"build:watch": "bun scripts/build-bundle.ts --watch",
|
||||
"build:prod": "bun scripts/build-bundle.ts --minify",
|
||||
"build:web": "bun scripts/build-web.ts",
|
||||
"build:web:watch": "bun scripts/build-web.ts --watch",
|
||||
"build:web:prod": "bun scripts/build-web.ts --minify",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "biome check src/",
|
||||
"lint:fix": "biome check --write src/",
|
||||
"format": "biome format --write src/",
|
||||
"check": "biome check src/ && tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.39.0",
|
||||
"@commander-js/extra-typings": "^13.1.0",
|
||||
"@growthbook/growthbook": "^1.4.0",
|
||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/api-logs": "^0.57.0",
|
||||
"@opentelemetry/core": "^1.30.0",
|
||||
"@opentelemetry/sdk-logs": "^0.57.0",
|
||||
"@opentelemetry/sdk-metrics": "^1.30.0",
|
||||
"@opentelemetry/sdk-trace-base": "^1.30.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-search": "^0.15.0",
|
||||
"@xterm/addon-unicode11": "^0.8.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
"@xterm/addon-webgl": "^0.18.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"auto-bind": "^5.0.1",
|
||||
"axios": "^1.7.0",
|
||||
"chalk": "^5.4.0",
|
||||
"chokidar": "^4.0.0",
|
||||
"cli-boxes": "^3.0.0",
|
||||
"code-excerpt": "^4.0.0",
|
||||
"diff": "^7.0.0",
|
||||
"execa": "^9.5.0",
|
||||
"figures": "^6.1.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"highlight.js": "^11.11.0",
|
||||
"ignore": "^6.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"marked": "^15.0.0",
|
||||
"node-pty": "^1.1.0",
|
||||
"p-map": "^7.0.0",
|
||||
"picomatch": "^4.0.0",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"qrcode": "^1.5.0",
|
||||
"react": "^19.0.0",
|
||||
"react-reconciler": "^0.31.0",
|
||||
"semver": "^7.6.0",
|
||||
"stack-utils": "^2.0.6",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"supports-hyperlinks": "^3.1.0",
|
||||
"tree-kill": "^1.2.2",
|
||||
"type-fest": "^4.30.0",
|
||||
"undici": "^7.3.0",
|
||||
"usehooks-ts": "^3.1.0",
|
||||
"wrap-ansi": "^9.0.0",
|
||||
"ws": "^8.18.0",
|
||||
"yaml": "^2.6.0",
|
||||
"zod": "^3.24.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.0",
|
||||
"@types/diff": "^7.0.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^22.10.0",
|
||||
"@types/picomatch": "^3.0.0",
|
||||
"@types/proper-lockfile": "^4.1.4",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/stack-utils": "^2.0.3",
|
||||
"@types/ws": "^8.5.0",
|
||||
"esbuild": "^0.25.0",
|
||||
"typescript": "^5.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"bun": ">=1.1.0"
|
||||
},
|
||||
"packageManager": "bun@1.1.0"
|
||||
}
|
||||
35
prompts/00-overview.md
Normal file
35
prompts/00-overview.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# Build-Out Prompt Index
|
||||
|
||||
Run these prompts **in order** in separate chat sessions. Each one is self-contained.
|
||||
|
||||
| # | File | What It Does | Depends On |
|
||||
|---|------|-------------|------------|
|
||||
| 01 | `01-install-bun-and-deps.md` | Install Bun runtime, install all dependencies | — |
|
||||
| 02 | `02-runtime-shims.md` | Create `bun:bundle` runtime shim + `MACRO` globals so code runs without Bun's bundler | 01 |
|
||||
| 03 | `03-build-config.md` | Create esbuild-based build system that bundles the CLI to a single runnable file | 01, 02 |
|
||||
| 04 | `04-fix-mcp-server.md` | Fix TypeScript errors in `mcp-server/` and make it build | 01 |
|
||||
| 05 | `05-env-and-auth.md` | Set up `.env` file, API key config, OAuth stubs | 01 |
|
||||
| 06 | `06-ink-react-terminal-ui.md` | Verify and fix the Ink/React terminal rendering pipeline | 01, 02, 03 |
|
||||
| 07 | `07-tool-system.md` | Audit and wire up the 40+ tool implementations (BashTool, FileEditTool, etc.) | 01–03 |
|
||||
| 08 | `08-command-system.md` | Audit and wire up the 50+ slash commands (/commit, /review, etc.) | 01–03, 07 |
|
||||
| 09 | `09-query-engine.md` | Get the core LLM call loop (QueryEngine) functional — streaming, tool calls, retries | 01–03, 05, 07 |
|
||||
| 10 | `10-context-and-prompts.md` | Wire up system prompt construction, context gathering, memory system | 01–03 |
|
||||
| 11 | `11-mcp-integration.md` | Get MCP client/server integration working — registry, tool discovery | 01–04 |
|
||||
| 12 | `12-services-layer.md` | Wire up analytics, policy limits, remote settings, session memory | 01–03, 05 |
|
||||
| 13 | `13-bridge-ide.md` | Stub out or implement the VS Code / JetBrains bridge layer | 01–03, 09 |
|
||||
| 14 | `14-dev-runner.md` | Create `npm run dev` / `bun run dev` script that launches the CLI in dev mode | 01–03 |
|
||||
| 15 | `15-production-bundle.md` | Create production build: minified bundle, platform-specific packaging | 03 |
|
||||
| 16 | `16-testing.md` | Add test infrastructure (vitest), write smoke tests for core subsystems | All |
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Open a new Copilot chat
|
||||
2. Paste the contents of `01-install-bun-and-deps.md`
|
||||
3. Follow the instructions / let the agent run
|
||||
4. Repeat for `02`, `03`, etc.
|
||||
|
||||
## Notes
|
||||
|
||||
- Prompts 07–13 can be run somewhat in **parallel** (they touch different subsystems)
|
||||
- If a prompt fails, fix the issue before moving to the next one
|
||||
- Each prompt is designed to be **independently verifiable** — it tells you how to confirm it worked
|
||||
38
prompts/01-install-bun-and-deps.md
Normal file
38
prompts/01-install-bun-and-deps.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# Prompt 01: Install Bun Runtime & Dependencies
|
||||
|
||||
## Context
|
||||
|
||||
You are working in `/workspaces/claude-code`, which contains the leaked source code of Anthropic's Claude Code CLI. It's a TypeScript/TSX project that uses **Bun** as its runtime (not Node.js). The `package.json` specifies `"engines": { "bun": ">=1.1.0" }`.
|
||||
|
||||
There is no `bun.lockb` lockfile — it was not included in the leak.
|
||||
|
||||
## Task
|
||||
|
||||
1. **Install Bun** (if not already installed):
|
||||
```
|
||||
curl -fsSL https://bun.sh/install | bash
|
||||
```
|
||||
Then ensure `bun` is on the PATH.
|
||||
|
||||
2. **Run `bun install`** in the project root (`/workspaces/claude-code`) to install all dependencies. This will generate a `bun.lockb` lockfile.
|
||||
|
||||
3. **Verify the install** — confirm that:
|
||||
- `node_modules/` exists and has the major packages: `@anthropic-ai/sdk`, `react`, `chalk`, `@commander-js/extra-typings`, `ink` (may not exist separately — check `@anthropic-ai/sdk`, `zod`, `@modelcontextprotocol/sdk`)
|
||||
- `bun --version` returns 1.1.0+
|
||||
|
||||
4. **Run the typecheck** to see current state:
|
||||
```
|
||||
bun run typecheck
|
||||
```
|
||||
Report any errors — don't fix them yet, just capture the output.
|
||||
|
||||
5. **Also install deps for the mcp-server sub-project**:
|
||||
```
|
||||
cd mcp-server && npm install && cd ..
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
- `bun --version` outputs >= 1.1.0
|
||||
- `ls node_modules/@anthropic-ai/sdk` succeeds
|
||||
- `bun run typecheck` runs (errors are expected at this stage, just report them)
|
||||
137
prompts/02-runtime-shims.md
Normal file
137
prompts/02-runtime-shims.md
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
# Prompt 02: Runtime Shims for `bun:bundle` Feature Flags & `MACRO` Globals
|
||||
|
||||
## Context
|
||||
|
||||
You are working in `/workspaces/claude-code`. This is the Claude Code CLI source. It was built to run under **Bun's bundler** which provides two build-time features that don't exist at runtime:
|
||||
|
||||
### 1. `bun:bundle` feature flags
|
||||
Throughout the code you'll find:
|
||||
```ts
|
||||
import { feature } from 'bun:bundle'
|
||||
if (feature('BRIDGE_MODE')) { ... }
|
||||
```
|
||||
Bun's bundler replaces `feature('X')` with `true`/`false` at build time for dead-code elimination. Without the bundler, this import fails at runtime.
|
||||
|
||||
**Current state**: There's a type stub at `src/types/bun-bundle.d.ts` that satisfies TypeScript, but there's no runtime module. We need a real module.
|
||||
|
||||
### 2. `MACRO` global object
|
||||
The code references a global `MACRO` object with these properties:
|
||||
- `MACRO.VERSION` — package version string (e.g., `"1.0.53"`)
|
||||
- `MACRO.PACKAGE_URL` — npm package name (e.g., `"@anthropic-ai/claude-code"`)
|
||||
- `MACRO.ISSUES_EXPLAINER` — feedback URL/instructions string
|
||||
|
||||
These are normally inlined by the bundler. Some files already guard with `typeof MACRO !== 'undefined'`, but most don't.
|
||||
|
||||
## Task
|
||||
|
||||
### Part A: Create `bun:bundle` runtime module
|
||||
|
||||
Create a file at `src/shims/bun-bundle.ts` that exports a `feature()` function. Feature flags should be configurable via environment variables so we can toggle them:
|
||||
|
||||
```ts
|
||||
// src/shims/bun-bundle.ts
|
||||
|
||||
// Map of feature flags to their enabled state.
|
||||
// In production Bun builds, these are compile-time constants.
|
||||
// For our dev build, we read from env vars with sensible defaults.
|
||||
const FEATURE_FLAGS: Record<string, boolean> = {
|
||||
PROACTIVE: envBool('CLAUDE_CODE_PROACTIVE', false),
|
||||
KAIROS: envBool('CLAUDE_CODE_KAIROS', false),
|
||||
BRIDGE_MODE: envBool('CLAUDE_CODE_BRIDGE_MODE', false),
|
||||
DAEMON: envBool('CLAUDE_CODE_DAEMON', false),
|
||||
VOICE_MODE: envBool('CLAUDE_CODE_VOICE_MODE', false),
|
||||
AGENT_TRIGGERS: envBool('CLAUDE_CODE_AGENT_TRIGGERS', false),
|
||||
MONITOR_TOOL: envBool('CLAUDE_CODE_MONITOR_TOOL', false),
|
||||
COORDINATOR_MODE: envBool('CLAUDE_CODE_COORDINATOR_MODE', false),
|
||||
ABLATION_BASELINE: false, // always off for external builds
|
||||
DUMP_SYSTEM_PROMPT: envBool('CLAUDE_CODE_DUMP_SYSTEM_PROMPT', false),
|
||||
BG_SESSIONS: envBool('CLAUDE_CODE_BG_SESSIONS', false),
|
||||
}
|
||||
|
||||
function envBool(key: string, fallback: boolean): boolean {
|
||||
const v = process.env[key]
|
||||
if (v === undefined) return fallback
|
||||
return v === '1' || v === 'true'
|
||||
}
|
||||
|
||||
export function feature(name: string): boolean {
|
||||
return FEATURE_FLAGS[name] ?? false
|
||||
}
|
||||
```
|
||||
|
||||
### Part B: Create `MACRO` global definition
|
||||
|
||||
Create a file at `src/shims/macro.ts` that defines and installs the global `MACRO` object:
|
||||
|
||||
```ts
|
||||
// src/shims/macro.ts
|
||||
|
||||
// Read version from package.json at startup
|
||||
import { readFileSync } from 'fs'
|
||||
import { resolve, dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const pkgPath = resolve(dirname(__filename), '..', '..', 'package.json')
|
||||
let version = '0.0.0-dev'
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
||||
version = pkg.version || version
|
||||
} catch {}
|
||||
|
||||
const MACRO_OBJ = {
|
||||
VERSION: version,
|
||||
PACKAGE_URL: '@anthropic-ai/claude-code',
|
||||
ISSUES_EXPLAINER: 'report issues at https://github.com/anthropics/claude-code/issues',
|
||||
}
|
||||
|
||||
// Install as global
|
||||
;(globalThis as any).MACRO = MACRO_OBJ
|
||||
|
||||
export default MACRO_OBJ
|
||||
```
|
||||
|
||||
### Part C: Create a preload/bootstrap file
|
||||
|
||||
Create `src/shims/preload.ts` that imports both shims so they're available before any app code runs:
|
||||
|
||||
```ts
|
||||
// src/shims/preload.ts
|
||||
// Must be loaded before any application code.
|
||||
// Provides runtime equivalents of Bun bundler build-time features.
|
||||
|
||||
import './macro.js'
|
||||
// bun:bundle is resolved via the build alias, not imported here
|
||||
```
|
||||
|
||||
### Part D: Update tsconfig.json `paths`
|
||||
|
||||
The current tsconfig.json has:
|
||||
```json
|
||||
"paths": {
|
||||
"bun:bundle": ["./src/types/bun-bundle.d.ts"]
|
||||
}
|
||||
```
|
||||
|
||||
This handles type-checking. For runtime, we'll need the build system (Prompt 03) to alias `bun:bundle` → `src/shims/bun-bundle.ts`. **Don't change tsconfig.json** — the type stub is correct for `tsc`. Just note this for the next prompt.
|
||||
|
||||
### Part E: Add global MACRO type declaration
|
||||
|
||||
Check if there's already a global type declaration for `MACRO`. If not, add one to `src/types/bun-bundle.d.ts` or a new `src/types/macro.d.ts`:
|
||||
|
||||
```ts
|
||||
declare const MACRO: {
|
||||
VERSION: string
|
||||
PACKAGE_URL: string
|
||||
ISSUES_EXPLAINER: string
|
||||
}
|
||||
```
|
||||
|
||||
Make sure `tsc --noEmit` still passes after your changes.
|
||||
|
||||
## Verification
|
||||
|
||||
1. `bun run typecheck` should pass (or have the same errors as before — no new errors)
|
||||
2. The files `src/shims/bun-bundle.ts`, `src/shims/macro.ts`, `src/shims/preload.ts` exist
|
||||
3. Running `bun -e "import { feature } from './src/shims/bun-bundle.ts'; console.log(feature('BRIDGE_MODE'))"` should print `false`
|
||||
4. Running `bun -e "import './src/shims/macro.ts'; console.log(MACRO.VERSION)"` should print the version
|
||||
165
prompts/03-build-config.md
Normal file
165
prompts/03-build-config.md
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
# Prompt 03: Create esbuild-Based Build System
|
||||
|
||||
## Context
|
||||
|
||||
You are working in `/workspaces/claude-code`. This is the Claude Code CLI — a TypeScript/TSX terminal app using React + Ink. It was originally built using **Bun's bundler** with feature flags, but that build config wasn't included in the leak.
|
||||
|
||||
We need to create a build system that:
|
||||
1. Bundles the entire `src/` tree into a runnable output
|
||||
2. Aliases `bun:bundle` → our shim at `src/shims/bun-bundle.ts`
|
||||
3. Injects the `MACRO` global (via `src/shims/macro.ts` preload)
|
||||
4. Handles TSX/JSX (React)
|
||||
5. Handles ESM `.js` extension imports (the code uses `import from './foo.js'` which maps to `./foo.ts`)
|
||||
6. Produces output that can run under **Bun** (primary) or **Node.js 20+** (secondary)
|
||||
|
||||
## Existing Files
|
||||
|
||||
- `src/shims/bun-bundle.ts` — runtime `feature()` function (created in Prompt 02)
|
||||
- `src/shims/macro.ts` — global `MACRO` object (created in Prompt 02)
|
||||
- `src/shims/preload.ts` — preload bootstrap (created in Prompt 02)
|
||||
- `src/entrypoints/cli.tsx` — main entrypoint
|
||||
- `tsconfig.json` — has `"jsx": "react-jsx"`, `"module": "ESNext"`, `"moduleResolution": "bundler"`
|
||||
|
||||
## Task
|
||||
|
||||
### Part A: Install esbuild
|
||||
|
||||
```bash
|
||||
bun add -d esbuild
|
||||
```
|
||||
|
||||
### Part B: Create build script
|
||||
|
||||
Create `scripts/build-bundle.ts` (a Bun-runnable build script):
|
||||
|
||||
```ts
|
||||
// scripts/build-bundle.ts
|
||||
// Usage: bun scripts/build-bundle.ts [--watch] [--minify]
|
||||
|
||||
import * as esbuild from 'esbuild'
|
||||
import { resolve } from 'path'
|
||||
|
||||
const ROOT = resolve(import.meta.dir, '..')
|
||||
const watch = process.argv.includes('--watch')
|
||||
const minify = process.argv.includes('--minify')
|
||||
|
||||
const buildOptions: esbuild.BuildOptions = {
|
||||
entryPoints: [resolve(ROOT, 'src/entrypoints/cli.tsx')],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
target: 'node20',
|
||||
format: 'esm',
|
||||
outdir: resolve(ROOT, 'dist'),
|
||||
outExtension: { '.js': '.mjs' },
|
||||
|
||||
// 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 native packages
|
||||
external: [
|
||||
// Node built-ins
|
||||
'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',
|
||||
'node:*',
|
||||
// Native addons that can't be bundled
|
||||
'fsevents',
|
||||
],
|
||||
|
||||
jsx: 'automatic',
|
||||
|
||||
// Source maps for debugging
|
||||
sourcemap: true,
|
||||
|
||||
minify,
|
||||
|
||||
// Banner: shebang for CLI + preload the MACRO global
|
||||
banner: {
|
||||
js: '#!/usr/bin/env node\n',
|
||||
},
|
||||
|
||||
// Handle the .js → .ts resolution that the codebase uses
|
||||
resolveExtensions: ['.tsx', '.ts', '.jsx', '.js', '.json'],
|
||||
|
||||
logLevel: 'info',
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (watch) {
|
||||
const ctx = await esbuild.context(buildOptions)
|
||||
await ctx.watch()
|
||||
console.log('Watching for changes...')
|
||||
} else {
|
||||
const result = await esbuild.build(buildOptions)
|
||||
if (result.errors.length > 0) {
|
||||
console.error('Build failed')
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('Build complete → dist/')
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
```
|
||||
|
||||
**Important**: This is a starting point. You will likely need to iterate on the externals list and alias configuration. The codebase has ~1,900 files — some imports may need special handling. When you run the build:
|
||||
|
||||
1. Run it: `bun scripts/build-bundle.ts`
|
||||
2. Look at the errors
|
||||
3. Fix them (add externals, fix aliases, etc.)
|
||||
4. Repeat until it bundles successfully
|
||||
|
||||
Common issues you'll hit:
|
||||
- **npm packages that use native modules** → add to `external`
|
||||
- **Dynamic `require()` calls** behind `process.env.USER_TYPE === 'ant'` → these are Anthropic-internal, wrap them or stub them
|
||||
- **Circular dependencies** → esbuild handles these but may warn
|
||||
- **Re-exports from barrel files** → should work but watch for issues
|
||||
|
||||
### Part C: Add npm scripts
|
||||
|
||||
Add these to `package.json` `"scripts"`:
|
||||
|
||||
```json
|
||||
{
|
||||
"build": "bun scripts/build-bundle.ts",
|
||||
"build:watch": "bun scripts/build-bundle.ts --watch",
|
||||
"build:prod": "bun scripts/build-bundle.ts --minify"
|
||||
}
|
||||
```
|
||||
|
||||
### Part D: Create dist output directory
|
||||
|
||||
Add `dist/` to `.gitignore` (create one if it doesn't exist).
|
||||
|
||||
### Part E: Iterate on build errors
|
||||
|
||||
Run the build and fix whatever comes up. The goal is a clean `bun scripts/build-bundle.ts` that produces `dist/cli.mjs`.
|
||||
|
||||
**Strategy for unresolvable modules**: If modules reference Anthropic-internal packages or Bun-specific APIs (like `Bun.hash`, `Bun.file`), create minimal stubs in `src/shims/` that provide compatible fallbacks.
|
||||
|
||||
### Part F: Test the output
|
||||
|
||||
After a successful build:
|
||||
```bash
|
||||
node dist/cli.mjs --version
|
||||
# or
|
||||
bun dist/cli.mjs --version
|
||||
```
|
||||
|
||||
This should print the version. It will likely crash after that because no API key is configured — that's fine for now.
|
||||
|
||||
## Verification
|
||||
|
||||
1. `bun scripts/build-bundle.ts` completes without errors
|
||||
2. `dist/cli.mjs` exists
|
||||
3. `bun dist/cli.mjs --version` or `node dist/cli.mjs --version` prints a version string
|
||||
4. `package.json` has `build`, `build:watch`, `build:prod` scripts
|
||||
85
prompts/04-fix-mcp-server.md
Normal file
85
prompts/04-fix-mcp-server.md
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# Prompt 04: Fix MCP Server Build
|
||||
|
||||
## Context
|
||||
|
||||
You are working in `/workspaces/claude-code/mcp-server/`. This is a separate sub-project that provides an MCP (Model Context Protocol) server for exploring the Claude Code source. It's a simpler, self-contained TypeScript project.
|
||||
|
||||
Currently `npm run build` (which runs `tsc`) fails with TypeScript errors.
|
||||
|
||||
## Task
|
||||
|
||||
1. **Run the build and capture errors**:
|
||||
```bash
|
||||
cd /workspaces/claude-code/mcp-server
|
||||
npm run build 2>&1
|
||||
```
|
||||
|
||||
2. **Fix all TypeScript errors** in `mcp-server/src/server.ts` and `mcp-server/src/index.ts`. Common issues include:
|
||||
- Duplicate function implementations
|
||||
- Missing imports
|
||||
- Type mismatches with the MCP SDK types
|
||||
|
||||
3. **Verify the fix**:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
Should complete with zero errors and produce output in `mcp-server/dist/`.
|
||||
|
||||
4. **Test the MCP server runs**:
|
||||
```bash
|
||||
node dist/index.js --help 2>&1 || node dist/index.js 2>&1 | head -5
|
||||
```
|
||||
It may hang waiting for stdio input (that's normal for an MCP server) — just verify it starts without crashing.
|
||||
|
||||
## Key Files
|
||||
|
||||
- `mcp-server/package.json` — build script and dependencies
|
||||
- `mcp-server/tsconfig.json` — TypeScript config
|
||||
- `mcp-server/src/server.ts` — Main server logic (tools, resources, prompts)
|
||||
- `mcp-server/src/index.ts` — Entrypoint (stdio transport)
|
||||
|
||||
## Verification
|
||||
|
||||
1. `cd mcp-server && npm run build` succeeds with zero errors
|
||||
2. `ls mcp-server/dist/` shows compiled `.js` files
|
||||
3. `node mcp-server/dist/index.js` starts without immediate crash
|
||||
|
||||
---
|
||||
|
||||
## Fix: GitHub Push Protection Blocked Push
|
||||
|
||||
`git push origin main` is rejected because commits `5e99ad6` and `7adaa5d` contain a **GitHub OAuth token** in `.mcpregistry_github_token`. The file was deleted from disk but still exists in git history. You must rewrite history to remove it.
|
||||
|
||||
### Step 1: Scrub secrets from history
|
||||
|
||||
```bash
|
||||
git filter-branch --force --index-filter \
|
||||
'git rm --cached --ignore-unmatch .mcpregistry_github_token .mcpregistry_registry_token mcp-server/.mcpregistry_github_token mcp-server/.mcpregistry_registry_token' \
|
||||
--prune-empty HEAD~5..HEAD
|
||||
```
|
||||
|
||||
### Step 2: Push
|
||||
|
||||
```bash
|
||||
git push origin main
|
||||
```
|
||||
|
||||
### Alternative: Interactive rebase
|
||||
|
||||
```bash
|
||||
git rebase -i HEAD~5
|
||||
# Change "pick" to "edit" for commits 5e99ad6 and 7adaa5d
|
||||
# At each stop, run:
|
||||
git rm --cached .mcpregistry_github_token .mcpregistry_registry_token 2>/dev/null
|
||||
git rm --cached mcp-server/.mcpregistry_github_token mcp-server/.mcpregistry_registry_token 2>/dev/null
|
||||
git commit --amend --no-edit
|
||||
git rebase --continue
|
||||
```
|
||||
|
||||
### Step 3: Prevent future leaks
|
||||
|
||||
```bash
|
||||
echo ".mcpregistry_github_token" >> .gitignore
|
||||
echo ".mcpregistry_registry_token" >> .gitignore
|
||||
git add .gitignore && git commit -m "chore: gitignore token files"
|
||||
```
|
||||
100
prompts/05-env-and-auth.md
Normal file
100
prompts/05-env-and-auth.md
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# Prompt 05: Environment Configuration & API Authentication
|
||||
|
||||
## Context
|
||||
|
||||
You are working in `/workspaces/claude-code`. The CLI needs an Anthropic API key to function. The auth system supports multiple backends:
|
||||
- **Direct API** (`ANTHROPIC_API_KEY`) — simplest
|
||||
- **OAuth** (Claude.ai subscription) — complex browser flow
|
||||
- **AWS Bedrock** — `AWS_*` env vars
|
||||
- **Google Vertex AI** — GCP credentials
|
||||
- **Azure Foundry** — `ANTHROPIC_FOUNDRY_API_KEY`
|
||||
|
||||
## Task
|
||||
|
||||
### Part A: Create `.env` file from the existing code
|
||||
|
||||
Search the codebase for all environment variables used. Key files to check:
|
||||
- `src/entrypoints/cli.tsx` (reads env vars at top level)
|
||||
- `src/services/api/client.ts` (API client construction)
|
||||
- `src/utils/auth.ts` (authentication)
|
||||
- `src/utils/config.ts` (config loading)
|
||||
- `src/constants/` (any hardcoded config)
|
||||
- `src/entrypoints/init.ts` (initialization reads)
|
||||
|
||||
Create a `.env.example` file (or update the existing one if it exists) with ALL discoverable env vars, organized by category, with documentation comments. At minimum include:
|
||||
|
||||
```env
|
||||
# ─── Authentication ───
|
||||
ANTHROPIC_API_KEY= # Required: Your Anthropic API key (sk-ant-...)
|
||||
|
||||
# ─── API Configuration ───
|
||||
ANTHROPIC_BASE_URL= # Custom API endpoint (default: https://api.anthropic.com)
|
||||
ANTHROPIC_MODEL= # Override default model (e.g., claude-sonnet-4-20250514)
|
||||
ANTHROPIC_SMALL_FAST_MODEL= # Model for fast/cheap operations (e.g., claude-haiku)
|
||||
|
||||
# ─── Feature Flags (used by bun:bundle shim) ───
|
||||
CLAUDE_CODE_PROACTIVE=false
|
||||
CLAUDE_CODE_BRIDGE_MODE=false
|
||||
CLAUDE_CODE_COORDINATOR_MODE=false
|
||||
CLAUDE_CODE_VOICE_MODE=false
|
||||
|
||||
# ─── Debug ───
|
||||
CLAUDE_CODE_DEBUG_LOG_LEVEL= # debug, info, warn, error
|
||||
DEBUG=false
|
||||
```
|
||||
|
||||
### Part B: Trace the API client setup
|
||||
|
||||
Read `src/services/api/client.ts` to understand how the Anthropic SDK is initialized. Document:
|
||||
1. What env vars it reads
|
||||
2. How it selects between API backends (direct, Bedrock, Vertex, etc.)
|
||||
3. Where the API key comes from (env var? keychain? OAuth token?)
|
||||
|
||||
Create a comment block at the top of `.env.example` explaining how auth works.
|
||||
|
||||
### Part C: Create a minimal auth test
|
||||
|
||||
Create `scripts/test-auth.ts`:
|
||||
```ts
|
||||
// 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()
|
||||
```
|
||||
|
||||
### Part D: Stub OAuth for development
|
||||
|
||||
The OAuth flow (`src/services/oauth/`) requires browser interaction and Anthropic's OAuth endpoints. For development, we want to bypass it.
|
||||
|
||||
Search for where the auth decision is made (likely in `src/utils/auth.ts` or `src/entrypoints/init.ts`). Document what would need to be stubbed to skip OAuth and use only `ANTHROPIC_API_KEY`.
|
||||
|
||||
Don't modify source files yet — just document findings in a comment at the bottom of `.env.example`.
|
||||
|
||||
## Verification
|
||||
|
||||
1. `.env.example` exists with comprehensive env var documentation
|
||||
2. `scripts/test-auth.ts` exists
|
||||
3. With a valid `ANTHROPIC_API_KEY` set: `bun scripts/test-auth.ts` prints success
|
||||
4. Without an API key: `bun scripts/test-auth.ts` prints a clear error
|
||||
110
prompts/06-ink-react-terminal-ui.md
Normal file
110
prompts/06-ink-react-terminal-ui.md
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
# Prompt 06: Verify and Fix the Ink/React Terminal UI Pipeline
|
||||
|
||||
## Context
|
||||
|
||||
You are working in `/workspaces/claude-code`. The CLI renders its UI using **React + Ink** — a framework that renders React components to the terminal (not a browser). This project includes a **custom fork of Ink** embedded directly in `src/ink/`.
|
||||
|
||||
Key files:
|
||||
- `src/ink.ts` — Public API (re-exports `render()` and `createRoot()`, wraps with `ThemeProvider`)
|
||||
- `src/ink/root.ts` — Ink's root renderer
|
||||
- `src/ink/ink.tsx` — Core Ink component
|
||||
- `src/ink/reconciler.ts` — React reconciler for terminal output
|
||||
- `src/ink/dom.ts` — Terminal DOM implementation
|
||||
- `src/ink/renderer.ts` — Renders virtual DOM to terminal strings
|
||||
- `src/ink/components/` — Built-in Ink components (Box, Text, etc.)
|
||||
- `src/components/` — Claude Code's ~140 custom components
|
||||
|
||||
## Task
|
||||
|
||||
### Part A: Trace the render pipeline
|
||||
|
||||
Read these files in order and document the rendering flow:
|
||||
|
||||
1. `src/ink.ts` → how `render()` and `createRoot()` work
|
||||
2. `src/ink/root.ts` → how Ink creates a root and mounts React
|
||||
3. `src/ink/reconciler.ts` → what React reconciler is used
|
||||
4. `src/ink/renderer.ts` → how the virtual DOM becomes terminal output
|
||||
5. `src/ink/dom.ts` → what the "DOM nodes" look like
|
||||
|
||||
Create a brief architecture doc in a comment block or README section.
|
||||
|
||||
### Part B: Verify Ink components compile
|
||||
|
||||
Check that the core Ink components are self-contained:
|
||||
```
|
||||
src/ink/components/
|
||||
```
|
||||
List them all and verify they don't have missing imports.
|
||||
|
||||
### Part C: Check the ThemeProvider
|
||||
|
||||
Read `src/components/design-system/ThemeProvider.tsx` (or wherever it lives). Verify it:
|
||||
1. Exists
|
||||
2. Exports a `ThemeProvider` component
|
||||
3. The theme system doesn't depend on external resources
|
||||
|
||||
### Part D: Create a minimal render test
|
||||
|
||||
Create `scripts/test-ink.tsx`:
|
||||
```tsx
|
||||
// scripts/test-ink.tsx
|
||||
// Minimal test that the Ink terminal UI renders
|
||||
// Usage: bun scripts/test-ink.tsx
|
||||
|
||||
import React from 'react'
|
||||
|
||||
// We need the shims loaded first
|
||||
import './src/shims/preload.js'
|
||||
|
||||
// Now try to use Ink
|
||||
import { render } from './src/ink.js'
|
||||
|
||||
// Minimal component
|
||||
function Hello() {
|
||||
return <Text>Hello from Claude Code Ink UI!</Text>
|
||||
}
|
||||
|
||||
// Need to import Text from Ink
|
||||
import { Text } from './src/ink/components/Text.js'
|
||||
|
||||
async function main() {
|
||||
const instance = await render(<Hello />)
|
||||
// Give it a moment to render
|
||||
setTimeout(() => {
|
||||
instance.unmount()
|
||||
process.exit(0)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('Ink render test failed:', err)
|
||||
process.exit(1)
|
||||
})
|
||||
```
|
||||
|
||||
Adjust the imports based on what you find — the Text component path may differ.
|
||||
|
||||
### Part E: Fix any issues
|
||||
|
||||
If Ink rendering fails, the common issues are:
|
||||
1. **Missing `yoga-wasm-web` or `yoga-layout`** — Ink uses Yoga for flexbox layout. Check if there's a Yoga dependency or if it's embedded.
|
||||
2. **React version mismatch** — The code uses React 19. Verify the reconciler is compatible.
|
||||
3. **Terminal detection** — Ink checks if stdout is a TTY. In some environments this may need to be forced.
|
||||
4. **Missing chalk/ansi dependency** — Terminal colors.
|
||||
|
||||
Fix whatever you find to make the test render successfully.
|
||||
|
||||
### Part F: Verify component imports
|
||||
|
||||
Check that `src/components/` components can import from the Ink system without errors. Pick 3-5 key components:
|
||||
- `src/components/MessageResponse.tsx` (or similar — the main chat message renderer)
|
||||
- `src/components/ToolUseResult.tsx` (or similar — tool output display)
|
||||
- `src/components/PermissionRequest.tsx` (or similar — permission modal)
|
||||
|
||||
Read their imports and verify nothing is missing.
|
||||
|
||||
## Verification
|
||||
|
||||
1. `scripts/test-ink.tsx` renders "Hello from Claude Code Ink UI!" to the terminal
|
||||
2. No new TypeScript errors introduced
|
||||
3. You've documented the render pipeline flow
|
||||
104
prompts/07-tool-system.md
Normal file
104
prompts/07-tool-system.md
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# Prompt 07: Audit and Wire Up the Tool System
|
||||
|
||||
## Context
|
||||
|
||||
You are working in `/workspaces/claude-code`. The Claude Code CLI has ~40 tools that the LLM can invoke during conversations. Each tool is in `src/tools/<ToolName>/` and follows a consistent pattern.
|
||||
|
||||
Key files:
|
||||
- `src/Tool.ts` (~29K lines) — Tool type definitions, `ToolUseContext`, `PermissionResult`, etc.
|
||||
- `src/tools.ts` — Tool registry (`getTools()` function that returns all available tools)
|
||||
- `src/tools/` — Individual tool directories
|
||||
|
||||
## Task
|
||||
|
||||
### Part A: Understand the Tool interface
|
||||
|
||||
Read `src/Tool.ts` and document the `Tool` interface. Key questions:
|
||||
1. What fields does a `Tool` have? (name, description, inputSchema, execute, etc.)
|
||||
2. What is `ToolUseContext`? What does it provide to tool execution?
|
||||
3. How do tool permissions work? (`PermissionResult`, `needsPermission`)
|
||||
4. How do tools declare their input schema? (JSON Schema / Zod)
|
||||
|
||||
### Part B: Audit the tool registry
|
||||
|
||||
Read `src/tools.ts` fully. It dynamically imports tools behind feature flags and env checks:
|
||||
```ts
|
||||
const REPLTool = process.env.USER_TYPE === 'ant' ? ... : null
|
||||
const SleepTool = feature('PROACTIVE') || feature('KAIROS') ? ... : null
|
||||
```
|
||||
|
||||
Create a complete inventory of:
|
||||
1. **Always-available tools** — imported unconditionally
|
||||
2. **Feature-gated tools** — which feature flag enables them
|
||||
3. **Ant-only tools** — gated behind `USER_TYPE === 'ant'` (Anthropic internal)
|
||||
4. **Broken/missing tools** — any tools referenced but not found
|
||||
|
||||
### Part C: Verify each tool compiles
|
||||
|
||||
For each tool directory in `src/tools/`, check:
|
||||
1. Does it have an `index.ts` or main file?
|
||||
2. Does it export a tool definition matching the `Tool` interface?
|
||||
3. Are its imports resolvable?
|
||||
|
||||
Focus on the **core 10 tools** that are essential for basic operation:
|
||||
- `BashTool` — shell command execution
|
||||
- `FileReadTool` — read files
|
||||
- `FileWriteTool` — write files
|
||||
- `FileEditTool` — edit files (search & replace)
|
||||
- `GlobTool` — find files by pattern
|
||||
- `GrepTool` — search file contents
|
||||
- `AgentTool` — spawn sub-agent
|
||||
- `WebFetchTool` — HTTP requests
|
||||
- `AskUserQuestionTool` — ask the user for input
|
||||
- `TodoWriteTool` — todo list management
|
||||
|
||||
### Part D: Fix import issues
|
||||
|
||||
The tool registry (`src/tools.ts`) uses dynamic imports with `bun:bundle` feature flags. With our runtime shim, these should work — but verify:
|
||||
|
||||
1. Feature-gated imports resolve when the flag is `false` (should be skipped)
|
||||
2. Feature-gated imports resolve when the flag is `true` (should load)
|
||||
3. Ant-only tools gracefully handle `process.env.USER_TYPE !== 'ant'`
|
||||
|
||||
Fix any import resolution errors.
|
||||
|
||||
### Part E: Create a tool smoke test
|
||||
|
||||
Create `scripts/test-tools.ts`:
|
||||
```ts
|
||||
// scripts/test-tools.ts
|
||||
// Verify all tools load without errors
|
||||
// Usage: bun scripts/test-tools.ts
|
||||
|
||||
import './src/shims/preload.js'
|
||||
|
||||
async function main() {
|
||||
const { getTools } = await import('./src/tools.js')
|
||||
|
||||
// getTools() may need arguments — check its signature
|
||||
const tools = getTools(/* ... */)
|
||||
|
||||
console.log(`Loaded ${tools.length} tools:\n`)
|
||||
for (const tool of tools) {
|
||||
console.log(` ✓ ${tool.name}`)
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('Tool loading failed:', err)
|
||||
process.exit(1)
|
||||
})
|
||||
```
|
||||
|
||||
Adapt the script to match the actual `getTools()` signature.
|
||||
|
||||
### Part F: Stub Anthropic-internal tools
|
||||
|
||||
Any tools gated behind `USER_TYPE === 'ant'` should be cleanly excluded. Verify the null checks work and don't cause runtime errors when these tools are missing from the registry.
|
||||
|
||||
## Verification
|
||||
|
||||
1. `scripts/test-tools.ts` runs and lists all available tools without errors
|
||||
2. The core 10 tools listed above are all present
|
||||
3. No TypeScript errors in `src/tools/` or `src/tools.ts`
|
||||
4. Ant-only tools are cleanly excluded (no crashes)
|
||||
90
prompts/08-command-system.md
Normal file
90
prompts/08-command-system.md
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
# Prompt 08: Audit and Wire Up the Command System
|
||||
|
||||
## Context
|
||||
|
||||
You are working in `/workspaces/claude-code`. The CLI has ~50 slash commands (e.g., `/commit`, `/review`, `/init`, `/config`). These are registered in `src/commands.ts` and implemented in `src/commands/`.
|
||||
|
||||
Key files:
|
||||
- `src/commands.ts` (~25K lines) — Command registry (`getCommands()`)
|
||||
- `src/commands/` — Individual command implementations
|
||||
- `src/types/command.ts` — Command type definition
|
||||
|
||||
## Task
|
||||
|
||||
### Part A: Understand the Command interface
|
||||
|
||||
Read `src/types/command.ts` and the top of `src/commands.ts`. Document:
|
||||
1. The `Command` type (name, description, execute, args, etc.)
|
||||
2. How commands are registered
|
||||
3. How command execution is triggered (from the REPL? from CLI args?)
|
||||
|
||||
### Part B: Audit the command registry
|
||||
|
||||
Read `src/commands.ts` fully. Create a complete inventory of all commands, organized by category:
|
||||
|
||||
**Essential commands** (needed for basic operation):
|
||||
- `/help` — show help
|
||||
- `/config` — view/edit configuration
|
||||
- `/init` — initialize a project
|
||||
- `/commit` — git commit
|
||||
- `/review` — code review
|
||||
|
||||
**Feature-gated commands** (behind feature flags or USER_TYPE):
|
||||
- List which flag enables each
|
||||
|
||||
**Potentially broken commands** (reference missing imports or services):
|
||||
- List any that can't resolve their imports
|
||||
|
||||
### Part C: Verify core commands compile
|
||||
|
||||
For the essential commands listed above, read their implementations and check:
|
||||
1. All imports resolve
|
||||
2. They don't depend on unavailable services
|
||||
3. The function signatures match the Command type
|
||||
|
||||
### Part D: Fix import issues
|
||||
|
||||
Similar to the tool system, commands may have:
|
||||
- Feature-gated imports that need the `bun:bundle` shim
|
||||
- Ant-only code paths
|
||||
- Dynamic imports that need correct paths
|
||||
|
||||
Fix whatever is broken.
|
||||
|
||||
### Part E: Handle "moved to plugin" commands
|
||||
|
||||
There's a file `src/commands/createMovedToPluginCommand.ts`. Read it — some commands have been migrated to the plugin system. These should gracefully tell the user the command has moved, not crash.
|
||||
|
||||
### Part F: Create a command smoke test
|
||||
|
||||
Create `scripts/test-commands.ts`:
|
||||
```ts
|
||||
// scripts/test-commands.ts
|
||||
// Verify all commands load without errors
|
||||
// Usage: bun scripts/test-commands.ts
|
||||
|
||||
import './src/shims/preload.js'
|
||||
|
||||
async function main() {
|
||||
const { getCommands } = await import('./src/commands.js')
|
||||
|
||||
const commands = getCommands(/* check signature */)
|
||||
|
||||
console.log(`Loaded ${commands.length} commands:\n`)
|
||||
for (const cmd of commands) {
|
||||
console.log(` /${cmd.name} — ${cmd.description || '(no description)'}`)
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('Command loading failed:', err)
|
||||
process.exit(1)
|
||||
})
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
1. `scripts/test-commands.ts` lists all available commands
|
||||
2. Core commands (`/help`, `/config`, `/init`, `/commit`) are present
|
||||
3. No runtime crashes from missing imports
|
||||
4. Moved-to-plugin commands show a friendly message instead of crashing
|
||||
118
prompts/09-query-engine.md
Normal file
118
prompts/09-query-engine.md
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
# Prompt 09: Get the QueryEngine (Core LLM Loop) Functional
|
||||
|
||||
## Context
|
||||
|
||||
You are working in `/workspaces/claude-code`. The `QueryEngine` (`src/QueryEngine.ts`, ~46K lines) is the heart of the CLI — it:
|
||||
1. Sends messages to the Anthropic API (streaming)
|
||||
2. Processes streaming responses (text, thinking, tool_use blocks)
|
||||
3. Executes tools when the LLM requests them (tool loop)
|
||||
4. Handles retries, rate limits, and errors
|
||||
5. Tracks token usage and costs
|
||||
6. Manages conversation context (message history)
|
||||
|
||||
This is the most complex single file. The goal is to get it functional enough for a basic conversation loop.
|
||||
|
||||
## Key Dependencies
|
||||
|
||||
The QueryEngine depends on:
|
||||
- `src/services/api/client.ts` — Anthropic SDK client
|
||||
- `src/services/api/claude.ts` — Message API wrapper
|
||||
- `src/Tool.ts` — Tool definitions
|
||||
- `src/tools.ts` — Tool registry
|
||||
- `src/context.ts` — System context
|
||||
- `src/constants/prompts.ts` — System prompt
|
||||
- Token counting utilities
|
||||
- Streaming event handlers
|
||||
|
||||
## Task
|
||||
|
||||
### Part A: Map the QueryEngine architecture
|
||||
|
||||
Read `src/QueryEngine.ts` and create a structural map:
|
||||
1. **Class structure** — What classes/interfaces are defined?
|
||||
2. **Public API** — What method starts a query? What does it return?
|
||||
3. **Message flow** — How does a user message become an API call?
|
||||
4. **Tool loop** — How are tool calls detected, executed, and fed back?
|
||||
5. **Streaming** — How are streaming events processed?
|
||||
6. **Retry logic** — How are API errors handled?
|
||||
|
||||
### Part B: Trace the API call path
|
||||
|
||||
Follow the chain from QueryEngine → API client:
|
||||
1. Read `src/services/api/client.ts` — how is the Anthropic SDK client created?
|
||||
2. Read `src/services/api/claude.ts` — what's the message creation wrapper?
|
||||
3. What parameters are passed? (model, max_tokens, system prompt, tools, messages)
|
||||
4. How is streaming handled? (SSE? SDK streaming?)
|
||||
|
||||
### Part C: Identify and fix blockers
|
||||
|
||||
The QueryEngine will have dependencies on many subsystems. For each dependency:
|
||||
- **If it's essential** (API client, tool execution) → make sure it works
|
||||
- **If it's optional** (analytics, telemetry, policy limits) → stub or skip it
|
||||
|
||||
Common blockers:
|
||||
1. **Missing API configuration** → needs `ANTHROPIC_API_KEY` (Prompt 05)
|
||||
2. **Policy limits service** → may block execution, needs stubbing
|
||||
3. **GrowthBook/analytics** → needs stubbing or graceful failure
|
||||
4. **Remote managed settings** → needs stubbing
|
||||
5. **Bootstrap data fetch** → may need to be optional
|
||||
|
||||
### Part D: Create a minimal conversation test
|
||||
|
||||
Create `scripts/test-query.ts` that exercises the QueryEngine directly:
|
||||
|
||||
```ts
|
||||
// scripts/test-query.ts
|
||||
// Minimal test of the QueryEngine — single query, no REPL
|
||||
// Usage: ANTHROPIC_API_KEY=sk-ant-... bun scripts/test-query.ts "What is 2+2?"
|
||||
|
||||
import './src/shims/preload.js'
|
||||
|
||||
async function main() {
|
||||
const query = process.argv[2] || 'What is 2+2?'
|
||||
|
||||
// Import and set up minimal dependencies
|
||||
// You'll need to figure out the exact imports and initialization
|
||||
// by reading src/QueryEngine.ts, src/query.ts, and src/replLauncher.tsx
|
||||
|
||||
// The basic flow should be:
|
||||
// 1. Create API client
|
||||
// 2. Build system prompt
|
||||
// 3. Create QueryEngine instance
|
||||
// 4. Send a query
|
||||
// 5. Print the response
|
||||
|
||||
console.log(`Query: ${query}`)
|
||||
console.log('---')
|
||||
|
||||
// TODO: Wire up the actual QueryEngine call
|
||||
// This is the hardest part — document what you need to do
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('Query test failed:', err)
|
||||
process.exit(1)
|
||||
})
|
||||
```
|
||||
|
||||
### Part E: Handle the streaming response
|
||||
|
||||
The QueryEngine likely uses the Anthropic SDK's streaming interface. Make sure:
|
||||
1. Text content is printed to stdout as it streams
|
||||
2. Thinking blocks are handled (displayed or hidden based on config)
|
||||
3. Tool use blocks trigger tool execution
|
||||
4. The tool loop feeds results back and continues
|
||||
|
||||
### Part F: Document what's still broken
|
||||
|
||||
After getting a basic query working, document:
|
||||
1. Which features work
|
||||
2. Which features are stubbed
|
||||
3. What would need to happen for full functionality
|
||||
|
||||
## Verification
|
||||
|
||||
1. `ANTHROPIC_API_KEY=sk-ant-... bun scripts/test-query.ts "What is 2+2?"` gets a response
|
||||
2. Streaming output appears in real-time
|
||||
3. No unhandled crashes (graceful error messages are fine)
|
||||
4. Architecture is documented
|
||||
100
prompts/10-context-and-prompts.md
Normal file
100
prompts/10-context-and-prompts.md
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# Prompt 10: Wire Up System Prompt, Context Gathering & Memory System
|
||||
|
||||
## Context
|
||||
|
||||
You are working in `/workspaces/claude-code`. The CLI constructs a detailed system prompt before each conversation. This prompt includes:
|
||||
1. **Static instructions** — core behavior rules (from `src/constants/prompts.ts`)
|
||||
2. **Dynamic context** — OS, shell, git status, working directory (from `src/context.ts`)
|
||||
3. **Tool descriptions** — auto-generated from tool schemas
|
||||
4. **Memory** — persistent `.claude.md` files (from `src/memdir/`)
|
||||
5. **User context** — config, preferences, project settings
|
||||
|
||||
## Key Files
|
||||
|
||||
- `src/constants/prompts.ts` — System prompt construction
|
||||
- `src/constants/system.ts` — System identity strings
|
||||
- `src/context.ts` — OS/shell/git context collection
|
||||
- `src/context/` — Additional context modules
|
||||
- `src/memdir/` — Memory directory system (reads `.claude.md`, `CLAUDE.md` files)
|
||||
- `src/utils/messages.ts` — Message construction helpers
|
||||
|
||||
## Task
|
||||
|
||||
### Part A: Trace the system prompt construction
|
||||
|
||||
Read `src/constants/prompts.ts` and map:
|
||||
1. What is `getSystemPrompt()`'s signature and return type?
|
||||
2. What sections does the system prompt contain?
|
||||
3. How are tools described in the prompt?
|
||||
4. What model-specific variations exist?
|
||||
5. Where does the `MACRO.ISSUES_EXPLAINER` reference resolve to?
|
||||
|
||||
### Part B: Fix the context gathering
|
||||
|
||||
Read `src/context.ts` and:
|
||||
1. Understand `getSystemContext()` and `getUserContext()`
|
||||
2. These collect OS info, shell version, git status, etc.
|
||||
3. Verify they work on Linux (this codebase was likely developed on macOS, so some paths may be macOS-specific)
|
||||
4. Fix any platform-specific issues
|
||||
|
||||
### Part C: Wire up the memory system
|
||||
|
||||
Read `src/memdir/` directory:
|
||||
1. How does it find `.claude.md` / `CLAUDE.md` files?
|
||||
2. How is memory content injected into the system prompt?
|
||||
3. Does it support project-level, user-level, and session-level memory?
|
||||
|
||||
Verify it works by:
|
||||
1. Creating a test `CLAUDE.md` in the project root
|
||||
2. Running the system prompt builder
|
||||
3. Checking the memory appears in the output
|
||||
|
||||
### Part D: Create a prompt inspection script
|
||||
|
||||
Create `scripts/test-prompt.ts`:
|
||||
```ts
|
||||
// scripts/test-prompt.ts
|
||||
// Dump the full system prompt that would be sent to the API
|
||||
// Usage: bun scripts/test-prompt.ts
|
||||
|
||||
import './src/shims/preload.js'
|
||||
|
||||
async function main() {
|
||||
// Import the prompt builder
|
||||
const { getSystemPrompt } = await import('./src/constants/prompts.js')
|
||||
|
||||
// May need to pass tools list and model name
|
||||
// Check the function signature
|
||||
const prompt = await getSystemPrompt([], 'claude-sonnet-4-20250514')
|
||||
|
||||
console.log('=== SYSTEM PROMPT ===')
|
||||
console.log(prompt.join('\n'))
|
||||
console.log('=== END ===')
|
||||
console.log(`\nTotal length: ${prompt.join('\n').length} characters`)
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('Prompt test failed:', err)
|
||||
process.exit(1)
|
||||
})
|
||||
```
|
||||
|
||||
### Part E: Fix MACRO references in prompts
|
||||
|
||||
The prompt system references `MACRO.ISSUES_EXPLAINER`. Make sure our `MACRO` global (from `src/shims/macro.ts`) provides this value. If the prompt references other `MACRO` fields, add them too.
|
||||
|
||||
### Part F: Context module audit
|
||||
|
||||
Check `src/context/` for additional context modules:
|
||||
- Project detection (language, framework)
|
||||
- Git integration (branch, status, recent commits)
|
||||
- Environment detection (CI, container, SSH)
|
||||
|
||||
Verify these work in our dev environment.
|
||||
|
||||
## Verification
|
||||
|
||||
1. `bun scripts/test-prompt.ts` dumps a complete system prompt
|
||||
2. The prompt includes: tool descriptions, OS context, memory content
|
||||
3. No `undefined` or `MACRO.` references in the output
|
||||
4. Memory system reads `.claude.md` from the project root
|
||||
113
prompts/11-mcp-integration.md
Normal file
113
prompts/11-mcp-integration.md
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
# Prompt 11: MCP Client/Server Integration
|
||||
|
||||
## Context
|
||||
|
||||
You are working in `/workspaces/claude-code`. The CLI has built-in MCP (Model Context Protocol) support:
|
||||
- **MCP Client** — connects to external MCP servers (tools, resources)
|
||||
- **MCP Server** — exposes Claude Code itself as an MCP server
|
||||
|
||||
MCP lets the CLI use tools provided by external servers and lets other clients use Claude Code as a tool provider.
|
||||
|
||||
## Key Files
|
||||
|
||||
- `src/services/mcp/` — MCP client implementation
|
||||
- `src/services/mcp/types.ts` — MCP config types
|
||||
- `src/entrypoints/mcp.ts` — MCP server mode entrypoint
|
||||
- `src/tools/MCPTool/` — Tool that calls MCP servers
|
||||
- `src/tools/ListMcpResourcesTool/` — Lists MCP resources
|
||||
- `src/tools/ReadMcpResourceTool/` — Reads MCP resources
|
||||
- `src/tools/McpAuthTool/` — MCP server authentication
|
||||
- `mcp-server/` — Standalone MCP server sub-project (from Prompt 04)
|
||||
|
||||
## Task
|
||||
|
||||
### Part A: Understand MCP client architecture
|
||||
|
||||
Read `src/services/mcp/` directory:
|
||||
1. How are MCP servers discovered? (`.mcp.json` config file?)
|
||||
2. How are MCP server connections established? (stdio, HTTP, SSE?)
|
||||
3. How are MCP tools registered and made available?
|
||||
4. What is the `ScopedMcpServerConfig` type?
|
||||
|
||||
### Part B: Understand MCP config format
|
||||
|
||||
Search for `.mcp.json` or MCP config loading code. Document:
|
||||
1. Where does the config file live? (`~/.claude/.mcp.json`? project root?)
|
||||
2. What's the config schema? (server name, command, args, env?)
|
||||
3. How are multiple servers configured?
|
||||
|
||||
Example config you might find:
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-server": {
|
||||
"command": "node",
|
||||
"args": ["path/to/server.js"],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Part C: Verify MCP SDK integration
|
||||
|
||||
The project uses `@modelcontextprotocol/sdk` (^1.12.1). Check:
|
||||
1. Is it installed in `node_modules/`?
|
||||
2. Does the import work: `import { Client } from '@modelcontextprotocol/sdk/client/index.js'`
|
||||
3. Are there version compatibility issues?
|
||||
|
||||
### Part D: Test MCP client with our own server
|
||||
|
||||
Create a test that:
|
||||
1. Starts the `mcp-server/` we fixed in Prompt 04 as a child process
|
||||
2. Connects to it via stdio using the MCP client from `src/services/mcp/`
|
||||
3. Lists available tools
|
||||
4. Calls one tool (e.g., `list_files` or `search_code`)
|
||||
|
||||
Create `scripts/test-mcp.ts`:
|
||||
```ts
|
||||
// scripts/test-mcp.ts
|
||||
// Test MCP client/server roundtrip
|
||||
// Usage: bun scripts/test-mcp.ts
|
||||
|
||||
import './src/shims/preload.js'
|
||||
|
||||
// TODO:
|
||||
// 1. Spawn mcp-server as a child process (stdio transport)
|
||||
// 2. Create MCP client from src/services/mcp/
|
||||
// 3. Connect client to server
|
||||
// 4. List tools
|
||||
// 5. Call a tool
|
||||
// 6. Print results
|
||||
```
|
||||
|
||||
### Part E: Test MCP server mode
|
||||
|
||||
The CLI can run as an MCP server itself (`src/entrypoints/mcp.ts`). Read this file and verify:
|
||||
1. What tools does it expose?
|
||||
2. What resources does it provide?
|
||||
3. Can it be started with `bun src/entrypoints/mcp.ts`?
|
||||
|
||||
### Part F: Create sample MCP config
|
||||
|
||||
Create a `.mcp.json` in the project root (or wherever the app looks for it) that configures the local MCP server:
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"claude-code-explorer": {
|
||||
"command": "node",
|
||||
"args": ["mcp-server/dist/index.js"],
|
||||
"env": {
|
||||
"CLAUDE_CODE_SRC_ROOT": "./src"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
1. MCP client code in `src/services/mcp/` loads without errors
|
||||
2. MCP server mode (`src/entrypoints/mcp.ts`) starts without crashing
|
||||
3. A roundtrip test (client → server → response) works
|
||||
4. `.mcp.json` config file is created and parseable
|
||||
123
prompts/12-services-layer.md
Normal file
123
prompts/12-services-layer.md
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
# Prompt 12: Wire Up Services Layer (Analytics, Policy, Settings, Sessions)
|
||||
|
||||
## Context
|
||||
|
||||
You are working in `/workspaces/claude-code`. The CLI has several background services that run during operation:
|
||||
- **Analytics/Telemetry** — GrowthBook feature flags, OpenTelemetry traces
|
||||
- **Policy Limits** — rate limiting, quota enforcement from Anthropic backend
|
||||
- **Remote Managed Settings** — server-pushed configuration
|
||||
- **Session Memory** — persistent conversation history across invocations
|
||||
- **Bootstrap Data** — initial config fetched from API on startup
|
||||
|
||||
Most of these talk to Anthropic's backend servers and will fail in our dev build. The goal is to make them fail gracefully (not crash the app) or provide stubs.
|
||||
|
||||
## Key Files
|
||||
|
||||
- `src/services/analytics/growthbook.ts` — GrowthBook feature flag client
|
||||
- `src/services/analytics/` — Telemetry, event logging
|
||||
- `src/services/policyLimits/` — Rate limit enforcement
|
||||
- `src/services/remoteManagedSettings/` — Server-pushed settings
|
||||
- `src/services/SessionMemory/` — Conversation persistence
|
||||
- `src/services/api/bootstrap.ts` — Initial data fetch
|
||||
- `src/entrypoints/init.ts` — Where most services are initialized
|
||||
- `src/cost-tracker.ts` — Token usage and cost tracking
|
||||
|
||||
## Task
|
||||
|
||||
### Part A: Map the initialization sequence
|
||||
|
||||
Read `src/entrypoints/init.ts` carefully. Document:
|
||||
1. What services are initialized, in what order?
|
||||
2. Which are blocking (must complete before app starts)?
|
||||
3. Which are fire-and-forget (async, can fail silently)?
|
||||
4. What happens if each one fails?
|
||||
|
||||
### Part B: Make GrowthBook optional
|
||||
|
||||
Read `src/services/analytics/growthbook.ts`:
|
||||
1. How is GrowthBook initialized?
|
||||
2. Where is it called from? (feature flag checks throughout the codebase)
|
||||
3. What happens if initialization fails?
|
||||
|
||||
**Goal**: Make GrowthBook fail silently — all feature flag checks should return `false` (default) if GrowthBook is unavailable. This may already be handled, but verify it.
|
||||
|
||||
### Part C: Stub policy limits
|
||||
|
||||
Read `src/services/policyLimits/`:
|
||||
1. What limits does it enforce? (messages per minute, tokens per day, etc.)
|
||||
2. What happens when a limit is hit?
|
||||
3. Where is `loadPolicyLimits()` called?
|
||||
|
||||
**Goal**: Make the app work without policy limits. Either:
|
||||
- Stub the service to return "no limits" (allow everything)
|
||||
- Or catch and ignore errors from the API call
|
||||
|
||||
### Part D: Make remote settings optional
|
||||
|
||||
Read `src/services/remoteManagedSettings/`:
|
||||
1. What settings does it manage?
|
||||
2. What's the fallback when the server is unreachable?
|
||||
|
||||
**Goal**: Ensure the app works with default settings when the remote endpoint fails.
|
||||
|
||||
### Part E: Handle bootstrap data
|
||||
|
||||
Read `src/services/api/bootstrap.ts`:
|
||||
1. What data does it fetch?
|
||||
2. What uses this data?
|
||||
3. What happens if the fetch fails?
|
||||
|
||||
**Goal**: Provide sensible defaults when bootstrap fails (no API key = no bootstrap).
|
||||
|
||||
### Part F: Verify session memory
|
||||
|
||||
Read `src/services/SessionMemory/`:
|
||||
1. Where is session data stored? (filesystem path)
|
||||
2. How are sessions identified?
|
||||
3. Does it work with the local filesystem?
|
||||
|
||||
**Goal**: Session memory should work out of the box since it's local filesystem.
|
||||
|
||||
### Part G: Wire up cost tracking
|
||||
|
||||
Read `src/cost-tracker.ts`:
|
||||
1. How are costs calculated?
|
||||
2. Where is usage reported?
|
||||
3. Does it persist across sessions?
|
||||
|
||||
**Goal**: Cost tracking should work locally (just display, no remote reporting needed).
|
||||
|
||||
### Part H: Create a services smoke test
|
||||
|
||||
Create `scripts/test-services.ts`:
|
||||
```ts
|
||||
// scripts/test-services.ts
|
||||
// Test that all services initialize without crashing
|
||||
// Usage: bun scripts/test-services.ts
|
||||
|
||||
import './src/shims/preload.js'
|
||||
|
||||
async function main() {
|
||||
console.log('Testing service initialization...')
|
||||
|
||||
// Try to run the init sequence
|
||||
try {
|
||||
const { init } = await import('./src/entrypoints/init.js')
|
||||
await init()
|
||||
console.log('✅ Services initialized')
|
||||
} catch (err: any) {
|
||||
console.error('❌ Init failed:', err.message)
|
||||
// Document which service failed and why
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
1. `bun scripts/test-services.ts` completes without crashing (warnings are fine)
|
||||
2. Missing remote services log warnings, not crashes
|
||||
3. Session memory reads/writes to the local filesystem
|
||||
4. Cost tracking displays locally
|
||||
5. The app can start even when Anthropic's backend is unreachable (with just an API key)
|
||||
75
prompts/13-bridge-ide.md
Normal file
75
prompts/13-bridge-ide.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# Prompt 13: Bridge Layer (VS Code / JetBrains IDE Integration)
|
||||
|
||||
## Context
|
||||
|
||||
You are working in `/workspaces/claude-code`. The "Bridge" is the subsystem that connects Claude Code to IDE extensions (VS Code, JetBrains). It enables:
|
||||
- Remote control of Claude Code from an IDE
|
||||
- Sharing file context between IDE and CLI
|
||||
- Permission approvals from the IDE UI
|
||||
- Session management across IDE and terminal
|
||||
|
||||
The Bridge is **gated behind `feature('BRIDGE_MODE')`** and is the most complex optional subsystem (~30 files in `src/bridge/`).
|
||||
|
||||
## Key Files
|
||||
|
||||
- `src/bridge/bridgeMain.ts` — Main bridge orchestration
|
||||
- `src/bridge/bridgeApi.ts` — Bridge API endpoints
|
||||
- `src/bridge/bridgeMessaging.ts` — WebSocket/HTTP messaging
|
||||
- `src/bridge/bridgeConfig.ts` — Bridge configuration
|
||||
- `src/bridge/bridgeUI.ts` — Bridge UI rendering
|
||||
- `src/bridge/jwtUtils.ts` — JWT authentication for bridge connections
|
||||
- `src/bridge/types.ts` — Bridge types
|
||||
- `src/bridge/initReplBridge.ts` — REPL integration
|
||||
- `src/bridge/replBridge.ts` — REPL bridge handle
|
||||
|
||||
## Task
|
||||
|
||||
### Part A: Understand the bridge architecture
|
||||
|
||||
Read `src/bridge/types.ts` and `src/bridge/bridgeMain.ts` (first 100 lines). Document:
|
||||
1. What protocols does the bridge use? (WebSocket, HTTP polling, etc.)
|
||||
2. How does authentication work? (JWT)
|
||||
3. What messages flow between IDE and CLI?
|
||||
4. How is the bridge lifecycle managed?
|
||||
|
||||
### Part B: Assess what's needed vs. what can be deferred
|
||||
|
||||
The bridge is a **nice-to-have** for initial build-out. Categorize:
|
||||
1. **Must work**: Feature flag gate (`feature('BRIDGE_MODE')` returns `false` → bridge code is skipped)
|
||||
2. **Can defer**: Full bridge functionality
|
||||
3. **Might break**: Code paths that assume bridge is available even when disabled
|
||||
|
||||
### Part C: Verify the feature gate works
|
||||
|
||||
Ensure that when `CLAUDE_CODE_BRIDGE_MODE=false` (or unset):
|
||||
1. Bridge code is not imported
|
||||
2. Bridge initialization is skipped
|
||||
3. No bridge-related errors appear
|
||||
4. The CLI works normally in terminal-only mode
|
||||
|
||||
### Part D: Stub the bridge for safety
|
||||
|
||||
If any code paths reference bridge functionality outside the feature gate:
|
||||
1. Create `src/bridge/stub.ts` with no-op implementations
|
||||
2. Make sure imports from `src/bridge/` resolve without crashing
|
||||
3. Ensure the REPL works without bridge
|
||||
|
||||
### Part E: Document bridge activation
|
||||
|
||||
For future work, document what would be needed to enable the bridge:
|
||||
1. Set `CLAUDE_CODE_BRIDGE_MODE=true`
|
||||
2. What IDE extension is needed?
|
||||
3. What authentication setup is required?
|
||||
4. What ports/sockets does it use?
|
||||
|
||||
### Part F: Check the Chrome extension bridge
|
||||
|
||||
There's a `--claude-in-chrome-mcp` and `--chrome-native-host` mode referenced in `src/entrypoints/cli.tsx`. Read these paths and document what they do. These can be deferred — just make sure they don't crash when not in use.
|
||||
|
||||
## Verification
|
||||
|
||||
1. CLI works normally with bridge disabled (default)
|
||||
2. No bridge-related errors in stdout/stderr
|
||||
3. `feature('BRIDGE_MODE')` correctly returns `false`
|
||||
4. Bridge architecture is documented for future enablement
|
||||
5. No dangling imports that crash when bridge is off
|
||||
137
prompts/14-dev-runner.md
Normal file
137
prompts/14-dev-runner.md
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
# Prompt 14: Create Development Runner
|
||||
|
||||
## Context
|
||||
|
||||
You are working in `/workspaces/claude-code`. By now you should have:
|
||||
- Bun installed (Prompt 01)
|
||||
- Runtime shims for `bun:bundle` and `MACRO` (Prompt 02)
|
||||
- A build system (Prompt 03)
|
||||
- Environment config (Prompt 05)
|
||||
|
||||
Now we need a way to **run the CLI in development mode** — quickly launching it without a full production build.
|
||||
|
||||
## Task
|
||||
|
||||
### Part A: Create `bun run dev` script
|
||||
|
||||
Bun can run TypeScript directly without compilation. Create a development launcher.
|
||||
|
||||
**Option 1: Direct Bun execution** (preferred)
|
||||
|
||||
Create `scripts/dev.ts`:
|
||||
```ts
|
||||
// scripts/dev.ts
|
||||
// Development launcher — runs the CLI directly via Bun
|
||||
// Usage: bun scripts/dev.ts [args...]
|
||||
// Or: bun run dev [args...]
|
||||
|
||||
// Load shims first
|
||||
import '../src/shims/preload.js'
|
||||
|
||||
// Register bun:bundle module resolver
|
||||
// Since Bun natively supports the module, we may need to
|
||||
// register our shim. Check if this is needed.
|
||||
|
||||
// Launch the CLI
|
||||
await import('../src/entrypoints/cli.js')
|
||||
```
|
||||
|
||||
**Option 2: Bun with preload**
|
||||
|
||||
Use Bun's `--preload` flag:
|
||||
```bash
|
||||
bun --preload ./src/shims/preload.ts src/entrypoints/cli.tsx
|
||||
```
|
||||
|
||||
**Investigate which approach works** with the `bun:bundle` import. The tricky part is that `bun:bundle` is a special Bun module name — at runtime (without the bundler), Bun may not recognize it. You'll need to either:
|
||||
1. Use Bun's `bunfig.toml` to create a module alias
|
||||
2. Use a loader/plugin to intercept the import
|
||||
3. Use a pre-transform step to rewrite imports
|
||||
|
||||
### Part B: Handle the `bun:bundle` import at runtime
|
||||
|
||||
This is the critical challenge. Options to investigate:
|
||||
|
||||
**Option A: `bunfig.toml` alias**
|
||||
```toml
|
||||
[resolve]
|
||||
alias = { "bun:bundle" = "./src/shims/bun-bundle.ts" }
|
||||
```
|
||||
|
||||
**Option B: Bun plugin**
|
||||
Create a Bun plugin that intercepts `bun:bundle`:
|
||||
```ts
|
||||
// scripts/bun-plugin-shims.ts
|
||||
import { plugin } from 'bun'
|
||||
|
||||
plugin({
|
||||
name: 'bun-bundle-shim',
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /^bun:bundle$/ }, () => ({
|
||||
path: resolve(import.meta.dir, '../src/shims/bun-bundle.ts'),
|
||||
}))
|
||||
},
|
||||
})
|
||||
```
|
||||
Then reference it in `bunfig.toml`:
|
||||
```toml
|
||||
preload = ["./scripts/bun-plugin-shims.ts"]
|
||||
```
|
||||
|
||||
**Option C: Patch at build time**
|
||||
If runtime aliasing doesn't work, use a quick pre-build transform that replaces `from 'bun:bundle'` with `from '../shims/bun-bundle.js'` across all files, outputting to a temp directory.
|
||||
|
||||
**Try the options in order** and go with whichever works.
|
||||
|
||||
### Part C: Add npm scripts
|
||||
|
||||
Add to `package.json`:
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "bun scripts/dev.ts",
|
||||
"dev:repl": "bun scripts/dev.ts --repl",
|
||||
"start": "bun scripts/dev.ts"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Part D: Create a `.env` loader
|
||||
|
||||
If the dev script doesn't automatically load `.env`, add dotenv support:
|
||||
```bash
|
||||
bun add -d dotenv-cli
|
||||
```
|
||||
Then wrap the dev command:
|
||||
```json
|
||||
"dev": "dotenv -e .env -- bun scripts/dev.ts"
|
||||
```
|
||||
|
||||
Or use Bun's built-in `.env` loading (Bun automatically reads `.env` files).
|
||||
|
||||
### Part E: Test the development runner
|
||||
|
||||
1. Set `ANTHROPIC_API_KEY` in `.env`
|
||||
2. Run `bun run dev --version` → should print version
|
||||
3. Run `bun run dev --help` → should print help text
|
||||
4. Run `bun run dev` → should start the interactive REPL (will need working Ink UI)
|
||||
5. Run `ANTHROPIC_API_KEY=sk-ant-... bun run dev -p "say hello"` → should make one API call and print response
|
||||
|
||||
### Part F: Add debug mode
|
||||
|
||||
Add a debug script that enables verbose logging:
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"dev:debug": "CLAUDE_CODE_DEBUG_LOG_LEVEL=debug bun scripts/dev.ts"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
1. `bun run dev --version` prints the version
|
||||
2. `bun run dev --help` prints help without errors
|
||||
3. The `bun:bundle` import resolves correctly at runtime
|
||||
4. `.env` variables are loaded
|
||||
5. No module resolution errors on startup
|
||||
123
prompts/15-production-bundle.md
Normal file
123
prompts/15-production-bundle.md
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
# Prompt 15: Production Bundle & Packaging
|
||||
|
||||
## Context
|
||||
|
||||
You are working in `/workspaces/claude-code`. By now you should have a working development runner (Prompt 14) and build system (Prompt 03). This prompt focuses on creating a production-quality bundle.
|
||||
|
||||
## Task
|
||||
|
||||
### Part A: Optimize the esbuild configuration
|
||||
|
||||
Update `scripts/build-bundle.ts` for production:
|
||||
|
||||
1. **Tree shaking** — esbuild does this by default, but verify:
|
||||
- Feature-gated code with `if (feature('X'))` where X is `false` should be eliminated
|
||||
- `process.env.USER_TYPE === 'ant'` branches should be eliminated (set `define` to replace with `false`)
|
||||
|
||||
2. **Define replacements** — Inline constants at build time:
|
||||
```ts
|
||||
define: {
|
||||
'process.env.USER_TYPE': '"external"', // Not 'ant' (Anthropic internal)
|
||||
'process.env.NODE_ENV': '"production"',
|
||||
}
|
||||
```
|
||||
|
||||
3. **Minification** — Enable for production (`--minify` flag)
|
||||
|
||||
4. **Source maps** — External source maps for production debugging
|
||||
|
||||
5. **Target** — Ensure compatibility with both Bun 1.1+ and Node.js 20+
|
||||
|
||||
### Part B: Handle chunking/splitting
|
||||
|
||||
The full bundle will be large (~2-5 MB minified). Consider:
|
||||
1. **Single file** — Simplest, works everywhere (recommended for CLI tools)
|
||||
2. **Code splitting** — Multiple chunks, only useful if we want lazy loading
|
||||
|
||||
Go with single file unless it causes issues.
|
||||
|
||||
### Part C: Create the executable
|
||||
|
||||
After bundling to `dist/cli.mjs`:
|
||||
|
||||
1. **Add shebang** — `#!/usr/bin/env node` (already in banner)
|
||||
2. **Make executable** — `chmod +x dist/cli.mjs`
|
||||
3. **Test it runs** — `./dist/cli.mjs --version`
|
||||
|
||||
### Part D: Platform packaging
|
||||
|
||||
Create packaging scripts for distribution:
|
||||
|
||||
**npm package** (`scripts/package-npm.ts`):
|
||||
```ts
|
||||
// Generate a publishable npm package in dist/npm/
|
||||
// - package.json with bin, main, version
|
||||
// - The bundled CLI file
|
||||
// - README.md
|
||||
```
|
||||
|
||||
**Standalone binary** (optional, via Bun):
|
||||
```bash
|
||||
bun build --compile src/entrypoints/cli.tsx --outfile dist/claude
|
||||
```
|
||||
This creates a single binary with Bun runtime embedded. Not all features will work, but it's worth testing.
|
||||
|
||||
### Part E: Docker build
|
||||
|
||||
Update the existing `Dockerfile` to produce a runnable container:
|
||||
|
||||
```dockerfile
|
||||
FROM oven/bun:1-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package.json bun.lockb* ./
|
||||
RUN bun install --frozen-lockfile || bun install
|
||||
COPY . .
|
||||
RUN bun run build:prod
|
||||
|
||||
FROM oven/bun:1-alpine
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/dist/cli.mjs /app/
|
||||
RUN apk add --no-cache git ripgrep
|
||||
ENTRYPOINT ["bun", "/app/cli.mjs"]
|
||||
```
|
||||
|
||||
### Part F: Verify production build
|
||||
|
||||
1. `bun run build:prod` succeeds
|
||||
2. `ls -lh dist/cli.mjs` — check file size
|
||||
3. `node dist/cli.mjs --version` — works with Node.js
|
||||
4. `bun dist/cli.mjs --version` — works with Bun
|
||||
5. `ANTHROPIC_API_KEY=... node dist/cli.mjs -p "hello"` — end-to-end works
|
||||
|
||||
### Part G: CI build script
|
||||
|
||||
Create `scripts/ci-build.sh`:
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "=== Installing dependencies ==="
|
||||
bun install
|
||||
|
||||
echo "=== Type checking ==="
|
||||
bun run typecheck
|
||||
|
||||
echo "=== Linting ==="
|
||||
bun run lint
|
||||
|
||||
echo "=== Building ==="
|
||||
bun run build:prod
|
||||
|
||||
echo "=== Verifying build ==="
|
||||
node dist/cli.mjs --version
|
||||
|
||||
echo "=== Done ==="
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
1. `bun run build:prod` produces `dist/cli.mjs`
|
||||
2. The bundle is < 10 MB (ideally < 5 MB)
|
||||
3. `node dist/cli.mjs --version` works
|
||||
4. `docker build .` succeeds (if Docker is available)
|
||||
5. CI script runs end-to-end without errors
|
||||
125
prompts/16-testing.md
Normal file
125
prompts/16-testing.md
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
# Prompt 16: Add Test Infrastructure & Smoke Tests
|
||||
|
||||
## Context
|
||||
|
||||
You are working in `/workspaces/claude-code`. The leaked source does not include any test files or test configuration (they were presumably in a separate directory or repo). We need to add a test framework and write smoke tests for core subsystems.
|
||||
|
||||
## Task
|
||||
|
||||
### Part A: Set up Vitest
|
||||
|
||||
```bash
|
||||
bun add -d vitest @types/node
|
||||
```
|
||||
|
||||
Create `vitest.config.ts`:
|
||||
```ts
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import { resolve } from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['tests/**/*.test.ts'],
|
||||
setupFiles: ['tests/setup.ts'],
|
||||
testTimeout: 30000,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'bun:bundle': resolve(__dirname, 'src/shims/bun-bundle.ts'),
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Create `tests/setup.ts`:
|
||||
```ts
|
||||
// Global test setup
|
||||
import '../src/shims/preload.js'
|
||||
```
|
||||
|
||||
Add to `package.json`:
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Part B: Write unit tests for shims
|
||||
|
||||
`tests/shims/bun-bundle.test.ts`:
|
||||
- Test `feature()` returns `false` for unknown flags
|
||||
- Test `feature()` returns `false` for disabled flags
|
||||
- Test `feature()` returns `true` when env var is set
|
||||
- Test `feature('ABLATION_BASELINE')` always returns `false`
|
||||
|
||||
`tests/shims/macro.test.ts`:
|
||||
- Test `MACRO.VERSION` is a string
|
||||
- Test `MACRO.PACKAGE_URL` is set
|
||||
- Test `MACRO.ISSUES_EXPLAINER` is set
|
||||
|
||||
### Part C: Write smoke tests for core modules
|
||||
|
||||
`tests/smoke/tools.test.ts`:
|
||||
- Test that `getTools()` returns an array
|
||||
- Test that each tool has: name, description, inputSchema
|
||||
- Test that BashTool, FileReadTool, FileWriteTool are present
|
||||
|
||||
`tests/smoke/commands.test.ts`:
|
||||
- Test that `getCommands()` returns an array
|
||||
- Test that each command has: name, execute function
|
||||
- Test that /help and /config commands exist
|
||||
|
||||
`tests/smoke/context.test.ts`:
|
||||
- Test that `getSystemContext()` returns OS info
|
||||
- Test that git status can be collected
|
||||
- Test that platform detection works on Linux
|
||||
|
||||
`tests/smoke/prompt.test.ts`:
|
||||
- Test that `getSystemPrompt()` returns a non-empty array
|
||||
- Test that the prompt includes tool descriptions
|
||||
- Test that MACRO references are resolved (no `undefined`)
|
||||
|
||||
### Part D: Write integration tests (if API key available)
|
||||
|
||||
`tests/integration/api.test.ts`:
|
||||
- Skip if `ANTHROPIC_API_KEY` is not set
|
||||
- Test API client creation
|
||||
- Test a simple message (hello world)
|
||||
- Test streaming works
|
||||
- Test tool use (calculator-style tool call)
|
||||
|
||||
`tests/integration/mcp.test.ts`:
|
||||
- Test MCP server starts
|
||||
- Test MCP client connects
|
||||
- Test tool listing
|
||||
- Test tool execution roundtrip
|
||||
|
||||
### Part E: Write build tests
|
||||
|
||||
`tests/build/bundle.test.ts`:
|
||||
- Test that `dist/cli.mjs` exists after build
|
||||
- Test that it has a shebang
|
||||
- Test that it's not empty
|
||||
- Test that `node dist/cli.mjs --version` exits cleanly
|
||||
|
||||
### Part F: Add pre-commit hook (optional)
|
||||
|
||||
If the project uses git hooks, add:
|
||||
```bash
|
||||
# In package.json or a git hook
|
||||
bun run typecheck && bun run test
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
1. `bun run test` runs all tests
|
||||
2. Shim tests pass
|
||||
3. Smoke tests pass (tools, commands, context, prompts load)
|
||||
4. Integration tests are skipped when no API key is set
|
||||
5. Integration tests pass when API key is available
|
||||
6. Test output is clear and readable
|
||||
197
scripts/build-bundle.ts
Normal file
197
scripts/build-bundle.ts
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
// 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, dirname } from 'path'
|
||||
import { chmodSync, readFileSync, existsSync } from 'fs'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
// Bun: import.meta.dir — Node 21+: import.meta.dirname — fallback
|
||||
const __dir: string =
|
||||
(import.meta as any).dir ??
|
||||
(import.meta as any).dirname ??
|
||||
dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const ROOT = resolve(__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'
|
||||
|
||||
// ── Plugin: resolve bare 'src/' imports (tsconfig baseUrl: ".") ──
|
||||
// The codebase uses `import ... from 'src/foo/bar.js'` which relies on
|
||||
// TypeScript's baseUrl resolution. This plugin maps those to real TS files.
|
||||
const srcResolverPlugin: esbuild.Plugin = {
|
||||
name: 'src-resolver',
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /^src\// }, (args) => {
|
||||
const basePath = resolve(ROOT, args.path)
|
||||
|
||||
// Already exists as-is
|
||||
if (existsSync(basePath)) {
|
||||
return { path: basePath }
|
||||
}
|
||||
|
||||
// Strip .js/.jsx and try TypeScript extensions
|
||||
const withoutExt = basePath.replace(/\.(js|jsx)$/, '')
|
||||
for (const ext of ['.ts', '.tsx', '.js', '.jsx']) {
|
||||
const candidate = withoutExt + ext
|
||||
if (existsSync(candidate)) {
|
||||
return { path: candidate }
|
||||
}
|
||||
}
|
||||
|
||||
// Try as directory with index file
|
||||
const dirPath = basePath.replace(/\.(js|jsx)$/, '')
|
||||
for (const ext of ['.ts', '.tsx', '.js', '.jsx']) {
|
||||
const candidate = resolve(dirPath, 'index' + ext)
|
||||
if (existsSync(candidate)) {
|
||||
return { path: candidate }
|
||||
}
|
||||
}
|
||||
|
||||
// Let esbuild handle it (will error if truly missing)
|
||||
return undefined
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
plugins: [srcResolverPlugin],
|
||||
|
||||
// Use tsconfig for baseUrl / paths resolution (complements plugin above)
|
||||
tsconfig: resolve(ROOT, 'tsconfig.json'),
|
||||
|
||||
// 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',
|
||||
'sharp',
|
||||
'image-processor-napi',
|
||||
// Anthropic-internal packages (not published externally)
|
||||
'@anthropic-ai/sandbox-runtime',
|
||||
'@anthropic-ai/claude-agent-sdk',
|
||||
// Anthropic-internal (@ant/) packages — gated behind USER_TYPE === 'ant'
|
||||
'@ant/*',
|
||||
],
|
||||
|
||||
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
|
||||
// MACRO.* — originally inlined by Bun's bundler at compile time
|
||||
// process.env.USER_TYPE — eliminates 'ant' (Anthropic-internal) code branches
|
||||
define: {
|
||||
'MACRO.VERSION': JSON.stringify(version),
|
||||
'MACRO.PACKAGE_URL': JSON.stringify('@anthropic-ai/claude-code'),
|
||||
'MACRO.ISSUES_EXPLAINER': JSON.stringify(
|
||||
'report issues at https://github.com/anthropics/claude-code/issues'
|
||||
),
|
||||
'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 as { bytes: number }).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)
|
||||
})
|
||||
58
scripts/build-web.ts
Normal file
58
scripts/build-web.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// scripts/build-web.ts
|
||||
// Bundles the browser-side terminal frontend.
|
||||
//
|
||||
// Usage:
|
||||
// bun scripts/build-web.ts # dev build
|
||||
// bun scripts/build-web.ts --watch # watch mode
|
||||
// bun scripts/build-web.ts --minify # production (minified)
|
||||
|
||||
import * as esbuild from 'esbuild'
|
||||
import { resolve, dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __dir: string =
|
||||
(import.meta as any).dir ??
|
||||
(import.meta as any).dirname ??
|
||||
dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const ROOT = resolve(__dir, '..')
|
||||
const ENTRY = resolve(ROOT, 'src/server/web/terminal.ts')
|
||||
const OUT_DIR = resolve(ROOT, 'src/server/web/public')
|
||||
|
||||
const watch = process.argv.includes('--watch')
|
||||
const minify = process.argv.includes('--minify')
|
||||
|
||||
const buildOptions: esbuild.BuildOptions = {
|
||||
entryPoints: [ENTRY],
|
||||
bundle: true,
|
||||
platform: 'browser',
|
||||
target: ['es2020', 'chrome90', 'firefox90', 'safari14'],
|
||||
format: 'esm',
|
||||
outdir: OUT_DIR,
|
||||
// CSS imported from JS is auto-emitted alongside the JS output
|
||||
loader: { '.css': 'css' },
|
||||
minify,
|
||||
sourcemap: minify ? false : 'inline',
|
||||
tsconfig: resolve(ROOT, 'src/server/web/tsconfig.json'),
|
||||
logLevel: 'info',
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (watch) {
|
||||
const ctx = await esbuild.context(buildOptions)
|
||||
await ctx.watch()
|
||||
console.log('Watching src/server/web/terminal.ts...')
|
||||
} else {
|
||||
const start = Date.now()
|
||||
const result = await esbuild.build(buildOptions)
|
||||
if (result.errors.length > 0) {
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(`Web build complete in ${Date.now() - start}ms → ${OUT_DIR}`)
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
58
scripts/build.sh
Normal file
58
scripts/build.sh
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
#!/usr/bin/env bash
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# build.sh — Minimal build / check script for the leaked source
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Usage:
|
||||
# ./scripts/build.sh # install + typecheck + lint
|
||||
# ./scripts/build.sh install # install deps only
|
||||
# ./scripts/build.sh check # typecheck + lint only
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
set -euo pipefail
|
||||
|
||||
STEP="${1:-all}"
|
||||
|
||||
install_deps() {
|
||||
echo "── Installing dependencies ──"
|
||||
if command -v bun &>/dev/null; then
|
||||
bun install
|
||||
elif command -v npm &>/dev/null; then
|
||||
npm install
|
||||
else
|
||||
echo "Error: neither bun nor npm found on PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
typecheck() {
|
||||
echo "── Running TypeScript type-check ──"
|
||||
npx tsc --noEmit
|
||||
}
|
||||
|
||||
lint() {
|
||||
echo "── Running Biome lint ──"
|
||||
npx @biomejs/biome check src/
|
||||
}
|
||||
|
||||
case "$STEP" in
|
||||
install)
|
||||
install_deps
|
||||
;;
|
||||
check)
|
||||
typecheck
|
||||
lint
|
||||
;;
|
||||
all)
|
||||
install_deps
|
||||
typecheck
|
||||
lint
|
||||
;;
|
||||
*)
|
||||
echo "Unknown step: $STEP"
|
||||
echo "Usage: $0 [install|check|all]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "── Done ──"
|
||||
|
||||
|
||||
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')
|
||||
91
scripts/package-npm.ts
Normal file
91
scripts/package-npm.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
// 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'
|
||||
|
||||
// Bun: import.meta.dir — Node 21+: import.meta.dirname — fallback
|
||||
const __dir: string =
|
||||
(import.meta as ImportMeta & { dir?: string; dirname?: string }).dir ??
|
||||
(import.meta as ImportMeta & { dir?: string; dirname?: string }).dirname ??
|
||||
new URL('.', import.meta.url).pathname
|
||||
|
||||
const ROOT = resolve(__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()
|
||||
64
scripts/test-commands.ts
Normal file
64
scripts/test-commands.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
// scripts/test-commands.ts
|
||||
// Verify all commands load without errors
|
||||
// Usage: bun scripts/test-commands.ts
|
||||
//
|
||||
// The bun:bundle shim is loaded automatically via bunfig.toml preload.
|
||||
|
||||
// Load MACRO global before any app code
|
||||
import '../src/shims/macro.js'
|
||||
|
||||
async function main() {
|
||||
const { getCommands } = await import('../src/commands.js')
|
||||
|
||||
const cwd = process.cwd()
|
||||
const commands = await getCommands(cwd)
|
||||
|
||||
console.log(`Loaded ${commands.length} commands:\n`)
|
||||
|
||||
// Group commands by type for readability
|
||||
const byType: Record<string, typeof commands> = {}
|
||||
for (const cmd of commands) {
|
||||
const t = cmd.type
|
||||
if (!byType[t]) byType[t] = []
|
||||
byType[t]!.push(cmd)
|
||||
}
|
||||
|
||||
for (const [type, cmds] of Object.entries(byType)) {
|
||||
console.log(` [${type}] (${cmds.length} commands)`)
|
||||
for (const cmd of cmds) {
|
||||
const aliases = cmd.aliases?.length ? ` (aliases: ${cmd.aliases.join(', ')})` : ''
|
||||
const hidden = cmd.isHidden ? ' [hidden]' : ''
|
||||
const source = cmd.type === 'prompt' ? ` (source: ${cmd.source})` : ''
|
||||
console.log(` /${cmd.name} — ${cmd.description || '(no description)'}${aliases}${hidden}${source}`)
|
||||
}
|
||||
console.log()
|
||||
}
|
||||
|
||||
// Verify essential commands are present
|
||||
const essential = ['help', 'config', 'init', 'commit', 'review']
|
||||
const commandNames = new Set(commands.map(c => c.name))
|
||||
const missing = essential.filter(n => !commandNames.has(n))
|
||||
|
||||
if (missing.length > 0) {
|
||||
console.error(`❌ Missing essential commands: ${missing.join(', ')}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(`✅ All ${essential.length} essential commands present: ${essential.join(', ')}`)
|
||||
|
||||
// Check moved-to-plugin commands
|
||||
const movedToPlugin = commands.filter(
|
||||
c => c.type === 'prompt' && c.description && c.name
|
||||
).filter(c => ['security-review', 'pr-comments'].includes(c.name))
|
||||
|
||||
if (movedToPlugin.length > 0) {
|
||||
console.log(`✅ Moved-to-plugin commands present and loadable: ${movedToPlugin.map(c => c.name).join(', ')}`)
|
||||
}
|
||||
|
||||
console.log(`\n✅ Command system loaded successfully (${commands.length} commands)`)
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('❌ Command loading failed:', err)
|
||||
process.exit(1)
|
||||
})
|
||||
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)
|
||||
})
|
||||
15
scripts/tsconfig.json
Normal file
15
scripts/tsconfig.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["./**/*.ts", "./types.d.ts"]
|
||||
}
|
||||
86
scripts/types.d.ts
vendored
Normal file
86
scripts/types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
// Local type declarations for scripts/ — avoids depending on installed packages
|
||||
// for type checking in build scripts.
|
||||
|
||||
// ── esbuild (minimal surface used by build-bundle.ts) ──
|
||||
declare module 'esbuild' {
|
||||
export interface Plugin {
|
||||
name: string
|
||||
setup(build: PluginBuild): void
|
||||
}
|
||||
|
||||
export interface PluginBuild {
|
||||
onResolve(
|
||||
options: { filter: RegExp },
|
||||
callback: (args: OnResolveArgs) => OnResolveResult | undefined | null,
|
||||
): void
|
||||
}
|
||||
|
||||
export interface OnResolveArgs {
|
||||
path: string
|
||||
importer: string
|
||||
namespace: string
|
||||
resolveDir: string
|
||||
kind: string
|
||||
pluginData: unknown
|
||||
}
|
||||
|
||||
export interface OnResolveResult {
|
||||
path?: string
|
||||
external?: boolean
|
||||
namespace?: string
|
||||
pluginData?: unknown
|
||||
}
|
||||
|
||||
export interface BuildOptions {
|
||||
entryPoints?: string[]
|
||||
bundle?: boolean
|
||||
platform?: string
|
||||
target?: string[]
|
||||
format?: string
|
||||
outdir?: string
|
||||
outExtension?: Record<string, string>
|
||||
splitting?: boolean
|
||||
plugins?: Plugin[]
|
||||
tsconfig?: string
|
||||
alias?: Record<string, string>
|
||||
external?: string[]
|
||||
jsx?: string
|
||||
sourcemap?: boolean | string
|
||||
minify?: boolean
|
||||
treeShaking?: boolean
|
||||
define?: Record<string, string>
|
||||
banner?: Record<string, string>
|
||||
resolveExtensions?: string[]
|
||||
logLevel?: string
|
||||
metafile?: boolean
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface Metafile {
|
||||
inputs: Record<string, { bytes: number; imports: unknown[] }>
|
||||
outputs: Record<string, { bytes: number; inputs: unknown[]; exports: string[] }>
|
||||
}
|
||||
|
||||
export interface BuildResult {
|
||||
errors: { text: string }[]
|
||||
warnings: { text: string }[]
|
||||
metafile?: Metafile
|
||||
}
|
||||
|
||||
export interface BuildContext {
|
||||
watch(): Promise<void>
|
||||
serve(options?: unknown): Promise<unknown>
|
||||
rebuild(): Promise<BuildResult>
|
||||
dispose(): Promise<void>
|
||||
}
|
||||
|
||||
export function build(options: BuildOptions): Promise<BuildResult>
|
||||
export function context(options: BuildOptions): Promise<BuildContext>
|
||||
export function analyzeMetafile(metafile: Metafile, options?: { verbose?: boolean }): Promise<string>
|
||||
}
|
||||
|
||||
// ── Bun's ImportMeta extensions ──
|
||||
interface ImportMeta {
|
||||
dir: string
|
||||
dirname: string
|
||||
}
|
||||
24
server.json
Normal file
24
server.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
cl{
|
||||
"$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": "Explore the Claude Code CLI source — browse tools, commands, search the 512K-line codebase.",
|
||||
"repository": {
|
||||
"url": "https://github.com/nirholas/claude-code",
|
||||
"source": "github",
|
||||
"subfolder": "mcp-server"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
1297
src/QueryEngine.ts
Normal file
1297
src/QueryEngine.ts
Normal file
File diff suppressed because it is too large
Load diff
127
src/Task.ts
Normal file
127
src/Task.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import { randomBytes } from 'crypto'
|
||||
import type { AppState } from './state/AppState.js'
|
||||
import type { AgentId } from './types/ids.js'
|
||||
import { getTaskOutputPath } from './utils/task/diskOutput.js'
|
||||
|
||||
export type TaskType =
|
||||
| 'local_bash'
|
||||
| 'local_agent'
|
||||
| 'remote_agent'
|
||||
| 'in_process_teammate'
|
||||
| 'local_workflow'
|
||||
| 'monitor_mcp'
|
||||
| 'dream'
|
||||
|
||||
export type TaskStatus =
|
||||
| 'pending'
|
||||
| 'running'
|
||||
| 'completed'
|
||||
| 'failed'
|
||||
| 'killed'
|
||||
|
||||
/**
|
||||
* True when a task is in a terminal state and will not transition further.
|
||||
* Used to guard against injecting messages into dead teammates, evicting
|
||||
* finished tasks from AppState, and orphan-cleanup paths.
|
||||
*/
|
||||
export function isTerminalTaskStatus(status: TaskStatus): boolean {
|
||||
return status === 'completed' || status === 'failed' || status === 'killed'
|
||||
}
|
||||
|
||||
export type TaskHandle = {
|
||||
taskId: string
|
||||
cleanup?: () => void
|
||||
}
|
||||
|
||||
export type SetAppState = (f: (prev: AppState) => AppState) => void
|
||||
|
||||
export type TaskContext = {
|
||||
abortController: AbortController
|
||||
getAppState: () => AppState
|
||||
setAppState: SetAppState
|
||||
}
|
||||
|
||||
// Base fields shared by all task states
|
||||
export type TaskStateBase = {
|
||||
id: string
|
||||
type: TaskType
|
||||
status: TaskStatus
|
||||
description: string
|
||||
toolUseId?: string
|
||||
startTime: number
|
||||
endTime?: number
|
||||
totalPausedMs?: number
|
||||
outputFile: string
|
||||
outputOffset: number
|
||||
notified: boolean
|
||||
}
|
||||
|
||||
export type LocalShellSpawnInput = {
|
||||
command: string
|
||||
description: string
|
||||
timeout?: number
|
||||
toolUseId?: string
|
||||
agentId?: AgentId
|
||||
/** UI display variant: description-as-label, dialog title, status bar pill. */
|
||||
kind?: 'bash' | 'monitor'
|
||||
}
|
||||
|
||||
// What getTaskByType dispatches for: kill. spawn/render were never
|
||||
// called polymorphically (removed in #22546). All six kill implementations
|
||||
// use only setAppState — getAppState/abortController were dead weight.
|
||||
export type Task = {
|
||||
name: string
|
||||
type: TaskType
|
||||
kill(taskId: string, setAppState: SetAppState): Promise<void>
|
||||
}
|
||||
|
||||
// Task ID prefixes
|
||||
const TASK_ID_PREFIXES: Record<string, string> = {
|
||||
local_bash: 'b', // Keep as 'b' for backward compatibility
|
||||
local_agent: 'a',
|
||||
remote_agent: 'r',
|
||||
in_process_teammate: 't',
|
||||
local_workflow: 'w',
|
||||
monitor_mcp: 'm',
|
||||
dream: 'd',
|
||||
}
|
||||
|
||||
// Get task ID prefix
|
||||
function getTaskIdPrefix(type: TaskType): string {
|
||||
return TASK_ID_PREFIXES[type] ?? 'x'
|
||||
}
|
||||
|
||||
// Case-insensitive-safe alphabet (digits + lowercase) for task IDs.
|
||||
// 36^8 ≈ 2.8 trillion combinations, sufficient to resist brute-force symlink attacks.
|
||||
const TASK_ID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'
|
||||
|
||||
export function generateTaskId(type: TaskType): string {
|
||||
const prefix = getTaskIdPrefix(type)
|
||||
const bytes = randomBytes(8)
|
||||
let id = prefix
|
||||
for (let i = 0; i < 8; i++) {
|
||||
id += TASK_ID_ALPHABET[bytes[i]! % TASK_ID_ALPHABET.length]
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
export function createTaskStateBase(
|
||||
id: string,
|
||||
type: TaskType,
|
||||
description: string,
|
||||
toolUseId?: string,
|
||||
): TaskStateBase {
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
status: 'pending',
|
||||
description,
|
||||
toolUseId,
|
||||
startTime: Date.now(),
|
||||
outputFile: getTaskOutputPath(id),
|
||||
outputOffset: 0,
|
||||
notified: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
794
src/Tool.ts
Normal file
794
src/Tool.ts
Normal file
|
|
@ -0,0 +1,794 @@
|
|||
import type {
|
||||
ToolResultBlockParam,
|
||||
ToolUseBlockParam,
|
||||
} from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import type {
|
||||
ElicitRequestURLParams,
|
||||
ElicitResult,
|
||||
} from '@modelcontextprotocol/sdk/types.js'
|
||||
import type { UUID } from 'crypto'
|
||||
import type { z } from 'zod/v4'
|
||||
import type { Command } from './commands.js'
|
||||
import type { CanUseToolFn } from './hooks/useCanUseTool.js'
|
||||
import type { ThinkingConfig } from './utils/thinking.js'
|
||||
|
||||
export type ToolInputJSONSchema = {
|
||||
[x: string]: unknown
|
||||
type: 'object'
|
||||
properties?: {
|
||||
[x: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
import type { Notification } from './context/notifications.js'
|
||||
import type {
|
||||
MCPServerConnection,
|
||||
ServerResource,
|
||||
} from './services/mcp/types.js'
|
||||
import type {
|
||||
AgentDefinition,
|
||||
AgentDefinitionsResult,
|
||||
} from './tools/AgentTool/loadAgentsDir.js'
|
||||
import type {
|
||||
AssistantMessage,
|
||||
AttachmentMessage,
|
||||
Message,
|
||||
ProgressMessage,
|
||||
SystemLocalCommandMessage,
|
||||
SystemMessage,
|
||||
UserMessage,
|
||||
} from './types/message.js'
|
||||
// Import permission types from centralized location to break import cycles
|
||||
// Import PermissionResult from centralized location to break import cycles
|
||||
import type {
|
||||
AdditionalWorkingDirectory,
|
||||
PermissionMode,
|
||||
PermissionResult,
|
||||
} from './types/permissions.js'
|
||||
// Import tool progress types from centralized location to break import cycles
|
||||
import type {
|
||||
AgentToolProgress,
|
||||
BashProgress,
|
||||
MCPProgress,
|
||||
REPLToolProgress,
|
||||
SkillToolProgress,
|
||||
TaskOutputProgress,
|
||||
ToolProgressData,
|
||||
WebSearchProgress,
|
||||
} from './types/tools.js'
|
||||
import type { FileStateCache } from './utils/fileStateCache.js'
|
||||
import type { DenialTrackingState } from './utils/permissions/denialTracking.js'
|
||||
import type { SystemPrompt } from './utils/systemPromptType.js'
|
||||
import type { ContentReplacementState } from './utils/toolResultStorage.js'
|
||||
|
||||
// Re-export progress types for backwards compatibility
|
||||
export type {
|
||||
AgentToolProgress,
|
||||
BashProgress,
|
||||
MCPProgress,
|
||||
REPLToolProgress,
|
||||
SkillToolProgress,
|
||||
TaskOutputProgress,
|
||||
WebSearchProgress,
|
||||
}
|
||||
|
||||
import type { SpinnerMode } from './components/Spinner.js'
|
||||
import type { QuerySource } from './constants/querySource.js'
|
||||
import type { SDKStatus } from './entrypoints/agentSdkTypes.js'
|
||||
import type { AppState } from './state/AppState.js'
|
||||
import type {
|
||||
HookProgress,
|
||||
PromptRequest,
|
||||
PromptResponse,
|
||||
} from './types/hooks.js'
|
||||
import type { AgentId } from './types/ids.js'
|
||||
import type { DeepImmutable } from './types/utils.js'
|
||||
import type { AttributionState } from './utils/commitAttribution.js'
|
||||
import type { FileHistoryState } from './utils/fileHistory.js'
|
||||
import type { Theme, ThemeName } from './utils/theme.js'
|
||||
|
||||
export type QueryChainTracking = {
|
||||
chainId: string
|
||||
depth: number
|
||||
}
|
||||
|
||||
export type ValidationResult =
|
||||
| { result: true }
|
||||
| {
|
||||
result: false
|
||||
message: string
|
||||
errorCode: number
|
||||
}
|
||||
|
||||
export type SetToolJSXFn = (
|
||||
args: {
|
||||
jsx: React.ReactNode | null
|
||||
shouldHidePromptInput: boolean
|
||||
shouldContinueAnimation?: true
|
||||
showSpinner?: boolean
|
||||
isLocalJSXCommand?: boolean
|
||||
isImmediate?: boolean
|
||||
/** Set to true to clear a local JSX command (e.g., from its onDone callback) */
|
||||
clearLocalJSX?: boolean
|
||||
} | null,
|
||||
) => void
|
||||
|
||||
// Import tool permission types from centralized location to break import cycles
|
||||
import type { ToolPermissionRulesBySource } from './types/permissions.js'
|
||||
|
||||
// Re-export for backwards compatibility
|
||||
export type { ToolPermissionRulesBySource }
|
||||
|
||||
// Apply DeepImmutable to the imported type
|
||||
export type ToolPermissionContext = DeepImmutable<{
|
||||
mode: PermissionMode
|
||||
additionalWorkingDirectories: Map<string, AdditionalWorkingDirectory>
|
||||
alwaysAllowRules: ToolPermissionRulesBySource
|
||||
alwaysDenyRules: ToolPermissionRulesBySource
|
||||
alwaysAskRules: ToolPermissionRulesBySource
|
||||
isBypassPermissionsModeAvailable: boolean
|
||||
isAutoModeAvailable?: boolean
|
||||
strippedDangerousRules?: ToolPermissionRulesBySource
|
||||
/** When true, permission prompts are auto-denied (e.g., background agents that can't show UI) */
|
||||
shouldAvoidPermissionPrompts?: boolean
|
||||
/** When true, automated checks (classifier, hooks) are awaited before showing the permission dialog (coordinator workers) */
|
||||
awaitAutomatedChecksBeforeDialog?: boolean
|
||||
/** Stores the permission mode before model-initiated plan mode entry, so it can be restored on exit */
|
||||
prePlanMode?: PermissionMode
|
||||
}>
|
||||
|
||||
export const getEmptyToolPermissionContext: () => ToolPermissionContext =
|
||||
() => ({
|
||||
mode: 'default',
|
||||
additionalWorkingDirectories: new Map(),
|
||||
alwaysAllowRules: {},
|
||||
alwaysDenyRules: {},
|
||||
alwaysAskRules: {},
|
||||
isBypassPermissionsModeAvailable: false,
|
||||
})
|
||||
|
||||
export type CompactProgressEvent =
|
||||
| {
|
||||
type: 'hooks_start'
|
||||
hookType: 'pre_compact' | 'post_compact' | 'session_start'
|
||||
}
|
||||
| { type: 'compact_start' }
|
||||
| { type: 'compact_end' }
|
||||
|
||||
export type ToolUseContext = {
|
||||
options: {
|
||||
commands: Command[]
|
||||
debug: boolean
|
||||
mainLoopModel: string
|
||||
tools: Tools
|
||||
verbose: boolean
|
||||
thinkingConfig: ThinkingConfig
|
||||
mcpClients: MCPServerConnection[]
|
||||
mcpResources: Record<string, ServerResource[]>
|
||||
isNonInteractiveSession: boolean
|
||||
agentDefinitions: AgentDefinitionsResult
|
||||
maxBudgetUsd?: number
|
||||
/** Custom system prompt that replaces the default system prompt */
|
||||
customSystemPrompt?: string
|
||||
/** Additional system prompt appended after the main system prompt */
|
||||
appendSystemPrompt?: string
|
||||
/** Override querySource for analytics tracking */
|
||||
querySource?: QuerySource
|
||||
/** Optional callback to get the latest tools (e.g., after MCP servers connect mid-query) */
|
||||
refreshTools?: () => Tools
|
||||
}
|
||||
abortController: AbortController
|
||||
readFileState: FileStateCache
|
||||
getAppState(): AppState
|
||||
setAppState(f: (prev: AppState) => AppState): void
|
||||
/**
|
||||
* Always-shared setAppState for session-scoped infrastructure (background
|
||||
* tasks, session hooks). Unlike setAppState, which is no-op for async agents
|
||||
* (see createSubagentContext), this always reaches the root store so agents
|
||||
* at any nesting depth can register/clean up infrastructure that outlives
|
||||
* a single turn. Only set by createSubagentContext; main-thread contexts
|
||||
* fall back to setAppState.
|
||||
*/
|
||||
setAppStateForTasks?: (f: (prev: AppState) => AppState) => void
|
||||
/**
|
||||
* Optional handler for URL elicitations triggered by tool call errors (-32042).
|
||||
* In print/SDK mode, this delegates to structuredIO.handleElicitation.
|
||||
* In REPL mode, this is undefined and the queue-based UI path is used.
|
||||
*/
|
||||
handleElicitation?: (
|
||||
serverName: string,
|
||||
params: ElicitRequestURLParams,
|
||||
signal: AbortSignal,
|
||||
) => Promise<ElicitResult>
|
||||
setToolJSX?: SetToolJSXFn
|
||||
addNotification?: (notif: Notification) => void
|
||||
/** Append a UI-only system message to the REPL message list. Stripped at the
|
||||
* normalizeMessagesForAPI boundary — the Exclude<> makes that type-enforced. */
|
||||
appendSystemMessage?: (
|
||||
msg: Exclude<SystemMessage, SystemLocalCommandMessage>,
|
||||
) => void
|
||||
/** Send an OS-level notification (iTerm2, Kitty, Ghostty, bell, etc.) */
|
||||
sendOSNotification?: (opts: {
|
||||
message: string
|
||||
notificationType: string
|
||||
}) => void
|
||||
nestedMemoryAttachmentTriggers?: Set<string>
|
||||
/**
|
||||
* CLAUDE.md paths already injected as nested_memory attachments this
|
||||
* session. Dedup for memoryFilesToAttachments — readFileState is an LRU
|
||||
* that evicts entries in busy sessions, so its .has() check alone can
|
||||
* re-inject the same CLAUDE.md dozens of times.
|
||||
*/
|
||||
loadedNestedMemoryPaths?: Set<string>
|
||||
dynamicSkillDirTriggers?: Set<string>
|
||||
/** Skill names surfaced via skill_discovery this session. Telemetry only (feeds was_discovered). */
|
||||
discoveredSkillNames?: Set<string>
|
||||
userModified?: boolean
|
||||
setInProgressToolUseIDs: (f: (prev: Set<string>) => Set<string>) => void
|
||||
/** Only wired in interactive (REPL) contexts; SDK/QueryEngine don't set this. */
|
||||
setHasInterruptibleToolInProgress?: (v: boolean) => void
|
||||
setResponseLength: (f: (prev: number) => number) => void
|
||||
/** Ant-only: push a new API metrics entry for OTPS tracking.
|
||||
* Called by subagent streaming when a new API request starts. */
|
||||
pushApiMetricsEntry?: (ttftMs: number) => void
|
||||
setStreamMode?: (mode: SpinnerMode) => void
|
||||
onCompactProgress?: (event: CompactProgressEvent) => void
|
||||
setSDKStatus?: (status: SDKStatus) => void
|
||||
openMessageSelector?: () => void
|
||||
updateFileHistoryState: (
|
||||
updater: (prev: FileHistoryState) => FileHistoryState,
|
||||
) => void
|
||||
updateAttributionState: (
|
||||
updater: (prev: AttributionState) => AttributionState,
|
||||
) => void
|
||||
setConversationId?: (id: UUID) => void
|
||||
agentId?: AgentId // Only set for subagents; use getSessionId() for session ID. Hooks use this to distinguish subagent calls.
|
||||
agentType?: string // Subagent type name. For the main thread's --agent type, hooks fall back to getMainThreadAgentType().
|
||||
/** When true, canUseTool must always be called even when hooks auto-approve.
|
||||
* Used by speculation for overlay file path rewriting. */
|
||||
requireCanUseTool?: boolean
|
||||
messages: Message[]
|
||||
fileReadingLimits?: {
|
||||
maxTokens?: number
|
||||
maxSizeBytes?: number
|
||||
}
|
||||
globLimits?: {
|
||||
maxResults?: number
|
||||
}
|
||||
toolDecisions?: Map<
|
||||
string,
|
||||
{
|
||||
source: string
|
||||
decision: 'accept' | 'reject'
|
||||
timestamp: number
|
||||
}
|
||||
>
|
||||
queryTracking?: QueryChainTracking
|
||||
/** Callback factory for requesting interactive prompts from the user.
|
||||
* Returns a prompt callback bound to the given source name.
|
||||
* Only available in interactive (REPL) contexts. */
|
||||
requestPrompt?: (
|
||||
sourceName: string,
|
||||
toolInputSummary?: string | null,
|
||||
) => (request: PromptRequest) => Promise<PromptResponse>
|
||||
toolUseId?: string
|
||||
criticalSystemReminder_EXPERIMENTAL?: string
|
||||
/** When true, preserve toolUseResult on messages even for subagents.
|
||||
* Used by in-process teammates whose transcripts are viewable by the user. */
|
||||
preserveToolUseResults?: boolean
|
||||
/** Local denial tracking state for async subagents whose setAppState is a
|
||||
* no-op. Without this, the denial counter never accumulates and the
|
||||
* fallback-to-prompting threshold is never reached. Mutable — the
|
||||
* permissions code updates it in place. */
|
||||
localDenialTracking?: DenialTrackingState
|
||||
/**
|
||||
* Per-conversation-thread content replacement state for the tool result
|
||||
* budget. When present, query.ts applies the aggregate tool result budget.
|
||||
* Main thread: REPL provisions once (never resets — stale UUID keys
|
||||
* are inert). Subagents: createSubagentContext clones the parent's state
|
||||
* by default (cache-sharing forks need identical decisions), or
|
||||
* resumeAgentBackground threads one reconstructed from sidechain records.
|
||||
*/
|
||||
contentReplacementState?: ContentReplacementState
|
||||
/**
|
||||
* Parent's rendered system prompt bytes, frozen at turn start.
|
||||
* Used by fork subagents to share the parent's prompt cache — re-calling
|
||||
* getSystemPrompt() at fork-spawn time can diverge (GrowthBook cold→warm)
|
||||
* and bust the cache. See forkSubagent.ts.
|
||||
*/
|
||||
renderedSystemPrompt?: SystemPrompt
|
||||
}
|
||||
|
||||
// Re-export ToolProgressData from centralized location
|
||||
export type { ToolProgressData }
|
||||
|
||||
export type Progress = ToolProgressData | HookProgress
|
||||
|
||||
export type ToolProgress<P extends ToolProgressData> = {
|
||||
toolUseID: string
|
||||
data: P
|
||||
}
|
||||
|
||||
export function filterToolProgressMessages(
|
||||
progressMessagesForMessage: ProgressMessage[],
|
||||
): ProgressMessage<ToolProgressData>[] {
|
||||
return progressMessagesForMessage.filter(
|
||||
(msg): msg is ProgressMessage<ToolProgressData> =>
|
||||
msg.data?.type !== 'hook_progress',
|
||||
)
|
||||
}
|
||||
|
||||
export type ToolResult<T> = {
|
||||
data: T
|
||||
newMessages?: (
|
||||
| UserMessage
|
||||
| AssistantMessage
|
||||
| AttachmentMessage
|
||||
| SystemMessage
|
||||
)[]
|
||||
// contextModifier is only honored for tools that aren't concurrency safe.
|
||||
contextModifier?: (context: ToolUseContext) => ToolUseContext
|
||||
/** MCP protocol metadata (structuredContent, _meta) to pass through to SDK consumers */
|
||||
mcpMeta?: {
|
||||
_meta?: Record<string, unknown>
|
||||
structuredContent?: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
export type ToolCallProgress<P extends ToolProgressData = ToolProgressData> = (
|
||||
progress: ToolProgress<P>,
|
||||
) => void
|
||||
|
||||
// Type for any schema that outputs an object with string keys
|
||||
export type AnyObject = z.ZodType<{ [key: string]: unknown }>
|
||||
|
||||
/**
|
||||
* Checks if a tool matches the given name (primary name or alias).
|
||||
*/
|
||||
export function toolMatchesName(
|
||||
tool: { name: string; aliases?: string[] },
|
||||
name: string,
|
||||
): boolean {
|
||||
return tool.name === name || (tool.aliases?.includes(name) ?? false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a tool by name or alias from a list of tools.
|
||||
*/
|
||||
export function findToolByName(tools: Tools, name: string): Tool | undefined {
|
||||
return tools.find(t => toolMatchesName(t, name))
|
||||
}
|
||||
|
||||
export type Tool<
|
||||
Input extends AnyObject = AnyObject,
|
||||
Output = unknown,
|
||||
P extends ToolProgressData = ToolProgressData,
|
||||
> = {
|
||||
/**
|
||||
* Optional aliases for backwards compatibility when a tool is renamed.
|
||||
* The tool can be looked up by any of these names in addition to its primary name.
|
||||
*/
|
||||
aliases?: string[]
|
||||
/**
|
||||
* One-line capability phrase used by ToolSearch for keyword matching.
|
||||
* Helps the model find this tool via keyword search when it's deferred.
|
||||
* 3–10 words, no trailing period.
|
||||
* Prefer terms not already in the tool name (e.g. 'jupyter' for NotebookEdit).
|
||||
*/
|
||||
searchHint?: string
|
||||
call(
|
||||
args: z.infer<Input>,
|
||||
context: ToolUseContext,
|
||||
canUseTool: CanUseToolFn,
|
||||
parentMessage: AssistantMessage,
|
||||
onProgress?: ToolCallProgress<P>,
|
||||
): Promise<ToolResult<Output>>
|
||||
description(
|
||||
input: z.infer<Input>,
|
||||
options: {
|
||||
isNonInteractiveSession: boolean
|
||||
toolPermissionContext: ToolPermissionContext
|
||||
tools: Tools
|
||||
},
|
||||
): Promise<string>
|
||||
readonly inputSchema: Input
|
||||
// Type for MCP tools that can specify their input schema directly in JSON Schema format
|
||||
// rather than converting from Zod schema
|
||||
readonly inputJSONSchema?: ToolInputJSONSchema
|
||||
// Optional because TungstenTool doesn't define this. TODO: Make it required.
|
||||
// When we do that, we can also go through and make this a bit more type-safe.
|
||||
outputSchema?: z.ZodType<unknown>
|
||||
inputsEquivalent?(a: z.infer<Input>, b: z.infer<Input>): boolean
|
||||
isConcurrencySafe(input: z.infer<Input>): boolean
|
||||
isEnabled(): boolean
|
||||
isReadOnly(input: z.infer<Input>): boolean
|
||||
/** Defaults to false. Only set when the tool performs irreversible operations (delete, overwrite, send). */
|
||||
isDestructive?(input: z.infer<Input>): boolean
|
||||
/**
|
||||
* What should happen when the user submits a new message while this tool
|
||||
* is running.
|
||||
*
|
||||
* - `'cancel'` — stop the tool and discard its result
|
||||
* - `'block'` — keep running; the new message waits
|
||||
*
|
||||
* Defaults to `'block'` when not implemented.
|
||||
*/
|
||||
interruptBehavior?(): 'cancel' | 'block'
|
||||
/**
|
||||
* Returns information about whether this tool use is a search or read operation
|
||||
* that should be collapsed into a condensed display in the UI. Examples include
|
||||
* file searching (Grep, Glob), file reading (Read), and bash commands like find,
|
||||
* grep, wc, etc.
|
||||
*
|
||||
* Returns an object indicating whether the operation is a search or read operation:
|
||||
* - `isSearch: true` for search operations (grep, find, glob patterns)
|
||||
* - `isRead: true` for read operations (cat, head, tail, file read)
|
||||
* - `isList: true` for directory-listing operations (ls, tree, du)
|
||||
* - All can be false if the operation shouldn't be collapsed
|
||||
*/
|
||||
isSearchOrReadCommand?(input: z.infer<Input>): {
|
||||
isSearch: boolean
|
||||
isRead: boolean
|
||||
isList?: boolean
|
||||
}
|
||||
isOpenWorld?(input: z.infer<Input>): boolean
|
||||
requiresUserInteraction?(): boolean
|
||||
isMcp?: boolean
|
||||
isLsp?: boolean
|
||||
/**
|
||||
* When true, this tool is deferred (sent with defer_loading: true) and requires
|
||||
* ToolSearch to be used before it can be called.
|
||||
*/
|
||||
readonly shouldDefer?: boolean
|
||||
/**
|
||||
* When true, this tool is never deferred — its full schema appears in the
|
||||
* initial prompt even when ToolSearch is enabled. For MCP tools, set via
|
||||
* `_meta['anthropic/alwaysLoad']`. Use for tools the model must see on
|
||||
* turn 1 without a ToolSearch round-trip.
|
||||
*/
|
||||
readonly alwaysLoad?: boolean
|
||||
/**
|
||||
* For MCP tools: the server and tool names as received from the MCP server (unnormalized).
|
||||
* Present on all MCP tools regardless of whether `name` is prefixed (mcp__server__tool)
|
||||
* or unprefixed (CLAUDE_AGENT_SDK_MCP_NO_PREFIX mode).
|
||||
*/
|
||||
mcpInfo?: { serverName: string; toolName: string }
|
||||
readonly name: string
|
||||
/**
|
||||
* Maximum size in characters for tool result before it gets persisted to disk.
|
||||
* When exceeded, the result is saved to a file and Claude receives a preview
|
||||
* with the file path instead of the full content.
|
||||
*
|
||||
* Set to Infinity for tools whose output must never be persisted (e.g. Read,
|
||||
* where persisting creates a circular Read→file→Read loop and the tool
|
||||
* already self-bounds via its own limits).
|
||||
*/
|
||||
maxResultSizeChars: number
|
||||
/**
|
||||
* When true, enables strict mode for this tool, which causes the API to
|
||||
* more strictly adhere to tool instructions and parameter schemas.
|
||||
* Only applied when the tengu_tool_pear is enabled.
|
||||
*/
|
||||
readonly strict?: boolean
|
||||
|
||||
/**
|
||||
* Called on copies of tool_use input before observers see it (SDK stream,
|
||||
* transcript, canUseTool, PreToolUse/PostToolUse hooks). Mutate in place
|
||||
* to add legacy/derived fields. Must be idempotent. The original API-bound
|
||||
* input is never mutated (preserves prompt cache). Not re-applied when a
|
||||
* hook/permission returns a fresh updatedInput — those own their shape.
|
||||
*/
|
||||
backfillObservableInput?(input: Record<string, unknown>): void
|
||||
|
||||
/**
|
||||
* Determines if this tool is allowed to run with this input in the current context.
|
||||
* It informs the model of why the tool use failed, and does not directly display any UI.
|
||||
* @param input
|
||||
* @param context
|
||||
*/
|
||||
validateInput?(
|
||||
input: z.infer<Input>,
|
||||
context: ToolUseContext,
|
||||
): Promise<ValidationResult>
|
||||
|
||||
/**
|
||||
* Determines if the user is asked for permission. Only called after validateInput() passes.
|
||||
* General permission logic is in permissions.ts. This method contains tool-specific logic.
|
||||
* @param input
|
||||
* @param context
|
||||
*/
|
||||
checkPermissions(
|
||||
input: z.infer<Input>,
|
||||
context: ToolUseContext,
|
||||
): Promise<PermissionResult>
|
||||
|
||||
// Optional method for tools that operate on a file path
|
||||
getPath?(input: z.infer<Input>): string
|
||||
|
||||
/**
|
||||
* Prepare a matcher for hook `if` conditions (permission-rule patterns like
|
||||
* "git *" from "Bash(git *)"). Called once per hook-input pair; any
|
||||
* expensive parsing happens here. Returns a closure that is called per
|
||||
* hook pattern. If not implemented, only tool-name-level matching works.
|
||||
*/
|
||||
preparePermissionMatcher?(
|
||||
input: z.infer<Input>,
|
||||
): Promise<(pattern: string) => boolean>
|
||||
|
||||
prompt(options: {
|
||||
getToolPermissionContext: () => Promise<ToolPermissionContext>
|
||||
tools: Tools
|
||||
agents: AgentDefinition[]
|
||||
allowedAgentTypes?: string[]
|
||||
}): Promise<string>
|
||||
userFacingName(input: Partial<z.infer<Input>> | undefined): string
|
||||
userFacingNameBackgroundColor?(
|
||||
input: Partial<z.infer<Input>> | undefined,
|
||||
): keyof Theme | undefined
|
||||
/**
|
||||
* Transparent wrappers (e.g. REPL) delegate all rendering to their progress
|
||||
* handler, which emits native-looking blocks for each inner tool call.
|
||||
* The wrapper itself shows nothing.
|
||||
*/
|
||||
isTransparentWrapper?(): boolean
|
||||
/**
|
||||
* Returns a short string summary of this tool use for display in compact views.
|
||||
* @param input The tool input
|
||||
* @returns A short string summary, or null to not display
|
||||
*/
|
||||
getToolUseSummary?(input: Partial<z.infer<Input>> | undefined): string | null
|
||||
/**
|
||||
* Returns a human-readable present-tense activity description for spinner display.
|
||||
* Example: "Reading src/foo.ts", "Running bun test", "Searching for pattern"
|
||||
* @param input The tool input
|
||||
* @returns Activity description string, or null to fall back to tool name
|
||||
*/
|
||||
getActivityDescription?(
|
||||
input: Partial<z.infer<Input>> | undefined,
|
||||
): string | null
|
||||
/**
|
||||
* Returns a compact representation of this tool use for the auto-mode
|
||||
* security classifier. Examples: `ls -la` for Bash, `/tmp/x: new content`
|
||||
* for Edit. Return '' to skip this tool in the classifier transcript
|
||||
* (e.g. tools with no security relevance). May return an object to avoid
|
||||
* double-encoding when the caller JSON-wraps the value.
|
||||
*/
|
||||
toAutoClassifierInput(input: z.infer<Input>): unknown
|
||||
mapToolResultToToolResultBlockParam(
|
||||
content: Output,
|
||||
toolUseID: string,
|
||||
): ToolResultBlockParam
|
||||
/**
|
||||
* Optional. When omitted, the tool result renders nothing (same as returning
|
||||
* null). Omit for tools whose results are surfaced elsewhere (e.g., TodoWrite
|
||||
* updates the todo panel, not the transcript).
|
||||
*/
|
||||
renderToolResultMessage?(
|
||||
content: Output,
|
||||
progressMessagesForMessage: ProgressMessage<P>[],
|
||||
options: {
|
||||
style?: 'condensed'
|
||||
theme: ThemeName
|
||||
tools: Tools
|
||||
verbose: boolean
|
||||
isTranscriptMode?: boolean
|
||||
isBriefOnly?: boolean
|
||||
/** Original tool_use input, when available. Useful for compact result
|
||||
* summaries that reference what was requested (e.g. "Sent to #foo"). */
|
||||
input?: unknown
|
||||
},
|
||||
): React.ReactNode
|
||||
/**
|
||||
* Flattened text of what renderToolResultMessage shows IN TRANSCRIPT
|
||||
* MODE (verbose=true, isTranscriptMode=true). For transcript search
|
||||
* indexing: the index counts occurrences in this string, the highlight
|
||||
* overlay scans the actual screen buffer. For count ≡ highlight, this
|
||||
* must return the text that ends up visible — not the model-facing
|
||||
* serialization from mapToolResultToToolResultBlockParam (which adds
|
||||
* system-reminders, persisted-output wrappers).
|
||||
*
|
||||
* Chrome can be skipped (under-count is fine). "Found 3 files in 12ms"
|
||||
* isn't worth indexing. Phantoms are not fine — text that's claimed
|
||||
* here but doesn't render is a count≠highlight bug.
|
||||
*
|
||||
* Optional: omitted → field-name heuristic in transcriptSearch.ts.
|
||||
* Drift caught by test/utils/transcriptSearch.renderFidelity.test.tsx
|
||||
* which renders sample outputs and flags text that's indexed-but-not-
|
||||
* rendered (phantom) or rendered-but-not-indexed (under-count warning).
|
||||
*/
|
||||
extractSearchText?(out: Output): string
|
||||
/**
|
||||
* Render the tool use message. Note that `input` is partial because we render
|
||||
* the message as soon as possible, possibly before tool parameters have fully
|
||||
* streamed in.
|
||||
*/
|
||||
renderToolUseMessage(
|
||||
input: Partial<z.infer<Input>>,
|
||||
options: { theme: ThemeName; verbose: boolean; commands?: Command[] },
|
||||
): React.ReactNode
|
||||
/**
|
||||
* Returns true when the non-verbose rendering of this output is truncated
|
||||
* (i.e., clicking to expand would reveal more content). Gates
|
||||
* click-to-expand in fullscreen — only messages where verbose actually
|
||||
* shows more get a hover/click affordance. Unset means never truncated.
|
||||
*/
|
||||
isResultTruncated?(output: Output): boolean
|
||||
/**
|
||||
* Renders an optional tag to display after the tool use message.
|
||||
* Used for additional metadata like timeout, model, resume ID, etc.
|
||||
* Returns null to not display anything.
|
||||
*/
|
||||
renderToolUseTag?(input: Partial<z.infer<Input>>): React.ReactNode
|
||||
/**
|
||||
* Optional. When omitted, no progress UI is shown while the tool runs.
|
||||
*/
|
||||
renderToolUseProgressMessage?(
|
||||
progressMessagesForMessage: ProgressMessage<P>[],
|
||||
options: {
|
||||
tools: Tools
|
||||
verbose: boolean
|
||||
terminalSize?: { columns: number; rows: number }
|
||||
inProgressToolCallCount?: number
|
||||
isTranscriptMode?: boolean
|
||||
},
|
||||
): React.ReactNode
|
||||
renderToolUseQueuedMessage?(): React.ReactNode
|
||||
/**
|
||||
* Optional. When omitted, falls back to <FallbackToolUseRejectedMessage />.
|
||||
* Only define this for tools that need custom rejection UI (e.g., file edits
|
||||
* that show the rejected diff).
|
||||
*/
|
||||
renderToolUseRejectedMessage?(
|
||||
input: z.infer<Input>,
|
||||
options: {
|
||||
columns: number
|
||||
messages: Message[]
|
||||
style?: 'condensed'
|
||||
theme: ThemeName
|
||||
tools: Tools
|
||||
verbose: boolean
|
||||
progressMessagesForMessage: ProgressMessage<P>[]
|
||||
isTranscriptMode?: boolean
|
||||
},
|
||||
): React.ReactNode
|
||||
/**
|
||||
* Optional. When omitted, falls back to <FallbackToolUseErrorMessage />.
|
||||
* Only define this for tools that need custom error UI (e.g., search tools
|
||||
* that show "File not found" instead of the raw error).
|
||||
*/
|
||||
renderToolUseErrorMessage?(
|
||||
result: ToolResultBlockParam['content'],
|
||||
options: {
|
||||
progressMessagesForMessage: ProgressMessage<P>[]
|
||||
tools: Tools
|
||||
verbose: boolean
|
||||
isTranscriptMode?: boolean
|
||||
},
|
||||
): React.ReactNode
|
||||
|
||||
/**
|
||||
* Renders multiple parallel instances of this tool as a group.
|
||||
* @returns React node to render, or null to fall back to individual rendering
|
||||
*/
|
||||
/**
|
||||
* Renders multiple tool uses as a group (non-verbose mode only).
|
||||
* In verbose mode, individual tool uses render at their original positions.
|
||||
* @returns React node to render, or null to fall back to individual rendering
|
||||
*/
|
||||
renderGroupedToolUse?(
|
||||
toolUses: Array<{
|
||||
param: ToolUseBlockParam
|
||||
isResolved: boolean
|
||||
isError: boolean
|
||||
isInProgress: boolean
|
||||
progressMessages: ProgressMessage<P>[]
|
||||
result?: {
|
||||
param: ToolResultBlockParam
|
||||
output: unknown
|
||||
}
|
||||
}>,
|
||||
options: {
|
||||
shouldAnimate: boolean
|
||||
tools: Tools
|
||||
},
|
||||
): React.ReactNode | null
|
||||
}
|
||||
|
||||
/**
|
||||
* A collection of tools. Use this type instead of `Tool[]` to make it easier
|
||||
* to track where tool sets are assembled, passed, and filtered across the codebase.
|
||||
*/
|
||||
export type Tools = readonly Tool[]
|
||||
|
||||
/**
|
||||
* Methods that `buildTool` supplies a default for. A `ToolDef` may omit these;
|
||||
* the resulting `Tool` always has them.
|
||||
*/
|
||||
type DefaultableToolKeys =
|
||||
| 'isEnabled'
|
||||
| 'isConcurrencySafe'
|
||||
| 'isReadOnly'
|
||||
| 'isDestructive'
|
||||
| 'checkPermissions'
|
||||
| 'toAutoClassifierInput'
|
||||
| 'userFacingName'
|
||||
|
||||
/**
|
||||
* Tool definition accepted by `buildTool`. Same shape as `Tool` but with the
|
||||
* defaultable methods optional — `buildTool` fills them in so callers always
|
||||
* see a complete `Tool`.
|
||||
*/
|
||||
export type ToolDef<
|
||||
Input extends AnyObject = AnyObject,
|
||||
Output = unknown,
|
||||
P extends ToolProgressData = ToolProgressData,
|
||||
> = Omit<Tool<Input, Output, P>, DefaultableToolKeys> &
|
||||
Partial<Pick<Tool<Input, Output, P>, DefaultableToolKeys>>
|
||||
|
||||
/**
|
||||
* Type-level spread mirroring `{ ...TOOL_DEFAULTS, ...def }`. For each
|
||||
* defaultable key: if D provides it (required), D's type wins; if D omits
|
||||
* it or has it optional (inherited from Partial<> in the constraint), the
|
||||
* default fills in. All other keys come from D verbatim — preserving arity,
|
||||
* optional presence, and literal types exactly as `satisfies Tool` did.
|
||||
*/
|
||||
type BuiltTool<D> = Omit<D, DefaultableToolKeys> & {
|
||||
[K in DefaultableToolKeys]-?: K extends keyof D
|
||||
? undefined extends D[K]
|
||||
? ToolDefaults[K]
|
||||
: D[K]
|
||||
: ToolDefaults[K]
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a complete `Tool` from a partial definition, filling in safe defaults
|
||||
* for the commonly-stubbed methods. All tool exports should go through this so
|
||||
* that defaults live in one place and callers never need `?.() ?? default`.
|
||||
*
|
||||
* Defaults (fail-closed where it matters):
|
||||
* - `isEnabled` → `true`
|
||||
* - `isConcurrencySafe` → `false` (assume not safe)
|
||||
* - `isReadOnly` → `false` (assume writes)
|
||||
* - `isDestructive` → `false`
|
||||
* - `checkPermissions` → `{ behavior: 'allow', updatedInput }` (defer to general permission system)
|
||||
* - `toAutoClassifierInput` → `''` (skip classifier — security-relevant tools must override)
|
||||
* - `userFacingName` → `name`
|
||||
*/
|
||||
const TOOL_DEFAULTS = {
|
||||
isEnabled: () => true,
|
||||
isConcurrencySafe: (_input?: unknown) => false,
|
||||
isReadOnly: (_input?: unknown) => false,
|
||||
isDestructive: (_input?: unknown) => false,
|
||||
checkPermissions: (
|
||||
input: { [key: string]: unknown },
|
||||
_ctx?: ToolUseContext,
|
||||
): Promise<PermissionResult> =>
|
||||
Promise.resolve({ behavior: 'allow', updatedInput: input }),
|
||||
toAutoClassifierInput: (_input?: unknown) => '',
|
||||
userFacingName: (_input?: unknown) => '',
|
||||
}
|
||||
|
||||
// The defaults type is the ACTUAL shape of TOOL_DEFAULTS (optional params so
|
||||
// both 0-arg and full-arg call sites type-check — stubs varied in arity and
|
||||
// tests relied on that), not the interface's strict signatures.
|
||||
type ToolDefaults = typeof TOOL_DEFAULTS
|
||||
|
||||
// D infers the concrete object-literal type from the call site. The
|
||||
// constraint provides contextual typing for method parameters; `any` in
|
||||
// constraint position is structural and never leaks into the return type.
|
||||
// BuiltTool<D> mirrors runtime `{...TOOL_DEFAULTS, ...def}` at the type level.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type AnyToolDef = ToolDef<any, any, any>
|
||||
|
||||
export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {
|
||||
// The runtime spread is straightforward; the `as` bridges the gap between
|
||||
// the structural-any constraint and the precise BuiltTool<D> return. The
|
||||
// type semantics are proven by the 0-error typecheck across all 60+ tools.
|
||||
return {
|
||||
...TOOL_DEFAULTS,
|
||||
userFacingName: () => def.name,
|
||||
...def,
|
||||
} as BuiltTool<D>
|
||||
}
|
||||
|
||||
|
||||
89
src/assistant/sessionHistory.ts
Normal file
89
src/assistant/sessionHistory.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import axios from 'axios'
|
||||
import { getOauthConfig } from '../constants/oauth.js'
|
||||
import type { SDKMessage } from '../entrypoints/agentSdkTypes.js'
|
||||
import { logForDebugging } from '../utils/debug.js'
|
||||
import { getOAuthHeaders, prepareApiRequest } from '../utils/teleport/api.js'
|
||||
|
||||
export const HISTORY_PAGE_SIZE = 100
|
||||
|
||||
export type HistoryPage = {
|
||||
/** Chronological order within the page. */
|
||||
events: SDKMessage[]
|
||||
/** Oldest event ID in this page → before_id cursor for next-older page. */
|
||||
firstId: string | null
|
||||
/** true = older events exist. */
|
||||
hasMore: boolean
|
||||
}
|
||||
|
||||
type SessionEventsResponse = {
|
||||
data: SDKMessage[]
|
||||
has_more: boolean
|
||||
first_id: string | null
|
||||
last_id: string | null
|
||||
}
|
||||
|
||||
export type HistoryAuthCtx = {
|
||||
baseUrl: string
|
||||
headers: Record<string, string>
|
||||
}
|
||||
|
||||
/** Prepare auth + headers + base URL once, reuse across pages. */
|
||||
export async function createHistoryAuthCtx(
|
||||
sessionId: string,
|
||||
): Promise<HistoryAuthCtx> {
|
||||
const { accessToken, orgUUID } = await prepareApiRequest()
|
||||
return {
|
||||
baseUrl: `${getOauthConfig().BASE_API_URL}/v1/sessions/${sessionId}/events`,
|
||||
headers: {
|
||||
...getOAuthHeaders(accessToken),
|
||||
'anthropic-beta': 'ccr-byoc-2025-07-29',
|
||||
'x-organization-uuid': orgUUID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchPage(
|
||||
ctx: HistoryAuthCtx,
|
||||
params: Record<string, string | number | boolean>,
|
||||
label: string,
|
||||
): Promise<HistoryPage | null> {
|
||||
const resp = await axios
|
||||
.get<SessionEventsResponse>(ctx.baseUrl, {
|
||||
headers: ctx.headers,
|
||||
params,
|
||||
timeout: 15000,
|
||||
validateStatus: () => true,
|
||||
})
|
||||
.catch(() => null)
|
||||
if (!resp || resp.status !== 200) {
|
||||
logForDebugging(`[${label}] HTTP ${resp?.status ?? 'error'}`)
|
||||
return null
|
||||
}
|
||||
return {
|
||||
events: Array.isArray(resp.data.data) ? resp.data.data : [],
|
||||
firstId: resp.data.first_id,
|
||||
hasMore: resp.data.has_more,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Newest page: last `limit` events, chronological, via anchor_to_latest.
|
||||
* has_more=true means older events exist.
|
||||
*/
|
||||
export async function fetchLatestEvents(
|
||||
ctx: HistoryAuthCtx,
|
||||
limit = HISTORY_PAGE_SIZE,
|
||||
): Promise<HistoryPage | null> {
|
||||
return fetchPage(ctx, { limit, anchor_to_latest: true }, 'fetchLatestEvents')
|
||||
}
|
||||
|
||||
/** Older page: events immediately before `beforeId` cursor. */
|
||||
export async function fetchOlderEvents(
|
||||
ctx: HistoryAuthCtx,
|
||||
beforeId: string,
|
||||
limit = HISTORY_PAGE_SIZE,
|
||||
): Promise<HistoryPage | null> {
|
||||
return fetchPage(ctx, { limit, before_id: beforeId }, 'fetchOlderEvents')
|
||||
}
|
||||
|
||||
|
||||
1760
src/bootstrap/state.ts
Normal file
1760
src/bootstrap/state.ts
Normal file
File diff suppressed because it is too large
Load diff
541
src/bridge/bridgeApi.ts
Normal file
541
src/bridge/bridgeApi.ts
Normal file
|
|
@ -0,0 +1,541 @@
|
|||
import axios from 'axios'
|
||||
|
||||
import { debugBody, extractErrorDetail } from './debugUtils.js'
|
||||
import {
|
||||
BRIDGE_LOGIN_INSTRUCTION,
|
||||
type BridgeApiClient,
|
||||
type BridgeConfig,
|
||||
type PermissionResponseEvent,
|
||||
type WorkResponse,
|
||||
} from './types.js'
|
||||
|
||||
type BridgeApiDeps = {
|
||||
baseUrl: string
|
||||
getAccessToken: () => string | undefined
|
||||
runnerVersion: string
|
||||
onDebug?: (msg: string) => void
|
||||
/**
|
||||
* Called on 401 to attempt OAuth token refresh. Returns true if refreshed,
|
||||
* in which case the request is retried once. Injected because
|
||||
* handleOAuth401Error from utils/auth.ts transitively pulls in config.ts →
|
||||
* file.ts → permissions/filesystem.ts → sessionStorage.ts → commands.ts
|
||||
* (~1300 modules). Daemon callers using env-var tokens omit this — their
|
||||
* tokens don't refresh, so 401 goes straight to BridgeFatalError.
|
||||
*/
|
||||
onAuth401?: (staleAccessToken: string) => Promise<boolean>
|
||||
/**
|
||||
* Returns the trusted device token to send as X-Trusted-Device-Token on
|
||||
* bridge API calls. Bridge sessions have SecurityTier=ELEVATED on the
|
||||
* server (CCR v2); when the server's enforcement flag is on,
|
||||
* ConnectBridgeWorker requires a trusted device at JWT-issuance.
|
||||
* Optional — when absent or returning undefined, the header is omitted
|
||||
* and the server falls through to its flag-off/no-op path. The CLI-side
|
||||
* gate is tengu_sessions_elevated_auth_enforcement (see trustedDevice.ts).
|
||||
*/
|
||||
getTrustedDeviceToken?: () => string | undefined
|
||||
}
|
||||
|
||||
const BETA_HEADER = 'environments-2025-11-01'
|
||||
|
||||
/** Allowlist pattern for server-provided IDs used in URL path segments. */
|
||||
const SAFE_ID_PATTERN = /^[a-zA-Z0-9_-]+$/
|
||||
|
||||
/**
|
||||
* Validate that a server-provided ID is safe to interpolate into a URL path.
|
||||
* Prevents path traversal (e.g. `../../admin`) and injection via IDs that
|
||||
* contain slashes, dots, or other special characters.
|
||||
*/
|
||||
export function validateBridgeId(id: string, label: string): string {
|
||||
if (!id || !SAFE_ID_PATTERN.test(id)) {
|
||||
throw new Error(`Invalid ${label}: contains unsafe characters`)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
/** Fatal bridge errors that should not be retried (e.g. auth failures). */
|
||||
export class BridgeFatalError extends Error {
|
||||
readonly status: number
|
||||
/** Server-provided error type, e.g. "environment_expired". */
|
||||
readonly errorType: string | undefined
|
||||
constructor(message: string, status: number, errorType?: string) {
|
||||
super(message)
|
||||
this.name = 'BridgeFatalError'
|
||||
this.status = status
|
||||
this.errorType = errorType
|
||||
}
|
||||
}
|
||||
|
||||
export function createBridgeApiClient(deps: BridgeApiDeps): BridgeApiClient {
|
||||
function debug(msg: string): void {
|
||||
deps.onDebug?.(msg)
|
||||
}
|
||||
|
||||
let consecutiveEmptyPolls = 0
|
||||
const EMPTY_POLL_LOG_INTERVAL = 100
|
||||
|
||||
function getHeaders(accessToken: string): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'anthropic-version': '2023-06-01',
|
||||
'anthropic-beta': BETA_HEADER,
|
||||
'x-environment-runner-version': deps.runnerVersion,
|
||||
}
|
||||
const deviceToken = deps.getTrustedDeviceToken?.()
|
||||
if (deviceToken) {
|
||||
headers['X-Trusted-Device-Token'] = deviceToken
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
function resolveAuth(): string {
|
||||
const accessToken = deps.getAccessToken()
|
||||
if (!accessToken) {
|
||||
throw new Error(BRIDGE_LOGIN_INSTRUCTION)
|
||||
}
|
||||
return accessToken
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an OAuth-authenticated request with a single retry on 401.
|
||||
* On 401, attempts token refresh via handleOAuth401Error (same pattern as
|
||||
* withRetry.ts for v1/messages). If refresh succeeds, retries the request
|
||||
* once with the new token. If refresh fails or the retry also returns 401,
|
||||
* the 401 response is returned for handleErrorStatus to throw BridgeFatalError.
|
||||
*/
|
||||
async function withOAuthRetry<T>(
|
||||
fn: (accessToken: string) => Promise<{ status: number; data: T }>,
|
||||
context: string,
|
||||
): Promise<{ status: number; data: T }> {
|
||||
const accessToken = resolveAuth()
|
||||
const response = await fn(accessToken)
|
||||
|
||||
if (response.status !== 401) {
|
||||
return response
|
||||
}
|
||||
|
||||
if (!deps.onAuth401) {
|
||||
debug(`[bridge:api] ${context}: 401 received, no refresh handler`)
|
||||
return response
|
||||
}
|
||||
|
||||
// Attempt token refresh — matches the pattern in withRetry.ts
|
||||
debug(`[bridge:api] ${context}: 401 received, attempting token refresh`)
|
||||
const refreshed = await deps.onAuth401(accessToken)
|
||||
if (refreshed) {
|
||||
debug(`[bridge:api] ${context}: Token refreshed, retrying request`)
|
||||
const newToken = resolveAuth()
|
||||
const retryResponse = await fn(newToken)
|
||||
if (retryResponse.status !== 401) {
|
||||
return retryResponse
|
||||
}
|
||||
debug(`[bridge:api] ${context}: Retry after refresh also got 401`)
|
||||
} else {
|
||||
debug(`[bridge:api] ${context}: Token refresh failed`)
|
||||
}
|
||||
|
||||
// Refresh failed — return 401 for handleErrorStatus to throw
|
||||
return response
|
||||
}
|
||||
|
||||
return {
|
||||
async registerBridgeEnvironment(
|
||||
config: BridgeConfig,
|
||||
): Promise<{ environment_id: string; environment_secret: string }> {
|
||||
debug(
|
||||
`[bridge:api] POST /v1/environments/bridge bridgeId=${config.bridgeId}`,
|
||||
)
|
||||
|
||||
const response = await withOAuthRetry(
|
||||
(token: string) =>
|
||||
axios.post<{
|
||||
environment_id: string
|
||||
environment_secret: string
|
||||
}>(
|
||||
`${deps.baseUrl}/v1/environments/bridge`,
|
||||
{
|
||||
machine_name: config.machineName,
|
||||
directory: config.dir,
|
||||
branch: config.branch,
|
||||
git_repo_url: config.gitRepoUrl,
|
||||
// Advertise session capacity so claude.ai/code can show
|
||||
// "2/4 sessions" badges and only block the picker when
|
||||
// actually at capacity. Backends that don't yet accept
|
||||
// this field will silently ignore it.
|
||||
max_sessions: config.maxSessions,
|
||||
// worker_type lets claude.ai filter environments by origin
|
||||
// (e.g. assistant picker only shows assistant-mode workers).
|
||||
// Desktop cowork app sends "cowork"; we send a distinct value.
|
||||
metadata: { worker_type: config.workerType },
|
||||
// Idempotent re-registration: if we have a backend-issued
|
||||
// environment_id from a prior session (--session-id resume),
|
||||
// send it back so the backend reattaches instead of creating
|
||||
// a new env. The backend may still hand back a fresh ID if
|
||||
// the old one expired — callers must compare the response.
|
||||
...(config.reuseEnvironmentId && {
|
||||
environment_id: config.reuseEnvironmentId,
|
||||
}),
|
||||
},
|
||||
{
|
||||
headers: getHeaders(token),
|
||||
timeout: 15_000,
|
||||
validateStatus: status => status < 500,
|
||||
},
|
||||
),
|
||||
'Registration',
|
||||
)
|
||||
|
||||
handleErrorStatus(response.status, response.data, 'Registration')
|
||||
debug(
|
||||
`[bridge:api] POST /v1/environments/bridge -> ${response.status} environment_id=${response.data.environment_id}`,
|
||||
)
|
||||
debug(
|
||||
`[bridge:api] >>> ${debugBody({ machine_name: config.machineName, directory: config.dir, branch: config.branch, git_repo_url: config.gitRepoUrl, max_sessions: config.maxSessions, metadata: { worker_type: config.workerType } })}`,
|
||||
)
|
||||
debug(`[bridge:api] <<< ${debugBody(response.data)}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async pollForWork(
|
||||
environmentId: string,
|
||||
environmentSecret: string,
|
||||
signal?: AbortSignal,
|
||||
reclaimOlderThanMs?: number,
|
||||
): Promise<WorkResponse | null> {
|
||||
validateBridgeId(environmentId, 'environmentId')
|
||||
|
||||
// Save and reset so errors break the "consecutive empty" streak.
|
||||
// Restored below when the response is truly empty.
|
||||
const prevEmptyPolls = consecutiveEmptyPolls
|
||||
consecutiveEmptyPolls = 0
|
||||
|
||||
const response = await axios.get<WorkResponse | null>(
|
||||
`${deps.baseUrl}/v1/environments/${environmentId}/work/poll`,
|
||||
{
|
||||
headers: getHeaders(environmentSecret),
|
||||
params:
|
||||
reclaimOlderThanMs !== undefined
|
||||
? { reclaim_older_than_ms: reclaimOlderThanMs }
|
||||
: undefined,
|
||||
timeout: 10_000,
|
||||
signal,
|
||||
validateStatus: status => status < 500,
|
||||
},
|
||||
)
|
||||
|
||||
handleErrorStatus(response.status, response.data, 'Poll')
|
||||
|
||||
// Empty body or null = no work available
|
||||
if (!response.data) {
|
||||
consecutiveEmptyPolls = prevEmptyPolls + 1
|
||||
if (
|
||||
consecutiveEmptyPolls === 1 ||
|
||||
consecutiveEmptyPolls % EMPTY_POLL_LOG_INTERVAL === 0
|
||||
) {
|
||||
debug(
|
||||
`[bridge:api] GET .../work/poll -> ${response.status} (no work, ${consecutiveEmptyPolls} consecutive empty polls)`,
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
debug(
|
||||
`[bridge:api] GET .../work/poll -> ${response.status} workId=${response.data.id} type=${response.data.data?.type}${response.data.data?.id ? ` sessionId=${response.data.data.id}` : ''}`,
|
||||
)
|
||||
debug(`[bridge:api] <<< ${debugBody(response.data)}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async acknowledgeWork(
|
||||
environmentId: string,
|
||||
workId: string,
|
||||
sessionToken: string,
|
||||
): Promise<void> {
|
||||
validateBridgeId(environmentId, 'environmentId')
|
||||
validateBridgeId(workId, 'workId')
|
||||
|
||||
debug(`[bridge:api] POST .../work/${workId}/ack`)
|
||||
|
||||
const response = await axios.post(
|
||||
`${deps.baseUrl}/v1/environments/${environmentId}/work/${workId}/ack`,
|
||||
{},
|
||||
{
|
||||
headers: getHeaders(sessionToken),
|
||||
timeout: 10_000,
|
||||
validateStatus: s => s < 500,
|
||||
},
|
||||
)
|
||||
|
||||
handleErrorStatus(response.status, response.data, 'Acknowledge')
|
||||
debug(`[bridge:api] POST .../work/${workId}/ack -> ${response.status}`)
|
||||
},
|
||||
|
||||
async stopWork(
|
||||
environmentId: string,
|
||||
workId: string,
|
||||
force: boolean,
|
||||
): Promise<void> {
|
||||
validateBridgeId(environmentId, 'environmentId')
|
||||
validateBridgeId(workId, 'workId')
|
||||
|
||||
debug(`[bridge:api] POST .../work/${workId}/stop force=${force}`)
|
||||
|
||||
const response = await withOAuthRetry(
|
||||
(token: string) =>
|
||||
axios.post(
|
||||
`${deps.baseUrl}/v1/environments/${environmentId}/work/${workId}/stop`,
|
||||
{ force },
|
||||
{
|
||||
headers: getHeaders(token),
|
||||
timeout: 10_000,
|
||||
validateStatus: s => s < 500,
|
||||
},
|
||||
),
|
||||
'StopWork',
|
||||
)
|
||||
|
||||
handleErrorStatus(response.status, response.data, 'StopWork')
|
||||
debug(`[bridge:api] POST .../work/${workId}/stop -> ${response.status}`)
|
||||
},
|
||||
|
||||
async deregisterEnvironment(environmentId: string): Promise<void> {
|
||||
validateBridgeId(environmentId, 'environmentId')
|
||||
|
||||
debug(`[bridge:api] DELETE /v1/environments/bridge/${environmentId}`)
|
||||
|
||||
const response = await withOAuthRetry(
|
||||
(token: string) =>
|
||||
axios.delete(
|
||||
`${deps.baseUrl}/v1/environments/bridge/${environmentId}`,
|
||||
{
|
||||
headers: getHeaders(token),
|
||||
timeout: 10_000,
|
||||
validateStatus: s => s < 500,
|
||||
},
|
||||
),
|
||||
'Deregister',
|
||||
)
|
||||
|
||||
handleErrorStatus(response.status, response.data, 'Deregister')
|
||||
debug(
|
||||
`[bridge:api] DELETE /v1/environments/bridge/${environmentId} -> ${response.status}`,
|
||||
)
|
||||
},
|
||||
|
||||
async archiveSession(sessionId: string): Promise<void> {
|
||||
validateBridgeId(sessionId, 'sessionId')
|
||||
|
||||
debug(`[bridge:api] POST /v1/sessions/${sessionId}/archive`)
|
||||
|
||||
const response = await withOAuthRetry(
|
||||
(token: string) =>
|
||||
axios.post(
|
||||
`${deps.baseUrl}/v1/sessions/${sessionId}/archive`,
|
||||
{},
|
||||
{
|
||||
headers: getHeaders(token),
|
||||
timeout: 10_000,
|
||||
validateStatus: s => s < 500,
|
||||
},
|
||||
),
|
||||
'ArchiveSession',
|
||||
)
|
||||
|
||||
// 409 = already archived (idempotent, not an error)
|
||||
if (response.status === 409) {
|
||||
debug(
|
||||
`[bridge:api] POST /v1/sessions/${sessionId}/archive -> 409 (already archived)`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
handleErrorStatus(response.status, response.data, 'ArchiveSession')
|
||||
debug(
|
||||
`[bridge:api] POST /v1/sessions/${sessionId}/archive -> ${response.status}`,
|
||||
)
|
||||
},
|
||||
|
||||
async reconnectSession(
|
||||
environmentId: string,
|
||||
sessionId: string,
|
||||
): Promise<void> {
|
||||
validateBridgeId(environmentId, 'environmentId')
|
||||
validateBridgeId(sessionId, 'sessionId')
|
||||
|
||||
debug(
|
||||
`[bridge:api] POST /v1/environments/${environmentId}/bridge/reconnect session_id=${sessionId}`,
|
||||
)
|
||||
|
||||
const response = await withOAuthRetry(
|
||||
(token: string) =>
|
||||
axios.post(
|
||||
`${deps.baseUrl}/v1/environments/${environmentId}/bridge/reconnect`,
|
||||
{ session_id: sessionId },
|
||||
{
|
||||
headers: getHeaders(token),
|
||||
timeout: 10_000,
|
||||
validateStatus: s => s < 500,
|
||||
},
|
||||
),
|
||||
'ReconnectSession',
|
||||
)
|
||||
|
||||
handleErrorStatus(response.status, response.data, 'ReconnectSession')
|
||||
debug(`[bridge:api] POST .../bridge/reconnect -> ${response.status}`)
|
||||
},
|
||||
|
||||
async heartbeatWork(
|
||||
environmentId: string,
|
||||
workId: string,
|
||||
sessionToken: string,
|
||||
): Promise<{ lease_extended: boolean; state: string }> {
|
||||
validateBridgeId(environmentId, 'environmentId')
|
||||
validateBridgeId(workId, 'workId')
|
||||
|
||||
debug(`[bridge:api] POST .../work/${workId}/heartbeat`)
|
||||
|
||||
const response = await axios.post<{
|
||||
lease_extended: boolean
|
||||
state: string
|
||||
last_heartbeat: string
|
||||
ttl_seconds: number
|
||||
}>(
|
||||
`${deps.baseUrl}/v1/environments/${environmentId}/work/${workId}/heartbeat`,
|
||||
{},
|
||||
{
|
||||
headers: getHeaders(sessionToken),
|
||||
timeout: 10_000,
|
||||
validateStatus: s => s < 500,
|
||||
},
|
||||
)
|
||||
|
||||
handleErrorStatus(response.status, response.data, 'Heartbeat')
|
||||
debug(
|
||||
`[bridge:api] POST .../work/${workId}/heartbeat -> ${response.status} lease_extended=${response.data.lease_extended} state=${response.data.state}`,
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async sendPermissionResponseEvent(
|
||||
sessionId: string,
|
||||
event: PermissionResponseEvent,
|
||||
sessionToken: string,
|
||||
): Promise<void> {
|
||||
validateBridgeId(sessionId, 'sessionId')
|
||||
|
||||
debug(
|
||||
`[bridge:api] POST /v1/sessions/${sessionId}/events type=${event.type}`,
|
||||
)
|
||||
|
||||
const response = await axios.post(
|
||||
`${deps.baseUrl}/v1/sessions/${sessionId}/events`,
|
||||
{ events: [event] },
|
||||
{
|
||||
headers: getHeaders(sessionToken),
|
||||
timeout: 10_000,
|
||||
validateStatus: s => s < 500,
|
||||
},
|
||||
)
|
||||
|
||||
handleErrorStatus(
|
||||
response.status,
|
||||
response.data,
|
||||
'SendPermissionResponseEvent',
|
||||
)
|
||||
debug(
|
||||
`[bridge:api] POST /v1/sessions/${sessionId}/events -> ${response.status}`,
|
||||
)
|
||||
debug(`[bridge:api] >>> ${debugBody({ events: [event] })}`)
|
||||
debug(`[bridge:api] <<< ${debugBody(response.data)}`)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function handleErrorStatus(
|
||||
status: number,
|
||||
data: unknown,
|
||||
context: string,
|
||||
): void {
|
||||
if (status === 200 || status === 204) {
|
||||
return
|
||||
}
|
||||
const detail = extractErrorDetail(data)
|
||||
const errorType = extractErrorTypeFromData(data)
|
||||
switch (status) {
|
||||
case 401:
|
||||
throw new BridgeFatalError(
|
||||
`${context}: Authentication failed (401)${detail ? `: ${detail}` : ''}. ${BRIDGE_LOGIN_INSTRUCTION}`,
|
||||
401,
|
||||
errorType,
|
||||
)
|
||||
case 403:
|
||||
throw new BridgeFatalError(
|
||||
isExpiredErrorType(errorType)
|
||||
? 'Remote Control session has expired. Please restart with `claude remote-control` or /remote-control.'
|
||||
: `${context}: Access denied (403)${detail ? `: ${detail}` : ''}. Check your organization permissions.`,
|
||||
403,
|
||||
errorType,
|
||||
)
|
||||
case 404:
|
||||
throw new BridgeFatalError(
|
||||
detail ??
|
||||
`${context}: Not found (404). Remote Control may not be available for this organization.`,
|
||||
404,
|
||||
errorType,
|
||||
)
|
||||
case 410:
|
||||
throw new BridgeFatalError(
|
||||
detail ??
|
||||
'Remote Control session has expired. Please restart with `claude remote-control` or /remote-control.',
|
||||
410,
|
||||
errorType ?? 'environment_expired',
|
||||
)
|
||||
case 429:
|
||||
throw new Error(`${context}: Rate limited (429). Polling too frequently.`)
|
||||
default:
|
||||
throw new Error(
|
||||
`${context}: Failed with status ${status}${detail ? `: ${detail}` : ''}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Check whether an error type string indicates a session/environment expiry. */
|
||||
export function isExpiredErrorType(errorType: string | undefined): boolean {
|
||||
if (!errorType) {
|
||||
return false
|
||||
}
|
||||
return errorType.includes('expired') || errorType.includes('lifetime')
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a BridgeFatalError is a suppressible 403 permission error.
|
||||
* These are 403 errors for scopes like 'external_poll_sessions' or operations
|
||||
* like StopWork that fail because the user's role lacks 'environments:manage'.
|
||||
* They don't affect core functionality and shouldn't be shown to users.
|
||||
*/
|
||||
export function isSuppressible403(err: BridgeFatalError): boolean {
|
||||
if (err.status !== 403) {
|
||||
return false
|
||||
}
|
||||
return (
|
||||
err.message.includes('external_poll_sessions') ||
|
||||
err.message.includes('environments:manage')
|
||||
)
|
||||
}
|
||||
|
||||
function extractErrorTypeFromData(data: unknown): string | undefined {
|
||||
if (data && typeof data === 'object') {
|
||||
if (
|
||||
'error' in data &&
|
||||
data.error &&
|
||||
typeof data.error === 'object' &&
|
||||
'type' in data.error &&
|
||||
typeof data.error.type === 'string'
|
||||
) {
|
||||
return data.error.type
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
||||
50
src/bridge/bridgeConfig.ts
Normal file
50
src/bridge/bridgeConfig.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* Shared bridge auth/URL resolution. Consolidates the ant-only
|
||||
* CLAUDE_BRIDGE_* dev overrides that were previously copy-pasted across
|
||||
* a dozen files — inboundAttachments, BriefTool/upload, bridgeMain,
|
||||
* initReplBridge, remoteBridgeCore, daemon workers, /rename,
|
||||
* /remote-control.
|
||||
*
|
||||
* Two layers: *Override() returns the ant-only env var (or undefined);
|
||||
* the non-Override versions fall through to the real OAuth store/config.
|
||||
* Callers that compose with a different auth source (e.g. daemon workers
|
||||
* using IPC auth) use the Override getters directly.
|
||||
*/
|
||||
|
||||
import { getOauthConfig } from '../constants/oauth.js'
|
||||
import { getClaudeAIOAuthTokens } from '../utils/auth.js'
|
||||
|
||||
/** Ant-only dev override: CLAUDE_BRIDGE_OAUTH_TOKEN, else undefined. */
|
||||
export function getBridgeTokenOverride(): string | undefined {
|
||||
return (
|
||||
(process.env.USER_TYPE === 'ant' &&
|
||||
process.env.CLAUDE_BRIDGE_OAUTH_TOKEN) ||
|
||||
undefined
|
||||
)
|
||||
}
|
||||
|
||||
/** Ant-only dev override: CLAUDE_BRIDGE_BASE_URL, else undefined. */
|
||||
export function getBridgeBaseUrlOverride(): string | undefined {
|
||||
return (
|
||||
(process.env.USER_TYPE === 'ant' && process.env.CLAUDE_BRIDGE_BASE_URL) ||
|
||||
undefined
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Access token for bridge API calls: dev override first, then the OAuth
|
||||
* keychain. Undefined means "not logged in".
|
||||
*/
|
||||
export function getBridgeAccessToken(): string | undefined {
|
||||
return getBridgeTokenOverride() ?? getClaudeAIOAuthTokens()?.accessToken
|
||||
}
|
||||
|
||||
/**
|
||||
* Base URL for bridge API calls: dev override first, then the production
|
||||
* OAuth config. Always returns a URL.
|
||||
*/
|
||||
export function getBridgeBaseUrl(): string {
|
||||
return getBridgeBaseUrlOverride() ?? getOauthConfig().BASE_API_URL
|
||||
}
|
||||
|
||||
|
||||
137
src/bridge/bridgeDebug.ts
Normal file
137
src/bridge/bridgeDebug.ts
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
import { logForDebugging } from '../utils/debug.js'
|
||||
import { BridgeFatalError } from './bridgeApi.js'
|
||||
import type { BridgeApiClient } from './types.js'
|
||||
|
||||
/**
|
||||
* Ant-only fault injection for manually testing bridge recovery paths.
|
||||
*
|
||||
* Real failure modes this targets (BQ 2026-03-12, 7-day window):
|
||||
* poll 404 not_found_error — 147K sessions/week, dead onEnvironmentLost gate
|
||||
* ws_closed 1002/1006 — 22K sessions/week, zombie poll after close
|
||||
* register transient failure — residual: network blips during doReconnect
|
||||
*
|
||||
* Usage: /bridge-kick <subcommand> from the REPL while Remote Control is
|
||||
* connected, then tail debug.log to watch the recovery machinery react.
|
||||
*
|
||||
* Module-level state is intentional here: one bridge per REPL process, the
|
||||
* /bridge-kick slash command has no other way to reach into initBridgeCore's
|
||||
* closures, and teardown clears the slot.
|
||||
*/
|
||||
|
||||
/** One-shot fault to inject on the next matching api call. */
|
||||
type BridgeFault = {
|
||||
method:
|
||||
| 'pollForWork'
|
||||
| 'registerBridgeEnvironment'
|
||||
| 'reconnectSession'
|
||||
| 'heartbeatWork'
|
||||
/** Fatal errors go through handleErrorStatus → BridgeFatalError. Transient
|
||||
* errors surface as plain axios rejections (5xx / network). Recovery code
|
||||
* distinguishes the two: fatal → teardown, transient → retry/backoff. */
|
||||
kind: 'fatal' | 'transient'
|
||||
status: number
|
||||
errorType?: string
|
||||
/** Remaining injections. Decremented on consume; removed at 0. */
|
||||
count: number
|
||||
}
|
||||
|
||||
export type BridgeDebugHandle = {
|
||||
/** Invoke the transport's permanent-close handler directly. Tests the
|
||||
* ws_closed → reconnectEnvironmentWithSession escalation (#22148). */
|
||||
fireClose: (code: number) => void
|
||||
/** Call reconnectEnvironmentWithSession() — same as SIGUSR2 but
|
||||
* reachable from the slash command. */
|
||||
forceReconnect: () => void
|
||||
/** Queue a fault for the next N calls to the named api method. */
|
||||
injectFault: (fault: BridgeFault) => void
|
||||
/** Abort the at-capacity sleep so an injected poll fault lands
|
||||
* immediately instead of up to 10min later. */
|
||||
wakePollLoop: () => void
|
||||
/** env/session IDs for the debug.log grep. */
|
||||
describe: () => string
|
||||
}
|
||||
|
||||
let debugHandle: BridgeDebugHandle | null = null
|
||||
const faultQueue: BridgeFault[] = []
|
||||
|
||||
export function registerBridgeDebugHandle(h: BridgeDebugHandle): void {
|
||||
debugHandle = h
|
||||
}
|
||||
|
||||
export function clearBridgeDebugHandle(): void {
|
||||
debugHandle = null
|
||||
faultQueue.length = 0
|
||||
}
|
||||
|
||||
export function getBridgeDebugHandle(): BridgeDebugHandle | null {
|
||||
return debugHandle
|
||||
}
|
||||
|
||||
export function injectBridgeFault(fault: BridgeFault): void {
|
||||
faultQueue.push(fault)
|
||||
logForDebugging(
|
||||
`[bridge:debug] Queued fault: ${fault.method} ${fault.kind}/${fault.status}${fault.errorType ? `/${fault.errorType}` : ''} ×${fault.count}`,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a BridgeApiClient so each call first checks the fault queue. If a
|
||||
* matching fault is queued, throw the specified error instead of calling
|
||||
* through. Delegates everything else to the real client.
|
||||
*
|
||||
* Only called when USER_TYPE === 'ant' — zero overhead in external builds.
|
||||
*/
|
||||
export function wrapApiForFaultInjection(
|
||||
api: BridgeApiClient,
|
||||
): BridgeApiClient {
|
||||
function consume(method: BridgeFault['method']): BridgeFault | null {
|
||||
const idx = faultQueue.findIndex(f => f.method === method)
|
||||
if (idx === -1) return null
|
||||
const fault = faultQueue[idx]!
|
||||
fault.count--
|
||||
if (fault.count <= 0) faultQueue.splice(idx, 1)
|
||||
return fault
|
||||
}
|
||||
|
||||
function throwFault(fault: BridgeFault, context: string): never {
|
||||
logForDebugging(
|
||||
`[bridge:debug] Injecting ${fault.kind} fault into ${context}: status=${fault.status} errorType=${fault.errorType ?? 'none'}`,
|
||||
)
|
||||
if (fault.kind === 'fatal') {
|
||||
throw new BridgeFatalError(
|
||||
`[injected] ${context} ${fault.status}`,
|
||||
fault.status,
|
||||
fault.errorType,
|
||||
)
|
||||
}
|
||||
// Transient: mimic an axios rejection (5xx / network). No .status on
|
||||
// the error itself — that's how the catch blocks distinguish.
|
||||
throw new Error(`[injected transient] ${context} ${fault.status}`)
|
||||
}
|
||||
|
||||
return {
|
||||
...api,
|
||||
async pollForWork(envId, secret, signal, reclaimMs) {
|
||||
const f = consume('pollForWork')
|
||||
if (f) throwFault(f, 'Poll')
|
||||
return api.pollForWork(envId, secret, signal, reclaimMs)
|
||||
},
|
||||
async registerBridgeEnvironment(config) {
|
||||
const f = consume('registerBridgeEnvironment')
|
||||
if (f) throwFault(f, 'Registration')
|
||||
return api.registerBridgeEnvironment(config)
|
||||
},
|
||||
async reconnectSession(envId, sessionId) {
|
||||
const f = consume('reconnectSession')
|
||||
if (f) throwFault(f, 'ReconnectSession')
|
||||
return api.reconnectSession(envId, sessionId)
|
||||
},
|
||||
async heartbeatWork(envId, workId, token) {
|
||||
const f = consume('heartbeatWork')
|
||||
if (f) throwFault(f, 'Heartbeat')
|
||||
return api.heartbeatWork(envId, workId, token)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
204
src/bridge/bridgeEnabled.ts
Normal file
204
src/bridge/bridgeEnabled.ts
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
import { feature } from 'bun:bundle'
|
||||
import {
|
||||
checkGate_CACHED_OR_BLOCKING,
|
||||
getDynamicConfig_CACHED_MAY_BE_STALE,
|
||||
getFeatureValue_CACHED_MAY_BE_STALE,
|
||||
} from '../services/analytics/growthbook.js'
|
||||
// Namespace import breaks the bridgeEnabled → auth → config → bridgeEnabled
|
||||
// cycle — authModule.foo is a live binding, so by the time the helpers below
|
||||
// call it, auth.js is fully loaded. Previously used require() for the same
|
||||
// deferral, but require() hits a CJS cache that diverges from the ESM
|
||||
// namespace after mock.module() (daemon/auth.test.ts), breaking spyOn.
|
||||
import * as authModule from '../utils/auth.js'
|
||||
import { isEnvTruthy } from '../utils/envUtils.js'
|
||||
import { lt } from '../utils/semver.js'
|
||||
|
||||
/**
|
||||
* Runtime check for bridge mode entitlement.
|
||||
*
|
||||
* Remote Control requires a claude.ai subscription (the bridge auths to CCR
|
||||
* with the claude.ai OAuth token). isClaudeAISubscriber() excludes
|
||||
* Bedrock/Vertex/Foundry, apiKeyHelper/gateway deployments, env-var API keys,
|
||||
* and Console API logins — none of which have the OAuth token CCR needs.
|
||||
* See github.com/deshaw/anthropic-issues/issues/24.
|
||||
*
|
||||
* The `feature('BRIDGE_MODE')` guard ensures the GrowthBook string literal
|
||||
* is only referenced when bridge mode is enabled at build time.
|
||||
*/
|
||||
export function isBridgeEnabled(): boolean {
|
||||
// Positive ternary pattern — see docs/feature-gating.md.
|
||||
// Negative pattern (if (!feature(...)) return) does not eliminate
|
||||
// inline string literals from external builds.
|
||||
return feature('BRIDGE_MODE')
|
||||
? isClaudeAISubscriber() &&
|
||||
getFeatureValue_CACHED_MAY_BE_STALE('tengu_ccr_bridge', false)
|
||||
: false
|
||||
}
|
||||
|
||||
/**
|
||||
* Blocking entitlement check for Remote Control.
|
||||
*
|
||||
* Returns cached `true` immediately (fast path). If the disk cache says
|
||||
* `false` or is missing, awaits GrowthBook init and fetches the fresh
|
||||
* server value (slow path, max ~5s), then writes it to disk.
|
||||
*
|
||||
* Use at entitlement gates where a stale `false` would unfairly block access.
|
||||
* For user-facing error paths, prefer `getBridgeDisabledReason()` which gives
|
||||
* a specific diagnostic. For render-body UI visibility checks, use
|
||||
* `isBridgeEnabled()` instead.
|
||||
*/
|
||||
export async function isBridgeEnabledBlocking(): Promise<boolean> {
|
||||
return feature('BRIDGE_MODE')
|
||||
? isClaudeAISubscriber() &&
|
||||
(await checkGate_CACHED_OR_BLOCKING('tengu_ccr_bridge'))
|
||||
: false
|
||||
}
|
||||
|
||||
/**
|
||||
* Diagnostic message for why Remote Control is unavailable, or null if
|
||||
* it's enabled. Call this instead of a bare `isBridgeEnabledBlocking()`
|
||||
* check when you need to show the user an actionable error.
|
||||
*
|
||||
* The GrowthBook gate targets on organizationUUID, which comes from
|
||||
* config.oauthAccount — populated by /api/oauth/profile during login.
|
||||
* That endpoint requires the user:profile scope. Tokens without it
|
||||
* (setup-token, CLAUDE_CODE_OAUTH_TOKEN env var, or pre-scope-expansion
|
||||
* logins) leave oauthAccount unpopulated, so the gate falls back to
|
||||
* false and users see a dead-end "not enabled" message with no hint
|
||||
* that re-login would fix it. See CC-1165 / gh-33105.
|
||||
*/
|
||||
export async function getBridgeDisabledReason(): Promise<string | null> {
|
||||
if (feature('BRIDGE_MODE')) {
|
||||
if (!isClaudeAISubscriber()) {
|
||||
return 'Remote Control requires a claude.ai subscription. Run `claude auth login` to sign in with your claude.ai account.'
|
||||
}
|
||||
if (!hasProfileScope()) {
|
||||
return 'Remote Control requires a full-scope login token. Long-lived tokens (from `claude setup-token` or CLAUDE_CODE_OAUTH_TOKEN) are limited to inference-only for security reasons. Run `claude auth login` to use Remote Control.'
|
||||
}
|
||||
if (!getOauthAccountInfo()?.organizationUuid) {
|
||||
return 'Unable to determine your organization for Remote Control eligibility. Run `claude auth login` to refresh your account information.'
|
||||
}
|
||||
if (!(await checkGate_CACHED_OR_BLOCKING('tengu_ccr_bridge'))) {
|
||||
return 'Remote Control is not yet enabled for your account.'
|
||||
}
|
||||
return null
|
||||
}
|
||||
return 'Remote Control is not available in this build.'
|
||||
}
|
||||
|
||||
// try/catch: main.tsx:5698 calls isBridgeEnabled() while defining the Commander
|
||||
// program, before enableConfigs() runs. isClaudeAISubscriber() → getGlobalConfig()
|
||||
// throws "Config accessed before allowed" there. Pre-config, no OAuth token can
|
||||
// exist anyway — false is correct. Same swallow getFeatureValue_CACHED_MAY_BE_STALE
|
||||
// already does at growthbook.ts:775-780.
|
||||
function isClaudeAISubscriber(): boolean {
|
||||
try {
|
||||
return authModule.isClaudeAISubscriber()
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
function hasProfileScope(): boolean {
|
||||
try {
|
||||
return authModule.hasProfileScope()
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
function getOauthAccountInfo(): ReturnType<
|
||||
typeof authModule.getOauthAccountInfo
|
||||
> {
|
||||
try {
|
||||
return authModule.getOauthAccountInfo()
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime check for the env-less (v2) REPL bridge path.
|
||||
* Returns true when the GrowthBook flag `tengu_bridge_repl_v2` is enabled.
|
||||
*
|
||||
* This gates which implementation initReplBridge uses — NOT whether bridge
|
||||
* is available at all (see isBridgeEnabled above). Daemon/print paths stay
|
||||
* on the env-based implementation regardless of this gate.
|
||||
*/
|
||||
export function isEnvLessBridgeEnabled(): boolean {
|
||||
return feature('BRIDGE_MODE')
|
||||
? getFeatureValue_CACHED_MAY_BE_STALE('tengu_bridge_repl_v2', false)
|
||||
: false
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill-switch for the `cse_*` → `session_*` client-side retag shim.
|
||||
*
|
||||
* The shim exists because compat/convert.go:27 validates TagSession and the
|
||||
* claude.ai frontend routes on `session_*`, while v2 worker endpoints hand out
|
||||
* `cse_*`. Once the server tags by environment_kind and the frontend accepts
|
||||
* `cse_*` directly, flip this to false to make toCompatSessionId a no-op.
|
||||
* Defaults to true — the shim stays active until explicitly disabled.
|
||||
*/
|
||||
export function isCseShimEnabled(): boolean {
|
||||
return feature('BRIDGE_MODE')
|
||||
? getFeatureValue_CACHED_MAY_BE_STALE(
|
||||
'tengu_bridge_repl_v2_cse_shim_enabled',
|
||||
true,
|
||||
)
|
||||
: true
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an error message if the current CLI version is below the
|
||||
* minimum required for the v1 (env-based) Remote Control path, or null if the
|
||||
* version is fine. The v2 (env-less) path uses checkEnvLessBridgeMinVersion()
|
||||
* in envLessBridgeConfig.ts instead — the two implementations have independent
|
||||
* version floors.
|
||||
*
|
||||
* Uses cached (non-blocking) GrowthBook config. If GrowthBook hasn't
|
||||
* loaded yet, the default '0.0.0' means the check passes — a safe fallback.
|
||||
*/
|
||||
export function checkBridgeMinVersion(): string | null {
|
||||
// Positive pattern — see docs/feature-gating.md.
|
||||
// Negative pattern (if (!feature(...)) return) does not eliminate
|
||||
// inline string literals from external builds.
|
||||
if (feature('BRIDGE_MODE')) {
|
||||
const config = getDynamicConfig_CACHED_MAY_BE_STALE<{
|
||||
minVersion: string
|
||||
}>('tengu_bridge_min_version', { minVersion: '0.0.0' })
|
||||
if (config.minVersion && lt(MACRO.VERSION, config.minVersion)) {
|
||||
return `Your version of Claude Code (${MACRO.VERSION}) is too old for Remote Control.\nVersion ${config.minVersion} or higher is required. Run \`claude update\` to update.`
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Default for remoteControlAtStartup when the user hasn't explicitly set it.
|
||||
* When the CCR_AUTO_CONNECT build flag is present (ant-only) and the
|
||||
* tengu_cobalt_harbor GrowthBook gate is on, all sessions connect to CCR by
|
||||
* default — the user can still opt out by setting remoteControlAtStartup=false
|
||||
* in config (explicit settings always win over this default).
|
||||
*
|
||||
* Defined here rather than in config.ts to avoid a direct
|
||||
* config.ts → growthbook.ts import cycle (growthbook.ts → user.ts → config.ts).
|
||||
*/
|
||||
export function getCcrAutoConnectDefault(): boolean {
|
||||
return feature('CCR_AUTO_CONNECT')
|
||||
? getFeatureValue_CACHED_MAY_BE_STALE('tengu_cobalt_harbor', false)
|
||||
: false
|
||||
}
|
||||
|
||||
/**
|
||||
* Opt-in CCR mirror mode — every local session spawns an outbound-only
|
||||
* Remote Control session that receives forwarded events. Separate from
|
||||
* getCcrAutoConnectDefault (bidirectional Remote Control). Env var wins for
|
||||
* local opt-in; GrowthBook controls rollout.
|
||||
*/
|
||||
export function isCcrMirrorEnabled(): boolean {
|
||||
return feature('CCR_MIRROR')
|
||||
? isEnvTruthy(process.env.CLAUDE_CODE_CCR_MIRROR) ||
|
||||
getFeatureValue_CACHED_MAY_BE_STALE('tengu_ccr_mirror', false)
|
||||
: false
|
||||
}
|
||||
|
||||
|
||||
3001
src/bridge/bridgeMain.ts
Normal file
3001
src/bridge/bridgeMain.ts
Normal file
File diff suppressed because it is too large
Load diff
463
src/bridge/bridgeMessaging.ts
Normal file
463
src/bridge/bridgeMessaging.ts
Normal file
|
|
@ -0,0 +1,463 @@
|
|||
/**
|
||||
* Shared transport-layer helpers for bridge message handling.
|
||||
*
|
||||
* Extracted from replBridge.ts so both the env-based core (initBridgeCore)
|
||||
* and the env-less core (initEnvLessBridgeCore) can use the same ingress
|
||||
* parsing, control-request handling, and echo-dedup machinery.
|
||||
*
|
||||
* Everything here is pure — no closure over bridge-specific state. All
|
||||
* collaborators (transport, sessionId, UUID sets, callbacks) are passed
|
||||
* as params.
|
||||
*/
|
||||
|
||||
import { randomUUID } from 'crypto'
|
||||
import type { SDKMessage } from '../entrypoints/agentSdkTypes.js'
|
||||
import type {
|
||||
SDKControlRequest,
|
||||
SDKControlResponse,
|
||||
} from '../entrypoints/sdk/controlTypes.js'
|
||||
import type { SDKResultSuccess } from '../entrypoints/sdk/coreTypes.js'
|
||||
import { logEvent } from '../services/analytics/index.js'
|
||||
import { EMPTY_USAGE } from '../services/api/emptyUsage.js'
|
||||
import type { Message } from '../types/message.js'
|
||||
import { normalizeControlMessageKeys } from '../utils/controlMessageCompat.js'
|
||||
import { logForDebugging } from '../utils/debug.js'
|
||||
import { stripDisplayTagsAllowEmpty } from '../utils/displayTags.js'
|
||||
import { errorMessage } from '../utils/errors.js'
|
||||
import type { PermissionMode } from '../utils/permissions/PermissionMode.js'
|
||||
import { jsonParse } from '../utils/slowOperations.js'
|
||||
import type { ReplBridgeTransport } from './replBridgeTransport.js'
|
||||
|
||||
// ─── Type guards ─────────────────────────────────────────────────────────────
|
||||
|
||||
/** Type predicate for parsed WebSocket messages. SDKMessage is a
|
||||
* discriminated union on `type` — validating the discriminant is
|
||||
* sufficient for the predicate; callers narrow further via the union. */
|
||||
export function isSDKMessage(value: unknown): value is SDKMessage {
|
||||
return (
|
||||
value !== null &&
|
||||
typeof value === 'object' &&
|
||||
'type' in value &&
|
||||
typeof value.type === 'string'
|
||||
)
|
||||
}
|
||||
|
||||
/** Type predicate for control_response messages from the server. */
|
||||
export function isSDKControlResponse(
|
||||
value: unknown,
|
||||
): value is SDKControlResponse {
|
||||
return (
|
||||
value !== null &&
|
||||
typeof value === 'object' &&
|
||||
'type' in value &&
|
||||
value.type === 'control_response' &&
|
||||
'response' in value
|
||||
)
|
||||
}
|
||||
|
||||
/** Type predicate for control_request messages from the server. */
|
||||
export function isSDKControlRequest(
|
||||
value: unknown,
|
||||
): value is SDKControlRequest {
|
||||
return (
|
||||
value !== null &&
|
||||
typeof value === 'object' &&
|
||||
'type' in value &&
|
||||
value.type === 'control_request' &&
|
||||
'request_id' in value &&
|
||||
'request' in value
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* True for message types that should be forwarded to the bridge transport.
|
||||
* The server only wants user/assistant turns and slash-command system events;
|
||||
* everything else (tool_result, progress, etc.) is internal REPL chatter.
|
||||
*/
|
||||
export function isEligibleBridgeMessage(m: Message): boolean {
|
||||
// Virtual messages (REPL inner calls) are display-only — bridge/SDK
|
||||
// consumers see the REPL tool_use/result which summarizes the work.
|
||||
if ((m.type === 'user' || m.type === 'assistant') && m.isVirtual) {
|
||||
return false
|
||||
}
|
||||
return (
|
||||
m.type === 'user' ||
|
||||
m.type === 'assistant' ||
|
||||
(m.type === 'system' && m.subtype === 'local_command')
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract title-worthy text from a Message for onUserMessage. Returns
|
||||
* undefined for messages that shouldn't title the session: non-user, meta
|
||||
* (nudges), tool results, compact summaries, non-human origins (task
|
||||
* notifications, channel messages), or pure display-tag content
|
||||
* (<ide_opened_file>, <session-start-hook>, etc.).
|
||||
*
|
||||
* Synthetic interrupts ([Request interrupted by user]) are NOT filtered here —
|
||||
* isSyntheticMessage lives in messages.ts (heavy import, pulls command
|
||||
* registry). The initialMessages path in initReplBridge checks it; the
|
||||
* writeMessages path reaching an interrupt as the *first* message is
|
||||
* implausible (an interrupt implies a prior prompt already flowed through).
|
||||
*/
|
||||
export function extractTitleText(m: Message): string | undefined {
|
||||
if (m.type !== 'user' || m.isMeta || m.toolUseResult || m.isCompactSummary)
|
||||
return undefined
|
||||
if (m.origin && m.origin.kind !== 'human') return undefined
|
||||
const content = m.message.content
|
||||
let raw: string | undefined
|
||||
if (typeof content === 'string') {
|
||||
raw = content
|
||||
} else {
|
||||
for (const block of content) {
|
||||
if (block.type === 'text') {
|
||||
raw = block.text
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!raw) return undefined
|
||||
const clean = stripDisplayTagsAllowEmpty(raw)
|
||||
return clean || undefined
|
||||
}
|
||||
|
||||
// ─── Ingress routing ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Parse an ingress WebSocket message and route it to the appropriate handler.
|
||||
* Ignores messages whose UUID is in recentPostedUUIDs (echoes of what we sent)
|
||||
* or in recentInboundUUIDs (re-deliveries we've already forwarded — e.g.
|
||||
* server replayed history after a transport swap lost the seq-num cursor).
|
||||
*/
|
||||
export function handleIngressMessage(
|
||||
data: string,
|
||||
recentPostedUUIDs: BoundedUUIDSet,
|
||||
recentInboundUUIDs: BoundedUUIDSet,
|
||||
onInboundMessage: ((msg: SDKMessage) => void | Promise<void>) | undefined,
|
||||
onPermissionResponse?: ((response: SDKControlResponse) => void) | undefined,
|
||||
onControlRequest?: ((request: SDKControlRequest) => void) | undefined,
|
||||
): void {
|
||||
try {
|
||||
const parsed: unknown = normalizeControlMessageKeys(jsonParse(data))
|
||||
|
||||
// control_response is not an SDKMessage — check before the type guard
|
||||
if (isSDKControlResponse(parsed)) {
|
||||
logForDebugging('[bridge:repl] Ingress message type=control_response')
|
||||
onPermissionResponse?.(parsed)
|
||||
return
|
||||
}
|
||||
|
||||
// control_request from the server (initialize, set_model, can_use_tool).
|
||||
// Must respond promptly or the server kills the WS (~10-14s timeout).
|
||||
if (isSDKControlRequest(parsed)) {
|
||||
logForDebugging(
|
||||
`[bridge:repl] Inbound control_request subtype=${parsed.request.subtype}`,
|
||||
)
|
||||
onControlRequest?.(parsed)
|
||||
return
|
||||
}
|
||||
|
||||
if (!isSDKMessage(parsed)) return
|
||||
|
||||
// Check for UUID to detect echoes of our own messages
|
||||
const uuid =
|
||||
'uuid' in parsed && typeof parsed.uuid === 'string'
|
||||
? parsed.uuid
|
||||
: undefined
|
||||
|
||||
if (uuid && recentPostedUUIDs.has(uuid)) {
|
||||
logForDebugging(
|
||||
`[bridge:repl] Ignoring echo: type=${parsed.type} uuid=${uuid}`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Defensive dedup: drop inbound prompts we've already forwarded. The
|
||||
// SSE seq-num carryover (lastTransportSequenceNum) is the primary fix
|
||||
// for history-replay; this catches edge cases where that negotiation
|
||||
// fails (server ignores from_sequence_num, transport died before
|
||||
// receiving any frames, etc).
|
||||
if (uuid && recentInboundUUIDs.has(uuid)) {
|
||||
logForDebugging(
|
||||
`[bridge:repl] Ignoring re-delivered inbound: type=${parsed.type} uuid=${uuid}`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
logForDebugging(
|
||||
`[bridge:repl] Ingress message type=${parsed.type}${uuid ? ` uuid=${uuid}` : ''}`,
|
||||
)
|
||||
|
||||
if (parsed.type === 'user') {
|
||||
if (uuid) recentInboundUUIDs.add(uuid)
|
||||
logEvent('tengu_bridge_message_received', {
|
||||
is_repl: true,
|
||||
})
|
||||
// Fire-and-forget — handler may be async (attachment resolution).
|
||||
void onInboundMessage?.(parsed)
|
||||
} else {
|
||||
logForDebugging(
|
||||
`[bridge:repl] Ignoring non-user inbound message: type=${parsed.type}`,
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
logForDebugging(
|
||||
`[bridge:repl] Failed to parse ingress message: ${errorMessage(err)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Server-initiated control requests ───────────────────────────────────────
|
||||
|
||||
export type ServerControlRequestHandlers = {
|
||||
transport: ReplBridgeTransport | null
|
||||
sessionId: string
|
||||
/**
|
||||
* When true, all mutable requests (interrupt, set_model, set_permission_mode,
|
||||
* set_max_thinking_tokens) reply with an error instead of false-success.
|
||||
* initialize still replies success — the server kills the connection otherwise.
|
||||
* Used by the outbound-only bridge mode and the SDK's /bridge subpath so claude.ai sees a
|
||||
* proper error instead of "action succeeded but nothing happened locally".
|
||||
*/
|
||||
outboundOnly?: boolean
|
||||
onInterrupt?: () => void
|
||||
onSetModel?: (model: string | undefined) => void
|
||||
onSetMaxThinkingTokens?: (maxTokens: number | null) => void
|
||||
onSetPermissionMode?: (
|
||||
mode: PermissionMode,
|
||||
) => { ok: true } | { ok: false; error: string }
|
||||
}
|
||||
|
||||
const OUTBOUND_ONLY_ERROR =
|
||||
'This session is outbound-only. Enable Remote Control locally to allow inbound control.'
|
||||
|
||||
/**
|
||||
* Respond to inbound control_request messages from the server. The server
|
||||
* sends these for session lifecycle events (initialize, set_model) and
|
||||
* for turn-level coordination (interrupt, set_max_thinking_tokens). If we
|
||||
* don't respond, the server hangs and kills the WS after ~10-14s.
|
||||
*
|
||||
* Previously a closure inside initBridgeCore's onWorkReceived; now takes
|
||||
* collaborators as params so both cores can use it.
|
||||
*/
|
||||
export function handleServerControlRequest(
|
||||
request: SDKControlRequest,
|
||||
handlers: ServerControlRequestHandlers,
|
||||
): void {
|
||||
const {
|
||||
transport,
|
||||
sessionId,
|
||||
outboundOnly,
|
||||
onInterrupt,
|
||||
onSetModel,
|
||||
onSetMaxThinkingTokens,
|
||||
onSetPermissionMode,
|
||||
} = handlers
|
||||
if (!transport) {
|
||||
logForDebugging(
|
||||
'[bridge:repl] Cannot respond to control_request: transport not configured',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
let response: SDKControlResponse
|
||||
|
||||
// Outbound-only: reply error for mutable requests so claude.ai doesn't show
|
||||
// false success. initialize must still succeed (server kills the connection
|
||||
// if it doesn't — see comment above).
|
||||
if (outboundOnly && request.request.subtype !== 'initialize') {
|
||||
response = {
|
||||
type: 'control_response',
|
||||
response: {
|
||||
subtype: 'error',
|
||||
request_id: request.request_id,
|
||||
error: OUTBOUND_ONLY_ERROR,
|
||||
},
|
||||
}
|
||||
const event = { ...response, session_id: sessionId }
|
||||
void transport.write(event)
|
||||
logForDebugging(
|
||||
`[bridge:repl] Rejected ${request.request.subtype} (outbound-only) request_id=${request.request_id}`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
switch (request.request.subtype) {
|
||||
case 'initialize':
|
||||
// Respond with minimal capabilities — the REPL handles
|
||||
// commands, models, and account info itself.
|
||||
response = {
|
||||
type: 'control_response',
|
||||
response: {
|
||||
subtype: 'success',
|
||||
request_id: request.request_id,
|
||||
response: {
|
||||
commands: [],
|
||||
output_style: 'normal',
|
||||
available_output_styles: ['normal'],
|
||||
models: [],
|
||||
account: {},
|
||||
pid: process.pid,
|
||||
},
|
||||
},
|
||||
}
|
||||
break
|
||||
|
||||
case 'set_model':
|
||||
onSetModel?.(request.request.model)
|
||||
response = {
|
||||
type: 'control_response',
|
||||
response: {
|
||||
subtype: 'success',
|
||||
request_id: request.request_id,
|
||||
},
|
||||
}
|
||||
break
|
||||
|
||||
case 'set_max_thinking_tokens':
|
||||
onSetMaxThinkingTokens?.(request.request.max_thinking_tokens)
|
||||
response = {
|
||||
type: 'control_response',
|
||||
response: {
|
||||
subtype: 'success',
|
||||
request_id: request.request_id,
|
||||
},
|
||||
}
|
||||
break
|
||||
|
||||
case 'set_permission_mode': {
|
||||
// The callback returns a policy verdict so we can send an error
|
||||
// control_response without importing isAutoModeGateEnabled /
|
||||
// isBypassPermissionsModeDisabled here (bootstrap-isolation). If no
|
||||
// callback is registered (daemon context, which doesn't wire this —
|
||||
// see daemonBridge.ts), return an error verdict rather than a silent
|
||||
// false-success: the mode is never actually applied in that context,
|
||||
// so success would lie to the client.
|
||||
const verdict = onSetPermissionMode?.(request.request.mode) ?? {
|
||||
ok: false,
|
||||
error:
|
||||
'set_permission_mode is not supported in this context (onSetPermissionMode callback not registered)',
|
||||
}
|
||||
if (verdict.ok) {
|
||||
response = {
|
||||
type: 'control_response',
|
||||
response: {
|
||||
subtype: 'success',
|
||||
request_id: request.request_id,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
response = {
|
||||
type: 'control_response',
|
||||
response: {
|
||||
subtype: 'error',
|
||||
request_id: request.request_id,
|
||||
error: verdict.error,
|
||||
},
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'interrupt':
|
||||
onInterrupt?.()
|
||||
response = {
|
||||
type: 'control_response',
|
||||
response: {
|
||||
subtype: 'success',
|
||||
request_id: request.request_id,
|
||||
},
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
// Unknown subtype — respond with error so the server doesn't
|
||||
// hang waiting for a reply that never comes.
|
||||
response = {
|
||||
type: 'control_response',
|
||||
response: {
|
||||
subtype: 'error',
|
||||
request_id: request.request_id,
|
||||
error: `REPL bridge does not handle control_request subtype: ${request.request.subtype}`,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const event = { ...response, session_id: sessionId }
|
||||
void transport.write(event)
|
||||
logForDebugging(
|
||||
`[bridge:repl] Sent control_response for ${request.request.subtype} request_id=${request.request_id} result=${response.response.subtype}`,
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Result message (for session archival on teardown) ───────────────────────
|
||||
|
||||
/**
|
||||
* Build a minimal `SDKResultSuccess` message for session archival.
|
||||
* The server needs this event before a WS close to trigger archival.
|
||||
*/
|
||||
export function makeResultMessage(sessionId: string): SDKResultSuccess {
|
||||
return {
|
||||
type: 'result',
|
||||
subtype: 'success',
|
||||
duration_ms: 0,
|
||||
duration_api_ms: 0,
|
||||
is_error: false,
|
||||
num_turns: 0,
|
||||
result: '',
|
||||
stop_reason: null,
|
||||
total_cost_usd: 0,
|
||||
usage: { ...EMPTY_USAGE },
|
||||
modelUsage: {},
|
||||
permission_denials: [],
|
||||
session_id: sessionId,
|
||||
uuid: randomUUID(),
|
||||
}
|
||||
}
|
||||
|
||||
// ─── BoundedUUIDSet (echo-dedup ring buffer) ─────────────────────────────────
|
||||
|
||||
/**
|
||||
* FIFO-bounded set backed by a circular buffer. Evicts the oldest entry
|
||||
* when capacity is reached, keeping memory usage constant at O(capacity).
|
||||
*
|
||||
* Messages are added in chronological order, so evicted entries are always
|
||||
* the oldest. The caller relies on external ordering (the hook's
|
||||
* lastWrittenIndexRef) as the primary dedup — this set is a secondary
|
||||
* safety net for echo filtering and race-condition dedup.
|
||||
*/
|
||||
export class BoundedUUIDSet {
|
||||
private readonly capacity: number
|
||||
private readonly ring: (string | undefined)[]
|
||||
private readonly set = new Set<string>()
|
||||
private writeIdx = 0
|
||||
|
||||
constructor(capacity: number) {
|
||||
this.capacity = capacity
|
||||
this.ring = new Array<string | undefined>(capacity)
|
||||
}
|
||||
|
||||
add(uuid: string): void {
|
||||
if (this.set.has(uuid)) return
|
||||
// Evict the entry at the current write position (if occupied)
|
||||
const evicted = this.ring[this.writeIdx]
|
||||
if (evicted !== undefined) {
|
||||
this.set.delete(evicted)
|
||||
}
|
||||
this.ring[this.writeIdx] = uuid
|
||||
this.set.add(uuid)
|
||||
this.writeIdx = (this.writeIdx + 1) % this.capacity
|
||||
}
|
||||
|
||||
has(uuid: string): boolean {
|
||||
return this.set.has(uuid)
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.set.clear()
|
||||
this.ring.fill(undefined)
|
||||
this.writeIdx = 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
45
src/bridge/bridgePermissionCallbacks.ts
Normal file
45
src/bridge/bridgePermissionCallbacks.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import type { PermissionUpdate } from '../utils/permissions/PermissionUpdateSchema.js'
|
||||
|
||||
type BridgePermissionResponse = {
|
||||
behavior: 'allow' | 'deny'
|
||||
updatedInput?: Record<string, unknown>
|
||||
updatedPermissions?: PermissionUpdate[]
|
||||
message?: string
|
||||
}
|
||||
|
||||
type BridgePermissionCallbacks = {
|
||||
sendRequest(
|
||||
requestId: string,
|
||||
toolName: string,
|
||||
input: Record<string, unknown>,
|
||||
toolUseId: string,
|
||||
description: string,
|
||||
permissionSuggestions?: PermissionUpdate[],
|
||||
blockedPath?: string,
|
||||
): void
|
||||
sendResponse(requestId: string, response: BridgePermissionResponse): void
|
||||
/** Cancel a pending control_request so the web app can dismiss its prompt. */
|
||||
cancelRequest(requestId: string): void
|
||||
onResponse(
|
||||
requestId: string,
|
||||
handler: (response: BridgePermissionResponse) => void,
|
||||
): () => void // returns unsubscribe
|
||||
}
|
||||
|
||||
/** Type predicate for validating a parsed control_response payload
|
||||
* as a BridgePermissionResponse. Checks the required `behavior`
|
||||
* discriminant rather than using an unsafe `as` cast. */
|
||||
function isBridgePermissionResponse(
|
||||
value: unknown,
|
||||
): value is BridgePermissionResponse {
|
||||
if (!value || typeof value !== 'object') return false
|
||||
return (
|
||||
'behavior' in value &&
|
||||
(value.behavior === 'allow' || value.behavior === 'deny')
|
||||
)
|
||||
}
|
||||
|
||||
export { isBridgePermissionResponse }
|
||||
export type { BridgePermissionCallbacks, BridgePermissionResponse }
|
||||
|
||||
|
||||
212
src/bridge/bridgePointer.ts
Normal file
212
src/bridge/bridgePointer.ts
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
import { mkdir, readFile, stat, unlink, writeFile } from 'fs/promises'
|
||||
import { dirname, join } from 'path'
|
||||
import { z } from 'zod/v4'
|
||||
import { logForDebugging } from '../utils/debug.js'
|
||||
import { isENOENT } from '../utils/errors.js'
|
||||
import { getWorktreePathsPortable } from '../utils/getWorktreePathsPortable.js'
|
||||
import { lazySchema } from '../utils/lazySchema.js'
|
||||
import {
|
||||
getProjectsDir,
|
||||
sanitizePath,
|
||||
} from '../utils/sessionStoragePortable.js'
|
||||
import { jsonParse, jsonStringify } from '../utils/slowOperations.js'
|
||||
|
||||
/**
|
||||
* Upper bound on worktree fanout. git worktree list is naturally bounded
|
||||
* (50 is a LOT), but this caps the parallel stat() burst and guards against
|
||||
* pathological setups. Above this, --continue falls back to current-dir-only.
|
||||
*/
|
||||
const MAX_WORKTREE_FANOUT = 50
|
||||
|
||||
/**
|
||||
* Crash-recovery pointer for Remote Control sessions.
|
||||
*
|
||||
* Written immediately after a bridge session is created, periodically
|
||||
* refreshed during the session, and cleared on clean shutdown. If the
|
||||
* process dies unclean (crash, kill -9, terminal closed), the pointer
|
||||
* persists. On next startup, `claude remote-control` detects it and offers
|
||||
* to resume via the --session-id flow from #20460.
|
||||
*
|
||||
* Staleness is checked against the file's mtime (not an embedded timestamp)
|
||||
* so that a periodic re-write with the same content serves as a refresh —
|
||||
* matches the backend's rolling BRIDGE_LAST_POLL_TTL (4h) semantics. A
|
||||
* bridge that's been polling for 5+ hours and then crashes still has a
|
||||
* fresh pointer as long as the refresh ran within the window.
|
||||
*
|
||||
* Scoped per working directory (alongside transcript JSONL files) so two
|
||||
* concurrent bridges in different repos don't clobber each other.
|
||||
*/
|
||||
|
||||
export const BRIDGE_POINTER_TTL_MS = 4 * 60 * 60 * 1000
|
||||
|
||||
const BridgePointerSchema = lazySchema(() =>
|
||||
z.object({
|
||||
sessionId: z.string(),
|
||||
environmentId: z.string(),
|
||||
source: z.enum(['standalone', 'repl']),
|
||||
}),
|
||||
)
|
||||
|
||||
export type BridgePointer = z.infer<ReturnType<typeof BridgePointerSchema>>
|
||||
|
||||
export function getBridgePointerPath(dir: string): string {
|
||||
return join(getProjectsDir(), sanitizePath(dir), 'bridge-pointer.json')
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the pointer. Also used to refresh mtime during long sessions —
|
||||
* calling with the same IDs is a cheap no-content-change write that bumps
|
||||
* the staleness clock. Best-effort — a crash-recovery file must never
|
||||
* itself cause a crash. Logs and swallows on error.
|
||||
*/
|
||||
export async function writeBridgePointer(
|
||||
dir: string,
|
||||
pointer: BridgePointer,
|
||||
): Promise<void> {
|
||||
const path = getBridgePointerPath(dir)
|
||||
try {
|
||||
await mkdir(dirname(path), { recursive: true })
|
||||
await writeFile(path, jsonStringify(pointer), 'utf8')
|
||||
logForDebugging(`[bridge:pointer] wrote ${path}`)
|
||||
} catch (err: unknown) {
|
||||
logForDebugging(`[bridge:pointer] write failed: ${err}`, { level: 'warn' })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the pointer and its age (ms since last write). Operates directly
|
||||
* and handles errors — no existence check (CLAUDE.md TOCTOU rule). Returns
|
||||
* null on any failure: missing file, corrupted JSON, schema mismatch, or
|
||||
* stale (mtime > 4h ago). Stale/invalid pointers are deleted so they don't
|
||||
* keep re-prompting after the backend has already GC'd the env.
|
||||
*/
|
||||
export async function readBridgePointer(
|
||||
dir: string,
|
||||
): Promise<(BridgePointer & { ageMs: number }) | null> {
|
||||
const path = getBridgePointerPath(dir)
|
||||
let raw: string
|
||||
let mtimeMs: number
|
||||
try {
|
||||
// stat for mtime (staleness anchor), then read. Two syscalls, but both
|
||||
// are needed — mtime IS the data we return, not a TOCTOU guard.
|
||||
mtimeMs = (await stat(path)).mtimeMs
|
||||
raw = await readFile(path, 'utf8')
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
|
||||
const parsed = BridgePointerSchema().safeParse(safeJsonParse(raw))
|
||||
if (!parsed.success) {
|
||||
logForDebugging(`[bridge:pointer] invalid schema, clearing: ${path}`)
|
||||
await clearBridgePointer(dir)
|
||||
return null
|
||||
}
|
||||
|
||||
const ageMs = Math.max(0, Date.now() - mtimeMs)
|
||||
if (ageMs > BRIDGE_POINTER_TTL_MS) {
|
||||
logForDebugging(`[bridge:pointer] stale (>4h mtime), clearing: ${path}`)
|
||||
await clearBridgePointer(dir)
|
||||
return null
|
||||
}
|
||||
|
||||
return { ...parsed.data, ageMs }
|
||||
}
|
||||
|
||||
/**
|
||||
* Worktree-aware read for `--continue`. The REPL bridge writes its pointer
|
||||
* to `getOriginalCwd()` which EnterWorktreeTool/activeWorktreeSession can
|
||||
* mutate to a worktree path — but `claude remote-control --continue` runs
|
||||
* with `resolve('.')` = shell CWD. This fans out across git worktree
|
||||
* siblings to find the freshest pointer, matching /resume's semantics.
|
||||
*
|
||||
* Fast path: checks `dir` first. Only shells out to `git worktree list` if
|
||||
* that misses — the common case (pointer in launch dir) is one stat, zero
|
||||
* exec. Fanout reads run in parallel; capped at MAX_WORKTREE_FANOUT.
|
||||
*
|
||||
* Returns the pointer AND the dir it was found in, so the caller can clear
|
||||
* the right file on resume failure.
|
||||
*/
|
||||
export async function readBridgePointerAcrossWorktrees(
|
||||
dir: string,
|
||||
): Promise<{ pointer: BridgePointer & { ageMs: number }; dir: string } | null> {
|
||||
// Fast path: current dir. Covers standalone bridge (always matches) and
|
||||
// REPL bridge when no worktree mutation happened.
|
||||
const here = await readBridgePointer(dir)
|
||||
if (here) {
|
||||
return { pointer: here, dir }
|
||||
}
|
||||
|
||||
// Fanout: scan worktree siblings. getWorktreePathsPortable has a 5s
|
||||
// timeout and returns [] on any error (not a git repo, git not installed).
|
||||
const worktrees = await getWorktreePathsPortable(dir)
|
||||
if (worktrees.length <= 1) return null
|
||||
if (worktrees.length > MAX_WORKTREE_FANOUT) {
|
||||
logForDebugging(
|
||||
`[bridge:pointer] ${worktrees.length} worktrees exceeds fanout cap ${MAX_WORKTREE_FANOUT}, skipping`,
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
// Dedupe against `dir` so we don't re-stat it. sanitizePath normalizes
|
||||
// case/separators so worktree-list output matches our fast-path key even
|
||||
// on Windows where git may emit C:/ vs stored c:/.
|
||||
const dirKey = sanitizePath(dir)
|
||||
const candidates = worktrees.filter(wt => sanitizePath(wt) !== dirKey)
|
||||
|
||||
// Parallel stat+read. Each readBridgePointer is a stat() that ENOENTs
|
||||
// for worktrees with no pointer (cheap) plus a ~100-byte read for the
|
||||
// rare ones that have one. Promise.all → latency ≈ slowest single stat.
|
||||
const results = await Promise.all(
|
||||
candidates.map(async wt => {
|
||||
const p = await readBridgePointer(wt)
|
||||
return p ? { pointer: p, dir: wt } : null
|
||||
}),
|
||||
)
|
||||
|
||||
// Pick freshest (lowest ageMs). The pointer stores environmentId so
|
||||
// resume reconnects to the right env regardless of which worktree
|
||||
// --continue was invoked from.
|
||||
let freshest: {
|
||||
pointer: BridgePointer & { ageMs: number }
|
||||
dir: string
|
||||
} | null = null
|
||||
for (const r of results) {
|
||||
if (r && (!freshest || r.pointer.ageMs < freshest.pointer.ageMs)) {
|
||||
freshest = r
|
||||
}
|
||||
}
|
||||
if (freshest) {
|
||||
logForDebugging(
|
||||
`[bridge:pointer] fanout found pointer in worktree ${freshest.dir} (ageMs=${freshest.pointer.ageMs})`,
|
||||
)
|
||||
}
|
||||
return freshest
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the pointer. Idempotent — ENOENT is expected when the process
|
||||
* shut down clean previously.
|
||||
*/
|
||||
export async function clearBridgePointer(dir: string): Promise<void> {
|
||||
const path = getBridgePointerPath(dir)
|
||||
try {
|
||||
await unlink(path)
|
||||
logForDebugging(`[bridge:pointer] cleared ${path}`)
|
||||
} catch (err: unknown) {
|
||||
if (!isENOENT(err)) {
|
||||
logForDebugging(`[bridge:pointer] clear failed: ${err}`, {
|
||||
level: 'warn',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function safeJsonParse(raw: string): unknown {
|
||||
try {
|
||||
return jsonParse(raw)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
165
src/bridge/bridgeStatusUtil.ts
Normal file
165
src/bridge/bridgeStatusUtil.ts
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
import {
|
||||
getClaudeAiBaseUrl,
|
||||
getRemoteSessionUrl,
|
||||
} from '../constants/product.js'
|
||||
import { stringWidth } from '../ink/stringWidth.js'
|
||||
import { formatDuration, truncateToWidth } from '../utils/format.js'
|
||||
import { getGraphemeSegmenter } from '../utils/intl.js'
|
||||
|
||||
/** Bridge status state machine states. */
|
||||
export type StatusState =
|
||||
| 'idle'
|
||||
| 'attached'
|
||||
| 'titled'
|
||||
| 'reconnecting'
|
||||
| 'failed'
|
||||
|
||||
/** How long a tool activity line stays visible after last tool_start (ms). */
|
||||
export const TOOL_DISPLAY_EXPIRY_MS = 30_000
|
||||
|
||||
/** Interval for the shimmer animation tick (ms). */
|
||||
export const SHIMMER_INTERVAL_MS = 150
|
||||
|
||||
export function timestamp(): string {
|
||||
const now = new Date()
|
||||
const h = String(now.getHours()).padStart(2, '0')
|
||||
const m = String(now.getMinutes()).padStart(2, '0')
|
||||
const s = String(now.getSeconds()).padStart(2, '0')
|
||||
return `${h}:${m}:${s}`
|
||||
}
|
||||
|
||||
export { formatDuration, truncateToWidth as truncatePrompt }
|
||||
|
||||
/** Abbreviate a tool activity summary for the trail display. */
|
||||
export function abbreviateActivity(summary: string): string {
|
||||
return truncateToWidth(summary, 30)
|
||||
}
|
||||
|
||||
/** Build the connect URL shown when the bridge is idle. */
|
||||
export function buildBridgeConnectUrl(
|
||||
environmentId: string,
|
||||
ingressUrl?: string,
|
||||
): string {
|
||||
const baseUrl = getClaudeAiBaseUrl(undefined, ingressUrl)
|
||||
return `${baseUrl}/code?bridge=${environmentId}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the session URL shown when a session is attached. Delegates to
|
||||
* getRemoteSessionUrl for the cse_→session_ prefix translation, then appends
|
||||
* the v1-specific ?bridge={environmentId} query.
|
||||
*/
|
||||
export function buildBridgeSessionUrl(
|
||||
sessionId: string,
|
||||
environmentId: string,
|
||||
ingressUrl?: string,
|
||||
): string {
|
||||
return `${getRemoteSessionUrl(sessionId, ingressUrl)}?bridge=${environmentId}`
|
||||
}
|
||||
|
||||
/** Compute the glimmer index for a reverse-sweep shimmer animation. */
|
||||
export function computeGlimmerIndex(
|
||||
tick: number,
|
||||
messageWidth: number,
|
||||
): number {
|
||||
const cycleLength = messageWidth + 20
|
||||
return messageWidth + 10 - (tick % cycleLength)
|
||||
}
|
||||
|
||||
/**
|
||||
* Split text into three segments by visual column position for shimmer rendering.
|
||||
*
|
||||
* Uses grapheme segmentation and `stringWidth` so the split is correct for
|
||||
* multi-byte characters, emoji, and CJK glyphs.
|
||||
*
|
||||
* Returns `{ before, shimmer, after }` strings. Both renderers (chalk in
|
||||
* bridgeUI.ts and React/Ink in bridge.tsx) apply their own coloring to
|
||||
* these segments.
|
||||
*/
|
||||
export function computeShimmerSegments(
|
||||
text: string,
|
||||
glimmerIndex: number,
|
||||
): { before: string; shimmer: string; after: string } {
|
||||
const messageWidth = stringWidth(text)
|
||||
const shimmerStart = glimmerIndex - 1
|
||||
const shimmerEnd = glimmerIndex + 1
|
||||
|
||||
// When shimmer is offscreen, return all text as "before"
|
||||
if (shimmerStart >= messageWidth || shimmerEnd < 0) {
|
||||
return { before: text, shimmer: '', after: '' }
|
||||
}
|
||||
|
||||
// Split into at most 3 segments by visual column position
|
||||
const clampedStart = Math.max(0, shimmerStart)
|
||||
let colPos = 0
|
||||
let before = ''
|
||||
let shimmer = ''
|
||||
let after = ''
|
||||
for (const { segment } of getGraphemeSegmenter().segment(text)) {
|
||||
const segWidth = stringWidth(segment)
|
||||
if (colPos + segWidth <= clampedStart) {
|
||||
before += segment
|
||||
} else if (colPos > shimmerEnd) {
|
||||
after += segment
|
||||
} else {
|
||||
shimmer += segment
|
||||
}
|
||||
colPos += segWidth
|
||||
}
|
||||
|
||||
return { before, shimmer, after }
|
||||
}
|
||||
|
||||
/** Computed bridge status label and color from connection state. */
|
||||
export type BridgeStatusInfo = {
|
||||
label:
|
||||
| 'Remote Control failed'
|
||||
| 'Remote Control reconnecting'
|
||||
| 'Remote Control active'
|
||||
| 'Remote Control connecting\u2026'
|
||||
color: 'error' | 'warning' | 'success'
|
||||
}
|
||||
|
||||
/** Derive a status label and color from the bridge connection state. */
|
||||
export function getBridgeStatus({
|
||||
error,
|
||||
connected,
|
||||
sessionActive,
|
||||
reconnecting,
|
||||
}: {
|
||||
error: string | undefined
|
||||
connected: boolean
|
||||
sessionActive: boolean
|
||||
reconnecting: boolean
|
||||
}): BridgeStatusInfo {
|
||||
if (error) return { label: 'Remote Control failed', color: 'error' }
|
||||
if (reconnecting)
|
||||
return { label: 'Remote Control reconnecting', color: 'warning' }
|
||||
if (sessionActive || connected)
|
||||
return { label: 'Remote Control active', color: 'success' }
|
||||
return { label: 'Remote Control connecting\u2026', color: 'warning' }
|
||||
}
|
||||
|
||||
/** Footer text shown when bridge is idle (Ready state). */
|
||||
export function buildIdleFooterText(url: string): string {
|
||||
return `Code everywhere with the Claude app or ${url}`
|
||||
}
|
||||
|
||||
/** Footer text shown when a session is active (Connected state). */
|
||||
export function buildActiveFooterText(url: string): string {
|
||||
return `Continue coding in the Claude app or ${url}`
|
||||
}
|
||||
|
||||
/** Footer text shown when the bridge has failed. */
|
||||
export const FAILED_FOOTER_TEXT = 'Something went wrong, please try again'
|
||||
|
||||
/**
|
||||
* Wrap text in an OSC 8 terminal hyperlink. Zero visual width for layout purposes.
|
||||
* strip-ansi (used by stringWidth) correctly strips these sequences, so
|
||||
* countVisualLines in bridgeUI.ts remains accurate.
|
||||
*/
|
||||
export function wrapWithOsc8Link(text: string, url: string): string {
|
||||
return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`
|
||||
}
|
||||
|
||||
|
||||
532
src/bridge/bridgeUI.ts
Normal file
532
src/bridge/bridgeUI.ts
Normal file
|
|
@ -0,0 +1,532 @@
|
|||
import chalk from 'chalk'
|
||||
import { toString as qrToString } from 'qrcode'
|
||||
import {
|
||||
BRIDGE_FAILED_INDICATOR,
|
||||
BRIDGE_READY_INDICATOR,
|
||||
BRIDGE_SPINNER_FRAMES,
|
||||
} from '../constants/figures.js'
|
||||
import { stringWidth } from '../ink/stringWidth.js'
|
||||
import { logForDebugging } from '../utils/debug.js'
|
||||
import {
|
||||
buildActiveFooterText,
|
||||
buildBridgeConnectUrl,
|
||||
buildBridgeSessionUrl,
|
||||
buildIdleFooterText,
|
||||
FAILED_FOOTER_TEXT,
|
||||
formatDuration,
|
||||
type StatusState,
|
||||
TOOL_DISPLAY_EXPIRY_MS,
|
||||
timestamp,
|
||||
truncatePrompt,
|
||||
wrapWithOsc8Link,
|
||||
} from './bridgeStatusUtil.js'
|
||||
import type {
|
||||
BridgeConfig,
|
||||
BridgeLogger,
|
||||
SessionActivity,
|
||||
SpawnMode,
|
||||
} from './types.js'
|
||||
|
||||
const QR_OPTIONS = {
|
||||
type: 'utf8' as const,
|
||||
errorCorrectionLevel: 'L' as const,
|
||||
small: true,
|
||||
}
|
||||
|
||||
/** Generate a QR code and return its lines. */
|
||||
async function generateQr(url: string): Promise<string[]> {
|
||||
const qr = await qrToString(url, QR_OPTIONS)
|
||||
return qr.split('\n').filter((line: string) => line.length > 0)
|
||||
}
|
||||
|
||||
export function createBridgeLogger(options: {
|
||||
verbose: boolean
|
||||
write?: (s: string) => void
|
||||
}): BridgeLogger {
|
||||
const write = options.write ?? ((s: string) => process.stdout.write(s))
|
||||
const verbose = options.verbose
|
||||
|
||||
// Track how many status lines are currently displayed at the bottom
|
||||
let statusLineCount = 0
|
||||
|
||||
// Status state machine
|
||||
let currentState: StatusState = 'idle'
|
||||
let currentStateText = 'Ready'
|
||||
let repoName = ''
|
||||
let branch = ''
|
||||
let debugLogPath = ''
|
||||
|
||||
// Connect URL (built in printBanner with correct base for staging/prod)
|
||||
let connectUrl = ''
|
||||
let cachedIngressUrl = ''
|
||||
let cachedEnvironmentId = ''
|
||||
let activeSessionUrl: string | null = null
|
||||
|
||||
// QR code lines for the current URL
|
||||
let qrLines: string[] = []
|
||||
let qrVisible = false
|
||||
|
||||
// Tool activity for the second status line
|
||||
let lastToolSummary: string | null = null
|
||||
let lastToolTime = 0
|
||||
|
||||
// Session count indicator (shown when multi-session mode is enabled)
|
||||
let sessionActive = 0
|
||||
let sessionMax = 1
|
||||
// Spawn mode shown in the session-count line + gates the `w` hint
|
||||
let spawnModeDisplay: 'same-dir' | 'worktree' | null = null
|
||||
let spawnMode: SpawnMode = 'single-session'
|
||||
|
||||
// Per-session display info for the multi-session bullet list (keyed by compat sessionId)
|
||||
const sessionDisplayInfo = new Map<
|
||||
string,
|
||||
{ title?: string; url: string; activity?: SessionActivity }
|
||||
>()
|
||||
|
||||
// Connecting spinner state
|
||||
let connectingTimer: ReturnType<typeof setInterval> | null = null
|
||||
let connectingTick = 0
|
||||
|
||||
/**
|
||||
* Count how many visual terminal rows a string occupies, accounting for
|
||||
* line wrapping. Each `\n` is one row, and content wider than the terminal
|
||||
* wraps to additional rows.
|
||||
*/
|
||||
function countVisualLines(text: string): number {
|
||||
// eslint-disable-next-line custom-rules/prefer-use-terminal-size
|
||||
const cols = process.stdout.columns || 80 // non-React CLI context
|
||||
let count = 0
|
||||
// Split on newlines to get logical lines
|
||||
for (const logical of text.split('\n')) {
|
||||
if (logical.length === 0) {
|
||||
// Empty segment between consecutive \n — counts as 1 row
|
||||
count++
|
||||
continue
|
||||
}
|
||||
const width = stringWidth(logical)
|
||||
count += Math.max(1, Math.ceil(width / cols))
|
||||
}
|
||||
// The trailing \n in "line\n" produces an empty last element — don't count it
|
||||
// because the cursor sits at the start of the next line, not a new visual row.
|
||||
if (text.endsWith('\n')) {
|
||||
count--
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
/** Write a status line and track its visual line count. */
|
||||
function writeStatus(text: string): void {
|
||||
write(text)
|
||||
statusLineCount += countVisualLines(text)
|
||||
}
|
||||
|
||||
/** Clear any currently displayed status lines. */
|
||||
function clearStatusLines(): void {
|
||||
if (statusLineCount <= 0) return
|
||||
logForDebugging(`[bridge:ui] clearStatusLines count=${statusLineCount}`)
|
||||
// Move cursor up to the start of the status block, then erase everything below
|
||||
write(`\x1b[${statusLineCount}A`) // cursor up N lines
|
||||
write('\x1b[J') // erase from cursor to end of screen
|
||||
statusLineCount = 0
|
||||
}
|
||||
|
||||
/** Print a permanent log line, clearing status first and restoring after. */
|
||||
function printLog(line: string): void {
|
||||
clearStatusLines()
|
||||
write(line)
|
||||
}
|
||||
|
||||
/** Regenerate the QR code with the given URL. */
|
||||
function regenerateQr(url: string): void {
|
||||
generateQr(url)
|
||||
.then(lines => {
|
||||
qrLines = lines
|
||||
renderStatusLine()
|
||||
})
|
||||
.catch(e => {
|
||||
logForDebugging(`QR code generation failed: ${e}`, { level: 'error' })
|
||||
})
|
||||
}
|
||||
|
||||
/** Render the connecting spinner line (shown before first updateIdleStatus). */
|
||||
function renderConnectingLine(): void {
|
||||
clearStatusLines()
|
||||
|
||||
const frame =
|
||||
BRIDGE_SPINNER_FRAMES[connectingTick % BRIDGE_SPINNER_FRAMES.length]!
|
||||
let suffix = ''
|
||||
if (repoName) {
|
||||
suffix += chalk.dim(' \u00b7 ') + chalk.dim(repoName)
|
||||
}
|
||||
if (branch) {
|
||||
suffix += chalk.dim(' \u00b7 ') + chalk.dim(branch)
|
||||
}
|
||||
writeStatus(
|
||||
`${chalk.yellow(frame)} ${chalk.yellow('Connecting')}${suffix}\n`,
|
||||
)
|
||||
}
|
||||
|
||||
/** Start the connecting spinner. Stopped by first updateIdleStatus(). */
|
||||
function startConnecting(): void {
|
||||
stopConnecting()
|
||||
renderConnectingLine()
|
||||
connectingTimer = setInterval(() => {
|
||||
connectingTick++
|
||||
renderConnectingLine()
|
||||
}, 150)
|
||||
}
|
||||
|
||||
/** Stop the connecting spinner. */
|
||||
function stopConnecting(): void {
|
||||
if (connectingTimer) {
|
||||
clearInterval(connectingTimer)
|
||||
connectingTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
/** Render and write the current status lines based on state. */
|
||||
function renderStatusLine(): void {
|
||||
if (currentState === 'reconnecting' || currentState === 'failed') {
|
||||
// These states are handled separately (updateReconnectingStatus /
|
||||
// updateFailedStatus). Return before clearing so callers like toggleQr
|
||||
// and setSpawnModeDisplay don't blank the display during these states.
|
||||
return
|
||||
}
|
||||
|
||||
clearStatusLines()
|
||||
|
||||
const isIdle = currentState === 'idle'
|
||||
|
||||
// QR code above the status line
|
||||
if (qrVisible) {
|
||||
for (const line of qrLines) {
|
||||
writeStatus(`${chalk.dim(line)}\n`)
|
||||
}
|
||||
}
|
||||
|
||||
// Determine indicator and colors based on state
|
||||
const indicator = BRIDGE_READY_INDICATOR
|
||||
const indicatorColor = isIdle ? chalk.green : chalk.cyan
|
||||
const baseColor = isIdle ? chalk.green : chalk.cyan
|
||||
const stateText = baseColor(currentStateText)
|
||||
|
||||
// Build the suffix with repo and branch
|
||||
let suffix = ''
|
||||
if (repoName) {
|
||||
suffix += chalk.dim(' \u00b7 ') + chalk.dim(repoName)
|
||||
}
|
||||
// In worktree mode each session gets its own branch, so showing the
|
||||
// bridge's branch would be misleading.
|
||||
if (branch && spawnMode !== 'worktree') {
|
||||
suffix += chalk.dim(' \u00b7 ') + chalk.dim(branch)
|
||||
}
|
||||
|
||||
if (process.env.USER_TYPE === 'ant' && debugLogPath) {
|
||||
writeStatus(
|
||||
`${chalk.yellow('[ANT-ONLY] Logs:')} ${chalk.dim(debugLogPath)}\n`,
|
||||
)
|
||||
}
|
||||
writeStatus(`${indicatorColor(indicator)} ${stateText}${suffix}\n`)
|
||||
|
||||
// Session count and per-session list (multi-session mode only)
|
||||
if (sessionMax > 1) {
|
||||
const modeHint =
|
||||
spawnMode === 'worktree'
|
||||
? 'New sessions will be created in an isolated worktree'
|
||||
: 'New sessions will be created in the current directory'
|
||||
writeStatus(
|
||||
` ${chalk.dim(`Capacity: ${sessionActive}/${sessionMax} \u00b7 ${modeHint}`)}\n`,
|
||||
)
|
||||
for (const [, info] of sessionDisplayInfo) {
|
||||
const titleText = info.title
|
||||
? truncatePrompt(info.title, 35)
|
||||
: chalk.dim('Attached')
|
||||
const titleLinked = wrapWithOsc8Link(titleText, info.url)
|
||||
const act = info.activity
|
||||
const showAct = act && act.type !== 'result' && act.type !== 'error'
|
||||
const actText = showAct
|
||||
? chalk.dim(` ${truncatePrompt(act.summary, 40)}`)
|
||||
: ''
|
||||
writeStatus(` ${titleLinked}${actText}
|
||||
`)
|
||||
}
|
||||
}
|
||||
|
||||
// Mode line for spawn modes with a single slot (or true single-session mode)
|
||||
if (sessionMax === 1) {
|
||||
const modeText =
|
||||
spawnMode === 'single-session'
|
||||
? 'Single session \u00b7 exits when complete'
|
||||
: spawnMode === 'worktree'
|
||||
? `Capacity: ${sessionActive}/1 \u00b7 New sessions will be created in an isolated worktree`
|
||||
: `Capacity: ${sessionActive}/1 \u00b7 New sessions will be created in the current directory`
|
||||
writeStatus(` ${chalk.dim(modeText)}\n`)
|
||||
}
|
||||
|
||||
// Tool activity line for single-session mode
|
||||
if (
|
||||
sessionMax === 1 &&
|
||||
!isIdle &&
|
||||
lastToolSummary &&
|
||||
Date.now() - lastToolTime < TOOL_DISPLAY_EXPIRY_MS
|
||||
) {
|
||||
writeStatus(` ${chalk.dim(truncatePrompt(lastToolSummary, 60))}\n`)
|
||||
}
|
||||
|
||||
// Blank line separator before footer
|
||||
const url = activeSessionUrl ?? connectUrl
|
||||
if (url) {
|
||||
writeStatus('\n')
|
||||
const footerText = isIdle
|
||||
? buildIdleFooterText(url)
|
||||
: buildActiveFooterText(url)
|
||||
const qrHint = qrVisible
|
||||
? chalk.dim.italic('space to hide QR code')
|
||||
: chalk.dim.italic('space to show QR code')
|
||||
const toggleHint = spawnModeDisplay
|
||||
? chalk.dim.italic(' \u00b7 w to toggle spawn mode')
|
||||
: ''
|
||||
writeStatus(`${chalk.dim(footerText)}\n`)
|
||||
writeStatus(`${qrHint}${toggleHint}\n`)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
printBanner(config: BridgeConfig, environmentId: string): void {
|
||||
cachedIngressUrl = config.sessionIngressUrl
|
||||
cachedEnvironmentId = environmentId
|
||||
connectUrl = buildBridgeConnectUrl(environmentId, cachedIngressUrl)
|
||||
regenerateQr(connectUrl)
|
||||
|
||||
if (verbose) {
|
||||
write(chalk.dim(`Remote Control`) + ` v${MACRO.VERSION}\n`)
|
||||
}
|
||||
if (verbose) {
|
||||
if (config.spawnMode !== 'single-session') {
|
||||
write(chalk.dim(`Spawn mode: `) + `${config.spawnMode}\n`)
|
||||
write(
|
||||
chalk.dim(`Max concurrent sessions: `) + `${config.maxSessions}\n`,
|
||||
)
|
||||
}
|
||||
write(chalk.dim(`Environment ID: `) + `${environmentId}\n`)
|
||||
}
|
||||
if (config.sandbox) {
|
||||
write(chalk.dim(`Sandbox: `) + `${chalk.green('Enabled')}\n`)
|
||||
}
|
||||
write('\n')
|
||||
|
||||
// Start connecting spinner — first updateIdleStatus() will stop it
|
||||
startConnecting()
|
||||
},
|
||||
|
||||
logSessionStart(sessionId: string, prompt: string): void {
|
||||
if (verbose) {
|
||||
const short = truncatePrompt(prompt, 80)
|
||||
printLog(
|
||||
chalk.dim(`[${timestamp()}]`) +
|
||||
` Session started: ${chalk.white(`"${short}"`)} (${chalk.dim(sessionId)})\n`,
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
logSessionComplete(sessionId: string, durationMs: number): void {
|
||||
printLog(
|
||||
chalk.dim(`[${timestamp()}]`) +
|
||||
` Session ${chalk.green('completed')} (${formatDuration(durationMs)}) ${chalk.dim(sessionId)}\n`,
|
||||
)
|
||||
},
|
||||
|
||||
logSessionFailed(sessionId: string, error: string): void {
|
||||
printLog(
|
||||
chalk.dim(`[${timestamp()}]`) +
|
||||
` Session ${chalk.red('failed')}: ${error} ${chalk.dim(sessionId)}\n`,
|
||||
)
|
||||
},
|
||||
|
||||
logStatus(message: string): void {
|
||||
printLog(chalk.dim(`[${timestamp()}]`) + ` ${message}\n`)
|
||||
},
|
||||
|
||||
logVerbose(message: string): void {
|
||||
if (verbose) {
|
||||
printLog(chalk.dim(`[${timestamp()}] ${message}`) + '\n')
|
||||
}
|
||||
},
|
||||
|
||||
logError(message: string): void {
|
||||
printLog(chalk.red(`[${timestamp()}] Error: ${message}`) + '\n')
|
||||
},
|
||||
|
||||
logReconnected(disconnectedMs: number): void {
|
||||
printLog(
|
||||
chalk.dim(`[${timestamp()}]`) +
|
||||
` ${chalk.green('Reconnected')} after ${formatDuration(disconnectedMs)}\n`,
|
||||
)
|
||||
},
|
||||
|
||||
setRepoInfo(repo: string, branchName: string): void {
|
||||
repoName = repo
|
||||
branch = branchName
|
||||
},
|
||||
|
||||
setDebugLogPath(path: string): void {
|
||||
debugLogPath = path
|
||||
},
|
||||
|
||||
updateIdleStatus(): void {
|
||||
stopConnecting()
|
||||
|
||||
currentState = 'idle'
|
||||
currentStateText = 'Ready'
|
||||
lastToolSummary = null
|
||||
lastToolTime = 0
|
||||
activeSessionUrl = null
|
||||
regenerateQr(connectUrl)
|
||||
renderStatusLine()
|
||||
},
|
||||
|
||||
setAttached(sessionId: string): void {
|
||||
stopConnecting()
|
||||
currentState = 'attached'
|
||||
currentStateText = 'Connected'
|
||||
lastToolSummary = null
|
||||
lastToolTime = 0
|
||||
// Multi-session: keep footer/QR on the environment connect URL so users
|
||||
// can spawn more sessions. Per-session links are in the bullet list.
|
||||
if (sessionMax <= 1) {
|
||||
activeSessionUrl = buildBridgeSessionUrl(
|
||||
sessionId,
|
||||
cachedEnvironmentId,
|
||||
cachedIngressUrl,
|
||||
)
|
||||
regenerateQr(activeSessionUrl)
|
||||
}
|
||||
renderStatusLine()
|
||||
},
|
||||
|
||||
updateReconnectingStatus(delayStr: string, elapsedStr: string): void {
|
||||
stopConnecting()
|
||||
clearStatusLines()
|
||||
currentState = 'reconnecting'
|
||||
|
||||
// QR code above the status line
|
||||
if (qrVisible) {
|
||||
for (const line of qrLines) {
|
||||
writeStatus(`${chalk.dim(line)}\n`)
|
||||
}
|
||||
}
|
||||
|
||||
const frame =
|
||||
BRIDGE_SPINNER_FRAMES[connectingTick % BRIDGE_SPINNER_FRAMES.length]!
|
||||
connectingTick++
|
||||
writeStatus(
|
||||
`${chalk.yellow(frame)} ${chalk.yellow('Reconnecting')} ${chalk.dim('\u00b7')} ${chalk.dim(`retrying in ${delayStr}`)} ${chalk.dim('\u00b7')} ${chalk.dim(`disconnected ${elapsedStr}`)}\n`,
|
||||
)
|
||||
},
|
||||
|
||||
updateFailedStatus(error: string): void {
|
||||
stopConnecting()
|
||||
clearStatusLines()
|
||||
currentState = 'failed'
|
||||
|
||||
let suffix = ''
|
||||
if (repoName) {
|
||||
suffix += chalk.dim(' \u00b7 ') + chalk.dim(repoName)
|
||||
}
|
||||
if (branch) {
|
||||
suffix += chalk.dim(' \u00b7 ') + chalk.dim(branch)
|
||||
}
|
||||
|
||||
writeStatus(
|
||||
`${chalk.red(BRIDGE_FAILED_INDICATOR)} ${chalk.red('Remote Control Failed')}${suffix}\n`,
|
||||
)
|
||||
writeStatus(`${chalk.dim(FAILED_FOOTER_TEXT)}\n`)
|
||||
|
||||
if (error) {
|
||||
writeStatus(`${chalk.red(error)}\n`)
|
||||
}
|
||||
},
|
||||
|
||||
updateSessionStatus(
|
||||
_sessionId: string,
|
||||
_elapsed: string,
|
||||
activity: SessionActivity,
|
||||
_trail: string[],
|
||||
): void {
|
||||
// Cache tool activity for the second status line
|
||||
if (activity.type === 'tool_start') {
|
||||
lastToolSummary = activity.summary
|
||||
lastToolTime = Date.now()
|
||||
}
|
||||
renderStatusLine()
|
||||
},
|
||||
|
||||
clearStatus(): void {
|
||||
stopConnecting()
|
||||
clearStatusLines()
|
||||
},
|
||||
|
||||
toggleQr(): void {
|
||||
qrVisible = !qrVisible
|
||||
renderStatusLine()
|
||||
},
|
||||
|
||||
updateSessionCount(active: number, max: number, mode: SpawnMode): void {
|
||||
if (sessionActive === active && sessionMax === max && spawnMode === mode)
|
||||
return
|
||||
sessionActive = active
|
||||
sessionMax = max
|
||||
spawnMode = mode
|
||||
// Don't re-render here — the status ticker calls renderStatusLine
|
||||
// on its own cadence, and the next tick will pick up the new values.
|
||||
},
|
||||
|
||||
setSpawnModeDisplay(mode: 'same-dir' | 'worktree' | null): void {
|
||||
if (spawnModeDisplay === mode) return
|
||||
spawnModeDisplay = mode
|
||||
// Also sync the #21118-added spawnMode so the next render shows correct
|
||||
// mode hint + branch visibility. Don't render here — matches
|
||||
// updateSessionCount: called before printBanner (initial setup) and
|
||||
// again from the `w` handler (which follows with refreshDisplay).
|
||||
if (mode) spawnMode = mode
|
||||
},
|
||||
|
||||
addSession(sessionId: string, url: string): void {
|
||||
sessionDisplayInfo.set(sessionId, { url })
|
||||
},
|
||||
|
||||
updateSessionActivity(sessionId: string, activity: SessionActivity): void {
|
||||
const info = sessionDisplayInfo.get(sessionId)
|
||||
if (!info) return
|
||||
info.activity = activity
|
||||
},
|
||||
|
||||
setSessionTitle(sessionId: string, title: string): void {
|
||||
const info = sessionDisplayInfo.get(sessionId)
|
||||
if (!info) return
|
||||
info.title = title
|
||||
// Guard against reconnecting/failed — renderStatusLine clears then returns
|
||||
// early for those states, which would erase the spinner/error.
|
||||
if (currentState === 'reconnecting' || currentState === 'failed') return
|
||||
if (sessionMax === 1) {
|
||||
// Single-session: show title in the main status line too.
|
||||
currentState = 'titled'
|
||||
currentStateText = truncatePrompt(title, 40)
|
||||
}
|
||||
renderStatusLine()
|
||||
},
|
||||
|
||||
removeSession(sessionId: string): void {
|
||||
sessionDisplayInfo.delete(sessionId)
|
||||
},
|
||||
|
||||
refreshDisplay(): void {
|
||||
// Skip during reconnecting/failed — renderStatusLine clears then returns
|
||||
// early for those states, which would erase the spinner/error.
|
||||
if (currentState === 'reconnecting' || currentState === 'failed') return
|
||||
renderStatusLine()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
58
src/bridge/capacityWake.ts
Normal file
58
src/bridge/capacityWake.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
* Shared capacity-wake primitive for bridge poll loops.
|
||||
*
|
||||
* Both replBridge.ts and bridgeMain.ts need to sleep while "at capacity"
|
||||
* but wake early when either (a) the outer loop signal aborts (shutdown),
|
||||
* or (b) capacity frees up (session done / transport lost). This module
|
||||
* encapsulates the mutable wake-controller + two-signal merger that both
|
||||
* poll loops previously duplicated byte-for-byte.
|
||||
*/
|
||||
|
||||
export type CapacitySignal = { signal: AbortSignal; cleanup: () => void }
|
||||
|
||||
export type CapacityWake = {
|
||||
/**
|
||||
* Create a signal that aborts when either the outer loop signal or the
|
||||
* capacity-wake controller fires. Returns the merged signal and a cleanup
|
||||
* function that removes listeners when the sleep resolves normally
|
||||
* (without abort).
|
||||
*/
|
||||
signal(): CapacitySignal
|
||||
/**
|
||||
* Abort the current at-capacity sleep and arm a fresh controller so the
|
||||
* poll loop immediately re-checks for new work.
|
||||
*/
|
||||
wake(): void
|
||||
}
|
||||
|
||||
export function createCapacityWake(outerSignal: AbortSignal): CapacityWake {
|
||||
let wakeController = new AbortController()
|
||||
|
||||
function wake(): void {
|
||||
wakeController.abort()
|
||||
wakeController = new AbortController()
|
||||
}
|
||||
|
||||
function signal(): CapacitySignal {
|
||||
const merged = new AbortController()
|
||||
const abort = (): void => merged.abort()
|
||||
if (outerSignal.aborted || wakeController.signal.aborted) {
|
||||
merged.abort()
|
||||
return { signal: merged.signal, cleanup: () => {} }
|
||||
}
|
||||
outerSignal.addEventListener('abort', abort, { once: true })
|
||||
const capSig = wakeController.signal
|
||||
capSig.addEventListener('abort', abort, { once: true })
|
||||
return {
|
||||
signal: merged.signal,
|
||||
cleanup: () => {
|
||||
outerSignal.removeEventListener('abort', abort)
|
||||
capSig.removeEventListener('abort', abort)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return { signal, wake }
|
||||
}
|
||||
|
||||
|
||||
170
src/bridge/codeSessionApi.ts
Normal file
170
src/bridge/codeSessionApi.ts
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
/**
|
||||
* Thin HTTP wrappers for the CCR v2 code-session API.
|
||||
*
|
||||
* Separate file from remoteBridgeCore.ts so the SDK /bridge subpath can
|
||||
* export createCodeSession + fetchRemoteCredentials without bundling the
|
||||
* heavy CLI tree (analytics, transport, etc.). Callers supply explicit
|
||||
* accessToken + baseUrl — no implicit auth or config reads.
|
||||
*/
|
||||
|
||||
import axios from 'axios'
|
||||
import { logForDebugging } from '../utils/debug.js'
|
||||
import { errorMessage } from '../utils/errors.js'
|
||||
import { jsonStringify } from '../utils/slowOperations.js'
|
||||
import { extractErrorDetail } from './debugUtils.js'
|
||||
|
||||
const ANTHROPIC_VERSION = '2023-06-01'
|
||||
|
||||
function oauthHeaders(accessToken: string): Record<string, string> {
|
||||
return {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'anthropic-version': ANTHROPIC_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
export async function createCodeSession(
|
||||
baseUrl: string,
|
||||
accessToken: string,
|
||||
title: string,
|
||||
timeoutMs: number,
|
||||
tags?: string[],
|
||||
): Promise<string | null> {
|
||||
const url = `${baseUrl}/v1/code/sessions`
|
||||
let response
|
||||
try {
|
||||
response = await axios.post(
|
||||
url,
|
||||
// bridge: {} is the positive signal for the oneof runner — omitting it
|
||||
// (or sending environment_id: "") now 400s. BridgeRunner is an empty
|
||||
// message today; it's a placeholder for future bridge-specific options.
|
||||
{ title, bridge: {}, ...(tags?.length ? { tags } : {}) },
|
||||
{
|
||||
headers: oauthHeaders(accessToken),
|
||||
timeout: timeoutMs,
|
||||
validateStatus: s => s < 500,
|
||||
},
|
||||
)
|
||||
} catch (err: unknown) {
|
||||
logForDebugging(
|
||||
`[code-session] Session create request failed: ${errorMessage(err)}`,
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
if (response.status !== 200 && response.status !== 201) {
|
||||
const detail = extractErrorDetail(response.data)
|
||||
logForDebugging(
|
||||
`[code-session] Session create failed ${response.status}${detail ? `: ${detail}` : ''}`,
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const data: unknown = response.data
|
||||
if (
|
||||
!data ||
|
||||
typeof data !== 'object' ||
|
||||
!('session' in data) ||
|
||||
!data.session ||
|
||||
typeof data.session !== 'object' ||
|
||||
!('id' in data.session) ||
|
||||
typeof data.session.id !== 'string' ||
|
||||
!data.session.id.startsWith('cse_')
|
||||
) {
|
||||
logForDebugging(
|
||||
`[code-session] No session.id (cse_*) in response: ${jsonStringify(data).slice(0, 200)}`,
|
||||
)
|
||||
return null
|
||||
}
|
||||
return data.session.id
|
||||
}
|
||||
|
||||
/**
|
||||
* Credentials from POST /bridge. JWT is opaque — do not decode.
|
||||
* Each /bridge call bumps worker_epoch server-side (it IS the register).
|
||||
*/
|
||||
export type RemoteCredentials = {
|
||||
worker_jwt: string
|
||||
api_base_url: string
|
||||
expires_in: number
|
||||
worker_epoch: number
|
||||
}
|
||||
|
||||
export async function fetchRemoteCredentials(
|
||||
sessionId: string,
|
||||
baseUrl: string,
|
||||
accessToken: string,
|
||||
timeoutMs: number,
|
||||
trustedDeviceToken?: string,
|
||||
): Promise<RemoteCredentials | null> {
|
||||
const url = `${baseUrl}/v1/code/sessions/${sessionId}/bridge`
|
||||
const headers = oauthHeaders(accessToken)
|
||||
if (trustedDeviceToken) {
|
||||
headers['X-Trusted-Device-Token'] = trustedDeviceToken
|
||||
}
|
||||
let response
|
||||
try {
|
||||
response = await axios.post(
|
||||
url,
|
||||
{},
|
||||
{
|
||||
headers,
|
||||
timeout: timeoutMs,
|
||||
validateStatus: s => s < 500,
|
||||
},
|
||||
)
|
||||
} catch (err: unknown) {
|
||||
logForDebugging(
|
||||
`[code-session] /bridge request failed: ${errorMessage(err)}`,
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
if (response.status !== 200) {
|
||||
const detail = extractErrorDetail(response.data)
|
||||
logForDebugging(
|
||||
`[code-session] /bridge failed ${response.status}${detail ? `: ${detail}` : ''}`,
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const data: unknown = response.data
|
||||
if (
|
||||
data === null ||
|
||||
typeof data !== 'object' ||
|
||||
!('worker_jwt' in data) ||
|
||||
typeof data.worker_jwt !== 'string' ||
|
||||
!('expires_in' in data) ||
|
||||
typeof data.expires_in !== 'number' ||
|
||||
!('api_base_url' in data) ||
|
||||
typeof data.api_base_url !== 'string' ||
|
||||
!('worker_epoch' in data)
|
||||
) {
|
||||
logForDebugging(
|
||||
`[code-session] /bridge response malformed (need worker_jwt, expires_in, api_base_url, worker_epoch): ${jsonStringify(data).slice(0, 200)}`,
|
||||
)
|
||||
return null
|
||||
}
|
||||
// protojson serializes int64 as a string to avoid JS precision loss;
|
||||
// Go may also return a number depending on encoder settings.
|
||||
const rawEpoch = data.worker_epoch
|
||||
const epoch = typeof rawEpoch === 'string' ? Number(rawEpoch) : rawEpoch
|
||||
if (
|
||||
typeof epoch !== 'number' ||
|
||||
!Number.isFinite(epoch) ||
|
||||
!Number.isSafeInteger(epoch)
|
||||
) {
|
||||
logForDebugging(
|
||||
`[code-session] /bridge worker_epoch invalid: ${jsonStringify(rawEpoch)}`,
|
||||
)
|
||||
return null
|
||||
}
|
||||
return {
|
||||
worker_jwt: data.worker_jwt,
|
||||
api_base_url: data.api_base_url,
|
||||
expires_in: data.expires_in,
|
||||
worker_epoch: epoch,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
386
src/bridge/createSession.ts
Normal file
386
src/bridge/createSession.ts
Normal file
|
|
@ -0,0 +1,386 @@
|
|||
import type { SDKMessage } from '../entrypoints/agentSdkTypes.js'
|
||||
import { logForDebugging } from '../utils/debug.js'
|
||||
import { errorMessage } from '../utils/errors.js'
|
||||
import { extractErrorDetail } from './debugUtils.js'
|
||||
import { toCompatSessionId } from './sessionIdCompat.js'
|
||||
|
||||
type GitSource = {
|
||||
type: 'git_repository'
|
||||
url: string
|
||||
revision?: string
|
||||
}
|
||||
|
||||
type GitOutcome = {
|
||||
type: 'git_repository'
|
||||
git_info: { type: 'github'; repo: string; branches: string[] }
|
||||
}
|
||||
|
||||
// Events must be wrapped in { type: 'event', data: <sdk_message> } for the
|
||||
// POST /v1/sessions endpoint (discriminated union format).
|
||||
type SessionEvent = {
|
||||
type: 'event'
|
||||
data: SDKMessage
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a session on a bridge environment via POST /v1/sessions.
|
||||
*
|
||||
* Used by both `claude remote-control` (empty session so the user has somewhere to
|
||||
* type immediately) and `/remote-control` (session pre-populated with conversation
|
||||
* history).
|
||||
*
|
||||
* Returns the session ID on success, or null if creation fails (non-fatal).
|
||||
*/
|
||||
export async function createBridgeSession({
|
||||
environmentId,
|
||||
title,
|
||||
events,
|
||||
gitRepoUrl,
|
||||
branch,
|
||||
signal,
|
||||
baseUrl: baseUrlOverride,
|
||||
getAccessToken,
|
||||
permissionMode,
|
||||
}: {
|
||||
environmentId: string
|
||||
title?: string
|
||||
events: SessionEvent[]
|
||||
gitRepoUrl: string | null
|
||||
branch: string
|
||||
signal: AbortSignal
|
||||
baseUrl?: string
|
||||
getAccessToken?: () => string | undefined
|
||||
permissionMode?: string
|
||||
}): Promise<string | null> {
|
||||
const { getClaudeAIOAuthTokens } = await import('../utils/auth.js')
|
||||
const { getOrganizationUUID } = await import('../services/oauth/client.js')
|
||||
const { getOauthConfig } = await import('../constants/oauth.js')
|
||||
const { getOAuthHeaders } = await import('../utils/teleport/api.js')
|
||||
const { parseGitHubRepository } = await import('../utils/detectRepository.js')
|
||||
const { getDefaultBranch } = await import('../utils/git.js')
|
||||
const { getMainLoopModel } = await import('../utils/model/model.js')
|
||||
const { default: axios } = await import('axios')
|
||||
|
||||
const accessToken =
|
||||
getAccessToken?.() ?? getClaudeAIOAuthTokens()?.accessToken
|
||||
if (!accessToken) {
|
||||
logForDebugging('[bridge] No access token for session creation')
|
||||
return null
|
||||
}
|
||||
|
||||
const orgUUID = await getOrganizationUUID()
|
||||
if (!orgUUID) {
|
||||
logForDebugging('[bridge] No org UUID for session creation')
|
||||
return null
|
||||
}
|
||||
|
||||
// Build git source and outcome context
|
||||
let gitSource: GitSource | null = null
|
||||
let gitOutcome: GitOutcome | null = null
|
||||
|
||||
if (gitRepoUrl) {
|
||||
const { parseGitRemote } = await import('../utils/detectRepository.js')
|
||||
const parsed = parseGitRemote(gitRepoUrl)
|
||||
if (parsed) {
|
||||
const { host, owner, name } = parsed
|
||||
const revision = branch || (await getDefaultBranch()) || undefined
|
||||
gitSource = {
|
||||
type: 'git_repository',
|
||||
url: `https://${host}/${owner}/${name}`,
|
||||
revision,
|
||||
}
|
||||
gitOutcome = {
|
||||
type: 'git_repository',
|
||||
git_info: {
|
||||
type: 'github',
|
||||
repo: `${owner}/${name}`,
|
||||
branches: [`claude/${branch || 'task'}`],
|
||||
},
|
||||
}
|
||||
} else {
|
||||
// Fallback: try parseGitHubRepository for owner/repo format
|
||||
const ownerRepo = parseGitHubRepository(gitRepoUrl)
|
||||
if (ownerRepo) {
|
||||
const [owner, name] = ownerRepo.split('/')
|
||||
if (owner && name) {
|
||||
const revision = branch || (await getDefaultBranch()) || undefined
|
||||
gitSource = {
|
||||
type: 'git_repository',
|
||||
url: `https://github.com/${owner}/${name}`,
|
||||
revision,
|
||||
}
|
||||
gitOutcome = {
|
||||
type: 'git_repository',
|
||||
git_info: {
|
||||
type: 'github',
|
||||
repo: `${owner}/${name}`,
|
||||
branches: [`claude/${branch || 'task'}`],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const requestBody = {
|
||||
...(title !== undefined && { title }),
|
||||
events,
|
||||
session_context: {
|
||||
sources: gitSource ? [gitSource] : [],
|
||||
outcomes: gitOutcome ? [gitOutcome] : [],
|
||||
model: getMainLoopModel(),
|
||||
},
|
||||
environment_id: environmentId,
|
||||
source: 'remote-control',
|
||||
...(permissionMode && { permission_mode: permissionMode }),
|
||||
}
|
||||
|
||||
const headers = {
|
||||
...getOAuthHeaders(accessToken),
|
||||
'anthropic-beta': 'ccr-byoc-2025-07-29',
|
||||
'x-organization-uuid': orgUUID,
|
||||
}
|
||||
|
||||
const url = `${baseUrlOverride ?? getOauthConfig().BASE_API_URL}/v1/sessions`
|
||||
let response
|
||||
try {
|
||||
response = await axios.post(url, requestBody, {
|
||||
headers,
|
||||
signal,
|
||||
validateStatus: s => s < 500,
|
||||
})
|
||||
} catch (err: unknown) {
|
||||
logForDebugging(
|
||||
`[bridge] Session creation request failed: ${errorMessage(err)}`,
|
||||
)
|
||||
return null
|
||||
}
|
||||
const isSuccess = response.status === 200 || response.status === 201
|
||||
|
||||
if (!isSuccess) {
|
||||
const detail = extractErrorDetail(response.data)
|
||||
logForDebugging(
|
||||
`[bridge] Session creation failed with status ${response.status}${detail ? `: ${detail}` : ''}`,
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const sessionData: unknown = response.data
|
||||
if (
|
||||
!sessionData ||
|
||||
typeof sessionData !== 'object' ||
|
||||
!('id' in sessionData) ||
|
||||
typeof sessionData.id !== 'string'
|
||||
) {
|
||||
logForDebugging('[bridge] No session ID in response')
|
||||
return null
|
||||
}
|
||||
|
||||
return sessionData.id
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a bridge session via GET /v1/sessions/{id}.
|
||||
*
|
||||
* Returns the session's environment_id (for `--session-id` resume) and title.
|
||||
* Uses the same org-scoped headers as create/archive — the environments-level
|
||||
* client in bridgeApi.ts uses a different beta header and no org UUID, which
|
||||
* makes the Sessions API return 404.
|
||||
*/
|
||||
export async function getBridgeSession(
|
||||
sessionId: string,
|
||||
opts?: { baseUrl?: string; getAccessToken?: () => string | undefined },
|
||||
): Promise<{ environment_id?: string; title?: string } | null> {
|
||||
const { getClaudeAIOAuthTokens } = await import('../utils/auth.js')
|
||||
const { getOrganizationUUID } = await import('../services/oauth/client.js')
|
||||
const { getOauthConfig } = await import('../constants/oauth.js')
|
||||
const { getOAuthHeaders } = await import('../utils/teleport/api.js')
|
||||
const { default: axios } = await import('axios')
|
||||
|
||||
const accessToken =
|
||||
opts?.getAccessToken?.() ?? getClaudeAIOAuthTokens()?.accessToken
|
||||
if (!accessToken) {
|
||||
logForDebugging('[bridge] No access token for session fetch')
|
||||
return null
|
||||
}
|
||||
|
||||
const orgUUID = await getOrganizationUUID()
|
||||
if (!orgUUID) {
|
||||
logForDebugging('[bridge] No org UUID for session fetch')
|
||||
return null
|
||||
}
|
||||
|
||||
const headers = {
|
||||
...getOAuthHeaders(accessToken),
|
||||
'anthropic-beta': 'ccr-byoc-2025-07-29',
|
||||
'x-organization-uuid': orgUUID,
|
||||
}
|
||||
|
||||
const url = `${opts?.baseUrl ?? getOauthConfig().BASE_API_URL}/v1/sessions/${sessionId}`
|
||||
logForDebugging(`[bridge] Fetching session ${sessionId}`)
|
||||
|
||||
let response
|
||||
try {
|
||||
response = await axios.get<{ environment_id?: string; title?: string }>(
|
||||
url,
|
||||
{ headers, timeout: 10_000, validateStatus: s => s < 500 },
|
||||
)
|
||||
} catch (err: unknown) {
|
||||
logForDebugging(
|
||||
`[bridge] Session fetch request failed: ${errorMessage(err)}`,
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
if (response.status !== 200) {
|
||||
const detail = extractErrorDetail(response.data)
|
||||
logForDebugging(
|
||||
`[bridge] Session fetch failed with status ${response.status}${detail ? `: ${detail}` : ''}`,
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Archive a bridge session via POST /v1/sessions/{id}/archive.
|
||||
*
|
||||
* The CCR server never auto-archives sessions — archival is always an
|
||||
* explicit client action. Both `claude remote-control` (standalone bridge) and the
|
||||
* always-on `/remote-control` REPL bridge call this during shutdown to archive any
|
||||
* sessions that are still alive.
|
||||
*
|
||||
* The archive endpoint accepts sessions in any status (running, idle,
|
||||
* requires_action, pending) and returns 409 if already archived, making
|
||||
* it safe to call even if the server-side runner already archived the
|
||||
* session.
|
||||
*
|
||||
* Callers must handle errors — this function has no try/catch; 5xx,
|
||||
* timeouts, and network errors throw. Archival is best-effort during
|
||||
* cleanup; call sites wrap with .catch().
|
||||
*/
|
||||
export async function archiveBridgeSession(
|
||||
sessionId: string,
|
||||
opts?: {
|
||||
baseUrl?: string
|
||||
getAccessToken?: () => string | undefined
|
||||
timeoutMs?: number
|
||||
},
|
||||
): Promise<void> {
|
||||
const { getClaudeAIOAuthTokens } = await import('../utils/auth.js')
|
||||
const { getOrganizationUUID } = await import('../services/oauth/client.js')
|
||||
const { getOauthConfig } = await import('../constants/oauth.js')
|
||||
const { getOAuthHeaders } = await import('../utils/teleport/api.js')
|
||||
const { default: axios } = await import('axios')
|
||||
|
||||
const accessToken =
|
||||
opts?.getAccessToken?.() ?? getClaudeAIOAuthTokens()?.accessToken
|
||||
if (!accessToken) {
|
||||
logForDebugging('[bridge] No access token for session archive')
|
||||
return
|
||||
}
|
||||
|
||||
const orgUUID = await getOrganizationUUID()
|
||||
if (!orgUUID) {
|
||||
logForDebugging('[bridge] No org UUID for session archive')
|
||||
return
|
||||
}
|
||||
|
||||
const headers = {
|
||||
...getOAuthHeaders(accessToken),
|
||||
'anthropic-beta': 'ccr-byoc-2025-07-29',
|
||||
'x-organization-uuid': orgUUID,
|
||||
}
|
||||
|
||||
const url = `${opts?.baseUrl ?? getOauthConfig().BASE_API_URL}/v1/sessions/${sessionId}/archive`
|
||||
logForDebugging(`[bridge] Archiving session ${sessionId}`)
|
||||
|
||||
const response = await axios.post(
|
||||
url,
|
||||
{},
|
||||
{
|
||||
headers,
|
||||
timeout: opts?.timeoutMs ?? 10_000,
|
||||
validateStatus: s => s < 500,
|
||||
},
|
||||
)
|
||||
|
||||
if (response.status === 200) {
|
||||
logForDebugging(`[bridge] Session ${sessionId} archived successfully`)
|
||||
} else {
|
||||
const detail = extractErrorDetail(response.data)
|
||||
logForDebugging(
|
||||
`[bridge] Session archive failed with status ${response.status}${detail ? `: ${detail}` : ''}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the title of a bridge session via PATCH /v1/sessions/{id}.
|
||||
*
|
||||
* Called when the user renames a session via /rename while a bridge
|
||||
* connection is active, so the title stays in sync on claude.ai/code.
|
||||
*
|
||||
* Errors are swallowed — title sync is best-effort.
|
||||
*/
|
||||
export async function updateBridgeSessionTitle(
|
||||
sessionId: string,
|
||||
title: string,
|
||||
opts?: { baseUrl?: string; getAccessToken?: () => string | undefined },
|
||||
): Promise<void> {
|
||||
const { getClaudeAIOAuthTokens } = await import('../utils/auth.js')
|
||||
const { getOrganizationUUID } = await import('../services/oauth/client.js')
|
||||
const { getOauthConfig } = await import('../constants/oauth.js')
|
||||
const { getOAuthHeaders } = await import('../utils/teleport/api.js')
|
||||
const { default: axios } = await import('axios')
|
||||
|
||||
const accessToken =
|
||||
opts?.getAccessToken?.() ?? getClaudeAIOAuthTokens()?.accessToken
|
||||
if (!accessToken) {
|
||||
logForDebugging('[bridge] No access token for session title update')
|
||||
return
|
||||
}
|
||||
|
||||
const orgUUID = await getOrganizationUUID()
|
||||
if (!orgUUID) {
|
||||
logForDebugging('[bridge] No org UUID for session title update')
|
||||
return
|
||||
}
|
||||
|
||||
const headers = {
|
||||
...getOAuthHeaders(accessToken),
|
||||
'anthropic-beta': 'ccr-byoc-2025-07-29',
|
||||
'x-organization-uuid': orgUUID,
|
||||
}
|
||||
|
||||
// Compat gateway only accepts session_* (compat/convert.go:27). v2 callers
|
||||
// pass raw cse_*; retag here so all callers can pass whatever they hold.
|
||||
// Idempotent for v1's session_* and bridgeMain's pre-converted compatSessionId.
|
||||
const compatId = toCompatSessionId(sessionId)
|
||||
const url = `${opts?.baseUrl ?? getOauthConfig().BASE_API_URL}/v1/sessions/${compatId}`
|
||||
logForDebugging(`[bridge] Updating session title: ${compatId} → ${title}`)
|
||||
|
||||
try {
|
||||
const response = await axios.patch(
|
||||
url,
|
||||
{ title },
|
||||
{ headers, timeout: 10_000, validateStatus: s => s < 500 },
|
||||
)
|
||||
|
||||
if (response.status === 200) {
|
||||
logForDebugging(`[bridge] Session title updated successfully`)
|
||||
} else {
|
||||
const detail = extractErrorDetail(response.data)
|
||||
logForDebugging(
|
||||
`[bridge] Session title update failed with status ${response.status}${detail ? `: ${detail}` : ''}`,
|
||||
)
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
logForDebugging(
|
||||
`[bridge] Session title update request failed: ${errorMessage(err)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
143
src/bridge/debugUtils.ts
Normal file
143
src/bridge/debugUtils.ts
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from '../services/analytics/index.js'
|
||||
import { logForDebugging } from '../utils/debug.js'
|
||||
import { errorMessage } from '../utils/errors.js'
|
||||
import { jsonStringify } from '../utils/slowOperations.js'
|
||||
|
||||
const DEBUG_MSG_LIMIT = 2000
|
||||
|
||||
const SECRET_FIELD_NAMES = [
|
||||
'session_ingress_token',
|
||||
'environment_secret',
|
||||
'access_token',
|
||||
'secret',
|
||||
'token',
|
||||
]
|
||||
|
||||
const SECRET_PATTERN = new RegExp(
|
||||
`"(${SECRET_FIELD_NAMES.join('|')})"\\s*:\\s*"([^"]*)"`,
|
||||
'g',
|
||||
)
|
||||
|
||||
const REDACT_MIN_LENGTH = 16
|
||||
|
||||
export function redactSecrets(s: string): string {
|
||||
return s.replace(SECRET_PATTERN, (_match, field: string, value: string) => {
|
||||
if (value.length < REDACT_MIN_LENGTH) {
|
||||
return `"${field}":"[REDACTED]"`
|
||||
}
|
||||
const redacted = `${value.slice(0, 8)}...${value.slice(-4)}`
|
||||
return `"${field}":"${redacted}"`
|
||||
})
|
||||
}
|
||||
|
||||
/** Truncate a string for debug logging, collapsing newlines. */
|
||||
export function debugTruncate(s: string): string {
|
||||
const flat = s.replace(/\n/g, '\\n')
|
||||
if (flat.length <= DEBUG_MSG_LIMIT) {
|
||||
return flat
|
||||
}
|
||||
return flat.slice(0, DEBUG_MSG_LIMIT) + `... (${flat.length} chars)`
|
||||
}
|
||||
|
||||
/** Truncate a JSON-serializable value for debug logging. */
|
||||
export function debugBody(data: unknown): string {
|
||||
const raw = typeof data === 'string' ? data : jsonStringify(data)
|
||||
const s = redactSecrets(raw)
|
||||
if (s.length <= DEBUG_MSG_LIMIT) {
|
||||
return s
|
||||
}
|
||||
return s.slice(0, DEBUG_MSG_LIMIT) + `... (${s.length} chars)`
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a descriptive error message from an axios error (or any error).
|
||||
* For HTTP errors, appends the server's response body message if available,
|
||||
* since axios's default message only includes the status code.
|
||||
*/
|
||||
export function describeAxiosError(err: unknown): string {
|
||||
const msg = errorMessage(err)
|
||||
if (err && typeof err === 'object' && 'response' in err) {
|
||||
const response = (err as { response?: { data?: unknown } }).response
|
||||
if (response?.data && typeof response.data === 'object') {
|
||||
const data = response.data as Record<string, unknown>
|
||||
const detail =
|
||||
typeof data.message === 'string'
|
||||
? data.message
|
||||
: typeof data.error === 'object' &&
|
||||
data.error &&
|
||||
'message' in data.error &&
|
||||
typeof (data.error as Record<string, unknown>).message ===
|
||||
'string'
|
||||
? (data.error as Record<string, unknown>).message
|
||||
: undefined
|
||||
if (detail) {
|
||||
return `${msg}: ${detail}`
|
||||
}
|
||||
}
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the HTTP status code from an axios error, if present.
|
||||
* Returns undefined for non-HTTP errors (e.g. network failures).
|
||||
*/
|
||||
export function extractHttpStatus(err: unknown): number | undefined {
|
||||
if (
|
||||
err &&
|
||||
typeof err === 'object' &&
|
||||
'response' in err &&
|
||||
(err as { response?: { status?: unknown } }).response &&
|
||||
typeof (err as { response: { status?: unknown } }).response.status ===
|
||||
'number'
|
||||
) {
|
||||
return (err as { response: { status: number } }).response.status
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull a human-readable message out of an API error response body.
|
||||
* Checks `data.message` first, then `data.error.message`.
|
||||
*/
|
||||
export function extractErrorDetail(data: unknown): string | undefined {
|
||||
if (!data || typeof data !== 'object') return undefined
|
||||
if ('message' in data && typeof data.message === 'string') {
|
||||
return data.message
|
||||
}
|
||||
if (
|
||||
'error' in data &&
|
||||
data.error !== null &&
|
||||
typeof data.error === 'object' &&
|
||||
'message' in data.error &&
|
||||
typeof data.error.message === 'string'
|
||||
) {
|
||||
return data.error.message
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a bridge init skip — debug message + `tengu_bridge_repl_skipped`
|
||||
* analytics event. Centralizes the event name and the AnalyticsMetadata
|
||||
* cast so call sites don't each repeat the 5-line boilerplate.
|
||||
*/
|
||||
export function logBridgeSkip(
|
||||
reason: string,
|
||||
debugMsg?: string,
|
||||
v2?: boolean,
|
||||
): void {
|
||||
if (debugMsg) {
|
||||
logForDebugging(debugMsg)
|
||||
}
|
||||
logEvent('tengu_bridge_repl_skipped', {
|
||||
reason:
|
||||
reason as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
...(v2 !== undefined && { v2 }),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
167
src/bridge/envLessBridgeConfig.ts
Normal file
167
src/bridge/envLessBridgeConfig.ts
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
import { z } from 'zod/v4'
|
||||
import { getFeatureValue_DEPRECATED } from '../services/analytics/growthbook.js'
|
||||
import { lazySchema } from '../utils/lazySchema.js'
|
||||
import { lt } from '../utils/semver.js'
|
||||
import { isEnvLessBridgeEnabled } from './bridgeEnabled.js'
|
||||
|
||||
export type EnvLessBridgeConfig = {
|
||||
// withRetry — init-phase backoff (createSession, POST /bridge, recovery /bridge)
|
||||
init_retry_max_attempts: number
|
||||
init_retry_base_delay_ms: number
|
||||
init_retry_jitter_fraction: number
|
||||
init_retry_max_delay_ms: number
|
||||
// axios timeout for POST /sessions, POST /bridge, POST /archive
|
||||
http_timeout_ms: number
|
||||
// BoundedUUIDSet ring size (echo + re-delivery dedup)
|
||||
uuid_dedup_buffer_size: number
|
||||
// CCRClient worker heartbeat cadence. Server TTL is 60s — 20s gives 3× margin.
|
||||
heartbeat_interval_ms: number
|
||||
// ±fraction of interval — per-beat jitter to spread fleet load.
|
||||
heartbeat_jitter_fraction: number
|
||||
// Fire proactive JWT refresh this long before expires_in. Larger buffer =
|
||||
// more frequent refresh (refresh cadence ≈ expires_in - buffer).
|
||||
token_refresh_buffer_ms: number
|
||||
// Archive POST timeout in teardown(). Distinct from http_timeout_ms because
|
||||
// gracefulShutdown races runCleanupFunctions() against a 2s cap — a 10s
|
||||
// axios timeout on a slow/stalled archive burns the whole budget on a
|
||||
// request that forceExit will kill anyway.
|
||||
teardown_archive_timeout_ms: number
|
||||
// Deadline for onConnect after transport.connect(). If neither onConnect
|
||||
// nor onClose fires before this, emit tengu_bridge_repl_connect_timeout
|
||||
// — the only telemetry for the ~1% of sessions that emit `started` then
|
||||
// go silent (no error, no event, just nothing).
|
||||
connect_timeout_ms: number
|
||||
// Semver floor for the env-less bridge path. Separate from the v1
|
||||
// tengu_bridge_min_version config so a v2-specific bug can force upgrades
|
||||
// without blocking v1 (env-based) clients, and vice versa.
|
||||
min_version: string
|
||||
// When true, tell users their claude.ai app may be too old to see v2
|
||||
// sessions — lets us roll the v2 bridge before the app ships the new
|
||||
// session-list query.
|
||||
should_show_app_upgrade_message: boolean
|
||||
}
|
||||
|
||||
export const DEFAULT_ENV_LESS_BRIDGE_CONFIG: EnvLessBridgeConfig = {
|
||||
init_retry_max_attempts: 3,
|
||||
init_retry_base_delay_ms: 500,
|
||||
init_retry_jitter_fraction: 0.25,
|
||||
init_retry_max_delay_ms: 4000,
|
||||
http_timeout_ms: 10_000,
|
||||
uuid_dedup_buffer_size: 2000,
|
||||
heartbeat_interval_ms: 20_000,
|
||||
heartbeat_jitter_fraction: 0.1,
|
||||
token_refresh_buffer_ms: 300_000,
|
||||
teardown_archive_timeout_ms: 1500,
|
||||
connect_timeout_ms: 15_000,
|
||||
min_version: '0.0.0',
|
||||
should_show_app_upgrade_message: false,
|
||||
}
|
||||
|
||||
// Floors reject the whole object on violation (fall back to DEFAULT) rather
|
||||
// than partially trusting — same defense-in-depth as pollConfig.ts.
|
||||
const envLessBridgeConfigSchema = lazySchema(() =>
|
||||
z.object({
|
||||
init_retry_max_attempts: z.number().int().min(1).max(10).default(3),
|
||||
init_retry_base_delay_ms: z.number().int().min(100).default(500),
|
||||
init_retry_jitter_fraction: z.number().min(0).max(1).default(0.25),
|
||||
init_retry_max_delay_ms: z.number().int().min(500).default(4000),
|
||||
http_timeout_ms: z.number().int().min(2000).default(10_000),
|
||||
uuid_dedup_buffer_size: z.number().int().min(100).max(50_000).default(2000),
|
||||
// Server TTL is 60s. Floor 5s prevents thrash; cap 30s keeps ≥2× margin.
|
||||
heartbeat_interval_ms: z
|
||||
.number()
|
||||
.int()
|
||||
.min(5000)
|
||||
.max(30_000)
|
||||
.default(20_000),
|
||||
// ±fraction per beat. Cap 0.5: at max interval (30s) × 1.5 = 45s worst case,
|
||||
// still under the 60s TTL.
|
||||
heartbeat_jitter_fraction: z.number().min(0).max(0.5).default(0.1),
|
||||
// Floor 30s prevents tight-looping. Cap 30min rejects buffer-vs-delay
|
||||
// semantic inversion: ops entering expires_in-5min (the *delay until
|
||||
// refresh*) instead of 5min (the *buffer before expiry*) yields
|
||||
// delayMs = expires_in - buffer ≈ 5min instead of ≈4h. Both are positive
|
||||
// durations so .min() alone can't distinguish; .max() catches the
|
||||
// inverted value since buffer ≥ 30min is nonsensical for a multi-hour JWT.
|
||||
token_refresh_buffer_ms: z
|
||||
.number()
|
||||
.int()
|
||||
.min(30_000)
|
||||
.max(1_800_000)
|
||||
.default(300_000),
|
||||
// Cap 2000 keeps this under gracefulShutdown's 2s cleanup race — a higher
|
||||
// timeout just lies to axios since forceExit kills the socket regardless.
|
||||
teardown_archive_timeout_ms: z
|
||||
.number()
|
||||
.int()
|
||||
.min(500)
|
||||
.max(2000)
|
||||
.default(1500),
|
||||
// Observed p99 connect is ~2-3s; 15s is ~5× headroom. Floor 5s bounds
|
||||
// false-positive rate under transient slowness; cap 60s bounds how long
|
||||
// a truly-stalled session stays dark.
|
||||
connect_timeout_ms: z.number().int().min(5_000).max(60_000).default(15_000),
|
||||
min_version: z
|
||||
.string()
|
||||
.refine(v => {
|
||||
try {
|
||||
lt(v, '0.0.0')
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
.default('0.0.0'),
|
||||
should_show_app_upgrade_message: z.boolean().default(false),
|
||||
}),
|
||||
)
|
||||
|
||||
/**
|
||||
* Fetch the env-less bridge timing config from GrowthBook. Read once per
|
||||
* initEnvLessBridgeCore call — config is fixed for the lifetime of a bridge
|
||||
* session.
|
||||
*
|
||||
* Uses the blocking getter (not _CACHED_MAY_BE_STALE) because /remote-control
|
||||
* runs well after GrowthBook init — initializeGrowthBook() resolves instantly,
|
||||
* so there's no startup penalty, and we get the fresh in-memory remoteEval
|
||||
* value instead of the stale-on-first-read disk cache. The _DEPRECATED suffix
|
||||
* warns against startup-path usage, which this isn't.
|
||||
*/
|
||||
export async function getEnvLessBridgeConfig(): Promise<EnvLessBridgeConfig> {
|
||||
const raw = await getFeatureValue_DEPRECATED<unknown>(
|
||||
'tengu_bridge_repl_v2_config',
|
||||
DEFAULT_ENV_LESS_BRIDGE_CONFIG,
|
||||
)
|
||||
const parsed = envLessBridgeConfigSchema().safeParse(raw)
|
||||
return parsed.success ? parsed.data : DEFAULT_ENV_LESS_BRIDGE_CONFIG
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an error message if the current CLI version is below the minimum
|
||||
* required for the env-less (v2) bridge path, or null if the version is fine.
|
||||
*
|
||||
* v2 analogue of checkBridgeMinVersion() — reads from tengu_bridge_repl_v2_config
|
||||
* instead of tengu_bridge_min_version so the two implementations can enforce
|
||||
* independent floors.
|
||||
*/
|
||||
export async function checkEnvLessBridgeMinVersion(): Promise<string | null> {
|
||||
const cfg = await getEnvLessBridgeConfig()
|
||||
if (cfg.min_version && lt(MACRO.VERSION, cfg.min_version)) {
|
||||
return `Your version of Claude Code (${MACRO.VERSION}) is too old for Remote Control.\nVersion ${cfg.min_version} or higher is required. Run \`claude update\` to update.`
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to nudge users toward upgrading their claude.ai app when a
|
||||
* Remote Control session starts. True only when the v2 bridge is active
|
||||
* AND the should_show_app_upgrade_message config bit is set — lets us
|
||||
* roll the v2 bridge before the app ships the new session-list query.
|
||||
*/
|
||||
export async function shouldShowAppUpgradeMessage(): Promise<boolean> {
|
||||
if (!isEnvLessBridgeEnabled()) return false
|
||||
const cfg = await getEnvLessBridgeConfig()
|
||||
return cfg.should_show_app_upgrade_message
|
||||
}
|
||||
|
||||
|
||||
73
src/bridge/flushGate.ts
Normal file
73
src/bridge/flushGate.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
/**
|
||||
* State machine for gating message writes during an initial flush.
|
||||
*
|
||||
* When a bridge session starts, historical messages are flushed to the
|
||||
* server via a single HTTP POST. During that flush, new messages must
|
||||
* be queued to prevent them from arriving at the server interleaved
|
||||
* with the historical messages.
|
||||
*
|
||||
* Lifecycle:
|
||||
* start() → enqueue() returns true, items are queued
|
||||
* end() → returns queued items for draining, enqueue() returns false
|
||||
* drop() → discards queued items (permanent transport close)
|
||||
* deactivate() → clears active flag without dropping items
|
||||
* (transport replacement — new transport will drain)
|
||||
*/
|
||||
export class FlushGate<T> {
|
||||
private _active = false
|
||||
private _pending: T[] = []
|
||||
|
||||
get active(): boolean {
|
||||
return this._active
|
||||
}
|
||||
|
||||
get pendingCount(): number {
|
||||
return this._pending.length
|
||||
}
|
||||
|
||||
/** Mark flush as in-progress. enqueue() will start queuing items. */
|
||||
start(): void {
|
||||
this._active = true
|
||||
}
|
||||
|
||||
/**
|
||||
* End the flush and return any queued items for draining.
|
||||
* Caller is responsible for sending the returned items.
|
||||
*/
|
||||
end(): T[] {
|
||||
this._active = false
|
||||
return this._pending.splice(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* If flush is active, queue the items and return true.
|
||||
* If flush is not active, return false (caller should send directly).
|
||||
*/
|
||||
enqueue(...items: T[]): boolean {
|
||||
if (!this._active) return false
|
||||
this._pending.push(...items)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Discard all queued items (permanent transport close).
|
||||
* Returns the number of items dropped.
|
||||
*/
|
||||
drop(): number {
|
||||
this._active = false
|
||||
const count = this._pending.length
|
||||
this._pending.length = 0
|
||||
return count
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the active flag without dropping queued items.
|
||||
* Used when the transport is replaced (onWorkReceived) — the new
|
||||
* transport's flush will drain the pending items.
|
||||
*/
|
||||
deactivate(): void {
|
||||
this._active = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
177
src/bridge/inboundAttachments.ts
Normal file
177
src/bridge/inboundAttachments.ts
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
/**
|
||||
* Resolve file_uuid attachments on inbound bridge user messages.
|
||||
*
|
||||
* Web composer uploads via cookie-authed /api/{org}/upload, sends file_uuid
|
||||
* alongside the message. Here we fetch each via GET /api/oauth/files/{uuid}/content
|
||||
* (oauth-authed, same store), write to ~/.claude/uploads/{sessionId}/, and
|
||||
* return @path refs to prepend. Claude's Read tool takes it from there.
|
||||
*
|
||||
* Best-effort: any failure (no token, network, non-2xx, disk) logs debug and
|
||||
* skips that attachment. The message still reaches Claude, just without @path.
|
||||
*/
|
||||
|
||||
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs'
|
||||
import axios from 'axios'
|
||||
import { randomUUID } from 'crypto'
|
||||
import { mkdir, writeFile } from 'fs/promises'
|
||||
import { basename, join } from 'path'
|
||||
import { z } from 'zod/v4'
|
||||
import { getSessionId } from '../bootstrap/state.js'
|
||||
import { logForDebugging } from '../utils/debug.js'
|
||||
import { getClaudeConfigHomeDir } from '../utils/envUtils.js'
|
||||
import { lazySchema } from '../utils/lazySchema.js'
|
||||
import { getBridgeAccessToken, getBridgeBaseUrl } from './bridgeConfig.js'
|
||||
|
||||
const DOWNLOAD_TIMEOUT_MS = 30_000
|
||||
|
||||
function debug(msg: string): void {
|
||||
logForDebugging(`[bridge:inbound-attach] ${msg}`)
|
||||
}
|
||||
|
||||
const attachmentSchema = lazySchema(() =>
|
||||
z.object({
|
||||
file_uuid: z.string(),
|
||||
file_name: z.string(),
|
||||
}),
|
||||
)
|
||||
const attachmentsArraySchema = lazySchema(() => z.array(attachmentSchema()))
|
||||
|
||||
export type InboundAttachment = z.infer<ReturnType<typeof attachmentSchema>>
|
||||
|
||||
/** Pull file_attachments off a loosely-typed inbound message. */
|
||||
export function extractInboundAttachments(msg: unknown): InboundAttachment[] {
|
||||
if (typeof msg !== 'object' || msg === null || !('file_attachments' in msg)) {
|
||||
return []
|
||||
}
|
||||
const parsed = attachmentsArraySchema().safeParse(msg.file_attachments)
|
||||
return parsed.success ? parsed.data : []
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip path components and keep only filename-safe chars. file_name comes
|
||||
* from the network (web composer), so treat it as untrusted even though the
|
||||
* composer controls it.
|
||||
*/
|
||||
function sanitizeFileName(name: string): string {
|
||||
const base = basename(name).replace(/[^a-zA-Z0-9._-]/g, '_')
|
||||
return base || 'attachment'
|
||||
}
|
||||
|
||||
function uploadsDir(): string {
|
||||
return join(getClaudeConfigHomeDir(), 'uploads', getSessionId())
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch + write one attachment. Returns the absolute path on success,
|
||||
* undefined on any failure.
|
||||
*/
|
||||
async function resolveOne(att: InboundAttachment): Promise<string | undefined> {
|
||||
const token = getBridgeAccessToken()
|
||||
if (!token) {
|
||||
debug('skip: no oauth token')
|
||||
return undefined
|
||||
}
|
||||
|
||||
let data: Buffer
|
||||
try {
|
||||
// getOauthConfig() (via getBridgeBaseUrl) throws on a non-allowlisted
|
||||
// CLAUDE_CODE_CUSTOM_OAUTH_URL — keep it inside the try so a bad
|
||||
// FedStart URL degrades to "no @path" instead of crashing print.ts's
|
||||
// reader loop (which has no catch around the await).
|
||||
const url = `${getBridgeBaseUrl()}/api/oauth/files/${encodeURIComponent(att.file_uuid)}/content`
|
||||
const response = await axios.get(url, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
responseType: 'arraybuffer',
|
||||
timeout: DOWNLOAD_TIMEOUT_MS,
|
||||
validateStatus: () => true,
|
||||
})
|
||||
if (response.status !== 200) {
|
||||
debug(`fetch ${att.file_uuid} failed: status=${response.status}`)
|
||||
return undefined
|
||||
}
|
||||
data = Buffer.from(response.data)
|
||||
} catch (e) {
|
||||
debug(`fetch ${att.file_uuid} threw: ${e}`)
|
||||
return undefined
|
||||
}
|
||||
|
||||
// uuid-prefix makes collisions impossible across messages and within one
|
||||
// (same filename, different files). 8 chars is enough — this isn't security.
|
||||
const safeName = sanitizeFileName(att.file_name)
|
||||
const prefix = (
|
||||
att.file_uuid.slice(0, 8) || randomUUID().slice(0, 8)
|
||||
).replace(/[^a-zA-Z0-9_-]/g, '_')
|
||||
const dir = uploadsDir()
|
||||
const outPath = join(dir, `${prefix}-${safeName}`)
|
||||
|
||||
try {
|
||||
await mkdir(dir, { recursive: true })
|
||||
await writeFile(outPath, data)
|
||||
} catch (e) {
|
||||
debug(`write ${outPath} failed: ${e}`)
|
||||
return undefined
|
||||
}
|
||||
|
||||
debug(`resolved ${att.file_uuid} → ${outPath} (${data.length} bytes)`)
|
||||
return outPath
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve all attachments on an inbound message to a prefix string of
|
||||
* @path refs. Empty string if none resolved.
|
||||
*/
|
||||
export async function resolveInboundAttachments(
|
||||
attachments: InboundAttachment[],
|
||||
): Promise<string> {
|
||||
if (attachments.length === 0) return ''
|
||||
debug(`resolving ${attachments.length} attachment(s)`)
|
||||
const paths = await Promise.all(attachments.map(resolveOne))
|
||||
const ok = paths.filter((p): p is string => p !== undefined)
|
||||
if (ok.length === 0) return ''
|
||||
// Quoted form — extractAtMentionedFiles truncates unquoted @refs at the
|
||||
// first space, which breaks any home dir with spaces (/Users/John Smith/).
|
||||
return ok.map(p => `@"${p}"`).join(' ') + ' '
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepend @path refs to content, whichever form it's in.
|
||||
* Targets the LAST text block — processUserInputBase reads inputString
|
||||
* from processedBlocks[processedBlocks.length - 1], so putting refs in
|
||||
* block[0] means they're silently ignored for [text, image] content.
|
||||
*/
|
||||
export function prependPathRefs(
|
||||
content: string | Array<ContentBlockParam>,
|
||||
prefix: string,
|
||||
): string | Array<ContentBlockParam> {
|
||||
if (!prefix) return content
|
||||
if (typeof content === 'string') return prefix + content
|
||||
const i = content.findLastIndex(b => b.type === 'text')
|
||||
if (i !== -1) {
|
||||
const b = content[i]!
|
||||
if (b.type === 'text') {
|
||||
return [
|
||||
...content.slice(0, i),
|
||||
{ ...b, text: prefix + b.text },
|
||||
...content.slice(i + 1),
|
||||
]
|
||||
}
|
||||
}
|
||||
// No text block — append one at the end so it's last.
|
||||
return [...content, { type: 'text', text: prefix.trimEnd() }]
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience: extract + resolve + prepend. No-op when the message has no
|
||||
* file_attachments field (fast path — no network, returns same reference).
|
||||
*/
|
||||
export async function resolveAndPrepend(
|
||||
msg: unknown,
|
||||
content: string | Array<ContentBlockParam>,
|
||||
): Promise<string | Array<ContentBlockParam>> {
|
||||
const attachments = extractInboundAttachments(msg)
|
||||
if (attachments.length === 0) return content
|
||||
const prefix = await resolveInboundAttachments(attachments)
|
||||
return prependPathRefs(content, prefix)
|
||||
}
|
||||
|
||||
|
||||
82
src/bridge/inboundMessages.ts
Normal file
82
src/bridge/inboundMessages.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import type {
|
||||
Base64ImageSource,
|
||||
ContentBlockParam,
|
||||
ImageBlockParam,
|
||||
} from '@anthropic-ai/sdk/resources/messages.mjs'
|
||||
import type { UUID } from 'crypto'
|
||||
import type { SDKMessage } from '../entrypoints/agentSdkTypes.js'
|
||||
import { detectImageFormatFromBase64 } from '../utils/imageResizer.js'
|
||||
|
||||
/**
|
||||
* Process an inbound user message from the bridge, extracting content
|
||||
* and UUID for enqueueing. Supports both string content and
|
||||
* ContentBlockParam[] (e.g. messages containing images).
|
||||
*
|
||||
* Normalizes image blocks from bridge clients that may use camelCase
|
||||
* `mediaType` instead of snake_case `media_type` (mobile-apps#5825).
|
||||
*
|
||||
* Returns the extracted fields, or undefined if the message should be
|
||||
* skipped (non-user type, missing/empty content).
|
||||
*/
|
||||
export function extractInboundMessageFields(
|
||||
msg: SDKMessage,
|
||||
):
|
||||
| { content: string | Array<ContentBlockParam>; uuid: UUID | undefined }
|
||||
| undefined {
|
||||
if (msg.type !== 'user') return undefined
|
||||
const content = msg.message?.content
|
||||
if (!content) return undefined
|
||||
if (Array.isArray(content) && content.length === 0) return undefined
|
||||
|
||||
const uuid =
|
||||
'uuid' in msg && typeof msg.uuid === 'string'
|
||||
? (msg.uuid as UUID)
|
||||
: undefined
|
||||
|
||||
return {
|
||||
content: Array.isArray(content) ? normalizeImageBlocks(content) : content,
|
||||
uuid,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize image content blocks from bridge clients. iOS/web clients may
|
||||
* send `mediaType` (camelCase) instead of `media_type` (snake_case), or
|
||||
* omit the field entirely. Without normalization, the bad block poisons
|
||||
* the session — every subsequent API call fails with
|
||||
* "media_type: Field required".
|
||||
*
|
||||
* Fast-path scan returns the original array reference when no
|
||||
* normalization is needed (zero allocation on the happy path).
|
||||
*/
|
||||
export function normalizeImageBlocks(
|
||||
blocks: Array<ContentBlockParam>,
|
||||
): Array<ContentBlockParam> {
|
||||
if (!blocks.some(isMalformedBase64Image)) return blocks
|
||||
|
||||
return blocks.map(block => {
|
||||
if (!isMalformedBase64Image(block)) return block
|
||||
const src = block.source as unknown as Record<string, unknown>
|
||||
const mediaType =
|
||||
typeof src.mediaType === 'string' && src.mediaType
|
||||
? src.mediaType
|
||||
: detectImageFormatFromBase64(block.source.data)
|
||||
return {
|
||||
...block,
|
||||
source: {
|
||||
type: 'base64' as const,
|
||||
media_type: mediaType as Base64ImageSource['media_type'],
|
||||
data: block.source.data,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function isMalformedBase64Image(
|
||||
block: ContentBlockParam,
|
||||
): block is ImageBlockParam & { source: Base64ImageSource } {
|
||||
if (block.type !== 'image' || block.source?.type !== 'base64') return false
|
||||
return !(block.source as unknown as Record<string, unknown>).media_type
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue