diff --git a/.claude/agents/hello-agent.md b/.claude/agents/hello-agent.md new file mode 100644 index 0000000000..2c7fc2a9a1 --- /dev/null +++ b/.claude/agents/hello-agent.md @@ -0,0 +1,17 @@ +--- +name: hello-agent +description: A friendly greeting agent that introduces the project +--- + +You are a friendly greeting agent. Your job is to greet the user and provide helpful information about the current project. + +Instructions: +1. Read the project's CLAUDE.md to understand the project context. +2. Greet the user warmly. +3. Provide a brief summary of the project based on what you learned from CLAUDE.md. +4. Offer to help with any questions about the project. + +Style: +- Be concise and friendly. +- Respond in 简体中文. +- Keep responses short — no more than a few sentences. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d0a0c39db..c4b6334cec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ on: jobs: ci: - runs-on: ubuntu-latest + runs-on: macos-latest steps: - uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index ec014c2209..f9d718ce3c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ coverage .idea .vscode *.suo -*.lock \ No newline at end of file +*.lock +src/utils/vendor/ \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 055bc9a23d..6dfdb4cb14 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,6 +15,9 @@ bun install # Dev mode (runs cli.tsx with MACRO defines injected via -d flags) bun run dev +# Dev mode with debugger (set BUN_INSPECT=9229 to pick port) +bun run dev:inspect + # Pipe mode echo "say hello" | bun run src/entrypoints/cli.tsx -p @@ -30,6 +33,15 @@ bun test --coverage # with coverage report bun run lint # check only bun run lint:fix # auto-fix bun run format # format all src/ + +# Health check +bun run health + +# Check unused exports +bun run check:unused + +# Docs dev server (Mintlify) +bun run docs:dev ``` 详细的测试规范、覆盖状态和改进计划见 `docs/testing-spec.md`。 @@ -39,20 +51,27 @@ bun run format # format all src/ ### Runtime & Build - **Runtime**: Bun (not Node.js). All imports, builds, and execution use Bun APIs. -- **Build**: `build.ts` 执行 `Bun.build()` with `splitting: true`,入口 `src/entrypoints/cli.tsx`,输出 `dist/cli.js` + ~450 chunk files。构建后自动替换 `import.meta.require` 为 Node.js 兼容版本(产物 bun/node 都可运行)。 -- **Dev mode**: `scripts/dev.ts` 通过 Bun `-d` flag 注入 `MACRO.*` defines,运行 `src/entrypoints/cli.tsx`。`scripts/defines.ts` 集中管理 define map。 +- **Build**: `build.ts` 执行 `Bun.build()` with `splitting: true`,入口 `src/entrypoints/cli.tsx`,输出 `dist/cli.js` + chunk files。默认启用 `AGENT_TRIGGERS_REMOTE` feature。构建后自动替换 `import.meta.require` 为 Node.js 兼容版本(产物 bun/node 都可运行)。 +- **Dev mode**: `scripts/dev.ts` 通过 Bun `-d` flag 注入 `MACRO.*` defines,运行 `src/entrypoints/cli.tsx`。默认启用 `BUDDY`、`TRANSCRIPT_CLASSIFIER`、`BRIDGE_MODE`、`AGENT_TRIGGERS_REMOTE` 四个 feature。 - **Module system**: ESM (`"type": "module"`), TSX with `react-jsx` transform. - **Monorepo**: Bun workspaces — internal packages live in `packages/` resolved via `workspace:*`. - **Lint/Format**: Biome (`biome.json`)。`bun run lint` / `bun run lint:fix` / `bun run format`。 +- **Defines**: 集中管理在 `scripts/defines.ts`。当前版本 `2.1.888`。 ### Entry & Bootstrap -1. **`src/entrypoints/cli.tsx`** — True entrypoint. Sets up runtime globals: - - `globalThis.MACRO` — build-time macro values (VERSION, BUILD_TIME, etc.),通过 `scripts/dev.ts` 的 `-d` flags 注入。 - - `BUILD_TARGET`, `BUILD_ENV`, `INTERFACE_TYPE` globals。 - - `feature()` 由 `bun:bundle` 内置模块提供,不需要在此 polyfill。 -2. **`src/main.tsx`** — Commander.js CLI definition. Parses args, initializes services (auth, analytics, policy), then launches the REPL or runs in pipe mode. -3. **`src/entrypoints/init.ts`** — One-time initialization (telemetry, config, trust dialog). +1. **`src/entrypoints/cli.tsx`** — True entrypoint。`main()` 函数按优先级处理多条快速路径: + - `--version` / `-v` — 零模块加载 + - `--dump-system-prompt` — feature-gated (DUMP_SYSTEM_PROMPT) + - `--claude-in-chrome-mcp` / `--chrome-native-host` + - `--daemon-worker=` — feature-gated (DAEMON) + - `remote-control` / `rc` / `bridge` — feature-gated (BRIDGE_MODE) + - `daemon` — feature-gated (DAEMON) + - `ps` / `logs` / `attach` / `kill` / `--bg` — feature-gated (BG_SESSIONS) + - `--tmux` + `--worktree` 组合 + - 默认路径:加载 `main.tsx` 启动完整 CLI +2. **`src/main.tsx`** (~4680 行) — Commander.js CLI definition。注册大量 subcommands:`mcp` (serve/add/remove/list...)、`server`、`ssh`、`open`、`auth`、`plugin`、`agents`、`auto-mode`、`doctor`、`update` 等。主 `.action()` 处理器负责权限、MCP、会话恢复、REPL/Headless 模式分发。 +3. **`src/entrypoints/init.ts`** — One-time initialization (telemetry, config, trust dialog)。 ### Core Loop @@ -70,25 +89,37 @@ bun run format # format all src/ - **`src/Tool.ts`** — Tool interface definition (`Tool` type) and utilities (`findToolByName`, `toolMatchesName`). - **`src/tools.ts`** — Tool registry. Assembles the tool list; some tools are conditionally loaded via `feature()` flags or `process.env.USER_TYPE`. -- **`src/tools//`** — Each tool in its own directory (e.g., `BashTool`, `FileEditTool`, `GrepTool`, `AgentTool`). -- Tools define: `name`, `description`, `inputSchema` (JSON Schema), `call()` (execution), and optionally a React component for rendering results. +- **`src/tools//`** — 61 个 tool 目录(如 BashTool, FileEditTool, GrepTool, AgentTool, WebFetchTool, LSPTool, MCPTool 等)。每个 tool 包含 `name`、`description`、`inputSchema`、`call()` 及可选的 React 渲染组件。 +- **`src/tools/shared/`** — Tool 共享工具函数。 ### UI Layer (Ink) - **`src/ink.ts`** — Ink render wrapper with ThemeProvider injection. - **`src/ink/`** — Custom Ink framework (forked/internal): custom reconciler, hooks (`useInput`, `useTerminalSize`, `useSearchHighlight`), virtual list rendering. -- **`src/components/`** — React components rendered in terminal via Ink. Key ones: - - `App.tsx` — Root provider (AppState, Stats, FpsMetrics). - - `Messages.tsx` / `MessageRow.tsx` — Conversation message rendering. - - `PromptInput/` — User input handling. - - `permissions/` — Tool permission approval UI. +- **`src/components/`** — 大量 React 组件(170+ 项),渲染于终端 Ink 环境中。关键组件: + - `App.tsx` — Root provider (AppState, Stats, FpsMetrics) + - `Messages.tsx` / `MessageRow.tsx` — Conversation message rendering + - `PromptInput/` — User input handling + - `permissions/` — Tool permission approval UI + - `design-system/` — 复用 UI 组件(Dialog, FuzzyPicker, ProgressBar, ThemeProvider 等) - Components use React Compiler runtime (`react/compiler-runtime`) — decompiled output has `_c()` memoization calls throughout. ### State Management - **`src/state/AppState.tsx`** — Central app state type and context provider. Contains messages, tools, permissions, MCP connections, etc. -- **`src/state/store.ts`** — Zustand-style store for AppState. -- **`src/bootstrap/state.ts`** — Module-level singletons for session-global state (session ID, CWD, project root, token counts). +- **`src/state/AppStateStore.ts`** — Default state and store factory. +- **`src/state/store.ts`** — Zustand-style store for AppState (`createStore`). +- **`src/state/selectors.ts`** — State selectors. +- **`src/bootstrap/state.ts`** — Module-level singletons for session-global state (session ID, CWD, project root, token counts, model overrides, client type, permission mode). + +### Bridge / Remote Control + +- **`src/bridge/`** (~35 files) — Remote Control / Bridge 模式。feature-gated by `BRIDGE_MODE`。包含 bridge API、会话管理、JWT 认证、消息传输、权限回调等。Entry: `bridgeMain.ts`。 +- CLI 快速路径: `claude remote-control` / `claude rc` / `claude bridge`。 + +### Daemon Mode + +- **`src/daemon/`** — Daemon 模式(长驻 supervisor)。feature-gated by `DAEMON`。包含 `main.ts`(entry)和 `workerRegistry.ts`(worker 管理)。 ### Context & System Prompt @@ -97,17 +128,16 @@ bun run format # format all src/ ### Feature Flag System -Feature flags control which functionality is enabled at runtime. The system works as follows: +Feature flags control which functionality is enabled at runtime: - **在代码中使用**: 统一通过 `import { feature } from 'bun:bundle'` 导入,调用 `feature('FLAG_NAME')` 返回 `boolean`。**不要**在 `cli.tsx` 或其他文件里自己定义 `feature` 函数或覆盖这个 import。 - **启用方式**: 通过环境变量 `FEATURE_=1`。例如 `FEATURE_BUDDY=1 bun run dev` 启用 BUDDY 功能。 -- **Dev 模式**: `scripts/dev.ts` 自动扫描所有 `FEATURE_*` 环境变量,转换为 Bun 的 `--feature` 参数传递给运行时。 -- **Build 模式**: `build.ts` 同样读取 `FEATURE_*` 环境变量,传入 `Bun.build({ features })` 数组。 -- **默认行为**: 不设置任何 `FEATURE_*` 环境变量时,所有 `feature()` 调用返回 `false`,即所有 feature-gated 代码不执行。 -- **常见 flag 名称**: `BUDDY`、`FORK_SUBAGENT`、`PROACTIVE`、`KAIROS`、`VOICE_MODE`、`DAEMON` 等(见 `src/commands.ts` 中的使用)。 +- **Dev 默认 features**: `BUDDY`、`TRANSCRIPT_CLASSIFIER`、`BRIDGE_MODE`、`AGENT_TRIGGERS_REMOTE`(见 `scripts/dev.ts`)。 +- **Build 默认 features**: `AGENT_TRIGGERS_REMOTE`(见 `build.ts`)。 +- **常见 flag**: `BUDDY`, `DAEMON`, `BRIDGE_MODE`, `BG_SESSIONS`, `PROACTIVE`, `KAIROS`, `VOICE_MODE`, `FORK_SUBAGENT`, `SSH_REMOTE`, `DIRECT_CONNECT`, `TEMPLATES`, `CHICAGO_MCP`, `BYOC_ENVIRONMENT_RUNNER`, `SELF_HOSTED_RUNNER`, `COORDINATOR_MODE`, `UDS_INBOX`, `LODESTONE`, `ABLATION_BASELINE` 等。 - **类型声明**: `src/types/internal-modules.d.ts` 中声明了 `bun:bundle` 模块的 `feature` 函数签名。 -**新增功能的正确做法**: 如果要让某个 feature-gated 模块(如 buddy)永久启用,应保留代码中 `import { feature } from 'bun:bundle'` + `feature('FLAG_NAME')` 的标准模式,在运行时通过环境变量或配置控制,而不是绕过 feature flag 直接 import。 +**新增功能的正确做法**: 保留 `import { feature } from 'bun:bundle'` + `feature('FLAG_NAME')` 的标准模式,在运行时通过环境变量或配置控制,不要绕过 feature flag 直接 import。 ### Stubbed/Deleted Modules @@ -131,17 +161,19 @@ Feature flags control which functionality is enabled at runtime. The system work - **框架**: `bun:test`(内置断言 + mock) - **单元测试**: 就近放置于 `src/**/__tests__/`,文件名 `.test.ts` -- **集成测试**: `tests/integration/`,共享 mock/fixture 在 `tests/mocks/` +- **集成测试**: `tests/integration/` — 4 个文件(cli-arguments, context-build, message-pipeline, tool-chain) +- **共享 mock/fixture**: `tests/mocks/`(api-responses, file-system, fixtures/) - **命名**: `describe("functionName")` + `test("behavior description")`,英文 - **Mock 模式**: 对重依赖模块使用 `mock.module()` + `await import()` 解锁(必须内联在测试文件中,不能从共享 helper 导入) -- **当前状态**: 1286 tests / 67 files / 0 fail(详见 `docs/testing-spec.md` 的覆盖状态表和评分) +- **当前状态**: ~1623 tests / 114 files (110 unit + 4 integration) / 0 fail(详见 `docs/testing-spec.md`) ## Working with This Codebase - **Don't try to fix all tsc errors** — they're from decompilation and don't affect runtime. -- **Feature flags** — 默认全部关闭(`feature()` 返回 `false`)。启用方式见上方 Feature Flag System 章节。不要在 `cli.tsx` 中重定义 `feature` 函数。 +- **Feature flags** — 默认全部关闭(`feature()` 返回 `false`)。Dev/build 各有自己的默认启用列表。不要在 `cli.tsx` 中重定义 `feature` 函数。 - **React Compiler output** — Components have decompiled memoization boilerplate (`const $ = _c(N)`). This is normal. - **`bun:bundle` import** — `import { feature } from 'bun:bundle'` 是 Bun 内置模块,由运行时/构建器解析。不要用自定义函数替代它。 - **`src/` path alias** — tsconfig maps `src/*` to `./src/*`. Imports like `import { ... } from 'src/utils/...'` are valid. - **MACRO defines** — 集中管理在 `scripts/defines.ts`。Dev mode 通过 `bun -d` 注入,build 通过 `Bun.build({ define })` 注入。修改版本号等常量只改这个文件。 - **构建产物兼容 Node.js** — `build.ts` 会自动后处理 `import.meta.require`,产物可直接用 `node dist/cli.js` 运行。 +- **Biome 配置** — 大量 lint 规则被关闭(decompiled 代码不适合严格 lint)。`.tsx` 文件用 120 行宽 + 强制分号;其他文件 80 行宽 + 按需分号。 diff --git a/DEV-LOG.md b/DEV-LOG.md index 3c2a94fc08..45b869ccab 100644 --- a/DEV-LOG.md +++ b/DEV-LOG.md @@ -1,5 +1,88 @@ # DEV-LOG +## OpenAI 接口兼容 (2026-04-03) + +**分支**: `feature/openai` + +在 `/login` 流程中新增 "OpenAI Compatible" 选项,支持 Ollama、DeepSeek、vLLM、One API 等兼容 OpenAI Chat Completions API 的第三方服务。用户通过 `/login` 配置后,所有 API 请求自动走 OpenAI 路径。 + +**改动文件(10 个,+384 / -134):** + +| 文件 | 变更 | +|------|------| +| `.github/workflows/ci.yml` | CI runner 从 `ubuntu-latest` 改为 `macos-latest` | +| `README.md` | TODO 列表新增 "OpenAI 接口兼容" 条目 | +| `src/components/ConsoleOAuthFlow.tsx` | 新增 `openai_chat_api` OAuth state(含 Base URL / API Key / 3 个模型映射字段);idle 选择列表新增 "OpenAI Compatible" 选项;完整表单 UI(Tab 切换、Enter 保存);保存时写入 `modelType: 'openai'` + env 到 settings.json;OAuth 登录时重置 `modelType` 为 `anthropic` | +| `src/services/api/openai/index.ts` | 从直接 `yield* adaptOpenAIStreamToAnthropic()` 改为完整流处理循环:累积 content blocks(text/tool_use/thinking)、按 `content_block_stop` yield `AssistantMessage`、同时 yield `StreamEvent` 用于实时显示;错误处理改用新签名 `createAssistantAPIErrorMessage({ content, apiError, error })` | +| `src/services/api/openai/convertMessages.ts` | 输入类型从 Anthropic SDK `BetaMessageParam[]` 改为内部 `(UserMessage \| AssistantMessage)[]`;通过 `msg.type` 而非 `msg.role` 判断角色;从 `msg.message.content` 读取内容;跳过 `cache_edits` / `server_tool_use` 等内部 block 类型 | +| `src/services/api/openai/modelMapping.ts` | 移除 `OPENAI_MODEL_MAP` JSON 环境变量 + 缓存机制;新增 `getModelFamily()` 按 haiku/sonnet/opus 分类;解析优先级改为:`OPENAI_MODEL` → `ANTHROPIC_DEFAULT_{FAMILY}_MODEL` → `DEFAULT_MODEL_MAP` → 原名透传 | +| `src/services/api/openai/__tests__/convertMessages.test.ts` | 测试输入从裸 `{ role, content }` 改为 `makeUserMsg()` / `makeAssistantMsg()` 包装的内部格式 | +| `src/services/api/openai/__tests__/modelMapping.test.ts` | 测试从 `OPENAI_MODEL_MAP` 改为 `ANTHROPIC_DEFAULT_{HAIKU,SONNET,OPUS}_MODEL`;新增 3 个 env var override 测试 | +| `src/utils/model/providers.ts` | `getAPIProvider()` 新增最高优先级:从 settings.json `modelType` 字段判断;环境变量 `CLAUDE_CODE_USE_OPENAI` 降为次优先 | +| `src/utils/settings/types.ts` | `SettingsSchema` 新增 `modelType` 字段:`z.enum(['anthropic', 'openai']).optional()` | + +**关键设计决策:** + +1. **`modelType` 存入 settings.json** — 而非纯环境变量,使 `/login` 配置持久化,重启后仍然生效 +2. **复用 `ANTHROPIC_DEFAULT_*_MODEL` 环境变量** — 而非新增 `OPENAI_MODEL_MAP`,与 Custom Platform 共用同一套模型映射配置,减少用户认知负担 +3. **流处理双 yield** — 同时 yield `AssistantMessage`(给消费方处理工具调用)和 `StreamEvent`(给 REPL 实时渲染),与 Anthropic 路径行为对齐 +4. **OAuth 登录重置 modelType** — 用户切换回官方 Anthropic 登录时自动重置为 `anthropic`,避免残留配置导致请求走错误路径 + +**配置方式:** + +``` +/login → 选择 "OpenAI Compatible" → 填写 Base URL / API Key / 模型名称 +``` + +或手动编辑 `~/.claude/settings.json`: + +```json +{ + "modelType": "openai", + "env": { + "OPENAI_BASE_URL": "http://localhost:11434/v1", + "OPENAI_API_KEY": "ollama", + "ANTHROPIC_DEFAULT_SONNET_MODEL": "qwen3:32b" + } +} +``` + +--- + +## Enable Remote Control / BRIDGE_MODE (2026-04-03) + +**PR**: [claude-code-best/claude-code#60](https://github.com/claude-code-best/claude-code/pull/60) + +Remote Control 功能将本地 CLI 注册为 bridge 环境,生成可分享的 URL(`https://claude.ai/code/session_xxx`),允许从浏览器、手机或其他设备远程查看输出、发送消息、审批工具调用。 + +**改动文件:** + +| 文件 | 变更 | +|------|------| +| `scripts/dev.ts` | `DEFAULT_FEATURES` 加入 `"BRIDGE_MODE"`,dev 模式默认启用 | +| `src/bridge/peerSessions.ts` | stub → 完整实现:通过 bridge API 发送跨会话消息,含三层安全防护(trim + validateBridgeId 白名单 + encodeURIComponent) | +| `src/bridge/webhookSanitizer.ts` | stub → 完整实现:正则 redact 8 类 secret(GitHub/Anthropic/AWS/npm/Slack token),先 redact 再截断,失败返回安全占位符 | +| `src/entrypoints/sdk/controlTypes.ts` | 12 个 `any` stub → `z.infer>` 从现有 Zod schema 推导类型 | +| `src/hooks/useReplBridge.tsx` | `tengu_bridge_system_init` 默认值 `false` → `true`,使 app 端显示 "active" 而非卡在 "connecting" | + +**关键设计决策:** + +1. **不改现有代码逻辑** — 只补全 stub、修正默认值、开启编译开关 +2. **`tengu_bridge_system_init`** — Anthropic 通过 GrowthBook 给订阅用户推送 `true`,但我们的 build 收不到推送;改默认值是唯一不侵入其他代码的方案 +3. **`peerSessions.ts` 认证** — 使用 `getBridgeAccessToken()` 获取 OAuth Bearer token,与 `bridgeApi.ts`/`codeSessionApi.ts` 认证模式一致 +4. **`webhookSanitizer.ts` 安全** — fail-closed(出错返回 `[webhook content redacted due to sanitization error]`),不泄露原始内容 + +**验证结果:** + +- `/remote-control` 命令可见且可用 +- CLI 连接 Anthropic CCR,生成可分享 URL +- App 端(claude.ai/code)显示 "Remote Control active" +- 手机端(Claude iOS app)通过 URL 连接,双向消息正常 + +![Remote Control on Mobile](docs/images/remote-control-mobile.png) + +--- + ## GrowthBook 自定义服务器适配器 (2026-04-03) GrowthBook 功能开关系统原为 Anthropic 内部构建设计,硬编码 SDK key 和 API 地址,外部构建因 `is1PEventLoggingEnabled()` 门控始终禁用。新增适配器模式,通过环境变量连接自定义 GrowthBook 服务器,无配置时所有 feature 读取返回代码默认值。 @@ -168,3 +251,37 @@ GrowthBook 功能开关系统原为 Anthropic 内部构建设计,硬编码 SDK 注意: - `USER_TYPE=ant` 启用 alt-screen 全屏模式,中心区域满屏是预期行为 - `global.d.ts` 中剩余未 stub 的全局函数(`getAntModels` 等)遇到 `X is not defined` 时按同样模式处理 + +--- + +## /login 添加 Custom Platform 选项 (2026-04-03) + +在 `/login` 命令的登录方式选择列表中新增 "Custom Platform" 选项(位于第一位),允许用户直接在终端配置第三方 API 兼容服务的 Base URL、API Key 和三种模型映射,保存到 `~/.claude/settings.json`。 + +**修改文件:** + +| 文件 | 变更 | +|------|------| +| `src/components/ConsoleOAuthFlow.tsx` | `OAuthStatus` 类型新增 `custom_platform` state(含 `baseUrl`、`apiKey`、`haikuModel`、`sonnetModel`、`opusModel`、`activeField`);`idle` case Select 选项新增 Custom Platform 并排第一位;新增 `custom_platform` case 渲染 5 字段表单(Tab/Shift+Tab 切换、focus 高亮、Enter 跳转/保存);Select onChange 处理 `custom_platform` 初始状态(从 `process.env` 预填当前值);`OAuthStatusMessageProps` 类型及调用处新增 `onDone` prop | +| `src/components/ConsoleOAuthFlow.tsx` | 新增 `updateSettingsForSource` import | + +**UI 交互:** +- 5 个字段同屏:Base URL、API Key、Haiku Model、Sonnet Model、Opus Model +- 当前活动字段的标签用 `suggestion` 背景色 + `inverseText` 反色高亮 +- Tab / Shift+Tab 在字段间切换,各自保留输入值 +- 每个字段按 Enter 跳到下一个,最后一个字段 (Opus) 按 Enter 保存 +- 模型字段自动从 `process.env` 读取当前配置作为预填值,无值则空 +- 保存时调用 `updateSettingsForSource('userSettings', { env })` 写入 settings.json,同时更新 `process.env` + +**保存的 settings.json env 字段:** +```json +{ + "ANTHROPIC_BASE_URL": "...", + "ANTHROPIC_AUTH_TOKEN": "...", + "ANTHROPIC_DEFAULT_HAIKU_MODEL": "...", + "ANTHROPIC_DEFAULT_SONNET_MODEL": "...", + "ANTHROPIC_DEFAULT_OPUS_MODEL": "..." +} +``` + +非空字段才写入,保存后立即生效(`onDone()` 触发 `onChangeAPIKey()` 刷新 API 客户端)。 diff --git a/README.md b/README.md index c7817b5b30..c9f30b4a39 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,21 @@ -# Claude Code Best V3 (CCB) +# Claude Code Best V5 (CCB) + +[![GitHub Stars](https://img.shields.io/github/stars/claude-code-best/claude-code?style=flat-square&logo=github&color=yellow)](https://github.com/claude-code-best/claude-code/stargazers) +[![GitHub Contributors](https://img.shields.io/github/contributors/claude-code-best/claude-code?style=flat-square&color=green)](https://github.com/claude-code-best/claude-code/graphs/contributors) +[![GitHub Issues](https://img.shields.io/github/issues/claude-code-best/claude-code?style=flat-square&color=orange)](https://github.com/claude-code-best/claude-code/issues) +[![GitHub License](https://img.shields.io/github/license/claude-code-best/claude-code?style=flat-square)](https://github.com/claude-code-best/claude-code/blob/main/LICENSE) +[![Last Commit](https://img.shields.io/github/last-commit/claude-code-best/claude-code?style=flat-square&color=blue)](https://github.com/claude-code-best/claude-code/commits/main) +[![Bun](https://img.shields.io/badge/runtime-Bun-black?style=flat-square&logo=bun)](https://bun.sh/) +[![Discord](https://img.shields.io/badge/Discord-Join-5865F2?style=flat-square&logo=discord)](https://discord.gg/qZU6zS7Q) + +> Which Claude do you like? The open source one is the best. 牢 A (Anthropic) 官方 [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI 工具的源码反编译/逆向还原项目。目标是将 Claude Code 大部分功能及工程化能力复现 (问就是老佛爷已经付过钱了)。虽然很难绷, 但是它叫做 CCB(踩踩背)... [文档在这里, 支持投稿 PR](https://ccb.agent-aura.top/) +[Discord 群组](https://discord.gg/qZU6zS7Q) + 赞助商占位符 - [x] v1 会完成跑通及基本的类型检查通过; @@ -22,6 +34,9 @@ - [x] 关闭自动更新; - [x] 添加自定义 sentry 错误上报支持 [文档](https://ccb.agent-aura.top/docs/internals/sentry-setup) - [x] 添加自定义 GrowthBook 支持 (GB 也是开源的, 现在你可以配置一个自定义的遥控平台) [文档](https://ccb.agent-aura.top/docs/internals/growthbook-adapter) + - [x] 自定义 login 模式, 大家可以用这个配置 Claude 的模型! + - [x] 修复搜索工具的 rg 缺失问题(需要重新 bun i) + - [ ] OpenAI 接口兼容! /login 然后配置 OpenAI 平台即可! - [ ] V6 大规模重构石山代码, 全面模块分包 - [ ] V6 将会为全新分支, 届时 main 分支将会封存为历史版本 @@ -63,6 +78,40 @@ bun run build 如果遇到 bug 请直接提一个 issues, 我们优先解决 +### 新人配置 /login + +首次运行后,在 REPL 中输入 `/login` 命令进入登录配置界面,选择 **Custom Platform** 即可对接第三方 API 兼容服务(无需 Anthropic 官方账号)。 + +需要填写的字段: + +| 字段 | 说明 | 示例 | +|------|------|------| +| Base URL | API 服务地址 | `https://api.example.com/v1` | +| API Key | 认证密钥 | `sk-xxx` | +| Haiku Model | 快速模型 ID | `claude-haiku-4-5-20251001` | +| Sonnet Model | 均衡模型 ID | `claude-sonnet-4-6` | +| Opus Model | 高性能模型 ID | `claude-opus-4-6` | + +- **Tab / Shift+Tab** 切换字段,**Enter** 确认并跳到下一个,最后一个字段按 Enter 保存 +- 模型字段会自动读取当前环境变量预填 +- 配置保存到 `~/.claude/settings.json` 的 `env` 字段,保存后立即生效 + +也可以直接编辑 `~/.claude/settings.json`: + +```json +{ + "env": { + "ANTHROPIC_BASE_URL": "https://api.example.com/v1", + "ANTHROPIC_AUTH_TOKEN": "sk-xxx", + "ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-haiku-4-5-20251001", + "ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4-6", + "ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-4-6" + } +} +``` + +> 支持所有 Anthropic API 兼容服务(如 OpenRouter、AWS Bedrock 代理等),只要接口兼容 Messages API 即可。 + ## Feature Flags 所有功能开关通过 `FEATURE_=1` 环境变量启用,例如: @@ -89,13 +138,18 @@ TUI (REPL) 模式需要真实终端,无法直接通过 VS Code launch 启动 - 在 `src/` 文件中打断点 - F5 → 选择 **"Attach to Bun (TUI debug)"** -> 注意:`dev:inspect` 和 `launch.json` 中的 WebSocket 地址会在每次启动时变化,需要同步更新两处。 ## 相关文档及网站 - **在线文档(Mintlify)**: [ccb.agent-aura.top](https://ccb.agent-aura.top/) — 文档源码位于 [`docs/`](docs/) 目录,欢迎投稿 PR - **DeepWiki**: +## Contributors + + + + + ## Star History diff --git a/README_EN.md b/README_EN.md new file mode 100644 index 0000000000..3b3053b1da --- /dev/null +++ b/README_EN.md @@ -0,0 +1,158 @@ +# Claude Code Best V5 (CCB) + +[![GitHub Stars](https://img.shields.io/github/stars/claude-code-best/claude-code?style=flat-square&logo=github&color=yellow)](https://github.com/claude-code-best/claude-code/stargazers) +[![GitHub Contributors](https://img.shields.io/github/contributors/claude-code-best/claude-code?style=flat-square&color=green)](https://github.com/claude-code-best/claude-code/graphs/contributors) +[![GitHub Issues](https://img.shields.io/github/issues/claude-code-best/claude-code?style=flat-square&color=orange)](https://github.com/claude-code-best/claude-code/issues) +[![GitHub License](https://img.shields.io/github/license/claude-code-best/claude-code?style=flat-square)](https://github.com/claude-code-best/claude-code/blob/main/LICENSE) +[![Last Commit](https://img.shields.io/github/last-commit/claude-code-best/claude-code?style=flat-square&color=blue)](https://github.com/claude-code-best/claude-code/commits/main) +[![Bun](https://img.shields.io/badge/runtime-Bun-black?style=flat-square&logo=bun)](https://bun.sh/) + +> Which Claude do you like? The open source one is the best. + +A reverse-engineered / decompiled source restoration of Anthropic's official [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI tool. The goal is to reproduce most of Claude Code's functionality and engineering capabilities. It's abbreviated as CCB. + +[Documentation (Chinese)](https://ccb.agent-aura.top/) — PR contributions welcome. + +Sponsor placeholder. + +- [x] v1: Basic runability and type checking pass +- [x] V2: Complete engineering infrastructure + - [ ] Biome formatting may not be implemented first to avoid code conflicts + - [x] Build pipeline complete, output runnable on both Node.js and Bun +- [x] V3: Extensive documentation and documentation site improvements +- [x] V4: Large-scale test suite for improved stability + - [x] Buddy pet feature restored [Docs](https://ccb.agent-aura.top/docs/features/buddy) + - [x] Auto Mode restored [Docs](https://ccb.agent-aura.top/docs/safety/auto-mode) + - [x] All features now configurable via environment variables instead of `bun --feature` +- [x] V5: Enterprise-grade monitoring/reporting, missing tools补全, restrictions removed + - [x] Removed anti-distillation code + - [x] Web search capability (using Bing) [Docs](https://ccb.agent-aura.top/docs/features/web-browser-tool) + - [x] Debug mode support [Docs](https://ccb.agent-aura.top/docs/features/debug-mode) + - [x] Disabled auto-updates + - [x] Custom Sentry error reporting support [Docs](https://ccb.agent-aura.top/docs/internals/sentry-setup) + - [x] Custom GrowthBook support (GB is open source — configure your own feature flag platform) [Docs](https://ccb.agent-aura.top/docs/internals/growthbook-adapter) + - [x] Custom login mode — configure Claude models your way +- [ ] V6: Large-scale refactoring, full modular packaging + - [ ] V6 will be a new branch; main branch will be archived as a historical version + +> I don't know how long this project will survive. Star + Fork + git clone + .zip is the safest bet. +> +> This project updates rapidly — Opus continuously optimizes in the background, with new changes almost every few hours. +> +> Claude has burned over $1000, out of budget, switching to GLM to continue; @zai-org GLM 5.1 is quite capable. + +## Quick Start + +### Prerequisites + +Make sure you're on the latest version of Bun, otherwise you'll run into all sorts of weird bugs. Run `bun upgrade`! + +- [Bun](https://bun.sh/) >= 1.3.11 +- Standard Claude Code configuration — each provider has its own setup method + +### Install + +```bash +bun install +``` + +### Run + +```bash +# Dev mode — if you see version 888, it's working +bun run dev + +# Build +bun run build +``` + +The build uses code splitting (`build.ts`), outputting to `dist/` (entry `dist/cli.js` + ~450 chunk files). + +The build output runs on both Bun and Node.js — you can publish to a private registry and run directly. + +If you encounter a bug, please open an issue — we'll prioritize it. + +### First-time Setup /login + +After the first run, enter `/login` in the REPL to access the login configuration screen. Select **Custom Platform** to connect to third-party API-compatible services (no Anthropic account required). + +Fields to fill in: + +| Field | Description | Example | +|-------|-------------|---------| +| Base URL | API service URL | `https://api.example.com/v1` | +| API Key | Authentication key | `sk-xxx` | +| Haiku Model | Fast model ID | `claude-haiku-4-5-20251001` | +| Sonnet Model | Balanced model ID | `claude-sonnet-4-6` | +| Opus Model | High-performance model ID | `claude-opus-4-6` | + +- **Tab / Shift+Tab** to switch fields, **Enter** to confirm and move to the next, press Enter on the last field to save +- Model fields auto-fill from current environment variables +- Configuration saves to `~/.claude/settings.json` under the `env` key, effective immediately + +You can also edit `~/.claude/settings.json` directly: + +```json +{ + "env": { + "ANTHROPIC_BASE_URL": "https://api.example.com/v1", + "ANTHROPIC_AUTH_TOKEN": "sk-xxx", + "ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-haiku-4-5-20251001", + "ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4-6", + "ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-4-6" + } +} +``` + +> Supports all Anthropic API-compatible services (e.g., OpenRouter, AWS Bedrock proxies, etc.) as long as the interface is compatible with the Messages API. + +## Feature Flags + +All feature toggles are enabled via `FEATURE_=1` environment variables, for example: + +```bash +FEATURE_BUDDY=1 FEATURE_FORK_SUBAGENT=1 bun run dev +``` + +See [`docs/features/`](docs/features/) for detailed descriptions of each feature. Contributions welcome. + +## VS Code Debugging + +The TUI (REPL) mode requires a real terminal and cannot be launched directly via VS Code's launch config. Use **attach mode**: + +### Steps + +1. **Start inspect server in terminal**: + ```bash + bun run dev:inspect + ``` + This outputs an address like `ws://localhost:8888/xxxxxxxx`. + +2. **Attach debugger from VS Code**: + - Set breakpoints in `src/` files + - Press F5 → select **"Attach to Bun (TUI debug)"** + +## Documentation & Links + +- **Online docs (Mintlify)**: [ccb.agent-aura.top](https://ccb.agent-aura.top/) — source in [`docs/`](docs/), PR contributions welcome +- **DeepWiki**: + +## Contributors + + + + + +## Star History + + + + + + Star History Chart + + + +## License + +This project is for educational and research purposes only. All rights to Claude Code belong to [Anthropic](https://www.anthropic.com/). diff --git a/build.ts b/build.ts index b179ec16d2..11c4a2481b 100644 --- a/build.ts +++ b/build.ts @@ -8,10 +8,15 @@ const outdir = "dist"; const { rmSync } = await import("fs"); rmSync(outdir, { recursive: true, force: true }); +// Default features that match the official CLI build. +// Additional features can be enabled via FEATURE_=1 env vars. +const DEFAULT_BUILD_FEATURES = ["AGENT_TRIGGERS_REMOTE"]; + // Collect FEATURE_* env vars → Bun.build features -const features = Object.keys(process.env) +const envFeatures = Object.keys(process.env) .filter(k => k.startsWith("FEATURE_")) .map(k => k.replace("FEATURE_", "")); +const features = [...new Set([...DEFAULT_BUILD_FEATURES, ...envFeatures])]; // Step 2: Bundle with splitting const result = await Bun.build({ @@ -53,3 +58,19 @@ for (const file of files) { console.log( `Bundled ${result.outputs.length} files to ${outdir}/ (patched ${patched} for Node.js compat)`, ); + +// Step 4: Bundle download-ripgrep script as standalone JS for postinstall +const rgScript = await Bun.build({ + entrypoints: ["scripts/download-ripgrep.ts"], + outdir, + target: "node", +}); +if (!rgScript.success) { + console.error("Failed to bundle download-ripgrep script:"); + for (const log of rgScript.logs) { + console.error(log); + } + // Non-fatal — postinstall fallback to bun run scripts/download-ripgrep.ts +} else { + console.log(`Bundled download-ripgrep script to ${outdir}/`); +} diff --git a/bun.lock b/bun.lock index d45cbf3019..5f53002de3 100644 --- a/bun.lock +++ b/bun.lock @@ -89,6 +89,7 @@ "lru-cache": "^11.2.7", "marked": "^17.0.5", "modifiers-napi": "workspace:*", + "openai": "^4.73.0", "p-map": "^7.0.4", "picomatch": "^4.0.4", "plist": "^3.1.0", @@ -741,6 +742,8 @@ "@types/node": ["@types/node@25.5.0", "https://registry.npmmirror.com/@types/node/-/node-25.5.0.tgz", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], + "@types/node-fetch": ["@types/node-fetch@2.6.13", "https://registry.npmmirror.com/@types/node-fetch/-/node-fetch-2.6.13.tgz", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], + "@types/pg": ["@types/pg@8.15.6", "https://registry.npmmirror.com/@types/pg/-/pg-8.15.6.tgz", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ=="], "@types/pg-pool": ["@types/pg-pool@2.0.7", "https://registry.npmmirror.com/@types/pg-pool/-/pg-pool-2.0.7.tgz", { "dependencies": { "@types/pg": "*" } }, "sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng=="], @@ -763,6 +766,8 @@ "@xmldom/xmldom": ["@xmldom/xmldom@0.8.12", "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.8.12.tgz", {}, "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg=="], + "abort-controller": ["abort-controller@3.0.0", "https://registry.npmmirror.com/abort-controller/-/abort-controller-3.0.0.tgz", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], + "accepts": ["accepts@2.0.0", "https://registry.npmmirror.com/accepts/-/accepts-2.0.0.tgz", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "acorn": ["acorn@8.16.0", "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], @@ -771,6 +776,8 @@ "agent-base": ["agent-base@8.0.0", "https://registry.npmmirror.com/agent-base/-/agent-base-8.0.0.tgz", {}, "sha512-QT8i0hCz6C/KQ+KTAbSNwCHDGdmUJl2tp2ZpNlGSWCfhUNVbYG2WLE3MdZGBAgXPV4GAvjGMxo+C1hroyxmZEg=="], + "agentkeepalive": ["agentkeepalive@4.6.0", "https://registry.npmmirror.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], + "ajv": ["ajv@8.18.0", "https://registry.npmmirror.com/ajv/-/ajv-8.18.0.tgz", { "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", "https://registry.npmmirror.com/ajv-formats/-/ajv-formats-3.0.1.tgz", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], @@ -927,6 +934,8 @@ "etag": ["etag@1.8.1", "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + "event-target-shim": ["event-target-shim@5.0.1", "https://registry.npmmirror.com/event-target-shim/-/event-target-shim-5.0.1.tgz", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], + "eventsource": ["eventsource@3.0.7", "https://registry.npmmirror.com/eventsource/-/eventsource-3.0.7.tgz", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], "eventsource-parser": ["eventsource-parser@3.0.6", "https://registry.npmmirror.com/eventsource-parser/-/eventsource-parser-3.0.6.tgz", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], @@ -973,8 +982,12 @@ "form-data": ["form-data@4.0.5", "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz", { "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", "https://registry.npmmirror.com/form-data-encoder/-/form-data-encoder-1.7.2.tgz", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="], + "formatly": ["formatly@0.3.0", "https://registry.npmmirror.com/formatly/-/formatly-0.3.0.tgz", { "dependencies": { "fd-package-json": "^2.0.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w=="], + "formdata-node": ["formdata-node@4.4.1", "https://registry.npmmirror.com/formdata-node/-/formdata-node-4.4.1.tgz", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="], + "formdata-polyfill": ["formdata-polyfill@4.0.10", "https://registry.npmmirror.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], "forwarded": ["forwarded@0.2.0", "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], @@ -1045,6 +1058,8 @@ "human-signals": ["human-signals@8.0.1", "https://registry.npmmirror.com/human-signals/-/human-signals-8.0.1.tgz", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], + "humanize-ms": ["humanize-ms@1.2.1", "https://registry.npmmirror.com/humanize-ms/-/humanize-ms-1.2.1.tgz", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], + "iconv-lite": ["iconv-lite@0.7.2", "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], "ignore": ["ignore@7.0.5", "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], @@ -1181,7 +1196,7 @@ "node-domexception": ["node-domexception@1.0.0", "https://registry.npmmirror.com/node-domexception/-/node-domexception-1.0.0.tgz", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], - "node-fetch": ["node-fetch@3.3.2", "https://registry.npmmirror.com/node-fetch/-/node-fetch-3.3.2.tgz", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + "node-fetch": ["node-fetch@2.7.0", "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], "node-forge": ["node-forge@1.4.0", "https://registry.npmmirror.com/node-forge/-/node-forge-1.4.0.tgz", {}, "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ=="], @@ -1197,6 +1212,8 @@ "open": ["open@10.2.0", "https://registry.npmmirror.com/open/-/open-10.2.0.tgz", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], + "openai": ["openai@4.104.0", "https://registry.npmmirror.com/openai/-/openai-4.104.0.tgz", { "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" }, "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA=="], + "os-tmpdir": ["os-tmpdir@1.0.2", "https://registry.npmmirror.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="], "oxc-parser": ["oxc-parser@0.121.0", "https://registry.npmmirror.com/oxc-parser/-/oxc-parser-0.121.0.tgz", { "dependencies": { "@oxc-project/types": "^0.121.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.121.0", "@oxc-parser/binding-android-arm64": "0.121.0", "@oxc-parser/binding-darwin-arm64": "0.121.0", "@oxc-parser/binding-darwin-x64": "0.121.0", "@oxc-parser/binding-freebsd-x64": "0.121.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.121.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.121.0", "@oxc-parser/binding-linux-arm64-gnu": "0.121.0", "@oxc-parser/binding-linux-arm64-musl": "0.121.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.121.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.121.0", "@oxc-parser/binding-linux-riscv64-musl": "0.121.0", "@oxc-parser/binding-linux-s390x-gnu": "0.121.0", "@oxc-parser/binding-linux-x64-gnu": "0.121.0", "@oxc-parser/binding-linux-x64-musl": "0.121.0", "@oxc-parser/binding-openharmony-arm64": "0.121.0", "@oxc-parser/binding-wasm32-wasi": "0.121.0", "@oxc-parser/binding-win32-arm64-msvc": "0.121.0", "@oxc-parser/binding-win32-ia32-msvc": "0.121.0", "@oxc-parser/binding-win32-x64-msvc": "0.121.0" } }, "sha512-ek9o58+SCv6AV7nchiAcUJy1DNE2CC5WRdBcO0mF+W4oRjNQfPO7b3pLjTHSFECpHkKGOZSQxx3hk8viIL5YCg=="], @@ -1417,7 +1434,7 @@ "walk-up-path": ["walk-up-path@4.0.0", "https://registry.npmmirror.com/walk-up-path/-/walk-up-path-4.0.0.tgz", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="], - "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + "web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="], "webidl-conversions": ["webidl-conversions@3.0.1", "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], @@ -1755,10 +1772,14 @@ "external-editor/iconv-lite": ["iconv-lite@0.4.24", "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + "fetch-blob/web-streams-polyfill": ["web-streams-polyfill@3.3.3", "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + "form-data/mime-types": ["mime-types@2.1.35", "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "gaxios/https-proxy-agent": ["https-proxy-agent@7.0.6", "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "gaxios/node-fetch": ["node-fetch@3.3.2", "https://registry.npmmirror.com/node-fetch/-/node-fetch-3.3.2.tgz", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + "gtoken/gaxios": ["gaxios@6.7.1", "https://registry.npmmirror.com/gaxios/-/gaxios-6.7.1.tgz", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", "node-fetch": "^2.6.9", "uuid": "^9.0.1" } }, "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ=="], "http-proxy-agent/agent-base": ["agent-base@7.1.4", "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], @@ -1773,6 +1794,8 @@ "npm-run-path/path-key": ["path-key@4.0.0", "https://registry.npmmirror.com/path-key/-/path-key-4.0.0.tgz", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + "openai/@types/node": ["@types/node@18.19.130", "https://registry.npmmirror.com/@types/node/-/node-18.19.130.tgz", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "parse5-htmlparser2-tree-adapter/parse5": ["parse5@6.0.1", "https://registry.npmmirror.com/parse5/-/parse5-6.0.1.tgz", {}, "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="], "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], @@ -1909,8 +1932,6 @@ "gtoken/gaxios/is-stream": ["is-stream@2.0.1", "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], - "gtoken/gaxios/node-fetch": ["node-fetch@2.7.0", "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "gtoken/gaxios/uuid": ["uuid@9.0.1", "https://registry.npmmirror.com/uuid/-/uuid-9.0.1.tgz", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], "image-processor-napi/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], @@ -1951,6 +1972,8 @@ "image-processor-napi/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "https://registry.npmmirror.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], + "openai/@types/node/undici-types": ["undici-types@5.26.5", "https://registry.npmmirror.com/undici-types/-/undici-types-5.26.5.tgz", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "qrcode/yargs/cliui": ["cliui@6.0.0", "https://registry.npmmirror.com/cliui/-/cliui-6.0.0.tgz", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], "qrcode/yargs/string-width": ["string-width@4.2.3", "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -1969,8 +1992,6 @@ "@anthropic-ai/vertex-sdk/google-auth-library/gaxios/is-stream": ["is-stream@2.0.1", "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], - "@anthropic-ai/vertex-sdk/google-auth-library/gaxios/node-fetch": ["node-fetch@2.7.0", "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "@anthropic-ai/vertex-sdk/google-auth-library/gaxios/uuid": ["uuid@9.0.1", "https://registry.npmmirror.com/uuid/-/uuid-9.0.1.tgz", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], "@anthropic-ai/vertex-sdk/google-auth-library/gcp-metadata/google-logging-utils": ["google-logging-utils@0.0.2", "https://registry.npmmirror.com/google-logging-utils/-/google-logging-utils-0.0.2.tgz", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="], diff --git a/docs/external-dependencies.md b/docs/external-dependencies.md new file mode 100644 index 0000000000..a28447e7c1 --- /dev/null +++ b/docs/external-dependencies.md @@ -0,0 +1,209 @@ +# Claude Code 远程服务器依赖 + +> 只列出代码中实际发起网络请求的远程服务。本地服务、npm 包依赖、展示用 URL 不包含在内。 + +## 总览表 + +| # | 服务 | 远程端点 | 协议 | 状态 | +|---|---|---|---|---| +| 1 | Anthropic API | `api.anthropic.com` | HTTPS | 默认启用 | +| 2 | AWS Bedrock | `bedrock-runtime.*.amazonaws.com` | HTTPS | 需 `CLAUDE_CODE_USE_BEDROCK=1` | +| 3 | Google Vertex AI | `{region}-aiplatform.googleapis.com` | HTTPS | 需 `CLAUDE_CODE_USE_VERTEX=1` | +| 4 | Azure Foundry | `{resource}.services.ai.azure.com` | HTTPS | 需 `CLAUDE_CODE_USE_FOUNDRY=1` | +| 5 | OAuth (Anthropic) | `platform.claude.com`, `claude.com`, `claude.ai` | HTTPS | 用户登录时 | +| 6 | GrowthBook | `api.anthropic.com` (remoteEval) | HTTPS | 默认启用 | +| 7 | Sentry | 可配置 (`SENTRY_DSN`) | HTTPS | 需设环境变量 | +| 8 | Datadog | 可配置 (`DATADOG_LOGS_ENDPOINT`) | HTTPS | 需设环境变量 | +| 9 | OpenTelemetry Collector | 可配置 (`OTEL_EXPORTER_OTLP_ENDPOINT`) | gRPC/HTTP | 需设环境变量 | +| 10 | 1P Event Logging | `api.anthropic.com/api/event_logging/batch` | HTTPS | 默认启用 | +| 11 | BigQuery Metrics | `api.anthropic.com/api/claude_code/metrics` | HTTPS | 默认启用 | +| 12 | MCP Proxy | `mcp-proxy.anthropic.com` | HTTPS+WS | 使用 MCP 工具时 | +| 13 | MCP Registry | `api.anthropic.com/mcp-registry` | HTTPS | 查询 MCP 服务器时 | +| 14 | Bing Search | `www.bing.com` | HTTPS | WebSearch 工具 | +| 15 | Google Cloud Storage (更新) | `storage.googleapis.com` | HTTPS | 版本检查 | +| 16 | GitHub Raw (Changelog/Stats) | `raw.githubusercontent.com` | HTTPS | 更新提示 | +| 17 | Claude in Chrome Bridge | `bridge.claudeusercontent.com` | WSS | Chrome 集成 | +| 18 | CCR Upstream Proxy | `api.anthropic.com` | WS | CCR 远程会话 | +| 19 | Voice STT | `api.anthropic.com/api/ws/...` | WSS | Voice Mode | +| 20 | Desktop App Download | `claude.ai/api/desktop/...` | HTTPS | 下载引导 | + +--- + +## 详细说明 + +### 1. Anthropic Messages API + +核心 LLM 推理服务,发送对话消息、接收流式响应。 + +- **端点**: `https://api.anthropic.com` (生产) / `https://api-staging.anthropic.com` (staging) +- **覆盖**: `ANTHROPIC_BASE_URL` 环境变量 +- **认证**: API Key / OAuth Token +- **文件**: `src/services/api/client.ts`, `src/services/api/claude.ts` + +### 2. AWS Bedrock + +- **端点**: `bedrock-runtime.{region}.amazonaws.com` +- **认证**: AWS 凭证链 / `AWS_BEARER_TOKEN_BEDROCK` +- **文件**: `src/services/api/client.ts:153-190`, `src/utils/aws.ts` + +### 3. Google Vertex AI + +- **端点**: `{region}-aiplatform.googleapis.com` +- **认证**: `GoogleAuth` + `cloud-platform` scope +- **文件**: `src/services/api/client.ts:228-298` + +### 4. Azure Foundry + +- **端点**: `https://{resource}.services.ai.azure.com/anthropic/v1/messages` +- **认证**: API Key 或 Azure AD `DefaultAzureCredential` +- **文件**: `src/services/api/client.ts:191-220` + +### 5. OAuth + +OAuth 2.0 + PKCE 授权码流程。 + +- **端点**: + - `https://platform.claude.com/oauth/authorize` — 授权页 + - `https://claude.com/cai/oauth/authorize` — Claude.ai 授权 + - `https://platform.claude.com/v1/oauth/token` — Token 交换 + - `https://api.anthropic.com/api/oauth/claude_cli/create_api_key` — 创建 API Key + - `https://api.anthropic.com/api/oauth/claude_cli/roles` — 获取角色 + - `https://claude.ai/oauth/claude-code-client-metadata` — MCP 客户端元数据 + - `https://claude.fedstart.com` — FedStart 政府部署 +- **文件**: `src/constants/oauth.ts`, `src/services/oauth/` + +### 6. GrowthBook (功能开关) + +- **端点**: `https://api.anthropic.com/` (remoteEval 模式) 或 `CLAUDE_GB_ADAPTER_URL` +- **SDK Keys**: `sdk-zAZezfDKGoZuXXKe` (外部), `sdk-xRVcrliHIlrg4og4` (ant prod), `sdk-yZQvlplybuXjYh6L` (ant dev) +- **文件**: `src/services/analytics/growthbook.ts`, `src/constants/keys.ts` + +### 7. Sentry (错误追踪) + +- **激活**: 设置 `SENTRY_DSN` (默认未配置) +- **行为**: 仅错误上报,自动过滤敏感 header +- **文件**: `src/utils/sentry.ts` + +### 8. Datadog (日志) + +- **激活**: 同时设 `DATADOG_LOGS_ENDPOINT` + `DATADOG_API_KEY` (默认未配置) +- **文件**: `src/services/analytics/datadog.ts` + +### 9. OpenTelemetry Collector + +- **激活**: `CLAUDE_CODE_ENABLE_TELEMETRY=1` 或 `OTEL_*` 环境变量 +- **协议**: gRPC / HTTP / Protobuf,支持 OTLP 和 Prometheus 导出 +- **文件**: `src/utils/telemetry/instrumentation.ts` + +### 10. 1P Event Logging (内部事件) + +- **端点**: `https://api.anthropic.com/api/event_logging/batch` +- **协议**: 批量导出 (10s 间隔, 每批 200 事件) +- **文件**: `src/services/analytics/firstPartyEventLoggingExporter.ts` + +### 11. BigQuery Metrics + +- **端点**: `https://api.anthropic.com/api/claude_code/metrics` +- **文件**: `src/utils/telemetry/bigqueryExporter.ts` + +### 12. MCP Proxy + +Anthropic 托管的 MCP 服务器代理。 + +- **端点**: `https://mcp-proxy.anthropic.com/v1/mcp/{server_id}` +- **认证**: Claude.ai OAuth tokens +- **文件**: `src/services/mcp/client.ts`, `src/constants/oauth.ts` + +### 13. MCP Registry + +获取官方 MCP 服务器列表。 + +- **端点**: `https://api.anthropic.com/mcp-registry/v0/servers?version=latest&visibility=commercial` +- **文件**: `src/services/mcp/officialRegistry.ts` + +### 14. Bing Search + +WebSearch 工具的默认适配器,抓取 Bing 搜索结果。 + +- **端点**: `https://www.bing.com/search?q={query}&setmkt=en-US` +- **文件**: `src/tools/WebSearchTool/adapters/bingAdapter.ts` + +另外还有 Domain Blocklist 查询: +- **端点**: `https://api.anthropic.com/api/web/domain_info?domain={domain}` +- **文件**: `src/tools/WebFetchTool/utils.ts` + +### 15. Google Cloud Storage (自动更新) + +- **端点**: `https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases` +- **文件**: `src/utils/autoUpdater.ts` + +### 16. GitHub Raw Content + +- **端点**: `https://raw.githubusercontent.com/anthropics/claude-code/refs/heads/main/CHANGELOG.md` +- **端点**: `https://raw.githubusercontent.com/anthropics/claude-plugins-official/refs/heads/stats/stats/plugin-installs.json` +- **文件**: `src/utils/releaseNotes.ts`, `src/utils/plugins/installCounts.ts` + +### 17. Claude in Chrome Bridge + +- **端点**: `wss://bridge.claudeusercontent.com` (生产) / `wss://bridge-staging.claudeusercontent.com` (staging) +- **文件**: `src/utils/claudeInChrome/mcpServer.ts` + +### 18. CCR Upstream Proxy + +- **端点**: `ws://api.anthropic.com/v1/code/upstreamproxy/ws` +- **激活**: `CLAUDE_CODE_REMOTE=1` + `CCR_UPSTREAM_PROXY_ENABLED=1` +- **文件**: `src/upstreamproxy/upstreamproxy.ts` + +### 19. Voice STT + +- **端点**: `wss://api.anthropic.com/api/ws/...` +- **文件**: `src/services/voiceStreamSTT.ts` + +### 20. Desktop App Download + +- **端点**: `https://claude.ai/api/desktop/win32/x64/exe/latest/redirect` (Windows) +- **端点**: `https://claude.ai/api/desktop/darwin/universal/dmg/latest/redirect` (macOS) +- **文件**: `src/components/DesktopHandoff.tsx` + +--- + +## Anthropic API 辅助端点汇总 + +以下端点都挂在 `api.anthropic.com` 上,按功能分类: + +| 端点路径 | 用途 | 文件 | +|---|---|---| +| `/api/event_logging/batch` | 事件批量上报 | `src/services/analytics/firstPartyEventLoggingExporter.ts` | +| `/api/claude_code/metrics` | BigQuery 指标导出 | `src/utils/telemetry/bigqueryExporter.ts` | +| `/api/oauth/claude_cli/create_api_key` | 创建 API Key | `src/constants/oauth.ts` | +| `/api/oauth/claude_cli/roles` | 获取用户角色 | `src/constants/oauth.ts` | +| `/api/oauth/accounts/grove` | 通知设置 | `src/services/api/grove.ts` | +| `/api/oauth/organizations/{id}/referral/*` | 推荐活动 | `src/services/api/referral.ts` | +| `/api/oauth/organizations/{id}/overage_credit_grant` | 超额信用 | `src/services/api/overageCreditGrant.ts` | +| `/api/oauth/organizations/{id}/admin_requests` | 管理请求 | `src/services/api/adminRequests.ts` | +| `/api/web/domain_info?domain={}` | 域名安全检查 | `src/tools/WebFetchTool/utils.ts` | +| `/api/claude_code/settings` | 设置同步 | `src/services/settingsSync/index.ts` | +| `/api/claude_code/managed_settings` | 企业托管设置 (1h 轮询) | `src/services/remoteManagedSettings/index.ts` | +| `/api/claude_code/team_memory?repo={}` | 团队记忆同步 | `src/services/teamMemorySync/index.ts` | +| `/api/auth/trusted_devices` | 可信设备注册 | `src/bridge/trustedDevice.ts` | +| `/api/organizations/{id}/claude_code/buddy_react` | Companion 反应 | `src/buddy/companionReact.ts` | +| `/mcp-registry/v0/servers` | MCP 服务器注册表 | `src/services/mcp/officialRegistry.ts` | +| `/v1/files` | 文件上传/下载 | `src/services/api/filesApi.ts` | +| `/v1/sessions/{id}/events` | 会话历史 | `src/assistant/sessionHistory.ts` | +| `/v1/code/triggers` | 远程触发器 | `src/tools/RemoteTriggerTool/RemoteTriggerTool.ts` | +| `/v1/organizations/{id}/mcp_servers` | 组织 MCP 配置 | `src/services/mcp/claudeai.ts` | + +## 非 Anthropic 远程域名汇总 + +| 域名 | 服务 | 协议 | +|---|---|---| +| `bedrock-runtime.*.amazonaws.com` | AWS Bedrock | HTTPS | +| `{region}-aiplatform.googleapis.com` | Google Vertex AI | HTTPS | +| `{resource}.services.ai.azure.com` | Azure Foundry | HTTPS | +| `www.bing.com` | Bing 搜索 | HTTPS | +| `storage.googleapis.com` | 自动更新 | HTTPS | +| `raw.githubusercontent.com` | Changelog / 插件统计 | HTTPS | +| `bridge.claudeusercontent.com` | Chrome Bridge | WSS | +| `platform.claude.com` | OAuth 授权页 | HTTPS | +| `claude.com` / `claude.ai` | OAuth / 下载 | HTTPS | +| `claude.fedstart.com` | FedStart OAuth | HTTPS | diff --git a/docs/internals/ant-only-world.mdx b/docs/internals/ant-only-world.mdx index c964461bb4..804d7954d9 100644 --- a/docs/internals/ant-only-world.mdx +++ b/docs/internals/ant-only-world.mdx @@ -11,13 +11,13 @@ keywords: ["Ant 特权", "USER_TYPE", "身份门控", "内部功能", "Anthropic `USER_TYPE` 是一个构建时常量,通过 Bun 打包器的 `--define` 注入。在 Anthropic 的内部构建中它被设为 `'ant'`,在公开发布的版本中是 `'external'`: ```typescript -// 反编译版本(src/entrypoints/cli.tsx 第 16 行) -(globalThis as any).BUILD_TARGET = "external"; +// 反编译版本(src/types/global.d.ts 第 63 行) +// Build-time constants BUILD_TARGET/BUILD_ENV/INTERFACE_TYPE — removed (zero runtime usage) ``` -由于这是编译时常量,Bun 会进行**常量折叠**——所有 `process.env.USER_TYPE === 'ant'` 在外部构建中直接变为 `false`,后续代码被 DCE 移除。但在反编译版本中,这些代码保留完整。 +`BUILD_TARGET` 等构建时常量在反编译版本中已被移除。`USER_TYPE` 通过 Bun 的 `--define` 或环境变量注入,Bun 会进行**常量折叠**——所有 `process.env.USER_TYPE === 'ant'` 在外部构建中直接变为 `false`,后续代码被 DCE 移除。但在反编译版本中,这些代码保留完整。 -`USER_TYPE === 'ant'` 出现在代码库的 **60+ 个位置**,控制着工具、命令、API、UI 等方方面面。 +`USER_TYPE === 'ant'` 在代码库中出现 **377+ 次**(含 `=== 'ant'` 291 次、`(process.env.USER_TYPE) === 'ant'` 86 次),另有 `!== 'ant'` 53 次、其他引用约 35 次,总计 **465 处引用**,控制着工具、命令、API、UI 等方方面面。 ## Ant-Only 工具 @@ -31,7 +31,9 @@ keywords: ["Ant 特权", "USER_TYPE", "身份门控", "内部功能", "Anthropic | **TungstenTool** | `src/tools/TungstenTool/` | 基于 tmux 的终端面板工具(反编译版中已 stub) | ```typescript -// src/tools.ts 第 16-24 行 +// src/tools.ts 第 14-24 行——条件导入 + Dead Code Elimination 标记 +// Dead code elimination: conditional import for ant-only tools +/* eslint-disable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */ const REPLTool = process.env.USER_TYPE === 'ant' ? require('./tools/REPLTool/REPLTool.js').REPLTool @@ -45,7 +47,7 @@ const SuggestBackgroundPRTool = ## Ant-Only 命令 -`src/commands.ts` 注册了 25+ 个仅在内部构建中可用的斜杠命令: +`src/commands.ts` 注册了 **28** 个仅在内部构建中可用的斜杠命令(`INTERNAL_ONLY_COMMANDS`,lines 225-254),在 `USER_TYPE === 'ant' && !IS_DEMO` 时才加载(line 343-345): @@ -55,6 +57,7 @@ const SuggestBackgroundPRTool = - `env` — 显示环境变量 - `mockLimits` — 模拟速率限制 - `resetLimits` — 重置速率限制 + - `resetLimitsNonInteractive` — 重置速率限制(非交互式) - `bughunter` — Bug 猎人模式 @@ -69,6 +72,9 @@ const SuggestBackgroundPRTool = - `autofixPr` — 自动修复 PR 中的问题 - `share` — 分享会话 - `summary` — 生成摘要 + - `subscribePr` — 订阅 PR(需要 `KAIROS_GITHUB_WEBHOOKS` feature flag) + - `forceSnip` — 强制截断历史(需要 `HISTORY_SNIP` feature flag) + - `ultraplan` — 超级规划(需要 `ULTRAPLAN` feature flag) - `backfillSessions` — 回填会话数据 @@ -88,30 +94,72 @@ const SuggestBackgroundPRTool = ## Beta API Headers -Claude Code 向 API 发送的 beta headers 也分为公开和内部两类: - -| Header | 功能 | 可见性 | -|--------|------|--------| -| `claude-code-20250219` | Claude Code 标识 | 公开 | -| `interleaved-thinking-2025-05-14` | 交错思考模式 | 公开 | -| `context-1m-2025-08-07` | 1M 上下文窗口 | 公开 | -| `context-management-2025-06-27` | 上下文管理 | 公开 | -| `web-search-2025-03-05` | 网页搜索 | 公开 | -| `effort-2025-11-24` | 推理强度控制 | 公开 | -| `fast-mode-2026-02-01` | 快速模式 | 公开 | -| `token-efficient-tools-2026-03-28` | Token 高效工具 | 公开 | -| `advisor-tool-2026-03-01` | 顾问工具 | 公开 | -| **`cli-internal-2026-02-09`** | 内部 CLI 功能 | **Ant-Only** | -| **`afk-mode-2026-01-31`** | AFK 模式(离开键盘自动审批) | **Feature Flag** | -| **`summarize-connector-text-2026-03-13`** | 连接器文本摘要 | **Feature Flag** | +Claude Code 向 API 发送的 beta headers 分布在 `src/constants/betas.ts`(主注册表)和其他文件中,按可见性分为以下几类: + +### 公开 Headers(所有构建均发送) + +| Header | 功能 | 额外条件 | +|--------|------|----------| +| `claude-code-20250219` | Claude Code 标识 | 非 Haiku 时始终发送;Haiku 在 agentic 模式下也发送 | +| `effort-2025-11-24` | 推理强度控制 | 动态注入 | +| `task-budgets-2026-03-13` | 任务预算 | 始终通过 `addAgenticBetas()` 注入 | +| `fast-mode-2026-02-01` | 快速模式 | 通过 sticky-on latch 动态注入 | +| `advisor-tool-2026-03-01` | 顾问工具 | 启用 advisor 时动态注入 | +| `advanced-tool-use-2025-11-20` | 工具搜索(1P) | Claude API / Foundry | +| `tool-search-tool-2025-10-19` | 工具搜索(3P) | Vertex / Bedrock | + +### 模型能力相关(有条件发送) + +| Header | 功能 | 条件 | +|--------|------|------| +| `interleaved-thinking-2025-05-14` | 交错思考模式 | 模型支持 ISP 且未禁用 | +| `context-1m-2025-08-07` | 1M 上下文窗口 | 模型支持 1M context | +| `context-management-2025-06-27` | 上下文管理 | Claude 4+ 或 ant 手动启用 | +| `structured-outputs-2025-12-15` | 结构化输出 | Claude 4.5/4.6 + GrowthBook `tengu_tool_pear` | +| `web-search-2025-03-05` | 网页搜索 | Vertex (Claude 4+) / Foundry | +| `redact-thinking-2026-02-12` | 思维摘要/脱敏 | ISP 模型 + 非交互 + 未强制显示思维 | +| `prompt-caching-scope-2026-01-05` | 提示缓存作用域 | firstParty/foundry + 全局缓存 | + +### Ant-Only Headers + +| Header | 功能 | 条件 | +|--------|------|------| +| **`cli-internal-2026-02-09`** | 内部 CLI 功能 | `USER_TYPE === 'ant'` + CLI 入口 | +| **`token-efficient-tools-2026-03-28`** | Token 高效工具 | `USER_TYPE === 'ant'` + GrowthBook `tengu_amber_json_tools` | + +### Feature Flag Gated + +| Header | 功能 | 条件 | +|--------|------|------| +| **`afk-mode-2026-01-31`** | AFK 模式(离开键盘自动审批) | `feature('TRANSCRIPT_CLASSIFIER')` | + +### 其他特殊 Headers + +| Header | 功能 | 来源 | +|--------|------|------| +| `oauth-2025-04-20` | OAuth 订阅者标识 | `src/constants/oauth.ts`,Pro/Max/Team/Enterprise | +| `environments-2025-11-01` | Bridge 环境 API | `src/bridge/bridgeApi.ts`,仅 Bridge 模式 | ```typescript -// src/constants/betas.ts 第 29-30 行 +// src/constants/betas.ts — 常量定义 +export const TOKEN_EFFICIENT_TOOLS_BETA_HEADER = + 'token-efficient-tools-2026-03-28' export const CLI_INTERNAL_BETA_HEADER = process.env.USER_TYPE === 'ant' ? 'cli-internal-2026-02-09' : '' ``` -`cli-internal` header 意味着 Anthropic 的 API 服务端也维护着一套 ant-only 的服务端行为——这不仅仅是客户端的门控。 +```typescript +// src/utils/betas.ts 第 315-321 行——TOKEN_EFFICIENT_TOOLS 的实际门控逻辑 +if ( + process.env.USER_TYPE === 'ant' && + includeFirstPartyOnlyBetas && + tokenEfficientToolsEnabled // GrowthBook 'tengu_amber_json_tools' flag +) { + betaHeaders.push(TOKEN_EFFICIENT_TOOLS_BETA_HEADER) +} +``` + +`cli-internal` header 意味着 Anthropic 的 API 服务端也维护着一套 ant-only 的服务端行为——这不仅仅是客户端的门控。`token-efficient-tools` 进一步需要 GrowthBook flag 开启,说明 Ant 员工内部也有分层灰度。 ## 内部代号体系 @@ -138,6 +186,8 @@ Anthropic 有浓厚的"动物命名"文化: - `DISABLE_AUTO_COMPACT` — 禁用自动压缩 - `CLAUDE_CODE_DISABLE_AUTO_MEMORY` — 禁用自动记忆 - `CLAUDE_CODE_DISABLE_BACKGROUND_TASKS` — 禁用后台任务 + - `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS` — 禁用实验性 beta headers + - `USE_API_CONTEXT_MANAGEMENT` — 上下文管理工具清除(需 ant) - `CLAUDE_CODE_VERIFY_PLAN` — 启用 VerifyPlanExecutionTool @@ -151,6 +201,7 @@ Anthropic 有浓厚的"动物命名"文化: - `CLAUDE_CODE_COORDINATOR_MODE` — 启用 Coordinator 模式 - `CLAUDE_INTERNAL_FC_OVERRIDES` — GrowthBook flag 覆盖(ant-only) - `IS_DEMO` — 演示模式(隐藏内部命令和敏感信息) + - `CLAUDE_CODE_ENTRYPOINT` — 入口类型标识(`cli` | 其他) diff --git a/docs/lsp-integration.md b/docs/lsp-integration.md new file mode 100644 index 0000000000..5f87bc3fcc --- /dev/null +++ b/docs/lsp-integration.md @@ -0,0 +1,264 @@ +# LSP Integration + +Claude Code 内置了 Language Server Protocol (LSP) 集成,提供代码智能功能(跳转定义、查找引用、悬停信息、文档符号等)和被动的诊断反馈。 + +## 快速开始 + +### 1. 安装 LSP 插件 + +在 Claude Code REPL 中使用 `/plugin` 命令搜索并安装 LSP 插件: + +``` +/plugin +``` + +搜索 `lsp`,找到对应语言的插件(如 `typescript-lsp`),选择安装。 + +安装后运行 `/reload-plugins` 使插件生效。 + +LSP 插件安装后,后台的 LSP Server Manager 会自动加载并启动对应的语言服务器,无需手动配置。 + +### 2. 启用 LSP Tool + +LSP Tool 需要通过环境变量显式启用,Claude 才能主动发起代码智能查询: + +```bash +ENABLE_LSP_TOOL=1 bun run dev +``` + +不启用时,LSP 服务器仍然在后台运行并推送被动的诊断反馈(类型错误等)。 + +## 自动推荐 + +除了手动 `/plugin` 搜索安装外,Claude Code 会在编辑文件时自动检测: + +1. 监听 `fileHistory.trackedFiles`,发现有新文件被编辑 +2. 扫描已安装的 marketplace,找到声明支持该文件扩展名的 LSP 插件 +3. 检查系统上是否已安装对应的 LSP 二进制(如 `typescript-language-server`) +4. 满足条件时弹出推荐对话框,可选择安装 + +``` +┌───── LSP Plugin Recommendation ─────────────┐ +│ │ +│ LSP provides code intelligence like │ +│ go-to-definition and error checking │ +│ │ +│ Plugin: typescript-lsp │ +│ Triggered by: .ts files │ +│ │ +│ Would you like to install this LSP plugin? │ +│ │ +│ > Yes, install typescript-lsp │ +│ No, not now │ +│ Never for typescript-lsp │ +│ Disable all LSP recommendations │ +└───────────────────────────────────────────────┘ +``` + +- 30 秒不操作自动关闭(算作 "No") +- 选 "Never" 不再推荐该插件 +- 选 "Disable" 关闭所有 LSP 推荐 +- 连续忽略 5 次后自动禁用推荐 + +## 架构概览 + +``` +┌─────────────────────────────────────────────────────┐ +│ LSP Tool │ +│ src/tools/LSPTool/LSPTool.ts │ +│ (Claude 可调用的工具,9 种操作) │ +└──────────────────────┬──────────────────────────────┘ + │ +┌──────────────────────▼──────────────────────────────┐ +│ LSP Server Manager (Singleton) │ +│ src/services/lsp/manager.ts │ +│ - initializeLspServerManager() │ +│ - reinitializeLspServerManager() │ +│ - shutdownLspServerManager() │ +└──────────────────────┬──────────────────────────────┘ + │ +┌──────────────────────▼──────────────────────────────┐ +│ LSP Server Manager (实例) │ +│ src/services/lsp/LSPServerManager.ts │ +│ - 管理多个 LSPServerInstance │ +│ - 按文件扩展名路由请求 │ +│ - 文件同步 (didOpen/didChange/didSave/didClose) │ +└──────────────────────┬──────────────────────────────┘ + │ + ┌─────────────┼─────────────┐ + ▼ ▼ ▼ +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ LSPServer │ │ LSPServer │ │ LSPServer │ +│ Instance │ │ Instance │ │ Instance │ +│ (typescript) │ │ (python) │ │ (rust...) │ +└──────┬───────┘ └──────┬───────┘ └──────┬───────┘ + │ │ │ +┌──────▼───────┐ ┌──────▼───────┐ ┌──────▼───────┐ +│ LSPClient │ │ LSPClient │ │ LSPClient │ +│ (JSON-RPC) │ │ (JSON-RPC) │ │ (JSON-RPC) │ +└──────┬───────┘ └──────┬───────┘ └──────┬───────┘ + │ │ │ + 子进程 (stdio) 子进程 (stdio) 子进程 (stdio) +``` + +### 被动诊断反馈 + +``` +LSP Server ──publishDiagnostics──▶ passiveFeedback.ts + │ + ▼ + LSPDiagnosticRegistry + (去重、容量限制) + │ + ▼ + Attachment System + (异步注入到对话) +``` + +LSP 服务器会异步推送 `textDocument/publishDiagnostics` 通知,经去重和容量限制后作为 attachment 注入到 Claude 的对话上下文中。 + +## 核心模块 + +| 文件 | 职责 | +|------|------| +| `src/services/lsp/manager.ts` | 全局单例,初始化/重初始化/关闭生命周期管理 | +| `src/services/lsp/LSPServerManager.ts` | 多服务器管理,按文件扩展名路由,文件同步 | +| `src/services/lsp/LSPServerInstance.ts` | 单个 LSP 服务器实例生命周期(启动/停止/重启/健康检查) | +| `src/services/lsp/LSPClient.ts` | JSON-RPC 通信层(基于 `vscode-jsonrpc`),子进程管理 | +| `src/services/lsp/config.ts` | 从插件加载 LSP 服务器配置 | +| `src/services/lsp/LSPDiagnosticRegistry.ts` | 诊断信息注册、去重、容量限制 | +| `src/services/lsp/passiveFeedback.ts` | 注册 `publishDiagnostics` 通知处理器 | +| `src/tools/LSPTool/LSPTool.ts` | LSP Tool 实现(暴露给 Claude) | +| `src/tools/LSPTool/schemas.ts` | 输入 schema(9 种操作的 discriminated union) | +| `src/tools/LSPTool/formatters.ts` | 各操作结果的格式化 | +| `src/tools/LSPTool/prompt.ts` | Tool 描述文本 | +| `src/utils/plugins/lspPluginIntegration.ts` | 从插件加载、验证、环境变量解析、作用域管理 | + +## LSP Tool 支持的操作 + +| 操作 | LSP Method | 说明 | +|------|-----------|------| +| `goToDefinition` | `textDocument/definition` | 跳转到符号定义 | +| `findReferences` | `textDocument/references` | 查找所有引用 | +| `hover` | `textDocument/hover` | 获取悬停信息(文档、类型) | +| `documentSymbol` | `textDocument/documentSymbol` | 获取文档内所有符号 | +| `workspaceSymbol` | `workspace/symbol` | 全工作区符号搜索 | +| `goToImplementation` | `textDocument/implementation` | 查找接口/抽象方法的实现 | +| `prepareCallHierarchy` | `textDocument/prepareCallHierarchy` | 获取位置处的调用层级项 | +| `incomingCalls` | `callHierarchy/incomingCalls` | 查找调用此函数的所有函数 | +| `outgoingCalls` | `callHierarchy/outgoingCalls` | 查找此函数调用的所有函数 | + +所有操作需要 `filePath`、`line`(1-based)和 `character`(1-based)参数。 + +## 插件开发:LSP 服务器配置 + +LSP 服务器通过插件提供。插件的 `manifest.json` 中可以声明 LSP 服务器,支持三种格式: + +**1. 内联配置(在 manifest 中直接定义)** + +```json +{ + "lspServers": { + "typescript": { + "command": "typescript-language-server", + "args": ["--stdio"], + "extensionToLanguage": { + ".ts": "typescript", + ".tsx": "typescriptreact" + } + } + } +} +``` + +**2. 引用外部 .lsp.json 文件** + +```json +{ + "lspServers": "path/to/.lsp.json" +} +``` + +**3. 数组混合格式** + +```json +{ + "lspServers": [ + "path/to/.lsp.json", + { + "another-server": { "command": "...", "extensionToLanguage": { "...": "..." } } + } + ] +} +``` + +也可以在插件目录下直接放置 `.lsp.json` 文件,无需在 manifest 中声明。 + +### LSP 服务器配置 Schema + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `command` | string | 是 | LSP 服务器可执行命令(不含空格) | +| `args` | string[] | 否 | 命令行参数 | +| `extensionToLanguage` | Record | 是 | 文件扩展名到语言 ID 的映射(至少一个) | +| `transport` | `"stdio"` \| `"socket"` | 否 | 通信方式,默认 `stdio` | +| `env` | Record | 否 | 启动服务器时设置的环境变量 | +| `initializationOptions` | unknown | 否 | 传给服务器的初始化选项 | +| `settings` | unknown | 否 | 通过 `workspace/didChangeConfiguration` 传递的设置 | +| `workspaceFolder` | string | 否 | 工作区目录路径 | +| `startupTimeout` | number | 否 | 启动超时时间(毫秒) | +| `maxRestarts` | number | 否 | 最大重启次数(默认 3) | + +### 环境变量替换 + +配置中的 `command`、`args`、`env`、`workspaceFolder` 支持: + +- `${CLAUDE_PLUGIN_ROOT}` — 插件根目录 +- `${CLAUDE_PLUGIN_DATA}` — 插件数据目录 +- `${user_config.KEY}` — 用户在插件启用时配置的值 +- `${VAR}` — 系统环境变量 + +## 生命周期管理 + +### 服务器状态机 + +``` +stopped → starting → running +running → stopping → stopped +any → error (失败时) +error → starting (重试时) +``` + +### 崩溃恢复 + +- LSP 服务器崩溃时状态设为 `error` +- 下次请求时自动尝试重启(通过 `ensureServerStarted`) +- 超过 `maxRestarts`(默认 3)次后放弃 + +### 瞬态错误重试 + +- `ContentModified` 错误(LSP 错误码 -32801)会自动重试,最多 3 次 +- 使用指数退避:500ms → 1000ms → 2000ms +- 常见于 rust-analyzer 等仍在索引项目的服务器 + +### 诊断信息容量限制 + +- 每个文件最多 10 条诊断 +- 总计最多 30 条诊断 +- 超出部分按严重性排序后截断(Error > Warning > Info > Hint) +- 跨 turn 去重:已发送过的相同诊断不会重复发送 +- 文件编辑后清除该文件的已发送记录,允许新诊断通过 + +### 插件刷新 + +安装/卸载插件后使用 `/reload-plugins`,会调用 `reinitializeLspServerManager()`: +1. 异步关闭旧服务器实例 +2. 重置状态为 `not-started` +3. 调用 `initializeLspServerManager()` 重新加载插件配置 + +## 依赖 + +- `vscode-jsonrpc` — JSON-RPC 通信(懒加载,仅在实际创建服务器实例时才 require) +- `vscode-languageserver-protocol` — LSP 协议类型 +- `vscode-languageserver-types` — LSP 类型定义 +- `lru-cache` — 诊断去重缓存 diff --git a/docs/plans/openai-compatibility.md b/docs/plans/openai-compatibility.md new file mode 100644 index 0000000000..68fa9f1582 --- /dev/null +++ b/docs/plans/openai-compatibility.md @@ -0,0 +1,421 @@ +# OpenAI 协议兼容层 + +## 概述 + +claude-code 支持通过 OpenAI Chat Completions API(`/v1/chat/completions`)兼容任意 OpenAI 协议端点,包括 Ollama、DeepSeek、vLLM、One API、LiteLLM 等。 + +核心策略为**流适配器模式**:在 `queryModel()` 中插入提前返回分支,将 Anthropic 格式请求转为 OpenAI 格式,调用 OpenAI SDK,再将 SSE 流转换回 `BetaRawMessageStreamEvent` 格式。下游代码(流处理循环、query.ts、QueryEngine.ts、REPL)**完全不改**。 + +## 环境变量 + +| 变量 | 必需 | 说明 | +|---|---|---| +| `CLAUDE_CODE_USE_OPENAI` | 是 | 设为 `1` 启用 OpenAI 后端 | +| `OPENAI_API_KEY` | 是 | API key(Ollama 等可设为任意值) | +| `OPENAI_BASE_URL` | 推荐 | 端点 URL(如 `http://localhost:11434/v1`) | +| `OPENAI_MODEL` | 可选 | 覆盖所有请求的模型名(跳过映射) | +| `OPENAI_MODEL_MAP` | 可选 | JSON 映射,如 `{"claude-sonnet-4-6":"gpt-4o"}` | +| `OPENAI_ORG_ID` | 可选 | Organization ID | +| `OPENAI_PROJECT_ID` | 可选 | Project ID | + +### 使用示例 + +```bash +# Ollama +CLAUDE_CODE_USE_OPENAI=1 \ +OPENAI_API_KEY=ollama \ +OPENAI_BASE_URL=http://localhost:11434/v1 \ +OPENAI_MODEL=qwen2.5-coder-32b \ +bun run dev + +# DeepSeek(自动支持 Thinking) +CLAUDE_CODE_USE_OPENAI=1 \ +OPENAI_API_KEY=sk-xxx \ +OPENAI_BASE_URL=https://api.deepseek.com/v1 \ +OPENAI_MODEL=deepseek-chat \ +bun run dev + +# vLLM +CLAUDE_CODE_USE_OPENAI=1 \ +OPENAI_API_KEY=token-abc123 \ +OPENAI_BASE_URL=http://localhost:8000/v1 \ +OPENAI_MODEL=Qwen/Qwen2.5-Coder-32B-Instruct \ +bun run dev + +# One API / LiteLLM +CLAUDE_CODE_USE_OPENAI=1 \ +OPENAI_API_KEY=sk-your-key \ +OPENAI_BASE_URL=https://your-one-api.example.com/v1 \ +OPENAI_MODEL=gpt-4o \ +bun run dev + +# 自定义模型映射 +CLAUDE_CODE_USE_OPENAI=1 \ +OPENAI_API_KEY=sk-xxx \ +OPENAI_BASE_URL=https://my-gateway.example.com/v1 \ +OPENAI_MODEL_MAP='{"claude-sonnet-4-6":"gpt-4o-2024-11-20","claude-haiku-4-5":"gpt-4o-mini"}' \ +bun run dev +``` + +## 架构 + +### 请求流程 + +``` +queryModel() [claude.ts] + ├── 共享预处理(消息归一化、工具过滤、媒体裁剪) + └── if (getAPIProvider() === 'openai') + └── queryModelOpenAI() [openai/index.ts] + ├── resolveOpenAIModel() → 解析模型名 + ├── normalizeMessagesForAPI() → 共享消息预处理 + ├── toolToAPISchema() → 构建工具 schema + ├── anthropicMessagesToOpenAI() → 消息格式转换 + ├── anthropicToolsToOpenAI() → 工具格式转换 + ├── openai.chat.completions.create({ stream: true }) + └── adaptOpenAIStreamToAnthropic() → 流格式转换 + ├── delta.reasoning_content → thinking 块 + ├── delta.content → text 块 + ├── delta.tool_calls → tool_use 块 + ├── usage.cached_tokens → cache_read_input_tokens + └── yield BetaRawMessageStreamEvent +``` + +### 模型名解析优先级 + +`resolveOpenAIModel()` 的解析顺序: + +1. `OPENAI_MODEL` 环境变量 → 直接使用,覆盖所有 +2. `OPENAI_MODEL_MAP` JSON 查表 → 自定义映射 +3. 内置默认映射(见下表) +4. 以上都不匹配 → 原名透传 + +### 内置模型映射 + +| Anthropic 模型 | OpenAI 映射 | +|---|---| +| `claude-sonnet-4-6` | `gpt-4o` | +| `claude-sonnet-4-5-20250929` | `gpt-4o` | +| `claude-sonnet-4-20250514` | `gpt-4o` | +| `claude-3-7-sonnet-20250219` | `gpt-4o` | +| `claude-3-5-sonnet-20241022` | `gpt-4o` | +| `claude-opus-4-6` | `o3` | +| `claude-opus-4-5-20251101` | `o3` | +| `claude-opus-4-1-20250805` | `o3` | +| `claude-opus-4-20250514` | `o3` | +| `claude-haiku-4-5-20251001` | `gpt-4o-mini` | +| `claude-3-5-haiku-20241022` | `gpt-4o-mini` | + +同时会自动剥离 `[1m]` 后缀(Claude 特有的 modifier)。 + +## 文件结构 + +### 新增文件 + +``` +src/services/api/openai/ +├── client.ts # OpenAI SDK 客户端工厂(~50 行) +├── convertMessages.ts # Anthropic → OpenAI 消息格式转换(~190 行) +├── convertTools.ts # Anthropic → OpenAI 工具格式转换(~70 行) +├── streamAdapter.ts # SSE 流转换核心,含 thinking + caching(~270 行) +├── modelMapping.ts # 模型名解析(~60 行) +├── index.ts # 公共入口 queryModelOpenAI()(~110 行) +└── __tests__/ + ├── convertMessages.test.ts # 10 个测试 + ├── convertTools.test.ts # 7 个测试 + ├── modelMapping.test.ts # 6 个测试 + └── streamAdapter.test.ts # 14 个测试(含 thinking + caching) +``` + +### 修改文件 + +| 文件 | 改动 | +|---|---| +| `src/utils/model/providers.ts` | 添加 `'openai'` provider 类型 + `CLAUDE_CODE_USE_OPENAI` 检查(最高优先级) | +| `src/utils/model/configs.ts` | 每个 ModelConfig 添加 `openai` 键 | +| `src/services/api/claude.ts` | 在 `stripExcessMediaItems()` 后插入 OpenAI 提前返回分支(~8 行) | +| `package.json` | 添加 `"openai": "^4.73.0"` 依赖 | + +## 消息转换规则 + +### Anthropic → OpenAI + +| Anthropic | OpenAI | +|---|---| +| `system` prompt(`string[]`) | `role: "system"` 消息(`\n\n` 拼接) | +| `user` + `text` 块 | `role: "user"` 消息 | +| `assistant` + `text` 块 | `role: "assistant"` + `content` | +| `assistant` + `tool_use` 块 | `role: "assistant"` + `tool_calls[]` | +| `user` + `tool_result` 块 | `role: "tool"` + `tool_call_id` | +| `thinking` 块 | 静默丢弃(请求侧) | + +### 工具转换 + +| Anthropic | OpenAI | +|---|---| +| `{ name, description, input_schema }` | `{ type: "function", function: { name, description, parameters } }` | +| `cache_control`, `defer_loading` 等字段 | 剥离 | +| `tool_choice: { type: "auto" }` | `"auto"` | +| `tool_choice: { type: "any" }` | `"required"` | +| `tool_choice: { type: "tool", name }` | `{ type: "function", function: { name } }` | + +### 消息转换示例 + +``` +Anthropic: OpenAI: +[ + system: ["You are helpful."], [ + { role: "system", + { role: "user", content: "You are helpful." }, + content: [ { role: "user", + { type: "text", text: "Run ls" } content: "Run ls" + ] }, + }, { role: "assistant", + { role: "assistant", content: "I'll check.", + content: [ tool_calls: [{ + { type: "text", text: "I'll check."}, id: "tu_123", + { type: "tool_use", type: "function", + id: "tu_123", name: "bash", function: { + input: { command: "ls" } } name: "bash", + ] arguments: '{"command":"ls"}' + }, }] } + { role: "user", { role: "tool", + content: [ tool_call_id: "tu_123", + { type: "tool_result", content: "file1\nfile2" + tool_use_id: "tu_123", } + content: "file1\nfile2" ] + ] + } +] +``` + +## 流转换规则 + +### SSE Chunk → Anthropic Event 映射 + +| OpenAI Chunk | Anthropic Event | +|---|---| +| 首个 chunk | `message_start`(含 usage) | +| `delta.reasoning_content` | `content_block_start(thinking)` + `thinking_delta` | +| `delta.content` | `content_block_start(text)` + `text_delta` | +| `delta.tool_calls` | `content_block_start(tool_use)` + `input_json_delta` | +| `finish_reason: "stop"` | `message_delta(stop_reason: "end_turn")` | +| `finish_reason: "tool_calls"` | `message_delta(stop_reason: "tool_use")` | +| `finish_reason: "length"` | `message_delta(stop_reason: "max_tokens")` | + +### 块顺序 + +当模型返回 `reasoning_content` 时(如 DeepSeek),块顺序与 Anthropic 一致: + +``` +thinking block (index 0) ← delta.reasoning_content +text block (index 1) ← delta.content +``` + +或: + +``` +thinking block (index 0) ← delta.reasoning_content +tool_use block (index 1) ← delta.tool_calls +``` + +无 `reasoning_content` 时: + +``` +text block (index 0) ← delta.content +tool_use block (index 1) ← delta.tool_calls(如果有) +``` + +### finish_reason 映射 + +| OpenAI | Anthropic | +|---|---| +| `stop` | `end_turn` | +| `tool_calls` | `tool_use` | +| `length` | `max_tokens` | +| `content_filter` | `end_turn` | + +### 事件序列示例 + +**纯文本响应**: +``` +OpenAI chunks: + delta.content = "Hello" + delta.content = " world" + finish_reason = "stop" + +→ Anthropic events: + message_start { message: { id, role: 'assistant', usage: {...} } } + content_block_start { index: 0, content_block: { type: 'text' } } + content_block_delta { index: 0, delta: { type: 'text_delta', text: 'Hello' } } + content_block_delta { index: 0, delta: { type: 'text_delta', text: ' world' } } + content_block_stop { index: 0 } + message_delta { delta: { stop_reason: 'end_turn' } } + message_stop +``` + +**Thinking + 文本(DeepSeek 风格)**: +``` +OpenAI chunks: + delta.reasoning_content = "Let me think..." + delta.reasoning_content = " step by step." + delta.content = "The answer is 42." + finish_reason = "stop" + +→ Anthropic events: + message_start { ... } + content_block_start { index: 0, content_block: { type: 'thinking', signature: '' } } + content_block_delta { index: 0, delta: { type: 'thinking_delta', thinking: 'Let me think...' } } + content_block_delta { index: 0, delta: { type: 'thinking_delta', thinking: ' step by step.' } } + content_block_stop { index: 0 } + content_block_start { index: 1, content_block: { type: 'text' } } + content_block_delta { index: 1, delta: { type: 'text_delta', text: 'The answer is 42.' } } + content_block_stop { index: 1 } + message_delta { delta: { stop_reason: 'end_turn' } } + message_stop +``` + +**工具调用**: +``` +OpenAI chunks: + delta.tool_calls[0] = { id: 'call_xxx', function: { name: 'bash', arguments: '' } } + delta.tool_calls[0].function.arguments = '{"comm' + delta.tool_calls[0].function.arguments = 'and":"ls"}' + finish_reason = "tool_calls" + +→ Anthropic events: + message_start { ... } + content_block_start { index: 0, content_block: { type: 'tool_use', id: 'call_xxx', name: 'bash' } } + content_block_delta { index: 0, delta: { type: 'input_json_delta', partial_json: '{"comm' } } + content_block_delta { index: 0, delta: { type: 'input_json_delta', partial_json: 'and":"ls"}' } } + content_block_stop { index: 0 } + message_delta { delta: { stop_reason: 'tool_use' } } + message_stop +``` + +## 功能支持 + +### Thinking(思维链) + +**请求侧**:不需要显式配置。支持思维链的模型(DeepSeek 等)会自动返回 `delta.reasoning_content`。 + +**响应侧**:`delta.reasoning_content` 被转换为 Anthropic `thinking` content block: + +```ts +// content_block_start +{ type: 'content_block_start', index: 0, + content_block: { type: 'thinking', thinking: '', signature: '' } } + +// content_block_delta +{ type: 'content_block_delta', index: 0, + delta: { type: 'thinking_delta', thinking: 'Let me analyze...' } } +``` + +thinking block 在 text/tool_use block 之前自动关闭,保持 Anthropic 的块顺序。 + +### Prompt Caching + +**请求侧**:OpenAI 端点使用自动缓存,无需显式设置 `cache_control`。 + +**响应侧**:OpenAI 的 `usage.prompt_tokens_details.cached_tokens` 被映射到 Anthropic 的 `cache_read_input_tokens`: + +``` +OpenAI: usage.prompt_tokens_details.cached_tokens = 800 + ↓ +Anthropic: message_start.message.usage.cache_read_input_tokens = 800 +``` + +在 `message_start` 的 usage 中报告缓存命中量。 + +### 工具调用(Tool Use) + +完整支持 OpenAI function calling 格式。所有本地工具(Bash、FileEdit、Grep、Glob、Agent 等)透明工作——它们通过 JSON 输入输出通信,格式无关。 + +工具参数以 `input_json_delta` 形式流式传输,由下游代码拼接解析。 + +### 不支持的功能 + +| 功能 | 策略 | +|---|---| +| Beta Headers | 不发送 | +| Server Tools (advisor) | 不发送 | +| Structured Output | 不发送 | +| Fast Mode / Effort | 不发送 | +| Tool Search / defer_loading | 不启用,所有工具直接发送 | +| Anthropic Signature | thinking block 的 `signature` 字段为空字符串 | +| cache_creation_input_tokens | 始终为 0(OpenAI 不区分创建/读取) | + +## 测试 + +```bash +# 运行所有 OpenAI 适配层测试 +bun test src/services/api/openai/__tests__/ + +# 单独运行 +bun test src/services/api/openai/__tests__/streamAdapter.test.ts # 14 tests(含 thinking + caching) +bun test src/services/api/openai/__tests__/convertMessages.test.ts # 10 tests +bun test src/services/api/openai/__tests__/convertTools.test.ts # 7 tests +bun test src/services/api/openai/__tests__/modelMapping.test.ts # 6 tests +``` + +当前测试覆盖:**39 tests / 73 assertions / 0 fail**。 + +### 测试覆盖矩阵 + +| 功能 | convertMessages | convertTools | streamAdapter | modelMapping | +|---|---|---|---|---| +| 文本消息转换 | ✅ | | | | +| tool_use 转换 | ✅ | | | | +| tool_result 转换 | ✅ | | | | +| thinking 剥离 | ✅ | | | | +| 完整对话流程 | ✅ | | | | +| 工具 schema 转换 | | ✅ | | | +| tool_choice 映射 | | ✅ | | | +| 纯文本流 | | | ✅ | | +| 工具调用流 | | | ✅ | | +| 混合文本+工具 | | | ✅ | | +| finish_reason 映射 | | | ✅ | | +| thinking 流 | | | ✅ | | +| thinking+text 切换 | | | ✅ | | +| thinking+tool_use 切换 | | | ✅ | | +| 块索引正确性 | | | ✅ | | +| cached_tokens 映射 | | | ✅ | | +| OPENAI_MODEL 覆盖 | | | | ✅ | +| 默认模型映射 | | | | ✅ | +| 未知模型透传 | | | | ✅ | +| [1m] 后缀剥离 | | | | ✅ | + +## 端到端验证 + +```bash +# 1. 安装依赖 +bun install + +# 2. 运行单元测试 +bun test src/services/api/openai/__tests__/ + +# 3. 连接实际端点(以 Ollama 为例) +CLAUDE_CODE_USE_OPENAI=1 \ +OPENAI_API_KEY=ollama \ +OPENAI_BASE_URL=http://localhost:11434/v1 \ +OPENAI_MODEL=qwen2.5-coder-32b \ +bun run dev + +# 4. 连接 DeepSeek(测试 thinking 支持) +CLAUDE_CODE_USE_OPENAI=1 \ +OPENAI_API_KEY=sk-xxx \ +OPENAI_BASE_URL=https://api.deepseek.com/v1 \ +OPENAI_MODEL=deepseek-reasoner \ +bun run dev + +# 5. 确认现有测试不受影响 +bun test # 无 CLAUDE_CODE_USE_OPENAI 时走原有路径 +``` + +## 代码统计 + +| 类别 | 行数 | +|---|---| +| 新增源码 | ~620 行 | +| 新增测试 | ~450 行 | +| 改动现有代码 | ~25 行 | +| **总计** | **~1100 行** | diff --git a/docs/projects-collection.md b/docs/projects-collection.md new file mode 100644 index 0000000000..861b847733 --- /dev/null +++ b/docs/projects-collection.md @@ -0,0 +1,35 @@ +# 社区项目 & Blog 合集 + +> 每日更新,欢迎自荐! + +## 工具 & 应用 + +| 项目 | 描述 | 作者 | +|------|------|------| +| [4qtask.vercel.app](https://4qtask.vercel.app/) | 免费四象限时间管理工具 | @kevinhuky | +| [kaying.studio](https://kaying.studio/) | 个人 AI 工具箱 | @kayingai | +| [supsub.ai](https://supsub.ai/) | 高效阅读工具 | @hidumou | +| [x-video-download.net](https://x-video-download.net/) | 视频下载工具 | @syakadou | +| [1openapi.com](https://1openapi.com/) | API 中转站 | @thinker007 | +| [claw-z.com](https://claw-z.com/) | 一键部署 OpenClaw AI Agent(场景驱动、全面管理) | @uhhc | +| [gemini-watermark-remover.net](https://gemini-watermark-remover.net/) | Gemini 水印移除工具 | @syakadou | + +## GitHub 开源项目 + +| 项目 | 描述 | 作者 | +|------|------|------| +| [VersperClaw](https://github.com/versperai/VersperClaw) | 全自动科研流 | @versperai | +| [claude-reviews-claude](https://github.com/openedclaude/claude-reviews-claude) | 原汤化原食——Claude 如何看待眼中的老己 | @openedclaude | +| [agentica](https://github.com/shibing624/agentica) | 自研 Agent 框架,借鉴 claude-code 多 Agent 处理 | @shibing624 | +| [macman](https://github.com/tonngw/macman) | Mac 从 0 到 1 保姆级配置教程 | @tonngw | +| [SuperSpec](https://github.com/asasugar/SuperSpec) | SDD / Spec-Driven Development | @asasugar | +| [adnify](https://github.com/adnaan-worker/adnify) | 高颜值高定制化 AI 编辑器 | @adnaan-worker | +| [another-rule-engine](https://github.com/eatmoreduck/another-rule-engine) | 基于 Groovy 的开源多功能决策引擎 | @eatmoreduck | +| [creative_master](https://github.com/chatabc/creative_master) | AI 驱动的创意灵感管理工具 | @chatabc | +| [RapidDoc](https://github.com/RapidAI/RapidDoc) | Office 文件解析工具转 Markdown(支持 PDF/Image/Word/PPT/Excel) | @hzkitt | + +## Blog + +| 链接 | 作者 | +|------|------| +| [blog.xiaohuangyu.space](https://blog.xiaohuangyu.space/) | @eatmoreduck | diff --git a/mint.json b/mint.json index 0c9f0ebc85..e72c94b624 100644 --- a/mint.json +++ b/mint.json @@ -65,15 +65,6 @@ "docs/tools/task-management" ] }, - { - "group": "安全与权限", - "pages": [ - "docs/safety/why-safety-matters", - "docs/safety/permission-model", - "docs/safety/sandbox", - "docs/safety/plan-mode" - ] - }, { "group": "上下文工程", "pages": [ @@ -100,6 +91,16 @@ "docs/extensibility/custom-agents" ] }, + { + "group": "安全与权限", + "pages": [ + "docs/safety/why-safety-matters", + "docs/safety/permission-model", + "docs/safety/sandbox", + "docs/safety/plan-mode", + "docs/safety/auto-mode" + ] + }, { "group": "揭秘:隐藏功能与内部机制", "pages": [ @@ -113,12 +114,66 @@ "docs/features/debug-mode", "docs/features/buddy" ] + }, + { + "group": "隐藏功能详解", + "pages": [ + { + "group": "Agent 与协作", + "pages": [ + "docs/features/coordinator-mode", + "docs/features/fork-subagent", + "docs/features/daemon", + "docs/features/teammem" + ] + }, + { + "group": "运行模式", + "pages": [ + "docs/features/kairos", + "docs/features/voice-mode", + "docs/features/bridge-mode", + "docs/features/proactive", + "docs/features/ultraplan" + ] + }, + { + "group": "工具增强", + "pages": [ + "docs/features/mcp-skills", + "docs/features/tree-sitter-bash", + "docs/features/bash-classifier", + "docs/features/web-browser-tool", + "docs/features/experimental-skill-search" + ] + }, + { + "group": "上下文与自动化", + "pages": [ + "docs/features/token-budget", + "docs/features/context-collapse", + "docs/features/workflow-scripts" + ] + }, + "docs/features/tier3-stubs" + ] + }, + { + "group": "基础设施与依赖", + "pages": [ + "docs/auto-updater", + "docs/lsp-integration", + "docs/external-dependencies", + "docs/telemetry-remote-config-audit" + ] } ], "excludes": [ "docs/test-plans/**", "docs/testing-spec.md", - "docs/REVISION-PLAN.md" + "docs/REVISION-PLAN.md", + "docs/feature-exploration-plan.md", + "docs/ultraplan-implementation.md" ], "footerSocials": { "github": "https://github.com/anthropics/claude-code" diff --git a/package.json b/package.json index a10cf6c28e..20637d6fd1 100644 --- a/package.json +++ b/package.json @@ -25,19 +25,21 @@ "bun": ">=1.2.0" }, "bin": { - "ccb": "dist/cli.js" + "ccb": "dist/cli.js", + "claude-code-best": "dist/cli.js" }, "workspaces": [ "packages/*", "packages/@ant/*" ], "files": [ - "dist" + "dist", + "scripts/download-ripgrep.ts" ], "scripts": { "build": "bun run build.ts", "dev": "bun run scripts/dev.ts", - "dev:inspect": "bun --inspect-wait=localhost:8888/2dc3gzl5xot run scripts/dev.ts", + "dev:inspect": "bun run scripts/dev-debug.ts", "prepublishOnly": "bun run build", "lint": "biome lint src/", "lint:fix": "biome lint --fix src/", @@ -46,10 +48,12 @@ "test": "bun test", "check:unused": "knip-bun", "health": "bun run scripts/health-check.ts", + "postinstall": "node dist/download-ripgrep.js || bun run scripts/download-ripgrep.ts || true", "docs:dev": "npx mintlify dev" }, "dependencies": {}, "devDependencies": { + "openai": "^4.73.0", "@alcalzone/ansi-tokenize": "^0.3.0", "@ant/claude-for-chrome-mcp": "workspace:*", "@ant/computer-use-input": "workspace:*", diff --git a/scripts/dev-debug.ts b/scripts/dev-debug.ts new file mode 100644 index 0000000000..9a0dfaf328 --- /dev/null +++ b/scripts/dev-debug.ts @@ -0,0 +1,2 @@ +process.env.BUN_INSPECT="localhost:8888/2dc3gzl5xot" +await import("./dev") diff --git a/scripts/dev.ts b/scripts/dev.ts index 40fa6ff607..437508988c 100644 --- a/scripts/dev.ts +++ b/scripts/dev.ts @@ -15,7 +15,7 @@ const defineArgs = Object.entries(defines).flatMap(([k, v]) => [ // Bun --feature flags: enable feature() gates at runtime. // Default features enabled in dev mode. -const DEFAULT_FEATURES = ["BUDDY", "TRANSCRIPT_CLASSIFIER"]; +const DEFAULT_FEATURES = ["BUDDY", "TRANSCRIPT_CLASSIFIER", "BRIDGE_MODE", "AGENT_TRIGGERS_REMOTE"]; // Any env var matching FEATURE_=1 will also enable that feature. // e.g. FEATURE_PROACTIVE=1 bun run dev @@ -26,8 +26,13 @@ const envFeatures = Object.entries(process.env) const allFeatures = [...new Set([...DEFAULT_FEATURES, ...envFeatures])]; const featureArgs = allFeatures.flatMap((name) => ["--feature", name]); +// If BUN_INSPECT is set, pass --inspect-wait to the child process +const inspectArgs = process.env.BUN_INSPECT + ? ["--inspect-wait=" + process.env.BUN_INSPECT] + : []; + const result = Bun.spawnSync( - ["bun", "run", ...defineArgs, ...featureArgs, "src/entrypoints/cli.tsx", ...process.argv.slice(2)], + ["bun", ...inspectArgs, "run", ...defineArgs, ...featureArgs, "src/entrypoints/cli.tsx", ...process.argv.slice(2)], { stdio: ["inherit", "inherit", "inherit"] }, ); diff --git a/scripts/download-ripgrep.ts b/scripts/download-ripgrep.ts new file mode 100644 index 0000000000..fce392f6a9 --- /dev/null +++ b/scripts/download-ripgrep.ts @@ -0,0 +1,191 @@ +/** + * Download ripgrep binary from GitHub releases. + * + * Run automatically via `bun install` (postinstall hook), + * or manually: `bun run scripts/download-ripgrep.ts [--force]` + * + * Idempotent — skips download if the binary already exists. + * Use --force to re-download. + */ + +import { existsSync, mkdirSync, renameSync, rmSync, statSync } from 'fs' +import { chmodSync } from 'fs' +import { spawnSync } from 'child_process' +import * as path from 'path' +import { fileURLToPath } from 'url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +const RG_VERSION = '15.0.1' +const BASE_URL = `https://github.com/microsoft/ripgrep-prebuilt/releases/download/v${RG_VERSION}` + +// --- Platform mapping --- + +type PlatformMapping = { + target: string + ext: 'tar.gz' | 'zip' +} + +function getPlatformMapping(): PlatformMapping { + const arch = process.arch + const platform = process.platform + + if (platform === 'darwin') { + if (arch === 'arm64') return { target: 'aarch64-apple-darwin', ext: 'tar.gz' } + if (arch === 'x64') return { target: 'x86_64-apple-darwin', ext: 'tar.gz' } + throw new Error(`Unsupported macOS arch: ${arch}`) + } + + if (platform === 'win32') { + if (arch === 'x64') return { target: 'x86_64-pc-windows-msvc', ext: 'zip' } + if (arch === 'arm64') return { target: 'aarch64-pc-windows-msvc', ext: 'zip' } + throw new Error(`Unsupported Windows arch: ${arch}`) + } + + if (platform === 'linux') { + const isMusl = detectMusl() + if (arch === 'x64') { + // x64 Linux always uses musl (statically linked, most portable) + return { target: 'x86_64-unknown-linux-musl', ext: 'tar.gz' } + } + if (arch === 'arm64') { + return isMusl + ? { target: 'aarch64-unknown-linux-musl', ext: 'tar.gz' } + : { target: 'aarch64-unknown-linux-gnu', ext: 'tar.gz' } + } + throw new Error(`Unsupported Linux arch: ${arch}`) + } + + throw new Error(`Unsupported platform: ${platform}`) +} + +function detectMusl(): boolean { + const muslArch = process.arch === 'x64' ? 'x86_64' : 'aarch64' + try { + statSync(`/lib/libc.musl-${muslArch}.so.1`) + return true + } catch { + return false + } +} + +// --- Target vendor path (must match ripgrep.ts logic) --- + +function getVendorDir(): string { + const packageRoot = path.resolve(__dirname, '..') + + // Dev mode: package root has src/ directory + // ripgrep.ts at src/utils/ripgrep.ts: __dirname = src/utils/ + // vendor path = src/utils/vendor/ripgrep/ + if (existsSync(path.join(packageRoot, 'src'))) { + return path.resolve(packageRoot, 'src', 'utils', 'vendor', 'ripgrep') + } + + // Published mode: compiled chunks are flat in dist/ + // ripgrep chunk at dist/xxxx.js: __dirname = dist/ + // vendor path = dist/vendor/ripgrep/ + return path.resolve(packageRoot, 'dist', 'vendor', 'ripgrep') +} + +function getBinaryPath(): string { + const dir = getVendorDir() + const subdir = `${process.arch}-${process.platform}` + const binary = process.platform === 'win32' ? 'rg.exe' : 'rg' + return path.resolve(dir, subdir, binary) +} + +// --- Download & extract --- + +async function downloadAndExtract(): Promise { + const { target, ext } = getPlatformMapping() + const assetName = `ripgrep-v${RG_VERSION}-${target}.${ext}` + const downloadUrl = `${BASE_URL}/${assetName}` + + const binaryPath = getBinaryPath() + const binaryDir = path.dirname(binaryPath) + + // Idempotent: skip if binary exists and has content + const force = process.argv.includes('--force') + if (!force && existsSync(binaryPath)) { + const stat = statSync(binaryPath) + if (stat.size > 0) { + console.log(`[ripgrep] Binary already exists at ${binaryPath}, skipping.`) + return + } + } + + console.log(`[ripgrep] Downloading v${RG_VERSION} for ${target}...`) + console.log(`[ripgrep] URL: ${downloadUrl}`) + + // Prepare temp directory + const tmpDir = path.join(binaryDir, '.tmp-download') + rmSync(tmpDir, { recursive: true, force: true }) + mkdirSync(tmpDir, { recursive: true }) + + try { + const archivePath = path.join(tmpDir, assetName) + + // Download + const response = await fetch(downloadUrl, { redirect: 'follow' }) + if (!response.ok) { + throw new Error(`Download failed: ${response.status} ${response.statusText}`) + } + + const buffer = Buffer.from(await response.arrayBuffer()) + const { writeFileSync } = await import('fs') + writeFileSync(archivePath, buffer) + console.log(`[ripgrep] Downloaded ${Math.round(buffer.length / 1024)} KB`) + + // Extract + mkdirSync(binaryDir, { recursive: true }) + + if (ext === 'tar.gz') { + const result = spawnSync('tar', ['xzf', archivePath, '-C', tmpDir], { + stdio: 'pipe', + }) + if (result.status !== 0) { + throw new Error(`tar extract failed: ${result.stderr?.toString()}`) + } + } else { + // .zip + const result = spawnSync('unzip', ['-o', archivePath, '-d', tmpDir], { + stdio: 'pipe', + }) + if (result.status !== 0) { + throw new Error(`unzip failed: ${result.stderr?.toString()}`) + } + } + + // Find the rg binary in the extracted directory + // microsoft/ripgrep-prebuilt archives extract flat: ./rg (no subdirectory) + const extractedBinary = process.platform === 'win32' ? 'rg.exe' : 'rg' + const srcBinary = path.join(tmpDir, extractedBinary) + + if (!existsSync(srcBinary)) { + throw new Error(`Binary not found at expected path: ${srcBinary}`) + } + + // Move to final location + renameSync(srcBinary, binaryPath) + + // Make executable (non-Windows) + if (process.platform !== 'win32') { + chmodSync(binaryPath, 0o755) + } + + console.log(`[ripgrep] Installed to ${binaryPath}`) + } finally { + // Cleanup temp directory + rmSync(tmpDir, { recursive: true, force: true }) + } +} + +// --- Main --- + +downloadAndExtract().catch(error => { + console.error(`[ripgrep] Download failed: ${error.message}`) + console.error(`[ripgrep] You can install ripgrep manually: https://github.com/BurntSushi/ripgrep#installation`) + // Don't exit with error code — postinstall should not break bun install + process.exit(0) +}) diff --git a/src/bridge/peerSessions.ts b/src/bridge/peerSessions.ts index 57fa165496..c194c9b624 100644 --- a/src/bridge/peerSessions.ts +++ b/src/bridge/peerSessions.ts @@ -1,3 +1,84 @@ -// Auto-generated stub — replace with real implementation -export {}; -export const postInterClaudeMessage: (target: string, message: string) => Promise<{ ok: boolean; error?: string }> = () => Promise.resolve({ ok: false }); +import axios from 'axios' +import { logForDebugging } from '../utils/debug.js' +import { errorMessage } from '../utils/errors.js' +import { validateBridgeId } from './bridgeApi.js' +import { getBridgeAccessToken } from './bridgeConfig.js' +import { getReplBridgeHandle } from './replBridgeHandle.js' +import { toCompatSessionId } from './sessionIdCompat.js' + +/** + * Send a plain-text message to another Claude session via the bridge API. + * + * Called by SendMessageTool when the target address scheme is "bridge:". + * Uses the current ReplBridgeHandle to derive the sender identity and + * the session ingress URL for the POST request. + * + * @param target - Target session ID (from the "bridge:" address) + * @param message - Plain text message content (structured messages are rejected upstream) + * @returns { ok: true } on success, { ok: false, error } on failure. Never throws. + */ +export async function postInterClaudeMessage( + target: string, + message: string, +): Promise<{ ok: true } | { ok: false; error: string }> { + try { + const handle = getReplBridgeHandle() + if (!handle) { + return { ok: false, error: 'Bridge not connected' } + } + + const normalizedTarget = target.trim() + if (!normalizedTarget) { + return { ok: false, error: 'No target session specified' } + } + + const accessToken = getBridgeAccessToken() + if (!accessToken) { + return { ok: false, error: 'No access token available' } + } + + const compatTarget = toCompatSessionId(normalizedTarget) + // Validate against path traversal — same allowlist as bridgeApi.ts + validateBridgeId(compatTarget, 'target sessionId') + const from = toCompatSessionId(handle.bridgeSessionId) + const baseUrl = handle.sessionIngressUrl + + const url = `${baseUrl}/v1/sessions/${encodeURIComponent(compatTarget)}/messages` + + const response = await axios.post( + url, + { + type: 'peer_message', + from, + content: message, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + 'anthropic-version': '2023-06-01', + }, + timeout: 10_000, + validateStatus: (s: number) => s < 500, + }, + ) + + if (response.status === 200 || response.status === 204) { + logForDebugging( + `[bridge:peer] Message sent to ${compatTarget} (${response.status})`, + ) + return { ok: true } + } + + const detail = + typeof response.data === 'object' && response.data?.error?.message + ? response.data.error.message + : `HTTP ${response.status}` + logForDebugging(`[bridge:peer] Send failed: ${detail}`) + return { ok: false, error: detail } + } catch (err: unknown) { + const msg = errorMessage(err) + logForDebugging(`[bridge:peer] postInterClaudeMessage error: ${msg}`) + return { ok: false, error: msg } + } +} diff --git a/src/bridge/webhookSanitizer.ts b/src/bridge/webhookSanitizer.ts index c32323e0f2..a2999b07c8 100644 --- a/src/bridge/webhookSanitizer.ts +++ b/src/bridge/webhookSanitizer.ts @@ -1,3 +1,57 @@ -// Auto-generated stub — replace with real implementation -export {}; -export const sanitizeInboundWebhookContent: (content: string) => string = (content) => content; +/** + * Sanitize inbound GitHub webhook payload content before it enters the session. + * + * Called from useReplBridge.tsx when feature('KAIROS_GITHUB_WEBHOOKS') is enabled. + * Strips known secret patterns (tokens, API keys, credentials) while preserving + * the meaningful content (PR titles, descriptions, commit messages, etc.). + * + * Must be synchronous and never throw — on error, returns a safe placeholder. + */ + +/** Patterns that match known secret/token formats. */ +const SECRET_PATTERNS: Array<{ pattern: RegExp; replacement: string }> = [ + // GitHub tokens (PAT, OAuth, App, Server-to-server) + { pattern: /\b(ghp|gho|ghs|ghu|github_pat)_[A-Za-z0-9_]{10,}\b/g, replacement: '[REDACTED_GITHUB_TOKEN]' }, + // Anthropic API keys + { pattern: /\bsk-ant-[A-Za-z0-9_-]{10,}\b/g, replacement: '[REDACTED_ANTHROPIC_KEY]' }, + // Generic Bearer tokens in headers + { pattern: /(Bearer\s+)[A-Za-z0-9._\-/+=]{20,}/gi, replacement: '$1[REDACTED_TOKEN]' }, + // AWS access keys + { pattern: /\b(AKIA|ASIA)[A-Z0-9]{16}\b/g, replacement: '[REDACTED_AWS_KEY]' }, + // AWS secret keys (40-char base64-like strings after common labels) + { pattern: /(aws_secret_access_key|secret_key|SecretAccessKey)['":\s=]+[A-Za-z0-9/+=]{30,}/gi, replacement: '$1=[REDACTED_AWS_SECRET]' }, + // Generic API key patterns (key=value or "key": "value") + { pattern: /(api[_-]?key|apikey|secret|password|token|credential)['":\s=]+["']?[A-Za-z0-9._\-/+=]{16,}["']?/gi, replacement: '$1=[REDACTED]' }, + // npm tokens + { pattern: /\bnpm_[A-Za-z0-9]{36}\b/g, replacement: '[REDACTED_NPM_TOKEN]' }, + // Slack tokens + { pattern: /\bxox[bporas]-[A-Za-z0-9-]{10,}\b/g, replacement: '[REDACTED_SLACK_TOKEN]' }, +] + +/** Maximum content length before truncation (100KB). */ +const MAX_CONTENT_LENGTH = 100_000 + +export function sanitizeInboundWebhookContent(content: string): string { + try { + if (!content) return content + + let sanitized = content + + // Redact known secret patterns first (before truncation to avoid + // splitting a secret across the truncation boundary) + for (const { pattern, replacement } of SECRET_PATTERNS) { + pattern.lastIndex = 0 + sanitized = sanitized.replace(pattern, replacement) + } + + // Truncate excessively large payloads after redaction + if (sanitized.length > MAX_CONTENT_LENGTH) { + sanitized = sanitized.slice(0, MAX_CONTENT_LENGTH) + '\n... [truncated]' + } + + return sanitized + } catch { + // Never throw, never return raw content — return a safe placeholder + return '[webhook content redacted due to sanitization error]' + } +} diff --git a/src/buddy/CompanionCard.tsx b/src/buddy/CompanionCard.tsx new file mode 100644 index 0000000000..f9264acf3f --- /dev/null +++ b/src/buddy/CompanionCard.tsx @@ -0,0 +1,110 @@ +/** + * Companion display card — shown by /buddy (no args). + * Mirrors official vc8 component: bordered box with sprite, stats, last reaction. + */ +import React from 'react'; +import { Box, Text } from '../ink.js'; +import { useInput } from '../ink.js'; +import { renderSprite } from './sprites.js'; +import { RARITY_COLORS, RARITY_STARS, STAT_NAMES, type Companion } from './types.js'; + +const CARD_WIDTH = 40; +const CARD_PADDING_X = 2; + +function StatBar({ name, value }: { name: string; value: number }) { + const clamped = Math.max(0, Math.min(100, value)); + const filled = Math.round(clamped / 10); + const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(10 - filled); + return ( + + {name.padEnd(10)} {bar} {String(value).padStart(3)} + + ); +} + +export function CompanionCard({ + companion, + lastReaction, + onDone, +}: { + companion: Companion; + lastReaction?: string; + onDone?: (result?: string, options?: { display?: string }) => void; +}) { + const color = RARITY_COLORS[companion.rarity]; + const stars = RARITY_STARS[companion.rarity]; + const sprite = renderSprite(companion, 0); + + // Press any key to dismiss + useInput( + () => { + onDone?.(undefined, { display: 'skip' }); + }, + { isActive: onDone !== undefined }, + ); + + return ( + + {/* Header: rarity + species */} + + + {stars} {companion.rarity.toUpperCase()} + + {companion.species.toUpperCase()} + + + {/* Shiny indicator */} + {companion.shiny && ( + + {'\u2728'} SHINY {'\u2728'} + + )} + + {/* Sprite */} + + {sprite.map((line, i) => ( + + {line} + + ))} + + + {/* Name */} + {companion.name} + + {/* Personality */} + + + "{companion.personality}" + + + + {/* Stats */} + + {STAT_NAMES.map(name => ( + + ))} + + + {/* Last reaction */} + {lastReaction && ( + + last said + + + {lastReaction} + + + + )} + + ); +} diff --git a/src/buddy/companionReact.ts b/src/buddy/companionReact.ts new file mode 100644 index 0000000000..021167e0d6 --- /dev/null +++ b/src/buddy/companionReact.ts @@ -0,0 +1,160 @@ +/** + * Companion reaction system — aligns with official ZUK + Dc8 pattern. + * + * Called from REPL.tsx after each query turn. Checks mute state, frequency + * limits, and @-mention detection, then calls the buddy_react API to + * generate a reaction shown in the CompanionSprite speech bubble. + */ +import { getCompanion } from './companion.js' +import { getGlobalConfig } from '../utils/config.js' +import { getClaudeAIOAuthTokens } from '../utils/auth.js' +import { getOauthConfig } from '../constants/oauth.js' +import { getUserAgent } from '../utils/http.js' +import type { Message } from '../types/message.js' + +// ─── Rate limiting ────────────────────────────────── + +let lastReactTime = 0 +const MIN_INTERVAL_MS = 45_000 // official is roughly 30-60s + +// ─── Recent reactions (avoid repetition) ──────────── + +const recentReactions: string[] = [] +const MAX_RECENT = 8 + +// ─── Public API ───────────────────────────────────── + +/** + * Trigger a companion reaction after a query turn. + * + * Mirrors official `ZUK()`: + * 1. Check companion exists and is not muted + * 2. Detect if user @-mentioned companion by name + * 3. Apply rate limiting (skip if not addressed and too soon) + * 4. Build conversation transcript + * 5. Call buddy_react API + * 6. Pass reaction text to setReaction callback + */ +export function triggerCompanionReaction( + messages: Message[], + setReaction: (text: string | undefined) => void, +): void { + const companion = getCompanion() + if (!companion || getGlobalConfig().companionMuted) return + + const addressed = isAddressed(messages, companion.name) + + const now = Date.now() + if (!addressed && now - lastReactTime < MIN_INTERVAL_MS) return + + const transcript = buildTranscript(messages) + if (!transcript.trim()) return + + lastReactTime = now + + void callBuddyReactAPI(companion, transcript, addressed) + .then(reaction => { + if (!reaction) return + recentReactions.push(reaction) + if (recentReactions.length > MAX_RECENT) recentReactions.shift() + setReaction(reaction) + }) + .catch(() => {}) +} + +// ─── Helpers ──────────────────────────────────────── + +function isAddressed(messages: Message[], name: string): boolean { + const pattern = new RegExp(`\\b${escapeRegex(name)}\\b`, 'i') + for ( + let i = messages.length - 1; + i >= Math.max(0, messages.length - 3); + i-- + ) { + const m = messages[i] + if (m?.type !== 'user') continue + const content = (m as any).message?.content + if (typeof content === 'string' && pattern.test(content)) return true + } + return false +} + +function escapeRegex(s: string): string { + return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +} + +function buildTranscript(messages: Message[]): string { + return messages + .slice(-12) + .filter(m => m.type === 'user' || m.type === 'assistant') + .map(m => { + const role = m.type === 'user' ? 'user' : 'claude' + const content = (m as any).message?.content + const text = + typeof content === 'string' + ? content.slice(0, 300) + : Array.isArray(content) + ? content + .filter((b: any) => b?.type === 'text') + .map((b: any) => b.text) + .join(' ') + .slice(0, 300) + : '' + return `${role}: ${text}` + }) + .join('\n') + .slice(0, 5000) +} + +// ─── API call ─────────────────────────────────────── + +async function callBuddyReactAPI( + companion: { + name: string + personality: string + species: string + rarity: string + stats: Record + }, + transcript: string, + addressed: boolean, +): Promise { + const tokens = getClaudeAIOAuthTokens() + if (!tokens?.accessToken) return null + + const orgId = getGlobalConfig().oauthAccount?.organizationUuid + if (!orgId) return null + + const baseUrl = getOauthConfig().BASE_API_URL + const url = `${baseUrl}/api/organizations/${orgId}/claude_code/buddy_react` + + const resp = await fetch(url, { + method: 'POST', + headers: { + Authorization: `Bearer ${tokens.accessToken}`, + 'Content-Type': 'application/json', + 'User-Agent': getUserAgent(), + }, + body: JSON.stringify({ + name: companion.name.slice(0, 32), + personality: companion.personality.slice(0, 200), + species: companion.species, + rarity: companion.rarity, + stats: companion.stats, + transcript, + reason: addressed ? 'addressed' : 'turn', + recent: recentReactions.map(r => r.slice(0, 200)), + addressed, + }), + signal: AbortSignal.timeout(10_000), + }) + + if (!resp.ok) return null + + try { + const data = (await resp.json()) as { reaction?: string } + return data.reaction?.trim() || null + } catch { + return null + } +} diff --git a/src/commands/buddy/buddy.ts b/src/commands/buddy/buddy.ts index ef5057648a..8d14ab4e6a 100644 --- a/src/commands/buddy/buddy.ts +++ b/src/commands/buddy/buddy.ts @@ -1,16 +1,19 @@ +import React from 'react' import { getCompanion, rollWithSeed, generateSeed, } from '../../buddy/companion.js' -import { - type StoredCompanion, - RARITY_STARS, - STAT_NAMES, -} from '../../buddy/types.js' +import { type StoredCompanion, RARITY_STARS } from '../../buddy/types.js' import { renderSprite } from '../../buddy/sprites.js' +import { CompanionCard } from '../../buddy/CompanionCard.js' import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js' -import type { LocalCommandCall } from '../../types/command.js' +import { triggerCompanionReaction } from '../../buddy/companionReact.js' +import type { ToolUseContext } from '../../Tool.js' +import type { + LocalJSXCommandContext, + LocalJSXCommandOnDone, +} from '../../types/command.js' // Species → default name fragments for hatch (no API needed) const SPECIES_NAMES: Record = { @@ -39,198 +42,128 @@ const SPECIES_PERSONALITY: Record = { goose: 'Assertive and honks at bad code. Takes no prisoners in code reviews.', blob: 'Adaptable and goes with the flow. Sometimes splits into two when confused.', cat: 'Independent and judgmental. Watches you type with mild disdain.', - dragon: 'Fiery and passionate about architecture. Hoards good variable names.', - octopus: 'Multitasker extraordinaire. Wraps tentacles around every problem at once.', + dragon: + 'Fiery and passionate about architecture. Hoards good variable names.', + octopus: + 'Multitasker extraordinaire. Wraps tentacles around every problem at once.', owl: 'Wise but verbose. Always says "let me think about that" for exactly 3 seconds.', penguin: 'Cool under pressure. Slides gracefully through merge conflicts.', turtle: 'Patient and thorough. Believes slow and steady wins the deploy.', snail: 'Methodical and leaves a trail of useful comments. Never rushes.', - ghost: 'Ethereal and appears at the worst possible moments with spooky insights.', + ghost: + 'Ethereal and appears at the worst possible moments with spooky insights.', axolotl: 'Regenerative and cheerful. Recovers from any bug with a smile.', capybara: 'Zen master. Remains calm while everything around is on fire.', - cactus: 'Prickly on the outside but full of good intentions. Thrives on neglect.', + cactus: + 'Prickly on the outside but full of good intentions. Thrives on neglect.', robot: 'Efficient and literal. Processes feedback in binary.', rabbit: 'Energetic and hops between tasks. Finishes before you start.', mushroom: 'Quietly insightful. Grows on you over time.', - chonk: 'Big, warm, and takes up the whole couch. Prioritizes comfort over elegance.', + chonk: + 'Big, warm, and takes up the whole couch. Prioritizes comfort over elegance.', } function speciesLabel(species: string): string { return species.charAt(0).toUpperCase() + species.slice(1) } -function renderStats(stats: Record): string { - const lines = STAT_NAMES.map(name => { - const val = stats[name] ?? 0 - const filled = Math.round(val / 5) - const bar = '█'.repeat(filled) + '░'.repeat(20 - filled) - return ` ${name.padEnd(10)} ${bar} ${val}` - }) - return lines.join('\n') -} - -export const call: LocalCommandCall = async (args, _context) => { - const sub = args.trim().toLowerCase() - const config = getGlobalConfig() +export async function call( + onDone: LocalJSXCommandOnDone, + context: ToolUseContext & LocalJSXCommandContext, + args: string, +): Promise { + const sub = args?.trim().toLowerCase() ?? '' + const setState = context.setAppState - // /buddy — show current companion or hint to hatch - if (sub === '') { - const companion = getCompanion() - if (!companion) { - return { - type: 'text', - value: - "You don't have a companion yet! Use /buddy hatch to get one.", - } - } - const stars = RARITY_STARS[companion.rarity] - const sprite = renderSprite(companion, 0) - const shiny = companion.shiny ? ' ✨ Shiny!' : '' - - const lines = [ - sprite.join('\n'), - '', - ` ${companion.name} the ${speciesLabel(companion.species)}${shiny}`, - ` Rarity: ${stars} (${companion.rarity})`, - ` Eye: ${companion.eye} Hat: ${companion.hat}`, - companion.personality ? `\n "${companion.personality}"` : '', - '', - ' Stats:', - renderStats(companion.stats), - '', - ' Commands: /buddy pet /buddy mute /buddy unmute /buddy hatch /buddy rehatch', - ] - return { type: 'text', value: lines.join('\n') } + // ── /buddy off — mute companion ── + if (sub === 'off') { + saveGlobalConfig(cfg => ({ ...cfg, companionMuted: true })) + onDone('companion muted', { display: 'system' }) + return null } - // /buddy hatch — create a new companion - if (sub === 'hatch') { - if (config.companion) { - return { - type: 'text', - value: `You already have a companion! Use /buddy to see it.\n(Tip: /buddy hatch again will re-roll a new one.)`, - } - } - - const seed = generateSeed() - const r = rollWithSeed(seed) - const name = SPECIES_NAMES[r.bones.species] ?? 'Buddy' - const personality = - SPECIES_PERSONALITY[r.bones.species] ?? 'Mysterious and code-savvy.' - - const stored: StoredCompanion = { - name, - personality, - seed, - hatchedAt: Date.now(), - } - - saveGlobalConfig(cfg => ({ ...cfg, companion: stored })) - - const stars = RARITY_STARS[r.bones.rarity] - const sprite = renderSprite(r.bones, 0) - const shiny = r.bones.shiny ? ' ✨ Shiny!' : '' - - const lines = [ - ' 🎉 A wild companion appeared!', - '', - sprite.join('\n'), - '', - ` ${name} the ${speciesLabel(r.bones.species)}${shiny}`, - ` Rarity: ${stars} (${r.bones.rarity})`, - ` "${personality}"`, - '', - ' Your companion will now appear beside your input box!', - ] - return { type: 'text', value: lines.join('\n') } + // ── /buddy on — unmute companion ── + if (sub === 'on') { + saveGlobalConfig(cfg => ({ ...cfg, companionMuted: false })) + onDone('companion unmuted', { display: 'system' }) + return null } - // /buddy pet — trigger heart animation + // ── /buddy pet — trigger heart animation + auto unmute ── if (sub === 'pet') { const companion = getCompanion() if (!companion) { - return { - type: 'text', - value: - "You don't have a companion yet! Use /buddy hatch to get one.", - } + onDone('no companion yet \u00b7 run /buddy first', { display: 'system' }) + return null } - try { - const { setAppState } = await import('../../state/AppStateStore.js') - setAppState(prev => ({ - ...prev, - companionPetAt: Date.now(), - })) - } catch { - // non-interactive mode — AppState not available - } - - return { - type: 'text', - value: ` ${renderSprite(companion, 0).join('\n')}\n\n ${companion.name} purrs happily! ♥`, - } + // Auto-unmute on pet + trigger heart animation + saveGlobalConfig(cfg => ({ ...cfg, companionMuted: false })) + setState?.(prev => ({ ...prev, companionPetAt: Date.now() })) + + // Trigger a post-pet reaction + triggerCompanionReaction(context.messages ?? [], reaction => + setState?.(prev => + prev.companionReaction === reaction + ? prev + : { ...prev, companionReaction: reaction }, + ), + ) + + onDone(`petted ${companion.name}`, { display: 'system' }) + return null } - // /buddy mute - if (sub === 'mute') { - if (config.companionMuted) { - return { type: 'text', value: ' Companion is already muted.' } - } - saveGlobalConfig(cfg => ({ ...cfg, companionMuted: true })) - return { type: 'text', value: ' Companion muted. It will hide quietly. Use /buddy unmute to bring it back.' } - } + // ── /buddy (no args) — show existing or hatch ── + const companion = getCompanion() - // /buddy unmute - if (sub === 'unmute') { - if (!config.companionMuted) { - return { type: 'text', value: ' Companion is not muted.' } - } + // Auto-unmute when viewing + if (companion && getGlobalConfig().companionMuted) { saveGlobalConfig(cfg => ({ ...cfg, companionMuted: false })) - return { type: 'text', value: ' Companion unmuted! Welcome back.' } } - // /buddy rehatch — re-roll a new companion (replaces existing) - if (sub === 'rehatch') { - const seed = generateSeed() - const r = rollWithSeed(seed) - const name = SPECIES_NAMES[r.bones.species] ?? 'Buddy' - const personality = - SPECIES_PERSONALITY[r.bones.species] ?? 'Mysterious and code-savvy.' - - const stored: StoredCompanion = { - name, - personality, - seed, - hatchedAt: Date.now(), - } - - saveGlobalConfig(cfg => ({ ...cfg, companion: stored })) - - const stars = RARITY_STARS[r.bones.rarity] - const sprite = renderSprite(r.bones, 0) - const shiny = r.bones.shiny ? ' ✨ Shiny!' : '' - - const lines = [ - ' 🎉 A new companion appeared!', - '', - sprite.join('\n'), - '', - ` ${name} the ${speciesLabel(r.bones.species)}${shiny}`, - ` Rarity: ${stars} (${r.bones.rarity})`, - ` "${personality}"`, - '', - ' Your old companion has been replaced!', - ] - return { type: 'text', value: lines.join('\n') } + if (companion) { + // Return JSX card — matches official vc8 component + const lastReaction = context.getAppState?.()?.companionReaction + return React.createElement(CompanionCard, { + companion, + lastReaction, + onDone, + }) } - // Unknown subcommand - return { - type: 'text', - value: - ' Unknown command: /buddy ' + - sub + - '\n Commands: /buddy (info) /buddy hatch /buddy rehatch /buddy pet /buddy mute /buddy unmute', + // ── No companion → hatch ── + const seed = generateSeed() + const r = rollWithSeed(seed) + const name = SPECIES_NAMES[r.bones.species] ?? 'Buddy' + const personality = + SPECIES_PERSONALITY[r.bones.species] ?? 'Mysterious and code-savvy.' + + const stored: StoredCompanion = { + name, + personality, + seed, + hatchedAt: Date.now(), } + + saveGlobalConfig(cfg => ({ ...cfg, companion: stored })) + + const stars = RARITY_STARS[r.bones.rarity] + const sprite = renderSprite(r.bones, 0) + const shiny = r.bones.shiny ? ' \u2728 Shiny!' : '' + + const lines = [ + 'A wild companion appeared!', + '', + ...sprite, + '', + `${name} the ${speciesLabel(r.bones.species)}${shiny}`, + `Rarity: ${stars} (${r.bones.rarity})`, + `"${personality}"`, + '', + 'Your companion will now appear beside your input box!', + 'Say its name to get its take \u00b7 /buddy pet \u00b7 /buddy off', + ] + onDone(lines.join('\n'), { display: 'system' }) + return null } diff --git a/src/commands/buddy/index.ts b/src/commands/buddy/index.ts index dca9df82d5..8df6830281 100644 --- a/src/commands/buddy/index.ts +++ b/src/commands/buddy/index.ts @@ -1,10 +1,15 @@ import type { Command } from '../../commands.js' +import { isBuddyLive } from '../../buddy/useBuddyNotification.js' const buddy = { - type: 'local', + type: 'local-jsx', name: 'buddy', - description: 'View and manage your companion buddy', - supportsNonInteractive: false, + description: 'Hatch a coding companion · pet, off', + argumentHint: '[pet|off]', + immediate: true, + get isHidden() { + return !isBuddyLive() + }, load: () => import('./buddy.js'), } satisfies Command diff --git a/src/components/ConsoleOAuthFlow.tsx b/src/components/ConsoleOAuthFlow.tsx index 8d5fc122ca..03b6e0e7ab 100644 --- a/src/components/ConsoleOAuthFlow.tsx +++ b/src/components/ConsoleOAuthFlow.tsx @@ -12,7 +12,7 @@ import { sendNotification } from '../services/notifier.js'; import { OAuthService } from '../services/oauth/index.js'; import { getOauthAccountInfo, validateForceLoginOrg } from '../utils/auth.js'; import { logError } from '../utils/log.js'; -import { getSettings_DEPRECATED } from '../utils/settings/settings.js'; +import { getSettings_DEPRECATED, updateSettingsForSource } from '../utils/settings/settings.js'; import { Select } from './CustomSelect/select.js'; import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js'; import { Spinner } from './Spinner.js'; @@ -29,6 +29,24 @@ type OAuthStatus = { | { state: 'platform_setup'; } // Show platform setup info (Bedrock/Vertex/Foundry) +| { + state: 'custom_platform'; + baseUrl: string; + apiKey: string; + haikuModel: string; + sonnetModel: string; + opusModel: string; + activeField: 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model'; +} // Custom platform: configure API endpoint and model names +| { + state: 'openai_chat_api'; + baseUrl: string; + apiKey: string; + haikuModel: string; + sonnetModel: string; + opusModel: string; + activeField: 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model'; +} // OpenAI Chat Completions API platform | { state: 'ready_to_start'; } // Flow started, waiting for browser to open @@ -237,6 +255,8 @@ export function ConsoleOAuthFlow({ if (!orgResult.valid) { throw new Error((orgResult as { valid: false; message: string }).message); } + // Reset modelType to anthropic when using OAuth login + updateSettingsForSource('userSettings', { modelType: 'anthropic' } as any); setOAuthStatus({ state: 'success' }); @@ -325,7 +345,7 @@ export function ConsoleOAuthFlow({ } - + ; } @@ -343,6 +363,7 @@ type OAuthStatusMessageProps = { handleSubmitCode: (value: string, url: string) => void; setOAuthStatus: (status: OAuthStatus) => void; setLoginWithClaudeAi: (value: boolean) => void; + onDone: () => void; }; function OAuthStatusMessage(t0) { const $ = _c(51); @@ -359,7 +380,8 @@ function OAuthStatusMessage(t0) { textInputColumns, handleSubmitCode, setOAuthStatus, - setLoginWithClaudeAi + setLoginWithClaudeAi, + onDone } = t0; switch (oauthStatus.state) { case "idle": @@ -402,7 +424,13 @@ function OAuthStatusMessage(t0) { } let t6; if ($[5] === Symbol.for("react.memo_cache_sentinel")) { - t6 = [t4, t5, { + t6 = [{ + label: Custom Platform ·{" "}Configure your own API endpoint{"\n"}, + value: "custom_platform" + }, { + label: OpenAI Compatible ·{" "}Ollama, DeepSeek, vLLM, One API, etc.{"\n"}, + value: "openai_chat_api" + }, t4, t5, { label: 3rd-party platform ·{" "}Amazon Bedrock, Microsoft Foundry, or Vertex AI{"\n"}, value: "platform" }]; @@ -413,7 +441,29 @@ function OAuthStatusMessage(t0) { let t7; if ($[6] !== setLoginWithClaudeAi || $[7] !== setOAuthStatus) { t7 =