mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 22:05:50 +00:00
Compare commits
22 Commits
lint/previ
...
refactor/p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d60e0eaccb | ||
|
|
a3fa374bb3 | ||
|
|
b5c299f5d2 | ||
|
|
ac42ce2d67 | ||
|
|
c659912517 | ||
|
|
a14b7f352b | ||
|
|
c5ab83a3fc | ||
|
|
03b7f9b453 | ||
|
|
bddd146f25 | ||
|
|
c8d08d235b | ||
|
|
a02dc0bded | ||
|
|
3cb1e50b25 | ||
|
|
cfab161e28 | ||
|
|
90027279e6 | ||
|
|
3470783ced | ||
|
|
8169b96250 | ||
|
|
fe08cacf8d | ||
|
|
5a4c820e1d | ||
|
|
1a4e9702c2 | ||
|
|
2273a0bcfe | ||
|
|
b80483c23e | ||
|
|
8442aaadd2 |
16
.github/workflows/ci.yml
vendored
16
.github/workflows/ci.yml
vendored
@@ -8,7 +8,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
ci:
|
||||||
runs-on: macos-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -23,8 +23,16 @@ jobs:
|
|||||||
- name: Type check
|
- name: Type check
|
||||||
run: bunx tsc --noEmit
|
run: bunx tsc --noEmit
|
||||||
|
|
||||||
- name: Test
|
- name: Test with Coverage
|
||||||
run: bun test
|
run: |
|
||||||
|
set -o pipefail
|
||||||
|
bun test --coverage --coverage-reporter=lcov 2>&1 | grep -vE '^\s*(\(pass\)|\(skip\))' | sed '/^.*\/__tests__\/.*:$/d' | cat -s
|
||||||
|
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
uses: codecov/codecov-action@v5
|
||||||
|
with:
|
||||||
|
file: ./coverage/lcov.info
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: bun run build
|
run: bun run build:vite
|
||||||
|
|||||||
204
02-kairos (1).md
204
02-kairos (1).md
@@ -1,204 +0,0 @@
|
|||||||
# KAIROS — 永不关机的 Claude
|
|
||||||
|
|
||||||
> 源码位置:`src/assistant/`、`src/proactive/`、`src/services/autoDream/`
|
|
||||||
> 编译开关:`feature('KAIROS')`、`feature('KAIROS_BRIEF')`、`feature('KAIROS_CHANNELS')`
|
|
||||||
> 远程开关:GrowthBook `tengu_kairos`
|
|
||||||
|
|
||||||
关掉终端 Claude 还在运行的持久助手模式。KAIROS 是 Claude Code 中最复杂的隐藏功能之一。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 核心概念
|
|
||||||
|
|
||||||
KAIROS 让 Claude 从"一次性对话工具"变成"持久运行的 AI 助手":
|
|
||||||
|
|
||||||
- 关闭终端后 Claude 仍在后台运行
|
|
||||||
- 每天自动写日志
|
|
||||||
- 晚上自动"做梦"整理记忆
|
|
||||||
- 没人说话时自己找活干
|
|
||||||
- 命令超 15 秒自动丢后台
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 激活流程
|
|
||||||
|
|
||||||
定义在 `src/main.tsx`(约第 1054-1092 行),需要通过五层检查:
|
|
||||||
|
|
||||||
```
|
|
||||||
1. feature('KAIROS') ← 编译时 flag
|
|
||||||
2. settings.assistant: true ← .claude/settings.json
|
|
||||||
3. 目录信任状态检查 ← 防恶意仓库劫持
|
|
||||||
4. tengu_kairos ← GrowthBook 远程开关
|
|
||||||
5. setKairosActive(true) ← 全局状态激活
|
|
||||||
```
|
|
||||||
|
|
||||||
`--assistant` CLI 参数可跳过远程开关检查(用于 Agent SDK daemon 模式)。
|
|
||||||
|
|
||||||
全局状态存储在 `src/bootstrap/state.ts`:
|
|
||||||
- `kairosActive: boolean`(默认 `false`)
|
|
||||||
- `getKairosActive()` / `setKairosActive(true)`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 跨会话持久运行
|
|
||||||
|
|
||||||
### 会话恢复
|
|
||||||
|
|
||||||
`src/utils/conversationRecovery.ts` 中使用 `feature('KAIROS')` 条件导入 `BriefTool` 和 `SendUserFileTool`。在反序列化会话时识别这些工具的结果为"终端工具结果",判断 turn 是正常完成还是被中断。
|
|
||||||
|
|
||||||
### 持久 Cron 任务
|
|
||||||
|
|
||||||
关键在 `.claude/scheduled_tasks.json`。标记为 `permanent: true` 的任务不受 7 天自动过期限制:
|
|
||||||
|
|
||||||
- `catch-up`:恢复中断的工作
|
|
||||||
- `morning-checkin`:每日早间签到
|
|
||||||
- `dream`:记忆整合
|
|
||||||
|
|
||||||
### 会话历史 API
|
|
||||||
|
|
||||||
`src/assistant/sessionHistory.ts` 通过 OAuth API 加载远程会话历史,使用 `v1/sessions/{sessionId}/events` 端点,支持分页拉取。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 做梦机制(Dream)
|
|
||||||
|
|
||||||
KAIROS 最精巧的子系统——后台运行的子代理,将分散的会话记忆整合为持久的结构化知识。
|
|
||||||
|
|
||||||
### 触发条件(三层门控,由廉到贵)
|
|
||||||
|
|
||||||
定义在 `src/services/autoDream/autoDream.ts`:
|
|
||||||
|
|
||||||
```
|
|
||||||
1. 时间门控:距上次整合超过 24 小时(minHours)
|
|
||||||
2. 会话门控:至少 5 个新会话(minSessions)
|
|
||||||
3. 锁门控:没有其他进程正在整合
|
|
||||||
```
|
|
||||||
|
|
||||||
阈值通过 GrowthBook `tengu_onyx_plover` 远程配置动态控制。
|
|
||||||
|
|
||||||
### 四阶段整合流程
|
|
||||||
|
|
||||||
定义在 `src/services/autoDream/consolidationPrompt.ts`:
|
|
||||||
|
|
||||||
| 阶段 | 动作 |
|
|
||||||
|------|------|
|
|
||||||
| **Orient** | 列出记忆目录、读取 `MEMORY.md` 索引、浏览已有主题文件 |
|
|
||||||
| **Gather** | 从每日日志、已有记忆、JSONL transcript 中搜集新信号 |
|
|
||||||
| **Consolidate** | 合并新信号到主题文件,转换相对日期为绝对日期,删除过时事实 |
|
|
||||||
| **Prune** | 更新 `MEMORY.md` 索引,保持在行数和大小限制内 |
|
|
||||||
|
|
||||||
### 锁机制
|
|
||||||
|
|
||||||
`src/services/autoDream/consolidationLock.ts`:
|
|
||||||
|
|
||||||
- 使用 `.consolidate-lock` 文件
|
|
||||||
- 文件 mtime = `lastConsolidatedAt`
|
|
||||||
- 文件内容 = 持有者 PID
|
|
||||||
- 支持 PID 存活检查(1 小时超时)
|
|
||||||
- double-write 后 re-read 验证防竞争
|
|
||||||
|
|
||||||
### 每日日志
|
|
||||||
|
|
||||||
路径由 `src/memdir/paths.ts` 的 `getAutoMemDailyLogPath()` 计算:
|
|
||||||
|
|
||||||
```
|
|
||||||
<autoMemPath>/logs/YYYY/MM/YYYY-MM-DD.md
|
|
||||||
```
|
|
||||||
|
|
||||||
### UI 呈现
|
|
||||||
|
|
||||||
- Footer pill 标签显示 **"dreaming"**
|
|
||||||
- `src/components/tasks/DreamDetailDialog.tsx` 提供专门的详情对话框
|
|
||||||
- 支持查看实时进度和手动中止
|
|
||||||
- `Shift+Down` 打开后台任务对话框
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 主动模式(Proactive Mode)
|
|
||||||
|
|
||||||
没人说话时 Claude 自己找活干。
|
|
||||||
|
|
||||||
### 核心状态
|
|
||||||
|
|
||||||
`src/proactive/index.ts` 维护三个状态:
|
|
||||||
|
|
||||||
| 状态 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| `active` | 是否激活 |
|
|
||||||
| `paused` | 是否暂停(用户按 Esc 取消时暂停,下次输入恢复) |
|
|
||||||
| `contextBlocked` | API 错误时阻塞 tick,防止 tick-error-tick 死循环 |
|
|
||||||
|
|
||||||
### 激活方式
|
|
||||||
|
|
||||||
- `--proactive` CLI 参数
|
|
||||||
- `CLAUDE_CODE_PROACTIVE` 环境变量
|
|
||||||
- 受 `feature('PROACTIVE') || feature('KAIROS')` 保护
|
|
||||||
|
|
||||||
### 系统提示
|
|
||||||
|
|
||||||
激活后追加:
|
|
||||||
|
|
||||||
```
|
|
||||||
# Proactive Mode
|
|
||||||
|
|
||||||
You are in proactive mode. Take initiative -- explore, act, and make progress
|
|
||||||
without waiting for instructions.
|
|
||||||
|
|
||||||
Start by briefly greeting the user.
|
|
||||||
|
|
||||||
You will receive periodic <tick> prompts. These are check-ins. Do whatever
|
|
||||||
seems most useful, or call Sleep if there's nothing to do.
|
|
||||||
```
|
|
||||||
|
|
||||||
### SleepTool 集成
|
|
||||||
|
|
||||||
设置中的 `minSleepDurationMs` 和 `maxSleepDurationMs` 控制 Sleep 持续时间范围,节流 proactive tick 频率。没活干就 Sleep 等着。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 后台任务管理
|
|
||||||
|
|
||||||
### Cron 调度器
|
|
||||||
|
|
||||||
`src/utils/cronScheduler.ts`:
|
|
||||||
|
|
||||||
- 每 1 秒 tick 一次(`CHECK_INTERVAL_MS = 1000`)
|
|
||||||
- 使用 chokidar 监视 `.claude/scheduled_tasks.json`
|
|
||||||
- 支持调度器锁(`src/utils/cronTasksLock.ts`),防止多实例重复触发
|
|
||||||
- 锁探测间隔 5 秒,持有者崩溃时自动接管
|
|
||||||
|
|
||||||
### 任务类型
|
|
||||||
|
|
||||||
| 类型 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| 一次性(`recurring: false`) | 触发后自动删除,支持错过任务检测 |
|
|
||||||
| 循环(`recurring: true`) | 触发后重新调度,默认 7 天过期 |
|
|
||||||
| 永久(`permanent: true`) | 不受过期限制(KAIROS 专用) |
|
|
||||||
| 会话级(`durable: false`) | 仅内存中,进程退出即消失 |
|
|
||||||
|
|
||||||
### Jitter 防雷群机制
|
|
||||||
|
|
||||||
`src/utils/cronJitterConfig.ts`:
|
|
||||||
|
|
||||||
- 循环任务:基于 taskId 的确定性延迟(interval 的 10%,上限 15 分钟)
|
|
||||||
- 一次性任务:在 :00 和 :30 施加最多 90 秒提前量
|
|
||||||
- 运维可在事故期间推送配置变更,60 秒内全客户端生效
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 关键源码文件
|
|
||||||
|
|
||||||
| 文件 | 职责 |
|
|
||||||
|------|------|
|
|
||||||
| `src/bootstrap/state.ts` | KAIROS 全局状态 |
|
|
||||||
| `src/assistant/index.ts` | 助手模式入口 |
|
|
||||||
| `src/assistant/sessionHistory.ts` | 远程会话历史 API |
|
|
||||||
| `src/proactive/index.ts` | 主动模式状态管理 |
|
|
||||||
| `src/services/autoDream/autoDream.ts` | Auto-Dream 引擎 |
|
|
||||||
| `src/services/autoDream/consolidationPrompt.ts` | 整合提示(四阶段) |
|
|
||||||
| `src/services/autoDream/consolidationLock.ts` | 整合锁 |
|
|
||||||
| `src/services/autoDream/config.ts` | Dream 配置 |
|
|
||||||
| `src/tasks/DreamTask/DreamTask.ts` | Dream 任务定义 |
|
|
||||||
| `src/utils/cronScheduler.ts` | Cron 调度器 |
|
|
||||||
| `src/utils/cronTasks.ts` | Cron 任务持久化 |
|
|
||||||
| `src/skills/bundled/dream.ts` | `/dream` Skill(存根) |
|
|
||||||
283
AGENTS.md
283
AGENTS.md
@@ -1,283 +0,0 @@
|
|||||||
# AGENTS.md
|
|
||||||
|
|
||||||
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
This is a **reverse-engineered / decompiled** version of Anthropic's official Codex CLI tool. The goal is to restore core functionality while trimming secondary capabilities. Many modules are stubbed or feature-flagged off. TypeScript strict mode is enforced — **`bunx tsc --noEmit` must pass with zero errors**.
|
|
||||||
|
|
||||||
## Git Commit Message Convention
|
|
||||||
|
|
||||||
使用 **Conventional Commits** 规范:
|
|
||||||
|
|
||||||
```
|
|
||||||
<type>: <描述>
|
|
||||||
```
|
|
||||||
|
|
||||||
常见 type:`feat`、`fix`、`docs`、`chore`、`refactor`
|
|
||||||
|
|
||||||
示例:
|
|
||||||
- `feat: 添加模型 1M 上下文切换`
|
|
||||||
- `fix: 修复初次登陆的校验问题`
|
|
||||||
- `chore: remove prefetchOfficialMcpUrls call on startup`
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install dependencies
|
|
||||||
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
|
|
||||||
|
|
||||||
# Build (code splitting, outputs dist/cli.js + chunk files)
|
|
||||||
bun run build
|
|
||||||
|
|
||||||
# Test
|
|
||||||
bun test # run all tests (2453 tests / 137 files / 0 fail)
|
|
||||||
bun test src/utils/__tests__/hash.test.ts # run single file
|
|
||||||
bun test --coverage # with coverage report
|
|
||||||
|
|
||||||
# Lint & Format (Biome)
|
|
||||||
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
|
|
||||||
|
|
||||||
# Remote Control Server
|
|
||||||
bun run rcs
|
|
||||||
|
|
||||||
# Docs dev server (Mintlify)
|
|
||||||
bun run docs:dev
|
|
||||||
```
|
|
||||||
|
|
||||||
详细的测试规范、覆盖状态和改进计划见 `docs/testing-spec.md`。
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### 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` + chunk files。Build 默认启用 19 个 feature(见下方 Feature Flag 段)。构建后自动替换 `import.meta.require` 为 Node.js 兼容版本(产物 bun/node 都可运行)。
|
|
||||||
- **Dev mode**: `scripts/dev.ts` 通过 Bun `-d` flag 注入 `MACRO.*` defines,运行 `src/entrypoints/cli.tsx`。默认启用全部 feature。
|
|
||||||
- **Module system**: ESM (`"type": "module"`), TSX with `react-jsx` transform.
|
|
||||||
- **Monorepo**: Bun workspaces — 14 个 internal packages 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`。
|
|
||||||
- **CI**: GitHub Actions — `ci.yml`(构建+测试)、`release-rcs.yml`(RCS 发布)、`update-contributors.yml`(自动更新贡献者)。
|
|
||||||
|
|
||||||
### Entry & Bootstrap
|
|
||||||
|
|
||||||
1. **`src/entrypoints/cli.tsx`** (323 行) — True entrypoint。`main()` 函数按优先级处理多条快速路径:
|
|
||||||
- `--version` / `-v` — 零模块加载
|
|
||||||
- `--dump-system-prompt` — feature-gated (DUMP_SYSTEM_PROMPT)
|
|
||||||
- `--Codex-in-chrome-mcp` / `--chrome-native-host`
|
|
||||||
- `--computer-use-mcp` — 独立 MCP server 模式
|
|
||||||
- `--daemon-worker=<kind>` — feature-gated (DAEMON)
|
|
||||||
- `remote-control` / `rc` / `remote` / `sync` / `bridge` — feature-gated (BRIDGE_MODE)
|
|
||||||
- `daemon` [subcommand] — feature-gated (DAEMON)
|
|
||||||
- `ps` / `logs` / `attach` / `kill` / `--bg` — feature-gated (BG_SESSIONS)
|
|
||||||
- `new` / `list` / `reply` — Template job commands
|
|
||||||
- `environment-runner` / `self-hosted-runner` — BYOC runner
|
|
||||||
- `--tmux` + `--worktree` 组合
|
|
||||||
- 默认路径:加载 `main.tsx` 启动完整 CLI
|
|
||||||
2. **`src/main.tsx`** (~6970 行) — 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
|
|
||||||
|
|
||||||
- **`src/query.ts`** — The main API query function. Sends messages to Codex API, handles streaming responses, processes tool calls, and manages the conversation turn loop.
|
|
||||||
- **`src/QueryEngine.ts`** — Higher-level orchestrator wrapping `query()`. Manages conversation state, compaction, file history snapshots, attribution, and turn-level bookkeeping. Used by the REPL screen.
|
|
||||||
- **`src/screens/REPL.tsx`** — The interactive REPL screen (React/Ink component). Handles user input, message display, tool permission prompts, and keyboard shortcuts.
|
|
||||||
|
|
||||||
### API Layer
|
|
||||||
|
|
||||||
- **`src/services/api/Codex.ts`** — Core API client. Builds request params (system prompt, messages, tools, betas), calls the Anthropic SDK streaming endpoint, and processes `BetaRawMessageStreamEvent` events.
|
|
||||||
- **7 providers**: `firstParty` (Anthropic direct), `bedrock` (AWS), `vertex` (Google Cloud), `foundry`, `openai`, `gemini`, `grok` (xAI)。
|
|
||||||
- Provider selection in `src/utils/model/providers.ts`。优先级:modelType 参数 > 环境变量 > 默认 firstParty。
|
|
||||||
|
|
||||||
### Tool System
|
|
||||||
|
|
||||||
- **`src/Tool.ts`** — Tool interface definition (`Tool` type) and utilities (`findToolByName`, `toolMatchesName`).
|
|
||||||
- **`src/tools.ts`** (387 行) — Tool registry. Assembles the tool list; some tools are conditionally loaded via `feature()` flags or `process.env.USER_TYPE`.
|
|
||||||
- **`src/tools/<ToolName>/`** — 55 个 tool 目录。主要分类:
|
|
||||||
- **文件操作**: FileEditTool, FileReadTool, FileWriteTool, GlobTool, GrepTool
|
|
||||||
- **Shell/执行**: BashTool, PowerShellTool, REPLTool
|
|
||||||
- **Agent 系统**: AgentTool, TaskCreateTool, TaskUpdateTool, TaskListTool, TaskGetTool
|
|
||||||
- **规划**: EnterPlanModeTool, ExitPlanModeV2Tool, VerifyPlanExecutionTool
|
|
||||||
- **Web/MCP**: WebFetchTool, WebSearchTool, MCPTool, McpAuthTool
|
|
||||||
- **调度**: CronCreateTool, CronDeleteTool, CronListTool
|
|
||||||
- **其他**: LSPTool, ConfigTool, SkillTool, EnterWorktreeTool, ExitWorktreeTool 等
|
|
||||||
- **`src/tools/shared/`** — Tool 共享工具函数。
|
|
||||||
|
|
||||||
### UI Layer (Ink)
|
|
||||||
|
|
||||||
- **`src/ink.ts`** — Ink render wrapper with ThemeProvider injection.
|
|
||||||
- **`packages/@ant/ink/`** — Custom Ink framework(forked/internal),包含 components、core、hooks、keybindings、theme、utils。注意:不是 `src/ink/`。
|
|
||||||
- **`src/components/`** — 149 个组件目录/文件,渲染于终端 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/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).
|
|
||||||
|
|
||||||
### Workspace Packages
|
|
||||||
|
|
||||||
| Package | 说明 |
|
|
||||||
|---------|------|
|
|
||||||
| `packages/@ant/ink/` | Forked Ink 框架(components、hooks、keybindings、theme) |
|
|
||||||
| `packages/@ant/computer-use-mcp/` | Computer Use MCP server(截图/键鼠/剪贴板/应用管理) |
|
|
||||||
| `packages/@ant/computer-use-input/` | 键鼠模拟(dispatcher + darwin/win32/linux backend) |
|
|
||||||
| `packages/@ant/computer-use-swift/` | 截图 + 应用管理(dispatcher + per-platform backend) |
|
|
||||||
| `packages/@ant/Codex-for-chrome-mcp/` | Chrome 浏览器控制(通过 `--chrome` 启用) |
|
|
||||||
| `packages/remote-control-server/` | 自托管 Remote Control Server(Docker 部署,含 Web UI) |
|
|
||||||
| `packages/swarm/` | Swarm 解耦模块 |
|
|
||||||
| `packages/shell/` | Shell 抽象 |
|
|
||||||
| `packages/audio-capture-napi/` | 原生音频捕获(已恢复) |
|
|
||||||
| `packages/color-diff-napi/` | 颜色差异计算(完整实现,11 tests) |
|
|
||||||
| `packages/image-processor-napi/` | 图像处理(已恢复) |
|
|
||||||
| `packages/modifiers-napi/` | 键盘修饰键检测(stub) |
|
|
||||||
| `packages/url-handler-napi/` | URL scheme 处理(stub) |
|
|
||||||
|
|
||||||
### Bridge / Remote Control
|
|
||||||
|
|
||||||
- **`src/bridge/`** (~37 files) — Remote Control / Bridge 模式。feature-gated by `BRIDGE_MODE`。包含 bridge API、会话管理、JWT 认证、消息传输、权限回调等。Entry: `bridgeMain.ts`。
|
|
||||||
- **`packages/remote-control-server/`** — 自托管 RCS,支持 Docker 部署,含 Web UI 控制面板。通过 `bun run rcs` 启动。
|
|
||||||
- CLI 快速路径: `Codex remote-control` / `Codex rc` / `Codex bridge`。
|
|
||||||
- 详见 `docs/features/remote-control-self-hosting.md`。
|
|
||||||
|
|
||||||
### Daemon Mode
|
|
||||||
|
|
||||||
- **`src/daemon/`** — Daemon 模式(长驻 supervisor)。feature-gated by `DAEMON`。包含 `main.ts`(entry)和 `workerRegistry.ts`(worker 管理)。
|
|
||||||
|
|
||||||
### Context & System Prompt
|
|
||||||
|
|
||||||
- **`src/context.ts`** — Builds system/user context for the API call (git status, date, AGENTS.md contents, memory files).
|
|
||||||
- **`src/utils/claudemd.ts`** — Discovers and loads AGENTS.md files from project hierarchy.
|
|
||||||
|
|
||||||
### Feature Flag System
|
|
||||||
|
|
||||||
Feature flags control which functionality is enabled at runtime. 代码中统一通过 `import { feature } from 'bun:bundle'` 导入,调用 `feature('FLAG_NAME')` 返回 `boolean`。
|
|
||||||
|
|
||||||
**启用方式**: 环境变量 `FEATURE_<FLAG_NAME>=1`。例如 `FEATURE_BUDDY=1 bun run dev`。
|
|
||||||
|
|
||||||
**Build 默认 features**(19 个,见 `build.ts`):
|
|
||||||
- 基础: `BUDDY`, `TRANSCRIPT_CLASSIFIER`, `BRIDGE_MODE`, `AGENT_TRIGGERS_REMOTE`, `CHICAGO_MCP`, `VOICE_MODE`
|
|
||||||
- 统计/缓存: `SHOT_STATS`, `PROMPT_CACHE_BREAK_DETECTION`, `TOKEN_BUDGET`
|
|
||||||
- P0 本地: `AGENT_TRIGGERS`, `ULTRATHINK`, `BUILTIN_EXPLORE_PLAN_AGENTS`, `LODESTONE`
|
|
||||||
- P1 API 依赖: `EXTRACT_MEMORIES`, `VERIFICATION_AGENT`, `KAIROS_BRIEF`, `AWAY_SUMMARY`, `ULTRAPLAN`
|
|
||||||
- P2: `DAEMON`
|
|
||||||
|
|
||||||
**Dev mode 默认**: 全部启用(见 `scripts/dev.ts`)。
|
|
||||||
|
|
||||||
**类型声明**: `src/types/internal-modules.d.ts` 中声明了 `bun:bundle` 模块的 `feature` 函数签名。
|
|
||||||
|
|
||||||
**新增功能的正确做法**: 保留 `import { feature } from 'bun:bundle'` + `feature('FLAG_NAME')` 的标准模式,在运行时通过环境变量或配置控制,不要绕过 feature flag 直接 import。
|
|
||||||
|
|
||||||
### Multi-API 兼容层
|
|
||||||
|
|
||||||
所有兼容层均采用流适配器模式:将第三方 API 格式转为 Anthropic 内部格式,下游代码完全不改。
|
|
||||||
|
|
||||||
#### OpenAI 兼容层
|
|
||||||
|
|
||||||
通过 `CLAUDE_CODE_USE_OPENAI=1` 启用,支持 Ollama/DeepSeek/vLLM 等任意 OpenAI Chat Completions 协议端点。含 DeepSeek thinking mode 支持。
|
|
||||||
|
|
||||||
- **`src/services/api/openai/`** — client、消息/工具转换、流适配、模型映射
|
|
||||||
- 关键环境变量:`CLAUDE_CODE_USE_OPENAI`、`OPENAI_API_KEY`、`OPENAI_BASE_URL`、`OPENAI_MODEL`
|
|
||||||
|
|
||||||
#### Gemini 兼容层
|
|
||||||
|
|
||||||
通过 `CLAUDE_CODE_USE_GEMINI=1` 启用。独立环境变量体系。
|
|
||||||
|
|
||||||
- **`src/services/api/gemini/`** — client、模型映射、类型定义
|
|
||||||
- 关键环境变量:`GEMINI_API_KEY`(必填)、`GEMINI_MODEL`(直接指定)、`GEMINI_DEFAULT_SONNET_MODEL`/`GEMINI_DEFAULT_OPUS_MODEL`(按能力映射)
|
|
||||||
- 模型映射优先级:`GEMINI_MODEL` > `GEMINI_DEFAULT_*_MODEL` > `ANTHROPIC_DEFAULT_*_MODEL`(已废弃) > 原样返回
|
|
||||||
|
|
||||||
#### Grok 兼容层
|
|
||||||
|
|
||||||
通过 `CLAUDE_CODE_USE_GROK=1` 启用。自定义模型映射支持 xAI Grok API。
|
|
||||||
|
|
||||||
- **`src/services/api/grok/`** — client、模型映射
|
|
||||||
|
|
||||||
详见各兼容层的 docs 文档。
|
|
||||||
|
|
||||||
### Stubbed/Deleted Modules
|
|
||||||
|
|
||||||
| Module | Status |
|
|
||||||
|--------|--------|
|
|
||||||
| Computer Use (`@ant/*`) | Restored — macOS + Windows + Linux(后端完整度不一) |
|
|
||||||
| `*-napi` packages | `audio-capture-napi`、`image-processor-napi` 已恢复;`color-diff-napi` 完整;`modifiers-napi`、`url-handler-napi` 仍为 stub |
|
|
||||||
| Voice Mode | Restored — Push-to-Talk 语音输入(需 Anthropic OAuth) |
|
|
||||||
| OpenAI/Gemini/Grok 兼容层 | Restored |
|
|
||||||
| Remote Control Server | Restored — 自托管 RCS + Web UI |
|
|
||||||
| Analytics / GrowthBook / Sentry | Empty implementations |
|
|
||||||
| Magic Docs / LSP Server | Removed |
|
|
||||||
| Plugins / Marketplace | Removed |
|
|
||||||
| MCP OAuth | Simplified |
|
|
||||||
|
|
||||||
### Key Type Files
|
|
||||||
|
|
||||||
- **`src/types/global.d.ts`** — Declares `MACRO`, `BUILD_TARGET`, `BUILD_ENV` and internal Anthropic-only identifiers.
|
|
||||||
- **`src/types/internal-modules.d.ts`** — Type declarations for `bun:bundle`, `bun:ffi`, `@anthropic-ai/mcpb`.
|
|
||||||
- **`src/types/message.ts`** — Message type hierarchy (UserMessage, AssistantMessage, SystemMessage, etc.).
|
|
||||||
- **`src/types/permissions.ts`** — Permission mode and result types.
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
- **框架**: `bun:test`(内置断言 + mock)
|
|
||||||
- **当前状态**: 2472 tests / 138 files / 0 fail
|
|
||||||
- **单元测试**: 就近放置于 `src/**/__tests__/`,文件名 `<module>.test.ts`
|
|
||||||
- **集成测试**: `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 导入)
|
|
||||||
- **包测试**: `packages/` 下各包也有独立测试(如 `color-diff-napi` 11 tests)
|
|
||||||
|
|
||||||
### 类型检查
|
|
||||||
|
|
||||||
项目使用 TypeScript strict 模式,**tsc 必须零错误**。每次修改后运行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bunx tsc --noEmit
|
|
||||||
```
|
|
||||||
|
|
||||||
**类型规范**:
|
|
||||||
- 生产代码禁止 `as any`;测试文件中 mock 数据可用 `as any`
|
|
||||||
- 类型不匹配优先用 `as unknown as SpecificType` 双重断言,或补充 interface
|
|
||||||
- 未知结构对象用 `Record<string, unknown>` 替代 `any`
|
|
||||||
- 联合类型用类型守卫(type guard)收窄,不要强转
|
|
||||||
- `msg.request` 属性访问:`const req = msg.request as Record<string, unknown>`
|
|
||||||
- Ink `color` prop:用 `as keyof Theme` 而非 `as any`
|
|
||||||
|
|
||||||
## Working with This Codebase
|
|
||||||
|
|
||||||
- **tsc must pass** — `bunx tsc --noEmit` 必须零错误,任何修改都不能引入新的类型错误。
|
|
||||||
- **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 内置模块,由运行时/构建器解析。不要用自定义函数替代它。**`feature()` 只能直接用在 `if` 语句或三元表达式的条件位置**(Bun 编译器限制),不能赋值给变量、不能放在箭头函数体里、不能作为 `&&` 链的一部分。正确:`if (feature('X')) {}` 或 `feature('X') ? a : b`。
|
|
||||||
- **`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 行宽 + 按需分号。
|
|
||||||
- **Ink 框架在 `packages/@ant/ink/`** — 不是 `src/ink/`(该目录不存在)。Ink 相关的组件、hooks、keybindings 都在 packages 中。
|
|
||||||
- **Provider 优先级** — `modelType` 参数 > 环境变量 > 默认 `firstParty`。新增 provider 需在 `src/utils/model/providers.ts` 注册。
|
|
||||||
15
CLAUDE.md
15
CLAUDE.md
@@ -55,6 +55,8 @@ bun run health
|
|||||||
# Check unused exports
|
# Check unused exports
|
||||||
bun run check:unused
|
bun run check:unused
|
||||||
|
|
||||||
|
bun run typecheck
|
||||||
|
|
||||||
# Remote Control Server
|
# Remote Control Server
|
||||||
bun run rcs
|
bun run rcs
|
||||||
|
|
||||||
@@ -245,14 +247,23 @@ Feature flags control which functionality is enabled at runtime. 代码中统一
|
|||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
- **框架**: `bun:test`(内置断言 + mock)
|
- **框架**: `bun:test`(内置断言 + mock)
|
||||||
- **当前状态**: 2472 tests / 138 files / 0 fail
|
- **当前状态**: 2992 tests / 188 files / 0 fail
|
||||||
- **单元测试**: 就近放置于 `src/**/__tests__/`,文件名 `<module>.test.ts`
|
- **单元测试**: 就近放置于 `src/**/__tests__/`,文件名 `<module>.test.ts`
|
||||||
- **集成测试**: `tests/integration/` — 4 个文件(cli-arguments, context-build, message-pipeline, tool-chain)
|
- **集成测试**: `tests/integration/` — 4 个文件(cli-arguments, context-build, message-pipeline, tool-chain)
|
||||||
- **共享 mock/fixture**: `tests/mocks/`(api-responses, file-system, fixtures/)
|
- **共享 mock/fixture**: `tests/mocks/`(api-responses, file-system, fixtures/)
|
||||||
- **命名**: `describe("functionName")` + `test("behavior description")`,英文
|
- **命名**: `describe("functionName")` + `test("behavior description")`,英文
|
||||||
- **Mock 模式**: 对重依赖模块使用 `mock.module()` + `await import()` 解锁(必须内联在测试文件中,不能从共享 helper 导入)
|
|
||||||
- **包测试**: `packages/` 下各包也有独立测试(如 `color-diff-napi` 11 tests)
|
- **包测试**: `packages/` 下各包也有独立测试(如 `color-diff-napi` 11 tests)
|
||||||
|
|
||||||
|
### Mock 使用规范
|
||||||
|
|
||||||
|
**只 mock 有副作用的依赖链,不 mock 纯函数/纯数据模块。**
|
||||||
|
|
||||||
|
被迫 mock 的根源:`log.ts` / `debug.ts` → `bootstrap/state.ts`(模块级 `realpathSync` / `randomUUID` 副作用)。必须 mock 的模块:`log.ts`、`debug.ts`、`bun:bundle`、`settings/settings.js`、`config.ts`、`auth.ts`、第三方网络库。
|
||||||
|
|
||||||
|
不要 mock:纯函数模块(`errors.ts`、`stringUtils.js`)、mock 值与真实实现相同的模块、mock 路径与实际 import 不匹配的模块。
|
||||||
|
|
||||||
|
路径规则:统一用 `.ts` 扩展名 + `src/*` 别名路径,禁止双重 mock 同一模块。
|
||||||
|
|
||||||
### 类型检查
|
### 类型检查
|
||||||
|
|
||||||
项目使用 TypeScript strict 模式,**tsc 必须零错误**。每次修改后运行:
|
项目使用 TypeScript strict 模式,**tsc 必须零错误**。每次修改后运行:
|
||||||
|
|||||||
20
README.md
20
README.md
@@ -10,27 +10,25 @@
|
|||||||
|
|
||||||
> Which Claude do you like? The open source one is the best.
|
> 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(踩踩背)...
|
牢 A (Anthropic) 官方 [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI 工具的源码反编译/逆向还原项目。目标是将 Claude Code 大部分功能及工程化能力复现 (问就是老佛爷已经付过钱了)。虽然很难绷, 但是它叫做 CCB(踩踩背)... 而且, 我们实现了企业版或者需要登陆 Claude 账号才能使用的特性, 实现技术普惠
|
||||||
|
|
||||||
[文档在这里, 支持投稿 PR](https://ccb.agent-aura.top/) | [留影文档在这里](./Friends.md) | [Discord 群组](https://discord.gg/qZU6zS7Q)
|
[文档在这里, 支持投稿 PR](https://ccb.agent-aura.top/) | [留影文档在这里](./Friends.md) | [Discord 群组](https://discord.gg/qZU6zS7Q)
|
||||||
|
|
||||||
| 特性 | 说明 | 文档 |
|
| 特性 | 说明 | 文档 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| **Claude 群控技术** | Pipe IPC 多实例协作:同机 main/sub 自动编排 + LAN 跨机器零配置发现与通讯,`/pipes` 选择面板 + `Shift+↓` 交互 + 消息广播路由 | [Pipe IPC](https://ccb.agent-aura.top/docs/features/pipes-and-lan) / [LAN](https://ccb.agent-aura.top/docs/features/lan-pipes) |
|
| **Claude 群控技术** | Pipe IPC 多实例协作:同机 main/sub 自动编排 + LAN 跨机器零配置发现与通讯,`/pipes` 选择面板 + `Shift+↓` 交互 + 消息广播路由 | [Pipe IPC](https://ccb.agent-aura.top/docs/features/pipes-and-lan) / [LAN](https://ccb.agent-aura.top/docs/features/lan-pipes) |
|
||||||
| Remote Control 私有部署 | Docker 自托管 RCS + Web UI | [文档](https://ccb.agent-aura.top/docs/features/remote-control-self-hosting) |
|
| **ACP 协议一等一支持** | 支持接入 Zed、Cursor 等 IDE,支持会话恢复、Skills、权限桥接 | [文档](https://ccb.agent-aura.top/docs/features/acp-zed) |
|
||||||
| /dream 记忆整理 | 自动整理和优化记忆文件 | [文档](https://ccb.agent-aura.top/docs/features/auto-dream) |
|
| **Remote Control 私有部署** | Docker 自托管远程界面, 可以手机上看 CC | [文档](https://ccb.agent-aura.top/docs/features/remote-control-self-hosting) |
|
||||||
| Web Search | 内置网页搜索工具 | [文档](https://ccb.agent-aura.top/docs/features/web-browser-tool) |
|
| **Langfuse 监控** | 企业级 Agent 监控, 可以清晰看到每次 agent loop 细节, 可以一键转化为数据集 | [文档](https://ccb.agent-aura.top/docs/features/langfuse-monitoring) |
|
||||||
| 自定义模型供应商 | OpenAI/Anthropic/Gemini/Grok 兼容 | [文档](https://ccb.agent-aura.top/docs/features/custom-platform-login) |
|
| **Web Search** | 内置网页搜索工具, 支持 bing 和 brave 搜索 | [文档](https://ccb.agent-aura.top/docs/features/web-browser-tool) |
|
||||||
|
| **Poor Mode** | 穷鬼模式,关闭记忆提取和键入建议,大幅度减少并发请求 | /poor 可以开关 |
|
||||||
|
| **自定义模型供应商** | OpenAI/Anthropic/Gemini/Grok 兼容 | [文档](https://ccb.agent-aura.top/docs/features/custom-platform-login) |
|
||||||
| Voice Mode | Push-to-Talk 语音输入 | [文档](https://ccb.agent-aura.top/docs/features/voice-mode) |
|
| Voice Mode | Push-to-Talk 语音输入 | [文档](https://ccb.agent-aura.top/docs/features/voice-mode) |
|
||||||
| Computer Use | 屏幕截图、键鼠控制 | [文档](https://ccb.agent-aura.top/docs/features/computer-use) |
|
| Computer Use | 屏幕截图、键鼠控制 | [文档](https://ccb.agent-aura.top/docs/features/computer-use) |
|
||||||
| Chrome Use | 浏览器自动化、表单填写、数据抓取 | [魔改版](docs/features/chrome-use-mcp) [原生版](https://ccb.agent-aura.top/docs/features/claude-in-chrome-mcp) |
|
| Chrome Use | 浏览器自动化、表单填写、数据抓取 | [自托管](https://ccb.agent-aura.top/docs/features/chrome-use-mcp) [原生版](https://ccb.agent-aura.top/docs/features/claude-in-chrome-mcp) |
|
||||||
| Sentry | 企业级错误追踪 | [文档](https://ccb.agent-aura.top/docs/internals/sentry-setup) |
|
| Sentry | 企业级错误追踪 | [文档](https://ccb.agent-aura.top/docs/internals/sentry-setup) |
|
||||||
| GrowthBook | 企业级特性开关 | [文档](https://ccb.agent-aura.top/docs/internals/growthbook-adapter) |
|
| GrowthBook | 企业级特性开关 | [文档](https://ccb.agent-aura.top/docs/internals/growthbook-adapter) |
|
||||||
| Langfuse 监控 | LLM 调用/工具执行/多 Agent 全链路追踪 | [文档](https://ccb.agent-aura.top/docs/features/langfuse-monitoring) |
|
| /dream 记忆整理 | 自动整理和优化记忆文件 | [文档](https://ccb.agent-aura.top/docs/features/auto-dream) |
|
||||||
| Poor Mode | 穷鬼模式,关闭记忆提取和键入建议 | /poor 可以开关 |
|
|
||||||
|
|
||||||
|
|
||||||
- 🔮 [ ] V6 — 大规模重构石山代码,全面模块分包(全新分支,main 封存为历史版本)
|
|
||||||
|
|
||||||
- 🚀 [想要启动项目](#快速开始源码版)
|
- 🚀 [想要启动项目](#快速开始源码版)
|
||||||
- 🐛 [想要调试项目](#vs-code-调试)
|
- 🐛 [想要调试项目](#vs-code-调试)
|
||||||
|
|||||||
64
build.ts
64
build.ts
@@ -30,6 +30,8 @@ const DEFAULT_BUILD_FEATURES = [
|
|||||||
'ULTRAPLAN',
|
'ULTRAPLAN',
|
||||||
// P2: daemon + remote control server
|
// P2: daemon + remote control server
|
||||||
'DAEMON',
|
'DAEMON',
|
||||||
|
// ACP (Agent Client Protocol) agent mode
|
||||||
|
'ACP',
|
||||||
// PR-package restored features
|
// PR-package restored features
|
||||||
'WORKFLOW_SCRIPTS',
|
'WORKFLOW_SCRIPTS',
|
||||||
'HISTORY_SNIP',
|
'HISTORY_SNIP',
|
||||||
@@ -90,8 +92,27 @@ for (const file of files) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also patch unguarded globalThis.Bun destructuring from third-party deps
|
||||||
|
// (e.g. @anthropic-ai/sandbox-runtime) so Node.js doesn't crash at import time.
|
||||||
|
let bunPatched = 0
|
||||||
|
const BUN_DESTRUCTURE = /var \{([^}]+)\} = globalThis\.Bun;?/g
|
||||||
|
const BUN_DESTRUCTURE_SAFE = 'var {$1} = typeof globalThis.Bun !== "undefined" ? globalThis.Bun : {};'
|
||||||
|
for (const file of files) {
|
||||||
|
if (!file.endsWith('.js')) continue
|
||||||
|
const filePath = join(outdir, file)
|
||||||
|
const content = await readFile(filePath, 'utf-8')
|
||||||
|
if (BUN_DESTRUCTURE.test(content)) {
|
||||||
|
await writeFile(
|
||||||
|
filePath,
|
||||||
|
content.replace(BUN_DESTRUCTURE, BUN_DESTRUCTURE_SAFE),
|
||||||
|
)
|
||||||
|
bunPatched++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BUN_DESTRUCTURE.lastIndex = 0
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Bundled ${result.outputs.length} files to ${outdir}/ (patched ${patched} for Node.js compat)`,
|
`Bundled ${result.outputs.length} files to ${outdir}/ (patched ${patched} for import.meta.require, ${bunPatched} for Bun destructure)`,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Step 4: Copy native .node addon files (audio-capture)
|
// Step 4: Copy native .node addon files (audio-capture)
|
||||||
@@ -121,46 +142,7 @@ const cliNode = join(outdir, 'cli-node.js')
|
|||||||
|
|
||||||
await writeFile(cliBun, '#!/usr/bin/env bun\nimport "./cli.js"\n')
|
await writeFile(cliBun, '#!/usr/bin/env bun\nimport "./cli.js"\n')
|
||||||
|
|
||||||
// Node.js entry needs a Bun API polyfill because Bun.build({ target: 'bun' })
|
await writeFile(cliNode, '#!/usr/bin/env node\nimport "./cli.js"\n')
|
||||||
// emits globalThis.Bun references (e.g. Bun.$ shell tag in computer-use-input,
|
|
||||||
// Bun.which in chunk-ys6smqg9) that crash at import time under plain Node.js.
|
|
||||||
const NODE_BUN_POLYFILL = `#!/usr/bin/env node
|
|
||||||
// Bun API polyfill for Node.js runtime
|
|
||||||
if (typeof globalThis.Bun === "undefined") {
|
|
||||||
const { execFileSync } = await import("child_process");
|
|
||||||
const { resolve, delimiter } = await import("path");
|
|
||||||
const { accessSync, constants: { X_OK } } = await import("fs");
|
|
||||||
function which(bin) {
|
|
||||||
const isWin = process.platform === "win32";
|
|
||||||
const pathExt = isWin ? (process.env.PATHEXT || ".EXE").split(";") : [""];
|
|
||||||
for (const dir of (process.env.PATH || "").split(delimiter)) {
|
|
||||||
for (const ext of pathExt) {
|
|
||||||
const candidate = resolve(dir, bin + ext);
|
|
||||||
try { accessSync(candidate, X_OK); return candidate; } catch {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// Bun.$ is the shell template tag (e.g. $\`osascript ...\`). Only used by
|
|
||||||
// computer-use-input/darwin — stub it so the top-level destructuring
|
|
||||||
// \`var { $ } = globalThis.Bun\` doesn't crash.
|
|
||||||
function $(parts, ...args) {
|
|
||||||
throw new Error("Bun.$ shell API is not available in Node.js. Use Bun runtime for this feature.");
|
|
||||||
}
|
|
||||||
function hash(data, seed) {
|
|
||||||
let h = ((seed || 0) ^ 0x811c9dc5) >>> 0;
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
|
||||||
h ^= data.charCodeAt(i);
|
|
||||||
h = Math.imul(h, 0x01000193) >>> 0;
|
|
||||||
}
|
|
||||||
return h;
|
|
||||||
}
|
|
||||||
globalThis.Bun = { which, $, hash };
|
|
||||||
}
|
|
||||||
import "./cli.js"
|
|
||||||
`
|
|
||||||
await writeFile(cliNode, NODE_BUN_POLYFILL)
|
|
||||||
// NOTE: when new Bun-specific globals appear in bundled output, add them here.
|
|
||||||
|
|
||||||
// Make both executable
|
// Make both executable
|
||||||
const { chmodSync } = await import('fs')
|
const { chmodSync } = await import('fs')
|
||||||
|
|||||||
119
bun.lock
119
bun.lock
@@ -5,7 +5,9 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "claude-code-best",
|
"name": "claude-code-best",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@claude-code-best/mcp-chrome-bridge": "^2.0.7",
|
"@agentclientprotocol/sdk": "^0.19.0",
|
||||||
|
"@claude-code-best/mcp-chrome-bridge": "^2.0.8",
|
||||||
|
"ws": "^8.20.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@alcalzone/ansi-tokenize": "^0.3.0",
|
"@alcalzone/ansi-tokenize": "^0.3.0",
|
||||||
@@ -13,6 +15,7 @@
|
|||||||
"@ant/computer-use-input": "workspace:*",
|
"@ant/computer-use-input": "workspace:*",
|
||||||
"@ant/computer-use-mcp": "workspace:*",
|
"@ant/computer-use-mcp": "workspace:*",
|
||||||
"@ant/computer-use-swift": "workspace:*",
|
"@ant/computer-use-swift": "workspace:*",
|
||||||
|
"@ant/model-provider": "workspace:*",
|
||||||
"@anthropic-ai/bedrock-sdk": "^0.26.4",
|
"@anthropic-ai/bedrock-sdk": "^0.26.4",
|
||||||
"@anthropic-ai/claude-agent-sdk": "^0.2.87",
|
"@anthropic-ai/claude-agent-sdk": "^0.2.87",
|
||||||
"@anthropic-ai/foundry-sdk": "^0.2.3",
|
"@anthropic-ai/foundry-sdk": "^0.2.3",
|
||||||
@@ -57,10 +60,11 @@
|
|||||||
"@sentry/node": "^10.47.0",
|
"@sentry/node": "^10.47.0",
|
||||||
"@smithy/core": "^3.23.13",
|
"@smithy/core": "^3.23.13",
|
||||||
"@smithy/node-http-handler": "^4.5.1",
|
"@smithy/node-http-handler": "^4.5.1",
|
||||||
"@types/bun": "^1.3.11",
|
"@types/bun": "^1.3.12",
|
||||||
"@types/cacache": "^20.0.1",
|
"@types/cacache": "^20.0.1",
|
||||||
"@types/he": "^1.2.3",
|
"@types/he": "^1.2.3",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
|
"@types/node": "^25.6.0",
|
||||||
"@types/picomatch": "^4.0.3",
|
"@types/picomatch": "^4.0.3",
|
||||||
"@types/plist": "^3.0.5",
|
"@types/plist": "^3.0.5",
|
||||||
"@types/proper-lockfile": "^4.1.4",
|
"@types/proper-lockfile": "^4.1.4",
|
||||||
@@ -116,6 +120,7 @@
|
|||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-compiler-runtime": "^1.0.0",
|
"react-compiler-runtime": "^1.0.0",
|
||||||
"react-reconciler": "^0.33.0",
|
"react-reconciler": "^0.33.0",
|
||||||
|
"rollup": "^4.60.1",
|
||||||
"semver": "^7.7.4",
|
"semver": "^7.7.4",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"shell-quote": "^1.8.3",
|
"shell-quote": "^1.8.3",
|
||||||
@@ -130,11 +135,11 @@
|
|||||||
"undici": "^7.24.6",
|
"undici": "^7.24.6",
|
||||||
"url-handler-napi": "workspace:*",
|
"url-handler-napi": "workspace:*",
|
||||||
"usehooks-ts": "^3.1.1",
|
"usehooks-ts": "^3.1.1",
|
||||||
|
"vite": "^8.0.8",
|
||||||
"vscode-jsonrpc": "^8.2.1",
|
"vscode-jsonrpc": "^8.2.1",
|
||||||
"vscode-languageserver-protocol": "^3.17.5",
|
"vscode-languageserver-protocol": "^3.17.5",
|
||||||
"vscode-languageserver-types": "^3.17.5",
|
"vscode-languageserver-types": "^3.17.5",
|
||||||
"wrap-ansi": "^10.0.0",
|
"wrap-ansi": "^10.0.0",
|
||||||
"ws": "^8.20.0",
|
|
||||||
"xss": "^1.0.15",
|
"xss": "^1.0.15",
|
||||||
"yaml": "^2.8.3",
|
"yaml": "^2.8.3",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6",
|
||||||
@@ -179,6 +184,14 @@
|
|||||||
"wrap-ansi": "^10.0.0",
|
"wrap-ansi": "^10.0.0",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"packages/@ant/model-provider": {
|
||||||
|
"name": "@ant/model-provider",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@anthropic-ai/sdk": "^0.80.0",
|
||||||
|
"openai": "^6.33.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
"packages/agent-tools": {
|
"packages/agent-tools": {
|
||||||
"name": "@claude-code-best/agent-tools",
|
"name": "@claude-code-best/agent-tools",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@@ -253,6 +266,8 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
|
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.19.0", "https://registry.npmmirror.com/@agentclientprotocol/sdk/-/sdk-0.19.0.tgz", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-U9I8ws9WTOk6jCBAWpXefGSDgVXn14/kV6HFzwWGcstQ02mOQgClMAROHmoIn9GqZbDBDEOkdIbP4P4TEMQdug=="],
|
||||||
|
|
||||||
"@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.3.0", "https://registry.npmmirror.com/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.3.0.tgz", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-p+CMKJ93HFmLkjXKlXiVGlMQEuRb6H0MokBSwUsX+S6BRX8eV5naFZpQJFfJHjRZY0Hmnqy1/r6UWl3x+19zYA=="],
|
"@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.3.0", "https://registry.npmmirror.com/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.3.0.tgz", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-p+CMKJ93HFmLkjXKlXiVGlMQEuRb6H0MokBSwUsX+S6BRX8eV5naFZpQJFfJHjRZY0Hmnqy1/r6UWl3x+19zYA=="],
|
||||||
|
|
||||||
"@ant/claude-for-chrome-mcp": ["@ant/claude-for-chrome-mcp@workspace:packages/@ant/claude-for-chrome-mcp"],
|
"@ant/claude-for-chrome-mcp": ["@ant/claude-for-chrome-mcp@workspace:packages/@ant/claude-for-chrome-mcp"],
|
||||||
@@ -263,6 +278,8 @@
|
|||||||
|
|
||||||
"@ant/computer-use-swift": ["@ant/computer-use-swift@workspace:packages/@ant/computer-use-swift"],
|
"@ant/computer-use-swift": ["@ant/computer-use-swift@workspace:packages/@ant/computer-use-swift"],
|
||||||
|
|
||||||
|
"@ant/model-provider": ["@ant/model-provider@workspace:packages/@ant/model-provider"],
|
||||||
|
|
||||||
"@anthropic-ai/bedrock-sdk": ["@anthropic-ai/bedrock-sdk@0.26.4", "https://registry.npmmirror.com/@anthropic-ai/bedrock-sdk/-/bedrock-sdk-0.26.4.tgz", { "dependencies": { "@anthropic-ai/sdk": ">=0.50.3 <1", "@aws-crypto/sha256-js": "^4.0.0", "@aws-sdk/client-bedrock-runtime": "^3.797.0", "@aws-sdk/credential-providers": "^3.796.0", "@smithy/eventstream-serde-node": "^2.0.10", "@smithy/fetch-http-handler": "^5.0.4", "@smithy/protocol-http": "^3.0.6", "@smithy/signature-v4": "^3.1.1", "@smithy/smithy-client": "^2.1.9", "@smithy/types": "^2.3.4", "@smithy/util-base64": "^2.0.0" } }, "sha512-0Z2NY3T2wnzT9esRit6BiWpQXvL+F2b3z3Z9in3mXh7MDf122rVi2bcPowQHmo9ITXAPJmv/3H3t0V1z3Fugfw=="],
|
"@anthropic-ai/bedrock-sdk": ["@anthropic-ai/bedrock-sdk@0.26.4", "https://registry.npmmirror.com/@anthropic-ai/bedrock-sdk/-/bedrock-sdk-0.26.4.tgz", { "dependencies": { "@anthropic-ai/sdk": ">=0.50.3 <1", "@aws-crypto/sha256-js": "^4.0.0", "@aws-sdk/client-bedrock-runtime": "^3.797.0", "@aws-sdk/credential-providers": "^3.796.0", "@smithy/eventstream-serde-node": "^2.0.10", "@smithy/fetch-http-handler": "^5.0.4", "@smithy/protocol-http": "^3.0.6", "@smithy/signature-v4": "^3.1.1", "@smithy/smithy-client": "^2.1.9", "@smithy/types": "^2.3.4", "@smithy/util-base64": "^2.0.0" } }, "sha512-0Z2NY3T2wnzT9esRit6BiWpQXvL+F2b3z3Z9in3mXh7MDf122rVi2bcPowQHmo9ITXAPJmv/3H3t0V1z3Fugfw=="],
|
||||||
|
|
||||||
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.104", "https://registry.npmmirror.com/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.104.tgz", { "dependencies": { "@anthropic-ai/sdk": "^0.81.0", "@modelcontextprotocol/sdk": "^1.29.0" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-lVm+nS79r6WWlDnv5AgRzTtAlbP8O6M6kkWmDZAWE3nt9agmngxls9frJFvH55uzws2+6l0yyup/JYspfijkzw=="],
|
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.104", "https://registry.npmmirror.com/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.104.tgz", { "dependencies": { "@anthropic-ai/sdk": "^0.81.0", "@modelcontextprotocol/sdk": "^1.29.0" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-lVm+nS79r6WWlDnv5AgRzTtAlbP8O6M6kkWmDZAWE3nt9agmngxls9frJFvH55uzws2+6l0yyup/JYspfijkzw=="],
|
||||||
@@ -443,7 +460,7 @@
|
|||||||
|
|
||||||
"@claude-code-best/builtin-tools": ["@claude-code-best/builtin-tools@workspace:packages/builtin-tools"],
|
"@claude-code-best/builtin-tools": ["@claude-code-best/builtin-tools@workspace:packages/builtin-tools"],
|
||||||
|
|
||||||
"@claude-code-best/mcp-chrome-bridge": ["@claude-code-best/mcp-chrome-bridge@2.0.7", "", { "dependencies": { "@fastify/cors": "^11.0.1", "@modelcontextprotocol/sdk": "^1.11.0", "chalk": "^5.4.1", "chrome-mcp-shared": "^1.0.2", "commander": "^13.1.0", "fastify": "^5.3.2", "is-admin": "^4.0.0", "pino": "^9.6.0", "uuid": "^11.1.0" }, "bin": { "mcp-chrome-bridge": "dist/cli.js", "mcp-chrome-stdio": "dist/mcp/mcp-server-stdio.js" } }, "sha512-gb64+Ga6li3A8Ll9NKV+ePBn5/U0fccCdrH43tGYveLKZIZxURz8cbY+Z3BdbTdYSPVdFXtfUlp3TMxu4OT5gg=="],
|
"@claude-code-best/mcp-chrome-bridge": ["@claude-code-best/mcp-chrome-bridge@2.0.8", "https://registry.npmmirror.com/@claude-code-best/mcp-chrome-bridge/-/mcp-chrome-bridge-2.0.8.tgz", { "dependencies": { "@fastify/cors": "^11.0.1", "@modelcontextprotocol/sdk": "^1.11.0", "chalk": "^5.4.1", "chrome-mcp-shared": "^1.0.2", "commander": "^13.1.0", "fastify": "^5.3.2", "is-admin": "^4.0.0", "pino": "^9.6.0", "uuid": "^11.1.0" }, "bin": { "mcp-chrome-bridge": "dist/cli.js", "mcp-chrome-stdio": "dist/mcp/mcp-server-stdio.js" } }, "sha512-f7J1e4PQ6qxXzdHwL7QRrMZ4lPfD/L1MWxWDbyHmHY7jaW2GL6WcArKpk/fApg3V/q0racqUWzXHQdpE/HJZqg=="],
|
||||||
|
|
||||||
"@claude-code-best/mcp-client": ["@claude-code-best/mcp-client@workspace:packages/mcp-client"],
|
"@claude-code-best/mcp-client": ["@claude-code-best/mcp-client@workspace:packages/mcp-client"],
|
||||||
|
|
||||||
@@ -507,21 +524,21 @@
|
|||||||
|
|
||||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
||||||
|
|
||||||
"@fastify/ajv-compiler": ["@fastify/ajv-compiler@4.0.5", "", { "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0" } }, "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A=="],
|
"@fastify/ajv-compiler": ["@fastify/ajv-compiler@4.0.5", "https://registry.npmmirror.com/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", { "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0" } }, "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A=="],
|
||||||
|
|
||||||
"@fastify/cors": ["@fastify/cors@11.2.0", "", { "dependencies": { "fastify-plugin": "^5.0.0", "toad-cache": "^3.7.0" } }, "sha512-LbLHBuSAdGdSFZYTLVA3+Ch2t+sA6nq3Ejc6XLAKiQ6ViS2qFnvicpj0htsx03FyYeLs04HfRNBsz/a8SvbcUw=="],
|
"@fastify/cors": ["@fastify/cors@11.2.0", "https://registry.npmmirror.com/@fastify/cors/-/cors-11.2.0.tgz", { "dependencies": { "fastify-plugin": "^5.0.0", "toad-cache": "^3.7.0" } }, "sha512-LbLHBuSAdGdSFZYTLVA3+Ch2t+sA6nq3Ejc6XLAKiQ6ViS2qFnvicpj0htsx03FyYeLs04HfRNBsz/a8SvbcUw=="],
|
||||||
|
|
||||||
"@fastify/error": ["@fastify/error@4.2.0", "", {}, "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ=="],
|
"@fastify/error": ["@fastify/error@4.2.0", "https://registry.npmmirror.com/@fastify/error/-/error-4.2.0.tgz", {}, "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ=="],
|
||||||
|
|
||||||
"@fastify/fast-json-stringify-compiler": ["@fastify/fast-json-stringify-compiler@5.0.3", "", { "dependencies": { "fast-json-stringify": "^6.0.0" } }, "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ=="],
|
"@fastify/fast-json-stringify-compiler": ["@fastify/fast-json-stringify-compiler@5.0.3", "https://registry.npmmirror.com/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.3.tgz", { "dependencies": { "fast-json-stringify": "^6.0.0" } }, "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ=="],
|
||||||
|
|
||||||
"@fastify/forwarded": ["@fastify/forwarded@3.0.1", "", {}, "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw=="],
|
"@fastify/forwarded": ["@fastify/forwarded@3.0.1", "https://registry.npmmirror.com/@fastify/forwarded/-/forwarded-3.0.1.tgz", {}, "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw=="],
|
||||||
|
|
||||||
"@fastify/merge-json-schemas": ["@fastify/merge-json-schemas@0.2.1", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A=="],
|
"@fastify/merge-json-schemas": ["@fastify/merge-json-schemas@0.2.1", "https://registry.npmmirror.com/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A=="],
|
||||||
|
|
||||||
"@fastify/otel": ["@fastify/otel@0.18.0", "https://registry.npmmirror.com/@fastify/otel/-/otel-0.18.0.tgz", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.212.0", "@opentelemetry/semantic-conventions": "^1.28.0", "minimatch": "^10.2.4" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0" } }, "sha512-3TASCATfw+ctICSb4ymrv7iCm0qJ0N9CarB+CZ7zIJ7KqNbwI5JjyDL1/sxoC0ccTO1Zyd1iQ+oqncPg5FJXaA=="],
|
"@fastify/otel": ["@fastify/otel@0.18.0", "https://registry.npmmirror.com/@fastify/otel/-/otel-0.18.0.tgz", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.212.0", "@opentelemetry/semantic-conventions": "^1.28.0", "minimatch": "^10.2.4" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0" } }, "sha512-3TASCATfw+ctICSb4ymrv7iCm0qJ0N9CarB+CZ7zIJ7KqNbwI5JjyDL1/sxoC0ccTO1Zyd1iQ+oqncPg5FJXaA=="],
|
||||||
|
|
||||||
"@fastify/proxy-addr": ["@fastify/proxy-addr@5.1.0", "", { "dependencies": { "@fastify/forwarded": "^3.0.0", "ipaddr.js": "^2.1.0" } }, "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw=="],
|
"@fastify/proxy-addr": ["@fastify/proxy-addr@5.1.0", "https://registry.npmmirror.com/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", { "dependencies": { "@fastify/forwarded": "^3.0.0", "ipaddr.js": "^2.1.0" } }, "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw=="],
|
||||||
|
|
||||||
"@growthbook/growthbook": ["@growthbook/growthbook@1.6.5", "https://registry.npmmirror.com/@growthbook/growthbook/-/growthbook-1.6.5.tgz", { "dependencies": { "dom-mutator": "^0.6.0" } }, "sha512-mUaMsgeUTpRIUOTn33EUXHRK6j7pxBjwqH4WpQyq+pukjd1AIzWlEa6w7i6bInJUcweGgP2beXZmaP6b6UPn7A=="],
|
"@growthbook/growthbook": ["@growthbook/growthbook@1.6.5", "https://registry.npmmirror.com/@growthbook/growthbook/-/growthbook-1.6.5.tgz", { "dependencies": { "dom-mutator": "^0.6.0" } }, "sha512-mUaMsgeUTpRIUOTn33EUXHRK6j7pxBjwqH4WpQyq+pukjd1AIzWlEa6w7i6bInJUcweGgP2beXZmaP6b6UPn7A=="],
|
||||||
|
|
||||||
@@ -841,6 +858,36 @@
|
|||||||
|
|
||||||
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "https://registry.npmmirror.com/@protobufjs/utf8/-/utf8-1.1.0.tgz", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="],
|
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "https://registry.npmmirror.com/@protobufjs/utf8/-/utf8-1.1.0.tgz", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", { "os": "android", "cpu": "arm64" }, "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz", { "os": "linux", "cpu": "arm" }, "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz", { "os": "linux", "cpu": "x64" }, "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz", { "os": "linux", "cpu": "x64" }, "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz", { "os": "none", "cpu": "arm64" }, "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz", { "dependencies": { "@emnapi/core": "1.9.2", "@emnapi/runtime": "1.9.2", "@napi-rs/wasm-runtime": "^1.1.3" }, "cpu": "none" }, "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz", { "os": "win32", "cpu": "x64" }, "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g=="],
|
||||||
|
|
||||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
|
||||||
|
|
||||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", { "os": "android", "cpu": "arm" }, "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA=="],
|
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", { "os": "android", "cpu": "arm" }, "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA=="],
|
||||||
@@ -1099,7 +1146,7 @@
|
|||||||
|
|
||||||
"@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=="],
|
"@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=="],
|
||||||
|
|
||||||
"abstract-logging": ["abstract-logging@2.0.1", "", {}, "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="],
|
"abstract-logging": ["abstract-logging@2.0.1", "https://registry.npmmirror.com/abstract-logging/-/abstract-logging-2.0.1.tgz", {}, "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="],
|
||||||
|
|
||||||
"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=="],
|
"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=="],
|
||||||
|
|
||||||
@@ -1133,7 +1180,7 @@
|
|||||||
|
|
||||||
"auto-bind": ["auto-bind@5.0.1", "https://registry.npmmirror.com/auto-bind/-/auto-bind-5.0.1.tgz", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="],
|
"auto-bind": ["auto-bind@5.0.1", "https://registry.npmmirror.com/auto-bind/-/auto-bind-5.0.1.tgz", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="],
|
||||||
|
|
||||||
"avvio": ["avvio@9.2.0", "", { "dependencies": { "@fastify/error": "^4.0.0", "fastq": "^1.17.1" } }, "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ=="],
|
"avvio": ["avvio@9.2.0", "https://registry.npmmirror.com/avvio/-/avvio-9.2.0.tgz", { "dependencies": { "@fastify/error": "^4.0.0", "fastq": "^1.17.1" } }, "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ=="],
|
||||||
|
|
||||||
"axios": ["axios@1.15.0", "https://registry.npmmirror.com/axios/-/axios-1.15.0.tgz", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q=="],
|
"axios": ["axios@1.15.0", "https://registry.npmmirror.com/axios/-/axios-1.15.0.tgz", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q=="],
|
||||||
|
|
||||||
@@ -1247,7 +1294,7 @@
|
|||||||
|
|
||||||
"depd": ["depd@2.0.0", "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
"depd": ["depd@2.0.0", "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||||
|
|
||||||
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
"dequal": ["dequal@2.0.3", "https://registry.npmmirror.com/dequal/-/dequal-2.0.3.tgz", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||||
|
|
||||||
"detect-libc": ["detect-libc@2.1.2", "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
"detect-libc": ["detect-libc@2.1.2", "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
|
|
||||||
@@ -1305,15 +1352,15 @@
|
|||||||
|
|
||||||
"external-editor": ["external-editor@3.1.0", "https://registry.npmmirror.com/external-editor/-/external-editor-3.1.0.tgz", { "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" } }, "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew=="],
|
"external-editor": ["external-editor@3.1.0", "https://registry.npmmirror.com/external-editor/-/external-editor-3.1.0.tgz", { "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" } }, "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew=="],
|
||||||
|
|
||||||
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
|
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "https://registry.npmmirror.com/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
|
||||||
|
|
||||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
"fast-deep-equal": ["fast-deep-equal@3.1.3", "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||||
|
|
||||||
"fast-glob": ["fast-glob@3.3.3", "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
"fast-glob": ["fast-glob@3.3.3", "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||||
|
|
||||||
"fast-json-stringify": ["fast-json-stringify@6.3.0", "", { "dependencies": { "@fastify/merge-json-schemas": "^0.2.0", "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0", "json-schema-ref-resolver": "^3.0.0", "rfdc": "^1.2.0" } }, "sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA=="],
|
"fast-json-stringify": ["fast-json-stringify@6.3.0", "https://registry.npmmirror.com/fast-json-stringify/-/fast-json-stringify-6.3.0.tgz", { "dependencies": { "@fastify/merge-json-schemas": "^0.2.0", "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0", "json-schema-ref-resolver": "^3.0.0", "rfdc": "^1.2.0" } }, "sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA=="],
|
||||||
|
|
||||||
"fast-querystring": ["fast-querystring@1.1.2", "", { "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg=="],
|
"fast-querystring": ["fast-querystring@1.1.2", "https://registry.npmmirror.com/fast-querystring/-/fast-querystring-1.1.2.tgz", { "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg=="],
|
||||||
|
|
||||||
"fast-uri": ["fast-uri@3.1.0", "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.1.0.tgz", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
"fast-uri": ["fast-uri@3.1.0", "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.1.0.tgz", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
||||||
|
|
||||||
@@ -1321,9 +1368,9 @@
|
|||||||
|
|
||||||
"fast-xml-parser": ["fast-xml-parser@5.5.8", "https://registry.npmmirror.com/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="],
|
"fast-xml-parser": ["fast-xml-parser@5.5.8", "https://registry.npmmirror.com/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="],
|
||||||
|
|
||||||
"fastify": ["fastify@5.8.4", "", { "dependencies": { "@fastify/ajv-compiler": "^4.0.5", "@fastify/error": "^4.0.0", "@fastify/fast-json-stringify-compiler": "^5.0.0", "@fastify/proxy-addr": "^5.0.0", "abstract-logging": "^2.0.1", "avvio": "^9.0.0", "fast-json-stringify": "^6.0.0", "find-my-way": "^9.0.0", "light-my-request": "^6.0.0", "pino": "^9.14.0 || ^10.1.0", "process-warning": "^5.0.0", "rfdc": "^1.3.1", "secure-json-parse": "^4.0.0", "semver": "^7.6.0", "toad-cache": "^3.7.0" } }, "sha512-sa42J1xylbBAYUWALSBoyXKPDUvM3OoNOibIefA+Oha57FryXKKCZarA1iDntOCWp3O35voZLuDg2mdODXtPzQ=="],
|
"fastify": ["fastify@5.8.4", "https://registry.npmmirror.com/fastify/-/fastify-5.8.4.tgz", { "dependencies": { "@fastify/ajv-compiler": "^4.0.5", "@fastify/error": "^4.0.0", "@fastify/fast-json-stringify-compiler": "^5.0.0", "@fastify/proxy-addr": "^5.0.0", "abstract-logging": "^2.0.1", "avvio": "^9.0.0", "fast-json-stringify": "^6.0.0", "find-my-way": "^9.0.0", "light-my-request": "^6.0.0", "pino": "^9.14.0 || ^10.1.0", "process-warning": "^5.0.0", "rfdc": "^1.3.1", "secure-json-parse": "^4.0.0", "semver": "^7.6.0", "toad-cache": "^3.7.0" } }, "sha512-sa42J1xylbBAYUWALSBoyXKPDUvM3OoNOibIefA+Oha57FryXKKCZarA1iDntOCWp3O35voZLuDg2mdODXtPzQ=="],
|
||||||
|
|
||||||
"fastify-plugin": ["fastify-plugin@5.1.0", "", {}, "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw=="],
|
"fastify-plugin": ["fastify-plugin@5.1.0", "https://registry.npmmirror.com/fastify-plugin/-/fastify-plugin-5.1.0.tgz", {}, "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw=="],
|
||||||
|
|
||||||
"fastq": ["fastq@1.20.1", "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
|
"fastq": ["fastq@1.20.1", "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
|
||||||
|
|
||||||
@@ -1341,7 +1388,7 @@
|
|||||||
|
|
||||||
"finalhandler": ["finalhandler@2.1.1", "https://registry.npmmirror.com/finalhandler/-/finalhandler-2.1.1.tgz", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
|
"finalhandler": ["finalhandler@2.1.1", "https://registry.npmmirror.com/finalhandler/-/finalhandler-2.1.1.tgz", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
|
||||||
|
|
||||||
"find-my-way": ["find-my-way@9.5.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", "safe-regex2": "^5.0.0" } }, "sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ=="],
|
"find-my-way": ["find-my-way@9.5.0", "https://registry.npmmirror.com/find-my-way/-/find-my-way-9.5.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", "safe-regex2": "^5.0.0" } }, "sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ=="],
|
||||||
|
|
||||||
"find-up": ["find-up@4.1.0", "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
|
"find-up": ["find-up@4.1.0", "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
|
||||||
|
|
||||||
@@ -1483,7 +1530,7 @@
|
|||||||
|
|
||||||
"json-bigint": ["json-bigint@1.0.0", "https://registry.npmmirror.com/json-bigint/-/json-bigint-1.0.0.tgz", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
|
"json-bigint": ["json-bigint@1.0.0", "https://registry.npmmirror.com/json-bigint/-/json-bigint-1.0.0.tgz", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
|
||||||
|
|
||||||
"json-schema-ref-resolver": ["json-schema-ref-resolver@3.0.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A=="],
|
"json-schema-ref-resolver": ["json-schema-ref-resolver@3.0.0", "https://registry.npmmirror.com/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A=="],
|
||||||
|
|
||||||
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "https://registry.npmmirror.com/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
|
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "https://registry.npmmirror.com/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
|
||||||
|
|
||||||
@@ -1505,7 +1552,7 @@
|
|||||||
|
|
||||||
"knip": ["knip@6.4.0", "https://registry.npmmirror.com/knip/-/knip-6.4.0.tgz", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "fast-glob": "^3.3.3", "formatly": "^0.3.0", "get-tsconfig": "4.13.7", "jiti": "^2.6.0", "minimist": "^1.2.8", "oxc-parser": "^0.121.0", "oxc-resolver": "^11.19.1", "picocolors": "^1.1.1", "picomatch": "^4.0.1", "smol-toml": "^1.6.1", "strip-json-comments": "5.0.3", "unbash": "^2.2.0", "yaml": "^2.8.2", "zod": "^4.1.11" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-SAEeggehgkPdoLZWVEcFKzPw+vNlnrUBDqcX8cOcHGydRInSn5pnn9LN3dDJ8SkDHKXR7xYzNq3HtRJaYmxOHg=="],
|
"knip": ["knip@6.4.0", "https://registry.npmmirror.com/knip/-/knip-6.4.0.tgz", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "fast-glob": "^3.3.3", "formatly": "^0.3.0", "get-tsconfig": "4.13.7", "jiti": "^2.6.0", "minimist": "^1.2.8", "oxc-parser": "^0.121.0", "oxc-resolver": "^11.19.1", "picocolors": "^1.1.1", "picomatch": "^4.0.1", "smol-toml": "^1.6.1", "strip-json-comments": "5.0.3", "unbash": "^2.2.0", "yaml": "^2.8.2", "zod": "^4.1.11" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-SAEeggehgkPdoLZWVEcFKzPw+vNlnrUBDqcX8cOcHGydRInSn5pnn9LN3dDJ8SkDHKXR7xYzNq3HtRJaYmxOHg=="],
|
||||||
|
|
||||||
"light-my-request": ["light-my-request@6.6.0", "", { "dependencies": { "cookie": "^1.0.1", "process-warning": "^4.0.0", "set-cookie-parser": "^2.6.0" } }, "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A=="],
|
"light-my-request": ["light-my-request@6.6.0", "https://registry.npmmirror.com/light-my-request/-/light-my-request-6.6.0.tgz", { "dependencies": { "cookie": "^1.0.1", "process-warning": "^4.0.0", "set-cookie-parser": "^2.6.0" } }, "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A=="],
|
||||||
|
|
||||||
"lightningcss": ["lightningcss@1.32.0", "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.32.0.tgz", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
"lightningcss": ["lightningcss@1.32.0", "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.32.0.tgz", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||||
|
|
||||||
@@ -1745,13 +1792,15 @@
|
|||||||
|
|
||||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||||
|
|
||||||
"ret": ["ret@0.5.0", "", {}, "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw=="],
|
"ret": ["ret@0.5.0", "https://registry.npmmirror.com/ret/-/ret-0.5.0.tgz", {}, "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw=="],
|
||||||
|
|
||||||
"retry": ["retry@0.12.0", "https://registry.npmmirror.com/retry/-/retry-0.12.0.tgz", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
|
"retry": ["retry@0.12.0", "https://registry.npmmirror.com/retry/-/retry-0.12.0.tgz", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
|
||||||
|
|
||||||
"reusify": ["reusify@1.1.0", "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
"reusify": ["reusify@1.1.0", "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||||
|
|
||||||
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
|
"rfdc": ["rfdc@1.4.1", "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
|
||||||
|
|
||||||
|
"rolldown": ["rolldown@1.0.0-rc.15", "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.0-rc.15.tgz", { "dependencies": { "@oxc-project/types": "=0.124.0", "@rolldown/pluginutils": "1.0.0-rc.15" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.15", "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", "@rolldown/binding-darwin-x64": "1.0.0-rc.15", "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g=="],
|
||||||
|
|
||||||
"rollup": ["rollup@4.60.1", "https://registry.npmmirror.com/rollup/-/rollup-4.60.1.tgz", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w=="],
|
"rollup": ["rollup@4.60.1", "https://registry.npmmirror.com/rollup/-/rollup-4.60.1.tgz", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w=="],
|
||||||
|
|
||||||
@@ -1763,7 +1812,7 @@
|
|||||||
|
|
||||||
"safe-buffer": ["safe-buffer@5.2.1", "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
"safe-buffer": ["safe-buffer@5.2.1", "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||||
|
|
||||||
"safe-regex2": ["safe-regex2@5.1.0", "", { "dependencies": { "ret": "~0.5.0" }, "bin": { "safe-regex2": "bin/safe-regex2.js" } }, "sha512-pNHAuBW7TrcleFHsxBr5QMi/Iyp0ENjUKz7GCcX1UO7cMh+NmVK6HxQckNL1tJp1XAJVjG6B8OKIPqodqj9rtw=="],
|
"safe-regex2": ["safe-regex2@5.1.0", "https://registry.npmmirror.com/safe-regex2/-/safe-regex2-5.1.0.tgz", { "dependencies": { "ret": "~0.5.0" }, "bin": { "safe-regex2": "bin/safe-regex2.js" } }, "sha512-pNHAuBW7TrcleFHsxBr5QMi/Iyp0ENjUKz7GCcX1UO7cMh+NmVK6HxQckNL1tJp1XAJVjG6B8OKIPqodqj9rtw=="],
|
||||||
|
|
||||||
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "https://registry.npmmirror.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
|
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "https://registry.npmmirror.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
|
||||||
|
|
||||||
@@ -1771,7 +1820,7 @@
|
|||||||
|
|
||||||
"scheduler": ["scheduler@0.27.0", "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
"scheduler": ["scheduler@0.27.0", "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
||||||
"secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="],
|
"secure-json-parse": ["secure-json-parse@4.1.0", "https://registry.npmmirror.com/secure-json-parse/-/secure-json-parse-4.1.0.tgz", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="],
|
||||||
|
|
||||||
"semver": ["semver@7.7.4", "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
"semver": ["semver@7.7.4", "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||||
|
|
||||||
@@ -1781,7 +1830,7 @@
|
|||||||
|
|
||||||
"set-blocking": ["set-blocking@2.0.0", "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="],
|
"set-blocking": ["set-blocking@2.0.0", "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="],
|
||||||
|
|
||||||
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
|
"set-cookie-parser": ["set-cookie-parser@2.7.2", "https://registry.npmmirror.com/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
|
||||||
|
|
||||||
"setprototypeof": ["setprototypeof@1.2.0", "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
"setprototypeof": ["setprototypeof@1.2.0", "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||||
|
|
||||||
@@ -1851,7 +1900,7 @@
|
|||||||
|
|
||||||
"to-regex-range": ["to-regex-range@5.0.1", "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
"to-regex-range": ["to-regex-range@5.0.1", "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||||
|
|
||||||
"toad-cache": ["toad-cache@3.7.0", "", {}, "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw=="],
|
"toad-cache": ["toad-cache@3.7.0", "https://registry.npmmirror.com/toad-cache/-/toad-cache-3.7.0.tgz", {}, "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw=="],
|
||||||
|
|
||||||
"toidentifier": ["toidentifier@1.0.1", "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
"toidentifier": ["toidentifier@1.0.1", "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||||
|
|
||||||
@@ -1893,7 +1942,7 @@
|
|||||||
|
|
||||||
"vary": ["vary@1.1.2", "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
"vary": ["vary@1.1.2", "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||||
|
|
||||||
"vite": ["vite@6.4.2", "https://registry.npmmirror.com/vite/-/vite-6.4.2.tgz", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ=="],
|
"vite": ["vite@8.0.8", "https://registry.npmmirror.com/vite/-/vite-8.0.8.tgz", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.15", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw=="],
|
||||||
|
|
||||||
"vscode-jsonrpc": ["vscode-jsonrpc@8.2.1", "https://registry.npmmirror.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz", {}, "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ=="],
|
"vscode-jsonrpc": ["vscode-jsonrpc@8.2.1", "https://registry.npmmirror.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz", {}, "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ=="],
|
||||||
|
|
||||||
@@ -1961,6 +2010,8 @@
|
|||||||
|
|
||||||
"@anthropic/remote-control-server/typescript": ["typescript@5.9.3", "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
"@anthropic/remote-control-server/typescript": ["typescript@5.9.3", "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"@anthropic/remote-control-server/vite": ["vite@6.4.2", "https://registry.npmmirror.com/vite/-/vite-6.4.2.tgz", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ=="],
|
||||||
|
|
||||||
"@aws-crypto/crc32/@aws-crypto/util": ["@aws-crypto/util@5.2.0", "https://registry.npmmirror.com/@aws-crypto/util/-/util-5.2.0.tgz", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
|
"@aws-crypto/crc32/@aws-crypto/util": ["@aws-crypto/util@5.2.0", "https://registry.npmmirror.com/@aws-crypto/util/-/util-5.2.0.tgz", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
|
||||||
|
|
||||||
"@aws-crypto/sha256-browser/@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "https://registry.npmmirror.com/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="],
|
"@aws-crypto/sha256-browser/@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "https://registry.npmmirror.com/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="],
|
||||||
@@ -2127,7 +2178,7 @@
|
|||||||
|
|
||||||
"@fastify/otel/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.212.0", "https://registry.npmmirror.com/@opentelemetry/instrumentation/-/instrumentation-0.212.0.tgz", { "dependencies": { "@opentelemetry/api-logs": "0.212.0", "import-in-the-middle": "^2.0.6", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IyXmpNnifNouMOe0I/gX7ENfv2ZCNdYTF0FpCsoBcpbIHzk81Ww9rQTYTnvghszCg7qGrIhNvWC8dhEifgX9Jg=="],
|
"@fastify/otel/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.212.0", "https://registry.npmmirror.com/@opentelemetry/instrumentation/-/instrumentation-0.212.0.tgz", { "dependencies": { "@opentelemetry/api-logs": "0.212.0", "import-in-the-middle": "^2.0.6", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IyXmpNnifNouMOe0I/gX7ENfv2ZCNdYTF0FpCsoBcpbIHzk81Ww9rQTYTnvghszCg7qGrIhNvWC8dhEifgX9Jg=="],
|
||||||
|
|
||||||
"@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
|
"@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-2.3.0.tgz", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
|
||||||
|
|
||||||
"@grpc/proto-loader/yargs": ["yargs@17.7.2", "https://registry.npmmirror.com/yargs/-/yargs-17.7.2.tgz", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
"@grpc/proto-loader/yargs": ["yargs@17.7.2", "https://registry.npmmirror.com/yargs/-/yargs-17.7.2.tgz", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||||
|
|
||||||
@@ -2295,9 +2346,9 @@
|
|||||||
|
|
||||||
"is-admin/execa": ["execa@5.1.1", "https://registry.npmmirror.com/execa/-/execa-5.1.1.tgz", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
|
"is-admin/execa": ["execa@5.1.1", "https://registry.npmmirror.com/execa/-/execa-5.1.1.tgz", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
|
||||||
|
|
||||||
"light-my-request/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
"light-my-request/cookie": ["cookie@1.1.1", "https://registry.npmmirror.com/cookie/-/cookie-1.1.1.tgz", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||||
|
|
||||||
"light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="],
|
"light-my-request/process-warning": ["process-warning@4.0.1", "https://registry.npmmirror.com/process-warning/-/process-warning-4.0.1.tgz", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="],
|
||||||
|
|
||||||
"micromatch/picomatch": ["picomatch@2.3.2", "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
"micromatch/picomatch": ["picomatch@2.3.2", "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||||
|
|
||||||
@@ -2313,6 +2364,10 @@
|
|||||||
|
|
||||||
"qrcode/yargs": ["yargs@15.4.1", "https://registry.npmmirror.com/yargs/-/yargs-15.4.1.tgz", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="],
|
"qrcode/yargs": ["yargs@15.4.1", "https://registry.npmmirror.com/yargs/-/yargs-15.4.1.tgz", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="],
|
||||||
|
|
||||||
|
"rolldown/@oxc-project/types": ["@oxc-project/types@0.124.0", "https://registry.npmmirror.com/@oxc-project/types/-/types-0.124.0.tgz", {}, "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg=="],
|
||||||
|
|
||||||
|
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", {}, "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g=="],
|
||||||
|
|
||||||
"vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "https://registry.npmmirror.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
|
"vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "https://registry.npmmirror.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
|
||||||
|
|
||||||
"xss/commander": ["commander@2.20.3", "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
"xss/commander": ["commander@2.20.3", "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
17
docs/diagrams/agent-loop-simple.mmd
Normal file
17
docs/diagrams/agent-loop-simple.mmd
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
flowchart TB
|
||||||
|
START((输入)) --> CTX["Context 管理"]
|
||||||
|
CTX --> LLM["LLM 流式输出"]
|
||||||
|
LLM --> TC{tool_use?}
|
||||||
|
|
||||||
|
TC --> |是| EXEC["执行工具"]
|
||||||
|
EXEC --> CTX
|
||||||
|
|
||||||
|
TC --> |否| DONE((完成))
|
||||||
|
|
||||||
|
classDef proc fill:#eef,stroke:#66c,color:#224
|
||||||
|
classDef decision fill:#fee,stroke:#c66,color:#422
|
||||||
|
classDef io fill:#eff,stroke:#6cc,color:#244
|
||||||
|
|
||||||
|
class CTX,LLM,EXEC proc
|
||||||
|
class TC decision
|
||||||
|
class START,DONE io
|
||||||
40
docs/diagrams/agent-loop.mmd
Normal file
40
docs/diagrams/agent-loop.mmd
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
flowchart TB
|
||||||
|
START((输入)) --> CTX["Context 管理"]
|
||||||
|
CTX --> PRE["Pre-sampling Hook"]
|
||||||
|
PRE --> LLM["LLM 流式输出"]
|
||||||
|
LLM --> TC{tool_use?}
|
||||||
|
|
||||||
|
TC --> |是| PERM{需权限?}
|
||||||
|
PERM --> |是| USER["👤 用户审批"]
|
||||||
|
USER --> |allow| TOOL_PRE
|
||||||
|
USER --> |deny| DENIED["拒绝"]
|
||||||
|
PERM --> |否| TOOL_PRE["Pre-tool Hook"]
|
||||||
|
TOOL_PRE --> EXEC["并发执行工具"]
|
||||||
|
EXEC --> TOOL_POST["Post-tool Hook"]
|
||||||
|
TOOL_POST --> CTX
|
||||||
|
DENIED --> CTX
|
||||||
|
|
||||||
|
TC --> |否| POST["Post-sampling Hook"]
|
||||||
|
POST --> STOP{"Stop Hook"}
|
||||||
|
STOP --> |不通过| CTX
|
||||||
|
STOP --> |通过| BUDGET{"Token Budget"}
|
||||||
|
BUDGET --> |继续| CTX
|
||||||
|
BUDGET --> |完成| DONE((完成))
|
||||||
|
|
||||||
|
subgraph SUB["子 Agent"]
|
||||||
|
FORK["AgentTool"] --> RECURSE["递归调用"]
|
||||||
|
end
|
||||||
|
|
||||||
|
EXEC -.-> FORK
|
||||||
|
|
||||||
|
classDef proc fill:#eef,stroke:#66c,color:#224
|
||||||
|
classDef decision fill:#fee,stroke:#c66,color:#422
|
||||||
|
classDef hook fill:#ffe,stroke:#cc6,color:#442
|
||||||
|
classDef io fill:#eff,stroke:#6cc,color:#244
|
||||||
|
classDef sub fill:#efe,stroke:#6a6,color:#242
|
||||||
|
|
||||||
|
class CTX,LLM,EXEC proc
|
||||||
|
class TC,PERM,STOP,BUDGET decision
|
||||||
|
class PRE,TOOL_PRE,TOOL_POST,POST hook
|
||||||
|
class START,DONE,USER,DENIED io
|
||||||
|
class FORK,RECURSE sub
|
||||||
189
docs/features/acp-zed.md
Normal file
189
docs/features/acp-zed.md
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
# ACP (Agent Client Protocol) — Zed / IDE 集成
|
||||||
|
|
||||||
|
> Feature Flag: `FEATURE_ACP=1`(build 和 dev 模式默认启用)
|
||||||
|
> 实现状态:可用(支持 Zed、Cursor 等 ACP 客户端)
|
||||||
|
> 源码目录:`src/services/acp/`
|
||||||
|
|
||||||
|
## 一、功能概述
|
||||||
|
|
||||||
|
ACP (Agent Client Protocol) 是一种标准化的 stdio 协议,允许 IDE 和编辑器通过 stdin/stdout 的 NDJSON 流驱动 AI Agent。CCB 实现了完整的 ACP agent 端,可以被 Zed、Cursor 等支持 ACP 的客户端直接调用。
|
||||||
|
|
||||||
|
### 核心特性
|
||||||
|
|
||||||
|
- **会话管理**:新建 / 恢复 / 加载 / 分叉 / 关闭会话
|
||||||
|
- **历史回放**:恢复会话时自动加载并回放对话历史
|
||||||
|
- **权限桥接**:ACP 客户端的权限决策映射到 CCB 的工具权限系统
|
||||||
|
- **斜杠命令 & Skills**:加载真实命令列表,支持 `/commit`、`/review` 等 prompt 型 skill
|
||||||
|
- **Context Window 跟踪**:精确的 usage_update,含 model prefix matching
|
||||||
|
- **Prompt 排队**:支持连续发送多条 prompt,自动排队处理
|
||||||
|
- **模式切换**:auto / default / acceptEdits / plan / dontAsk / bypassPermissions
|
||||||
|
- **模型切换**:运行时切换 AI 模型
|
||||||
|
|
||||||
|
## 二、架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────┐ NDJSON/stdio ┌──────────────────┐
|
||||||
|
│ Zed / IDE │ ◄────────────────► │ CCB ACP Agent │
|
||||||
|
│ (Client) │ stdin / stdout │ (Agent) │
|
||||||
|
└──────────────┘ │ │
|
||||||
|
│ entry.ts │ ← stdio → NDJSON stream
|
||||||
|
│ agent.ts │ ← ACP protocol handler
|
||||||
|
│ bridge.ts │ ← SDKMessage → ACP SessionUpdate
|
||||||
|
│ permissions.ts │ ← 权限桥接
|
||||||
|
│ utils.ts │ ← 通用工具
|
||||||
|
│ │
|
||||||
|
│ QueryEngine │ ← 内部查询引擎
|
||||||
|
└──────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 文件职责
|
||||||
|
|
||||||
|
| 文件 | 职责 |
|
||||||
|
|------|------|
|
||||||
|
| `entry.ts` | 入口,创建 stdio → NDJSON stream,启动 `AgentSideConnection` |
|
||||||
|
| `agent.ts` | 实现 ACP `Agent` 接口:会话 CRUD、prompt、cancel、模式/模型切换 |
|
||||||
|
| `bridge.ts` | `SDKMessage` → ACP `SessionUpdate` 转换:文本/思考/工具/用量/编辑 diff |
|
||||||
|
| `permissions.ts` | ACP `requestPermission()` → CCB `CanUseToolFn` 桥接 |
|
||||||
|
| `utils.ts` | Pushable、流转换、权限模式解析、session fingerprint、路径显示 |
|
||||||
|
|
||||||
|
## 三、配置 Zed 编辑器
|
||||||
|
|
||||||
|
### 3.1 Zed settings.json 配置
|
||||||
|
|
||||||
|
打开 Zed 的 `settings.json`(`Cmd+,` → Open Settings),添加 `agent_servers` 配置:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"agent_servers": {
|
||||||
|
"ccb": {
|
||||||
|
"type": "custom",
|
||||||
|
"command": "ccb",
|
||||||
|
"args": ["--acp"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 API 认证配置
|
||||||
|
|
||||||
|
CCB 的 ACP agent 在启动时会自动加载 `settings.json` 中的环境变量(`ANTHROPIC_BASE_URL`、`ANTHROPIC_AUTH_TOKEN` 等)。确保已通过 `/login` 配置好 API 供应商。
|
||||||
|
|
||||||
|
也可通过环境变量传入:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"agent_servers": {
|
||||||
|
"claude-code": {
|
||||||
|
"command": "ccb",
|
||||||
|
"args": ["--acp"],
|
||||||
|
"env": {
|
||||||
|
"ANTHROPIC_BASE_URL": "https://api.example.com/v1",
|
||||||
|
"ANTHROPIC_AUTH_TOKEN": "sk-xxx"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 在 Zed 中使用
|
||||||
|
|
||||||
|
1. 配置完成后重启 Zed
|
||||||
|
2. 打开任意项目目录
|
||||||
|
3. 按 `Cmd+'`(macOS)或 `Ctrl+'`(Linux)打开 Agent Panel
|
||||||
|
4. 在 Agent Panel 顶部的下拉菜单中选择 **claude-code**
|
||||||
|
5. 开始对话
|
||||||
|
|
||||||
|
### 3.5 功能说明
|
||||||
|
|
||||||
|
| 功能 | 操作 |
|
||||||
|
|------|------|
|
||||||
|
| 对话 | 在 Agent Panel 中直接输入消息 |
|
||||||
|
| 斜杠命令 | 输入 `/` 查看可用 skills 列表(如 `/commit`、`/review`) |
|
||||||
|
| 工具权限 | 弹出权限请求时选择 Allow / Reject / Always Allow |
|
||||||
|
| 模式切换 | 通过 Agent Panel 的设置菜单切换 auto/default/plan 等模式 |
|
||||||
|
| 模型切换 | 通过 Agent Panel 的设置菜单切换 AI 模型 |
|
||||||
|
| 会话恢复 | 关闭重开 Zed 后,之前的会话可自动恢复(含历史消息) |
|
||||||
|
|
||||||
|
## 四、配置其他 ACP 客户端
|
||||||
|
|
||||||
|
ACP 是开放协议,任何支持 ACP 的客户端都可以连接 CCB。通用配置模式:
|
||||||
|
|
||||||
|
```
|
||||||
|
命令: ccb --acp
|
||||||
|
参数: ["--acp"]
|
||||||
|
通信: stdin/stdout NDJSON
|
||||||
|
协议版本: ACP v1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.1 Cursor
|
||||||
|
|
||||||
|
在 Cursor 的设置中配置 MCP / Agent Server,使用同样的 `ccb --acp` 命令。
|
||||||
|
|
||||||
|
### 4.2 自定义客户端
|
||||||
|
|
||||||
|
使用 `@agentclientprotocol/sdk` 可以快速构建 ACP 客户端:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ClientSideConnection, ndJsonStream } from '@agentclientprotocol/sdk'
|
||||||
|
|
||||||
|
// 创建连接(将 ccb --acp 作为子进程启动)
|
||||||
|
const child = spawn('ccb', ['--acp'])
|
||||||
|
const stream = ndJsonStream(
|
||||||
|
Writable.toWeb(child.stdin),
|
||||||
|
Readable.toWeb(child.stdout),
|
||||||
|
)
|
||||||
|
|
||||||
|
const client = new ClientSideConnection(stream)
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
await client.initialize({ clientCapabilities: {} })
|
||||||
|
|
||||||
|
// 创建会话
|
||||||
|
const { sessionId } = await client.newSession({
|
||||||
|
cwd: '/path/to/project',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 发送 prompt
|
||||||
|
const response = await client.prompt({
|
||||||
|
sessionId,
|
||||||
|
prompt: [{ type: 'text', text: 'Hello, explain this project' }],
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听 session 更新
|
||||||
|
client.on('sessionUpdate', (update) => {
|
||||||
|
console.log('Update:', update)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 五、ACP 协议支持矩阵
|
||||||
|
|
||||||
|
| 方法 | 状态 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `initialize` | ✅ | 返回 agent 信息和能力 |
|
||||||
|
| `authenticate` | ✅ | 无需认证(自托管) |
|
||||||
|
| `newSession` | ✅ | 创建新会话 |
|
||||||
|
| `resumeSession` | ✅ | 恢复已有会话(含历史回放) |
|
||||||
|
| `loadSession` | ✅ | 加载指定会话(含历史回放) |
|
||||||
|
| `listSessions` | ✅ | 列出可用会话 |
|
||||||
|
| `forkSession` | ✅ | 分叉会话 |
|
||||||
|
| `closeSession` | ✅ | 关闭会话 |
|
||||||
|
| `prompt` | ✅ | 发送消息,支持排队 |
|
||||||
|
| `cancel` | ✅ | 取消当前/排队的 prompt |
|
||||||
|
| `setSessionMode` | ✅ | 切换权限模式 |
|
||||||
|
| `setSessionModel` | ✅ | 切换 AI 模型 |
|
||||||
|
| `setSessionConfigOption` | ✅ | 动态修改配置 |
|
||||||
|
|
||||||
|
### SessionUpdate 类型
|
||||||
|
|
||||||
|
| 类型 | 状态 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `agent_message_chunk` | ✅ | 助手文本消息 |
|
||||||
|
| `agent_thought_chunk` | ✅ | 思考/推理内容 |
|
||||||
|
| `user_message_chunk` | ✅ | 用户消息(历史回放) |
|
||||||
|
| `tool_call` | ✅ | 工具调用开始 |
|
||||||
|
| `tool_call_update` | ✅ | 工具调用结果/状态更新 |
|
||||||
|
| `usage_update` | ✅ | token 用量 + context window |
|
||||||
|
| `plan` | ✅ | TodoWrite → plan entries |
|
||||||
|
| `available_commands_update` | ✅ | 斜杠命令 & skills 列表 |
|
||||||
|
| `current_mode_update` | ✅ | 模式切换通知 |
|
||||||
|
| `config_option_update` | ✅ | 配置更新通知 |
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
### 第一步:安装 Chrome 扩展
|
### 第一步:安装 Chrome 扩展
|
||||||
|
|
||||||
1. 下载扩展:https://github.com/hangwin/mcp-chrome/releases(下载最新 zip)
|
1. 下载扩展:https://github.com/hangwin/mcp-chrome/releases
|
||||||
2. 解压 zip 文件
|
2. 解压 zip 文件
|
||||||
3. 打开 Chrome 访问 `chrome://extensions/`
|
3. 打开 Chrome 访问 `chrome://extensions/`
|
||||||
4. 开启右上角「开发者模式」
|
4. 开启右上角「开发者模式」
|
||||||
|
|||||||
@@ -138,13 +138,19 @@ bun run dist/cli.js
|
|||||||
/remote-control
|
/remote-control
|
||||||
```
|
```
|
||||||
|
|
||||||
CLI 会向 RCS 注册环境,注册成功后在终端显示连接 URL:
|
环境型 Remote Control(例如 `claude remote-control` 子命令)会向 RCS 注册环境,注册成功后在终端显示连接 URL:
|
||||||
|
|
||||||
```
|
```
|
||||||
https://rcs.example.com/code?bridge=<environmentId>
|
https://rcs.example.com/code?bridge=<environmentId>
|
||||||
```
|
```
|
||||||
|
|
||||||
同时支持 QR 码扫码打开。该 URL 即 Web UI 控制面板入口,在浏览器中打开即可远程操控当前会话。
|
交互式 REPL 方式(`--remote-control` 或 `/remote-control`)在某些桥接模式下也可能直接给出会话 URL:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://rcs.example.com/code/session_<id>
|
||||||
|
```
|
||||||
|
|
||||||
|
两种 URL 都可以直接在浏览器打开并远程操控当前会话;只有 environment 模式才会出现在 Web UI 的环境列表中。
|
||||||
|
|
||||||
若已连接,再次执行 `/remote-control` 会显示对话框,包含以下选项:
|
若已连接,再次执行 `/remote-control` 会显示对话框,包含以下选项:
|
||||||
- **Disconnect this session** — 断开远程连接
|
- **Disconnect this session** — 断开远程连接
|
||||||
@@ -165,7 +171,7 @@ claude bridge
|
|||||||
|
|
||||||
通过 `/remote-control` 命令获取 URL 后,在浏览器打开即可使用。功能:
|
通过 `/remote-control` 命令获取 URL 后,在浏览器打开即可使用。功能:
|
||||||
|
|
||||||
- 查看已注册的运行环境
|
- 查看已注册的运行环境(environment 模式)
|
||||||
- 创建和管理会话
|
- 创建和管理会话
|
||||||
- 实时查看对话消息和工具调用
|
- 实时查看对话消息和工具调用
|
||||||
- 审批 Claude Code 的工具权限请求
|
- 审批 Claude Code 的工具权限请求
|
||||||
@@ -275,4 +281,3 @@ curl https://rcs.example.com/health
|
|||||||
| 依赖 | claude.ai 订阅 + OAuth | 仅需 API Key |
|
| 依赖 | claude.ai 订阅 + OAuth | 仅需 API Key |
|
||||||
|
|
||||||
自托管模式的核心优势是:设置 `CLAUDE_BRIDGE_BASE_URL` 后,代码自动调用 `isSelfHostedBridge()` 返回 `true`,跳过所有 GrowthBook 和订阅检查,无需 claude.ai 账户即可使用。
|
自托管模式的核心优势是:设置 `CLAUDE_BRIDGE_BASE_URL` 后,代码自动调用 `isSelfHostedBridge()` 返回 `true`,跳过所有 GrowthBook 和订阅检查,无需 claude.ai 账户即可使用。
|
||||||
|
|
||||||
|
|||||||
36
package.json
36
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-code-best",
|
"name": "claude-code-best",
|
||||||
"version": "1.3.5",
|
"version": "1.4.1",
|
||||||
"description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal",
|
"description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"author": "claude-code-best <claude-code-best@proton.me>",
|
"author": "claude-code-best <claude-code-best@proton.me>",
|
||||||
@@ -31,7 +31,8 @@
|
|||||||
},
|
},
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
"packages/@ant/*"
|
"packages/@ant/*",
|
||||||
|
"packages/@anthropic-ai/*"
|
||||||
],
|
],
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
@@ -40,6 +41,9 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "bun run build.ts",
|
"build": "bun run build.ts",
|
||||||
|
"build:vite": "vite build && bun run scripts/post-build.ts",
|
||||||
|
"build:vite:only": "vite build",
|
||||||
|
"build:bun": "bun run build.ts",
|
||||||
"dev": "bun run scripts/dev.ts",
|
"dev": "bun run scripts/dev.ts",
|
||||||
"dev:inspect": "bun run scripts/dev-debug.ts",
|
"dev:inspect": "bun run scripts/dev-debug.ts",
|
||||||
"prepublishOnly": "bun run build",
|
"prepublishOnly": "bun run build",
|
||||||
@@ -50,19 +54,19 @@
|
|||||||
"test": "bun test",
|
"test": "bun test",
|
||||||
"check:unused": "knip-bun",
|
"check:unused": "knip-bun",
|
||||||
"health": "bun run scripts/health-check.ts",
|
"health": "bun run scripts/health-check.ts",
|
||||||
"postinstall": "node scripts/postinstall.cjs && node scripts/setup-chrome-mcp.mjs",
|
"postinstall": "node scripts/run-parallel.mjs scripts/postinstall.cjs scripts/setup-chrome-mcp.mjs",
|
||||||
"docs:dev": "npx mintlify dev",
|
"docs:dev": "npx mintlify dev",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
"rcs": "bun run scripts/rcs.ts"
|
"rcs": "bun run scripts/rcs.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@claude-code-best/mcp-chrome-bridge": "^2.0.7"
|
"@agentclientprotocol/sdk": "^0.19.0",
|
||||||
|
"@claude-code-best/mcp-chrome-bridge": "^2.0.8",
|
||||||
|
"ws": "^8.20.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/he": "^1.2.3",
|
|
||||||
"@langfuse/otel": "^5.1.0",
|
|
||||||
"@langfuse/tracing": "^5.1.0",
|
|
||||||
"@types/lodash-es": "^4.17.12",
|
|
||||||
"@alcalzone/ansi-tokenize": "^0.3.0",
|
"@alcalzone/ansi-tokenize": "^0.3.0",
|
||||||
|
"@ant/model-provider": "workspace:*",
|
||||||
"@ant/claude-for-chrome-mcp": "workspace:*",
|
"@ant/claude-for-chrome-mcp": "workspace:*",
|
||||||
"@ant/computer-use-input": "workspace:*",
|
"@ant/computer-use-input": "workspace:*",
|
||||||
"@ant/computer-use-mcp": "workspace:*",
|
"@ant/computer-use-mcp": "workspace:*",
|
||||||
@@ -75,9 +79,6 @@
|
|||||||
"@anthropic-ai/sdk": "^0.80.0",
|
"@anthropic-ai/sdk": "^0.80.0",
|
||||||
"@anthropic-ai/vertex-sdk": "^0.14.4",
|
"@anthropic-ai/vertex-sdk": "^0.14.4",
|
||||||
"@anthropic/ink": "workspace:*",
|
"@anthropic/ink": "workspace:*",
|
||||||
"@claude-code-best/builtin-tools": "workspace:*",
|
|
||||||
"@claude-code-best/agent-tools": "workspace:*",
|
|
||||||
"@claude-code-best/mcp-client": "workspace:*",
|
|
||||||
"@aws-sdk/client-bedrock": "^3.1020.0",
|
"@aws-sdk/client-bedrock": "^3.1020.0",
|
||||||
"@aws-sdk/client-bedrock-runtime": "^3.1020.0",
|
"@aws-sdk/client-bedrock-runtime": "^3.1020.0",
|
||||||
"@aws-sdk/client-sts": "^3.1020.0",
|
"@aws-sdk/client-sts": "^3.1020.0",
|
||||||
@@ -85,8 +86,13 @@
|
|||||||
"@aws-sdk/credential-providers": "^3.1020.0",
|
"@aws-sdk/credential-providers": "^3.1020.0",
|
||||||
"@azure/identity": "^4.13.1",
|
"@azure/identity": "^4.13.1",
|
||||||
"@biomejs/biome": "^2.4.10",
|
"@biomejs/biome": "^2.4.10",
|
||||||
|
"@claude-code-best/agent-tools": "workspace:*",
|
||||||
|
"@claude-code-best/builtin-tools": "workspace:*",
|
||||||
|
"@claude-code-best/mcp-client": "workspace:*",
|
||||||
"@commander-js/extra-typings": "^14.0.0",
|
"@commander-js/extra-typings": "^14.0.0",
|
||||||
"@growthbook/growthbook": "^1.6.5",
|
"@growthbook/growthbook": "^1.6.5",
|
||||||
|
"@langfuse/otel": "^5.1.0",
|
||||||
|
"@langfuse/tracing": "^5.1.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
"@opentelemetry/api": "^1.9.1",
|
"@opentelemetry/api": "^1.9.1",
|
||||||
"@opentelemetry/api-logs": "^0.214.0",
|
"@opentelemetry/api-logs": "^0.214.0",
|
||||||
@@ -109,8 +115,11 @@
|
|||||||
"@sentry/node": "^10.47.0",
|
"@sentry/node": "^10.47.0",
|
||||||
"@smithy/core": "^3.23.13",
|
"@smithy/core": "^3.23.13",
|
||||||
"@smithy/node-http-handler": "^4.5.1",
|
"@smithy/node-http-handler": "^4.5.1",
|
||||||
"@types/bun": "^1.3.11",
|
"@types/bun": "^1.3.12",
|
||||||
"@types/cacache": "^20.0.1",
|
"@types/cacache": "^20.0.1",
|
||||||
|
"@types/he": "^1.2.3",
|
||||||
|
"@types/lodash-es": "^4.17.12",
|
||||||
|
"@types/node": "^25.6.0",
|
||||||
"@types/picomatch": "^4.0.3",
|
"@types/picomatch": "^4.0.3",
|
||||||
"@types/plist": "^3.0.5",
|
"@types/plist": "^3.0.5",
|
||||||
"@types/proper-lockfile": "^4.1.4",
|
"@types/proper-lockfile": "^4.1.4",
|
||||||
@@ -166,6 +175,7 @@
|
|||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-compiler-runtime": "^1.0.0",
|
"react-compiler-runtime": "^1.0.0",
|
||||||
"react-reconciler": "^0.33.0",
|
"react-reconciler": "^0.33.0",
|
||||||
|
"rollup": "^4.60.1",
|
||||||
"semver": "^7.7.4",
|
"semver": "^7.7.4",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"shell-quote": "^1.8.3",
|
"shell-quote": "^1.8.3",
|
||||||
@@ -180,11 +190,11 @@
|
|||||||
"undici": "^7.24.6",
|
"undici": "^7.24.6",
|
||||||
"url-handler-napi": "workspace:*",
|
"url-handler-napi": "workspace:*",
|
||||||
"usehooks-ts": "^3.1.1",
|
"usehooks-ts": "^3.1.1",
|
||||||
|
"vite": "^8.0.8",
|
||||||
"vscode-jsonrpc": "^8.2.1",
|
"vscode-jsonrpc": "^8.2.1",
|
||||||
"vscode-languageserver-protocol": "^3.17.5",
|
"vscode-languageserver-protocol": "^3.17.5",
|
||||||
"vscode-languageserver-types": "^3.17.5",
|
"vscode-languageserver-types": "^3.17.5",
|
||||||
"wrap-ansi": "^10.0.0",
|
"wrap-ansi": "^10.0.0",
|
||||||
"ws": "^8.20.0",
|
|
||||||
"xss": "^1.0.15",
|
"xss": "^1.0.15",
|
||||||
"yaml": "^2.8.3",
|
"yaml": "^2.8.3",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
|
|||||||
5
packages/@ant/claude-for-chrome-mcp/tsconfig.json
Normal file
5
packages/@ant/claude-for-chrome-mcp/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../tsconfig.base.json",
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
@@ -5,9 +5,12 @@
|
|||||||
* mouse and keyboard via CoreGraphics events and System Events.
|
* mouse and keyboard via CoreGraphics events and System Events.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { $ } from 'bun'
|
import { execFile, execFileSync } from 'child_process'
|
||||||
|
import { promisify } from 'util'
|
||||||
import type { FrontmostAppInfo, InputBackend } from '../types.js'
|
import type { FrontmostAppInfo, InputBackend } from '../types.js'
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile)
|
||||||
|
|
||||||
const KEY_MAP: Record<string, number> = {
|
const KEY_MAP: Record<string, number> = {
|
||||||
return: 36, enter: 36, tab: 48, space: 49, delete: 51, backspace: 51,
|
return: 36, enter: 36, tab: 48, space: 49, delete: 51, backspace: 51,
|
||||||
escape: 53, esc: 53,
|
escape: 53, esc: 53,
|
||||||
@@ -25,13 +28,17 @@ const MODIFIER_MAP: Record<string, string> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function osascript(script: string): Promise<string> {
|
async function osascript(script: string): Promise<string> {
|
||||||
const result = await $`osascript -e ${script}`.quiet().nothrow().text()
|
const { stdout } = await execFileAsync('osascript', ['-e', script], {
|
||||||
return result.trim()
|
encoding: 'utf-8',
|
||||||
|
})
|
||||||
|
return stdout.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function jxa(script: string): Promise<string> {
|
async function jxa(script: string): Promise<string> {
|
||||||
const result = await $`osascript -l JavaScript -e ${script}`.quiet().nothrow().text()
|
const { stdout } = await execFileAsync('osascript', ['-l', 'JavaScript', '-e', script], {
|
||||||
return result.trim()
|
encoding: 'utf-8',
|
||||||
|
})
|
||||||
|
return stdout.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildMouseJxa(eventType: string, x: number, y: number, btn: number, clickState?: number): string {
|
function buildMouseJxa(eventType: string, x: number, y: number, btn: number, clickState?: number): string {
|
||||||
@@ -115,19 +122,14 @@ export const typeText: InputBackend['typeText'] = async (text) => {
|
|||||||
|
|
||||||
export const getFrontmostAppInfo: InputBackend['getFrontmostAppInfo'] = () => {
|
export const getFrontmostAppInfo: InputBackend['getFrontmostAppInfo'] = () => {
|
||||||
try {
|
try {
|
||||||
const result = Bun.spawnSync({
|
const output = execFileSync('osascript', ['-e', `
|
||||||
cmd: ['osascript', '-e', `
|
|
||||||
tell application "System Events"
|
tell application "System Events"
|
||||||
set frontApp to first application process whose frontmost is true
|
set frontApp to first application process whose frontmost is true
|
||||||
set appName to name of frontApp
|
set appName to name of frontApp
|
||||||
set bundleId to bundle identifier of frontApp
|
set bundleId to bundle identifier of frontApp
|
||||||
return bundleId & "|" & appName
|
return bundleId & "|" & appName
|
||||||
end tell
|
end tell
|
||||||
`],
|
`], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim()
|
||||||
stdout: 'pipe',
|
|
||||||
stderr: 'pipe',
|
|
||||||
})
|
|
||||||
const output = new TextDecoder().decode(result.stdout).trim()
|
|
||||||
if (!output || !output.includes('|')) return null
|
if (!output || !output.includes('|')) return null
|
||||||
const [bundleId, appName] = output.split('|', 2)
|
const [bundleId, appName] = output.split('|', 2)
|
||||||
return { bundleId: bundleId!, appName: appName! }
|
return { bundleId: bundleId!, appName: appName! }
|
||||||
|
|||||||
5
packages/@ant/computer-use-input/tsconfig.json
Normal file
5
packages/@ant/computer-use-input/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../tsconfig.base.json",
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
5
packages/@ant/computer-use-mcp/tsconfig.json
Normal file
5
packages/@ant/computer-use-mcp/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../tsconfig.base.json",
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
@@ -274,4 +274,9 @@ export const screenshot: ScreenshotAPI = {
|
|||||||
if (displayId !== undefined) args.push('-D', String(displayId))
|
if (displayId !== undefined) args.push('-D', String(displayId))
|
||||||
return captureScreenToBase64(args)
|
return captureScreenToBase64(args)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
captureWindowTarget(_titleOrHwnd: string | number): ScreenshotResult | null {
|
||||||
|
// Window capture not supported on macOS via this backend
|
||||||
|
return null
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -275,4 +275,9 @@ export const screenshot: ScreenshotAPI = {
|
|||||||
return { base64: '', width: 0, height: 0 }
|
return { base64: '', width: 0, height: 0 }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
captureWindowTarget(_titleOrHwnd: string | number): ScreenshotResult | null {
|
||||||
|
// Window capture not supported on Linux via this backend
|
||||||
|
return null
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ export interface ScreenshotAPI {
|
|||||||
x: number, y: number, w: number, h: number,
|
x: number, y: number, w: number, h: number,
|
||||||
outW: number, outH: number, quality: number, displayId?: number,
|
outW: number, outH: number, quality: number, displayId?: number,
|
||||||
): Promise<ScreenshotResult>
|
): Promise<ScreenshotResult>
|
||||||
|
captureWindowTarget(titleOrHwnd: string | number): ScreenshotResult | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SwiftBackend {
|
export interface SwiftBackend {
|
||||||
|
|||||||
5
packages/@ant/computer-use-swift/tsconfig.json
Normal file
5
packages/@ant/computer-use-swift/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../tsconfig.json",
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
5
packages/@ant/ink/tsconfig.json
Normal file
5
packages/@ant/ink/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../tsconfig.base.json",
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
18
packages/@ant/model-provider/package.json
Normal file
18
packages/@ant/model-provider/package.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "@ant/model-provider",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "./src/index.ts",
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts",
|
||||||
|
"./types": "./src/types/index.ts",
|
||||||
|
"./hooks": "./src/hooks/index.ts",
|
||||||
|
"./client": "./src/client/index.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@anthropic-ai/sdk": "^0.80.0",
|
||||||
|
"openai": "^6.33.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
27
packages/@ant/model-provider/src/client/index.ts
Normal file
27
packages/@ant/model-provider/src/client/index.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import type { ClientFactories } from './types.js'
|
||||||
|
|
||||||
|
let registeredFactories: ClientFactories | null = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register client factories from the main project.
|
||||||
|
* Call this during application initialization.
|
||||||
|
*/
|
||||||
|
export function registerClientFactories(factories: ClientFactories): void {
|
||||||
|
registeredFactories = factories
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get registered client factories.
|
||||||
|
* Throws if not registered (fail-fast).
|
||||||
|
*/
|
||||||
|
export function getClientFactories(): ClientFactories {
|
||||||
|
if (!registeredFactories) {
|
||||||
|
throw new Error(
|
||||||
|
'Client factories not registered. ' +
|
||||||
|
'Call registerClientFactories() during app initialization.',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return registeredFactories
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { ClientFactories }
|
||||||
35
packages/@ant/model-provider/src/client/types.ts
Normal file
35
packages/@ant/model-provider/src/client/types.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* Client factory interfaces.
|
||||||
|
* Authentication is handled externally — main project provides factory implementations.
|
||||||
|
*/
|
||||||
|
export interface ClientFactories {
|
||||||
|
/** Get Anthropic client (1st party, Bedrock, Foundry, Vertex) */
|
||||||
|
getAnthropicClient: (params: {
|
||||||
|
model?: string
|
||||||
|
maxRetries: number
|
||||||
|
fetchOverride?: unknown
|
||||||
|
source?: string
|
||||||
|
}) => Promise<unknown>
|
||||||
|
|
||||||
|
/** Get OpenAI-compatible client */
|
||||||
|
getOpenAIClient: (params: {
|
||||||
|
maxRetries: number
|
||||||
|
fetchOverride?: unknown
|
||||||
|
source?: string
|
||||||
|
}) => unknown
|
||||||
|
|
||||||
|
/** Stream Gemini generate content */
|
||||||
|
streamGeminiGenerateContent: (params: {
|
||||||
|
model: string
|
||||||
|
signal?: AbortSignal
|
||||||
|
fetchOverride?: unknown
|
||||||
|
body: Record<string, unknown>
|
||||||
|
}) => AsyncIterable<unknown>
|
||||||
|
|
||||||
|
/** Get Grok client (OpenAI-compatible) */
|
||||||
|
getGrokClient: (params: {
|
||||||
|
maxRetries: number
|
||||||
|
fetchOverride?: unknown
|
||||||
|
source?: string
|
||||||
|
}) => unknown
|
||||||
|
}
|
||||||
238
packages/@ant/model-provider/src/errorUtils.ts
Normal file
238
packages/@ant/model-provider/src/errorUtils.ts
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
import type { APIError } from '@anthropic-ai/sdk'
|
||||||
|
|
||||||
|
// SSL/TLS error codes from OpenSSL (used by both Node.js and Bun)
|
||||||
|
// See: https://www.openssl.org/docs/man3.1/man3/X509_STORE_CTX_get_error.html
|
||||||
|
const SSL_ERROR_CODES = new Set([
|
||||||
|
// Certificate verification errors
|
||||||
|
'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
|
||||||
|
'UNABLE_TO_GET_ISSUER_CERT',
|
||||||
|
'UNABLE_TO_GET_ISSUER_CERT_LOCALLY',
|
||||||
|
'CERT_SIGNATURE_FAILURE',
|
||||||
|
'CERT_NOT_YET_VALID',
|
||||||
|
'CERT_HAS_EXPIRED',
|
||||||
|
'CERT_REVOKED',
|
||||||
|
'CERT_REJECTED',
|
||||||
|
'CERT_UNTRUSTED',
|
||||||
|
// Self-signed certificate errors
|
||||||
|
'DEPTH_ZERO_SELF_SIGNED_CERT',
|
||||||
|
'SELF_SIGNED_CERT_IN_CHAIN',
|
||||||
|
// Chain errors
|
||||||
|
'CERT_CHAIN_TOO_LONG',
|
||||||
|
'PATH_LENGTH_EXCEEDED',
|
||||||
|
// Hostname/altname errors
|
||||||
|
'ERR_TLS_CERT_ALTNAME_INVALID',
|
||||||
|
'HOSTNAME_MISMATCH',
|
||||||
|
// TLS handshake errors
|
||||||
|
'ERR_TLS_HANDSHAKE_TIMEOUT',
|
||||||
|
'ERR_SSL_WRONG_VERSION_NUMBER',
|
||||||
|
'ERR_SSL_DECRYPTION_FAILED_OR_BAD_RECORD_MAC',
|
||||||
|
])
|
||||||
|
|
||||||
|
export type ConnectionErrorDetails = {
|
||||||
|
code: string
|
||||||
|
message: string
|
||||||
|
isSSLError: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts connection error details from the error cause chain.
|
||||||
|
* The Anthropic SDK wraps underlying errors in the `cause` property.
|
||||||
|
* This function walks the cause chain to find the root error code/message.
|
||||||
|
*/
|
||||||
|
export function extractConnectionErrorDetails(
|
||||||
|
error: unknown,
|
||||||
|
): ConnectionErrorDetails | null {
|
||||||
|
if (!error || typeof error !== 'object') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk the cause chain to find the root error with a code
|
||||||
|
let current: unknown = error
|
||||||
|
const maxDepth = 5 // Prevent infinite loops
|
||||||
|
let depth = 0
|
||||||
|
|
||||||
|
while (current && depth < maxDepth) {
|
||||||
|
if (
|
||||||
|
current instanceof Error &&
|
||||||
|
'code' in current &&
|
||||||
|
typeof current.code === 'string'
|
||||||
|
) {
|
||||||
|
const code = current.code
|
||||||
|
const isSSLError = SSL_ERROR_CODES.has(code)
|
||||||
|
return {
|
||||||
|
code,
|
||||||
|
message: current.message,
|
||||||
|
isSSLError,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to the next cause in the chain
|
||||||
|
if (
|
||||||
|
current instanceof Error &&
|
||||||
|
'cause' in current &&
|
||||||
|
current.cause !== current
|
||||||
|
) {
|
||||||
|
current = current.cause
|
||||||
|
depth++
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an actionable hint for SSL/TLS errors, intended for contexts outside
|
||||||
|
* the main API client (OAuth token exchange, preflight connectivity checks)
|
||||||
|
* where `formatAPIError` doesn't apply.
|
||||||
|
*/
|
||||||
|
export function getSSLErrorHint(error: unknown): string | null {
|
||||||
|
const details = extractConnectionErrorDetails(error)
|
||||||
|
if (!details?.isSSLError) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return `SSL certificate error (${details.code}). If you are behind a corporate proxy or TLS-intercepting firewall, set NODE_EXTRA_CA_CERTS to your CA bundle path, or ask IT to allowlist *.anthropic.com. Run /doctor for details.`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strips HTML content (e.g., CloudFlare error pages) from a message string,
|
||||||
|
* returning a user-friendly title or empty string if HTML is detected.
|
||||||
|
* Returns the original message unchanged if no HTML is found.
|
||||||
|
*/
|
||||||
|
function sanitizeMessageHTML(message: string): string {
|
||||||
|
if (message.includes('<!DOCTYPE html') || message.includes('<html')) {
|
||||||
|
const titleMatch = message.match(/<title>([^<]+)<\/title>/)
|
||||||
|
if (titleMatch && titleMatch[1]) {
|
||||||
|
return titleMatch[1].trim()
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects if an error message contains HTML content (e.g., CloudFlare error pages)
|
||||||
|
* and returns a user-friendly message instead
|
||||||
|
*/
|
||||||
|
export function sanitizeAPIError(apiError: APIError): string {
|
||||||
|
const message = apiError.message
|
||||||
|
if (!message) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return sanitizeMessageHTML(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shapes of deserialized API errors from session JSONL.
|
||||||
|
*/
|
||||||
|
type NestedAPIError = {
|
||||||
|
error?: {
|
||||||
|
message?: string
|
||||||
|
error?: { message?: string }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasNestedError(value: unknown): value is NestedAPIError {
|
||||||
|
return (
|
||||||
|
typeof value === 'object' &&
|
||||||
|
value !== null &&
|
||||||
|
'error' in value &&
|
||||||
|
typeof value.error === 'object' &&
|
||||||
|
value.error !== null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a human-readable message from a deserialized API error that lacks
|
||||||
|
* a top-level `.message`.
|
||||||
|
*/
|
||||||
|
function extractNestedErrorMessage(error: APIError): string | null {
|
||||||
|
if (!hasNestedError(error)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const narrowed: NestedAPIError = error
|
||||||
|
const nested = narrowed.error
|
||||||
|
|
||||||
|
// Standard Anthropic API shape: { error: { error: { message } } }
|
||||||
|
const deepMsg = nested?.error?.message
|
||||||
|
if (typeof deepMsg === 'string' && deepMsg.length > 0) {
|
||||||
|
const sanitized = sanitizeMessageHTML(deepMsg)
|
||||||
|
if (sanitized.length > 0) {
|
||||||
|
return sanitized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bedrock shape: { error: { message } }
|
||||||
|
const msg = nested?.message
|
||||||
|
if (typeof msg === 'string' && msg.length > 0) {
|
||||||
|
const sanitized = sanitizeMessageHTML(msg)
|
||||||
|
if (sanitized.length > 0) {
|
||||||
|
return sanitized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatAPIError(error: APIError): string {
|
||||||
|
// Extract connection error details from the cause chain
|
||||||
|
const connectionDetails = extractConnectionErrorDetails(error)
|
||||||
|
|
||||||
|
if (connectionDetails) {
|
||||||
|
const { code, isSSLError } = connectionDetails
|
||||||
|
|
||||||
|
// Handle timeout errors
|
||||||
|
if (code === 'ETIMEDOUT') {
|
||||||
|
return 'Request timed out. Check your internet connection and proxy settings'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle SSL/TLS errors with specific messages
|
||||||
|
if (isSSLError) {
|
||||||
|
switch (code) {
|
||||||
|
case 'UNABLE_TO_VERIFY_LEAF_SIGNATURE':
|
||||||
|
case 'UNABLE_TO_GET_ISSUER_CERT':
|
||||||
|
case 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY':
|
||||||
|
return 'Unable to connect to API: SSL certificate verification failed. Check your proxy or corporate SSL certificates'
|
||||||
|
case 'CERT_HAS_EXPIRED':
|
||||||
|
return 'Unable to connect to API: SSL certificate has expired'
|
||||||
|
case 'CERT_REVOKED':
|
||||||
|
return 'Unable to connect to API: SSL certificate has been revoked'
|
||||||
|
case 'DEPTH_ZERO_SELF_SIGNED_CERT':
|
||||||
|
case 'SELF_SIGNED_CERT_IN_CHAIN':
|
||||||
|
return 'Unable to connect to API: Self-signed certificate detected. Check your proxy or corporate SSL certificates'
|
||||||
|
case 'ERR_TLS_CERT_ALTNAME_INVALID':
|
||||||
|
case 'HOSTNAME_MISMATCH':
|
||||||
|
return 'Unable to connect to API: SSL certificate hostname mismatch'
|
||||||
|
case 'CERT_NOT_YET_VALID':
|
||||||
|
return 'Unable to connect to API: SSL certificate is not yet valid'
|
||||||
|
default:
|
||||||
|
return `Unable to connect to API: SSL error (${code})`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.message === 'Connection error.') {
|
||||||
|
// If we have a code but it's not SSL, include it for debugging
|
||||||
|
if (connectionDetails?.code) {
|
||||||
|
return `Unable to connect to API (${connectionDetails.code})`
|
||||||
|
}
|
||||||
|
return 'Unable to connect to API. Check your internet connection'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guard: when deserialized from JSONL (e.g. --resume), the error object may
|
||||||
|
// be a plain object without a `.message` property.
|
||||||
|
if (!error.message) {
|
||||||
|
return (
|
||||||
|
extractNestedErrorMessage(error) ??
|
||||||
|
`API error (status ${error.status ?? 'unknown'})`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizedMessage = sanitizeAPIError(error)
|
||||||
|
// Use sanitized message if it's different from the original (i.e., HTML was sanitized)
|
||||||
|
return sanitizedMessage !== error.message && sanitizedMessage.length > 0
|
||||||
|
? sanitizedMessage
|
||||||
|
: error.message
|
||||||
|
}
|
||||||
27
packages/@ant/model-provider/src/hooks/index.ts
Normal file
27
packages/@ant/model-provider/src/hooks/index.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import type { ModelProviderHooks } from './types.js'
|
||||||
|
|
||||||
|
let registeredHooks: ModelProviderHooks | null = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register hooks from the main project.
|
||||||
|
* Call this during application initialization.
|
||||||
|
*/
|
||||||
|
export function registerHooks(hooks: ModelProviderHooks): void {
|
||||||
|
registeredHooks = hooks
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get registered hooks.
|
||||||
|
* Throws if hooks not registered (fail-fast).
|
||||||
|
*/
|
||||||
|
export function getHooks(): ModelProviderHooks {
|
||||||
|
if (!registeredHooks) {
|
||||||
|
throw new Error(
|
||||||
|
'ModelProvider hooks not registered. ' +
|
||||||
|
'Call registerHooks() during app initialization.',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return registeredHooks
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { ModelProviderHooks }
|
||||||
48
packages/@ant/model-provider/src/hooks/types.ts
Normal file
48
packages/@ant/model-provider/src/hooks/types.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* Hooks for dependency injection.
|
||||||
|
* Main project provides implementations; model-provider calls them.
|
||||||
|
*
|
||||||
|
* This decouples the model-provider from main project specifics like
|
||||||
|
* analytics, cost tracking, feature flags, etc.
|
||||||
|
*/
|
||||||
|
export interface ModelProviderHooks {
|
||||||
|
/** Log an analytics event (replaces direct logEvent calls) */
|
||||||
|
logEvent: (eventName: string, metadata?: Record<string, unknown>) => void
|
||||||
|
|
||||||
|
/** Report API cost after each response */
|
||||||
|
reportCost: (params: {
|
||||||
|
costUSD: number
|
||||||
|
usage: Record<string, unknown>
|
||||||
|
model: string
|
||||||
|
}) => void
|
||||||
|
|
||||||
|
/** Get tool permission context */
|
||||||
|
getToolPermissionContext?: () => Promise<Record<string, unknown>>
|
||||||
|
|
||||||
|
/** Debug logging */
|
||||||
|
logForDebugging: (msg: string, opts?: { level?: string }) => void
|
||||||
|
|
||||||
|
/** Error logging */
|
||||||
|
logError: (error: Error) => void
|
||||||
|
|
||||||
|
/** Get feature flag value */
|
||||||
|
getFeatureFlag?: (flagName: string) => unknown
|
||||||
|
|
||||||
|
/** Get session ID */
|
||||||
|
getSessionId: () => string
|
||||||
|
|
||||||
|
/** Add a notification */
|
||||||
|
addNotification?: (notification: Record<string, unknown>) => void
|
||||||
|
|
||||||
|
/** Get API provider name */
|
||||||
|
getAPIProvider: () => string
|
||||||
|
|
||||||
|
/** Get user ID */
|
||||||
|
getOrCreateUserID: () => string
|
||||||
|
|
||||||
|
/** Check if non-interactive session */
|
||||||
|
isNonInteractiveSession: () => boolean
|
||||||
|
|
||||||
|
/** Get OAuth account info */
|
||||||
|
getOauthAccountInfo?: () => Record<string, unknown> | undefined
|
||||||
|
}
|
||||||
63
packages/@ant/model-provider/src/index.ts
Normal file
63
packages/@ant/model-provider/src/index.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
// @ant/model-provider
|
||||||
|
// Model provider abstraction layer for Claude Code
|
||||||
|
//
|
||||||
|
// This package owns the model calling logic and provides:
|
||||||
|
// - Core query functions (queryModelWithStreaming, etc.)
|
||||||
|
// - Provider implementations (Anthropic, OpenAI, Gemini, Grok)
|
||||||
|
// - Type definitions (Message, Tool, Usage, etc.)
|
||||||
|
// - Dependency injection hooks (analytics, cost tracking, etc.)
|
||||||
|
//
|
||||||
|
// Initialization:
|
||||||
|
// registerClientFactories({ ... }) // inject auth clients
|
||||||
|
// registerHooks({ ... }) // inject analytics/cost/logging
|
||||||
|
|
||||||
|
// Hooks (dependency injection)
|
||||||
|
export { registerHooks, getHooks } from './hooks/index.js'
|
||||||
|
export type { ModelProviderHooks } from './hooks/types.js'
|
||||||
|
|
||||||
|
// Client factories
|
||||||
|
export { registerClientFactories, getClientFactories } from './client/index.js'
|
||||||
|
export type { ClientFactories } from './client/types.js'
|
||||||
|
|
||||||
|
// Types
|
||||||
|
export * from './types/index.js'
|
||||||
|
|
||||||
|
// Provider model mappings
|
||||||
|
export { resolveOpenAIModel } from './providers/openai/modelMapping.js'
|
||||||
|
export { resolveGrokModel } from './providers/grok/modelMapping.js'
|
||||||
|
export { resolveGeminiModel } from './providers/gemini/modelMapping.js'
|
||||||
|
|
||||||
|
// Gemini provider utilities
|
||||||
|
export { anthropicMessagesToGemini } from './providers/gemini/convertMessages.js'
|
||||||
|
export { anthropicToolsToGemini, anthropicToolChoiceToGemini } from './providers/gemini/convertTools.js'
|
||||||
|
export { adaptGeminiStreamToAnthropic } from './providers/gemini/streamAdapter.js'
|
||||||
|
export {
|
||||||
|
GEMINI_THOUGHT_SIGNATURE_FIELD,
|
||||||
|
type GeminiContent,
|
||||||
|
type GeminiGenerateContentRequest,
|
||||||
|
type GeminiPart,
|
||||||
|
type GeminiStreamChunk,
|
||||||
|
type GeminiTool,
|
||||||
|
type GeminiFunctionCallingConfig,
|
||||||
|
type GeminiFunctionDeclaration,
|
||||||
|
type GeminiFunctionCall,
|
||||||
|
type GeminiFunctionResponse,
|
||||||
|
type GeminiInlineData,
|
||||||
|
type GeminiUsageMetadata,
|
||||||
|
type GeminiCandidate,
|
||||||
|
} from './providers/gemini/types.js'
|
||||||
|
|
||||||
|
// Error utilities
|
||||||
|
export {
|
||||||
|
formatAPIError,
|
||||||
|
extractConnectionErrorDetails,
|
||||||
|
sanitizeAPIError,
|
||||||
|
getSSLErrorHint,
|
||||||
|
type ConnectionErrorDetails,
|
||||||
|
} from './errorUtils.js'
|
||||||
|
|
||||||
|
// Shared OpenAI conversion utilities
|
||||||
|
export { anthropicMessagesToOpenAI } from './shared/openaiConvertMessages.js'
|
||||||
|
export type { ConvertMessagesOptions } from './shared/openaiConvertMessages.js'
|
||||||
|
export { anthropicToolsToOpenAI, anthropicToolChoiceToOpenAI } from './shared/openaiConvertTools.js'
|
||||||
|
export { adaptOpenAIStreamToAnthropic } from './shared/openaiStreamAdapter.js'
|
||||||
@@ -2,7 +2,7 @@ import { describe, expect, test } from 'bun:test'
|
|||||||
import type {
|
import type {
|
||||||
AssistantMessage,
|
AssistantMessage,
|
||||||
UserMessage,
|
UserMessage,
|
||||||
} from '../../../../types/message.js'
|
} from '../../../types/message.js'
|
||||||
import { anthropicMessagesToGemini } from '../convertMessages.js'
|
import { anthropicMessagesToGemini } from '../convertMessages.js'
|
||||||
|
|
||||||
function makeUserMsg(content: string | any[]): UserMessage {
|
function makeUserMsg(content: string | any[]): UserMessage {
|
||||||
@@ -23,9 +23,10 @@ function makeAssistantMsg(content: string | any[]): AssistantMessage {
|
|||||||
|
|
||||||
describe('anthropicMessagesToGemini', () => {
|
describe('anthropicMessagesToGemini', () => {
|
||||||
test('converts system prompt to systemInstruction', () => {
|
test('converts system prompt to systemInstruction', () => {
|
||||||
const result = anthropicMessagesToGemini([makeUserMsg('hello')], [
|
const result = anthropicMessagesToGemini(
|
||||||
'You are helpful.',
|
[makeUserMsg('hello')],
|
||||||
] as any)
|
['You are helpful.'] as any,
|
||||||
|
)
|
||||||
|
|
||||||
expect(result.systemInstruction).toEqual({
|
expect(result.systemInstruction).toEqual({
|
||||||
parts: [{ text: 'You are helpful.' }],
|
parts: [{ text: 'You are helpful.' }],
|
||||||
@@ -201,8 +202,7 @@ describe('anthropicMessagesToGemini', () => {
|
|||||||
|
|
||||||
test('converts base64 image to inlineData', () => {
|
test('converts base64 image to inlineData', () => {
|
||||||
const result = anthropicMessagesToGemini(
|
const result = anthropicMessagesToGemini(
|
||||||
[
|
[makeUserMsg([
|
||||||
makeUserMsg([
|
|
||||||
{ type: 'text', text: 'describe this' },
|
{ type: 'text', text: 'describe this' },
|
||||||
{
|
{
|
||||||
type: 'image',
|
type: 'image',
|
||||||
@@ -212,8 +212,7 @@ describe('anthropicMessagesToGemini', () => {
|
|||||||
data: 'iVBORw0KGgo=',
|
data: 'iVBORw0KGgo=',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]),
|
])],
|
||||||
],
|
|
||||||
[] as any,
|
[] as any,
|
||||||
)
|
)
|
||||||
expect(result.contents).toEqual([
|
expect(result.contents).toEqual([
|
||||||
@@ -229,8 +228,7 @@ describe('anthropicMessagesToGemini', () => {
|
|||||||
|
|
||||||
test('converts url image to text fallback', () => {
|
test('converts url image to text fallback', () => {
|
||||||
const result = anthropicMessagesToGemini(
|
const result = anthropicMessagesToGemini(
|
||||||
[
|
[makeUserMsg([
|
||||||
makeUserMsg([
|
|
||||||
{
|
{
|
||||||
type: 'image',
|
type: 'image',
|
||||||
source: {
|
source: {
|
||||||
@@ -238,8 +236,7 @@ describe('anthropicMessagesToGemini', () => {
|
|||||||
url: 'https://example.com/img.png',
|
url: 'https://example.com/img.png',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]),
|
])],
|
||||||
],
|
|
||||||
[] as any,
|
[] as any,
|
||||||
)
|
)
|
||||||
expect(result.contents).toEqual([
|
expect(result.contents).toEqual([
|
||||||
@@ -252,8 +249,7 @@ describe('anthropicMessagesToGemini', () => {
|
|||||||
|
|
||||||
test('defaults to image/png when media_type is missing', () => {
|
test('defaults to image/png when media_type is missing', () => {
|
||||||
const result = anthropicMessagesToGemini(
|
const result = anthropicMessagesToGemini(
|
||||||
[
|
[makeUserMsg([
|
||||||
makeUserMsg([
|
|
||||||
{
|
{
|
||||||
type: 'image',
|
type: 'image',
|
||||||
source: {
|
source: {
|
||||||
@@ -261,8 +257,7 @@ describe('anthropicMessagesToGemini', () => {
|
|||||||
data: 'ABC123',
|
data: 'ABC123',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]),
|
])],
|
||||||
],
|
|
||||||
[] as any,
|
[] as any,
|
||||||
)
|
)
|
||||||
expect(result.contents[0].parts[0]).toEqual({
|
expect(result.contents[0].parts[0]).toEqual({
|
||||||
@@ -120,11 +120,11 @@ describe('anthropicToolChoiceToGemini', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('maps explicit tool choice', () => {
|
test('maps explicit tool choice', () => {
|
||||||
expect(anthropicToolChoiceToGemini({ type: 'tool', name: 'bash' })).toEqual(
|
expect(
|
||||||
{
|
anthropicToolChoiceToGemini({ type: 'tool', name: 'bash' }),
|
||||||
|
).toEqual({
|
||||||
mode: 'ANY',
|
mode: 'ANY',
|
||||||
allowedFunctionNames: ['bash'],
|
allowedFunctionNames: ['bash'],
|
||||||
},
|
})
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -57,8 +57,7 @@ describe('adaptGeminiStreamToAnthropic', () => {
|
|||||||
|
|
||||||
const textDeltas = events.filter(
|
const textDeltas = events.filter(
|
||||||
event =>
|
event =>
|
||||||
event.type === 'content_block_delta' &&
|
event.type === 'content_block_delta' && event.delta.type === 'text_delta',
|
||||||
event.delta.type === 'text_delta',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(events[0].type).toBe('message_start')
|
expect(events[0].type).toBe('message_start')
|
||||||
@@ -93,9 +92,7 @@ describe('adaptGeminiStreamToAnthropic', () => {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
const blockStart = events.find(
|
const blockStart = events.find(event => event.type === 'content_block_start')
|
||||||
event => event.type === 'content_block_start',
|
|
||||||
)
|
|
||||||
expect(blockStart.content_block.type).toBe('thinking')
|
expect(blockStart.content_block.type).toBe('thinking')
|
||||||
|
|
||||||
const signatureDelta = events.find(
|
const signatureDelta = events.find(
|
||||||
@@ -128,9 +125,7 @@ describe('adaptGeminiStreamToAnthropic', () => {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
const blockStart = events.find(
|
const blockStart = events.find(event => event.type === 'content_block_start')
|
||||||
event => event.type === 'content_block_start',
|
|
||||||
)
|
|
||||||
expect(blockStart.content_block.type).toBe('tool_use')
|
expect(blockStart.content_block.type).toBe('tool_use')
|
||||||
expect(blockStart.content_block.name).toBe('bash')
|
expect(blockStart.content_block.name).toBe('bash')
|
||||||
|
|
||||||
@@ -2,9 +2,8 @@ import type {
|
|||||||
BetaToolResultBlockParam,
|
BetaToolResultBlockParam,
|
||||||
BetaToolUseBlock,
|
BetaToolUseBlock,
|
||||||
} from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
} from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
||||||
import type { AssistantMessage, UserMessage } from '../../../types/message.js'
|
import type { AssistantMessage, UserMessage } from '../../types/message.js'
|
||||||
import { safeParseJSON } from '../../../utils/json.js'
|
import type { SystemPrompt } from '../../types/systemPrompt.js'
|
||||||
import type { SystemPrompt } from '../../../utils/systemPromptType.js'
|
|
||||||
import {
|
import {
|
||||||
GEMINI_THOUGHT_SIGNATURE_FIELD,
|
GEMINI_THOUGHT_SIGNATURE_FIELD,
|
||||||
type GeminiContent,
|
type GeminiContent,
|
||||||
@@ -12,6 +11,16 @@ import {
|
|||||||
type GeminiPart,
|
type GeminiPart,
|
||||||
} from './types.js'
|
} from './types.js'
|
||||||
|
|
||||||
|
// Simple JSON parse utility (replaces safeParseJSON from main project)
|
||||||
|
function safeParseJSON(json: string | null | undefined): unknown {
|
||||||
|
if (!json) return null
|
||||||
|
try {
|
||||||
|
return JSON.parse(json)
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function anthropicMessagesToGemini(
|
export function anthropicMessagesToGemini(
|
||||||
messages: (UserMessage | AssistantMessage)[],
|
messages: (UserMessage | AssistantMessage)[],
|
||||||
systemPrompt: SystemPrompt,
|
systemPrompt: SystemPrompt,
|
||||||
@@ -84,10 +93,7 @@ function convertInternalUserMessage(
|
|||||||
return {
|
return {
|
||||||
role: 'user',
|
role: 'user',
|
||||||
parts: content.flatMap(block =>
|
parts: content.flatMap(block =>
|
||||||
convertUserContentBlockToGeminiParts(
|
convertUserContentBlockToGeminiParts(block as unknown as string | Record<string, unknown>, toolNamesById),
|
||||||
block as unknown as string | Record<string, unknown>,
|
|
||||||
toolNamesById,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,15 +115,14 @@ function convertUserContentBlockToGeminiParts(
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
functionResponse: {
|
functionResponse: {
|
||||||
name:
|
name: toolNamesById.get(toolResult.tool_use_id) ?? toolResult.tool_use_id,
|
||||||
toolNamesById.get(toolResult.tool_use_id) ?? toolResult.tool_use_id,
|
|
||||||
response: toolResultToResponseObject(toolResult),
|
response: toolResultToResponseObject(toolResult),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将 Anthropic image 块转换为 Gemini inlineData
|
// Convert Anthropic image blocks to Gemini inlineData
|
||||||
if (block.type === 'image') {
|
if (block.type === 'image') {
|
||||||
const source = block.source as Record<string, unknown> | undefined
|
const source = block.source as Record<string, unknown> | undefined
|
||||||
if (source?.type === 'base64' && typeof source.data === 'string') {
|
if (source?.type === 'base64' && typeof source.data === 'string') {
|
||||||
@@ -131,7 +136,7 @@ function convertUserContentBlockToGeminiParts(
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
// url 类型的图片,Gemini 不直接支持,转为文本描述
|
// URL images not directly supported by Gemini, convert to text description
|
||||||
if (source?.type === 'url' && typeof source.url === 'string') {
|
if (source?.type === 'url' && typeof source.url === 'string') {
|
||||||
return createTextGeminiParts(`[image: ${source.url}]`)
|
return createTextGeminiParts(`[image: ${source.url}]`)
|
||||||
}
|
}
|
||||||
@@ -165,9 +170,7 @@ function convertInternalAssistantMessage(msg: AssistantMessage): GeminiContent {
|
|||||||
parts.push(
|
parts.push(
|
||||||
...createTextGeminiParts(
|
...createTextGeminiParts(
|
||||||
block.text,
|
block.text,
|
||||||
getGeminiThoughtSignature(
|
getGeminiThoughtSignature(block as unknown as Record<string, unknown>),
|
||||||
block as unknown as Record<string, unknown>,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
@@ -191,12 +194,8 @@ function convertInternalAssistantMessage(msg: AssistantMessage): GeminiContent {
|
|||||||
name: toolUse.name,
|
name: toolUse.name,
|
||||||
args: normalizeToolUseInput(toolUse.input),
|
args: normalizeToolUseInput(toolUse.input),
|
||||||
},
|
},
|
||||||
...(getGeminiThoughtSignature(
|
...(getGeminiThoughtSignature(block as unknown as Record<string, unknown>) && {
|
||||||
block as unknown as Record<string, unknown>,
|
thoughtSignature: getGeminiThoughtSignature(block as unknown as Record<string, unknown>),
|
||||||
) && {
|
|
||||||
thoughtSignature: getGeminiThoughtSignature(
|
|
||||||
block as unknown as Record<string, unknown>,
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -256,10 +255,12 @@ function toolResultToResponseObject(
|
|||||||
block: BetaToolResultBlockParam,
|
block: BetaToolResultBlockParam,
|
||||||
): Record<string, unknown> {
|
): Record<string, unknown> {
|
||||||
const result = normalizeToolResultContent(block.content)
|
const result = normalizeToolResultContent(block.content)
|
||||||
if (result && typeof result === 'object' && !Array.isArray(result)) {
|
if (
|
||||||
return block.is_error
|
result &&
|
||||||
? { ...(result as Record<string, unknown>), is_error: true }
|
typeof result === 'object' &&
|
||||||
: (result as Record<string, unknown>)
|
!Array.isArray(result)
|
||||||
|
) {
|
||||||
|
return block.is_error ? { ...(result as Record<string, unknown>), is_error: true } : result as Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -298,9 +299,7 @@ function normalizeToolResultContent(content: unknown): unknown {
|
|||||||
return content ?? ''
|
return content ?? ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGeminiThoughtSignature(
|
function getGeminiThoughtSignature(block: Record<string, unknown>): string | undefined {
|
||||||
block: Record<string, unknown>,
|
|
||||||
): string | undefined {
|
|
||||||
const signature = block[GEMINI_THOUGHT_SIGNATURE_FIELD]
|
const signature = block[GEMINI_THOUGHT_SIGNATURE_FIELD]
|
||||||
return typeof signature === 'string' && signature.length > 0
|
return typeof signature === 'string' && signature.length > 0
|
||||||
? signature
|
? signature
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
||||||
import type { GeminiFunctionCallingConfig, GeminiTool } from './types.js'
|
import type {
|
||||||
|
GeminiFunctionCallingConfig,
|
||||||
|
GeminiTool,
|
||||||
|
} from './types.js'
|
||||||
|
|
||||||
const GEMINI_JSON_SCHEMA_TYPES = new Set([
|
const GEMINI_JSON_SCHEMA_TYPES = new Set([
|
||||||
'string',
|
'string',
|
||||||
@@ -31,9 +34,7 @@ function normalizeGeminiJsonSchemaType(
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function inferGeminiJsonSchemaTypeFromValue(
|
function inferGeminiJsonSchemaTypeFromValue(value: unknown): string | undefined {
|
||||||
value: unknown,
|
|
||||||
): string | undefined {
|
|
||||||
if (value === null) return 'null'
|
if (value === null) return 'null'
|
||||||
if (Array.isArray(value)) return 'array'
|
if (Array.isArray(value)) return 'array'
|
||||||
if (typeof value === 'string') return 'string'
|
if (typeof value === 'string') return 'string'
|
||||||
@@ -96,7 +97,9 @@ function sanitizeGeminiJsonSchemaArray(
|
|||||||
return sanitized.length > 0 ? sanitized : undefined
|
return sanitized.length > 0 ? sanitized : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeGeminiJsonSchema(schema: unknown): Record<string, unknown> {
|
function sanitizeGeminiJsonSchema(
|
||||||
|
schema: unknown,
|
||||||
|
): Record<string, unknown> {
|
||||||
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
|
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
@@ -233,17 +236,14 @@ export function anthropicToolsToGemini(tools: BetaToolUnion[]): GeminiTool[] {
|
|||||||
const functionDeclarations = tools
|
const functionDeclarations = tools
|
||||||
.filter(tool => {
|
.filter(tool => {
|
||||||
const toolType = (tool as unknown as { type?: string }).type
|
const toolType = (tool as unknown as { type?: string }).type
|
||||||
return (
|
return tool.type === 'custom' || !('type' in tool) || toolType !== 'server'
|
||||||
tool.type === 'custom' || !('type' in tool) || toolType !== 'server'
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.map(tool => {
|
.map(tool => {
|
||||||
const anyTool = tool as unknown as Record<string, unknown>
|
const anyTool = tool as unknown as Record<string, unknown>
|
||||||
const name = (anyTool.name as string) || ''
|
const name = (anyTool.name as string) || ''
|
||||||
const description = (anyTool.description as string) || ''
|
const description = (anyTool.description as string) || ''
|
||||||
const inputSchema = (anyTool.input_schema as
|
const inputSchema =
|
||||||
| Record<string, unknown>
|
(anyTool.input_schema as Record<string, unknown> | undefined) ?? {
|
||||||
| undefined) ?? {
|
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {},
|
properties: {},
|
||||||
}
|
}
|
||||||
@@ -255,7 +255,9 @@ export function anthropicToolsToGemini(tools: BetaToolUnion[]): GeminiTool[] {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return functionDeclarations.length > 0 ? [{ functionDeclarations }] : []
|
return functionDeclarations.length > 0
|
||||||
|
? [{ functionDeclarations }]
|
||||||
|
: []
|
||||||
}
|
}
|
||||||
|
|
||||||
export function anthropicToolChoiceToGemini(
|
export function anthropicToolChoiceToGemini(
|
||||||
@@ -17,14 +17,12 @@ export function resolveGeminiModel(anthropicModel: string): string {
|
|||||||
return cleanModel
|
return cleanModel
|
||||||
}
|
}
|
||||||
|
|
||||||
// First, try Gemini-specific DEFAULT variables (separated from Anthropic)
|
|
||||||
const geminiEnvVar = `GEMINI_DEFAULT_${family.toUpperCase()}_MODEL`
|
const geminiEnvVar = `GEMINI_DEFAULT_${family.toUpperCase()}_MODEL`
|
||||||
const geminiModel = process.env[geminiEnvVar]
|
const geminiModel = process.env[geminiEnvVar]
|
||||||
if (geminiModel) {
|
if (geminiModel) {
|
||||||
return geminiModel
|
return geminiModel
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to Anthropic DEFAULT variables for backward compatibility
|
|
||||||
const sharedEnvVar = `ANTHROPIC_DEFAULT_${family.toUpperCase()}_MODEL`
|
const sharedEnvVar = `ANTHROPIC_DEFAULT_${family.toUpperCase()}_MODEL`
|
||||||
const resolvedModel = process.env[sharedEnvVar]
|
const resolvedModel = process.env[sharedEnvVar]
|
||||||
if (resolvedModel) {
|
if (resolvedModel) {
|
||||||
@@ -10,8 +10,9 @@ export async function* adaptGeminiStreamToAnthropic(
|
|||||||
let started = false
|
let started = false
|
||||||
let stopped = false
|
let stopped = false
|
||||||
let nextContentIndex = 0
|
let nextContentIndex = 0
|
||||||
let openTextLikeBlock: { index: number; type: 'text' | 'thinking' } | null =
|
let openTextLikeBlock:
|
||||||
null
|
| { index: number; type: 'text' | 'thinking' }
|
||||||
|
| null = null
|
||||||
let sawToolUse = false
|
let sawToolUse = false
|
||||||
let finishReason: string | undefined
|
let finishReason: string | undefined
|
||||||
let inputTokens = 0
|
let inputTokens = 0
|
||||||
@@ -84,10 +85,7 @@ export async function* adaptGeminiStreamToAnthropic(
|
|||||||
} as BetaRawMessageStreamEvent
|
} as BetaRawMessageStreamEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (part.functionCall.args && Object.keys(part.functionCall.args).length > 0) {
|
||||||
part.functionCall.args &&
|
|
||||||
Object.keys(part.functionCall.args).length > 0
|
|
||||||
) {
|
|
||||||
yield {
|
yield {
|
||||||
type: 'content_block_delta',
|
type: 'content_block_delta',
|
||||||
index: toolIndex,
|
index: toolIndex,
|
||||||
@@ -215,7 +213,9 @@ export async function* adaptGeminiStreamToAnthropic(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTextLikeBlockType(part: GeminiPart): 'text' | 'thinking' | null {
|
function getTextLikeBlockType(
|
||||||
|
part: GeminiPart,
|
||||||
|
): 'text' | 'thinking' | null {
|
||||||
if (typeof part.text !== 'string') {
|
if (typeof part.text !== 'string') {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -33,14 +33,11 @@ describe('resolveGrokModel', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('maps haiku models to grok-3-mini-fast', () => {
|
test('maps haiku models to grok-3-mini-fast', () => {
|
||||||
expect(resolveGrokModel('claude-haiku-4-5-20251001')).toBe(
|
expect(resolveGrokModel('claude-haiku-4-5-20251001')).toBe('grok-3-mini-fast')
|
||||||
'grok-3-mini-fast',
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('GROK_MODEL_MAP overrides family mapping', () => {
|
test('GROK_MODEL_MAP overrides family mapping', () => {
|
||||||
process.env.GROK_MODEL_MAP =
|
process.env.GROK_MODEL_MAP = '{"opus":"grok-4","sonnet":"grok-3","haiku":"grok-mini"}'
|
||||||
'{"opus":"grok-4","sonnet":"grok-3","haiku":"grok-mini"}'
|
|
||||||
expect(resolveGrokModel('claude-opus-4-6')).toBe('grok-4')
|
expect(resolveGrokModel('claude-opus-4-6')).toBe('grok-4')
|
||||||
expect(resolveGrokModel('claude-sonnet-4-6')).toBe('grok-3')
|
expect(resolveGrokModel('claude-sonnet-4-6')).toBe('grok-3')
|
||||||
expect(resolveGrokModel('claude-haiku-4-5-20251001')).toBe('grok-mini')
|
expect(resolveGrokModel('claude-haiku-4-5-20251001')).toBe('grok-mini')
|
||||||
@@ -65,8 +62,6 @@ describe('resolveGrokModel', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('falls back to family default for unlisted model', () => {
|
test('falls back to family default for unlisted model', () => {
|
||||||
expect(resolveGrokModel('claude-opus-99-20300101')).toBe(
|
expect(resolveGrokModel('claude-opus-99-20300101')).toBe('grok-4.20-reasoning')
|
||||||
'grok-4.20-reasoning',
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -2,8 +2,7 @@
|
|||||||
* Default mapping from Anthropic model names to Grok model names.
|
* Default mapping from Anthropic model names to Grok model names.
|
||||||
*
|
*
|
||||||
* Users can override per-family via GROK_DEFAULT_{FAMILY}_MODEL env vars,
|
* Users can override per-family via GROK_DEFAULT_{FAMILY}_MODEL env vars,
|
||||||
* or override the entire mapping via GROK_MODEL_MAP env var (JSON string):
|
* or override the entire mapping via GROK_MODEL_MAP env var (JSON string).
|
||||||
* GROK_MODEL_MAP='{"opus":"grok-4","sonnet":"grok-3","haiku":"grok-3-mini-fast"}'
|
|
||||||
*/
|
*/
|
||||||
const DEFAULT_MODEL_MAP: Record<string, string> = {
|
const DEFAULT_MODEL_MAP: Record<string, string> = {
|
||||||
'claude-sonnet-4-20250514': 'grok-3-mini-fast',
|
'claude-sonnet-4-20250514': 'grok-3-mini-fast',
|
||||||
@@ -19,9 +18,6 @@ const DEFAULT_MODEL_MAP: Record<string, string> = {
|
|||||||
'claude-3-5-sonnet-20241022': 'grok-3-mini-fast',
|
'claude-3-5-sonnet-20241022': 'grok-3-mini-fast',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Family-level mapping defaults (used by GROK_MODEL_MAP).
|
|
||||||
*/
|
|
||||||
const DEFAULT_FAMILY_MAP: Record<string, string> = {
|
const DEFAULT_FAMILY_MAP: Record<string, string> = {
|
||||||
opus: 'grok-4.20-reasoning',
|
opus: 'grok-4.20-reasoning',
|
||||||
sonnet: 'grok-3-mini-fast',
|
sonnet: 'grok-3-mini-fast',
|
||||||
@@ -35,10 +31,6 @@ function getModelFamily(model: string): 'haiku' | 'sonnet' | 'opus' | null {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse user-provided model map from GROK_MODEL_MAP env var.
|
|
||||||
* Accepts JSON like: {"opus":"grok-4","sonnet":"grok-3","haiku":"grok-3-mini-fast"}
|
|
||||||
*/
|
|
||||||
function getUserModelMap(): Record<string, string> | null {
|
function getUserModelMap(): Record<string, string> | null {
|
||||||
const raw = process.env.GROK_MODEL_MAP
|
const raw = process.env.GROK_MODEL_MAP
|
||||||
if (!raw) return null
|
if (!raw) return null
|
||||||
@@ -55,18 +47,8 @@ function getUserModelMap(): Record<string, string> | null {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the Grok model name for a given Anthropic model.
|
* Resolve the Grok model name for a given Anthropic model.
|
||||||
*
|
|
||||||
* Priority:
|
|
||||||
* 1. GROK_MODEL env var (override all)
|
|
||||||
* 2. GROK_MODEL_MAP env var — JSON family map (e.g. {"opus":"grok-4"})
|
|
||||||
* 3. GROK_DEFAULT_{FAMILY}_MODEL env var (e.g. GROK_DEFAULT_OPUS_MODEL)
|
|
||||||
* 4. ANTHROPIC_DEFAULT_{FAMILY}_MODEL env var (backward compat)
|
|
||||||
* 5. DEFAULT_MODEL_MAP lookup
|
|
||||||
* 6. Family-level default
|
|
||||||
* 7. Pass through original model name
|
|
||||||
*/
|
*/
|
||||||
export function resolveGrokModel(anthropicModel: string): string {
|
export function resolveGrokModel(anthropicModel: string): string {
|
||||||
// 1. Global override
|
|
||||||
if (process.env.GROK_MODEL) {
|
if (process.env.GROK_MODEL) {
|
||||||
return process.env.GROK_MODEL
|
return process.env.GROK_MODEL
|
||||||
}
|
}
|
||||||
@@ -74,34 +56,28 @@ export function resolveGrokModel(anthropicModel: string): string {
|
|||||||
const cleanModel = anthropicModel.replace(/\[1m\]$/, '')
|
const cleanModel = anthropicModel.replace(/\[1m\]$/, '')
|
||||||
const family = getModelFamily(cleanModel)
|
const family = getModelFamily(cleanModel)
|
||||||
|
|
||||||
// 2. User-provided model map
|
|
||||||
const userMap = getUserModelMap()
|
const userMap = getUserModelMap()
|
||||||
if (userMap && family && userMap[family]) {
|
if (userMap && family && userMap[family]) {
|
||||||
return userMap[family]
|
return userMap[family]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (family) {
|
if (family) {
|
||||||
// 3. Grok-specific family override
|
|
||||||
const grokEnvVar = `GROK_DEFAULT_${family.toUpperCase()}_MODEL`
|
const grokEnvVar = `GROK_DEFAULT_${family.toUpperCase()}_MODEL`
|
||||||
const grokOverride = process.env[grokEnvVar]
|
const grokOverride = process.env[grokEnvVar]
|
||||||
if (grokOverride) return grokOverride
|
if (grokOverride) return grokOverride
|
||||||
|
|
||||||
// 4. Anthropic env var (backward compat)
|
|
||||||
const anthropicEnvVar = `ANTHROPIC_DEFAULT_${family.toUpperCase()}_MODEL`
|
const anthropicEnvVar = `ANTHROPIC_DEFAULT_${family.toUpperCase()}_MODEL`
|
||||||
const anthropicOverride = process.env[anthropicEnvVar]
|
const anthropicOverride = process.env[anthropicEnvVar]
|
||||||
if (anthropicOverride) return anthropicOverride
|
if (anthropicOverride) return anthropicOverride
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Exact model name lookup
|
|
||||||
if (DEFAULT_MODEL_MAP[cleanModel]) {
|
if (DEFAULT_MODEL_MAP[cleanModel]) {
|
||||||
return DEFAULT_MODEL_MAP[cleanModel]
|
return DEFAULT_MODEL_MAP[cleanModel]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Family-level default
|
|
||||||
if (family && DEFAULT_FAMILY_MAP[family]) {
|
if (family && DEFAULT_FAMILY_MAP[family]) {
|
||||||
return DEFAULT_FAMILY_MAP[family]
|
return DEFAULT_FAMILY_MAP[family]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Pass through
|
|
||||||
return cleanModel
|
return cleanModel
|
||||||
}
|
}
|
||||||
@@ -16,9 +16,6 @@ const DEFAULT_MODEL_MAP: Record<string, string> = {
|
|||||||
'claude-3-5-sonnet-20241022': 'gpt-4o',
|
'claude-3-5-sonnet-20241022': 'gpt-4o',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine the model family (haiku / sonnet / opus) from an Anthropic model ID.
|
|
||||||
*/
|
|
||||||
function getModelFamily(model: string): 'haiku' | 'sonnet' | 'opus' | null {
|
function getModelFamily(model: string): 'haiku' | 'sonnet' | 'opus' | null {
|
||||||
if (/haiku/i.test(model)) return 'haiku'
|
if (/haiku/i.test(model)) return 'haiku'
|
||||||
if (/opus/i.test(model)) return 'opus'
|
if (/opus/i.test(model)) return 'opus'
|
||||||
@@ -37,23 +34,18 @@ function getModelFamily(model: string): 'haiku' | 'sonnet' | 'opus' | null {
|
|||||||
* 5. Pass through original model name
|
* 5. Pass through original model name
|
||||||
*/
|
*/
|
||||||
export function resolveOpenAIModel(anthropicModel: string): string {
|
export function resolveOpenAIModel(anthropicModel: string): string {
|
||||||
// Highest priority: explicit override
|
|
||||||
if (process.env.OPENAI_MODEL) {
|
if (process.env.OPENAI_MODEL) {
|
||||||
return process.env.OPENAI_MODEL
|
return process.env.OPENAI_MODEL
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strip [1m] suffix if present (Claude-specific modifier)
|
|
||||||
const cleanModel = anthropicModel.replace(/\[1m\]$/, '')
|
const cleanModel = anthropicModel.replace(/\[1m\]$/, '')
|
||||||
|
|
||||||
// Check family-specific overrides
|
|
||||||
const family = getModelFamily(cleanModel)
|
const family = getModelFamily(cleanModel)
|
||||||
if (family) {
|
if (family) {
|
||||||
// OpenAI-specific family override (preferred for openai provider)
|
|
||||||
const openaiEnvVar = `OPENAI_DEFAULT_${family.toUpperCase()}_MODEL`
|
const openaiEnvVar = `OPENAI_DEFAULT_${family.toUpperCase()}_MODEL`
|
||||||
const openaiOverride = process.env[openaiEnvVar]
|
const openaiOverride = process.env[openaiEnvVar]
|
||||||
if (openaiOverride) return openaiOverride
|
if (openaiOverride) return openaiOverride
|
||||||
|
|
||||||
// Anthropic env var (backward compatibility)
|
|
||||||
const anthropicEnvVar = `ANTHROPIC_DEFAULT_${family.toUpperCase()}_MODEL`
|
const anthropicEnvVar = `ANTHROPIC_DEFAULT_${family.toUpperCase()}_MODEL`
|
||||||
const anthropicOverride = process.env[anthropicEnvVar]
|
const anthropicOverride = process.env[anthropicEnvVar]
|
||||||
if (anthropicOverride) return anthropicOverride
|
if (anthropicOverride) return anthropicOverride
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
import { describe, expect, test } from 'bun:test'
|
import { describe, expect, test } from 'bun:test'
|
||||||
import { anthropicMessagesToOpenAI } from '../convertMessages.js'
|
import { anthropicMessagesToOpenAI } from '../openaiConvertMessages.js'
|
||||||
import type {
|
import type { UserMessage, AssistantMessage } from '../../types/message.js'
|
||||||
UserMessage,
|
|
||||||
AssistantMessage,
|
|
||||||
} from '../../../../types/message.js'
|
|
||||||
|
|
||||||
// Helpers to create internal-format messages
|
// Helpers to create internal-format messages
|
||||||
function makeUserMsg(content: string | any[]): UserMessage {
|
function makeUserMsg(content: string | any[]): UserMessage {
|
||||||
@@ -24,22 +21,26 @@ function makeAssistantMsg(content: string | any[]): AssistantMessage {
|
|||||||
|
|
||||||
describe('anthropicMessagesToOpenAI', () => {
|
describe('anthropicMessagesToOpenAI', () => {
|
||||||
test('converts system prompt to system message', () => {
|
test('converts system prompt to system message', () => {
|
||||||
const result = anthropicMessagesToOpenAI([makeUserMsg('hello')], [
|
const result = anthropicMessagesToOpenAI(
|
||||||
'You are helpful.',
|
[makeUserMsg('hello')],
|
||||||
] as any)
|
['You are helpful.'] as any,
|
||||||
|
)
|
||||||
expect(result[0]).toEqual({ role: 'system', content: 'You are helpful.' })
|
expect(result[0]).toEqual({ role: 'system', content: 'You are helpful.' })
|
||||||
})
|
})
|
||||||
|
|
||||||
test('joins multiple system prompt strings', () => {
|
test('joins multiple system prompt strings', () => {
|
||||||
const result = anthropicMessagesToOpenAI([makeUserMsg('hi')], [
|
const result = anthropicMessagesToOpenAI(
|
||||||
'Part 1',
|
[makeUserMsg('hi')],
|
||||||
'Part 2',
|
['Part 1', 'Part 2'] as any,
|
||||||
] as any)
|
)
|
||||||
expect(result[0]).toEqual({ role: 'system', content: 'Part 1\n\nPart 2' })
|
expect(result[0]).toEqual({ role: 'system', content: 'Part 1\n\nPart 2' })
|
||||||
})
|
})
|
||||||
|
|
||||||
test('skips empty system prompt', () => {
|
test('skips empty system prompt', () => {
|
||||||
const result = anthropicMessagesToOpenAI([makeUserMsg('hi')], [] as any)
|
const result = anthropicMessagesToOpenAI(
|
||||||
|
[makeUserMsg('hi')],
|
||||||
|
[] as any,
|
||||||
|
)
|
||||||
expect(result[0].role).toBe('user')
|
expect(result[0].role).toBe('user')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -53,12 +54,10 @@ describe('anthropicMessagesToOpenAI', () => {
|
|||||||
|
|
||||||
test('converts user message with content array', () => {
|
test('converts user message with content array', () => {
|
||||||
const result = anthropicMessagesToOpenAI(
|
const result = anthropicMessagesToOpenAI(
|
||||||
[
|
[makeUserMsg([
|
||||||
makeUserMsg([
|
|
||||||
{ type: 'text', text: 'line 1' },
|
{ type: 'text', text: 'line 1' },
|
||||||
{ type: 'text', text: 'line 2' },
|
{ type: 'text', text: 'line 2' },
|
||||||
]),
|
])],
|
||||||
],
|
|
||||||
[] as any,
|
[] as any,
|
||||||
)
|
)
|
||||||
expect(result).toEqual([{ role: 'user', content: 'line 1\nline 2' }])
|
expect(result).toEqual([{ role: 'user', content: 'line 1\nline 2' }])
|
||||||
@@ -74,8 +73,7 @@ describe('anthropicMessagesToOpenAI', () => {
|
|||||||
|
|
||||||
test('converts assistant message with tool_use', () => {
|
test('converts assistant message with tool_use', () => {
|
||||||
const result = anthropicMessagesToOpenAI(
|
const result = anthropicMessagesToOpenAI(
|
||||||
[
|
[makeAssistantMsg([
|
||||||
makeAssistantMsg([
|
|
||||||
{ type: 'text', text: 'Let me help.' },
|
{ type: 'text', text: 'Let me help.' },
|
||||||
{
|
{
|
||||||
type: 'tool_use' as const,
|
type: 'tool_use' as const,
|
||||||
@@ -83,55 +81,44 @@ describe('anthropicMessagesToOpenAI', () => {
|
|||||||
name: 'bash',
|
name: 'bash',
|
||||||
input: { command: 'ls' },
|
input: { command: 'ls' },
|
||||||
},
|
},
|
||||||
]),
|
])],
|
||||||
],
|
|
||||||
[] as any,
|
[] as any,
|
||||||
)
|
)
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([{
|
||||||
{
|
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: 'Let me help.',
|
content: 'Let me help.',
|
||||||
tool_calls: [
|
tool_calls: [{
|
||||||
{
|
|
||||||
id: 'toolu_123',
|
id: 'toolu_123',
|
||||||
type: 'function',
|
type: 'function',
|
||||||
function: { name: 'bash', arguments: '{"command":"ls"}' },
|
function: { name: 'bash', arguments: '{"command":"ls"}' },
|
||||||
},
|
}],
|
||||||
],
|
}])
|
||||||
},
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('converts tool_result to tool message', () => {
|
test('converts tool_result to tool message', () => {
|
||||||
const result = anthropicMessagesToOpenAI(
|
const result = anthropicMessagesToOpenAI(
|
||||||
[
|
[makeUserMsg([
|
||||||
makeUserMsg([
|
|
||||||
{
|
{
|
||||||
type: 'tool_result' as const,
|
type: 'tool_result' as const,
|
||||||
tool_use_id: 'toolu_123',
|
tool_use_id: 'toolu_123',
|
||||||
content: 'file1.txt\nfile2.txt',
|
content: 'file1.txt\nfile2.txt',
|
||||||
},
|
},
|
||||||
]),
|
])],
|
||||||
],
|
|
||||||
[] as any,
|
[] as any,
|
||||||
)
|
)
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([{
|
||||||
{
|
|
||||||
role: 'tool',
|
role: 'tool',
|
||||||
tool_call_id: 'toolu_123',
|
tool_call_id: 'toolu_123',
|
||||||
content: 'file1.txt\nfile2.txt',
|
content: 'file1.txt\nfile2.txt',
|
||||||
},
|
}])
|
||||||
])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('strips thinking blocks', () => {
|
test('strips thinking blocks', () => {
|
||||||
const result = anthropicMessagesToOpenAI(
|
const result = anthropicMessagesToOpenAI(
|
||||||
[
|
[makeAssistantMsg([
|
||||||
makeAssistantMsg([
|
|
||||||
{ type: 'thinking' as const, thinking: 'internal thoughts...' },
|
{ type: 'thinking' as const, thinking: 'internal thoughts...' },
|
||||||
{ type: 'text', text: 'visible response' },
|
{ type: 'text', text: 'visible response' },
|
||||||
]),
|
])],
|
||||||
],
|
|
||||||
[] as any,
|
[] as any,
|
||||||
)
|
)
|
||||||
expect(result).toEqual([{ role: 'assistant', content: 'visible response' }])
|
expect(result).toEqual([{ role: 'assistant', content: 'visible response' }])
|
||||||
@@ -170,8 +157,7 @@ describe('anthropicMessagesToOpenAI', () => {
|
|||||||
|
|
||||||
test('converts base64 image to image_url', () => {
|
test('converts base64 image to image_url', () => {
|
||||||
const result = anthropicMessagesToOpenAI(
|
const result = anthropicMessagesToOpenAI(
|
||||||
[
|
[makeUserMsg([
|
||||||
makeUserMsg([
|
|
||||||
{ type: 'text', text: 'what is this?' },
|
{ type: 'text', text: 'what is this?' },
|
||||||
{
|
{
|
||||||
type: 'image' as const,
|
type: 'image' as const,
|
||||||
@@ -181,12 +167,10 @@ describe('anthropicMessagesToOpenAI', () => {
|
|||||||
data: 'iVBORw0KGgo=',
|
data: 'iVBORw0KGgo=',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]),
|
])],
|
||||||
],
|
|
||||||
[] as any,
|
[] as any,
|
||||||
)
|
)
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([{
|
||||||
{
|
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: [
|
content: [
|
||||||
{ type: 'text', text: 'what is this?' },
|
{ type: 'text', text: 'what is this?' },
|
||||||
@@ -195,14 +179,12 @@ describe('anthropicMessagesToOpenAI', () => {
|
|||||||
image_url: { url: 'data:image/png;base64,iVBORw0KGgo=' },
|
image_url: { url: 'data:image/png;base64,iVBORw0KGgo=' },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
}])
|
||||||
])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('converts url image to image_url', () => {
|
test('converts url image to image_url', () => {
|
||||||
const result = anthropicMessagesToOpenAI(
|
const result = anthropicMessagesToOpenAI(
|
||||||
[
|
[makeUserMsg([
|
||||||
makeUserMsg([
|
|
||||||
{
|
{
|
||||||
type: 'image' as const,
|
type: 'image' as const,
|
||||||
source: {
|
source: {
|
||||||
@@ -210,12 +192,10 @@ describe('anthropicMessagesToOpenAI', () => {
|
|||||||
url: 'https://example.com/img.png',
|
url: 'https://example.com/img.png',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]),
|
])],
|
||||||
],
|
|
||||||
[] as any,
|
[] as any,
|
||||||
)
|
)
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([{
|
||||||
{
|
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
@@ -223,14 +203,12 @@ describe('anthropicMessagesToOpenAI', () => {
|
|||||||
image_url: { url: 'https://example.com/img.png' },
|
image_url: { url: 'https://example.com/img.png' },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
}])
|
||||||
])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('converts image-only message without text', () => {
|
test('converts image-only message without text', () => {
|
||||||
const result = anthropicMessagesToOpenAI(
|
const result = anthropicMessagesToOpenAI(
|
||||||
[
|
[makeUserMsg([
|
||||||
makeUserMsg([
|
|
||||||
{
|
{
|
||||||
type: 'image' as const,
|
type: 'image' as const,
|
||||||
source: {
|
source: {
|
||||||
@@ -239,12 +217,10 @@ describe('anthropicMessagesToOpenAI', () => {
|
|||||||
data: '/9j/4AAQ',
|
data: '/9j/4AAQ',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]),
|
])],
|
||||||
],
|
|
||||||
[] as any,
|
[] as any,
|
||||||
)
|
)
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([{
|
||||||
{
|
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
@@ -252,14 +228,12 @@ describe('anthropicMessagesToOpenAI', () => {
|
|||||||
image_url: { url: 'data:image/jpeg;base64,/9j/4AAQ' },
|
image_url: { url: 'data:image/jpeg;base64,/9j/4AAQ' },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
}])
|
||||||
])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('defaults to image/png when media_type is missing', () => {
|
test('defaults to image/png when media_type is missing', () => {
|
||||||
const result = anthropicMessagesToOpenAI(
|
const result = anthropicMessagesToOpenAI(
|
||||||
[
|
[makeUserMsg([
|
||||||
makeUserMsg([
|
|
||||||
{
|
{
|
||||||
type: 'image' as const,
|
type: 'image' as const,
|
||||||
source: {
|
source: {
|
||||||
@@ -267,8 +241,7 @@ describe('anthropicMessagesToOpenAI', () => {
|
|||||||
data: 'ABC123',
|
data: 'ABC123',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]),
|
])],
|
||||||
],
|
|
||||||
[] as any,
|
[] as any,
|
||||||
)
|
)
|
||||||
expect((result[0].content as any[])[0].image_url.url).toBe(
|
expect((result[0].content as any[])[0].image_url.url).toBe(
|
||||||
@@ -280,16 +253,10 @@ describe('anthropicMessagesToOpenAI', () => {
|
|||||||
describe('DeepSeek thinking mode (enableThinking)', () => {
|
describe('DeepSeek thinking mode (enableThinking)', () => {
|
||||||
test('preserves thinking block as reasoning_content when enabled', () => {
|
test('preserves thinking block as reasoning_content when enabled', () => {
|
||||||
const result = anthropicMessagesToOpenAI(
|
const result = anthropicMessagesToOpenAI(
|
||||||
[
|
[makeUserMsg('question'), makeAssistantMsg([
|
||||||
makeUserMsg('question'),
|
{ type: 'thinking' as const, thinking: 'Let me reason about this...' },
|
||||||
makeAssistantMsg([
|
|
||||||
{
|
|
||||||
type: 'thinking' as const,
|
|
||||||
thinking: 'Let me reason about this...',
|
|
||||||
},
|
|
||||||
{ type: 'text', text: 'The answer is 42.' },
|
{ type: 'text', text: 'The answer is 42.' },
|
||||||
]),
|
])],
|
||||||
],
|
|
||||||
[] as any,
|
[] as any,
|
||||||
{ enableThinking: true },
|
{ enableThinking: true },
|
||||||
)
|
)
|
||||||
@@ -304,12 +271,10 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
|
|||||||
|
|
||||||
test('drops thinking block when enableThinking is false (default)', () => {
|
test('drops thinking block when enableThinking is false (default)', () => {
|
||||||
const result = anthropicMessagesToOpenAI(
|
const result = anthropicMessagesToOpenAI(
|
||||||
[
|
[makeAssistantMsg([
|
||||||
makeAssistantMsg([
|
|
||||||
{ type: 'thinking' as const, thinking: 'internal thoughts...' },
|
{ type: 'thinking' as const, thinking: 'internal thoughts...' },
|
||||||
{ type: 'text', text: 'visible response' },
|
{ type: 'text', text: 'visible response' },
|
||||||
]),
|
])],
|
||||||
],
|
|
||||||
[] as any,
|
[] as any,
|
||||||
)
|
)
|
||||||
const assistant = result[0] as any
|
const assistant = result[0] as any
|
||||||
@@ -322,10 +287,7 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
|
|||||||
[
|
[
|
||||||
makeUserMsg('what is the weather?'),
|
makeUserMsg('what is the weather?'),
|
||||||
makeAssistantMsg([
|
makeAssistantMsg([
|
||||||
{
|
{ type: 'thinking' as const, thinking: 'I need to call the weather tool.' },
|
||||||
type: 'thinking' as const,
|
|
||||||
thinking: 'I need to call the weather tool.',
|
|
||||||
},
|
|
||||||
{ type: 'text', text: '' },
|
{ type: 'text', text: '' },
|
||||||
{
|
{
|
||||||
type: 'tool_use' as const,
|
type: 'tool_use' as const,
|
||||||
@@ -434,34 +396,21 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
|
|||||||
{ enableThinking: true },
|
{ enableThinking: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
// All 3 assistant messages are in the current turn (after last user msg is the last tool_result,
|
|
||||||
// but the "last user message" boundary logic finds the last user-typed message).
|
|
||||||
// Actually, tool_result messages are also UserMessage type, so the last user message
|
|
||||||
// is the one with tool_result for toolu_002. All assistant messages after that should have reasoning.
|
|
||||||
const assistants = result.filter(m => m.role === 'assistant')
|
const assistants = result.filter(m => m.role === 'assistant')
|
||||||
expect(assistants.length).toBe(3)
|
expect(assistants.length).toBe(3)
|
||||||
// All iterations within the same turn preserve reasoning
|
// All iterations within the same turn preserve reasoning
|
||||||
expect((assistants[0] as any).reasoning_content).toBe(
|
expect((assistants[0] as any).reasoning_content).toBe('I need the date first.')
|
||||||
'I need the date first.',
|
expect((assistants[1] as any).reasoning_content).toBe('Now I can get the weather.')
|
||||||
)
|
expect((assistants[2] as any).reasoning_content).toBe('I have the info now.')
|
||||||
expect((assistants[1] as any).reasoning_content).toBe(
|
|
||||||
'Now I can get the weather.',
|
|
||||||
)
|
|
||||||
expect((assistants[2] as any).reasoning_content).toBe(
|
|
||||||
'I have the info now.',
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('handles multiple thinking blocks in single assistant message', () => {
|
test('handles multiple thinking blocks in single assistant message', () => {
|
||||||
const result = anthropicMessagesToOpenAI(
|
const result = anthropicMessagesToOpenAI(
|
||||||
[
|
[makeUserMsg('question'), makeAssistantMsg([
|
||||||
makeUserMsg('question'),
|
|
||||||
makeAssistantMsg([
|
|
||||||
{ type: 'thinking' as const, thinking: 'First thought.' },
|
{ type: 'thinking' as const, thinking: 'First thought.' },
|
||||||
{ type: 'thinking' as const, thinking: 'Second thought.' },
|
{ type: 'thinking' as const, thinking: 'Second thought.' },
|
||||||
{ type: 'text', text: 'Final answer.' },
|
{ type: 'text', text: 'Final answer.' },
|
||||||
]),
|
])],
|
||||||
],
|
|
||||||
[] as any,
|
[] as any,
|
||||||
{ enableThinking: true },
|
{ enableThinking: true },
|
||||||
)
|
)
|
||||||
@@ -471,13 +420,10 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
|
|||||||
|
|
||||||
test('skips empty thinking blocks', () => {
|
test('skips empty thinking blocks', () => {
|
||||||
const result = anthropicMessagesToOpenAI(
|
const result = anthropicMessagesToOpenAI(
|
||||||
[
|
[makeUserMsg('question'), makeAssistantMsg([
|
||||||
makeUserMsg('question'),
|
|
||||||
makeAssistantMsg([
|
|
||||||
{ type: 'thinking' as const, thinking: '' },
|
{ type: 'thinking' as const, thinking: '' },
|
||||||
{ type: 'text', text: 'Answer.' },
|
{ type: 'text', text: 'Answer.' },
|
||||||
]),
|
])],
|
||||||
],
|
|
||||||
[] as any,
|
[] as any,
|
||||||
{ enableThinking: true },
|
{ enableThinking: true },
|
||||||
)
|
)
|
||||||
@@ -485,11 +431,57 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
|
|||||||
expect(assistant.reasoning_content).toBeUndefined()
|
expect(assistant.reasoning_content).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('sets content to null when only thinking and tool_calls present', () => {
|
// ── fix: reorder tool and user messages for OpenAI API compatibility (#168) ──
|
||||||
|
|
||||||
|
test('tool messages come BEFORE user text when mixed in same turn', () => {
|
||||||
|
// OpenAI requires: assistant(tool_calls) → tool → user
|
||||||
|
// Bug: previously user text was emitted before tool messages
|
||||||
const result = anthropicMessagesToOpenAI(
|
const result = anthropicMessagesToOpenAI(
|
||||||
[
|
[
|
||||||
makeUserMsg('question'),
|
makeUserMsg('run ls'),
|
||||||
makeAssistantMsg([
|
makeAssistantMsg([
|
||||||
|
{ type: 'tool_use' as const, id: 'toolu_1', name: 'bash', input: { command: 'ls' } },
|
||||||
|
]),
|
||||||
|
makeUserMsg([
|
||||||
|
{ type: 'tool_result' as const, tool_use_id: 'toolu_1', content: 'file.txt' },
|
||||||
|
{ type: 'text' as const, text: 'looks good' },
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
[] as any,
|
||||||
|
)
|
||||||
|
// Find the tool message and the user text message
|
||||||
|
const toolIdx = result.findIndex(m => m.role === 'tool')
|
||||||
|
const userTextIdx = result.findIndex(
|
||||||
|
m => m.role === 'user' && typeof m.content === 'string' && m.content.includes('looks good'),
|
||||||
|
)
|
||||||
|
expect(toolIdx).toBeGreaterThanOrEqual(0)
|
||||||
|
expect(userTextIdx).toBeGreaterThanOrEqual(0)
|
||||||
|
// Tool MUST come before user text
|
||||||
|
expect(toolIdx).toBeLessThan(userTextIdx)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('tool message immediately follows assistant tool_calls (no user message in between)', () => {
|
||||||
|
const result = anthropicMessagesToOpenAI(
|
||||||
|
[
|
||||||
|
makeUserMsg('do something'),
|
||||||
|
makeAssistantMsg([
|
||||||
|
{ type: 'tool_use' as const, id: 'toolu_2', name: 'bash', input: { command: 'pwd' } },
|
||||||
|
]),
|
||||||
|
makeUserMsg([
|
||||||
|
{ type: 'tool_result' as const, tool_use_id: 'toolu_2', content: '/home/user' },
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
[] as any,
|
||||||
|
)
|
||||||
|
const assistantIdx = result.findIndex(m => m.role === 'assistant' && (m as any).tool_calls)
|
||||||
|
const toolIdx = result.findIndex(m => m.role === 'tool')
|
||||||
|
expect(assistantIdx).toBeGreaterThanOrEqual(0)
|
||||||
|
expect(toolIdx).toBe(assistantIdx + 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('sets content to null when only thinking and tool_calls present', () => {
|
||||||
|
const result = anthropicMessagesToOpenAI(
|
||||||
|
[makeUserMsg('question'), makeAssistantMsg([
|
||||||
{ type: 'thinking' as const, thinking: 'Reasoning only.' },
|
{ type: 'thinking' as const, thinking: 'Reasoning only.' },
|
||||||
{
|
{
|
||||||
type: 'tool_use' as const,
|
type: 'tool_use' as const,
|
||||||
@@ -497,8 +489,7 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
|
|||||||
name: 'bash',
|
name: 'bash',
|
||||||
input: { command: 'ls' },
|
input: { command: 'ls' },
|
||||||
},
|
},
|
||||||
]),
|
])],
|
||||||
],
|
|
||||||
[] as any,
|
[] as any,
|
||||||
{ enableThinking: true },
|
{ enableThinking: true },
|
||||||
)
|
)
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
import { describe, expect, test } from 'bun:test'
|
import { describe, expect, test } from 'bun:test'
|
||||||
import {
|
import { anthropicToolsToOpenAI, anthropicToolChoiceToOpenAI } from '../openaiConvertTools.js'
|
||||||
anthropicToolsToOpenAI,
|
|
||||||
anthropicToolChoiceToOpenAI,
|
|
||||||
} from '../convertTools.js'
|
|
||||||
|
|
||||||
describe('anthropicToolsToOpenAI', () => {
|
describe('anthropicToolsToOpenAI', () => {
|
||||||
test('converts basic tool', () => {
|
test('converts basic tool', () => {
|
||||||
@@ -21,8 +18,7 @@ describe('anthropicToolsToOpenAI', () => {
|
|||||||
|
|
||||||
const result = anthropicToolsToOpenAI(tools as any)
|
const result = anthropicToolsToOpenAI(tools as any)
|
||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([{
|
||||||
{
|
|
||||||
type: 'function',
|
type: 'function',
|
||||||
function: {
|
function: {
|
||||||
name: 'bash',
|
name: 'bash',
|
||||||
@@ -33,17 +29,14 @@ describe('anthropicToolsToOpenAI', () => {
|
|||||||
required: ['command'],
|
required: ['command'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}])
|
||||||
])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('uses empty schema when input_schema missing', () => {
|
test('uses empty schema when input_schema missing', () => {
|
||||||
const tools = [{ type: 'custom', name: 'noop', description: 'no-op' }]
|
const tools = [{ type: 'custom', name: 'noop', description: 'no-op' }]
|
||||||
const result = anthropicToolsToOpenAI(tools as any)
|
const result = anthropicToolsToOpenAI(tools as any)
|
||||||
|
|
||||||
expect(
|
expect((result[0] as { function: { parameters: unknown } }).function.parameters).toEqual({ type: 'object', properties: {} })
|
||||||
(result[0] as { function: { parameters: unknown } }).function.parameters,
|
|
||||||
).toEqual({ type: 'object', properties: {} })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('strips Anthropic-specific fields', () => {
|
test('strips Anthropic-specific fields', () => {
|
||||||
@@ -83,8 +76,7 @@ describe('anthropicToolsToOpenAI', () => {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
const result = anthropicToolsToOpenAI(tools as any)
|
const result = anthropicToolsToOpenAI(tools as any)
|
||||||
const props = (result[0] as { function: { parameters: any } }).function
|
const props = (result[0] as { function: { parameters: any } }).function.parameters as any
|
||||||
.parameters as any
|
|
||||||
expect(props.properties.mode).toEqual({ enum: ['read'] })
|
expect(props.properties.mode).toEqual({ enum: ['read'] })
|
||||||
expect(props.properties.mode.const).toBeUndefined()
|
expect(props.properties.mode.const).toBeUndefined()
|
||||||
expect(props.properties.name).toEqual({ type: 'string' })
|
expect(props.properties.name).toEqual({ type: 'string' })
|
||||||
@@ -118,11 +110,8 @@ describe('anthropicToolsToOpenAI', () => {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
const result = anthropicToolsToOpenAI(tools as any)
|
const result = anthropicToolsToOpenAI(tools as any)
|
||||||
const params = (result[0] as { function: { parameters: any } }).function
|
const params = (result[0] as { function: { parameters: any } }).function.parameters as any
|
||||||
.parameters as any
|
expect(params.properties.outer.properties.inner).toEqual({ enum: ['fixed'] })
|
||||||
expect(params.properties.outer.properties.inner).toEqual({
|
|
||||||
enum: ['fixed'],
|
|
||||||
})
|
|
||||||
expect(params.definitions.MyType.properties.field).toEqual({ enum: [42] })
|
expect(params.definitions.MyType.properties.field).toEqual({ enum: [42] })
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -136,17 +125,18 @@ describe('anthropicToolsToOpenAI', () => {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
val: {
|
val: {
|
||||||
anyOf: [{ const: 'a' }, { const: 'b' }, { type: 'string' }],
|
anyOf: [
|
||||||
|
{ const: 'a' },
|
||||||
|
{ const: 'b' },
|
||||||
|
{ type: 'string' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
const result = anthropicToolsToOpenAI(tools as any)
|
const result = anthropicToolsToOpenAI(tools as any)
|
||||||
const anyOf = (
|
const anyOf = ((result[0] as { function: { parameters: any } }).function.parameters as any).properties.val.anyOf
|
||||||
(result[0] as { function: { parameters: any } }).function
|
|
||||||
.parameters as any
|
|
||||||
).properties.val.anyOf
|
|
||||||
expect(anyOf[0]).toEqual({ enum: ['a'] })
|
expect(anyOf[0]).toEqual({ enum: ['a'] })
|
||||||
expect(anyOf[1]).toEqual({ enum: ['b'] })
|
expect(anyOf[1]).toEqual({ enum: ['b'] })
|
||||||
expect(anyOf[2]).toEqual({ type: 'string' })
|
expect(anyOf[2]).toEqual({ type: 'string' })
|
||||||
@@ -1,29 +1,9 @@
|
|||||||
import { describe, expect, test } from 'bun:test'
|
import { describe, expect, test } from 'bun:test'
|
||||||
import type { ChatCompletionChunk } from 'openai/resources/chat/completions/completions.mjs'
|
import type { ChatCompletionChunk } from 'openai/resources/chat/completions/completions.mjs'
|
||||||
import { join, dirname } from 'path'
|
import { adaptOpenAIStreamToAnthropic } from '../openaiStreamAdapter.js'
|
||||||
import { fileURLToPath } from 'url'
|
|
||||||
import { readFileSync, writeFileSync, mkdirSync } from 'fs'
|
|
||||||
import { tmpdir } from 'os'
|
|
||||||
|
|
||||||
// Guard against mock pollution from queryModelOpenAI.test.ts which replaces
|
|
||||||
// ../streamAdapter.js process-wide via mock.module (bun has no un-mock API).
|
|
||||||
// We copy the source to a unique temp path so the import bypasses bun's
|
|
||||||
// module mock cache completely.
|
|
||||||
const _testDir = dirname(fileURLToPath(import.meta.url))
|
|
||||||
const _realSource = readFileSync(
|
|
||||||
join(_testDir, '..', 'streamAdapter.ts'),
|
|
||||||
'utf-8',
|
|
||||||
)
|
|
||||||
const _tempDir = join(tmpdir(), `stream-adapter-test-${Date.now()}`)
|
|
||||||
mkdirSync(_tempDir, { recursive: true })
|
|
||||||
const _tempFile = join(_tempDir, 'streamAdapter.ts')
|
|
||||||
writeFileSync(_tempFile, _realSource, 'utf-8')
|
|
||||||
const { adaptOpenAIStreamToAnthropic } = await import(_tempFile)
|
|
||||||
|
|
||||||
/** Helper to create a mock async iterable from chunk array */
|
/** Helper to create a mock async iterable from chunk array */
|
||||||
function mockStream(
|
function mockStream(chunks: ChatCompletionChunk[]): AsyncIterable<ChatCompletionChunk> {
|
||||||
chunks: ChatCompletionChunk[],
|
|
||||||
): AsyncIterable<ChatCompletionChunk> {
|
|
||||||
return {
|
return {
|
||||||
[Symbol.asyncIterator]() {
|
[Symbol.asyncIterator]() {
|
||||||
let i = 0
|
let i = 0
|
||||||
@@ -38,9 +18,7 @@ function mockStream(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Create a minimal ChatCompletionChunk */
|
/** Create a minimal ChatCompletionChunk */
|
||||||
function makeChunk(
|
function makeChunk(overrides: Partial<ChatCompletionChunk> & any = {}): ChatCompletionChunk {
|
||||||
overrides: Partial<ChatCompletionChunk> & any = {},
|
|
||||||
): ChatCompletionChunk {
|
|
||||||
return {
|
return {
|
||||||
id: 'chatcmpl-test',
|
id: 'chatcmpl-test',
|
||||||
object: 'chat.completion.chunk',
|
object: 'chat.completion.chunk',
|
||||||
@@ -53,16 +31,8 @@ function makeChunk(
|
|||||||
|
|
||||||
/** Collect all emitted Anthropic events from the stream adapter for assertion */
|
/** Collect all emitted Anthropic events from the stream adapter for assertion */
|
||||||
async function collectEvents(chunks: ChatCompletionChunk[]) {
|
async function collectEvents(chunks: ChatCompletionChunk[]) {
|
||||||
const realModuleUrl = new URL(
|
|
||||||
`../streamAdapter.js?real=${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
||||||
import.meta.url,
|
|
||||||
).href
|
|
||||||
const { adaptOpenAIStreamToAnthropic } = await import(realModuleUrl)
|
|
||||||
const events: any[] = []
|
const events: any[] = []
|
||||||
for await (const event of adaptOpenAIStreamToAnthropic(
|
for await (const event of adaptOpenAIStreamToAnthropic(mockStream(chunks), 'gpt-4o')) {
|
||||||
mockStream(chunks),
|
|
||||||
'gpt-4o',
|
|
||||||
)) {
|
|
||||||
events.push(event)
|
events.push(event)
|
||||||
}
|
}
|
||||||
return events
|
return events
|
||||||
@@ -72,31 +42,25 @@ describe('adaptOpenAIStreamToAnthropic', () => {
|
|||||||
test('emits message_start on first chunk', async () => {
|
test('emits message_start on first chunk', async () => {
|
||||||
const events = await collectEvents([
|
const events = await collectEvents([
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [
|
choices: [{
|
||||||
{
|
|
||||||
index: 0,
|
index: 0,
|
||||||
delta: { role: 'assistant', content: '' },
|
delta: { role: 'assistant', content: '' },
|
||||||
finish_reason: null,
|
finish_reason: null,
|
||||||
},
|
}],
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [
|
choices: [{
|
||||||
{
|
|
||||||
index: 0,
|
index: 0,
|
||||||
delta: { content: 'hello' },
|
delta: { content: 'hello' },
|
||||||
finish_reason: null,
|
finish_reason: null,
|
||||||
},
|
}],
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [
|
choices: [{
|
||||||
{
|
|
||||||
index: 0,
|
index: 0,
|
||||||
delta: {},
|
delta: {},
|
||||||
finish_reason: 'stop',
|
finish_reason: 'stop',
|
||||||
},
|
}],
|
||||||
],
|
|
||||||
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
@@ -109,14 +73,10 @@ describe('adaptOpenAIStreamToAnthropic', () => {
|
|||||||
test('converts text content stream', async () => {
|
test('converts text content stream', async () => {
|
||||||
const events = await collectEvents([
|
const events = await collectEvents([
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [
|
choices: [{ index: 0, delta: { content: 'Hello' }, finish_reason: null }],
|
||||||
{ index: 0, delta: { content: 'Hello' }, finish_reason: null },
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [
|
choices: [{ index: 0, delta: { content: ' world' }, finish_reason: null }],
|
||||||
{ index: 0, delta: { content: ' world' }, finish_reason: null },
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
|
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
|
||||||
@@ -131,9 +91,7 @@ describe('adaptOpenAIStreamToAnthropic', () => {
|
|||||||
expect(types).toContain('message_delta')
|
expect(types).toContain('message_delta')
|
||||||
expect(types).toContain('message_stop')
|
expect(types).toContain('message_stop')
|
||||||
|
|
||||||
const textDeltas = events.filter(
|
const textDeltas = events.filter(e => e.type === 'content_block_delta') as any[]
|
||||||
e => e.type === 'content_block_delta',
|
|
||||||
) as any[]
|
|
||||||
expect(textDeltas[0].delta.text).toBe('Hello')
|
expect(textDeltas[0].delta.text).toBe('Hello')
|
||||||
expect(textDeltas[1].delta.text).toBe(' world')
|
expect(textDeltas[1].delta.text).toBe(' world')
|
||||||
})
|
})
|
||||||
@@ -141,54 +99,42 @@ describe('adaptOpenAIStreamToAnthropic', () => {
|
|||||||
test('converts tool_calls stream', async () => {
|
test('converts tool_calls stream', async () => {
|
||||||
const events = await collectEvents([
|
const events = await collectEvents([
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [
|
choices: [{
|
||||||
{
|
|
||||||
index: 0,
|
index: 0,
|
||||||
delta: {
|
delta: {
|
||||||
tool_calls: [
|
tool_calls: [{
|
||||||
{
|
|
||||||
index: 0,
|
index: 0,
|
||||||
id: 'call_abc',
|
id: 'call_abc',
|
||||||
type: 'function',
|
type: 'function',
|
||||||
function: { name: 'bash', arguments: '' },
|
function: { name: 'bash', arguments: '' },
|
||||||
},
|
}],
|
||||||
],
|
|
||||||
},
|
},
|
||||||
finish_reason: null,
|
finish_reason: null,
|
||||||
},
|
}],
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [
|
choices: [{
|
||||||
{
|
|
||||||
index: 0,
|
index: 0,
|
||||||
delta: {
|
delta: {
|
||||||
tool_calls: [
|
tool_calls: [{
|
||||||
{
|
|
||||||
index: 0,
|
index: 0,
|
||||||
function: { arguments: '{"comm' },
|
function: { arguments: '{"comm' },
|
||||||
},
|
}],
|
||||||
],
|
|
||||||
},
|
},
|
||||||
finish_reason: null,
|
finish_reason: null,
|
||||||
},
|
}],
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [
|
choices: [{
|
||||||
{
|
|
||||||
index: 0,
|
index: 0,
|
||||||
delta: {
|
delta: {
|
||||||
tool_calls: [
|
tool_calls: [{
|
||||||
{
|
|
||||||
index: 0,
|
index: 0,
|
||||||
function: { arguments: 'and":"ls"}' },
|
function: { arguments: 'and":"ls"}' },
|
||||||
},
|
}],
|
||||||
],
|
|
||||||
},
|
},
|
||||||
finish_reason: null,
|
finish_reason: null,
|
||||||
},
|
}],
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [{ index: 0, delta: {}, finish_reason: 'tool_calls' }],
|
choices: [{ index: 0, delta: {}, finish_reason: 'tool_calls' }],
|
||||||
@@ -200,8 +146,7 @@ describe('adaptOpenAIStreamToAnthropic', () => {
|
|||||||
expect(blockStart.content_block.name).toBe('bash')
|
expect(blockStart.content_block.name).toBe('bash')
|
||||||
|
|
||||||
const jsonDeltas = events.filter(
|
const jsonDeltas = events.filter(
|
||||||
e =>
|
e => e.type === 'content_block_delta' && e.delta.type === 'input_json_delta',
|
||||||
e.type === 'content_block_delta' && e.delta.type === 'input_json_delta',
|
|
||||||
) as any[]
|
) as any[]
|
||||||
const fullArgs = jsonDeltas.map(d => d.delta.partial_json).join('')
|
const fullArgs = jsonDeltas.map(d => d.delta.partial_json).join('')
|
||||||
expect(fullArgs).toBe('{"command":"ls"}')
|
expect(fullArgs).toBe('{"command":"ls"}')
|
||||||
@@ -226,21 +171,13 @@ describe('adaptOpenAIStreamToAnthropic', () => {
|
|||||||
// return finish_reason "stop" when they actually made tool calls.
|
// return finish_reason "stop" when they actually made tool calls.
|
||||||
const events = await collectEvents([
|
const events = await collectEvents([
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [
|
choices: [{
|
||||||
{
|
|
||||||
index: 0,
|
index: 0,
|
||||||
delta: {
|
delta: {
|
||||||
tool_calls: [
|
tool_calls: [{ index: 0, id: 'call_1', function: { name: 'bash', arguments: '{"cmd":"ls"}' } }],
|
||||||
{
|
|
||||||
index: 0,
|
|
||||||
id: 'call_1',
|
|
||||||
function: { name: 'bash', arguments: '{"cmd":"ls"}' },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
finish_reason: null,
|
finish_reason: null,
|
||||||
},
|
}],
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
|
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
|
||||||
@@ -254,21 +191,13 @@ describe('adaptOpenAIStreamToAnthropic', () => {
|
|||||||
test('maps finish_reason tool_calls to tool_use', async () => {
|
test('maps finish_reason tool_calls to tool_use', async () => {
|
||||||
const events = await collectEvents([
|
const events = await collectEvents([
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [
|
choices: [{
|
||||||
{
|
|
||||||
index: 0,
|
index: 0,
|
||||||
delta: {
|
delta: {
|
||||||
tool_calls: [
|
tool_calls: [{ index: 0, id: 'call_1', function: { name: 'bash', arguments: '{}' } }],
|
||||||
{
|
|
||||||
index: 0,
|
|
||||||
id: 'call_1',
|
|
||||||
function: { name: 'bash', arguments: '{}' },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
finish_reason: null,
|
finish_reason: null,
|
||||||
},
|
}],
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [{ index: 0, delta: {}, finish_reason: 'tool_calls' }],
|
choices: [{ index: 0, delta: {}, finish_reason: 'tool_calls' }],
|
||||||
@@ -282,9 +211,7 @@ describe('adaptOpenAIStreamToAnthropic', () => {
|
|||||||
test('maps finish_reason length to max_tokens', async () => {
|
test('maps finish_reason length to max_tokens', async () => {
|
||||||
const events = await collectEvents([
|
const events = await collectEvents([
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [
|
choices: [{ index: 0, delta: { content: 'truncated' }, finish_reason: null }],
|
||||||
{ index: 0, delta: { content: 'truncated' }, finish_reason: null },
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [{ index: 0, delta: {}, finish_reason: 'length' }],
|
choices: [{ index: 0, delta: {}, finish_reason: 'length' }],
|
||||||
@@ -298,35 +225,23 @@ describe('adaptOpenAIStreamToAnthropic', () => {
|
|||||||
test('handles mixed text and tool_calls', async () => {
|
test('handles mixed text and tool_calls', async () => {
|
||||||
const events = await collectEvents([
|
const events = await collectEvents([
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [
|
choices: [{ index: 0, delta: { content: 'Thinking...' }, finish_reason: null }],
|
||||||
{ index: 0, delta: { content: 'Thinking...' }, finish_reason: null },
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [
|
choices: [{
|
||||||
{
|
|
||||||
index: 0,
|
index: 0,
|
||||||
delta: {
|
delta: {
|
||||||
tool_calls: [
|
tool_calls: [{ index: 0, id: 'call_1', function: { name: 'grep', arguments: '{"p":"test"}' } }],
|
||||||
{
|
|
||||||
index: 0,
|
|
||||||
id: 'call_1',
|
|
||||||
function: { name: 'grep', arguments: '{"p":"test"}' },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
finish_reason: null,
|
finish_reason: null,
|
||||||
},
|
}],
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [{ index: 0, delta: {}, finish_reason: 'tool_calls' }],
|
choices: [{ index: 0, delta: {}, finish_reason: 'tool_calls' }],
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
const blockStarts = events.filter(
|
const blockStarts = events.filter(e => e.type === 'content_block_start') as any[]
|
||||||
e => e.type === 'content_block_start',
|
|
||||||
) as any[]
|
|
||||||
expect(blockStarts.length).toBe(2)
|
expect(blockStarts.length).toBe(2)
|
||||||
expect(blockStarts[0].content_block.type).toBe('text')
|
expect(blockStarts[0].content_block.type).toBe('text')
|
||||||
expect(blockStarts[1].content_block.type).toBe('tool_use')
|
expect(blockStarts[1].content_block.type).toBe('tool_use')
|
||||||
@@ -337,22 +252,18 @@ describe('thinking support (reasoning_content)', () => {
|
|||||||
test('converts reasoning_content to thinking block', async () => {
|
test('converts reasoning_content to thinking block', async () => {
|
||||||
const events = await collectEvents([
|
const events = await collectEvents([
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [
|
choices: [{
|
||||||
{
|
|
||||||
index: 0,
|
index: 0,
|
||||||
delta: { reasoning_content: 'Let me analyze this...' },
|
delta: { reasoning_content: 'Let me analyze this...' },
|
||||||
finish_reason: null,
|
finish_reason: null,
|
||||||
},
|
}],
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [
|
choices: [{
|
||||||
{
|
|
||||||
index: 0,
|
index: 0,
|
||||||
delta: { reasoning_content: ' step by step.' },
|
delta: { reasoning_content: ' step by step.' },
|
||||||
finish_reason: null,
|
finish_reason: null,
|
||||||
},
|
}],
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
|
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
|
||||||
@@ -366,8 +277,7 @@ describe('thinking support (reasoning_content)', () => {
|
|||||||
|
|
||||||
// Should have thinking_delta events
|
// Should have thinking_delta events
|
||||||
const thinkingDeltas = events.filter(
|
const thinkingDeltas = events.filter(
|
||||||
e =>
|
e => e.type === 'content_block_delta' && e.delta.type === 'thinking_delta',
|
||||||
e.type === 'content_block_delta' && e.delta.type === 'thinking_delta',
|
|
||||||
) as any[]
|
) as any[]
|
||||||
expect(thinkingDeltas.length).toBe(2)
|
expect(thinkingDeltas.length).toBe(2)
|
||||||
expect(thinkingDeltas[0].delta.thinking).toBe('Let me analyze this...')
|
expect(thinkingDeltas[0].delta.thinking).toBe('Let me analyze this...')
|
||||||
@@ -377,22 +287,18 @@ describe('thinking support (reasoning_content)', () => {
|
|||||||
test('converts reasoning then content (DeepSeek-style)', async () => {
|
test('converts reasoning then content (DeepSeek-style)', async () => {
|
||||||
const events = await collectEvents([
|
const events = await collectEvents([
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [
|
choices: [{
|
||||||
{
|
|
||||||
index: 0,
|
index: 0,
|
||||||
delta: { reasoning_content: 'Thinking about the answer...' },
|
delta: { reasoning_content: 'Thinking about the answer...' },
|
||||||
finish_reason: null,
|
finish_reason: null,
|
||||||
},
|
}],
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [
|
choices: [{
|
||||||
{
|
|
||||||
index: 0,
|
index: 0,
|
||||||
delta: { content: 'Here is my answer.' },
|
delta: { content: 'Here is my answer.' },
|
||||||
finish_reason: null,
|
finish_reason: null,
|
||||||
},
|
}],
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
|
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
|
||||||
@@ -400,17 +306,13 @@ describe('thinking support (reasoning_content)', () => {
|
|||||||
])
|
])
|
||||||
|
|
||||||
// Should have two content blocks: thinking + text
|
// Should have two content blocks: thinking + text
|
||||||
const blockStarts = events.filter(
|
const blockStarts = events.filter(e => e.type === 'content_block_start') as any[]
|
||||||
e => e.type === 'content_block_start',
|
|
||||||
) as any[]
|
|
||||||
expect(blockStarts.length).toBe(2)
|
expect(blockStarts.length).toBe(2)
|
||||||
expect(blockStarts[0].content_block.type).toBe('thinking')
|
expect(blockStarts[0].content_block.type).toBe('thinking')
|
||||||
expect(blockStarts[1].content_block.type).toBe('text')
|
expect(blockStarts[1].content_block.type).toBe('text')
|
||||||
|
|
||||||
// Thinking block should be closed before text block starts
|
// Thinking block should be closed before text block starts
|
||||||
const blockStops = events.filter(
|
const blockStops = events.filter(e => e.type === 'content_block_stop') as any[]
|
||||||
e => e.type === 'content_block_stop',
|
|
||||||
) as any[]
|
|
||||||
expect(blockStops[0].index).toBe(0) // thinking block closed at index 0
|
expect(blockStops[0].index).toBe(0) // thinking block closed at index 0
|
||||||
expect(blockStarts[1].index).toBe(1) // text block starts at index 1
|
expect(blockStarts[1].index).toBe(1) // text block starts at index 1
|
||||||
|
|
||||||
@@ -424,39 +326,27 @@ describe('thinking support (reasoning_content)', () => {
|
|||||||
test('handles reasoning then tool_calls', async () => {
|
test('handles reasoning then tool_calls', async () => {
|
||||||
const events = await collectEvents([
|
const events = await collectEvents([
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [
|
choices: [{
|
||||||
{
|
|
||||||
index: 0,
|
index: 0,
|
||||||
delta: { reasoning_content: 'I need to run a command.' },
|
delta: { reasoning_content: 'I need to run a command.' },
|
||||||
finish_reason: null,
|
finish_reason: null,
|
||||||
},
|
}],
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [
|
choices: [{
|
||||||
{
|
|
||||||
index: 0,
|
index: 0,
|
||||||
delta: {
|
delta: {
|
||||||
tool_calls: [
|
tool_calls: [{ index: 0, id: 'call_1', function: { name: 'bash', arguments: '{"c":"ls"}' } }],
|
||||||
{
|
|
||||||
index: 0,
|
|
||||||
id: 'call_1',
|
|
||||||
function: { name: 'bash', arguments: '{"c":"ls"}' },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
finish_reason: null,
|
finish_reason: null,
|
||||||
},
|
}],
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [{ index: 0, delta: {}, finish_reason: 'tool_calls' }],
|
choices: [{ index: 0, delta: {}, finish_reason: 'tool_calls' }],
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
const blockStarts = events.filter(
|
const blockStarts = events.filter(e => e.type === 'content_block_start') as any[]
|
||||||
e => e.type === 'content_block_start',
|
|
||||||
) as any[]
|
|
||||||
expect(blockStarts.length).toBe(2)
|
expect(blockStarts.length).toBe(2)
|
||||||
expect(blockStarts[0].content_block.type).toBe('thinking')
|
expect(blockStarts[0].content_block.type).toBe('thinking')
|
||||||
expect(blockStarts[1].content_block.type).toBe('tool_use')
|
expect(blockStarts[1].content_block.type).toBe('tool_use')
|
||||||
@@ -465,31 +355,25 @@ describe('thinking support (reasoning_content)', () => {
|
|||||||
test('thinking block index is 0, text block index is 1', async () => {
|
test('thinking block index is 0, text block index is 1', async () => {
|
||||||
const events = await collectEvents([
|
const events = await collectEvents([
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [
|
choices: [{
|
||||||
{
|
|
||||||
index: 0,
|
index: 0,
|
||||||
delta: { reasoning_content: 'reason' },
|
delta: { reasoning_content: 'reason' },
|
||||||
finish_reason: null,
|
finish_reason: null,
|
||||||
},
|
}],
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [
|
choices: [{
|
||||||
{
|
|
||||||
index: 0,
|
index: 0,
|
||||||
delta: { content: 'answer' },
|
delta: { content: 'answer' },
|
||||||
finish_reason: null,
|
finish_reason: null,
|
||||||
},
|
}],
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
|
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
const blockStarts = events.filter(
|
const blockStarts = events.filter(e => e.type === 'content_block_start') as any[]
|
||||||
e => e.type === 'content_block_start',
|
|
||||||
) as any[]
|
|
||||||
expect(blockStarts[0].index).toBe(0)
|
expect(blockStarts[0].index).toBe(0)
|
||||||
expect(blockStarts[1].index).toBe(1)
|
expect(blockStarts[1].index).toBe(1)
|
||||||
})
|
})
|
||||||
@@ -499,13 +383,11 @@ describe('prompt caching support', () => {
|
|||||||
test('maps cached_tokens to cache_read_input_tokens', async () => {
|
test('maps cached_tokens to cache_read_input_tokens', async () => {
|
||||||
const events = await collectEvents([
|
const events = await collectEvents([
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [
|
choices: [{
|
||||||
{
|
|
||||||
index: 0,
|
index: 0,
|
||||||
delta: { content: 'hi' },
|
delta: { content: 'hi' },
|
||||||
finish_reason: null,
|
finish_reason: null,
|
||||||
},
|
}],
|
||||||
],
|
|
||||||
usage: {
|
usage: {
|
||||||
prompt_tokens: 1000,
|
prompt_tokens: 1000,
|
||||||
completion_tokens: 0,
|
completion_tokens: 0,
|
||||||
@@ -581,9 +463,7 @@ describe('prompt caching support', () => {
|
|||||||
// emitted before the trailing chunk and always has input_tokens=0.
|
// emitted before the trailing chunk and always has input_tokens=0.
|
||||||
const events = await collectEvents([
|
const events = await collectEvents([
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [
|
choices: [{ index: 0, delta: { content: 'hello' }, finish_reason: null }],
|
||||||
{ index: 0, delta: { content: 'hello' }, finish_reason: null },
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
// finish_reason chunk — usage not yet available
|
// finish_reason chunk — usage not yet available
|
||||||
makeChunk({
|
makeChunk({
|
||||||
@@ -613,20 +493,14 @@ describe('prompt caching support', () => {
|
|||||||
// the autocompact threshold (~33k), so compaction never fires.
|
// the autocompact threshold (~33k), so compaction never fires.
|
||||||
const events = await collectEvents([
|
const events = await collectEvents([
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [
|
choices: [{ index: 0, delta: { content: 'answer' }, finish_reason: null }],
|
||||||
{ index: 0, delta: { content: 'answer' }, finish_reason: null },
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
|
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
|
||||||
}),
|
}),
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [],
|
choices: [],
|
||||||
usage: {
|
usage: { prompt_tokens: 800, completion_tokens: 200, total_tokens: 1000 },
|
||||||
prompt_tokens: 800,
|
|
||||||
completion_tokens: 200,
|
|
||||||
total_tokens: 1000,
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -640,21 +514,13 @@ describe('prompt caching support', () => {
|
|||||||
// when the model made tool calls and usage arrives in a trailing chunk.
|
// when the model made tool calls and usage arrives in a trailing chunk.
|
||||||
const events = await collectEvents([
|
const events = await collectEvents([
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [
|
choices: [{
|
||||||
{
|
|
||||||
index: 0,
|
index: 0,
|
||||||
delta: {
|
delta: {
|
||||||
tool_calls: [
|
tool_calls: [{ index: 0, id: 'call_x', function: { name: 'bash', arguments: '{"cmd":"ls"}' } }],
|
||||||
{
|
|
||||||
index: 0,
|
|
||||||
id: 'call_x',
|
|
||||||
function: { name: 'bash', arguments: '{"cmd":"ls"}' },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
finish_reason: null,
|
finish_reason: null,
|
||||||
},
|
}],
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [{ index: 0, delta: {}, finish_reason: 'tool_calls' }],
|
choices: [{ index: 0, delta: {}, finish_reason: 'tool_calls' }],
|
||||||
@@ -674,14 +540,9 @@ describe('prompt caching support', () => {
|
|||||||
test('message_delta always comes before message_stop', async () => {
|
test('message_delta always comes before message_stop', async () => {
|
||||||
// Verifies event ordering is preserved after deferring to post-loop emission.
|
// Verifies event ordering is preserved after deferring to post-loop emission.
|
||||||
const events = await collectEvents([
|
const events = await collectEvents([
|
||||||
makeChunk({
|
makeChunk({ choices: [{ index: 0, delta: { content: 'x' }, finish_reason: null }] }),
|
||||||
choices: [{ index: 0, delta: { content: 'x' }, finish_reason: null }],
|
|
||||||
}),
|
|
||||||
makeChunk({ choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] }),
|
makeChunk({ choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] }),
|
||||||
makeChunk({
|
makeChunk({ choices: [], usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 } }),
|
||||||
choices: [],
|
|
||||||
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
|
||||||
}),
|
|
||||||
])
|
])
|
||||||
|
|
||||||
const types = events.map(e => e.type)
|
const types = events.map(e => e.type)
|
||||||
@@ -700,9 +561,7 @@ describe('prompt caching support', () => {
|
|||||||
// queryModelOpenAI's spread — even though cachedTokens was captured internally.
|
// queryModelOpenAI's spread — even though cachedTokens was captured internally.
|
||||||
const events = await collectEvents([
|
const events = await collectEvents([
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [
|
choices: [{ index: 0, delta: { content: 'answer' }, finish_reason: null }],
|
||||||
{ index: 0, delta: { content: 'answer' }, finish_reason: null },
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
|
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
|
||||||
@@ -779,9 +638,7 @@ describe('prompt caching support', () => {
|
|||||||
// Some endpoints send usage in the finish_reason chunk instead of a trailing chunk.
|
// Some endpoints send usage in the finish_reason chunk instead of a trailing chunk.
|
||||||
const events = await collectEvents([
|
const events = await collectEvents([
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [
|
choices: [{ index: 0, delta: { content: 'result' }, finish_reason: null }],
|
||||||
{ index: 0, delta: { content: 'result' }, finish_reason: null },
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
makeChunk({
|
makeChunk({
|
||||||
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
|
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
|
||||||
@@ -10,8 +10,8 @@ import type {
|
|||||||
ChatCompletionToolMessageParam,
|
ChatCompletionToolMessageParam,
|
||||||
ChatCompletionUserMessageParam,
|
ChatCompletionUserMessageParam,
|
||||||
} from 'openai/resources/chat/completions/completions.mjs'
|
} from 'openai/resources/chat/completions/completions.mjs'
|
||||||
import type { AssistantMessage, UserMessage } from '../../../types/message.js'
|
import type { AssistantMessage, UserMessage } from '../types/message.js'
|
||||||
import type { SystemPrompt } from '../../../utils/systemPromptType.js'
|
import type { SystemPrompt } from '../types/systemPrompt.js'
|
||||||
|
|
||||||
export interface ConvertMessagesOptions {
|
export interface ConvertMessagesOptions {
|
||||||
/** When true, preserve thinking blocks as reasoning_content on assistant messages
|
/** When true, preserve thinking blocks as reasoning_content on assistant messages
|
||||||
@@ -62,11 +62,9 @@ export function anthropicMessagesToOpenAI(
|
|||||||
// A user message starts a new turn if it contains any non-tool_result content
|
// A user message starts a new turn if it contains any non-tool_result content
|
||||||
// (text, image, or other media). Tool results alone do NOT start a new turn
|
// (text, image, or other media). Tool results alone do NOT start a new turn
|
||||||
// because they are continuations of the previous assistant tool call.
|
// because they are continuations of the previous assistant tool call.
|
||||||
const startsNewUserTurn =
|
const startsNewUserTurn = typeof content === 'string'
|
||||||
typeof content === 'string'
|
|
||||||
? content.length > 0
|
? content.length > 0
|
||||||
: Array.isArray(content) &&
|
: Array.isArray(content) && content.some(
|
||||||
content.some(
|
|
||||||
(b: any) =>
|
(b: any) =>
|
||||||
typeof b === 'string' ||
|
typeof b === 'string' ||
|
||||||
(b &&
|
(b &&
|
||||||
@@ -90,8 +88,7 @@ export function anthropicMessagesToOpenAI(
|
|||||||
case 'assistant':
|
case 'assistant':
|
||||||
// Preserve reasoning_content unless we're before a turn boundary
|
// Preserve reasoning_content unless we're before a turn boundary
|
||||||
// (i.e., from a previous user Q&A round)
|
// (i.e., from a previous user Q&A round)
|
||||||
const preserveReasoning =
|
const preserveReasoning = enableThinking && !isBeforeAnyTurnBoundary(i, turnBoundaries)
|
||||||
enableThinking && !isBeforeAnyTurnBoundary(i, turnBoundaries)
|
|
||||||
result.push(...convertInternalAssistantMessage(msg, preserveReasoning))
|
result.push(...convertInternalAssistantMessage(msg, preserveReasoning))
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
@@ -104,7 +101,9 @@ export function anthropicMessagesToOpenAI(
|
|||||||
|
|
||||||
function systemPromptToText(systemPrompt: SystemPrompt): string {
|
function systemPromptToText(systemPrompt: SystemPrompt): string {
|
||||||
if (!systemPrompt || systemPrompt.length === 0) return ''
|
if (!systemPrompt || systemPrompt.length === 0) return ''
|
||||||
return systemPrompt.filter(Boolean).join('\n\n')
|
return systemPrompt
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -132,8 +131,7 @@ function convertInternalUserMessage(
|
|||||||
} else if (Array.isArray(content)) {
|
} else if (Array.isArray(content)) {
|
||||||
const textParts: string[] = []
|
const textParts: string[] = []
|
||||||
const toolResults: BetaToolResultBlockParam[] = []
|
const toolResults: BetaToolResultBlockParam[] = []
|
||||||
const imageParts: Array<{ type: 'image_url'; image_url: { url: string } }> =
|
const imageParts: Array<{ type: 'image_url'; image_url: { url: string } }> = []
|
||||||
[]
|
|
||||||
|
|
||||||
for (const block of content) {
|
for (const block of content) {
|
||||||
if (typeof block === 'string') {
|
if (typeof block === 'string') {
|
||||||
@@ -143,9 +141,7 @@ function convertInternalUserMessage(
|
|||||||
} else if (block.type === 'tool_result') {
|
} else if (block.type === 'tool_result') {
|
||||||
toolResults.push(block as BetaToolResultBlockParam)
|
toolResults.push(block as BetaToolResultBlockParam)
|
||||||
} else if (block.type === 'image') {
|
} else if (block.type === 'image') {
|
||||||
const imagePart = convertImageBlockToOpenAI(
|
const imagePart = convertImageBlockToOpenAI(block as unknown as Record<string, unknown>)
|
||||||
block as unknown as Record<string, unknown>,
|
|
||||||
)
|
|
||||||
if (imagePart) {
|
if (imagePart) {
|
||||||
imageParts.push(imagePart)
|
imageParts.push(imagePart)
|
||||||
}
|
}
|
||||||
@@ -156,17 +152,13 @@ function convertInternalUserMessage(
|
|||||||
// OpenAI API requires that a tool message immediately follows the assistant
|
// OpenAI API requires that a tool message immediately follows the assistant
|
||||||
// message with tool_calls. If we emit a user message first, the API will
|
// message with tool_calls. If we emit a user message first, the API will
|
||||||
// reject the request with "insufficient tool messages following tool_calls".
|
// reject the request with "insufficient tool messages following tool_calls".
|
||||||
// See: https://github.com/anthropics/claude-code/issues/xxx
|
|
||||||
for (const tr of toolResults) {
|
for (const tr of toolResults) {
|
||||||
result.push(convertToolResult(tr))
|
result.push(convertToolResult(tr))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果有图片,构建多模态 content 数组
|
// 如果有图片,构建多模态 content 数组
|
||||||
if (imageParts.length > 0) {
|
if (imageParts.length > 0) {
|
||||||
const multiContent: Array<
|
const multiContent: Array<{ type: 'text'; text: string } | { type: 'image_url'; image_url: { url: string } }> = []
|
||||||
| { type: 'text'; text: string }
|
|
||||||
| { type: 'image_url'; image_url: { url: string } }
|
|
||||||
> = []
|
|
||||||
if (textParts.length > 0) {
|
if (textParts.length > 0) {
|
||||||
multiContent.push({ type: 'text', text: textParts.join('\n') })
|
multiContent.push({ type: 'text', text: textParts.join('\n') })
|
||||||
}
|
}
|
||||||
@@ -237,9 +229,7 @@ function convertInternalAssistantMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const textParts: string[] = []
|
const textParts: string[] = []
|
||||||
const toolCalls: NonNullable<
|
const toolCalls: NonNullable<ChatCompletionAssistantMessageParam['tool_calls']> = []
|
||||||
ChatCompletionAssistantMessageParam['tool_calls']
|
|
||||||
> = []
|
|
||||||
const reasoningParts: string[] = []
|
const reasoningParts: string[] = []
|
||||||
|
|
||||||
for (const block of content) {
|
for (const block of content) {
|
||||||
@@ -260,8 +250,7 @@ function convertInternalAssistantMessage(
|
|||||||
})
|
})
|
||||||
} else if (block.type === 'thinking' && preserveReasoning) {
|
} else if (block.type === 'thinking' && preserveReasoning) {
|
||||||
// DeepSeek thinking mode: preserve reasoning_content for tool call iterations
|
// DeepSeek thinking mode: preserve reasoning_content for tool call iterations
|
||||||
const thinkingText = (block as unknown as Record<string, unknown>)
|
const thinkingText = (block as unknown as Record<string, unknown>).thinking
|
||||||
.thinking
|
|
||||||
if (typeof thinkingText === 'string' && thinkingText) {
|
if (typeof thinkingText === 'string' && thinkingText) {
|
||||||
reasoningParts.push(thinkingText)
|
reasoningParts.push(thinkingText)
|
||||||
}
|
}
|
||||||
@@ -273,9 +262,7 @@ function convertInternalAssistantMessage(
|
|||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: textParts.length > 0 ? textParts.join('\n') : null,
|
content: textParts.length > 0 ? textParts.join('\n') : null,
|
||||||
...(toolCalls.length > 0 && { tool_calls: toolCalls }),
|
...(toolCalls.length > 0 && { tool_calls: toolCalls }),
|
||||||
...(reasoningParts.length > 0 && {
|
...(reasoningParts.length > 0 && { reasoning_content: reasoningParts.join('\n') }),
|
||||||
reasoning_content: reasoningParts.join('\n'),
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [result]
|
return [result]
|
||||||
@@ -16,27 +16,21 @@ export function anthropicToolsToOpenAI(
|
|||||||
.filter(tool => {
|
.filter(tool => {
|
||||||
// Only convert standard tools (skip server tools like computer_use, etc.)
|
// Only convert standard tools (skip server tools like computer_use, etc.)
|
||||||
const toolType = (tool as unknown as { type?: string }).type
|
const toolType = (tool as unknown as { type?: string }).type
|
||||||
return (
|
return tool.type === 'custom' || !('type' in tool) || toolType !== 'server'
|
||||||
tool.type === 'custom' || !('type' in tool) || toolType !== 'server'
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.map(tool => {
|
.map(tool => {
|
||||||
// Handle the various tool shapes from Anthropic SDK
|
// Handle the various tool shapes from Anthropic SDK
|
||||||
const anyTool = tool as unknown as Record<string, unknown>
|
const anyTool = tool as unknown as Record<string, unknown>
|
||||||
const name = (anyTool.name as string) || ''
|
const name = (anyTool.name as string) || ''
|
||||||
const description = (anyTool.description as string) || ''
|
const description = (anyTool.description as string) || ''
|
||||||
const inputSchema = anyTool.input_schema as
|
const inputSchema = anyTool.input_schema as Record<string, unknown> | undefined
|
||||||
| Record<string, unknown>
|
|
||||||
| undefined
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'function' as const,
|
type: 'function' as const,
|
||||||
function: {
|
function: {
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
parameters: sanitizeJsonSchema(
|
parameters: sanitizeJsonSchema(inputSchema || { type: 'object', properties: {} }),
|
||||||
inputSchema || { type: 'object', properties: {} },
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
} satisfies ChatCompletionTool
|
} satisfies ChatCompletionTool
|
||||||
})
|
})
|
||||||
@@ -49,9 +43,7 @@ export function anthropicToolsToOpenAI(
|
|||||||
* support the `const` keyword in JSON Schema. Convert it to `enum` with a
|
* support the `const` keyword in JSON Schema. Convert it to `enum` with a
|
||||||
* single-element array, which is semantically equivalent.
|
* single-element array, which is semantically equivalent.
|
||||||
*/
|
*/
|
||||||
function sanitizeJsonSchema(
|
function sanitizeJsonSchema(schema: Record<string, unknown>): Record<string, unknown> {
|
||||||
schema: Record<string, unknown>,
|
|
||||||
): Record<string, unknown> {
|
|
||||||
if (!schema || typeof schema !== 'object') return schema
|
if (!schema || typeof schema !== 'object') return schema
|
||||||
|
|
||||||
const result = { ...schema }
|
const result = { ...schema }
|
||||||
@@ -63,37 +55,20 @@ function sanitizeJsonSchema(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Recursively process nested schemas
|
// Recursively process nested schemas
|
||||||
const objectKeys = [
|
const objectKeys = ['properties', 'definitions', '$defs', 'patternProperties'] as const
|
||||||
'properties',
|
|
||||||
'definitions',
|
|
||||||
'$defs',
|
|
||||||
'patternProperties',
|
|
||||||
] as const
|
|
||||||
for (const key of objectKeys) {
|
for (const key of objectKeys) {
|
||||||
const nested = result[key]
|
const nested = result[key]
|
||||||
if (nested && typeof nested === 'object') {
|
if (nested && typeof nested === 'object') {
|
||||||
const sanitized: Record<string, unknown> = {}
|
const sanitized: Record<string, unknown> = {}
|
||||||
for (const [k, v] of Object.entries(nested as Record<string, unknown>)) {
|
for (const [k, v] of Object.entries(nested as Record<string, unknown>)) {
|
||||||
sanitized[k] =
|
sanitized[k] = v && typeof v === 'object' ? sanitizeJsonSchema(v as Record<string, unknown>) : v
|
||||||
v && typeof v === 'object'
|
|
||||||
? sanitizeJsonSchema(v as Record<string, unknown>)
|
|
||||||
: v
|
|
||||||
}
|
}
|
||||||
result[key] = sanitized
|
result[key] = sanitized
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursively process single-schema keys
|
// Recursively process single-schema keys
|
||||||
const singleKeys = [
|
const singleKeys = ['items', 'additionalProperties', 'not', 'if', 'then', 'else', 'contains', 'propertyNames'] as const
|
||||||
'items',
|
|
||||||
'additionalProperties',
|
|
||||||
'not',
|
|
||||||
'if',
|
|
||||||
'then',
|
|
||||||
'else',
|
|
||||||
'contains',
|
|
||||||
'propertyNames',
|
|
||||||
] as const
|
|
||||||
for (const key of singleKeys) {
|
for (const key of singleKeys) {
|
||||||
const nested = result[key]
|
const nested = result[key]
|
||||||
if (nested && typeof nested === 'object' && !Array.isArray(nested)) {
|
if (nested && typeof nested === 'object' && !Array.isArray(nested)) {
|
||||||
@@ -107,9 +82,7 @@ function sanitizeJsonSchema(
|
|||||||
const nested = result[key]
|
const nested = result[key]
|
||||||
if (Array.isArray(nested)) {
|
if (Array.isArray(nested)) {
|
||||||
result[key] = nested.map(item =>
|
result[key] = nested.map(item =>
|
||||||
item && typeof item === 'object'
|
item && typeof item === 'object' ? sanitizeJsonSchema(item as Record<string, unknown>) : item
|
||||||
? sanitizeJsonSchema(item as Record<string, unknown>)
|
|
||||||
: item,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,10 +42,7 @@ export async function* adaptOpenAIStreamToAnthropic(
|
|||||||
let currentContentIndex = -1
|
let currentContentIndex = -1
|
||||||
|
|
||||||
// Track tool_use blocks: tool_calls index → { contentIndex, id, name, arguments }
|
// Track tool_use blocks: tool_calls index → { contentIndex, id, name, arguments }
|
||||||
const toolBlocks = new Map<
|
const toolBlocks = new Map<number, { contentIndex: number; id: string; name: string; arguments: string }>()
|
||||||
number,
|
|
||||||
{ contentIndex: number; id: string; name: string; arguments: string }
|
|
||||||
>()
|
|
||||||
|
|
||||||
// Track thinking block state
|
// Track thinking block state
|
||||||
let thinkingBlockOpen = false
|
let thinkingBlockOpen = false
|
||||||
@@ -54,10 +51,6 @@ export async function* adaptOpenAIStreamToAnthropic(
|
|||||||
let textBlockOpen = false
|
let textBlockOpen = false
|
||||||
|
|
||||||
// Track usage — all four Anthropic fields, populated from OpenAI usage fields:
|
// Track usage — all four Anthropic fields, populated from OpenAI usage fields:
|
||||||
// prompt_tokens → input_tokens
|
|
||||||
// completion_tokens → output_tokens
|
|
||||||
// prompt_tokens_details.cached_tokens → cache_read_input_tokens
|
|
||||||
// (no standard OpenAI equivalent) → cache_creation_input_tokens (always 0)
|
|
||||||
let inputTokens = 0
|
let inputTokens = 0
|
||||||
let outputTokens = 0
|
let outputTokens = 0
|
||||||
let cachedReadTokens = 0
|
let cachedReadTokens = 0
|
||||||
@@ -65,10 +58,7 @@ export async function* adaptOpenAIStreamToAnthropic(
|
|||||||
// Track all open content block indices (for cleanup)
|
// Track all open content block indices (for cleanup)
|
||||||
const openBlockIndices = new Set<number>()
|
const openBlockIndices = new Set<number>()
|
||||||
|
|
||||||
// Deferred finish state: populated when finish_reason is encountered so that
|
// Deferred finish state
|
||||||
// message_delta / message_stop are emitted AFTER the stream loop ends.
|
|
||||||
// This ensures usage chunks that arrive after the finish_reason chunk are
|
|
||||||
// captured before we emit the final token counts.
|
|
||||||
let pendingFinishReason: string | null = null
|
let pendingFinishReason: string | null = null
|
||||||
let pendingHasToolCalls = false
|
let pendingHasToolCalls = false
|
||||||
|
|
||||||
@@ -77,16 +67,9 @@ export async function* adaptOpenAIStreamToAnthropic(
|
|||||||
const delta = choice?.delta
|
const delta = choice?.delta
|
||||||
|
|
||||||
// Extract usage from any chunk that carries it.
|
// Extract usage from any chunk that carries it.
|
||||||
// Many OpenAI-compatible endpoints (e.g. DeepSeek) send usage in a separate
|
|
||||||
// final chunk that arrives AFTER the finish_reason chunk. Reading it here
|
|
||||||
// (before emitting message_delta) ensures the token counts are available
|
|
||||||
// when we later emit message_delta.
|
|
||||||
if (chunk.usage) {
|
if (chunk.usage) {
|
||||||
inputTokens = chunk.usage.prompt_tokens ?? inputTokens
|
inputTokens = chunk.usage.prompt_tokens ?? inputTokens
|
||||||
outputTokens = chunk.usage.completion_tokens ?? outputTokens
|
outputTokens = chunk.usage.completion_tokens ?? outputTokens
|
||||||
// OpenAI prompt caching: prompt_tokens_details.cached_tokens
|
|
||||||
// → Anthropic cache_read_input_tokens
|
|
||||||
// Note: OpenAI has no equivalent for cache_creation_input_tokens.
|
|
||||||
const details = (chunk.usage as any).prompt_tokens_details
|
const details = (chunk.usage as any).prompt_tokens_details
|
||||||
if (details?.cached_tokens != null) {
|
if (details?.cached_tokens != null) {
|
||||||
cachedReadTokens = details.cached_tokens
|
cachedReadTokens = details.cached_tokens
|
||||||
@@ -121,7 +104,6 @@ export async function* adaptOpenAIStreamToAnthropic(
|
|||||||
if (!delta) continue
|
if (!delta) continue
|
||||||
|
|
||||||
// Handle reasoning_content → Anthropic thinking block
|
// Handle reasoning_content → Anthropic thinking block
|
||||||
// DeepSeek and compatible providers send delta.reasoning_content
|
|
||||||
const reasoningContent = (delta as any).reasoning_content
|
const reasoningContent = (delta as any).reasoning_content
|
||||||
if (reasoningContent != null && reasoningContent !== '') {
|
if (reasoningContent != null && reasoningContent !== '') {
|
||||||
if (!thinkingBlockOpen) {
|
if (!thinkingBlockOpen) {
|
||||||
@@ -153,7 +135,7 @@ export async function* adaptOpenAIStreamToAnthropic(
|
|||||||
// Handle text content
|
// Handle text content
|
||||||
if (delta.content != null && delta.content !== '') {
|
if (delta.content != null && delta.content !== '') {
|
||||||
if (!textBlockOpen) {
|
if (!textBlockOpen) {
|
||||||
// Close thinking block if still open (reasoning done, now generating answer)
|
// Close thinking block if still open
|
||||||
if (thinkingBlockOpen) {
|
if (thinkingBlockOpen) {
|
||||||
yield {
|
yield {
|
||||||
type: 'content_block_stop',
|
type: 'content_block_stop',
|
||||||
@@ -215,8 +197,7 @@ export async function* adaptOpenAIStreamToAnthropic(
|
|||||||
|
|
||||||
// Start new tool_use block
|
// Start new tool_use block
|
||||||
currentContentIndex++
|
currentContentIndex++
|
||||||
const toolId =
|
const toolId = tc.id || `toolu_${randomUUID().replace(/-/g, '').slice(0, 24)}`
|
||||||
tc.id || `toolu_${randomUUID().replace(/-/g, '').slice(0, 24)}`
|
|
||||||
const toolName = tc.function?.name || ''
|
const toolName = tc.function?.name || ''
|
||||||
|
|
||||||
toolBlocks.set(tcIndex, {
|
toolBlocks.set(tcIndex, {
|
||||||
@@ -255,12 +236,8 @@ export async function* adaptOpenAIStreamToAnthropic(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle finish: close all open content blocks and record the finish_reason.
|
// Handle finish
|
||||||
// message_delta + message_stop are emitted AFTER the stream loop so that any
|
|
||||||
// trailing usage chunk (sent after the finish chunk by some endpoints)
|
|
||||||
// is captured first — ensuring token counts are non-zero.
|
|
||||||
if (choice?.finish_reason) {
|
if (choice?.finish_reason) {
|
||||||
// Close thinking block if still open
|
|
||||||
if (thinkingBlockOpen) {
|
if (thinkingBlockOpen) {
|
||||||
yield {
|
yield {
|
||||||
type: 'content_block_stop',
|
type: 'content_block_stop',
|
||||||
@@ -270,7 +247,6 @@ export async function* adaptOpenAIStreamToAnthropic(
|
|||||||
thinkingBlockOpen = false
|
thinkingBlockOpen = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close text block if still open
|
|
||||||
if (textBlockOpen) {
|
if (textBlockOpen) {
|
||||||
yield {
|
yield {
|
||||||
type: 'content_block_stop',
|
type: 'content_block_stop',
|
||||||
@@ -280,7 +256,6 @@ export async function* adaptOpenAIStreamToAnthropic(
|
|||||||
textBlockOpen = false
|
textBlockOpen = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close all tool blocks that haven't been closed yet
|
|
||||||
for (const [, block] of toolBlocks) {
|
for (const [, block] of toolBlocks) {
|
||||||
if (openBlockIndices.has(block.contentIndex)) {
|
if (openBlockIndices.has(block.contentIndex)) {
|
||||||
yield {
|
yield {
|
||||||
@@ -291,14 +266,12 @@ export async function* adaptOpenAIStreamToAnthropic(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Defer message_delta / message_stop until after the loop so that any
|
|
||||||
// trailing usage chunk is processed before we emit the final token counts.
|
|
||||||
pendingFinishReason = choice.finish_reason
|
pendingFinishReason = choice.finish_reason
|
||||||
pendingHasToolCalls = toolBlocks.size > 0
|
pendingHasToolCalls = toolBlocks.size > 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Safety: close any remaining open blocks if stream ended without finish_reason
|
// Safety: close any remaining open blocks
|
||||||
for (const idx of openBlockIndices) {
|
for (const idx of openBlockIndices) {
|
||||||
yield {
|
yield {
|
||||||
type: 'content_block_stop',
|
type: 'content_block_stop',
|
||||||
@@ -306,15 +279,8 @@ export async function* adaptOpenAIStreamToAnthropic(
|
|||||||
} as BetaRawMessageStreamEvent
|
} as BetaRawMessageStreamEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit message_delta + message_stop now that the stream is fully consumed.
|
// Emit message_delta + message_stop
|
||||||
// Usage values (inputTokens / outputTokens) reflect all chunks including any
|
|
||||||
// trailing usage-only chunk sent after the finish_reason chunk.
|
|
||||||
if (pendingFinishReason !== null) {
|
if (pendingFinishReason !== null) {
|
||||||
// Map finish_reason to Anthropic stop_reason.
|
|
||||||
// CRITICAL: When finish_reason is 'length' (token budget exhausted), always
|
|
||||||
// report 'max_tokens' regardless of whether partial tool calls were received.
|
|
||||||
// Otherwise the query loop would try to execute tool calls with incomplete
|
|
||||||
// JSON arguments instead of triggering the max_tokens retry/recovery path.
|
|
||||||
const stopReason =
|
const stopReason =
|
||||||
pendingFinishReason === 'length'
|
pendingFinishReason === 'length'
|
||||||
? 'max_tokens'
|
? 'max_tokens'
|
||||||
@@ -328,19 +294,6 @@ export async function* adaptOpenAIStreamToAnthropic(
|
|||||||
stop_reason: stopReason,
|
stop_reason: stopReason,
|
||||||
stop_sequence: null,
|
stop_sequence: null,
|
||||||
},
|
},
|
||||||
// Carry all four Anthropic usage fields so queryModelOpenAI's message_delta
|
|
||||||
// handler (which spreads this into the accumulated usage object) can override
|
|
||||||
// every field that message_start emitted as 0. For endpoints that send usage
|
|
||||||
// in a trailing chunk (e.g. DeepSeek), message_start is emitted on the first
|
|
||||||
// content chunk before the trailing usage chunk arrives, so all four fields
|
|
||||||
// start at 0. By the time we reach here (post-loop) the trailing chunk has
|
|
||||||
// been processed and all values reflect the real counts.
|
|
||||||
//
|
|
||||||
// OpenAI → Anthropic field mapping:
|
|
||||||
// prompt_tokens → input_tokens
|
|
||||||
// completion_tokens → output_tokens
|
|
||||||
// prompt_tokens_details.cached_tokens → cache_read_input_tokens
|
|
||||||
// (no OpenAI equivalent) → cache_creation_input_tokens (stays 0)
|
|
||||||
usage: {
|
usage: {
|
||||||
input_tokens: inputTokens,
|
input_tokens: inputTokens,
|
||||||
output_tokens: outputTokens,
|
output_tokens: outputTokens,
|
||||||
@@ -357,11 +310,6 @@ export async function* adaptOpenAIStreamToAnthropic(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Map OpenAI finish_reason to Anthropic stop_reason.
|
* Map OpenAI finish_reason to Anthropic stop_reason.
|
||||||
*
|
|
||||||
* stop → end_turn
|
|
||||||
* tool_calls → tool_use
|
|
||||||
* length → max_tokens
|
|
||||||
* content_filter → end_turn
|
|
||||||
*/
|
*/
|
||||||
function mapFinishReason(reason: string): string {
|
function mapFinishReason(reason: string): string {
|
||||||
switch (reason) {
|
switch (reason) {
|
||||||
54
packages/@ant/model-provider/src/types/errors.ts
Normal file
54
packages/@ant/model-provider/src/types/errors.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// Error type constants for the model provider package.
|
||||||
|
// Error string constants extracted from src/services/api/errors.ts.
|
||||||
|
// The full error handling functions remain in the main project (Phase 4).
|
||||||
|
|
||||||
|
export const API_ERROR_MESSAGE_PREFIX = 'API Error'
|
||||||
|
|
||||||
|
export const PROMPT_TOO_LONG_ERROR_MESSAGE = 'Prompt is too long'
|
||||||
|
|
||||||
|
export const CREDIT_BALANCE_TOO_LOW_ERROR_MESSAGE = 'Credit balance is too low'
|
||||||
|
export const INVALID_API_KEY_ERROR_MESSAGE = 'Not logged in · Please run /login'
|
||||||
|
export const INVALID_API_KEY_ERROR_MESSAGE_EXTERNAL =
|
||||||
|
'Invalid API key · Fix external API key'
|
||||||
|
export const ORG_DISABLED_ERROR_MESSAGE_ENV_KEY_WITH_OAUTH =
|
||||||
|
'Your ANTHROPIC_API_KEY belongs to a disabled organization · Unset the environment variable to use your subscription instead'
|
||||||
|
export const ORG_DISABLED_ERROR_MESSAGE_ENV_KEY =
|
||||||
|
'Your ANTHROPIC_API_KEY belongs to a disabled organization · Update or unset the environment variable'
|
||||||
|
export const TOKEN_REVOKED_ERROR_MESSAGE =
|
||||||
|
'OAuth token revoked · Please run /login'
|
||||||
|
export const CCR_AUTH_ERROR_MESSAGE =
|
||||||
|
'Authentication error · This may be a temporary network issue, please try again'
|
||||||
|
export const REPEATED_529_ERROR_MESSAGE = 'Repeated 529 Overloaded errors'
|
||||||
|
export const CUSTOM_OFF_SWITCH_MESSAGE =
|
||||||
|
'Opus is experiencing high load, please use /model to switch to Sonnet'
|
||||||
|
export const API_TIMEOUT_ERROR_MESSAGE = 'Request timed out'
|
||||||
|
export const OAUTH_ORG_NOT_ALLOWED_ERROR_MESSAGE =
|
||||||
|
'Your account does not have access to Claude Code. Please run /login.'
|
||||||
|
|
||||||
|
/** Error classification types returned by classifyAPIError */
|
||||||
|
export type APIErrorClassification =
|
||||||
|
| 'aborted'
|
||||||
|
| 'api_timeout'
|
||||||
|
| 'repeated_529'
|
||||||
|
| 'capacity_off_switch'
|
||||||
|
| 'rate_limit'
|
||||||
|
| 'server_overload'
|
||||||
|
| 'prompt_too_long'
|
||||||
|
| 'pdf_too_large'
|
||||||
|
| 'pdf_password_protected'
|
||||||
|
| 'image_too_large'
|
||||||
|
| 'tool_use_mismatch'
|
||||||
|
| 'unexpected_tool_result'
|
||||||
|
| 'duplicate_tool_use_id'
|
||||||
|
| 'invalid_model'
|
||||||
|
| 'credit_balance_low'
|
||||||
|
| 'invalid_api_key'
|
||||||
|
| 'token_revoked'
|
||||||
|
| 'oauth_org_not_allowed'
|
||||||
|
| 'auth_error'
|
||||||
|
| 'bedrock_model_access'
|
||||||
|
| 'server_error'
|
||||||
|
| 'client_error'
|
||||||
|
| 'ssl_cert_error'
|
||||||
|
| 'connection_error'
|
||||||
|
| 'unknown'
|
||||||
6
packages/@ant/model-provider/src/types/index.ts
Normal file
6
packages/@ant/model-provider/src/types/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// Type definitions for @ant/model-provider
|
||||||
|
|
||||||
|
export * from './message.js'
|
||||||
|
export * from './usage.js'
|
||||||
|
export * from './errors.js'
|
||||||
|
export * from './systemPrompt.js'
|
||||||
129
packages/@ant/model-provider/src/types/message.ts
Normal file
129
packages/@ant/model-provider/src/types/message.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
// Core message types for the model provider package.
|
||||||
|
// Moved from src/types/message.ts to decouple the API layer from the main project.
|
||||||
|
|
||||||
|
import type { UUID } from 'crypto'
|
||||||
|
import type {
|
||||||
|
ContentBlockParam,
|
||||||
|
ContentBlock,
|
||||||
|
} from '@anthropic-ai/sdk/resources/index.mjs'
|
||||||
|
import type { BetaUsage } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base message type with discriminant `type` field and common properties.
|
||||||
|
* Individual message subtypes (UserMessage, AssistantMessage, etc.) extend
|
||||||
|
* this with narrower `type` literals and additional fields.
|
||||||
|
*/
|
||||||
|
export type MessageType = 'user' | 'assistant' | 'system' | 'attachment' | 'progress' | 'grouped_tool_use' | 'collapsed_read_search'
|
||||||
|
|
||||||
|
/** A single content element inside message.content arrays. */
|
||||||
|
export type ContentItem = ContentBlockParam | ContentBlock
|
||||||
|
|
||||||
|
export type MessageContent = string | ContentBlockParam[] | ContentBlock[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Typed content array — used in narrowed message subtypes so that
|
||||||
|
* `message.content[0]` resolves to `ContentItem` instead of
|
||||||
|
* `string | ContentBlockParam | ContentBlock`.
|
||||||
|
*/
|
||||||
|
export type TypedMessageContent = ContentItem[]
|
||||||
|
|
||||||
|
export type Message = {
|
||||||
|
type: MessageType
|
||||||
|
uuid: UUID
|
||||||
|
isMeta?: boolean
|
||||||
|
isCompactSummary?: boolean
|
||||||
|
toolUseResult?: unknown
|
||||||
|
isVisibleInTranscriptOnly?: boolean
|
||||||
|
attachment?: { type: string; toolUseID?: string; [key: string]: unknown; addedNames: string[]; addedLines: string[]; removedNames: string[] }
|
||||||
|
message?: {
|
||||||
|
role?: string
|
||||||
|
id?: string
|
||||||
|
content?: MessageContent
|
||||||
|
usage?: BetaUsage | Record<string, unknown>
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AssistantMessage = Message & {
|
||||||
|
type: 'assistant'
|
||||||
|
message: NonNullable<Message['message']>
|
||||||
|
}
|
||||||
|
export type AttachmentMessage<T = { type: string; [key: string]: unknown }> = Message & { type: 'attachment'; attachment: T }
|
||||||
|
export type ProgressMessage<T = unknown> = Message & { type: 'progress'; data: T }
|
||||||
|
export type SystemLocalCommandMessage = Message & { type: 'system' }
|
||||||
|
export type SystemMessage = Message & { type: 'system' }
|
||||||
|
export type UserMessage = Message & {
|
||||||
|
type: 'user'
|
||||||
|
message: NonNullable<Message['message']>
|
||||||
|
imagePasteIds?: number[]
|
||||||
|
}
|
||||||
|
export type NormalizedUserMessage = UserMessage
|
||||||
|
export type RequestStartEvent = { type: string; [key: string]: unknown }
|
||||||
|
export type StreamEvent = { type: string; [key: string]: unknown }
|
||||||
|
export type SystemCompactBoundaryMessage = Message & {
|
||||||
|
type: 'system'
|
||||||
|
compactMetadata: {
|
||||||
|
preservedSegment?: {
|
||||||
|
headUuid: UUID
|
||||||
|
tailUuid: UUID
|
||||||
|
anchorUuid: UUID
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export type TombstoneMessage = Message
|
||||||
|
export type ToolUseSummaryMessage = Message
|
||||||
|
export type MessageOrigin = string
|
||||||
|
export type CompactMetadata = Record<string, unknown>
|
||||||
|
export type SystemAPIErrorMessage = Message & { type: 'system' }
|
||||||
|
export type SystemFileSnapshotMessage = Message & { type: 'system' }
|
||||||
|
export type NormalizedAssistantMessage<T = unknown> = AssistantMessage
|
||||||
|
export type NormalizedMessage = Message
|
||||||
|
export type PartialCompactDirection = string
|
||||||
|
|
||||||
|
export type StopHookInfo = {
|
||||||
|
command?: string
|
||||||
|
durationMs?: number
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SystemAgentsKilledMessage = Message & { type: 'system' }
|
||||||
|
export type SystemApiMetricsMessage = Message & { type: 'system' }
|
||||||
|
export type SystemAwaySummaryMessage = Message & { type: 'system' }
|
||||||
|
export type SystemBridgeStatusMessage = Message & { type: 'system' }
|
||||||
|
export type SystemInformationalMessage = Message & { type: 'system' }
|
||||||
|
export type SystemMemorySavedMessage = Message & { type: 'system' }
|
||||||
|
export type SystemMessageLevel = string
|
||||||
|
export type SystemMicrocompactBoundaryMessage = Message & { type: 'system' }
|
||||||
|
export type SystemPermissionRetryMessage = Message & { type: 'system' }
|
||||||
|
export type SystemScheduledTaskFireMessage = Message & { type: 'system' }
|
||||||
|
|
||||||
|
export type SystemStopHookSummaryMessage = Message & {
|
||||||
|
type: 'system'
|
||||||
|
subtype: string
|
||||||
|
hookLabel: string
|
||||||
|
hookCount: number
|
||||||
|
totalDurationMs?: number
|
||||||
|
hookInfos: StopHookInfo[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SystemTurnDurationMessage = Message & { type: 'system' }
|
||||||
|
|
||||||
|
export type GroupedToolUseMessage = Message & {
|
||||||
|
type: 'grouped_tool_use'
|
||||||
|
toolName: string
|
||||||
|
messages: NormalizedAssistantMessage[]
|
||||||
|
results: NormalizedUserMessage[]
|
||||||
|
displayMessage: NormalizedAssistantMessage | NormalizedUserMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
// CollapsibleMessage is used by the main project's CollapsedReadSearchGroup
|
||||||
|
export type CollapsibleMessage =
|
||||||
|
| AssistantMessage
|
||||||
|
| UserMessage
|
||||||
|
| GroupedToolUseMessage
|
||||||
|
|
||||||
|
export type HookResultMessage = Message
|
||||||
|
export type SystemThinkingMessage = Message & { type: 'system' }
|
||||||
10
packages/@ant/model-provider/src/types/systemPrompt.ts
Normal file
10
packages/@ant/model-provider/src/types/systemPrompt.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// System prompt branded type.
|
||||||
|
// Dependency-free so it can be imported from anywhere without circular imports.
|
||||||
|
|
||||||
|
export type SystemPrompt = readonly string[] & {
|
||||||
|
readonly __brand: 'SystemPrompt'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function asSystemPrompt(value: readonly string[]): SystemPrompt {
|
||||||
|
return value as SystemPrompt
|
||||||
|
}
|
||||||
49
packages/@ant/model-provider/src/types/usage.ts
Normal file
49
packages/@ant/model-provider/src/types/usage.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// Usage types for the model provider package.
|
||||||
|
// Moved from src/entrypoints/sdk/sdkUtilityTypes.ts and src/services/api/emptyUsage.ts
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Non-nullable usage object representing token consumption from an API response.
|
||||||
|
* Moved from src/entrypoints/sdk/sdkUtilityTypes.ts
|
||||||
|
*/
|
||||||
|
export type NonNullableUsage = {
|
||||||
|
inputTokens?: number
|
||||||
|
outputTokens?: number
|
||||||
|
cacheReadInputTokens?: number
|
||||||
|
cacheCreationInputTokens?: number
|
||||||
|
input_tokens: number
|
||||||
|
cache_creation_input_tokens: number
|
||||||
|
cache_read_input_tokens: number
|
||||||
|
output_tokens: number
|
||||||
|
server_tool_use: { web_search_requests: number; web_fetch_requests: number }
|
||||||
|
service_tier: string
|
||||||
|
cache_creation: {
|
||||||
|
ephemeral_1h_input_tokens: number
|
||||||
|
ephemeral_5m_input_tokens: number
|
||||||
|
}
|
||||||
|
inference_geo: string
|
||||||
|
iterations: unknown[]
|
||||||
|
speed: string
|
||||||
|
cache_deleted_input_tokens?: number
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zero-initialized usage object. Extracted from logging.ts so that
|
||||||
|
* bridge/replBridge.ts can import it without transitively pulling in
|
||||||
|
* api/errors.ts → utils/messages.ts → BashTool.tsx → the world.
|
||||||
|
*/
|
||||||
|
export const EMPTY_USAGE: Readonly<NonNullableUsage> = {
|
||||||
|
input_tokens: 0,
|
||||||
|
cache_creation_input_tokens: 0,
|
||||||
|
cache_read_input_tokens: 0,
|
||||||
|
output_tokens: 0,
|
||||||
|
server_tool_use: { web_search_requests: 0, web_fetch_requests: 0 },
|
||||||
|
service_tier: 'standard',
|
||||||
|
cache_creation: {
|
||||||
|
ephemeral_1h_input_tokens: 0,
|
||||||
|
ephemeral_5m_input_tokens: 0,
|
||||||
|
},
|
||||||
|
inference_geo: '',
|
||||||
|
iterations: [],
|
||||||
|
speed: 'standard',
|
||||||
|
}
|
||||||
7
packages/@ant/model-provider/tsconfig.json
Normal file
7
packages/@ant/model-provider/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx"]
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from 'bun:test'
|
import { describe, expect, test } from 'bun:test'
|
||||||
import type { CoreTool, Tool, Tools, AnyObject, ToolResult, ValidationResult, PermissionResult } from '@claude-code-best/agent-tools'
|
import type { CoreTool, Tool, Tools, AnyObject, ToolResult, ValidationResult, PermissionResult } from '@claude-code-best/agent-tools'
|
||||||
import type { Tool as HostTool } from '../../src/Tool.js'
|
import type { Tool as HostTool } from '../../../../src/Tool.js'
|
||||||
|
|
||||||
describe('agent-tools compatibility', () => {
|
describe('agent-tools compatibility', () => {
|
||||||
test('CoreTool structural compatibility with host Tool', () => {
|
test('CoreTool structural compatibility with host Tool', () => {
|
||||||
@@ -27,7 +27,7 @@ describe('agent-tools compatibility', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// This assignment should work if HostTool structurally extends CoreTool
|
// This assignment should work if HostTool structurally extends CoreTool
|
||||||
const coreTool: CoreTool = mockHostTool as CoreTool
|
const coreTool: CoreTool = mockHostTool as unknown as CoreTool
|
||||||
expect(coreTool.name).toBe('test')
|
expect(coreTool.name).toBe('test')
|
||||||
expect(coreTool.isEnabled()).toBe(true)
|
expect(coreTool.isEnabled()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|||||||
5
packages/agent-tools/tsconfig.json
Normal file
5
packages/agent-tools/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
5
packages/audio-capture-napi/tsconfig.json
Normal file
5
packages/audio-capture-napi/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
@@ -7,6 +7,17 @@ mock.module("src/utils/model/agent.js", () => ({
|
|||||||
|
|
||||||
mock.module("src/utils/settings/constants.js", () => ({
|
mock.module("src/utils/settings/constants.js", () => ({
|
||||||
getSourceDisplayName: (source: string) => source,
|
getSourceDisplayName: (source: string) => source,
|
||||||
|
getSourceDisplayNameLowercase: (source: string) => source,
|
||||||
|
getSourceDisplayNameCapitalized: (source: string) => source,
|
||||||
|
getSettingSourceName: (source: string) => source,
|
||||||
|
getSettingSourceDisplayNameLowercase: (source: string) => source,
|
||||||
|
getSettingSourceDisplayNameCapitalized: (source: string) => source,
|
||||||
|
parseSettingSourcesFlag: () => [],
|
||||||
|
getEnabledSettingSources: () => [],
|
||||||
|
isSettingSourceEnabled: () => true,
|
||||||
|
SETTING_SOURCES: ["localSettings", "userSettings", "projectSettings"],
|
||||||
|
SOURCES: ["localSettings", "userSettings", "projectSettings"],
|
||||||
|
CLAUDE_CODE_SETTINGS_SCHEMA_URL: "https://json.schemastore.org/claude-code-settings.json",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ mock.module("src/tasks/LocalAgentTask/LocalAgentTask.js", () => ({
|
|||||||
updateProgressFromMessage: noop,
|
updateProgressFromMessage: noop,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
mock.module("src/utils/debug.js", () => ({
|
mock.module("src/utils/debug.ts", () => ({
|
||||||
getMinDebugLogLevel: () => "warn",
|
getMinDebugLogLevel: () => "warn",
|
||||||
isDebugMode: () => false,
|
isDebugMode: () => false,
|
||||||
enableDebugLogging: () => false,
|
enableDebugLogging: () => false,
|
||||||
|
|||||||
@@ -1,11 +1,4 @@
|
|||||||
import { mock, describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
// Mock commands.ts to cut the heavy shell/prefix.ts → analytics → api chain
|
|
||||||
mock.module("src/utils/bash/commands.ts", () => ({
|
|
||||||
splitCommand_DEPRECATED: (cmd: string) =>
|
|
||||||
cmd.split(/\s*(?:[|;&]+)\s*/).filter(Boolean),
|
|
||||||
quote: (args: string[]) => args.join(" "),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { interpretCommandResult } = await import("../commandSemantics");
|
const { interpretCommandResult } = await import("../commandSemantics");
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,10 @@
|
|||||||
import { mock, describe, expect, test } from "bun:test";
|
import { mock, describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
mock.module("src/utils/debug.js", () => ({
|
mock.module("src/utils/debug.ts", () => ({
|
||||||
logForDebugging: () => {},
|
logForDebugging: () => {},
|
||||||
isDebugMode: () => false,
|
isDebugMode: () => false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
mock.module("src/utils/errors.js", () => ({
|
|
||||||
errorMessage: (e: unknown) => String(e),
|
|
||||||
}));
|
|
||||||
|
|
||||||
mock.module("src/utils/stringUtils.js", () => ({
|
|
||||||
plural: (n: number, singular: string, plural?: string) =>
|
|
||||||
n === 1 ? singular : (plural ?? singular + "s"),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
formatGoToDefinitionResult,
|
formatGoToDefinitionResult,
|
||||||
formatFindReferencesResult,
|
formatFindReferencesResult,
|
||||||
|
|||||||
@@ -7,6 +7,18 @@ mock.module("src/utils/cwd.js", () => ({
|
|||||||
getCwd: () => mockCwd,
|
getCwd: () => mockCwd,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Defensive: agent.test.ts can corrupt Bun's src/* path alias at runtime.
|
||||||
|
mock.module("src/utils/powershell/parser.js", () => ({
|
||||||
|
PS_TOKENIZER_DASH_CHARS: new Set(['-', '\u2013', '\u2014', '\u2015']),
|
||||||
|
COMMON_ALIASES: {},
|
||||||
|
commandHasArgAbbreviation: () => false,
|
||||||
|
deriveSecurityFlags: () => ({}),
|
||||||
|
getAllCommands: () => [],
|
||||||
|
getVariablesByScope: () => [],
|
||||||
|
hasCommandNamed: () => false,
|
||||||
|
parsePowerShellCommandCached: () => ({ valid: false, errors: [], statements: [], variables: [], hasStopParsing: false, originalCommand: "" }),
|
||||||
|
}))
|
||||||
|
|
||||||
const { isGitInternalPathPS, isDotGitPathPS } = await import("../gitSafety");
|
const { isGitInternalPathPS, isDotGitPathPS } = await import("../gitSafety");
|
||||||
|
|
||||||
describe("isGitInternalPathPS", () => {
|
describe("isGitInternalPathPS", () => {
|
||||||
|
|||||||
@@ -32,6 +32,58 @@ mock.module("src/utils/powershell/dangerousCmdlets.js", () => ({
|
|||||||
]),
|
]),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Defensive: agent.test.ts can corrupt Bun's src/* path alias at runtime.
|
||||||
|
// Provide parser stubs so powershellSecurity.ts loads without the alias.
|
||||||
|
// The tests build ParsedPowerShellCommand objects manually via makeParsed(),
|
||||||
|
// so the real parser implementations are not needed for these specific tests.
|
||||||
|
const MOCK_COMMON_ALIASES: Record<string, string> = {
|
||||||
|
iex: "Invoke-Expression",
|
||||||
|
ii: "Invoke-Item",
|
||||||
|
sal: "Set-Alias",
|
||||||
|
ipmo: "Import-Module",
|
||||||
|
iwmi: "Invoke-WmiMethod",
|
||||||
|
saps: "Start-Process",
|
||||||
|
start: "Start-Process",
|
||||||
|
};
|
||||||
|
|
||||||
|
mock.module("src/utils/powershell/parser.js", () => ({
|
||||||
|
COMMON_ALIASES: MOCK_COMMON_ALIASES,
|
||||||
|
commandHasArgAbbreviation: (cmd: any, fullParam: string, minPrefix: string) => {
|
||||||
|
const fullLower = fullParam.toLowerCase()
|
||||||
|
const prefixLower = minPrefix.toLowerCase()
|
||||||
|
return cmd.args.some((a: string) => {
|
||||||
|
const lower = a.toLowerCase()
|
||||||
|
const colonIdx = lower.indexOf(':')
|
||||||
|
const paramPart = colonIdx > 0 ? lower.slice(0, colonIdx) : lower
|
||||||
|
return paramPart.startsWith(prefixLower) && fullLower.startsWith(paramPart)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deriveSecurityFlags: () => ({ hasRedirectToVariable: false, hasPipelineVariable: false, hasFormatHex: false, hasScriptBlocks: false, hasSubExpressions: false, hasExpandableStrings: false, hasSplatting: false, hasStopParsing: false, hasMemberInvocations: false, hasAssignments: false }),
|
||||||
|
getAllCommands: (parsed: any) => parsed.statements.flatMap((s: any) => s.commands || []),
|
||||||
|
getVariablesByScope: () => [],
|
||||||
|
hasCommandNamed: (parsed: any, name: string) => {
|
||||||
|
const lower = name.toLowerCase()
|
||||||
|
const canonicalFromAlias = MOCK_COMMON_ALIASES[lower]?.toLowerCase()
|
||||||
|
return parsed.statements.some((s: any) => (s.commands || []).some((c: any) => {
|
||||||
|
const cmdLower = c.name.toLowerCase()
|
||||||
|
if (cmdLower === lower) return true
|
||||||
|
const canonical = MOCK_COMMON_ALIASES[cmdLower]?.toLowerCase()
|
||||||
|
if (canonical === lower) return true
|
||||||
|
if (canonicalFromAlias && cmdLower === canonicalFromAlias) return true
|
||||||
|
return false
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
parsePowerShellCommandCached: () => ({ valid: false, errors: [], statements: [], variables: [], hasStopParsing: false, originalCommand: "" }),
|
||||||
|
PARSE_SCRIPT_BODY: "",
|
||||||
|
WINDOWS_MAX_COMMAND_LENGTH: 32000,
|
||||||
|
MAX_COMMAND_LENGTH: 32000,
|
||||||
|
PS_TOKENIZER_DASH_CHARS: new Set(['-', '\u2013', '\u2014', '\u2015']),
|
||||||
|
mapStatementType: (t: string) => t,
|
||||||
|
mapElementType: (t: string) => t,
|
||||||
|
classifyCommandName: () => ({ type: 'external', name: '' }),
|
||||||
|
stripModulePrefix: (n: string) => n,
|
||||||
|
}));
|
||||||
|
|
||||||
// Real parser functions work without mocks since they're pure
|
// Real parser functions work without mocks since they're pure
|
||||||
const { powershellCommandIsSafe } = await import("../powershellSecurity.js");
|
const { powershellCommandIsSafe } = await import("../powershellSecurity.js");
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ let isFirstPartyBaseUrl = true
|
|||||||
// Only mock the external dependency that controls adapter selection
|
// Only mock the external dependency that controls adapter selection
|
||||||
mock.module('src/utils/model/providers.js', () => ({
|
mock.module('src/utils/model/providers.js', () => ({
|
||||||
isFirstPartyAnthropicBaseUrl: () => isFirstPartyBaseUrl,
|
isFirstPartyAnthropicBaseUrl: () => isFirstPartyBaseUrl,
|
||||||
|
getAPIProvider: () => 'firstParty',
|
||||||
|
getAPIProviderForStatsig: () => 'firstParty',
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const { createAdapter } = await import('../adapters/index')
|
const { createAdapter } = await import('../adapters/index')
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
import { describe, expect, mock, test } from 'bun:test'
|
import { describe, expect, mock, test } from 'bun:test'
|
||||||
|
|
||||||
|
const _abortMock = () => ({
|
||||||
|
AbortError: class AbortError extends Error {
|
||||||
|
constructor(message?: string) { super(message); this.name = 'AbortError' }
|
||||||
|
},
|
||||||
|
isAbortError: (e: unknown) => e instanceof Error && (e as Error).name === 'AbortError',
|
||||||
|
})
|
||||||
|
mock.module('src/utils/errors.js', _abortMock)
|
||||||
|
mock.module('src/utils/errors', _abortMock)
|
||||||
|
|
||||||
import { extractBingResults, decodeHtmlEntities } from '../adapters/bingAdapter'
|
import { extractBingResults, decodeHtmlEntities } from '../adapters/bingAdapter'
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||||
|
|
||||||
|
// Defensive mock: agent.test.ts mocks config.js which can corrupt Bun's
|
||||||
|
// src/* path alias resolution. Provide AbortError directly so the dynamic
|
||||||
|
// import in createAdapter() never needs to resolve the alias at runtime.
|
||||||
|
const _abortMock = () => ({
|
||||||
|
AbortError: class AbortError extends Error {
|
||||||
|
constructor(message?: string) { super(message); this.name = 'AbortError' }
|
||||||
|
},
|
||||||
|
isAbortError: (e: unknown) => e instanceof Error && (e as Error).name === 'AbortError',
|
||||||
|
})
|
||||||
|
mock.module('src/utils/errors.js', _abortMock)
|
||||||
|
mock.module('src/utils/errors', _abortMock)
|
||||||
|
|
||||||
const originalBraveSearchApiKey = process.env.BRAVE_SEARCH_API_KEY
|
const originalBraveSearchApiKey = process.env.BRAVE_SEARCH_API_KEY
|
||||||
const originalBraveApiKey = process.env.BRAVE_API_KEY
|
const originalBraveApiKey = process.env.BRAVE_API_KEY
|
||||||
|
|
||||||
|
|||||||
5
packages/builtin-tools/tsconfig.json
Normal file
5
packages/builtin-tools/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
@@ -72,18 +72,18 @@ describe("detectColorMode", () => {
|
|||||||
|
|
||||||
describe("detectLanguage", () => {
|
describe("detectLanguage", () => {
|
||||||
test("detects language from file extension", () => {
|
test("detects language from file extension", () => {
|
||||||
expect(detectLanguage("index.ts")).toBe("ts");
|
expect(detectLanguage("index.ts", null)).toBe("ts");
|
||||||
expect(detectLanguage("main.py")).toBe("py");
|
expect(detectLanguage("main.py", null)).toBe("py");
|
||||||
expect(detectLanguage("style.css")).toBe("css");
|
expect(detectLanguage("style.css", null)).toBe("css");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("detects language from known filenames", () => {
|
test("detects language from known filenames", () => {
|
||||||
expect(detectLanguage("Makefile")).toBe("makefile");
|
expect(detectLanguage("Makefile", null)).toBe("makefile");
|
||||||
expect(detectLanguage("Dockerfile")).toBe("dockerfile");
|
expect(detectLanguage("Dockerfile", null)).toBe("dockerfile");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("returns null for unknown extensions", () => {
|
test("returns null for unknown extensions", () => {
|
||||||
expect(detectLanguage("file.xyz123")).toBeNull();
|
expect(detectLanguage("file.xyz123", null)).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
5
packages/color-diff-napi/tsconfig.json
Normal file
5
packages/color-diff-napi/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
5
packages/image-processor-napi/tsconfig.json
Normal file
5
packages/image-processor-napi/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@ describe('InProcessTransport', () => {
|
|||||||
let received: JSONRPCMessage | null = null
|
let received: JSONRPCMessage | null = null
|
||||||
client.onmessage = (msg) => { received = msg }
|
client.onmessage = (msg) => { received = msg }
|
||||||
|
|
||||||
await server.send({ jsonrpc: '2.0', result: 42, id: 1 })
|
await server.send({ jsonrpc: '2.0', result: 42, id: 1 } as any)
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 10))
|
await new Promise(resolve => setTimeout(resolve, 10))
|
||||||
|
|
||||||
|
|||||||
@@ -57,9 +57,9 @@ describe('discoverTools', () => {
|
|||||||
expect(tool.name).toBe('mcp__my-server__search')
|
expect(tool.name).toBe('mcp__my-server__search')
|
||||||
expect(tool.mcpInfo).toEqual({ serverName: 'my-server', toolName: 'search' })
|
expect(tool.mcpInfo).toEqual({ serverName: 'my-server', toolName: 'search' })
|
||||||
expect(tool.isMcp).toBe(true)
|
expect(tool.isMcp).toBe(true)
|
||||||
expect(tool.isReadOnly()).toBe(true)
|
expect(tool.isReadOnly({} as any)).toBe(true)
|
||||||
expect(tool.userFacingName()).toBe('Search Items')
|
expect(tool.userFacingName(undefined)).toBe('Search Items')
|
||||||
expect(await tool.description()).toBe('Search for items')
|
expect(await tool.description({} as any, { isNonInteractiveSession: false, toolPermissionContext: {}, tools: [] })).toBe('Search for items')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('respects skipPrefix option', async () => {
|
test('respects skipPrefix option', async () => {
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ describe('createMcpManager', () => {
|
|||||||
|
|
||||||
const result = await manager.connect('test-server', { command: 'npx', args: [] })
|
const result = await manager.connect('test-server', { command: 'npx', args: [] })
|
||||||
expect(result.type).toBe('connected')
|
expect(result.type).toBe('connected')
|
||||||
expect(connectedEvent).toBe('test-server')
|
expect(connectedEvent as unknown as string).toBe('test-server')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('disconnect calls cleanup and emits disconnected', async () => {
|
test('disconnect calls cleanup and emits disconnected', async () => {
|
||||||
|
|||||||
5
packages/mcp-client/tsconfig.json
Normal file
5
packages/mcp-client/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
5
packages/modifiers-napi/tsconfig.json
Normal file
5
packages/modifiers-napi/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
@@ -25,17 +25,18 @@ import {
|
|||||||
storeUpdateSession,
|
storeUpdateSession,
|
||||||
storeGetEnvironment,
|
storeGetEnvironment,
|
||||||
storeGetSession,
|
storeGetSession,
|
||||||
storeListActiveEnvironments,
|
|
||||||
} from "../store";
|
} from "../store";
|
||||||
|
import { getEventBus, getAllEventBuses, removeEventBus } from "../transport/event-bus";
|
||||||
|
import { runDisconnectMonitorSweep } from "../services/disconnect-monitor";
|
||||||
|
|
||||||
describe("Disconnect Monitor Logic", () => {
|
describe("Disconnect Monitor Logic", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
storeReset();
|
storeReset();
|
||||||
|
for (const [key] of getAllEventBuses()) {
|
||||||
|
removeEventBus(key);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test the logic directly rather than the interval-based monitor
|
|
||||||
// to avoid long-running tests with timers
|
|
||||||
|
|
||||||
test("environment times out when lastPollAt is too old", () => {
|
test("environment times out when lastPollAt is too old", () => {
|
||||||
const env = storeCreateEnvironment({ secret: "s" });
|
const env = storeCreateEnvironment({ secret: "s" });
|
||||||
const timeoutMs = 300 * 1000; // 5 minutes
|
const timeoutMs = 300 * 1000; // 5 minutes
|
||||||
@@ -44,14 +45,7 @@ describe("Disconnect Monitor Logic", () => {
|
|||||||
const oldDate = new Date(Date.now() - timeoutMs - 60000);
|
const oldDate = new Date(Date.now() - timeoutMs - 60000);
|
||||||
storeUpdateEnvironment(env.id, { lastPollAt: oldDate });
|
storeUpdateEnvironment(env.id, { lastPollAt: oldDate });
|
||||||
|
|
||||||
// Check the timeout logic (same as in disconnect-monitor.ts)
|
runDisconnectMonitorSweep();
|
||||||
const now = Date.now();
|
|
||||||
const envs = storeListActiveEnvironments();
|
|
||||||
for (const e of envs) {
|
|
||||||
if (e.lastPollAt && now - e.lastPollAt.getTime() > timeoutMs) {
|
|
||||||
storeUpdateEnvironment(e.id, { status: "disconnected" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updated = storeGetEnvironment(env.id);
|
const updated = storeGetEnvironment(env.id);
|
||||||
expect(updated?.status).toBe("disconnected");
|
expect(updated?.status).toBe("disconnected");
|
||||||
@@ -59,43 +53,56 @@ describe("Disconnect Monitor Logic", () => {
|
|||||||
|
|
||||||
test("environment stays active when lastPollAt is recent", () => {
|
test("environment stays active when lastPollAt is recent", () => {
|
||||||
const env = storeCreateEnvironment({ secret: "s" });
|
const env = storeCreateEnvironment({ secret: "s" });
|
||||||
const timeoutMs = 300 * 1000;
|
runDisconnectMonitorSweep();
|
||||||
|
|
||||||
// lastPollAt is recent (just created)
|
|
||||||
const now = Date.now();
|
|
||||||
const envs = storeListActiveEnvironments();
|
|
||||||
for (const e of envs) {
|
|
||||||
if (e.lastPollAt && now - e.lastPollAt.getTime() > timeoutMs) {
|
|
||||||
storeUpdateEnvironment(e.id, { status: "disconnected" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updated = storeGetEnvironment(env.id);
|
const updated = storeGetEnvironment(env.id);
|
||||||
expect(updated?.status).toBe("active");
|
expect(updated?.status).toBe("active");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("session becomes inactive when updatedAt is too old", () => {
|
test("session becomes inactive when updatedAt is too old", () => {
|
||||||
const session = storeCreateSession({ status: "idle" });
|
const session = storeCreateSession({});
|
||||||
storeUpdateSession(session.id, { status: "running" });
|
storeUpdateSession(session.id, { status: "running" });
|
||||||
const timeoutMs = 300 * 1000 * 2; // 2x disconnect timeout
|
|
||||||
|
|
||||||
// Simulate updatedAt being older than 2x timeout
|
|
||||||
// We can't directly set updatedAt, but we can verify the logic
|
|
||||||
// by checking that recently updated sessions are not marked inactive
|
|
||||||
const now = Date.now();
|
|
||||||
const rec = storeGetSession(session.id);
|
const rec = storeGetSession(session.id);
|
||||||
// Session was just updated, should not be inactive
|
expect(rec).toBeTruthy();
|
||||||
expect(rec?.status).toBe("running");
|
if (!rec) return;
|
||||||
expect(now - rec!.updatedAt.getTime()).toBeLessThan(timeoutMs);
|
|
||||||
|
rec.updatedAt = new Date(Date.now() - 300 * 1000 * 2 - 60000);
|
||||||
|
|
||||||
|
runDisconnectMonitorSweep();
|
||||||
|
|
||||||
|
const updated = storeGetSession(session.id);
|
||||||
|
expect(updated?.status).toBe("inactive");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("session stays running when recently updated", () => {
|
test("session stays running when recently updated", () => {
|
||||||
const session = storeCreateSession({});
|
const session = storeCreateSession({});
|
||||||
storeUpdateSession(session.id, { status: "running" });
|
storeUpdateSession(session.id, { status: "running" });
|
||||||
|
|
||||||
const timeoutMs = 300 * 1000 * 2;
|
runDisconnectMonitorSweep();
|
||||||
|
|
||||||
|
const updated = storeGetSession(session.id);
|
||||||
|
expect(updated?.status).toBe("running");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("session timeout publishes an inactive session_status event", () => {
|
||||||
|
const session = storeCreateSession({});
|
||||||
|
storeUpdateSession(session.id, { status: "idle" });
|
||||||
const rec = storeGetSession(session.id);
|
const rec = storeGetSession(session.id);
|
||||||
expect(rec?.status).toBe("running");
|
expect(rec).toBeTruthy();
|
||||||
expect(Date.now() - rec!.updatedAt.getTime()).toBeLessThan(timeoutMs);
|
if (!rec) return;
|
||||||
|
rec.updatedAt = new Date(Date.now() - 300 * 1000 * 2 - 60000);
|
||||||
|
|
||||||
|
const bus = getEventBus(session.id);
|
||||||
|
const events: Array<{ type: string; payload: { status?: string } }> = [];
|
||||||
|
bus.subscribe((event) => {
|
||||||
|
events.push({ type: event.type, payload: event.payload as { status?: string } });
|
||||||
|
});
|
||||||
|
|
||||||
|
runDisconnectMonitorSweep();
|
||||||
|
|
||||||
|
expect(events).toContainEqual({
|
||||||
|
type: "session_status",
|
||||||
|
payload: { status: "inactive" },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,16 +19,18 @@ mock.module("../config", () => ({
|
|||||||
|
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { storeReset, storeCreateSession, storeCreateEnvironment, storeBindSession } from "../store";
|
import { storeReset, storeCreateSession, storeCreateEnvironment, storeBindSession } from "../store";
|
||||||
import { removeEventBus, getAllEventBuses } from "../transport/event-bus";
|
import { removeEventBus, getAllEventBuses, getEventBus } from "../transport/event-bus";
|
||||||
import { issueToken } from "../auth/token";
|
import { issueToken } from "../auth/token";
|
||||||
|
import { publishSessionEvent } from "../services/transport";
|
||||||
|
|
||||||
// Import route modules
|
// Import route modules
|
||||||
import v1Sessions from "../routes/v1/sessions";
|
import v1Sessions from "../routes/v1/sessions";
|
||||||
import v1Environments from "../routes/v1/environments";
|
import v1Environments from "../routes/v1/environments";
|
||||||
import v1EnvironmentsWork from "../routes/v1/environments.work";
|
import v1EnvironmentsWork from "../routes/v1/environments.work";
|
||||||
import v1SessionIngress from "../routes/v1/session-ingress";
|
import v1SessionIngress, { websocket as sessionIngressWebsocket } from "../routes/v1/session-ingress";
|
||||||
import v2CodeSessions from "../routes/v2/code-sessions";
|
import v2CodeSessions from "../routes/v2/code-sessions";
|
||||||
import v2Worker from "../routes/v2/worker";
|
import v2Worker from "../routes/v2/worker";
|
||||||
|
import v2WorkerEventsStream from "../routes/v2/worker-events-stream";
|
||||||
import v2WorkerEvents from "../routes/v2/worker-events";
|
import v2WorkerEvents from "../routes/v2/worker-events";
|
||||||
import webAuth from "../routes/web/auth";
|
import webAuth from "../routes/web/auth";
|
||||||
import webSessions from "../routes/web/sessions";
|
import webSessions from "../routes/web/sessions";
|
||||||
@@ -43,6 +45,7 @@ function createApp() {
|
|||||||
app.route("/v2/session_ingress", v1SessionIngress);
|
app.route("/v2/session_ingress", v1SessionIngress);
|
||||||
app.route("/v1/code/sessions", v2CodeSessions);
|
app.route("/v1/code/sessions", v2CodeSessions);
|
||||||
app.route("/v1/code/sessions", v2Worker);
|
app.route("/v1/code/sessions", v2Worker);
|
||||||
|
app.route("/v1/code/sessions", v2WorkerEventsStream);
|
||||||
app.route("/v1/code/sessions", v2WorkerEvents);
|
app.route("/v1/code/sessions", v2WorkerEvents);
|
||||||
app.route("/web", webAuth);
|
app.route("/web", webAuth);
|
||||||
app.route("/web", webSessions);
|
app.route("/web", webSessions);
|
||||||
@@ -53,6 +56,11 @@ function createApp() {
|
|||||||
|
|
||||||
const AUTH_HEADERS = { Authorization: "Bearer test-api-key", "X-Username": "testuser" };
|
const AUTH_HEADERS = { Authorization: "Bearer test-api-key", "X-Username": "testuser" };
|
||||||
|
|
||||||
|
function toWebSessionId(sessionId: string): string {
|
||||||
|
if (!sessionId.startsWith("cse_")) return sessionId;
|
||||||
|
return `session_${sessionId.slice("cse_".length)}`;
|
||||||
|
}
|
||||||
|
|
||||||
describe("V1 Session Routes", () => {
|
describe("V1 Session Routes", () => {
|
||||||
let app: Hono;
|
let app: Hono;
|
||||||
|
|
||||||
@@ -109,6 +117,24 @@ describe("V1 Session Routes", () => {
|
|||||||
expect(res.status).toBe(404);
|
expect(res.status).toBe(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("GET /v1/sessions/:id — resolves compat code session IDs", async () => {
|
||||||
|
const createRes = await app.request("/v1/code/sessions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
session: { id },
|
||||||
|
} = await createRes.json();
|
||||||
|
|
||||||
|
const getRes = await app.request(`/v1/sessions/${toWebSessionId(id)}`, {
|
||||||
|
headers: AUTH_HEADERS,
|
||||||
|
});
|
||||||
|
expect(getRes.status).toBe(200);
|
||||||
|
const body = await getRes.json();
|
||||||
|
expect(body.id).toBe(id);
|
||||||
|
});
|
||||||
|
|
||||||
test("PATCH /v1/sessions/:id — updates title", async () => {
|
test("PATCH /v1/sessions/:id — updates title", async () => {
|
||||||
const createRes = await app.request("/v1/sessions", {
|
const createRes = await app.request("/v1/sessions", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -142,6 +168,32 @@ describe("V1 Session Routes", () => {
|
|||||||
expect(archiveRes.status).toBe(200);
|
expect(archiveRes.status).toBe(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("POST /v1/sessions/:id/archive — archives compat code session IDs", async () => {
|
||||||
|
const createRes = await app.request("/v1/code/sessions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
session: { id },
|
||||||
|
} = await createRes.json();
|
||||||
|
const compatId = toWebSessionId(id);
|
||||||
|
|
||||||
|
const archiveRes = await app.request(`/v1/sessions/${compatId}/archive`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: AUTH_HEADERS,
|
||||||
|
});
|
||||||
|
expect(archiveRes.status).toBe(200);
|
||||||
|
|
||||||
|
const getRes = await app.request(`/v1/sessions/${compatId}`, {
|
||||||
|
headers: AUTH_HEADERS,
|
||||||
|
});
|
||||||
|
expect(getRes.status).toBe(200);
|
||||||
|
const body = await getRes.json();
|
||||||
|
expect(body.id).toBe(id);
|
||||||
|
expect(body.status).toBe("archived");
|
||||||
|
});
|
||||||
|
|
||||||
test("POST /v1/sessions/:id/events — publishes events", async () => {
|
test("POST /v1/sessions/:id/events — publishes events", async () => {
|
||||||
const createRes = await app.request("/v1/sessions", {
|
const createRes = await app.request("/v1/sessions", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -160,6 +212,30 @@ describe("V1 Session Routes", () => {
|
|||||||
expect(body.events).toBe(1);
|
expect(body.events).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("POST /v1/sessions/:id/events — resolves compat code session IDs", async () => {
|
||||||
|
const createRes = await app.request("/v1/code/sessions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
session: { id },
|
||||||
|
} = await createRes.json();
|
||||||
|
const compatId = toWebSessionId(id);
|
||||||
|
|
||||||
|
const eventsRes = await app.request(`/v1/sessions/${compatId}/events`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ events: [{ type: "user", content: "hello from compat" }] }),
|
||||||
|
});
|
||||||
|
expect(eventsRes.status).toBe(200);
|
||||||
|
|
||||||
|
const events = getEventBus(id).getEventsSince(0);
|
||||||
|
expect(events).toHaveLength(1);
|
||||||
|
expect(events[0]?.type).toBe("user");
|
||||||
|
expect((events[0]?.payload as { content?: string }).content).toBe("hello from compat");
|
||||||
|
});
|
||||||
|
|
||||||
test("POST /v1/sessions with environment_id creates work item", async () => {
|
test("POST /v1/sessions with environment_id creates work item", async () => {
|
||||||
// First register an environment
|
// First register an environment
|
||||||
const envRes = await app.request("/v1/environments/bridge", {
|
const envRes = await app.request("/v1/environments/bridge", {
|
||||||
@@ -443,6 +519,26 @@ describe("Web Auth Routes", () => {
|
|||||||
expect(body.ok).toBe(true);
|
expect(body.ok).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("POST /web/bind — binds compat code session ID to UUID", async () => {
|
||||||
|
const sessRes = await app.request("/v1/code/sessions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
const body = await sessRes.json();
|
||||||
|
const compatId = toWebSessionId(body.session.id);
|
||||||
|
|
||||||
|
const bindRes = await app.request("/web/bind?uuid=test-uuid", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ sessionId: compatId }),
|
||||||
|
});
|
||||||
|
expect(bindRes.status).toBe(200);
|
||||||
|
const bindBody = await bindRes.json();
|
||||||
|
expect(bindBody.ok).toBe(true);
|
||||||
|
expect(bindBody.sessionId).toBe(compatId);
|
||||||
|
});
|
||||||
|
|
||||||
test("POST /web/bind — 404 for unknown session", async () => {
|
test("POST /web/bind — 404 for unknown session", async () => {
|
||||||
const res = await app.request("/web/bind?uuid=test-uuid", {
|
const res = await app.request("/web/bind?uuid=test-uuid", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -501,6 +597,24 @@ describe("Web Session Routes", () => {
|
|||||||
expect(sessions[0].id).toBe(id);
|
expect(sessions[0].id).toBe(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("GET /web/sessions and /all — serialize owned code sessions as compat IDs", async () => {
|
||||||
|
const codeSession = storeCreateSession({ idPrefix: "cse_" });
|
||||||
|
storeBindSession(codeSession.id, "user-1");
|
||||||
|
const compatId = toWebSessionId(codeSession.id);
|
||||||
|
|
||||||
|
const listRes = await app.request("/web/sessions?uuid=user-1");
|
||||||
|
expect(listRes.status).toBe(200);
|
||||||
|
const sessions = await listRes.json();
|
||||||
|
expect(sessions).toHaveLength(1);
|
||||||
|
expect(sessions[0].id).toBe(compatId);
|
||||||
|
|
||||||
|
const allRes = await app.request("/web/sessions/all?uuid=user-1");
|
||||||
|
expect(allRes.status).toBe(200);
|
||||||
|
const summaries = await allRes.json();
|
||||||
|
expect(summaries).toHaveLength(1);
|
||||||
|
expect(summaries[0].id).toBe(compatId);
|
||||||
|
});
|
||||||
|
|
||||||
test("GET /web/sessions — requires UUID", async () => {
|
test("GET /web/sessions — requires UUID", async () => {
|
||||||
const res = await app.request("/web/sessions");
|
const res = await app.request("/web/sessions");
|
||||||
expect(res.status).toBe(401);
|
expect(res.status).toBe(401);
|
||||||
@@ -525,6 +639,33 @@ describe("Web Session Routes", () => {
|
|||||||
expect(sessions).toHaveLength(1); // only user-1's session, not user-2's
|
expect(sessions).toHaveLength(1); // only user-1's session, not user-2's
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("GET /web/sessions and /all — hides archived and inactive sessions", async () => {
|
||||||
|
const archived = storeCreateSession({});
|
||||||
|
const inactive = storeCreateSession({});
|
||||||
|
const open = storeCreateSession({});
|
||||||
|
storeBindSession(archived.id, "user-1");
|
||||||
|
storeBindSession(inactive.id, "user-1");
|
||||||
|
storeBindSession(open.id, "user-1");
|
||||||
|
|
||||||
|
await app.request(`/v1/sessions/${archived.id}/archive`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: AUTH_HEADERS,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { storeUpdateSession } = await import("../store");
|
||||||
|
storeUpdateSession(inactive.id, { status: "inactive" });
|
||||||
|
|
||||||
|
const listRes = await app.request("/web/sessions?uuid=user-1");
|
||||||
|
expect(listRes.status).toBe(200);
|
||||||
|
const sessions = await listRes.json();
|
||||||
|
expect(sessions.map((session: { id: string }) => session.id)).toEqual([open.id]);
|
||||||
|
|
||||||
|
const allRes = await app.request("/web/sessions/all?uuid=user-1");
|
||||||
|
expect(allRes.status).toBe(200);
|
||||||
|
const summaries = await allRes.json();
|
||||||
|
expect(summaries.map((session: { id: string }) => session.id)).toEqual([open.id]);
|
||||||
|
});
|
||||||
|
|
||||||
test("GET /web/sessions/:id — returns owned session", async () => {
|
test("GET /web/sessions/:id — returns owned session", async () => {
|
||||||
const createRes = await app.request("/web/sessions?uuid=user-1", {
|
const createRes = await app.request("/web/sessions?uuid=user-1", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -563,6 +704,22 @@ describe("Web Session Routes", () => {
|
|||||||
expect(body.events).toEqual([]);
|
expect(body.events).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("GET /web/sessions/:id and history — supports compat code session IDs", async () => {
|
||||||
|
const codeSession = storeCreateSession({ idPrefix: "cse_" });
|
||||||
|
storeBindSession(codeSession.id, "user-1");
|
||||||
|
const compatId = toWebSessionId(codeSession.id);
|
||||||
|
|
||||||
|
const getRes = await app.request(`/web/sessions/${compatId}?uuid=user-1`);
|
||||||
|
expect(getRes.status).toBe(200);
|
||||||
|
const session = await getRes.json();
|
||||||
|
expect(session.id).toBe(compatId);
|
||||||
|
|
||||||
|
const histRes = await app.request(`/web/sessions/${compatId}/history?uuid=user-1`);
|
||||||
|
expect(histRes.status).toBe(200);
|
||||||
|
const history = await histRes.json();
|
||||||
|
expect(history.events).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
test("GET /web/sessions/:id/history — 403 for non-owner", async () => {
|
test("GET /web/sessions/:id/history — 403 for non-owner", async () => {
|
||||||
const createRes = await app.request("/web/sessions?uuid=user-1", {
|
const createRes = await app.request("/web/sessions?uuid=user-1", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -647,6 +804,24 @@ describe("Web Session Routes", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("GET /web/sessions/:id/events — supports compat code session IDs", async () => {
|
||||||
|
const codeSession = storeCreateSession({ idPrefix: "cse_" });
|
||||||
|
storeBindSession(codeSession.id, "user-1");
|
||||||
|
const compatId = toWebSessionId(codeSession.id);
|
||||||
|
|
||||||
|
const eventsRes = await app.request(`/web/sessions/${compatId}/events?uuid=user-1`);
|
||||||
|
expect(eventsRes.status).toBe(200);
|
||||||
|
expect(eventsRes.headers.get("Content-Type")).toBe("text/event-stream");
|
||||||
|
|
||||||
|
const reader = eventsRes.body?.getReader();
|
||||||
|
if (reader) {
|
||||||
|
const { value } = await reader.read();
|
||||||
|
const text = new TextDecoder().decode(value!);
|
||||||
|
expect(text).toContain(": keepalive");
|
||||||
|
reader.cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("GET /web/sessions/:id/events — 403 for non-owner", async () => {
|
test("GET /web/sessions/:id/events — 403 for non-owner", async () => {
|
||||||
const createRes = await app.request("/web/sessions?uuid=user-1", {
|
const createRes = await app.request("/web/sessions?uuid=user-1", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -658,6 +833,25 @@ describe("Web Session Routes", () => {
|
|||||||
const eventsRes = await app.request(`/web/sessions/${id}/events?uuid=user-2`);
|
const eventsRes = await app.request(`/web/sessions/${id}/events?uuid=user-2`);
|
||||||
expect(eventsRes.status).toBe(403);
|
expect(eventsRes.status).toBe(403);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("GET /web/sessions/:id/events — 409 for archived session", async () => {
|
||||||
|
const createRes = await app.request("/web/sessions?uuid=user-1", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
const { id } = await createRes.json();
|
||||||
|
|
||||||
|
await app.request(`/v1/sessions/${id}/archive`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: AUTH_HEADERS,
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await app.request(`/web/sessions/${id}/events?uuid=user-1`);
|
||||||
|
expect(res.status).toBe(409);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.error.type).toBe("session_closed");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Web Control Routes", () => {
|
describe("Web Control Routes", () => {
|
||||||
@@ -692,6 +886,32 @@ describe("Web Control Routes", () => {
|
|||||||
expect(body.event).toBeTruthy();
|
expect(body.event).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("POST /web/sessions/:id/events/control/interrupt — supports compat code session IDs", async () => {
|
||||||
|
const rawSessionId = storeCreateSession({ idPrefix: "cse_" }).id;
|
||||||
|
storeBindSession(rawSessionId, "user-1");
|
||||||
|
const compatId = toWebSessionId(rawSessionId);
|
||||||
|
|
||||||
|
const eventsRes = await app.request(`/web/sessions/${compatId}/events?uuid=user-1`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ type: "user", content: "hello" }),
|
||||||
|
});
|
||||||
|
expect(eventsRes.status).toBe(200);
|
||||||
|
|
||||||
|
const controlRes = await app.request(`/web/sessions/${compatId}/control?uuid=user-1`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ type: "permission_response", approved: true, request_id: "r1" }),
|
||||||
|
});
|
||||||
|
expect(controlRes.status).toBe(200);
|
||||||
|
|
||||||
|
const interruptRes = await app.request(`/web/sessions/${compatId}/interrupt?uuid=user-1`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
expect(interruptRes.status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
test("POST /web/sessions/:id/events — 403 for non-owner", async () => {
|
test("POST /web/sessions/:id/events — 403 for non-owner", async () => {
|
||||||
const res = await app.request(`/web/sessions/${sessionId}/events?uuid=user-2`, {
|
const res = await app.request(`/web/sessions/${sessionId}/events?uuid=user-2`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -743,6 +963,33 @@ describe("Web Control Routes", () => {
|
|||||||
});
|
});
|
||||||
expect(res.status).toBe(403);
|
expect(res.status).toBe(403);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("POST /web/sessions/:id/events/control/interrupt — 409 for archived session", async () => {
|
||||||
|
await app.request(`/v1/sessions/${sessionId}/archive`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: AUTH_HEADERS,
|
||||||
|
});
|
||||||
|
|
||||||
|
const eventsRes = await app.request(`/web/sessions/${sessionId}/events?uuid=user-1`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ type: "user", content: "hello" }),
|
||||||
|
});
|
||||||
|
expect(eventsRes.status).toBe(409);
|
||||||
|
|
||||||
|
const controlRes = await app.request(`/web/sessions/${sessionId}/control?uuid=user-1`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ type: "permission_response", approved: true, request_id: "r1" }),
|
||||||
|
});
|
||||||
|
expect(controlRes.status).toBe(409);
|
||||||
|
|
||||||
|
const interruptRes = await app.request(`/web/sessions/${sessionId}/interrupt?uuid=user-1`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
expect(interruptRes.status).toBe(409);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Web Environment Routes", () => {
|
describe("Web Environment Routes", () => {
|
||||||
@@ -822,6 +1069,81 @@ describe("V1 Session Ingress Routes (HTTP)", () => {
|
|||||||
});
|
});
|
||||||
expect(res.status).toBe(404);
|
expect(res.status).toBe(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("POST /v2/session_ingress/session/:sessionId/events — resolves compat code session IDs", async () => {
|
||||||
|
const sessRes = await app.request("/v1/code/sessions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
session: { id },
|
||||||
|
} = await sessRes.json();
|
||||||
|
const compatId = toWebSessionId(id);
|
||||||
|
|
||||||
|
const res = await app.request(`/v2/session_ingress/session/${compatId}/events`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ events: [{ type: "assistant", message: { role: "assistant", content: "compat ok" } }] }),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
|
||||||
|
const events = getEventBus(id).getEventsSince(0);
|
||||||
|
expect(events).toHaveLength(1);
|
||||||
|
expect(events[0]?.type).toBe("assistant");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("GET /v2/session_ingress/ws/:sessionId — resolves compat code session IDs", async () => {
|
||||||
|
const sessRes = await app.request("/v1/code/sessions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
session: { id },
|
||||||
|
} = await sessRes.json();
|
||||||
|
const compatId = toWebSessionId(id);
|
||||||
|
|
||||||
|
publishSessionEvent(id, "user", { content: "compat ws replay" }, "outbound");
|
||||||
|
|
||||||
|
const server = Bun.serve({
|
||||||
|
port: 0,
|
||||||
|
fetch: app.fetch,
|
||||||
|
websocket: {
|
||||||
|
...sessionIngressWebsocket,
|
||||||
|
idleTimeout: 30,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const message = await new Promise<string>((resolve, reject) => {
|
||||||
|
const ws = new WebSocket(`ws://127.0.0.1:${server.port}/v2/session_ingress/ws/${compatId}?token=test-api-key`);
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
ws.close();
|
||||||
|
reject(new Error("Timed out waiting for compat WebSocket replay"));
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const data = typeof event.data === "string" ? event.data : String(event.data);
|
||||||
|
if (data.includes("\"type\":\"user\"")) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
ws.close();
|
||||||
|
resolve(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ws.onerror = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
reject(new Error("Compat WebSocket connection failed"));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(message).toContain("\"type\":\"user\"");
|
||||||
|
expect(message).toContain(`\"session_id\":\"${id}\"`);
|
||||||
|
expect(message).toContain("compat ws replay");
|
||||||
|
} finally {
|
||||||
|
await server.stop(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("V2 Worker Events Routes", () => {
|
describe("V2 Worker Events Routes", () => {
|
||||||
@@ -856,6 +1178,112 @@ describe("V2 Worker Events Routes", () => {
|
|||||||
expect(body.count).toBe(1);
|
expect(body.count).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("POST /v1/code/sessions/:id/worker/events — unwraps CCR batch payloads", async () => {
|
||||||
|
const sessRes = await app.request("/v1/code/sessions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
const { session: { id } } = await sessRes.json();
|
||||||
|
|
||||||
|
const res = await app.request(`/v1/code/sessions/${id}/worker/events`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
worker_epoch: 1,
|
||||||
|
events: [{ payload: { type: "assistant", content: "response" } }],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.count).toBe(1);
|
||||||
|
|
||||||
|
const events = getEventBus(id).getEventsSince(0);
|
||||||
|
expect(events).toHaveLength(1);
|
||||||
|
expect(events[0]?.type).toBe("assistant");
|
||||||
|
expect((events[0]?.payload as { content?: string }).content).toBe("response");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("GET/PUT /v1/code/sessions/:id/worker — stores worker state", async () => {
|
||||||
|
const sessRes = await app.request("/v1/code/sessions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
const { session: { id } } = await sessRes.json();
|
||||||
|
|
||||||
|
const putRes = await app.request(`/v1/code/sessions/${id}/worker`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
worker_epoch: 1,
|
||||||
|
worker_status: "running",
|
||||||
|
external_metadata: { permission_mode: "default" },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expect(putRes.status).toBe(200);
|
||||||
|
|
||||||
|
const getRes = await app.request(`/v1/code/sessions/${id}/worker`, {
|
||||||
|
headers: AUTH_HEADERS,
|
||||||
|
});
|
||||||
|
expect(getRes.status).toBe(200);
|
||||||
|
const body = await getRes.json();
|
||||||
|
expect(body.worker.worker_status).toBe("running");
|
||||||
|
expect(body.worker.external_metadata.permission_mode).toBe("default");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("POST /v1/code/sessions/:id/worker/heartbeat — updates heartbeat", async () => {
|
||||||
|
const sessRes = await app.request("/v1/code/sessions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
const { session: { id } } = await sessRes.json();
|
||||||
|
|
||||||
|
const heartbeatRes = await app.request(`/v1/code/sessions/${id}/worker/heartbeat`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ worker_epoch: 1 }),
|
||||||
|
});
|
||||||
|
expect(heartbeatRes.status).toBe(200);
|
||||||
|
|
||||||
|
const getRes = await app.request(`/v1/code/sessions/${id}/worker`, {
|
||||||
|
headers: AUTH_HEADERS,
|
||||||
|
});
|
||||||
|
const body = await getRes.json();
|
||||||
|
expect(body.worker.last_heartbeat_at).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("GET /v1/code/sessions/:id/worker/events/stream — emits CCR client_event frames", async () => {
|
||||||
|
const sessRes = await app.request("/v1/code/sessions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
const { session: { id } } = await sessRes.json();
|
||||||
|
|
||||||
|
const streamRes = await app.request(`/v1/code/sessions/${id}/worker/events/stream`, {
|
||||||
|
headers: AUTH_HEADERS,
|
||||||
|
});
|
||||||
|
expect(streamRes.status).toBe(200);
|
||||||
|
|
||||||
|
const reader = streamRes.body?.getReader();
|
||||||
|
expect(reader).toBeTruthy();
|
||||||
|
if (!reader) return;
|
||||||
|
|
||||||
|
const firstChunk = await reader.read();
|
||||||
|
const keepalive = new TextDecoder().decode(firstChunk.value!);
|
||||||
|
expect(keepalive).toContain(": keepalive");
|
||||||
|
|
||||||
|
publishSessionEvent(id, "user", { type: "user", content: "hello" }, "outbound");
|
||||||
|
|
||||||
|
const secondChunk = await reader.read();
|
||||||
|
const frame = new TextDecoder().decode(secondChunk.value!);
|
||||||
|
expect(frame).toContain("event: client_event");
|
||||||
|
expect(frame).toContain("\"payload\":{\"type\":\"user\",\"content\":\"hello\",\"message\":{\"content\":\"hello\"}}");
|
||||||
|
reader.cancel();
|
||||||
|
});
|
||||||
|
|
||||||
test("PUT /v1/code/sessions/:id/worker/state — updates session status", async () => {
|
test("PUT /v1/code/sessions/:id/worker/state — updates session status", async () => {
|
||||||
const sessRes = await app.request("/v1/sessions", {
|
const sessRes = await app.request("/v1/sessions", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -903,4 +1331,20 @@ describe("V2 Worker Events Routes", () => {
|
|||||||
});
|
});
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("POST /v1/code/sessions/:id/worker/events/delivery — batch no-op", async () => {
|
||||||
|
const sessRes = await app.request("/v1/code/sessions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
const { session: { id } } = await sessRes.json();
|
||||||
|
|
||||||
|
const res = await app.request(`/v1/code/sessions/${id}/worker/events/delivery`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ worker_epoch: 1, updates: [{ event_id: "evt123", status: "received" }] }),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -345,6 +345,14 @@ describe("Transport Service", () => {
|
|||||||
expect(result.message).toEqual(msg);
|
expect(result.message).toEqual(msg);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("preserves uuid field", () => {
|
||||||
|
const result = normalizePayload("user", {
|
||||||
|
uuid: "msg_123",
|
||||||
|
content: "hi",
|
||||||
|
});
|
||||||
|
expect(result.uuid).toBe("msg_123");
|
||||||
|
});
|
||||||
|
|
||||||
test("uses name as tool_name fallback", () => {
|
test("uses name as tool_name fallback", () => {
|
||||||
const result = normalizePayload("tool", { name: "Read" });
|
const result = normalizePayload("tool", { name: "Read" });
|
||||||
expect(result.tool_name).toBe("Read");
|
expect(result.tool_name).toBe("Read");
|
||||||
|
|||||||
@@ -336,6 +336,26 @@ describe("ws-handler", () => {
|
|||||||
expect(lastMsg.message.content).toBe("hello world");
|
expect(lastMsg.message.content).toBe("hello world");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("preserves payload uuid for outbound user events", () => {
|
||||||
|
const bus = getEventBus("um2");
|
||||||
|
const ws = createMockWs();
|
||||||
|
handleWebSocketOpen(ws, "um2");
|
||||||
|
|
||||||
|
bus.publish({
|
||||||
|
id: "internal-event-id",
|
||||||
|
sessionId: "um2",
|
||||||
|
type: "user",
|
||||||
|
payload: { uuid: "web-message-uuid", content: "hello from web" },
|
||||||
|
direction: "outbound",
|
||||||
|
});
|
||||||
|
|
||||||
|
const sent = ws.getSentData();
|
||||||
|
const lastMsg = JSON.parse(sent[sent.length - 1]);
|
||||||
|
expect(lastMsg.type).toBe("user");
|
||||||
|
expect(lastMsg.uuid).toBe("web-message-uuid");
|
||||||
|
expect(lastMsg.message.content).toBe("hello from web");
|
||||||
|
});
|
||||||
|
|
||||||
test("converts generic event type", () => {
|
test("converts generic event type", () => {
|
||||||
const bus = getEventBus("gen1");
|
const bus = getEventBus("gen1");
|
||||||
const ws = createMockWs();
|
const ws = createMockWs();
|
||||||
|
|||||||
@@ -14,14 +14,14 @@ app.post("/bridge", acceptCliHeaders, apiKeyAuth, async (c) => {
|
|||||||
|
|
||||||
/** DELETE /v1/environments/bridge/:id — Deregister */
|
/** DELETE /v1/environments/bridge/:id — Deregister */
|
||||||
app.delete("/bridge/:id", acceptCliHeaders, apiKeyAuth, async (c) => {
|
app.delete("/bridge/:id", acceptCliHeaders, apiKeyAuth, async (c) => {
|
||||||
const envId = c.req.param("id");
|
const envId = c.req.param("id")!;
|
||||||
deregisterEnvironment(envId);
|
deregisterEnvironment(envId);
|
||||||
return c.json({ status: "ok" }, 200);
|
return c.json({ status: "ok" }, 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
/** POST /v1/environments/:id/bridge/reconnect — Reconnect */
|
/** POST /v1/environments/:id/bridge/reconnect — Reconnect */
|
||||||
app.post("/:id/bridge/reconnect", acceptCliHeaders, apiKeyAuth, async (c) => {
|
app.post("/:id/bridge/reconnect", acceptCliHeaders, apiKeyAuth, async (c) => {
|
||||||
const envId = c.req.param("id");
|
const envId = c.req.param("id")!;
|
||||||
reconnectEnvironment(envId);
|
reconnectEnvironment(envId);
|
||||||
const { reconnectWorkForEnvironment } = await import("../../services/work-dispatch");
|
const { reconnectWorkForEnvironment } = await import("../../services/work-dispatch");
|
||||||
await reconnectWorkForEnvironment(envId);
|
await reconnectWorkForEnvironment(envId);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const app = new Hono();
|
|||||||
|
|
||||||
/** GET /v1/environments/:id/work/poll — Long-poll for work */
|
/** GET /v1/environments/:id/work/poll — Long-poll for work */
|
||||||
app.get("/:id/work/poll", acceptCliHeaders, apiKeyAuth, async (c) => {
|
app.get("/:id/work/poll", acceptCliHeaders, apiKeyAuth, async (c) => {
|
||||||
const envId = c.req.param("id");
|
const envId = c.req.param("id")!;
|
||||||
updatePollTime(envId);
|
updatePollTime(envId);
|
||||||
const result = await pollWork(envId);
|
const result = await pollWork(envId);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
@@ -19,21 +19,21 @@ app.get("/:id/work/poll", acceptCliHeaders, apiKeyAuth, async (c) => {
|
|||||||
|
|
||||||
/** POST /v1/environments/:id/work/:workId/ack — Acknowledge work */
|
/** POST /v1/environments/:id/work/:workId/ack — Acknowledge work */
|
||||||
app.post("/:id/work/:workId/ack", acceptCliHeaders, apiKeyAuth, async (c) => {
|
app.post("/:id/work/:workId/ack", acceptCliHeaders, apiKeyAuth, async (c) => {
|
||||||
const workId = c.req.param("workId");
|
const workId = c.req.param("workId")!;
|
||||||
ackWork(workId);
|
ackWork(workId);
|
||||||
return c.json({ status: "ok" }, 200);
|
return c.json({ status: "ok" }, 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
/** POST /v1/environments/:id/work/:workId/stop — Stop work */
|
/** POST /v1/environments/:id/work/:workId/stop — Stop work */
|
||||||
app.post("/:id/work/:workId/stop", acceptCliHeaders, apiKeyAuth, async (c) => {
|
app.post("/:id/work/:workId/stop", acceptCliHeaders, apiKeyAuth, async (c) => {
|
||||||
const workId = c.req.param("workId");
|
const workId = c.req.param("workId")!;
|
||||||
stopWork(workId);
|
stopWork(workId);
|
||||||
return c.json({ status: "ok" }, 200);
|
return c.json({ status: "ok" }, 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
/** POST /v1/environments/:id/work/:workId/heartbeat — Heartbeat */
|
/** POST /v1/environments/:id/work/:workId/heartbeat — Heartbeat */
|
||||||
app.post("/:id/work/:workId/heartbeat", acceptCliHeaders, apiKeyAuth, async (c) => {
|
app.post("/:id/work/:workId/heartbeat", acceptCliHeaders, apiKeyAuth, async (c) => {
|
||||||
const workId = c.req.param("workId");
|
const workId = c.req.param("workId")!;
|
||||||
const result = heartbeatWork(workId);
|
const result = heartbeatWork(workId);
|
||||||
return c.json(result, 200);
|
return c.json(result, 200);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
handleWebSocketClose,
|
handleWebSocketClose,
|
||||||
ingestBridgeMessage,
|
ingestBridgeMessage,
|
||||||
} from "../../transport/ws-handler";
|
} from "../../transport/ws-handler";
|
||||||
import { getSession } from "../../services/session";
|
import { getSession, resolveExistingSessionId } from "../../services/session";
|
||||||
|
|
||||||
const { upgradeWebSocket, websocket } = createBunWebSocket();
|
const { upgradeWebSocket, websocket } = createBunWebSocket();
|
||||||
|
|
||||||
@@ -44,7 +44,8 @@ function authenticateRequest(c: any, label: string, expectedSessionId?: string):
|
|||||||
|
|
||||||
/** POST /v2/session_ingress/session/:sessionId/events — HTTP POST (HybridTransport writes) */
|
/** POST /v2/session_ingress/session/:sessionId/events — HTTP POST (HybridTransport writes) */
|
||||||
app.post("/session/:sessionId/events", async (c) => {
|
app.post("/session/:sessionId/events", async (c) => {
|
||||||
const sessionId = c.req.param("sessionId")!;
|
const requestedSessionId = c.req.param("sessionId")!;
|
||||||
|
const sessionId = resolveExistingSessionId(requestedSessionId) ?? requestedSessionId;
|
||||||
|
|
||||||
if (!authenticateRequest(c, `POST session/${sessionId}`, sessionId)) {
|
if (!authenticateRequest(c, `POST session/${sessionId}`, sessionId)) {
|
||||||
return c.json({ error: { type: "unauthorized", message: "Invalid auth" } }, 401);
|
return c.json({ error: { type: "unauthorized", message: "Invalid auth" } }, 401);
|
||||||
@@ -72,7 +73,8 @@ app.post("/session/:sessionId/events", async (c) => {
|
|||||||
app.get(
|
app.get(
|
||||||
"/ws/:sessionId",
|
"/ws/:sessionId",
|
||||||
upgradeWebSocket(async (c) => {
|
upgradeWebSocket(async (c) => {
|
||||||
const sessionId = c.req.param("sessionId")!;
|
const requestedSessionId = c.req.param("sessionId")!;
|
||||||
|
const sessionId = resolveExistingSessionId(requestedSessionId) ?? requestedSessionId;
|
||||||
|
|
||||||
if (!authenticateRequest(c, `WS ${sessionId}`, sessionId)) {
|
if (!authenticateRequest(c, `WS ${sessionId}`, sessionId)) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
getSession,
|
getSession,
|
||||||
updateSessionTitle,
|
updateSessionTitle,
|
||||||
archiveSession,
|
archiveSession,
|
||||||
|
resolveExistingSessionId,
|
||||||
} from "../../services/session";
|
} from "../../services/session";
|
||||||
import { createWorkItem } from "../../services/work-dispatch";
|
import { createWorkItem } from "../../services/work-dispatch";
|
||||||
import { apiKeyAuth, acceptCliHeaders } from "../../auth/middleware";
|
import { apiKeyAuth, acceptCliHeaders } from "../../auth/middleware";
|
||||||
@@ -39,7 +40,8 @@ app.post("/", acceptCliHeaders, apiKeyAuth, async (c) => {
|
|||||||
|
|
||||||
/** GET /v1/sessions/:id — Get session */
|
/** GET /v1/sessions/:id — Get session */
|
||||||
app.get("/:id", acceptCliHeaders, apiKeyAuth, async (c) => {
|
app.get("/:id", acceptCliHeaders, apiKeyAuth, async (c) => {
|
||||||
const session = getSession(c.req.param("id"));
|
const sessionId = resolveExistingSessionId(c.req.param("id")!) ?? c.req.param("id")!;
|
||||||
|
const session = getSession(sessionId);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
}
|
}
|
||||||
@@ -48,27 +50,43 @@ app.get("/:id", acceptCliHeaders, apiKeyAuth, async (c) => {
|
|||||||
|
|
||||||
/** PATCH /v1/sessions/:id — Update session title */
|
/** PATCH /v1/sessions/:id — Update session title */
|
||||||
app.patch("/:id", acceptCliHeaders, apiKeyAuth, async (c) => {
|
app.patch("/:id", acceptCliHeaders, apiKeyAuth, async (c) => {
|
||||||
|
const sessionId = resolveExistingSessionId(c.req.param("id")!) ?? c.req.param("id")!;
|
||||||
|
const existing = getSession(sessionId);
|
||||||
|
if (!existing) {
|
||||||
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
|
}
|
||||||
const body = await c.req.json();
|
const body = await c.req.json();
|
||||||
if (body.title) {
|
if (body.title) {
|
||||||
updateSessionTitle(c.req.param("id"), body.title);
|
updateSessionTitle(sessionId, body.title);
|
||||||
}
|
}
|
||||||
const session = getSession(c.req.param("id"));
|
const session = getSession(sessionId);
|
||||||
return c.json(session, 200);
|
return c.json(session, 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
/** POST /v1/sessions/:id/archive — Archive session */
|
/** POST /v1/sessions/:id/archive — Archive session */
|
||||||
app.post("/:id/archive", acceptCliHeaders, apiKeyAuth, async (c) => {
|
app.post("/:id/archive", acceptCliHeaders, apiKeyAuth, async (c) => {
|
||||||
|
const sessionId = resolveExistingSessionId(c.req.param("id")!) ?? c.req.param("id")!;
|
||||||
|
const session = getSession(sessionId);
|
||||||
|
if (!session) {
|
||||||
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
archiveSession(c.req.param("id"));
|
archiveSession(sessionId);
|
||||||
} catch {
|
} catch {
|
||||||
return c.json({ status: "ok" }, 409);
|
return c.json({ status: "ok" }, 409);
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.json({ status: "ok" }, 200);
|
return c.json({ status: "ok" }, 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
/** POST /v1/sessions/:id/events — Send event to session */
|
/** POST /v1/sessions/:id/events — Send event to session */
|
||||||
app.post("/:id/events", acceptCliHeaders, apiKeyAuth, async (c) => {
|
app.post("/:id/events", acceptCliHeaders, apiKeyAuth, async (c) => {
|
||||||
const sessionId = c.req.param("id");
|
const sessionId = resolveExistingSessionId(c.req.param("id")!) ?? c.req.param("id")!;
|
||||||
|
const session = getSession(sessionId);
|
||||||
|
if (!session) {
|
||||||
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
|
}
|
||||||
const body = await c.req.json();
|
const body = await c.req.json();
|
||||||
|
|
||||||
const events = body.events
|
const events = body.events
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ app.post("/", acceptCliHeaders, apiKeyAuth, async (c) => {
|
|||||||
|
|
||||||
/** POST /v1/code/sessions/:id/bridge — Get connection info + worker JWT */
|
/** POST /v1/code/sessions/:id/bridge — Get connection info + worker JWT */
|
||||||
app.post("/:id/bridge", acceptCliHeaders, apiKeyAuth, async (c) => {
|
app.post("/:id/bridge", acceptCliHeaders, apiKeyAuth, async (c) => {
|
||||||
const sessionId = c.req.param("id");
|
const sessionId = c.req.param("id")!;
|
||||||
const session = getSession(sessionId);
|
const session = getSession(sessionId);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { sessionIngressAuth, acceptCliHeaders } from "../../auth/middleware";
|
import { sessionIngressAuth, acceptCliHeaders } from "../../auth/middleware";
|
||||||
import { createSSEStream } from "../../transport/sse-writer";
|
import { createWorkerEventStream } from "../../transport/sse-writer";
|
||||||
import { getSession } from "../../services/session";
|
import { getSession } from "../../services/session";
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
|
|
||||||
/** SSE /v1/code/sessions/:id/worker/events/stream — SSE event stream */
|
/** SSE /v1/code/sessions/:id/worker/events/stream — SSE event stream */
|
||||||
app.get("/:id/worker/events/stream", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
app.get("/:id/worker/events/stream", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
||||||
const sessionId = c.req.param("id");
|
const sessionId = c.req.param("id")!;
|
||||||
const session = getSession(sessionId);
|
const session = getSession(sessionId);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
@@ -18,7 +18,7 @@ app.get("/:id/worker/events/stream", acceptCliHeaders, sessionIngressAuth, async
|
|||||||
const fromSeq = c.req.query("from_sequence_num");
|
const fromSeq = c.req.query("from_sequence_num");
|
||||||
const fromSeqNum = fromSeq ? parseInt(fromSeq) : lastEventId ? parseInt(lastEventId) : 0;
|
const fromSeqNum = fromSeq ? parseInt(fromSeq) : lastEventId ? parseInt(lastEventId) : 0;
|
||||||
|
|
||||||
return createSSEStream(c, sessionId, fromSeqNum);
|
return createWorkerEventStream(c, sessionId, fromSeqNum);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
@@ -1,32 +1,66 @@
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { sessionIngressAuth, acceptCliHeaders } from "../../auth/middleware";
|
import { sessionIngressAuth, acceptCliHeaders } from "../../auth/middleware";
|
||||||
import { publishSessionEvent } from "../../services/transport";
|
import { publishSessionEvent } from "../../services/transport";
|
||||||
import { getSession, updateSessionStatus } from "../../services/session";
|
import { getSession, touchSession, updateSessionStatus } from "../../services/session";
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
|
|
||||||
|
function extractWorkerEvents(body: unknown): Array<Record<string, unknown>> {
|
||||||
|
if (!body || typeof body !== "object") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = body as Record<string, unknown>;
|
||||||
|
const rawEvents = Array.isArray(payload.events)
|
||||||
|
? payload.events
|
||||||
|
: Array.isArray(body)
|
||||||
|
? body
|
||||||
|
: [body];
|
||||||
|
|
||||||
|
return rawEvents
|
||||||
|
.filter((evt): evt is Record<string, unknown> => !!evt && typeof evt === "object")
|
||||||
|
.map((evt) => {
|
||||||
|
const wrappedPayload = evt.payload;
|
||||||
|
if (wrappedPayload && typeof wrappedPayload === "object" && !Array.isArray(wrappedPayload)) {
|
||||||
|
return wrappedPayload as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
return evt;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** POST /v1/code/sessions/:id/worker/events — Write events */
|
/** POST /v1/code/sessions/:id/worker/events — Write events */
|
||||||
app.post("/:id/worker/events", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
app.post("/:id/worker/events", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
||||||
const sessionId = c.req.param("id");
|
const sessionId = c.req.param("id")!;
|
||||||
|
if (!getSession(sessionId)) {
|
||||||
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
|
}
|
||||||
const body = await c.req.json();
|
const body = await c.req.json();
|
||||||
|
|
||||||
const events = Array.isArray(body) ? body : [body];
|
const events = extractWorkerEvents(body);
|
||||||
const published = [];
|
const published = [];
|
||||||
for (const evt of events) {
|
for (const evt of events) {
|
||||||
const result = publishSessionEvent(sessionId, evt.type || "message", evt, "inbound");
|
const eventType = typeof evt.type === "string" ? evt.type : "message";
|
||||||
|
const result = publishSessionEvent(sessionId, eventType, evt, "inbound");
|
||||||
published.push(result);
|
published.push(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
touchSession(sessionId);
|
||||||
|
|
||||||
return c.json({ status: "ok", count: published.length }, 200);
|
return c.json({ status: "ok", count: published.length }, 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
/** PUT /v1/code/sessions/:id/worker/state — Report worker state */
|
/** PUT /v1/code/sessions/:id/worker/state — Report worker state */
|
||||||
app.put("/:id/worker/state", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
app.put("/:id/worker/state", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
||||||
const sessionId = c.req.param("id");
|
const sessionId = c.req.param("id")!;
|
||||||
|
if (!getSession(sessionId)) {
|
||||||
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
|
}
|
||||||
const body = await c.req.json();
|
const body = await c.req.json();
|
||||||
|
|
||||||
if (body.status) {
|
if (body.status) {
|
||||||
updateSessionStatus(sessionId, body.status);
|
updateSessionStatus(sessionId, body.status);
|
||||||
|
} else {
|
||||||
|
touchSession(sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.json({ status: "ok" }, 200);
|
return c.json({ status: "ok" }, 200);
|
||||||
@@ -34,12 +68,29 @@ app.put("/:id/worker/state", acceptCliHeaders, sessionIngressAuth, async (c) =>
|
|||||||
|
|
||||||
/** PUT /v1/code/sessions/:id/worker/external_metadata — Report worker metadata (no-op) */
|
/** PUT /v1/code/sessions/:id/worker/external_metadata — Report worker metadata (no-op) */
|
||||||
app.put("/:id/worker/external_metadata", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
app.put("/:id/worker/external_metadata", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
||||||
|
const sessionId = c.req.param("id")!;
|
||||||
|
if (!getSession(sessionId)) {
|
||||||
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
|
}
|
||||||
// TUI's CCRClient calls this for metadata reporting. Accept and discard.
|
// TUI's CCRClient calls this for metadata reporting. Accept and discard.
|
||||||
return c.json({ status: "ok" }, 200);
|
return c.json({ status: "ok" }, 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** POST /v1/code/sessions/:id/worker/events/delivery — Batch delivery tracking (no-op) */
|
||||||
|
app.post("/:id/worker/events/delivery", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
||||||
|
const sessionId = c.req.param("id")!;
|
||||||
|
if (!getSession(sessionId)) {
|
||||||
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
|
}
|
||||||
|
return c.json({ status: "ok" }, 200);
|
||||||
|
});
|
||||||
|
|
||||||
/** POST /v1/code/sessions/:id/worker/events/:eventId/delivery — Delivery tracking (no-op) */
|
/** POST /v1/code/sessions/:id/worker/events/:eventId/delivery — Delivery tracking (no-op) */
|
||||||
app.post("/:id/worker/events/:eventId/delivery", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
app.post("/:id/worker/events/:eventId/delivery", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
||||||
|
const sessionId = c.req.param("id")!;
|
||||||
|
if (!getSession(sessionId)) {
|
||||||
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
|
}
|
||||||
// TUI's CCRClient reports event delivery status (received/processing/processed).
|
// TUI's CCRClient reports event delivery status (received/processing/processed).
|
||||||
// Accept and discard — event bus doesn't track per-event delivery.
|
// Accept and discard — event bus doesn't track per-event delivery.
|
||||||
return c.json({ status: "ok" }, 200);
|
return c.json({ status: "ok" }, 200);
|
||||||
|
|||||||
@@ -1,12 +1,78 @@
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { getSession, incrementEpoch } from "../../services/session";
|
import { getSession, incrementEpoch, touchSession, updateSessionStatus } from "../../services/session";
|
||||||
import { apiKeyAuth, acceptCliHeaders } from "../../auth/middleware";
|
import { apiKeyAuth, acceptCliHeaders, sessionIngressAuth } from "../../auth/middleware";
|
||||||
|
import { storeGetSessionWorker, storeUpsertSessionWorker } from "../../store";
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
|
|
||||||
|
/** GET /v1/code/sessions/:id/worker — Read worker state */
|
||||||
|
app.get("/:id/worker", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
||||||
|
const sessionId = c.req.param("id")!;
|
||||||
|
const session = getSession(sessionId);
|
||||||
|
if (!session) {
|
||||||
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const worker = storeGetSessionWorker(sessionId);
|
||||||
|
return c.json({
|
||||||
|
worker: {
|
||||||
|
worker_status: worker?.workerStatus ?? session.status,
|
||||||
|
external_metadata: worker?.externalMetadata ?? null,
|
||||||
|
requires_action_details: worker?.requiresActionDetails ?? null,
|
||||||
|
last_heartbeat_at: worker?.lastHeartbeatAt?.toISOString() ?? null,
|
||||||
|
},
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** PUT /v1/code/sessions/:id/worker — Update worker state */
|
||||||
|
app.put("/:id/worker", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
||||||
|
const sessionId = c.req.param("id")!;
|
||||||
|
const session = getSession(sessionId);
|
||||||
|
if (!session) {
|
||||||
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await c.req.json();
|
||||||
|
if (body.worker_status) {
|
||||||
|
updateSessionStatus(sessionId, body.worker_status);
|
||||||
|
} else {
|
||||||
|
touchSession(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const worker = storeUpsertSessionWorker(sessionId, {
|
||||||
|
workerStatus: body.worker_status,
|
||||||
|
externalMetadata: body.external_metadata,
|
||||||
|
requiresActionDetails: body.requires_action_details,
|
||||||
|
});
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
status: "ok",
|
||||||
|
worker: {
|
||||||
|
worker_status: worker.workerStatus ?? session.status,
|
||||||
|
external_metadata: worker.externalMetadata,
|
||||||
|
requires_action_details: worker.requiresActionDetails,
|
||||||
|
last_heartbeat_at: worker.lastHeartbeatAt?.toISOString() ?? null,
|
||||||
|
},
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** POST /v1/code/sessions/:id/worker/heartbeat — Keep worker alive */
|
||||||
|
app.post("/:id/worker/heartbeat", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
||||||
|
const sessionId = c.req.param("id")!;
|
||||||
|
const session = getSession(sessionId);
|
||||||
|
if (!session) {
|
||||||
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
storeUpsertSessionWorker(sessionId, { lastHeartbeatAt: now });
|
||||||
|
touchSession(sessionId);
|
||||||
|
return c.json({ status: "ok", last_heartbeat_at: now.toISOString() }, 200);
|
||||||
|
});
|
||||||
|
|
||||||
/** POST /v1/code/sessions/:id/worker/register — Register worker */
|
/** POST /v1/code/sessions/:id/worker/register — Register worker */
|
||||||
app.post("/:id/worker/register", acceptCliHeaders, apiKeyAuth, async (c) => {
|
app.post("/:id/worker/register", acceptCliHeaders, apiKeyAuth, async (c) => {
|
||||||
const sessionId = c.req.param("id");
|
const sessionId = c.req.param("id")!;
|
||||||
const session = getSession(sessionId);
|
const session = getSession(sessionId);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { storeGetSession, storeBindSession } from "../../store";
|
import { storeBindSession } from "../../store";
|
||||||
|
import { resolveExistingWebSessionId, toWebSessionId } from "../../services/session";
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
|
|
||||||
@@ -14,13 +15,13 @@ app.post("/bind", async (c) => {
|
|||||||
return c.json({ error: "sessionId and uuid are required" }, 400);
|
return c.json({ error: "sessionId and uuid are required" }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = storeGetSession(sessionId);
|
const resolvedSessionId = resolveExistingWebSessionId(sessionId);
|
||||||
if (!session) {
|
if (!resolvedSessionId) {
|
||||||
return c.json({ error: "Session not found" }, 404);
|
return c.json({ error: "Session not found" }, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
storeBindSession(sessionId, uuid);
|
storeBindSession(resolvedSessionId, uuid);
|
||||||
return c.json({ ok: true, sessionId });
|
return c.json({ ok: true, sessionId: toWebSessionId(resolvedSessionId) });
|
||||||
});
|
});
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
@@ -1,32 +1,47 @@
|
|||||||
import { log, error as logError } from "../../logger";
|
import { log, error as logError } from "../../logger";
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { uuidAuth } from "../../auth/middleware";
|
import { uuidAuth } from "../../auth/middleware";
|
||||||
import { getSession, updateSessionStatus } from "../../services/session";
|
import { getSession, isSessionClosedStatus, resolveOwnedWebSessionId, updateSessionStatus } from "../../services/session";
|
||||||
import { publishSessionEvent } from "../../services/transport";
|
import { publishSessionEvent } from "../../services/transport";
|
||||||
import { getEventBus } from "../../transport/event-bus";
|
import { getEventBus } from "../../transport/event-bus";
|
||||||
import { storeIsSessionOwner } from "../../store";
|
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
|
|
||||||
function checkOwnership(c: { get: (key: string) => string | undefined }, sessionId: string) {
|
type OwnershipCheckResult =
|
||||||
const uuid = c.get("uuid");
|
| { error: true }
|
||||||
if (!storeIsSessionOwner(sessionId, uuid)) {
|
| { error: true; reason: string }
|
||||||
return { error: true, session: null };
|
| { error: false; session: NonNullable<ReturnType<typeof getSession>>; sessionId: string };
|
||||||
|
|
||||||
|
function checkOwnership(c: { get: (key: string) => string | undefined }, sessionId: string): OwnershipCheckResult {
|
||||||
|
const uuid = c.get("uuid")!;
|
||||||
|
const resolvedSessionId = resolveOwnedWebSessionId(sessionId, uuid);
|
||||||
|
if (!resolvedSessionId) {
|
||||||
|
return { error: true };
|
||||||
}
|
}
|
||||||
const session = getSession(sessionId);
|
const session = getSession(resolvedSessionId);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return { error: true, session: null };
|
return { error: true };
|
||||||
}
|
}
|
||||||
return { error: false, session };
|
if (isSessionClosedStatus(session.status)) {
|
||||||
|
return { error: true, reason: `Session is ${session.status}` };
|
||||||
|
}
|
||||||
|
return { error: false, session, sessionId: resolvedSessionId };
|
||||||
|
}
|
||||||
|
|
||||||
|
function closedSessionResponse(message: string) {
|
||||||
|
return { error: { type: "session_closed", message } };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** POST /web/sessions/:id/events — Send user message to session */
|
/** POST /web/sessions/:id/events — Send user message to session */
|
||||||
app.post("/sessions/:id/events", uuidAuth, async (c) => {
|
app.post("/sessions/:id/events", uuidAuth, async (c) => {
|
||||||
const sessionId = c.req.param("id")!;
|
const requestedSessionId = c.req.param("id")!;
|
||||||
const { error } = checkOwnership(c, sessionId);
|
const ownership = checkOwnership(c, requestedSessionId);
|
||||||
if (error) {
|
if (ownership.error) {
|
||||||
return c.json({ error: { type: "forbidden", message: "Not your session" } }, 403);
|
const message = "reason" in ownership ? ownership.reason : "Not your session";
|
||||||
|
const status = "reason" in ownership ? 409 : 403;
|
||||||
|
return c.json("reason" in ownership ? closedSessionResponse(message) : { error: { type: "forbidden", message } }, status);
|
||||||
}
|
}
|
||||||
|
const { sessionId } = ownership;
|
||||||
|
|
||||||
const body = await c.req.json();
|
const body = await c.req.json();
|
||||||
const eventType = body.type || "user";
|
const eventType = body.type || "user";
|
||||||
@@ -38,11 +53,14 @@ app.post("/sessions/:id/events", uuidAuth, async (c) => {
|
|||||||
|
|
||||||
/** POST /web/sessions/:id/control — Send control request (permission approval etc) */
|
/** POST /web/sessions/:id/control — Send control request (permission approval etc) */
|
||||||
app.post("/sessions/:id/control", uuidAuth, async (c) => {
|
app.post("/sessions/:id/control", uuidAuth, async (c) => {
|
||||||
const sessionId = c.req.param("id")!;
|
const requestedSessionId = c.req.param("id")!;
|
||||||
const { error } = checkOwnership(c, sessionId);
|
const ownership = checkOwnership(c, requestedSessionId);
|
||||||
if (error) {
|
if (ownership.error) {
|
||||||
return c.json({ error: { type: "forbidden", message: "Not your session" } }, 403);
|
const message = "reason" in ownership ? ownership.reason : "Not your session";
|
||||||
|
const status = "reason" in ownership ? 409 : 403;
|
||||||
|
return c.json("reason" in ownership ? closedSessionResponse(message) : { error: { type: "forbidden", message } }, status);
|
||||||
}
|
}
|
||||||
|
const { sessionId } = ownership;
|
||||||
|
|
||||||
const body = await c.req.json();
|
const body = await c.req.json();
|
||||||
const event = publishSessionEvent(sessionId, body.type || "control_request", body, "outbound");
|
const event = publishSessionEvent(sessionId, body.type || "control_request", body, "outbound");
|
||||||
@@ -51,11 +69,14 @@ app.post("/sessions/:id/control", uuidAuth, async (c) => {
|
|||||||
|
|
||||||
/** POST /web/sessions/:id/interrupt — Interrupt session */
|
/** POST /web/sessions/:id/interrupt — Interrupt session */
|
||||||
app.post("/sessions/:id/interrupt", uuidAuth, async (c) => {
|
app.post("/sessions/:id/interrupt", uuidAuth, async (c) => {
|
||||||
const sessionId = c.req.param("id")!;
|
const requestedSessionId = c.req.param("id")!;
|
||||||
const { error } = checkOwnership(c, sessionId);
|
const ownership = checkOwnership(c, requestedSessionId);
|
||||||
if (error) {
|
if (ownership.error) {
|
||||||
return c.json({ error: { type: "forbidden", message: "Not your session" } }, 403);
|
const message = "reason" in ownership ? ownership.reason : "Not your session";
|
||||||
|
const status = "reason" in ownership ? 409 : 403;
|
||||||
|
return c.json("reason" in ownership ? closedSessionResponse(message) : { error: { type: "forbidden", message } }, status);
|
||||||
}
|
}
|
||||||
|
const { sessionId } = ownership;
|
||||||
|
|
||||||
publishSessionEvent(sessionId, "interrupt", { action: "interrupt" }, "outbound");
|
publishSessionEvent(sessionId, "interrupt", { action: "interrupt" }, "outbound");
|
||||||
updateSessionStatus(sessionId, "idle");
|
updateSessionStatus(sessionId, "idle");
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
import { log, error as logError } from "../../logger";
|
import { log, error as logError } from "../../logger";
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { uuidAuth } from "../../auth/middleware";
|
import { uuidAuth } from "../../auth/middleware";
|
||||||
import { getSession, createSession } from "../../services/session";
|
import {
|
||||||
import { storeListSessionsByOwnerUuid, storeIsSessionOwner, storeBindSession } from "../../store";
|
createSession,
|
||||||
|
getSession,
|
||||||
|
isSessionClosedStatus,
|
||||||
|
listWebSessionSummariesByOwnerUuid,
|
||||||
|
listWebSessionsByOwnerUuid,
|
||||||
|
resolveOwnedWebSessionId,
|
||||||
|
toWebSessionResponse,
|
||||||
|
} from "../../services/session";
|
||||||
|
import { storeBindSession } from "../../store";
|
||||||
import { createWorkItem } from "../../services/work-dispatch";
|
import { createWorkItem } from "../../services/work-dispatch";
|
||||||
import { listSessionSummariesByOwnerUuid } from "../../services/session";
|
|
||||||
import { createSSEStream } from "../../transport/sse-writer";
|
import { createSSEStream } from "../../transport/sse-writer";
|
||||||
import { getEventBus } from "../../transport/event-bus";
|
import { getEventBus } from "../../transport/event-bus";
|
||||||
|
|
||||||
@@ -12,7 +19,7 @@ const app = new Hono();
|
|||||||
|
|
||||||
/** POST /web/sessions — Create a session from web UI */
|
/** POST /web/sessions — Create a session from web UI */
|
||||||
app.post("/sessions", uuidAuth, async (c) => {
|
app.post("/sessions", uuidAuth, async (c) => {
|
||||||
const uuid = c.get("uuid");
|
const uuid = c.get("uuid")!;
|
||||||
const body = await c.req.json();
|
const body = await c.req.json();
|
||||||
const session = createSession({
|
const session = createSession({
|
||||||
environment_id: body.environment_id || null,
|
environment_id: body.environment_id || null,
|
||||||
@@ -38,37 +45,37 @@ app.post("/sessions", uuidAuth, async (c) => {
|
|||||||
|
|
||||||
/** GET /web/sessions — List sessions owned by the requesting UUID */
|
/** GET /web/sessions — List sessions owned by the requesting UUID */
|
||||||
app.get("/sessions", uuidAuth, async (c) => {
|
app.get("/sessions", uuidAuth, async (c) => {
|
||||||
const uuid = c.get("uuid");
|
const uuid = c.get("uuid")!;
|
||||||
const sessions = storeListSessionsByOwnerUuid(uuid);
|
const sessions = listWebSessionsByOwnerUuid(uuid);
|
||||||
return c.json(sessions, 200);
|
return c.json(sessions, 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
/** GET /web/sessions/all — List sessions owned by the requesting UUID (unowned sessions excluded) */
|
/** GET /web/sessions/all — List sessions owned by the requesting UUID (unowned sessions excluded) */
|
||||||
app.get("/sessions/all", uuidAuth, async (c) => {
|
app.get("/sessions/all", uuidAuth, async (c) => {
|
||||||
const uuid = c.get("uuid");
|
const uuid = c.get("uuid")!;
|
||||||
const sessions = listSessionSummariesByOwnerUuid(uuid);
|
const sessions = listWebSessionSummariesByOwnerUuid(uuid);
|
||||||
return c.json(sessions, 200);
|
return c.json(sessions, 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
/** GET /web/sessions/:id — Session detail */
|
/** GET /web/sessions/:id — Session detail */
|
||||||
app.get("/sessions/:id", uuidAuth, async (c) => {
|
app.get("/sessions/:id", uuidAuth, async (c) => {
|
||||||
const uuid = c.get("uuid");
|
const uuid = c.get("uuid")!;
|
||||||
const sessionId = c.req.param("id")!;
|
const sessionId = resolveOwnedWebSessionId(c.req.param("id")!, uuid);
|
||||||
if (!storeIsSessionOwner(sessionId, uuid)) {
|
if (!sessionId) {
|
||||||
return c.json({ error: { type: "forbidden", message: "Not your session" } }, 403);
|
return c.json({ error: { type: "forbidden", message: "Not your session" } }, 403);
|
||||||
}
|
}
|
||||||
const session = getSession(sessionId);
|
const session = getSession(sessionId);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
}
|
}
|
||||||
return c.json(session, 200);
|
return c.json(toWebSessionResponse(session), 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
/** GET /web/sessions/:id/history — Historical events for session */
|
/** GET /web/sessions/:id/history — Historical events for session */
|
||||||
app.get("/sessions/:id/history", uuidAuth, async (c) => {
|
app.get("/sessions/:id/history", uuidAuth, async (c) => {
|
||||||
const uuid = c.get("uuid");
|
const uuid = c.get("uuid")!;
|
||||||
const sessionId = c.req.param("id")!;
|
const sessionId = resolveOwnedWebSessionId(c.req.param("id")!, uuid);
|
||||||
if (!storeIsSessionOwner(sessionId, uuid)) {
|
if (!sessionId) {
|
||||||
return c.json({ error: { type: "forbidden", message: "Not your session" } }, 403);
|
return c.json({ error: { type: "forbidden", message: "Not your session" } }, 403);
|
||||||
}
|
}
|
||||||
const session = getSession(sessionId);
|
const session = getSession(sessionId);
|
||||||
@@ -83,15 +90,18 @@ app.get("/sessions/:id/history", uuidAuth, async (c) => {
|
|||||||
|
|
||||||
/** SSE /web/sessions/:id/events — Real-time event stream */
|
/** SSE /web/sessions/:id/events — Real-time event stream */
|
||||||
app.get("/sessions/:id/events", uuidAuth, async (c) => {
|
app.get("/sessions/:id/events", uuidAuth, async (c) => {
|
||||||
const uuid = c.get("uuid");
|
const uuid = c.get("uuid")!;
|
||||||
const sessionId = c.req.param("id")!;
|
const sessionId = resolveOwnedWebSessionId(c.req.param("id")!, uuid);
|
||||||
if (!storeIsSessionOwner(sessionId, uuid)) {
|
if (!sessionId) {
|
||||||
return c.json({ error: { type: "forbidden", message: "Not your session" } }, 403);
|
return c.json({ error: { type: "forbidden", message: "Not your session" } }, 403);
|
||||||
}
|
}
|
||||||
const session = getSession(sessionId);
|
const session = getSession(sessionId);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
}
|
}
|
||||||
|
if (isSessionClosedStatus(session.status)) {
|
||||||
|
return c.json({ error: { type: "session_closed", message: `Session is ${session.status}` } }, 409);
|
||||||
|
}
|
||||||
|
|
||||||
const lastEventId = c.req.header("Last-Event-ID");
|
const lastEventId = c.req.header("Last-Event-ID");
|
||||||
const fromSeqNum = lastEventId ? parseInt(lastEventId) : 0;
|
const fromSeqNum = lastEventId ? parseInt(lastEventId) : 0;
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import { log, error as logError } from "../logger";
|
import { log, error as logError } from "../logger";
|
||||||
import { storeListActiveEnvironments, storeUpdateEnvironment } from "../store";
|
import { storeListActiveEnvironments, storeUpdateEnvironment } from "../store";
|
||||||
import { storeListSessions, storeUpdateSession } from "../store";
|
import { storeListSessions } from "../store";
|
||||||
import { config } from "../config";
|
import { config } from "../config";
|
||||||
|
import { updateSessionStatus } from "./session";
|
||||||
|
|
||||||
export function startDisconnectMonitor() {
|
export function runDisconnectMonitorSweep(now = Date.now()) {
|
||||||
const timeoutMs = config.disconnectTimeout * 1000;
|
const timeoutMs = config.disconnectTimeout * 1000;
|
||||||
|
|
||||||
setInterval(() => {
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
// Check environment heartbeat timeout
|
// Check environment heartbeat timeout
|
||||||
const envs = storeListActiveEnvironments();
|
const envs = storeListActiveEnvironments();
|
||||||
for (const env of envs) {
|
for (const env of envs) {
|
||||||
@@ -25,9 +23,14 @@ export function startDisconnectMonitor() {
|
|||||||
const elapsed = now - session.updatedAt.getTime();
|
const elapsed = now - session.updatedAt.getTime();
|
||||||
if (elapsed > timeoutMs * 2) {
|
if (elapsed > timeoutMs * 2) {
|
||||||
log(`[RCS] Session ${session.id} marked inactive (no update for ${Math.round(elapsed / 1000)}s)`);
|
log(`[RCS] Session ${session.id} marked inactive (no update for ${Math.round(elapsed / 1000)}s)`);
|
||||||
storeUpdateSession(session.id, { status: "inactive" });
|
updateSessionStatus(session.id, "inactive");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startDisconnectMonitor() {
|
||||||
|
setInterval(() => {
|
||||||
|
runDisconnectMonitorSweep();
|
||||||
}, 60_000); // Check every minute
|
}, 60_000); // Check every minute
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
import {
|
import {
|
||||||
storeCreateSession,
|
storeCreateSession,
|
||||||
storeGetSession,
|
storeGetSession,
|
||||||
|
storeIsSessionOwner,
|
||||||
storeUpdateSession,
|
storeUpdateSession,
|
||||||
storeListSessions,
|
storeListSessions,
|
||||||
storeListSessionsByUsername,
|
storeListSessionsByUsername,
|
||||||
storeListSessionsByEnvironment,
|
storeListSessionsByEnvironment,
|
||||||
storeListSessionsByOwnerUuid,
|
storeListSessionsByOwnerUuid,
|
||||||
} from "../store";
|
} from "../store";
|
||||||
import { removeEventBus } from "../transport/event-bus";
|
import { getAllEventBuses, removeEventBus } from "../transport/event-bus";
|
||||||
import type { CreateSessionRequest, CreateCodeSessionRequest, SessionResponse, SessionSummaryResponse } from "../types/api";
|
import type { CreateSessionRequest, CreateCodeSessionRequest, SessionResponse, SessionSummaryResponse } from "../types/api";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
|
||||||
|
const CODE_SESSION_PREFIX = "cse_";
|
||||||
|
const WEB_SESSION_PREFIX = "session_";
|
||||||
|
const CLOSED_SESSION_STATUSES = new Set(["archived", "inactive"]);
|
||||||
|
|
||||||
function toResponse(row: { id: string; environmentId: string | null; title: string | null; status: string; source: string; permissionMode: string | null; workerEpoch: number; username: string | null; createdAt: Date; updatedAt: Date }): SessionResponse {
|
function toResponse(row: { id: string; environmentId: string | null; title: string | null; status: string; source: string; permissionMode: string | null; workerEpoch: number; username: string | null; createdAt: Date; updatedAt: Date }): SessionResponse {
|
||||||
return {
|
return {
|
||||||
@@ -25,6 +31,24 @@ function toResponse(row: { id: string; environmentId: string | null; title: stri
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function toWebSessionId(sessionId: string): string {
|
||||||
|
if (!sessionId.startsWith(CODE_SESSION_PREFIX)) return sessionId;
|
||||||
|
return `${WEB_SESSION_PREFIX}${sessionId.slice(CODE_SESSION_PREFIX.length)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toCompatibleCodeSessionId(sessionId: string): string | null {
|
||||||
|
if (!sessionId.startsWith(WEB_SESSION_PREFIX)) return null;
|
||||||
|
return `${CODE_SESSION_PREFIX}${sessionId.slice(WEB_SESSION_PREFIX.length)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toWebSessionResponse(session: SessionResponse): SessionResponse {
|
||||||
|
return { ...session, id: toWebSessionId(session.id) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function toWebSessionSummaryResponse(session: SessionSummaryResponse): SessionSummaryResponse {
|
||||||
|
return { ...session, id: toWebSessionId(session.id) };
|
||||||
|
}
|
||||||
|
|
||||||
export function createSession(req: CreateSessionRequest & { username?: string }): SessionResponse {
|
export function createSession(req: CreateSessionRequest & { username?: string }): SessionResponse {
|
||||||
const record = storeCreateSession({
|
const record = storeCreateSession({
|
||||||
environmentId: req.environment_id,
|
environmentId: req.environment_id,
|
||||||
@@ -51,16 +75,78 @@ export function getSession(sessionId: string): SessionResponse | null {
|
|||||||
return record ? toResponse(record) : null;
|
return record ? toResponse(record) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isSessionClosedStatus(status: string | null | undefined): boolean {
|
||||||
|
return !!status && CLOSED_SESSION_STATUSES.has(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveExistingSessionId(sessionId: string): string | null {
|
||||||
|
if (storeGetSession(sessionId)) {
|
||||||
|
return sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const compatibleCodeSessionId = toCompatibleCodeSessionId(sessionId);
|
||||||
|
if (compatibleCodeSessionId && storeGetSession(compatibleCodeSessionId)) {
|
||||||
|
return compatibleCodeSessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveExistingWebSessionId(sessionId: string): string | null {
|
||||||
|
return resolveExistingSessionId(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveOwnedWebSessionId(sessionId: string, uuid: string): string | null {
|
||||||
|
if (storeIsSessionOwner(sessionId, uuid)) {
|
||||||
|
return sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const compatibleCodeSessionId = toCompatibleCodeSessionId(sessionId);
|
||||||
|
if (compatibleCodeSessionId && storeIsSessionOwner(compatibleCodeSessionId, uuid)) {
|
||||||
|
return compatibleCodeSessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listWebSessionsByOwnerUuid(uuid: string): SessionResponse[] {
|
||||||
|
return storeListSessionsByOwnerUuid(uuid)
|
||||||
|
.filter((session) => !isSessionClosedStatus(session.status))
|
||||||
|
.map(toResponse)
|
||||||
|
.map(toWebSessionResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listWebSessionSummariesByOwnerUuid(uuid: string): SessionSummaryResponse[] {
|
||||||
|
return storeListSessionsByOwnerUuid(uuid)
|
||||||
|
.filter((session) => !isSessionClosedStatus(session.status))
|
||||||
|
.map(toSummaryResponse)
|
||||||
|
.map(toWebSessionSummaryResponse);
|
||||||
|
}
|
||||||
|
|
||||||
export function updateSessionTitle(sessionId: string, title: string) {
|
export function updateSessionTitle(sessionId: string, title: string) {
|
||||||
storeUpdateSession(sessionId, { title });
|
storeUpdateSession(sessionId, { title });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateSessionStatus(sessionId: string, status: string) {
|
export function updateSessionStatus(sessionId: string, status: string) {
|
||||||
storeUpdateSession(sessionId, { status });
|
storeUpdateSession(sessionId, { status });
|
||||||
|
const bus = getAllEventBuses().get(sessionId);
|
||||||
|
if (!bus) return;
|
||||||
|
|
||||||
|
bus.publish({
|
||||||
|
id: uuid(),
|
||||||
|
sessionId,
|
||||||
|
type: "session_status",
|
||||||
|
payload: { status },
|
||||||
|
direction: "inbound",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function touchSession(sessionId: string) {
|
||||||
|
storeUpdateSession(sessionId, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function archiveSession(sessionId: string) {
|
export function archiveSession(sessionId: string) {
|
||||||
storeUpdateSession(sessionId, { status: "archived" });
|
updateSessionStatus(sessionId, "archived");
|
||||||
removeEventBus(sessionId);
|
removeEventBus(sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ export function normalizePayload(type: string, payload: unknown): Record<string,
|
|||||||
raw: payload,
|
raw: payload,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (typeof p.uuid === "string" && p.uuid) normalized.uuid = p.uuid;
|
||||||
|
|
||||||
// Preserve tool fields
|
// Preserve tool fields
|
||||||
if (p.tool_name) normalized.tool_name = p.tool_name;
|
if (p.tool_name) normalized.tool_name = p.tool_name;
|
||||||
if (p.name) normalized.tool_name = p.name;
|
if (p.name) normalized.tool_name = p.name;
|
||||||
|
|||||||
@@ -47,6 +47,16 @@ export interface WorkItemRecord {
|
|||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SessionWorkerRecord {
|
||||||
|
sessionId: string;
|
||||||
|
workerStatus: string | null;
|
||||||
|
externalMetadata: Record<string, unknown> | null;
|
||||||
|
requiresActionDetails: Record<string, unknown> | null;
|
||||||
|
lastHeartbeatAt: Date | null;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- Stores (in-memory Maps) ----------
|
// ---------- Stores (in-memory Maps) ----------
|
||||||
|
|
||||||
const users = new Map<string, UserRecord>();
|
const users = new Map<string, UserRecord>();
|
||||||
@@ -54,6 +64,7 @@ const tokenToUser = new Map<string, { username: string; createdAt: Date }>();
|
|||||||
const environments = new Map<string, EnvironmentRecord>();
|
const environments = new Map<string, EnvironmentRecord>();
|
||||||
const sessions = new Map<string, SessionRecord>();
|
const sessions = new Map<string, SessionRecord>();
|
||||||
const workItems = new Map<string, WorkItemRecord>();
|
const workItems = new Map<string, WorkItemRecord>();
|
||||||
|
const sessionWorkers = new Map<string, SessionWorkerRecord>();
|
||||||
|
|
||||||
// UUID → session ownership: sessionId → Set of UUIDs
|
// UUID → session ownership: sessionId → Set of UUIDs
|
||||||
const sessionOwners = new Map<string, Set<string>>();
|
const sessionOwners = new Map<string, Set<string>>();
|
||||||
@@ -190,9 +201,59 @@ export function storeListSessionsByEnvironment(envId: string): SessionRecord[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function storeDeleteSession(id: string): boolean {
|
export function storeDeleteSession(id: string): boolean {
|
||||||
|
sessionWorkers.delete(id);
|
||||||
return sessions.delete(id);
|
return sessions.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- Session Worker ----------
|
||||||
|
|
||||||
|
export function storeGetSessionWorker(sessionId: string): SessionWorkerRecord | undefined {
|
||||||
|
return sessionWorkers.get(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function storeUpsertSessionWorker(sessionId: string, patch: {
|
||||||
|
workerStatus?: string | null;
|
||||||
|
externalMetadata?: Record<string, unknown> | null;
|
||||||
|
requiresActionDetails?: Record<string, unknown> | null;
|
||||||
|
lastHeartbeatAt?: Date | null;
|
||||||
|
}): SessionWorkerRecord {
|
||||||
|
const now = new Date();
|
||||||
|
const existing = sessionWorkers.get(sessionId);
|
||||||
|
const record: SessionWorkerRecord = existing ?? {
|
||||||
|
sessionId,
|
||||||
|
workerStatus: null,
|
||||||
|
externalMetadata: null,
|
||||||
|
requiresActionDetails: null,
|
||||||
|
lastHeartbeatAt: null,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (patch.workerStatus !== undefined) {
|
||||||
|
record.workerStatus = patch.workerStatus;
|
||||||
|
}
|
||||||
|
if (patch.externalMetadata !== undefined) {
|
||||||
|
if (patch.externalMetadata === null) {
|
||||||
|
record.externalMetadata = null;
|
||||||
|
} else {
|
||||||
|
record.externalMetadata = {
|
||||||
|
...(record.externalMetadata ?? {}),
|
||||||
|
...patch.externalMetadata,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (patch.requiresActionDetails !== undefined) {
|
||||||
|
record.requiresActionDetails = patch.requiresActionDetails;
|
||||||
|
}
|
||||||
|
if (patch.lastHeartbeatAt !== undefined) {
|
||||||
|
record.lastHeartbeatAt = patch.lastHeartbeatAt;
|
||||||
|
}
|
||||||
|
record.updatedAt = now;
|
||||||
|
|
||||||
|
sessionWorkers.set(sessionId, record);
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- Work Items ----------
|
// ---------- Work Items ----------
|
||||||
|
|
||||||
// ---------- Session Ownership (UUID-based) ----------
|
// ---------- Session Ownership (UUID-based) ----------
|
||||||
@@ -272,5 +333,6 @@ export function storeReset() {
|
|||||||
environments.clear();
|
environments.clear();
|
||||||
sessions.clear();
|
sessions.clear();
|
||||||
workItems.clear();
|
workItems.clear();
|
||||||
|
sessionWorkers.clear();
|
||||||
sessionOwners.clear();
|
sessionOwners.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,3 +116,109 @@ export function createSSEStream(c: Context, sessionId: string, fromSeqNum = 0) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toWorkerClientPayload(event: SessionEvent): Record<string, unknown> {
|
||||||
|
const normalized =
|
||||||
|
event.payload && typeof event.payload === "object"
|
||||||
|
? (event.payload as Record<string, unknown>)
|
||||||
|
: undefined;
|
||||||
|
const raw =
|
||||||
|
normalized?.raw && typeof normalized.raw === "object" && !Array.isArray(normalized.raw)
|
||||||
|
? (normalized.raw as Record<string, unknown>)
|
||||||
|
: undefined;
|
||||||
|
const payload: Record<string, unknown> = {
|
||||||
|
...(raw ?? normalized ?? {}),
|
||||||
|
type: event.type,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (event.type === "user") {
|
||||||
|
const message = payload.message;
|
||||||
|
if (!message || typeof message !== "object" || !("content" in message)) {
|
||||||
|
const content =
|
||||||
|
typeof normalized?.content === "string"
|
||||||
|
? normalized.content
|
||||||
|
: typeof payload.content === "string"
|
||||||
|
? payload.content
|
||||||
|
: typeof event.payload === "string"
|
||||||
|
? event.payload
|
||||||
|
: "";
|
||||||
|
payload.content = content;
|
||||||
|
payload.message = { content };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toWorkerClientFrame(event: SessionEvent): string {
|
||||||
|
const data = JSON.stringify({
|
||||||
|
event_id: event.id,
|
||||||
|
sequence_num: event.seqNum,
|
||||||
|
event_type: event.type,
|
||||||
|
source: "client",
|
||||||
|
payload: toWorkerClientPayload(event),
|
||||||
|
created_at: new Date(event.createdAt).toISOString(),
|
||||||
|
});
|
||||||
|
return `id: ${event.seqNum}\nevent: client_event\ndata: ${data}\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create CCR worker SSE stream (client_event frames, outbound events only). */
|
||||||
|
export function createWorkerEventStream(c: Context, sessionId: string, fromSeqNum = 0) {
|
||||||
|
const bus = getEventBus(sessionId);
|
||||||
|
|
||||||
|
const stream = new ReadableStream({
|
||||||
|
start(controller) {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
|
||||||
|
if (fromSeqNum > 0) {
|
||||||
|
const missed = bus
|
||||||
|
.getEventsSince(fromSeqNum)
|
||||||
|
.filter((event) => event.direction === "outbound");
|
||||||
|
for (const event of missed) {
|
||||||
|
controller.enqueue(encoder.encode(toWorkerClientFrame(event)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.enqueue(encoder.encode(": keepalive\n\n"));
|
||||||
|
|
||||||
|
const unsub = bus.subscribe((event) => {
|
||||||
|
if (event.direction !== "outbound") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
controller.enqueue(encoder.encode(toWorkerClientFrame(event)));
|
||||||
|
} catch {
|
||||||
|
unsub();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const keepalive = setInterval(() => {
|
||||||
|
try {
|
||||||
|
controller.enqueue(encoder.encode(": keepalive\n\n"));
|
||||||
|
} catch {
|
||||||
|
clearInterval(keepalive);
|
||||||
|
unsub();
|
||||||
|
}
|
||||||
|
}, 15000);
|
||||||
|
|
||||||
|
c.req.raw.signal.addEventListener("abort", () => {
|
||||||
|
unsub();
|
||||||
|
clearInterval(keepalive);
|
||||||
|
try {
|
||||||
|
controller.close();
|
||||||
|
} catch {
|
||||||
|
// already closed
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(stream, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/event-stream",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
Connection: "keep-alive",
|
||||||
|
"X-Accel-Buffering": "no",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,13 +25,14 @@ const SERVER_KEEPALIVE_INTERVAL_MS = 60_000;
|
|||||||
*/
|
*/
|
||||||
function toSDKMessage(event: SessionEvent): string {
|
function toSDKMessage(event: SessionEvent): string {
|
||||||
const payload = event.payload as Record<string, unknown> | null;
|
const payload = event.payload as Record<string, unknown> | null;
|
||||||
|
const messageUuid = typeof payload?.uuid === "string" && payload.uuid ? payload.uuid : event.id;
|
||||||
|
|
||||||
let msg: Record<string, unknown>;
|
let msg: Record<string, unknown>;
|
||||||
|
|
||||||
if (event.type === "user" || event.type === "user_message") {
|
if (event.type === "user" || event.type === "user_message") {
|
||||||
msg = {
|
msg = {
|
||||||
type: "user",
|
type: "user",
|
||||||
uuid: event.id,
|
uuid: messageUuid,
|
||||||
session_id: event.sessionId,
|
session_id: event.sessionId,
|
||||||
message: {
|
message: {
|
||||||
role: "user",
|
role: "user",
|
||||||
@@ -83,7 +84,7 @@ function toSDKMessage(event: SessionEvent): string {
|
|||||||
} else {
|
} else {
|
||||||
msg = {
|
msg = {
|
||||||
type: event.type,
|
type: event.type,
|
||||||
uuid: event.id,
|
uuid: messageUuid,
|
||||||
session_id: event.sessionId,
|
session_id: event.sessionId,
|
||||||
message: payload,
|
message: payload,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
/** SDK 消息类型 — 与 CC CLI bridge 模块兼容 */
|
|
||||||
export interface SDKMessage {
|
|
||||||
type: string;
|
|
||||||
content?: unknown;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UserMessage extends SDKMessage {
|
|
||||||
type: "user";
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AssistantMessage extends SDKMessage {
|
|
||||||
type: "assistant";
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PermissionRequest extends SDKMessage {
|
|
||||||
type: "permission_request";
|
|
||||||
tool_name: string;
|
|
||||||
tool_input: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PermissionResponse extends SDKMessage {
|
|
||||||
type: "permission_response";
|
|
||||||
approved: boolean;
|
|
||||||
request_id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ControlRequest extends SDKMessage {
|
|
||||||
type: "control_request";
|
|
||||||
action: string;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SessionEventType =
|
|
||||||
| "user"
|
|
||||||
| "assistant"
|
|
||||||
| "permission_request"
|
|
||||||
| "permission_response"
|
|
||||||
| "control_request"
|
|
||||||
| "tool_use"
|
|
||||||
| "tool_result"
|
|
||||||
| "status"
|
|
||||||
| "error";
|
|
||||||
|
|
||||||
// --- Normalized Event Payloads (SSE contract) ---
|
|
||||||
|
|
||||||
export interface NormalizedEventPayload {
|
|
||||||
content: string;
|
|
||||||
raw?: unknown;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UserEventPayload extends NormalizedEventPayload {
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AssistantEventPayload extends NormalizedEventPayload {
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ToolUseEventPayload extends NormalizedEventPayload {
|
|
||||||
content: string;
|
|
||||||
tool_name: string;
|
|
||||||
tool_input: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ToolResultEventPayload extends NormalizedEventPayload {
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PermissionEventPayload extends NormalizedEventPayload {
|
|
||||||
content: string;
|
|
||||||
request_id: string;
|
|
||||||
request: {
|
|
||||||
subtype: string;
|
|
||||||
tool_name: string;
|
|
||||||
tool_input: unknown;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,5 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"extends": "../../tsconfig.base.json",
|
||||||
"target": "ES2022",
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"strict": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"outDir": "dist",
|
|
||||||
"rootDir": ".",
|
|
||||||
"declaration": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"types": ["bun-types"]
|
|
||||||
},
|
|
||||||
"include": ["src/**/*.ts"],
|
"include": ["src/**/*.ts"],
|
||||||
"exclude": ["node_modules", "dist", "web"]
|
"exclude": ["node_modules", "dist", "web"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,18 +4,26 @@
|
|||||||
*/
|
*/
|
||||||
import { getUuid, setUuid, apiBind, apiFetchSessions, apiFetchAllSessions, apiFetchEnvironments, apiFetchSession, apiFetchSessionHistory, apiSendEvent, apiSendControl, apiInterrupt, apiCreateSession } from "./api.js";
|
import { getUuid, setUuid, apiBind, apiFetchSessions, apiFetchAllSessions, apiFetchEnvironments, apiFetchSession, apiFetchSessionHistory, apiSendEvent, apiSendControl, apiInterrupt, apiCreateSession } from "./api.js";
|
||||||
import { connectSSE, disconnectSSE } from "./sse.js";
|
import { connectSSE, disconnectSSE } from "./sse.js";
|
||||||
import { appendEvent, renderPermissionRequest, showLoading, isLoading, resetReplayState, renderReplayPendingRequests } from "./render.js";
|
import { appendEvent, showLoading, isLoading, removeLoading, resetReplayState, renderReplayPendingRequests } from "./render.js";
|
||||||
import { initTaskPanel, toggleTaskPanel, resetTaskState } from "./task-panel.js";
|
import { initTaskPanel, toggleTaskPanel, resetTaskState } from "./task-panel.js";
|
||||||
import { esc, formatTime, statusClass } from "./utils.js";
|
import { esc, formatTime, statusClass, isClosedSessionStatus } from "./utils.js";
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// State
|
// State
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
let currentSessionId = null;
|
let currentSessionId = null;
|
||||||
|
let currentSessionStatus = null;
|
||||||
let dashboardInterval = null;
|
let dashboardInterval = null;
|
||||||
let cachedEnvs = [];
|
let cachedEnvs = [];
|
||||||
|
|
||||||
|
function generateMessageUuid() {
|
||||||
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
||||||
|
return crypto.randomUUID();
|
||||||
|
}
|
||||||
|
return `msg_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Router
|
// Router
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -43,6 +51,69 @@ function navigate(path) {
|
|||||||
}
|
}
|
||||||
window.navigate = navigate;
|
window.navigate = navigate;
|
||||||
|
|
||||||
|
function applySessionStatus(status) {
|
||||||
|
currentSessionStatus = status || null;
|
||||||
|
|
||||||
|
const badge = document.getElementById("session-status");
|
||||||
|
if (badge) {
|
||||||
|
badge.textContent = status || "";
|
||||||
|
badge.className = `status-badge status-${statusClass(status)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const closed = isClosedSessionStatus(status);
|
||||||
|
const input = document.getElementById("msg-input");
|
||||||
|
if (input) {
|
||||||
|
input.disabled = closed;
|
||||||
|
input.placeholder = closed ? "Session is closed" : "Type a message...";
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionBtn = document.getElementById("action-btn");
|
||||||
|
if (actionBtn) {
|
||||||
|
actionBtn.disabled = closed;
|
||||||
|
actionBtn.title = closed ? "Session is closed" : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closed) {
|
||||||
|
removeLoading();
|
||||||
|
window.__updateActionBtn?.(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSessionEvent(event) {
|
||||||
|
if (event?.type === "session_status" && typeof event.payload?.status === "string") {
|
||||||
|
applySessionStatus(event.payload.status);
|
||||||
|
if (isClosedSessionStatus(event.payload.status)) {
|
||||||
|
disconnectSSE();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
appendEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncClosedSessionState(err, actionLabel) {
|
||||||
|
if (!(err instanceof Error)) {
|
||||||
|
alert(`${actionLabel}: unknown error`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentSessionId || !/session is /i.test(err.message)) {
|
||||||
|
alert(`${actionLabel}: ${err.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const session = await apiFetchSession(currentSessionId);
|
||||||
|
applySessionStatus(session.status);
|
||||||
|
if (isClosedSessionStatus(session.status)) {
|
||||||
|
appendEvent({ type: "session_status", payload: { status: session.status } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall back to the original error if the refresh also fails.
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(`${actionLabel}: ${err.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
async function handleRoute() {
|
async function handleRoute() {
|
||||||
// Ensure we have a UUID
|
// Ensure we have a UUID
|
||||||
getUuid();
|
getUuid();
|
||||||
@@ -86,6 +157,8 @@ async function handleRoute() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Default: /code → dashboard
|
// Default: /code → dashboard
|
||||||
|
currentSessionId = null;
|
||||||
|
currentSessionStatus = null;
|
||||||
showPage("dashboard");
|
showPage("dashboard");
|
||||||
disconnectSSE();
|
disconnectSSE();
|
||||||
renderDashboard();
|
renderDashboard();
|
||||||
@@ -172,9 +245,7 @@ async function renderSessionDetail(id) {
|
|||||||
document.getElementById("session-id").textContent = session.id;
|
document.getElementById("session-id").textContent = session.id;
|
||||||
document.getElementById("session-env").textContent = session.environment_id || "";
|
document.getElementById("session-env").textContent = session.environment_id || "";
|
||||||
document.getElementById("session-time").textContent = formatTime(session.created_at);
|
document.getElementById("session-time").textContent = formatTime(session.created_at);
|
||||||
const badge = document.getElementById("session-status");
|
applySessionStatus(session.status);
|
||||||
badge.textContent = session.status;
|
|
||||||
badge.className = `status-badge status-${statusClass(session.status)}`;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert("Failed to load session: " + err.message);
|
alert("Failed to load session: " + err.message);
|
||||||
navigate("/code/");
|
navigate("/code/");
|
||||||
@@ -201,7 +272,13 @@ async function renderSessionDetail(id) {
|
|||||||
// Re-render any still-unresolved permission prompts from history
|
// Re-render any still-unresolved permission prompts from history
|
||||||
renderReplayPendingRequests();
|
renderReplayPendingRequests();
|
||||||
|
|
||||||
connectSSE(id, appendEvent, lastSeqNum);
|
if (isClosedSessionStatus(currentSessionStatus)) {
|
||||||
|
appendEvent({ type: "session_status", payload: { status: currentSessionStatus } });
|
||||||
|
disconnectSSE();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectSSE(id, handleSessionEvent, lastSeqNum);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -237,28 +314,35 @@ function setupControlBar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function doInterrupt() {
|
async function doInterrupt() {
|
||||||
if (!currentSessionId) return;
|
if (!currentSessionId || isClosedSessionStatus(currentSessionStatus)) return;
|
||||||
const btn = document.getElementById("action-btn");
|
const btn = document.getElementById("action-btn");
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
try {
|
try {
|
||||||
await apiInterrupt(currentSessionId);
|
await apiInterrupt(currentSessionId);
|
||||||
appendEvent({ type: "interrupt", payload: { message: "Session interrupted" } });
|
appendEvent({ type: "interrupt", payload: { message: "Session interrupted" } });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert("Interrupt failed: " + err.message);
|
await syncClosedSessionState(err, "Interrupt failed");
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false;
|
btn.disabled = isClosedSessionStatus(currentSessionStatus);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendMessage() {
|
async function sendMessage() {
|
||||||
const input = document.getElementById("msg-input");
|
const input = document.getElementById("msg-input");
|
||||||
const text = input.value.trim();
|
const text = input.value.trim();
|
||||||
if (!text || !currentSessionId) return;
|
if (!text || !currentSessionId || isClosedSessionStatus(currentSessionStatus)) return;
|
||||||
input.value = "";
|
input.value = "";
|
||||||
|
const uuid = generateMessageUuid();
|
||||||
try {
|
try {
|
||||||
await apiSendEvent(currentSessionId, { type: "user", content: text });
|
await apiSendEvent(currentSessionId, {
|
||||||
|
type: "user",
|
||||||
|
uuid,
|
||||||
|
content: text,
|
||||||
|
message: { content: text },
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert("Failed to send: " + err.message);
|
input.value = text;
|
||||||
|
await syncClosedSessionState(err, "Failed to send");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user