chore: remove obsolete test and vendor source, add CLI SDK docs
Browse files- Remove test_feishu_ws.ts (contained secrets)
- Remove vendor/audio-capture-src/index.ts (replaced by
packages/audio-capture-napi)
- Add docs/cli/ build system documentation
- Add src/cli/ SDK transport and handler modules
Co-Authored-By: Claude Big Pickle <noreply@anthropic.com>
- docs/cli/build-system.md +676 -0
- docs/cli/overview.md +266 -0
- src/cli/bg.ts +34 -0
- src/cli/exit.ts +25 -0
- src/cli/handlers/agents.ts +9 -0
- src/cli/handlers/ant.ts +34 -0
- src/cli/handlers/auth.ts +24 -0
- src/cli/handlers/autoMode.ts +170 -0
- src/cli/handlers/mcp.ts +9 -0
- src/cli/handlers/plugins.ts +59 -0
- src/cli/handlers/templateJobs.ts +34 -0
- src/cli/handlers/util.ts +11 -0
- src/cli/print.ts +255 -0
- src/cli/structuredIO.ts +6 -0
- src/cli/transports/HybridTransport.ts +27 -0
- src/cli/transports/SSETransport.ts +27 -0
- src/cli/transports/ccrClient.ts +27 -0
- src/cli/update.ts +9 -0
- test_feishu_ws.ts +0 -44
- vendor/audio-capture-src/index.ts +0 -147
docs/cli/build-system.md
ADDED
|
@@ -0,0 +1,676 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 构建系统与功能标记架构
|
| 2 |
+
|
| 3 |
+
## 1. 构建管道
|
| 4 |
+
|
| 5 |
+
构建入口位于 `scripts/build.ts`,基于 Bun 的原生打包工具 `bun build`。整个构建流程由 `package.json` 中的 npm scripts 驱动。
|
| 6 |
+
|
| 7 |
+
### 1.1 构建模式
|
| 8 |
+
|
| 9 |
+
| 命令 | 对应脚本参数 | 输出格式 | 说明 |
|
| 10 |
+
| --- | --- | --- | --- |
|
| 11 |
+
| `bun run build` | 无 | 源码产物 (`./cli`) | 默认构建,不编译为二进制 |
|
| 12 |
+
| `bun run build:dev` | `--dev` | 源码产物 (`./VersperClaw`) | 开发版本,自动附加 git SHA 和构建时间作为开发版本号 |
|
| 13 |
+
| `bun run build:dev:full` | `--dev --feature-set=dev-full` | 源码产物 (`./VersperClaw`) | 开发版本,启用全部实验性功能标记 |
|
| 14 |
+
| `bun run compile` | `--compile` | 二进制可执行文件 (`./dist/cli`) | 生产构建,编译为 Bun 原生二进制 |
|
| 15 |
+
| `bun run dev` | — | 直接运行 | 通过 `bun run ./src/entrypoints/cli.tsx` 直接执行,跳过构建步骤 |
|
| 16 |
+
|
| 17 |
+
### 1.2 构建流程步骤
|
| 18 |
+
|
| 19 |
+
1. **Friend 前端构建检查**:检查 `src/components/friend/frontend/dist/index.html` 是否存在,若不存在则调用 `npm run build` 构建 VRM 头像前端(该前端基于 Vite + React + Three.js,位于 `src/components/friend/frontend/`)。
|
| 20 |
+
2. **版本号计算**:
|
| 21 |
+
- 开发模式(`--dev`):使用 `git rev-parse --short=8 HEAD` 获取当前 commit SHA,生成格式为 `{baseVersion}-dev.{YYYYMMDD}.t{HHmmss}.sha{commit}` 的版本号。
|
| 22 |
+
- 生产模式:直接使用 `package.json` 中的版本字段 `2.1.0`。
|
| 23 |
+
- 同时获取最近的 20 条 git log 作为 changelog(仅开发模式)。
|
| 24 |
+
3. **编译时宏定义注入**:通过 `--define` 注入 `MACRO.*` 常量(见下文 2.1 节)。
|
| 25 |
+
4. **Bun 打包**:调用 `bun build`,以 `./src/entrypoints/cli.tsx` 为入口,使用以下关键参数:
|
| 26 |
+
- `--compile`:仅在 `compile` 模式下启用
|
| 27 |
+
- `--target bun`:目标运行时为 Bun
|
| 28 |
+
- `--format esm`:输出 ESM 格式
|
| 29 |
+
- `--minify`:启用代码压缩
|
| 30 |
+
- `--bytecode`:启用字节码缓存
|
| 31 |
+
- `--packages bundle`:将所有依赖打包进产物
|
| 32 |
+
- `--conditions bun`:使用 Bun 条件导出
|
| 33 |
+
5. **vendors 复制**:非编译模式下,将 `vendor/` 目录(包含 `audio-capture`、`opus-encdec` 等原生二进制库)复制到输出目录的 `vendor/` 子目录下。
|
| 34 |
+
6. **权限设置**:产物文件设置 `0o755` 可执行权限。
|
| 35 |
+
|
| 36 |
+
### 1.3 Friend VRM 前端构建集成
|
| 37 |
+
|
| 38 |
+
Friend 是桌面宠物 VRM 伴侣功能,其前端是一个独立的 Vite + React + Three.js 应用,位于 `src/components/friend/frontend/`。构建系统在主构建前检查其 `dist/` 目录是否已存在:
|
| 39 |
+
|
| 40 |
+
- 若已存在(如之前构建过),跳过前端构建步骤。
|
| 41 |
+
- 若不存在,自动执行 `npm run build`(调用 Vite 进行生产构建)。
|
| 42 |
+
- 构建产出包含 VRM 模型文件(`.vrm`)、FBX 动画文件、WASM 运行时(onnxruntime-web)、VAD 模型(silero_vad)和音效文件等静态资源。
|
| 43 |
+
|
| 44 |
+
此步骤是构建流程的**前置必要条件**——若 Friend 前端构建失败,整个构建过程退出并返回错误码 1。
|
| 45 |
+
|
| 46 |
+
### 1.4 外部依赖排除
|
| 47 |
+
|
| 48 |
+
以下 native 模块在构建时被声明为 `--external`,不会打包进最终产物,需在运行时由 Bun 动态解析:
|
| 49 |
+
|
| 50 |
+
- `@ant/*`(Anthropic 内部包)
|
| 51 |
+
- `audio-capture-napi`
|
| 52 |
+
- `image-processor-napi`
|
| 53 |
+
- `modifiers-napi`
|
| 54 |
+
- `url-handler-napi`
|
| 55 |
+
|
| 56 |
+
这些模块通过 workspace 管理或直接从 vendor 目录加载。
|
| 57 |
+
|
| 58 |
+
---
|
| 59 |
+
|
| 60 |
+
## 2. Feature Flag 系统
|
| 61 |
+
|
| 62 |
+
### 2.1 `bun:bundle` 编译时 Feature 标记
|
| 63 |
+
|
| 64 |
+
系统使用 Bun 内置的 `import { feature } from 'bun:bundle'` 实现编译时条件编译。`feature('NAME')` 是一个**编译时布尔常量**,在 `bun build` 阶段根据 `--feature=NAME` 参数确定值:
|
| 65 |
+
|
| 66 |
+
- 如果 `NAME` 在传递给 `bun build` 的 feature 集合中,`feature('NAME')` 求值为 `true`
|
| 67 |
+
- 否则求值为 `false`
|
| 68 |
+
- Bun 的打包器会对 `feature('NAME')` 条件分支进行**死代码消除**(Dead Code Elimination, DCE)
|
| 69 |
+
|
| 70 |
+
代码中典型的使用模式:
|
| 71 |
+
|
| 72 |
+
```typescript
|
| 73 |
+
import { feature } from 'bun:bundle'
|
| 74 |
+
|
| 75 |
+
// 编译时条件导入——false 分支在产物中完全不存在
|
| 76 |
+
const bridge = feature('BRIDGE_MODE')
|
| 77 |
+
? require('./commands/bridge/index.js').default
|
| 78 |
+
: null
|
| 79 |
+
|
| 80 |
+
// 编译时条件执行
|
| 81 |
+
if (feature('VOICE_MODE')) {
|
| 82 |
+
// 启用语音模式的代码
|
| 83 |
+
}
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
### 2.2 Feature 传递机制
|
| 87 |
+
|
| 88 |
+
Feature 标记通过三种方式传递给构建系统:
|
| 89 |
+
|
| 90 |
+
1. **编译参数**:`bun run ./scripts/build.ts --feature=NAME`
|
| 91 |
+
2. **批量设置**:`--feature-set=dev-full` 启用 `fullExperimentalFeatures` 数组中的所有标记
|
| 92 |
+
3. **默认集合**:`defaultFeatures = ['VOICE_MODE']` 始终包含在构建中
|
| 93 |
+
|
| 94 |
+
构建脚本中的优先级逻辑:
|
| 95 |
+
|
| 96 |
+
```typescript
|
| 97 |
+
const defaultFeatures = ['VOICE_MODE']
|
| 98 |
+
const featureSet = new Set(defaultFeatures)
|
| 99 |
+
// 解析 --feature-set=dev-full
|
| 100 |
+
// 解析 --feature NAME
|
| 101 |
+
// 解析 --feature=NAME
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
最终所有选中的 feature 通过 `--feature=${feature}` 逐条传递给 `bun build` 命��。
|
| 105 |
+
|
| 106 |
+
### 2.3 编译时宏注入
|
| 107 |
+
|
| 108 |
+
除 `feature()` 系统外,构建脚本还通过 `--define` 注入一系列编译时常量(`MACRO.*`)。这些常量在 TypeScript 环境中通过 `env.d.ts` 声明类型:
|
| 109 |
+
|
| 110 |
+
| 宏 | 含义 | 值来源 |
|
| 111 |
+
| --- | --- | --- |
|
| 112 |
+
| `MACRO.VERSION` | 当前版本号 | `package.json` version 或开发版 git 版本 |
|
| 113 |
+
| `MACRO.BUILD_TIME` | 构建时间 | `new Date().toISOString()` |
|
| 114 |
+
| `MACRO.PACKAGE_URL` | 包 URL | `package.json` name |
|
| 115 |
+
| `MACRO.NATIVE_PACKAGE_URL` | 原生包 URL | 始终为 `undefined`(外部构建) |
|
| 116 |
+
| `MACRO.FEEDBACK_CHANNEL` | 反馈渠道 | 固定为 `'github'` |
|
| 117 |
+
| `MACRO.ISSUES_EXPLAINER` | Issue 说明文本 | 固定字符串 |
|
| 118 |
+
| `MACRO.VERSION_CHANGELOG` | 版本变更日志 | 开发模式取 git log;生产模式固定 URL |
|
| 119 |
+
|
| 120 |
+
除 `MACRO.*` 外,以下 `process.env` 变量也在构建时注入:
|
| 121 |
+
|
| 122 |
+
| 变量 | 值 | 说明 |
|
| 123 |
+
| --- | --- | --- |
|
| 124 |
+
| `process.env.USER_TYPE` | `'external'` | 标记为外部构建(非 Anthropic 内部) |
|
| 125 |
+
| `process.env.CLAUDE_CODE_FORCE_FULL_LOGO` | `'true'` | 强制显示完整 logo |
|
| 126 |
+
| `process.env.NODE_ENV` | `'development'`(仅 dev 模式) | 运行时环境标识 |
|
| 127 |
+
| `process.env.CLAUDE_CODE_EXPERIMENTAL_BUILD` | `'true'`(仅 dev 模式) | 标记为实验性构建 |
|
| 128 |
+
| `process.env.CLAUDE_CODE_VERIFY_PLAN` | `'false'` | 禁用计划验证 |
|
| 129 |
+
| `process.env.CCR_FORCE_BUNDLE` | `'true'` | 强制打包 CCR 相关代码 |
|
| 130 |
+
|
| 131 |
+
### 2.4 运行时环境变量动态配置
|
| 132 |
+
|
| 133 |
+
开发环境中,`preload.ts` 在运行时会覆盖 `MACRO` 的值,使用环境变量进行本地开发配置:
|
| 134 |
+
|
| 135 |
+
```typescript
|
| 136 |
+
const version = process.env.CLAUDE_CODE_LOCAL_VERSION ?? '999.0.0-local'
|
| 137 |
+
// ...
|
| 138 |
+
Object.assign(globalThis, {
|
| 139 |
+
MACRO: {
|
| 140 |
+
VERSION: version,
|
| 141 |
+
PACKAGE_URL: packageUrl,
|
| 142 |
+
NATIVE_PACKAGE_URL: packageUrl,
|
| 143 |
+
BUILD_TIME: buildTime,
|
| 144 |
+
FEEDBACK_CHANNEL: 'local',
|
| 145 |
+
// ...
|
| 146 |
+
},
|
| 147 |
+
})
|
| 148 |
+
```
|
| 149 |
+
|
| 150 |
+
### 2.5 Feature 死代码消除机制详解
|
| 151 |
+
|
| 152 |
+
`feature()` 的死代码消除分为两种形态:
|
| 153 |
+
|
| 154 |
+
**形态 A — 条件导入(模块级 DCE)**:
|
| 155 |
+
|
| 156 |
+
```typescript
|
| 157 |
+
const remoteControlServerCommand =
|
| 158 |
+
feature('DAEMON') && feature('BRIDGE_MODE')
|
| 159 |
+
? require('./commands/remoteControlServer/index.js').default
|
| 160 |
+
: null
|
| 161 |
+
```
|
| 162 |
+
|
| 163 |
+
当 `DAEMON` 或 `BRIDGE_MODE` 未启用时,`require()` 调用和整个依赖图(包括被导入模块的所有递归依赖)均被消除。这是最有效的 DCE 形式,可大幅减小产物体积。
|
| 164 |
+
|
| 165 |
+
**形态 B — 条件执行(语句级 DCE)**:
|
| 166 |
+
|
| 167 |
+
```typescript
|
| 168 |
+
if (feature('VOICE_MODE')) {
|
| 169 |
+
// 语音相关逻辑
|
| 170 |
+
}
|
| 171 |
+
```
|
| 172 |
+
|
| 173 |
+
当 `VOICE_MODE` 未启用时,整个 `if` 块被消除。但被调用的模块如果已在前面的代码中被无条件导入,则仍会保留在产物中。
|
| 174 |
+
|
| 175 |
+
### 2.6 完整 Feature Flag 列表
|
| 176 |
+
|
| 177 |
+
以下是从代码库中提取的所有 `feature('NAME')` 调用,按类别分组:
|
| 178 |
+
|
| 179 |
+
#### 语音与输入
|
| 180 |
+
|
| 181 |
+
| Feature | 用途 | 涉及文件数 |
|
| 182 |
+
| --- | --- | --- |
|
| 183 |
+
| `VOICE_MODE` | 语音模式:语音录制、流式 STT(语音转文字)、语音状态指示器、VAD(语音活动检测);默认启用 | ~60+ 处调用 |
|
| 184 |
+
| `NATIVE_CLIPBOARD_IMAGE` | 原生剪贴板图片支持 | 1 |
|
| 185 |
+
|
| 186 |
+
#### 助手/Kairos 会话模式
|
| 187 |
+
|
| 188 |
+
| Feature | 用途 | 涉及文件数 |
|
| 189 |
+
| --- | --- | --- |
|
| 190 |
+
| `KAIROS` | Kairos 助手模式:会话管理、--session-id、--continue 参数、assistant 命令/模块、团队上下文、消息队列管理、daily-log 提示词 | ~100+ 处调用(最广泛) |
|
| 191 |
+
| `KAIROS_BRIEF` | Brief 摘要模式:简化版助手界面、brief 命令、SendUserMessage 替代 | ~25 处调用 |
|
| 192 |
+
| `KAIROS_CHANNELS` | Kairos 频道/通道系统:消息通道路由 | ~8 处调用 |
|
| 193 |
+
| `KAIROS_PUSH_NOTIFICATION` | 推送通知功能 | ~4 处调用 |
|
| 194 |
+
| `KAIROS_GITHUB_WEBHOOKS` | GitHub Webhook 订阅功能(subscribe-pr 命令) | ~3 处调用 |
|
| 195 |
+
| `KAIROS_BRIEF` | 同上(独立用途) | — |
|
| 196 |
+
|
| 197 |
+
#### 桥接/远程控制
|
| 198 |
+
|
| 199 |
+
| Feature | 用途 | 涉及文件数 |
|
| 200 |
+
| --- | --- | --- |
|
| 201 |
+
| `BRIDGE_MODE` | 桥接模式:CLI 与 mobile/web 客户端通信、remote-control 子命令 | ~30 处调用 |
|
| 202 |
+
| `CCR_AUTO_CONNECT` | CCR(Claude Code Remote)自动连接 | 2 |
|
| 203 |
+
| `CCR_MIRROR` | CCR 镜像模式 | ~5 处调用 |
|
| 204 |
+
| `CCR_REMOTE_SETUP` | 远程设置向导(web 命令) | 2 |
|
| 205 |
+
| `DAEMON` | 守护进程模式:daemon worker、后台长期运行 | 4 |
|
| 206 |
+
| `DIRECT_CONNECT` | 直接连接模式(URL/二维码直接连接) | ~8 处调用 |
|
| 207 |
+
| `SSH_REMOTE` | SSH 远程模式:通过 SSH 连接远程会话 | ~6 处调用 |
|
| 208 |
+
| `UDS_INBOX` | Unix Domain Socket 收件箱:对等节点发现、跨进程消息传递 | ~30 处调用 |
|
| 209 |
+
|
| 210 |
+
#### 协作与团队
|
| 211 |
+
|
| 212 |
+
| Feature | 用途 | 涉及文件数 |
|
| 213 |
+
| --- | --- | --- |
|
| 214 |
+
| `COORDINATOR_MODE` | 协调者模式:多 agent 协作、任务分配、worker 管理 | ~20 处调用 |
|
| 215 |
+
| `TEAMMEM` | 团队记忆系统:共享记忆文件、团队上下文读取/搜索/写入 | ~25 处调用 |
|
| 216 |
+
| `BUDDY` | 桌面宠物伴侣:CompanionSprite 渲染、提示词注入、通知 | ~15 处调用 |
|
| 217 |
+
| `FORK_SUBAGENT` | 子 agent 分支:fork 命令、agent 分支执行 | ~3 处调用 |
|
| 218 |
+
| `AGENT_TRIGGERS` | Agent 触发器:cron 定时任务(Create/Delete/List) | ~8 处调用 |
|
| 219 |
+
| `AGENT_TRIGGERS_REMOTE` | 远程触发器 | ~3 处调用 |
|
| 220 |
+
| `AGENT_MEMORY_SNAPSHOT` | Agent 记忆快照:自定义 agent 记忆持久化 | 2 |
|
| 221 |
+
| `COWORKER_TYPE_TELEMETRY` | 同事类型遥测 | 1 |
|
| 222 |
+
|
| 223 |
+
#### 权限与安全
|
| 224 |
+
|
| 225 |
+
| Feature | 用途 | 涉及文件数 |
|
| 226 |
+
| --- | --- | --- |
|
| 227 |
+
| `TRANSCRIPT_CLASSIFIER` | 转录分类器:自动模式(auto mode)、权限模式扩展、YOLO 分类器 | ~60 处调用 |
|
| 228 |
+
| `BASH_CLASSIFIER` | Bash 命令分类器:对 bash 命令进行安全分类、自动批准 | ~20 处调用 |
|
| 229 |
+
| `POWERSHELL_AUTO_MODE` | PowerShell 自动模式 | ~3 处调用 |
|
| 230 |
+
| `PROACTIVE` | 主动模式:模型主动发起交互、背景任务提示 | ~25 处调用 |
|
| 231 |
+
| `BYOC_ENVIRONMENT_RUNNER` | BYOC 环境运行器 | 1 |
|
| 232 |
+
| `SELF_HOSTED_RUNNER` | 自托管运行器 | 1 |
|
| 233 |
+
|
| 234 |
+
#### 上下文管理
|
| 235 |
+
|
| 236 |
+
| Feature | 用途 | 涉及文件数 |
|
| 237 |
+
| --- | --- | --- |
|
| 238 |
+
| `CONTEXT_COLLAPSE` | 上下文折叠:长上下文管理、413 错误处理 | ~15 处调用 |
|
| 239 |
+
| `CACHED_MICROCOMPACT` | 缓存微压缩:在对话流中增量压缩上下文 | ~15 处调用 |
|
| 240 |
+
| `COMPACTION_REMINDERS` | 压缩提醒 | 1 |
|
| 241 |
+
| `HISTORY_SNIP` | 历史摘要截取:SnipTool、会话上下文裁剪 | ~10 处调用 |
|
| 242 |
+
| `HISTORY_PICKER` | 历史选择器:对话历史浏览 | ~5 处调用 |
|
| 243 |
+
| `REACTIVE_COMPACT` | 响应式压缩 | 3 |
|
| 244 |
+
| `PROMPT_CACHE_BREAK_DETECTION` | 提示缓存断裂检测 | 3 |
|
| 245 |
+
| `BREAK_CACHE_COMMAND` | 缓存断裂命令 | 2 |
|
| 246 |
+
| `EXTRACT_MEMORIES` | 记忆提取:会话结束时的自动记忆提取 | ~10 处调用 |
|
| 247 |
+
| `MEMORY_SHAPE_TELEMETRY` | 记忆形状遥测 | 4 |
|
| 248 |
+
|
| 249 |
+
#### 工具
|
| 250 |
+
|
| 251 |
+
| Feature | 用途 | 涉及文件数 |
|
| 252 |
+
| --- | --- | --- |
|
| 253 |
+
| `WORKFLOW_SCRIPTS` | 工作流脚本:WorkflowTool、workflows 命令、后台工作流任务 | ~15 处调用 |
|
| 254 |
+
| `MONITOR_TOOL` | 监控工具:MonitorMcpTask、后台监控 | ~8 处调用 |
|
| 255 |
+
| `WEB_BROWSER_TOOL` | Web 浏览器工具:基于 Bun WebView 的浏览器 | ~4 处调用 |
|
| 256 |
+
| `OVERFLOW_TEST_TOOL` | 溢出测试工具 | 3 |
|
| 257 |
+
| `TERMINAL_PANEL` | 终端面板工具 | 2 |
|
| 258 |
+
| `TORCH` | Torch 命令 | 2 |
|
| 259 |
+
| `ULTRAPLAN` | 超计划模式:增强规划能力 | ~10 处调用 |
|
| 260 |
+
| `ULTRATHINK` | 超思考模式 | 1 |
|
| 261 |
+
| `VERIFICATION_AGENT` | 验证 agent | 2 |
|
| 262 |
+
| `EXPERIMENTAL_SKILL_SEARCH` | 实验性技能搜索 | ~10 处调用 |
|
| 263 |
+
| `SKILL_IMPROVEMENT` | 技能改进反馈 | 1 |
|
| 264 |
+
| `TEMPLATES` | 模板系统:new/list/reply 命令 | ~6 处调用 |
|
| 265 |
+
|
| 266 |
+
#### 构建与发布
|
| 267 |
+
|
| 268 |
+
| Feature | 用途 | 涉及文件数 |
|
| 269 |
+
| --- | --- | --- |
|
| 270 |
+
| `ABLATION_BASELINE` | 消融实验基线:设置多个环境变量 | 1 |
|
| 271 |
+
| `ALLOW_TEST_VERSIONS` | 允许测试版本(99.99.x) | 2 |
|
| 272 |
+
| `DUMP_SYSTEM_PROMPT` | 导出系统提示词(`--dump-system-prompt`) | 1 |
|
| 273 |
+
| `NEW_INIT` | 新的初始化流程 | 2 |
|
| 274 |
+
|
| 275 |
+
#### 用户界面
|
| 276 |
+
|
| 277 |
+
| Feature | 用途 | 涉及文件数 |
|
| 278 |
+
| --- | --- | --- |
|
| 279 |
+
| `MESSAGE_ACTIONS` | 消息操作:消息级交互操作 | ~4 处调用 |
|
| 280 |
+
| `QUICK_SEARCH` | 快速搜索:PromptInput 内联搜索 | ~5 处调用 |
|
| 281 |
+
| `MCP_RICH_OUTPUT` | MCP 富文本输出 | 3 |
|
| 282 |
+
| `AUTO_THEME` | 自动主题:跟随终端主题变化 | 1 |
|
| 283 |
+
| `BUDDY` | 同上(见协作与团队分组) | — |
|
| 284 |
+
|
| 285 |
+
#### 内部与实验性
|
| 286 |
+
|
| 287 |
+
| Feature | 用途 | 涉及文件数 |
|
| 288 |
+
| --- | --- | --- |
|
| 289 |
+
| `ANTI_DISTILLATION_CC` | 反蒸馏保护 | 1 |
|
| 290 |
+
| `BG_SESSIONS` | 后台会话:ps/logs/attach/kill 命令 | ~8 处调用 |
|
| 291 |
+
| `BUILDING_CLAUDE_APPS` | 构建 Claude Apps 技能 | 1 |
|
| 292 |
+
| `BUILTIN_EXPLORE_PLAN_AGENTS` | 内置探索/计划 agent | 1 |
|
| 293 |
+
| `CHICAGO_MCP` | Chicago MCP 协议:MCP 配置、computer-use-mcp 入口 | ~10 处调用 |
|
| 294 |
+
| `COMMIT_ATTRIBUTION` | 提交归属追踪 | ~4 处调用 |
|
| 295 |
+
| `CONNECTOR_TEXT` | Connector 文本块处理 | ~6 处调用 |
|
| 296 |
+
| `DOWNLOAD_USER_SETTINGS` | 下载用户设置 | 1 |
|
| 297 |
+
| `FILE_PERSISTENCE` | 文件持久化(public API / sessions) | 1 |
|
| 298 |
+
| `HARD_FAIL` | 硬失败模式 | 1 |
|
| 299 |
+
| `HOOK_PROMPTS` | Hook 提示词注入 | 1 |
|
| 300 |
+
| `IS_LIBC_GLIBC` | 检测是否使用 glibc | 1 |
|
| 301 |
+
| `IS_LIBC_MUSL` | 检测是否使用 musl libc | 1 |
|
| 302 |
+
| `LODESTONE` | LODESTONE 协议注册 | ~6 处调用 |
|
| 303 |
+
| `MCP_SKILLS` | MCP 技能 | 2 |
|
| 304 |
+
| `NATIVE_CLIENT_ATTESTATION` | 原生客户端认证 | 1 |
|
| 305 |
+
| `PERFETTO_TRACING` | Perfetto 性能追踪 | 1 |
|
| 306 |
+
| `REVIEW_ARTIFACT` | 审查构件技能 | 1 |
|
| 307 |
+
| `RUN_SKILL_GENERATOR` | 运行技能生成器 | 1 |
|
| 308 |
+
| `SHOT_STATS` | 射击统计(对话轮次分布) | 3 |
|
| 309 |
+
| `SLOW_OPERATION_LOGGING` | 慢操作日志 | 1 |
|
| 310 |
+
| `TOKEN_BUDGET` | Token 预算跟踪 | ~10 处调用 |
|
| 311 |
+
| `TREE_SITTER_BASH` | Tree-sitter Bash 解析器 | 1 |
|
| 312 |
+
| `TREE_SITTER_BASH_SHADOW` | Tree-sitter Bash 影子解析 | 1 |
|
| 313 |
+
| `UNATTENDED_RETRY` | 无人值守重试 | 1 |
|
| 314 |
+
| `UPLOAD_USER_SETTINGS` | 上传用户设置 | 1 |
|
| 315 |
+
|
| 316 |
+
### 2.7 实验性功能全集(`fullExperimentalFeatures`)
|
| 317 |
+
|
| 318 |
+
在 `scripts/build.ts` 中定义了一个`fullExperimentalFeatures` 常量数组,通过 `--feature-set=dev-full` 批量启用。完整列表:
|
| 319 |
+
|
| 320 |
+
```
|
| 321 |
+
AGENT_MEMORY_SNAPSHOT, AGENT_TRIGGERS, AGENT_TRIGGERS_REMOTE,
|
| 322 |
+
AWAY_SUMMARY, BASH_CLASSIFIER, BUDDY, BRIDGE_MODE,
|
| 323 |
+
BUILTIN_EXPLORE_PLAN_AGENTS, CACHED_MICROCOMPACT,
|
| 324 |
+
CCR_AUTO_CONNECT, CCR_MIRROR, CCR_REMOTE_SETUP,
|
| 325 |
+
COMPACTION_REMINDERS, CONNECTOR_TEXT, EXTRACT_MEMORIES,
|
| 326 |
+
HISTORY_PICKER, HOOK_PROMPTS, KAIROS_BRIEF, KAIROS_CHANNELS,
|
| 327 |
+
LODESTONE, MCP_RICH_OUTPUT, MESSAGE_ACTIONS, NATIVE_CLIPBOARD_IMAGE,
|
| 328 |
+
NEW_INIT, POWERSHELL_AUTO_MODE, PROMPT_CACHE_BREAK_DETECTION,
|
| 329 |
+
QUICK_SEARCH, SHOT_STATS, TEAMMEM, TOKEN_BUDGET, TREE_SITTER_BASH,
|
| 330 |
+
TREE_SITTER_BASH_SHADOW, TRANSCRIPT_CLASSIFIER, ULTRAPLAN, ULTRATHINK,
|
| 331 |
+
UNATTENDED_RETRY, VERIFICATION_AGENT, VOICE_MODE
|
| 332 |
+
```
|
| 333 |
+
|
| 334 |
+
注意:`VOICE_MODE` 同时出现在默认集合 `defaultFeatures` 和实验性集合中,此重复不会造成问题(Set 去重)。
|
| 335 |
+
|
| 336 |
+
---
|
| 337 |
+
|
| 338 |
+
## 3. 命令可用性门控
|
| 339 |
+
|
| 340 |
+
### 3.1 `availability` 声明
|
| 341 |
+
|
| 342 |
+
每个命令可以通过 `availability` 字段声明其适用的认证/供应商环境。`src/types/command.ts` 中定义了 `CommandAvailability` 类型:
|
| 343 |
+
|
| 344 |
+
```typescript
|
| 345 |
+
export type CommandAvailability =
|
| 346 |
+
| 'claude-ai' // claude.ai OAuth 订阅用户(Pro/Max/Team/Enterprise)
|
| 347 |
+
| 'console' // Console API key 用户(直接使用 api.anthropic.com)
|
| 348 |
+
```
|
| 349 |
+
|
| 350 |
+
可用性声明的命令示例:
|
| 351 |
+
|
| 352 |
+
```typescript
|
| 353 |
+
// src/commands/usage/index.ts
|
| 354 |
+
{ name: 'usage', availability: ['claude-ai'], ... }
|
| 355 |
+
|
| 356 |
+
// src/commands/fast/index.ts
|
| 357 |
+
{ name: 'fast', availability: ['claude-ai', 'console'], ... }
|
| 358 |
+
```
|
| 359 |
+
|
| 360 |
+
`availability` 与 `isEnabled()` 的职责分离:
|
| 361 |
+
- **`availability`** = 谁能使用(基于认证/供应商的静态检查)
|
| 362 |
+
- **`isEnabled()`** = 当前是否开启(基于 feature flags、GrowthBook、环境变量等动态条件)
|
| 363 |
+
|
| 364 |
+
不存在 `availability` 字段的命令被视为通用命令,在所有环境中可用。
|
| 365 |
+
|
| 366 |
+
### 3.2 `meetsAvailabilityRequirement()` 检查链
|
| 367 |
+
|
| 368 |
+
`src/commands.ts` 中实现了 `meetsAvailabilityRequirement()` 函数,负责检查命令是否满足可用性要求:
|
| 369 |
+
|
| 370 |
+
```typescript
|
| 371 |
+
export function meetsAvailabilityRequirement(cmd: Command): boolean {
|
| 372 |
+
if (!cmd.availability) return true // 无限制
|
| 373 |
+
for (const a of cmd.availability) {
|
| 374 |
+
switch (a) {
|
| 375 |
+
case 'claude-ai':
|
| 376 |
+
if (isClaudeAISubscriber()) return true
|
| 377 |
+
break
|
| 378 |
+
case 'console':
|
| 379 |
+
if (!isClaudeAISubscriber() && !isUsing3PServices() && isFirstPartyAnthropicBaseUrl())
|
| 380 |
+
return true
|
| 381 |
+
break
|
| 382 |
+
}
|
| 383 |
+
}
|
| 384 |
+
return false
|
| 385 |
+
}
|
| 386 |
+
```
|
| 387 |
+
|
| 388 |
+
检查逻辑:
|
| 389 |
+
- **`claude-ai`**:用户通过 claude.ai OAuth 认证且为订阅用户(Pro/Max/Team/Enterprise)
|
| 390 |
+
- **`console`**:用户非 claude.ai 订阅用户、不使用第三方服务(Bedrock/Vertex/Foundry)、且使用官方 Anthropic API base URL
|
| 391 |
+
|
| 392 |
+
该检查在命令列表构建时执行,且**不缓存**——因为认证状态可在会话中变化(例如通过 `/login` 命令)。
|
| 393 |
+
|
| 394 |
+
### 3.3 `getCommands()` 中的过滤链
|
| 395 |
+
|
| 396 |
+
命令的最终可用性由 `getCommands()` 函数(`src/commands.ts`)计算,过滤链如下:
|
| 397 |
+
|
| 398 |
+
```typescript
|
| 399 |
+
const allCommands = await loadAllCommands(cwd)
|
| 400 |
+
const baseCommands = allCommands.filter(
|
| 401 |
+
_ => meetsAvailabilityRequirement(_) && isCommandEnabled(_),
|
| 402 |
+
)
|
| 403 |
+
```
|
| 404 |
+
|
| 405 |
+
每个命令需**同时满足**:
|
| 406 |
+
1. `meetsAvailabilityRequirement()` — 认证/供应商匹配
|
| 407 |
+
2. `isCommandEnabled()` — 命令级启用检查(可关连 feature flag 或动态条件)
|
| 408 |
+
|
| 409 |
+
### 3.4 命令级 `isEnabled()` / `isHidden()` 控制
|
| 410 |
+
|
| 411 |
+
除了全局的 feature flag 和 availability 机制,每个命令还可以单独定义 `isEnabled()` 和 `isHidden`:
|
| 412 |
+
|
| 413 |
+
**`isEnabled()` 使用示例**:
|
| 414 |
+
|
| 415 |
+
```typescript
|
| 416 |
+
// src/commands/voice/index.ts
|
| 417 |
+
{ isEnabled: () => isVoiceGrowthBookEnabled() }
|
| 418 |
+
|
| 419 |
+
// src/commands/review.ts
|
| 420 |
+
{ isEnabled: () => isUltrareviewEnabled() }
|
| 421 |
+
|
| 422 |
+
// src/commands/session/index.ts
|
| 423 |
+
{ isEnabled: () => getIsRemoteMode() }
|
| 424 |
+
|
| 425 |
+
// src/commands/extra-usage/index.ts
|
| 426 |
+
{ isEnabled: () => isExtraUsageAllowed() && !getIsNonInteractiveSession() }
|
| 427 |
+
```
|
| 428 |
+
|
| 429 |
+
**`isHidden` 使用模式**:
|
| 430 |
+
|
| 431 |
+
```typescript
|
| 432 |
+
// 内部命令(外部构建中完全隐藏——这些命令的 stub 文件导出此配置)
|
| 433 |
+
// src/commands/share/index.js
|
| 434 |
+
export default { isEnabled: () => false, isHidden: true, name: 'stub' }
|
| 435 |
+
|
| 436 |
+
// 条件隐藏
|
| 437 |
+
// src/commands/cost/index.ts
|
| 438 |
+
{ get isHidden() { /* 动态条件 */ } }
|
| 439 |
+
|
| 440 |
+
// 始终隐藏
|
| 441 |
+
// src/commands/heapdump/index.ts
|
| 442 |
+
{ isHidden: true }
|
| 443 |
+
```
|
| 444 |
+
|
| 445 |
+
`isCommandEnabled()` 的默认值为 `true`(未定义时),`isHidden` 默认值为 `false`。
|
| 446 |
+
|
| 447 |
+
### 3.5 `USER_TYPE` 环境变量
|
| 448 |
+
|
| 449 |
+
构建时通过 `process.env.USER_TYPE` 控制内部/外部构建的差异:
|
| 450 |
+
|
| 451 |
+
- 外部构建:`USER_TYPE = 'external'`(当前版本如此设置)
|
| 452 |
+
- 内部构建:`USER_TYPE = 'ant'`(Anthropic 内部)
|
| 453 |
+
|
| 454 |
+
`USER_TYPE` 控制以下差异:
|
| 455 |
+
|
| 456 |
+
1. **内部命令注册**:`INTERNAL_ONLY_COMMANDS` 数组(包含 `backfillSessions`、`breakCache`、`bughunter`、`initVerifiers` 等 ~30 个内部命令)仅在 `USER_TYPE === 'ant'` 时注册。
|
| 457 |
+
2. **工具可用性**:`ConfigTool`、`TungstenTool`、`REPLTool` 仅在内部构建中可用。
|
| 458 |
+
3. **YOLO 分类器**:内部构建使用更详细的权限分类模板。
|
| 459 |
+
|
| 460 |
+
---
|
| 461 |
+
|
| 462 |
+
## 4. 工具过滤
|
| 463 |
+
|
| 464 |
+
### 4.1 `filterToolsByDenyRules()`
|
| 465 |
+
|
| 466 |
+
`src/tools.ts` 中的 `filterToolsByDenyRules()` 函数根据权限上下文中的拒绝规则过滤工具:
|
| 467 |
+
|
| 468 |
+
```typescript
|
| 469 |
+
export function filterToolsByDenyRules<T extends { name: string; mcpInfo?: ... }>(
|
| 470 |
+
tools: readonly T[],
|
| 471 |
+
permissionContext: ToolPermissionContext,
|
| 472 |
+
): T[] {
|
| 473 |
+
return tools.filter(tool => !getDenyRuleForTool(permissionContext, tool))
|
| 474 |
+
}
|
| 475 |
+
```
|
| 476 |
+
|
| 477 |
+
此过滤在工具列表最终组装前执行,确保被拒绝的工具(包括按 MCP 服务器前缀拒绝的)在模型看到之前就已移除。
|
| 478 |
+
|
| 479 |
+
### 4.2 完整工具组装流程
|
| 480 |
+
|
| 481 |
+
`getTools()` 函数(`src/tools.ts`)的组装流程:
|
| 482 |
+
|
| 483 |
+
1. **Simple 模式**(`CLAUDE_CODE_SIMPLE=1`):仅返回 `BashTool`、`FileReadTool`、`FileEditTool`(或 REPL 模式下的 `REPLTool`),加上协调者模式所需的 `AgentTool` + `TaskStopTool`。
|
| 484 |
+
2. **完整模式**:通过 `getAllBaseTools()` 获取所有工具,移除特殊工具(`ListMcpResourcesTool`、`ReadMcpResourceTool`、`SYNTHETIC_OUTPUT_TOOL_NAME`)。
|
| 485 |
+
3. **应用拒绝规则**:`filterToolsByDenyRules()`。
|
| 486 |
+
4. **REPL 模式屏蔽**:当 REPL 启用时,隐藏 `REPL_ONLY_TOOLS` 集合中的原始工具。
|
| 487 |
+
5. **应用 `isEnabled()`**:每个工具自身的 `isEnabled()` 检查。
|
| 488 |
+
|
| 489 |
+
### 4.3 条件工具(`feature()` 门控导入)
|
| 490 |
+
|
| 491 |
+
`src/tools.ts` 中使用 `feature()` 进行条件导入的工具清单:
|
| 492 |
+
|
| 493 |
+
| Feature | 工具类 | 说明 |
|
| 494 |
+
| --- | --- | --- |
|
| 495 |
+
| `PROACTIVE` / `KAIROS` | `SleepTool` | 计划休眠工具 |
|
| 496 |
+
| `AGENT_TRIGGERS` | `CronCreateTool`, `CronDeleteTool`, `CronListTool` | 定时任务管理 |
|
| 497 |
+
| `AGENT_TRIGGERS_REMOTE` | `RemoteTriggerTool` | 远程触发器 |
|
| 498 |
+
| `MONITOR_TOOL` | `MonitorTool` | 监控工具 |
|
| 499 |
+
| `KAIROS` | `SendUserFileTool` | 发送用户文件 |
|
| 500 |
+
| `KAIROS` / `KAIROS_PUSH_NOTIFICATION` | `PushNotificationTool` | 推送通知 |
|
| 501 |
+
| `KAIROS_GITHUB_WEBHOOKS` | `SubscribePRTool` | PR 订阅 |
|
| 502 |
+
| `OVERFLOW_TEST_TOOL` | `OverflowTestTool` | 溢出测试 |
|
| 503 |
+
| `CONTEXT_COLLAPSE` | `CtxInspectTool` | 上下文检查 |
|
| 504 |
+
| `TERMINAL_PANEL` | `TerminalCaptureTool` | 终端捕获 |
|
| 505 |
+
| `WEB_BROWSER_TOOL` | `WebBrowserTool` | 浏览器工具 |
|
| 506 |
+
| `HISTORY_SNIP` | `SnipTool` | 历史摘要 |
|
| 507 |
+
| `UDS_INBOX` | `ListPeersTool` | 对等节点列表 |
|
| 508 |
+
| `WORKFLOW_SCRIPTS` | `WorkflowTool` | 工作流执行 |
|
| 509 |
+
|
| 510 |
+
内部构建特有的工具(不受 `feature()` 控制,受 `USER_TYPE === 'ant'` 控制):
|
| 511 |
+
|
| 512 |
+
- `REPLTool`:REPL 交互式开发环境
|
| 513 |
+
- `SuggestBackgroundPRTool`:PR 建议工具
|
| 514 |
+
- `ConfigTool`、`TungstenTool`:内部配置工具
|
| 515 |
+
|
| 516 |
+
### 4.4 MCP 工具合并
|
| 517 |
+
|
| 518 |
+
`assembleToolPool()` 函数(`src/tools.ts`)合并内置工具和 MCP 工具:
|
| 519 |
+
|
| 520 |
+
1. 通过 `getTools()` 获取内置工具
|
| 521 |
+
2. 通过 `filterToolsByDenyRules()` 过滤 MCP 工具
|
| 522 |
+
3. 使用 `uniqBy()` 按名称去重(内置工具优先)
|
| 523 |
+
4. 按名称排序以保证提示缓存稳定性
|
| 524 |
+
|
| 525 |
+
---
|
| 526 |
+
|
| 527 |
+
## 5. 构建产物
|
| 528 |
+
|
| 529 |
+
### 5.1 输出目录结构
|
| 530 |
+
|
| 531 |
+
构建产物的 `dist/` 目录结构:
|
| 532 |
+
|
| 533 |
+
```
|
| 534 |
+
dist/
|
| 535 |
+
├── cli # 生产二进制(`--compile` 模式,~192MB)
|
| 536 |
+
├── cli.js # 生产源码产物(非编译模式,~20MB)
|
| 537 |
+
├── VersperClaw # 开发二进制(`--dev --compile` 模式,~202MB)
|
| 538 |
+
└── vendor/ # 原生二进制库(仅非编译模式)
|
| 539 |
+
├── audio-capture/ # 音频捕获原生模块
|
| 540 |
+
├── audio-capture-src/ # 音频捕获源码
|
| 541 |
+
└── opus-encdec/ # Opus 编码/解码
|
| 542 |
+
```
|
| 543 |
+
|
| 544 |
+
### 5.2 各模式产物对比
|
| 545 |
+
|
| 546 |
+
| 构建模式 | 入口文件 | 产物路径 | 大小 | 类型 |
|
| 547 |
+
| --- | --- | --- | --- | --- |
|
| 548 |
+
| `build`(默认) | `scripts/build.ts` | `./cli` | ~0(源码引用) | 源码(Bun bundle) |
|
| 549 |
+
| `build:dev` | `scripts/build.ts --dev` | `./VersperClaw` | ~0(源码引用) | 源码 |
|
| 550 |
+
| `compile` | `scripts/build.ts --compile` | `./dist/cli` | ~192MB | Bun 编译二进制 |
|
| 551 |
+
| `compile + dev` | `scripts/build.ts --compile --dev` | `./dist/VersperClaw` | ~202MB | Bun 编译二进制(调试) |
|
| 552 |
+
|
| 553 |
+
### 5.3 预加载脚本
|
| 554 |
+
|
| 555 |
+
`preload.ts` 是开发模式下的运行时预加载脚本,在执行入口文件前:
|
| 556 |
+
- 设置 `MACRO` 全局变量
|
| 557 |
+
- 设置 `CLAUDE_CODE_LOCAL_SKIP_REMOTE_PREFETCH=1` 跳过远程预取
|
| 558 |
+
- 切换到调用目录(`process.chdir(CALLER_DIR)`)
|
| 559 |
+
|
| 560 |
+
### 5.4 Friend 前端静态资源
|
| 561 |
+
|
| 562 |
+
Friend VRM 前端构建产出包含大量静态资源(位于 `src/components/friend/frontend/dist/`):
|
| 563 |
+
|
| 564 |
+
- **VRM 模型**:5 个 `.vrm` 文件(11MB-48MB 不等)
|
| 565 |
+
- **FBX 动画**:约 10 个动画文件(`angry.fbx`、`happy.fbx`、`greeting.fbx` 等)
|
| 566 |
+
- **VMD/VRMA 动作**:`jile.vmd`、`idle_loop.vrma`、`playFingers.vrma` 等
|
| 567 |
+
- **WASM 文件**:onnxruntime-web 运行时的多�� WASM 二进制(`ort-wasm-simd-threaded.wasm` 等,合计约 80MB)
|
| 568 |
+
- **VAD 模型**:`silero_vad_legacy.onnx`(1.8MB)、`silero_vad_v5.onnx`(2.3MB)
|
| 569 |
+
- **音效**:`jile.mp3`(4.2MB)、`love.mp3`(11MB)
|
| 570 |
+
- **音频工作集**:`vad.worklet.bundle.min.js`(2.5KB)
|
| 571 |
+
- **HTML 入口**:`index.html`
|
| 572 |
+
|
| 573 |
+
### 5.5 WASM 与原生库
|
| 574 |
+
|
| 575 |
+
项目依赖多个 WASM 和原生库:
|
| 576 |
+
|
| 577 |
+
- **onnxruntime-web**:用于语音活动检测(VAD)的推理引擎
|
| 578 |
+
- **audio-capture-napi**(workspace):原生音频捕获库,通过 workspace `packages/audio-capture-napi` 管理
|
| 579 |
+
- **doubaoime-asr**:字节跳动豆包语音识别引擎(workspace symlink)
|
| 580 |
+
|
| 581 |
+
---
|
| 582 |
+
|
| 583 |
+
## 6. 开发工作流
|
| 584 |
+
|
| 585 |
+
### 6.1 开发模式启动
|
| 586 |
+
|
| 587 |
+
```bash
|
| 588 |
+
# 直接运行(无需构建,适合快速迭代)
|
| 589 |
+
bun run dev
|
| 590 |
+
# 等价于:bun run ./src/entrypoints/cli.tsx
|
| 591 |
+
|
| 592 |
+
# 开发构建 + 运行
|
| 593 |
+
bun run build:dev
|
| 594 |
+
./VersperClaw
|
| 595 |
+
|
| 596 |
+
# 开发构建(全部实验特性)+ 运行
|
| 597 |
+
bun run build:dev:full
|
| 598 |
+
./VersperClaw
|
| 599 |
+
```
|
| 600 |
+
|
| 601 |
+
### 6.2 生产构建
|
| 602 |
+
|
| 603 |
+
```bash
|
| 604 |
+
# 生产编译
|
| 605 |
+
bun run compile
|
| 606 |
+
# 输出:./dist/cli(~192MB 二进制)
|
| 607 |
+
|
| 608 |
+
# 测试构建
|
| 609 |
+
bun run build:dev:compile # 注意:此命令需要额外添加 --compile 参数
|
| 610 |
+
```
|
| 611 |
+
|
| 612 |
+
### 6.3 包管理
|
| 613 |
+
|
| 614 |
+
项目使用 **Bun workspaces** 管理 monorepo:
|
| 615 |
+
|
| 616 |
+
```json
|
| 617 |
+
{
|
| 618 |
+
"workspaces": ["packages/*"],
|
| 619 |
+
"packageManager": "bun@1.3.11",
|
| 620 |
+
"engines": { "bun": ">=1.3.11" }
|
| 621 |
+
}
|
| 622 |
+
```
|
| 623 |
+
|
| 624 |
+
workspace 包:
|
| 625 |
+
|
| 626 |
+
- `packages/audio-capture-napi/`:音频捕获原生 N-API 模块
|
| 627 |
+
- `packages/doubaoime-asr/`:豆包语音识别(symlink 到 .bun 缓存)
|
| 628 |
+
|
| 629 |
+
TypeScript 配置(`tsconfig.json`):
|
| 630 |
+
|
| 631 |
+
```json
|
| 632 |
+
{
|
| 633 |
+
"compilerOptions": {
|
| 634 |
+
"lib": ["ESNext"],
|
| 635 |
+
"target": "ESNext",
|
| 636 |
+
"module": "Preserve",
|
| 637 |
+
"jsx": "react-jsx",
|
| 638 |
+
"types": ["bun"],
|
| 639 |
+
"moduleResolution": "bundler",
|
| 640 |
+
"noEmit": true,
|
| 641 |
+
"strict": false,
|
| 642 |
+
"skipLibCheck": true
|
| 643 |
+
},
|
| 644 |
+
"include": ["src", "scripts", "env.d.ts"]
|
| 645 |
+
}
|
| 646 |
+
```
|
| 647 |
+
|
| 648 |
+
### 6.4 依赖特性说明
|
| 649 |
+
|
| 650 |
+
- **运行时 UI**:基于 Ink(React for CLI)和 JSX 构建交互式终端界面
|
| 651 |
+
- **语音处理**:使用 `@ericedouard/vad-node-realtime`(VAD)、`node-edge-tts`(TTS)
|
| 652 |
+
- **AI 服务**:多供应商支持(Anthropic SDK、AWS Bedrock、Azure、Google Vertex、Groq)
|
| 653 |
+
- **MCP 协议**:`@modelcontextprotocol/sdk` 和 `@anthropic-ai/mcpb`
|
| 654 |
+
- **功能标记运行时**:`@growthbook/growthbook` 用于运行时功能开关(与编译时 `feature()` 互补)
|
| 655 |
+
- **WebView**:通过 Bun 内置的 `WebView` 支持 Web 浏览器工具
|
| 656 |
+
- **Feishu/Lark**:`@larksuiteoapi/node-sdk` 集成飞书机器人
|
| 657 |
+
- **Telegram**:telegram 命令集成
|
| 658 |
+
|
| 659 |
+
### 6.5 构建脚本快速参考
|
| 660 |
+
|
| 661 |
+
| 命令 | 完整脚本 |
|
| 662 |
+
| --- | --- |
|
| 663 |
+
| `bun run dev` | `bun run ./src/entrypoints/cli.tsx` |
|
| 664 |
+
| `bun run build` | `bun run ./scripts/build.ts` |
|
| 665 |
+
| `bun run build:dev` | `bun run ./scripts/build.ts --dev` |
|
| 666 |
+
| `bun run build:dev:full` | `bun run ./scripts/build.ts --dev --feature-set=dev-full` |
|
| 667 |
+
| `bun run compile` | `bun run ./scripts/build.ts --compile` |
|
| 668 |
+
|
| 669 |
+
自定义构建示例:
|
| 670 |
+
```bash
|
| 671 |
+
# 带自定义 feature 的开发构建
|
| 672 |
+
bun run ./scripts/build.ts --dev --feature=KAIROS --feature=BUDDY
|
| 673 |
+
|
| 674 |
+
# 编译 + 特定 feature
|
| 675 |
+
bun run ./scripts/build.ts --compile --feature=TRANSCRIPT_CLASSIFIER
|
| 676 |
+
```
|
docs/cli/overview.md
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CLI 命令系统
|
| 2 |
+
|
| 3 |
+
## 架构概述
|
| 4 |
+
|
| 5 |
+
### 入口
|
| 6 |
+
|
| 7 |
+
CLI 入口由 **Commander.js** 解析命令行参数,随后启动 **REPL**(交互式循环)。REPL 负责接收用户输入、解析斜杠命令、调用对应的处理函数。
|
| 8 |
+
|
| 9 |
+
### 命令注册
|
| 10 |
+
|
| 11 |
+
命令通过 **6 种来源** 注册到系统:
|
| 12 |
+
|
| 13 |
+
| 来源 | 说明 | 注册位置 |
|
| 14 |
+
|------|------|----------|
|
| 15 |
+
| **内置命令** | `COMMANDS()` 函数返回的核心命令列表 | `src/commands.ts:261-354` |
|
| 16 |
+
| **内置技能** | 随 CLI 打包的 Markdown 技能文件 | `getBundledSkills()` |
|
| 17 |
+
| **内置插件** | 内置插件导出的技能命令 | `getBuiltinPluginSkillCommands()` |
|
| 18 |
+
| **用户技能目录** | 用户自定义的 Markdown 技能文件 | `getSkillDirCommands()` |
|
| 19 |
+
| **工作流** | 多步骤脚本工作流 | `getWorkflowCommands()` |
|
| 20 |
+
| **市场插件** | 从插件市场安装的外部插件 | `getPluginCommands()`, `getPluginSkills()` |
|
| 21 |
+
|
| 22 |
+
### 命令过滤
|
| 23 |
+
|
| 24 |
+
`getCommands()` 是命令的最终汇聚函数,执行以下过滤:
|
| 25 |
+
|
| 26 |
+
1. **availability 检查**:`meetsAvailabilityRequirement()` — 验证当前用户是否有权限(如 claude.ai 订阅者、Console API 用户)
|
| 27 |
+
2. **isEnabled 检查**:`isCommandEnabled()` — feature flag 控制是否启用
|
| 28 |
+
3. **去重**:内置命令与动态技能(`getDynamicSkills()`)间去重,内置命令优先
|
| 29 |
+
4. **动态技能插入**:在插件命令之后、内置命令之前插入
|
| 30 |
+
|
| 31 |
+
### 远程模式过滤
|
| 32 |
+
|
| 33 |
+
`REMOTE_SAFE_COMMANDS` 集合定义了可在 `--remote` 模式下安全使用的命令(仅影响本地 TUI 状态)。
|
| 34 |
+
|
| 35 |
+
`BRIDGE_SAFE_COMMANDS` 和 `isBridgeSafeCommand()` 定义了可通过 Remote Control bridge(移动端/web 客户端)执行的命令。
|
| 36 |
+
|
| 37 |
+
---
|
| 38 |
+
|
| 39 |
+
## 内置命令详表
|
| 40 |
+
|
| 41 |
+
以下为 `COMMANDS()` 中注册的所有内置命令,按功能分组:
|
| 42 |
+
|
| 43 |
+
### 导航与信息
|
| 44 |
+
|
| 45 |
+
| 命令 | 别名 | 描述 |
|
| 46 |
+
|------|------|------|
|
| 47 |
+
| `/help` | - | 显示帮助信息,列出所有可用命令 |
|
| 48 |
+
| `/clear` | - | 清屏,重置终端显示 |
|
| 49 |
+
| `/exit` | - | 退出 CLI |
|
| 50 |
+
| `/init` | - | 在项目中初始化 Claude Code 配置文件 |
|
| 51 |
+
| `/resume` | - | 恢复之前的会话 |
|
| 52 |
+
| `/status` | - | 显示当前会话状态 |
|
| 53 |
+
| `/stats` | - | 统计信息和指标 |
|
| 54 |
+
|
| 55 |
+
### 配置管理
|
| 56 |
+
|
| 57 |
+
| 命令 | 别名 | 描述 |
|
| 58 |
+
|------|------|------|
|
| 59 |
+
| `/config` | - | 系统设置管理(theme, model, permissions 等) |
|
| 60 |
+
| `/model` | - | 切换 AI 模型 |
|
| 61 |
+
| `/theme` | - | 切换终端主题 |
|
| 62 |
+
| `/color` | - | 更改 AI 回复颜色 |
|
| 63 |
+
| `/permissions` | - | 权限模式管理 |
|
| 64 |
+
| `/privacySettings` | - | 隐私设置 |
|
| 65 |
+
| `/outputStyle` | - | 输出风格切换 |
|
| 66 |
+
| `/statusline` | - | 状态行开关 |
|
| 67 |
+
| `/effort` | - | 设置思考/推理投入度 |
|
| 68 |
+
| `/fast` | - | 快速模式切换 |
|
| 69 |
+
| `/env` | ant 内部 | 环境变量管理 |
|
| 70 |
+
| `/remoteEnv` | - | 远程环境变量管理 |
|
| 71 |
+
| `/passes` | - | 管理预设的 always-allow/always-deny 规则 |
|
| 72 |
+
|
| 73 |
+
### 会话管理
|
| 74 |
+
|
| 75 |
+
| 命令 | 别名 | 描述 |
|
| 76 |
+
|------|------|------|
|
| 77 |
+
| `/session` | - | 会话管理(分享、导出、查看记录) |
|
| 78 |
+
| `/cost` | - | 显示当前会话费用 |
|
| 79 |
+
| `/usage` | - | 显示 API 用量 |
|
| 80 |
+
| `/compact` | - | 压缩上下文,减少 token 消耗 |
|
| 81 |
+
| `/copy` | - | 复制最后一条消息 |
|
| 82 |
+
| `/rename` | - | 重命名当前会话 |
|
| 83 |
+
| `/tag` | - | 为会话添加标签 |
|
| 84 |
+
| `/btw` | - | 快速记笔记,补充上下文 |
|
| 85 |
+
| `/rewind` | - | 回滚到之前的对话状态 |
|
| 86 |
+
|
| 87 |
+
### 工具与技能
|
| 88 |
+
|
| 89 |
+
| 命令 | 别名 | 描述 |
|
| 90 |
+
|------|------|------|
|
| 91 |
+
| `/skills` | - | 管理技能(列出、启用、禁用) |
|
| 92 |
+
| `/mcp` | - | MCP 服务器管理(添加、连接、查看) |
|
| 93 |
+
| `/plugin` | - | 插件管理 |
|
| 94 |
+
| `/reloadPlugins` | - | 重新加载所有插件 |
|
| 95 |
+
| `/hooks` | - | 钩子系统管理 |
|
| 96 |
+
| `/keys` / `/keybindings` | - | 快捷键管理 |
|
| 97 |
+
| `/add-dir` | - | 添加技能目录 |
|
| 98 |
+
| `/terminalSetup` | - | 终端设置工具 |
|
| 99 |
+
|
| 100 |
+
### 语音与 Friend
|
| 101 |
+
|
| 102 |
+
| 命令 | 别名 | 描述 |
|
| 103 |
+
|------|------|------|
|
| 104 |
+
| `/voice` | feature 控制 | 语音听写模式 |
|
| 105 |
+
| `/friend` | - | Friend 虚拟宠物管理 |
|
| 106 |
+
| `/thinkback` | - | Thinkback 回放 |
|
| 107 |
+
| `/thinkbackPlay` | - | Thinkback 播放控制 |
|
| 108 |
+
|
| 109 |
+
### Goal 与规划
|
| 110 |
+
|
| 111 |
+
| 命令 | 别名 | 描述 |
|
| 112 |
+
|------|------|------|
|
| 113 |
+
| `/goal` / `/goals` | - | 目标管理(创建、查看、更新) |
|
| 114 |
+
| `/plan` | - | 规划模式切换 |
|
| 115 |
+
| `/ultraplan` | feature 控制 | 高级规划模式 |
|
| 116 |
+
| `/agent` / `/agents` | - | 代理管理(列出、配置) |
|
| 117 |
+
| `/tasks` | - | 后台任务管理 |
|
| 118 |
+
|
| 119 |
+
### Git 与文件
|
| 120 |
+
|
| 121 |
+
| 命令 | 别名 | 描述 |
|
| 122 |
+
|------|------|------|
|
| 123 |
+
| `/diff` | - | 显示 Git diff |
|
| 124 |
+
| `/branch` | - | 分支管理 |
|
| 125 |
+
| `/files` | - | 显示会话中跟踪的文件 |
|
| 126 |
+
| `/commit` | ant 内部 | 创建 Git 提交 |
|
| 127 |
+
| `/commit-push-pr` | ant 内部 | 提交、推送、创建 PR |
|
| 128 |
+
| `/fork` | feature 控制 | Fork 子代理 |
|
| 129 |
+
| `/buddy` | feature 控制 | 协作编程伙伴 |
|
| 130 |
+
|
| 131 |
+
### 集成与外部服务
|
| 132 |
+
|
| 133 |
+
| 命令 | 别名 | 描述 |
|
| 134 |
+
|------|------|------|
|
| 135 |
+
| `/login` | - | 登录(��� 3P 用户) |
|
| 136 |
+
| `/logout` | - | 登出 |
|
| 137 |
+
| `/feishu` | - | 飞书集成 |
|
| 138 |
+
| `/telegram` | - | Telegram 集成 |
|
| 139 |
+
| `/desktop` | - | 桌面应用模式 |
|
| 140 |
+
| `/mobile` | - | 移动端二维码 |
|
| 141 |
+
| `/install-github-app` | - | 安装 GitHub App |
|
| 142 |
+
| `/install-slack-app` | - | 安装 Slack App |
|
| 143 |
+
| `/bridge` | feature 控制 | 桥接模式 |
|
| 144 |
+
| `/peers` | feature 控制 | UDS 对等节点管理 |
|
| 145 |
+
| `/subscribe-pr` | feature 控制 | 订阅 PR 通知 |
|
| 146 |
+
|
| 147 |
+
### 诊断与开发
|
| 148 |
+
|
| 149 |
+
| 命令 | 别名 | 描述 |
|
| 150 |
+
|------|------|------|
|
| 151 |
+
| `/doctor` | - | 系统诊断,检查配置和依赖 |
|
| 152 |
+
| `/upgrade` | - | 升级 CLI 版本 |
|
| 153 |
+
| `/version` | ant 内部 | 显示版本信息 |
|
| 154 |
+
| `/heapdump` | - | 堆转储(调试用) |
|
| 155 |
+
| `/sandbox-toggle` | - | 沙箱模式开关 |
|
| 156 |
+
| `/debug-tool-call` | ant 内部 | 调试工具调用 |
|
| 157 |
+
| `/perf-issue` | ant 内部 | 性能问题报告 |
|
| 158 |
+
| `/ant-trace` | ant 内部 | Ant 追踪 |
|
| 159 |
+
| `/oauth-refresh` | ant 内部 | OAuth token 刷新 |
|
| 160 |
+
| `/color` | - | 更改 AI 颜色 |
|
| 161 |
+
| `/stickers` | - | 贴纸管理 |
|
| 162 |
+
|
| 163 |
+
### 反馈与分析
|
| 164 |
+
|
| 165 |
+
| 命令 | 别名 | 描述 |
|
| 166 |
+
|------|------|------|
|
| 167 |
+
| `/feedback` | - | 发送反馈 |
|
| 168 |
+
| `/review` | - | 代码审查 |
|
| 169 |
+
| `/ultrareview` | - | 深度代码审查 |
|
| 170 |
+
| `/security-review` | - | 安全审查 |
|
| 171 |
+
| `/insights` | - | 会话分析报告 |
|
| 172 |
+
| `/extra-usage` | - | 额外用量显示 |
|
| 173 |
+
| `/rate-limit-options` | - | 速率限制选项 |
|
| 174 |
+
| `/summary` | ant 内部 | 会话摘要生成 |
|
| 175 |
+
| `/share` | ant 内部 | 分享会话 |
|
| 176 |
+
| `/release-notes` | - | 版本发布说明 |
|
| 177 |
+
| `/cost` | - | 会话费用 |
|
| 178 |
+
| `/usage` | - | 使用情况 |
|
| 179 |
+
|
| 180 |
+
### 实验性 & Feature-gated
|
| 181 |
+
|
| 182 |
+
| 命令 | 别名 | 描述 |
|
| 183 |
+
|------|------|------|
|
| 184 |
+
| `/proactive` | feature 控制 | 主动模式 |
|
| 185 |
+
| `/brief` | feature 控制 | 简报模式 |
|
| 186 |
+
| `/assistant` | feature 控制 | 助手模式 |
|
| 187 |
+
| `/torch` | feature 控制 | Torch 调试工具 |
|
| 188 |
+
| `/web` / `/remote-setup` | feature 控制 | 远程设置 |
|
| 189 |
+
| `/workflows` | feature 控制 | 工作流管理 |
|
| 190 |
+
| `/force-snip` | feature 控制 | 强制截断上下文 |
|
| 191 |
+
| `/remoteControlServer` | feature 控制 | 远程控制服务器 |
|
| 192 |
+
| `/buddy` | feature 控制 | 编程伙伴 |
|
| 193 |
+
| `/agents-platform` | ant 内部 | 代理平台管理 |
|
| 194 |
+
| `/bughunter` | ant 内部 | Bug 猎人工具 |
|
| 195 |
+
| `/autofix-pr` | ant 内部 | 自动修复 PR |
|
| 196 |
+
| `/backfill-sessions` | ant 内部 | 回填会话数据 |
|
| 197 |
+
| `/issue` | ant 内部 | 问题管理 |
|
| 198 |
+
| `/onboarding` | ant 内部 | 引导流程 |
|
| 199 |
+
| `/teleport` | ant 内部 | 远程会话跳转 |
|
| 200 |
+
| `/good-claude` | ant 内部 | 内部工具 |
|
| 201 |
+
| `/ctx_viz` | ant 内部 | 上下文可视化 |
|
| 202 |
+
| `/mock-limits` | ant 内部 | Mock 限制测试 |
|
| 203 |
+
| `/bridge-kick` | ant 内部 | 桥接踢出 |
|
| 204 |
+
| `/reset-limits` | ant 内部 | 重置限制 |
|
| 205 |
+
|
| 206 |
+
---
|
| 207 |
+
|
| 208 |
+
## Skill 系统
|
| 209 |
+
|
| 210 |
+
技能(Skills)是以 Markdown 文件形式定义的提示词模板,可被 LLM 或用户调用。技能系统分为:
|
| 211 |
+
|
| 212 |
+
- **打包技能(Bundled Skills)**:随 CLI 发布的内置 `.md` 文件
|
| 213 |
+
- **用户技能目录(Skill Dir Commands)**:用户在工作区 `./claude/skills/` 或全局 `~/.claude/skills/` 中定义的技能
|
| 214 |
+
- **插件技能(Plugin Skills)**:从插件加载的技能
|
| 215 |
+
- **MCP 技能**:通过 MCP 协议提供的技能(`getMcpSkillCommands()`)
|
| 216 |
+
|
| 217 |
+
技能文件的 YAML frontmatter 定义 `name`、`description`、`model` 等属性,正文为发送给 LLM 的提示词。
|
| 218 |
+
|
| 219 |
+
### 技能相关工具
|
| 220 |
+
|
| 221 |
+
- `SkillTool`:AI 可调用的技能执行工具
|
| 222 |
+
- `getSkillToolCommands()`:获取所有可被模型调用的 prompt 类型命令
|
| 223 |
+
- `getSlashCommandToolSkills()`:获取斜杠命令可用的技能列表
|
| 224 |
+
|
| 225 |
+
---
|
| 226 |
+
|
| 227 |
+
## 工作流系统
|
| 228 |
+
|
| 229 |
+
工作流(Workflows)是通过 `WorkflowTool` 实现的多步骤脚本,支持:
|
| 230 |
+
|
| 231 |
+
- 顺序执行多个步骤
|
| 232 |
+
- 条件分支
|
| 233 |
+
- 变量传递
|
| 234 |
+
- 用户确认点
|
| 235 |
+
|
| 236 |
+
当 `WORKFLOW_SCRIPTS` feature flag 启用时,工作流命令通过 `createWorkflowCommand()` 注册。
|
| 237 |
+
|
| 238 |
+
---
|
| 239 |
+
|
| 240 |
+
## 命令类型
|
| 241 |
+
|
| 242 |
+
`Command` 类型包括三种变体:
|
| 243 |
+
|
| 244 |
+
| 类型 | 说明 |
|
| 245 |
+
|------|------|
|
| 246 |
+
| `prompt` | 扩展为提示词发送给模型(技能、工作流) |
|
| 247 |
+
| `local` | 本地执行,输出纯文本 |
|
| 248 |
+
| `local-jsx` | 本地执行,渲染 Ink UI 组件 |
|
| 249 |
+
|
| 250 |
+
---
|
| 251 |
+
|
| 252 |
+
## 配置管理
|
| 253 |
+
|
| 254 |
+
`/config` 命令管理系统设置。配置值存储在:
|
| 255 |
+
|
| 256 |
+
- **用户设置**:`~/.claude/settings.json`
|
| 257 |
+
- **项目设置**:`.claude/settings.json`(项目级覆盖)
|
| 258 |
+
- **会话设置**:仅当前会话有效
|
| 259 |
+
|
| 260 |
+
支持的配置项包括 theme、model、permissions(权限模式)、音效、通知等。
|
| 261 |
+
|
| 262 |
+
相关文件:
|
| 263 |
+
- `/home/yuki/Code/Agent/VersperClaw/src/commands.ts` — 命令注册与过滤核心
|
| 264 |
+
- `/home/yuki/Code/Agent/VersperClaw/src/types/command.ts` — Command 类型定义
|
| 265 |
+
- `/home/yuki/Code/Agent/VersperClaw/src/skills/loadSkillsDir.ts` — 技能目录加载
|
| 266 |
+
- `/home/yuki/Code/Agent/VersperClaw/src/tools/WorkflowTool/` — 工作流工具实现
|
src/cli/bg.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// @generated stub from scan-missing-imports
|
| 2 |
+
// 该文件自动生成,对应 ant-internal 的 feature() gated 模块。
|
| 3 |
+
// 所有外部 build 的代码路径在 DCE 后都不会真的执行这里的代码,这只是
|
| 4 |
+
// bun build resolver 的占位符。
|
| 5 |
+
const __target = function noop() {}
|
| 6 |
+
const __handler: ProxyHandler<any> = {
|
| 7 |
+
get(_t, prop) {
|
| 8 |
+
if (prop === '__esModule') return true
|
| 9 |
+
if (prop === 'default') return new Proxy(__target, __handler)
|
| 10 |
+
if (prop === Symbol.toPrimitive) return () => undefined
|
| 11 |
+
if (prop === Symbol.iterator) return function* () {}
|
| 12 |
+
if (prop === Symbol.asyncIterator) return async function* () {}
|
| 13 |
+
if (prop === 'then') return undefined
|
| 14 |
+
return new Proxy(__target, __handler)
|
| 15 |
+
},
|
| 16 |
+
apply() {
|
| 17 |
+
return new Proxy(__target, __handler)
|
| 18 |
+
},
|
| 19 |
+
construct() {
|
| 20 |
+
return new Proxy(__target, __handler)
|
| 21 |
+
},
|
| 22 |
+
}
|
| 23 |
+
const stub: any = new Proxy(__target, __handler)
|
| 24 |
+
export default stub
|
| 25 |
+
export const __stubMissing = true
|
| 26 |
+
// 兼容常见的命名导出 —— 没列在这里的也会通过 default Proxy 兜底
|
| 27 |
+
export const createCachedMCState = stub
|
| 28 |
+
export const isCachedMicrocompactEnabled = stub
|
| 29 |
+
export const isModelSupportedForCacheEditing = stub
|
| 30 |
+
export const getCachedMCConfig = stub
|
| 31 |
+
export const markToolsSentToAPI = stub
|
| 32 |
+
export const resetCachedMCState = stub
|
| 33 |
+
export const checkProtectedNamespace = stub
|
| 34 |
+
export const getCoordinatorUserContext = stub
|
src/cli/exit.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Exit handler
|
| 3 |
+
* This handler is used to exit the CLI with a status code
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
export function exit(): never {
|
| 7 |
+
process.exit(0);
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export function exitWithError(message: string, code: number = 1): never {
|
| 11 |
+
console.error(message);
|
| 12 |
+
process.exit(code);
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export function cliError(message: string): never {
|
| 16 |
+
console.error(message);
|
| 17 |
+
process.exit(1);
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
export function cliOk(message?: string): never {
|
| 21 |
+
if (message) {
|
| 22 |
+
console.log(message);
|
| 23 |
+
}
|
| 24 |
+
process.exit(0);
|
| 25 |
+
}
|
src/cli/handlers/agents.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Agents command handlers
|
| 3 |
+
* These handlers are used by the CLI agents commands
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
export async function agentsHandler(): Promise<void> {
|
| 7 |
+
console.error('Agents handler not implemented');
|
| 8 |
+
process.exit(1);
|
| 9 |
+
}
|
src/cli/handlers/ant.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// @generated stub from scan-missing-imports
|
| 2 |
+
// 该文件自动生成,对应 ant-internal 的 feature() gated 模块。
|
| 3 |
+
// 所有外部 build 的代码路径在 DCE 后都不会真的执行这里的代码,这只是
|
| 4 |
+
// bun build resolver 的占位符。
|
| 5 |
+
const __target = function noop() {}
|
| 6 |
+
const __handler: ProxyHandler<any> = {
|
| 7 |
+
get(_t, prop) {
|
| 8 |
+
if (prop === '__esModule') return true
|
| 9 |
+
if (prop === 'default') return new Proxy(__target, __handler)
|
| 10 |
+
if (prop === Symbol.toPrimitive) return () => undefined
|
| 11 |
+
if (prop === Symbol.iterator) return function* () {}
|
| 12 |
+
if (prop === Symbol.asyncIterator) return async function* () {}
|
| 13 |
+
if (prop === 'then') return undefined
|
| 14 |
+
return new Proxy(__target, __handler)
|
| 15 |
+
},
|
| 16 |
+
apply() {
|
| 17 |
+
return new Proxy(__target, __handler)
|
| 18 |
+
},
|
| 19 |
+
construct() {
|
| 20 |
+
return new Proxy(__target, __handler)
|
| 21 |
+
},
|
| 22 |
+
}
|
| 23 |
+
const stub: any = new Proxy(__target, __handler)
|
| 24 |
+
export default stub
|
| 25 |
+
export const __stubMissing = true
|
| 26 |
+
// 兼容常见的命名导出 —— 没列在这里的也会通过 default Proxy 兜底
|
| 27 |
+
export const createCachedMCState = stub
|
| 28 |
+
export const isCachedMicrocompactEnabled = stub
|
| 29 |
+
export const isModelSupportedForCacheEditing = stub
|
| 30 |
+
export const getCachedMCConfig = stub
|
| 31 |
+
export const markToolsSentToAPI = stub
|
| 32 |
+
export const resetCachedMCState = stub
|
| 33 |
+
export const checkProtectedNamespace = stub
|
| 34 |
+
export const getCoordinatorUserContext = stub
|
src/cli/handlers/auth.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Auth command handlers
|
| 3 |
+
* These handlers are used by the CLI auth commands
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
export async function installOAuthTokens(result: any): Promise<void> {
|
| 7 |
+
console.error('Install OAuth tokens handler not implemented');
|
| 8 |
+
process.exit(1);
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
export async function authLogin(options: any): Promise<void> {
|
| 12 |
+
console.error('Auth login handler not implemented');
|
| 13 |
+
process.exit(1);
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export async function authLogout(): Promise<void> {
|
| 17 |
+
console.error('Auth logout handler not implemented');
|
| 18 |
+
process.exit(1);
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
export async function authStatus(opts: any): Promise<void> {
|
| 22 |
+
console.error('Auth status handler not implemented');
|
| 23 |
+
process.exit(1);
|
| 24 |
+
}
|
src/cli/handlers/autoMode.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Auto mode subcommand handlers — dump default/merged classifier rules and
|
| 3 |
+
* critique user-written rules. Dynamically imported when `claude auto-mode ...` runs.
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
import { errorMessage } from '../../utils/errors.js'
|
| 7 |
+
import {
|
| 8 |
+
getMainLoopModel,
|
| 9 |
+
parseUserSpecifiedModel,
|
| 10 |
+
} from '../../utils/model/model.js'
|
| 11 |
+
import {
|
| 12 |
+
type AutoModeRules,
|
| 13 |
+
buildDefaultExternalSystemPrompt,
|
| 14 |
+
getDefaultExternalAutoModeRules,
|
| 15 |
+
} from '../../utils/permissions/yoloClassifier.js'
|
| 16 |
+
import { getAutoModeConfig } from '../../utils/settings/settings.js'
|
| 17 |
+
import { sideQuery } from '../../utils/sideQuery.js'
|
| 18 |
+
import { jsonStringify } from '../../utils/slowOperations.js'
|
| 19 |
+
|
| 20 |
+
function writeRules(rules: AutoModeRules): void {
|
| 21 |
+
process.stdout.write(jsonStringify(rules, null, 2) + '\n')
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
export function autoModeDefaultsHandler(): void {
|
| 25 |
+
writeRules(getDefaultExternalAutoModeRules())
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
/**
|
| 29 |
+
* Dump the effective auto mode config: user settings where provided, external
|
| 30 |
+
* defaults otherwise. Per-section REPLACE semantics — matches how
|
| 31 |
+
* buildYoloSystemPrompt resolves the external template (a non-empty user
|
| 32 |
+
* section replaces that section's defaults entirely; an empty/absent section
|
| 33 |
+
* falls through to defaults).
|
| 34 |
+
*/
|
| 35 |
+
export function autoModeConfigHandler(): void {
|
| 36 |
+
const config = getAutoModeConfig()
|
| 37 |
+
const defaults = getDefaultExternalAutoModeRules()
|
| 38 |
+
writeRules({
|
| 39 |
+
allow: config?.allow?.length ? config.allow : defaults.allow,
|
| 40 |
+
soft_deny: config?.soft_deny?.length
|
| 41 |
+
? config.soft_deny
|
| 42 |
+
: defaults.soft_deny,
|
| 43 |
+
environment: config?.environment?.length
|
| 44 |
+
? config.environment
|
| 45 |
+
: defaults.environment,
|
| 46 |
+
})
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
const CRITIQUE_SYSTEM_PROMPT =
|
| 50 |
+
'You are an expert reviewer of auto mode classifier rules for Claude Code.\n' +
|
| 51 |
+
'\n' +
|
| 52 |
+
'Claude Code has an "auto mode" that uses an AI classifier to decide whether ' +
|
| 53 |
+
'tool calls should be auto-approved or require user confirmation. Users can ' +
|
| 54 |
+
'write custom rules in three categories:\n' +
|
| 55 |
+
'\n' +
|
| 56 |
+
'- **allow**: Actions the classifier should auto-approve\n' +
|
| 57 |
+
'- **soft_deny**: Actions the classifier should block (require user confirmation)\n' +
|
| 58 |
+
"- **environment**: Context about the user's setup that helps the classifier make decisions\n" +
|
| 59 |
+
'\n' +
|
| 60 |
+
"Your job is to critique the user's custom rules for clarity, completeness, " +
|
| 61 |
+
'and potential issues. The classifier is an LLM that reads these rules as ' +
|
| 62 |
+
'part of its system prompt.\n' +
|
| 63 |
+
'\n' +
|
| 64 |
+
'For each rule, evaluate:\n' +
|
| 65 |
+
'1. **Clarity**: Is the rule unambiguous? Could the classifier misinterpret it?\n' +
|
| 66 |
+
"2. **Completeness**: Are there gaps or edge cases the rule doesn't cover?\n" +
|
| 67 |
+
'3. **Conflicts**: Do any of the rules conflict with each other?\n' +
|
| 68 |
+
'4. **Actionability**: Is the rule specific enough for the classifier to act on?\n' +
|
| 69 |
+
'\n' +
|
| 70 |
+
'Be concise and constructive. Only comment on rules that could be improved. ' +
|
| 71 |
+
'If all rules look good, say so.'
|
| 72 |
+
|
| 73 |
+
export async function autoModeCritiqueHandler(options: {
|
| 74 |
+
model?: string
|
| 75 |
+
}): Promise<void> {
|
| 76 |
+
const config = getAutoModeConfig()
|
| 77 |
+
const hasCustomRules =
|
| 78 |
+
(config?.allow?.length ?? 0) > 0 ||
|
| 79 |
+
(config?.soft_deny?.length ?? 0) > 0 ||
|
| 80 |
+
(config?.environment?.length ?? 0) > 0
|
| 81 |
+
|
| 82 |
+
if (!hasCustomRules) {
|
| 83 |
+
process.stdout.write(
|
| 84 |
+
'No custom auto mode rules found.\n\n' +
|
| 85 |
+
'Add rules to your settings file under autoMode.{allow, soft_deny, environment}.\n' +
|
| 86 |
+
'Run `claude auto-mode defaults` to see the default rules for reference.\n',
|
| 87 |
+
)
|
| 88 |
+
return
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
const model = options.model
|
| 92 |
+
? parseUserSpecifiedModel(options.model)
|
| 93 |
+
: getMainLoopModel()
|
| 94 |
+
|
| 95 |
+
const defaults = getDefaultExternalAutoModeRules()
|
| 96 |
+
const classifierPrompt = buildDefaultExternalSystemPrompt()
|
| 97 |
+
|
| 98 |
+
const userRulesSummary =
|
| 99 |
+
formatRulesForCritique('allow', config?.allow ?? [], defaults.allow) +
|
| 100 |
+
formatRulesForCritique(
|
| 101 |
+
'soft_deny',
|
| 102 |
+
config?.soft_deny ?? [],
|
| 103 |
+
defaults.soft_deny,
|
| 104 |
+
) +
|
| 105 |
+
formatRulesForCritique(
|
| 106 |
+
'environment',
|
| 107 |
+
config?.environment ?? [],
|
| 108 |
+
defaults.environment,
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
process.stdout.write('Analyzing your auto mode rules…\n\n')
|
| 112 |
+
|
| 113 |
+
let response
|
| 114 |
+
try {
|
| 115 |
+
response = await sideQuery({
|
| 116 |
+
querySource: 'auto_mode_critique',
|
| 117 |
+
model,
|
| 118 |
+
system: CRITIQUE_SYSTEM_PROMPT,
|
| 119 |
+
skipSystemPromptPrefix: true,
|
| 120 |
+
max_tokens: 4096,
|
| 121 |
+
messages: [
|
| 122 |
+
{
|
| 123 |
+
role: 'user',
|
| 124 |
+
content:
|
| 125 |
+
'Here is the full classifier system prompt that the auto mode classifier receives:\n\n' +
|
| 126 |
+
'<classifier_system_prompt>\n' +
|
| 127 |
+
classifierPrompt +
|
| 128 |
+
'\n</classifier_system_prompt>\n\n' +
|
| 129 |
+
"Here are the user's custom rules that REPLACE the corresponding default sections:\n\n" +
|
| 130 |
+
userRulesSummary +
|
| 131 |
+
'\nPlease critique these custom rules.',
|
| 132 |
+
},
|
| 133 |
+
],
|
| 134 |
+
})
|
| 135 |
+
} catch (error) {
|
| 136 |
+
process.stderr.write(
|
| 137 |
+
'Failed to analyze rules: ' + errorMessage(error) + '\n',
|
| 138 |
+
)
|
| 139 |
+
process.exitCode = 1
|
| 140 |
+
return
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
const textBlock = response.content.find(block => block.type === 'text')
|
| 144 |
+
if (textBlock?.type === 'text') {
|
| 145 |
+
process.stdout.write(textBlock.text + '\n')
|
| 146 |
+
} else {
|
| 147 |
+
process.stdout.write('No critique was generated. Please try again.\n')
|
| 148 |
+
}
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
function formatRulesForCritique(
|
| 152 |
+
section: string,
|
| 153 |
+
userRules: string[],
|
| 154 |
+
defaultRules: string[],
|
| 155 |
+
): string {
|
| 156 |
+
if (userRules.length === 0) return ''
|
| 157 |
+
const customLines = userRules.map(r => '- ' + r).join('\n')
|
| 158 |
+
const defaultLines = defaultRules.map(r => '- ' + r).join('\n')
|
| 159 |
+
return (
|
| 160 |
+
'## ' +
|
| 161 |
+
section +
|
| 162 |
+
' (custom rules replacing defaults)\n' +
|
| 163 |
+
'Custom:\n' +
|
| 164 |
+
customLines +
|
| 165 |
+
'\n\n' +
|
| 166 |
+
'Defaults being replaced:\n' +
|
| 167 |
+
defaultLines +
|
| 168 |
+
'\n\n'
|
| 169 |
+
)
|
| 170 |
+
}
|
src/cli/handlers/mcp.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* MCP command handlers
|
| 3 |
+
* These handlers are used by the CLI MCP commands
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
export async function mcpServeHandler(options: { debug?: boolean; verbose?: boolean }): Promise<void> {
|
| 7 |
+
console.error('MCP serve handler not implemented');
|
| 8 |
+
process.exit(1);
|
| 9 |
+
}
|
src/cli/handlers/plugins.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Plugin command handlers
|
| 3 |
+
* These handlers are used by the CLI plugin commands
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
export async function pluginInstallHandler(plugin: string, options: any): Promise<void> {
|
| 7 |
+
console.error('Plugin install handler not implemented');
|
| 8 |
+
process.exit(1);
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
export async function pluginUninstallHandler(plugin: string, options: any): Promise<void> {
|
| 12 |
+
console.error('Plugin uninstall handler not implemented');
|
| 13 |
+
process.exit(1);
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export async function pluginEnableHandler(plugin: string, options: any): Promise<void> {
|
| 17 |
+
console.error('Plugin enable handler not implemented');
|
| 18 |
+
process.exit(1);
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
export async function pluginDisableHandler(plugin: string | undefined, options: any): Promise<void> {
|
| 22 |
+
console.error('Plugin disable handler not implemented');
|
| 23 |
+
process.exit(1);
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
export async function pluginUpdateHandler(plugin: string, options: any): Promise<void> {
|
| 27 |
+
console.error('Plugin update handler not implemented');
|
| 28 |
+
process.exit(1);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
export async function pluginValidateHandler(manifestPath: string, options: any): Promise<void> {
|
| 32 |
+
console.error('Plugin validate handler not implemented');
|
| 33 |
+
process.exit(1);
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
export async function pluginListHandler(options: any): Promise<void> {
|
| 37 |
+
console.error('Plugin list handler not implemented');
|
| 38 |
+
process.exit(1);
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
export async function marketplaceAddHandler(source: string, options: any): Promise<void> {
|
| 42 |
+
console.error('Marketplace add handler not implemented');
|
| 43 |
+
process.exit(1);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
export async function marketplaceListHandler(options: any): Promise<void> {
|
| 47 |
+
console.error('Marketplace list handler not implemented');
|
| 48 |
+
process.exit(1);
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
export async function marketplaceRemoveHandler(name: string, options: any): Promise<void> {
|
| 52 |
+
console.error('Marketplace remove handler not implemented');
|
| 53 |
+
process.exit(1);
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
export async function marketplaceUpdateHandler(name: string | undefined, options: any): Promise<void> {
|
| 57 |
+
console.error('Marketplace update handler not implemented');
|
| 58 |
+
process.exit(1);
|
| 59 |
+
}
|
src/cli/handlers/templateJobs.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// @generated stub from scan-missing-imports
|
| 2 |
+
// 该文件自动生成,对应 ant-internal 的 feature() gated 模块。
|
| 3 |
+
// 所有外部 build 的代码路径在 DCE 后都不会真的执行这里的代码,这只是
|
| 4 |
+
// bun build resolver 的占位符。
|
| 5 |
+
const __target = function noop() {}
|
| 6 |
+
const __handler: ProxyHandler<any> = {
|
| 7 |
+
get(_t, prop) {
|
| 8 |
+
if (prop === '__esModule') return true
|
| 9 |
+
if (prop === 'default') return new Proxy(__target, __handler)
|
| 10 |
+
if (prop === Symbol.toPrimitive) return () => undefined
|
| 11 |
+
if (prop === Symbol.iterator) return function* () {}
|
| 12 |
+
if (prop === Symbol.asyncIterator) return async function* () {}
|
| 13 |
+
if (prop === 'then') return undefined
|
| 14 |
+
return new Proxy(__target, __handler)
|
| 15 |
+
},
|
| 16 |
+
apply() {
|
| 17 |
+
return new Proxy(__target, __handler)
|
| 18 |
+
},
|
| 19 |
+
construct() {
|
| 20 |
+
return new Proxy(__target, __handler)
|
| 21 |
+
},
|
| 22 |
+
}
|
| 23 |
+
const stub: any = new Proxy(__target, __handler)
|
| 24 |
+
export default stub
|
| 25 |
+
export const __stubMissing = true
|
| 26 |
+
// 兼容常见的命名导出 —— 没列在这里的也会通过 default Proxy 兜底
|
| 27 |
+
export const createCachedMCState = stub
|
| 28 |
+
export const isCachedMicrocompactEnabled = stub
|
| 29 |
+
export const isModelSupportedForCacheEditing = stub
|
| 30 |
+
export const getCachedMCConfig = stub
|
| 31 |
+
export const markToolsSentToAPI = stub
|
| 32 |
+
export const resetCachedMCState = stub
|
| 33 |
+
export const checkProtectedNamespace = stub
|
| 34 |
+
export const getCoordinatorUserContext = stub
|
src/cli/handlers/util.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Utility handlers
|
| 3 |
+
* These handlers are used by various CLI commands
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
import type { Root } from '../../ink.js';
|
| 7 |
+
|
| 8 |
+
export async function setupTokenHandler(root: Root): Promise<void> {
|
| 9 |
+
console.error('Setup token handler not implemented');
|
| 10 |
+
process.exit(1);
|
| 11 |
+
}
|
src/cli/print.ts
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Print utility functions — headless / SDK bridge
|
| 3 |
+
*
|
| 4 |
+
* The desktop server spawns the CLI subprocess with --print --sdk-url and
|
| 5 |
+
* expects it to connect back via a WebSocket. This file exports:
|
| 6 |
+
* - runHeadless() — SDK WebSocket ↔ QueryEngine bridge
|
| 7 |
+
* - externalMetadataToAppState() — stub for headless metadata
|
| 8 |
+
*/
|
| 9 |
+
|
| 10 |
+
import { getCwd } from '../utils/cwd.js'
|
| 11 |
+
import { createAbortController } from '../utils/abortController.js'
|
| 12 |
+
import { cloneFileStateCache } from '../utils/fileStateCache.js'
|
| 13 |
+
|
| 14 |
+
export function externalMetadataToAppState(metadata: any): any {
|
| 15 |
+
console.error('externalMetadataToAppState not implemented');
|
| 16 |
+
return {};
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
// ── SDK protocol helpers ──────────────────────────────────────────────
|
| 20 |
+
|
| 21 |
+
function extractUserText(msg: any): string {
|
| 22 |
+
const content = msg?.message?.content
|
| 23 |
+
if (!Array.isArray(content)) return ''
|
| 24 |
+
return content
|
| 25 |
+
.filter((block: any) => block?.type === 'text' && typeof block.text === 'string')
|
| 26 |
+
.map((block: any) => block.text)
|
| 27 |
+
.join(' ')
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
function sendWs(ws: WebSocket, payload: Record<string, unknown>) {
|
| 31 |
+
if (ws.readyState === WebSocket.OPEN) {
|
| 32 |
+
ws.send(JSON.stringify(payload) + '\n')
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// ── runHeadless — SDK mode entry point ────────────────────────────────
|
| 37 |
+
|
| 38 |
+
export async function runHeadless(
|
| 39 |
+
_inputPrompt: unknown,
|
| 40 |
+
getState: () => any,
|
| 41 |
+
setState: (fn: (state: any) => any) => void,
|
| 42 |
+
commandsHeadless: readonly any[],
|
| 43 |
+
tools: readonly any[],
|
| 44 |
+
_sdkMcpConfigs: unknown,
|
| 45 |
+
activeAgents: readonly any[],
|
| 46 |
+
options: {
|
| 47 |
+
verbose?: boolean
|
| 48 |
+
outputFormat?: string
|
| 49 |
+
jsonSchema?: Record<string, unknown>
|
| 50 |
+
allowedTools?: string[]
|
| 51 |
+
thinkingConfig?: { type: string; [key: string]: unknown }
|
| 52 |
+
maxTurns?: number
|
| 53 |
+
maxBudgetUsd?: number
|
| 54 |
+
taskBudget?: { total: number }
|
| 55 |
+
systemPrompt?: string
|
| 56 |
+
appendSystemPrompt?: string
|
| 57 |
+
userSpecifiedModel?: string
|
| 58 |
+
fallbackModel?: string
|
| 59 |
+
sdkUrl?: string
|
| 60 |
+
replayUserMessages?: boolean
|
| 61 |
+
includePartialMessages?: boolean
|
| 62 |
+
enableAuthStatus?: boolean
|
| 63 |
+
agent?: unknown
|
| 64 |
+
},
|
| 65 |
+
): Promise<void> {
|
| 66 |
+
const sdkUrl = options.sdkUrl
|
| 67 |
+
if (!sdkUrl) {
|
| 68 |
+
console.error('[runHeadless] Non-SDK headless mode is not implemented')
|
| 69 |
+
return
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
console.error(`[runHeadless] Connecting SDK WebSocket …`)
|
| 73 |
+
|
| 74 |
+
const ws = new WebSocket(sdkUrl)
|
| 75 |
+
await new Promise<void>((resolve, reject) => {
|
| 76 |
+
ws.addEventListener('open', () => {
|
| 77 |
+
console.error('[runHeadless] SDK WebSocket connected')
|
| 78 |
+
resolve()
|
| 79 |
+
})
|
| 80 |
+
ws.addEventListener('error', (err) => {
|
| 81 |
+
console.error('[runHeadless] SDK WebSocket error:', err)
|
| 82 |
+
reject(err)
|
| 83 |
+
})
|
| 84 |
+
})
|
| 85 |
+
|
| 86 |
+
const abortController = createAbortController()
|
| 87 |
+
|
| 88 |
+
// QueryEngine is lazy-imported so the first invocation bears the cost.
|
| 89 |
+
const { QueryEngine } = await import('../QueryEngine.js')
|
| 90 |
+
|
| 91 |
+
let engine: InstanceType<typeof QueryEngine> | null = null
|
| 92 |
+
let processing = false
|
| 93 |
+
const pendingMessages: any[] = []
|
| 94 |
+
|
| 95 |
+
async function processNextMessage(msg: any): Promise<void> {
|
| 96 |
+
const text = extractUserText(msg)
|
| 97 |
+
if (!text.trim()) return
|
| 98 |
+
|
| 99 |
+
const state = getState()
|
| 100 |
+
|
| 101 |
+
if (!engine) {
|
| 102 |
+
const mcpClients = state?.mcp?.clients ?? []
|
| 103 |
+
engine = new QueryEngine({
|
| 104 |
+
cwd: getCwd(),
|
| 105 |
+
tools: tools as any,
|
| 106 |
+
commands: commandsHeadless as any,
|
| 107 |
+
mcpClients,
|
| 108 |
+
agents: activeAgents as any,
|
| 109 |
+
/** In SDK mode tool permission decisions are owned by the host
|
| 110 |
+
* (desktop server / remote client). The CLI always allows;
|
| 111 |
+
* the host gates execution on its side. */
|
| 112 |
+
canUseTool: async () => ({ behavior: 'allow' as const }),
|
| 113 |
+
getAppState: getState,
|
| 114 |
+
setAppState: setState,
|
| 115 |
+
initialMessages: [],
|
| 116 |
+
readFileCache: state?.files ? cloneFileStateCache(state.files) : cloneFileStateCache(undefined),
|
| 117 |
+
verbose: !!options.verbose,
|
| 118 |
+
thinkingConfig: options.thinkingConfig as any,
|
| 119 |
+
maxTurns: options.maxTurns,
|
| 120 |
+
maxBudgetUsd: options.maxBudgetUsd,
|
| 121 |
+
taskBudget: options.taskBudget,
|
| 122 |
+
jsonSchema: options.jsonSchema,
|
| 123 |
+
replayUserMessages: !!options.replayUserMessages,
|
| 124 |
+
includePartialMessages: !!options.includePartialMessages,
|
| 125 |
+
userSpecifiedModel: options.userSpecifiedModel || undefined,
|
| 126 |
+
fallbackModel: options.fallbackModel || undefined,
|
| 127 |
+
setSDKStatus: (_status) => { /* no-op for now */ },
|
| 128 |
+
abortController,
|
| 129 |
+
})
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
try {
|
| 133 |
+
for await (const sdkMsg of engine.submitMessage(text)) {
|
| 134 |
+
sendWs(ws, sdkMsg as Record<string, unknown>)
|
| 135 |
+
}
|
| 136 |
+
} catch (err) {
|
| 137 |
+
console.error('[runHeadless] submitMessage error:', err)
|
| 138 |
+
sendWs(ws, {
|
| 139 |
+
type: 'result',
|
| 140 |
+
subtype: 'error',
|
| 141 |
+
is_error: true,
|
| 142 |
+
result: err instanceof Error ? err.message : String(err),
|
| 143 |
+
usage: { input_tokens: 0, output_tokens: 0 },
|
| 144 |
+
})
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
// Create a fresh engine for the next user turn.
|
| 148 |
+
engine = null
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
async function processQueue(): Promise<void> {
|
| 152 |
+
if (processing || pendingMessages.length === 0) return
|
| 153 |
+
processing = true
|
| 154 |
+
try {
|
| 155 |
+
while (pendingMessages.length > 0) {
|
| 156 |
+
const msg = pendingMessages.shift()!
|
| 157 |
+
await processNextMessage(msg)
|
| 158 |
+
}
|
| 159 |
+
} finally {
|
| 160 |
+
processing = false
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
ws.addEventListener('message', (event) => {
|
| 165 |
+
const payload = typeof event.data === 'string' ? event.data : String(event.data)
|
| 166 |
+
const lines = payload.split('\n').map((l) => l.trim()).filter(Boolean)
|
| 167 |
+
|
| 168 |
+
for (const line of lines) {
|
| 169 |
+
try {
|
| 170 |
+
const msg = JSON.parse(line)
|
| 171 |
+
if (msg.type === 'user') {
|
| 172 |
+
pendingMessages.push(msg)
|
| 173 |
+
void processQueue()
|
| 174 |
+
} else if (msg.type === 'control_request') {
|
| 175 |
+
handleControlRequest(msg, ws, setState, abortController)
|
| 176 |
+
}
|
| 177 |
+
} catch (err) {
|
| 178 |
+
console.error('[runHeadless] Invalid JSON line:', err)
|
| 179 |
+
}
|
| 180 |
+
}
|
| 181 |
+
})
|
| 182 |
+
|
| 183 |
+
// Wait for the WebSocket to close (SDK session ended).
|
| 184 |
+
await new Promise<void>((resolve) => {
|
| 185 |
+
ws.addEventListener('close', () => {
|
| 186 |
+
console.error('[runHeadless] SDK WebSocket closed')
|
| 187 |
+
resolve()
|
| 188 |
+
})
|
| 189 |
+
})
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
// ── Control request handling ──────────────────────────────────────────
|
| 193 |
+
|
| 194 |
+
function handleControlRequest(
|
| 195 |
+
msg: any,
|
| 196 |
+
ws: WebSocket,
|
| 197 |
+
setState: (fn: (state: any) => any) => void,
|
| 198 |
+
abortController: AbortController,
|
| 199 |
+
): void {
|
| 200 |
+
const { request_id, request } = msg
|
| 201 |
+
if (!request_id) return
|
| 202 |
+
|
| 203 |
+
switch (request?.subtype) {
|
| 204 |
+
case 'interrupt': {
|
| 205 |
+
abortController.abort()
|
| 206 |
+
sendWs(ws, {
|
| 207 |
+
type: 'result',
|
| 208 |
+
subtype: 'success',
|
| 209 |
+
is_error: false,
|
| 210 |
+
result: 'Interrupted',
|
| 211 |
+
usage: { input_tokens: 0, output_tokens: 0 },
|
| 212 |
+
session_id: msg.session_id ?? '',
|
| 213 |
+
})
|
| 214 |
+
break
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
case 'set_permission_mode': {
|
| 218 |
+
setState((prev: any) => ({
|
| 219 |
+
...prev,
|
| 220 |
+
toolPermissionContext: {
|
| 221 |
+
...prev.toolPermissionContext,
|
| 222 |
+
mode: request.mode ?? prev.toolPermissionContext?.mode ?? 'default',
|
| 223 |
+
},
|
| 224 |
+
}))
|
| 225 |
+
sendWs(ws, {
|
| 226 |
+
type: 'control_response',
|
| 227 |
+
response: { subtype: 'success', request_id, response: {} },
|
| 228 |
+
session_id: msg.session_id ?? '',
|
| 229 |
+
})
|
| 230 |
+
break
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
case 'set_max_thinking_tokens': {
|
| 234 |
+
sendWs(ws, {
|
| 235 |
+
type: 'control_response',
|
| 236 |
+
response: { subtype: 'success', request_id, response: {} },
|
| 237 |
+
session_id: msg.session_id ?? '',
|
| 238 |
+
})
|
| 239 |
+
break
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
default: {
|
| 243 |
+
sendWs(ws, {
|
| 244 |
+
type: 'control_response',
|
| 245 |
+
response: {
|
| 246 |
+
subtype: 'error',
|
| 247 |
+
request_id,
|
| 248 |
+
error: `Unknown control request: ${request?.subtype ?? '<unknown>'}`,
|
| 249 |
+
},
|
| 250 |
+
session_id: msg.session_id ?? '',
|
| 251 |
+
})
|
| 252 |
+
break
|
| 253 |
+
}
|
| 254 |
+
}
|
| 255 |
+
}
|
src/cli/structuredIO.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Structured I/O constants
|
| 3 |
+
* These constants are used for structured input/output operations
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
export const SANDBOX_NETWORK_ACCESS_TOOL_NAME = 'sandbox_network_access';
|
src/cli/transports/HybridTransport.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Hybrid Transport
|
| 3 |
+
* This transport combines multiple transport mechanisms for hybrid communication
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
export class HybridTransport {
|
| 7 |
+
constructor() {
|
| 8 |
+
console.error('HybridTransport not implemented');
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
async connect(): Promise<void> {
|
| 12 |
+
console.error('HybridTransport.connect not implemented');
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
async disconnect(): Promise<void> {
|
| 16 |
+
console.error('HybridTransport.disconnect not implemented');
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
async send(data: any): Promise<void> {
|
| 20 |
+
console.error('HybridTransport.send not implemented');
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
async receive(): Promise<any> {
|
| 24 |
+
console.error('HybridTransport.receive not implemented');
|
| 25 |
+
return null;
|
| 26 |
+
}
|
| 27 |
+
}
|
src/cli/transports/SSETransport.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* SSE Transport
|
| 3 |
+
* This transport uses Server-Sent Events for communication
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
export class SSETransport {
|
| 7 |
+
constructor() {
|
| 8 |
+
console.error('SSETransport not implemented');
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
async connect(): Promise<void> {
|
| 12 |
+
console.error('SSETransport.connect not implemented');
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
async disconnect(): Promise<void> {
|
| 16 |
+
console.error('SSETransport.disconnect not implemented');
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
async send(data: any): Promise<void> {
|
| 20 |
+
console.error('SSETransport.send not implemented');
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
async receive(): Promise<any> {
|
| 24 |
+
console.error('SSETransport.receive not implemented');
|
| 25 |
+
return null;
|
| 26 |
+
}
|
| 27 |
+
}
|
src/cli/transports/ccrClient.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* CCR Client
|
| 3 |
+
* This client is used for CCR (Claude Code Remote) communication
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
export class CCRClient {
|
| 7 |
+
constructor() {
|
| 8 |
+
console.error('CCRClient not implemented');
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
async connect(): Promise<void> {
|
| 12 |
+
console.error('CCRClient.connect not implemented');
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
async disconnect(): Promise<void> {
|
| 16 |
+
console.error('CCRClient.disconnect not implemented');
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
async send(data: any): Promise<void> {
|
| 20 |
+
console.error('CCRClient.send not implemented');
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
async receive(): Promise<any> {
|
| 24 |
+
console.error('CCRClient.receive not implemented');
|
| 25 |
+
return null;
|
| 26 |
+
}
|
| 27 |
+
}
|
src/cli/update.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Update command handler
|
| 3 |
+
* This handler is used by the CLI update command
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
export async function update(): Promise<void> {
|
| 7 |
+
console.error('Update handler not implemented');
|
| 8 |
+
process.exit(1);
|
| 9 |
+
}
|
test_feishu_ws.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
| 1 |
-
import * as Lark from '@larksuiteoapi/node-sdk'
|
| 2 |
-
|
| 3 |
-
const appId = 'cli_aaa6659174b85bec'
|
| 4 |
-
const appSecret = 'nMV7WvGm9Pbvtf0PA3RVLbqNujzeqP7I'
|
| 5 |
-
|
| 6 |
-
const client = new Lark.Client({
|
| 7 |
-
appId,
|
| 8 |
-
appSecret,
|
| 9 |
-
appType: Lark.AppType.SelfBuild,
|
| 10 |
-
domain: Lark.Domain.Feishu,
|
| 11 |
-
})
|
| 12 |
-
|
| 13 |
-
// Test bot info
|
| 14 |
-
try {
|
| 15 |
-
const info = await client.request({
|
| 16 |
-
method: 'GET',
|
| 17 |
-
url: '/open-apis/bot/v3/info',
|
| 18 |
-
})
|
| 19 |
-
console.log('Bot info:', JSON.stringify(info, null, 2))
|
| 20 |
-
} catch (e) {
|
| 21 |
-
console.error('Bot info failed:', e)
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
// Test WSClient
|
| 25 |
-
const dispatcher = new Lark.EventDispatcher({})
|
| 26 |
-
|
| 27 |
-
dispatcher.register({
|
| 28 |
-
'im.message.receive_v1': async (data: any) => {
|
| 29 |
-
console.log('Received message:', JSON.stringify(data, null, 2))
|
| 30 |
-
},
|
| 31 |
-
} as any)
|
| 32 |
-
|
| 33 |
-
const ws = new Lark.WSClient({
|
| 34 |
-
appId,
|
| 35 |
-
appSecret,
|
| 36 |
-
domain: Lark.Domain.Feishu,
|
| 37 |
-
loggerLevel: Lark.LoggerLevel.debug,
|
| 38 |
-
})
|
| 39 |
-
|
| 40 |
-
await ws.start({ eventDispatcher: dispatcher })
|
| 41 |
-
console.log('[WS] started, waiting 10s...')
|
| 42 |
-
await new Promise(r => setTimeout(r, 10000))
|
| 43 |
-
console.log('[WS] done')
|
| 44 |
-
ws.stop?.()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
vendor/audio-capture-src/index.ts
DELETED
|
@@ -1,147 +0,0 @@
|
|
| 1 |
-
type AudioCaptureNapi = {
|
| 2 |
-
startRecording(onData: (data: Buffer) => void, onEnd: () => void): boolean
|
| 3 |
-
stopRecording(): void
|
| 4 |
-
isRecording(): boolean
|
| 5 |
-
startPlayback(sampleRate: number, channels: number): boolean
|
| 6 |
-
writePlaybackData(data: Buffer): void
|
| 7 |
-
stopPlayback(): void
|
| 8 |
-
isPlaying(): boolean
|
| 9 |
-
// TCC microphone authorization status (macOS only):
|
| 10 |
-
// 0 = notDetermined, 1 = restricted, 2 = denied, 3 = authorized.
|
| 11 |
-
// Linux: always returns 3 (authorized) — no system-level microphone permission API.
|
| 12 |
-
// Windows: returns 3 (authorized) if registry key absent or allowed,
|
| 13 |
-
// 2 (denied) if microphone access is explicitly denied.
|
| 14 |
-
microphoneAuthorizationStatus?(): number
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
-
let cachedModule: AudioCaptureNapi | null = null
|
| 18 |
-
let loadAttempted = false
|
| 19 |
-
|
| 20 |
-
function loadModule(): AudioCaptureNapi | null {
|
| 21 |
-
if (loadAttempted) {
|
| 22 |
-
return cachedModule
|
| 23 |
-
}
|
| 24 |
-
loadAttempted = true
|
| 25 |
-
|
| 26 |
-
// Supported platforms: macOS (darwin), Linux, Windows (win32)
|
| 27 |
-
const platform = process.platform
|
| 28 |
-
if (platform !== 'darwin' && platform !== 'linux' && platform !== 'win32') {
|
| 29 |
-
return null
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
// Candidate 1: native-embed path (bun compile). AUDIO_CAPTURE_NODE_PATH is
|
| 33 |
-
// defined at build time in build-with-plugins.ts for native builds only — the
|
| 34 |
-
// define resolves it to the static literal "../../audio-capture.node" so bun
|
| 35 |
-
// compile can rewrite it to /$bunfs/root/audio-capture.node. MUST stay a
|
| 36 |
-
// direct require(env var) — bun cannot analyze require(variable) from a loop.
|
| 37 |
-
if (process.env.AUDIO_CAPTURE_NODE_PATH) {
|
| 38 |
-
try {
|
| 39 |
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
| 40 |
-
cachedModule = require(
|
| 41 |
-
process.env.AUDIO_CAPTURE_NODE_PATH,
|
| 42 |
-
) as AudioCaptureNapi
|
| 43 |
-
return cachedModule
|
| 44 |
-
} catch {
|
| 45 |
-
// fall through to runtime fallbacks below
|
| 46 |
-
}
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
// Candidates 2/3: npm-install and dev/source layouts. Dynamic require is
|
| 50 |
-
// fine here — in bundled output (node --target build) require() resolves at
|
| 51 |
-
// runtime relative to cli.js at the package root; in dev it resolves
|
| 52 |
-
// relative to this file (vendor/audio-capture-src/index.ts).
|
| 53 |
-
const platformDir = `${process.arch}-${platform}`
|
| 54 |
-
const fallbacks = [
|
| 55 |
-
`./vendor/audio-capture/${platformDir}/audio-capture.node`,
|
| 56 |
-
`../audio-capture/${platformDir}/audio-capture.node`,
|
| 57 |
-
]
|
| 58 |
-
for (const p of fallbacks) {
|
| 59 |
-
try {
|
| 60 |
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
| 61 |
-
cachedModule = require(p) as AudioCaptureNapi
|
| 62 |
-
return cachedModule
|
| 63 |
-
} catch {
|
| 64 |
-
// try next
|
| 65 |
-
}
|
| 66 |
-
}
|
| 67 |
-
return null
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
export function isNativeAudioAvailable(): boolean {
|
| 71 |
-
return loadModule() !== null
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
export function startNativeRecording(
|
| 75 |
-
onData: (data: Buffer) => void,
|
| 76 |
-
onEnd: () => void,
|
| 77 |
-
): boolean {
|
| 78 |
-
const mod = loadModule()
|
| 79 |
-
if (!mod) {
|
| 80 |
-
return false
|
| 81 |
-
}
|
| 82 |
-
return mod.startRecording(onData, onEnd)
|
| 83 |
-
}
|
| 84 |
-
|
| 85 |
-
export function stopNativeRecording(): void {
|
| 86 |
-
const mod = loadModule()
|
| 87 |
-
if (!mod) {
|
| 88 |
-
return
|
| 89 |
-
}
|
| 90 |
-
mod.stopRecording()
|
| 91 |
-
}
|
| 92 |
-
|
| 93 |
-
export function isNativeRecordingActive(): boolean {
|
| 94 |
-
const mod = loadModule()
|
| 95 |
-
if (!mod) {
|
| 96 |
-
return false
|
| 97 |
-
}
|
| 98 |
-
return mod.isRecording()
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
export function startNativePlayback(
|
| 102 |
-
sampleRate: number,
|
| 103 |
-
channels: number,
|
| 104 |
-
): boolean {
|
| 105 |
-
const mod = loadModule()
|
| 106 |
-
if (!mod) {
|
| 107 |
-
return false
|
| 108 |
-
}
|
| 109 |
-
return mod.startPlayback(sampleRate, channels)
|
| 110 |
-
}
|
| 111 |
-
|
| 112 |
-
export function writeNativePlaybackData(data: Buffer): void {
|
| 113 |
-
const mod = loadModule()
|
| 114 |
-
if (!mod) {
|
| 115 |
-
return
|
| 116 |
-
}
|
| 117 |
-
mod.writePlaybackData(data)
|
| 118 |
-
}
|
| 119 |
-
|
| 120 |
-
export function stopNativePlayback(): void {
|
| 121 |
-
const mod = loadModule()
|
| 122 |
-
if (!mod) {
|
| 123 |
-
return
|
| 124 |
-
}
|
| 125 |
-
mod.stopPlayback()
|
| 126 |
-
}
|
| 127 |
-
|
| 128 |
-
export function isNativePlaying(): boolean {
|
| 129 |
-
const mod = loadModule()
|
| 130 |
-
if (!mod) {
|
| 131 |
-
return false
|
| 132 |
-
}
|
| 133 |
-
return mod.isPlaying()
|
| 134 |
-
}
|
| 135 |
-
|
| 136 |
-
// Returns the microphone authorization status.
|
| 137 |
-
// On macOS, returns the TCC status: 0=notDetermined, 1=restricted, 2=denied, 3=authorized.
|
| 138 |
-
// On Linux, always returns 3 (authorized) — no system-level mic permission API.
|
| 139 |
-
// On Windows, returns 3 (authorized) if registry key absent or allowed, 2 (denied) if explicitly denied.
|
| 140 |
-
// Returns 0 (notDetermined) if the native module is unavailable.
|
| 141 |
-
export function microphoneAuthorizationStatus(): number {
|
| 142 |
-
const mod = loadModule()
|
| 143 |
-
if (!mod || !mod.microphoneAuthorizationStatus) {
|
| 144 |
-
return 0
|
| 145 |
-
}
|
| 146 |
-
return mod.microphoneAuthorizationStatus()
|
| 147 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|