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: jobs:
ci: ci:
runs-on: ubuntu-latest runs-on: macos-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -23,16 +23,8 @@ jobs:
- name: Type check - name: Type check
run: bunx tsc --noEmit run: bunx tsc --noEmit
- name: Test with Coverage - name: Test
run: | run: bun test
set -o pipefail
bun test --coverage --coverage-reporter=lcov 2>&1 | grep -vE '^\s*(\(pass\)|\(skip\))' | sed '/^.*\/__tests__\/.*:$/d' | cat -s
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
file: ./coverage/lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
- name: Build - name: Build
run: bun run build:vite run: bun run build

10
.gitignore vendored
View File

@@ -13,6 +13,7 @@ src/utils/vendor/
# AI tool runtime directories # AI tool runtime directories
.agents/ .agents/
.claude/ .claude/
.codex/
.omx/ .omx/
.docs/task/ .docs/task/
# Binary / screenshot files (root only) # Binary / screenshot files (root only)
@@ -29,12 +30,3 @@ __pycache__/
logs logs
data 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 # Check unused exports
bun run check:unused bun run check:unused
bun run typecheck
# Remote Control Server # Remote Control Server
bun run rcs bun run rcs
@@ -247,23 +245,14 @@ Feature flags control which functionality is enabled at runtime. 代码中统一
## Testing ## Testing
- **框架**: `bun:test`(内置断言 + mock - **框架**: `bun:test`(内置断言 + mock
- **当前状态**: 2992 tests / 188 files / 0 fail - **当前状态**: 2472 tests / 138 files / 0 fail
- **单元测试**: 就近放置于 `src/**/__tests__/`,文件名 `<module>.test.ts` - **单元测试**: 就近放置于 `src/**/__tests__/`,文件名 `<module>.test.ts`
- **集成测试**: `tests/integration/` — 4 个文件cli-arguments, context-build, message-pipeline, tool-chain - **集成测试**: `tests/integration/` — 4 个文件cli-arguments, context-build, message-pipeline, tool-chain
- **共享 mock/fixture**: `tests/mocks/`api-responses, file-system, fixtures/ - **共享 mock/fixture**: `tests/mocks/`api-responses, file-system, fixtures/
- **命名**: `describe("functionName")` + `test("behavior description")`,英文 - **命名**: `describe("functionName")` + `test("behavior description")`,英文
- **Mock 模式**: 对重依赖模块使用 `mock.module()` + `await import()` 解锁(必须内联在测试文件中,不能从共享 helper 导入)
- **包测试**: `packages/` 下各包也有独立测试(如 `color-diff-napi` 11 tests - **包测试**: `packages/` 下各包也有独立测试(如 `color-diff-napi` 11 tests
### Mock 使用规范
**只 mock 有副作用的依赖链,不 mock 纯函数/纯数据模块。**
被迫 mock 的根源:`log.ts` / `debug.ts``bootstrap/state.ts`(模块级 `realpathSync` / `randomUUID` 副作用)。必须 mock 的模块:`log.ts``debug.ts``bun:bundle``settings/settings.js``config.ts``auth.ts`、第三方网络库。
不要 mock纯函数模块`errors.ts``stringUtils.js`、mock 值与真实实现相同的模块、mock 路径与实际 import 不匹配的模块。
路径规则:统一用 `.ts` 扩展名 + `src/*` 别名路径,禁止双重 mock 同一模块。
### 类型检查 ### 类型检查
项目使用 TypeScript strict 模式,**tsc 必须零错误**。每次修改后运行: 项目使用 TypeScript strict 模式,**tsc 必须零错误**。每次修改后运行:

View File

@@ -10,25 +10,27 @@
> Which Claude do you like? The open source one is the best. > Which Claude do you like? The open source one is the best.
牢 A (Anthropic) 官方 [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI 工具的源码反编译/逆向还原项目。目标是将 Claude Code 大部分功能及工程化能力复现 (问就是老佛爷已经付过钱了)。虽然很难绷, 但是它叫做 CCB(踩踩背)... 而且, 我们实现了企业版或者需要登陆 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) [文档在这里, 支持投稿 PR](https://ccb.agent-aura.top/) | [留影文档在这里](./Friends.md) | [Discord 群组](https://discord.gg/qZU6zS7Q)
| 特性 | 说明 | 文档 | | 特性 | 说明 | 文档 |
|------|------|------| |------|------|------|
| **Claude 群控技术** | Pipe IPC 多实例协作:同机 main/sub 自动编排 + LAN 跨机器零配置发现与通讯,`/pipes` 选择面板 + `Shift+↓` 交互 + 消息广播路由 | [Pipe IPC](https://ccb.agent-aura.top/docs/features/pipes-and-lan) / [LAN](https://ccb.agent-aura.top/docs/features/lan-pipes) | | **Claude 群控技术** | Pipe IPC 多实例协作:同机 main/sub 自动编排 + LAN 跨机器零配置发现与通讯,`/pipes` 选择面板 + `Shift+↓` 交互 + 消息广播路由 | [Pipe IPC](https://ccb.agent-aura.top/docs/features/pipes-and-lan) / [LAN](https://ccb.agent-aura.top/docs/features/lan-pipes) |
| **ACP 协议一等一支持** | 支持接入 Zed、Cursor 等 IDE支持会话恢复、Skills、权限桥接 | [文档](https://ccb.agent-aura.top/docs/features/acp-zed) | | Remote Control 私有部署 | Docker 自托管 RCS + Web UI | [文档](https://ccb.agent-aura.top/docs/features/remote-control-self-hosting) |
| **Remote Control 私有部署** | Docker 自托管远程界面, 可以手机上看 CC | [文档](https://ccb.agent-aura.top/docs/features/remote-control-self-hosting) | | /dream 记忆整理 | 自动整理和优化记忆文件 | [文档](https://ccb.agent-aura.top/docs/features/auto-dream) |
| **Langfuse 监控** | 企业级 Agent 监控, 可以清晰看到每次 agent loop 细节, 可以一键转化为数据集 | [文档](https://ccb.agent-aura.top/docs/features/langfuse-monitoring) | | Web Search | 内置网页搜索工具 | [文档](https://ccb.agent-aura.top/docs/features/web-browser-tool) |
| **Web Search** | 内置网页搜索工具, 支持 bing 和 brave 搜索 | [文档](https://ccb.agent-aura.top/docs/features/web-browser-tool) | | 自定义模型供应商 | OpenAI/Anthropic/Gemini/Grok 兼容 | [文档](https://ccb.agent-aura.top/docs/features/custom-platform-login) |
| **Poor Mode** | 穷鬼模式,关闭记忆提取和键入建议,大幅度减少并发请求 | /poor 可以开关 |
| **自定义模型供应商** | OpenAI/Anthropic/Gemini/Grok 兼容 | [文档](https://ccb.agent-aura.top/docs/features/custom-platform-login) |
| Voice Mode | Push-to-Talk 语音输入 | [文档](https://ccb.agent-aura.top/docs/features/voice-mode) | | Voice Mode | Push-to-Talk 语音输入 | [文档](https://ccb.agent-aura.top/docs/features/voice-mode) |
| Computer Use | 屏幕截图、键鼠控制 | [文档](https://ccb.agent-aura.top/docs/features/computer-use) | | Computer Use | 屏幕截图、键鼠控制 | [文档](https://ccb.agent-aura.top/docs/features/computer-use) |
| Chrome Use | 浏览器自动化、表单填写、数据抓取 | [自托管](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) | | Sentry | 企业级错误追踪 | [文档](https://ccb.agent-aura.top/docs/internals/sentry-setup) |
| GrowthBook | 企业级特性开关 | [文档](https://ccb.agent-aura.top/docs/internals/growthbook-adapter) | | GrowthBook | 企业级特性开关 | [文档](https://ccb.agent-aura.top/docs/internals/growthbook-adapter) |
| /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-调试) - 🐛 [想要调试项目](#vs-code-调试)

View File

@@ -30,8 +30,6 @@ const DEFAULT_BUILD_FEATURES = [
'ULTRAPLAN', 'ULTRAPLAN',
// P2: daemon + remote control server // P2: daemon + remote control server
'DAEMON', 'DAEMON',
// ACP (Agent Client Protocol) agent mode
'ACP',
// PR-package restored features // PR-package restored features
'WORKFLOW_SCRIPTS', 'WORKFLOW_SCRIPTS',
'HISTORY_SNIP', 'HISTORY_SNIP',
@@ -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( 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) // 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(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 // Make both executable
const { chmodSync } = await import('fs') const { chmodSync } = await import('fs')

119
bun.lock
View File

@@ -5,9 +5,7 @@
"": { "": {
"name": "claude-code-best", "name": "claude-code-best",
"dependencies": { "dependencies": {
"@agentclientprotocol/sdk": "^0.19.0", "@claude-code-best/mcp-chrome-bridge": "^2.0.7",
"@claude-code-best/mcp-chrome-bridge": "^2.0.8",
"ws": "^8.20.0",
}, },
"devDependencies": { "devDependencies": {
"@alcalzone/ansi-tokenize": "^0.3.0", "@alcalzone/ansi-tokenize": "^0.3.0",
@@ -15,7 +13,6 @@
"@ant/computer-use-input": "workspace:*", "@ant/computer-use-input": "workspace:*",
"@ant/computer-use-mcp": "workspace:*", "@ant/computer-use-mcp": "workspace:*",
"@ant/computer-use-swift": "workspace:*", "@ant/computer-use-swift": "workspace:*",
"@ant/model-provider": "workspace:*",
"@anthropic-ai/bedrock-sdk": "^0.26.4", "@anthropic-ai/bedrock-sdk": "^0.26.4",
"@anthropic-ai/claude-agent-sdk": "^0.2.87", "@anthropic-ai/claude-agent-sdk": "^0.2.87",
"@anthropic-ai/foundry-sdk": "^0.2.3", "@anthropic-ai/foundry-sdk": "^0.2.3",
@@ -60,11 +57,10 @@
"@sentry/node": "^10.47.0", "@sentry/node": "^10.47.0",
"@smithy/core": "^3.23.13", "@smithy/core": "^3.23.13",
"@smithy/node-http-handler": "^4.5.1", "@smithy/node-http-handler": "^4.5.1",
"@types/bun": "^1.3.12", "@types/bun": "^1.3.11",
"@types/cacache": "^20.0.1", "@types/cacache": "^20.0.1",
"@types/he": "^1.2.3", "@types/he": "^1.2.3",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^25.6.0",
"@types/picomatch": "^4.0.3", "@types/picomatch": "^4.0.3",
"@types/plist": "^3.0.5", "@types/plist": "^3.0.5",
"@types/proper-lockfile": "^4.1.4", "@types/proper-lockfile": "^4.1.4",
@@ -120,7 +116,6 @@
"react": "^19.2.4", "react": "^19.2.4",
"react-compiler-runtime": "^1.0.0", "react-compiler-runtime": "^1.0.0",
"react-reconciler": "^0.33.0", "react-reconciler": "^0.33.0",
"rollup": "^4.60.1",
"semver": "^7.7.4", "semver": "^7.7.4",
"sharp": "^0.34.5", "sharp": "^0.34.5",
"shell-quote": "^1.8.3", "shell-quote": "^1.8.3",
@@ -135,11 +130,11 @@
"undici": "^7.24.6", "undici": "^7.24.6",
"url-handler-napi": "workspace:*", "url-handler-napi": "workspace:*",
"usehooks-ts": "^3.1.1", "usehooks-ts": "^3.1.1",
"vite": "^8.0.8",
"vscode-jsonrpc": "^8.2.1", "vscode-jsonrpc": "^8.2.1",
"vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-protocol": "^3.17.5",
"vscode-languageserver-types": "^3.17.5", "vscode-languageserver-types": "^3.17.5",
"wrap-ansi": "^10.0.0", "wrap-ansi": "^10.0.0",
"ws": "^8.20.0",
"xss": "^1.0.15", "xss": "^1.0.15",
"yaml": "^2.8.3", "yaml": "^2.8.3",
"zod": "^4.3.6", "zod": "^4.3.6",
@@ -184,14 +179,6 @@
"wrap-ansi": "^10.0.0", "wrap-ansi": "^10.0.0",
}, },
}, },
"packages/@ant/model-provider": {
"name": "@ant/model-provider",
"version": "1.0.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.80.0",
"openai": "^6.33.0",
},
},
"packages/agent-tools": { "packages/agent-tools": {
"name": "@claude-code-best/agent-tools", "name": "@claude-code-best/agent-tools",
"version": "1.0.0", "version": "1.0.0",
@@ -266,8 +253,6 @@
}, },
}, },
"packages": { "packages": {
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.19.0", "https://registry.npmmirror.com/@agentclientprotocol/sdk/-/sdk-0.19.0.tgz", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-U9I8ws9WTOk6jCBAWpXefGSDgVXn14/kV6HFzwWGcstQ02mOQgClMAROHmoIn9GqZbDBDEOkdIbP4P4TEMQdug=="],
"@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.3.0", "https://registry.npmmirror.com/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.3.0.tgz", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-p+CMKJ93HFmLkjXKlXiVGlMQEuRb6H0MokBSwUsX+S6BRX8eV5naFZpQJFfJHjRZY0Hmnqy1/r6UWl3x+19zYA=="], "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.3.0", "https://registry.npmmirror.com/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.3.0.tgz", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-p+CMKJ93HFmLkjXKlXiVGlMQEuRb6H0MokBSwUsX+S6BRX8eV5naFZpQJFfJHjRZY0Hmnqy1/r6UWl3x+19zYA=="],
"@ant/claude-for-chrome-mcp": ["@ant/claude-for-chrome-mcp@workspace:packages/@ant/claude-for-chrome-mcp"], "@ant/claude-for-chrome-mcp": ["@ant/claude-for-chrome-mcp@workspace:packages/@ant/claude-for-chrome-mcp"],
@@ -278,8 +263,6 @@
"@ant/computer-use-swift": ["@ant/computer-use-swift@workspace:packages/@ant/computer-use-swift"], "@ant/computer-use-swift": ["@ant/computer-use-swift@workspace:packages/@ant/computer-use-swift"],
"@ant/model-provider": ["@ant/model-provider@workspace:packages/@ant/model-provider"],
"@anthropic-ai/bedrock-sdk": ["@anthropic-ai/bedrock-sdk@0.26.4", "https://registry.npmmirror.com/@anthropic-ai/bedrock-sdk/-/bedrock-sdk-0.26.4.tgz", { "dependencies": { "@anthropic-ai/sdk": ">=0.50.3 <1", "@aws-crypto/sha256-js": "^4.0.0", "@aws-sdk/client-bedrock-runtime": "^3.797.0", "@aws-sdk/credential-providers": "^3.796.0", "@smithy/eventstream-serde-node": "^2.0.10", "@smithy/fetch-http-handler": "^5.0.4", "@smithy/protocol-http": "^3.0.6", "@smithy/signature-v4": "^3.1.1", "@smithy/smithy-client": "^2.1.9", "@smithy/types": "^2.3.4", "@smithy/util-base64": "^2.0.0" } }, "sha512-0Z2NY3T2wnzT9esRit6BiWpQXvL+F2b3z3Z9in3mXh7MDf122rVi2bcPowQHmo9ITXAPJmv/3H3t0V1z3Fugfw=="], "@anthropic-ai/bedrock-sdk": ["@anthropic-ai/bedrock-sdk@0.26.4", "https://registry.npmmirror.com/@anthropic-ai/bedrock-sdk/-/bedrock-sdk-0.26.4.tgz", { "dependencies": { "@anthropic-ai/sdk": ">=0.50.3 <1", "@aws-crypto/sha256-js": "^4.0.0", "@aws-sdk/client-bedrock-runtime": "^3.797.0", "@aws-sdk/credential-providers": "^3.796.0", "@smithy/eventstream-serde-node": "^2.0.10", "@smithy/fetch-http-handler": "^5.0.4", "@smithy/protocol-http": "^3.0.6", "@smithy/signature-v4": "^3.1.1", "@smithy/smithy-client": "^2.1.9", "@smithy/types": "^2.3.4", "@smithy/util-base64": "^2.0.0" } }, "sha512-0Z2NY3T2wnzT9esRit6BiWpQXvL+F2b3z3Z9in3mXh7MDf122rVi2bcPowQHmo9ITXAPJmv/3H3t0V1z3Fugfw=="],
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.104", "https://registry.npmmirror.com/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.104.tgz", { "dependencies": { "@anthropic-ai/sdk": "^0.81.0", "@modelcontextprotocol/sdk": "^1.29.0" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-lVm+nS79r6WWlDnv5AgRzTtAlbP8O6M6kkWmDZAWE3nt9agmngxls9frJFvH55uzws2+6l0yyup/JYspfijkzw=="], "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.104", "https://registry.npmmirror.com/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.104.tgz", { "dependencies": { "@anthropic-ai/sdk": "^0.81.0", "@modelcontextprotocol/sdk": "^1.29.0" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-lVm+nS79r6WWlDnv5AgRzTtAlbP8O6M6kkWmDZAWE3nt9agmngxls9frJFvH55uzws2+6l0yyup/JYspfijkzw=="],
@@ -460,7 +443,7 @@
"@claude-code-best/builtin-tools": ["@claude-code-best/builtin-tools@workspace:packages/builtin-tools"], "@claude-code-best/builtin-tools": ["@claude-code-best/builtin-tools@workspace:packages/builtin-tools"],
"@claude-code-best/mcp-chrome-bridge": ["@claude-code-best/mcp-chrome-bridge@2.0.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"], "@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=="], "@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/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=="], "@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=="], "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "https://registry.npmmirror.com/@protobufjs/utf8/-/utf8-1.1.0.tgz", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", { "os": "android", "cpu": "arm64" }, "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz", { "os": "linux", "cpu": "arm" }, "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz", { "os": "linux", "cpu": "x64" }, "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz", { "os": "linux", "cpu": "x64" }, "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz", { "os": "none", "cpu": "arm64" }, "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg=="],
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz", { "dependencies": { "@emnapi/core": "1.9.2", "@emnapi/runtime": "1.9.2", "@napi-rs/wasm-runtime": "^1.1.3" }, "cpu": "none" }, "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz", { "os": "win32", "cpu": "x64" }, "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", { "os": "android", "cpu": "arm" }, "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", { "os": "android", "cpu": "arm" }, "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA=="],
@@ -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=="], "@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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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-deep-equal": ["fast-deep-equal@3.1.3", "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-glob": ["fast-glob@3.3.3", "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], "fast-glob": ["fast-glob@3.3.3", "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
"fast-json-stringify": ["fast-json-stringify@6.3.0", "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=="], "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=="], "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=="], "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=="], "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=="], "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-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=="], "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=="], "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=="], "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=="], "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=="], "retry": ["retry@0.12.0", "https://registry.npmmirror.com/retry/-/retry-0.12.0.tgz", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
"reusify": ["reusify@1.1.0", "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], "reusify": ["reusify@1.1.0", "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"rfdc": ["rfdc@1.4.1", "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
"rolldown": ["rolldown@1.0.0-rc.15", "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.0-rc.15.tgz", { "dependencies": { "@oxc-project/types": "=0.124.0", "@rolldown/pluginutils": "1.0.0-rc.15" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.15", "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", "@rolldown/binding-darwin-x64": "1.0.0-rc.15", "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g=="],
"rollup": ["rollup@4.60.1", "https://registry.npmmirror.com/rollup/-/rollup-4.60.1.tgz", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w=="], "rollup": ["rollup@4.60.1", "https://registry.npmmirror.com/rollup/-/rollup-4.60.1.tgz", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w=="],
@@ -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-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=="], "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=="], "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=="], "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-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=="], "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=="], "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=="], "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=="], "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=="], "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/typescript": ["typescript@5.9.3", "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"@anthropic/remote-control-server/vite": ["vite@6.4.2", "https://registry.npmmirror.com/vite/-/vite-6.4.2.tgz", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ=="],
"@aws-crypto/crc32/@aws-crypto/util": ["@aws-crypto/util@5.2.0", "https://registry.npmmirror.com/@aws-crypto/util/-/util-5.2.0.tgz", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], "@aws-crypto/crc32/@aws-crypto/util": ["@aws-crypto/util@5.2.0", "https://registry.npmmirror.com/@aws-crypto/util/-/util-5.2.0.tgz", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
"@aws-crypto/sha256-browser/@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "https://registry.npmmirror.com/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], "@aws-crypto/sha256-browser/@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "https://registry.npmmirror.com/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="],
@@ -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/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=="], "@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=="], "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=="], "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=="], "qrcode/yargs": ["yargs@15.4.1", "https://registry.npmmirror.com/yargs/-/yargs-15.4.1.tgz", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="],
"rolldown/@oxc-project/types": ["@oxc-project/types@0.124.0", "https://registry.npmmirror.com/@oxc-project/types/-/types-0.124.0.tgz", {}, "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg=="],
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", {}, "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g=="],
"vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "https://registry.npmmirror.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="], "vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "https://registry.npmmirror.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
"xss/commander": ["commander@2.20.3", "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "xss/commander": ["commander@2.20.3", "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

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

View File

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

View File

@@ -1,7 +1,7 @@
# PROACTIVE — 主动模式 # PROACTIVE — 主动模式
> Feature Flag: `FEATURE_PROACTIVE=1`(与 `FEATURE_KAIROS=1` 共享功能) > Feature Flag: `FEATURE_PROACTIVE=1`(与 `FEATURE_KAIROS=1` 共享功能)
> 实现状态:核心循环与 SleepTool 已落地,部分外围文档仍在补齐 > 实现状态:核心模块全部 Stub布线完整
> 引用数37 > 引用数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` | | SleepTool 提示 | `src/tools/SleepTool/prompt.ts` | **完整** | 工具提示定义(工具名:`Sleep` |
| 命令注册 | `src/commands.ts:62-65` | **布线** | 动态加载 `./commands/proactive.js` | | 命令注册 | `src/commands.ts:62-65` | **布线** | 动态加载 `./commands/proactive.js` |
| 工具注册 | `src/tools.ts:26-28` | **布线** | SleepTool 动态加载 | | 工具注册 | `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/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 系统提示内容 ### 2.2 系统提示内容
@@ -46,7 +46,7 @@ PROACTIVE 实现 Tick 驱动的自主代理。CLI 在用户不输入时也能持
### 2.3 数据流 ### 2.3 数据流
``` ```
activateProactive() activateProactive() [需要实现]
Tick 调度器启动 Tick 调度器启动
@@ -62,22 +62,20 @@ Tick 调度器启动
└── 无事可做 → 必须调用 SleepTool └── 无事可做 → 必须调用 SleepTool
SleepTool 等待 SleepTool 等待 [需要实现]
├── 用户插入新工作 / 队列中有命令 → 立即唤醒
├── proactive 被关闭 → 立即中断
└── 进入休眠时向远端 surfaces 上报 `automation_state = sleeping`
下一个 tick 到达 下一个 tick 到达
``` ```
## 三、当前行为补充 ## 三、需要补全的内容
- `standby`proactive 已开启,当前没有执行中的 turn且已调度下一个 tick。 | 优先级 | 模块 | 工作量 | 说明 |
- `sleeping`:模型显式调用 `SleepTool` 进入等待窗口。 |--------|------|--------|------|
- remote-control/CCR 通过 `external_metadata.automation_state` 接收这两个状态,用于 Web UI 的 Autopilot 状态显示。 | 1 | `src/proactive/index.ts` | 中 | Tick 调度器、activate/deactivate 状态机、pause/resume |
- `SleepTool` 现在不是纯定时器;它会在共享命令队列出现新工作时提前醒来。 | 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/prompt.ts` | SleepTool 工具提示 |
| `src/tools/SleepTool/SleepTool.ts` | 休眠/唤醒执行逻辑 |
| `src/constants/prompts.ts:860-914` | 自主工作系统提示 | | `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/sessionStorage.ts:4892-4912` | Tick 消息注入 |
| `src/utils/sessionState.ts` | bridge/CCR metadata 镜像 |
| `src/components/PromptInput/PromptInputFooterLeftSide.tsx` | 页脚 UI 状态 | | `src/components/PromptInput/PromptInputFooterLeftSide.tsx` | 页脚 UI 状态 |

View File

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

View File

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

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

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)) if (displayId !== undefined) args.push('-D', String(displayId))
return captureScreenToBase64(args) return captureScreenToBase64(args)
}, },
captureWindowTarget(_titleOrHwnd: string | number): ScreenshotResult | null {
// Window capture not supported on macOS via this backend
return null
},
} }

View File

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

View File

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

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 { describe, expect, test } from 'bun:test'
import type { CoreTool, Tool, Tools, AnyObject, ToolResult, ValidationResult, PermissionResult } from '@claude-code-best/agent-tools' import type { CoreTool, Tool, Tools, AnyObject, ToolResult, ValidationResult, PermissionResult } from '@claude-code-best/agent-tools'
import type { Tool as HostTool } from '../../../../src/Tool.js' import type { Tool as HostTool } from '../../src/Tool.js'
describe('agent-tools compatibility', () => { describe('agent-tools compatibility', () => {
test('CoreTool structural compatibility with host Tool', () => { test('CoreTool structural compatibility with host Tool', () => {
@@ -27,7 +27,7 @@ describe('agent-tools compatibility', () => {
} }
// This assignment should work if HostTool structurally extends CoreTool // This assignment should work if HostTool structurally extends CoreTool
const coreTool: CoreTool = mockHostTool as unknown as CoreTool const coreTool: CoreTool = mockHostTool as CoreTool
expect(coreTool.name).toBe('test') expect(coreTool.name).toBe('test')
expect(coreTool.isEnabled()).toBe(true) 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", () => ({ mock.module("src/utils/settings/constants.js", () => ({
getSourceDisplayName: (source: string) => source, getSourceDisplayName: (source: string) => source,
getSourceDisplayNameLowercase: (source: string) => source,
getSourceDisplayNameCapitalized: (source: string) => source,
getSettingSourceName: (source: string) => source,
getSettingSourceDisplayNameLowercase: (source: string) => source,
getSettingSourceDisplayNameCapitalized: (source: string) => source,
parseSettingSourcesFlag: () => [],
getEnabledSettingSources: () => [],
isSettingSourceEnabled: () => true,
SETTING_SOURCES: ["localSettings", "userSettings", "projectSettings"],
SOURCES: ["localSettings", "userSettings", "projectSettings"],
CLAUDE_CODE_SETTINGS_SCHEMA_URL: "https://json.schemastore.org/claude-code-settings.json",
})); }));
const { const {

View File

@@ -87,7 +87,7 @@ mock.module("src/tasks/LocalAgentTask/LocalAgentTask.js", () => ({
updateProgressFromMessage: noop, updateProgressFromMessage: noop,
})); }));
mock.module("src/utils/debug.ts", () => ({ mock.module("src/utils/debug.js", () => ({
getMinDebugLogLevel: () => "warn", getMinDebugLogLevel: () => "warn",
isDebugMode: () => false, isDebugMode: () => false,
enableDebugLogging: () => 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"); const { interpretCommandResult } = await import("../commandSemantics");

View File

@@ -1,10 +1,19 @@
import { mock, describe, expect, test } from "bun:test"; import { mock, describe, expect, test } from "bun:test";
mock.module("src/utils/debug.ts", () => ({ mock.module("src/utils/debug.js", () => ({
logForDebugging: () => {}, logForDebugging: () => {},
isDebugMode: () => false, isDebugMode: () => false,
})); }));
mock.module("src/utils/errors.js", () => ({
errorMessage: (e: unknown) => String(e),
}));
mock.module("src/utils/stringUtils.js", () => ({
plural: (n: number, singular: string, plural?: string) =>
n === 1 ? singular : (plural ?? singular + "s"),
}));
const { const {
formatGoToDefinitionResult, formatGoToDefinitionResult,
formatFindReferencesResult, formatFindReferencesResult,

View File

@@ -7,18 +7,6 @@ mock.module("src/utils/cwd.js", () => ({
getCwd: () => mockCwd, getCwd: () => mockCwd,
})); }));
// Defensive: agent.test.ts can corrupt Bun's src/* path alias at runtime.
mock.module("src/utils/powershell/parser.js", () => ({
PS_TOKENIZER_DASH_CHARS: new Set(['-', '\u2013', '\u2014', '\u2015']),
COMMON_ALIASES: {},
commandHasArgAbbreviation: () => false,
deriveSecurityFlags: () => ({}),
getAllCommands: () => [],
getVariablesByScope: () => [],
hasCommandNamed: () => false,
parsePowerShellCommandCached: () => ({ valid: false, errors: [], statements: [], variables: [], hasStopParsing: false, originalCommand: "" }),
}))
const { isGitInternalPathPS, isDotGitPathPS } = await import("../gitSafety"); const { isGitInternalPathPS, isDotGitPathPS } = await import("../gitSafety");
describe("isGitInternalPathPS", () => { describe("isGitInternalPathPS", () => {

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 // Real parser functions work without mocks since they're pure
const { powershellCommandIsSafe } = await import("../powershellSecurity.js"); 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 type { ToolResultBlockParam } from 'src/Tool.js'
import { buildTool } from 'src/Tool.js' import { buildTool } from 'src/Tool.js'
import { lazySchema } from 'src/utils/lazySchema.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' import { SLEEP_TOOL_NAME, DESCRIPTION, SLEEP_TOOL_PROMPT } from './prompt.js'
const SLEEP_WAKE_CHECK_INTERVAL_MS = 500
const inputSchema = lazySchema(() => const inputSchema = lazySchema(() =>
z.strictObject({ z.strictObject({
duration_seconds: z duration_seconds: z
@@ -22,36 +19,6 @@ type SleepInput = z.infer<InputSchema>
type SleepOutput = { slept_seconds: number; interrupted: boolean } 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({ export const SleepTool = buildTool({
name: SLEEP_TOOL_NAME, name: SLEEP_TOOL_NAME,
searchHint: 'wait pause sleep rest idle duration timer', searchHint: 'wait pause sleep rest idle duration timer',
@@ -75,9 +42,6 @@ export const SleepTool = buildTool({
isReadOnly() { isReadOnly() {
return true return true
}, },
interruptBehavior() {
return 'cancel'
},
userFacingName() { userFacingName() {
return SLEEP_TOOL_NAME return SLEEP_TOOL_NAME
@@ -103,84 +67,53 @@ export const SleepTool = buildTool({
}, },
async call(input: SleepInput, context) { async call(input: SleepInput, context) {
// Don't enter sleep if proactive was disabled or new work arrived while // Refuse to sleep when proactive mode is off — prevents the model from
// the model was deciding to wait. // re-issuing Sleep after an interruption caused by /proactive disable.
if (shouldInterruptSleep()) { if (feature('PROACTIVE') || feature('KAIROS')) {
return { const mod =
data: { require('src/proactive/index.js') as typeof import('src/proactive/index.js')
slept_seconds: 0, if (!mod.isProactiveActive()) {
interrupted: true, return {
}, data: {
slept_seconds: 0,
interrupted: true,
},
}
} }
} }
const { duration_seconds } = input const { duration_seconds } = input
const startTime = Date.now() 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 { try {
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
let timer: ReturnType<typeof setTimeout> | null = null const timer = setTimeout(resolve, duration_seconds * 1000)
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)
// Abort via user interrupt // Abort via user interrupt
if (context.abortController.signal.aborted) { context.abortController.signal.addEventListener(
interrupt() 'abort',
return () => {
} clearTimeout(timer)
context.abortController.signal.addEventListener('abort', onAbort, { clearInterval(proactiveCheck)
once: true, reject(new Error('interrupted'))
}) },
{ once: true },
)
// Poll proactive state and the shared command queue so new work can // Poll proactive state — if deactivated mid-sleep, interrupt early
// wake Sleep without waiting for the full duration. // so the user doesn't have to wait for the full duration.
wakeCheck = setInterval(() => { const proactiveCheck =
if (shouldInterruptSleep()) { feature('PROACTIVE') || feature('KAIROS')
interrupt() ? setInterval(() => {
} const mod =
}, SLEEP_WAKE_CHECK_INTERVAL_MS) 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 { return {
data: { data: {
@@ -196,17 +129,6 @@ export const SleepTool = buildTool({
interrupted: true, 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 // Only mock the external dependency that controls adapter selection
mock.module('src/utils/model/providers.js', () => ({ mock.module('src/utils/model/providers.js', () => ({
isFirstPartyAnthropicBaseUrl: () => isFirstPartyBaseUrl, isFirstPartyAnthropicBaseUrl: () => isFirstPartyBaseUrl,
getAPIProvider: () => 'firstParty',
getAPIProviderForStatsig: () => 'firstParty',
})) }))
const { createAdapter } = await import('../adapters/index') const { createAdapter } = await import('../adapters/index')

View File

@@ -1,14 +1,4 @@
import { describe, expect, mock, test } from 'bun:test' import { describe, expect, mock, test } from 'bun:test'
const _abortMock = () => ({
AbortError: class AbortError extends Error {
constructor(message?: string) { super(message); this.name = 'AbortError' }
},
isAbortError: (e: unknown) => e instanceof Error && (e as Error).name === 'AbortError',
})
mock.module('src/utils/errors.js', _abortMock)
mock.module('src/utils/errors', _abortMock)
import { extractBingResults, decodeHtmlEntities } from '../adapters/bingAdapter' import { extractBingResults, decodeHtmlEntities } from '../adapters/bingAdapter'
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@@ -1,17 +1,5 @@
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test' import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
// Defensive mock: agent.test.ts mocks config.js which can corrupt Bun's
// src/* path alias resolution. Provide AbortError directly so the dynamic
// import in createAdapter() never needs to resolve the alias at runtime.
const _abortMock = () => ({
AbortError: class AbortError extends Error {
constructor(message?: string) { super(message); this.name = 'AbortError' }
},
isAbortError: (e: unknown) => e instanceof Error && (e as Error).name === 'AbortError',
})
mock.module('src/utils/errors.js', _abortMock)
mock.module('src/utils/errors', _abortMock)
const originalBraveSearchApiKey = process.env.BRAVE_SEARCH_API_KEY const originalBraveSearchApiKey = process.env.BRAVE_SEARCH_API_KEY
const originalBraveApiKey = process.env.BRAVE_API_KEY const originalBraveApiKey = process.env.BRAVE_API_KEY

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", () => { describe("detectLanguage", () => {
test("detects language from file extension", () => { test("detects language from file extension", () => {
expect(detectLanguage("index.ts", null)).toBe("ts"); expect(detectLanguage("index.ts")).toBe("ts");
expect(detectLanguage("main.py", null)).toBe("py"); expect(detectLanguage("main.py")).toBe("py");
expect(detectLanguage("style.css", null)).toBe("css"); expect(detectLanguage("style.css")).toBe("css");
}); });
test("detects language from known filenames", () => { test("detects language from known filenames", () => {
expect(detectLanguage("Makefile", null)).toBe("makefile"); expect(detectLanguage("Makefile")).toBe("makefile");
expect(detectLanguage("Dockerfile", null)).toBe("dockerfile"); expect(detectLanguage("Dockerfile")).toBe("dockerfile");
}); });
test("returns null for unknown extensions", () => { 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 let received: JSONRPCMessage | null = null
client.onmessage = (msg) => { received = msg } client.onmessage = (msg) => { received = msg }
await server.send({ jsonrpc: '2.0', result: 42, id: 1 } as any) await server.send({ jsonrpc: '2.0', result: 42, id: 1 })
await new Promise(resolve => setTimeout(resolve, 10)) 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.name).toBe('mcp__my-server__search')
expect(tool.mcpInfo).toEqual({ serverName: 'my-server', toolName: 'search' }) expect(tool.mcpInfo).toEqual({ serverName: 'my-server', toolName: 'search' })
expect(tool.isMcp).toBe(true) expect(tool.isMcp).toBe(true)
expect(tool.isReadOnly({} as any)).toBe(true) expect(tool.isReadOnly()).toBe(true)
expect(tool.userFacingName(undefined)).toBe('Search Items') expect(tool.userFacingName()).toBe('Search Items')
expect(await tool.description({} as any, { isNonInteractiveSession: false, toolPermissionContext: {}, tools: [] })).toBe('Search for items') expect(await tool.description()).toBe('Search for items')
}) })
test('respects skipPrefix option', async () => { test('respects skipPrefix option', async () => {

View File

@@ -65,7 +65,7 @@ describe('createMcpManager', () => {
const result = await manager.connect('test-server', { command: 'npx', args: [] }) const result = await manager.connect('test-server', { command: 'npx', args: [] })
expect(result.type).toBe('connected') expect(result.type).toBe('connected')
expect(connectedEvent as unknown as string).toBe('test-server') expect(connectedEvent).toBe('test-server')
}) })
test('disconnect calls cleanup and emits disconnected', async () => { 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, storeUpdateSession,
storeGetEnvironment, storeGetEnvironment,
storeGetSession, storeGetSession,
storeListActiveEnvironments,
} from "../store"; } from "../store";
import { getEventBus, getAllEventBuses, removeEventBus } from "../transport/event-bus";
import { runDisconnectMonitorSweep } from "../services/disconnect-monitor";
describe("Disconnect Monitor Logic", () => { describe("Disconnect Monitor Logic", () => {
beforeEach(() => { beforeEach(() => {
storeReset(); storeReset();
for (const [key] of getAllEventBuses()) {
removeEventBus(key);
}
}); });
// Test the logic directly rather than the interval-based monitor
// to avoid long-running tests with timers
test("environment times out when lastPollAt is too old", () => { test("environment times out when lastPollAt is too old", () => {
const env = storeCreateEnvironment({ secret: "s" }); const env = storeCreateEnvironment({ secret: "s" });
const timeoutMs = 300 * 1000; // 5 minutes const timeoutMs = 300 * 1000; // 5 minutes
@@ -45,7 +44,14 @@ describe("Disconnect Monitor Logic", () => {
const oldDate = new Date(Date.now() - timeoutMs - 60000); const oldDate = new Date(Date.now() - timeoutMs - 60000);
storeUpdateEnvironment(env.id, { lastPollAt: oldDate }); storeUpdateEnvironment(env.id, { lastPollAt: oldDate });
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); const updated = storeGetEnvironment(env.id);
expect(updated?.status).toBe("disconnected"); expect(updated?.status).toBe("disconnected");
@@ -53,56 +59,43 @@ describe("Disconnect Monitor Logic", () => {
test("environment stays active when lastPollAt is recent", () => { test("environment stays active when lastPollAt is recent", () => {
const env = storeCreateEnvironment({ secret: "s" }); const env = storeCreateEnvironment({ secret: "s" });
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); const updated = storeGetEnvironment(env.id);
expect(updated?.status).toBe("active"); expect(updated?.status).toBe("active");
}); });
test("session becomes inactive when updatedAt is too old", () => { test("session becomes inactive when updatedAt is too old", () => {
const session = storeCreateSession({}); const session = storeCreateSession({ status: "idle" });
storeUpdateSession(session.id, { status: "running" }); storeUpdateSession(session.id, { status: "running" });
const timeoutMs = 300 * 1000 * 2; // 2x disconnect timeout
// Simulate updatedAt being older than 2x timeout
// We can't directly set updatedAt, but we can verify the logic
// by checking that recently updated sessions are not marked inactive
const now = Date.now();
const rec = storeGetSession(session.id); const rec = storeGetSession(session.id);
expect(rec).toBeTruthy(); // Session was just updated, should not be inactive
if (!rec) return; expect(rec?.status).toBe("running");
expect(now - rec!.updatedAt.getTime()).toBeLessThan(timeoutMs);
rec.updatedAt = new Date(Date.now() - 300 * 1000 * 2 - 60000);
runDisconnectMonitorSweep();
const updated = storeGetSession(session.id);
expect(updated?.status).toBe("inactive");
}); });
test("session stays running when recently updated", () => { test("session stays running when recently updated", () => {
const session = storeCreateSession({}); const session = storeCreateSession({});
storeUpdateSession(session.id, { status: "running" }); storeUpdateSession(session.id, { status: "running" });
runDisconnectMonitorSweep(); const timeoutMs = 300 * 1000 * 2;
const updated = storeGetSession(session.id);
expect(updated?.status).toBe("running");
});
test("session timeout publishes an inactive session_status event", () => {
const session = storeCreateSession({});
storeUpdateSession(session.id, { status: "idle" });
const rec = storeGetSession(session.id); const rec = storeGetSession(session.id);
expect(rec).toBeTruthy(); expect(rec?.status).toBe("running");
if (!rec) return; expect(Date.now() - rec!.updatedAt.getTime()).toBeLessThan(timeoutMs);
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" },
});
}); });
}); });

View File

@@ -19,18 +19,16 @@ mock.module("../config", () => ({
import { Hono } from "hono"; import { Hono } from "hono";
import { storeReset, storeCreateSession, storeCreateEnvironment, storeBindSession } from "../store"; import { storeReset, storeCreateSession, storeCreateEnvironment, storeBindSession } from "../store";
import { removeEventBus, getAllEventBuses, getEventBus } from "../transport/event-bus"; import { removeEventBus, getAllEventBuses } from "../transport/event-bus";
import { issueToken } from "../auth/token"; import { issueToken } from "../auth/token";
import { publishSessionEvent } from "../services/transport";
// Import route modules // Import route modules
import v1Sessions from "../routes/v1/sessions"; import v1Sessions from "../routes/v1/sessions";
import v1Environments from "../routes/v1/environments"; import v1Environments from "../routes/v1/environments";
import v1EnvironmentsWork from "../routes/v1/environments.work"; import v1EnvironmentsWork from "../routes/v1/environments.work";
import v1SessionIngress, { websocket as sessionIngressWebsocket } from "../routes/v1/session-ingress"; import v1SessionIngress from "../routes/v1/session-ingress";
import v2CodeSessions from "../routes/v2/code-sessions"; import v2CodeSessions from "../routes/v2/code-sessions";
import v2Worker from "../routes/v2/worker"; import v2Worker from "../routes/v2/worker";
import v2WorkerEventsStream from "../routes/v2/worker-events-stream";
import v2WorkerEvents from "../routes/v2/worker-events"; import v2WorkerEvents from "../routes/v2/worker-events";
import webAuth from "../routes/web/auth"; import webAuth from "../routes/web/auth";
import webSessions from "../routes/web/sessions"; import webSessions from "../routes/web/sessions";
@@ -45,7 +43,6 @@ function createApp() {
app.route("/v2/session_ingress", v1SessionIngress); app.route("/v2/session_ingress", v1SessionIngress);
app.route("/v1/code/sessions", v2CodeSessions); app.route("/v1/code/sessions", v2CodeSessions);
app.route("/v1/code/sessions", v2Worker); app.route("/v1/code/sessions", v2Worker);
app.route("/v1/code/sessions", v2WorkerEventsStream);
app.route("/v1/code/sessions", v2WorkerEvents); app.route("/v1/code/sessions", v2WorkerEvents);
app.route("/web", webAuth); app.route("/web", webAuth);
app.route("/web", webSessions); app.route("/web", webSessions);
@@ -56,11 +53,6 @@ function createApp() {
const AUTH_HEADERS = { Authorization: "Bearer test-api-key", "X-Username": "testuser" }; const AUTH_HEADERS = { Authorization: "Bearer test-api-key", "X-Username": "testuser" };
function toWebSessionId(sessionId: string): string {
if (!sessionId.startsWith("cse_")) return sessionId;
return `session_${sessionId.slice("cse_".length)}`;
}
describe("V1 Session Routes", () => { describe("V1 Session Routes", () => {
let app: Hono; let app: Hono;
@@ -117,24 +109,6 @@ describe("V1 Session Routes", () => {
expect(res.status).toBe(404); expect(res.status).toBe(404);
}); });
test("GET /v1/sessions/:id — resolves compat code session IDs", async () => {
const createRes = await app.request("/v1/code/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const {
session: { id },
} = await createRes.json();
const getRes = await app.request(`/v1/sessions/${toWebSessionId(id)}`, {
headers: AUTH_HEADERS,
});
expect(getRes.status).toBe(200);
const body = await getRes.json();
expect(body.id).toBe(id);
});
test("PATCH /v1/sessions/:id — updates title", async () => { test("PATCH /v1/sessions/:id — updates title", async () => {
const createRes = await app.request("/v1/sessions", { const createRes = await app.request("/v1/sessions", {
method: "POST", method: "POST",
@@ -168,32 +142,6 @@ describe("V1 Session Routes", () => {
expect(archiveRes.status).toBe(200); expect(archiveRes.status).toBe(200);
}); });
test("POST /v1/sessions/:id/archive — archives compat code session IDs", async () => {
const createRes = await app.request("/v1/code/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const {
session: { id },
} = await createRes.json();
const compatId = toWebSessionId(id);
const archiveRes = await app.request(`/v1/sessions/${compatId}/archive`, {
method: "POST",
headers: AUTH_HEADERS,
});
expect(archiveRes.status).toBe(200);
const getRes = await app.request(`/v1/sessions/${compatId}`, {
headers: AUTH_HEADERS,
});
expect(getRes.status).toBe(200);
const body = await getRes.json();
expect(body.id).toBe(id);
expect(body.status).toBe("archived");
});
test("POST /v1/sessions/:id/events — publishes events", async () => { test("POST /v1/sessions/:id/events — publishes events", async () => {
const createRes = await app.request("/v1/sessions", { const createRes = await app.request("/v1/sessions", {
method: "POST", method: "POST",
@@ -212,30 +160,6 @@ describe("V1 Session Routes", () => {
expect(body.events).toBe(1); expect(body.events).toBe(1);
}); });
test("POST /v1/sessions/:id/events — resolves compat code session IDs", async () => {
const createRes = await app.request("/v1/code/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const {
session: { id },
} = await createRes.json();
const compatId = toWebSessionId(id);
const eventsRes = await app.request(`/v1/sessions/${compatId}/events`, {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({ events: [{ type: "user", content: "hello from compat" }] }),
});
expect(eventsRes.status).toBe(200);
const events = getEventBus(id).getEventsSince(0);
expect(events).toHaveLength(1);
expect(events[0]?.type).toBe("user");
expect((events[0]?.payload as { content?: string }).content).toBe("hello from compat");
});
test("POST /v1/sessions with environment_id creates work item", async () => { test("POST /v1/sessions with environment_id creates work item", async () => {
// First register an environment // First register an environment
const envRes = await app.request("/v1/environments/bridge", { const envRes = await app.request("/v1/environments/bridge", {
@@ -519,26 +443,6 @@ describe("Web Auth Routes", () => {
expect(body.ok).toBe(true); expect(body.ok).toBe(true);
}); });
test("POST /web/bind — binds compat code session ID to UUID", async () => {
const sessRes = await app.request("/v1/code/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const body = await sessRes.json();
const compatId = toWebSessionId(body.session.id);
const bindRes = await app.request("/web/bind?uuid=test-uuid", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sessionId: compatId }),
});
expect(bindRes.status).toBe(200);
const bindBody = await bindRes.json();
expect(bindBody.ok).toBe(true);
expect(bindBody.sessionId).toBe(compatId);
});
test("POST /web/bind — 404 for unknown session", async () => { test("POST /web/bind — 404 for unknown session", async () => {
const res = await app.request("/web/bind?uuid=test-uuid", { const res = await app.request("/web/bind?uuid=test-uuid", {
method: "POST", method: "POST",
@@ -597,24 +501,6 @@ describe("Web Session Routes", () => {
expect(sessions[0].id).toBe(id); expect(sessions[0].id).toBe(id);
}); });
test("GET /web/sessions and /all — serialize owned code sessions as compat IDs", async () => {
const codeSession = storeCreateSession({ idPrefix: "cse_" });
storeBindSession(codeSession.id, "user-1");
const compatId = toWebSessionId(codeSession.id);
const listRes = await app.request("/web/sessions?uuid=user-1");
expect(listRes.status).toBe(200);
const sessions = await listRes.json();
expect(sessions).toHaveLength(1);
expect(sessions[0].id).toBe(compatId);
const allRes = await app.request("/web/sessions/all?uuid=user-1");
expect(allRes.status).toBe(200);
const summaries = await allRes.json();
expect(summaries).toHaveLength(1);
expect(summaries[0].id).toBe(compatId);
});
test("GET /web/sessions — requires UUID", async () => { test("GET /web/sessions — requires UUID", async () => {
const res = await app.request("/web/sessions"); const res = await app.request("/web/sessions");
expect(res.status).toBe(401); expect(res.status).toBe(401);
@@ -639,33 +525,6 @@ describe("Web Session Routes", () => {
expect(sessions).toHaveLength(1); // only user-1's session, not user-2's expect(sessions).toHaveLength(1); // only user-1's session, not user-2's
}); });
test("GET /web/sessions and /all — hides archived and inactive sessions", async () => {
const archived = storeCreateSession({});
const inactive = storeCreateSession({});
const open = storeCreateSession({});
storeBindSession(archived.id, "user-1");
storeBindSession(inactive.id, "user-1");
storeBindSession(open.id, "user-1");
await app.request(`/v1/sessions/${archived.id}/archive`, {
method: "POST",
headers: AUTH_HEADERS,
});
const { storeUpdateSession } = await import("../store");
storeUpdateSession(inactive.id, { status: "inactive" });
const listRes = await app.request("/web/sessions?uuid=user-1");
expect(listRes.status).toBe(200);
const sessions = await listRes.json();
expect(sessions.map((session: { id: string }) => session.id)).toEqual([open.id]);
const allRes = await app.request("/web/sessions/all?uuid=user-1");
expect(allRes.status).toBe(200);
const summaries = await allRes.json();
expect(summaries.map((session: { id: string }) => session.id)).toEqual([open.id]);
});
test("GET /web/sessions/:id — returns owned session", async () => { test("GET /web/sessions/:id — returns owned session", async () => {
const createRes = await app.request("/web/sessions?uuid=user-1", { const createRes = await app.request("/web/sessions?uuid=user-1", {
method: "POST", method: "POST",
@@ -678,44 +537,6 @@ describe("Web Session Routes", () => {
expect(getRes.status).toBe(200); 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 () => { test("GET /web/sessions/:id — 403 for non-owner", async () => {
const createRes = await app.request("/web/sessions?uuid=user-1", { const createRes = await app.request("/web/sessions?uuid=user-1", {
method: "POST", method: "POST",
@@ -742,51 +563,6 @@ describe("Web Session Routes", () => {
expect(body.events).toEqual([]); 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 () => { test("GET /web/sessions/:id/history — 403 for non-owner", async () => {
const createRes = await app.request("/web/sessions?uuid=user-1", { const createRes = await app.request("/web/sessions?uuid=user-1", {
method: "POST", method: "POST",
@@ -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 () => { test("GET /web/sessions/:id/events — 403 for non-owner", async () => {
const createRes = await app.request("/web/sessions?uuid=user-1", { const createRes = await app.request("/web/sessions?uuid=user-1", {
method: "POST", method: "POST",
@@ -900,25 +658,6 @@ describe("Web Session Routes", () => {
const eventsRes = await app.request(`/web/sessions/${id}/events?uuid=user-2`); const eventsRes = await app.request(`/web/sessions/${id}/events?uuid=user-2`);
expect(eventsRes.status).toBe(403); expect(eventsRes.status).toBe(403);
}); });
test("GET /web/sessions/:id/events — 409 for archived session", async () => {
const createRes = await app.request("/web/sessions?uuid=user-1", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { id } = await createRes.json();
await app.request(`/v1/sessions/${id}/archive`, {
method: "POST",
headers: AUTH_HEADERS,
});
const res = await app.request(`/web/sessions/${id}/events?uuid=user-1`);
expect(res.status).toBe(409);
const body = await res.json();
expect(body.error.type).toBe("session_closed");
});
}); });
describe("Web Control Routes", () => { describe("Web Control Routes", () => {
@@ -953,32 +692,6 @@ describe("Web Control Routes", () => {
expect(body.event).toBeTruthy(); expect(body.event).toBeTruthy();
}); });
test("POST /web/sessions/:id/events/control/interrupt — supports compat code session IDs", async () => {
const rawSessionId = storeCreateSession({ idPrefix: "cse_" }).id;
storeBindSession(rawSessionId, "user-1");
const compatId = toWebSessionId(rawSessionId);
const eventsRes = await app.request(`/web/sessions/${compatId}/events?uuid=user-1`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ type: "user", content: "hello" }),
});
expect(eventsRes.status).toBe(200);
const controlRes = await app.request(`/web/sessions/${compatId}/control?uuid=user-1`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ type: "permission_response", approved: true, request_id: "r1" }),
});
expect(controlRes.status).toBe(200);
const interruptRes = await app.request(`/web/sessions/${compatId}/interrupt?uuid=user-1`, {
method: "POST",
headers: { "Content-Type": "application/json" },
});
expect(interruptRes.status).toBe(200);
});
test("POST /web/sessions/:id/events — 403 for non-owner", async () => { test("POST /web/sessions/:id/events — 403 for non-owner", async () => {
const res = await app.request(`/web/sessions/${sessionId}/events?uuid=user-2`, { const res = await app.request(`/web/sessions/${sessionId}/events?uuid=user-2`, {
method: "POST", method: "POST",
@@ -1030,33 +743,6 @@ describe("Web Control Routes", () => {
}); });
expect(res.status).toBe(403); expect(res.status).toBe(403);
}); });
test("POST /web/sessions/:id/events/control/interrupt — 409 for archived session", async () => {
await app.request(`/v1/sessions/${sessionId}/archive`, {
method: "POST",
headers: AUTH_HEADERS,
});
const eventsRes = await app.request(`/web/sessions/${sessionId}/events?uuid=user-1`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ type: "user", content: "hello" }),
});
expect(eventsRes.status).toBe(409);
const controlRes = await app.request(`/web/sessions/${sessionId}/control?uuid=user-1`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ type: "permission_response", approved: true, request_id: "r1" }),
});
expect(controlRes.status).toBe(409);
const interruptRes = await app.request(`/web/sessions/${sessionId}/interrupt?uuid=user-1`, {
method: "POST",
headers: { "Content-Type": "application/json" },
});
expect(interruptRes.status).toBe(409);
});
}); });
describe("Web Environment Routes", () => { describe("Web Environment Routes", () => {
@@ -1136,81 +822,6 @@ describe("V1 Session Ingress Routes (HTTP)", () => {
}); });
expect(res.status).toBe(404); expect(res.status).toBe(404);
}); });
test("POST /v2/session_ingress/session/:sessionId/events — resolves compat code session IDs", async () => {
const sessRes = await app.request("/v1/code/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const {
session: { id },
} = await sessRes.json();
const compatId = toWebSessionId(id);
const res = await app.request(`/v2/session_ingress/session/${compatId}/events`, {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({ events: [{ type: "assistant", message: { role: "assistant", content: "compat ok" } }] }),
});
expect(res.status).toBe(200);
const events = getEventBus(id).getEventsSince(0);
expect(events).toHaveLength(1);
expect(events[0]?.type).toBe("assistant");
});
test("GET /v2/session_ingress/ws/:sessionId — resolves compat code session IDs", async () => {
const sessRes = await app.request("/v1/code/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const {
session: { id },
} = await sessRes.json();
const compatId = toWebSessionId(id);
publishSessionEvent(id, "user", { content: "compat ws replay" }, "outbound");
const server = Bun.serve({
port: 0,
fetch: app.fetch,
websocket: {
...sessionIngressWebsocket,
idleTimeout: 30,
},
});
try {
const message = await new Promise<string>((resolve, reject) => {
const ws = new WebSocket(`ws://127.0.0.1:${server.port}/v2/session_ingress/ws/${compatId}?token=test-api-key`);
const timeout = setTimeout(() => {
ws.close();
reject(new Error("Timed out waiting for compat WebSocket replay"));
}, 2000);
ws.onmessage = (event) => {
const data = typeof event.data === "string" ? event.data : String(event.data);
if (data.includes("\"type\":\"user\"")) {
clearTimeout(timeout);
ws.close();
resolve(data);
}
};
ws.onerror = () => {
clearTimeout(timeout);
reject(new Error("Compat WebSocket connection failed"));
};
});
expect(message).toContain("\"type\":\"user\"");
expect(message).toContain(`\"session_id\":\"${id}\"`);
expect(message).toContain("compat ws replay");
} finally {
await server.stop(true);
}
});
}); });
describe("V2 Worker Events Routes", () => { describe("V2 Worker Events Routes", () => {
@@ -1245,252 +856,6 @@ describe("V2 Worker Events Routes", () => {
expect(body.count).toBe(1); expect(body.count).toBe(1);
}); });
test("POST /v1/code/sessions/:id/worker/events — unwraps CCR batch payloads", async () => {
const sessRes = await app.request("/v1/code/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { session: { id } } = await sessRes.json();
const res = await app.request(`/v1/code/sessions/${id}/worker/events`, {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({
worker_epoch: 1,
events: [{ payload: { type: "assistant", content: "response" } }],
}),
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.count).toBe(1);
const events = getEventBus(id).getEventsSince(0);
expect(events).toHaveLength(1);
expect(events[0]?.type).toBe("assistant");
expect((events[0]?.payload as { content?: string }).content).toBe("response");
});
test("GET/PUT /v1/code/sessions/:id/worker — stores worker state", async () => {
const sessRes = await app.request("/v1/code/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { session: { id } } = await sessRes.json();
const putRes = await app.request(`/v1/code/sessions/${id}/worker`, {
method: "PUT",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({
worker_epoch: 1,
worker_status: "running",
external_metadata: {
permission_mode: "default",
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 () => { test("PUT /v1/code/sessions/:id/worker/state — updates session status", async () => {
const sessRes = await app.request("/v1/sessions", { const sessRes = await app.request("/v1/sessions", {
method: "POST", method: "POST",
@@ -1538,20 +903,4 @@ describe("V2 Worker Events Routes", () => {
}); });
expect(res.status).toBe(200); expect(res.status).toBe(200);
}); });
test("POST /v1/code/sessions/:id/worker/events/delivery — batch no-op", async () => {
const sessRes = await app.request("/v1/code/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { session: { id } } = await sessRes.json();
const res = await app.request(`/v1/code/sessions/${id}/worker/events/delivery`, {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({ worker_epoch: 1, updates: [{ event_id: "evt123", status: "received" }] }),
});
expect(res.status).toBe(200);
});
}); });

View File

@@ -345,22 +345,6 @@ describe("Transport Service", () => {
expect(result.message).toEqual(msg); expect(result.message).toEqual(msg);
}); });
test("preserves uuid field", () => {
const result = normalizePayload("user", {
uuid: "msg_123",
content: "hi",
});
expect(result.uuid).toBe("msg_123");
});
test("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", () => { test("uses name as tool_name fallback", () => {
const result = normalizePayload("tool", { name: "Read" }); const result = normalizePayload("tool", { name: "Read" });
expect(result.tool_name).toBe("Read"); expect(result.tool_name).toBe("Read");
@@ -378,28 +362,6 @@ describe("Transport Service", () => {
expect(result.content).toBe(""); 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", () => { test("handles undefined payload", () => {
const result = normalizePayload("user", undefined); const result = normalizePayload("user", undefined);
expect(result.content).toBe(""); expect(result.content).toBe("");

View File

@@ -69,19 +69,6 @@ describe("ws-handler", () => {
expect((events[0] as any).direction).toBe("inbound"); 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", () => { test("derives type from message.role for assistant messages", () => {
const bus = getEventBus("s1"); const bus = getEventBus("s1");
const events: unknown[] = []; const events: unknown[] = [];
@@ -176,24 +163,6 @@ describe("ws-handler", () => {
expect(msg.type).toBe("user"); 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", () => { test("replaces existing connection for same session", () => {
const ws1 = createMockWs(); const ws1 = createMockWs();
const ws2 = createMockWs(); const ws2 = createMockWs();
@@ -367,26 +336,6 @@ describe("ws-handler", () => {
expect(lastMsg.message.content).toBe("hello world"); expect(lastMsg.message.content).toBe("hello world");
}); });
test("preserves payload uuid for outbound user events", () => {
const bus = getEventBus("um2");
const ws = createMockWs();
handleWebSocketOpen(ws, "um2");
bus.publish({
id: "internal-event-id",
sessionId: "um2",
type: "user",
payload: { uuid: "web-message-uuid", content: "hello from web" },
direction: "outbound",
});
const sent = ws.getSentData();
const lastMsg = JSON.parse(sent[sent.length - 1]);
expect(lastMsg.type).toBe("user");
expect(lastMsg.uuid).toBe("web-message-uuid");
expect(lastMsg.message.content).toBe("hello from web");
});
test("converts generic event type", () => { test("converts generic event type", () => {
const bus = getEventBus("gen1"); const bus = getEventBus("gen1");
const ws = createMockWs(); const ws = createMockWs();

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,13 @@
import { Hono } from "hono"; import { Hono } from "hono";
import { sessionIngressAuth, acceptCliHeaders } from "../../auth/middleware"; import { sessionIngressAuth, acceptCliHeaders } from "../../auth/middleware";
import { createWorkerEventStream } from "../../transport/sse-writer"; import { createSSEStream } from "../../transport/sse-writer";
import { getSession } from "../../services/session"; import { getSession } from "../../services/session";
const app = new Hono(); const app = new Hono();
/** SSE /v1/code/sessions/:id/worker/events/stream — SSE event stream */ /** SSE /v1/code/sessions/:id/worker/events/stream — SSE event stream */
app.get("/:id/worker/events/stream", acceptCliHeaders, sessionIngressAuth, async (c) => { app.get("/:id/worker/events/stream", acceptCliHeaders, sessionIngressAuth, async (c) => {
const sessionId = c.req.param("id")!; const sessionId = c.req.param("id");
const session = getSession(sessionId); const session = getSession(sessionId);
if (!session) { if (!session) {
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404); return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
@@ -18,7 +18,7 @@ app.get("/:id/worker/events/stream", acceptCliHeaders, sessionIngressAuth, async
const fromSeq = c.req.query("from_sequence_num"); const fromSeq = c.req.query("from_sequence_num");
const fromSeqNum = fromSeq ? parseInt(fromSeq) : lastEventId ? parseInt(lastEventId) : 0; const fromSeqNum = fromSeq ? parseInt(fromSeq) : lastEventId ? parseInt(lastEventId) : 0;
return createWorkerEventStream(c, sessionId, fromSeqNum); return createSSEStream(c, sessionId, fromSeqNum);
}); });
export default app; export default app;

View File

@@ -1,66 +1,32 @@
import { Hono } from "hono"; import { Hono } from "hono";
import { sessionIngressAuth, acceptCliHeaders } from "../../auth/middleware"; import { sessionIngressAuth, acceptCliHeaders } from "../../auth/middleware";
import { publishSessionEvent } from "../../services/transport"; import { publishSessionEvent } from "../../services/transport";
import { getSession, touchSession, updateSessionStatus } from "../../services/session"; import { getSession, updateSessionStatus } from "../../services/session";
const app = new Hono(); const app = new Hono();
function extractWorkerEvents(body: unknown): Array<Record<string, unknown>> {
if (!body || typeof body !== "object") {
return [];
}
const payload = body as Record<string, unknown>;
const rawEvents = Array.isArray(payload.events)
? payload.events
: Array.isArray(body)
? body
: [body];
return rawEvents
.filter((evt): evt is Record<string, unknown> => !!evt && typeof evt === "object")
.map((evt) => {
const wrappedPayload = evt.payload;
if (wrappedPayload && typeof wrappedPayload === "object" && !Array.isArray(wrappedPayload)) {
return wrappedPayload as Record<string, unknown>;
}
return evt;
});
}
/** POST /v1/code/sessions/:id/worker/events — Write events */ /** POST /v1/code/sessions/:id/worker/events — Write events */
app.post("/:id/worker/events", acceptCliHeaders, sessionIngressAuth, async (c) => { app.post("/:id/worker/events", acceptCliHeaders, sessionIngressAuth, async (c) => {
const sessionId = c.req.param("id")!; const sessionId = c.req.param("id");
if (!getSession(sessionId)) {
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
}
const body = await c.req.json(); const body = await c.req.json();
const events = extractWorkerEvents(body); const events = Array.isArray(body) ? body : [body];
const published = []; const published = [];
for (const evt of events) { for (const evt of events) {
const eventType = typeof evt.type === "string" ? evt.type : "message"; const result = publishSessionEvent(sessionId, evt.type || "message", evt, "inbound");
const result = publishSessionEvent(sessionId, eventType, evt, "inbound");
published.push(result); published.push(result);
} }
touchSession(sessionId);
return c.json({ status: "ok", count: published.length }, 200); return c.json({ status: "ok", count: published.length }, 200);
}); });
/** PUT /v1/code/sessions/:id/worker/state — Report worker state */ /** PUT /v1/code/sessions/:id/worker/state — Report worker state */
app.put("/:id/worker/state", acceptCliHeaders, sessionIngressAuth, async (c) => { app.put("/:id/worker/state", acceptCliHeaders, sessionIngressAuth, async (c) => {
const sessionId = c.req.param("id")!; const sessionId = c.req.param("id");
if (!getSession(sessionId)) {
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
}
const body = await c.req.json(); const body = await c.req.json();
if (body.status) { if (body.status) {
updateSessionStatus(sessionId, body.status); updateSessionStatus(sessionId, body.status);
} else {
touchSession(sessionId);
} }
return c.json({ status: "ok" }, 200); return c.json({ status: "ok" }, 200);
@@ -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) */ /** PUT /v1/code/sessions/:id/worker/external_metadata — Report worker metadata (no-op) */
app.put("/:id/worker/external_metadata", acceptCliHeaders, sessionIngressAuth, async (c) => { app.put("/:id/worker/external_metadata", acceptCliHeaders, sessionIngressAuth, async (c) => {
const sessionId = c.req.param("id")!;
if (!getSession(sessionId)) {
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
}
// TUI's CCRClient calls this for metadata reporting. Accept and discard. // TUI's CCRClient calls this for metadata reporting. Accept and discard.
return c.json({ status: "ok" }, 200); return c.json({ status: "ok" }, 200);
}); });
/** POST /v1/code/sessions/:id/worker/events/delivery — Batch delivery tracking (no-op) */
app.post("/:id/worker/events/delivery", acceptCliHeaders, sessionIngressAuth, async (c) => {
const sessionId = c.req.param("id")!;
if (!getSession(sessionId)) {
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
}
return c.json({ status: "ok" }, 200);
});
/** POST /v1/code/sessions/:id/worker/events/:eventId/delivery — Delivery tracking (no-op) */ /** POST /v1/code/sessions/:id/worker/events/:eventId/delivery — Delivery tracking (no-op) */
app.post("/:id/worker/events/:eventId/delivery", acceptCliHeaders, sessionIngressAuth, async (c) => { app.post("/:id/worker/events/:eventId/delivery", acceptCliHeaders, sessionIngressAuth, async (c) => {
const sessionId = c.req.param("id")!;
if (!getSession(sessionId)) {
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
}
// TUI's CCRClient reports event delivery status (received/processing/processed). // TUI's CCRClient reports event delivery status (received/processing/processed).
// Accept and discard — event bus doesn't track per-event delivery. // Accept and discard — event bus doesn't track per-event delivery.
return c.json({ status: "ok" }, 200); return c.json({ status: "ok" }, 200);

View File

@@ -1,98 +1,12 @@
import { Hono } from "hono"; import { Hono } from "hono";
import { getSession, incrementEpoch, touchSession, updateSessionStatus } from "../../services/session"; import { getSession, incrementEpoch } from "../../services/session";
import { import { apiKeyAuth, acceptCliHeaders } from "../../auth/middleware";
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";
const app = new Hono(); const app = new Hono();
/** GET /v1/code/sessions/:id/worker — Read worker state */
app.get("/:id/worker", acceptCliHeaders, sessionIngressAuth, async (c) => {
const sessionId = c.req.param("id")!;
const session = getSession(sessionId);
if (!session) {
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
}
const worker = storeGetSessionWorker(sessionId);
return c.json({
worker: {
worker_status: worker?.workerStatus ?? session.status,
external_metadata: worker?.externalMetadata ?? null,
requires_action_details: worker?.requiresActionDetails ?? null,
last_heartbeat_at: worker?.lastHeartbeatAt?.toISOString() ?? null,
},
}, 200);
});
/** PUT /v1/code/sessions/:id/worker — Update worker state */
app.put("/:id/worker", acceptCliHeaders, sessionIngressAuth, async (c) => {
const sessionId = c.req.param("id")!;
const session = getSession(sessionId);
if (!session) {
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
}
const body = await c.req.json();
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 */ /** POST /v1/code/sessions/:id/worker/register — Register worker */
app.post("/:id/worker/register", acceptCliHeaders, apiKeyAuth, async (c) => { app.post("/:id/worker/register", acceptCliHeaders, apiKeyAuth, async (c) => {
const sessionId = c.req.param("id")!; const sessionId = c.req.param("id");
const session = getSession(sessionId); const session = getSession(sessionId);
if (!session) { if (!session) {
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404); return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);

View File

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

View File

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

View File

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

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 { log, error as logError } from "../logger";
import { storeListActiveEnvironments, storeUpdateEnvironment } from "../store"; import { storeListActiveEnvironments, storeUpdateEnvironment } from "../store";
import { storeListSessions } from "../store"; import { storeListSessions, storeUpdateSession } from "../store";
import { config } from "../config"; 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() { export function startDisconnectMonitor() {
const timeoutMs = config.disconnectTimeout * 1000;
setInterval(() => { 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 }, 60_000); // Check every minute
} }

View File

@@ -1,20 +1,14 @@
import { import {
storeCreateSession, storeCreateSession,
storeGetSession, storeGetSession,
storeIsSessionOwner,
storeUpdateSession, storeUpdateSession,
storeListSessions, storeListSessions,
storeListSessionsByUsername, storeListSessionsByUsername,
storeListSessionsByEnvironment, storeListSessionsByEnvironment,
storeListSessionsByOwnerUuid, storeListSessionsByOwnerUuid,
} from "../store"; } 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 type { CreateSessionRequest, CreateCodeSessionRequest, SessionResponse, SessionSummaryResponse } from "../types/api";
import { v4 as uuid } from "uuid";
const CODE_SESSION_PREFIX = "cse_";
const WEB_SESSION_PREFIX = "session_";
const CLOSED_SESSION_STATUSES = new Set(["archived", "inactive"]);
function toResponse(row: { id: string; environmentId: string | null; title: string | null; status: string; source: string; permissionMode: string | null; workerEpoch: number; username: string | null; createdAt: Date; updatedAt: Date }): SessionResponse { function toResponse(row: { id: string; environmentId: string | null; title: string | null; status: string; source: string; permissionMode: string | null; workerEpoch: number; username: string | null; createdAt: Date; updatedAt: Date }): SessionResponse {
return { return {
@@ -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 { export function createSession(req: CreateSessionRequest & { username?: string }): SessionResponse {
const record = storeCreateSession({ const record = storeCreateSession({
environmentId: req.environment_id, environmentId: req.environment_id,
@@ -75,78 +51,16 @@ export function getSession(sessionId: string): SessionResponse | null {
return record ? toResponse(record) : null; return record ? toResponse(record) : null;
} }
export function isSessionClosedStatus(status: string | null | undefined): boolean {
return !!status && CLOSED_SESSION_STATUSES.has(status);
}
export function resolveExistingSessionId(sessionId: string): string | null {
if (storeGetSession(sessionId)) {
return sessionId;
}
const compatibleCodeSessionId = toCompatibleCodeSessionId(sessionId);
if (compatibleCodeSessionId && storeGetSession(compatibleCodeSessionId)) {
return compatibleCodeSessionId;
}
return null;
}
export function resolveExistingWebSessionId(sessionId: string): string | null {
return resolveExistingSessionId(sessionId);
}
export function resolveOwnedWebSessionId(sessionId: string, uuid: string): string | null {
if (storeIsSessionOwner(sessionId, uuid)) {
return sessionId;
}
const compatibleCodeSessionId = toCompatibleCodeSessionId(sessionId);
if (compatibleCodeSessionId && storeIsSessionOwner(compatibleCodeSessionId, uuid)) {
return compatibleCodeSessionId;
}
return null;
}
export function listWebSessionsByOwnerUuid(uuid: string): SessionResponse[] {
return storeListSessionsByOwnerUuid(uuid)
.filter((session) => !isSessionClosedStatus(session.status))
.map(toResponse)
.map(toWebSessionResponse);
}
export function listWebSessionSummariesByOwnerUuid(uuid: string): SessionSummaryResponse[] {
return storeListSessionsByOwnerUuid(uuid)
.filter((session) => !isSessionClosedStatus(session.status))
.map(toSummaryResponse)
.map(toWebSessionSummaryResponse);
}
export function updateSessionTitle(sessionId: string, title: string) { export function updateSessionTitle(sessionId: string, title: string) {
storeUpdateSession(sessionId, { title }); storeUpdateSession(sessionId, { title });
} }
export function updateSessionStatus(sessionId: string, status: string) { export function updateSessionStatus(sessionId: string, status: string) {
storeUpdateSession(sessionId, { status }); storeUpdateSession(sessionId, { status });
const bus = getAllEventBuses().get(sessionId);
if (!bus) return;
bus.publish({
id: uuid(),
sessionId,
type: "session_status",
payload: { status },
direction: "inbound",
});
}
export function touchSession(sessionId: string) {
storeUpdateSession(sessionId, {});
} }
export function archiveSession(sessionId: string) { export function archiveSession(sessionId: string) {
updateSessionStatus(sessionId, "archived"); storeUpdateSession(sessionId, { status: "archived" });
removeEventBus(sessionId); removeEventBus(sessionId);
} }

View File

@@ -51,11 +51,6 @@ export function normalizePayload(type: string, payload: unknown): Record<string,
raw: payload, 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 // Preserve tool fields
if (p.tool_name) normalized.tool_name = p.tool_name; if (p.tool_name) normalized.tool_name = p.tool_name;
if (p.name) normalized.tool_name = p.name; if (p.name) normalized.tool_name = p.name;
@@ -71,12 +66,6 @@ export function normalizePayload(type: string, payload: unknown): Record<string,
// Preserve message field for backward compat // Preserve message field for backward compat
if (p.message) normalized.message = p.message; 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; return normalized;
} }

View File

@@ -47,16 +47,6 @@ export interface WorkItemRecord {
updatedAt: Date; updatedAt: Date;
} }
export interface SessionWorkerRecord {
sessionId: string;
workerStatus: string | null;
externalMetadata: Record<string, unknown> | null;
requiresActionDetails: Record<string, unknown> | null;
lastHeartbeatAt: Date | null;
createdAt: Date;
updatedAt: Date;
}
// ---------- Stores (in-memory Maps) ---------- // ---------- Stores (in-memory Maps) ----------
const users = new Map<string, UserRecord>(); const users = new Map<string, UserRecord>();
@@ -64,7 +54,6 @@ const tokenToUser = new Map<string, { username: string; createdAt: Date }>();
const environments = new Map<string, EnvironmentRecord>(); const environments = new Map<string, EnvironmentRecord>();
const sessions = new Map<string, SessionRecord>(); const sessions = new Map<string, SessionRecord>();
const workItems = new Map<string, WorkItemRecord>(); const workItems = new Map<string, WorkItemRecord>();
const sessionWorkers = new Map<string, SessionWorkerRecord>();
// UUID → session ownership: sessionId → Set of UUIDs // UUID → session ownership: sessionId → Set of UUIDs
const sessionOwners = new Map<string, Set<string>>(); const sessionOwners = new Map<string, Set<string>>();
@@ -201,59 +190,9 @@ export function storeListSessionsByEnvironment(envId: string): SessionRecord[] {
} }
export function storeDeleteSession(id: string): boolean { export function storeDeleteSession(id: string): boolean {
sessionWorkers.delete(id);
return sessions.delete(id); return sessions.delete(id);
} }
// ---------- Session Worker ----------
export function storeGetSessionWorker(sessionId: string): SessionWorkerRecord | undefined {
return sessionWorkers.get(sessionId);
}
export function storeUpsertSessionWorker(sessionId: string, patch: {
workerStatus?: string | null;
externalMetadata?: Record<string, unknown> | null;
requiresActionDetails?: Record<string, unknown> | null;
lastHeartbeatAt?: Date | null;
}): SessionWorkerRecord {
const now = new Date();
const existing = sessionWorkers.get(sessionId);
const record: SessionWorkerRecord = existing ?? {
sessionId,
workerStatus: null,
externalMetadata: null,
requiresActionDetails: null,
lastHeartbeatAt: null,
createdAt: now,
updatedAt: now,
};
if (patch.workerStatus !== undefined) {
record.workerStatus = patch.workerStatus;
}
if (patch.externalMetadata !== undefined) {
if (patch.externalMetadata === null) {
record.externalMetadata = null;
} else {
record.externalMetadata = {
...(record.externalMetadata ?? {}),
...patch.externalMetadata,
};
}
}
if (patch.requiresActionDetails !== undefined) {
record.requiresActionDetails = patch.requiresActionDetails;
}
if (patch.lastHeartbeatAt !== undefined) {
record.lastHeartbeatAt = patch.lastHeartbeatAt;
}
record.updatedAt = now;
sessionWorkers.set(sessionId, record);
return record;
}
// ---------- Work Items ---------- // ---------- Work Items ----------
// ---------- Session Ownership (UUID-based) ---------- // ---------- Session Ownership (UUID-based) ----------
@@ -333,6 +272,5 @@ export function storeReset() {
environments.clear(); environments.clear();
sessions.clear(); sessions.clear();
workItems.clear(); workItems.clear();
sessionWorkers.clear();
sessionOwners.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 { Context } from "hono";
import type { SessionEvent } from "./event-bus"; import type { SessionEvent } from "./event-bus";
import { getEventBus } from "./event-bus"; import { getEventBus } from "./event-bus";
import { toClientPayload } from "./client-payload";
export interface SSEWriter { export interface SSEWriter {
send(event: SessionEvent): void; 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 type { SessionEvent } from "./event-bus";
import { publishSessionEvent } from "../services/transport"; import { publishSessionEvent } from "../services/transport";
import { log, error as logError } from "../logger"; import { log, error as logError } from "../logger";
import { toClientPayload } from "./client-payload";
// Per-connection cleanup, keyed by sessionId (only one WS per session) // Per-connection cleanup, keyed by sessionId (only one WS per session)
interface CleanupEntry { interface CleanupEntry {
@@ -25,9 +24,74 @@ const SERVER_KEEPALIVE_INTERVAL_MS = 60_000;
* Convert internal EventBus event -> SDK message for bridge client. * Convert internal EventBus event -> SDK message for bridge client.
*/ */
function toSDKMessage(event: SessionEvent): string { 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 // NDJSON format: each message MUST end with \n so the child process's
// line-based parser can split messages correctly. // 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 */ /** 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 }; payload = { message: msg.message, uuid: msg.uuid, content: text };
} else if (eventType === "user" || eventType === "system") { } else if (eventType === "user" || eventType === "system") {
payload = { payload = { message: msg.message, uuid: msg.uuid };
message: msg.message,
uuid: msg.uuid,
...(typeof msg.isSynthetic === "boolean" ? { isSynthetic: msg.isSynthetic } : {}),
};
} else if (eventType === "control_request") { } else if (eventType === "control_request") {
payload = { request_id: msg.request_id, request: msg.request }; payload = { request_id: msg.request_id, request: msg.request };
} else if (eventType === "control_response") { } else if (eventType === "control_response") {

View File

@@ -70,14 +70,6 @@ export interface SessionResponse {
username: string | null; username: string | null;
created_at: number; created_at: number;
updated_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 --- // --- v2 Code Sessions ---

View File

@@ -36,7 +36,6 @@ export interface ControlRequest extends SDKMessage {
export type SessionEventType = export type SessionEventType =
| "user" | "user"
| "assistant" | "assistant"
| "automation_state"
| "permission_request" | "permission_request"
| "permission_response" | "permission_response"
| "control_request" | "control_request"
@@ -50,7 +49,6 @@ export type SessionEventType =
export interface NormalizedEventPayload { export interface NormalizedEventPayload {
content: string; content: string;
raw?: unknown; raw?: unknown;
isSynthetic?: boolean;
[key: string]: unknown; [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"], "include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist", "web"] "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 { getUuid, setUuid, apiBind, apiFetchSessions, apiFetchAllSessions, apiFetchEnvironments, apiFetchSession, apiFetchSessionHistory, apiSendEvent, apiSendControl, apiInterrupt, apiCreateSession } from "./api.js";
import { connectSSE, disconnectSSE } from "./sse.js"; import { connectSSE, disconnectSSE } from "./sse.js";
import { import { appendEvent, renderPermissionRequest, showLoading, isLoading, resetReplayState, renderReplayPendingRequests } from "./render.js";
appendEvent,
getActivityMode,
removeLoading,
resetReplayState,
renderReplayPendingRequests,
setAutomationActivity,
showLoading,
} from "./render.js";
import { initTaskPanel, toggleTaskPanel, resetTaskState } from "./task-panel.js"; import { initTaskPanel, toggleTaskPanel, resetTaskState } from "./task-panel.js";
import { import { esc, formatTime, statusClass } from "./utils.js";
createAutomationState,
getAutomationActivity,
getAutomationIndicator,
reduceAutomationState,
renderAutomationIcon,
shouldPulseAutomationIndicator,
} from "./automation.js";
import { esc, formatTime, statusClass, isClosedSessionStatus } from "./utils.js";
// ============================================================ // ============================================================
// State // State
// ============================================================ // ============================================================
let currentSessionId = null; let currentSessionId = null;
let currentSessionStatus = null;
let dashboardInterval = null; let dashboardInterval = null;
let cachedEnvs = []; let cachedEnvs = [];
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 // Router
@@ -145,72 +43,6 @@ function navigate(path) {
} }
window.navigate = navigate; window.navigate = navigate;
function applySessionStatus(status) {
currentSessionStatus = status || null;
const badge = document.getElementById("session-status");
if (badge) {
badge.textContent = status || "";
badge.className = `status-badge status-${statusClass(status)}`;
}
const closed = isClosedSessionStatus(status);
const input = document.getElementById("msg-input");
if (input) {
input.disabled = closed;
input.placeholder = closed ? "Session is closed" : "Type a message...";
}
const actionBtn = document.getElementById("action-btn");
if (actionBtn) {
actionBtn.disabled = closed;
actionBtn.title = closed ? "Session is closed" : "";
}
if (closed) {
removeLoading();
window.__updateActionBtn?.("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() { async function handleRoute() {
// Ensure we have a UUID // Ensure we have a UUID
getUuid(); getUuid();
@@ -254,9 +86,6 @@ async function handleRoute() {
} }
// Default: /code → dashboard // Default: /code → dashboard
currentSessionId = null;
currentSessionStatus = null;
resetAutomationIndicator();
showPage("dashboard"); showPage("dashboard");
disconnectSSE(); disconnectSSE();
renderDashboard(); renderDashboard();
@@ -331,8 +160,6 @@ function stopDashboardRefresh() {
async function renderSessionDetail(id) { async function renderSessionDetail(id) {
currentSessionId = id; currentSessionId = id;
resetAutomationIndicator();
let session = null;
// Reset task state for new session and init panel // Reset task state for new session and init panel
resetTaskState(); resetTaskState();
@@ -340,12 +167,14 @@ async function renderSessionDetail(id) {
if (taskPanelEl) initTaskPanel(taskPanelEl); if (taskPanelEl) initTaskPanel(taskPanelEl);
try { try {
session = await apiFetchSession(id); const session = await apiFetchSession(id);
document.getElementById("session-title").textContent = session.title || session.id; document.getElementById("session-title").textContent = session.title || session.id;
document.getElementById("session-id").textContent = session.id; document.getElementById("session-id").textContent = session.id;
document.getElementById("session-env").textContent = session.environment_id || ""; document.getElementById("session-env").textContent = session.environment_id || "";
document.getElementById("session-time").textContent = formatTime(session.created_at); document.getElementById("session-time").textContent = formatTime(session.created_at);
applySessionStatus(session.status); const badge = document.getElementById("session-status");
badge.textContent = session.status;
badge.className = `status-badge status-${statusClass(session.status)}`;
} catch (err) { } catch (err) {
alert("Failed to load session: " + err.message); alert("Failed to load session: " + err.message);
navigate("/code/"); navigate("/code/");
@@ -354,7 +183,6 @@ async function renderSessionDetail(id) {
document.getElementById("event-stream").innerHTML = ""; document.getElementById("event-stream").innerHTML = "";
document.getElementById("permission-area").innerHTML = ""; document.getElementById("permission-area").innerHTML = "";
document.getElementById("permission-area").classList.add("hidden"); document.getElementById("permission-area").classList.add("hidden");
applyAutomationSnapshot(session?.automation_state);
// Load historical events before connecting to live stream // Load historical events before connecting to live stream
resetReplayState(); resetReplayState();
@@ -363,7 +191,6 @@ async function renderSessionDetail(id) {
const { events } = await apiFetchSessionHistory(id); const { events } = await apiFetchSessionHistory(id);
if (events && events.length > 0) { if (events && events.length > 0) {
for (const event of events) { for (const event of events) {
applyAutomationEvent(event, { replay: true });
appendEvent(event, { replay: true }); appendEvent(event, { replay: true });
if (event.seqNum && event.seqNum > lastSeqNum) lastSeqNum = event.seqNum; 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 // Re-render any still-unresolved permission prompts from history
renderReplayPendingRequests(); renderReplayPendingRequests();
if (isClosedSessionStatus(currentSessionStatus)) { connectSSE(id, appendEvent, lastSeqNum);
const closedEvent = { type: "session_status", payload: { status: currentSessionStatus } };
applyAutomationEvent(closedEvent);
appendEvent(closedEvent);
disconnectSSE();
return;
}
connectSSE(id, handleSessionEvent, lastSeqNum);
} }
// ============================================================ // ============================================================
@@ -395,20 +214,17 @@ function setupControlBar() {
const iconSend = document.getElementById("action-icon-send"); const iconSend = document.getElementById("action-icon-send");
const iconStop = document.getElementById("action-icon-stop"); const iconStop = document.getElementById("action-icon-stop");
function setBtnState(mode) { function setBtnState(loading) {
const working = mode === "working"; actionBtn.classList.toggle("loading", loading);
actionBtn.classList.toggle("loading", working); actionBtn.setAttribute("aria-label", loading ? "Stop" : "Send");
actionBtn.dataset.mode = mode || "idle"; iconSend.classList.toggle("hidden", loading);
actionBtn.setAttribute("aria-label", working ? "Stop" : "Send"); iconStop.classList.toggle("hidden", !loading);
iconSend.classList.toggle("hidden", working);
iconStop.classList.toggle("hidden", !working);
} }
window.__updateActionBtn = setBtnState; window.__updateActionBtn = setBtnState;
setBtnState(getActivityMode());
actionBtn.addEventListener("click", () => { actionBtn.addEventListener("click", () => {
if (getActivityMode() === "working") { if (isLoading()) {
doInterrupt(); doInterrupt();
} else { } else {
sendMessage(); sendMessage();
@@ -421,34 +237,28 @@ function setupControlBar() {
} }
async function doInterrupt() { async function doInterrupt() {
if (!currentSessionId || isClosedSessionStatus(currentSessionStatus)) return; if (!currentSessionId) return;
const btn = document.getElementById("action-btn"); const btn = document.getElementById("action-btn");
btn.disabled = true; btn.disabled = true;
try { try {
await apiInterrupt(currentSessionId); await apiInterrupt(currentSessionId);
appendEvent({ type: "interrupt", payload: { message: "Session interrupted" } });
} catch (err) { } catch (err) {
await syncClosedSessionState(err, "Interrupt failed"); alert("Interrupt failed: " + err.message);
} finally { } finally {
btn.disabled = isClosedSessionStatus(currentSessionStatus); btn.disabled = false;
} }
} }
async function sendMessage() { async function sendMessage() {
const input = document.getElementById("msg-input"); const input = document.getElementById("msg-input");
const text = input.value.trim(); const text = input.value.trim();
if (!text || !currentSessionId || isClosedSessionStatus(currentSessionStatus)) return; if (!text || !currentSessionId) return;
input.value = ""; input.value = "";
const uuid = generateMessageUuid();
try { try {
await apiSendEvent(currentSessionId, { await apiSendEvent(currentSessionId, { type: "user", content: text });
type: "user",
uuid,
content: text,
message: { content: text },
});
} catch (err) { } catch (err) {
input.value = text; alert("Failed to send: " + err.message);
await syncClosedSessionState(err, "Failed to send");
} }
} }
@@ -566,28 +376,11 @@ window._submitAnswers = async function (requestId, btn) {
function removePermissionPrompt(btn) { function removePermissionPrompt(btn) {
const prompt = btn.closest(".permission-prompt, .ask-panel, .plan-panel"); const prompt = btn.closest(".permission-prompt, .ask-panel, .plan-panel");
const requestId = prompt?.dataset?.requestId || null;
if (prompt) prompt.remove(); 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"); const area = document.getElementById("permission-area");
if (area && area.children.length === 0) area.classList.add("hidden"); 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 // ExitPlanMode interactions
// ============================================================ // ============================================================
@@ -632,7 +425,6 @@ window._submitPlanResponse = async function (requestId, btn) {
...(feedback ? { message: feedback } : {}), ...(feedback ? { message: feedback } : {}),
}); });
removePermissionPrompt(btn); removePermissionPrompt(btn);
appendLocalSystemMessage("Feedback sent. Continuing in plan mode.");
} else { } else {
// Approval with permission mode // Approval with permission mode
const modeMap = { 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-active, .status-running { background: var(--green-bg); color: var(--green); }
.status-idle { background: var(--yellow-bg); color: var(--yellow); } .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-requires_action { background: var(--orange-bg); color: var(--orange); }
.status-archived { background: #F0ECE7; color: var(--text-secondary); } .status-archived { background: #F0ECE7; color: var(--text-secondary); }
.status-error { background: var(--red-bg); color: var(--red); } .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.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <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="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> </head>
<body> <body>
<!-- Nav Bar --> <!-- Nav Bar -->
@@ -81,7 +81,6 @@
<div class="session-meta-row"> <div class="session-meta-row">
<span id="session-id" class="meta-item"></span> <span id="session-id" class="meta-item"></span>
<span id="session-status" class="status-badge"></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-env" class="meta-item"></span>
<span id="session-time" 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"> <button id="task-panel-toggle" class="nav-link btn-text" title="Tasks & Todos">
@@ -147,6 +146,6 @@
<!-- QR Libraries --> <!-- QR Libraries -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script> <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 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> </body>
</html> </html>

View File

@@ -24,7 +24,6 @@
.msg-row.user { align-self: flex-end; } .msg-row.user { align-self: flex-end; }
.msg-row.assistant { align-self: flex-start; } .msg-row.assistant { align-self: flex-start; }
.msg-row.tool { align-self: flex-start; max-width: 95%; } .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.system { align-self: center; }
.msg-row.result { align-self: center; } .msg-row.result { align-self: center; }
@@ -52,124 +51,6 @@
box-shadow: var(--shadow-sm); 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 { .msg-row.system .msg-bubble {
background: transparent; background: transparent;
color: var(--text-muted); color: var(--text-muted);
@@ -217,7 +98,6 @@
font-size: 0.7rem; font-size: 0.7rem;
transition: transform var(--transition-fast); transition: transform var(--transition-fast);
} }
.tool-card-header.is-open .tool-icon,
.tool-card-header:hover .tool-icon { transform: rotate(90deg); } .tool-card-header:hover .tool-icon { transform: rotate(90deg); }
.tool-card-body { .tool-card-body {
@@ -449,51 +329,15 @@
line-height: 1.6; line-height: 1.6;
color: var(--text-primary); 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 { .plan-panel .plan-content pre {
background: var(--bg-tool-card); background: var(--bg-tool-card);
padding: 10px; padding: 10px;
border-radius: 6px; border-radius: 6px;
overflow-x: auto; overflow-x: auto;
margin: 10px 0; margin: 6px 0;
font-family: var(--font-mono); font-family: var(--font-mono);
font-size: 0.82rem; 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 { .plan-panel .plan-content code {
background: var(--bg-tool-card); background: var(--bg-tool-card);
padding: 2px 5px; padding: 2px 5px;
@@ -635,58 +479,3 @@
font-family: var(--font-mono); font-family: var(--font-mono);
margin-left: auto; 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; gap: 12px;
align-items: center; 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 { .meta-item {
font-size: 0.8rem; font-size: 0.8rem;
color: var(--text-secondary); 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 { esc } from "./utils.js";
import { import { processAssistantEvent } from "./task-panel.js";
extractEventText,
renderAutomationIcon,
shouldHideAutomationUserEvent,
shouldStartAutomationWorkFromUserEvent,
} from "./automation.js";
import { applyTaskStateEvent, processAssistantEvent } from "./task-panel.js";
// ============================================================ // ============================================================
// Replay state — tracks unresolved permission requests during history replay // 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 replayPendingRequests = new Map(); // request_id → event data (unresolved)
const replayRespondedRequests = new Set(); // request_ids that have a response 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) */ /** Clear replay tracking state (call before each history load) */
export function resetReplayState() { export function resetReplayState() {
replayPendingRequests.clear(); replayPendingRequests.clear();
replayRespondedRequests.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 */ /** 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. * Server-side normalization guarantees payload.content is a string.
* Falls back to raw/message parsing for backward compat. * Falls back to raw/message parsing for backward compat.
*/ */
export const extractText = extractEventText; export function extractText(payload) {
if (!payload) return "";
function formatInlineContent(content) { // Normalized format (server standardized)
let html = esc(content); if (typeof payload.content === "string" && payload.content) return payload.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>'); // Fallback: raw message.content (child process format)
// Bold: **...** const msg = payload.message;
html = html.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>"); if (msg && typeof msg === "object") {
return html; 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) { function formatAssistantContent(content) {
@@ -177,140 +77,13 @@ function formatAssistantContent(content) {
html = html.replace(/```(\w*)\n?([\s\S]*?)```/g, (_, lang, code) => { 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>`; 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; 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 // Event Router
// ============================================================ // ============================================================
@@ -330,75 +103,26 @@ export function appendEvent(data, { replay = false } = {}) {
// During history replay, only render messages & tools — skip interactive/stateful events // During history replay, only render messages & tools — skip interactive/stateful events
// Exception: unresolved permission/control requests are re-shown as pending prompts. // Exception: unresolved permission/control requests are re-shown as pending prompts.
if (replay) { if (replay) {
const histEls = []; let histEl;
switch (type) { switch (type) {
case "user": case "user":
{ if (direction === "outbound") histEl = renderUserMessage(payload, direction);
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));
}
}
break; break;
case "assistant": case "assistant":
{ {
const text = extractText(payload); const text = extractText(payload);
const toolUseBlocks = getEmbeddedToolBlocks(payload, "tool_use"); if (text && text.trim()) histEl = renderAssistantMessage(payload);
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,
);
}
processAssistantEvent(payload); processAssistantEvent(payload);
} }
break; break;
case "task_state":
applyTaskStateEvent(payload);
return;
case "automation_state":
return;
case "status":
if (isConversationClearedStatus(payload)) {
clearTranscriptView();
}
return;
case "tool_use": case "tool_use":
appendToolEntryToActiveTrace("use", payload, histEls); histEl = renderToolUse(payload);
break; break;
case "tool_result": case "tool_result":
appendToolEntryToActiveTrace("result", payload, histEls); histEl = renderToolResult(payload);
break; break;
case "error": case "error":
histEls.push(renderSystemMessage(`Error: ${payload.message || payload.content || "Unknown error"}`)); histEl = 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}`));
}
break; break;
case "control_request": case "control_request":
case "permission_request": case "permission_request":
@@ -425,84 +149,45 @@ export function appendEvent(data, { replay = false } = {}) {
default: default:
return; return;
} }
for (const histEl of histEls) { if (histEl) {
stream.appendChild(histEl); stream.appendChild(histEl);
stream.scrollTop = stream.scrollHeight; stream.scrollTop = stream.scrollHeight;
} }
return; return;
} }
const els = []; let el;
let needLoading = false; let needLoading = false;
switch (type) { switch (type) {
case "user": case "user":
{ // Skip inbound user messages — they're echoes of what we already sent
const toolResultBlocks = getEmbeddedToolBlocks(payload, "tool_result"); if (direction === "inbound") return;
if (toolResultBlocks.length > 0) { el = renderUserMessage(payload, direction);
for (const block of toolResultBlocks) { needLoading = true;
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);
}
}
break; break;
case "partial_assistant": case "partial_assistant":
// Skip partial assistant — wait for the final "assistant" event // Skip partial assistant — wait for the final "assistant" event
// to avoid blank/duplicate messages during streaming // to avoid blank/duplicate messages during streaming
return; return;
case "assistant": case "assistant":
removeLoading();
{ {
const text = extractText(payload); const text = extractText(payload);
const toolUseBlocks = getEmbeddedToolBlocks(payload, "tool_use"); if (text && text.trim()) el = renderAssistantMessage(payload);
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,
);
}
processAssistantEvent(payload); processAssistantEvent(payload);
} }
break; break;
case "task_state":
applyTaskStateEvent(payload);
return;
case "automation_state":
return;
case "result": case "result":
case "result_success": case "result_success":
removeLoading(); removeLoading();
// Skip result — it just repeats the assistant message content // Skip result — it just repeats the assistant message content
return; return;
case "tool_use": case "tool_use":
appendToolEntryToActiveTrace("use", payload, els); el = renderToolUse(payload);
break; break;
case "tool_result": case "tool_result":
appendToolEntryToActiveTrace("result", payload, els); el = renderToolResult(payload);
break; break;
case "control_request": case "control_request":
case "permission_request": case "permission_request":
@@ -510,27 +195,27 @@ export function appendEvent(data, { replay = false } = {}) {
const toolName = payload.request.tool_name || "unknown"; const toolName = payload.request.tool_name || "unknown";
const toolInput = payload.request.input || payload.request.tool_input || {}; const toolInput = payload.request.input || payload.request.tool_input || {};
if (toolName === "AskUserQuestion") { if (toolName === "AskUserQuestion") {
els.push(renderAskUserQuestion({ el = renderAskUserQuestion({
request_id: payload.request_id || data.id, request_id: payload.request_id || data.id,
tool_input: toolInput, tool_input: toolInput,
description: payload.request.description || "", description: payload.request.description || "",
})); });
} else if (toolName === "ExitPlanMode") { } else if (toolName === "ExitPlanMode") {
els.push(renderExitPlanMode({ el = renderExitPlanMode({
request_id: payload.request_id || data.id, request_id: payload.request_id || data.id,
tool_input: toolInput, tool_input: toolInput,
description: payload.request.description || "", description: payload.request.description || "",
})); });
} else { } else {
els.push(renderPermissionRequest({ el = renderPermissionRequest({
request_id: payload.request_id || data.id, request_id: payload.request_id || data.id,
tool_name: toolName, tool_name: toolName,
tool_input: toolInput, tool_input: toolInput,
description: payload.request.description || "", description: payload.request.description || "",
})); });
} }
} else { } else {
els.push(renderSystemMessage(`Control: ${payload.request?.subtype || "unknown"}`)); el = renderSystemMessage(`Control: ${payload.request?.subtype || "unknown"}`);
} }
break; break;
case "control_response": case "control_response":
@@ -539,31 +224,21 @@ export function appendEvent(data, { replay = false } = {}) {
return; return;
case "status": case "status":
// Skip connecting/waiting status noise from bridge // Skip connecting/waiting status noise from bridge
if (isConversationClearedStatus(payload)) {
clearTranscriptView();
return;
}
{ {
const msg = payload.message || payload.content || ""; const msg = payload.message || payload.content || "";
const fullText = typeof payload === "string" ? payload : JSON.stringify(payload); const fullText = typeof payload === "string" ? payload : JSON.stringify(payload);
if (/connecting|waiting|initializing|Remote Control/i.test(msg + " " + fullText)) return; if (/connecting|waiting|initializing|Remote Control/i.test(msg + " " + fullText)) return;
if (!msg.trim()) return; if (!msg.trim()) return;
els.push(renderSystemMessage(msg)); el = renderSystemMessage(msg);
} }
break; break;
case "error": case "error":
removeLoading(); removeLoading();
els.push(renderSystemMessage(`Error: ${payload.message || payload.content || "Unknown error"}`)); el = 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}`));
}
break; break;
case "interrupt": case "interrupt":
removeLoading(); removeLoading();
els.push(renderSystemMessage("Session interrupted")); el = renderSystemMessage("Session interrupted");
break; break;
case "system": case "system":
// Skip raw system/init messages — they're noise // Skip raw system/init messages — they're noise
@@ -572,11 +247,11 @@ export function appendEvent(data, { replay = false } = {}) {
// Skip noise from bridge init // Skip noise from bridge init
const raw = JSON.stringify(payload); const raw = JSON.stringify(payload);
if (/Remote Control connecting/i.test(raw)) return; 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.appendChild(el);
stream.scrollTop = stream.scrollHeight; stream.scrollTop = stream.scrollHeight;
} }
@@ -597,90 +272,12 @@ function renderUserMessage(payload, direction) {
return row; 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) { function renderAssistantMessage(payload) {
const content = extractText(payload).trim(); const content = extractText(payload);
const result = addAssistantToolTraceHost(toolTraceState, content); const row = document.createElement("div");
toolTraceState = result.state; row.className = "msg-row assistant";
return ensureTraceHostRow(result.host, null, content); row.innerHTML = `<div class="msg-bubble">${formatAssistantContent(content)}</div>`;
return row;
} }
function renderResult(payload) { function renderResult(payload) {
@@ -691,64 +288,37 @@ function renderResult(payload) {
return row; 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) { function renderToolUse(payload) {
const name = payload.tool_name || payload.name || "tool"; const name = payload.tool_name || payload.name || "tool";
const input = payload.tool_input || payload.input || {}; const input = payload.tool_input || payload.input || {};
const inputStr = typeof input === "string" ? input : JSON.stringify(input, null, 2); const inputStr = typeof input === "string" ? input : JSON.stringify(input, null, 2);
return renderToolCard({
titleHtml: `Tool: <strong>${esc(name)}</strong>`, const card = document.createElement("div");
body: inputStr || "", 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) { function renderToolResult(payload) {
const content = payload.content || payload.output || ""; const content = payload.content || payload.output || "";
const contentStr = typeof content === "string" ? content : JSON.stringify(content, null, 2); 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 card = document.createElement("div");
const result = addToolTraceEntry(toolTraceState, entryKind); card.className = "msg-row tool";
toolTraceState = result.state; card.innerHTML = `
<div class="tool-card">
if (result.createdHost) { <div class="tool-card-header" onclick="this.nextElementSibling.classList.toggle('collapsed')">
ensureTraceHostRow(result.createdHost, rows); <span class="tool-icon">&#9654;</span> Tool Result
} </div>
<div class="tool-card-body collapsed">${esc(truncate(contentStr, 2000))}</div>
const refs = traceHostElements.get(result.host.id); </div>`;
if (!refs) return; return card;
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);
} }
export function renderPermissionRequest(payload) { export function renderPermissionRequest(payload) {
@@ -859,9 +429,7 @@ export function renderAskUserQuestion(payload) {
el._answers = {}; el._answers = {};
el._questions = questions; el._questions = questions;
const status = renderSystemMessage("Waiting for your response..."); return renderSystemMessage("Waiting for your response...");
status.dataset.pendingRequestId = requestId;
return status;
} }
export function renderExitPlanMode(payload) { export function renderExitPlanMode(payload) {
@@ -896,7 +464,7 @@ export function renderExitPlanMode(payload) {
} else { } else {
el.innerHTML = ` el.innerHTML = `
<div class="plan-title">Ready to code?</div> <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"> <div class="plan-options">
<button class="plan-option" data-value="yes-accept-edits" onclick="window._selectPlanOption(this, 'yes-accept-edits')"> <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> <span class="plan-option-label">Yes, auto-accept edits</span>
@@ -925,9 +493,7 @@ export function renderExitPlanMode(payload) {
el._planContent = planContent; el._planContent = planContent;
el._isEmpty = isEmpty; el._isEmpty = isEmpty;
const status = renderSystemMessage("Waiting for your response..."); return renderSystemMessage("Waiting for your response...");
status.dataset.pendingRequestId = requestId;
return status;
} }
function renderSystemMessage(text) { function renderSystemMessage(text) {
@@ -941,10 +507,10 @@ function renderSystemMessage(text) {
// Loading Indicator — TUI star spinner style // 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) // TUI star spinner frames (same as Claude Code CLI)
const SPINNER_FRAMES = ["·", "✢", "", "✶", "✻", "✽"]; const SPINNER_FRAMES = ["·", "✢", "", "✶", "✻", "✽"];
const SPINNER_CYCLE = [...SPINNER_FRAMES, ...SPINNER_FRAMES.slice().reverse()]; const SPINNER_CYCLE = [...SPINNER_FRAMES, ...SPINNER_FRAMES.slice().reverse()];
// 204 verbs from TUI src/constants/spinnerVerbs.ts // 204 verbs from TUI src/constants/spinnerVerbs.ts
@@ -987,85 +553,35 @@ const SPINNER_VERBS = [
let spinnerInterval = null; let spinnerInterval = null;
let timerInterval = null; let timerInterval = null;
let stalledCheckInterval = null; let stalledCheckInterval = null;
let activityCountdownInterval = null;
let spinnerFrame = 0; let spinnerFrame = 0;
let loadingStartTime = 0; let loadingStartTime = 0;
let lastActivityTime = 0; let lastActivityTime = 0;
let isStalled = false; let isStalled = false;
let workingActive = false; let loadingActive = 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);
}
export function isLoading() { export function isLoading() {
return getActivityModeInternal() === "working"; return loadingActive;
} }
export function getActivityMode() { function syncActionBtn(state) {
return getActivityModeInternal(); if (typeof window.__updateActionBtn === "function") window.__updateActionBtn(state);
} }
function syncActionBtn(mode) { export function showLoading() {
if (typeof window.__updateActionBtn === "function") window.__updateActionBtn(mode); removeLoading();
} const stream = document.getElementById("event-stream");
if (!stream) return;
function clearWorkingTimers() { loadingActive = true;
if (spinnerInterval) { clearInterval(spinnerInterval); spinnerInterval = null; } syncActionBtn(true);
if (timerInterval) { clearInterval(timerInterval); timerInterval = null; }
if (stalledCheckInterval) { clearInterval(stalledCheckInterval); stalledCheckInterval = null; }
isStalled = false;
}
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)]; const verb = SPINNER_VERBS[Math.floor(Math.random() * SPINNER_VERBS.length)];
loadingStartTime = Date.now(); loadingStartTime = Date.now();
lastActivityTime = Date.now(); lastActivityTime = Date.now();
isStalled = false; isStalled = false;
const el = document.createElement("div"); const el = document.createElement("div");
el.id = ACTIVITY_ID; el.id = LOADING_ID;
el.className = "msg-row loading-row"; 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>`; 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); stream.appendChild(el);
@@ -1075,12 +591,14 @@ function renderWorkingIndicator(stream) {
const timerEl = el.querySelector(".tui-timer"); const timerEl = el.querySelector(".tui-timer");
const loadingEl = el; const loadingEl = el;
// Spinner animation — 120ms interval, same as TUI
spinnerFrame = 0; spinnerFrame = 0;
spinnerInterval = setInterval(() => { spinnerInterval = setInterval(() => {
spinnerFrame = (spinnerFrame + 1) % SPINNER_CYCLE.length; spinnerFrame = (spinnerFrame + 1) % SPINNER_CYCLE.length;
if (spinnerEl) spinnerEl.textContent = SPINNER_CYCLE[spinnerFrame]; if (spinnerEl) spinnerEl.textContent = SPINNER_CYCLE[spinnerFrame];
}, 120); }, 120);
// Timer — update every second
timerInterval = setInterval(() => { timerInterval = setInterval(() => {
if (timerEl) { if (timerEl) {
const elapsed = Math.floor((Date.now() - loadingStartTime) / 1000); const elapsed = Math.floor((Date.now() - loadingStartTime) / 1000);
@@ -1088,6 +606,7 @@ function renderWorkingIndicator(stream) {
} }
}, 1000); }, 1000);
// Stalled detection — check every 120ms (aligned with spinner)
stalledCheckInterval = setInterval(() => { stalledCheckInterval = setInterval(() => {
if (!isStalled && Date.now() - lastActivityTime > 3000) { if (!isStalled && Date.now() - lastActivityTime > 3000) {
isStalled = true; isStalled = true;
@@ -1096,62 +615,15 @@ function renderWorkingIndicator(stream) {
}, 120); }, 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() { export function removeLoading() {
workingActive = false; if (spinnerInterval) { clearInterval(spinnerInterval); spinnerInterval = null; }
renderActivityIndicator(); 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 */ /** Reset stalled timer — call when SSE events arrive */
@@ -1159,7 +631,7 @@ export function refreshLoadingActivity() {
lastActivityTime = Date.now(); lastActivityTime = Date.now();
if (isStalled) { if (isStalled) {
isStalled = false; isStalled = false;
const loadingEl = document.getElementById(ACTIVITY_ID); const loadingEl = document.getElementById(LOADING_ID);
if (loadingEl) loadingEl.classList.remove("stalled"); if (loadingEl) loadingEl.classList.remove("stalled");
} }
} }

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