Compare commits

..

4 Commits

Author SHA1 Message Date
unraid
c5f52cd668 fix: daemon 子进程 spawn 跨平台修复 + CliLaunchSpec 集中化重构
- 新建 src/utils/cliLaunch.ts: 集中化 CLI 子进程启动层
  - buildCliLaunch(): 标准化启动规范(execArgv snapshot + bundled mode 检测)
  - spawnCli(): 统一 spawn(自动 windowsHide)
  - quoteCliLaunch(): tmux shell 引用
- 修复 --daemon-worker=kind 等号格式解析(cli.tsx)
- 修复 daemon/bg fast path 缺少 setShellIfWindows()(Windows git-bash 发现)
- 修复 checkPathExists 用 execSync('dir') 改为 existsSync(消除 cmd.exe 弹窗)
- 修复 CLAUDE_CODE_GIT_BASH_PATH env 传播给子进程
- 7 个 spawn 站点迁移到 CliLaunchSpec
- BgEngine 接口新增 supportsInteractiveInput capability
- daemon bg 无 -p 时 detached 引擎给出清晰错误提示
2026-04-14 22:16:35 +08:00
unraid
4c409df35d chore: full Biome lint cleanup — zero errors, zero warnings
- Remove 203 unused biome-ignore suppression comments (noConsole rule is off)
- Apply all FIXABLE transforms: Math.pow->**, parseInt radix,
  noUselessContinue, noUselessUndefinedInitialization, useIndexOf,
  useRegexLiterals, useShorthandFunctionType, noPrototypeBuiltins
- Add targeted suppressions for 31 intentional patterns
- Format all src/ files via Biome (quote style, import line width)
- Result: 0 errors, 0 warnings across 2649 files
2026-04-14 19:56:13 +08:00
unraid
ee369549a8 fix: correct detectMimeFromBase64 to decode raw bytes from base64
The charCodeAt approach compared raw byte magic numbers against
base64-encoded characters which never matched. Decode first 12 raw
bytes and check standard magic byte signatures directly.
Fixes API 400 on Windows (JPEG) and macOS (PNG) screenshots.
2026-04-14 19:54:11 +08:00
unraid
637c9081f6 feat: integrate 5 feature branches + daemon/job 命令层级化 + 跨平台后台引擎 + TypeScript 错误修复
Squashed merge of:
1. fix/mcp-tsc-errors — 修复上游 MCP 重构后的 tsc 错误和测试失败
2. feat/pipe-mute-disconnect — Pipe IPC 逻辑断开、/lang 命令、mute 状态机
3. feat/stub-recovery-all — 实现全部 stub 恢复 (task 001-012)
4. feat/kairos-activation — KAIROS 激活解除阻塞 + 工具实现
5. codex/openclaw-autonomy-pr — 自治权限系统、运行记录、managed flows

Additional:
6. daemon/job 命令层级化重构 (subcommand 架构)
7. 跨平台后台引擎抽象 (detached/tmux engines)
8. 修复 src/ 中 43 个预存在的 TypeScript 类型错误
9. 修复 langfuse isolated test mock 完整性
10. 修复 CodeRabbit 审查的 Critical/Major/Minor 问题
11. remote-control-server logger 抽象 (测试 stderr 静默化)
12. /simplify 审查修复 (代码复用、质量、效率)
2026-04-14 19:53:36 +08:00
1726 changed files with 52671 additions and 73872 deletions

View File

@@ -8,7 +8,7 @@ on:
jobs:
ci:
runs-on: ubuntu-latest
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
@@ -23,16 +23,8 @@ jobs:
- name: Type check
run: bunx tsc --noEmit
- name: Test with Coverage
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: Test
run: bun test
- name: Build
run: bun run build:vite
run: bun run build

10
.gitignore vendored
View File

@@ -13,6 +13,7 @@ src/utils/vendor/
# AI tool runtime directories
.agents/
.claude/
.codex/
.omx/
.docs/task/
# Binary / screenshot files (root only)
@@ -29,12 +30,3 @@ __pycache__/
logs
data
.omc
.codex/*
!.codex/agents/
!.codex/agents/**
!.codex/skills/
!.codex/skills/**
.codex/skills/.system/**
!.codex/prompts/
!.codex/prompts/**

204
02-kairos (1).md Normal file
View File

@@ -0,0 +1,204 @@
# 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 Normal file
View File

@@ -0,0 +1,283 @@
# 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 frameworkforked/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 ServerDocker 部署,含 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` 注册。

View File

@@ -55,8 +55,6 @@ bun run health
# Check unused exports
bun run check:unused
bun run typecheck
# Remote Control Server
bun run rcs
@@ -247,23 +245,14 @@ Feature flags control which functionality is enabled at runtime. 代码中统一
## Testing
- **框架**: `bun:test`(内置断言 + mock
- **当前状态**: 2992 tests / 188 files / 0 fail
- **当前状态**: 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
### 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 必须零错误**。每次修改后运行:

View File

@@ -10,25 +10,27 @@
> 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(踩踩背)... 而且, 我们实现了企业版或者需要登陆 Claude 账号才能使用的特性, 实现技术普惠
牢 A (Anthropic) 官方 [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI 工具的源码反编译/逆向还原项目。目标是将 Claude Code 大部分功能及工程化能力复现 (问就是老佛爷已经付过钱了)。虽然很难绷, 但是它叫做 CCB(踩踩背)...
[文档在这里, 支持投稿 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) |
| **ACP 协议一等一支持** | 支持接入 Zed、Cursor 等 IDE支持会话恢复、Skills、权限桥接 | [文档](https://ccb.agent-aura.top/docs/features/acp-zed) |
| **Remote Control 私有部署** | Docker 自托管远程界面, 可以手机上看 CC | [文档](https://ccb.agent-aura.top/docs/features/remote-control-self-hosting) |
| **Langfuse 监控** | 企业级 Agent 监控, 可以清晰看到每次 agent loop 细节, 可以一键转化为数据集 | [文档](https://ccb.agent-aura.top/docs/features/langfuse-monitoring) |
| **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) |
| Remote Control 私有部署 | Docker 自托管 RCS + Web UI | [文档](https://ccb.agent-aura.top/docs/features/remote-control-self-hosting) |
| /dream 记忆整理 | 自动整理和优化记忆文件 | [文档](https://ccb.agent-aura.top/docs/features/auto-dream) |
| Web Search | 内置网页搜索工具 | [文档](https://ccb.agent-aura.top/docs/features/web-browser-tool) |
| 自定义模型供应商 | 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) |
| Computer Use | 屏幕截图、键鼠控制 | [文档](https://ccb.agent-aura.top/docs/features/computer-use) |
| Chrome Use | 浏览器自动化、表单填写、数据抓取 | [自托管](https://ccb.agent-aura.top/docs/features/chrome-use-mcp) [原生版](https://ccb.agent-aura.top/docs/features/claude-in-chrome-mcp) |
| Chrome Use | 浏览器自动化、表单填写、数据抓取 | [魔改版](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) |
| GrowthBook | 企业级特性开关 | [文档](https://ccb.agent-aura.top/docs/internals/growthbook-adapter) |
| /dream 记忆整理 | 自动整理和优化记忆文件 | [文档](https://ccb.agent-aura.top/docs/features/auto-dream) |
| Langfuse 监控 | LLM 调用/工具执行/多 Agent 全链路追踪 | [文档](https://ccb.agent-aura.top/docs/features/langfuse-monitoring) |
| Poor Mode | 穷鬼模式,关闭记忆提取和键入建议 | /poor 可以开关 |
- 🔮 [ ] V6 — 大规模重构石山代码全面模块分包全新分支main 封存为历史版本)
- 🚀 [想要启动项目](#快速开始源码版)
- 🐛 [想要调试项目](#vs-code-调试)

View File

@@ -30,8 +30,6 @@ const DEFAULT_BUILD_FEATURES = [
'ULTRAPLAN',
// P2: daemon + remote control server
'DAEMON',
// ACP (Agent Client Protocol) agent mode
'ACP',
// PR-package restored features
'WORKFLOW_SCRIPTS',
'HISTORY_SNIP',
@@ -92,27 +90,8 @@ 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(
`Bundled ${result.outputs.length} files to ${outdir}/ (patched ${patched} for import.meta.require, ${bunPatched} for Bun destructure)`,
`Bundled ${result.outputs.length} files to ${outdir}/ (patched ${patched} for Node.js compat)`,
)
// Step 4: Copy native .node addon files (audio-capture)
@@ -142,7 +121,46 @@ const cliNode = join(outdir, 'cli-node.js')
await writeFile(cliBun, '#!/usr/bin/env bun\nimport "./cli.js"\n')
await writeFile(cliNode, '#!/usr/bin/env node\nimport "./cli.js"\n')
// Node.js entry needs a Bun API polyfill because Bun.build({ target: 'bun' })
// 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
const { chmodSync } = await import('fs')

119
bun.lock
View File

@@ -5,9 +5,7 @@
"": {
"name": "claude-code-best",
"dependencies": {
"@agentclientprotocol/sdk": "^0.19.0",
"@claude-code-best/mcp-chrome-bridge": "^2.0.8",
"ws": "^8.20.0",
"@claude-code-best/mcp-chrome-bridge": "^2.0.7",
},
"devDependencies": {
"@alcalzone/ansi-tokenize": "^0.3.0",
@@ -15,7 +13,6 @@
"@ant/computer-use-input": "workspace:*",
"@ant/computer-use-mcp": "workspace:*",
"@ant/computer-use-swift": "workspace:*",
"@ant/model-provider": "workspace:*",
"@anthropic-ai/bedrock-sdk": "^0.26.4",
"@anthropic-ai/claude-agent-sdk": "^0.2.87",
"@anthropic-ai/foundry-sdk": "^0.2.3",
@@ -60,11 +57,10 @@
"@sentry/node": "^10.47.0",
"@smithy/core": "^3.23.13",
"@smithy/node-http-handler": "^4.5.1",
"@types/bun": "^1.3.12",
"@types/bun": "^1.3.11",
"@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/plist": "^3.0.5",
"@types/proper-lockfile": "^4.1.4",
@@ -120,7 +116,6 @@
"react": "^19.2.4",
"react-compiler-runtime": "^1.0.0",
"react-reconciler": "^0.33.0",
"rollup": "^4.60.1",
"semver": "^7.7.4",
"sharp": "^0.34.5",
"shell-quote": "^1.8.3",
@@ -135,11 +130,11 @@
"undici": "^7.24.6",
"url-handler-napi": "workspace:*",
"usehooks-ts": "^3.1.1",
"vite": "^8.0.8",
"vscode-jsonrpc": "^8.2.1",
"vscode-languageserver-protocol": "^3.17.5",
"vscode-languageserver-types": "^3.17.5",
"wrap-ansi": "^10.0.0",
"ws": "^8.20.0",
"xss": "^1.0.15",
"yaml": "^2.8.3",
"zod": "^4.3.6",
@@ -184,14 +179,6 @@
"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": {
"name": "@claude-code-best/agent-tools",
"version": "1.0.0",
@@ -266,8 +253,6 @@
},
},
"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=="],
"@ant/claude-for-chrome-mcp": ["@ant/claude-for-chrome-mcp@workspace:packages/@ant/claude-for-chrome-mcp"],
@@ -278,8 +263,6 @@
"@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/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=="],
@@ -460,7 +443,7 @@
"@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.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-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-client": ["@claude-code-best/mcp-client@workspace:packages/mcp-client"],
@@ -524,21 +507,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=="],
"@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/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/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/cors": ["@fastify/cors@11.2.0", "", { "dependencies": { "fastify-plugin": "^5.0.0", "toad-cache": "^3.7.0" } }, "sha512-LbLHBuSAdGdSFZYTLVA3+Ch2t+sA6nq3Ejc6XLAKiQ6ViS2qFnvicpj0htsx03FyYeLs04HfRNBsz/a8SvbcUw=="],
"@fastify/error": ["@fastify/error@4.2.0", "https://registry.npmmirror.com/@fastify/error/-/error-4.2.0.tgz", {}, "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ=="],
"@fastify/error": ["@fastify/error@4.2.0", "", {}, "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ=="],
"@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/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/forwarded": ["@fastify/forwarded@3.0.1", "https://registry.npmmirror.com/@fastify/forwarded/-/forwarded-3.0.1.tgz", {}, "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw=="],
"@fastify/forwarded": ["@fastify/forwarded@3.0.1", "", {}, "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw=="],
"@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/merge-json-schemas": ["@fastify/merge-json-schemas@0.2.1", "", { "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/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=="],
"@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=="],
"@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=="],
@@ -858,36 +841,6 @@
"@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=="],
"@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=="],
@@ -1146,7 +1099,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=="],
"abstract-logging": ["abstract-logging@2.0.1", "https://registry.npmmirror.com/abstract-logging/-/abstract-logging-2.0.1.tgz", {}, "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="],
"abstract-logging": ["abstract-logging@2.0.1", "", {}, "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=="],
@@ -1180,7 +1133,7 @@
"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", "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=="],
"avvio": ["avvio@9.2.0", "", { "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=="],
@@ -1294,7 +1247,7 @@
"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", "https://registry.npmmirror.com/dequal/-/dequal-2.0.3.tgz", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"dequal": ["dequal@2.0.3", "", {}, "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=="],
@@ -1352,15 +1305,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=="],
"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-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "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-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", "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-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-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-querystring": ["fast-querystring@1.1.2", "", { "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=="],
@@ -1368,9 +1321,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=="],
"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": ["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-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=="],
"fastify-plugin": ["fastify-plugin@5.1.0", "", {}, "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=="],
@@ -1388,7 +1341,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=="],
"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-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-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=="],
@@ -1530,7 +1483,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-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-ref-resolver": ["json-schema-ref-resolver@3.0.0", "", { "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=="],
@@ -1552,7 +1505,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=="],
"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=="],
"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=="],
"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=="],
@@ -1792,15 +1745,13 @@
"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", "https://registry.npmmirror.com/ret/-/ret-0.5.0.tgz", {}, "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw=="],
"ret": ["ret@0.5.0", "", {}, "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=="],
"reusify": ["reusify@1.1.0", "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"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=="],
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
"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=="],
@@ -1812,7 +1763,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-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-regex2": ["safe-regex2@5.1.0", "", { "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=="],
@@ -1820,7 +1771,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=="],
"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=="],
"secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "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=="],
@@ -1830,7 +1781,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-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=="],
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "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=="],
@@ -1900,7 +1851,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=="],
"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=="],
"toad-cache": ["toad-cache@3.7.0", "", {}, "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=="],
@@ -1942,7 +1893,7 @@
"vary": ["vary@1.1.2", "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"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=="],
"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=="],
"vscode-jsonrpc": ["vscode-jsonrpc@8.2.1", "https://registry.npmmirror.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz", {}, "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ=="],
@@ -2010,8 +1961,6 @@
"@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/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=="],
@@ -2178,7 +2127,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/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=="],
"@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "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=="],
@@ -2346,9 +2295,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=="],
"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/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
"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=="],
"light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "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=="],
@@ -2364,10 +2313,6 @@
"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=="],
"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

View File

@@ -1,17 +0,0 @@
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

View File

@@ -1,40 +0,0 @@
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

View File

@@ -1,189 +0,0 @@
# 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` | ✅ | 配置更新通知 |

View File

@@ -6,7 +6,7 @@
### 第一步:安装 Chrome 扩展
1. 下载扩展https://github.com/hangwin/mcp-chrome/releases
1. 下载扩展https://github.com/hangwin/mcp-chrome/releases(下载最新 zip
2. 解压 zip 文件
3. 打开 Chrome 访问 `chrome://extensions/`
4. 开启右上角「开发者模式」

View File

@@ -1,7 +1,7 @@
# KAIROS — 常驻助手模式
> Feature Flag: `FEATURE_KAIROS=1`(及子 Feature
> 实现状态:核心框架完整,部分子模块为 stubproactive/sleep 节奏控制已可用
> 实现状态:核心框架完整,部分子模块为 stub
> 引用数154全库最大
## 一、功能概述
@@ -74,9 +74,8 @@ KAIROS 在系统提示中注入两大段落:
SleepTool 是 KAIROS/Proactive 的节奏控制核心。工具描述让模型理解"休眠"概念:
- 工具名:`Sleep`
- 功能:等待指定时间后响应 tick prompt;若队列出现新工作或 proactive 被关闭,会提前唤醒
- 功能:等待指定时间后响应 tick prompt
-`<tick_tag>` 配合实现心跳式自主工作
- 远程控制 surfaces 可通过 `automation_state` 看到 `standby` / `sleeping` 两种状态
### 3.3 Bridge 集成
@@ -173,10 +172,8 @@ FEATURE_KAIROS=1 FEATURE_TOKEN_BUDGET=1 bun run dev
| `src/assistant/AssistantSessionChooser.ts` | — | Session 选择 UIstub |
| `src/tools/BriefTool/` | — | BriefTool 实现stub |
| `src/tools/SleepTool/prompt.ts` | ~30 | SleepTool 工具提示 |
| `src/tools/SleepTool/SleepTool.ts` | ~200 | 休眠/唤醒与 automation metadata |
| `src/services/mcp/channelNotification.ts` | 5 | 频道消息接入stub |
| `src/memdir/memdir.ts` | — | 记忆目录管理stub |
| `src/constants/prompts.ts:552-554,843-914` | 72 | 系统提示注入 |
| `src/components/tasks/src/tasks/DreamTask/` | 3 | Dream 任务stub |
| `src/proactive/index.ts` | — | Proactive 核心KAIROS 共享) |
| `src/utils/sessionState.ts` | — | 向 bridge/CCR 暴露 automation 状态 |
| `src/proactive/index.ts` | — | Proactive 核心(stubKAIROS 共享) |

View File

@@ -1,7 +1,7 @@
# PROACTIVE — 主动模式
> Feature Flag: `FEATURE_PROACTIVE=1`(与 `FEATURE_KAIROS=1` 共享功能)
> 实现状态:核心循环与 SleepTool 已落地,部分外围文档仍在补齐
> 实现状态:核心模块全部 Stub布线完整
> 引用数37
## 一、功能概述
@@ -21,13 +21,13 @@ PROACTIVE 实现 Tick 驱动的自主代理。CLI 在用户不输入时也能持
| 模块 | 文件 | 状态 | 说明 |
|------|------|------|------|
| 核心逻辑 | `src/proactive/index.ts` | **已实现** | `activateProactive()``deactivateProactive()``pause/resume``nextTickAt` 调度状态 |
| 核心逻辑 | `src/proactive/index.ts` | **Stub** | `activateProactive()``deactivateProactive()``isProactiveActive() => false` |
| SleepTool 提示 | `src/tools/SleepTool/prompt.ts` | **完整** | 工具提示定义(工具名:`Sleep` |
| 命令注册 | `src/commands.ts:62-65` | **布线** | 动态加载 `./commands/proactive.js` |
| 工具注册 | `src/tools.ts:26-28` | **布线** | SleepTool 动态加载 |
| REPL 集成 | `src/screens/REPL.tsx` | **已实现** | tick 驱动、standby/sleeping 状态、页脚与 bridge automation metadata 上报 |
| REPL 集成 | `src/screens/REPL.tsx` | **布线** | tick 驱动逻辑、占位符、页脚 UI |
| 系统提示 | `src/constants/prompts.ts:860-914` | **完整** | 自主工作行为指令(~55 行详细 prompt |
| 远控状态镜像 | `src/utils/sessionState.ts` | **已实现** | 向 remote-control/CCR 暴露 `automation_state` 元数据 |
| 会话存储 | `src/utils/sessionStorage.ts:4892-4912` | **布线** | tick 消息注入对话流 |
### 2.2 系统提示内容
@@ -46,7 +46,7 @@ PROACTIVE 实现 Tick 驱动的自主代理。CLI 在用户不输入时也能持
### 2.3 数据流
```
activateProactive()
activateProactive() [需要实现]
Tick 调度器启动
@@ -62,22 +62,20 @@ Tick 调度器启动
└── 无事可做 → 必须调用 SleepTool
SleepTool 等待
├── 用户插入新工作 / 队列中有命令 → 立即唤醒
├── proactive 被关闭 → 立即中断
└── 进入休眠时向远端 surfaces 上报 `automation_state = sleeping`
SleepTool 等待 [需要实现]
下一个 tick 到达
```
## 三、当前行为补充
## 三、需要补全的内容
- `standby`proactive 已开启,当前没有执行中的 turn且已调度下一个 tick。
- `sleeping`:模型显式调用 `SleepTool` 进入等待窗口。
- remote-control/CCR 通过 `external_metadata.automation_state` 接收这两个状态,用于 Web UI 的 Autopilot 状态显示。
- `SleepTool` 现在不是纯定时器;它会在共享命令队列出现新工作时提前醒来。
| 优先级 | 模块 | 工作量 | 说明 |
|--------|------|--------|------|
| 1 | `src/proactive/index.ts` | 中 | Tick 调度器、activate/deactivate 状态机、pause/resume |
| 2 | `src/tools/SleepTool/SleepTool.ts` | 小 | 工具执行(等待指定时间后触发 tick |
| 3 | `src/commands/proactive.js` | 小 | `/proactive` 斜杠命令处理器 |
| 4 | `src/hooks/useProactive.ts` | 中 | React hookREPL 引用但不存在) |
## 四、关键设计决策
@@ -103,11 +101,9 @@ FEATURE_PROACTIVE=1 FEATURE_KAIROS=1 FEATURE_KAIROS_BRIEF=1 bun run dev
| 文件 | 职责 |
|------|------|
| `src/proactive/index.ts` | 核心逻辑与 next-tick 状态 |
| `src/proactive/index.ts` | 核心逻辑stub |
| `src/tools/SleepTool/prompt.ts` | SleepTool 工具提示 |
| `src/tools/SleepTool/SleepTool.ts` | 休眠/唤醒执行逻辑 |
| `src/constants/prompts.ts:860-914` | 自主工作系统提示 |
| `src/screens/REPL.tsx` | REPL tick 集成与 automation 状态上报 |
| `src/screens/REPL.tsx` | REPL tick 集成 |
| `src/utils/sessionStorage.ts:4892-4912` | Tick 消息注入 |
| `src/utils/sessionState.ts` | bridge/CCR metadata 镜像 |
| `src/components/PromptInput/PromptInputFooterLeftSide.tsx` | 页脚 UI 状态 |

View File

@@ -138,19 +138,13 @@ bun run dist/cli.js
/remote-control
```
环境型 Remote Control例如 `claude remote-control` 子命令)会向 RCS 注册环境,注册成功后在终端显示连接 URL
CLI 会向 RCS 注册环境,注册成功后在终端显示连接 URL
```
https://rcs.example.com/code?bridge=<environmentId>
```
交互式 REPL 方式(`--remote-control``/remote-control`)在某些桥接模式下也可能直接给出会话 URL
```
https://rcs.example.com/code/session_<id>
```
两种 URL 都可以直接在浏览器打开并远程操控当前会话;只有 environment 模式才会出现在 Web UI 的环境列表中。
同时支持 QR 码扫码打开。该 URL 即 Web UI 控制面板入口,在浏览器中打开即可远程操控当前会话。
若已连接,再次执行 `/remote-control` 会显示对话框,包含以下选项:
- **Disconnect this session** — 断开远程连接
@@ -171,11 +165,9 @@ claude bridge
通过 `/remote-control` 命令获取 URL 后,在浏览器打开即可使用。功能:
- 查看已注册的运行环境environment 模式)
- 查看已注册的运行环境
- 创建和管理会话
- 实时查看对话消息和工具调用
- 查看 Autopilot 状态(`standby` / `sleeping`)和自动运行指示
- 查看 authoritative task snapshots 驱动的 Tasks 面板
- 审批 Claude Code 的工具权限请求
Web UI 使用 UUID 认证(无需用户账户),适合受信任网络环境。
@@ -217,7 +209,6 @@ Web UI 使用 UUID 认证(无需用户账户),适合受信任网络环境
9. 双向通信
CLI ──消息/工具调用结果──► RCS ──► Browser
CLI ◄──权限审批/指令───── RCS ◄──── Browser
CLI ──automation_state / task_state──► RCS ──► Browser
10. 心跳保活(每 20 秒)
CLI ──POST /v1/environments/:id/work/:workId/heartbeat──► RCS
@@ -227,13 +218,6 @@ Web UI 使用 UUID 认证(无需用户账户),适合受信任网络环境
## 故障排查
### Web UI 看不到当前 Autopilot 状态
- `standby`proactive 已开启,正在等待下一个 tick
- `sleeping`:模型正在 `SleepTool` 等待窗口中
这两个状态通过 worker `external_metadata.automation_state` 上报。如果页面只显示普通 working spinner优先检查 CLI 和 RCS 之间的 worker metadata PUT 是否成功。
### CLI 无法连接
```
@@ -291,3 +275,4 @@ curl https://rcs.example.com/health
| 依赖 | claude.ai 订阅 + OAuth | 仅需 API Key |
自托管模式的核心优势是:设置 `CLAUDE_BRIDGE_BASE_URL` 后,代码自动调用 `isSelfHostedBridge()` 返回 `true`,跳过所有 GrowthBook 和订阅检查,无需 claude.ai 账户即可使用。

View File

@@ -1,6 +1,6 @@
{
"name": "claude-code-best",
"version": "1.4.1",
"version": "1.3.5",
"description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal",
"type": "module",
"author": "claude-code-best <claude-code-best@proton.me>",
@@ -31,8 +31,7 @@
},
"workspaces": [
"packages/*",
"packages/@ant/*",
"packages/@anthropic-ai/*"
"packages/@ant/*"
],
"files": [
"dist",
@@ -41,9 +40,6 @@
],
"scripts": {
"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:inspect": "bun run scripts/dev-debug.ts",
"prepublishOnly": "bun run build",
@@ -54,19 +50,19 @@
"test": "bun test",
"check:unused": "knip-bun",
"health": "bun run scripts/health-check.ts",
"postinstall": "node scripts/run-parallel.mjs scripts/postinstall.cjs scripts/setup-chrome-mcp.mjs",
"postinstall": "node scripts/postinstall.cjs && node scripts/setup-chrome-mcp.mjs",
"docs:dev": "npx mintlify dev",
"typecheck": "tsc --noEmit",
"rcs": "bun run scripts/rcs.ts"
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.19.0",
"@claude-code-best/mcp-chrome-bridge": "^2.0.8",
"ws": "^8.20.0"
"@claude-code-best/mcp-chrome-bridge": "^2.0.7"
},
"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",
"@ant/model-provider": "workspace:*",
"@ant/claude-for-chrome-mcp": "workspace:*",
"@ant/computer-use-input": "workspace:*",
"@ant/computer-use-mcp": "workspace:*",
@@ -79,6 +75,9 @@
"@anthropic-ai/sdk": "^0.80.0",
"@anthropic-ai/vertex-sdk": "^0.14.4",
"@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-runtime": "^3.1020.0",
"@aws-sdk/client-sts": "^3.1020.0",
@@ -86,13 +85,8 @@
"@aws-sdk/credential-providers": "^3.1020.0",
"@azure/identity": "^4.13.1",
"@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",
"@growthbook/growthbook": "^1.6.5",
"@langfuse/otel": "^5.1.0",
"@langfuse/tracing": "^5.1.0",
"@modelcontextprotocol/sdk": "^1.29.0",
"@opentelemetry/api": "^1.9.1",
"@opentelemetry/api-logs": "^0.214.0",
@@ -115,11 +109,8 @@
"@sentry/node": "^10.47.0",
"@smithy/core": "^3.23.13",
"@smithy/node-http-handler": "^4.5.1",
"@types/bun": "^1.3.12",
"@types/bun": "^1.3.11",
"@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/plist": "^3.0.5",
"@types/proper-lockfile": "^4.1.4",
@@ -175,7 +166,6 @@
"react": "^19.2.4",
"react-compiler-runtime": "^1.0.0",
"react-reconciler": "^0.33.0",
"rollup": "^4.60.1",
"semver": "^7.7.4",
"sharp": "^0.34.5",
"shell-quote": "^1.8.3",
@@ -190,11 +180,11 @@
"undici": "^7.24.6",
"url-handler-napi": "workspace:*",
"usehooks-ts": "^3.1.1",
"vite": "^8.0.8",
"vscode-jsonrpc": "^8.2.1",
"vscode-languageserver-protocol": "^3.17.5",
"vscode-languageserver-types": "^3.17.5",
"wrap-ansi": "^10.0.0",
"ws": "^8.20.0",
"xss": "^1.0.15",
"yaml": "^2.8.3",
"zod": "^4.3.6"

View File

@@ -1,5 +0,0 @@
{
"extends": "../../../tsconfig.base.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -5,12 +5,9 @@
* mouse and keyboard via CoreGraphics events and System Events.
*/
import { execFile, execFileSync } from 'child_process'
import { promisify } from 'util'
import { $ } from 'bun'
import type { FrontmostAppInfo, InputBackend } from '../types.js'
const execFileAsync = promisify(execFile)
const KEY_MAP: Record<string, number> = {
return: 36, enter: 36, tab: 48, space: 49, delete: 51, backspace: 51,
escape: 53, esc: 53,
@@ -28,17 +25,13 @@ const MODIFIER_MAP: Record<string, string> = {
}
async function osascript(script: string): Promise<string> {
const { stdout } = await execFileAsync('osascript', ['-e', script], {
encoding: 'utf-8',
})
return stdout.trim()
const result = await $`osascript -e ${script}`.quiet().nothrow().text()
return result.trim()
}
async function jxa(script: string): Promise<string> {
const { stdout } = await execFileAsync('osascript', ['-l', 'JavaScript', '-e', script], {
encoding: 'utf-8',
})
return stdout.trim()
const result = await $`osascript -l JavaScript -e ${script}`.quiet().nothrow().text()
return result.trim()
}
function buildMouseJxa(eventType: string, x: number, y: number, btn: number, clickState?: number): string {
@@ -122,14 +115,19 @@ export const typeText: InputBackend['typeText'] = async (text) => {
export const getFrontmostAppInfo: InputBackend['getFrontmostAppInfo'] = () => {
try {
const output = execFileSync('osascript', ['-e', `
tell application "System Events"
set frontApp to first application process whose frontmost is true
set appName to name of frontApp
set bundleId to bundle identifier of frontApp
return bundleId & "|" & appName
end tell
`], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim()
const result = Bun.spawnSync({
cmd: ['osascript', '-e', `
tell application "System Events"
set frontApp to first application process whose frontmost is true
set appName to name of frontApp
set bundleId to bundle identifier of frontApp
return bundleId & "|" & appName
end tell
`],
stdout: 'pipe',
stderr: 'pipe',
})
const output = new TextDecoder().decode(result.stdout).trim()
if (!output || !output.includes('|')) return null
const [bundleId, appName] = output.split('|', 2)
return { bundleId: bundleId!, appName: appName! }

View File

@@ -1,5 +0,0 @@
{
"extends": "../../../tsconfig.base.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -1,5 +0,0 @@
{
"extends": "../../../tsconfig.base.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -274,9 +274,4 @@ export const screenshot: ScreenshotAPI = {
if (displayId !== undefined) args.push('-D', String(displayId))
return captureScreenToBase64(args)
},
captureWindowTarget(_titleOrHwnd: string | number): ScreenshotResult | null {
// Window capture not supported on macOS via this backend
return null
},
}

View File

@@ -275,9 +275,4 @@ export const screenshot: ScreenshotAPI = {
return { base64: '', width: 0, height: 0 }
}
},
captureWindowTarget(_titleOrHwnd: string | number): ScreenshotResult | null {
// Window capture not supported on Linux via this backend
return null
},
}

View File

@@ -76,7 +76,6 @@ export interface ScreenshotAPI {
x: number, y: number, w: number, h: number,
outW: number, outH: number, quality: number, displayId?: number,
): Promise<ScreenshotResult>
captureWindowTarget(titleOrHwnd: string | number): ScreenshotResult | null
}
export interface SwiftBackend {

View File

@@ -1,5 +0,0 @@
{
"extends": "../../../tsconfig.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -1,5 +0,0 @@
{
"extends": "../../../tsconfig.base.json",
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -1,18 +0,0 @@
{
"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"
}
}

View File

@@ -1,27 +0,0 @@
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 }

View File

@@ -1,35 +0,0 @@
/**
* 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
}

View File

@@ -1,238 +0,0 @@
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
}

View File

@@ -1,27 +0,0 @@
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 }

View File

@@ -1,48 +0,0 @@
/**
* 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
}

View File

@@ -1,63 +0,0 @@
// @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'

View File

@@ -1,54 +0,0 @@
// 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'

View File

@@ -1,6 +0,0 @@
// Type definitions for @ant/model-provider
export * from './message.js'
export * from './usage.js'
export * from './errors.js'
export * from './systemPrompt.js'

View File

@@ -1,129 +0,0 @@
// 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' }

View File

@@ -1,10 +0,0 @@
// 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
}

View File

@@ -1,49 +0,0 @@
// 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',
}

View File

@@ -1,7 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true
},
"include": ["src/**/*.ts", "src/**/*.tsx"]
}

View File

@@ -1,6 +1,6 @@
import { describe, expect, test } from 'bun:test'
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', () => {
test('CoreTool structural compatibility with host Tool', () => {
@@ -27,7 +27,7 @@ describe('agent-tools compatibility', () => {
}
// This assignment should work if HostTool structurally extends CoreTool
const coreTool: CoreTool = mockHostTool as unknown as CoreTool
const coreTool: CoreTool = mockHostTool as CoreTool
expect(coreTool.name).toBe('test')
expect(coreTool.isEnabled()).toBe(true)
})

View File

@@ -1,5 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -1,5 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -7,17 +7,6 @@ mock.module("src/utils/model/agent.js", () => ({
mock.module("src/utils/settings/constants.js", () => ({
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 {

View File

@@ -87,7 +87,7 @@ mock.module("src/tasks/LocalAgentTask/LocalAgentTask.js", () => ({
updateProgressFromMessage: noop,
}));
mock.module("src/utils/debug.ts", () => ({
mock.module("src/utils/debug.js", () => ({
getMinDebugLogLevel: () => "warn",
isDebugMode: () => false,
enableDebugLogging: () => false,

View File

@@ -1,4 +1,11 @@
import { describe, expect, test } from "bun:test";
import { mock, 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");

View File

@@ -1,10 +1,19 @@
import { mock, describe, expect, test } from "bun:test";
mock.module("src/utils/debug.ts", () => ({
mock.module("src/utils/debug.js", () => ({
logForDebugging: () => {},
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 {
formatGoToDefinitionResult,
formatFindReferencesResult,

View File

@@ -7,18 +7,6 @@ mock.module("src/utils/cwd.js", () => ({
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");
describe("isGitInternalPathPS", () => {

View File

@@ -32,58 +32,6 @@ 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
const { powershellCommandIsSafe } = await import("../powershellSecurity.js");

View File

@@ -3,11 +3,8 @@ import { z } from 'zod/v4'
import type { ToolResultBlockParam } from 'src/Tool.js'
import { buildTool } from 'src/Tool.js'
import { lazySchema } from 'src/utils/lazySchema.js'
import { notifyAutomationStateChanged } from 'src/utils/sessionState.js'
import { SLEEP_TOOL_NAME, DESCRIPTION, SLEEP_TOOL_PROMPT } from './prompt.js'
const SLEEP_WAKE_CHECK_INTERVAL_MS = 500
const inputSchema = lazySchema(() =>
z.strictObject({
duration_seconds: z
@@ -22,36 +19,6 @@ type SleepInput = z.infer<InputSchema>
type SleepOutput = { slept_seconds: number; interrupted: boolean }
function isProactiveAutomationEnabled(): boolean {
if (!(feature('PROACTIVE') || feature('KAIROS'))) {
return false
}
const mod =
require('src/proactive/index.js') as typeof import('src/proactive/index.js')
return mod.isProactiveActive()
}
function isProactiveSleepAllowed(): boolean {
if (!(feature('PROACTIVE') || feature('KAIROS'))) {
return true
}
const mod =
require('src/proactive/index.js') as typeof import('src/proactive/index.js')
return mod.isProactiveActive()
}
function hasQueuedWakeSignal(): boolean {
const queue =
require('src/utils/messageQueueManager.js') as typeof import('src/utils/messageQueueManager.js')
return queue.hasCommandsInQueue()
}
function shouldInterruptSleep(): boolean {
return !isProactiveSleepAllowed() || hasQueuedWakeSignal()
}
export const SleepTool = buildTool({
name: SLEEP_TOOL_NAME,
searchHint: 'wait pause sleep rest idle duration timer',
@@ -75,9 +42,6 @@ export const SleepTool = buildTool({
isReadOnly() {
return true
},
interruptBehavior() {
return 'cancel'
},
userFacingName() {
return SLEEP_TOOL_NAME
@@ -103,84 +67,53 @@ export const SleepTool = buildTool({
},
async call(input: SleepInput, context) {
// Don't enter sleep if proactive was disabled or new work arrived while
// the model was deciding to wait.
if (shouldInterruptSleep()) {
return {
data: {
slept_seconds: 0,
interrupted: true,
},
// Refuse to sleep when proactive mode is off — prevents the model from
// re-issuing Sleep after an interruption caused by /proactive disable.
if (feature('PROACTIVE') || feature('KAIROS')) {
const mod =
require('src/proactive/index.js') as typeof import('src/proactive/index.js')
if (!mod.isProactiveActive()) {
return {
data: {
slept_seconds: 0,
interrupted: true,
},
}
}
}
const { duration_seconds } = input
const startTime = Date.now()
const sleepUntil = startTime + duration_seconds * 1000
if (isProactiveAutomationEnabled()) {
notifyAutomationStateChanged({
enabled: true,
phase: 'sleeping',
next_tick_at: null,
sleep_until: sleepUntil,
})
}
try {
await new Promise<void>((resolve, reject) => {
let timer: ReturnType<typeof setTimeout> | null = null
let wakeCheck: ReturnType<typeof setInterval> | null = null
let settled = false
const cleanup = () => {
if (timer !== null) {
clearTimeout(timer)
timer = null
}
if (wakeCheck !== null) {
clearInterval(wakeCheck)
wakeCheck = null
}
context.abortController.signal.removeEventListener('abort', onAbort)
}
const finish = () => {
if (settled) return
settled = true
cleanup()
resolve()
}
const interrupt = () => {
if (settled) return
settled = true
cleanup()
reject(new Error('interrupted'))
}
const onAbort = () => {
interrupt()
}
timer = setTimeout(finish, duration_seconds * 1000)
const timer = setTimeout(resolve, duration_seconds * 1000)
// Abort via user interrupt
if (context.abortController.signal.aborted) {
interrupt()
return
}
context.abortController.signal.addEventListener('abort', onAbort, {
once: true,
})
context.abortController.signal.addEventListener(
'abort',
() => {
clearTimeout(timer)
clearInterval(proactiveCheck)
reject(new Error('interrupted'))
},
{ once: true },
)
// Poll proactive state and the shared command queue so new work can
// wake Sleep without waiting for the full duration.
wakeCheck = setInterval(() => {
if (shouldInterruptSleep()) {
interrupt()
}
}, SLEEP_WAKE_CHECK_INTERVAL_MS)
// Poll proactive state — if deactivated mid-sleep, interrupt early
// so the user doesn't have to wait for the full duration.
const proactiveCheck =
feature('PROACTIVE') || feature('KAIROS')
? setInterval(() => {
const mod =
require('src/proactive/index.js') as typeof import('src/proactive/index.js')
if (!mod.isProactiveActive()) {
clearTimeout(timer)
clearInterval(proactiveCheck)
reject(new Error('interrupted'))
}
}, 500)
: (null as unknown as ReturnType<typeof setInterval>)
})
return {
data: {
@@ -196,17 +129,6 @@ export const SleepTool = buildTool({
interrupted: true,
},
}
} finally {
notifyAutomationStateChanged(
isProactiveAutomationEnabled()
? {
enabled: true,
phase: null,
next_tick_at: null,
sleep_until: null,
}
: null,
)
}
},
})

View File

@@ -1,41 +0,0 @@
import { beforeEach, describe, expect, test } from 'bun:test'
import { SleepTool } from '../SleepTool'
import {
enqueue,
getCommandQueue,
resetCommandQueue,
} from 'src/utils/messageQueueManager.js'
describe('SleepTool', () => {
beforeEach(() => {
resetCommandQueue()
})
test('declares cancel interrupt behavior', () => {
expect(SleepTool.interruptBehavior()).toBe('cancel')
})
test('wakes early when queued work arrives', async () => {
const sleepPromise = SleepTool.call(
{ duration_seconds: 10 },
{ abortController: new AbortController() } as any,
)
setTimeout(() => {
enqueue({
value: 'wake up',
mode: 'prompt',
})
}, 20)
const result = await sleepPromise
expect(result.data.interrupted).toBe(true)
expect(result.data.slept_seconds).toBeLessThan(10)
expect(getCommandQueue()).toHaveLength(1)
expect(getCommandQueue()[0]).toMatchObject({
value: 'wake up',
mode: 'prompt',
})
})
})

View File

@@ -5,8 +5,6 @@ let isFirstPartyBaseUrl = true
// Only mock the external dependency that controls adapter selection
mock.module('src/utils/model/providers.js', () => ({
isFirstPartyAnthropicBaseUrl: () => isFirstPartyBaseUrl,
getAPIProvider: () => 'firstParty',
getAPIProviderForStatsig: () => 'firstParty',
}))
const { createAdapter } = await import('../adapters/index')

View File

@@ -1,14 +1,4 @@
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'
// ---------------------------------------------------------------------------

View File

@@ -1,17 +1,5 @@
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 originalBraveApiKey = process.env.BRAVE_API_KEY

View File

@@ -1,5 +0,0 @@
{
"extends": "../../tsconfig.json",
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -72,18 +72,18 @@ describe("detectColorMode", () => {
describe("detectLanguage", () => {
test("detects language from file extension", () => {
expect(detectLanguage("index.ts", null)).toBe("ts");
expect(detectLanguage("main.py", null)).toBe("py");
expect(detectLanguage("style.css", null)).toBe("css");
expect(detectLanguage("index.ts")).toBe("ts");
expect(detectLanguage("main.py")).toBe("py");
expect(detectLanguage("style.css")).toBe("css");
});
test("detects language from known filenames", () => {
expect(detectLanguage("Makefile", null)).toBe("makefile");
expect(detectLanguage("Dockerfile", null)).toBe("dockerfile");
expect(detectLanguage("Makefile")).toBe("makefile");
expect(detectLanguage("Dockerfile")).toBe("dockerfile");
});
test("returns null for unknown extensions", () => {
expect(detectLanguage("file.xyz123", null)).toBeNull();
expect(detectLanguage("file.xyz123")).toBeNull();
});
});

View File

@@ -1,5 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -1,5 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -38,7 +38,7 @@ describe('InProcessTransport', () => {
let received: JSONRPCMessage | null = null
client.onmessage = (msg) => { received = msg }
await server.send({ jsonrpc: '2.0', result: 42, id: 1 } as any)
await server.send({ jsonrpc: '2.0', result: 42, id: 1 })
await new Promise(resolve => setTimeout(resolve, 10))

View File

@@ -57,9 +57,9 @@ describe('discoverTools', () => {
expect(tool.name).toBe('mcp__my-server__search')
expect(tool.mcpInfo).toEqual({ serverName: 'my-server', toolName: 'search' })
expect(tool.isMcp).toBe(true)
expect(tool.isReadOnly({} as any)).toBe(true)
expect(tool.userFacingName(undefined)).toBe('Search Items')
expect(await tool.description({} as any, { isNonInteractiveSession: false, toolPermissionContext: {}, tools: [] })).toBe('Search for items')
expect(tool.isReadOnly()).toBe(true)
expect(tool.userFacingName()).toBe('Search Items')
expect(await tool.description()).toBe('Search for items')
})
test('respects skipPrefix option', async () => {

View File

@@ -65,7 +65,7 @@ describe('createMcpManager', () => {
const result = await manager.connect('test-server', { command: 'npx', args: [] })
expect(result.type).toBe('connected')
expect(connectedEvent as unknown as string).toBe('test-server')
expect(connectedEvent).toBe('test-server')
})
test('disconnect calls cleanup and emits disconnected', async () => {

View File

@@ -1,5 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -1,5 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -25,18 +25,17 @@ import {
storeUpdateSession,
storeGetEnvironment,
storeGetSession,
storeListActiveEnvironments,
} from "../store";
import { getEventBus, getAllEventBuses, removeEventBus } from "../transport/event-bus";
import { runDisconnectMonitorSweep } from "../services/disconnect-monitor";
describe("Disconnect Monitor Logic", () => {
beforeEach(() => {
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", () => {
const env = storeCreateEnvironment({ secret: "s" });
const timeoutMs = 300 * 1000; // 5 minutes
@@ -45,7 +44,14 @@ describe("Disconnect Monitor Logic", () => {
const oldDate = new Date(Date.now() - timeoutMs - 60000);
storeUpdateEnvironment(env.id, { lastPollAt: oldDate });
runDisconnectMonitorSweep();
// Check the timeout logic (same as in disconnect-monitor.ts)
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);
expect(updated?.status).toBe("disconnected");
@@ -53,56 +59,43 @@ describe("Disconnect Monitor Logic", () => {
test("environment stays active when lastPollAt is recent", () => {
const env = storeCreateEnvironment({ secret: "s" });
runDisconnectMonitorSweep();
const timeoutMs = 300 * 1000;
// 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);
expect(updated?.status).toBe("active");
});
test("session becomes inactive when updatedAt is too old", () => {
const session = storeCreateSession({});
const session = storeCreateSession({ status: "idle" });
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);
expect(rec).toBeTruthy();
if (!rec) return;
rec.updatedAt = new Date(Date.now() - 300 * 1000 * 2 - 60000);
runDisconnectMonitorSweep();
const updated = storeGetSession(session.id);
expect(updated?.status).toBe("inactive");
// Session was just updated, should not be inactive
expect(rec?.status).toBe("running");
expect(now - rec!.updatedAt.getTime()).toBeLessThan(timeoutMs);
});
test("session stays running when recently updated", () => {
const session = storeCreateSession({});
storeUpdateSession(session.id, { status: "running" });
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 timeoutMs = 300 * 1000 * 2;
const rec = storeGetSession(session.id);
expect(rec).toBeTruthy();
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" },
});
expect(rec?.status).toBe("running");
expect(Date.now() - rec!.updatedAt.getTime()).toBeLessThan(timeoutMs);
});
});

View File

@@ -19,18 +19,16 @@ mock.module("../config", () => ({
import { Hono } from "hono";
import { storeReset, storeCreateSession, storeCreateEnvironment, storeBindSession } from "../store";
import { removeEventBus, getAllEventBuses, getEventBus } from "../transport/event-bus";
import { removeEventBus, getAllEventBuses } from "../transport/event-bus";
import { issueToken } from "../auth/token";
import { publishSessionEvent } from "../services/transport";
// Import route modules
import v1Sessions from "../routes/v1/sessions";
import v1Environments from "../routes/v1/environments";
import v1EnvironmentsWork from "../routes/v1/environments.work";
import v1SessionIngress, { websocket as sessionIngressWebsocket } from "../routes/v1/session-ingress";
import v1SessionIngress from "../routes/v1/session-ingress";
import v2CodeSessions from "../routes/v2/code-sessions";
import v2Worker from "../routes/v2/worker";
import v2WorkerEventsStream from "../routes/v2/worker-events-stream";
import v2WorkerEvents from "../routes/v2/worker-events";
import webAuth from "../routes/web/auth";
import webSessions from "../routes/web/sessions";
@@ -45,7 +43,6 @@ function createApp() {
app.route("/v2/session_ingress", v1SessionIngress);
app.route("/v1/code/sessions", v2CodeSessions);
app.route("/v1/code/sessions", v2Worker);
app.route("/v1/code/sessions", v2WorkerEventsStream);
app.route("/v1/code/sessions", v2WorkerEvents);
app.route("/web", webAuth);
app.route("/web", webSessions);
@@ -56,11 +53,6 @@ function createApp() {
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", () => {
let app: Hono;
@@ -117,24 +109,6 @@ describe("V1 Session Routes", () => {
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 () => {
const createRes = await app.request("/v1/sessions", {
method: "POST",
@@ -168,32 +142,6 @@ describe("V1 Session Routes", () => {
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 () => {
const createRes = await app.request("/v1/sessions", {
method: "POST",
@@ -212,30 +160,6 @@ describe("V1 Session Routes", () => {
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 () => {
// First register an environment
const envRes = await app.request("/v1/environments/bridge", {
@@ -519,26 +443,6 @@ describe("Web Auth Routes", () => {
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 () => {
const res = await app.request("/web/bind?uuid=test-uuid", {
method: "POST",
@@ -597,24 +501,6 @@ describe("Web Session Routes", () => {
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 () => {
const res = await app.request("/web/sessions");
expect(res.status).toBe(401);
@@ -639,33 +525,6 @@ describe("Web Session Routes", () => {
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 () => {
const createRes = await app.request("/web/sessions?uuid=user-1", {
method: "POST",
@@ -678,44 +537,6 @@ describe("Web Session Routes", () => {
expect(getRes.status).toBe(200);
});
test("GET /web/sessions/:id — includes automation_state snapshot when worker metadata has it", 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();
storeBindSession(id, "user-1");
await app.request(`/v1/code/sessions/${id}/worker`, {
method: "PUT",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({
worker_epoch: 1,
external_metadata: {
automation_state: {
enabled: true,
phase: "standby",
next_tick_at: 123456,
sleep_until: null,
},
},
}),
});
const getRes = await app.request(`/web/sessions/${toWebSessionId(id)}?uuid=user-1`);
expect(getRes.status).toBe(200);
const body = await getRes.json();
expect(body.automation_state).toEqual({
enabled: true,
phase: "standby",
next_tick_at: 123456,
sleep_until: null,
});
});
test("GET /web/sessions/:id — 403 for non-owner", async () => {
const createRes = await app.request("/web/sessions?uuid=user-1", {
method: "POST",
@@ -742,51 +563,6 @@ describe("Web Session Routes", () => {
expect(body.events).toEqual([]);
});
test("GET /web/sessions/:id/history — returns task_state snapshots", 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();
publishSessionEvent(
id,
"task_state",
{
task_list_id: "team-alpha",
tasks: [{ id: "1", subject: "Investigate", status: "pending" }],
},
"inbound",
);
const histRes = await app.request(`/web/sessions/${id}/history?uuid=user-1`);
expect(histRes.status).toBe(200);
const body = await histRes.json();
expect(body.events).toHaveLength(1);
expect(body.events[0]?.type).toBe("task_state");
expect(body.events[0]?.payload.task_list_id).toBe("team-alpha");
expect(body.events[0]?.payload.tasks).toEqual([
{ id: "1", subject: "Investigate", status: "pending" },
]);
});
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 () => {
const createRes = await app.request("/web/sessions?uuid=user-1", {
method: "POST",
@@ -871,24 +647,6 @@ 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 () => {
const createRes = await app.request("/web/sessions?uuid=user-1", {
method: "POST",
@@ -900,25 +658,6 @@ describe("Web Session Routes", () => {
const eventsRes = await app.request(`/web/sessions/${id}/events?uuid=user-2`);
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", () => {
@@ -953,32 +692,6 @@ describe("Web Control Routes", () => {
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 () => {
const res = await app.request(`/web/sessions/${sessionId}/events?uuid=user-2`, {
method: "POST",
@@ -1030,33 +743,6 @@ describe("Web Control Routes", () => {
});
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", () => {
@@ -1136,81 +822,6 @@ describe("V1 Session Ingress Routes (HTTP)", () => {
});
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", () => {
@@ -1245,252 +856,6 @@ describe("V2 Worker Events Routes", () => {
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",
automation_state: {
enabled: true,
phase: "sleeping",
next_tick_at: null,
sleep_until: 123456,
},
},
}),
});
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");
expect(body.worker.external_metadata.automation_state).toEqual({
enabled: true,
phase: "sleeping",
next_tick_at: null,
sleep_until: 123456,
});
const events = getEventBus(id).getEventsSince(0);
expect(events.some((event) => event.type === "automation_state")).toBe(true);
expect(events.at(-1)?.payload).toEqual({
enabled: true,
phase: "sleeping",
next_tick_at: null,
sleep_until: 123456,
});
});
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("GET /v1/code/sessions/:id/worker/events/stream — normalizes web permission approvals to control_response", 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();
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;
await reader.read(); // initial keepalive
const controlRes = await app.request(`/web/sessions/${id}/control?uuid=user-1`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
type: "permission_response",
approved: true,
request_id: "req-1",
}),
});
expect(controlRes.status).toBe(200);
const chunk = await reader.read();
const frame = new TextDecoder().decode(chunk.value!);
expect(frame).toContain("event: client_event");
expect(frame).toContain("\"event_type\":\"permission_response\"");
expect(frame).toContain("\"payload\":{\"type\":\"control_response\"");
expect(frame).toContain("\"request_id\":\"req-1\"");
expect(frame).toContain("\"behavior\":\"allow\"");
reader.cancel();
});
test("GET /v1/code/sessions/:id/worker/events/stream — normalizes web plan rejection feedback to deny control_response", 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();
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;
await reader.read(); // initial keepalive
const controlRes = await app.request(`/web/sessions/${id}/control?uuid=user-1`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
type: "permission_response",
approved: false,
request_id: "req-2",
message: "Need more detail",
}),
});
expect(controlRes.status).toBe(200);
const chunk = await reader.read();
const frame = new TextDecoder().decode(chunk.value!);
expect(frame).toContain("event: client_event");
expect(frame).toContain("\"event_type\":\"permission_response\"");
expect(frame).toContain("\"payload\":{\"type\":\"control_response\"");
expect(frame).toContain("\"request_id\":\"req-2\"");
expect(frame).toContain("\"subtype\":\"error\"");
expect(frame).toContain("\"behavior\":\"deny\"");
expect(frame).toContain("\"message\":\"Need more detail\"");
reader.cancel();
});
test("GET /v1/code/sessions/:id/worker/events/stream — normalizes web interrupts to control_request", 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();
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;
await reader.read(); // initial keepalive
const interruptRes = await app.request(`/web/sessions/${id}/interrupt?uuid=user-1`, {
method: "POST",
headers: { "Content-Type": "application/json" },
});
expect(interruptRes.status).toBe(200);
const chunk = await reader.read();
const frame = new TextDecoder().decode(chunk.value!);
expect(frame).toContain("event: client_event");
expect(frame).toContain("\"event_type\":\"interrupt\"");
expect(frame).toContain("\"payload\":{\"type\":\"control_request\"");
expect(frame).toContain("\"subtype\":\"interrupt\"");
reader.cancel();
});
test("PUT /v1/code/sessions/:id/worker/state — updates session status", async () => {
const sessRes = await app.request("/v1/sessions", {
method: "POST",
@@ -1538,20 +903,4 @@ describe("V2 Worker Events Routes", () => {
});
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);
});
});

View File

@@ -345,22 +345,6 @@ describe("Transport Service", () => {
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("preserves isSynthetic field", () => {
const result = normalizePayload("user", {
content: "scheduled job: refresh analytics cache",
isSynthetic: true,
});
expect(result.isSynthetic).toBe(true);
});
test("uses name as tool_name fallback", () => {
const result = normalizePayload("tool", { name: "Read" });
expect(result.tool_name).toBe("Read");
@@ -378,28 +362,6 @@ describe("Transport Service", () => {
expect(result.content).toBe("");
});
test("preserves task_state fields", () => {
const result = normalizePayload("task_state", {
task_list_id: "team-alpha",
tasks: [{ id: "1", subject: "Task 1", status: "pending" }],
});
expect(result.task_list_id).toBe("team-alpha");
expect(result.tasks).toEqual([
{ id: "1", subject: "Task 1", status: "pending" },
]);
});
test("preserves status metadata for conversation reset events", () => {
const result = normalizePayload("status", {
status: "conversation_cleared",
subtype: "status",
message: "conversation_cleared",
});
expect(result.status).toBe("conversation_cleared");
expect(result.subtype).toBe("status");
expect(result.message).toBe("conversation_cleared");
});
test("handles undefined payload", () => {
const result = normalizePayload("user", undefined);
expect(result.content).toBe("");

View File

@@ -69,19 +69,6 @@ describe("ws-handler", () => {
expect((events[0] as any).direction).toBe("inbound");
});
test("preserves synthetic flag on inbound user messages", () => {
const bus = getEventBus("s1");
const events: unknown[] = [];
bus.subscribe((e) => events.push(e));
ingestBridgeMessage("s1", {
message: { role: "user", content: "scheduled job: refresh analytics cache" },
uuid: "u_synth",
isSynthetic: true,
});
expect(events).toHaveLength(1);
expect((events[0] as any).payload.isSynthetic).toBe(true);
});
test("derives type from message.role for assistant messages", () => {
const bus = getEventBus("s1");
const events: unknown[] = [];
@@ -176,24 +163,6 @@ describe("ws-handler", () => {
expect(msg.type).toBe("user");
});
test("replays synthetic user metadata back to the bridge", () => {
const bus = getEventBus("s3");
bus.publish({
id: "e1",
sessionId: "s3",
type: "user",
payload: { content: "scheduled job: refresh analytics cache", isSynthetic: true },
direction: "outbound",
});
const ws = createMockWs();
handleWebSocketOpen(ws, "s3");
const msg = JSON.parse(ws.getSentData()[0]);
expect(msg.type).toBe("user");
expect(msg.isSynthetic).toBe(true);
});
test("replaces existing connection for same session", () => {
const ws1 = createMockWs();
const ws2 = createMockWs();
@@ -367,26 +336,6 @@ describe("ws-handler", () => {
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", () => {
const bus = getEventBus("gen1");
const ws = createMockWs();

View File

@@ -14,14 +14,14 @@ app.post("/bridge", acceptCliHeaders, apiKeyAuth, async (c) => {
/** DELETE /v1/environments/bridge/:id — Deregister */
app.delete("/bridge/:id", acceptCliHeaders, apiKeyAuth, async (c) => {
const envId = c.req.param("id")!;
const envId = c.req.param("id");
deregisterEnvironment(envId);
return c.json({ status: "ok" }, 200);
});
/** POST /v1/environments/:id/bridge/reconnect — Reconnect */
app.post("/:id/bridge/reconnect", acceptCliHeaders, apiKeyAuth, async (c) => {
const envId = c.req.param("id")!;
const envId = c.req.param("id");
reconnectEnvironment(envId);
const { reconnectWorkForEnvironment } = await import("../../services/work-dispatch");
await reconnectWorkForEnvironment(envId);

View File

@@ -7,7 +7,7 @@ const app = new Hono();
/** GET /v1/environments/:id/work/poll — Long-poll for work */
app.get("/:id/work/poll", acceptCliHeaders, apiKeyAuth, async (c) => {
const envId = c.req.param("id")!;
const envId = c.req.param("id");
updatePollTime(envId);
const result = await pollWork(envId);
if (!result) {
@@ -19,21 +19,21 @@ app.get("/:id/work/poll", acceptCliHeaders, apiKeyAuth, async (c) => {
/** POST /v1/environments/:id/work/:workId/ack — Acknowledge work */
app.post("/:id/work/:workId/ack", acceptCliHeaders, apiKeyAuth, async (c) => {
const workId = c.req.param("workId")!;
const workId = c.req.param("workId");
ackWork(workId);
return c.json({ status: "ok" }, 200);
});
/** POST /v1/environments/:id/work/:workId/stop — Stop work */
app.post("/:id/work/:workId/stop", acceptCliHeaders, apiKeyAuth, async (c) => {
const workId = c.req.param("workId")!;
const workId = c.req.param("workId");
stopWork(workId);
return c.json({ status: "ok" }, 200);
});
/** POST /v1/environments/:id/work/:workId/heartbeat — Heartbeat */
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);
return c.json(result, 200);
});

View File

@@ -9,7 +9,7 @@ import {
handleWebSocketClose,
ingestBridgeMessage,
} from "../../transport/ws-handler";
import { getSession, resolveExistingSessionId } from "../../services/session";
import { getSession } from "../../services/session";
const { upgradeWebSocket, websocket } = createBunWebSocket();
@@ -44,8 +44,7 @@ function authenticateRequest(c: any, label: string, expectedSessionId?: string):
/** POST /v2/session_ingress/session/:sessionId/events — HTTP POST (HybridTransport writes) */
app.post("/session/:sessionId/events", async (c) => {
const requestedSessionId = c.req.param("sessionId")!;
const sessionId = resolveExistingSessionId(requestedSessionId) ?? requestedSessionId;
const sessionId = c.req.param("sessionId")!;
if (!authenticateRequest(c, `POST session/${sessionId}`, sessionId)) {
return c.json({ error: { type: "unauthorized", message: "Invalid auth" } }, 401);
@@ -73,8 +72,7 @@ app.post("/session/:sessionId/events", async (c) => {
app.get(
"/ws/:sessionId",
upgradeWebSocket(async (c) => {
const requestedSessionId = c.req.param("sessionId")!;
const sessionId = resolveExistingSessionId(requestedSessionId) ?? requestedSessionId;
const sessionId = c.req.param("sessionId")!;
if (!authenticateRequest(c, `WS ${sessionId}`, sessionId)) {
return {

View File

@@ -5,7 +5,6 @@ import {
getSession,
updateSessionTitle,
archiveSession,
resolveExistingSessionId,
} from "../../services/session";
import { createWorkItem } from "../../services/work-dispatch";
import { apiKeyAuth, acceptCliHeaders } from "../../auth/middleware";
@@ -40,8 +39,7 @@ app.post("/", acceptCliHeaders, apiKeyAuth, async (c) => {
/** GET /v1/sessions/:id — Get session */
app.get("/:id", acceptCliHeaders, apiKeyAuth, async (c) => {
const sessionId = resolveExistingSessionId(c.req.param("id")!) ?? c.req.param("id")!;
const session = getSession(sessionId);
const session = getSession(c.req.param("id"));
if (!session) {
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
}
@@ -50,43 +48,27 @@ app.get("/:id", acceptCliHeaders, apiKeyAuth, async (c) => {
/** PATCH /v1/sessions/:id — Update session title */
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();
if (body.title) {
updateSessionTitle(sessionId, body.title);
updateSessionTitle(c.req.param("id"), body.title);
}
const session = getSession(sessionId);
const session = getSession(c.req.param("id"));
return c.json(session, 200);
});
/** POST /v1/sessions/:id/archive — Archive session */
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 {
archiveSession(sessionId);
archiveSession(c.req.param("id"));
} catch {
return c.json({ status: "ok" }, 409);
}
return c.json({ status: "ok" }, 200);
});
/** POST /v1/sessions/:id/events — Send event to session */
app.post("/:id/events", 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);
}
const sessionId = c.req.param("id");
const body = await c.req.json();
const events = body.events

View File

@@ -15,7 +15,7 @@ app.post("/", acceptCliHeaders, apiKeyAuth, async (c) => {
/** POST /v1/code/sessions/:id/bridge — Get connection info + worker JWT */
app.post("/:id/bridge", acceptCliHeaders, apiKeyAuth, async (c) => {
const sessionId = c.req.param("id")!;
const sessionId = c.req.param("id");
const session = getSession(sessionId);
if (!session) {
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);

View File

@@ -1,13 +1,13 @@
import { Hono } from "hono";
import { sessionIngressAuth, acceptCliHeaders } from "../../auth/middleware";
import { createWorkerEventStream } from "../../transport/sse-writer";
import { createSSEStream } from "../../transport/sse-writer";
import { getSession } from "../../services/session";
const app = new Hono();
/** SSE /v1/code/sessions/:id/worker/events/stream — SSE event stream */
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);
if (!session) {
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 fromSeqNum = fromSeq ? parseInt(fromSeq) : lastEventId ? parseInt(lastEventId) : 0;
return createWorkerEventStream(c, sessionId, fromSeqNum);
return createSSEStream(c, sessionId, fromSeqNum);
});
export default app;

View File

@@ -1,66 +1,32 @@
import { Hono } from "hono";
import { sessionIngressAuth, acceptCliHeaders } from "../../auth/middleware";
import { publishSessionEvent } from "../../services/transport";
import { getSession, touchSession, updateSessionStatus } from "../../services/session";
import { getSession, updateSessionStatus } from "../../services/session";
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 */
app.post("/:id/worker/events", 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);
}
const sessionId = c.req.param("id");
const body = await c.req.json();
const events = extractWorkerEvents(body);
const events = Array.isArray(body) ? body : [body];
const published = [];
for (const evt of events) {
const eventType = typeof evt.type === "string" ? evt.type : "message";
const result = publishSessionEvent(sessionId, eventType, evt, "inbound");
const result = publishSessionEvent(sessionId, evt.type || "message", evt, "inbound");
published.push(result);
}
touchSession(sessionId);
return c.json({ status: "ok", count: published.length }, 200);
});
/** PUT /v1/code/sessions/:id/worker/state — Report worker state */
app.put("/:id/worker/state", 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);
}
const sessionId = c.req.param("id");
const body = await c.req.json();
if (body.status) {
updateSessionStatus(sessionId, body.status);
} else {
touchSession(sessionId);
}
return c.json({ status: "ok" }, 200);
@@ -68,29 +34,12 @@ app.put("/:id/worker/state", acceptCliHeaders, sessionIngressAuth, async (c) =>
/** PUT /v1/code/sessions/:id/worker/external_metadata — Report worker metadata (no-op) */
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.
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) */
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).
// Accept and discard — event bus doesn't track per-event delivery.
return c.json({ status: "ok" }, 200);

View File

@@ -1,98 +1,12 @@
import { Hono } from "hono";
import { getSession, incrementEpoch, touchSession, updateSessionStatus } from "../../services/session";
import {
automationStatesEqual,
getAutomationStateEventPayload,
} from "../../services/automationState";
import { apiKeyAuth, acceptCliHeaders, sessionIngressAuth } from "../../auth/middleware";
import { getEventBus } from "../../transport/event-bus";
import { storeGetSessionWorker, storeUpsertSessionWorker } from "../../store";
import { v4 as uuid } from "uuid";
import { getSession, incrementEpoch } from "../../services/session";
import { apiKeyAuth, acceptCliHeaders } from "../../auth/middleware";
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();
const prevAutomationState = getAutomationStateEventPayload(
storeGetSessionWorker(sessionId)?.externalMetadata,
);
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,
});
const nextAutomationState = getAutomationStateEventPayload(worker.externalMetadata);
if (!automationStatesEqual(prevAutomationState, nextAutomationState)) {
getEventBus(sessionId).publish({
id: uuid(),
sessionId,
type: "automation_state",
payload: nextAutomationState,
direction: "inbound",
});
}
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 */
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);
if (!session) {
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);

View File

@@ -1,6 +1,5 @@
import { Hono } from "hono";
import { storeBindSession } from "../../store";
import { resolveExistingWebSessionId, toWebSessionId } from "../../services/session";
import { storeGetSession, storeBindSession } from "../../store";
const app = new Hono();
@@ -15,13 +14,13 @@ app.post("/bind", async (c) => {
return c.json({ error: "sessionId and uuid are required" }, 400);
}
const resolvedSessionId = resolveExistingWebSessionId(sessionId);
if (!resolvedSessionId) {
const session = storeGetSession(sessionId);
if (!session) {
return c.json({ error: "Session not found" }, 404);
}
storeBindSession(resolvedSessionId, uuid);
return c.json({ ok: true, sessionId: toWebSessionId(resolvedSessionId) });
storeBindSession(sessionId, uuid);
return c.json({ ok: true, sessionId });
});
export default app;

View File

@@ -1,47 +1,32 @@
import { log, error as logError } from "../../logger";
import { Hono } from "hono";
import { uuidAuth } from "../../auth/middleware";
import { getSession, isSessionClosedStatus, resolveOwnedWebSessionId, updateSessionStatus } from "../../services/session";
import { getSession, updateSessionStatus } from "../../services/session";
import { publishSessionEvent } from "../../services/transport";
import { getEventBus } from "../../transport/event-bus";
import { storeIsSessionOwner } from "../../store";
const app = new Hono();
type OwnershipCheckResult =
| { error: true }
| { error: true; reason: string }
| { 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 };
function checkOwnership(c: { get: (key: string) => string | undefined }, sessionId: string) {
const uuid = c.get("uuid");
if (!storeIsSessionOwner(sessionId, uuid)) {
return { error: true, session: null };
}
const session = getSession(resolvedSessionId);
const session = getSession(sessionId);
if (!session) {
return { error: true };
return { error: true, session: null };
}
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 } };
return { error: false, session };
}
/** POST /web/sessions/:id/events — Send user message to session */
app.post("/sessions/:id/events", uuidAuth, async (c) => {
const requestedSessionId = c.req.param("id")!;
const ownership = checkOwnership(c, requestedSessionId);
if (ownership.error) {
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 = c.req.param("id")!;
const { error } = checkOwnership(c, sessionId);
if (error) {
return c.json({ error: { type: "forbidden", message: "Not your session" } }, 403);
}
const { sessionId } = ownership;
const body = await c.req.json();
const eventType = body.type || "user";
@@ -53,14 +38,11 @@ app.post("/sessions/:id/events", uuidAuth, async (c) => {
/** POST /web/sessions/:id/control — Send control request (permission approval etc) */
app.post("/sessions/:id/control", uuidAuth, async (c) => {
const requestedSessionId = c.req.param("id")!;
const ownership = checkOwnership(c, requestedSessionId);
if (ownership.error) {
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 = c.req.param("id")!;
const { error } = checkOwnership(c, sessionId);
if (error) {
return c.json({ error: { type: "forbidden", message: "Not your session" } }, 403);
}
const { sessionId } = ownership;
const body = await c.req.json();
const event = publishSessionEvent(sessionId, body.type || "control_request", body, "outbound");
@@ -69,14 +51,11 @@ app.post("/sessions/:id/control", uuidAuth, async (c) => {
/** POST /web/sessions/:id/interrupt — Interrupt session */
app.post("/sessions/:id/interrupt", uuidAuth, async (c) => {
const requestedSessionId = c.req.param("id")!;
const ownership = checkOwnership(c, requestedSessionId);
if (ownership.error) {
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 = c.req.param("id")!;
const { error } = checkOwnership(c, sessionId);
if (error) {
return c.json({ error: { type: "forbidden", message: "Not your session" } }, 403);
}
const { sessionId } = ownership;
publishSessionEvent(sessionId, "interrupt", { action: "interrupt" }, "outbound");
updateSessionStatus(sessionId, "idle");

View File

@@ -1,18 +1,10 @@
import { log, error as logError } from "../../logger";
import { Hono } from "hono";
import { uuidAuth } from "../../auth/middleware";
import { getAutomationStateSnapshot } from "../../services/automationState";
import {
createSession,
getSession,
isSessionClosedStatus,
listWebSessionSummariesByOwnerUuid,
listWebSessionsByOwnerUuid,
resolveOwnedWebSessionId,
toWebSessionResponse,
} from "../../services/session";
import { storeBindSession, storeGetSessionWorker } from "../../store";
import { getSession, createSession } from "../../services/session";
import { storeListSessionsByOwnerUuid, storeIsSessionOwner, storeBindSession } from "../../store";
import { createWorkItem } from "../../services/work-dispatch";
import { listSessionSummariesByOwnerUuid } from "../../services/session";
import { createSSEStream } from "../../transport/sse-writer";
import { getEventBus } from "../../transport/event-bus";
@@ -20,7 +12,7 @@ const app = new Hono();
/** POST /web/sessions — Create a session from web UI */
app.post("/sessions", uuidAuth, async (c) => {
const uuid = c.get("uuid")!;
const uuid = c.get("uuid");
const body = await c.req.json();
const session = createSession({
environment_id: body.environment_id || null,
@@ -46,43 +38,37 @@ app.post("/sessions", uuidAuth, async (c) => {
/** GET /web/sessions — List sessions owned by the requesting UUID */
app.get("/sessions", uuidAuth, async (c) => {
const uuid = c.get("uuid")!;
const sessions = listWebSessionsByOwnerUuid(uuid);
const uuid = c.get("uuid");
const sessions = storeListSessionsByOwnerUuid(uuid);
return c.json(sessions, 200);
});
/** GET /web/sessions/all — List sessions owned by the requesting UUID (unowned sessions excluded) */
app.get("/sessions/all", uuidAuth, async (c) => {
const uuid = c.get("uuid")!;
const sessions = listWebSessionSummariesByOwnerUuid(uuid);
const uuid = c.get("uuid");
const sessions = listSessionSummariesByOwnerUuid(uuid);
return c.json(sessions, 200);
});
/** GET /web/sessions/:id — Session detail */
app.get("/sessions/:id", uuidAuth, async (c) => {
const uuid = c.get("uuid")!;
const sessionId = resolveOwnedWebSessionId(c.req.param("id")!, uuid);
if (!sessionId) {
const uuid = c.get("uuid");
const sessionId = c.req.param("id")!;
if (!storeIsSessionOwner(sessionId, uuid)) {
return c.json({ error: { type: "forbidden", message: "Not your session" } }, 403);
}
const session = getSession(sessionId);
if (!session) {
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
}
const worker = storeGetSessionWorker(sessionId);
const automationState = getAutomationStateSnapshot(worker?.externalMetadata);
const response = toWebSessionResponse(session);
return c.json(
automationState === undefined ? response : { ...response, automation_state: automationState },
200,
);
return c.json(session, 200);
});
/** GET /web/sessions/:id/history — Historical events for session */
app.get("/sessions/:id/history", uuidAuth, async (c) => {
const uuid = c.get("uuid")!;
const sessionId = resolveOwnedWebSessionId(c.req.param("id")!, uuid);
if (!sessionId) {
const uuid = c.get("uuid");
const sessionId = c.req.param("id")!;
if (!storeIsSessionOwner(sessionId, uuid)) {
return c.json({ error: { type: "forbidden", message: "Not your session" } }, 403);
}
const session = getSession(sessionId);
@@ -97,18 +83,15 @@ app.get("/sessions/:id/history", uuidAuth, async (c) => {
/** SSE /web/sessions/:id/events — Real-time event stream */
app.get("/sessions/:id/events", uuidAuth, async (c) => {
const uuid = c.get("uuid")!;
const sessionId = resolveOwnedWebSessionId(c.req.param("id")!, uuid);
if (!sessionId) {
const uuid = c.get("uuid");
const sessionId = c.req.param("id")!;
if (!storeIsSessionOwner(sessionId, uuid)) {
return c.json({ error: { type: "forbidden", message: "Not your session" } }, 403);
}
const session = getSession(sessionId);
if (!session) {
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 fromSeqNum = lastEventId ? parseInt(lastEventId) : 0;

View File

@@ -1,64 +0,0 @@
import type { AutomationStateResponse } from "../types/api";
const DISABLED_AUTOMATION_STATE: AutomationStateResponse = Object.freeze({
enabled: false,
phase: null,
next_tick_at: null,
sleep_until: null,
});
function cloneAutomationState(state: AutomationStateResponse): AutomationStateResponse {
return { ...state };
}
function normalizeAutomationState(raw: unknown): AutomationStateResponse {
if (!raw || typeof raw !== "object") {
return cloneAutomationState(DISABLED_AUTOMATION_STATE);
}
const state = raw as Record<string, unknown>;
return {
enabled: state.enabled === true,
phase: state.phase === "standby" || state.phase === "sleeping" ? state.phase : null,
next_tick_at: typeof state.next_tick_at === "number" ? state.next_tick_at : null,
sleep_until: typeof state.sleep_until === "number" ? state.sleep_until : null,
};
}
function readAutomationStateValue(metadata: Record<string, unknown> | null | undefined): unknown {
if (!metadata || typeof metadata !== "object") {
return undefined;
}
if (!Object.prototype.hasOwnProperty.call(metadata, "automation_state")) {
return undefined;
}
return metadata.automation_state;
}
export function getAutomationStateSnapshot(
metadata: Record<string, unknown> | null | undefined,
): AutomationStateResponse | undefined {
const raw = readAutomationStateValue(metadata);
if (raw === undefined) {
return undefined;
}
return normalizeAutomationState(raw);
}
export function getAutomationStateEventPayload(
metadata: Record<string, unknown> | null | undefined,
): AutomationStateResponse {
return getAutomationStateSnapshot(metadata) ?? cloneAutomationState(DISABLED_AUTOMATION_STATE);
}
export function automationStatesEqual(
a: AutomationStateResponse,
b: AutomationStateResponse,
): boolean {
return (
a.enabled === b.enabled &&
a.phase === b.phase &&
a.next_tick_at === b.next_tick_at &&
a.sleep_until === b.sleep_until
);
}

View File

@@ -1,36 +1,33 @@
import { log, error as logError } from "../logger";
import { storeListActiveEnvironments, storeUpdateEnvironment } from "../store";
import { storeListSessions } from "../store";
import { storeListSessions, storeUpdateSession } from "../store";
import { config } from "../config";
import { updateSessionStatus } from "./session";
export function runDisconnectMonitorSweep(now = Date.now()) {
const timeoutMs = config.disconnectTimeout * 1000;
// Check environment heartbeat timeout
const envs = storeListActiveEnvironments();
for (const env of envs) {
if (env.lastPollAt && now - env.lastPollAt.getTime() > timeoutMs) {
log(`[RCS] Environment ${env.id} timed out (no poll for ${Math.round((now - env.lastPollAt.getTime()) / 1000)}s)`);
storeUpdateEnvironment(env.id, { status: "disconnected" });
}
}
// Check session timeout (2x disconnect timeout with no update)
const sessions = storeListSessions();
for (const session of sessions) {
if (session.status === "running" || session.status === "idle") {
const elapsed = now - session.updatedAt.getTime();
if (elapsed > timeoutMs * 2) {
log(`[RCS] Session ${session.id} marked inactive (no update for ${Math.round(elapsed / 1000)}s)`);
updateSessionStatus(session.id, "inactive");
}
}
}
}
export function startDisconnectMonitor() {
const timeoutMs = config.disconnectTimeout * 1000;
setInterval(() => {
runDisconnectMonitorSweep();
const now = Date.now();
// Check environment heartbeat timeout
const envs = storeListActiveEnvironments();
for (const env of envs) {
if (env.lastPollAt && now - env.lastPollAt.getTime() > timeoutMs) {
log(`[RCS] Environment ${env.id} timed out (no poll for ${Math.round((now - env.lastPollAt.getTime()) / 1000)}s)`);
storeUpdateEnvironment(env.id, { status: "disconnected" });
}
}
// Check session timeout (2x disconnect timeout with no update)
const sessions = storeListSessions();
for (const session of sessions) {
if (session.status === "running" || session.status === "idle") {
const elapsed = now - session.updatedAt.getTime();
if (elapsed > timeoutMs * 2) {
log(`[RCS] Session ${session.id} marked inactive (no update for ${Math.round(elapsed / 1000)}s)`);
storeUpdateSession(session.id, { status: "inactive" });
}
}
}
}, 60_000); // Check every minute
}

View File

@@ -1,20 +1,14 @@
import {
storeCreateSession,
storeGetSession,
storeIsSessionOwner,
storeUpdateSession,
storeListSessions,
storeListSessionsByUsername,
storeListSessionsByEnvironment,
storeListSessionsByOwnerUuid,
} from "../store";
import { getAllEventBuses, removeEventBus } from "../transport/event-bus";
import { removeEventBus } from "../transport/event-bus";
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 {
return {
@@ -31,24 +25,6 @@ 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 {
const record = storeCreateSession({
environmentId: req.environment_id,
@@ -75,78 +51,16 @@ export function getSession(sessionId: string): SessionResponse | 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) {
storeUpdateSession(sessionId, { title });
}
export function updateSessionStatus(sessionId: string, status: string) {
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) {
updateSessionStatus(sessionId, "archived");
storeUpdateSession(sessionId, { status: "archived" });
removeEventBus(sessionId);
}

View File

@@ -51,11 +51,6 @@ export function normalizePayload(type: string, payload: unknown): Record<string,
raw: payload,
};
if (typeof p.uuid === "string" && p.uuid) normalized.uuid = p.uuid;
if (typeof p.isSynthetic === "boolean") normalized.isSynthetic = p.isSynthetic;
if (typeof p.status === "string") normalized.status = p.status;
if (typeof p.subtype === "string") normalized.subtype = p.subtype;
// Preserve tool fields
if (p.tool_name) normalized.tool_name = p.tool_name;
if (p.name) normalized.tool_name = p.name;
@@ -71,12 +66,6 @@ export function normalizePayload(type: string, payload: unknown): Record<string,
// Preserve message field for backward compat
if (p.message) normalized.message = p.message;
if (type === "task_state") {
if (typeof p.task_list_id === "string") normalized.task_list_id = p.task_list_id;
if (typeof p.taskListId === "string") normalized.taskListId = p.taskListId;
if (Array.isArray(p.tasks)) normalized.tasks = p.tasks;
}
return normalized;
}

View File

@@ -47,16 +47,6 @@ export interface WorkItemRecord {
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) ----------
const users = new Map<string, UserRecord>();
@@ -64,7 +54,6 @@ const tokenToUser = new Map<string, { username: string; createdAt: Date }>();
const environments = new Map<string, EnvironmentRecord>();
const sessions = new Map<string, SessionRecord>();
const workItems = new Map<string, WorkItemRecord>();
const sessionWorkers = new Map<string, SessionWorkerRecord>();
// UUID → session ownership: sessionId → Set of UUIDs
const sessionOwners = new Map<string, Set<string>>();
@@ -201,59 +190,9 @@ export function storeListSessionsByEnvironment(envId: string): SessionRecord[] {
}
export function storeDeleteSession(id: string): boolean {
sessionWorkers.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 ----------
// ---------- Session Ownership (UUID-based) ----------
@@ -333,6 +272,5 @@ export function storeReset() {
environments.clear();
sessions.clear();
workItems.clear();
sessionWorkers.clear();
sessionOwners.clear();
}

View File

@@ -1,80 +0,0 @@
import type { SessionEvent } from "./event-bus";
/**
* Convert an internal session event into the SDK/control message shape that
* bridge workers consume on both the legacy WS path and the v2 worker SSE path.
*/
export function toClientPayload(event: SessionEvent): Record<string, unknown> {
const payload = event.payload as Record<string, unknown> | null;
const messageUuid =
typeof payload?.uuid === "string" && payload.uuid ? payload.uuid : event.id;
if (event.type === "user" || event.type === "user_message") {
return {
type: "user",
uuid: messageUuid,
session_id: event.sessionId,
...(payload?.isSynthetic === true ? { isSynthetic: true } : {}),
message: {
role: "user",
content: payload?.content ?? payload?.message ?? "",
},
};
}
if (event.type === "permission_response" || event.type === "control_response") {
const approved = !!payload?.approved;
const existingResponse = payload?.response as Record<string, unknown> | undefined;
if (existingResponse) {
return { type: "control_response", response: existingResponse };
}
const updatedInput = payload?.updated_input as Record<string, unknown> | undefined;
const updatedPermissions = payload?.updated_permissions as Record<string, unknown>[] | undefined;
const feedbackMessage = payload?.message as string | undefined;
return {
type: "control_response",
response: {
subtype: approved ? "success" : "error",
request_id: payload?.request_id ?? "",
...(approved
? {
response: {
behavior: "allow" as const,
...(updatedInput ? { updatedInput } : {}),
...(updatedPermissions ? { updatedPermissions } : {}),
},
}
: {
error: "Permission denied by user",
response: { behavior: "deny" as const },
...(feedbackMessage ? { message: feedbackMessage } : {}),
}),
},
};
}
if (event.type === "interrupt") {
return {
type: "control_request",
request_id: event.id,
request: { subtype: "interrupt" },
};
}
if (event.type === "control_request") {
return {
type: "control_request",
request_id: payload?.request_id ?? event.id,
request: payload?.request ?? payload,
};
}
return {
type: event.type,
uuid: messageUuid,
session_id: event.sessionId,
message: payload,
};
}

View File

@@ -2,7 +2,6 @@ import { log, error as logError } from "../logger";
import type { Context } from "hono";
import type { SessionEvent } from "./event-bus";
import { getEventBus } from "./event-bus";
import { toClientPayload } from "./client-payload";
export interface SSEWriter {
send(event: SessionEvent): void;
@@ -117,118 +116,3 @@ export function createSSEStream(c: Context, sessionId: string, fromSeqNum = 0) {
},
});
}
function toWorkerClientPayload(event: SessionEvent): Record<string, unknown> {
if (
event.type === "permission_response" ||
event.type === "control_response" ||
event.type === "control_request" ||
event.type === "interrupt"
) {
return toClientPayload(event);
}
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",
},
});
}

View File

@@ -3,7 +3,6 @@ import { getEventBus } from "./event-bus";
import type { SessionEvent } from "./event-bus";
import { publishSessionEvent } from "../services/transport";
import { log, error as logError } from "../logger";
import { toClientPayload } from "./client-payload";
// Per-connection cleanup, keyed by sessionId (only one WS per session)
interface CleanupEntry {
@@ -25,9 +24,74 @@ const SERVER_KEEPALIVE_INTERVAL_MS = 60_000;
* Convert internal EventBus event -> SDK message for bridge client.
*/
function toSDKMessage(event: SessionEvent): string {
const payload = event.payload as Record<string, unknown> | null;
let msg: Record<string, unknown>;
if (event.type === "user" || event.type === "user_message") {
msg = {
type: "user",
uuid: event.id,
session_id: event.sessionId,
message: {
role: "user",
content: payload?.content ?? payload?.message ?? "",
},
};
} else if (event.type === "permission_response" || event.type === "control_response") {
const approved = !!payload?.approved;
const existingResponse = payload?.response as Record<string, unknown> | undefined;
if (existingResponse) {
msg = { type: "control_response", response: existingResponse };
} else {
const updatedInput = payload?.updated_input as Record<string, unknown> | undefined;
const updatedPermissions = payload?.updated_permissions as Record<string, unknown>[] | undefined;
const feedbackMessage = payload?.message as string | undefined;
msg = {
type: "control_response",
response: {
subtype: approved ? "success" : "error",
request_id: payload?.request_id ?? "",
...(approved
? {
response: {
behavior: "allow" as const,
...(updatedInput ? { updatedInput } : {}),
...(updatedPermissions ? { updatedPermissions } : {}),
},
}
: {
error: "Permission denied by user",
response: { behavior: "deny" as const },
...(feedbackMessage ? { message: feedbackMessage } : {}),
}),
},
};
}
} else if (event.type === "interrupt") {
msg = {
type: "control_request",
request_id: event.id,
request: { subtype: "interrupt" },
};
} else if (event.type === "control_request") {
msg = {
type: "control_request",
request_id: payload?.request_id ?? event.id,
request: payload?.request ?? payload,
};
} else {
msg = {
type: event.type,
uuid: event.id,
session_id: event.sessionId,
message: payload,
};
}
// NDJSON format: each message MUST end with \n so the child process's
// line-based parser can split messages correctly.
return JSON.stringify(toClientPayload(event)) + "\n";
return JSON.stringify(msg) + "\n";
}
/** Called from onOpen — subscribes to event bus, forwards outbound events to bridge WS */
@@ -171,11 +235,7 @@ export function ingestBridgeMessage(sessionId: string, msg: Record<string, unkno
}
payload = { message: msg.message, uuid: msg.uuid, content: text };
} else if (eventType === "user" || eventType === "system") {
payload = {
message: msg.message,
uuid: msg.uuid,
...(typeof msg.isSynthetic === "boolean" ? { isSynthetic: msg.isSynthetic } : {}),
};
payload = { message: msg.message, uuid: msg.uuid };
} else if (eventType === "control_request") {
payload = { request_id: msg.request_id, request: msg.request };
} else if (eventType === "control_response") {

View File

@@ -70,14 +70,6 @@ export interface SessionResponse {
username: string | null;
created_at: number;
updated_at: number;
automation_state?: AutomationStateResponse;
}
export interface AutomationStateResponse {
enabled: boolean;
phase: "standby" | "sleeping" | null;
next_tick_at: number | null;
sleep_until: number | null;
}
// --- v2 Code Sessions ---

View File

@@ -36,7 +36,6 @@ export interface ControlRequest extends SDKMessage {
export type SessionEventType =
| "user"
| "assistant"
| "automation_state"
| "permission_request"
| "permission_response"
| "control_request"
@@ -50,7 +49,6 @@ export type SessionEventType =
export interface NormalizedEventPayload {
content: string;
raw?: unknown;
isSynthetic?: boolean;
[key: string]: unknown;
}

View File

@@ -1,5 +1,17 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"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"],
"exclude": ["node_modules", "dist", "web"]
}

View File

@@ -4,119 +4,17 @@
*/
import { getUuid, setUuid, apiBind, apiFetchSessions, apiFetchAllSessions, apiFetchEnvironments, apiFetchSession, apiFetchSessionHistory, apiSendEvent, apiSendControl, apiInterrupt, apiCreateSession } from "./api.js";
import { connectSSE, disconnectSSE } from "./sse.js";
import {
appendEvent,
getActivityMode,
removeLoading,
resetReplayState,
renderReplayPendingRequests,
setAutomationActivity,
showLoading,
} from "./render.js";
import { appendEvent, renderPermissionRequest, showLoading, isLoading, resetReplayState, renderReplayPendingRequests } from "./render.js";
import { initTaskPanel, toggleTaskPanel, resetTaskState } from "./task-panel.js";
import {
createAutomationState,
getAutomationActivity,
getAutomationIndicator,
reduceAutomationState,
renderAutomationIcon,
shouldPulseAutomationIndicator,
} from "./automation.js";
import { esc, formatTime, statusClass, isClosedSessionStatus } from "./utils.js";
import { esc, formatTime, statusClass } from "./utils.js";
// ============================================================
// State
// ============================================================
let currentSessionId = null;
let currentSessionStatus = null;
let dashboardInterval = null;
let cachedEnvs = [];
let automationState = createAutomationState();
let automationPulseTimer = null;
function generateMessageUuid() {
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
return crypto.randomUUID();
}
return `msg_${Date.now()}_${Math.random().toString(16).slice(2)}`;
}
function renderAutomationIndicator() {
const indicatorEl = document.getElementById("session-automation");
if (!indicatorEl) return;
const indicator = getAutomationIndicator(automationState);
if (!indicator.visible) {
indicatorEl.className = "automation-pill hidden";
indicatorEl.dataset.pulsing = "false";
indicatorEl.innerHTML = "";
indicatorEl.removeAttribute("title");
return;
}
indicatorEl.className = `automation-pill automation-pill-${indicator.tone}`;
if (indicatorEl.dataset.pulsing === "true") {
indicatorEl.classList.add("is-pulsing");
}
indicatorEl.innerHTML = `
${renderAutomationIcon(indicator.iconVariant, { className: "automation-pill-icon" })}
<span class="automation-pill-label">${esc(indicator.label)}</span>
`;
indicatorEl.title = indicator.title;
}
function syncAutomationUI() {
renderAutomationIndicator();
setAutomationActivity(getAutomationActivity(automationState));
}
function stopAutomationPulse() {
if (automationPulseTimer) {
clearTimeout(automationPulseTimer);
automationPulseTimer = null;
}
const indicatorEl = document.getElementById("session-automation");
if (indicatorEl) {
indicatorEl.dataset.pulsing = "false";
indicatorEl.classList.remove("is-pulsing");
}
}
function pulseAutomationIndicator() {
if (!getAutomationIndicator(automationState).visible) return;
stopAutomationPulse();
const indicatorEl = document.getElementById("session-automation");
if (!indicatorEl) return;
indicatorEl.dataset.pulsing = "true";
indicatorEl.classList.add("is-pulsing");
automationPulseTimer = setTimeout(() => {
indicatorEl.dataset.pulsing = "false";
indicatorEl.classList.remove("is-pulsing");
automationPulseTimer = null;
}, 1200);
}
function resetAutomationIndicator() {
automationState = createAutomationState();
stopAutomationPulse();
syncAutomationUI();
}
function applyAutomationEvent(event, { replay = false } = {}) {
automationState = reduceAutomationState(automationState, event);
syncAutomationUI();
if (!replay && shouldPulseAutomationIndicator(event)) {
pulseAutomationIndicator();
}
}
function applyAutomationSnapshot(snapshot) {
if (snapshot === undefined) return;
applyAutomationEvent({ type: "automation_state", payload: snapshot }, { replay: true });
}
// ============================================================
// Router
@@ -145,72 +43,6 @@ function navigate(path) {
}
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?.("idle");
}
}
function handleSessionEvent(event) {
if (event?.type === "session_status" && typeof event.payload?.status === "string") {
applySessionStatus(event.payload.status);
if (isClosedSessionStatus(event.payload.status)) {
disconnectSSE();
}
}
applyAutomationEvent(event);
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)) {
const closedEvent = { type: "session_status", payload: { status: session.status } };
applyAutomationEvent(closedEvent);
appendEvent(closedEvent);
return;
}
} catch {
// Fall back to the original error if the refresh also fails.
}
alert(`${actionLabel}: ${err.message}`);
}
async function handleRoute() {
// Ensure we have a UUID
getUuid();
@@ -254,9 +86,6 @@ async function handleRoute() {
}
// Default: /code → dashboard
currentSessionId = null;
currentSessionStatus = null;
resetAutomationIndicator();
showPage("dashboard");
disconnectSSE();
renderDashboard();
@@ -331,8 +160,6 @@ function stopDashboardRefresh() {
async function renderSessionDetail(id) {
currentSessionId = id;
resetAutomationIndicator();
let session = null;
// Reset task state for new session and init panel
resetTaskState();
@@ -340,12 +167,14 @@ async function renderSessionDetail(id) {
if (taskPanelEl) initTaskPanel(taskPanelEl);
try {
session = await apiFetchSession(id);
const session = await apiFetchSession(id);
document.getElementById("session-title").textContent = session.title || session.id;
document.getElementById("session-id").textContent = session.id;
document.getElementById("session-env").textContent = session.environment_id || "";
document.getElementById("session-time").textContent = formatTime(session.created_at);
applySessionStatus(session.status);
const badge = document.getElementById("session-status");
badge.textContent = session.status;
badge.className = `status-badge status-${statusClass(session.status)}`;
} catch (err) {
alert("Failed to load session: " + err.message);
navigate("/code/");
@@ -354,7 +183,6 @@ async function renderSessionDetail(id) {
document.getElementById("event-stream").innerHTML = "";
document.getElementById("permission-area").innerHTML = "";
document.getElementById("permission-area").classList.add("hidden");
applyAutomationSnapshot(session?.automation_state);
// Load historical events before connecting to live stream
resetReplayState();
@@ -363,7 +191,6 @@ async function renderSessionDetail(id) {
const { events } = await apiFetchSessionHistory(id);
if (events && events.length > 0) {
for (const event of events) {
applyAutomationEvent(event, { replay: true });
appendEvent(event, { replay: true });
if (event.seqNum && event.seqNum > lastSeqNum) lastSeqNum = event.seqNum;
}
@@ -374,15 +201,7 @@ async function renderSessionDetail(id) {
// Re-render any still-unresolved permission prompts from history
renderReplayPendingRequests();
if (isClosedSessionStatus(currentSessionStatus)) {
const closedEvent = { type: "session_status", payload: { status: currentSessionStatus } };
applyAutomationEvent(closedEvent);
appendEvent(closedEvent);
disconnectSSE();
return;
}
connectSSE(id, handleSessionEvent, lastSeqNum);
connectSSE(id, appendEvent, lastSeqNum);
}
// ============================================================
@@ -395,20 +214,17 @@ function setupControlBar() {
const iconSend = document.getElementById("action-icon-send");
const iconStop = document.getElementById("action-icon-stop");
function setBtnState(mode) {
const working = mode === "working";
actionBtn.classList.toggle("loading", working);
actionBtn.dataset.mode = mode || "idle";
actionBtn.setAttribute("aria-label", working ? "Stop" : "Send");
iconSend.classList.toggle("hidden", working);
iconStop.classList.toggle("hidden", !working);
function setBtnState(loading) {
actionBtn.classList.toggle("loading", loading);
actionBtn.setAttribute("aria-label", loading ? "Stop" : "Send");
iconSend.classList.toggle("hidden", loading);
iconStop.classList.toggle("hidden", !loading);
}
window.__updateActionBtn = setBtnState;
setBtnState(getActivityMode());
actionBtn.addEventListener("click", () => {
if (getActivityMode() === "working") {
if (isLoading()) {
doInterrupt();
} else {
sendMessage();
@@ -421,34 +237,28 @@ function setupControlBar() {
}
async function doInterrupt() {
if (!currentSessionId || isClosedSessionStatus(currentSessionStatus)) return;
if (!currentSessionId) return;
const btn = document.getElementById("action-btn");
btn.disabled = true;
try {
await apiInterrupt(currentSessionId);
appendEvent({ type: "interrupt", payload: { message: "Session interrupted" } });
} catch (err) {
await syncClosedSessionState(err, "Interrupt failed");
alert("Interrupt failed: " + err.message);
} finally {
btn.disabled = isClosedSessionStatus(currentSessionStatus);
btn.disabled = false;
}
}
async function sendMessage() {
const input = document.getElementById("msg-input");
const text = input.value.trim();
if (!text || !currentSessionId || isClosedSessionStatus(currentSessionStatus)) return;
if (!text || !currentSessionId) return;
input.value = "";
const uuid = generateMessageUuid();
try {
await apiSendEvent(currentSessionId, {
type: "user",
uuid,
content: text,
message: { content: text },
});
await apiSendEvent(currentSessionId, { type: "user", content: text });
} catch (err) {
input.value = text;
await syncClosedSessionState(err, "Failed to send");
alert("Failed to send: " + err.message);
}
}
@@ -566,28 +376,11 @@ window._submitAnswers = async function (requestId, btn) {
function removePermissionPrompt(btn) {
const prompt = btn.closest(".permission-prompt, .ask-panel, .plan-panel");
const requestId = prompt?.dataset?.requestId || null;
if (prompt) prompt.remove();
if (requestId) {
const stream = document.getElementById("event-stream");
stream?.querySelectorAll("[data-pending-request-id]").forEach((row) => {
if (row.dataset.pendingRequestId === requestId) row.remove();
});
}
const area = document.getElementById("permission-area");
if (area && area.children.length === 0) area.classList.add("hidden");
}
function appendLocalSystemMessage(text) {
const stream = document.getElementById("event-stream");
if (!stream) return;
const row = document.createElement("div");
row.className = "msg-row system";
row.innerHTML = `<div class="msg-bubble">${esc(text)}</div>`;
stream.appendChild(row);
stream.scrollTop = stream.scrollHeight;
}
// ============================================================
// ExitPlanMode interactions
// ============================================================
@@ -632,7 +425,6 @@ window._submitPlanResponse = async function (requestId, btn) {
...(feedback ? { message: feedback } : {}),
});
removePermissionPrompt(btn);
appendLocalSystemMessage("Feedback sent. Continuing in plan mode.");
} else {
// Approval with permission mode
const modeMap = {

View File

@@ -1,380 +0,0 @@
/**
* Remote Control — Automation helpers
*
* Centralizes detection of non-human inputs so the web UI can hide
* internal prompts while still surfacing session state.
*/
export const PROACTIVE_ENABLED_TEXT =
"Proactive mode enabled — model will work autonomously between ticks";
export const PROACTIVE_DISABLED_TEXT = "Proactive mode disabled";
const CLOSED_SESSION_STATUSES = new Set(["archived", "inactive"]);
const HIDDEN_AUTOMATION_TAGS = new Set([
"bash-input",
"bash-stderr",
"bash-stdout",
"channel",
"channel-message",
"command-args",
"command-message",
"command-name",
"cross-session-message",
"fork-boilerplate",
"local-command-caveat",
"local-command-stderr",
"local-command-stdout",
"output-file",
"reason",
"remote-review",
"remote-review-progress",
"status",
"summary",
"system-reminder",
"task-id",
"task-notification",
"task-type",
"teammate-message",
"tick",
"tool-use-id",
"ultraplan",
"worktree",
"worktreeBranch",
"worktreePath",
]);
const PRIMARY_AUTOMATION_TAGS = new Set([
"bash-input",
"bash-stderr",
"bash-stdout",
"channel-message",
"command-args",
"command-message",
"command-name",
"cross-session-message",
"fork-boilerplate",
"local-command-caveat",
"local-command-stderr",
"local-command-stdout",
"remote-review",
"remote-review-progress",
"system-reminder",
"task-notification",
"teammate-message",
"tick",
"ultraplan",
]);
const WORKING_AUTOMATION_TAGS = new Set(
[...PRIMARY_AUTOMATION_TAGS].filter(
(tag) => tag !== "local-command-caveat" && tag !== "system-reminder",
),
);
const XML_ONLY_BLOCK_PATTERN =
/^(?:\s*<([a-z][\w-]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>\s*)+$/;
const XML_BLOCK_PATTERN =
/\s*<([a-z][\w-]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>\s*/gy;
function normalizeAutomationStatePayload(payload) {
if (!payload || typeof payload !== "object") {
return {
enabled: false,
phase: null,
next_tick_at: null,
sleep_until: null,
};
}
return {
enabled: payload.enabled === true,
phase: payload.phase === "standby" || payload.phase === "sleeping" ? payload.phase : null,
next_tick_at: typeof payload.next_tick_at === "number" ? payload.next_tick_at : null,
sleep_until: typeof payload.sleep_until === "number" ? payload.sleep_until : null,
};
}
export function extractEventText(payload) {
if (!payload) return "";
if (typeof payload.content === "string" && payload.content) return payload.content;
const msg = payload.message;
if (msg && typeof msg === "object") {
const mc = msg.content;
if (typeof mc === "string") return mc;
if (Array.isArray(mc)) {
return mc
.filter((block) => block && typeof block === "object" && block.type === "text")
.map((block) => block.text || "")
.join("");
}
}
return typeof payload === "string" ? payload : JSON.stringify(payload);
}
function getOpeningTagNames(text) {
const trimmed = String(text).trim();
if (!trimmed) return [];
XML_BLOCK_PATTERN.lastIndex = 0;
const tags = [];
while (XML_BLOCK_PATTERN.lastIndex < trimmed.length) {
const match = XML_BLOCK_PATTERN.exec(trimmed);
if (!match) return [];
tags.push(match[1]);
}
return tags;
}
export function isAutomationEnvelopeText(text) {
const trimmed = typeof text === "string" ? text.trim() : "";
if (!trimmed) return false;
if (!XML_ONLY_BLOCK_PATTERN.test(trimmed)) return false;
const tagNames = getOpeningTagNames(trimmed);
return (
tagNames.length > 0 &&
tagNames.every((tagName) => HIDDEN_AUTOMATION_TAGS.has(tagName)) &&
tagNames.some((tagName) => PRIMARY_AUTOMATION_TAGS.has(tagName))
);
}
export function isHiddenAutomationUserPayload(payload) {
if (!payload || typeof payload !== "object") return false;
if (payload.isSynthetic === true) return true;
return isAutomationEnvelopeText(extractEventText(payload));
}
export function shouldHideAutomationUserEvent(payload, direction = "inbound") {
return direction === "inbound" && isHiddenAutomationUserPayload(payload);
}
export function shouldStartAutomationWorkFromUserEvent(payload, direction = "inbound") {
if (!shouldHideAutomationUserEvent(payload, direction)) {
return false;
}
const text = extractEventText(payload).trim();
if (!text || !XML_ONLY_BLOCK_PATTERN.test(text)) {
return payload?.isSynthetic === true;
}
const tagNames = getOpeningTagNames(text);
return tagNames.some((tagName) => WORKING_AUTOMATION_TAGS.has(tagName));
}
export function createAutomationState() {
return {
proactive: false,
autoRun: false,
hasAuthority: false,
enabled: false,
phase: null,
nextTickAt: null,
sleepUntil: null,
};
}
function applyAuthoritativeAutomationState(state, payload) {
const normalized = normalizeAutomationStatePayload(payload);
state.hasAuthority = true;
state.enabled = normalized.enabled;
state.phase = normalized.phase;
state.nextTickAt = normalized.next_tick_at;
state.sleepUntil = normalized.sleep_until;
state.proactive = normalized.enabled;
state.autoRun = false;
return state;
}
export function reduceAutomationState(state, event) {
const next = state ? { ...state } : createAutomationState();
if (!event || typeof event !== "object") return next;
const type = event.type || "unknown";
const payload = event.payload || {};
const direction = event.direction || "inbound";
if (type === "automation_state") {
return applyAuthoritativeAutomationState(next, payload);
}
if (type === "session_status") {
if (CLOSED_SESSION_STATUSES.has(payload.status)) {
if (next.hasAuthority) {
return applyAuthoritativeAutomationState(next, null);
}
next.proactive = false;
next.autoRun = false;
}
return next;
}
if (next.hasAuthority) {
return next;
}
if (type === "assistant") {
const text = extractEventText(payload).trim();
if (text === PROACTIVE_ENABLED_TEXT) {
next.proactive = true;
next.autoRun = false;
return next;
}
if (text === PROACTIVE_DISABLED_TEXT) {
next.proactive = false;
next.autoRun = false;
return next;
}
next.autoRun = false;
return next;
}
if (type === "result" || type === "result_success" || type === "error" || type === "interrupt") {
next.autoRun = false;
return next;
}
if (type === "user" && shouldHideAutomationUserEvent(payload, direction)) {
next.autoRun = true;
}
return next;
}
export function shouldPulseAutomationIndicator(event) {
if (!event || typeof event !== "object") return false;
if (event.type === "automation_state") {
return event.payload?.enabled === true;
}
if (event.type === "assistant") {
const text = extractEventText(event.payload || {}).trim();
return text === PROACTIVE_ENABLED_TEXT;
}
return event.type === "user" && shouldHideAutomationUserEvent(event.payload || {}, event.direction || "inbound");
}
export function getAutomationIndicator(state) {
if (state?.hasAuthority) {
if (!state.enabled) {
return {
visible: false,
label: "",
tone: "",
title: "",
iconVariant: "active",
};
}
if (state.phase === "sleeping") {
return {
visible: true,
label: "Autopilot",
tone: "sleeping",
title: "Claude Code is in proactive mode and currently sleeping until the next wake-up or user message.",
iconVariant: "sleeping",
};
}
if (state.phase === "standby") {
return {
visible: true,
label: "Autopilot",
tone: "proactive",
title: "Claude Code is in proactive mode and waiting for the next scheduled check-in.",
iconVariant: "standby",
};
}
return {
visible: true,
label: "Autopilot",
tone: "proactive",
title: "Claude Code is in proactive mode and may continue working between user messages.",
iconVariant: "active",
};
}
if (state?.proactive) {
return {
visible: true,
label: "Autopilot",
tone: "proactive",
title: "Claude Code is in proactive mode and may continue working between user messages.",
iconVariant: "active",
};
}
if (state?.autoRun) {
return {
visible: true,
label: "Auto Run",
tone: "auto-run",
title: "Claude Code is processing an automatic background trigger.",
iconVariant: "active",
};
}
return {
visible: false,
label: "",
tone: "",
title: "",
iconVariant: "active",
};
}
export function getAutomationActivity(state) {
if (!state?.hasAuthority || !state.enabled) {
return null;
}
if (state.phase === "standby") {
return {
mode: "standby",
label: "standby",
endsAt: state.nextTickAt,
iconVariant: "standby",
};
}
if (state.phase === "sleeping") {
return {
mode: "sleeping",
label: "sleeping",
endsAt: state.sleepUntil,
iconVariant: "sleeping",
};
}
return null;
}
export function renderAutomationIcon(variant = "active", { className = "", decorative = true } = {}) {
const classes = ["clawd-icon", `clawd-icon-${variant}`, className].filter(Boolean).join(" ");
const ariaAttrs = decorative ? 'aria-hidden="true"' : 'role="img" aria-label="Claude Code status"';
return `
<span class="${classes}" ${ariaAttrs}>
<svg viewBox="0 0 40 30" fill="none">
<path class="clawd-arm clawd-arm-left" d="M8.5 13.4C6.6 12.8 5.4 11.4 4.8 9.4C4.6 8.6 4.9 7.7 5.6 7.3C6.3 6.9 7.2 7 7.8 7.6L10.8 10.6L8.5 13.4Z" />
<path class="clawd-arm clawd-arm-right" d="M31.5 13.4C33.4 12.8 34.6 11.4 35.2 9.4C35.4 8.6 35.1 7.7 34.4 7.3C33.7 6.9 32.8 7 32.2 7.6L29.2 10.6L31.5 13.4Z" />
<path class="clawd-shell" d="M10 12.2C10 7.9 13.5 4.4 17.8 4.4H22.2C26.5 4.4 30 7.9 30 12.2V17.3C30 21 27 24 23.3 24H16.7C13 24 10 21 10 17.3V12.2Z" />
<circle class="clawd-eye clawd-eye-left" cx="17.2" cy="13.4" r="1.55" />
<circle class="clawd-eye clawd-eye-right" cx="22.8" cy="13.4" r="1.55" />
<path class="clawd-eye-line clawd-eye-line-left" d="M15.9 13.6C16.3 12.8 17 12.4 17.9 12.4" />
<path class="clawd-eye-line clawd-eye-line-right" d="M22.1 12.4C23 12.4 23.7 12.8 24.1 13.6" />
<path class="clawd-foot clawd-foot-left" d="M14.3 25.1C14.3 24 15.2 23.1 16.3 23.1C17.4 23.1 18.3 24 18.3 25.1V25.8H14.3V25.1Z" />
<path class="clawd-foot clawd-foot-right" d="M21.7 25.1C21.7 24 22.6 23.1 23.7 23.1C24.8 23.1 25.7 24 25.7 25.1V25.8H21.7V25.1Z" />
</svg>
<span class="clawd-z clawd-z-1">Z</span>
<span class="clawd-z clawd-z-2">Z</span>
</span>
`;
}

View File

@@ -1,207 +0,0 @@
import { describe, expect, test } from "bun:test";
import {
PROACTIVE_DISABLED_TEXT,
PROACTIVE_ENABLED_TEXT,
createAutomationState,
getAutomationActivity,
getAutomationIndicator,
isAutomationEnvelopeText,
reduceAutomationState,
shouldHideAutomationUserEvent,
shouldStartAutomationWorkFromUserEvent,
} from "./automation.js";
describe("automation helpers", () => {
test("keeps real user text visible", () => {
expect(shouldHideAutomationUserEvent({ content: "hello from a human" }, "inbound")).toBe(false);
});
test("hides internal xml wrappers without synthetic metadata", () => {
expect(isAutomationEnvelopeText("<tick>2:56:47 PM</tick>")).toBe(true);
expect(isAutomationEnvelopeText("<system-reminder>\nDo useful work.\n</system-reminder>")).toBe(true);
expect(
isAutomationEnvelopeText(
"<task-notification><summary>Finished</summary><output-file>/tmp/out.log</output-file></task-notification>",
),
).toBe(true);
expect(
shouldHideAutomationUserEvent(
{ content: "<local-command-caveat>Generated while running local commands.</local-command-caveat>" },
"inbound",
),
).toBe(true);
});
test("does not treat slash-command scaffolding as active work", () => {
expect(
shouldStartAutomationWorkFromUserEvent(
{ content: "<local-command-caveat>Generated while running local commands.</local-command-caveat>" },
"inbound",
),
).toBe(false);
expect(
shouldStartAutomationWorkFromUserEvent(
{
content:
"<system-reminder>\nProactive mode is now enabled. You will receive periodic <tick> prompts.\n</system-reminder>",
isSynthetic: true,
},
"inbound",
),
).toBe(false);
});
test("keeps true automatic triggers eligible for loading state", () => {
expect(
shouldStartAutomationWorkFromUserEvent(
{ content: "<tick>2:56:47 PM</tick>", isSynthetic: true },
"inbound",
),
).toBe(true);
expect(
shouldStartAutomationWorkFromUserEvent(
{ content: "scheduled job: refresh analytics cache", isSynthetic: true },
"inbound",
),
).toBe(true);
});
test("hides synthetic automatic prompts even when they are plain text", () => {
expect(
shouldHideAutomationUserEvent(
{ content: "scheduled job: refresh analytics cache", isSynthetic: true },
"inbound",
),
).toBe(true);
});
test("keeps mixed human text with tags visible", () => {
expect(
shouldHideAutomationUserEvent(
{ content: "Please keep this: <system-reminder>not metadata</system-reminder>" },
"inbound",
),
).toBe(false);
});
test("shows autopilot while proactive mode remains active", () => {
let state = createAutomationState();
state = reduceAutomationState(state, {
type: "assistant",
payload: { content: PROACTIVE_ENABLED_TEXT },
});
expect(getAutomationIndicator(state)).toEqual({
visible: true,
label: "Autopilot",
tone: "proactive",
title: "Claude Code is in proactive mode and may continue working between user messages.",
iconVariant: "active",
});
state = reduceAutomationState(state, {
type: "user",
direction: "inbound",
payload: { content: "<tick>3:15:00 PM</tick>" },
});
expect(getAutomationIndicator(state).label).toBe("Autopilot");
state = reduceAutomationState(state, {
type: "assistant",
payload: { content: "Working on background maintenance." },
});
expect(getAutomationIndicator(state).label).toBe("Autopilot");
state = reduceAutomationState(state, {
type: "assistant",
payload: { content: PROACTIVE_DISABLED_TEXT },
});
expect(getAutomationIndicator(state).visible).toBe(false);
});
test("shows auto run until an automatic trigger settles", () => {
let state = createAutomationState();
state = reduceAutomationState(state, {
type: "user",
direction: "inbound",
payload: { content: "scheduled job: refresh analytics cache", isSynthetic: true },
});
expect(getAutomationIndicator(state).label).toBe("Auto Run");
expect(getAutomationIndicator(state).iconVariant).toBe("active");
state = reduceAutomationState(state, {
type: "assistant",
payload: { content: "Completed scheduled refresh." },
});
expect(getAutomationIndicator(state).visible).toBe(false);
});
test("authoritative automation_state drives standby and sleeping states", () => {
let state = createAutomationState();
state = reduceAutomationState(state, {
type: "automation_state",
payload: {
enabled: true,
phase: "standby",
next_tick_at: 123456,
sleep_until: null,
},
});
expect(getAutomationIndicator(state)).toEqual({
visible: true,
label: "Autopilot",
tone: "proactive",
title: "Claude Code is in proactive mode and waiting for the next scheduled check-in.",
iconVariant: "standby",
});
expect(getAutomationActivity(state)).toEqual({
mode: "standby",
label: "standby",
endsAt: 123456,
iconVariant: "standby",
});
state = reduceAutomationState(state, {
type: "automation_state",
payload: {
enabled: true,
phase: "sleeping",
next_tick_at: null,
sleep_until: 999999,
},
});
expect(getAutomationIndicator(state).tone).toBe("sleeping");
expect(getAutomationIndicator(state).iconVariant).toBe("sleeping");
expect(getAutomationActivity(state)).toEqual({
mode: "sleeping",
label: "sleeping",
endsAt: 999999,
iconVariant: "sleeping",
});
});
test("authoritative disabled snapshot suppresses heuristic auto-run fallback", () => {
let state = createAutomationState();
state = reduceAutomationState(state, {
type: "automation_state",
payload: {
enabled: false,
phase: null,
next_tick_at: null,
sleep_until: null,
},
});
state = reduceAutomationState(state, {
type: "user",
direction: "inbound",
payload: { content: "<tick>3:15:00 PM</tick>" },
});
expect(getAutomationIndicator(state).visible).toBe(false);
expect(getAutomationActivity(state)).toBeNull();
});
});

View File

@@ -150,7 +150,6 @@ nav {
.status-active, .status-running { background: var(--green-bg); color: var(--green); }
.status-idle { background: var(--yellow-bg); color: var(--yellow); }
.status-inactive { background: #F0ECE7; color: var(--text-secondary); }
.status-requires_action { background: var(--orange-bg); color: var(--orange); }
.status-archived { background: #F0ECE7; color: var(--text-secondary); }
.status-error { background: var(--red-bg); color: var(--red); }

View File

@@ -7,7 +7,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,500;12..96,600;12..96,700&family=Figtree:wght@300;400;500;600;700&family=Fira+Code:wght@400;500&display=swap" />
<link rel="stylesheet" href="/code/style.css" />
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<!-- Nav Bar -->
@@ -81,7 +81,6 @@
<div class="session-meta-row">
<span id="session-id" class="meta-item"></span>
<span id="session-status" class="status-badge"></span>
<span id="session-automation" class="automation-pill hidden" aria-live="polite"></span>
<span id="session-env" class="meta-item"></span>
<span id="session-time" class="meta-item"></span>
<button id="task-panel-toggle" class="nav-link btn-text" title="Tasks & Todos">
@@ -147,6 +146,6 @@
<!-- QR Libraries -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.js"></script>
<script type="module" src="/code/app.js"></script>
<script type="module" src="./app.js"></script>
</body>
</html>

View File

@@ -24,7 +24,6 @@
.msg-row.user { align-self: flex-end; }
.msg-row.assistant { align-self: flex-start; }
.msg-row.tool { align-self: flex-start; max-width: 95%; }
.msg-row.tool-trace-row { align-self: flex-start; max-width: 92%; }
.msg-row.system { align-self: center; }
.msg-row.result { align-self: center; }
@@ -52,124 +51,6 @@
box-shadow: var(--shadow-sm);
}
.assistant-turn {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
}
.assistant-turn-orphan {
gap: 0;
}
.assistant-trace {
display: flex;
flex-direction: column;
gap: 10px;
align-items: flex-start;
}
.assistant-trace.hidden {
display: none;
}
.assistant-trace-toggle {
display: inline-flex;
align-items: center;
gap: 10px;
border: 1px solid rgba(160, 120, 96, 0.16);
background: rgba(245, 243, 239, 0.78);
color: var(--text-secondary);
border-radius: 999px;
padding: 6px 10px 6px 8px;
font-size: 0.76rem;
font-weight: 600;
line-height: 1;
transition: all var(--transition-fast);
backdrop-filter: blur(8px);
}
.assistant-trace-toggle:hover {
color: var(--text-primary);
border-color: rgba(217, 119, 87, 0.28);
background: rgba(250, 247, 242, 0.98);
}
.assistant-trace-toggle.has-error {
color: var(--red);
border-color: rgba(196, 64, 64, 0.24);
background: rgba(252, 238, 238, 0.88);
}
.assistant-trace-glyph {
display: inline-flex;
align-items: flex-end;
gap: 2px;
min-width: 14px;
}
.assistant-trace-glyph span {
display: block;
width: 3px;
border-radius: 999px;
background: currentColor;
opacity: 0.82;
}
.assistant-trace-glyph span:nth-child(1) { height: 7px; }
.assistant-trace-glyph span:nth-child(2) { height: 10px; }
.assistant-trace-glyph span:nth-child(3) { height: 5px; }
.assistant-trace-count {
min-width: 1ch;
font-family: var(--font-mono);
font-size: 0.75rem;
}
.assistant-trace-chevron {
font-size: 0.9rem;
transition: transform var(--transition-fast);
}
.assistant-trace-toggle.is-open .assistant-trace-chevron {
transform: rotate(90deg);
}
.assistant-trace-panel {
width: min(100%, 720px);
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px;
border-radius: 16px;
border: 1px solid rgba(160, 120, 96, 0.14);
background:
linear-gradient(180deg, rgba(250, 247, 242, 0.98), rgba(245, 243, 239, 0.92)),
var(--bg-card);
box-shadow: var(--shadow-sm);
}
.assistant-trace-panel.hidden {
display: none;
}
.assistant-trace-card {
box-shadow: none;
}
.assistant-trace-card:hover {
border-color: rgba(217, 119, 87, 0.24);
}
.assistant-trace-card-error {
border-color: rgba(196, 64, 64, 0.24);
}
.assistant-trace-card-error:hover {
border-color: rgba(196, 64, 64, 0.4);
}
.msg-row.system .msg-bubble {
background: transparent;
color: var(--text-muted);
@@ -217,7 +98,6 @@
font-size: 0.7rem;
transition: transform var(--transition-fast);
}
.tool-card-header.is-open .tool-icon,
.tool-card-header:hover .tool-icon { transform: rotate(90deg); }
.tool-card-body {
@@ -449,51 +329,15 @@
line-height: 1.6;
color: var(--text-primary);
}
.plan-panel .plan-content > :first-child { margin-top: 0; }
.plan-panel .plan-content > :last-child { margin-bottom: 0; }
.plan-panel .plan-content h1,
.plan-panel .plan-content h2,
.plan-panel .plan-content h3,
.plan-panel .plan-content h4,
.plan-panel .plan-content h5,
.plan-panel .plan-content h6 {
margin: 0 0 10px;
line-height: 1.3;
font-family: var(--font-display);
font-weight: 600;
}
.plan-panel .plan-content h1 { font-size: 1.15rem; }
.plan-panel .plan-content h2 { font-size: 1.05rem; }
.plan-panel .plan-content h3,
.plan-panel .plan-content h4,
.plan-panel .plan-content h5,
.plan-panel .plan-content h6 { font-size: 0.95rem; }
.plan-panel .plan-content p {
margin: 0 0 10px;
}
.plan-panel .plan-content ul,
.plan-panel .plan-content ol {
margin: 0 0 12px 1.35em;
padding: 0;
}
.plan-panel .plan-content li + li {
margin-top: 4px;
}
.plan-panel .plan-content pre {
background: var(--bg-tool-card);
padding: 10px;
border-radius: 6px;
overflow-x: auto;
margin: 10px 0;
margin: 6px 0;
font-family: var(--font-mono);
font-size: 0.82rem;
}
.plan-panel .plan-content pre code {
background: transparent;
padding: 0;
border-radius: 0;
font-size: inherit;
}
.plan-panel .plan-content code {
background: var(--bg-tool-card);
padding: 2px 5px;
@@ -635,58 +479,3 @@
font-family: var(--font-mono);
margin-left: auto;
}
.automation-activity-row {
align-self: flex-start;
max-width: 92%;
}
.automation-activity-card {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
border-radius: 18px;
border: 1px solid rgba(217, 119, 87, 0.16);
background:
linear-gradient(135deg, rgba(217, 119, 87, 0.08), rgba(250, 247, 242, 0.94)),
var(--bg-card);
box-shadow: var(--shadow-sm);
}
.automation-activity-standby .automation-activity-card {
color: var(--accent-hover);
}
.automation-activity-sleeping .automation-activity-card {
color: var(--green);
border-color: rgba(59, 138, 106, 0.16);
background:
linear-gradient(135deg, rgba(59, 138, 106, 0.08), rgba(250, 247, 242, 0.94)),
var(--bg-card);
}
.automation-activity-icon {
width: 34px;
height: 26px;
}
.automation-activity-copy {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.automation-activity-label {
font-size: 0.9rem;
font-weight: 600;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.automation-activity-countdown {
margin-left: auto;
padding: 5px 9px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.72);
border: 1px solid rgba(160, 120, 96, 0.14);
font-family: var(--font-mono);
font-size: 0.78rem;
color: currentColor;
flex-shrink: 0;
}

View File

@@ -234,164 +234,6 @@
gap: 12px;
align-items: center;
}
.automation-pill {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 4px 10px 4px 8px;
border-radius: 999px;
font-size: 0.76rem;
font-weight: 700;
letter-spacing: 0.02em;
border: 1px solid transparent;
box-shadow: var(--shadow-sm);
transition: transform var(--transition-fast), box-shadow var(--transition-fast), opacity var(--transition-fast);
}
.automation-pill-icon { width: 24px; height: 18px; flex-shrink: 0; }
.automation-pill-label { line-height: 1; }
.automation-pill-proactive {
color: var(--accent-hover);
background:
linear-gradient(135deg, rgba(217, 119, 87, 0.12), rgba(217, 119, 87, 0.06)),
var(--bg-card);
border-color: rgba(217, 119, 87, 0.18);
}
.automation-pill-sleeping {
color: var(--green);
background:
linear-gradient(135deg, rgba(59, 138, 106, 0.14), rgba(59, 138, 106, 0.05)),
var(--bg-card);
border-color: rgba(59, 138, 106, 0.18);
}
.automation-pill-auto-run {
color: var(--green);
background:
linear-gradient(135deg, rgba(59, 138, 106, 0.12), rgba(59, 138, 106, 0.05)),
var(--bg-card);
border-color: rgba(59, 138, 106, 0.18);
}
.automation-pill.is-pulsing {
animation: automationPillPulse 1.2s ease-out;
}
.automation-pill.is-pulsing .clawd-icon {
animation: automationDotPulse 1.2s ease-out;
}
@keyframes automationPillPulse {
0% { transform: translateY(0) scale(1); box-shadow: var(--shadow-sm); }
35% { transform: translateY(-1px) scale(1.02); box-shadow: var(--shadow-md); }
100% { transform: translateY(0) scale(1); box-shadow: var(--shadow-sm); }
}
@keyframes automationDotPulse {
0% { transform: scale(1); opacity: 0.9; }
35% { transform: scale(1.5); opacity: 1; }
100% { transform: scale(1); opacity: 0.92; }
}
.clawd-icon {
position: relative;
display: inline-flex;
width: 30px;
height: 22px;
flex-shrink: 0;
color: inherit;
}
.clawd-icon svg {
width: 100%;
height: 100%;
overflow: visible;
}
.clawd-shell,
.clawd-foot { fill: currentColor; }
.clawd-shell { opacity: 0.9; }
.clawd-arm { fill: currentColor; opacity: 0.74; }
.clawd-eye {
fill: var(--text-primary);
transform-box: fill-box;
transform-origin: center;
}
.clawd-eye-line {
display: none;
stroke: var(--text-primary);
stroke-width: 1.8;
stroke-linecap: round;
}
.clawd-z {
position: absolute;
top: -3px;
right: -2px;
font-family: var(--font-mono);
font-size: 0.56rem;
font-weight: 700;
color: currentColor;
opacity: 0;
pointer-events: none;
}
.clawd-z-2 {
top: -9px;
right: 4px;
font-size: 0.48rem;
}
.clawd-icon-standby svg {
animation: clawdStandbyBob 2.4s ease-in-out infinite;
}
.clawd-icon-standby .clawd-eye-left {
animation: clawdLookLeft 2.4s ease-in-out infinite;
}
.clawd-icon-standby .clawd-eye-right {
animation: clawdLookRight 2.4s ease-in-out infinite;
}
.clawd-icon-sleeping svg {
animation: clawdSleepFloat 3.2s ease-in-out infinite;
}
.clawd-icon-sleeping .clawd-eye {
display: none;
}
.clawd-icon-sleeping .clawd-eye-line {
display: block;
}
.clawd-icon-sleeping .clawd-z {
opacity: 0.88;
}
.clawd-icon-sleeping .clawd-z-1 {
animation: clawdSleepZ 2.7s ease-in-out infinite;
}
.clawd-icon-sleeping .clawd-z-2 {
animation: clawdSleepZ 2.7s ease-in-out infinite 0.45s;
}
@keyframes clawdStandbyBob {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-1px); }
}
@keyframes clawdLookLeft {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-0.8px); }
55% { transform: translateX(0.6px); }
}
@keyframes clawdLookRight {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-0.6px); }
55% { transform: translateX(0.8px); }
}
@keyframes clawdSleepFloat {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(1px); }
}
@keyframes clawdSleepZ {
0% { transform: translate(0, 0) scale(0.94); opacity: 0; }
20% { opacity: 0.88; }
100% { transform: translate(4px, -8px) scale(1.04); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.automation-pill.is-pulsing,
.automation-pill.is-pulsing .clawd-icon,
.clawd-icon-standby svg,
.clawd-icon-standby .clawd-eye-left,
.clawd-icon-standby .clawd-eye-right,
.clawd-icon-sleeping svg,
.clawd-icon-sleeping .clawd-z-1,
.clawd-icon-sleeping .clawd-z-2 {
animation: none;
}
}
.meta-item {
font-size: 0.8rem;
color: var(--text-secondary);

View File

@@ -1,30 +0,0 @@
import { describe, expect, test } from "bun:test";
import {
formatCountdownRemaining,
resolveActivityMode,
shouldRenderTranscriptActivity,
} from "./render.js";
describe("render activity helpers", () => {
test("authoritative standby and sleeping states override stale working spinners", () => {
expect(resolveActivityMode(true, { mode: "standby" })).toBe("standby");
expect(resolveActivityMode(true, { mode: "sleeping" })).toBe("sleeping");
expect(resolveActivityMode(true, null)).toBe("working");
expect(resolveActivityMode(false, null)).toBe("idle");
});
test("formats countdowns compactly", () => {
expect(formatCountdownRemaining(35_000, 0)).toBe("35s");
expect(formatCountdownRemaining(185_000, 0)).toBe("3m 5s");
expect(formatCountdownRemaining(3_900_000, 0)).toBe("1h 5m");
expect(formatCountdownRemaining(null, 0)).toBe("");
});
test("renders transcript activity only for active work", () => {
expect(shouldRenderTranscriptActivity("working")).toBe(true);
expect(shouldRenderTranscriptActivity("standby")).toBe(false);
expect(shouldRenderTranscriptActivity("sleeping")).toBe(false);
expect(shouldRenderTranscriptActivity("idle")).toBe(false);
});
});

View File

@@ -1,36 +0,0 @@
import { describe, expect, test } from "bun:test";
import { formatPlanContent } from "./render.js";
describe("formatPlanContent", () => {
test("renders headings, paragraphs, and lists for plan panels", () => {
const html = formatPlanContent(`## Summary
Line one
Line two
- First item
- Second item
1. Step one
2. Step two`);
expect(html).toContain("<h2>Summary</h2>");
expect(html).toContain("<p>Line one<br>Line two</p>");
expect(html).toContain("<ul><li>First item</li><li>Second item</li></ul>");
expect(html).toContain("<ol><li>Step one</li><li>Step two</li></ol>");
});
test("escapes unsafe markup and preserves inline formatting plus code blocks", () => {
const html = formatPlanContent(`**Bold** with \`inline\` and <script>alert(1)</script>
\`\`\`js
const markup = "<div>";
\`\`\``);
expect(html).toContain("<strong>Bold</strong>");
expect(html).toContain("<code");
expect(html).toContain("inline</code>");
expect(html).toContain("&lt;script&gt;alert(1)&lt;/script&gt;");
expect(html).toContain("<pre><code>const markup = &quot;&lt;div&gt;&quot;;</code></pre>");
});
});

View File

@@ -1,24 +0,0 @@
import { describe, expect, test } from "bun:test";
import { isConversationClearedStatus } from "./render.js";
describe("status helpers", () => {
test("detects direct conversation reset markers", () => {
expect(isConversationClearedStatus({ status: "conversation_cleared" })).toBe(true);
});
test("detects nested raw conversation reset markers", () => {
expect(
isConversationClearedStatus({
status: "",
raw: { status: "conversation_cleared" },
}),
).toBe(true);
});
test("ignores unrelated status payloads", () => {
expect(isConversationClearedStatus({ status: "running" })).toBe(false);
expect(isConversationClearedStatus({})).toBe(false);
expect(isConversationClearedStatus(null)).toBe(false);
});
});

View File

@@ -1,90 +0,0 @@
import { describe, expect, test } from "bun:test";
import {
addAssistantToolTraceHost,
addToolTraceEntry,
clearActiveToolTraceHost,
createToolTraceState,
} from "./render.js";
describe("tool trace grouping state", () => {
test("keeps tool entries attached to the current assistant turn", () => {
let state = createToolTraceState();
const assistant = addAssistantToolTraceHost(state, "Checking the repo");
state = assistant.state;
const toolUse = addToolTraceEntry(state, "use");
state = toolUse.state;
const toolResult = addToolTraceEntry(state, "result");
state = toolResult.state;
expect(assistant.host).toEqual({
id: "trace-1",
kind: "assistant",
assistantContent: "Checking the repo",
entryKinds: [],
});
expect(toolUse.createdHost).toBeNull();
expect(toolResult.createdHost).toBeNull();
expect(state.hosts).toEqual([
{
id: "trace-1",
kind: "assistant",
assistantContent: "Checking the repo",
entryKinds: ["use", "result"],
},
]);
});
test("creates an orphan trace host when tool activity has no assistant turn", () => {
const result = addToolTraceEntry(createToolTraceState(), "use");
expect(result.createdHost).toEqual({
id: "trace-1",
kind: "orphan",
assistantContent: "",
entryKinds: ["use"],
});
expect(result.state.hosts).toEqual([
{
id: "trace-1",
kind: "orphan",
assistantContent: "",
entryKinds: ["use"],
},
]);
});
test("starts a new orphan host after a visible user turn clears the active assistant host", () => {
let state = createToolTraceState();
state = addAssistantToolTraceHost(state, "Running tools").state;
state = addToolTraceEntry(state, "use").state;
state = clearActiveToolTraceHost(state);
const nextResult = addToolTraceEntry(state, "result");
expect(nextResult.createdHost).toEqual({
id: "trace-2",
kind: "orphan",
assistantContent: "",
entryKinds: ["result"],
});
expect(nextResult.state.hosts).toEqual([
{
id: "trace-1",
kind: "assistant",
assistantContent: "Running tools",
entryKinds: ["use"],
},
{
id: "trace-2",
kind: "orphan",
assistantContent: "",
entryKinds: ["result"],
},
]);
});
});

View File

@@ -5,13 +5,7 @@
*/
import { esc } from "./utils.js";
import {
extractEventText,
renderAutomationIcon,
shouldHideAutomationUserEvent,
shouldStartAutomationWorkFromUserEvent,
} from "./automation.js";
import { applyTaskStateEvent, processAssistantEvent } from "./task-panel.js";
import { processAssistantEvent } from "./task-panel.js";
// ============================================================
// Replay state — tracks unresolved permission requests during history replay
@@ -19,117 +13,11 @@ import { applyTaskStateEvent, processAssistantEvent } from "./task-panel.js";
const replayPendingRequests = new Map(); // request_id → event data (unresolved)
const replayRespondedRequests = new Set(); // request_ids that have a response
const renderedUserUuids = new Set();
const traceHostElements = new Map(); // host_id → DOM refs for inline tool traces
export function createToolTraceState() {
return {
nextHostId: 1,
activeHostId: null,
hosts: [],
};
}
function cloneToolTraceState(state) {
return {
nextHostId: state.nextHostId,
activeHostId: state.activeHostId,
hosts: state.hosts.map((host) => ({
...host,
entryKinds: [...host.entryKinds],
})),
};
}
function createToolTraceHost(nextState, kind, assistantContent = "") {
const host = {
id: `trace-${nextState.nextHostId}`,
kind,
assistantContent,
entryKinds: [],
};
nextState.nextHostId += 1;
nextState.activeHostId = host.id;
nextState.hosts.push(host);
return host;
}
export function addAssistantToolTraceHost(state, content) {
const nextState = cloneToolTraceState(state);
const host = createToolTraceHost(nextState, "assistant", content);
return { state: nextState, host };
}
export function clearActiveToolTraceHost(state) {
if (!state.activeHostId) return state;
const nextState = cloneToolTraceState(state);
nextState.activeHostId = null;
return nextState;
}
export function addToolTraceEntry(state, entryKind) {
const nextState = cloneToolTraceState(state);
let host = nextState.hosts.find((item) => item.id === nextState.activeHostId);
let createdHost = null;
if (!host) {
createdHost = createToolTraceHost(nextState, "orphan");
host = createdHost;
}
host.entryKinds.push(entryKind);
return { state: nextState, host, createdHost };
}
let toolTraceState = createToolTraceState();
function resetToolTraceRuntime() {
toolTraceState = createToolTraceState();
traceHostElements.clear();
}
/** Clear replay tracking state (call before each history load) */
export function resetReplayState() {
replayPendingRequests.clear();
replayRespondedRequests.clear();
renderedUserUuids.clear();
resetToolTraceRuntime();
}
export function isConversationClearedStatus(payload) {
if (!payload || typeof payload !== "object") return false;
if (payload.status === "conversation_cleared") return true;
const raw = payload.raw;
return !!raw && typeof raw === "object" && raw.status === "conversation_cleared";
}
function clearTranscriptView() {
const stream = document.getElementById("event-stream");
if (!stream) return;
let preservedClearCommand = null;
for (let i = stream.children.length - 1; i >= 0; i -= 1) {
const row = stream.children[i];
if (!row || typeof row.textContent !== "string") continue;
if (row.textContent.trim() === "/clear") {
preservedClearCommand = row.cloneNode(true);
break;
}
}
stream.innerHTML = "";
if (preservedClearCommand) {
stream.appendChild(preservedClearCommand);
}
const permissionArea = document.getElementById("permission-area");
if (permissionArea) {
permissionArea.innerHTML = "";
permissionArea.classList.add("hidden");
}
removeLoading();
resetReplayState();
}
/** After replay finishes, render any still-unresolved permission prompts */
@@ -160,15 +48,27 @@ function truncate(str, max) {
* Server-side normalization guarantees payload.content is a string.
* Falls back to raw/message parsing for backward compat.
*/
export const extractText = extractEventText;
export function extractText(payload) {
if (!payload) return "";
function formatInlineContent(content) {
let html = esc(content);
// Inline code: `...`
html = html.replace(/`([^`]+)`/g, '<code style="background:var(--bg-tool-card);padding:2px 5px;border-radius:3px;font-family:var(--font-mono);font-size:0.85em;">$1</code>');
// Bold: **...**
html = html.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
return html;
// Normalized format (server standardized)
if (typeof payload.content === "string" && payload.content) return payload.content;
// Fallback: raw message.content (child process format)
const msg = payload.message;
if (msg && typeof msg === "object") {
const mc = msg.content;
if (typeof mc === "string") return mc;
if (Array.isArray(mc)) {
return mc
.filter((b) => b && typeof b === "object" && b.type === "text")
.map((b) => b.text || "")
.join("");
}
}
// Final fallback
return typeof payload === "string" ? payload : JSON.stringify(payload);
}
function formatAssistantContent(content) {
@@ -177,140 +77,13 @@ function formatAssistantContent(content) {
html = html.replace(/```(\w*)\n?([\s\S]*?)```/g, (_, lang, code) => {
return `<pre style="background:var(--bg-tool-card);padding:10px;border-radius:6px;overflow-x:auto;margin:6px 0;font-family:var(--font-mono);font-size:0.82rem;">${code.trim()}</pre>`;
});
html = formatInlineContent(html);
// Inline code: `...`
html = html.replace(/`([^`]+)`/g, '<code style="background:var(--bg-tool-card);padding:2px 5px;border-radius:3px;font-family:var(--font-mono);font-size:0.85em;">$1</code>');
// Bold: **...**
html = html.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
return html;
}
function renderPlanCodeBlock(code) {
return `<pre><code>${esc(code.trim())}</code></pre>`;
}
function formatPlanTextBlock(content) {
const blocks = [];
const lines = content.split(/\r?\n/);
let paragraph = [];
let listType = null;
let listItems = [];
function flushParagraph() {
if (paragraph.length === 0) return;
blocks.push(`<p>${paragraph.map(line => formatInlineContent(line)).join("<br>")}</p>`);
paragraph = [];
}
function flushList() {
if (!listType || listItems.length === 0) return;
blocks.push(`<${listType}>${listItems.map(item => `<li>${item}</li>`).join("")}</${listType}>`);
listType = null;
listItems = [];
}
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) {
flushParagraph();
flushList();
continue;
}
const headingMatch = trimmed.match(/^(#{1,6})\s+(.*)$/);
if (headingMatch) {
flushParagraph();
flushList();
const level = Math.min(headingMatch[1].length, 6);
blocks.push(`<h${level}>${formatInlineContent(headingMatch[2])}</h${level}>`);
continue;
}
const unorderedMatch = trimmed.match(/^[-*]\s+(.*)$/);
if (unorderedMatch) {
flushParagraph();
if (listType !== "ul") {
flushList();
listType = "ul";
}
listItems.push(formatInlineContent(unorderedMatch[1]));
continue;
}
const orderedMatch = trimmed.match(/^\d+\.\s+(.*)$/);
if (orderedMatch) {
flushParagraph();
if (listType !== "ol") {
flushList();
listType = "ol";
}
listItems.push(formatInlineContent(orderedMatch[1]));
continue;
}
flushList();
paragraph.push(trimmed);
}
flushParagraph();
flushList();
return blocks.join("");
}
export function formatPlanContent(content) {
const parts = [];
const codeBlockPattern = /```(\w*)\n?([\s\S]*?)```/g;
let lastIndex = 0;
let match;
while ((match = codeBlockPattern.exec(content)) !== null) {
const precedingText = content.slice(lastIndex, match.index);
if (precedingText.trim()) {
parts.push(formatPlanTextBlock(precedingText));
}
parts.push(renderPlanCodeBlock(match[2]));
lastIndex = codeBlockPattern.lastIndex;
}
const trailingText = content.slice(lastIndex);
if (trailingText.trim()) {
parts.push(formatPlanTextBlock(trailingText));
}
return parts.join("");
}
function getUserUuid(payload) {
if (!payload || typeof payload !== "object") return null;
if (typeof payload.uuid === "string" && payload.uuid) return payload.uuid;
if (payload.raw && typeof payload.raw === "object" && typeof payload.raw.uuid === "string" && payload.raw.uuid) {
return payload.raw.uuid;
}
return null;
}
function shouldProcessUserEvent(payload, direction) {
const uuid = getUserUuid(payload);
if (uuid) {
if (renderedUserUuids.has(uuid)) return false;
renderedUserUuids.add(uuid);
return true;
}
// Legacy fallback with no uuid: inbound human messages are usually echoes
// of a web-sent prompt, but hidden automation inputs still need to drive
// loading state and the session status marker.
return direction === "outbound" || shouldHideAutomationUserEvent(payload, direction);
}
function getMessageContentBlocks(payload) {
if (!payload || typeof payload !== "object") return [];
const msg = payload.message;
if (!msg || typeof msg !== "object" || !Array.isArray(msg.content)) return [];
return msg.content.filter((block) => block && typeof block === "object");
}
function getEmbeddedToolBlocks(payload, blockType) {
return getMessageContentBlocks(payload).filter((block) => block.type === blockType);
}
// ============================================================
// Event Router
// ============================================================
@@ -330,75 +103,26 @@ export function appendEvent(data, { replay = false } = {}) {
// During history replay, only render messages & tools — skip interactive/stateful events
// Exception: unresolved permission/control requests are re-shown as pending prompts.
if (replay) {
const histEls = [];
let histEl;
switch (type) {
case "user":
{
const toolResultBlocks = getEmbeddedToolBlocks(payload, "tool_result");
if (toolResultBlocks.length > 0) {
for (const block of toolResultBlocks) {
appendToolEntryToActiveTrace(
"result",
{
content: block.content || "",
output: block.content || "",
is_error: !!block.is_error,
},
histEls,
);
}
break;
}
if (shouldProcessUserEvent(payload, direction)) {
if (shouldHideAutomationUserEvent(payload, direction)) {
break;
}
toolTraceState = clearActiveToolTraceHost(toolTraceState);
histEls.push(renderUserMessage(payload, direction));
}
}
if (direction === "outbound") histEl = renderUserMessage(payload, direction);
break;
case "assistant":
{
const text = extractText(payload);
const toolUseBlocks = getEmbeddedToolBlocks(payload, "tool_use");
if (text && text.trim()) histEls.push(renderAssistantMessage(payload));
for (const block of toolUseBlocks) {
appendToolEntryToActiveTrace(
"use",
{
tool_name: block.name || "tool",
tool_input: block.input || {},
},
histEls,
);
}
if (text && text.trim()) histEl = renderAssistantMessage(payload);
processAssistantEvent(payload);
}
break;
case "task_state":
applyTaskStateEvent(payload);
return;
case "automation_state":
return;
case "status":
if (isConversationClearedStatus(payload)) {
clearTranscriptView();
}
return;
case "tool_use":
appendToolEntryToActiveTrace("use", payload, histEls);
histEl = renderToolUse(payload);
break;
case "tool_result":
appendToolEntryToActiveTrace("result", payload, histEls);
histEl = renderToolResult(payload);
break;
case "error":
histEls.push(renderSystemMessage(`Error: ${payload.message || payload.content || "Unknown error"}`));
break;
case "session_status":
if (payload.status === "archived" || payload.status === "inactive") {
histEls.push(renderSystemMessage(`Session ${payload.status}`));
}
histEl = renderSystemMessage(`Error: ${payload.message || payload.content || "Unknown error"}`);
break;
case "control_request":
case "permission_request":
@@ -425,84 +149,45 @@ export function appendEvent(data, { replay = false } = {}) {
default:
return;
}
for (const histEl of histEls) {
if (histEl) {
stream.appendChild(histEl);
stream.scrollTop = stream.scrollHeight;
}
return;
}
const els = [];
let el;
let needLoading = false;
switch (type) {
switch (type) {
case "user":
{
const toolResultBlocks = getEmbeddedToolBlocks(payload, "tool_result");
if (toolResultBlocks.length > 0) {
for (const block of toolResultBlocks) {
appendToolEntryToActiveTrace(
"result",
{
content: block.content || "",
output: block.content || "",
is_error: !!block.is_error,
},
els,
);
}
break;
}
if (!shouldProcessUserEvent(payload, direction)) return;
if (!shouldHideAutomationUserEvent(payload, direction)) {
toolTraceState = clearActiveToolTraceHost(toolTraceState);
els.push(renderUserMessage(payload, direction));
needLoading = true;
} else {
needLoading = shouldStartAutomationWorkFromUserEvent(payload, direction);
}
}
// Skip inbound user messages — they're echoes of what we already sent
if (direction === "inbound") return;
el = renderUserMessage(payload, direction);
needLoading = true;
break;
case "partial_assistant":
// Skip partial assistant — wait for the final "assistant" event
// to avoid blank/duplicate messages during streaming
return;
case "assistant":
removeLoading();
{
const text = extractText(payload);
const toolUseBlocks = getEmbeddedToolBlocks(payload, "tool_use");
if (text && text.trim()) {
removeLoading();
els.push(renderAssistantMessage(payload));
}
for (const block of toolUseBlocks) {
appendToolEntryToActiveTrace(
"use",
{
tool_name: block.name || "tool",
tool_input: block.input || {},
},
els,
);
}
if (text && text.trim()) el = renderAssistantMessage(payload);
processAssistantEvent(payload);
}
break;
case "task_state":
applyTaskStateEvent(payload);
return;
case "automation_state":
return;
case "result":
case "result_success":
removeLoading();
// Skip result — it just repeats the assistant message content
return;
case "tool_use":
appendToolEntryToActiveTrace("use", payload, els);
el = renderToolUse(payload);
break;
case "tool_result":
appendToolEntryToActiveTrace("result", payload, els);
el = renderToolResult(payload);
break;
case "control_request":
case "permission_request":
@@ -510,27 +195,27 @@ export function appendEvent(data, { replay = false } = {}) {
const toolName = payload.request.tool_name || "unknown";
const toolInput = payload.request.input || payload.request.tool_input || {};
if (toolName === "AskUserQuestion") {
els.push(renderAskUserQuestion({
el = renderAskUserQuestion({
request_id: payload.request_id || data.id,
tool_input: toolInput,
description: payload.request.description || "",
}));
});
} else if (toolName === "ExitPlanMode") {
els.push(renderExitPlanMode({
el = renderExitPlanMode({
request_id: payload.request_id || data.id,
tool_input: toolInput,
description: payload.request.description || "",
}));
});
} else {
els.push(renderPermissionRequest({
el = renderPermissionRequest({
request_id: payload.request_id || data.id,
tool_name: toolName,
tool_input: toolInput,
description: payload.request.description || "",
}));
});
}
} else {
els.push(renderSystemMessage(`Control: ${payload.request?.subtype || "unknown"}`));
el = renderSystemMessage(`Control: ${payload.request?.subtype || "unknown"}`);
}
break;
case "control_response":
@@ -539,31 +224,21 @@ export function appendEvent(data, { replay = false } = {}) {
return;
case "status":
// Skip connecting/waiting status noise from bridge
if (isConversationClearedStatus(payload)) {
clearTranscriptView();
return;
}
{
const msg = payload.message || payload.content || "";
const fullText = typeof payload === "string" ? payload : JSON.stringify(payload);
if (/connecting|waiting|initializing|Remote Control/i.test(msg + " " + fullText)) return;
if (!msg.trim()) return;
els.push(renderSystemMessage(msg));
el = renderSystemMessage(msg);
}
break;
case "error":
removeLoading();
els.push(renderSystemMessage(`Error: ${payload.message || payload.content || "Unknown error"}`));
break;
case "session_status":
if (payload.status === "archived" || payload.status === "inactive") {
removeLoading();
els.push(renderSystemMessage(`Session ${payload.status}`));
}
el = renderSystemMessage(`Error: ${payload.message || payload.content || "Unknown error"}`);
break;
case "interrupt":
removeLoading();
els.push(renderSystemMessage("Session interrupted"));
el = renderSystemMessage("Session interrupted");
break;
case "system":
// Skip raw system/init messages — they're noise
@@ -572,11 +247,11 @@ export function appendEvent(data, { replay = false } = {}) {
// Skip noise from bridge init
const raw = JSON.stringify(payload);
if (/Remote Control connecting/i.test(raw)) return;
els.push(renderSystemMessage(`${type}: ${truncate(raw, 200)}`));
el = renderSystemMessage(`${type}: ${truncate(raw, 200)}`);
}
}
for (const el of els) {
if (el) {
stream.appendChild(el);
stream.scrollTop = stream.scrollHeight;
}
@@ -597,90 +272,12 @@ function renderUserMessage(payload, direction) {
return row;
}
function renderTraceToggleGlyph() {
return `
<span class="assistant-trace-glyph" aria-hidden="true">
<span></span>
<span></span>
<span></span>
</span>`;
}
function bindTraceToggle(toggleEl, panelEl, traceEl) {
if (!toggleEl || !panelEl || !traceEl) return;
toggleEl.addEventListener("click", () => {
const expanded = toggleEl.getAttribute("aria-expanded") === "true";
toggleEl.setAttribute("aria-expanded", expanded ? "false" : "true");
toggleEl.classList.toggle("is-open", !expanded);
traceEl.classList.toggle("is-expanded", !expanded);
panelEl.classList.toggle("hidden", expanded);
});
}
function updateTraceHostDisplay(refs) {
if (!refs) return;
refs.traceEl.classList.toggle("hidden", refs.entryCount === 0);
refs.countEl.textContent = String(refs.entryCount);
refs.toggleEl.classList.toggle("has-error", refs.hasError);
refs.row.classList.toggle("has-tool-error", refs.hasError);
refs.toggleEl.title = refs.hasError ? "Tool trace (contains errors)" : "Tool trace";
}
function createTraceHostRow(host, content = "") {
const row = document.createElement("div");
row.className = host.kind === "assistant" ? "msg-row assistant" : "msg-row tool-trace-row";
row.dataset.traceHostId = host.id;
row.innerHTML = `
<div class="assistant-turn${host.kind === "orphan" ? " assistant-turn-orphan" : ""}">
${content ? `<div class="msg-bubble">${formatAssistantContent(content)}</div>` : ""}
<div class="assistant-trace hidden">
<button type="button" class="assistant-trace-toggle" aria-expanded="false">
${renderTraceToggleGlyph()}
<span class="assistant-trace-count">0</span>
<span class="assistant-trace-chevron" aria-hidden="true"></span>
</button>
<div class="assistant-trace-panel hidden"></div>
</div>
</div>`;
const traceEl = row.querySelector(".assistant-trace");
const panelEl = row.querySelector(".assistant-trace-panel");
const toggleEl = row.querySelector(".assistant-trace-toggle");
const countEl = row.querySelector(".assistant-trace-count");
bindTraceToggle(toggleEl, panelEl, traceEl);
const refs = {
hostId: host.id,
row,
traceEl,
panelEl,
toggleEl,
countEl,
entryCount: host.entryKinds.length,
hasError: false,
};
traceHostElements.set(host.id, refs);
updateTraceHostDisplay(refs);
return row;
}
function ensureTraceHostRow(host, rows = null, content = "") {
const existing = traceHostElements.get(host.id);
if (existing) return existing.row;
const row = createTraceHostRow(host, content || host.assistantContent || "");
if (Array.isArray(rows)) {
rows.push(row);
}
return row;
}
function renderAssistantMessage(payload) {
const content = extractText(payload).trim();
const result = addAssistantToolTraceHost(toolTraceState, content);
toolTraceState = result.state;
return ensureTraceHostRow(result.host, null, content);
const content = extractText(payload);
const row = document.createElement("div");
row.className = "msg-row assistant";
row.innerHTML = `<div class="msg-bubble">${formatAssistantContent(content)}</div>`;
return row;
}
function renderResult(payload) {
@@ -691,64 +288,37 @@ function renderResult(payload) {
return row;
}
function renderToolCard({ titleHtml, body, isError = false }) {
const card = document.createElement("div");
card.className = `tool-card assistant-trace-card${isError ? " assistant-trace-card-error" : ""}`;
card.innerHTML = `
<div class="tool-card-header">
<span class="tool-icon">&#9654;</span>
${titleHtml}
</div>
<div class="tool-card-body collapsed">${esc(body)}</div>`;
const header = card.querySelector(".tool-card-header");
const bodyEl = card.querySelector(".tool-card-body");
header?.addEventListener("click", () => {
bodyEl?.classList.toggle("collapsed");
header.classList.toggle("is-open", !bodyEl?.classList.contains("collapsed"));
});
return card;
}
function renderToolUse(payload) {
const name = payload.tool_name || payload.name || "tool";
const input = payload.tool_input || payload.input || {};
const inputStr = typeof input === "string" ? input : JSON.stringify(input, null, 2);
return renderToolCard({
titleHtml: `Tool: <strong>${esc(name)}</strong>`,
body: inputStr || "",
});
const card = document.createElement("div");
card.className = "msg-row tool";
card.innerHTML = `
<div class="tool-card">
<div class="tool-card-header" onclick="this.nextElementSibling.classList.toggle('collapsed')">
<span class="tool-icon">&#9654;</span> Tool: <strong>${esc(name)}</strong>
</div>
<div class="tool-card-body collapsed">${esc(truncate(inputStr, 2000))}</div>
</div>`;
return card;
}
function renderToolResult(payload) {
const content = payload.content || payload.output || "";
const contentStr = typeof content === "string" ? content : JSON.stringify(content, null, 2);
return renderToolCard({
titleHtml: payload.is_error ? "<strong>Tool Error</strong>" : "Tool Result",
body: contentStr || "",
isError: !!payload.is_error,
});
}
function appendToolEntryToActiveTrace(entryKind, payload, rows) {
const result = addToolTraceEntry(toolTraceState, entryKind);
toolTraceState = result.state;
if (result.createdHost) {
ensureTraceHostRow(result.createdHost, rows);
}
const refs = traceHostElements.get(result.host.id);
if (!refs) return;
const card = entryKind === "use" ? renderToolUse(payload) : renderToolResult(payload);
refs.panelEl.appendChild(card);
refs.entryCount += 1;
if (entryKind === "result" && payload.is_error) {
refs.hasError = true;
}
updateTraceHostDisplay(refs);
const card = document.createElement("div");
card.className = "msg-row tool";
card.innerHTML = `
<div class="tool-card">
<div class="tool-card-header" onclick="this.nextElementSibling.classList.toggle('collapsed')">
<span class="tool-icon">&#9654;</span> Tool Result
</div>
<div class="tool-card-body collapsed">${esc(truncate(contentStr, 2000))}</div>
</div>`;
return card;
}
export function renderPermissionRequest(payload) {
@@ -859,9 +429,7 @@ export function renderAskUserQuestion(payload) {
el._answers = {};
el._questions = questions;
const status = renderSystemMessage("Waiting for your response...");
status.dataset.pendingRequestId = requestId;
return status;
return renderSystemMessage("Waiting for your response...");
}
export function renderExitPlanMode(payload) {
@@ -896,7 +464,7 @@ export function renderExitPlanMode(payload) {
} else {
el.innerHTML = `
<div class="plan-title">Ready to code?</div>
<div class="plan-content">${formatPlanContent(planContent)}</div>
<div class="plan-content">${formatAssistantContent(planContent)}</div>
<div class="plan-options">
<button class="plan-option" data-value="yes-accept-edits" onclick="window._selectPlanOption(this, 'yes-accept-edits')">
<span class="plan-option-label">Yes, auto-accept edits</span>
@@ -925,9 +493,7 @@ export function renderExitPlanMode(payload) {
el._planContent = planContent;
el._isEmpty = isEmpty;
const status = renderSystemMessage("Waiting for your response...");
status.dataset.pendingRequestId = requestId;
return status;
return renderSystemMessage("Waiting for your response...");
}
function renderSystemMessage(text) {
@@ -941,10 +507,10 @@ function renderSystemMessage(text) {
// Loading Indicator — TUI star spinner style
// ============================================================
const ACTIVITY_ID = "session-activity-indicator";
const LOADING_ID = "loading-indicator";
// TUI star spinner frames (same as Claude Code CLI)
const SPINNER_FRAMES = ["·", "✢", "", "✶", "✻", "✽"];
const SPINNER_FRAMES = ["·", "✢", "", "✶", "✻", "✽"];
const SPINNER_CYCLE = [...SPINNER_FRAMES, ...SPINNER_FRAMES.slice().reverse()];
// 204 verbs from TUI src/constants/spinnerVerbs.ts
@@ -987,85 +553,35 @@ const SPINNER_VERBS = [
let spinnerInterval = null;
let timerInterval = null;
let stalledCheckInterval = null;
let activityCountdownInterval = null;
let spinnerFrame = 0;
let loadingStartTime = 0;
let lastActivityTime = 0;
let isStalled = false;
let workingActive = false;
let automationActivity = null;
export function resolveActivityMode(working, activity) {
if (activity?.mode === "standby" || activity?.mode === "sleeping") {
return activity.mode;
}
return working ? "working" : "idle";
}
export function shouldRenderTranscriptActivity(mode) {
return mode === "working";
}
export function formatCountdownRemaining(endsAt, now = Date.now()) {
if (typeof endsAt !== "number") return "";
const remainingSeconds = Math.max(0, Math.ceil((endsAt - now) / 1000));
const hours = Math.floor(remainingSeconds / 3600);
const minutes = Math.floor((remainingSeconds % 3600) / 60);
const seconds = remainingSeconds % 60;
if (hours > 0) {
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
}
if (minutes > 0) {
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
}
return `${seconds}s`;
}
function getActivityModeInternal() {
return resolveActivityMode(workingActive, automationActivity);
}
let loadingActive = false;
export function isLoading() {
return getActivityModeInternal() === "working";
return loadingActive;
}
export function getActivityMode() {
return getActivityModeInternal();
function syncActionBtn(state) {
if (typeof window.__updateActionBtn === "function") window.__updateActionBtn(state);
}
function syncActionBtn(mode) {
if (typeof window.__updateActionBtn === "function") window.__updateActionBtn(mode);
}
export function showLoading() {
removeLoading();
const stream = document.getElementById("event-stream");
if (!stream) return;
function clearWorkingTimers() {
if (spinnerInterval) { clearInterval(spinnerInterval); spinnerInterval = null; }
if (timerInterval) { clearInterval(timerInterval); timerInterval = null; }
if (stalledCheckInterval) { clearInterval(stalledCheckInterval); stalledCheckInterval = null; }
isStalled = false;
}
loadingActive = true;
syncActionBtn(true);
function clearActivityCountdownTimer() {
if (activityCountdownInterval) {
clearInterval(activityCountdownInterval);
activityCountdownInterval = null;
}
}
function removeActivityElement() {
const el = document.getElementById(ACTIVITY_ID);
if (el) el.remove();
}
function renderWorkingIndicator(stream) {
const verb = SPINNER_VERBS[Math.floor(Math.random() * SPINNER_VERBS.length)];
loadingStartTime = Date.now();
lastActivityTime = Date.now();
isStalled = false;
const el = document.createElement("div");
el.id = ACTIVITY_ID;
el.id = LOADING_ID;
el.className = "msg-row loading-row";
el.innerHTML = `<span class="tui-spinner">${SPINNER_CYCLE[0]}</span><span class="tui-verb glimmer-text">${esc(verb)}…</span><span class="tui-timer">0s</span>`;
stream.appendChild(el);
@@ -1075,12 +591,14 @@ function renderWorkingIndicator(stream) {
const timerEl = el.querySelector(".tui-timer");
const loadingEl = el;
// Spinner animation — 120ms interval, same as TUI
spinnerFrame = 0;
spinnerInterval = setInterval(() => {
spinnerFrame = (spinnerFrame + 1) % SPINNER_CYCLE.length;
if (spinnerEl) spinnerEl.textContent = SPINNER_CYCLE[spinnerFrame];
}, 120);
// Timer — update every second
timerInterval = setInterval(() => {
if (timerEl) {
const elapsed = Math.floor((Date.now() - loadingStartTime) / 1000);
@@ -1088,6 +606,7 @@ function renderWorkingIndicator(stream) {
}
}, 1000);
// Stalled detection — check every 120ms (aligned with spinner)
stalledCheckInterval = setInterval(() => {
if (!isStalled && Date.now() - lastActivityTime > 3000) {
isStalled = true;
@@ -1096,62 +615,15 @@ function renderWorkingIndicator(stream) {
}, 120);
}
function renderAutomationIndicator(stream, activity) {
const el = document.createElement("div");
el.id = ACTIVITY_ID;
el.className = `msg-row automation-activity-row automation-activity-${activity.mode}`;
el.innerHTML = `
<div class="automation-activity-card">
${renderAutomationIcon(activity.iconVariant, { className: "automation-activity-icon" })}
<div class="automation-activity-copy">
<span class="automation-activity-label">${esc(activity.label)}</span>
</div>
<span class="automation-activity-countdown"></span>
</div>`;
stream.appendChild(el);
stream.scrollTop = stream.scrollHeight;
const countdownEl = el.querySelector(".automation-activity-countdown");
const updateCountdown = () => {
if (countdownEl) {
countdownEl.textContent = formatCountdownRemaining(activity.endsAt);
}
};
updateCountdown();
activityCountdownInterval = setInterval(updateCountdown, 1000);
}
function renderActivityIndicator() {
clearWorkingTimers();
clearActivityCountdownTimer();
removeActivityElement();
const mode = getActivityModeInternal();
syncActionBtn(mode);
const stream = document.getElementById("event-stream");
if (!stream) return;
if (shouldRenderTranscriptActivity(mode)) {
renderWorkingIndicator(stream);
}
}
export function setAutomationActivity(activity) {
automationActivity = activity ? { ...activity } : null;
renderActivityIndicator();
}
export function showLoading() {
automationActivity = null;
workingActive = true;
renderActivityIndicator();
}
export function removeLoading() {
workingActive = false;
renderActivityIndicator();
if (spinnerInterval) { clearInterval(spinnerInterval); spinnerInterval = null; }
if (timerInterval) { clearInterval(timerInterval); timerInterval = null; }
if (stalledCheckInterval) { clearInterval(stalledCheckInterval); stalledCheckInterval = null; }
isStalled = false;
loadingActive = false;
syncActionBtn(false);
const el = document.getElementById(LOADING_ID);
if (el) el.remove();
}
/** Reset stalled timer — call when SSE events arrive */
@@ -1159,7 +631,7 @@ export function refreshLoadingActivity() {
lastActivityTime = Date.now();
if (isStalled) {
isStalled = false;
const loadingEl = document.getElementById(ACTIVITY_ID);
const loadingEl = document.getElementById(LOADING_ID);
if (loadingEl) loadingEl.classList.remove("stalled");
}
}

Some files were not shown because too many files have changed in this diff Show More