diff --git a/CLAUDE.md b/CLAUDE.md
index a5d486ae..beb60a43 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -8,6 +8,7 @@ Electron 28.x, React 18.x, TypeScript 5.x, Tailwind CSS 3.x, Zustand 4.x
## Commands
Always use pnpm (not npm/yarn) for this project.
Do NOT run `pnpm lint:fix` unless the user explicitly asks for it — it interferes with agents running in parallel.
+When running build/typecheck/test commands, pipe through `tail -20` to avoid flooding the context window (e.g. `pnpm typecheck 2>&1 | tail -20`).
- `pnpm install` - Install dependencies
- `pnpm dev` - Dev server with hot reload
diff --git a/README.md b/README.md
index 788f3ac4..4c3bb632 100644
--- a/README.md
+++ b/README.md
@@ -14,52 +14,87 @@
- 100% free, open source. No API keys. No configuration.
+ 100% free, open source. No API keys. No configuration. Runs entirely locally.
+## Table of Contents
+
+- [What is this](#what-is-this)
+- [Quick start](#quick-start)
+- [Installation](#installation)
+- [FAQ](#faq)
+- [Development](#development)
+- [Roadmap](#roadmap)
+- [Links](#links)
+- [Contributing](#contributing)
+- [Security](#security)
+- [License](#license)
+
+---
+
## What is this
-A new approach to task management with AI agents.
+**Claude Agent Teams UI** is a desktop app that turns Claude Code's "Orchestrate Teams" feature into a full task management experience. Create agent teams, watch them work on a kanban board, review their code changes, and stay in control — all running locally on your machine.
-- **Assemble your team** — create agent teams with different roles that work autonomously in parallel
-- **Agents talk to each other** — they communicate, create and manage their own tasks, and leave comments
-- **Sit back and watch** — tasks change status on the kanban board while agents handle everything on their own
-- **Review changes like in Cursor** — see what code each task changed, then approve, reject, or comment
-- **Full tool visibility** — inspect exactly which tools an agent used to complete each task
-- **Live process section** — see which agents are running processes and open URLs directly in the browser
-- **Stay in control** — send a direct message to any agent, drop a comment on a task, or pick a quick action right on the kanban card whenever you want to clarify something or add new work
+### Who is this for
+
+- **Developers** who want AI agents to handle tasks in parallel while they oversee progress
+- **Teams** using Claude Code and needing a shared task board, code review workflow, and team messaging
+- **Anyone** who wants to browse and analyze Claude Code session history without running agents
+
+### How it works
+
+1. **Create a team** — Define roles (e.g. lead, frontend, backend) and a provisioning prompt. The app spawns Claude Code sessions as autonomous team members.
+2. **Tasks flow automatically** — Agents create tasks, assign each other, move cards, leave comments, and send messages. You see everything on the kanban board in real time.
+3. **Review like in Cursor** — When a task is done, you see the diff, approve, reject, or request changes. Agents get notified and can fix issues.
+4. **Stay in control** — Send a direct message to any agent, add a comment on a task, or use quick actions on cards whenever you need to clarify or add work.
+
+### Key features
+
+| Feature | Description |
+|---------|-------------|
+| **Kanban board** | Tasks move through columns as agents work. Drag-and-drop, filters, quick actions. |
+| **Code review** | Full diff view per task. Approve, reject, or request changes. Agents get notified. |
+| **Team messaging** | Agents send direct messages. Inbox, notifications, clarification flags. |
+| **Tool visibility** | See exactly which tools agents used (Bash, Read, Edit, etc.) for each task. |
+| **Live processes** | See which agents are running. Open URLs directly in the browser. |
+| **Session analysis** | Deep breakdown of each Claude session: commands, reasoning, subprocesses. |
More features
-- **Recent tasks across projects** — browse the latest completed tasks from all your projects in one place
-- **Deep session analysis** — detailed breakdown of what happened in each Claude session: bash commands, reasoning, subprocesses
-- **Smart task-to-log matching** — automatically links Claude session logs to specific tasks based on status change timestamps, even when a task moves back and forth between states
-- **Solo mode** — a one-member team: a single agent that creates its own tasks, leaves comments, and shows live progress on the kanban board — saves tokens compared to a full team and can be expanded to a full team at any time
-- **Zero-setup onboarding** — built-in Claude Code installation and authentication, ready to go out of the box
-- **Built-in code editor** — edit project files with Git support and other essential features without leaving the app
-- **Branch strategy control** — choose via prompt whether all agents work on a single branch or each gets its own git worktree
-- **Team member stats** — global performance statistics for every member of the team
-- **Attach code context** — reference files or code snippets in your messages, just like in Cursor
-- **Notification system** — configurable alerts when tasks complete, agents need attention, or errors occur
+- **Recent tasks across projects** — Browse the latest completed tasks from all your projects in one place
+- **Solo mode** — One-member team: a single agent that creates its own tasks and shows live progress. Saves tokens; can expand to a full team anytime
+- **Advanced context monitoring** — Token breakdown: user messages, CLAUDE.md, tool outputs, thinking text, team coordination. See usage, context window %, and cost per category
+- **Smart task-to-log matching** — Links Claude session logs to tasks by status change timestamps, even when tasks move back and forth
+- **Zero-setup onboarding** — Built-in Claude Code installation and authentication
+- **Built-in code editor** — Edit project files with Git support without leaving the app
+- **Branch strategy** — Choose via prompt: single branch or git worktree per agent
+- **Team member stats** — Global performance statistics per member
+- **Attach code context** — Reference files or snippets in messages, like in Cursor
+- **Notification system** — Configurable alerts when tasks complete, agents need attention, or errors occur
+- **MCP integration** — Built-in [mcp-server](./mcp-server) for Cursor, Claude Desktop, and other MCP clients. 13 tools for task CRUD, kanban, reviews, and messaging — see [mcp-server/README.md](./mcp-server/README.md)
----
+### Tech stack
-
## Installation
@@ -101,6 +136,8 @@ No prerequisites — Claude Code can be installed and configured directly from t
+**System requirements:** macOS 10.15+, Windows 10+, or Linux (glibc 2.28+). Node.js is not required for the desktop app.
+
---
## FAQ
@@ -114,19 +151,19 @@ No. The app includes built-in installation and authentication — just launch an
Does it read or upload my code?
-No. Everything runs locally on your machine. The app reads Claude Code's session logs from ~/.claude/ — your source code is never sent anywhere.
+No. Everything runs locally. The app reads Claude Code's session logs from ~/.claude/ — your source code is never sent anywhere.
Can agents communicate with each other?
-Yes. Agents send direct messages, create shared tasks, and leave comments — all coordinated automatically through Claude Code's team protocol.
+Yes. Agents send direct messages, create shared tasks, and leave comments — all coordinated through Claude Code's team protocol.
Is it free?
-Yes, completely free and open source. The app itself requires no API keys or subscriptions. You only need a Claude Code plan from Anthropic to run agents.
+Yes, completely free and open source. The app requires no API keys or subscriptions. You only need a Claude Code plan from Anthropic to run agents.
@@ -138,13 +175,13 @@ Yes. Every task shows a full diff view where you can accept, reject, or comment
What happens if an agent gets stuck?
-You can send a direct message to any agent at any time to course-correct, or stop and restart it from the process dashboard. If an agent needs your input, you'll get a notification and the task will be marked with a distinct badge on the board so you won't miss it.
+Send a direct message to course-correct, or stop and restart from the process dashboard. If an agent needs your input, you'll get a notification and the task will show a distinct badge on the board.
Can I use it just to view past sessions without running agents?
-Yes. The app works as a session viewer too — browse, search, and analyze any Claude Code session history.
+Yes. The app works as a session viewer — browse, search, and analyze any Claude Code session history.
@@ -171,9 +208,9 @@ pnpm install
pnpm dev
```
-The app auto-discovers your Claude Code projects from `~/.claude/`.
+The app auto-discovers Claude Code projects from `~/.claude/`.
-#### Build for Distribution
+### Build for distribution
```bash
pnpm dist:mac:arm64 # macOS Apple Silicon (.dmg)
@@ -183,32 +220,48 @@ pnpm dist:linux # Linux (AppImage/.deb/.rpm/.pacman)
pnpm dist # macOS + Windows + Linux
```
-#### Scripts
+### Scripts
| Command | Description |
|---------|-------------|
| `pnpm dev` | Development with hot reload |
| `pnpm build` | Production build |
| `pnpm typecheck` | TypeScript type checking |
+| `pnpm lint` | Lint (no auto-fix) |
| `pnpm lint:fix` | Lint and auto-fix |
+| `pnpm format` | Format code with Prettier |
| `pnpm test` | Run all tests |
| `pnpm test:watch` | Watch mode |
| `pnpm test:coverage` | Coverage report |
+| `pnpm test:coverage:critical` | Critical path coverage |
| `pnpm check` | Full quality gate (types + lint + test + build) |
+| `pnpm fix` | Lint fix + format |
+| `pnpm quality` | Full check + format check + knip |
---
-## TODO
+## Roadmap
-- [ ] CLI runtime: Run not only on a local PC but in any headless/console environment (web UI), e.g. VPS, remote server, etc.
+- [ ] CLI runtime: Run in headless/console environments (web UI), e.g. VPS, remote server
- [ ] Visual workflow editor ([@xyflow/react](https://github.com/xyflow/xyflow)) for building and orchestrating agent pipelines with drag & drop
-- [ ] Context management: control and curate what context each agent sees (files, docs, MCP servers, skills)
+- [ ] Install skills, MCP, and integrations via an intuitive UI, and only for selected agents
+- [ ] Planning mode to organize agent plans before execution
+- [ ] Curate what context each agent sees (files, docs, MCP servers, skills)
- [ ] Multi-model support: proxy layer to use other popular LLMs (GPT, Gemini, DeepSeek, Llama, etc.), including offline/local models
---
+## Links
+
+- [Homepage](https://github.com/777genius/claude_agent_teams_ui)
+- [Releases](https://github.com/777genius/claude_agent_teams_ui/releases)
+- [Issues](https://github.com/777genius/claude_agent_teams_ui/issues)
+- [MCP Server](./mcp-server) — Use Claude Agent Teams UI tools from Cursor, Claude Desktop, and other MCP clients
+
+---
+
## Contributing
See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for development guidelines. Please read our [Code of Conduct](.github/CODE_OF_CONDUCT.md).
@@ -219,4 +272,4 @@ IPC handlers validate all inputs with strict path containment checks. File reads
## License
-[MIT](LICENSE)
+[AGPL-3.0](LICENSE)
diff --git a/docs/research/model-agnostic-proxy.md b/docs/research/model-agnostic-proxy.md
new file mode 100644
index 00000000..7b74d6e5
--- /dev/null
+++ b/docs/research/model-agnostic-proxy.md
@@ -0,0 +1,467 @@
+# Model-Agnostic Proxy для Claude Code Agent Teams
+
+Дата исследования: 2026-03-06
+
+## Цель
+
+Сделать Claude Code Agent Teams model-agnostic: lead остаётся на Claude, а teammates могут работать на GPT-4o, Gemini, DeepSeek, Kimi K2.5 и других моделях через прокси, который транслирует Anthropic Messages API в формат целевого провайдера.
+
+## Ключевой механизм
+
+Claude Code обращается к Anthropic Messages API (`/v1/messages`). Переменная `ANTHROPIC_BASE_URL` позволяет перенаправить запросы на локальный прокси. Прокси:
+
+1. Принимает запрос в формате Anthropic Messages API
+2. Транслирует в формат целевого провайдера (OpenAI Chat Completions и др.)
+3. Пересылает провайдеру
+4. Получает SSE-стрим ответа
+5. Транслирует обратно в формат Anthropic SSE-событий
+6. Отдаёт Claude Code CLI как будто это ответ от Claude
+
+Team tools (TeamCreate, TaskCreate, SendMessage, TaskGet, TaskList, TaskUpdate, TeamDelete) исполняются **локально Claude Code CLI**. LLM только генерирует `tool_use` блоки. Значит прокси не нужно знать о team-семантике — достаточно корректно транслировать tool_use формат.
+
+---
+
+## Исследованные проекты
+
+### 1. HydraTeams
+
+- **URL**: https://github.com/Pickle-Pixel/HydraTeams
+- **Назначение**: Прокси-переводчик API специально для Agent Teams
+- **Язык**: TypeScript (~580 строк, 8 файлов)
+- **Зависимости**: Zero runtime dependencies (только Node.js builtins)
+- **Лицензия**: Не указана явно
+- **Stars**: 33 | **Forks**: 9 | **Commits**: 4 | **Создан**: 2026-02-08
+
+#### Архитектура
+
+```
+src/
+ index.ts (35) — точка входа, banner, graceful shutdown
+ proxy.ts (280) — HTTP-сервер, маршрутизация, retry
+ config.ts (95) — CLI-аргументы, env vars, Codex JWT
+ logger.ts (190) — логирование, идентификация сессий
+ translators/
+ types.ts (120) — интерфейсы Anthropic + OpenAI
+ messages.ts (85) — конвертация истории сообщений
+ request.ts (65) — Anthropic req -> OpenAI Chat Completions req
+ request-responses.ts (145) — Anthropic req -> ChatGPT Responses API req
+ response.ts (185) — OpenAI SSE -> Anthropic SSE
+ response-responses.ts (235) — Responses API SSE -> Anthropic SSE
+```
+
+#### Два конвейера трансляции
+
+1. **OpenAI Chat Completions API** (`--provider openai`) — для GPT-4o, GPT-4o-mini, o3-mini
+2. **ChatGPT Responses API** (`--provider chatgpt`) — для ChatGPT Subscription ($0 дополнительных затрат)
+
+#### Lead vs Teammate детекция
+
+- Lead: маркер `` в CLAUDE.md (попадает в system prompt)
+- Teammate: фраза `"the user interacts primarily with the team lead"` в system prompt
+- Lead-запросы passthrough на настоящий Anthropic API
+- Teammate-запросы транслируются на целевую модель
+
+#### Что работает правильно
+
+- Tool definitions: `input_schema` -> `parameters`, `tool_choice` маппинг корректен
+- tool_use блоки ассистанта -> `tool_calls` формат OpenAI
+- tool_result -> `role: "tool"` сообщения
+- Streaming: StreamState отслеживает blockIndex, activeToolCalls, textBlockStarted
+- Правильная SSE-последовательность: message_start -> content_block_start -> content_block_delta -> content_block_stop -> message_delta -> message_stop
+- Retry с exponential backoff на 429 (до 5 попыток)
+
+#### Найденные баги (code review)
+
+| # | Баг | Серьёзность |
+|---|-----|-------------|
+| 1 | **response-responses.ts**: `content_index` используется вместо Anthropic `blockIndex` в `response.output_text.done` — content_block_stop уйдёт с неправильным index | Высокая |
+| 2 | **proxy.ts**: `shouldPassthrough()` получает `parsed.system` (может быть массив `AnthropicSystemBlock[]`) как string — `count_tokens` passthrough для lead не сработает | Средняя |
+| 3 | **proxy.ts**: Non-streaming `JSON.parse(tc.function.arguments)` — если OpenAI вернёт невалидный JSON, exception убьёт весь запрос | Средняя |
+| 4 | **proxy.ts**: `shouldPassthrough("*")` означает "все Claude модели", а не "всё" — контринтуитивно | Низкая |
+| 5 | **logger.ts**: Warmup-запросы определяются по `toolCount === 0` — может ложно классифицировать обычные запросы | Низкая |
+
+#### Критические проблемы
+
+| Проблема | Серьёзность |
+|----------|-------------|
+| 0 тестов | Критично |
+| Нет таймаутов на upstream — fetch() без AbortController, зависнет навсегда | Критично |
+| Слушает на 0.0.0.0 — доступен в сети, релеит auth headers | Критично |
+| Teammate детекция по строке "the user interacts primarily with the team lead" — сломается при обновлении Claude Code | Высокая |
+| Spoofed model hardcoded `claude-sonnet-4-5-20250929` — устареет | Средняя |
+| Token counting = `JSON.length / 4` — грубая заглушка | Средняя |
+| Нет extended thinking — thinking блоки игнорируются | Средняя |
+| Нет image/multimodal — молча теряются | Низкая |
+
+#### Безопасность
+
+- Сервер слушает на 0.0.0.0 (все интерфейсы) — в сети это дыра
+- Passthrough релеит auth headers (x-api-key, authorization, cookie) к api.anthropic.com
+- JWT парсинг без валидации подписи
+- Нет rate limiting на входящие запросы
+- Логи могут содержать API-ключи в ответах об ошибках
+
+#### Итоговые оценки
+
+| Аспект | Оценка |
+|--------|:------:|
+| Архитектура | 7/10 |
+| API трансляция | 6/10 |
+| Lead/Teammate детекция | 5/10 |
+| Обработка ошибок | 4/10 |
+| Streaming | 7/10 |
+| Безопасность | 3/10 |
+| Production readiness | 3/10 |
+| **Общая** | **5/10** |
+
+---
+
+### 2. free-claude-code
+
+- **URL**: https://github.com/Alishahryar1/free-claude-code
+- **Назначение**: Прокси для использования Claude Code с бесплатными моделями
+- **Язык**: Python (FastAPI + uvicorn)
+- **Stars**: 814 | **Forks**: 95 | **Issues**: 4 | **Создан**: 2026-01-28
+- **Лицензия**: MIT
+- **Тесты**: 85+ файлов, pytest, GitHub Actions CI
+
+#### Архитектура
+
+```
+server.py — точка входа (uvicorn)
+api/
+ app.py — FastAPI factory + lifespan
+ routes.py — POST /v1/messages, GET /health
+ detection.py — эвристики определения типа запроса
+ optimization_handlers.py — 5 fast-path перехватчиков
+ request_utils.py — подсчёт токенов (tiktoken cl100k_base)
+ models/anthropic.py — Pydantic модели Anthropic request
+providers/
+ base.py — BaseProvider (ABC)
+ openai_compat.py — OpenAICompatibleProvider (основная логика)
+ common/
+ message_converter.py — Anthropic <-> OpenAI конвертер (~200 строк)
+ sse_builder.py — SSE event builder Anthropic формат (~300 строк)
+ think_parser.py — парсер тегов (~80 строк)
+ heuristic_tool_parser.py — парсер tool calls из текста
+ nvidia_nim/, open_router/, lmstudio/ — конкретные провайдеры
+```
+
+#### Провайдеры и модели
+
+##### NVIDIA NIM (бесплатно, 40 req/min)
+
+**Tier S (флагманы):**
+
+| Model ID | Thinking | Tool Calling |
+|----------|:--------:|:------------:|
+| `moonshotai/kimi-k2.5` | Да | Native |
+| `qwen/qwen3-coder-480b-a35b-instruct` | Да | Native |
+| `z-ai/glm5` | Да | Native |
+| `deepseek-ai/deepseek-v3.2` | Да | Native |
+| `mistralai/mistral-large-3-675b-instruct` | Да | Native |
+| `minimaxai/minimax-m2.5` | Да | Native |
+
+**Tier A:**
+
+| Model ID | Thinking | Tool Calling |
+|----------|:--------:|:------------:|
+| `z-ai/glm4.7` | Да | Native |
+| `mistralai/devstral-2-123b-instruct` | Да | Native |
+| `openai/gpt-oss-120b` | Да | Native |
+| `meta/llama-3.1-405b-instruct` | Нет | Native |
+
+**Tier B (быстрые):**
+
+| Model ID | Thinking | Tool Calling |
+|----------|:--------:|:------------:|
+| `qwen/qwen2.5-coder-32b-instruct` | Нет | Native |
+| `stepfun-ai/step-3.5-flash` | Да | Native |
+| `meta/llama-3.3-70b-instruct` | Нет | Native |
+
+Всего в каталоге NIM **185 моделей**, все бесплатные. max_tokens: 81920.
+
+##### OpenRouter (free модели с суффиксом `:free`)
+
+**С Tool Calling + Thinking:**
+
+| Model ID | Context | Max Output |
+|----------|:-------:|:----------:|
+| `openai/gpt-oss-120b:free` | 131K | 131K |
+| `stepfun/step-3.5-flash:free` | 256K | 256K |
+| `qwen/qwen3-coder:free` | 262K | 262K |
+| `qwen/qwen3-235b-a22b-thinking-2507:free` | 131K | — |
+| `z-ai/glm-4.5-air:free` | 131K | 96K |
+
+**С Tool Calling, без Thinking:**
+
+| Model ID | Context |
+|----------|:-------:|
+| `meta-llama/llama-3.3-70b-instruct:free` | 128K |
+| `mistralai/mistral-small-3.1-24b-instruct:free` | 128K |
+| `google/gemma-3-27b-it:free` | 131K |
+
+Всего **28 бесплатных моделей** + мета-роутер `openrouter/free`.
+
+##### LM Studio (полностью локально, без лимитов)
+
+| Модель | VRAM | Качество кода |
+|--------|:----:|:---:|
+| `unsloth/MiniMax-M2.5-GGUF` | 48GB+ | 7/10 |
+| `unsloth/Qwen3-Coder-480B-A35B-Instruct-GGUF` | 48GB+ | 8/10 |
+| `unsloth/Qwen3.5-35B-A3B-GGUF` | 24GB | 6/10 |
+| `unsloth/GLM-4.7-Flash-GGUF` | 24GB | 6/10 |
+| `unsloth/Qwen2.5-Coder-32B-Instruct-GGUF` | 24GB | 6/10 |
+
+#### Что лучше чем HydraTeams
+
+| Аспект | free-claude-code | HydraTeams |
+|--------|:---:|:---:|
+| Тесты | **85+ файлов**, CI | 0 |
+| Thinking blocks | **Два пути** (native + `` парсер) | Нет |
+| Token counting | **tiktoken** (реальный подсчёт) | `JSON.length / 4` |
+| Provider abstraction | **ABC + наследование** | Hardcoded if/else |
+| Error handling | **Graceful shutdown**, rate limiter | Базовый try/catch |
+| Heuristic tool parser | **Есть** (для моделей без native tool use) | Нет |
+
+#### Проблемы для Agent Teams
+
+1. **Task tool patching** — принудительно ставит `run_in_background=False` в трёх местах. Опасная мина для team coordination
+2. **Optimization interceptors** — 5 эвристик могут ложно сработать на teammate messages
+3. **Общий rate limiter** — singleton `GlobalRateLimiter` (40 req/min, max_concurrency=5). 5 teammates = мгновенный bottleneck. Один 429 блокирует ВСЕХ
+4. **Python 3.14 requirement** — ещё в beta, проблема для bundling
+5. **tiktoken** требует Rust-скомпилированный .so — кросс-платформенная сборка сложная
+6. **Нет lead/teammate разделения** — все запросы на один провайдер
+
+#### Bundling с Electron
+
+| Критерий | free-claude-code (Python) | HydraTeams (TypeScript) |
+|----------|:---:|:---:|
+| Bundling в Electron | PyInstaller ~100-150MB, Python 3.14, Rust deps | Прямо в main process, 0 deps |
+| Размер | ~50MB минимум | ~580 строк, КБ |
+| Кросс-платформа | tiktoken .so для каждой платформы | Нативный Node.js |
+| Запуск | Child process + Python runtime | Просто import |
+
+#### Переиспользуемое ядро (~900 строк Python)
+
+- `message_converter.py` (~200 строк) — полностью независим
+- `sse_builder.py` (~300 строк) — почти независим (убрать Task patching)
+- `think_parser.py` (~80 строк) — полностью независим
+- `openai_compat.py` (~250 строк) — stream_response логика
+
+#### Итоговые оценки
+
+| Критерий | Оценка |
+|----------|:------:|
+| Качество кода | 7/10 |
+| Тесты | 8/10 |
+| Bundling с Electron | 2/10 |
+| Agent Teams совместимость | 3/10 |
+| Адаптация под наш стек | 3/10 |
+
+---
+
+### 3. LiteLLM Proxy
+
+- **URL**: https://github.com/BerriAI/litellm
+- **Лицензия**: MIT (enterprise-фичи проприетарные)
+- **Stars**: ~38,000 | **Forks**: ~6,200
+- **Язык**: Python
+- **Провайдеры**: 100+ (OpenAI, Gemini, Bedrock, Azure, Groq, DeepSeek...)
+
+#### Как работает с Claude Code
+
+```bash
+export ANTHROPIC_BASE_URL="http://0.0.0.0:4000"
+export ANTHROPIC_AUTH_TOKEN="$LITELLM_MASTER_KEY"
+claude --model gpt-4o
+```
+
+Нативный Anthropic SDK-совместимый endpoint `/messages`. Полная трансляция tool_use, streaming SSE.
+
+#### Почему НЕ подходит для Electron
+
+| Проблема | Детали |
+|----------|--------|
+| Python | 70+ зависимостей, Prisma с Node binaries, grpcio |
+| Размер | ~2 ГБ (Docker-образ) |
+| RAM | Рекомендация 8 ГБ для production |
+| Bundling | Никто никогда не бандлил с desktop app |
+
+#### Известные баги с Claude Code
+
+- [#21446](https://github.com/BerriAI/litellm/issues/21446) — Gemini не работает через LiteLLM
+- [#14194](https://github.com/BerriAI/litellm/issues/14194) — Bedrock thinking + tools конфликтуют
+- [#12222](https://github.com/BerriAI/litellm/issues/12222) — Gemini падает на tools с optional args
+- [#18730](https://github.com/BerriAI/litellm/issues/18730) — Concurrent requests обходят rate limits
+
+**Вердикт: enterprise-серверный gateway, не для десктопа. 1/10 для нашего кейса.**
+
+---
+
+### 4. Bifrost (Maxim AI)
+
+- **URL**: https://github.com/maximhq/bifrost
+- **Лицензия**: Apache 2.0
+- **Stars**: ~2,700 | **Commits**: 3,341
+- **Язык**: Go (11 мкс overhead при 5000 RPS)
+- **Провайдеры**: 20+ (OpenAI, Gemini, Bedrock, Azure, Mistral, Ollama...)
+
+#### Bundling с Electron
+
+Bifrost компилируется в единый статический Go-бинарник. npm-пакет `@maximhq/bifrost` скачивает prebuilt binary с CDN под нужную платформу.
+
+```
+https://downloads.getmaxim.ai/bifrost/{version}/{platform}/{arch}/bifrost-http
+```
+
+Поддерживаемые платформы:
+- darwin/arm64 (macOS Apple Silicon)
+- darwin/amd64 (macOS Intel)
+- linux/amd64, linux/386
+- windows/amd64, windows/arm64
+
+Размер: ~30-60 MB. Запуск: `child_process.spawn(bifrostBinaryPath)`.
+
+**Bundling: 9/10** — скачать бинарник, положить рядом, запустить как child process.
+
+#### Проблемы для Agent Teams
+
+| Issue | Описание | Статус |
+|-------|----------|--------|
+| [#1164](https://github.com/maximhq/bifrost/issues/1164) | Parallel tool calls через Bedrock не работают | Открыт |
+| [#1829](https://github.com/maximhq/bifrost/issues/1829) | Streaming tool call deltas мёрджатся | Закрыт |
+| [#1804](https://github.com/maximhq/bifrost/issues/1804) | Streaming tool calls с агентскими клиентами не работают | Открыт |
+| [#828](https://github.com/maximhq/bifrost/issues/828) | Goroutine leak при context cancellation | Открыт |
+| [#1613](https://github.com/maximhq/bifrost/issues/1613) | SSE streaming от Gemini ломается | Открыт |
+
+Не использует anthropic-go-sdk, дублирует типы вручную (Discussion #1259).
+
+**Никто не тестировал Bifrost с Agent Teams.**
+
+#### Итоговые оценки
+
+| Критерий | Оценка |
+|----------|:------:|
+| Bundling с Electron | 9/10 |
+| Agent Teams (passthrough) | 8/10 |
+| Agent Teams (трансляция) | 4/10 |
+| Зрелость | 7/10 |
+
+---
+
+## Сравнение качества моделей vs Claude
+
+| Модель | Кодинг | vs Sonnet | vs Opus |
+|--------|:------:|:---------:|:-------:|
+| Kimi K2.5 (NIM) | 8/10 | ~80% | ~60% |
+| Qwen3 Coder 480B (NIM) | 8/10 | ~80% | ~60% |
+| GLM-5 (NIM) | 7/10 | ~70% | ~50% |
+| GPT-OSS 120B (NIM/OR) | 7/10 | ~70% | ~50% |
+| GLM-4.7 (NIM) | 7/10 | ~65% | ~45% |
+| Step 3.5 Flash (OR) | 6/10 | ~55% | ~35% |
+| Llama 3.3 70B (OR) | 5/10 | ~45% | ~30% |
+
+Ни одна бесплатная модель не дотягивает до Claude по качеству агентного кодинга.
+
+---
+
+## Юридические аспекты
+
+### Что разрешено Anthropic
+
+- `ANTHROPIC_BASE_URL` — **официально поддерживается** для LLM Gateway
+- Использование прокси с собственными API-ключами других провайдеров — легально
+- Документация описывает LLM Gateway конфигурацию: endpoint должен реализовывать Anthropic Messages API
+
+### Что запрещено
+
+- Использование OAuth-токенов от Claude Free/Pro/Max подписок в сторонних продуктах
+- Anthropic активно блокирует несанкционированное использование подписочных токенов
+- Использование Claude для обучения конкурирующих моделей (Section D.4 Commercial Terms)
+
+### ChatGPT backend API (HydraTeams)
+
+- `chatgpt.com/backend-api/codex/responses` — недокументированный API
+- Нарушение ToS ChatGPT при автоматизации через backend API
+- Может быть заблокирован в любой момент
+
+---
+
+## Влияние на наш проект (Claude DevTools)
+
+JSONL формат сессий **не меняется** — Claude Code CLI генерирует одинаковую структуру независимо от backend-модели. TeamCreate, TaskCreate, SendMessage и прочие team tools остаются теми же. Парсинг и chunk building будет работать без изменений.
+
+Единственное потенциальное отличие — metadata о модели в сообщениях (model field).
+
+---
+
+## Рекомендация по реализации
+
+### Лучший путь: форк HydraTeams + hardening
+
+HydraTeams — единственный проект заточенный под Agent Teams, на TypeScript (наш стек), zero deps, встраивается в Electron main process.
+
+**Что нужно починить обязательно:**
+1. Localhost-only binding (`127.0.0.1`)
+2. AbortController + таймауты на upstream fetch
+3. Баги с indices в response-responses.ts
+4. Тесты на трансляцию (unit-тесты на каждый translator)
+5. Убрать hardcoded spoofModel -> сделать конфигурируемым
+
+**Что заимствовать из free-claude-code:**
+1. ThinkTagParser — переписать на TS (~50 строк)
+2. Provider abstraction — интерфейс `TranslationProvider`
+3. Структуру тестов
+4. Heuristic tool parser для моделей без native tool calling
+
+**Что переосмыслить:**
+- Lead/teammate детекция — маркер по строке хрупкий. Мы знаем роли из TeamDataService, можно передавать через env var `HYDRA_ROLE=lead|teammate`
+
+### Оценка трудозатрат
+
+~2-3 дня на hardening + тесты. Итого: TypeScript-пакет ~800-1000 строк с тестами, встраиваемый в Electron main process.
+
+### Альтернативы
+
+| Вариант | Надёжность | Уверенность |
+|---------|:----------:|:-----------:|
+| Форк HydraTeams + hardening | 7/10 | 8/10 |
+| Свой proxy с нуля на TS (вдохновлённый обоими) | 8/10 | 7/10 |
+| Bifrost binary + thin TS translator | 5/10 | 6/10 |
+| LiteLLM как Docker sidecar | 6/10 | 7/10 |
+| free-claude-code (Python child process) | 3/10 | 8/10 |
+
+---
+
+## Другие найденные проекты
+
+| Проект | Описание | Применимость |
+|--------|----------|:---:|
+| [1rgs/claude-code-proxy](https://github.com/1rgs/claude-code-proxy) | На базе LiteLLM, BIG/SMALL model маппинг | 5/10 |
+| [fuergaosi233/claude-code-proxy](https://github.com/fuergaosi233/claude-code-proxy) | Anthropic -> OpenAI конвертер | 4/10 |
+| [nielspeter/claude-code-proxy](https://github.com/nielspeter/claude-code-proxy) | Легковесный бинарник, OpenRouter | 4/10 |
+| [9router](https://github.com/decolua/9router) | Smart router с fallback-каскадом | 5/10 |
+| [claude-code-teams-mcp](https://github.com/cs50victor/claude-code-teams-mcp) | MCP-сервер, реимплементация Agent Teams | 3/10 |
+
+---
+
+## Архитектурная схема целевого решения
+
+```
+[Lead Agent Process]
+ ANTHROPIC_BASE_URL=http://127.0.0.1:{port}
+ HYDRA_ROLE=lead
+ |
+ v
+[Proxy (TypeScript, в Electron main process)]
+ if role=lead --> passthrough к api.anthropic.com
+ if role=teammate --> трансляция к целевому провайдеру
+ |
+ v
+[Teammate 1] --> OpenAI API (GPT-4o)
+[Teammate 2] --> NVIDIA NIM (Kimi K2.5)
+[Teammate 3] --> Local (LM Studio)
+```
+
+Stream-json протокол (stdin/stdout между lead и teammates) **не затрагивается** — прокси работает на уровне HTTP API запросов к LLM.
diff --git a/mcp-server/README.md b/mcp-server/README.md
new file mode 100644
index 00000000..f6b0d7de
--- /dev/null
+++ b/mcp-server/README.md
@@ -0,0 +1,301 @@
+# @claude-team/mcp-server
+
+**Model Context Protocol (MCP) server for managing Claude Agent Teams kanban board and tasks.**
+
+Exposes 13 tools so AI agents (Claude, Cursor, or any MCP-compatible client) can create tasks, manage the kanban board, conduct code reviews, and send messages — backed by the same `teamctl.js` CLI that powers the Claude Agent Teams UI desktop app.
+
+---
+
+## Table of Contents
+
+- [Overview](#overview)
+- [Relationship to Claude Agent Teams UI](#relationship-to-claude-agent-teams-ui)
+- [Quick start](#quick-start)
+- [Prerequisites](#prerequisites)
+- [Installation](#installation)
+- [Configuration](#configuration)
+- [Tools Reference](#tools-reference)
+- [Data Storage](#data-storage)
+- [Development](#development)
+- [Testing](#testing)
+- [Troubleshooting](#troubleshooting)
+- [License](#license)
+
+---
+
+## Overview
+
+Implements the [Model Context Protocol](https://modelcontextprotocol.io/) over **stdio**. All operations are delegated to `teamctl.js`, which:
+
+- Stores task data as JSON under `~/.claude/tasks/{teamName}/`
+- Stores team config under `~/.claude/teams/{teamName}/`
+- Is installed automatically when you run Claude Agent Teams UI at least once
+
+### Use cases
+
+| Scenario | Description |
+|----------|-------------|
+| **Cursor / Claude Desktop** | Add the server to MCP config so AI assistants can create tasks, assign owners, move cards, and send messages without leaving the chat |
+| **Automation scripts** | Programmatic interface to the same task board that Claude Code agents use |
+| **Multi-agent workflows** | Multiple AI agents sharing one task board, coordinating via comments and messages |
+
+### Architecture
+
+```
+┌─────────────────────┐ stdio ┌──────────────────────┐ spawn ┌─────────────────┐
+│ MCP Client │ ◄────────────► │ @claude-team/ │ ◄────────────► │ teamctl.js │
+│ (Cursor, Claude, │ │ mcp-server │ │ ~/.claude/ │
+│ custom scripts) │ │ (FastMCP + 13 tools) │ │ tools/ │
+└─────────────────────┘ └──────────────────────┘ └─────────────────┘
+```
+
+---
+
+## Relationship to Claude Agent Teams UI
+
+| Component | Role |
+|-----------|------|
+| **Claude Agent Teams UI** | Desktop app (Electron). Visualizes sessions, kanban board, code review, team messaging. Installs `teamctl.js` on first run. |
+| **teamctl.js** | CLI at `~/.claude/tools/teamctl.js`. Reads/writes task JSON, manages kanban, inboxes, reviews. Used by both the app and agents. |
+| **@claude-team/mcp-server** | MCP server wrapping `teamctl.js` so Cursor, Claude Desktop, and other MCP clients can call the same operations as tools. |
+
+Agents in Claude Code use `teamctl.js` via Bash. Agents in Cursor or Claude Desktop use this MCP server. Both operate on the same data.
+
+---
+
+## Quick start
+
+1. Run **Claude Agent Teams UI** at least once (installs `teamctl.js`)
+2. Build the MCP server: `cd mcp-server && pnpm install && pnpm build`
+3. Add to Cursor MCP config (`~/.cursor/mcp.json`):
+
+```json
+{
+ "mcpServers": {
+ "claude-team-tools": {
+ "command": "node",
+ "args": ["/absolute/path/to/claude_agent_teams_ui/mcp-server/dist/index.js"]
+ }
+ }
+}
+```
+
+4. Restart Cursor. The 13 tools will appear for the AI assistant.
+
+---
+
+## Prerequisites
+
+- **Node.js 20+**
+- **Claude Agent Teams UI** run at least once (installs `teamctl.js` to `~/.claude/tools/teamctl.js`)
+
+If `teamctl.js` is missing, the server throws at startup:
+
+```
+teamctl.js not found at ~/.claude/tools/teamctl.js.
+Make sure Claude Agent Teams UI has been run at least once,
+or set the TEAMCTL_PATH environment variable.
+```
+
+---
+
+## Installation
+
+### From the monorepo (development)
+
+```bash
+cd mcp-server
+pnpm install
+pnpm build
+```
+
+### As a dependency
+
+```bash
+pnpm add @claude-team/mcp-server
+# or
+npm install @claude-team/mcp-server
+```
+
+---
+
+## Configuration
+
+### Cursor
+
+Add to `~/.cursor/mcp.json` or project `.cursor/mcp.json`:
+
+```json
+{
+ "mcpServers": {
+ "claude-team-tools": {
+ "command": "node",
+ "args": ["/path/to/claude_agent_teams_ui/mcp-server/dist/index.js"]
+ }
+ }
+}
+```
+
+With global install (`pnpm link` or `npm link`):
+
+```json
+{
+ "mcpServers": {
+ "claude-team-tools": {
+ "command": "team-mcp-server"
+ }
+ }
+}
+```
+
+### Claude Desktop
+
+Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or equivalent:
+
+```json
+{
+ "mcpServers": {
+ "claude-team-tools": {
+ "command": "node",
+ "args": ["/absolute/path/to/mcp-server/dist/index.js"]
+ }
+ }
+}
+```
+
+### Environment variables
+
+| Variable | Description |
+|----------|-------------|
+| `TEAMCTL_PATH` | Path to `teamctl.js` if not at `~/.claude/tools/teamctl.js` |
+
+---
+
+## Tools Reference
+
+All tools require a `team` parameter (team name, folder under `~/.claude/teams/`).
+
+### Task CRUD
+
+| Tool | Description |
+|------|-------------|
+| `task_create` | Create a task. Optional: `owner`, `description`, `blocked_by`, `related`, `status`, `notify`, `from` |
+| `task_get` | Get a task by ID. Returns full JSON (status, owner, comments, dependencies, work intervals, history) |
+| `task_list` | List all tasks. Returns JSON array (filter internal tasks client-side if needed) |
+| `task_set_status` | Set status: `pending` → `in_progress` → `completed` → `deleted`. Records work intervals |
+| `task_set_owner` | Assign or unassign owner. Use `owner="clear"` to unassign. Optional: `notify`, `from` |
+
+### Task collaboration
+
+| Tool | Description |
+|------|-------------|
+| `task_comment` | Add a comment. Sends inbox notification to owner (unless commenter is owner). Optional: `from` |
+| `task_link` | Link/unlink dependencies. Types: `blocked-by`, `blocks`, `related`. Bidirectional; circular deps rejected |
+| `task_briefing` | Human-readable briefing for a member: their assigned tasks vs full board |
+| `task_attach` | Attach a file. Modes: `copy`, `link`. MIME auto-detected (PNG, JPEG, GIF, WebP, PDF, ZIP). Max 20 MB |
+
+### Kanban board
+
+| Tool | Description |
+|------|-------------|
+| `kanban_move` | Move to `review` or `approved`, or `clear` from board. Moving to column sets status to `completed` |
+| `kanban_reviewers` | Manage reviewers: `list` (JSON array), `add`, `remove` |
+
+### Code review
+
+| Tool | Description |
+|------|-------------|
+| `review_action` | `approve` — mark approved (optional comment, `notify_owner`). `request-changes` — remove from kanban, reset to `in_progress`, notify owner (comment required) |
+
+### Messaging
+
+| Tool | Description |
+|------|-------------|
+| `message_send` | Send inbox message to a member. Optional: `summary`, `from`. Triggers notifications |
+
+---
+
+## Data Storage
+
+| Location | Contents |
+|----------|----------|
+| `~/.claude/tasks/{teamName}/` | Task JSON files (`1.json`, `2.json`, …) |
+| `~/.claude/teams/{teamName}/` | Team config, kanban reviewers, inboxes |
+
+Task IDs are numeric (highwatermark). Team and member names must be safe path segments (no `.`, `..`, `/`, `\`, null bytes).
+
+---
+
+## Development
+
+### Scripts
+
+| Command | Description |
+|---------|-------------|
+| `pnpm build` | Build with tsup → `dist/index.js` |
+| `pnpm dev` | Run with tsx (no build) |
+| `pnpm test` | Run Vitest tests |
+| `pnpm test:watch` | Watch mode |
+| `pnpm typecheck` | TypeScript check |
+
+### Project structure
+
+```
+mcp-server/
+├── src/
+│ ├── index.ts # FastMCP server, registers all tools
+│ ├── teamctl-runner.ts # Spawns teamctl.js subprocess
+│ ├── output-parser.ts # Parses JSON / "OK ..." from teamctl stdout
+│ ├── schemas.ts # Zod schemas (team, taskId, member, etc.)
+│ └── tools/
+│ ├── index.ts # registerAllTools()
+│ ├── task-create.ts
+│ ├── task-get.ts
+│ ├── ...
+│ └── message-send.ts
+├── test/
+│ ├── tools/ # Per-tool tests
+│ ├── teamctl-runner.test.ts
+│ ├── output-parser.test.ts
+│ └── schemas.test.ts
+├── package.json
+├── tsup.config.ts
+└── vitest.config.ts
+```
+
+### Adding a new tool
+
+1. Create `src/tools/your-tool.ts` with `register(server, runner)`
+2. Add to `ALL_TOOLS` in `src/tools/index.ts`
+3. Add Zod parameters and map to `teamctl` CLI args
+4. Use `parseJsonOutput`, `parseOkOutput`, or `parseTextOutput` from `output-parser.ts`
+5. Add tests in `test/tools/your-tool.test.ts`
+
+---
+
+## Testing
+
+Tests use a mock `ITeamctlRunner` (no real `teamctl.js` required):
+
+```bash
+pnpm test
+```
+
+For integration tests with real `teamctl.js`, use the main app: `test/main/services/team/teamctl.test.ts`.
+
+---
+
+## Troubleshooting
+
+| Issue | Solution |
+|-------|----------|
+| **teamctl.js not found** | Run Claude Agent Teams UI at least once, or set `TEAMCTL_PATH` |
+| **Invalid team/member name** | Names: 1–128 chars, no `.`, `..`, `/`, `\`, null bytes |
+| **MCP client not discovering tools** | Check server starts without errors; use absolute path in config; some clients require it |
+| **Timeout errors** | Default 10s. Increase via `TeamctlRunnerOptions.timeoutMs` (code change) |
+
+---
+
+## License
+
+Same as the parent project: [AGPL-3.0](../LICENSE).
diff --git a/mcp-server/package.json b/mcp-server/package.json
new file mode 100644
index 00000000..ac6f5a7d
--- /dev/null
+++ b/mcp-server/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "@claude-team/mcp-server",
+ "version": "1.0.0",
+ "description": "MCP server for managing Claude Agent Teams kanban board and tasks",
+ "type": "module",
+ "main": "dist/index.js",
+ "bin": {
+ "team-mcp-server": "dist/index.js"
+ },
+ "scripts": {
+ "build": "tsup",
+ "dev": "tsx src/index.ts",
+ "test": "vitest run",
+ "test:watch": "vitest",
+ "typecheck": "tsc --noEmit"
+ },
+ "dependencies": {
+ "fastmcp": "^3.34.0",
+ "zod": "^4.3.6"
+ },
+ "devDependencies": {
+ "tsup": "^8.5.1",
+ "tsx": "^4.21.0",
+ "typescript": "^5.8.2",
+ "vitest": "^3.1.4",
+ "@types/node": "^22.15.18"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+}
diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts
new file mode 100644
index 00000000..a30a5b06
--- /dev/null
+++ b/mcp-server/src/index.ts
@@ -0,0 +1,20 @@
+import { FastMCP } from 'fastmcp';
+import { TeamctlRunner } from './teamctl-runner.js';
+import { registerAllTools } from './tools/index.js';
+
+const server = new FastMCP({
+ name: 'claude-team-tools',
+ version: '1.0.0',
+ instructions: `MCP server for managing Claude Agent Teams kanban board and tasks.
+
+Provides 13 tools for task CRUD, kanban board management, code reviews, and team messaging.
+All operations are backed by teamctl.js — the battle-tested CLI tool from Claude Agent Teams UI.
+
+Data is stored as JSON files in ~/.claude/tasks/{teamName}/ and ~/.claude/teams/{teamName}/.`,
+});
+
+const runner = new TeamctlRunner();
+
+registerAllTools(server, runner);
+
+server.start({ transportType: 'stdio' });
diff --git a/mcp-server/src/output-parser.ts b/mcp-server/src/output-parser.ts
new file mode 100644
index 00000000..e2b7c1a5
--- /dev/null
+++ b/mcp-server/src/output-parser.ts
@@ -0,0 +1,52 @@
+/**
+ * Parses teamctl stdout into structured results.
+ *
+ * teamctl outputs in three formats:
+ * 1. JSON — task create/get/list, attach, message send, kanban reviewers list
+ * 2. "OK ..." text — status changes, comments, links, kanban moves, reviews
+ * 3. Plain text — task briefing (multi-line human-readable report)
+ */
+
+/** Parse JSON from teamctl stdout (task create/get/list, attach, message send) */
+export function parseJsonOutput(stdout: string): T {
+ const trimmed = stdout.trim();
+ if (!trimmed) {
+ throw new Error('Empty output from teamctl (expected JSON)');
+ }
+ try {
+ return JSON.parse(trimmed) as T;
+ } catch {
+ throw new Error(
+ `Failed to parse teamctl JSON output: ${trimmed.slice(0, 200)}`,
+ );
+ }
+}
+
+/** Parse "OK ..." acknowledgment lines from teamctl */
+export function parseOkOutput(stdout: string): string {
+ const trimmed = stdout.trim();
+ if (trimmed.startsWith('OK ')) {
+ return trimmed.slice(3); // Strip "OK " prefix, keep the rest
+ }
+ // Some commands output just "OK\n"
+ if (trimmed === 'OK') {
+ return 'OK';
+ }
+ // Return as-is if format is unexpected — don't throw
+ return trimmed;
+}
+
+/** Return plain text as-is (briefing, help output) */
+export function parseTextOutput(stdout: string): string {
+ return stdout.trim();
+}
+
+/**
+ * Format teamctl stderr into a user-friendly error message.
+ * teamctl writes errors to stderr via `die(message)` and exits with code 1.
+ */
+export function formatError(stderr: string, stdout: string): string {
+ const msg = stderr.trim() || stdout.trim();
+ if (!msg) return 'Unknown teamctl error';
+ return msg;
+}
diff --git a/mcp-server/src/schemas.ts b/mcp-server/src/schemas.ts
new file mode 100644
index 00000000..cc808e0b
--- /dev/null
+++ b/mcp-server/src/schemas.ts
@@ -0,0 +1,115 @@
+import { z } from 'zod';
+import { sep, isAbsolute } from 'node:path';
+
+// ---------------------------------------------------------------------------
+// Identifiers
+// ---------------------------------------------------------------------------
+
+/**
+ * Matches teamctl's `isSafePathSegment()`:
+ * rejects empty, '.', '..', and strings containing '/', '\\', '\0', or '..'
+ */
+const safePathSegment = (label: string) =>
+ z
+ .string()
+ .min(1)
+ .max(128)
+ .refine(
+ (v) =>
+ v.trim().length > 0 &&
+ v !== '.' &&
+ v !== '..' &&
+ !v.includes('/') &&
+ !v.includes('\\') &&
+ !v.includes('\0') &&
+ !v.includes('..'),
+ { message: `Invalid ${label}: must be a safe path segment` },
+ );
+
+/** Team name — folder inside `~/.claude/teams/` */
+export const teamNameSchema = safePathSegment('team name').describe(
+ 'Team name (folder in ~/.claude/teams/)',
+);
+
+/** Numeric task ID produced by teamctl's highwatermark counter */
+export const taskIdSchema = z
+ .string()
+ .regex(/^\d{1,10}$/, 'Task ID must be a positive integer (e.g. "1", "42")')
+ .describe('Numeric task ID');
+
+/** Team member name — folder inside inboxes, safe path segment */
+export const memberNameSchema = safePathSegment('member name').describe(
+ 'Team member name',
+);
+
+// ---------------------------------------------------------------------------
+// Enums — match teamctl's normalizeStatus / normalizeColumn / etc.
+// ---------------------------------------------------------------------------
+
+export const taskStatusSchema = z.enum([
+ 'pending',
+ 'in_progress',
+ 'completed',
+ 'deleted',
+]);
+
+export const kanbanColumnSchema = z
+ .enum(['review', 'approved'])
+ .describe('Kanban column to move task to');
+
+export const clarificationSchema = z
+ .enum(['lead', 'user', 'clear'])
+ .describe('Who needs to clarify: lead, user, or clear the flag');
+
+export const linkTypeSchema = z
+ .enum(['blocked-by', 'blocks', 'related'])
+ .describe('Relationship type between tasks');
+
+export const linkOperationSchema = z.enum(['link', 'unlink']);
+
+export const reviewDecisionSchema = z.enum(['approve', 'request-changes']);
+
+export const reviewerOperationSchema = z.enum(['list', 'add', 'remove']);
+
+// ---------------------------------------------------------------------------
+// Composite schemas
+// ---------------------------------------------------------------------------
+
+/** Comma-separated task IDs sent as a single CLI argument */
+export const taskIdsArraySchema = z
+ .array(taskIdSchema)
+ .describe('Array of task IDs (e.g. ["1", "3"])');
+
+// ---------------------------------------------------------------------------
+// File / attachment schemas — defence-in-depth for CLI arguments
+// ---------------------------------------------------------------------------
+
+/** Absolute file path without traversal sequences */
+export const filePathSchema = z
+ .string()
+ .min(1)
+ .refine((p) => isAbsolute(p), { message: 'Path must be absolute' })
+ .refine((p) => !p.split(sep).includes('..'), {
+ message: 'Path must not contain traversal sequences (..)',
+ })
+ .refine((p) => !p.includes('\0'), {
+ message: 'Path must not contain null bytes',
+ });
+
+/** Safe filename — no path separators, no null bytes, reasonable length */
+export const safeFilenameSchema = z
+ .string()
+ .min(1)
+ .max(255)
+ .refine(
+ (f) => !f.includes('/') && !f.includes('\\') && !f.includes('\0'),
+ { message: 'Filename must not contain path separators or null bytes' },
+ );
+
+/** MIME type — standard type/subtype format */
+export const mimeTypeSchema = z
+ .string()
+ .regex(
+ /^[a-zA-Z0-9][a-zA-Z0-9!#$&\-^_]*\/[a-zA-Z0-9][a-zA-Z0-9!#$&\-^_.+]*$/,
+ 'Invalid MIME type format (expected type/subtype)',
+ );
diff --git a/mcp-server/src/teamctl-runner.ts b/mcp-server/src/teamctl-runner.ts
new file mode 100644
index 00000000..d9892106
--- /dev/null
+++ b/mcp-server/src/teamctl-runner.ts
@@ -0,0 +1,155 @@
+import { spawn } from 'node:child_process';
+import { existsSync } from 'node:fs';
+import { homedir } from 'node:os';
+import { join } from 'node:path';
+
+// ---------------------------------------------------------------------------
+// Types
+// ---------------------------------------------------------------------------
+
+export interface TeamctlResult {
+ stdout: string;
+ stderr: string;
+ exitCode: number;
+}
+
+export interface ITeamctlRunner {
+ execute(args: string[]): Promise;
+}
+
+export interface TeamctlRunnerOptions {
+ /** Explicit path to teamctl.js. Falls back to TEAMCTL_PATH env, then default. */
+ teamctlPath?: string;
+ /** Max concurrent subprocess calls (default: 5) */
+ maxConcurrent?: number;
+ /** Subprocess timeout in ms (default: 10 000) */
+ timeoutMs?: number;
+}
+
+// ---------------------------------------------------------------------------
+// Semaphore — limits concurrent subprocess spawns
+// ---------------------------------------------------------------------------
+
+class Semaphore {
+ private current = 0;
+ private queue: Array<() => void> = [];
+
+ constructor(private readonly max: number) {}
+
+ async acquire(): Promise {
+ if (this.current < this.max) {
+ this.current++;
+ return;
+ }
+ return new Promise((resolve) => {
+ this.queue.push(() => {
+ this.current++;
+ resolve();
+ });
+ });
+ }
+
+ release(): void {
+ this.current--;
+ const next = this.queue.shift();
+ if (next) next();
+ }
+}
+
+// ---------------------------------------------------------------------------
+// TeamctlRunner
+// ---------------------------------------------------------------------------
+
+export class TeamctlRunner implements ITeamctlRunner {
+ readonly teamctlPath: string;
+ private readonly timeoutMs: number;
+ private readonly semaphore: Semaphore;
+
+ constructor(options?: TeamctlRunnerOptions) {
+ this.teamctlPath = resolveTeamctlPath(options?.teamctlPath);
+ this.timeoutMs = options?.timeoutMs ?? 10_000;
+ this.semaphore = new Semaphore(options?.maxConcurrent ?? 5);
+
+ // Fail fast if teamctl.js doesn't exist
+ if (!existsSync(this.teamctlPath)) {
+ throw new Error(
+ `teamctl.js not found at ${this.teamctlPath}. ` +
+ 'Make sure Claude Agent Teams UI has been run at least once, ' +
+ 'or set the TEAMCTL_PATH environment variable.',
+ );
+ }
+ }
+
+ async execute(args: string[]): Promise {
+ await this.semaphore.acquire();
+ try {
+ return await this.spawn(args);
+ } finally {
+ this.semaphore.release();
+ }
+ }
+
+ private spawn(args: string[]): Promise {
+ return new Promise((resolve, reject) => {
+ const child = spawn('node', [this.teamctlPath, ...args], {
+ stdio: ['ignore', 'pipe', 'pipe'],
+ timeout: this.timeoutMs,
+ env: { ...process.env },
+ });
+
+ const stdoutChunks: Buffer[] = [];
+ const stderrChunks: Buffer[] = [];
+
+ child.stdout.on('data', (chunk: Buffer) => stdoutChunks.push(chunk));
+ child.stderr.on('data', (chunk: Buffer) => stderrChunks.push(chunk));
+
+ let settled = false;
+
+ child.on('error', (err) => {
+ if (!settled) {
+ settled = true;
+ reject(new Error(`Failed to spawn teamctl: ${err.message}`));
+ }
+ });
+
+ child.on('close', (code, signal) => {
+ if (settled) return;
+ settled = true;
+
+ const stdout = Buffer.concat(stdoutChunks).toString('utf-8');
+ const stderr = Buffer.concat(stderrChunks).toString('utf-8');
+
+ if (signal === 'SIGTERM') {
+ const partial = stdout.slice(0, 500) || stderr.slice(0, 500);
+ reject(
+ new Error(
+ `teamctl timed out after ${this.timeoutMs}ms` +
+ (partial ? `. Partial output: ${partial}` : ''),
+ ),
+ );
+ return;
+ }
+
+ resolve({
+ stdout,
+ stderr,
+ exitCode: code ?? 1,
+ });
+ });
+ });
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Path resolution
+// ---------------------------------------------------------------------------
+
+function resolveTeamctlPath(explicit?: string): string {
+ if (explicit) return explicit;
+
+ const fromEnv = process.env['TEAMCTL_PATH'];
+ if (fromEnv) return fromEnv;
+
+ // Default: ~/.claude/tools/teamctl.js
+ return join(homedir(), '.claude', 'tools', 'teamctl.js');
+}
diff --git a/mcp-server/src/tools/index.ts b/mcp-server/src/tools/index.ts
new file mode 100644
index 00000000..6a4da6f9
--- /dev/null
+++ b/mcp-server/src/tools/index.ts
@@ -0,0 +1,42 @@
+import type { FastMCP } from 'fastmcp';
+import type { ITeamctlRunner } from '../teamctl-runner.js';
+
+import { register as taskCreate } from './task-create.js';
+import { register as taskSetStatus } from './task-set-status.js';
+import { register as taskSetOwner } from './task-set-owner.js';
+import { register as taskGet } from './task-get.js';
+import { register as taskList } from './task-list.js';
+import { register as taskComment } from './task-comment.js';
+import { register as taskLink } from './task-link.js';
+import { register as taskBriefing } from './task-briefing.js';
+import { register as taskAttach } from './task-attach.js';
+import { register as kanbanMove } from './kanban-move.js';
+import { register as kanbanReviewers } from './kanban-reviewers.js';
+import { register as reviewAction } from './review-action.js';
+import { register as messageSend } from './message-send.js';
+
+const ALL_TOOLS = [
+ taskCreate,
+ taskSetStatus,
+ taskSetOwner,
+ taskGet,
+ taskList,
+ taskComment,
+ taskLink,
+ taskBriefing,
+ taskAttach,
+ kanbanMove,
+ kanbanReviewers,
+ reviewAction,
+ messageSend,
+] as const;
+
+/**
+ * Register all 13 MCP tools with the server.
+ * Each tool wraps a teamctl CLI command via the runner.
+ */
+export function registerAllTools(server: FastMCP, runner: ITeamctlRunner): void {
+ for (const register of ALL_TOOLS) {
+ register(server, runner);
+ }
+}
diff --git a/mcp-server/src/tools/kanban-move.ts b/mcp-server/src/tools/kanban-move.ts
new file mode 100644
index 00000000..565a836f
--- /dev/null
+++ b/mcp-server/src/tools/kanban-move.ts
@@ -0,0 +1,45 @@
+import { z } from 'zod';
+import { UserError } from 'fastmcp';
+import type { FastMCP } from 'fastmcp';
+import type { ITeamctlRunner } from '../teamctl-runner.js';
+import { parseOkOutput } from '../output-parser.js';
+import { teamNameSchema, taskIdSchema, kanbanColumnSchema } from '../schemas.js';
+
+export function register(server: FastMCP, runner: ITeamctlRunner): void {
+ server.addTool({
+ name: 'kanban_move',
+ description: `Move a task to a kanban column or clear it from the kanban board.
+
+Columns: "review" (awaiting code review) or "approved" (review passed).
+Use operation "clear" to remove a task from the kanban board (returns to status-based display).
+Moving to a kanban column also sets the task status to "completed".`,
+ annotations: {
+ readOnlyHint: false,
+ destructiveHint: false,
+ },
+ parameters: z.object({
+ team: teamNameSchema,
+ task_id: taskIdSchema,
+ operation: z.enum(['set-column', 'clear']).describe('"set-column" to move, "clear" to remove from kanban'),
+ column: kanbanColumnSchema.optional().describe('Target column (required for set-column)'),
+ }),
+ execute: async (args) => {
+ let cliArgs: string[];
+
+ if (args.operation === 'set-column') {
+ if (!args.column) {
+ throw new UserError('column is required when operation is "set-column"');
+ }
+ cliArgs = ['--team', args.team, 'kanban', 'set-column', args.task_id, args.column];
+ } else {
+ cliArgs = ['--team', args.team, 'kanban', 'clear', args.task_id];
+ }
+
+ const result = await runner.execute(cliArgs);
+ if (result.exitCode !== 0) {
+ throw new UserError(`Failed to update kanban: ${result.stderr.trim() || result.stdout.trim()}`);
+ }
+ return parseOkOutput(result.stdout);
+ },
+ });
+}
diff --git a/mcp-server/src/tools/kanban-reviewers.ts b/mcp-server/src/tools/kanban-reviewers.ts
new file mode 100644
index 00000000..03569247
--- /dev/null
+++ b/mcp-server/src/tools/kanban-reviewers.ts
@@ -0,0 +1,46 @@
+import { z } from 'zod';
+import { UserError } from 'fastmcp';
+import type { FastMCP } from 'fastmcp';
+import type { ITeamctlRunner } from '../teamctl-runner.js';
+import { parseJsonOutput, parseOkOutput } from '../output-parser.js';
+import { teamNameSchema, memberNameSchema, reviewerOperationSchema } from '../schemas.js';
+
+export function register(server: FastMCP, runner: ITeamctlRunner): void {
+ server.addTool({
+ name: 'kanban_reviewers',
+ description: `Manage the kanban board's reviewer list.
+
+Operations:
+- "list": returns JSON array of reviewer names
+- "add": adds a reviewer (name required)
+- "remove": removes a reviewer (name required)`,
+ annotations: {
+ readOnlyHint: false,
+ destructiveHint: false,
+ },
+ parameters: z.object({
+ team: teamNameSchema,
+ operation: reviewerOperationSchema.describe('"list", "add", or "remove"'),
+ name: memberNameSchema.optional().describe('Reviewer name (required for add/remove)'),
+ }),
+ execute: async (args) => {
+ if (args.operation !== 'list' && !args.name) {
+ throw new UserError(`name is required for "${args.operation}" operation`);
+ }
+
+ const cliArgs = ['--team', args.team, 'kanban', 'reviewers', args.operation];
+ if (args.name) cliArgs.push(args.name);
+
+ const result = await runner.execute(cliArgs);
+ if (result.exitCode !== 0) {
+ throw new UserError(`Failed to manage reviewers: ${result.stderr.trim() || result.stdout.trim()}`);
+ }
+
+ // "list" returns JSON array, "add"/"remove" return "OK ..." text
+ if (args.operation === 'list') {
+ return parseJsonOutput(result.stdout);
+ }
+ return parseOkOutput(result.stdout);
+ },
+ });
+}
diff --git a/mcp-server/src/tools/message-send.ts b/mcp-server/src/tools/message-send.ts
new file mode 100644
index 00000000..0693bd33
--- /dev/null
+++ b/mcp-server/src/tools/message-send.ts
@@ -0,0 +1,39 @@
+import { z } from 'zod';
+import { UserError } from 'fastmcp';
+import type { FastMCP } from 'fastmcp';
+import type { ITeamctlRunner } from '../teamctl-runner.js';
+import { parseJsonOutput } from '../output-parser.js';
+import { teamNameSchema, memberNameSchema } from '../schemas.js';
+
+export function register(server: FastMCP, runner: ITeamctlRunner): void {
+ server.addTool({
+ name: 'message_send',
+ description: `Send an inbox message to a team member. Returns delivery confirmation JSON.
+
+Messages appear in the member's inbox and can trigger notifications.
+The "source" field is automatically stripped for security — external callers cannot impersonate system notifications.`,
+ annotations: {
+ readOnlyHint: false,
+ destructiveHint: false,
+ },
+ parameters: z.object({
+ team: teamNameSchema,
+ to: memberNameSchema.describe('Recipient member name'),
+ text: z.string().min(1).max(10000).describe('Message text'),
+ summary: z.string().max(200).optional().describe('Short summary for notification preview'),
+ from: memberNameSchema.optional().describe('Sender name'),
+ }),
+ execute: async (args) => {
+ const cliArgs = ['--team', args.team, 'message', 'send', '--to', args.to, '--text', args.text];
+
+ if (args.summary) cliArgs.push('--summary', args.summary);
+ if (args.from) cliArgs.push('--from', args.from);
+
+ const result = await runner.execute(cliArgs);
+ if (result.exitCode !== 0) {
+ throw new UserError(`Failed to send message: ${result.stderr.trim() || result.stdout.trim()}`);
+ }
+ return parseJsonOutput(result.stdout);
+ },
+ });
+}
diff --git a/mcp-server/src/tools/review-action.ts b/mcp-server/src/tools/review-action.ts
new file mode 100644
index 00000000..64bad45d
--- /dev/null
+++ b/mcp-server/src/tools/review-action.ts
@@ -0,0 +1,50 @@
+import { z } from 'zod';
+import { UserError } from 'fastmcp';
+import type { FastMCP } from 'fastmcp';
+import type { ITeamctlRunner } from '../teamctl-runner.js';
+import { parseOkOutput } from '../output-parser.js';
+import { teamNameSchema, taskIdSchema, memberNameSchema, reviewDecisionSchema } from '../schemas.js';
+
+export function register(server: FastMCP, runner: ITeamctlRunner): void {
+ server.addTool({
+ name: 'review_action',
+ description: `Approve a task or request changes.
+
+- "approve": marks the task as approved in kanban, optionally posts a review comment
+- "request-changes": removes from kanban, resets status to "in_progress", notifies the task owner with the change request comment`,
+ annotations: {
+ readOnlyHint: false,
+ destructiveHint: false,
+ },
+ parameters: z.object({
+ team: teamNameSchema,
+ task_id: taskIdSchema,
+ decision: reviewDecisionSchema.describe('"approve" or "request-changes"'),
+ comment: z.string().max(5000).optional().describe('Review comment (required for request-changes)'),
+ from: memberNameSchema.optional().describe('Reviewer name'),
+ notify_owner: z.boolean().optional().describe('Notify the task owner (for approve)'),
+ }),
+ execute: async (args) => {
+ if (args.decision === 'request-changes' && !args.comment) {
+ throw new UserError('comment is required when requesting changes');
+ }
+
+ const cliArgs = ['--team', args.team, 'review', args.decision, args.task_id];
+
+ // approve uses --note for optional comment; request-changes uses --comment
+ if (args.decision === 'request-changes' && args.comment) {
+ cliArgs.push('--comment', args.comment);
+ } else if (args.decision === 'approve' && args.comment) {
+ cliArgs.push('--note', args.comment);
+ }
+ if (args.from) cliArgs.push('--from', args.from);
+ if (args.notify_owner) cliArgs.push('--notify-owner');
+
+ const result = await runner.execute(cliArgs);
+ if (result.exitCode !== 0) {
+ throw new UserError(`Failed to ${args.decision}: ${result.stderr.trim() || result.stdout.trim()}`);
+ }
+ return parseOkOutput(result.stdout);
+ },
+ });
+}
diff --git a/mcp-server/src/tools/task-attach.ts b/mcp-server/src/tools/task-attach.ts
new file mode 100644
index 00000000..a0e9830d
--- /dev/null
+++ b/mcp-server/src/tools/task-attach.ts
@@ -0,0 +1,42 @@
+import { z } from 'zod';
+import { UserError } from 'fastmcp';
+import type { FastMCP } from 'fastmcp';
+import type { ITeamctlRunner } from '../teamctl-runner.js';
+import { parseJsonOutput } from '../output-parser.js';
+import { teamNameSchema, taskIdSchema, memberNameSchema, filePathSchema, safeFilenameSchema, mimeTypeSchema } from '../schemas.js';
+
+export function register(server: FastMCP, runner: ITeamctlRunner): void {
+ server.addTool({
+ name: 'task_attach',
+ description: `Attach a file to a task. Returns attachment metadata JSON.
+
+Supports copy (default) or hardlink mode. MIME type is auto-detected from file content (PNG, JPEG, GIF, WebP, PDF, ZIP) with fallback to application/octet-stream. Max file size: 20 MB.`,
+ annotations: {
+ readOnlyHint: false,
+ destructiveHint: false,
+ },
+ parameters: z.object({
+ team: teamNameSchema,
+ task_id: taskIdSchema,
+ file: filePathSchema.describe('Absolute path to the file to attach'),
+ filename: safeFilenameSchema.optional().describe('Override stored filename'),
+ mime_type: mimeTypeSchema.optional().describe('Override MIME type (auto-detected by default)'),
+ mode: z.enum(['copy', 'link']).optional().describe('Storage mode: copy (default) or hardlink'),
+ from: memberNameSchema.optional().describe('Uploader name'),
+ }),
+ execute: async (args) => {
+ const cliArgs = ['--team', args.team, 'task', 'attach', args.task_id, '--file', args.file];
+
+ if (args.filename) cliArgs.push('--filename', args.filename);
+ if (args.mime_type) cliArgs.push('--mime-type', args.mime_type);
+ if (args.mode) cliArgs.push('--mode', args.mode);
+ if (args.from) cliArgs.push('--from', args.from);
+
+ const result = await runner.execute(cliArgs);
+ if (result.exitCode !== 0) {
+ throw new UserError(`Failed to attach file: ${result.stderr.trim() || result.stdout.trim()}`);
+ }
+ return parseJsonOutput(result.stdout);
+ },
+ });
+}
diff --git a/mcp-server/src/tools/task-briefing.ts b/mcp-server/src/tools/task-briefing.ts
new file mode 100644
index 00000000..9273a6d7
--- /dev/null
+++ b/mcp-server/src/tools/task-briefing.ts
@@ -0,0 +1,32 @@
+import { z } from 'zod';
+import { UserError } from 'fastmcp';
+import type { FastMCP } from 'fastmcp';
+import type { ITeamctlRunner } from '../teamctl-runner.js';
+import { parseTextOutput } from '../output-parser.js';
+import { teamNameSchema, memberNameSchema } from '../schemas.js';
+
+export function register(server: FastMCP, runner: ITeamctlRunner): void {
+ server.addTool({
+ name: 'task_briefing',
+ description: `Generate a text briefing for a team member showing their assigned tasks vs the team board.
+
+Returns a human-readable multi-line report. Automatically filters out internal bookkeeping tasks.`,
+ annotations: {
+ readOnlyHint: true,
+ destructiveHint: false,
+ },
+ parameters: z.object({
+ team: teamNameSchema,
+ member: memberNameSchema.describe('Member name to generate briefing for'),
+ }),
+ execute: async (args) => {
+ const cliArgs = ['--team', args.team, 'task', 'briefing', '--for', args.member];
+
+ const result = await runner.execute(cliArgs);
+ if (result.exitCode !== 0) {
+ throw new UserError(`Failed to get briefing: ${result.stderr.trim() || result.stdout.trim()}`);
+ }
+ return parseTextOutput(result.stdout);
+ },
+ });
+}
diff --git a/mcp-server/src/tools/task-comment.ts b/mcp-server/src/tools/task-comment.ts
new file mode 100644
index 00000000..951ef0a9
--- /dev/null
+++ b/mcp-server/src/tools/task-comment.ts
@@ -0,0 +1,34 @@
+import { z } from 'zod';
+import { UserError } from 'fastmcp';
+import type { FastMCP } from 'fastmcp';
+import type { ITeamctlRunner } from '../teamctl-runner.js';
+import { parseOkOutput } from '../output-parser.js';
+import { teamNameSchema, taskIdSchema, memberNameSchema } from '../schemas.js';
+
+export function register(server: FastMCP, runner: ITeamctlRunner): void {
+ server.addTool({
+ name: 'task_comment',
+ description: `Add a comment to a task. Sends an inbox notification to the task owner (unless the commenter is the owner).`,
+ annotations: {
+ readOnlyHint: false,
+ destructiveHint: false,
+ },
+ parameters: z.object({
+ team: teamNameSchema,
+ task_id: taskIdSchema,
+ text: z.string().min(1).max(10000).describe('Comment text'),
+ from: memberNameSchema.optional().describe('Author name (skips self-notification)'),
+ }),
+ execute: async (args) => {
+ const cliArgs = ['--team', args.team, 'task', 'comment', args.task_id, '--text', args.text];
+
+ if (args.from) cliArgs.push('--from', args.from);
+
+ const result = await runner.execute(cliArgs);
+ if (result.exitCode !== 0) {
+ throw new UserError(`Failed to add comment: ${result.stderr.trim() || result.stdout.trim()}`);
+ }
+ return parseOkOutput(result.stdout);
+ },
+ });
+}
diff --git a/mcp-server/src/tools/task-create.ts b/mcp-server/src/tools/task-create.ts
new file mode 100644
index 00000000..5c08e4e7
--- /dev/null
+++ b/mcp-server/src/tools/task-create.ts
@@ -0,0 +1,52 @@
+import { z } from 'zod';
+import { UserError } from 'fastmcp';
+import type { FastMCP } from 'fastmcp';
+import type { ITeamctlRunner } from '../teamctl-runner.js';
+import { parseJsonOutput } from '../output-parser.js';
+import { teamNameSchema, memberNameSchema, taskIdsArraySchema } from '../schemas.js';
+
+export function register(server: FastMCP, runner: ITeamctlRunner): void {
+ server.addTool({
+ name: 'task_create',
+ description: `Create a new task in a team's task board. Returns the created task JSON.
+
+Behavior:
+- If owner is set and no blockers → status defaults to "in_progress"
+- If blocked_by specified → status defaults to "pending" (even with owner)
+- If notify is true, sends an inbox notification to the assigned owner`,
+ annotations: {
+ readOnlyHint: false,
+ destructiveHint: false,
+ },
+ parameters: z.object({
+ team: teamNameSchema,
+ subject: z.string().min(1).max(500).describe('Task title'),
+ description: z.string().max(5000).optional().describe('Detailed task description'),
+ owner: memberNameSchema.optional().describe('Assign to a team member'),
+ blocked_by: taskIdsArraySchema.optional().describe('Task IDs that block this task'),
+ related: taskIdsArraySchema.optional().describe('Related (non-blocking) task IDs'),
+ status: z.enum(['pending', 'in_progress']).optional().describe('Initial status override'),
+ active_form: z.string().max(200).optional().describe('Active form hint for CLI display'),
+ notify: z.boolean().optional().describe('Send inbox notification to owner'),
+ from: memberNameSchema.optional().describe('Author name for notifications'),
+ }),
+ execute: async (args) => {
+ const cliArgs = ['--team', args.team, 'task', 'create', '--subject', args.subject];
+
+ if (args.description) cliArgs.push('--description', args.description);
+ if (args.owner) cliArgs.push('--owner', args.owner);
+ if (args.blocked_by?.length) cliArgs.push('--blocked-by', args.blocked_by.join(','));
+ if (args.related?.length) cliArgs.push('--related', args.related.join(','));
+ if (args.status) cliArgs.push('--status', args.status);
+ if (args.active_form) cliArgs.push('--activeForm', args.active_form);
+ if (args.notify) cliArgs.push('--notify');
+ if (args.from) cliArgs.push('--from', args.from);
+
+ const result = await runner.execute(cliArgs);
+ if (result.exitCode !== 0) {
+ throw new UserError(`Failed to create task: ${result.stderr.trim() || result.stdout.trim()}`);
+ }
+ return parseJsonOutput(result.stdout);
+ },
+ });
+}
diff --git a/mcp-server/src/tools/task-get.ts b/mcp-server/src/tools/task-get.ts
new file mode 100644
index 00000000..cf56ec6d
--- /dev/null
+++ b/mcp-server/src/tools/task-get.ts
@@ -0,0 +1,30 @@
+import { z } from 'zod';
+import { UserError } from 'fastmcp';
+import type { FastMCP } from 'fastmcp';
+import type { ITeamctlRunner } from '../teamctl-runner.js';
+import { parseJsonOutput } from '../output-parser.js';
+import { teamNameSchema, taskIdSchema } from '../schemas.js';
+
+export function register(server: FastMCP, runner: ITeamctlRunner): void {
+ server.addTool({
+ name: 'task_get',
+ description: `Get a single task by its ID. Returns the full task JSON including status, owner, comments, dependencies, work intervals, and status history.`,
+ annotations: {
+ readOnlyHint: true,
+ destructiveHint: false,
+ },
+ parameters: z.object({
+ team: teamNameSchema,
+ task_id: taskIdSchema,
+ }),
+ execute: async (args) => {
+ const cliArgs = ['--team', args.team, 'task', 'get', args.task_id];
+
+ const result = await runner.execute(cliArgs);
+ if (result.exitCode !== 0) {
+ throw new UserError(`Failed to get task: ${result.stderr.trim() || result.stdout.trim()}`);
+ }
+ return parseJsonOutput(result.stdout);
+ },
+ });
+}
diff --git a/mcp-server/src/tools/task-link.ts b/mcp-server/src/tools/task-link.ts
new file mode 100644
index 00000000..0d43021c
--- /dev/null
+++ b/mcp-server/src/tools/task-link.ts
@@ -0,0 +1,51 @@
+import { z } from 'zod';
+import { UserError } from 'fastmcp';
+import type { FastMCP } from 'fastmcp';
+import type { ITeamctlRunner } from '../teamctl-runner.js';
+import { parseOkOutput } from '../output-parser.js';
+import {
+ teamNameSchema,
+ taskIdSchema,
+ linkTypeSchema,
+ linkOperationSchema,
+} from '../schemas.js';
+
+export function register(server: FastMCP, runner: ITeamctlRunner): void {
+ server.addTool({
+ name: 'task_link',
+ description: `Link or unlink task dependencies.
+
+Relationship types:
+- "blocked-by": this task is blocked by the target
+- "blocks": this task blocks the target
+- "related": non-blocking relationship
+
+Links are bidirectional: linking A blocked-by B also sets B blocks A.
+Circular dependencies are detected and rejected.`,
+ annotations: {
+ readOnlyHint: false,
+ destructiveHint: false,
+ },
+ parameters: z.object({
+ team: teamNameSchema,
+ task_id: taskIdSchema,
+ operation: linkOperationSchema.describe('"link" to add, "unlink" to remove'),
+ relationship: linkTypeSchema,
+ target_id: taskIdSchema.describe('The other task ID to link/unlink'),
+ }),
+ execute: async (args) => {
+ const cliArgs = [
+ '--team', args.team,
+ 'task', args.operation,
+ args.task_id,
+ `--${args.relationship}`, args.target_id,
+ ];
+
+ const result = await runner.execute(cliArgs);
+ if (result.exitCode !== 0) {
+ throw new UserError(`Failed to ${args.operation} tasks: ${result.stderr.trim() || result.stdout.trim()}`);
+ }
+ return parseOkOutput(result.stdout);
+ },
+ });
+}
diff --git a/mcp-server/src/tools/task-list.ts b/mcp-server/src/tools/task-list.ts
new file mode 100644
index 00000000..37c05bfd
--- /dev/null
+++ b/mcp-server/src/tools/task-list.ts
@@ -0,0 +1,31 @@
+import { z } from 'zod';
+import { UserError } from 'fastmcp';
+import type { FastMCP } from 'fastmcp';
+import type { ITeamctlRunner } from '../teamctl-runner.js';
+import { parseJsonOutput } from '../output-parser.js';
+import { teamNameSchema } from '../schemas.js';
+
+export function register(server: FastMCP, runner: ITeamctlRunner): void {
+ server.addTool({
+ name: 'task_list',
+ description: `List all tasks for a team. Returns a JSON array of task objects.
+
+Note: includes internal bookkeeping tasks (metadata._internal). Filter client-side if needed.`,
+ annotations: {
+ readOnlyHint: true,
+ destructiveHint: false,
+ },
+ parameters: z.object({
+ team: teamNameSchema,
+ }),
+ execute: async (args) => {
+ const cliArgs = ['--team', args.team, 'task', 'list'];
+
+ const result = await runner.execute(cliArgs);
+ if (result.exitCode !== 0) {
+ throw new UserError(`Failed to list tasks: ${result.stderr.trim() || result.stdout.trim()}`);
+ }
+ return parseJsonOutput(result.stdout);
+ },
+ });
+}
diff --git a/mcp-server/src/tools/task-set-owner.ts b/mcp-server/src/tools/task-set-owner.ts
new file mode 100644
index 00000000..4cfd04bf
--- /dev/null
+++ b/mcp-server/src/tools/task-set-owner.ts
@@ -0,0 +1,38 @@
+import { z } from 'zod';
+import { UserError } from 'fastmcp';
+import type { FastMCP } from 'fastmcp';
+import type { ITeamctlRunner } from '../teamctl-runner.js';
+import { parseOkOutput } from '../output-parser.js';
+import { teamNameSchema, taskIdSchema, memberNameSchema } from '../schemas.js';
+
+export function register(server: FastMCP, runner: ITeamctlRunner): void {
+ server.addTool({
+ name: 'task_set_owner',
+ description: `Assign a task to a team member or clear the assignment.
+
+Pass owner="clear" to unassign. Optionally sends an inbox notification to the new owner.`,
+ annotations: {
+ readOnlyHint: false,
+ destructiveHint: false,
+ },
+ parameters: z.object({
+ team: teamNameSchema,
+ task_id: taskIdSchema,
+ owner: z.union([memberNameSchema, z.literal('clear')]).describe('Member name to assign, or "clear" to unassign'),
+ notify: z.boolean().optional().describe('Send inbox notification to new owner'),
+ from: memberNameSchema.optional().describe('Author name for notification'),
+ }),
+ execute: async (args) => {
+ const cliArgs = ['--team', args.team, 'task', 'set-owner', args.task_id, args.owner];
+
+ if (args.notify) cliArgs.push('--notify');
+ if (args.from) cliArgs.push('--from', args.from);
+
+ const result = await runner.execute(cliArgs);
+ if (result.exitCode !== 0) {
+ throw new UserError(`Failed to set owner: ${result.stderr.trim() || result.stdout.trim()}`);
+ }
+ return parseOkOutput(result.stdout);
+ },
+ });
+}
diff --git a/mcp-server/src/tools/task-set-status.ts b/mcp-server/src/tools/task-set-status.ts
new file mode 100644
index 00000000..a5b96294
--- /dev/null
+++ b/mcp-server/src/tools/task-set-status.ts
@@ -0,0 +1,35 @@
+import { z } from 'zod';
+import { UserError } from 'fastmcp';
+import type { FastMCP } from 'fastmcp';
+import type { ITeamctlRunner } from '../teamctl-runner.js';
+import { parseOkOutput } from '../output-parser.js';
+import { teamNameSchema, taskIdSchema, taskStatusSchema } from '../schemas.js';
+
+export function register(server: FastMCP, runner: ITeamctlRunner): void {
+ server.addTool({
+ name: 'task_set_status',
+ description: `Change the status of a task.
+
+Valid transitions: pending → in_progress → completed → deleted (and back).
+Shortcuts: status "in_progress" is equivalent to "task start", "completed" to "task complete".
+Records status history and tracks work intervals (time spent in_progress).`,
+ annotations: {
+ readOnlyHint: false,
+ destructiveHint: false,
+ },
+ parameters: z.object({
+ team: teamNameSchema,
+ task_id: taskIdSchema,
+ status: taskStatusSchema.describe('New status for the task'),
+ }),
+ execute: async (args) => {
+ const cliArgs = ['--team', args.team, 'task', 'set-status', args.task_id, args.status];
+
+ const result = await runner.execute(cliArgs);
+ if (result.exitCode !== 0) {
+ throw new UserError(`Failed to set status: ${result.stderr.trim() || result.stdout.trim()}`);
+ }
+ return parseOkOutput(result.stdout);
+ },
+ });
+}
diff --git a/mcp-server/test/output-parser.test.ts b/mcp-server/test/output-parser.test.ts
new file mode 100644
index 00000000..52f8179f
--- /dev/null
+++ b/mcp-server/test/output-parser.test.ts
@@ -0,0 +1,76 @@
+import { describe, it, expect } from 'vitest';
+import {
+ parseJsonOutput,
+ parseOkOutput,
+ parseTextOutput,
+ formatError,
+} from '../src/output-parser.js';
+
+describe('parseJsonOutput', () => {
+ it('parses valid JSON object', () => {
+ const input = '{"id":"42","subject":"Fix bug"}\n';
+ expect(parseJsonOutput(input)).toEqual({ id: '42', subject: 'Fix bug' });
+ });
+
+ it('parses valid JSON array', () => {
+ const input = '[{"id":"1"},{"id":"2"}]\n';
+ expect(parseJsonOutput(input)).toEqual([{ id: '1' }, { id: '2' }]);
+ });
+
+ it('trims whitespace', () => {
+ const input = ' \n {"ok":true} \n ';
+ expect(parseJsonOutput(input)).toEqual({ ok: true });
+ });
+
+ it('throws on empty output', () => {
+ expect(() => parseJsonOutput('')).toThrow('Empty output');
+ expect(() => parseJsonOutput(' \n ')).toThrow('Empty output');
+ });
+
+ it('throws on invalid JSON', () => {
+ expect(() => parseJsonOutput('not json')).toThrow('Failed to parse');
+ });
+});
+
+describe('parseOkOutput', () => {
+ it('strips "OK " prefix', () => {
+ expect(parseOkOutput('OK task #1 status=completed\n')).toBe('task #1 status=completed');
+ });
+
+ it('handles bare "OK"', () => {
+ expect(parseOkOutput('OK\n')).toBe('OK');
+ });
+
+ it('returns as-is for unexpected format', () => {
+ expect(parseOkOutput('Something else')).toBe('Something else');
+ });
+
+ it('trims whitespace', () => {
+ expect(parseOkOutput(' OK kanban #1 cleared \n')).toBe('kanban #1 cleared');
+ });
+});
+
+describe('parseTextOutput', () => {
+ it('trims and returns text', () => {
+ const briefing = '=== Task Briefing for alice ===\nTask #1: Fix bug\n';
+ expect(parseTextOutput(briefing)).toBe('=== Task Briefing for alice ===\nTask #1: Fix bug');
+ });
+
+ it('handles empty string', () => {
+ expect(parseTextOutput('')).toBe('');
+ });
+});
+
+describe('formatError', () => {
+ it('uses stderr when available', () => {
+ expect(formatError('Task not found: #42\n', '')).toBe('Task not found: #42');
+ });
+
+ it('falls back to stdout', () => {
+ expect(formatError('', 'Unexpected error\n')).toBe('Unexpected error');
+ });
+
+ it('returns default for empty', () => {
+ expect(formatError('', '')).toBe('Unknown teamctl error');
+ });
+});
diff --git a/mcp-server/test/schemas.test.ts b/mcp-server/test/schemas.test.ts
new file mode 100644
index 00000000..cfd8b5f6
--- /dev/null
+++ b/mcp-server/test/schemas.test.ts
@@ -0,0 +1,220 @@
+import { describe, it, expect } from 'vitest';
+import {
+ teamNameSchema,
+ taskIdSchema,
+ memberNameSchema,
+ taskStatusSchema,
+ kanbanColumnSchema,
+ clarificationSchema,
+ linkTypeSchema,
+ linkOperationSchema,
+ reviewDecisionSchema,
+ reviewerOperationSchema,
+ taskIdsArraySchema,
+ filePathSchema,
+ safeFilenameSchema,
+ mimeTypeSchema,
+} from '../src/schemas.js';
+
+describe('teamNameSchema', () => {
+ it('accepts valid team names', () => {
+ expect(teamNameSchema.parse('acme')).toBe('acme');
+ expect(teamNameSchema.parse('my-team')).toBe('my-team');
+ expect(teamNameSchema.parse('My_Team')).toBe('My_Team');
+ expect(teamNameSchema.parse('team123')).toBe('team123');
+ });
+
+ it('rejects empty', () => {
+ expect(() => teamNameSchema.parse('')).toThrow();
+ });
+
+ it('rejects path traversal', () => {
+ expect(() => teamNameSchema.parse('..')).toThrow();
+ expect(() => teamNameSchema.parse('.')).toThrow();
+ expect(() => teamNameSchema.parse('a/b')).toThrow();
+ expect(() => teamNameSchema.parse('a\\b')).toThrow();
+ expect(() => teamNameSchema.parse('a..b')).toThrow();
+ });
+
+ it('rejects null bytes', () => {
+ expect(() => teamNameSchema.parse('a\0b')).toThrow();
+ });
+
+ it('rejects too long', () => {
+ expect(() => teamNameSchema.parse('a'.repeat(129))).toThrow();
+ });
+});
+
+describe('taskIdSchema', () => {
+ it('accepts numeric IDs', () => {
+ expect(taskIdSchema.parse('1')).toBe('1');
+ expect(taskIdSchema.parse('42')).toBe('42');
+ expect(taskIdSchema.parse('1234567890')).toBe('1234567890');
+ });
+
+ it('accepts zero', () => {
+ expect(taskIdSchema.parse('0')).toBe('0');
+ });
+
+ it('accepts leading zeros', () => {
+ expect(taskIdSchema.parse('007')).toBe('007');
+ });
+
+ it('rejects non-numeric', () => {
+ expect(() => taskIdSchema.parse('abc')).toThrow();
+ expect(() => taskIdSchema.parse('')).toThrow();
+ expect(() => taskIdSchema.parse('1.5')).toThrow();
+ expect(() => taskIdSchema.parse('-1')).toThrow();
+ });
+
+ it('rejects too long', () => {
+ expect(() => taskIdSchema.parse('12345678901')).toThrow();
+ });
+});
+
+describe('memberNameSchema', () => {
+ it('accepts valid member names', () => {
+ expect(memberNameSchema.parse('alice')).toBe('alice');
+ expect(memberNameSchema.parse('bob-smith')).toBe('bob-smith');
+ expect(memberNameSchema.parse('user_1')).toBe('user_1');
+ });
+
+ it('rejects path traversal', () => {
+ expect(() => memberNameSchema.parse('..')).toThrow();
+ expect(() => memberNameSchema.parse('a/b')).toThrow();
+ expect(() => memberNameSchema.parse('a\\b')).toThrow();
+ expect(() => memberNameSchema.parse('a..b')).toThrow();
+ });
+
+ it('rejects null bytes', () => {
+ expect(() => memberNameSchema.parse('a\0b')).toThrow();
+ });
+
+ it('rejects too long', () => {
+ expect(() => memberNameSchema.parse('a'.repeat(129))).toThrow();
+ });
+
+ it('rejects empty', () => {
+ expect(() => memberNameSchema.parse('')).toThrow();
+ });
+});
+
+describe('enum schemas', () => {
+ it('taskStatusSchema accepts valid values', () => {
+ expect(taskStatusSchema.parse('pending')).toBe('pending');
+ expect(taskStatusSchema.parse('in_progress')).toBe('in_progress');
+ expect(taskStatusSchema.parse('completed')).toBe('completed');
+ expect(taskStatusSchema.parse('deleted')).toBe('deleted');
+ expect(() => taskStatusSchema.parse('invalid')).toThrow();
+ });
+
+ it('kanbanColumnSchema accepts valid values', () => {
+ expect(kanbanColumnSchema.parse('review')).toBe('review');
+ expect(kanbanColumnSchema.parse('approved')).toBe('approved');
+ expect(() => kanbanColumnSchema.parse('todo')).toThrow();
+ expect(() => kanbanColumnSchema.parse('')).toThrow();
+ });
+
+ it('clarificationSchema accepts valid values', () => {
+ expect(clarificationSchema.parse('lead')).toBe('lead');
+ expect(clarificationSchema.parse('user')).toBe('user');
+ expect(clarificationSchema.parse('clear')).toBe('clear');
+ expect(() => clarificationSchema.parse('nobody')).toThrow();
+ });
+
+ it('linkTypeSchema accepts valid values', () => {
+ expect(linkTypeSchema.parse('blocked-by')).toBe('blocked-by');
+ expect(linkTypeSchema.parse('blocks')).toBe('blocks');
+ expect(linkTypeSchema.parse('related')).toBe('related');
+ });
+
+ it('linkOperationSchema accepts valid values', () => {
+ expect(linkOperationSchema.parse('link')).toBe('link');
+ expect(linkOperationSchema.parse('unlink')).toBe('unlink');
+ });
+
+ it('reviewDecisionSchema accepts valid values', () => {
+ expect(reviewDecisionSchema.parse('approve')).toBe('approve');
+ expect(reviewDecisionSchema.parse('request-changes')).toBe('request-changes');
+ });
+
+ it('reviewerOperationSchema accepts valid values', () => {
+ expect(reviewerOperationSchema.parse('list')).toBe('list');
+ expect(reviewerOperationSchema.parse('add')).toBe('add');
+ expect(reviewerOperationSchema.parse('remove')).toBe('remove');
+ });
+});
+
+describe('taskIdsArraySchema', () => {
+ it('accepts valid arrays', () => {
+ expect(taskIdsArraySchema.parse(['1', '2', '3'])).toEqual(['1', '2', '3']);
+ expect(taskIdsArraySchema.parse([])).toEqual([]);
+ });
+
+ it('rejects arrays with invalid IDs', () => {
+ expect(() => taskIdsArraySchema.parse(['abc'])).toThrow();
+ expect(() => taskIdsArraySchema.parse(['1', 'bad'])).toThrow();
+ });
+});
+
+describe('filePathSchema', () => {
+ it('accepts absolute paths', () => {
+ expect(filePathSchema.parse('/home/user/file.txt')).toBe('/home/user/file.txt');
+ expect(filePathSchema.parse('/tmp/attachment.pdf')).toBe('/tmp/attachment.pdf');
+ });
+
+ it('rejects relative paths', () => {
+ expect(() => filePathSchema.parse('relative/path.txt')).toThrow();
+ expect(() => filePathSchema.parse('./file.txt')).toThrow();
+ });
+
+ it('rejects path traversal', () => {
+ expect(() => filePathSchema.parse('/home/user/../etc/passwd')).toThrow();
+ expect(() => filePathSchema.parse('/home/../../../etc/shadow')).toThrow();
+ });
+
+ it('rejects null bytes', () => {
+ expect(() => filePathSchema.parse('/home/user/\0evil')).toThrow();
+ });
+
+ it('rejects empty', () => {
+ expect(() => filePathSchema.parse('')).toThrow();
+ });
+});
+
+describe('safeFilenameSchema', () => {
+ it('accepts valid filenames', () => {
+ expect(safeFilenameSchema.parse('report.pdf')).toBe('report.pdf');
+ expect(safeFilenameSchema.parse('my-file_v2.tar.gz')).toBe('my-file_v2.tar.gz');
+ });
+
+ it('rejects path separators', () => {
+ expect(() => safeFilenameSchema.parse('../../evil')).toThrow();
+ expect(() => safeFilenameSchema.parse('dir/file')).toThrow();
+ expect(() => safeFilenameSchema.parse('dir\\file')).toThrow();
+ });
+
+ it('rejects null bytes', () => {
+ expect(() => safeFilenameSchema.parse('file\0name')).toThrow();
+ });
+
+ it('rejects too long', () => {
+ expect(() => safeFilenameSchema.parse('a'.repeat(256))).toThrow();
+ });
+});
+
+describe('mimeTypeSchema', () => {
+ it('accepts valid MIME types', () => {
+ expect(mimeTypeSchema.parse('application/pdf')).toBe('application/pdf');
+ expect(mimeTypeSchema.parse('image/png')).toBe('image/png');
+ expect(mimeTypeSchema.parse('text/plain')).toBe('text/plain');
+ expect(mimeTypeSchema.parse('application/octet-stream')).toBe('application/octet-stream');
+ });
+
+ it('rejects invalid formats', () => {
+ expect(() => mimeTypeSchema.parse('invalid')).toThrow();
+ expect(() => mimeTypeSchema.parse('/pdf')).toThrow();
+ expect(() => mimeTypeSchema.parse('application/')).toThrow();
+ expect(() => mimeTypeSchema.parse('')).toThrow();
+ });
+});
diff --git a/mcp-server/test/teamctl-runner.test.ts b/mcp-server/test/teamctl-runner.test.ts
new file mode 100644
index 00000000..54b703f7
--- /dev/null
+++ b/mcp-server/test/teamctl-runner.test.ts
@@ -0,0 +1,78 @@
+import { describe, it, expect, vi } from 'vitest';
+import { TeamctlRunner } from '../src/teamctl-runner.js';
+import type { ITeamctlRunner, TeamctlResult } from '../src/teamctl-runner.js';
+
+// We can't easily test the real subprocess without teamctl.js installed,
+// so we test the interface contract and error handling.
+
+describe('TeamctlRunner', () => {
+ it('throws if teamctl.js does not exist', () => {
+ expect(
+ () => new TeamctlRunner({ teamctlPath: '/nonexistent/teamctl.js' }),
+ ).toThrow('teamctl.js not found');
+ });
+
+ it('resolves path from TEAMCTL_PATH env', () => {
+ const original = process.env['TEAMCTL_PATH'];
+ try {
+ process.env['TEAMCTL_PATH'] = '/tmp/test-teamctl.js';
+ // Will throw because file doesn't exist, but we can check the error message
+ expect(
+ () => new TeamctlRunner(),
+ ).toThrow('/tmp/test-teamctl.js');
+ } finally {
+ if (original !== undefined) {
+ process.env['TEAMCTL_PATH'] = original;
+ } else {
+ delete process.env['TEAMCTL_PATH'];
+ }
+ }
+ });
+});
+
+// Mock runner for tool tests
+export function createMockRunner(
+ responses: Map | TeamctlResult,
+): ITeamctlRunner {
+ return {
+ execute: vi.fn(async (args: string[]): Promise => {
+ if (responses instanceof Map) {
+ const key = args.join(' ');
+ const result = responses.get(key);
+ if (result) return result;
+ // Fallback: check if any key is a prefix
+ for (const [k, v] of responses) {
+ if (key.startsWith(k)) return v;
+ }
+ return { stdout: '', stderr: 'No mock for: ' + key, exitCode: 1 };
+ }
+ return responses;
+ }),
+ };
+}
+
+describe('ITeamctlRunner interface', () => {
+ it('mock runner returns success', async () => {
+ const runner = createMockRunner({
+ stdout: '{"id":"1","subject":"Test"}\n',
+ stderr: '',
+ exitCode: 0,
+ });
+
+ const result = await runner.execute(['--team', 'test', 'task', 'create']);
+ expect(result.exitCode).toBe(0);
+ expect(JSON.parse(result.stdout)).toHaveProperty('id', '1');
+ });
+
+ it('mock runner returns error', async () => {
+ const runner = createMockRunner({
+ stdout: '',
+ stderr: 'Task not found: #99\n',
+ exitCode: 1,
+ });
+
+ const result = await runner.execute(['--team', 'test', 'task', 'get', '99']);
+ expect(result.exitCode).toBe(1);
+ expect(result.stderr).toContain('Task not found');
+ });
+});
diff --git a/mcp-server/test/tools/kanban-move.test.ts b/mcp-server/test/tools/kanban-move.test.ts
new file mode 100644
index 00000000..e0468131
--- /dev/null
+++ b/mcp-server/test/tools/kanban-move.test.ts
@@ -0,0 +1,42 @@
+import { describe, it, expect } from 'vitest';
+import { register } from '../../src/tools/kanban-move.js';
+import { createMockRunner, createMockServer, ok, fail } from './test-helpers.js';
+
+describe('kanban_move', () => {
+ function setup(response = ok('OK kanban #1 column=review\n')) {
+ const runner = createMockRunner(response);
+ const { server, tools } = createMockServer();
+ register(server, runner);
+ return { runner, tool: tools.get('kanban_move')! };
+ }
+
+ it('builds set-column CLI args', async () => {
+ const { runner, tool } = setup();
+ await tool.execute({ team: 'acme', task_id: '1', operation: 'set-column', column: 'review' });
+ expect(runner.execute).toHaveBeenCalledWith([
+ '--team', 'acme', 'kanban', 'set-column', '1', 'review',
+ ]);
+ });
+
+ it('builds clear CLI args', async () => {
+ const { runner, tool } = setup(ok('OK kanban #1 cleared\n'));
+ await tool.execute({ team: 'acme', task_id: '1', operation: 'clear' });
+ expect(runner.execute).toHaveBeenCalledWith([
+ '--team', 'acme', 'kanban', 'clear', '1',
+ ]);
+ });
+
+ it('throws UserError when set-column called without column', async () => {
+ const { tool } = setup();
+ await expect(
+ tool.execute({ team: 'acme', task_id: '1', operation: 'set-column' }),
+ ).rejects.toThrow('column is required');
+ });
+
+ it('throws on CLI failure', async () => {
+ const { tool } = setup(fail('task not found'));
+ await expect(
+ tool.execute({ team: 'acme', task_id: '99', operation: 'clear' }),
+ ).rejects.toThrow('Failed to update kanban');
+ });
+});
diff --git a/mcp-server/test/tools/kanban-reviewers.test.ts b/mcp-server/test/tools/kanban-reviewers.test.ts
new file mode 100644
index 00000000..ee854535
--- /dev/null
+++ b/mcp-server/test/tools/kanban-reviewers.test.ts
@@ -0,0 +1,61 @@
+import { describe, it, expect } from 'vitest';
+import { register } from '../../src/tools/kanban-reviewers.js';
+import { createMockRunner, createMockServer, ok, fail } from './test-helpers.js';
+
+describe('kanban_reviewers', () => {
+ function setup(response = ok('["alice","bob"]')) {
+ const runner = createMockRunner(response);
+ const { server, tools } = createMockServer();
+ register(server, runner);
+ return { runner, tool: tools.get('kanban_reviewers')! };
+ }
+
+ it('builds list CLI args', async () => {
+ const { runner, tool } = setup();
+ await tool.execute({ team: 'acme', operation: 'list' });
+ expect(runner.execute).toHaveBeenCalledWith([
+ '--team', 'acme', 'kanban', 'reviewers', 'list',
+ ]);
+ });
+
+ it('returns JSON array for list', async () => {
+ const { tool } = setup();
+ const result = await tool.execute({ team: 'acme', operation: 'list' });
+ expect(result).toEqual(['alice', 'bob']);
+ });
+
+ it('builds add CLI args with name', async () => {
+ const { runner, tool } = setup(ok('OK reviewer added\n'));
+ await tool.execute({ team: 'acme', operation: 'add', name: 'charlie' });
+ expect(runner.execute).toHaveBeenCalledWith([
+ '--team', 'acme', 'kanban', 'reviewers', 'add', 'charlie',
+ ]);
+ });
+
+ it('returns OK text for add/remove', async () => {
+ const { tool } = setup(ok('OK reviewer added\n'));
+ const result = await tool.execute({ team: 'acme', operation: 'add', name: 'charlie' });
+ expect(result).toBe('reviewer added');
+ });
+
+ it('throws UserError when add called without name', async () => {
+ const { tool } = setup();
+ await expect(
+ tool.execute({ team: 'acme', operation: 'add' }),
+ ).rejects.toThrow('name is required');
+ });
+
+ it('throws UserError when remove called without name', async () => {
+ const { tool } = setup();
+ await expect(
+ tool.execute({ team: 'acme', operation: 'remove' }),
+ ).rejects.toThrow('name is required');
+ });
+
+ it('throws on CLI failure', async () => {
+ const { tool } = setup(fail('reviewer not found'));
+ await expect(
+ tool.execute({ team: 'acme', operation: 'remove', name: 'nobody' }),
+ ).rejects.toThrow('Failed to manage reviewers');
+ });
+});
diff --git a/mcp-server/test/tools/message-send.test.ts b/mcp-server/test/tools/message-send.test.ts
new file mode 100644
index 00000000..8bb206e8
--- /dev/null
+++ b/mcp-server/test/tools/message-send.test.ts
@@ -0,0 +1,48 @@
+import { describe, it, expect } from 'vitest';
+import { register } from '../../src/tools/message-send.js';
+import { createMockRunner, createMockServer, ok, fail } from './test-helpers.js';
+
+describe('message_send', () => {
+ const deliveryJson = '{"deliveredToInbox":true,"messageId":"msg_abc"}';
+
+ function setup(response = ok(deliveryJson)) {
+ const runner = createMockRunner(response);
+ const { server, tools } = createMockServer();
+ register(server, runner);
+ return { runner, tool: tools.get('message_send')! };
+ }
+
+ it('builds minimal CLI args', async () => {
+ const { runner, tool } = setup();
+ await tool.execute({ team: 'acme', to: 'alice', text: 'Hello!' });
+ expect(runner.execute).toHaveBeenCalledWith([
+ '--team', 'acme', 'message', 'send', '--to', 'alice', '--text', 'Hello!',
+ ]);
+ });
+
+ it('includes summary and from flags', async () => {
+ const { runner, tool } = setup();
+ await tool.execute({
+ team: 'acme', to: 'alice', text: 'Task done',
+ summary: 'Completed task #1', from: 'bob',
+ });
+ const args = runner.execute.mock.calls[0]![0] as string[];
+ expect(args).toContain('--summary');
+ expect(args[args.indexOf('--summary') + 1]).toBe('Completed task #1');
+ expect(args).toContain('--from');
+ expect(args[args.indexOf('--from') + 1]).toBe('bob');
+ });
+
+ it('returns parsed JSON', async () => {
+ const { tool } = setup();
+ const result = await tool.execute({ team: 'acme', to: 'alice', text: 'Hello!' });
+ expect(result).toEqual({ deliveredToInbox: true, messageId: 'msg_abc' });
+ });
+
+ it('throws on CLI failure', async () => {
+ const { tool } = setup(fail('recipient inbox not found'));
+ await expect(
+ tool.execute({ team: 'acme', to: 'nobody', text: 'Hi' }),
+ ).rejects.toThrow('Failed to send message');
+ });
+});
diff --git a/mcp-server/test/tools/register-all.test.ts b/mcp-server/test/tools/register-all.test.ts
new file mode 100644
index 00000000..d562525e
--- /dev/null
+++ b/mcp-server/test/tools/register-all.test.ts
@@ -0,0 +1,28 @@
+import { describe, it, expect } from 'vitest';
+import { registerAllTools } from '../../src/tools/index.js';
+import { createMockRunner, createMockServer } from './test-helpers.js';
+
+describe('registerAllTools', () => {
+ it('registers exactly 13 tools', () => {
+ const runner = createMockRunner({ stdout: '', stderr: '', exitCode: 0 });
+ const { server, tools } = createMockServer();
+ registerAllTools(server, runner);
+ expect(tools.size).toBe(13);
+ });
+
+ it('registers all expected tool names', () => {
+ const runner = createMockRunner({ stdout: '', stderr: '', exitCode: 0 });
+ const { server, tools } = createMockServer();
+ registerAllTools(server, runner);
+
+ const expected = [
+ 'task_create', 'task_set_status', 'task_set_owner',
+ 'task_get', 'task_list', 'task_comment', 'task_link',
+ 'task_briefing', 'task_attach', 'kanban_move',
+ 'kanban_reviewers', 'review_action', 'message_send',
+ ];
+ for (const name of expected) {
+ expect(tools.has(name), `missing tool: ${name}`).toBe(true);
+ }
+ });
+});
diff --git a/mcp-server/test/tools/review-action.test.ts b/mcp-server/test/tools/review-action.test.ts
new file mode 100644
index 00000000..952f1ae9
--- /dev/null
+++ b/mcp-server/test/tools/review-action.test.ts
@@ -0,0 +1,66 @@
+import { describe, it, expect } from 'vitest';
+import { register } from '../../src/tools/review-action.js';
+import { createMockRunner, createMockServer, ok, fail } from './test-helpers.js';
+
+describe('review_action', () => {
+ function setup(response = ok('OK review #1 approved\n')) {
+ const runner = createMockRunner(response);
+ const { server, tools } = createMockServer();
+ register(server, runner);
+ return { runner, tool: tools.get('review_action')! };
+ }
+
+ it('builds approve CLI args (no comment)', async () => {
+ const { runner, tool } = setup();
+ await tool.execute({ team: 'acme', task_id: '1', decision: 'approve' });
+ expect(runner.execute).toHaveBeenCalledWith([
+ '--team', 'acme', 'review', 'approve', '1',
+ ]);
+ });
+
+ it('builds approve CLI args with --note (not --comment)', async () => {
+ const { runner, tool } = setup();
+ await tool.execute({ team: 'acme', task_id: '1', decision: 'approve', comment: 'LGTM' });
+ const args = runner.execute.mock.calls[0]![0] as string[];
+ // approve uses --note, NOT --comment
+ expect(args).toContain('--note');
+ expect(args[args.indexOf('--note') + 1]).toBe('LGTM');
+ expect(args).not.toContain('--comment');
+ });
+
+ it('builds request-changes CLI args with --comment (not --note)', async () => {
+ const { runner, tool } = setup(ok('OK review #1 requested changes\n'));
+ await tool.execute({
+ team: 'acme', task_id: '1', decision: 'request-changes', comment: 'Fix tests',
+ });
+ const args = runner.execute.mock.calls[0]![0] as string[];
+ expect(args).toContain('--comment');
+ expect(args[args.indexOf('--comment') + 1]).toBe('Fix tests');
+ expect(args).not.toContain('--note');
+ });
+
+ it('throws when request-changes has no comment', async () => {
+ const { tool } = setup();
+ await expect(
+ tool.execute({ team: 'acme', task_id: '1', decision: 'request-changes' }),
+ ).rejects.toThrow('comment is required when requesting changes');
+ });
+
+ it('includes from and notify_owner flags', async () => {
+ const { runner, tool } = setup();
+ await tool.execute({
+ team: 'acme', task_id: '1', decision: 'approve',
+ from: 'alice', notify_owner: true,
+ });
+ const args = runner.execute.mock.calls[0]![0] as string[];
+ expect(args).toContain('--from');
+ expect(args).toContain('--notify-owner');
+ });
+
+ it('throws on CLI failure', async () => {
+ const { tool } = setup(fail('task not in review'));
+ await expect(
+ tool.execute({ team: 'acme', task_id: '1', decision: 'approve' }),
+ ).rejects.toThrow('Failed to approve');
+ });
+});
diff --git a/mcp-server/test/tools/task-attach.test.ts b/mcp-server/test/tools/task-attach.test.ts
new file mode 100644
index 00000000..e8b63fe5
--- /dev/null
+++ b/mcp-server/test/tools/task-attach.test.ts
@@ -0,0 +1,49 @@
+import { describe, it, expect } from 'vitest';
+import { register } from '../../src/tools/task-attach.js';
+import { createMockRunner, createMockServer, ok, fail } from './test-helpers.js';
+
+describe('task_attach', () => {
+ const attachJson = '{"id":"att_123","filename":"report.pdf","mimeType":"application/pdf"}';
+
+ function setup(response = ok(attachJson)) {
+ const runner = createMockRunner(response);
+ const { server, tools } = createMockServer();
+ register(server, runner);
+ return { runner, tool: tools.get('task_attach')! };
+ }
+
+ it('builds minimal CLI args', async () => {
+ const { runner, tool } = setup();
+ await tool.execute({ team: 'acme', task_id: '1', file: '/tmp/report.pdf' });
+ expect(runner.execute).toHaveBeenCalledWith([
+ '--team', 'acme', 'task', 'attach', '1', '--file', '/tmp/report.pdf',
+ ]);
+ });
+
+ it('includes all optional flags', async () => {
+ const { runner, tool } = setup();
+ await tool.execute({
+ team: 'acme', task_id: '1', file: '/tmp/file.pdf',
+ filename: 'renamed.pdf', mime_type: 'application/pdf',
+ mode: 'link', from: 'alice',
+ });
+ const args = runner.execute.mock.calls[0]![0] as string[];
+ expect(args).toContain('--filename');
+ expect(args).toContain('--mime-type');
+ expect(args).toContain('--mode');
+ expect(args).toContain('--from');
+ });
+
+ it('returns parsed JSON', async () => {
+ const { tool } = setup();
+ const result = await tool.execute({ team: 'acme', task_id: '1', file: '/tmp/report.pdf' });
+ expect(result).toEqual({ id: 'att_123', filename: 'report.pdf', mimeType: 'application/pdf' });
+ });
+
+ it('throws on CLI failure', async () => {
+ const { tool } = setup(fail('file too large'));
+ await expect(
+ tool.execute({ team: 'acme', task_id: '1', file: '/tmp/huge.bin' }),
+ ).rejects.toThrow('Failed to attach file');
+ });
+});
diff --git a/mcp-server/test/tools/task-briefing.test.ts b/mcp-server/test/tools/task-briefing.test.ts
new file mode 100644
index 00000000..b915e994
--- /dev/null
+++ b/mcp-server/test/tools/task-briefing.test.ts
@@ -0,0 +1,33 @@
+import { describe, it, expect } from 'vitest';
+import { register } from '../../src/tools/task-briefing.js';
+import { createMockRunner, createMockServer, ok, fail } from './test-helpers.js';
+
+describe('task_briefing', () => {
+ const briefingText = '=== Task Briefing for alice ===\nTask #1: Fix bug [in_progress]\n';
+
+ function setup(response = ok(briefingText)) {
+ const runner = createMockRunner(response);
+ const { server, tools } = createMockServer();
+ register(server, runner);
+ return { runner, tool: tools.get('task_briefing')! };
+ }
+
+ it('builds correct CLI args', async () => {
+ const { runner, tool } = setup();
+ await tool.execute({ team: 'acme', member: 'alice' });
+ expect(runner.execute).toHaveBeenCalledWith([
+ '--team', 'acme', 'task', 'briefing', '--for', 'alice',
+ ]);
+ });
+
+ it('returns trimmed plain text', async () => {
+ const { tool } = setup();
+ const result = await tool.execute({ team: 'acme', member: 'alice' });
+ expect(result).toBe('=== Task Briefing for alice ===\nTask #1: Fix bug [in_progress]');
+ });
+
+ it('throws on CLI failure', async () => {
+ const { tool } = setup(fail('member not found'));
+ await expect(tool.execute({ team: 'acme', member: 'nobody' })).rejects.toThrow('Failed to get briefing');
+ });
+});
diff --git a/mcp-server/test/tools/task-comment.test.ts b/mcp-server/test/tools/task-comment.test.ts
new file mode 100644
index 00000000..2c340e48
--- /dev/null
+++ b/mcp-server/test/tools/task-comment.test.ts
@@ -0,0 +1,33 @@
+import { describe, it, expect } from 'vitest';
+import { register } from '../../src/tools/task-comment.js';
+import { createMockRunner, createMockServer, ok, fail } from './test-helpers.js';
+
+describe('task_comment', () => {
+ function setup(response = ok('OK comment added to task #1\n')) {
+ const runner = createMockRunner(response);
+ const { server, tools } = createMockServer();
+ register(server, runner);
+ return { runner, tool: tools.get('task_comment')! };
+ }
+
+ it('builds correct CLI args', async () => {
+ const { runner, tool } = setup();
+ await tool.execute({ team: 'acme', task_id: '1', text: 'Looking good!' });
+ expect(runner.execute).toHaveBeenCalledWith([
+ '--team', 'acme', 'task', 'comment', '1', '--text', 'Looking good!',
+ ]);
+ });
+
+ it('includes from flag', async () => {
+ const { runner, tool } = setup();
+ await tool.execute({ team: 'acme', task_id: '1', text: 'Done', from: 'alice' });
+ const args = runner.execute.mock.calls[0]![0] as string[];
+ expect(args).toContain('--from');
+ expect(args[args.indexOf('--from') + 1]).toBe('alice');
+ });
+
+ it('throws on CLI failure', async () => {
+ const { tool } = setup(fail('task not found'));
+ await expect(tool.execute({ team: 'acme', task_id: '99', text: 'Hi' })).rejects.toThrow('Failed to add comment');
+ });
+});
diff --git a/mcp-server/test/tools/task-create.test.ts b/mcp-server/test/tools/task-create.test.ts
new file mode 100644
index 00000000..5866e9f9
--- /dev/null
+++ b/mcp-server/test/tools/task-create.test.ts
@@ -0,0 +1,65 @@
+import { describe, it, expect } from 'vitest';
+import { register } from '../../src/tools/task-create.js';
+import { createMockRunner, createMockServer, ok, fail } from './test-helpers.js';
+
+describe('task_create', () => {
+ function setup(response = ok('{"id":"1","subject":"Fix bug","status":"in_progress"}')) {
+ const runner = createMockRunner(response);
+ const { server, tools } = createMockServer();
+ register(server, runner);
+ return { runner, tool: tools.get('task_create')! };
+ }
+
+ it('builds minimal CLI args', async () => {
+ const { runner, tool } = setup();
+ await tool.execute({ team: 'acme', subject: 'Fix bug' });
+ expect(runner.execute).toHaveBeenCalledWith([
+ '--team', 'acme', 'task', 'create', '--subject', 'Fix bug',
+ ]);
+ });
+
+ it('includes all optional flags', async () => {
+ const { runner, tool } = setup();
+ await tool.execute({
+ team: 'acme',
+ subject: 'Big task',
+ description: 'Details here',
+ owner: 'alice',
+ blocked_by: ['1', '2'],
+ related: ['3'],
+ status: 'pending',
+ active_form: 'Fixing bug',
+ notify: true,
+ from: 'bob',
+ });
+ const args = runner.execute.mock.calls[0]![0] as string[];
+ expect(args).toContain('--description');
+ expect(args).toContain('--owner');
+ expect(args).toContain('--blocked-by');
+ expect(args[args.indexOf('--blocked-by') + 1]).toBe('1,2');
+ expect(args).toContain('--related');
+ expect(args[args.indexOf('--related') + 1]).toBe('3');
+ expect(args).toContain('--status');
+ expect(args).toContain('--activeForm');
+ expect(args).toContain('--notify');
+ expect(args).toContain('--from');
+ });
+
+ it('skips empty blocked_by array', async () => {
+ const { runner, tool } = setup();
+ await tool.execute({ team: 'acme', subject: 'Task', blocked_by: [] });
+ const args = runner.execute.mock.calls[0]![0] as string[];
+ expect(args).not.toContain('--blocked-by');
+ });
+
+ it('returns parsed JSON', async () => {
+ const { tool } = setup();
+ const result = await tool.execute({ team: 'acme', subject: 'Fix bug' });
+ expect(result).toEqual({ id: '1', subject: 'Fix bug', status: 'in_progress' });
+ });
+
+ it('throws on CLI failure', async () => {
+ const { tool } = setup(fail('team not found'));
+ await expect(tool.execute({ team: 'bad', subject: 'X' })).rejects.toThrow('Failed to create task');
+ });
+});
diff --git a/mcp-server/test/tools/task-get.test.ts b/mcp-server/test/tools/task-get.test.ts
new file mode 100644
index 00000000..79a3d97f
--- /dev/null
+++ b/mcp-server/test/tools/task-get.test.ts
@@ -0,0 +1,29 @@
+import { describe, it, expect } from 'vitest';
+import { register } from '../../src/tools/task-get.js';
+import { createMockRunner, createMockServer, ok, fail } from './test-helpers.js';
+
+describe('task_get', () => {
+ function setup(response = ok('{"id":"42","subject":"Test","status":"pending"}')) {
+ const runner = createMockRunner(response);
+ const { server, tools } = createMockServer();
+ register(server, runner);
+ return { runner, tool: tools.get('task_get')! };
+ }
+
+ it('builds correct CLI args', async () => {
+ const { runner, tool } = setup();
+ await tool.execute({ team: 'acme', task_id: '42' });
+ expect(runner.execute).toHaveBeenCalledWith(['--team', 'acme', 'task', 'get', '42']);
+ });
+
+ it('returns parsed JSON', async () => {
+ const { tool } = setup();
+ const result = await tool.execute({ team: 'acme', task_id: '42' });
+ expect(result).toEqual({ id: '42', subject: 'Test', status: 'pending' });
+ });
+
+ it('throws on CLI failure', async () => {
+ const { tool } = setup(fail('Task not found: #99'));
+ await expect(tool.execute({ team: 'acme', task_id: '99' })).rejects.toThrow('Failed to get task');
+ });
+});
diff --git a/mcp-server/test/tools/task-link.test.ts b/mcp-server/test/tools/task-link.test.ts
new file mode 100644
index 00000000..7e16b802
--- /dev/null
+++ b/mcp-server/test/tools/task-link.test.ts
@@ -0,0 +1,44 @@
+import { describe, it, expect } from 'vitest';
+import { register } from '../../src/tools/task-link.js';
+import { createMockRunner, createMockServer, ok, fail } from './test-helpers.js';
+
+describe('task_link', () => {
+ function setup(response = ok('OK task #1 blocked-by #2\n')) {
+ const runner = createMockRunner(response);
+ const { server, tools } = createMockServer();
+ register(server, runner);
+ return { runner, tool: tools.get('task_link')! };
+ }
+
+ it('builds link CLI args', async () => {
+ const { runner, tool } = setup();
+ await tool.execute({
+ team: 'acme', task_id: '1', operation: 'link',
+ relationship: 'blocked-by', target_id: '2',
+ });
+ expect(runner.execute).toHaveBeenCalledWith([
+ '--team', 'acme', 'task', 'link', '1', '--blocked-by', '2',
+ ]);
+ });
+
+ it('builds unlink CLI args', async () => {
+ const { runner, tool } = setup(ok('OK task #1 unlinked from #2\n'));
+ await tool.execute({
+ team: 'acme', task_id: '1', operation: 'unlink',
+ relationship: 'related', target_id: '3',
+ });
+ expect(runner.execute).toHaveBeenCalledWith([
+ '--team', 'acme', 'task', 'unlink', '1', '--related', '3',
+ ]);
+ });
+
+ it('throws on CLI failure', async () => {
+ const { tool } = setup(fail('circular dependency'));
+ await expect(
+ tool.execute({
+ team: 'acme', task_id: '1', operation: 'link',
+ relationship: 'blocked-by', target_id: '1',
+ }),
+ ).rejects.toThrow('Failed to link tasks');
+ });
+});
diff --git a/mcp-server/test/tools/task-list.test.ts b/mcp-server/test/tools/task-list.test.ts
new file mode 100644
index 00000000..be1a25af
--- /dev/null
+++ b/mcp-server/test/tools/task-list.test.ts
@@ -0,0 +1,29 @@
+import { describe, it, expect } from 'vitest';
+import { register } from '../../src/tools/task-list.js';
+import { createMockRunner, createMockServer, ok, fail } from './test-helpers.js';
+
+describe('task_list', () => {
+ function setup(response = ok('[{"id":"1"},{"id":"2"}]')) {
+ const runner = createMockRunner(response);
+ const { server, tools } = createMockServer();
+ register(server, runner);
+ return { runner, tool: tools.get('task_list')! };
+ }
+
+ it('builds correct CLI args', async () => {
+ const { runner, tool } = setup();
+ await tool.execute({ team: 'acme' });
+ expect(runner.execute).toHaveBeenCalledWith(['--team', 'acme', 'task', 'list']);
+ });
+
+ it('returns parsed JSON array', async () => {
+ const { tool } = setup();
+ const result = await tool.execute({ team: 'acme' });
+ expect(result).toEqual([{ id: '1' }, { id: '2' }]);
+ });
+
+ it('throws on CLI failure', async () => {
+ const { tool } = setup(fail('team dir not found'));
+ await expect(tool.execute({ team: 'bad' })).rejects.toThrow('Failed to list tasks');
+ });
+});
diff --git a/mcp-server/test/tools/task-set-owner.test.ts b/mcp-server/test/tools/task-set-owner.test.ts
new file mode 100644
index 00000000..d9859066
--- /dev/null
+++ b/mcp-server/test/tools/task-set-owner.test.ts
@@ -0,0 +1,42 @@
+import { describe, it, expect } from 'vitest';
+import { register } from '../../src/tools/task-set-owner.js';
+import { createMockRunner, createMockServer, ok, fail } from './test-helpers.js';
+
+describe('task_set_owner', () => {
+ function setup(response = ok('OK task #1 owner=alice\n')) {
+ const runner = createMockRunner(response);
+ const { server, tools } = createMockServer();
+ register(server, runner);
+ return { runner, tool: tools.get('task_set_owner')! };
+ }
+
+ it('builds args for assignment', async () => {
+ const { runner, tool } = setup();
+ await tool.execute({ team: 'acme', task_id: '1', owner: 'alice' });
+ expect(runner.execute).toHaveBeenCalledWith([
+ '--team', 'acme', 'task', 'set-owner', '1', 'alice',
+ ]);
+ });
+
+ it('builds args for clear', async () => {
+ const { runner, tool } = setup(ok('OK task #1 owner=cleared\n'));
+ await tool.execute({ team: 'acme', task_id: '1', owner: 'clear' });
+ expect(runner.execute).toHaveBeenCalledWith([
+ '--team', 'acme', 'task', 'set-owner', '1', 'clear',
+ ]);
+ });
+
+ it('includes notify and from flags', async () => {
+ const { runner, tool } = setup();
+ await tool.execute({ team: 'acme', task_id: '1', owner: 'alice', notify: true, from: 'bob' });
+ const args = runner.execute.mock.calls[0]![0] as string[];
+ expect(args).toContain('--notify');
+ expect(args).toContain('--from');
+ expect(args[args.indexOf('--from') + 1]).toBe('bob');
+ });
+
+ it('throws on CLI failure', async () => {
+ const { tool } = setup(fail('member not found'));
+ await expect(tool.execute({ team: 'acme', task_id: '1', owner: 'nobody' })).rejects.toThrow('Failed to set owner');
+ });
+});
diff --git a/mcp-server/test/tools/task-set-status.test.ts b/mcp-server/test/tools/task-set-status.test.ts
new file mode 100644
index 00000000..0ca68d0d
--- /dev/null
+++ b/mcp-server/test/tools/task-set-status.test.ts
@@ -0,0 +1,31 @@
+import { describe, it, expect } from 'vitest';
+import { register } from '../../src/tools/task-set-status.js';
+import { createMockRunner, createMockServer, ok, fail } from './test-helpers.js';
+
+describe('task_set_status', () => {
+ function setup(response = ok('OK task #1 status=completed\n')) {
+ const runner = createMockRunner(response);
+ const { server, tools } = createMockServer();
+ register(server, runner);
+ return { runner, tool: tools.get('task_set_status')! };
+ }
+
+ it('builds correct CLI args', async () => {
+ const { runner, tool } = setup();
+ await tool.execute({ team: 'acme', task_id: '1', status: 'completed' });
+ expect(runner.execute).toHaveBeenCalledWith([
+ '--team', 'acme', 'task', 'set-status', '1', 'completed',
+ ]);
+ });
+
+ it('returns parsed OK text', async () => {
+ const { tool } = setup();
+ const result = await tool.execute({ team: 'acme', task_id: '1', status: 'completed' });
+ expect(result).toBe('task #1 status=completed');
+ });
+
+ it('throws on CLI failure', async () => {
+ const { tool } = setup(fail('invalid transition'));
+ await expect(tool.execute({ team: 'acme', task_id: '1', status: 'deleted' })).rejects.toThrow('Failed to set status');
+ });
+});
diff --git a/mcp-server/test/tools/test-helpers.ts b/mcp-server/test/tools/test-helpers.ts
new file mode 100644
index 00000000..fb0a8e01
--- /dev/null
+++ b/mcp-server/test/tools/test-helpers.ts
@@ -0,0 +1,58 @@
+import { vi } from 'vitest';
+import type { FastMCP } from 'fastmcp';
+import type { ITeamctlRunner, TeamctlResult } from '../../src/teamctl-runner.js';
+
+/**
+ * Creates a mock ITeamctlRunner that records calls and returns predetermined results.
+ */
+export function createMockRunner(
+ response: TeamctlResult | ((args: string[]) => TeamctlResult),
+): ITeamctlRunner & { execute: ReturnType } {
+ return {
+ execute: vi.fn(async (args: string[]): Promise => {
+ return typeof response === 'function' ? response(args) : response;
+ }),
+ };
+}
+
+/** Success response helpers */
+export const ok = (stdout: string): TeamctlResult => ({
+ stdout,
+ stderr: '',
+ exitCode: 0,
+});
+
+export const fail = (stderr: string): TeamctlResult => ({
+ stdout: '',
+ stderr,
+ exitCode: 1,
+});
+
+/**
+ * Captures registered tools via a mock FastMCP-like server.
+ * Returns a map of tool name → { execute function, parameters schema }.
+ */
+export interface CapturedTool {
+ name: string;
+ execute: (args: Record) => Promise;
+ parameters: unknown;
+}
+
+export function createMockServer(): {
+ server: FastMCP;
+ tools: Map;
+} {
+ const tools = new Map();
+
+ const server = {
+ addTool: (def: { name: string; execute: CapturedTool['execute']; parameters: unknown }) => {
+ tools.set(def.name, {
+ name: def.name,
+ execute: def.execute,
+ parameters: def.parameters,
+ });
+ },
+ } as unknown as FastMCP;
+
+ return { server, tools };
+}
diff --git a/mcp-server/tsconfig.json b/mcp-server/tsconfig.json
new file mode 100644
index 00000000..2bd3eb89
--- /dev/null
+++ b/mcp-server/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "lib": ["ES2022"],
+ "outDir": "dist",
+ "rootDir": "src",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "isolatedModules": true
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": ["node_modules", "dist", "test"]
+}
diff --git a/mcp-server/tsup.config.ts b/mcp-server/tsup.config.ts
new file mode 100644
index 00000000..5cd234bb
--- /dev/null
+++ b/mcp-server/tsup.config.ts
@@ -0,0 +1,14 @@
+import { defineConfig } from 'tsup';
+
+export default defineConfig({
+ entry: ['src/index.ts'],
+ format: ['esm'],
+ target: 'node20',
+ outDir: 'dist',
+ clean: true,
+ sourcemap: true,
+ dts: false,
+ banner: {
+ js: '#!/usr/bin/env node',
+ },
+});
diff --git a/mcp-server/vitest.config.ts b/mcp-server/vitest.config.ts
new file mode 100644
index 00000000..6497809d
--- /dev/null
+++ b/mcp-server/vitest.config.ts
@@ -0,0 +1,10 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ test: {
+ globals: true,
+ environment: 'node',
+ include: ['test/**/*.test.ts'],
+ testTimeout: 15_000,
+ },
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 147ae6f3..80ad3475 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -370,6 +370,31 @@ importers:
specifier: ^3.1.4
version: 3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@20.0.2)(terser@5.46.0)
+ mcp-server:
+ dependencies:
+ fastmcp:
+ specifier: ^3.34.0
+ version: 3.34.0
+ zod:
+ specifier: ^4.3.6
+ version: 4.3.6
+ devDependencies:
+ '@types/node':
+ specifier: ^22.15.18
+ version: 22.19.15
+ tsup:
+ specifier: ^8.5.1
+ version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
+ tsx:
+ specifier: ^4.21.0
+ version: 4.21.0
+ typescript:
+ specifier: ^5.8.2
+ version: 5.9.3
+ vitest:
+ specifier: ^3.1.4
+ version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.15)(happy-dom@20.0.2)(terser@5.46.0)
+
packages:
7zip-bin@5.2.0:
@@ -479,6 +504,9 @@ packages:
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
engines: {node: '>=18'}
+ '@borewit/text-codec@0.2.1':
+ resolution: {integrity: sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==}
+
'@boundaries/elements@1.1.2':
resolution: {integrity: sha512-DnGHL+v36YVMoWhWZqyJYVZ9dapNm7h4N3/P0lDPirJj0CHVPkjChMCCotj74cg6LW7iPJZFGrdEfh0X0g2bmQ==}
engines: {node: '>=18.18'}
@@ -1063,6 +1091,12 @@ packages:
'@gar/promisify@1.1.3':
resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==}
+ '@hono/node-server@1.19.11':
+ resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==}
+ engines: {node: '>=18.14.1'}
+ peerDependencies:
+ hono: ^4
+
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'}
@@ -1203,6 +1237,16 @@ packages:
'@mermaid-js/parser@1.0.0':
resolution: {integrity: sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw==}
+ '@modelcontextprotocol/sdk@1.27.1':
+ resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@cfworker/json-schema': ^4.1.1
+ zod: ^3.25 || ^4.0
+ peerDependenciesMeta:
+ '@cfworker/json-schema':
+ optional: true
+
'@napi-rs/wasm-runtime@0.2.12':
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
@@ -1893,10 +1937,20 @@ packages:
'@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
+ '@sec-ant/readable-stream@0.4.1':
+ resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
+
'@sindresorhus/is@4.6.0':
resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==}
engines: {node: '>=10'}
+ '@sindresorhus/merge-streams@4.0.0':
+ resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
+ engines: {node: '>=18'}
+
+ '@standard-schema/spec@1.1.0':
+ resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
+
'@szmarczak/http-timer@4.0.6':
resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==}
engines: {node: '>=10'}
@@ -1915,6 +1969,13 @@ packages:
'@tanstack/virtual-core@3.13.18':
resolution: {integrity: sha512-Mx86Hqu1k39icq2Zusq+Ey2J6dDWTjDvEv43PJtRCoEYTLyfaPnxIQ6iy7YAOK0NV/qOEmZQ/uCufrppZxTgcg==}
+ '@tokenizer/inflate@0.4.1':
+ resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==}
+ engines: {node: '>=18'}
+
+ '@tokenizer/token@0.3.0':
+ resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
+
'@tootallnate/once@2.0.0':
resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
engines: {node: '>= 10'}
@@ -2078,6 +2139,9 @@ packages:
'@types/node@20.19.33':
resolution: {integrity: sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==}
+ '@types/node@22.19.15':
+ resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==}
+
'@types/node@24.10.12':
resolution: {integrity: sha512-68e+T28EbdmLSTkPgs3+UacC6rzmqrcWFPQs1C8mwJhI/r5Uxr0yEuQotczNRROd1gq30NGxee+fo0rSIxpyAw==}
@@ -2346,6 +2410,14 @@ packages:
abstract-logging@2.0.1:
resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==}
+ accepts@1.3.8:
+ resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
+ engines: {node: '>= 0.6'}
+
+ accepts@2.0.0:
+ resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
+ engines: {node: '>= 0.6'}
+
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
@@ -2570,6 +2642,9 @@ packages:
resolution: {integrity: sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==}
engines: {node: '>=4'}
+ axios@1.13.6:
+ resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==}
+
axobject-query@4.1.0:
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
engines: {node: '>= 0.4'}
@@ -2607,6 +2682,10 @@ packages:
bluebird@3.7.2:
resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==}
+ body-parser@2.2.2:
+ resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
+ engines: {node: '>=18'}
+
boolean@3.2.0:
resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
@@ -2669,6 +2748,12 @@ packages:
resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==}
engines: {node: '>=6'}
+ bundle-require@5.1.0:
+ resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ peerDependencies:
+ esbuild: '>=0.18'
+
bytes@3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'}
@@ -2805,6 +2890,10 @@ packages:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
+ cliui@9.0.1:
+ resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==}
+ engines: {node: '>=20'}
+
clone-response@1.0.3:
resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==}
@@ -2886,6 +2975,10 @@ packages:
config-file-ts@0.2.8-rc1:
resolution: {integrity: sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg==}
+ consola@3.4.2:
+ resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==}
+ engines: {node: ^14.18.0 || >=16.10.0}
+
console-control-strings@1.1.0:
resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
@@ -2893,19 +2986,39 @@ packages:
resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==}
engines: {node: '>=18'}
+ content-type@1.0.5:
+ resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
+ engines: {node: '>= 0.6'}
+
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+ cookie-signature@1.2.2:
+ resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
+ engines: {node: '>=6.6.0'}
+
+ cookie@0.7.2:
+ resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
+ engines: {node: '>= 0.6'}
+
cookie@1.1.1:
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
engines: {node: '>=18'}
+ cookies@0.9.1:
+ resolution: {integrity: sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==}
+ engines: {node: '>= 0.8'}
+
core-util-is@1.0.2:
resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
+ cors@2.8.6:
+ resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
+ engines: {node: '>= 0.10'}
+
cose-base@1.0.3:
resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==}
@@ -3148,6 +3261,9 @@ packages:
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
engines: {node: '>=6'}
+ deep-equal@1.0.1:
+ resolution: {integrity: sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==}
+
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
@@ -3176,6 +3292,10 @@ packages:
delegates@1.0.0:
resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
+ depd@1.1.2:
+ resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==}
+ engines: {node: '>= 0.6'}
+
depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
@@ -3184,6 +3304,10 @@ packages:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
+ destroy@1.2.0:
+ resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
+ engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
+
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
@@ -3251,6 +3375,9 @@ packages:
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
+ ee-first@1.1.1:
+ resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
+
ejs@3.1.10:
resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==}
engines: {node: '>=0.10.0'}
@@ -3301,6 +3428,10 @@ packages:
emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
+ encodeurl@2.0.0:
+ resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
+ engines: {node: '>= 0.8'}
+
encoding@0.1.13:
resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==}
@@ -3544,9 +3675,25 @@ packages:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
+ etag@1.8.1:
+ resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
+ engines: {node: '>= 0.6'}
+
eventemitter3@5.0.4:
resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
+ eventsource-parser@3.0.6:
+ resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==}
+ engines: {node: '>=18.0.0'}
+
+ eventsource@3.0.7:
+ resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==}
+ engines: {node: '>=18.0.0'}
+
+ execa@9.6.1:
+ resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==}
+ engines: {node: ^18.19.0 || >=20.5.0}
+
expect-type@1.3.0:
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
engines: {node: '>=12.0.0'}
@@ -3554,6 +3701,16 @@ packages:
exponential-backoff@3.1.3:
resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==}
+ express-rate-limit@8.3.0:
+ resolution: {integrity: sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==}
+ engines: {node: '>= 16'}
+ peerDependencies:
+ express: '>= 4.11'
+
+ express@5.2.1:
+ resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==}
+ engines: {node: '>= 18'}
+
extend@3.0.2:
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
@@ -3597,6 +3754,15 @@ packages:
fastify@5.7.4:
resolution: {integrity: sha512-e6l5NsRdaEP8rdD8VR0ErJASeyaRbzXYpmkrpr2SuvuMq6Si3lvsaVy5C+7gLanEkvjpMDzBXWE5HPeb/hgTxA==}
+ fastmcp@3.34.0:
+ resolution: {integrity: sha512-xKOXjU+MK7OZy91BY3FS5aenSiclJBCRMaZtXb3HYaKZVFbq4qYvAlFu6xYI3UU1NGLtv+h8izoStnOQ1By0BA==}
+ hasBin: true
+ peerDependencies:
+ jose: ^5.0.0
+ peerDependenciesMeta:
+ jose:
+ optional: true
+
fastq@1.20.1:
resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
@@ -3615,10 +3781,18 @@ packages:
picomatch:
optional: true
+ figures@6.1.0:
+ resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==}
+ engines: {node: '>=18'}
+
file-entry-cache@8.0.0:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
+ file-type@21.3.0:
+ resolution: {integrity: sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA==}
+ engines: {node: '>=20'}
+
filelist@1.0.4:
resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==}
@@ -3626,6 +3800,10 @@ packages:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
+ finalhandler@2.1.1:
+ resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==}
+ engines: {node: '>= 18.0.0'}
+
find-my-way@9.4.0:
resolution: {integrity: sha512-5Ye4vHsypZRYtS01ob/iwHzGRUDELlsoCftI/OZFhcLs1M0tkGPcXldE80TAZC5yYuJMBPJQQ43UHlqbJWiX2w==}
engines: {node: '>=20'}
@@ -3634,6 +3812,9 @@ packages:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'}
+ fix-dts-default-cjs-exports@1.0.1:
+ resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==}
+
flat-cache@4.0.1:
resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
engines: {node: '>=16'}
@@ -3641,6 +3822,15 @@ packages:
flatted@3.3.3:
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
+ follow-redirects@1.15.11:
+ resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
+ engines: {node: '>=4.0'}
+ peerDependencies:
+ debug: '*'
+ peerDependenciesMeta:
+ debug:
+ optional: true
+
for-each@0.3.5:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'}
@@ -3658,9 +3848,21 @@ packages:
engines: {node: '>=18.3.0'}
hasBin: true
+ forwarded@0.2.0:
+ resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
+ engines: {node: '>= 0.6'}
+
fraction.js@5.3.4:
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
+ fresh@0.5.2:
+ resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
+ engines: {node: '>= 0.6'}
+
+ fresh@2.0.0:
+ resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
+ engines: {node: '>= 0.8'}
+
fs-constants@1.0.0:
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
@@ -3709,6 +3911,10 @@ packages:
functions-have-names@1.2.3:
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
+ fuse.js@7.1.0:
+ resolution: {integrity: sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==}
+ engines: {node: '>=10'}
+
gauge@4.0.4:
resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==}
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
@@ -3746,6 +3952,10 @@ packages:
resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==}
engines: {node: '>=8'}
+ get-stream@9.0.1:
+ resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==}
+ engines: {node: '>=18'}
+
get-symbol-description@1.1.0:
resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
engines: {node: '>= 0.4'}
@@ -3885,6 +4095,10 @@ packages:
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
engines: {node: '>=12.0.0'}
+ hono@4.12.5:
+ resolution: {integrity: sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==}
+ engines: {node: '>=16.9.0'}
+
hosted-git-info@4.1.0:
resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==}
engines: {node: '>=10'}
@@ -3898,9 +4112,17 @@ packages:
html-void-elements@3.0.0:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
+ http-assert@1.5.0:
+ resolution: {integrity: sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==}
+ engines: {node: '>= 0.8'}
+
http-cache-semantics@4.2.0:
resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==}
+ http-errors@1.8.1:
+ resolution: {integrity: sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==}
+ engines: {node: '>= 0.6'}
+
http-errors@2.0.1:
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
engines: {node: '>= 0.8'}
@@ -3925,6 +4147,13 @@ packages:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'}
+ human-readable-ids@1.0.4:
+ resolution: {integrity: sha512-h1zwThTims8A/SpqFGWyTx+jG1+WRMJaEeZgbtPGrIpj2AZjsOgy8Y+iNzJ0yAyN669Q6F02EK66WMWcst+2FA==}
+
+ human-signals@8.0.1:
+ resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==}
+ engines: {node: '>=18.18.0'}
+
humanize-ms@1.2.1:
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
@@ -3942,6 +4171,10 @@ packages:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
+ iconv-lite@0.7.2:
+ resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
+ engines: {node: '>=0.10.0'}
+
idb-keyval@6.2.2:
resolution: {integrity: sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==}
@@ -3996,6 +4229,10 @@ packages:
resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==}
engines: {node: '>= 12'}
+ ipaddr.js@1.9.1:
+ resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
+ engines: {node: '>= 0.10'}
+
ipaddr.js@2.3.0:
resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==}
engines: {node: '>= 10'}
@@ -4106,6 +4343,9 @@ packages:
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
engines: {node: '>=12'}
+ is-promise@4.0.0:
+ resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
+
is-regex@1.2.1:
resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
engines: {node: '>= 0.4'}
@@ -4118,6 +4358,10 @@ packages:
resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==}
engines: {node: '>= 0.4'}
+ is-stream@4.0.1:
+ resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==}
+ engines: {node: '>=18'}
+
is-string@1.1.1:
resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==}
engines: {node: '>= 0.4'}
@@ -4134,6 +4378,10 @@ packages:
resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
engines: {node: '>=10'}
+ is-unicode-supported@2.1.0:
+ resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
+ engines: {node: '>=18'}
+
is-weakmap@2.0.2:
resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==}
engines: {node: '>= 0.4'}
@@ -4207,6 +4455,13 @@ packages:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
+ jose@6.2.0:
+ resolution: {integrity: sha512-xsfE1TcSCbUdo6U07tR0mvhg0flGxU8tPLbF03mirl2ukGQENhUg4ubGYQnhVH0b5stLlPM+WOqDkEl1R1y5sQ==}
+
+ joycon@3.1.1:
+ resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
+ engines: {node: '>=10'}
+
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -4234,6 +4489,9 @@ packages:
json-schema-traverse@1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
+ json-schema-typed@8.0.2:
+ resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==}
+
json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
@@ -4267,6 +4525,10 @@ packages:
resolution: {integrity: sha512-q3N5u+1sY9Bu7T4nlXoiRBXWfwSefNGoKeOwekV+gw0cAXQlz2Ww6BLcmBxVDeXBMUDQv6fK5bcNaJLxob3ZQA==}
hasBin: true
+ keygrip@1.1.0:
+ resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==}
+ engines: {node: '>= 0.6'}
+
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@@ -4281,6 +4543,21 @@ packages:
'@types/node': '>=18'
typescript: '>=5.0.4 <7'
+ knuth-shuffle@1.0.8:
+ resolution: {integrity: sha512-IdC4Hpp+mx53zTt6VAGsAtbGM0g4BV9fP8tTcviCosSwocHcRDw9uG5Rnv6wLWckF4r72qeXFoK9NkvV1gUJCQ==}
+
+ koa-compose@4.1.0:
+ resolution: {integrity: sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==}
+
+ koa-router@14.0.0:
+ resolution: {integrity: sha512-Ue8f/PRsLLNm6b7y+eS6xkqvsG2xH11d2VB1HPcfdfW6p5736kCHf2pXaq8q9XPQ01x0Dk7V/P5Il9pe+tGTxA==}
+ engines: {node: '>= 20'}
+ deprecated: 'Please use @koa/router instead, starting from v9! '
+
+ koa@3.1.2:
+ resolution: {integrity: sha512-2LOQnFKu3m0VxpE+5sb5+BRTSKrXmNxGgxVRiKwD9s5KQB1zID/FRXhtzeV7RT1L2GVpdEEAfVuclFOMGl1ikA==}
+ engines: {node: '>= 18'}
+
langium@4.2.1:
resolution: {integrity: sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ==}
engines: {node: '>=20.10.0', npm: '>=10.2.3'}
@@ -4328,6 +4605,10 @@ packages:
resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==}
engines: {node: '>=20.0.0'}
+ load-tsconfig@0.2.5:
+ resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+
locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
@@ -4445,6 +4726,10 @@ packages:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
+ mcp-proxy@6.4.1:
+ resolution: {integrity: sha512-vzKLpJEZRqkbxz9sBqYSOWo9mJq0aGYQQLJoaMe+egYF/EA1PvcKvLx9pA1/bRpV/hfguEuB67gcs0C5KJiROQ==}
+ hasBin: true
+
mdast-util-find-and-replace@3.0.2:
resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==}
@@ -4490,6 +4775,14 @@ packages:
mdast-util-to-string@4.0.0:
resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
+ media-typer@1.1.0:
+ resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
+ engines: {node: '>= 0.8'}
+
+ merge-descriptors@2.0.0:
+ resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==}
+ engines: {node: '>=18'}
+
merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
@@ -4589,10 +4882,18 @@ packages:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
+ mime-db@1.54.0:
+ resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
+ engines: {node: '>= 0.6'}
+
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
+ mime-types@3.0.2:
+ resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
+ engines: {node: '>=18'}
+
mime@2.6.0:
resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==}
engines: {node: '>=4.0.0'}
@@ -4723,6 +5024,10 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+ negotiator@0.6.3:
+ resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
+ engines: {node: '>= 0.6'}
+
negotiator@0.6.4:
resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==}
engines: {node: '>= 0.6'}
@@ -4789,6 +5094,10 @@ packages:
resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==}
engines: {node: '>=10'}
+ npm-run-path@6.0.0:
+ resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==}
+ engines: {node: '>=18'}
+
npmlog@6.0.2:
resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==}
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
@@ -4834,6 +5143,10 @@ packages:
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
engines: {node: '>=14.0.0'}
+ on-finished@2.4.1:
+ resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
+ engines: {node: '>= 0.8'}
+
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
@@ -4893,9 +5206,17 @@ packages:
parse-entities@4.0.2:
resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
+ parse-ms@4.0.0:
+ resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==}
+ engines: {node: '>=18'}
+
parse5@7.3.0:
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
+ parseurl@1.3.3:
+ resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
+ engines: {node: '>= 0.8'}
+
path-data-parser@0.1.0:
resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==}
@@ -4911,6 +5232,10 @@ packages:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
+ path-key@4.0.0:
+ resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
+ engines: {node: '>=12'}
+
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
@@ -4922,6 +5247,9 @@ packages:
resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==}
engines: {node: 20 || >=22}
+ path-to-regexp@8.3.0:
+ resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==}
+
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
@@ -4966,10 +5294,19 @@ packages:
resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==}
hasBin: true
+ pipenet@1.4.0:
+ resolution: {integrity: sha512-Uc3EH2i8hnJUD0Eupj9z2jaZPjjAbooaiHGh0iFdExbE8/BDt6Lf0919Dtwx5VM83elHNWFzCOsvzsViTD5YZg==}
+ engines: {node: '>=22.0.0'}
+ hasBin: true
+
pirates@4.0.7:
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
engines: {node: '>= 6'}
+ pkce-challenge@5.0.1:
+ resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==}
+ engines: {node: '>=16.20.0'}
+
pkg-types@1.3.1:
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
@@ -5102,6 +5439,10 @@ packages:
engines: {node: '>=14'}
hasBin: true
+ pretty-ms@9.3.0:
+ resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==}
+ engines: {node: '>=18'}
+
proc-log@5.0.0:
resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==}
engines: {node: ^18.17.0 || >=20.5.0}
@@ -5137,6 +5478,13 @@ packages:
property-information@7.1.0:
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
+ proxy-addr@2.0.7:
+ resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
+ engines: {node: '>= 0.10'}
+
+ proxy-from-env@1.1.0:
+ resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
+
pump@3.0.3:
resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
@@ -5144,6 +5492,10 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
+ qs@6.15.0:
+ resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==}
+ engines: {node: '>=0.6'}
+
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@@ -5154,6 +5506,14 @@ packages:
resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
engines: {node: '>=10'}
+ range-parser@1.2.1:
+ resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
+ engines: {node: '>= 0.6'}
+
+ raw-body@3.0.2:
+ resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==}
+ engines: {node: '>= 0.10'}
+
react-dom@18.3.1:
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
peerDependencies:
@@ -5296,6 +5656,10 @@ packages:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
+ resolve-from@5.0.0:
+ resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
+ engines: {node: '>=8'}
+
resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
@@ -5354,6 +5718,10 @@ packages:
roughjs@4.6.6:
resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==}
+ router@2.2.0:
+ resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
+ engines: {node: '>= 18'}
+
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
@@ -5425,10 +5793,18 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ send@1.2.1:
+ resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==}
+ engines: {node: '>= 18'}
+
serialize-error@7.0.1:
resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==}
engines: {node: '>=10'}
+ serve-static@2.2.1:
+ resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==}
+ engines: {node: '>= 18'}
+
set-blocking@2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
@@ -5533,6 +5909,10 @@ packages:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
+ source-map@0.7.6:
+ resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
+ engines: {node: '>= 12'}
+
space-separated-tokens@2.0.2:
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
@@ -5569,6 +5949,10 @@ packages:
resolution: {integrity: sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==}
engines: {node: '>= 6'}
+ statuses@1.5.0:
+ resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==}
+ engines: {node: '>= 0.6'}
+
statuses@2.0.2:
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
engines: {node: '>= 0.8'}
@@ -5580,6 +5964,9 @@ packages:
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
engines: {node: '>= 0.4'}
+ strict-event-emitter-types@2.0.0:
+ resolution: {integrity: sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==}
+
string-argv@0.3.2:
resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
engines: {node: '>=0.6.19'}
@@ -5644,6 +6031,10 @@ packages:
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
engines: {node: '>=4'}
+ strip-final-newline@4.0.0:
+ resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==}
+ engines: {node: '>=18'}
+
strip-json-comments@3.1.1:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
@@ -5655,6 +6046,10 @@ packages:
strip-literal@3.1.0:
resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==}
+ strtok3@10.3.4:
+ resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==}
+ engines: {node: '>=18'}
+
style-mod@4.1.3:
resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==}
@@ -5762,6 +6157,10 @@ packages:
resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
engines: {node: '>=14.0.0'}
+ tldjs@2.3.2:
+ resolution: {integrity: sha512-EORDwFMSZKrHPUVDhejCMDeAovRS5d8jZKiqALFiPp3cjKjEldPkxBY39ZSx3c45awz3RpKwJD1cCgGxEfy8/A==}
+ engines: {node: '>= 20'}
+
tmp-promise@3.0.3:
resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==}
@@ -5781,6 +6180,14 @@ packages:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
+ token-types@6.1.2:
+ resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==}
+ engines: {node: '>=14.16'}
+
+ tree-kill@1.2.2:
+ resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
+ hasBin: true
+
trim-lines@3.0.1:
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
@@ -5809,6 +6216,29 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+ tsscmp@1.0.6:
+ resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==}
+ engines: {node: '>=0.6.x'}
+
+ tsup@8.5.1:
+ resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==}
+ engines: {node: '>=18'}
+ hasBin: true
+ peerDependencies:
+ '@microsoft/api-extractor': ^7.36.0
+ '@swc/core': ^1
+ postcss: ^8.4.12
+ typescript: '>=4.5.0'
+ peerDependenciesMeta:
+ '@microsoft/api-extractor':
+ optional: true
+ '@swc/core':
+ optional: true
+ postcss:
+ optional: true
+ typescript:
+ optional: true
+
tsx@4.21.0:
resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
engines: {node: '>=18.0.0'}
@@ -5825,6 +6255,10 @@ packages:
resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==}
engines: {node: '>=10'}
+ type-is@2.0.1:
+ resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
+ engines: {node: '>= 0.6'}
+
typed-array-buffer@1.0.3:
resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
engines: {node: '>= 0.4'}
@@ -5861,6 +6295,10 @@ packages:
engines: {node: '>=0.8.0'}
hasBin: true
+ uint8array-extras@1.5.0:
+ resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==}
+ engines: {node: '>=18'}
+
unbox-primitive@1.1.0:
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
engines: {node: '>= 0.4'}
@@ -5874,6 +6312,14 @@ packages:
undici-types@7.16.0:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
+ undici@7.22.0:
+ resolution: {integrity: sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==}
+ engines: {node: '>=20.18.1'}
+
+ unicorn-magic@0.3.0:
+ resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
+ engines: {node: '>=18'}
+
unified@11.0.5:
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
@@ -5919,6 +6365,10 @@ packages:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'}
+ unpipe@1.0.0:
+ resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
+ engines: {node: '>= 0.8'}
+
unrs-resolver@1.11.1:
resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==}
@@ -5931,6 +6381,9 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+ uri-templates@0.2.0:
+ resolution: {integrity: sha512-EWkjYEN0L6KOfEoOH6Wj4ghQqU7eBZMJqRHQnxQAq+dSEzRPClkWjf8557HkWQXF6BrAUoLSAyy9i3RVTliaNg==}
+
use-callback-ref@1.3.3:
resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
engines: {node: '>=10'}
@@ -5966,6 +6419,10 @@ packages:
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
hasBin: true
+ vary@1.1.2:
+ resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
+ engines: {node: '>= 0.8'}
+
verror@1.10.1:
resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==}
engines: {node: '>=0.6.0'}
@@ -6140,6 +6597,29 @@ packages:
resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==}
engines: {node: '>=8.0'}
+ xsschema@0.4.3:
+ resolution: {integrity: sha512-8MFtONewz49S+R5ZfOGfJzEaPAmJ+Gw1ILjvuerYG0mOMVYe7OrsVQNlpiUTct4L3toAJAKD5NMwmCfmTIGYHQ==}
+ peerDependencies:
+ '@valibot/to-json-schema': ^1.0.0
+ arktype: ^2.1.20
+ effect: ^3.16.0
+ sury: ^10.0.0
+ zod: ^3.25.0 || ^4.0.0
+ zod-to-json-schema: ^3.25.0
+ peerDependenciesMeta:
+ '@valibot/to-json-schema':
+ optional: true
+ arktype:
+ optional: true
+ effect:
+ optional: true
+ sury:
+ optional: true
+ zod:
+ optional: true
+ zod-to-json-schema:
+ optional: true
+
y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
@@ -6163,10 +6643,18 @@ packages:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
+ yargs-parser@22.0.0:
+ resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=23}
+
yargs@17.7.2:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
engines: {node: '>=12'}
+ yargs@18.0.0:
+ resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=23}
+
yauzl@2.10.0:
resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}
@@ -6188,10 +6676,19 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
+ yoctocolors@2.1.2:
+ resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==}
+ engines: {node: '>=18'}
+
zip-stream@4.1.1:
resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==}
engines: {node: '>= 10'}
+ zod-to-json-schema@3.25.1:
+ resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==}
+ peerDependencies:
+ zod: ^3.25 || ^4
+
zod-validation-error@4.0.2:
resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
engines: {node: '>=18.0.0'}
@@ -6354,6 +6851,8 @@ snapshots:
'@bcoe/v8-coverage@1.0.2': {}
+ '@borewit/text-codec@0.2.1': {}
+
'@boundaries/elements@1.1.2(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@1.21.7))':
dependencies:
eslint-import-resolver-node: 0.3.9
@@ -7081,6 +7580,10 @@ snapshots:
'@gar/promisify@1.1.3': {}
+ '@hono/node-server@1.19.11(hono@4.12.5)':
+ dependencies:
+ hono: 4.12.5
+
'@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.7':
@@ -7272,6 +7775,28 @@ snapshots:
dependencies:
langium: 4.2.1
+ '@modelcontextprotocol/sdk@1.27.1(zod@4.3.6)':
+ dependencies:
+ '@hono/node-server': 1.19.11(hono@4.12.5)
+ ajv: 8.18.0
+ ajv-formats: 3.0.1(ajv@8.18.0)
+ content-type: 1.0.5
+ cors: 2.8.6
+ cross-spawn: 7.0.6
+ eventsource: 3.0.7
+ eventsource-parser: 3.0.6
+ express: 5.2.1
+ express-rate-limit: 8.3.0(express@5.2.1)
+ hono: 4.12.5
+ jose: 6.2.0
+ json-schema-typed: 8.0.2
+ pkce-challenge: 5.0.1
+ raw-body: 3.0.2
+ zod: 4.3.6
+ zod-to-json-schema: 3.25.1(zod@4.3.6)
+ transitivePeerDependencies:
+ - supports-color
+
'@napi-rs/wasm-runtime@0.2.12':
dependencies:
'@emnapi/core': 1.8.1
@@ -7891,8 +8416,14 @@ snapshots:
'@rtsao/scc@1.1.0': {}
+ '@sec-ant/readable-stream@0.4.1': {}
+
'@sindresorhus/is@4.6.0': {}
+ '@sindresorhus/merge-streams@4.0.0': {}
+
+ '@standard-schema/spec@1.1.0': {}
+
'@szmarczak/http-timer@4.0.6':
dependencies:
defer-to-connect: 2.0.1
@@ -7910,6 +8441,15 @@ snapshots:
'@tanstack/virtual-core@3.13.18': {}
+ '@tokenizer/inflate@0.4.1':
+ dependencies:
+ debug: 4.4.3
+ token-types: 6.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ '@tokenizer/token@0.3.0': {}
+
'@tootallnate/once@2.0.0': {}
'@tybys/wasm-util@0.10.1':
@@ -8113,6 +8653,10 @@ snapshots:
dependencies:
undici-types: 6.21.0
+ '@types/node@22.19.15':
+ dependencies:
+ undici-types: 6.21.0
+
'@types/node@24.10.12':
dependencies:
undici-types: 7.16.0
@@ -8402,6 +8946,16 @@ snapshots:
abstract-logging@2.0.1: {}
+ accepts@1.3.8:
+ dependencies:
+ mime-types: 2.1.35
+ negotiator: 0.6.3
+
+ accepts@2.0.0:
+ dependencies:
+ mime-types: 3.0.2
+ negotiator: 1.0.0
+
acorn-jsx@5.3.2(acorn@8.15.0):
dependencies:
acorn: 8.15.0
@@ -8720,6 +9274,14 @@ snapshots:
axe-core@4.11.1: {}
+ axios@1.13.6(debug@4.4.3):
+ dependencies:
+ follow-redirects: 1.15.11(debug@4.4.3)
+ form-data: 4.0.5
+ proxy-from-env: 1.1.0
+ transitivePeerDependencies:
+ - debug
+
axobject-query@4.1.0: {}
bail@2.0.2: {}
@@ -8750,6 +9312,20 @@ snapshots:
bluebird@3.7.2: {}
+ body-parser@2.2.2:
+ dependencies:
+ bytes: 3.1.2
+ content-type: 1.0.5
+ debug: 4.4.3
+ http-errors: 2.0.1
+ iconv-lite: 0.7.2
+ on-finished: 2.4.1
+ qs: 6.15.0
+ raw-body: 3.0.2
+ type-is: 2.0.1
+ transitivePeerDependencies:
+ - supports-color
+
boolean@3.2.0:
optional: true
@@ -8857,6 +9433,11 @@ snapshots:
builtin-modules@3.3.0: {}
+ bundle-require@5.1.0(esbuild@0.27.2):
+ dependencies:
+ esbuild: 0.27.2
+ load-tsconfig: 0.2.5
+
bytes@3.1.2: {}
cac@6.7.14: {}
@@ -9030,6 +9611,12 @@ snapshots:
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
+ cliui@9.0.1:
+ dependencies:
+ string-width: 7.2.0
+ strip-ansi: 7.1.2
+ wrap-ansi: 9.0.2
+
clone-response@1.0.3:
dependencies:
mimic-response: 1.0.1
@@ -9102,19 +9689,37 @@ snapshots:
glob: 10.5.0
typescript: 5.9.3
+ consola@3.4.2: {}
+
console-control-strings@1.1.0: {}
content-disposition@1.0.1: {}
+ content-type@1.0.5: {}
+
convert-source-map@2.0.0: {}
+ cookie-signature@1.2.2: {}
+
+ cookie@0.7.2: {}
+
cookie@1.1.1: {}
+ cookies@0.9.1:
+ dependencies:
+ depd: 2.0.0
+ keygrip: 1.1.0
+
core-util-is@1.0.2:
optional: true
core-util-is@1.0.3: {}
+ cors@2.8.6:
+ dependencies:
+ object-assign: 4.1.1
+ vary: 1.1.2
+
cose-base@1.0.3:
dependencies:
layout-base: 1.0.2
@@ -9379,6 +9984,8 @@ snapshots:
deep-eql@5.0.2: {}
+ deep-equal@1.0.1: {}
+
deep-is@0.1.4: {}
defaults@1.0.4:
@@ -9407,10 +10014,14 @@ snapshots:
delegates@1.0.0: {}
+ depd@1.1.2: {}
+
depd@2.0.0: {}
dequal@2.0.3: {}
+ destroy@1.2.0: {}
+
detect-libc@2.1.2: {}
detect-node-es@1.1.0: {}
@@ -9491,6 +10102,8 @@ snapshots:
eastasianwidth@0.2.0: {}
+ ee-first@1.1.1: {}
+
ejs@3.1.10:
dependencies:
jake: 10.9.4
@@ -9587,6 +10200,8 @@ snapshots:
emoji-regex@9.2.2: {}
+ encodeurl@2.0.0: {}
+
encoding@0.1.13:
dependencies:
iconv-lite: 0.6.3
@@ -10019,12 +10634,73 @@ snapshots:
esutils@2.0.3: {}
+ etag@1.8.1: {}
+
eventemitter3@5.0.4: {}
+ eventsource-parser@3.0.6: {}
+
+ eventsource@3.0.7:
+ dependencies:
+ eventsource-parser: 3.0.6
+
+ execa@9.6.1:
+ dependencies:
+ '@sindresorhus/merge-streams': 4.0.0
+ cross-spawn: 7.0.6
+ figures: 6.1.0
+ get-stream: 9.0.1
+ human-signals: 8.0.1
+ is-plain-obj: 4.1.0
+ is-stream: 4.0.1
+ npm-run-path: 6.0.0
+ pretty-ms: 9.3.0
+ signal-exit: 4.1.0
+ strip-final-newline: 4.0.0
+ yoctocolors: 2.1.2
+
expect-type@1.3.0: {}
exponential-backoff@3.1.3: {}
+ express-rate-limit@8.3.0(express@5.2.1):
+ dependencies:
+ express: 5.2.1
+ ip-address: 10.1.0
+
+ express@5.2.1:
+ dependencies:
+ accepts: 2.0.0
+ body-parser: 2.2.2
+ content-disposition: 1.0.1
+ content-type: 1.0.5
+ cookie: 0.7.2
+ cookie-signature: 1.2.2
+ debug: 4.4.3
+ depd: 2.0.0
+ encodeurl: 2.0.0
+ escape-html: 1.0.3
+ etag: 1.8.1
+ finalhandler: 2.1.1
+ fresh: 2.0.0
+ http-errors: 2.0.1
+ merge-descriptors: 2.0.0
+ mime-types: 3.0.2
+ on-finished: 2.4.1
+ once: 1.4.0
+ parseurl: 1.3.3
+ proxy-addr: 2.0.7
+ qs: 6.15.0
+ range-parser: 1.2.1
+ router: 2.2.0
+ send: 1.2.1
+ serve-static: 2.2.1
+ statuses: 2.0.2
+ type-is: 2.0.1
+ vary: 1.1.2
+ transitivePeerDependencies:
+ - supports-color
+
extend@3.0.2: {}
extract-zip@2.0.1:
@@ -10091,6 +10767,30 @@ snapshots:
semver: 7.7.3
toad-cache: 3.7.0
+ fastmcp@3.34.0:
+ dependencies:
+ '@modelcontextprotocol/sdk': 1.27.1(zod@4.3.6)
+ '@standard-schema/spec': 1.1.0
+ execa: 9.6.1
+ file-type: 21.3.0
+ fuse.js: 7.1.0
+ hono: 4.12.5
+ mcp-proxy: 6.4.1
+ strict-event-emitter-types: 2.0.0
+ undici: 7.22.0
+ uri-templates: 0.2.0
+ xsschema: 0.4.3(zod-to-json-schema@3.25.1(zod@4.3.6))(zod@4.3.6)
+ yargs: 18.0.0
+ zod: 4.3.6
+ zod-to-json-schema: 3.25.1(zod@4.3.6)
+ transitivePeerDependencies:
+ - '@cfworker/json-schema'
+ - '@valibot/to-json-schema'
+ - arktype
+ - effect
+ - supports-color
+ - sury
+
fastq@1.20.1:
dependencies:
reusify: 1.1.0
@@ -10107,10 +10807,23 @@ snapshots:
optionalDependencies:
picomatch: 4.0.3
+ figures@6.1.0:
+ dependencies:
+ is-unicode-supported: 2.1.0
+
file-entry-cache@8.0.0:
dependencies:
flat-cache: 4.0.1
+ file-type@21.3.0:
+ dependencies:
+ '@tokenizer/inflate': 0.4.1
+ strtok3: 10.3.4
+ token-types: 6.1.2
+ uint8array-extras: 1.5.0
+ transitivePeerDependencies:
+ - supports-color
+
filelist@1.0.4:
dependencies:
minimatch: 5.1.7
@@ -10119,6 +10832,17 @@ snapshots:
dependencies:
to-regex-range: 5.0.1
+ finalhandler@2.1.1:
+ dependencies:
+ debug: 4.4.3
+ encodeurl: 2.0.0
+ escape-html: 1.0.3
+ on-finished: 2.4.1
+ parseurl: 1.3.3
+ statuses: 2.0.2
+ transitivePeerDependencies:
+ - supports-color
+
find-my-way@9.4.0:
dependencies:
fast-deep-equal: 3.1.3
@@ -10130,6 +10854,12 @@ snapshots:
locate-path: 6.0.0
path-exists: 4.0.0
+ fix-dts-default-cjs-exports@1.0.1:
+ dependencies:
+ magic-string: 0.30.21
+ mlly: 1.8.0
+ rollup: 4.55.1
+
flat-cache@4.0.1:
dependencies:
flatted: 3.3.3
@@ -10137,6 +10867,10 @@ snapshots:
flatted@3.3.3: {}
+ follow-redirects@1.15.11(debug@4.4.3):
+ optionalDependencies:
+ debug: 4.4.3
+
for-each@0.3.5:
dependencies:
is-callable: 1.2.7
@@ -10158,8 +10892,14 @@ snapshots:
dependencies:
fd-package-json: 2.0.0
+ forwarded@0.2.0: {}
+
fraction.js@5.3.4: {}
+ fresh@0.5.2: {}
+
+ fresh@2.0.0: {}
+
fs-constants@1.0.0: {}
fs-extra@10.1.0:
@@ -10215,6 +10955,8 @@ snapshots:
functions-have-names@1.2.3: {}
+ fuse.js@7.1.0: {}
+
gauge@4.0.4:
dependencies:
aproba: 2.1.0
@@ -10258,6 +11000,11 @@ snapshots:
dependencies:
pump: 3.0.3
+ get-stream@9.0.1:
+ dependencies:
+ '@sec-ant/readable-stream': 0.4.1
+ is-stream: 4.0.1
+
get-symbol-description@1.1.0:
dependencies:
call-bound: 1.0.4
@@ -10478,6 +11225,8 @@ snapshots:
highlight.js@11.11.1: {}
+ hono@4.12.5: {}
+
hosted-git-info@4.1.0:
dependencies:
lru-cache: 6.0.0
@@ -10488,8 +11237,21 @@ snapshots:
html-void-elements@3.0.0: {}
+ http-assert@1.5.0:
+ dependencies:
+ deep-equal: 1.0.1
+ http-errors: 1.8.1
+
http-cache-semantics@4.2.0: {}
+ http-errors@1.8.1:
+ dependencies:
+ depd: 1.1.2
+ inherits: 2.0.4
+ setprototypeof: 1.2.0
+ statuses: 1.5.0
+ toidentifier: 1.0.1
+
http-errors@2.0.1:
dependencies:
depd: 2.0.0
@@ -10532,6 +11294,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ human-readable-ids@1.0.4:
+ dependencies:
+ knuth-shuffle: 1.0.8
+
+ human-signals@8.0.1: {}
+
humanize-ms@1.2.1:
dependencies:
ms: 2.1.3
@@ -10548,6 +11316,10 @@ snapshots:
dependencies:
safer-buffer: 2.1.2
+ iconv-lite@0.7.2:
+ dependencies:
+ safer-buffer: 2.1.2
+
idb-keyval@6.2.2: {}
ieee754@1.2.1: {}
@@ -10588,6 +11360,8 @@ snapshots:
ip-address@10.1.0: {}
+ ipaddr.js@1.9.1: {}
+
ipaddr.js@2.3.0: {}
is-alphabetical@2.0.1: {}
@@ -10694,6 +11468,8 @@ snapshots:
is-plain-obj@4.1.0: {}
+ is-promise@4.0.0: {}
+
is-regex@1.2.1:
dependencies:
call-bound: 1.0.4
@@ -10707,6 +11483,8 @@ snapshots:
dependencies:
call-bound: 1.0.4
+ is-stream@4.0.1: {}
+
is-string@1.1.1:
dependencies:
call-bound: 1.0.4
@@ -10724,6 +11502,8 @@ snapshots:
is-unicode-supported@0.1.0: {}
+ is-unicode-supported@2.1.0: {}
+
is-weakmap@2.0.2: {}
is-weakref@1.1.1:
@@ -10795,6 +11575,10 @@ snapshots:
jiti@2.6.1: {}
+ jose@6.2.0: {}
+
+ joycon@3.1.1: {}
+
js-tokens@4.0.0: {}
js-tokens@9.0.1: {}
@@ -10815,6 +11599,8 @@ snapshots:
json-schema-traverse@1.0.0: {}
+ json-schema-typed@8.0.2: {}
+
json-stable-stringify-without-jsonify@1.0.1: {}
json-stringify-safe@5.0.1:
@@ -10849,6 +11635,10 @@ snapshots:
dependencies:
commander: 8.3.0
+ keygrip@1.1.0:
+ dependencies:
+ tsscmp: 1.0.6
+
keyv@4.5.4:
dependencies:
json-buffer: 3.0.1
@@ -10872,6 +11662,40 @@ snapshots:
typescript: 5.9.3
zod: 4.3.6
+ knuth-shuffle@1.0.8: {}
+
+ koa-compose@4.1.0: {}
+
+ koa-router@14.0.0:
+ dependencies:
+ debug: 4.4.3
+ http-errors: 2.0.1
+ koa-compose: 4.1.0
+ path-to-regexp: 8.3.0
+ transitivePeerDependencies:
+ - supports-color
+
+ koa@3.1.2:
+ dependencies:
+ accepts: 1.3.8
+ content-disposition: 1.0.1
+ content-type: 1.0.5
+ cookies: 0.9.1
+ delegates: 1.0.0
+ destroy: 1.2.0
+ encodeurl: 2.0.0
+ escape-html: 1.0.3
+ fresh: 0.5.2
+ http-assert: 1.5.0
+ http-errors: 2.0.1
+ koa-compose: 4.1.0
+ mime-types: 3.0.2
+ on-finished: 2.4.1
+ parseurl: 1.3.3
+ statuses: 2.0.2
+ type-is: 2.0.1
+ vary: 1.1.2
+
langium@4.2.1:
dependencies:
chevrotain: 11.1.2
@@ -10930,6 +11754,8 @@ snapshots:
rfdc: 1.4.1
wrap-ansi: 9.0.2
+ load-tsconfig@0.2.5: {}
+
locate-path@6.0.0:
dependencies:
p-locate: 5.0.0
@@ -11064,6 +11890,12 @@ snapshots:
math-intrinsics@1.1.0: {}
+ mcp-proxy@6.4.1:
+ dependencies:
+ pipenet: 1.4.0
+ transitivePeerDependencies:
+ - supports-color
+
mdast-util-find-and-replace@3.0.2:
dependencies:
'@types/mdast': 4.0.4
@@ -11217,6 +12049,10 @@ snapshots:
dependencies:
'@types/mdast': 4.0.4
+ media-typer@1.1.0: {}
+
+ merge-descriptors@2.0.0: {}
+
merge2@1.4.1: {}
mermaid@11.12.3:
@@ -11440,10 +12276,16 @@ snapshots:
mime-db@1.52.0: {}
+ mime-db@1.54.0: {}
+
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
+ mime-types@3.0.2:
+ dependencies:
+ mime-db: 1.54.0
+
mime@2.6.0: {}
mime@3.0.0: {}
@@ -11559,6 +12401,8 @@ snapshots:
natural-compare@1.4.0: {}
+ negotiator@0.6.3: {}
+
negotiator@0.6.4: {}
negotiator@1.0.0: {}
@@ -11634,6 +12478,11 @@ snapshots:
normalize-url@6.1.0: {}
+ npm-run-path@6.0.0:
+ dependencies:
+ path-key: 4.0.0
+ unicorn-magic: 0.3.0
+
npmlog@6.0.2:
dependencies:
are-we-there-yet: 3.0.1
@@ -11687,6 +12536,10 @@ snapshots:
on-exit-leak-free@2.1.2: {}
+ on-finished@2.4.1:
+ dependencies:
+ ee-first: 1.1.1
+
once@1.4.0:
dependencies:
wrappy: 1.0.2
@@ -11783,10 +12636,14 @@ snapshots:
is-decimal: 2.0.1
is-hexadecimal: 2.0.1
+ parse-ms@4.0.0: {}
+
parse5@7.3.0:
dependencies:
entities: 6.0.1
+ parseurl@1.3.3: {}
+
path-data-parser@0.1.0: {}
path-exists@4.0.0: {}
@@ -11795,6 +12652,8 @@ snapshots:
path-key@3.1.1: {}
+ path-key@4.0.0: {}
+
path-parse@1.0.7: {}
path-scurry@1.11.1:
@@ -11807,6 +12666,8 @@ snapshots:
lru-cache: 11.2.6
minipass: 7.1.2
+ path-to-regexp@8.3.0: {}
+
pathe@2.0.3: {}
pathval@2.0.1: {}
@@ -11845,8 +12706,23 @@ snapshots:
sonic-boom: 4.2.1
thread-stream: 4.0.0
+ pipenet@1.4.0:
+ dependencies:
+ axios: 1.13.6(debug@4.4.3)
+ debug: 4.4.3
+ human-readable-ids: 1.0.4
+ koa: 3.1.2
+ koa-router: 14.0.0
+ pump: 3.0.3
+ tldjs: 2.3.2
+ yargs: 18.0.0
+ transitivePeerDependencies:
+ - supports-color
+
pirates@4.0.7: {}
+ pkce-challenge@5.0.1: {}
+
pkg-types@1.3.1:
dependencies:
confbox: 0.1.8
@@ -11889,6 +12765,15 @@ snapshots:
tsx: 4.21.0
yaml: 2.8.2
+ postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2):
+ dependencies:
+ lilconfig: 3.1.3
+ optionalDependencies:
+ jiti: 2.6.1
+ postcss: 8.5.6
+ tsx: 4.21.0
+ yaml: 2.8.2
+
postcss-nested@6.2.0(postcss@8.5.6):
dependencies:
postcss: 8.5.6
@@ -11920,6 +12805,10 @@ snapshots:
prettier@3.8.1: {}
+ pretty-ms@9.3.0:
+ dependencies:
+ parse-ms: 4.0.0
+
proc-log@5.0.0: {}
process-nextick-args@2.0.1: {}
@@ -11945,6 +12834,13 @@ snapshots:
property-information@7.1.0: {}
+ proxy-addr@2.0.7:
+ dependencies:
+ forwarded: 0.2.0
+ ipaddr.js: 1.9.1
+
+ proxy-from-env@1.1.0: {}
+
pump@3.0.3:
dependencies:
end-of-stream: 1.4.5
@@ -11952,12 +12848,25 @@ snapshots:
punycode@2.3.1: {}
+ qs@6.15.0:
+ dependencies:
+ side-channel: 1.1.0
+
queue-microtask@1.2.3: {}
quick-format-unescaped@4.0.4: {}
quick-lru@5.1.1: {}
+ range-parser@1.2.1: {}
+
+ raw-body@3.0.2:
+ dependencies:
+ bytes: 3.1.2
+ http-errors: 2.0.1
+ iconv-lite: 0.7.2
+ unpipe: 1.0.0
+
react-dom@18.3.1(react@18.3.1):
dependencies:
loose-envify: 1.4.0
@@ -12155,6 +13064,8 @@ snapshots:
resolve-from@4.0.0: {}
+ resolve-from@5.0.0: {}
+
resolve-pkg-maps@1.0.0: {}
resolve@1.22.11:
@@ -12245,6 +13156,16 @@ snapshots:
points-on-curve: 0.2.0
points-on-path: 0.2.1
+ router@2.2.0:
+ dependencies:
+ debug: 4.4.3
+ depd: 2.0.0
+ is-promise: 4.0.0
+ parseurl: 1.3.3
+ path-to-regexp: 8.3.0
+ transitivePeerDependencies:
+ - supports-color
+
run-parallel@1.2.0:
dependencies:
queue-microtask: 1.2.3
@@ -12313,11 +13234,36 @@ snapshots:
semver@7.7.4: {}
+ send@1.2.1:
+ dependencies:
+ debug: 4.4.3
+ encodeurl: 2.0.0
+ escape-html: 1.0.3
+ etag: 1.8.1
+ fresh: 2.0.0
+ http-errors: 2.0.1
+ mime-types: 3.0.2
+ ms: 2.1.3
+ on-finished: 2.4.1
+ range-parser: 1.2.1
+ statuses: 2.0.2
+ transitivePeerDependencies:
+ - supports-color
+
serialize-error@7.0.1:
dependencies:
type-fest: 0.13.1
optional: true
+ serve-static@2.2.1:
+ dependencies:
+ encodeurl: 2.0.0
+ escape-html: 1.0.3
+ parseurl: 1.3.3
+ send: 1.2.1
+ transitivePeerDependencies:
+ - supports-color
+
set-blocking@2.0.0: {}
set-cookie-parser@2.7.2: {}
@@ -12448,6 +13394,8 @@ snapshots:
source-map@0.6.1: {}
+ source-map@0.7.6: {}
+
space-separated-tokens@2.0.2: {}
split2@4.2.0: {}
@@ -12479,6 +13427,8 @@ snapshots:
stat-mode@1.0.0: {}
+ statuses@1.5.0: {}
+
statuses@2.0.2: {}
std-env@3.10.0: {}
@@ -12488,6 +13438,8 @@ snapshots:
es-errors: 1.3.0
internal-slot: 1.1.0
+ strict-event-emitter-types@2.0.0: {}
+
string-argv@0.3.2: {}
string-width@4.2.3:
@@ -12586,6 +13538,8 @@ snapshots:
strip-bom@3.0.0: {}
+ strip-final-newline@4.0.0: {}
+
strip-json-comments@3.1.1: {}
strip-json-comments@5.0.3: {}
@@ -12594,6 +13548,10 @@ snapshots:
dependencies:
js-tokens: 9.0.1
+ strtok3@10.3.4:
+ dependencies:
+ '@tokenizer/token': 0.3.0
+
style-mod@4.1.3: {}
style-to-js@1.1.21:
@@ -12737,6 +13695,10 @@ snapshots:
tinyspy@4.0.4: {}
+ tldjs@2.3.2:
+ dependencies:
+ punycode: 2.3.1
+
tmp-promise@3.0.3:
dependencies:
tmp: 0.2.5
@@ -12751,6 +13713,14 @@ snapshots:
toidentifier@1.0.1: {}
+ token-types@6.1.2:
+ dependencies:
+ '@borewit/text-codec': 0.2.1
+ '@tokenizer/token': 0.3.0
+ ieee754: 1.2.1
+
+ tree-kill@1.2.2: {}
+
trim-lines@3.0.1: {}
trough@2.2.0: {}
@@ -12776,6 +13746,36 @@ snapshots:
tslib@2.8.1: {}
+ tsscmp@1.0.6: {}
+
+ tsup@8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2):
+ dependencies:
+ bundle-require: 5.1.0(esbuild@0.27.2)
+ cac: 6.7.14
+ chokidar: 4.0.3
+ consola: 3.4.2
+ debug: 4.4.3
+ esbuild: 0.27.2
+ fix-dts-default-cjs-exports: 1.0.1
+ joycon: 3.1.1
+ picocolors: 1.1.1
+ postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2)
+ resolve-from: 5.0.0
+ rollup: 4.55.1
+ source-map: 0.7.6
+ sucrase: 3.35.1
+ tinyexec: 0.3.2
+ tinyglobby: 0.2.15
+ tree-kill: 1.2.2
+ optionalDependencies:
+ postcss: 8.5.6
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - jiti
+ - supports-color
+ - tsx
+ - yaml
+
tsx@4.21.0:
dependencies:
esbuild: 0.27.2
@@ -12792,6 +13792,12 @@ snapshots:
type-fest@0.13.1:
optional: true
+ type-is@2.0.1:
+ dependencies:
+ content-type: 1.0.5
+ media-typer: 1.1.0
+ mime-types: 3.0.2
+
typed-array-buffer@1.0.3:
dependencies:
call-bound: 1.0.4
@@ -12843,6 +13849,8 @@ snapshots:
uglify-js@3.19.3:
optional: true
+ uint8array-extras@1.5.0: {}
+
unbox-primitive@1.1.0:
dependencies:
call-bound: 1.0.4
@@ -12856,6 +13864,10 @@ snapshots:
undici-types@7.16.0: {}
+ undici@7.22.0: {}
+
+ unicorn-magic@0.3.0: {}
+
unified@11.0.5:
dependencies:
'@types/unist': 3.0.3
@@ -12914,6 +13926,8 @@ snapshots:
universalify@2.0.1: {}
+ unpipe@1.0.0: {}
+
unrs-resolver@1.11.1:
dependencies:
napi-postinstall: 0.3.4
@@ -12948,6 +13962,8 @@ snapshots:
dependencies:
punycode: 2.3.1
+ uri-templates@0.2.0: {}
+
use-callback-ref@1.3.3(@types/react@18.3.27)(react@18.3.1):
dependencies:
react: 18.3.1
@@ -12973,6 +13989,8 @@ snapshots:
uuid@11.1.0: {}
+ vary@1.1.2: {}
+
verror@1.10.1:
dependencies:
assert-plus: 1.0.0
@@ -12995,6 +14013,24 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
+ vite-node@3.2.4(@types/node@22.19.15)(terser@5.46.0):
+ dependencies:
+ cac: 6.7.14
+ debug: 4.4.3
+ es-module-lexer: 1.7.0
+ pathe: 2.0.3
+ vite: 5.4.21(@types/node@22.19.15)(terser@5.46.0)
+ transitivePeerDependencies:
+ - '@types/node'
+ - less
+ - lightningcss
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+
vite-node@3.2.4(@types/node@25.0.7)(terser@5.46.0):
dependencies:
cac: 6.7.14
@@ -13013,6 +14049,16 @@ snapshots:
- supports-color
- terser
+ vite@5.4.21(@types/node@22.19.15)(terser@5.46.0):
+ dependencies:
+ esbuild: 0.21.5
+ postcss: 8.5.6
+ rollup: 4.55.1
+ optionalDependencies:
+ '@types/node': 22.19.15
+ fsevents: 2.3.3
+ terser: 5.46.0
+
vite@5.4.21(@types/node@25.0.7)(terser@5.46.0):
dependencies:
esbuild: 0.21.5
@@ -13023,6 +14069,46 @@ snapshots:
fsevents: 2.3.3
terser: 5.46.0
+ vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.15)(happy-dom@20.0.2)(terser@5.46.0):
+ dependencies:
+ '@types/chai': 5.2.3
+ '@vitest/expect': 3.2.4
+ '@vitest/mocker': 3.2.4(vite@5.4.21(@types/node@25.0.7)(terser@5.46.0))
+ '@vitest/pretty-format': 3.2.4
+ '@vitest/runner': 3.2.4
+ '@vitest/snapshot': 3.2.4
+ '@vitest/spy': 3.2.4
+ '@vitest/utils': 3.2.4
+ chai: 5.3.3
+ debug: 4.4.3
+ expect-type: 1.3.0
+ magic-string: 0.30.21
+ pathe: 2.0.3
+ picomatch: 4.0.3
+ std-env: 3.10.0
+ tinybench: 2.9.0
+ tinyexec: 0.3.2
+ tinyglobby: 0.2.15
+ tinypool: 1.1.1
+ tinyrainbow: 2.0.0
+ vite: 5.4.21(@types/node@22.19.15)(terser@5.46.0)
+ vite-node: 3.2.4(@types/node@22.19.15)(terser@5.46.0)
+ why-is-node-running: 2.3.0
+ optionalDependencies:
+ '@types/debug': 4.1.12
+ '@types/node': 22.19.15
+ happy-dom: 20.0.2
+ transitivePeerDependencies:
+ - less
+ - lightningcss
+ - msw
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+
vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@20.0.2)(terser@5.46.0):
dependencies:
'@types/chai': 5.2.3
@@ -13176,6 +14262,11 @@ snapshots:
xmlbuilder@15.1.1: {}
+ xsschema@0.4.3(zod-to-json-schema@3.25.1(zod@4.3.6))(zod@4.3.6):
+ optionalDependencies:
+ zod: 4.3.6
+ zod-to-json-schema: 3.25.1(zod@4.3.6)
+
y18n@5.0.8: {}
yallist@3.1.1: {}
@@ -13188,6 +14279,8 @@ snapshots:
yargs-parser@21.1.1: {}
+ yargs-parser@22.0.0: {}
+
yargs@17.7.2:
dependencies:
cliui: 8.0.1
@@ -13198,6 +14291,15 @@ snapshots:
y18n: 5.0.8
yargs-parser: 21.1.1
+ yargs@18.0.0:
+ dependencies:
+ cliui: 9.0.1
+ escalade: 3.2.0
+ get-caller-file: 2.0.5
+ string-width: 7.2.0
+ y18n: 5.0.8
+ yargs-parser: 22.0.0
+
yauzl@2.10.0:
dependencies:
buffer-crc32: 0.2.13
@@ -13213,12 +14315,18 @@ snapshots:
yocto-queue@0.1.0: {}
+ yoctocolors@2.1.2: {}
+
zip-stream@4.1.1:
dependencies:
archiver-utils: 3.0.4
compress-commons: 4.1.2
readable-stream: 3.6.2
+ zod-to-json-schema@3.25.1(zod@4.3.6):
+ dependencies:
+ zod: 4.3.6
+
zod-validation-error@4.0.2(zod@4.3.6):
dependencies:
zod: 4.3.6
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index c5739b74..0caafff1 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,2 +1,4 @@
+packages:
+ - mcp-server
ignoredBuiltDependencies:
- esbuild
diff --git a/src/main/ipc/sessions.ts b/src/main/ipc/sessions.ts
index e99caa7c..cea9d76e 100644
--- a/src/main/ipc/sessions.ts
+++ b/src/main/ipc/sessions.ts
@@ -198,7 +198,8 @@ async function handleGetSessionsByIds(
async function handleGetSessionDetail(
_event: IpcMainInvokeEvent,
projectId: string,
- sessionId: string
+ sessionId: string,
+ options?: { bypassCache?: boolean }
): Promise {
try {
const validatedProject = validateProjectId(projectId);
@@ -220,7 +221,7 @@ async function handleGetSessionDetail(
// Check cache first
let sessionDetail = dataCache.get(cacheKey);
- if (sessionDetail) {
+ if (sessionDetail && !options?.bypassCache) {
return sessionDetail;
}
diff --git a/src/main/ipc/subagents.ts b/src/main/ipc/subagents.ts
index 747605d3..9dc10d52 100644
--- a/src/main/ipc/subagents.ts
+++ b/src/main/ipc/subagents.ts
@@ -56,7 +56,8 @@ async function handleGetSubagentDetail(
_event: IpcMainInvokeEvent,
projectId: string,
sessionId: string,
- subagentId: string
+ subagentId: string,
+ options?: { bypassCache?: boolean }
): Promise {
try {
const validatedProject = validateProjectId(projectId);
@@ -85,7 +86,7 @@ async function handleGetSubagentDetail(
// Check cache first
let subagentDetail = dataCache.getSubagent(cacheKey);
- if (subagentDetail) {
+ if (subagentDetail && !options?.bypassCache) {
return subagentDetail;
}
diff --git a/src/main/ipc/teams.ts b/src/main/ipc/teams.ts
index edc4da79..85f36da7 100644
--- a/src/main/ipc/teams.ts
+++ b/src/main/ipc/teams.ts
@@ -402,11 +402,13 @@ async function handleGetData(
}
const normalizeText = (text: string): string => text.trim().replace(/\r\n/g, '\n');
- const leadSessionTextFingerprints = new Set();
+
+ // Collect text fingerprints from ALL non-live messages (inbox, lead_session, sentMessages)
+ // so we can dedup lead_process live messages against them.
+ const existingTextFingerprints = new Set();
for (const msg of data.messages) {
- if ((msg as { source?: unknown }).source !== 'lead_session') continue;
if (typeof msg.from !== 'string' || typeof msg.text !== 'string') continue;
- leadSessionTextFingerprints.add(`${msg.from}\0${normalizeText(msg.text)}`);
+ existingTextFingerprints.add(`${msg.from}\0${normalizeText(msg.text)}`);
}
const keyFor = (m: {
@@ -421,14 +423,24 @@ async function handleGetData(
return `${m.timestamp}\0${m.from}\0${(m.text ?? '').slice(0, 80)}`;
};
+ // Text-based fingerprints for lead_process messages to catch duplicates
+ // with different messageIds (e.g. lead-turn-* vs lead-sendmsg-* with same text)
+ const leadProcessTextFingerprints = new Set();
+
const merged: typeof data.messages = [];
const seen = new Set();
for (const msg of [...data.messages, ...live]) {
- if ((msg as { source?: unknown }).source === 'lead_process') {
+ if ((msg as { source?: unknown }).source === 'lead_process' && !msg.to) {
const fp = `${msg.from}\0${normalizeText(msg.text ?? '')}`;
- if (leadSessionTextFingerprints.has(fp)) {
+ // Skip if same text already exists from any source (inbox, lead_session, etc.)
+ if (existingTextFingerprints.has(fp)) {
continue;
}
+ // Dedup lead_process messages with same text but different messageIds
+ if (leadProcessTextFingerprints.has(fp)) {
+ continue;
+ }
+ leadProcessTextFingerprints.add(fp);
}
const key = keyFor(msg);
if (seen.has(key)) continue;
diff --git a/src/main/services/team/MemberStatsComputer.ts b/src/main/services/team/MemberStatsComputer.ts
index 6443436b..96ce9994 100644
--- a/src/main/services/team/MemberStatsComputer.ts
+++ b/src/main/services/team/MemberStatsComputer.ts
@@ -9,8 +9,18 @@ import type { FileLineStats, MemberFullStats } from '@shared/types';
const logger = createLogger('Service:MemberStatsComputer');
-function isValidFilePath(value: string): boolean {
- return value.length > 0 && value !== 'null' && value !== 'undefined' && value !== 'None';
+const TRAILING_PUNCT_CHARS = new Set([';', '.', ',']);
+const INVALID_NAMES = new Set(['null', 'undefined', 'None', 'false', 'true', '']);
+
+function stripTrailingPunct(s: string): string {
+ let end = s.length;
+ while (end > 0 && TRAILING_PUNCT_CHARS.has(s[end - 1])) end--;
+ return end === s.length ? s : s.slice(0, end);
+}
+
+export function isValidFilePath(value: string): boolean {
+ const cleaned = stripTrailingPunct(value.trim());
+ return cleaned.length > 1 && !INVALID_NAMES.has(cleaned) && cleaned.includes('/');
}
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
@@ -73,11 +83,19 @@ export class MemberStatsComputer {
.filter(isValidFilePath)
.sort((a, b) => a.localeCompare(b));
+ // Also filter perFileStats keys to exclude invalid paths
+ const filteredFileStats: Record = {};
+ for (const [fp, fls] of Object.entries(perFileStats)) {
+ if (isValidFilePath(fp)) {
+ filteredFileStats[fp] = fls;
+ }
+ }
+
const stats: MemberFullStats = {
linesAdded,
linesRemoved,
filesTouched: validFiles,
- fileStats: perFileStats,
+ fileStats: filteredFileStats,
toolUsage,
inputTokens,
outputTokens,
@@ -121,18 +139,24 @@ export class MemberStatsComputer {
// Track last known content per file for accurate Write/NotebookEdit diffs
const fileLastContent = new Map();
+ const cleanPath = (fp: string): string => stripTrailingPunct(fp.trim());
+
const trackFile = (fp: string): void => {
- if (typeof fp === 'string' && isValidFilePath(fp)) filesTouchedSet.add(fp);
+ if (typeof fp === 'string') {
+ const cleaned = cleanPath(fp);
+ if (isValidFilePath(cleaned)) filesTouchedSet.add(cleaned);
+ }
};
const addFileLines = (fp: string, added: number, removed: number): void => {
- if (!isValidFilePath(fp)) return;
- const existing = perFileStats[fp];
+ const cleaned = cleanPath(fp);
+ if (!isValidFilePath(cleaned)) return;
+ const existing = perFileStats[cleaned];
if (existing) {
existing.added += added;
existing.removed += removed;
} else {
- perFileStats[fp] = { added, removed };
+ perFileStats[cleaned] = { added, removed };
}
};
diff --git a/src/main/services/team/TeamAgentToolsInstaller.ts b/src/main/services/team/TeamAgentToolsInstaller.ts
index 7868cc1c..a2f7fc04 100644
--- a/src/main/services/team/TeamAgentToolsInstaller.ts
+++ b/src/main/services/team/TeamAgentToolsInstaller.ts
@@ -865,6 +865,7 @@ function sendInboxMessage(paths, teamName, flags) {
summary,
messageId,
};
+ if (flags.source) payload.source = flags.source;
var lastErr;
for (var attempt = 0; attempt < 8; attempt++) {
@@ -915,6 +916,7 @@ function reviewApprove(paths, teamName, taskId, flags) {
text: inboxText,
summary: 'Approved #' + String(taskId),
from,
+ source: 'system_notification',
});
}
@@ -957,6 +959,7 @@ function reviewRequestChanges(paths, teamName, taskId, flags) {
text: inboxText,
summary: 'Fix request for #' + String(taskId),
from,
+ source: 'system_notification',
});
}
@@ -1282,6 +1285,7 @@ async function main() {
text: parts.join('\n'),
summary: 'New task #' + String(task.id) + ' assigned',
from,
+ source: 'system_notification',
});
}
}
@@ -1319,6 +1323,7 @@ async function main() {
text: 'Comment on task #' + String(result.taskId) + ' "' + String(result.subject) + '":\n\n' + (typeof args.flags.text === 'string' ? args.flags.text.trim() : ''),
summary: 'Comment on #' + String(result.taskId),
from: from,
+ source: 'system_notification',
});
} catch (e) { /* best-effort */ }
}
@@ -1396,6 +1401,7 @@ async function main() {
text: parts.join('\n'),
summary: 'Task #' + String(task.id) + ' assigned',
from,
+ source: 'system_notification',
});
}
return;
@@ -1493,7 +1499,10 @@ async function main() {
if (domain === 'message') {
if (action === 'send') {
- const result = sendInboxMessage(paths, teamName, args.flags);
+ // Strip source from agent flags — only internal callers may set it
+ var msgFlags = Object.assign({}, args.flags);
+ delete msgFlags.source;
+ const result = sendInboxMessage(paths, teamName, msgFlags);
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
return;
}
diff --git a/src/main/services/team/TeamDataService.ts b/src/main/services/team/TeamDataService.ts
index ec3c8a43..5b4d3ede 100644
--- a/src/main/services/team/TeamDataService.ts
+++ b/src/main/services/team/TeamDataService.ts
@@ -9,10 +9,15 @@ import {
} from '@main/utils/pathDecoder';
import { isProcessAlive } from '@main/utils/processHealth';
import { killProcessByPid } from '@main/utils/processKill';
-import { AGENT_BLOCK_CLOSE, AGENT_BLOCK_OPEN } from '@shared/constants/agentBlocks';
+import {
+ AGENT_BLOCK_CLOSE,
+ AGENT_BLOCK_OPEN,
+ stripAgentBlocks,
+} from '@shared/constants/agentBlocks';
import { getMemberColor } from '@shared/constants/memberColors';
import { createLogger } from '@shared/utils/logger';
import { parseNumericSuffixName } from '@shared/utils/teamMemberName';
+import { extractToolPreview, formatToolSummaryFromCalls } from '@shared/utils/toolSummary';
import { randomUUID } from 'crypto';
import * as fs from 'fs';
import * as path from 'path';
@@ -54,13 +59,14 @@ import type {
TeamTask,
TeamTaskStatus,
TeamTaskWithKanban,
+ ToolCallMeta,
UpdateKanbanPatch,
} from '@shared/types';
const logger = createLogger('Service:TeamDataService');
const MIN_TEXT_LENGTH = 30;
-const MAX_LEAD_TEXTS = 50;
+const MAX_LEAD_TEXTS = 150;
const PROCESS_HEALTH_INTERVAL_MS = 2_000;
const MAX_PROCESSES_FILE_BYTES = 2 * 1024 * 1024;
const TASK_MAP_YIELD_EVERY = 250;
@@ -283,6 +289,7 @@ export class TeamDataService {
// Dedup: if a lead_process message text is also present in lead_session, prefer lead_session.
// This avoids double-rendering when we persist lead process messages and later load the lead JSONL.
+ // Exception: lead_process messages with `to` field are captured SendMessage — never dedup those.
if (leadTexts.length > 0 && sentMessages.length > 0) {
const normalizeText = (text: string): string => text.trim().replace(/\r\n/g, '\n');
const leadSessionFingerprints = new Set();
@@ -292,17 +299,67 @@ export class TeamDataService {
}
messages = messages.filter((m) => {
if (m.source !== 'lead_process') return true;
+ // Captured SendMessage messages (with recipient) are real messages — never dedup
+ if (m.to) return true;
const fp = `${m.from}\0${normalizeText(m.text ?? '')}`;
return !leadSessionFingerprints.has(fp);
});
}
- // Enrich messages without leadSessionId: assign current session for lead_process/user_sent.
- // lead_process messages surviving dedup are from the current session;
- // user_sent messages written before this feature lack the field.
- if (config.leadSessionId) {
- for (const msg of messages) {
- if (!msg.leadSessionId && (msg.source === 'lead_process' || msg.source === 'user_sent')) {
+ // Enrich inbox messages without leadSessionId by assigning the nearest neighbor's
+ // session ID (by timestamp). This avoids the old forward-only propagation bug where
+ // messages between two sessions always inherited the *earlier* session, causing a
+ // spurious "New session" divider even when the message is chronologically closer to
+ // the later session.
+ if (config.leadSessionId || messages.some((m) => m.leadSessionId)) {
+ messages.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
+
+ // Collect indices of messages that already have a leadSessionId (anchors).
+ const anchors: { index: number; time: number; sessionId: string }[] = [];
+ for (let i = 0; i < messages.length; i++) {
+ if (messages[i].leadSessionId) {
+ anchors.push({
+ index: i,
+ time: Date.parse(messages[i].timestamp),
+ sessionId: messages[i].leadSessionId!,
+ });
+ }
+ }
+
+ if (anchors.length > 0) {
+ // For each message without leadSessionId, find the closest anchor by timestamp
+ // and inherit its sessionId.
+ let anchorIdx = 0;
+ for (let i = 0; i < messages.length; i++) {
+ if (messages[i].leadSessionId) {
+ // Advance anchorIdx to track current position for efficient lookup
+ while (anchorIdx < anchors.length - 1 && anchors[anchorIdx].index < i) {
+ anchorIdx++;
+ }
+ continue;
+ }
+
+ const msgTime = Date.parse(messages[i].timestamp);
+
+ // Find closest anchor by timestamp (binary-search-like scan from current position)
+ let bestAnchor = anchors[0];
+ let bestDist = Math.abs(msgTime - bestAnchor.time);
+ for (const anchor of anchors) {
+ const dist = Math.abs(msgTime - anchor.time);
+ if (dist < bestDist) {
+ bestDist = dist;
+ bestAnchor = anchor;
+ } else if (dist > bestDist && anchor.time > msgTime) {
+ // Anchors are sorted by index (asc time) — once distance grows past the
+ // message time, further anchors will only be farther.
+ break;
+ }
+ }
+ messages[i].leadSessionId = bestAnchor.sessionId;
+ }
+ } else if (config.leadSessionId) {
+ // No anchors at all — fall back to config.leadSessionId for everything.
+ for (const msg of messages) {
msg.leadSessionId = config.leadSessionId;
}
}
@@ -1103,7 +1160,19 @@ export class TeamDataService {
}
async sendMessage(teamName: string, request: SendMessageRequest): Promise {
- return this.inboxWriter.sendMessage(teamName, request);
+ // Enrich with leadSessionId so session boundary separators work
+ let enrichedRequest = request;
+ if (!enrichedRequest.leadSessionId) {
+ try {
+ const config = await this.configReader.getConfig(teamName);
+ if (config?.leadSessionId) {
+ enrichedRequest = { ...enrichedRequest, leadSessionId: config.leadSessionId };
+ }
+ } catch {
+ // non-critical
+ }
+ }
+ return this.inboxWriter.sendMessage(teamName, enrichedRequest);
}
private resolveLeadNameFromConfig(config: TeamConfig | null): string {
@@ -1357,13 +1426,29 @@ export class TeamDataService {
const projectId = encodePath(config.projectPath);
const baseDir = extractBaseDir(projectId);
- const jsonlPath = path.join(getProjectsBasePath(), baseDir, `${config.leadSessionId}.jsonl`);
+ let jsonlPath = path.join(getProjectsBasePath(), baseDir, `${config.leadSessionId}.jsonl`);
try {
await fs.promises.access(jsonlPath, fs.constants.F_OK);
} catch {
- logger.debug(`Lead session JSONL not found: ${jsonlPath}`);
- return [];
+ // Claude Code encodes underscores as hyphens in project directory names;
+ // our encodePath only handles slashes. Try the underscore-to-hyphen variant.
+ const altBaseDir = baseDir.replace(/_/g, '-');
+ if (altBaseDir !== baseDir) {
+ const altPath = path.join(
+ getProjectsBasePath(),
+ altBaseDir,
+ `${config.leadSessionId}.jsonl`
+ );
+ try {
+ await fs.promises.access(altPath, fs.constants.F_OK);
+ jsonlPath = altPath;
+ } catch {
+ return [];
+ }
+ } else {
+ return [];
+ }
}
const leadName = config.members?.find((m) => m.agentType === 'team-lead')?.name ?? 'team-lead';
@@ -1374,6 +1459,7 @@ export class TeamDataService {
const INITIAL_SCAN_BYTES = 256 * 1024; // 256KB
const textsReversed: InboxMessage[] = [];
+ const seenMessageIds = new Set();
const handle = await fs.promises.open(jsonlPath, 'r');
try {
const stat = await handle.stat();
@@ -1410,20 +1496,69 @@ export class TeamDataService {
const timestamp =
typeof msg.timestamp === 'string' ? msg.timestamp : new Date().toISOString();
+ const textParts: string[] = [];
for (const block of content as Record[]) {
if (block.type !== 'text' || typeof block.text !== 'string') continue;
- const text = block.text.trim();
- if (text.length < MIN_TEXT_LENGTH) continue;
- textsReversed.push({
- from: leadName,
- text,
- timestamp,
- read: true,
- source: 'lead_session',
- leadSessionId: config.leadSessionId,
- });
- if (textsReversed.length >= MAX_LEAD_TEXTS) break;
+ textParts.push(block.text);
}
+ if (textParts.length === 0) continue;
+
+ const combined = stripAgentBlocks(textParts.join('\n')).trim();
+ if (combined.length < MIN_TEXT_LENGTH) continue;
+
+ // Collect tool_use details from following lines (text and tool_use are separate in JSONL).
+ // tool_result (type=user) lines are interleaved between tool_use lines — skip them.
+ const toolCallsList: ToolCallMeta[] = [];
+ const lookaheadLimit = Math.min(i + 200, lines.length);
+ for (let j = i + 1; j < lookaheadLimit; j++) {
+ const tLine = lines[j]?.trim();
+ if (!tLine) continue;
+ let tMsg: Record;
+ try {
+ tMsg = JSON.parse(tLine) as Record;
+ } catch {
+ continue;
+ }
+ if (tMsg.type !== 'assistant') continue; // skip tool_result (type=user) lines
+ const tMessage = (tMsg.message ?? tMsg) as Record;
+ const tContent = tMessage.content;
+ if (!Array.isArray(tContent)) continue;
+ const tBlocks = tContent as Record[];
+ if (tBlocks.some((b) => b.type === 'text')) break; // next text = stop
+ for (const b of tBlocks) {
+ if (b.type === 'tool_use' && typeof b.name === 'string' && b.name !== 'SendMessage') {
+ const input = (b.input ?? {}) as Record;
+ toolCallsList.push({
+ name: b.name,
+ preview: extractToolPreview(b.name, input),
+ });
+ }
+ }
+ }
+ const toolCalls = toolCallsList.length > 0 ? toolCallsList : undefined;
+ const toolSummary = toolCalls ? formatToolSummaryFromCalls(toolCalls) : undefined;
+
+ // Stable messageId: timestamp + text prefix (survives tail-scan range changes)
+ const textPrefix = combined
+ .slice(0, 50)
+ .replace(/[^\p{L}\p{N}]/gu, '')
+ .slice(0, 20);
+
+ const messageId = `lead-session-${timestamp}-${textPrefix}`;
+ if (seenMessageIds.has(messageId)) continue;
+ seenMessageIds.add(messageId);
+
+ textsReversed.push({
+ from: leadName,
+ text: combined,
+ timestamp,
+ read: true,
+ source: 'lead_session',
+ leadSessionId: config.leadSessionId,
+ messageId,
+ toolSummary,
+ toolCalls,
+ });
if (textsReversed.length >= MAX_LEAD_TEXTS) break;
}
diff --git a/src/main/services/team/TeamInboxReader.ts b/src/main/services/team/TeamInboxReader.ts
index b4e9cc5e..0e9a8849 100644
--- a/src/main/services/team/TeamInboxReader.ts
+++ b/src/main/services/team/TeamInboxReader.ts
@@ -99,6 +99,8 @@ export class TeamInboxReader {
summary: typeof row.summary === 'string' ? row.summary : undefined,
color: typeof row.color === 'string' ? row.color : undefined,
messageId: typeof row.messageId === 'string' ? row.messageId : undefined,
+ source: typeof row.source === 'string' ? (row.source as InboxMessage['source']) : undefined,
+ leadSessionId: typeof row.leadSessionId === 'string' ? row.leadSessionId : undefined,
});
}
diff --git a/src/main/services/team/TeamInboxWriter.ts b/src/main/services/team/TeamInboxWriter.ts
index a0776db4..8ac414ec 100644
--- a/src/main/services/team/TeamInboxWriter.ts
+++ b/src/main/services/team/TeamInboxWriter.ts
@@ -30,6 +30,7 @@ export class TeamInboxWriter {
messageId,
attachments: attachmentMeta?.length ? attachmentMeta : undefined,
...(request.source && { source: request.source }),
+ ...(request.leadSessionId && { leadSessionId: request.leadSessionId }),
};
await withInboxLock(inboxPath, async () => {
diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts
index 2376468d..e80a3342 100644
--- a/src/main/services/team/TeamProvisioningService.ts
+++ b/src/main/services/team/TeamProvisioningService.ts
@@ -22,6 +22,7 @@ import { resolveLanguageName } from '@shared/utils/agentLanguage';
import { isInboxNoiseMessage } from '@shared/utils/inboxNoise';
import { createLogger } from '@shared/utils/logger';
import { createCliAutoSuffixNameGuard } from '@shared/utils/teamMemberName';
+import { extractToolPreview, formatToolSummaryFromCalls } from '@shared/utils/toolSummary';
import { spawn } from 'child_process';
import { randomUUID } from 'crypto';
import * as fs from 'fs';
@@ -49,6 +50,7 @@ import type {
TeamProvisioningProgress,
TeamProvisioningState,
TeamTask,
+ ToolCallMeta,
} from '@shared/types';
const logger = createLogger('Service:TeamProvisioning');
@@ -60,7 +62,7 @@ const STDOUT_RING_LIMIT = 64 * 1024;
const LOG_PROGRESS_THROTTLE_MS = 300;
const UI_LOGS_TAIL_LIMIT = 128 * 1024;
const SHELL_ENV_TIMEOUT_MS = 12000;
-const PROBE_CACHE_TTL_MS = 10 * 60_000;
+const PROBE_CACHE_TTL_MS = 36 * 60 * 60 * 1000;
const PREFLIGHT_TIMEOUT_MS = 60000;
const PREFLIGHT_AUTH_RETRY_DELAY_MS = 2000;
const PREFLIGHT_AUTH_MAX_RETRIES = 2;
@@ -79,8 +81,6 @@ const PREFLIGHT_PING_ARGS = [
'haiku',
'--max-turns',
'1',
- '--max-budget-usd',
- '0.05',
'--no-session-persistence',
] as const;
const PREFLIGHT_EXPECTED = 'PONG';
@@ -163,15 +163,10 @@ interface ProvisioningRun {
rejectOnce: (error: string) => void;
timeoutHandle: NodeJS.Timeout;
} | null;
- /**
- * Accumulates assistant text for direct user→lead messages (no relay capture active).
- * Flushed to liveLeadProcessMessages on result.success.
- */
- directReplyParts: string[];
- /** Monotonic counter for stream-json turns (incremented on result). */
- leadTurnSeq: number;
- /** Stable timestamp used for the current aggregated lead turn message. */
- leadTurnMessageTimestamp: string | null;
+ /** Monotonic counter for individual lead assistant messages. */
+ leadMsgSeq: number;
+ /** Accumulated tool_use details between text messages. */
+ pendingToolCalls: ToolCallMeta[];
/** Throttle timestamp for emitting inbox refresh events for lead text. */
lastLeadTextEmitMs: number;
/**
@@ -1752,9 +1747,8 @@ export class TeamProvisioningService {
isLaunch: false,
fsPhase: 'waiting_config',
leadRelayCapture: null,
- directReplyParts: [],
- leadTurnSeq: 0,
- leadTurnMessageTimestamp: null,
+ leadMsgSeq: 0,
+ pendingToolCalls: [],
lastLeadTextEmitMs: 0,
silentUserDmForward: null,
silentUserDmForwardClearHandle: null,
@@ -2053,9 +2047,8 @@ export class TeamProvisioningService {
isLaunch: true,
fsPhase: 'waiting_members',
leadRelayCapture: null,
- directReplyParts: [],
- leadTurnSeq: 0,
- leadTurnMessageTimestamp: null,
+ leadMsgSeq: 0,
+ pendingToolCalls: [],
lastLeadTextEmitMs: 0,
silentUserDmForward: null,
silentUserDmForwardClearHandle: null,
@@ -2493,8 +2486,6 @@ export class TeamProvisioningService {
},
};
run.leadRelayCapture = capture;
- // Clear any direct reply parts — relay capture takes priority
- run.directReplyParts = [];
});
try {
@@ -2797,6 +2788,16 @@ export class TeamProvisioningService {
}
pushLiveLeadProcessMessage(teamName: string, message: InboxMessage): void {
+ // Enrich with leadSessionId if missing — needed for session boundary separators
+ if (!message.leadSessionId) {
+ const runId = this.activeByTeam.get(teamName);
+ if (runId) {
+ const run = this.runs.get(runId);
+ if (run?.detectedSessionId) {
+ message.leadSessionId = run.detectedSessionId;
+ }
+ }
+ }
const MAX = 100;
const list = this.liveLeadProcessMessages.get(teamName) ?? [];
const id = typeof message.messageId === 'string' ? message.messageId.trim() : '';
@@ -2876,7 +2877,7 @@ export class TeamProvisioningService {
const hasSendMessageToUser = (content ?? []).some((part) => {
if (!part || typeof part !== 'object') return false;
if (part.type !== 'tool_use' || part.name !== 'SendMessage') return false;
- const input = (part as Record).input;
+ const input = part.input;
if (!input || typeof input !== 'object') return false;
return (input as Record).recipient === 'user';
});
@@ -2885,7 +2886,7 @@ export class TeamProvisioningService {
.filter((part) => part.type === 'text' && typeof part.text === 'string')
.map((part) => part.text as string);
if (textParts.length > 0) {
- const text = textParts.join('');
+ const text = textParts.join('\n');
// Auth failures sometimes show up as assistant text (e.g. "401", "Please run /login")
// rather than stderr or a result.subtype=error. Detect early to avoid false "ready".
this.handleAuthFailureInOutput(run, text, 'assistant');
@@ -2908,34 +2909,37 @@ export class TeamProvisioningService {
clearTimeout(capture.idleHandle);
}
capture.idleHandle = setTimeout(() => {
- const combined = capture.textParts.join('').trim();
+ const combined = capture.textParts.join('\n').trim();
capture.resolveOnce(combined);
}, capture.idleMs);
}
} else if (run.provisioningComplete) {
- // Accumulate assistant text for a single "live lead turn" message in Messages.
- // If the same assistant message includes SendMessage(to:"user"), prefer the captured
- // SendMessage and avoid duplicating it as a separate lead text entry.
+ // Push each assistant text block as a separate live message (per-message pattern).
+ // When the same assistant message includes SendMessage(to:"user"), skip text —
+ // captureSendMessageToUser() handles it separately.
if (!run.silentUserDmForward && !hasSendMessageToUser) {
- run.directReplyParts.push(text);
- const raw = run.directReplyParts.join('');
- const cleanText = stripAgentBlocks(raw).trim();
- if (cleanText.length >= TeamProvisioningService.LEAD_TEXT_MIN_LENGTH) {
+ const cleanText = stripAgentBlocks(text).trim();
+ if (cleanText.length > 0) {
+ run.leadMsgSeq += 1;
const leadName =
run.request.members.find((m) => m.role?.toLowerCase().includes('lead'))?.name ||
'team-lead';
- if (!run.leadTurnMessageTimestamp) {
- run.leadTurnMessageTimestamp = nowIso();
- }
- const messageId = `lead-turn-${run.runId}-${run.leadTurnSeq}`;
+ const messageId = `lead-turn-${run.runId}-${run.leadMsgSeq}`;
+ // Attach accumulated tool call details from preceding tool_use messages, then reset.
+ const toolCalls =
+ run.pendingToolCalls.length > 0 ? [...run.pendingToolCalls] : undefined;
+ const toolSummary = toolCalls ? formatToolSummaryFromCalls(toolCalls) : undefined;
+ run.pendingToolCalls = [];
const leadMsg: InboxMessage = {
from: leadName,
text: cleanText,
- timestamp: run.leadTurnMessageTimestamp,
+ timestamp: nowIso(),
read: true,
summary: cleanText.length > 60 ? cleanText.slice(0, 57) + '...' : cleanText,
messageId,
source: 'lead_process',
+ toolSummary,
+ toolCalls,
};
this.pushLiveLeadProcessMessage(run.teamName, leadMsg);
@@ -2952,13 +2956,24 @@ export class TeamProvisioningService {
});
}
}
- } else if (hasSendMessageToUser) {
- run.directReplyParts = [];
- run.leadTurnMessageTimestamp = null;
- this.removeLiveLeadProcessMessage(
- run.teamName,
- `lead-turn-${run.runId}-${run.leadTurnSeq}`
- );
+ }
+ }
+ }
+
+ // Accumulate tool_use details from tool-only messages (text + tool_use are separate in stream-json).
+ // These details will be attached to the next text message as toolCalls/toolSummary.
+ if (run.provisioningComplete) {
+ for (const block of content ?? []) {
+ if (
+ block?.type === 'tool_use' &&
+ typeof block.name === 'string' &&
+ block.name !== 'SendMessage'
+ ) {
+ const input = (block.input ?? {}) as Record;
+ run.pendingToolCalls.push({
+ name: block.name,
+ preview: extractToolPreview(block.name, input),
+ });
}
}
}
@@ -3098,49 +3113,8 @@ export class TeamProvisioningService {
}
if (run.leadRelayCapture) {
const capture = run.leadRelayCapture;
- const combined = capture.textParts.join('').trim();
+ const combined = capture.textParts.join('\n').trim();
capture.resolveOnce(combined);
- } else if (run.provisioningComplete && run.directReplyParts.length > 0) {
- // Finalize the current live lead turn message (single messageId per turn).
- const rawReply = run.directReplyParts.join('').trim();
- run.directReplyParts = [];
- const leadName =
- run.request.members.find((m) => m.role?.toLowerCase().includes('lead'))?.name ||
- 'team-lead';
- const replyText = stripAgentBlocks(rawReply).trim();
- const finalText =
- replyText.length > 0
- ? replyText
- : rawReply.length > 0
- ? '(Message received and processed)'
- : '';
- if (finalText.length > 0) {
- if (!run.leadTurnMessageTimestamp) {
- run.leadTurnMessageTimestamp = nowIso();
- }
- const messageId = `lead-turn-${run.runId}-${run.leadTurnSeq}`;
- const replyMsg: InboxMessage = {
- from: leadName,
- text: finalText,
- timestamp: run.leadTurnMessageTimestamp,
- read: true,
- summary: finalText.length > 60 ? finalText.slice(0, 57) + '...' : finalText,
- messageId,
- source: 'lead_process',
- };
- this.pushLiveLeadProcessMessage(run.teamName, replyMsg);
- this.teamChangeEmitter?.({
- type: 'inbox',
- teamName: run.teamName,
- detail: 'lead-turn-final',
- });
- }
- }
- // Turn boundary: advance lead turn sequence.
- if (run.provisioningComplete) {
- run.leadTurnSeq += 1;
- run.leadTurnMessageTimestamp = null;
- run.directReplyParts = [];
}
// Clear silent relay flag after any successful turn.
run.silentUserDmForward = null;
@@ -3158,12 +3132,6 @@ export class TeamProvisioningService {
if (run.leadRelayCapture) {
run.leadRelayCapture.rejectOnce(errorMsg);
}
- // Turn boundary: advance lead turn sequence.
- if (run.provisioningComplete) {
- run.leadTurnSeq += 1;
- run.leadTurnMessageTimestamp = null;
- run.directReplyParts = [];
- }
// Clear silent relay flag after any errored turn.
run.silentUserDmForward = null;
if (run.silentUserDmForwardClearHandle) {
diff --git a/src/main/services/team/TeamSentMessagesStore.ts b/src/main/services/team/TeamSentMessagesStore.ts
index 88ef5b9e..6e7e5bd4 100644
--- a/src/main/services/team/TeamSentMessagesStore.ts
+++ b/src/main/services/team/TeamSentMessagesStore.ts
@@ -76,6 +76,20 @@ export class TeamSentMessagesStore {
attachments: Array.isArray(row.attachments) ? row.attachments : undefined,
source: typeof row.source === 'string' ? (row.source as InboxMessage['source']) : undefined,
leadSessionId: typeof row.leadSessionId === 'string' ? row.leadSessionId : undefined,
+ toolSummary: typeof row.toolSummary === 'string' ? row.toolSummary : undefined,
+ toolCalls: Array.isArray(row.toolCalls)
+ ? (row.toolCalls as unknown[])
+ .filter(
+ (tc): tc is { name: string; preview?: string } =>
+ tc != null &&
+ typeof tc === 'object' &&
+ typeof (tc as Record).name === 'string'
+ )
+ .map((tc) => ({
+ name: tc.name,
+ preview: typeof tc.preview === 'string' ? tc.preview : undefined,
+ }))
+ : undefined,
});
}
diff --git a/src/preload/index.ts b/src/preload/index.ts
index 687132ad..9f361333 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -347,14 +347,18 @@ const electronAPI: ElectronAPI = {
ipcRenderer.invoke('search-sessions', projectId, query, maxResults),
searchAllProjects: (query: string, maxResults?: number) =>
ipcRenderer.invoke('search-all-projects', query, maxResults),
- getSessionDetail: (projectId: string, sessionId: string) =>
- ipcRenderer.invoke('get-session-detail', projectId, sessionId),
+ getSessionDetail: (projectId: string, sessionId: string, options?: { bypassCache?: boolean }) =>
+ ipcRenderer.invoke('get-session-detail', projectId, sessionId, options),
getSessionMetrics: (projectId: string, sessionId: string) =>
ipcRenderer.invoke('get-session-metrics', projectId, sessionId),
getWaterfallData: (projectId: string, sessionId: string) =>
ipcRenderer.invoke('get-waterfall-data', projectId, sessionId),
- getSubagentDetail: (projectId: string, sessionId: string, subagentId: string) =>
- ipcRenderer.invoke('get-subagent-detail', projectId, sessionId, subagentId),
+ getSubagentDetail: (
+ projectId: string,
+ sessionId: string,
+ subagentId: string,
+ options?: { bypassCache?: boolean }
+ ) => ipcRenderer.invoke('get-subagent-detail', projectId, sessionId, subagentId, options),
getSessionGroups: (projectId: string, sessionId: string) =>
ipcRenderer.invoke('get-session-groups', projectId, sessionId),
getSessionsByIds: (projectId: string, sessionIds: string[], options?: SessionsByIdsOptions) =>
diff --git a/src/renderer/api/httpClient.ts b/src/renderer/api/httpClient.ts
index 7fc64c7c..033bebe5 100644
--- a/src/renderer/api/httpClient.ts
+++ b/src/renderer/api/httpClient.ts
@@ -246,7 +246,11 @@ export class HttpAPIClient implements ElectronAPI {
return this.get(`/api/search?${params}`);
};
- getSessionDetail = (projectId: string, sessionId: string): Promise =>
+ getSessionDetail = (
+ projectId: string,
+ sessionId: string,
+ _options?: { bypassCache?: boolean }
+ ): Promise =>
this.get(
`/api/projects/${encodeURIComponent(projectId)}/sessions/${encodeURIComponent(sessionId)}`
);
@@ -264,7 +268,8 @@ export class HttpAPIClient implements ElectronAPI {
getSubagentDetail = (
projectId: string,
sessionId: string,
- subagentId: string
+ subagentId: string,
+ _options?: { bypassCache?: boolean }
): Promise =>
this.get(
`/api/projects/${encodeURIComponent(projectId)}/sessions/${encodeURIComponent(sessionId)}/subagents/${encodeURIComponent(subagentId)}`
diff --git a/src/renderer/components/chat/ChatHistory.tsx b/src/renderer/components/chat/ChatHistory.tsx
index 243a635e..20a6f69a 100644
--- a/src/renderer/components/chat/ChatHistory.tsx
+++ b/src/renderer/components/chat/ChatHistory.tsx
@@ -16,6 +16,8 @@ const SCROLL_THRESHOLD = 300;
/** Must match the `w-80` (320px) context panel width used in the layout below. */
const CONTEXT_PANEL_WIDTH_PX = 320;
+import { formatPercentOfTotal, sumContextInjectionTokens } from '@renderer/utils/contextMath';
+
import { ChatHistoryEmptyState } from './ChatHistoryEmptyState';
import { ChatHistoryItem } from './ChatHistoryItem';
import { ChatHistoryLoadingState } from './ChatHistoryLoadingState';
@@ -190,6 +192,11 @@ export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => {
return { allContextInjections: injections, lastAiGroupTotalTokens: totalTokens };
}, [sessionContextStats, conversation, selectedContextPhase, sessionPhaseInfo]);
+ const visibleContextPercentLabel = useMemo(() => {
+ const visibleTokens = sumContextInjectionTokens(allContextInjections);
+ return formatPercentOfTotal(visibleTokens, lastAiGroupTotalTokens);
+ }, [allContextInjections, lastAiGroupTotalTokens]);
+
// State for navigation highlight (blue, used for Turn navigation from CLAUDE.md panel)
const [isNavigationHighlight, setIsNavigationHighlight] = useState(false);
const navigationHighlightTimerRef = useRef | null>(null);
@@ -828,7 +835,7 @@ export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => {
: 'var(--color-text-secondary)',
}}
>
- Context ({allContextInjections.length})
+ {visibleContextPercentLabel ?? `Context (${allContextInjections.length})`}
)}
diff --git a/src/renderer/components/layout/SortableTab.tsx b/src/renderer/components/layout/SortableTab.tsx
index bf366be6..499849f4 100644
--- a/src/renderer/components/layout/SortableTab.tsx
+++ b/src/renderer/components/layout/SortableTab.tsx
@@ -9,6 +9,7 @@ import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { getTeamColorSet } from '@renderer/constants/teamColors';
import { useStore } from '@renderer/store';
+import { nameColorSet } from '@renderer/utils/projectColor';
import {
Activity,
Bell,
@@ -67,12 +68,17 @@ export const SortableTab = ({
)
);
- const teamColor = useStore((s) => {
+ const teamColorSet = useStore((s) => {
if (tab.type !== 'team' || !tab.teamName) return null;
const team = s.teamByName[tab.teamName];
- return team?.color ?? null;
+ const explicitColor =
+ team?.color ??
+ (s.selectedTeamName === tab.teamName ? s.selectedTeamData?.config.color : undefined);
+ if (explicitColor) return getTeamColorSet(explicitColor);
+ // Fallback: deterministic color derived from display name
+ const displayName = team?.displayName ?? tab.label;
+ return nameColorSet(displayName);
});
- const teamColorSet = teamColor ? getTeamColorSet(teamColor) : null;
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: tab.id,
@@ -96,8 +102,15 @@ export const SortableTab = ({
? teamColorSet
? teamColorSet.badge
: 'var(--color-surface-overlay)'
- : 'transparent',
- color: isActive || isHovered ? 'var(--color-text)' : 'var(--color-text-muted)',
+ : teamColorSet
+ ? teamColorSet.badge
+ : 'transparent',
+ color:
+ isActive || isHovered
+ ? 'var(--color-text)'
+ : teamColorSet
+ ? teamColorSet.text
+ : 'var(--color-text-muted)',
outline: isSelected ? '1px solid var(--color-border-emphasis)' : 'none',
outlineOffset: '-1px',
borderLeft: isActive && teamColorSet ? `2px solid ${teamColorSet.border}` : undefined,
@@ -125,7 +138,7 @@ export const SortableTab = ({
role="tab"
tabIndex={0}
aria-selected={isActive}
- className="group flex min-w-0 max-w-[200px] shrink-0 cursor-grab items-center gap-2 rounded-md px-3 py-1.5"
+ className="group flex shrink-0 cursor-grab items-center gap-2 rounded-md px-3 py-1.5"
style={style}
onClick={(e) => onTabClick(tab.id, e)}
onMouseDown={(e) => onMouseDown(tab.id, e)}
@@ -150,7 +163,11 @@ export const SortableTab = ({
)}
- {tab.label}
+ 20 ? 'max-w-[200px] truncate' : ''} whitespace-nowrap text-sm`}
+ >
+ {tab.label}
+
{isTeamTab && (
{
return (
);
};
diff --git a/src/renderer/components/layout/TabBar.tsx b/src/renderer/components/layout/TabBar.tsx
index 74b5f215..5b59053c 100644
--- a/src/renderer/components/layout/TabBar.tsx
+++ b/src/renderer/components/layout/TabBar.tsx
@@ -352,7 +352,7 @@ export const TabBar = ({ paneId }: TabBarProps): React.JSX.Element => {
Gives users a reliable window-drag target regardless of how many tabs are open.
Only applied on the leftmost pane in Electron to match the TabBar drag region logic. */}