diff --git a/docs/plans/2026-03-05-mcp-server-design.md b/docs/plans/2026-03-05-mcp-server-design.md new file mode 100644 index 0000000..dd44a99 --- /dev/null +++ b/docs/plans/2026-03-05-mcp-server-design.md @@ -0,0 +1,357 @@ +# SuperCrew MCP Server 设计方案 + +**Date:** 2026-03-05 +**Status:** Approved +**前置文档:** [Vibe Kanban 对比分析](./2026-03-05-vibe-kanban-comparison.md) + +--- + +## 一、背景与目标 + +### 当前痛点 + +SuperCrew 现有架构存在"闭环割裂"问题: + +``` +Web 看 → CLI 做 → git 同步 +``` + +| 问题 | 影响 | +|------|------| +| Web 只读 | 无法在 Web 创建/编辑 Feature | +| 同步延迟 | 必须 push 后 Web 才能看到更新 | +| 分支盲区 | Web 只读 main,feature branch 不可见 | + +### 目标 + +实现**双向协作闭环**: + +> Web 可创建/编辑 Feature,Claude Code 可执行,两边实时同步,自动聚合所有分支 + +借鉴 Vibe Kanban 的 MCP Server 架构,但保留 GitHub 作为持久化和团队共享渠道。 + +--- + +## 二、架构设计 + +### 整体架构 + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ SuperCrew MCP Server │ +│ (本地运行,监听 localhost:3456) │ +│ │ +│ ┌───────────────┐ ┌───────────────┐ ┌─────────────────────────┐ │ +│ │ Feature Store │ │ Event Bus │ │ GitHub Sync Worker │ │ +│ │ (SQLite) │ │ (广播变更) │ │ (异步推送/拉取 GitHub) │ │ +│ └───────────────┘ └───────────────┘ └─────────────────────────┘ │ +│ │ │ │ │ +│ ┌──────┴───────────────────┴───────────────────────┴─────────────┐ │ +│ │ Branch Scanner │ │ +│ │ (扫描所有分支,聚合 Features) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +└──────────┬───────────────────┬───────────────────┬──────────────────┘ + │ │ │ + MCP Protocol WebSocket GitHub API + │ │ │ + ▼ ▼ ▼ + ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ + │ Claude Code │ │ Web UI │ │ GitHub Repo │ + │ (Agent) │ │ (Browser) │ │ (备份) │ + └─────────────┘ └─────────────┘ └─────────────┘ +``` + +### 核心组件 + +| 组件 | 职责 | +|------|------| +| **Feature Store (SQLite)** | 本地数据库,快速读写,即时响应 | +| **Event Bus** | 广播变更事件,通知所有连接方 | +| **GitHub Sync Worker** | 异步同步到 GitHub,作为持久化备份 | +| **Branch Scanner** | 扫描所有分支,聚合去重 Features | +| **MCP Endpoint** | Claude Code 通过 MCP 协议连接 | +| **WebSocket Endpoint** | Web UI 通过 WebSocket 实时连接 | + +--- + +## 三、数据设计 + +### 数据库 vs Git 文件的关系 + +| 层 | 存储 | 职责 | 特点 | +|---|------|------|------| +| **主数据** | SQLite | 实时读写 | 快、即时、本地 | +| **备份** | GitHub Repo | 持久化 + 团队共享 | 慢、异步、远端 | + +### SQLite Schema + +```sql +CREATE TABLE features ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + status TEXT NOT NULL, -- planning/designing/ready/active/blocked/done + owner TEXT, + priority TEXT, -- P0/P1/P2/P3 + branch TEXT NOT NULL, -- 所在分支 + teams TEXT, -- JSON array + tags TEXT, -- JSON array + blocked_by TEXT, -- JSON array + target_release TEXT, + created_at TEXT, + updated_at TEXT, + meta_yaml TEXT, -- 原始 meta.yaml 内容 + design_md TEXT, -- 原始 design.md 内容 + plan_md TEXT, -- 原始 plan.md 内容 + log_md TEXT, -- 原始 log.md 内容 + synced_at TEXT -- 最后同步到 GitHub 的时间 +); + +CREATE INDEX idx_features_status ON features(status); +CREATE INDEX idx_features_branch ON features(branch); +``` + +### 数据流 + +**场景 1:Web UI 创建 Feature** +``` +1. Web UI → POST /api/features → MCP Server +2. MCP Server → 写入 SQLite +3. MCP Server → Event Bus 广播 "feature:created" +4. Claude Code (MCP Client) 收到事件 → 更新本地状态 +5. GitHub Sync Worker 异步 → push 到 GitHub +``` + +**场景 2:Claude Code 更新 Feature 状态** +``` +1. Claude Code → MCP call "update_feature_status" +2. MCP Server → 写入 SQLite +3. MCP Server → Event Bus 广播 "feature:updated" +4. Web UI (WebSocket) 收到事件 → 即时刷新 +5. GitHub Sync Worker 异步 → push 到 GitHub +``` + +**场景 3:团队成员同步** +``` +1. 成员 A push 到 GitHub +2. 成员 B 的 MCP Server 定期 pull → 检测到变化 +3. 更新本地 SQLite → 广播事件 → Web/CLI 刷新 +``` + +--- + +## 四、分支聚合设计 + +### 扫描逻辑 + +``` +1. 列出所有分支: main, feature/*, fix/* +2. 扫描每个分支的 .supercrew/features/ +3. 聚合去重 → 写入 SQLite +``` + +### 去重优先级 + +同一个 Feature 可能在多个分支存在: + +| 场景 | 处理方式 | +|------|---------| +| Feature 只在 `feature/x` | 显示,标记分支 | +| Feature 只在 `main` | 显示,标记 main | +| Feature 在两个分支都有 | **以 feature branch 为准**(更新的版本) | + +```typescript +// 去重优先级:feature/* > fix/* > main +function dedupeFeatures(featuresPerBranch: Map) { + const result = new Map() + + // 先加 main 的 + for (const f of featuresPerBranch.get('main') ?? []) { + result.set(f.id, { ...f, branch: 'main' }) + } + + // 再用 feature/* 覆盖(更新的版本) + for (const [branch, features] of featuresPerBranch) { + if (branch.startsWith('feature/') || branch.startsWith('fix/')) { + for (const f of features) { + result.set(f.id, { ...f, branch }) + } + } + } + + return Array.from(result.values()) +} +``` + +### 扫描触发时机 + +| 触发方式 | 说明 | +|---------|------| +| **启动时** | MCP Server 启动时全量扫描 | +| **定时** | 每 30 秒增量检查 | +| **Webhook** | GitHub push 事件触发扫描(可选) | +| **手动** | Web UI "Refresh" 按钮 / CLI 命令 | + +--- + +## 五、MCP Tools 定义 + +Claude Code 可以通过 MCP 调用这些工具: + +```typescript +const tools = [ + { + name: "list_features", + description: "列出所有 features", + handler: () => featureStore.listAll() + }, + { + name: "get_feature", + description: "获取单个 feature 详情", + inputSchema: { id: "string" }, + handler: ({ id }) => featureStore.get(id) + }, + { + name: "create_feature", + description: "创建新 feature", + inputSchema: { + title: "string", + priority: "string", + owner: "string", + teams: "string[]" + }, + handler: (data) => { + const feature = featureStore.create(data) + eventBus.emit("feature:created", feature) + githubSync.queue("create", feature) + return feature + } + }, + { + name: "update_feature_status", + description: "更新 feature 状态", + inputSchema: { id: "string", status: "SupercrewStatus" }, + handler: ({ id, status }) => { + const feature = featureStore.updateStatus(id, status) + eventBus.emit("feature:updated", feature) + githubSync.queue("update", feature) + return feature + } + }, + { + name: "update_feature_plan", + description: "更新 feature plan.md", + inputSchema: { id: "string", content: "string" }, + handler: ({ id, content }) => { + const feature = featureStore.updatePlan(id, content) + eventBus.emit("feature:updated", feature) + githubSync.queue("update", feature) + return feature + } + }, + { + name: "log_progress", + description: "追加 feature log 记录", + inputSchema: { id: "string", entry: "string" }, + handler: ({ id, entry }) => { + const feature = featureStore.appendLog(id, entry) + eventBus.emit("feature:updated", feature) + githubSync.queue("update", feature) + return feature + } + }, + { + name: "sync_now", + description: "立即同步到 GitHub", + handler: () => githubSync.flushAll() + } +] +``` + +--- + +## 六、Web UI 变更 + +### 新增功能 + +| 功能 | 说明 | +|------|------| +| **创建 Feature** | 表单创建,调用 MCP Server API | +| **编辑 Feature** | 内联编辑 status、owner、priority | +| **分支标记** | 卡片显示所在分支 | +| **实时更新** | WebSocket 连接,无需手动刷新 | + +### UI 示例 + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ SuperCrew Kanban [+ New Feature] [Refresh] │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ Planning Designing Ready Active Blocked Done │ +│ ───────── ───────── ───────── ───────── ──────── ──── │ +│ │ +│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ +│ │ signup │ │ oauth │ │ login │ │ +│ │ @alice │ │ @bob │ │ @charlie│ │ +│ │ P2 │ │ P1 │ │ P0 │ │ +│ │ 🌿 main │ │ 🌿 feat/│ │ 🌿 feat/│ │ +│ │ │ │ oauth │ │ login │ │ +│ └─────────┘ └─────────┘ └─────────┘ │ +│ │ +│ ● Connected Updated: Now │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 七、与现有架构对比 + +| 维度 | 现有架构 | MCP Server 架构 | +|------|---------|----------------| +| **数据源** | GitHub Repo (远端) | SQLite (本地) + GitHub (备份) | +| **Web 读取** | GitHub Contents API | MCP Server HTTP | +| **Web 写入** | 不支持 | MCP Server API | +| **CLI 读取** | 本地文件 / git pull | MCP Protocol | +| **CLI 写入** | 本地文件 → push | MCP Protocol | +| **同步延迟** | 需要 push/pull | 即时 (毫秒级) | +| **分支支持** | 只读 main | 聚合所有分支 | +| **离线能力** | Web 不可用 | 本地可用 | + +--- + +## 八、实施计划 + +| 阶段 | 目标 | 工作量 | +|------|------|--------| +| **Phase 1** | MCP Server 基础 + SQLite + Branch Scanner | 1 周 | +| **Phase 2** | Claude Code MCP 集成 (Tools) | 0.5 周 | +| **Phase 3** | Web UI WebSocket + 实时更新 | 1 周 | +| **Phase 4** | Web UI 创建/编辑功能 | 0.5 周 | +| **Phase 5** | GitHub Sync Worker + 多机同步 | 1 周 | + +**总计:约 4 周** + +--- + +## 九、风险与缓解 + +| 风险 | 缓解措施 | +|------|---------| +| **MCP Server 未启动** | Web 降级为只读模式(现有架构) | +| **多机数据冲突** | GitHub 作为 source of truth,定期 pull 覆盖本地 | +| **分支过多性能问题** | 只扫描 main + 最近 30 天活跃的 feature/* 分支 | +| **SQLite 损坏** | 启动时从 GitHub 重建 | + +--- + +## 十、决策点 + +以下问题已在设计讨论中确认: + +| 决策点 | 结论 | 理由 | +|--------|------|------| +| **MCP Server 运行位置** | 用户本地(Claude Code 机器上) | MCP 协议为本地通信设计,支持离线,团队通过 GitHub 同步 | +| **启动方式** | 随 SuperCrew 插件安装自动配置 | 与 skills/hooks 一致,可装在系统目录或项目目录 | +| **多项目支持** | 每个 repo 一个实例 | 隔离清晰,与 Claude Code 一个项目一个 session 的模式匹配 | +| **冲突策略** | 手动解决,提示用户选择 | 用户完全控制,避免意外覆盖 | + diff --git a/docs/plans/2026-03-05-mcp-server-impl.md b/docs/plans/2026-03-05-mcp-server-impl.md new file mode 100644 index 0000000..3205a0f --- /dev/null +++ b/docs/plans/2026-03-05-mcp-server-impl.md @@ -0,0 +1,1854 @@ +# SuperCrew MCP Server Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Build a local MCP Server that enables real-time bidirectional sync between Web UI and Claude Code, with GitHub as backup. + +**Architecture:** MCP Server runs locally (per-repo instance), stores features in SQLite for fast read/write, syncs to GitHub asynchronously. Claude Code connects via MCP Protocol, Web UI connects via WebSocket. Branch Scanner aggregates features from all branches. + +**Tech Stack:** TypeScript, Bun, SQLite (better-sqlite3), MCP SDK (@modelcontextprotocol/sdk), WebSocket (ws), Hono (HTTP API) + +**Design Doc:** [2026-03-05-mcp-server-design.md](./2026-03-05-mcp-server-design.md) + +--- + +## Phase 1: MCP Server 基础 + SQLite + Feature Store + +### Task 1.1: 创建 MCP Server 项目结构 + +**Files:** +- Create: `mcp-server/package.json` +- Create: `mcp-server/tsconfig.json` +- Create: `mcp-server/src/index.ts` + +**Step 1: 创建目录结构** + +```bash +mkdir -p mcp-server/src +``` + +**Step 2: 创建 package.json** + +```json +{ + "name": "@supercrew/mcp-server", + "version": "0.1.0", + "type": "module", + "main": "src/index.ts", + "bin": { + "supercrew-mcp": "./src/index.ts" + }, + "scripts": { + "dev": "bun run --watch src/index.ts", + "start": "bun run src/index.ts", + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0", + "better-sqlite3": "^11.0.0", + "hono": "^4.12.3", + "ws": "^8.18.0", + "gray-matter": "^4.0.3", + "js-yaml": "^4.1.1", + "zod": "^3.23.0" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.11", + "@types/bun": "latest", + "@types/ws": "^8.5.12", + "vitest": "^3.2.4", + "typescript": "^5.0.0" + } +} +``` + +**Step 3: 创建 tsconfig.json** + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "types": ["bun-types"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} +``` + +**Step 4: 创建入口文件 src/index.ts** + +```typescript +#!/usr/bin/env bun + +console.log('SuperCrew MCP Server starting...') + +// Placeholder - will be implemented in subsequent tasks +export {} +``` + +**Step 5: 安装依赖** + +Run: `cd mcp-server && bun install` + +**Step 6: 验证启动** + +Run: `cd mcp-server && bun run src/index.ts` +Expected: "SuperCrew MCP Server starting..." + +**Step 7: Commit** + +```bash +git add mcp-server/ +git commit -m "feat(mcp-server): initialize project structure" +``` + +--- + +### Task 1.2: 实现 SQLite Feature Store + +**Files:** +- Create: `mcp-server/src/store/db.ts` +- Create: `mcp-server/src/store/feature-store.ts` +- Create: `mcp-server/src/types.ts` +- Test: `mcp-server/src/__tests__/feature-store.test.ts` + +**Step 1: 创建类型定义 src/types.ts** + +```typescript +export type SupercrewStatus = + | 'planning' + | 'designing' + | 'ready' + | 'active' + | 'blocked' + | 'done' + +export type Priority = 'P0' | 'P1' | 'P2' | 'P3' + +export interface Feature { + id: string + title: string + status: SupercrewStatus + owner: string | null + priority: Priority | null + branch: string + teams: string[] + tags: string[] + blocked_by: string[] + target_release: string | null + created_at: string + updated_at: string + meta_yaml: string | null + design_md: string | null + plan_md: string | null + log_md: string | null + synced_at: string | null +} + +export interface FeatureInput { + id: string + title: string + status?: SupercrewStatus + owner?: string + priority?: Priority + branch?: string + teams?: string[] + tags?: string[] +} +``` + +**Step 2: 创建数据库初始化 src/store/db.ts** + +```typescript +import Database from 'better-sqlite3' +import { join } from 'path' + +const SCHEMA = ` +CREATE TABLE IF NOT EXISTS features ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'planning', + owner TEXT, + priority TEXT, + branch TEXT NOT NULL DEFAULT 'main', + teams TEXT DEFAULT '[]', + tags TEXT DEFAULT '[]', + blocked_by TEXT DEFAULT '[]', + target_release TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + meta_yaml TEXT, + design_md TEXT, + plan_md TEXT, + log_md TEXT, + synced_at TEXT +); + +CREATE INDEX IF NOT EXISTS idx_features_status ON features(status); +CREATE INDEX IF NOT EXISTS idx_features_branch ON features(branch); +` + +export function createDb(dbPath: string): Database.Database { + const db = new Database(dbPath) + db.exec(SCHEMA) + return db +} + +export function getDefaultDbPath(repoRoot: string): string { + return join(repoRoot, '.supercrew', '.mcp-server.db') +} +``` + +**Step 3: 创建 Feature Store src/store/feature-store.ts** + +```typescript +import type Database from 'better-sqlite3' +import type { Feature, FeatureInput, SupercrewStatus } from '../types.js' + +export class FeatureStore { + constructor(private db: Database.Database) {} + + listAll(): Feature[] { + const rows = this.db.prepare('SELECT * FROM features ORDER BY updated_at DESC').all() + return rows.map(this.rowToFeature) + } + + get(id: string): Feature | null { + const row = this.db.prepare('SELECT * FROM features WHERE id = ?').get(id) + return row ? this.rowToFeature(row) : null + } + + create(input: FeatureInput): Feature { + const now = new Date().toISOString() + const feature: Feature = { + id: input.id, + title: input.title, + status: input.status ?? 'planning', + owner: input.owner ?? null, + priority: input.priority ?? null, + branch: input.branch ?? 'main', + teams: input.teams ?? [], + tags: input.tags ?? [], + blocked_by: [], + target_release: null, + created_at: now, + updated_at: now, + meta_yaml: null, + design_md: null, + plan_md: null, + log_md: null, + synced_at: null, + } + + this.db.prepare(` + INSERT INTO features (id, title, status, owner, priority, branch, teams, tags, blocked_by, target_release, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `).run( + feature.id, + feature.title, + feature.status, + feature.owner, + feature.priority, + feature.branch, + JSON.stringify(feature.teams), + JSON.stringify(feature.tags), + JSON.stringify(feature.blocked_by), + feature.target_release, + feature.created_at, + feature.updated_at + ) + + return feature + } + + updateStatus(id: string, status: SupercrewStatus): Feature | null { + const now = new Date().toISOString() + this.db.prepare('UPDATE features SET status = ?, updated_at = ? WHERE id = ?').run(status, now, id) + return this.get(id) + } + + updatePlan(id: string, content: string): Feature | null { + const now = new Date().toISOString() + this.db.prepare('UPDATE features SET plan_md = ?, updated_at = ? WHERE id = ?').run(content, now, id) + return this.get(id) + } + + appendLog(id: string, entry: string): Feature | null { + const feature = this.get(id) + if (!feature) return null + + const now = new Date().toISOString() + const timestamp = now.split('T')[0] + const newEntry = `\n## ${timestamp}\n\n${entry}\n` + const newLog = (feature.log_md ?? '') + newEntry + + this.db.prepare('UPDATE features SET log_md = ?, updated_at = ? WHERE id = ?').run(newLog, now, id) + return this.get(id) + } + + delete(id: string): boolean { + const result = this.db.prepare('DELETE FROM features WHERE id = ?').run(id) + return result.changes > 0 + } + + upsertFromGitHub(feature: Feature): void { + const existing = this.get(feature.id) + if (existing) { + this.db.prepare(` + UPDATE features SET + title = ?, status = ?, owner = ?, priority = ?, branch = ?, + teams = ?, tags = ?, blocked_by = ?, target_release = ?, + meta_yaml = ?, design_md = ?, plan_md = ?, log_md = ?, + updated_at = ?, synced_at = ? + WHERE id = ? + `).run( + feature.title, feature.status, feature.owner, feature.priority, feature.branch, + JSON.stringify(feature.teams), JSON.stringify(feature.tags), JSON.stringify(feature.blocked_by), + feature.target_release, feature.meta_yaml, feature.design_md, feature.plan_md, feature.log_md, + feature.updated_at, new Date().toISOString(), feature.id + ) + } else { + this.db.prepare(` + INSERT INTO features (id, title, status, owner, priority, branch, teams, tags, blocked_by, target_release, created_at, updated_at, meta_yaml, design_md, plan_md, log_md, synced_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `).run( + feature.id, feature.title, feature.status, feature.owner, feature.priority, feature.branch, + JSON.stringify(feature.teams), JSON.stringify(feature.tags), JSON.stringify(feature.blocked_by), + feature.target_release, feature.created_at, feature.updated_at, + feature.meta_yaml, feature.design_md, feature.plan_md, feature.log_md, new Date().toISOString() + ) + } + } + + private rowToFeature(row: any): Feature { + return { + ...row, + teams: JSON.parse(row.teams ?? '[]'), + tags: JSON.parse(row.tags ?? '[]'), + blocked_by: JSON.parse(row.blocked_by ?? '[]'), + } + } +} +``` + +**Step 4: 创建测试目录和测试文件** + +```bash +mkdir -p mcp-server/src/__tests__ +``` + +**Step 5: 创建 feature-store.test.ts** + +```typescript +import { describe, it, expect, beforeEach, afterEach } from 'vitest' +import Database from 'better-sqlite3' +import { FeatureStore } from '../store/feature-store.js' +import { createDb } from '../store/db.js' + +describe('FeatureStore', () => { + let db: Database.Database + let store: FeatureStore + + beforeEach(() => { + db = createDb(':memory:') + store = new FeatureStore(db) + }) + + afterEach(() => { + db.close() + }) + + it('creates and retrieves a feature', () => { + const feature = store.create({ + id: 'test-feature', + title: 'Test Feature', + status: 'planning', + owner: 'alice', + }) + + expect(feature.id).toBe('test-feature') + expect(feature.title).toBe('Test Feature') + expect(feature.status).toBe('planning') + expect(feature.owner).toBe('alice') + + const retrieved = store.get('test-feature') + expect(retrieved).not.toBeNull() + expect(retrieved!.id).toBe('test-feature') + }) + + it('lists all features', () => { + store.create({ id: 'f1', title: 'Feature 1' }) + store.create({ id: 'f2', title: 'Feature 2' }) + + const all = store.listAll() + expect(all.length).toBe(2) + }) + + it('updates feature status', () => { + store.create({ id: 'f1', title: 'Feature 1', status: 'planning' }) + + const updated = store.updateStatus('f1', 'active') + expect(updated!.status).toBe('active') + }) + + it('appends to log', () => { + store.create({ id: 'f1', title: 'Feature 1' }) + + const updated = store.appendLog('f1', 'Started implementation') + expect(updated!.log_md).toContain('Started implementation') + }) + + it('deletes a feature', () => { + store.create({ id: 'f1', title: 'Feature 1' }) + + const deleted = store.delete('f1') + expect(deleted).toBe(true) + expect(store.get('f1')).toBeNull() + }) +}) +``` + +**Step 6: 运行测试验证失败** + +Run: `cd mcp-server && bun test` +Expected: Tests should fail (files not created yet) + +**Step 7: 创建所有文件后运行测试** + +Run: `cd mcp-server && bun test` +Expected: All tests pass + +**Step 8: Commit** + +```bash +git add mcp-server/src/ +git commit -m "feat(mcp-server): add SQLite feature store with CRUD operations" +``` + +--- + +### Task 1.3: 实现 Event Bus + +**Files:** +- Create: `mcp-server/src/events/event-bus.ts` +- Test: `mcp-server/src/__tests__/event-bus.test.ts` + +**Step 1: 创建 event-bus.ts** + +```typescript +import { EventEmitter } from 'events' +import type { Feature } from '../types.js' + +export type FeatureEvent = + | { type: 'feature:created'; feature: Feature } + | { type: 'feature:updated'; feature: Feature } + | { type: 'feature:deleted'; featureId: string } + | { type: 'sync:started' } + | { type: 'sync:completed'; count: number } + | { type: 'conflict:detected'; featureId: string; local: Feature; remote: Feature } + +export class EventBus extends EventEmitter { + emit(event: FeatureEvent['type'], ...args: any[]): boolean { + return super.emit(event, ...args) + } + + on(event: FeatureEvent['type'], listener: (...args: any[]) => void): this { + return super.on(event, listener) + } + + featureCreated(feature: Feature): void { + this.emit('feature:created', feature) + } + + featureUpdated(feature: Feature): void { + this.emit('feature:updated', feature) + } + + featureDeleted(featureId: string): void { + this.emit('feature:deleted', featureId) + } + + conflictDetected(featureId: string, local: Feature, remote: Feature): void { + this.emit('conflict:detected', featureId, local, remote) + } +} + +export const eventBus = new EventBus() +``` + +**Step 2: 创建 event-bus.test.ts** + +```typescript +import { describe, it, expect, vi } from 'vitest' +import { EventBus } from '../events/event-bus.js' +import type { Feature } from '../types.js' + +describe('EventBus', () => { + it('emits feature:created event', () => { + const bus = new EventBus() + const handler = vi.fn() + + bus.on('feature:created', handler) + + const feature: Feature = { + id: 'test', + title: 'Test', + status: 'planning', + owner: null, + priority: null, + branch: 'main', + teams: [], + tags: [], + blocked_by: [], + target_release: null, + created_at: '2026-03-05', + updated_at: '2026-03-05', + meta_yaml: null, + design_md: null, + plan_md: null, + log_md: null, + synced_at: null, + } + + bus.featureCreated(feature) + + expect(handler).toHaveBeenCalledWith(feature) + }) + + it('emits conflict:detected event', () => { + const bus = new EventBus() + const handler = vi.fn() + + bus.on('conflict:detected', handler) + + const local = { id: 'f1' } as Feature + const remote = { id: 'f1' } as Feature + + bus.conflictDetected('f1', local, remote) + + expect(handler).toHaveBeenCalledWith('f1', local, remote) + }) +}) +``` + +**Step 3: 运行测试** + +Run: `cd mcp-server && bun test` +Expected: All tests pass + +**Step 4: Commit** + +```bash +git add mcp-server/src/events/ +git commit -m "feat(mcp-server): add event bus for real-time notifications" +``` + +--- + +### Task 1.4: 实现 Branch Scanner + +**Files:** +- Create: `mcp-server/src/scanner/branch-scanner.ts` +- Create: `mcp-server/src/scanner/feature-parser.ts` +- Test: `mcp-server/src/__tests__/branch-scanner.test.ts` + +**Step 1: 创建 feature-parser.ts** + +```typescript +import matter from 'gray-matter' +import yaml from 'js-yaml' +import type { Feature, SupercrewStatus, Priority } from '../types.js' + +export function parseMetaYaml(content: string, featureId: string): Partial { + const data = yaml.load(content) as Record + return { + id: data.id ?? featureId, + title: data.title ?? '', + status: (data.status ?? 'planning') as SupercrewStatus, + owner: data.owner ?? null, + priority: data.priority as Priority ?? null, + teams: data.teams ?? [], + tags: data.tags ?? [], + blocked_by: data.blocked_by ?? [], + target_release: data.target_release ?? null, + created_at: data.created ?? new Date().toISOString(), + updated_at: data.updated ?? new Date().toISOString(), + meta_yaml: content, + } +} + +export function parseDesignMd(content: string): { body: string } { + const { content: body } = matter(content) + return { body: body.trim() } +} + +export function parsePlanMd(content: string): { body: string; total: number; completed: number } { + const { data, content: body } = matter(content) + return { + body: body.trim(), + total: data.total_tasks ?? 0, + completed: data.completed_tasks ?? 0, + } +} +``` + +**Step 2: 创建 branch-scanner.ts** + +```typescript +import { execSync } from 'child_process' +import { join } from 'path' +import { existsSync, readFileSync } from 'fs' +import type { Feature } from '../types.js' +import { parseMetaYaml } from './feature-parser.js' + +export interface ScanResult { + features: Feature[] + branches: string[] +} + +export class BranchScanner { + constructor(private repoRoot: string) {} + + async scanAllBranches(): Promise { + const branches = this.listBranches() + const featuresPerBranch = new Map() + + for (const branch of branches) { + const features = await this.scanBranch(branch) + featuresPerBranch.set(branch, features) + } + + const deduped = this.dedupeFeatures(featuresPerBranch) + return { features: deduped, branches } + } + + async scanCurrentBranch(): Promise { + const branch = this.getCurrentBranch() + return this.scanBranch(branch) + } + + private listBranches(): string[] { + try { + const output = execSync('git branch -a --format="%(refname:short)"', { + cwd: this.repoRoot, + encoding: 'utf-8', + }) + return output + .split('\n') + .map(b => b.trim()) + .filter(b => b && !b.includes('->')) + .filter(b => b === 'main' || b.startsWith('feature/') || b.startsWith('fix/')) + } catch { + return ['main'] + } + } + + private getCurrentBranch(): string { + try { + return execSync('git branch --show-current', { + cwd: this.repoRoot, + encoding: 'utf-8', + }).trim() + } catch { + return 'main' + } + } + + private async scanBranch(branch: string): Promise { + const featuresDir = join(this.repoRoot, '.supercrew', 'features') + + if (!existsSync(featuresDir)) { + return [] + } + + const features: Feature[] = [] + + try { + // For current branch, read from filesystem + const currentBranch = this.getCurrentBranch() + if (branch === currentBranch) { + const entries = Bun.file(featuresDir).name ? [] : + Array.from(new Bun.Glob('*').scanSync(featuresDir)) + + for (const entry of entries) { + const featurePath = join(featuresDir, entry) + const feature = this.parseFeatureDir(featurePath, entry, branch) + if (feature) features.push(feature) + } + } else { + // For other branches, use git show + const featureIds = this.listFeaturesInBranch(branch) + for (const id of featureIds) { + const feature = this.parseFeatureFromGit(branch, id) + if (feature) features.push(feature) + } + } + } catch (e) { + console.error(`Error scanning branch ${branch}:`, e) + } + + return features + } + + private listFeaturesInBranch(branch: string): string[] { + try { + const output = execSync( + `git ls-tree -d --name-only ${branch}:.supercrew/features/ 2>/dev/null || true`, + { cwd: this.repoRoot, encoding: 'utf-8' } + ) + return output.split('\n').filter(Boolean) + } catch { + return [] + } + } + + private parseFeatureDir(dirPath: string, featureId: string, branch: string): Feature | null { + const metaPath = join(dirPath, 'meta.yaml') + if (!existsSync(metaPath)) return null + + try { + const metaContent = readFileSync(metaPath, 'utf-8') + const partial = parseMetaYaml(metaContent, featureId) + + const designPath = join(dirPath, 'design.md') + const planPath = join(dirPath, 'plan.md') + const logPath = join(dirPath, 'log.md') + + return { + ...partial, + branch, + design_md: existsSync(designPath) ? readFileSync(designPath, 'utf-8') : null, + plan_md: existsSync(planPath) ? readFileSync(planPath, 'utf-8') : null, + log_md: existsSync(logPath) ? readFileSync(logPath, 'utf-8') : null, + synced_at: null, + } as Feature + } catch { + return null + } + } + + private parseFeatureFromGit(branch: string, featureId: string): Feature | null { + try { + const metaContent = execSync( + `git show ${branch}:.supercrew/features/${featureId}/meta.yaml 2>/dev/null`, + { cwd: this.repoRoot, encoding: 'utf-8' } + ) + const partial = parseMetaYaml(metaContent, featureId) + + const getFile = (filename: string): string | null => { + try { + return execSync( + `git show ${branch}:.supercrew/features/${featureId}/${filename} 2>/dev/null`, + { cwd: this.repoRoot, encoding: 'utf-8' } + ) + } catch { + return null + } + } + + return { + ...partial, + branch, + design_md: getFile('design.md'), + plan_md: getFile('plan.md'), + log_md: getFile('log.md'), + synced_at: null, + } as Feature + } catch { + return null + } + } + + private dedupeFeatures(featuresPerBranch: Map): Feature[] { + const result = new Map() + + // First add main branch features + for (const f of featuresPerBranch.get('main') ?? []) { + result.set(f.id, { ...f, branch: 'main' }) + } + + // Then override with feature/* and fix/* branches (more recent) + for (const [branch, features] of featuresPerBranch) { + if (branch.startsWith('feature/') || branch.startsWith('fix/')) { + for (const f of features) { + result.set(f.id, { ...f, branch }) + } + } + } + + return Array.from(result.values()) + } +} +``` + +**Step 3: 创建 branch-scanner.test.ts** + +```typescript +import { describe, it, expect } from 'vitest' +import { parseMetaYaml } from '../scanner/feature-parser.js' + +describe('Feature Parser', () => { + it('parses meta.yaml correctly', () => { + const content = ` +id: login-feature +title: User Login +status: active +owner: alice +priority: P1 +teams: + - frontend + - backend +tags: + - auth +` + const result = parseMetaYaml(content, 'login-feature') + + expect(result.id).toBe('login-feature') + expect(result.title).toBe('User Login') + expect(result.status).toBe('active') + expect(result.owner).toBe('alice') + expect(result.priority).toBe('P1') + expect(result.teams).toEqual(['frontend', 'backend']) + expect(result.tags).toEqual(['auth']) + }) + + it('uses defaults for missing fields', () => { + const content = ` +title: Minimal Feature +` + const result = parseMetaYaml(content, 'minimal') + + expect(result.id).toBe('minimal') + expect(result.status).toBe('planning') + expect(result.teams).toEqual([]) + }) +}) +``` + +**Step 4: 运行测试** + +Run: `cd mcp-server && bun test` +Expected: All tests pass + +**Step 5: Commit** + +```bash +git add mcp-server/src/scanner/ +git commit -m "feat(mcp-server): add branch scanner for multi-branch feature aggregation" +``` + +--- + +## Phase 2: MCP Protocol 集成 + +### Task 2.1: 实现 MCP Server 入口 + +**Files:** +- Modify: `mcp-server/src/index.ts` +- Create: `mcp-server/src/mcp/server.ts` +- Create: `mcp-server/src/mcp/tools.ts` + +**Step 1: 创建 MCP Tools 定义 src/mcp/tools.ts** + +```typescript +import type { FeatureStore } from '../store/feature-store.js' +import type { EventBus } from '../events/event-bus.js' +import type { SupercrewStatus } from '../types.js' + +export function createTools(store: FeatureStore, eventBus: EventBus) { + return { + list_features: { + description: '列出所有 features', + inputSchema: { type: 'object', properties: {} }, + handler: async () => { + const features = store.listAll() + return { content: [{ type: 'text', text: JSON.stringify(features, null, 2) }] } + }, + }, + + get_feature: { + description: '获取单个 feature 详情', + inputSchema: { + type: 'object', + properties: { id: { type: 'string', description: 'Feature ID' } }, + required: ['id'], + }, + handler: async ({ id }: { id: string }) => { + const feature = store.get(id) + if (!feature) { + return { content: [{ type: 'text', text: `Feature ${id} not found` }], isError: true } + } + return { content: [{ type: 'text', text: JSON.stringify(feature, null, 2) }] } + }, + }, + + create_feature: { + description: '创建新 feature', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Feature ID (e.g., login-page)' }, + title: { type: 'string', description: 'Feature title' }, + priority: { type: 'string', enum: ['P0', 'P1', 'P2', 'P3'] }, + owner: { type: 'string', description: 'Owner username' }, + }, + required: ['id', 'title'], + }, + handler: async (input: { id: string; title: string; priority?: string; owner?: string }) => { + const feature = store.create({ + id: input.id, + title: input.title, + priority: input.priority as any, + owner: input.owner, + }) + eventBus.featureCreated(feature) + return { content: [{ type: 'text', text: `Created feature: ${feature.id}` }] } + }, + }, + + update_feature_status: { + description: '更新 feature 状态', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + status: { type: 'string', enum: ['planning', 'designing', 'ready', 'active', 'blocked', 'done'] }, + }, + required: ['id', 'status'], + }, + handler: async ({ id, status }: { id: string; status: SupercrewStatus }) => { + const feature = store.updateStatus(id, status) + if (!feature) { + return { content: [{ type: 'text', text: `Feature ${id} not found` }], isError: true } + } + eventBus.featureUpdated(feature) + return { content: [{ type: 'text', text: `Updated ${id} status to ${status}` }] } + }, + }, + + log_progress: { + description: '追加 feature log 记录', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + entry: { type: 'string', description: 'Progress entry to append' }, + }, + required: ['id', 'entry'], + }, + handler: async ({ id, entry }: { id: string; entry: string }) => { + const feature = store.appendLog(id, entry) + if (!feature) { + return { content: [{ type: 'text', text: `Feature ${id} not found` }], isError: true } + } + eventBus.featureUpdated(feature) + return { content: [{ type: 'text', text: `Added log entry to ${id}` }] } + }, + }, + } +} +``` + +**Step 2: 创建 MCP Server src/mcp/server.ts** + +```typescript +import { Server } from '@modelcontextprotocol/sdk/server/index.js' +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' +import { createTools } from './tools.js' +import type { FeatureStore } from '../store/feature-store.js' +import type { EventBus } from '../events/event-bus.js' + +export async function startMcpServer(store: FeatureStore, eventBus: EventBus) { + const server = new Server( + { name: 'supercrew-mcp', version: '0.1.0' }, + { capabilities: { tools: {} } } + ) + + const tools = createTools(store, eventBus) + + server.setRequestHandler('tools/list', async () => ({ + tools: Object.entries(tools).map(([name, tool]) => ({ + name, + description: tool.description, + inputSchema: tool.inputSchema, + })), + })) + + server.setRequestHandler('tools/call', async (request) => { + const { name, arguments: args } = request.params as { name: string; arguments: any } + const tool = tools[name as keyof typeof tools] + if (!tool) { + return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true } + } + return tool.handler(args) + }) + + const transport = new StdioServerTransport() + await server.connect(transport) + + console.error('SuperCrew MCP Server running on stdio') + return server +} +``` + +**Step 3: 更新入口文件 src/index.ts** + +```typescript +#!/usr/bin/env bun + +import { createDb, getDefaultDbPath } from './store/db.js' +import { FeatureStore } from './store/feature-store.js' +import { EventBus } from './events/event-bus.js' +import { BranchScanner } from './scanner/branch-scanner.js' +import { startMcpServer } from './mcp/server.js' + +async function main() { + const repoRoot = process.cwd() + const dbPath = getDefaultDbPath(repoRoot) + + // Ensure .supercrew directory exists + const { mkdirSync } = await import('fs') + mkdirSync(`${repoRoot}/.supercrew`, { recursive: true }) + + const db = createDb(dbPath) + const store = new FeatureStore(db) + const eventBus = new EventBus() + const scanner = new BranchScanner(repoRoot) + + // Initial scan + console.error('Scanning branches for features...') + const { features } = await scanner.scanAllBranches() + for (const feature of features) { + store.upsertFromGitHub(feature) + } + console.error(`Loaded ${features.length} features from ${repoRoot}`) + + // Start MCP server + await startMcpServer(store, eventBus) +} + +main().catch(console.error) +``` + +**Step 4: 验证 MCP Server** + +Run: `cd mcp-server && echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | bun run src/index.ts` +Expected: JSON response listing available tools + +**Step 5: Commit** + +```bash +git add mcp-server/src/ +git commit -m "feat(mcp-server): implement MCP protocol with tools for feature management" +``` + +--- + +### Task 2.2: 添加 MCP Server 到 Claude Code 配置 + +**Files:** +- Modify: `plugins/supercrew/.claude-plugin/plugin.json` +- Create: `plugins/supercrew/mcp/settings.json` + +**Step 1: 创建 MCP 配置模板** + +```bash +mkdir -p plugins/supercrew/mcp +``` + +**Step 2: 创建 settings.json 模板** + +```json +{ + "mcpServers": { + "supercrew": { + "command": "bunx", + "args": ["@supercrew/mcp-server"], + "env": {} + } + } +} +``` + +**Step 3: 更新 plugin.json** + +```json +{ + "name": "supercrew", + "description": "AI-driven feature lifecycle management with real-time MCP sync.", + "version": "0.2.0", + "author": { + "name": "steinsz" + }, + "homepage": "https://github.com/nicepkg/supercrew", + "repository": "https://github.com/nicepkg/supercrew", + "license": "MIT", + "keywords": ["feature-management", "kanban", "lifecycle", "planning", "tracking", "mcp"], + "mcp": { + "server": { + "command": "bunx", + "args": ["@supercrew/mcp-server"] + } + } +} +``` + +**Step 4: Commit** + +```bash +git add plugins/supercrew/ +git commit -m "feat(supercrew): add MCP server configuration to plugin" +``` + +--- + +## Phase 3: WebSocket + HTTP API + +### Task 3.1: 添加 HTTP API 和 WebSocket 服务 + +**Files:** +- Create: `mcp-server/src/http/server.ts` +- Create: `mcp-server/src/http/routes.ts` +- Create: `mcp-server/src/ws/server.ts` +- Modify: `mcp-server/src/index.ts` + +**Step 1: 创建 HTTP 路由 src/http/routes.ts** + +```typescript +import { Hono } from 'hono' +import { cors } from 'hono/cors' +import type { FeatureStore } from '../store/feature-store.js' +import type { EventBus } from '../events/event-bus.js' +import type { SupercrewStatus } from '../types.js' + +export function createHttpApp(store: FeatureStore, eventBus: EventBus) { + const app = new Hono() + + app.use('*', cors({ + origin: ['http://localhost:5173', 'http://localhost:5174'], + allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], + allowHeaders: ['Content-Type'], + })) + + app.get('/health', (c) => c.json({ ok: true })) + + app.get('/api/features', (c) => { + const features = store.listAll() + return c.json(features) + }) + + app.get('/api/features/:id', (c) => { + const feature = store.get(c.req.param('id')) + if (!feature) return c.json({ error: 'Not found' }, 404) + return c.json(feature) + }) + + app.post('/api/features', async (c) => { + const body = await c.req.json() + const feature = store.create(body) + eventBus.featureCreated(feature) + return c.json(feature, 201) + }) + + app.patch('/api/features/:id/status', async (c) => { + const { status } = await c.req.json() as { status: SupercrewStatus } + const feature = store.updateStatus(c.req.param('id'), status) + if (!feature) return c.json({ error: 'Not found' }, 404) + eventBus.featureUpdated(feature) + return c.json(feature) + }) + + app.delete('/api/features/:id', (c) => { + const deleted = store.delete(c.req.param('id')) + if (!deleted) return c.json({ error: 'Not found' }, 404) + eventBus.featureDeleted(c.req.param('id')) + return c.json({ ok: true }) + }) + + app.get('/api/board', (c) => { + const features = store.listAll() + const featuresByStatus = { + planning: features.filter(f => f.status === 'planning'), + designing: features.filter(f => f.status === 'designing'), + ready: features.filter(f => f.status === 'ready'), + active: features.filter(f => f.status === 'active'), + blocked: features.filter(f => f.status === 'blocked'), + done: features.filter(f => f.status === 'done'), + } + return c.json({ features, featuresByStatus }) + }) + + return app +} +``` + +**Step 2: 创建 HTTP Server src/http/server.ts** + +```typescript +import type { Hono } from 'hono' + +export function startHttpServer(app: Hono, port: number) { + return Bun.serve({ + port, + fetch: app.fetch, + }) +} +``` + +**Step 3: 创建 WebSocket Server src/ws/server.ts** + +```typescript +import type { EventBus } from '../events/event-bus.js' +import type { Feature } from '../types.js' + +interface WebSocketClient { + ws: WebSocket + send: (data: any) => void +} + +export class WebSocketServer { + private clients: Set = new Set() + + constructor(private eventBus: EventBus) { + this.setupEventListeners() + } + + setupEventListeners() { + this.eventBus.on('feature:created', (feature: Feature) => { + this.broadcast({ type: 'feature:created', feature }) + }) + + this.eventBus.on('feature:updated', (feature: Feature) => { + this.broadcast({ type: 'feature:updated', feature }) + }) + + this.eventBus.on('feature:deleted', (featureId: string) => { + this.broadcast({ type: 'feature:deleted', featureId }) + }) + + this.eventBus.on('conflict:detected', (featureId: string, local: Feature, remote: Feature) => { + this.broadcast({ type: 'conflict:detected', featureId, local, remote }) + }) + } + + handleConnection(ws: WebSocket) { + const client: WebSocketClient = { + ws, + send: (data) => ws.send(JSON.stringify(data)), + } + this.clients.add(client) + + ws.addEventListener('close', () => { + this.clients.delete(client) + }) + + // Send initial connection confirmation + client.send({ type: 'connected', clientCount: this.clients.size }) + } + + broadcast(data: any) { + for (const client of this.clients) { + try { + client.send(data) + } catch (e) { + this.clients.delete(client) + } + } + } + + get clientCount() { + return this.clients.size + } +} +``` + +**Step 4: 更新入口文件整合所有服务** + +```typescript +#!/usr/bin/env bun + +import { createDb, getDefaultDbPath } from './store/db.js' +import { FeatureStore } from './store/feature-store.js' +import { EventBus } from './events/event-bus.js' +import { BranchScanner } from './scanner/branch-scanner.js' +import { startMcpServer } from './mcp/server.js' +import { createHttpApp } from './http/routes.js' +import { WebSocketServer } from './ws/server.js' + +const HTTP_PORT = parseInt(process.env.MCP_HTTP_PORT ?? '3456') +const WS_PORT = parseInt(process.env.MCP_WS_PORT ?? '3457') + +async function main() { + const repoRoot = process.cwd() + const dbPath = getDefaultDbPath(repoRoot) + + const { mkdirSync } = await import('fs') + mkdirSync(`${repoRoot}/.supercrew`, { recursive: true }) + + const db = createDb(dbPath) + const store = new FeatureStore(db) + const eventBus = new EventBus() + const scanner = new BranchScanner(repoRoot) + + // Initial scan + console.error('Scanning branches for features...') + const { features } = await scanner.scanAllBranches() + for (const feature of features) { + store.upsertFromGitHub(feature) + } + console.error(`Loaded ${features.length} features`) + + // Check if running in MCP mode (stdin connected) + const isMcpMode = !process.stdin.isTTY + + if (isMcpMode) { + // MCP Server mode (Claude Code) + await startMcpServer(store, eventBus) + } else { + // HTTP + WebSocket mode (Web UI) + const httpApp = createHttpApp(store, eventBus) + const wsServer = new WebSocketServer(eventBus) + + // HTTP Server + Bun.serve({ + port: HTTP_PORT, + fetch: httpApp.fetch, + }) + console.error(`HTTP server running on http://localhost:${HTTP_PORT}`) + + // WebSocket Server + Bun.serve({ + port: WS_PORT, + fetch(req, server) { + if (server.upgrade(req)) return + return new Response('WebSocket upgrade required', { status: 426 }) + }, + websocket: { + open(ws) { + wsServer.handleConnection(ws as any) + }, + message() {}, + close() {}, + }, + }) + console.error(`WebSocket server running on ws://localhost:${WS_PORT}`) + + // Periodic rescan + setInterval(async () => { + const { features: newFeatures } = await scanner.scanAllBranches() + for (const f of newFeatures) { + store.upsertFromGitHub(f) + } + }, 30000) + } +} + +main().catch(console.error) +``` + +**Step 5: 测试 HTTP API** + +Run: `cd mcp-server && bun run src/index.ts &` +Run: `curl http://localhost:3456/health` +Expected: `{"ok":true}` + +**Step 6: Commit** + +```bash +git add mcp-server/src/ +git commit -m "feat(mcp-server): add HTTP API and WebSocket for Web UI integration" +``` + +--- + +## Phase 4: GitHub Sync Worker + +### Task 4.1: 实现 GitHub 同步 + +**Files:** +- Create: `mcp-server/src/sync/github-sync.ts` +- Create: `mcp-server/src/sync/conflict-resolver.ts` +- Test: `mcp-server/src/__tests__/github-sync.test.ts` + +**Step 1: 创建 github-sync.ts** + +```typescript +import { execSync } from 'child_process' +import { writeFileSync, mkdirSync, existsSync } from 'fs' +import { join } from 'path' +import yaml from 'js-yaml' +import type { Feature } from '../types.js' +import type { EventBus } from '../events/event-bus.js' + +interface SyncTask { + action: 'create' | 'update' | 'delete' + feature: Feature +} + +export class GitHubSyncWorker { + private queue: SyncTask[] = [] + private syncing = false + + constructor( + private repoRoot: string, + private eventBus: EventBus + ) {} + + queueCreate(feature: Feature) { + this.queue.push({ action: 'create', feature }) + this.processQueue() + } + + queueUpdate(feature: Feature) { + // Dedupe: remove pending updates for same feature + this.queue = this.queue.filter(t => t.feature.id !== feature.id) + this.queue.push({ action: 'update', feature }) + this.processQueue() + } + + queueDelete(feature: Feature) { + this.queue = this.queue.filter(t => t.feature.id !== feature.id) + this.queue.push({ action: 'delete', feature }) + this.processQueue() + } + + async flushAll() { + while (this.queue.length > 0) { + await this.processQueue() + } + } + + private async processQueue() { + if (this.syncing || this.queue.length === 0) return + + this.syncing = true + this.eventBus.emit('sync:started') + + let count = 0 + while (this.queue.length > 0) { + const task = this.queue.shift()! + try { + await this.executeTask(task) + count++ + } catch (e) { + console.error(`Sync failed for ${task.feature.id}:`, e) + } + } + + this.eventBus.emit('sync:completed', count) + this.syncing = false + } + + private async executeTask(task: SyncTask) { + const { feature, action } = task + const featureDir = join(this.repoRoot, '.supercrew', 'features', feature.id) + + switch (action) { + case 'create': + case 'update': + mkdirSync(featureDir, { recursive: true }) + this.writeFeatureFiles(featureDir, feature) + this.gitCommit(`feat: ${action} feature ${feature.id}`) + break + + case 'delete': + if (existsSync(featureDir)) { + execSync(`rm -rf "${featureDir}"`, { cwd: this.repoRoot }) + this.gitCommit(`chore: delete feature ${feature.id}`) + } + break + } + } + + private writeFeatureFiles(dir: string, feature: Feature) { + // meta.yaml + const meta = { + id: feature.id, + title: feature.title, + status: feature.status, + owner: feature.owner, + priority: feature.priority, + teams: feature.teams, + tags: feature.tags, + blocked_by: feature.blocked_by, + target_release: feature.target_release, + created: feature.created_at, + updated: feature.updated_at, + } + writeFileSync(join(dir, 'meta.yaml'), yaml.dump(meta)) + + // design.md + if (feature.design_md) { + writeFileSync(join(dir, 'design.md'), feature.design_md) + } + + // plan.md + if (feature.plan_md) { + writeFileSync(join(dir, 'plan.md'), feature.plan_md) + } + + // log.md + if (feature.log_md) { + writeFileSync(join(dir, 'log.md'), feature.log_md) + } + } + + private gitCommit(message: string) { + try { + execSync('git add .supercrew/', { cwd: this.repoRoot }) + execSync(`git commit -m "${message}" --allow-empty`, { cwd: this.repoRoot }) + } catch (e) { + // Ignore if nothing to commit + } + } + + async gitPush() { + try { + execSync('git push', { cwd: this.repoRoot }) + } catch (e) { + console.error('Git push failed:', e) + } + } +} +``` + +**Step 2: 创建 conflict-resolver.ts** + +```typescript +import type { Feature } from '../types.js' +import type { EventBus } from '../events/event-bus.js' + +export interface ConflictResolution { + featureId: string + choice: 'local' | 'remote' +} + +export class ConflictResolver { + private pendingConflicts: Map = new Map() + + constructor(private eventBus: EventBus) {} + + detectConflict(local: Feature, remote: Feature): boolean { + // Conflict if both have been updated since last sync + if (!local.synced_at) return false + + const localUpdated = new Date(local.updated_at).getTime() + const remoteUpdated = new Date(remote.updated_at).getTime() + const lastSync = new Date(local.synced_at).getTime() + + return localUpdated > lastSync && remoteUpdated > lastSync + } + + registerConflict(local: Feature, remote: Feature) { + this.pendingConflicts.set(local.id, { local, remote }) + this.eventBus.conflictDetected(local.id, local, remote) + } + + resolveConflict(featureId: string, choice: 'local' | 'remote'): Feature | null { + const conflict = this.pendingConflicts.get(featureId) + if (!conflict) return null + + this.pendingConflicts.delete(featureId) + return choice === 'local' ? conflict.local : conflict.remote + } + + getPendingConflicts(): string[] { + return Array.from(this.pendingConflicts.keys()) + } +} +``` + +**Step 3: 创建测试** + +```typescript +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { ConflictResolver } from '../sync/conflict-resolver.js' +import { EventBus } from '../events/event-bus.js' +import type { Feature } from '../types.js' + +describe('ConflictResolver', () => { + let resolver: ConflictResolver + let eventBus: EventBus + + beforeEach(() => { + eventBus = new EventBus() + resolver = new ConflictResolver(eventBus) + }) + + it('detects conflict when both local and remote updated after sync', () => { + const local: Feature = { + id: 'f1', + title: 'Test', + status: 'active', + owner: null, + priority: null, + branch: 'main', + teams: [], + tags: [], + blocked_by: [], + target_release: null, + created_at: '2026-03-01T00:00:00Z', + updated_at: '2026-03-05T10:00:00Z', + meta_yaml: null, + design_md: null, + plan_md: null, + log_md: null, + synced_at: '2026-03-04T00:00:00Z', + } + + const remote: Feature = { + ...local, + updated_at: '2026-03-05T09:00:00Z', + } + + expect(resolver.detectConflict(local, remote)).toBe(true) + }) + + it('resolves conflict with chosen version', () => { + const local = { id: 'f1', title: 'Local Title' } as Feature + const remote = { id: 'f1', title: 'Remote Title' } as Feature + + resolver.registerConflict(local, remote) + + const resolved = resolver.resolveConflict('f1', 'remote') + expect(resolved?.title).toBe('Remote Title') + expect(resolver.getPendingConflicts()).toEqual([]) + }) +}) +``` + +**Step 4: 运行测试** + +Run: `cd mcp-server && bun test` +Expected: All tests pass + +**Step 5: Commit** + +```bash +git add mcp-server/src/sync/ +git commit -m "feat(mcp-server): add GitHub sync worker with conflict detection" +``` + +--- + +## Phase 5: 集成测试与文档 + +### Task 5.1: 添加集成测试 + +**Files:** +- Create: `mcp-server/src/__tests__/integration.test.ts` + +**Step 1: 创建集成测试** + +```typescript +import { describe, it, expect, beforeEach, afterEach } from 'vitest' +import Database from 'better-sqlite3' +import { createDb } from '../store/db.js' +import { FeatureStore } from '../store/feature-store.js' +import { EventBus } from '../events/event-bus.js' +import { createHttpApp } from '../http/routes.js' + +describe('Integration: HTTP API', () => { + let db: Database.Database + let store: FeatureStore + let eventBus: EventBus + let app: ReturnType + + beforeEach(() => { + db = createDb(':memory:') + store = new FeatureStore(db) + eventBus = new EventBus() + app = createHttpApp(store, eventBus) + }) + + afterEach(() => { + db.close() + }) + + it('creates and retrieves feature via API', async () => { + // Create + const createRes = await app.request('/api/features', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: 'test-api', title: 'API Test' }), + }) + expect(createRes.status).toBe(201) + + // List + const listRes = await app.request('/api/features') + const features = await listRes.json() + expect(features.length).toBe(1) + expect(features[0].id).toBe('test-api') + + // Get single + const getRes = await app.request('/api/features/test-api') + const feature = await getRes.json() + expect(feature.title).toBe('API Test') + }) + + it('updates feature status', async () => { + store.create({ id: 'f1', title: 'Feature 1' }) + + const res = await app.request('/api/features/f1/status', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status: 'active' }), + }) + + expect(res.status).toBe(200) + const updated = await res.json() + expect(updated.status).toBe('active') + }) + + it('returns board aggregation', async () => { + store.create({ id: 'f1', title: 'F1', status: 'planning' }) + store.create({ id: 'f2', title: 'F2', status: 'active' }) + + const res = await app.request('/api/board') + const board = await res.json() + + expect(board.featuresByStatus.planning.length).toBe(1) + expect(board.featuresByStatus.active.length).toBe(1) + }) +}) +``` + +**Step 2: 运行所有测试** + +Run: `cd mcp-server && bun test` +Expected: All tests pass + +**Step 3: Commit** + +```bash +git add mcp-server/src/__tests__/ +git commit -m "test(mcp-server): add integration tests for HTTP API" +``` + +--- + +### Task 5.2: 更新 Makefile 和 README + +**Files:** +- Modify: `Makefile` (if exists) or create +- Create: `mcp-server/README.md` + +**Step 1: 创建 mcp-server/README.md** + +```markdown +# SuperCrew MCP Server + +Local MCP server for real-time feature management with Claude Code and Web UI. + +## Quick Start + +```bash +# Install dependencies +bun install + +# Run in HTTP mode (for Web UI) +bun run src/index.ts + +# Run in MCP mode (auto-detected when stdin is piped) +echo '{"method":"tools/list"}' | bun run src/index.ts +``` + +## Configuration + +| Environment Variable | Default | Description | +|---------------------|---------|-------------| +| `MCP_HTTP_PORT` | 3456 | HTTP API port | +| `MCP_WS_PORT` | 3457 | WebSocket port | + +## API Endpoints + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/health` | Health check | +| GET | `/api/features` | List all features | +| GET | `/api/features/:id` | Get feature by ID | +| POST | `/api/features` | Create feature | +| PATCH | `/api/features/:id/status` | Update status | +| DELETE | `/api/features/:id` | Delete feature | +| GET | `/api/board` | Get board aggregation | + +## MCP Tools + +| Tool | Description | +|------|-------------| +| `list_features` | List all features | +| `get_feature` | Get feature details | +| `create_feature` | Create new feature | +| `update_feature_status` | Update status | +| `log_progress` | Append log entry | + +## WebSocket Events + +Connect to `ws://localhost:3457` for real-time updates: + +- `feature:created` - New feature added +- `feature:updated` - Feature modified +- `feature:deleted` - Feature removed +- `conflict:detected` - Sync conflict detected +``` + +**Step 2: Commit** + +```bash +git add mcp-server/README.md +git commit -m "docs(mcp-server): add README with usage instructions" +``` + +--- + +### Task 5.3: 最终验证 + +**Step 1: 完整测试套件** + +Run: `cd mcp-server && bun test` +Expected: All tests pass + +**Step 2: TypeScript 检查** + +Run: `cd mcp-server && bun run typecheck` +Expected: No errors + +**Step 3: 手动测试 HTTP API** + +```bash +cd mcp-server && bun run src/index.ts & +curl -X POST http://localhost:3456/api/features \ + -H "Content-Type: application/json" \ + -d '{"id":"test","title":"Test Feature"}' +curl http://localhost:3456/api/board +``` + +**Step 4: Final Commit** + +```bash +git add . +git commit -m "feat(mcp-server): complete MCP Server implementation" +``` + +--- + +## Summary + +| Phase | Tasks | Estimated Time | +|-------|-------|----------------| +| Phase 1 | Project setup, SQLite, Event Bus, Branch Scanner | 1 week | +| Phase 2 | MCP Protocol integration | 0.5 week | +| Phase 3 | HTTP API + WebSocket | 1 week | +| Phase 4 | GitHub Sync Worker | 0.5 week | +| Phase 5 | Integration tests + docs | 0.5 week | + +**Total: ~3.5 weeks** diff --git a/docs/plans/2026-03-05-vibe-kanban-comparison.md b/docs/plans/2026-03-05-vibe-kanban-comparison.md new file mode 100644 index 0000000..0f31a21 --- /dev/null +++ b/docs/plans/2026-03-05-vibe-kanban-comparison.md @@ -0,0 +1,182 @@ +# Vibe Kanban vs SuperCrew 对比分析 + +**Date:** 2026-03-05 +**Status:** Analysis + +--- + +## 一、产品定位 + +| 维度 | Vibe Kanban | SuperCrew | +|------|-------------|-----------| +| **一句话定位** | AI Agent 执行环境管理器 | 团队 Feature 生命周期可视化 | +| **核心问题** | "如何让多个 AI Agent 高效并行工作" | "团队如何共享 Feature 进度" | +| **目标用户** | 个人开发者 (指挥多 Agent) | 团队 (共享可视化看板) | +| **设计哲学** | Agent-centric (以 Agent 为中心) | Feature-centric (以 Feature 为中心) | + +--- + +## 二、数据架构 + +| 维度 | Vibe Kanban | SuperCrew | +|------|-------------|-----------| +| **数据归属** | 本地 SQLite + Git worktree | GitHub 远端 repo (`.supercrew/features/`) | +| **持久化** | 本地 SQLite 数据库 | GitHub Contents API (只读) + Vercel KV (用户绑定) | +| **同步方式** | 本地文件,无需 push | **必须 push 才能同步** | +| **离线能力** | 完全支持 | 需要网络连接 | +| **写入能力** | SQLite 直接写 | UI 只读,需本地 Claude Code 写入后 push | + +### SuperCrew 数据流 + +``` +Claude Code (本地写入 .supercrew/features/) + ↓ git push +GitHub Repo (远端存储) + ↓ GitHub Contents API +Kanban Backend (只读获取) + ↓ +Kanban Frontend (可视化展示) +``` + +--- + +## 三、Task/Feature 管理 + +| 维度 | Vibe Kanban | SuperCrew | +|------|-------------|-----------| +| **管理粒度** | Issue/Task 级 | Feature 级 (含完整 spec) | +| **编辑入口** | UI 直接编辑 | Claude Code CLI 编辑 → push | +| **状态列** | 自定义 | 固定 6 列:planning → designing → ready → active → blocked → done | +| **上下文深度** | 轻量 issue 描述 | 完整 spec:meta.yaml + design.md + plan.md + log.md | +| **进度追踪** | Agent 执行状态 | plan.md 中的 checklist 进度 | + +### SuperCrew Feature 结构 + +``` +.supercrew/features// +├── meta.yaml # ID, title, status, owner, priority, dates +├── design.md # 需求、架构、约束 +├── plan.md # 任务分解、checklist、进度 +└── log.md # 时间线进展记录 +``` + +--- + +## 四、AI Agent 集成 + +| 维度 | Vibe Kanban | SuperCrew | +|------|-------------|-----------| +| **支持 Agent** | 10+ (Claude, Codex, Copilot, Cursor...) | Claude Code 专属 | +| **执行环境** | 每 workspace 独立 branch/terminal/devserver | 用户本地 Claude Code session | +| **连接方式** | MCP Server (UI ↔ Agent 双向) | Skills/Hooks (单向上下文加载) | +| **交互闭环** | UI inline comment → Agent 即时响应 | 手动更新 log.md → push | +| **环境隔离** | 自动创建 Git worktree | 用户手动管理分支 | + +### Vibe Kanban 执行流 + +``` +UI 创建 Issue → 自动创建 Workspace (worktree) +→ Agent 在隔离环境执行 → UI 实时显示 diff +→ Inline 反馈 → Agent 修改 → 满意后 PR +``` + +### SuperCrew 执行流 + +``` +/supercrew:new-feature → 创建 spec 文件 +→ /supercrew:work-on → 加载上下文到 Claude Code +→ 手动开发 → 更新 log.md → git push +→ Kanban 刷新看到新状态 +``` + +--- + +## 五、工作流对比 + +| 阶段 | Vibe Kanban | SuperCrew | +|------|-------------|-----------| +| **创建任务** | UI 直接创建 Issue | CLI: `/supercrew:new-feature` | +| **分配执行** | UI 选择 Agent + 创建 Workspace | CLI: `/supercrew:work-on ` | +| **执行过程** | Agent 自动执行,UI 实时 diff 预览 | 开发者在 Claude Code 中工作 | +| **反馈迭代** | UI inline comment → Agent 即时响应 | 手动编辑 → commit → push | +| **代码审查** | UI 内置 diff 审查 + inline 评论 | GitHub PR 流程 | +| **合并完成** | AI 生成 PR 描述 → 一键合并 | 标准 GitHub PR 流程 | + +--- + +## 六、用户关注点 + +| 用户角色 | Vibe Kanban 关注 | SuperCrew 关注 | +|----------|-----------------|----------------| +| **开发者** | Agent 执行状态、diff 预览、inline 反馈 | Feature 当前阶段、plan 进度、阻塞原因 | +| **队友** | (单人场景为主) | 谁在做什么、整体进度、依赖关系 | +| **管理者** | (不适用) | Feature 状态分布、瓶颈在哪、燃尽趋势 | + +--- + +## 七、技术架构 + +| 维度 | Vibe Kanban | SuperCrew | +|------|-------------|-----------| +| **后端语言** | Rust | TypeScript (Bun/Node.js + Hono) | +| **前端** | TypeScript + 自研 | React + Vite + TanStack | +| **数据库** | SQLite (sqlx-cli) | 无 DB,GitHub API + Vercel KV | +| **部署** | 本地 / 自托管 Docker | Vercel Serverless | +| **Agent 通信** | MCP Server | 无直接通信,靠 git 同步 | + +--- + +## 八、核心差异总结 + +| 差异点 | Vibe Kanban | SuperCrew | +|--------|-------------|-----------| +| **数据所有权** | 用户本地 | 用户 GitHub repo | +| **实时性** | 本地即时 | 依赖 push + API 延迟 | +| **UI 写入** | 支持 | 只读 | +| **多 Agent** | 支持 10+ Agent 切换 | 仅 Claude Code | +| **团队协作** | 弱 (本地为主) | 强 (GitHub 天然共享) | +| **环境隔离** | 自动 worktree | 手动分支管理 | +| **审查体验** | 内置 diff + inline | 依赖 GitHub PR | + +--- + +## 九、优劣势分析 + +### Vibe Kanban 优势 + +1. **即时反馈** — 本地 SQLite + MCP,Agent 执行和反馈都是实时的 +2. **多 Agent 支持** — 可以在不同任务间切换 Agent +3. **环境隔离** — 自动 worktree,每个任务独立分支/终端 +4. **一体化体验** — UI 集成了 diff 审查、inline 评论、PR 生成 +5. **离线工作** — 不依赖网络 + +### Vibe Kanban 劣势 + +1. **单人场景** — 团队协作能力弱 +2. **本地绑定** — 数据在本地,换机器不好迁移 +3. **技术门槛** — Rust + SQLite,定制成本高 + +### SuperCrew 优势 + +1. **团队可见** — GitHub 天然共享,团队成员都能看到进度 +2. **Feature 深度** — 完整 spec (design + plan + log),不只是轻量 issue +3. **Git 原生** — 数据就是代码仓库的一部分,天然版本控制 +4. **低运维** — Vercel serverless,无需自己维护服务器 +5. **结构化流程** — brainstorming → writing-plans → subagent-driven-development + +### SuperCrew 劣势 + +1. **同步延迟** — 必须 push 才能更新,UI 只读 +2. **单 Agent** — 仅支持 Claude Code +3. **无实时反馈** — 没有 MCP 双向通信 +4. **无环境隔离** — 需用户手动管理分支 + +--- + +## 十、定位总结 + +| | Vibe Kanban | SuperCrew | +|---|-------------|-----------| +| **核心场景** | 个人 vibe coding session | 团队 feature 进度追踪 | +| **类比** | "AI Agent 的 IDE" | "团队 Feature 的 Dashboard" | +| **价值主张** | 提升个人 Agent 执行效率 | 提升团队 Feature 可见度 | diff --git a/mcp-server/README.md b/mcp-server/README.md new file mode 100644 index 0000000..45b75e1 --- /dev/null +++ b/mcp-server/README.md @@ -0,0 +1,114 @@ +# SuperCrew MCP Server + +Local MCP server for real-time feature management with Claude Code and Web UI. + +## Quick Start + +```bash +# Install dependencies +cd mcp-server +bun install + +# Run in HTTP mode (for Web UI) +bun run src/index.ts + +# Run in MCP mode (auto-detected when stdin is piped) +echo '{"method":"tools/list"}' | bun run src/index.ts +``` + +## Configuration + +| Environment Variable | Default | Description | +|---------------------|---------|-------------| +| `MCP_HTTP_PORT` | 3456 | HTTP API port | +| `MCP_WS_PORT` | 3457 | WebSocket port | + +## API Endpoints + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/health` | Health check | +| GET | `/api/features` | List all features | +| GET | `/api/features/:id` | Get feature by ID | +| POST | `/api/features` | Create feature | +| PATCH | `/api/features/:id/status` | Update status | +| DELETE | `/api/features/:id` | Delete feature | +| GET | `/api/board` | Get board aggregation | + +## MCP Tools + +| Tool | Description | +|------|-------------| +| `list_features` | List all features | +| `get_feature` | Get feature details | +| `create_feature` | Create a new feature | +| `update_feature_status` | Update feature status | +| `log_progress` | Append to feature log | + +## Dual Mode Operation + +The server automatically detects its runtime mode: + +- **MCP Mode**: When stdin is not a TTY (piped from Claude Code), runs as MCP server using stdio transport +- **HTTP Mode**: When stdin is a TTY (terminal), runs HTTP + WebSocket servers for Web UI + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ SuperCrew MCP Server │ +│ (runs on localhost:3456/3457) │ +│ │ +│ ┌───────────────┐ ┌───────────────┐ ┌─────────────────────────┐ │ +│ │ Feature Store │ │ Event Bus │ │ GitHub Sync Worker │ │ +│ │ (SQLite) │ │ (broadcasts) │ │ (async push/pull) │ │ +│ └───────────────┘ └───────────────┘ └─────────────────────────┘ │ +│ │ │ │ │ +│ ┌──────┴───────────────────┴───────────────────────┴─────────────┐ │ +│ │ Branch Scanner │ │ +│ │ (scans all branches, aggregates features) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +└──────────┬───────────────────┬───────────────────┬──────────────────┘ + │ │ │ + MCP Protocol WebSocket HTTP API + │ │ │ + ▼ ▼ ▼ + ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ + │ Claude Code │ │ Web UI │ │ Clients │ + │ (Agent) │ │ (Browser) │ │ │ + └─────────────┘ └─────────────┘ └─────────────┘ +``` + +## WebSocket Events + +The WebSocket server broadcasts real-time events: + +| Event | Payload | Description | +|-------|---------|-------------| +| `connected` | `{ clientCount }` | Connection confirmed | +| `feature:created` | `{ feature }` | Feature created | +| `feature:updated` | `{ feature }` | Feature updated | +| `feature:deleted` | `{ featureId }` | Feature deleted | +| `conflict:detected` | `{ featureId, local, remote }` | Conflict detected | + +## Development + +```bash +# Run tests +bun test + +# Type check +bun run typecheck + +# Watch mode +bun run dev +``` + +## Data Storage + +- **SQLite**: Primary data store at `.supercrew/.mcp-server.db` +- **Git**: Features synced to `.supercrew/features//` as backup + +## License + +MIT diff --git a/mcp-server/bun.lock b/mcp-server/bun.lock new file mode 100644 index 0000000..58b7a13 --- /dev/null +++ b/mcp-server/bun.lock @@ -0,0 +1,517 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "@supercrew/mcp-server", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0", + "better-sqlite3": "^11.0.0", + "gray-matter": "^4.0.3", + "hono": "^4.12.3", + "js-yaml": "^4.1.1", + "ws": "^8.18.0", + "zod": "^3.23.0", + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.11", + "@types/bun": "^1.2.9", + "@types/js-yaml": "^4.0.9", + "@types/ws": "^8.5.12", + "typescript": "^5.0.0", + "vitest": "^3.2.4", + }, + }, + }, + "packages": { + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], + + "@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], + + "@types/better-sqlite3": ["@types/better-sqlite3@7.6.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA=="], + + "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], + + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="], + + "@types/node": ["@types/node@25.3.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ=="], + + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + + "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], + + "@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], + + "@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="], + + "@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="], + + "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], + + "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], + + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "better-sqlite3": ["better-sqlite3@11.10.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ=="], + + "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], + + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], + + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], + + "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], + + "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + + "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], + + "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], + + "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + + "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], + + "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], + + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + + "express-rate-limit": ["express-rate-limit@8.2.1", "", { "dependencies": { "ip-address": "10.0.1" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g=="], + + "extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], + + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "gray-matter": ["gray-matter@4.0.3", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "hono": ["hono@4.12.5", "", {}, "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg=="], + + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + + "ip-address": ["ip-address@10.0.1", "", {}, "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="], + + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jose": ["jose@6.2.0", "", {}, "sha512-xsfE1TcSCbUdo6U07tR0mvhg0flGxU8tPLbF03mirl2ukGQENhUg4ubGYQnhVH0b5stLlPM+WOqDkEl1R1y5sQ=="], + + "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + + "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], + + "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], + + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "node-abi": ["node-abi@3.87.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], + + "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], + + "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + + "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], + + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], + + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="], + + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "send": ["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" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], + + "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="], + + "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + + "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], + + "tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], + + "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], + + "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], + + "tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], + + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], + + "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], + + "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@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.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], + + "gray-matter/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], + + "gray-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + } +} diff --git a/mcp-server/package.json b/mcp-server/package.json new file mode 100644 index 0000000..8da546b --- /dev/null +++ b/mcp-server/package.json @@ -0,0 +1,38 @@ +{ + "name": "@supercrew/mcp-server", + "version": "0.1.0", + "description": "Local MCP server for SuperCrew feature management with real-time sync", + "type": "module", + "main": "src/index.ts", + "bin": { + "supercrew-mcp": "./src/index.ts" + }, + "scripts": { + "dev": "bun run --watch src/index.ts", + "start": "bun run src/index.ts", + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0", + "better-sqlite3": "^11.0.0", + "hono": "^4.12.3", + "ws": "^8.18.0", + "gray-matter": "^4.0.3", + "js-yaml": "^4.1.1", + "zod": "^3.23.0" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.11", + "@types/bun": "^1.2.9", + "@types/js-yaml": "^4.0.9", + "@types/ws": "^8.5.12", + "vitest": "^3.2.4", + "typescript": "^5.0.0" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/iamtouchskyer/supercrew" + } +} diff --git a/mcp-server/src/__tests__/branch-scanner.test.ts b/mcp-server/src/__tests__/branch-scanner.test.ts new file mode 100644 index 0000000..12b5238 --- /dev/null +++ b/mcp-server/src/__tests__/branch-scanner.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect } from 'vitest' +import { parseMetaYaml } from '../scanner/feature-parser.js' + +describe('Feature Parser', () => { + it('parses meta.yaml correctly', () => { + const content = ` +id: login-feature +title: User Login +status: active +owner: alice +priority: P1 +teams: + - frontend + - backend +tags: + - auth +` + const result = parseMetaYaml(content, 'fallback-id') + + expect(result.id).toBe('login-feature') + expect(result.title).toBe('User Login') + expect(result.status).toBe('active') + expect(result.owner).toBe('alice') + expect(result.priority).toBe('P1') + expect(result.teams).toEqual(['frontend', 'backend']) + expect(result.tags).toEqual(['auth']) + }) + + it('uses fallback id when not in yaml', () => { + const content = ` +title: Some Feature +status: planning +` + const result = parseMetaYaml(content, 'fallback-id') + expect(result.id).toBe('fallback-id') + }) + + it('handles minimal yaml', () => { + const content = `title: Minimal` + const result = parseMetaYaml(content, 'test-id') + + expect(result.title).toBe('Minimal') + expect(result.status).toBe('planning') + expect(result.owner).toBeNull() + expect(result.priority).toBeNull() + expect(result.teams).toEqual([]) + expect(result.tags).toEqual([]) + }) +}) diff --git a/mcp-server/src/__tests__/event-bus.test.ts b/mcp-server/src/__tests__/event-bus.test.ts new file mode 100644 index 0000000..ea69702 --- /dev/null +++ b/mcp-server/src/__tests__/event-bus.test.ts @@ -0,0 +1,104 @@ +import { describe, it, expect, vi } from 'vitest' +import { EventBus } from '../events/event-bus.js' +import type { Feature } from '../types.js' + +describe('EventBus', () => { + it('emits feature:created event', () => { + const bus = new EventBus() + const handler = vi.fn() + + bus.on('feature:created', handler) + + const feature: Feature = { + id: 'test', + title: 'Test', + status: 'planning', + owner: null, + priority: null, + branch: 'main', + teams: [], + tags: [], + blocked_by: [], + target_release: null, + created_at: '2026-03-05', + updated_at: '2026-03-05', + meta_yaml: null, + design_md: null, + plan_md: null, + log_md: null, + synced_at: null, + } + + bus.featureCreated(feature) + + expect(handler).toHaveBeenCalledWith(feature) + }) + + it('emits conflict:detected event', () => { + const bus = new EventBus() + const handler = vi.fn() + + bus.on('conflict:detected', handler) + + const local = { id: 'f1' } as Feature + const remote = { id: 'f1' } as Feature + + bus.conflictDetected('f1', local, remote) + + expect(handler).toHaveBeenCalledWith('f1', local, remote) + }) + + it('emits feature:updated event', () => { + const bus = new EventBus() + const handler = vi.fn() + bus.on('feature:updated', handler) + + const feature = { id: 'test' } as Feature + bus.featureUpdated(feature) + + expect(handler).toHaveBeenCalledWith(feature) + }) + + it('emits feature:deleted event', () => { + const bus = new EventBus() + const handler = vi.fn() + bus.on('feature:deleted', handler) + + bus.featureDeleted('test-id') + + expect(handler).toHaveBeenCalledWith('test-id') + }) + + it('emits sync:started event', () => { + const bus = new EventBus() + const handler = vi.fn() + bus.on('sync:started', handler) + + bus.syncStarted() + + expect(handler).toHaveBeenCalledWith() + }) + + it('emits sync:completed event', () => { + const bus = new EventBus() + const handler = vi.fn() + bus.on('sync:completed', handler) + + bus.syncCompleted(5) + + expect(handler).toHaveBeenCalledWith(5) + }) + + it('cleans up listeners with dispose', () => { + const bus = new EventBus() + const handler = vi.fn() + bus.on('feature:created', handler) + + bus.dispose() + + const feature = { id: 'test' } as Feature + bus.featureCreated(feature) + + expect(handler).not.toHaveBeenCalled() + }) +}) diff --git a/mcp-server/src/__tests__/feature-store.test.ts b/mcp-server/src/__tests__/feature-store.test.ts new file mode 100644 index 0000000..297338e --- /dev/null +++ b/mcp-server/src/__tests__/feature-store.test.ts @@ -0,0 +1,200 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { createDb } from '../store/db' +import { FeatureStore } from '../store/feature-store' +import type { FeatureInput } from '../types' + +describe('FeatureStore', () => { + let store: FeatureStore + + beforeEach(() => { + // Use in-memory database for tests + const db = createDb(':memory:') + store = new FeatureStore(db) + }) + + it('creates and retrieves a feature', () => { + const input: FeatureInput = { + id: 'test-feature-1', + title: 'Test Feature', + status: 'planning', + owner: 'alice', + priority: 'P1', + branch: 'feature/test', + teams: ['frontend', 'backend'], + tags: ['enhancement'] + } + + const created = store.create(input) + + expect(created.id).toBe('test-feature-1') + expect(created.title).toBe('Test Feature') + expect(created.status).toBe('planning') + expect(created.owner).toBe('alice') + expect(created.priority).toBe('P1') + expect(created.branch).toBe('feature/test') + expect(created.teams).toEqual(['frontend', 'backend']) + expect(created.tags).toEqual(['enhancement']) + expect(created.blocked_by).toEqual([]) + expect(created.created_at).toBeTruthy() + expect(created.updated_at).toBeTruthy() + + const retrieved = store.get('test-feature-1') + expect(retrieved).toEqual(created) + }) + + it('lists all features', () => { + store.create({ id: 'feature-1', title: 'Feature One' }) + store.create({ id: 'feature-2', title: 'Feature Two' }) + store.create({ id: 'feature-3', title: 'Feature Three' }) + + const all = store.listAll() + expect(all).toHaveLength(3) + expect(all.map(f => f.id)).toContain('feature-1') + expect(all.map(f => f.id)).toContain('feature-2') + expect(all.map(f => f.id)).toContain('feature-3') + }) + + it('updates feature status', () => { + store.create({ id: 'feature-1', title: 'Feature One', status: 'planning' }) + + const updated = store.updateStatus('feature-1', 'active') + expect(updated).toBeTruthy() + expect(updated?.status).toBe('active') + + const retrieved = store.get('feature-1') + expect(retrieved?.status).toBe('active') + }) + + it('updates feature plan', () => { + store.create({ id: 'feature-1', title: 'Feature One' }) + + const planContent = '## Plan\n\n1. Step one\n2. Step two' + const updated = store.updatePlan('feature-1', planContent) + + expect(updated).toBeTruthy() + expect(updated?.plan_md).toBe(planContent) + + const retrieved = store.get('feature-1') + expect(retrieved?.plan_md).toBe(planContent) + }) + + it('appends to log', () => { + store.create({ id: 'feature-1', title: 'Feature One' }) + + const entry1 = '2024-01-01: Started implementation' + store.appendLog('feature-1', entry1) + + let feature = store.get('feature-1') + expect(feature?.log_md).toBe(entry1) + + const entry2 = '2024-01-02: Completed testing' + store.appendLog('feature-1', entry2) + + feature = store.get('feature-1') + expect(feature?.log_md).toBe(`${entry1}\n\n${entry2}`) + }) + + it('deletes a feature', () => { + store.create({ id: 'feature-1', title: 'Feature One' }) + + expect(store.get('feature-1')).toBeTruthy() + + const deleted = store.delete('feature-1') + expect(deleted).toBe(true) + + expect(store.get('feature-1')).toBeNull() + }) + + it('returns null when getting non-existent feature', () => { + const feature = store.get('non-existent') + expect(feature).toBeNull() + }) + + it('returns null when updating non-existent feature', () => { + const updated = store.updateStatus('non-existent', 'active') + expect(updated).toBeNull() + }) + + it('returns false when deleting non-existent feature', () => { + const deleted = store.delete('non-existent') + expect(deleted).toBe(false) + }) + + it('upserts from GitHub - creates new feature', () => { + const feature = { + id: 'github-feature-1', + title: 'GitHub Feature', + status: 'ready' as const, + owner: 'bob', + priority: 'P2' as const, + branch: 'feature/github', + teams: ['platform'], + tags: ['sync'], + blocked_by: [], + target_release: 'v1.0', + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + meta_yaml: 'key: value', + design_md: '## Design', + plan_md: '## Plan', + log_md: '## Log', + synced_at: new Date().toISOString() + } + + store.upsertFromGitHub(feature) + + const retrieved = store.get('github-feature-1') + expect(retrieved).toBeTruthy() + expect(retrieved?.title).toBe('GitHub Feature') + expect(retrieved?.status).toBe('ready') + }) + + it('upserts from GitHub - updates existing feature', () => { + store.create({ id: 'feature-1', title: 'Original Title', status: 'planning' }) + + const updated = { + id: 'feature-1', + title: 'Updated Title', + status: 'active' as const, + owner: 'charlie', + priority: 'P0' as const, + branch: 'main', + teams: ['ops'], + tags: ['urgent'], + blocked_by: [], + target_release: 'v2.0', + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + meta_yaml: null, + design_md: null, + plan_md: '## Updated Plan', + log_md: null, + synced_at: new Date().toISOString() + } + + store.upsertFromGitHub(updated) + + const retrieved = store.get('feature-1') + expect(retrieved?.title).toBe('Updated Title') + expect(retrieved?.status).toBe('active') + expect(retrieved?.owner).toBe('charlie') + expect(retrieved?.plan_md).toBe('## Updated Plan') + }) + + it('handles features with default values', () => { + const input: FeatureInput = { + id: 'minimal-feature', + title: 'Minimal Feature' + } + + const created = store.create(input) + + expect(created.status).toBe('planning') + expect(created.owner).toBeNull() + expect(created.priority).toBeNull() + expect(created.branch).toBe('main') + expect(created.teams).toEqual([]) + expect(created.tags).toEqual([]) + expect(created.blocked_by).toEqual([]) + }) +}) diff --git a/mcp-server/src/__tests__/github-sync.test.ts b/mcp-server/src/__tests__/github-sync.test.ts new file mode 100644 index 0000000..dfcfa0f --- /dev/null +++ b/mcp-server/src/__tests__/github-sync.test.ts @@ -0,0 +1,101 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { ConflictResolver } from '../sync/conflict-resolver.js' +import { EventBus } from '../events/event-bus.js' +import type { Feature } from '../types.js' + +describe('ConflictResolver', () => { + let resolver: ConflictResolver + let eventBus: EventBus + + beforeEach(() => { + eventBus = new EventBus() + resolver = new ConflictResolver(eventBus) + }) + + it('detects conflict when both local and remote updated after sync', () => { + const now = Date.now() + const local: Feature = { + id: 'f1', + title: 'Test', + status: 'active', + owner: null, + priority: null, + branch: 'main', + teams: [], + tags: [], + blocked_by: [], + target_release: null, + created_at: new Date(now - 10000).toISOString(), + updated_at: new Date(now - 1000).toISOString(), // Updated after sync + meta_yaml: null, + design_md: null, + plan_md: null, + log_md: null, + synced_at: new Date(now - 5000).toISOString(), // Synced before update + } + + const remote: Feature = { + ...local, + updated_at: new Date(now - 2000).toISOString(), // Also updated after sync + } + + expect(resolver.detectConflict(local, remote)).toBe(true) + }) + + it('does not detect conflict when only local updated', () => { + const now = Date.now() + const local: Feature = { + id: 'f1', + title: 'Test', + status: 'active', + owner: null, + priority: null, + branch: 'main', + teams: [], + tags: [], + blocked_by: [], + target_release: null, + created_at: new Date(now - 10000).toISOString(), + updated_at: new Date(now - 1000).toISOString(), + meta_yaml: null, + design_md: null, + plan_md: null, + log_md: null, + synced_at: new Date(now - 5000).toISOString(), + } + + const remote: Feature = { + ...local, + updated_at: new Date(now - 6000).toISOString(), // Updated before sync + } + + expect(resolver.detectConflict(local, remote)).toBe(false) + }) + + it('registers and resolves conflict with local choice', () => { + const local = { id: 'f1', title: 'Local' } as Feature + const remote = { id: 'f1', title: 'Remote' } as Feature + + const handler = vi.fn() + eventBus.on('conflict:detected', handler) + + resolver.registerConflict(local, remote) + + expect(handler).toHaveBeenCalledWith('f1', local, remote) + expect(resolver.getPendingConflicts()).toContain('f1') + + const resolved = resolver.resolveConflict('f1', 'local') + expect(resolved?.title).toBe('Local') + expect(resolver.getPendingConflicts()).not.toContain('f1') + }) + + it('registers and resolves conflict with remote choice', () => { + const local = { id: 'f1', title: 'Local' } as Feature + const remote = { id: 'f1', title: 'Remote' } as Feature + + resolver.registerConflict(local, remote) + + const resolved = resolver.resolveConflict('f1', 'remote') + expect(resolved?.title).toBe('Remote') + }) +}) diff --git a/mcp-server/src/__tests__/integration.test.ts b/mcp-server/src/__tests__/integration.test.ts new file mode 100644 index 0000000..41730bd --- /dev/null +++ b/mcp-server/src/__tests__/integration.test.ts @@ -0,0 +1,115 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest' +import type { Database } from 'bun:sqlite' +import { createDb } from '../store/db.js' +import { FeatureStore } from '../store/feature-store.js' +import { EventBus } from '../events/event-bus.js' +import { createHttpApp } from '../http/routes.js' + +describe('Integration: HTTP API', () => { + let db: Database + let store: FeatureStore + let eventBus: EventBus + let app: ReturnType + + beforeEach(() => { + db = createDb(':memory:') + store = new FeatureStore(db) + eventBus = new EventBus() + app = createHttpApp(store, eventBus) + }) + + afterEach(() => { + db.close() + eventBus.dispose() + }) + + it('creates and retrieves feature via API', async () => { + // Create + const createRes = await app.request('/api/features', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: 'test-api', title: 'API Test' }), + }) + expect(createRes.status).toBe(201) + + // List + const listRes = await app.request('/api/features') + const features = await listRes.json() + expect(features.length).toBe(1) + expect(features[0].id).toBe('test-api') + + // Get single + const getRes = await app.request('/api/features/test-api') + const feature = await getRes.json() + expect(feature.title).toBe('API Test') + }) + + it('updates feature status', async () => { + store.create({ id: 'f1', title: 'Feature 1' }) + + const res = await app.request('/api/features/f1/status', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status: 'active' }), + }) + + expect(res.status).toBe(200) + const updated = await res.json() + expect(updated.status).toBe('active') + }) + + it('returns board aggregation', async () => { + store.create({ id: 'f1', title: 'F1', status: 'planning' }) + store.create({ id: 'f2', title: 'F2', status: 'active' }) + + const res = await app.request('/api/board') + const board = await res.json() + + expect(board.featuresByStatus.planning.length).toBe(1) + expect(board.featuresByStatus.active.length).toBe(1) + }) + + it('returns 404 for non-existent feature', async () => { + const res = await app.request('/api/features/non-existent') + expect(res.status).toBe(404) + }) + + it('deletes a feature', async () => { + store.create({ id: 'to-delete', title: 'To Delete' }) + + const deleteRes = await app.request('/api/features/to-delete', { + method: 'DELETE', + }) + expect(deleteRes.status).toBe(200) + + const getRes = await app.request('/api/features/to-delete') + expect(getRes.status).toBe(404) + }) + + it('health check returns ok', async () => { + const res = await app.request('/health') + expect(res.status).toBe(200) + const data = await res.json() + expect(data.ok).toBe(true) + }) + + it('returns 400 for invalid feature input', async () => { + const res = await app.request('/api/features', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ invalid: 'data' }), // Missing required fields + }) + expect(res.status).toBe(400) + }) + + it('returns 400 for invalid status update', async () => { + store.create({ id: 'f1', title: 'F1' }) + + const res = await app.request('/api/features/f1/status', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status: 'invalid-status' }), + }) + expect(res.status).toBe(400) + }) +}) diff --git a/mcp-server/src/events/event-bus.ts b/mcp-server/src/events/event-bus.ts new file mode 100644 index 0000000..89240d2 --- /dev/null +++ b/mcp-server/src/events/event-bus.ts @@ -0,0 +1,64 @@ +import { EventEmitter } from 'events' +import type { Feature } from '../types.js' + +export type FeatureEvent = + | { type: 'feature:created'; feature: Feature } + | { type: 'feature:updated'; feature: Feature } + | { type: 'feature:deleted'; featureId: string } + | { type: 'sync:started' } + | { type: 'sync:completed'; count: number } + | { type: 'conflict:detected'; featureId: string; local: Feature; remote: Feature } + +type EventMap = { + 'feature:created': [feature: Feature] + 'feature:updated': [feature: Feature] + 'feature:deleted': [featureId: string] + 'sync:started': [] + 'sync:completed': [count: number] + 'conflict:detected': [featureId: string, local: Feature, remote: Feature] +} + +export class EventBus extends EventEmitter { + constructor() { + super() + this.setMaxListeners(20) + } + + emit(event: K, ...args: EventMap[K]): boolean { + return super.emit(event, ...args) + } + + on(event: K, listener: (...args: EventMap[K]) => void): this { + return super.on(event, listener) + } + + featureCreated(feature: Feature): void { + this.emit('feature:created', feature) + } + + featureUpdated(feature: Feature): void { + this.emit('feature:updated', feature) + } + + featureDeleted(featureId: string): void { + this.emit('feature:deleted', featureId) + } + + syncStarted(): void { + this.emit('sync:started') + } + + syncCompleted(count: number): void { + this.emit('sync:completed', count) + } + + conflictDetected(featureId: string, local: Feature, remote: Feature): void { + this.emit('conflict:detected', featureId, local, remote) + } + + dispose(): void { + this.removeAllListeners() + } +} + +export const eventBus = new EventBus() diff --git a/mcp-server/src/http/routes.ts b/mcp-server/src/http/routes.ts new file mode 100644 index 0000000..b2352ed --- /dev/null +++ b/mcp-server/src/http/routes.ts @@ -0,0 +1,134 @@ +import { Hono } from 'hono' +import { cors } from 'hono/cors' +import { z } from 'zod' +import type { FeatureStore } from '../store/feature-store.js' +import type { EventBus } from '../events/event-bus.js' +import type { SupercrewStatus } from '../types.js' + +// Validation schemas +const SupercrewStatusSchema = z.enum(['planning', 'designing', 'ready', 'active', 'blocked', 'done']) +const PrioritySchema = z.enum(['P0', 'P1', 'P2', 'P3']) + +const FeatureInputSchema = z.object({ + id: z.string().min(1), + title: z.string().min(1), + status: SupercrewStatusSchema.optional(), + owner: z.string().optional(), + priority: PrioritySchema.optional(), + branch: z.string().optional(), + teams: z.array(z.string()).optional(), + tags: z.array(z.string()).optional(), +}) + +const UpdateStatusSchema = z.object({ + status: SupercrewStatusSchema, +}) + +export function createHttpApp(store: FeatureStore, eventBus: EventBus) { + const app = new Hono() + + // Error handling middleware + app.onError((err, c) => { + console.error('HTTP Error:', err) + + // Handle Zod validation errors + if (err instanceof z.ZodError) { + return c.json({ + error: 'Validation failed', + details: err.errors + }, 400) + } + + // Handle other errors + return c.json({ + error: err.message || 'Internal server error' + }, 500) + }) + + app.use('*', cors({ + origin: ['http://localhost:5173', 'http://localhost:5174'], + allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], + allowHeaders: ['Content-Type'], + })) + + app.get('/health', (c) => c.json({ ok: true })) + + app.get('/api/features', (c) => { + try { + const features = store.listAll() + return c.json(features) + } catch (error) { + throw new Error(`Failed to list features: ${error instanceof Error ? error.message : 'Unknown error'}`) + } + }) + + app.get('/api/features/:id', (c) => { + try { + const feature = store.get(c.req.param('id')) + if (!feature) return c.json({ error: 'Not found' }, 404) + return c.json(feature) + } catch (error) { + throw new Error(`Failed to get feature: ${error instanceof Error ? error.message : 'Unknown error'}`) + } + }) + + app.post('/api/features', async (c) => { + const body = await c.req.json() + const validatedInput = FeatureInputSchema.parse(body) + + try { + const feature = store.create(validatedInput) + eventBus.featureCreated(feature) + return c.json(feature, 201) + } catch (error) { + throw new Error(`Failed to create feature: ${error instanceof Error ? error.message : 'Unknown error'}`) + } + }) + + app.patch('/api/features/:id/status', async (c) => { + const body = await c.req.json() + const { status } = UpdateStatusSchema.parse(body) + + try { + const feature = store.updateStatus(c.req.param('id'), status) + if (!feature) return c.json({ error: 'Not found' }, 404) + eventBus.featureUpdated(feature) + return c.json(feature) + } catch (error) { + if (error instanceof Error && error.message.includes('Not found')) { + return c.json({ error: 'Not found' }, 404) + } + throw new Error(`Failed to update feature status: ${error instanceof Error ? error.message : 'Unknown error'}`) + } + }) + + app.delete('/api/features/:id', (c) => { + try { + const deleted = store.delete(c.req.param('id')) + if (!deleted) return c.json({ error: 'Not found' }, 404) + eventBus.featureDeleted(c.req.param('id')) + return c.json({ ok: true }) + } catch (error) { + throw new Error(`Failed to delete feature: ${error instanceof Error ? error.message : 'Unknown error'}`) + } + }) + + app.get('/api/board', (c) => { + try { + const features = store.listAll() + const featuresByStatus = { + planning: features.filter(f => f.status === 'planning'), + designing: features.filter(f => f.status === 'designing'), + ready: features.filter(f => f.status === 'ready'), + active: features.filter(f => f.status === 'active'), + blocked: features.filter(f => f.status === 'blocked'), + done: features.filter(f => f.status === 'done'), + } + return c.json({ features, featuresByStatus }) + } catch (error) { + throw new Error(`Failed to get board: ${error instanceof Error ? error.message : 'Unknown error'}`) + } + }) + + return app +} diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts new file mode 100644 index 0000000..f0be127 --- /dev/null +++ b/mcp-server/src/index.ts @@ -0,0 +1,81 @@ +#!/usr/bin/env bun + +import { createDb, getDefaultDbPath } from './store/db.js' +import { FeatureStore } from './store/feature-store.js' +import { EventBus } from './events/event-bus.js' +import { BranchScanner } from './scanner/branch-scanner.js' +import { startMcpServer } from './mcp/server.js' +import { createHttpApp } from './http/routes.js' +import { WebSocketServer } from './ws/server.js' + +const HTTP_PORT = parseInt(process.env.MCP_HTTP_PORT ?? '3456') +const WS_PORT = parseInt(process.env.MCP_WS_PORT ?? '3457') + +async function main() { + const repoRoot = process.cwd() + const dbPath = getDefaultDbPath(repoRoot) + + // Ensure .supercrew directory exists + const { mkdirSync } = await import('fs') + mkdirSync(`${repoRoot}/.supercrew`, { recursive: true }) + + const db = createDb(dbPath) + const store = new FeatureStore(db) + const eventBus = new EventBus() + const scanner = new BranchScanner(repoRoot) + + // Initial scan + console.error('Scanning branches for features...') + const { features } = await scanner.scanAllBranches() + for (const feature of features) { + store.upsertFromGitHub(feature) + } + console.error(`Loaded ${features.length} features from ${repoRoot}`) + + // Check if running in MCP mode (stdin connected from Claude Code) + const isMcpMode = !process.stdin.isTTY + + if (isMcpMode) { + // MCP Server mode (Claude Code) + await startMcpServer(store, eventBus) + } else { + // HTTP + WebSocket mode (Web UI) + const httpApp = createHttpApp(store, eventBus) + const wsServer = new WebSocketServer(eventBus) + + // HTTP Server + Bun.serve({ + port: HTTP_PORT, + fetch: httpApp.fetch, + }) + console.error(`HTTP server running on http://localhost:${HTTP_PORT}`) + + // WebSocket Server + Bun.serve({ + port: WS_PORT, + fetch(req, server) { + if (server.upgrade(req)) return + return new Response('WebSocket upgrade required', { status: 426 }) + }, + websocket: { + open(ws) { + // Bun's ServerWebSocket is compatible with WebSocket interface + wsServer.handleConnection(ws as unknown as WebSocket) + }, + message() {}, + close() {}, + }, + }) + console.error(`WebSocket server running on ws://localhost:${WS_PORT}`) + + // Periodic rescan every 30 seconds + setInterval(async () => { + const { features: newFeatures } = await scanner.scanAllBranches() + for (const f of newFeatures) { + store.upsertFromGitHub(f) + } + }, 30000) + } +} + +main().catch(console.error) diff --git a/mcp-server/src/mcp/server.ts b/mcp-server/src/mcp/server.ts new file mode 100644 index 0000000..8a1125e --- /dev/null +++ b/mcp-server/src/mcp/server.ts @@ -0,0 +1,28 @@ +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' +import { createTools } from './tools.js' +import type { FeatureStore } from '../store/feature-store.js' +import type { EventBus } from '../events/event-bus.js' + +export async function startMcpServer(store: FeatureStore, eventBus: EventBus) { + const server = new McpServer( + { name: 'supercrew-mcp', version: '0.1.0' }, + { capabilities: {} } + ) + + const tools = createTools(store, eventBus) + + // Register all tools + for (const [name, tool] of Object.entries(tools)) { + server.registerTool(name, { + description: tool.description, + inputSchema: tool.inputSchema, + }, tool.handler) + } + + const transport = new StdioServerTransport() + await server.connect(transport) + + console.error('SuperCrew MCP Server running on stdio') + return server +} diff --git a/mcp-server/src/mcp/tools.ts b/mcp-server/src/mcp/tools.ts new file mode 100644 index 0000000..ec83f96 --- /dev/null +++ b/mcp-server/src/mcp/tools.ts @@ -0,0 +1,83 @@ +import { z } from 'zod' +import type { FeatureStore } from '../store/feature-store.js' +import type { EventBus } from '../events/event-bus.js' +import type { SupercrewStatus } from '../types.js' + +export function createTools(store: FeatureStore, eventBus: EventBus) { + return { + list_features: { + description: '列出所有 features', + inputSchema: {}, + handler: async () => { + const features = store.listAll() + return { content: [{ type: 'text' as const, text: JSON.stringify(features, null, 2) }] } + }, + }, + + get_feature: { + description: '获取单个 feature 详情', + inputSchema: { + id: z.string().describe('Feature ID'), + }, + handler: async ({ id }: { id: string }) => { + const feature = store.get(id) + if (!feature) { + return { content: [{ type: 'text' as const, text: `Feature ${id} not found` }], isError: true } + } + return { content: [{ type: 'text' as const, text: JSON.stringify(feature, null, 2) }] } + }, + }, + + create_feature: { + description: '创建新 feature', + inputSchema: { + id: z.string().describe('Feature ID (e.g., login-page)'), + title: z.string().describe('Feature title'), + priority: z.enum(['P0', 'P1', 'P2', 'P3']).optional(), + owner: z.string().optional().describe('Owner username'), + }, + handler: async (input: { id: string; title: string; priority?: string; owner?: string }) => { + const feature = store.create({ + id: input.id, + title: input.title, + priority: input.priority as any, + owner: input.owner, + }) + eventBus.featureCreated(feature) + return { content: [{ type: 'text' as const, text: `Created feature: ${feature.id}` }] } + }, + }, + + update_feature_status: { + description: '更新 feature 状态', + inputSchema: { + id: z.string(), + status: z.enum(['planning', 'designing', 'ready', 'active', 'blocked', 'done']), + }, + handler: async ({ id, status }: { id: string; status: SupercrewStatus }) => { + const feature = store.updateStatus(id, status) + if (!feature) { + return { content: [{ type: 'text' as const, text: `Feature ${id} not found` }], isError: true } + } + eventBus.featureUpdated(feature) + return { content: [{ type: 'text' as const, text: `Updated ${id} status to ${status}` }] } + }, + }, + + log_progress: { + description: '追加 feature log 记录', + inputSchema: { + id: z.string(), + entry: z.string().describe('Progress entry to append'), + }, + handler: async ({ id, entry }: { id: string; entry: string }) => { + const feature = store.appendLog(id, entry) + if (!feature) { + return { content: [{ type: 'text' as const, text: `Feature ${id} not found` }], isError: true } + } + eventBus.featureUpdated(feature) + return { content: [{ type: 'text' as const, text: `Added log entry to ${id}` }] } + }, + }, + } +} diff --git a/mcp-server/src/scanner/branch-scanner.ts b/mcp-server/src/scanner/branch-scanner.ts new file mode 100644 index 0000000..9b5db55 --- /dev/null +++ b/mcp-server/src/scanner/branch-scanner.ts @@ -0,0 +1,183 @@ +import { execSync } from 'child_process' +import { join } from 'path' +import { existsSync, readFileSync, readdirSync } from 'fs' +import type { Feature } from '../types.js' +import { parseMetaYaml } from './feature-parser.js' + +export interface ScanResult { + features: Feature[] + branches: string[] +} + +export class BranchScanner { + constructor(private repoRoot: string) {} + + async scanAllBranches(): Promise { + const branches = this.listBranches() + const featuresPerBranch = new Map() + + for (const branch of branches) { + const features = await this.scanBranch(branch) + featuresPerBranch.set(branch, features) + } + + const deduped = this.dedupeFeatures(featuresPerBranch) + return { features: deduped, branches } + } + + async scanCurrentBranch(): Promise { + const branch = this.getCurrentBranch() + return this.scanBranch(branch) + } + + private listBranches(): string[] { + try { + const output = execSync('git branch -a --format="%(refname:short)"', { + cwd: this.repoRoot, + encoding: 'utf-8', + }) + return output + .split('\n') + .map(b => b.trim()) + .filter(b => b && !b.includes('->')) + .filter(b => b === 'main' || b.startsWith('feature/') || b.startsWith('fix/')) + } catch { + return ['main'] + } + } + + private getCurrentBranch(): string { + try { + return execSync('git branch --show-current', { + cwd: this.repoRoot, + encoding: 'utf-8', + }).trim() + } catch { + return 'main' + } + } + + private async scanBranch(branch: string): Promise { + const featuresDir = join(this.repoRoot, '.supercrew', 'features') + + if (!existsSync(featuresDir)) { + return [] + } + + const features: Feature[] = [] + + try { + const currentBranch = this.getCurrentBranch() + if (branch === currentBranch) { + // For current branch, read from filesystem + const entries = readdirSync(featuresDir) + + for (const entry of entries) { + const featurePath = join(featuresDir, entry) + const feature = this.parseFeatureDir(featurePath, entry, branch) + if (feature) features.push(feature) + } + } else { + // For other branches, use git show + const featureIds = this.listFeaturesInBranch(branch) + for (const id of featureIds) { + const feature = this.parseFeatureFromGit(branch, id) + if (feature) features.push(feature) + } + } + } catch (e) { + console.error(`Error scanning branch ${branch}:`, e) + } + + return features + } + + private listFeaturesInBranch(branch: string): string[] { + try { + const output = execSync( + `git ls-tree -d --name-only ${branch}:.supercrew/features/ 2>/dev/null || true`, + { cwd: this.repoRoot, encoding: 'utf-8' } + ) + return output.split('\n').filter(Boolean) + } catch { + return [] + } + } + + private parseFeatureDir(dirPath: string, featureId: string, branch: string): Feature | null { + const metaPath = join(dirPath, 'meta.yaml') + if (!existsSync(metaPath)) return null + + try { + const metaContent = readFileSync(metaPath, 'utf-8') + const partial = parseMetaYaml(metaContent, featureId) + + const designPath = join(dirPath, 'design.md') + const planPath = join(dirPath, 'plan.md') + const logPath = join(dirPath, 'log.md') + + return { + ...partial, + branch, + design_md: existsSync(designPath) ? readFileSync(designPath, 'utf-8') : null, + plan_md: existsSync(planPath) ? readFileSync(planPath, 'utf-8') : null, + log_md: existsSync(logPath) ? readFileSync(logPath, 'utf-8') : null, + synced_at: null, + } as Feature + } catch { + return null + } + } + + private parseFeatureFromGit(branch: string, featureId: string): Feature | null { + try { + const metaContent = execSync( + `git show ${branch}:.supercrew/features/${featureId}/meta.yaml 2>/dev/null`, + { cwd: this.repoRoot, encoding: 'utf-8' } + ) + const partial = parseMetaYaml(metaContent, featureId) + + const getFile = (filename: string): string | null => { + try { + return execSync( + `git show ${branch}:.supercrew/features/${featureId}/${filename} 2>/dev/null`, + { cwd: this.repoRoot, encoding: 'utf-8' } + ) + } catch { + return null + } + } + + return { + ...partial, + branch, + design_md: getFile('design.md'), + plan_md: getFile('plan.md'), + log_md: getFile('log.md'), + synced_at: null, + } as Feature + } catch { + return null + } + } + + private dedupeFeatures(featuresPerBranch: Map): Feature[] { + const result = new Map() + + // First add main branch features + for (const f of featuresPerBranch.get('main') ?? []) { + result.set(f.id, { ...f, branch: 'main' }) + } + + // Then override with feature/* and fix/* branches (more recent) + for (const [branch, features] of featuresPerBranch) { + if (branch.startsWith('feature/') || branch.startsWith('fix/')) { + for (const f of features) { + result.set(f.id, { ...f, branch }) + } + } + } + + return Array.from(result.values()) + } +} diff --git a/mcp-server/src/scanner/feature-parser.ts b/mcp-server/src/scanner/feature-parser.ts new file mode 100644 index 0000000..d66e639 --- /dev/null +++ b/mcp-server/src/scanner/feature-parser.ts @@ -0,0 +1,20 @@ +import yaml from 'js-yaml' +import type { Feature, SupercrewStatus, Priority } from '../types.js' + +export function parseMetaYaml(content: string, featureId: string): Partial { + const data = yaml.load(content) as Record + return { + id: data.id ?? featureId, + title: data.title ?? '', + status: (data.status ?? 'planning') as SupercrewStatus, + owner: data.owner ?? null, + priority: data.priority as Priority ?? null, + teams: data.teams ?? [], + tags: data.tags ?? [], + blocked_by: data.blocked_by ?? [], + target_release: data.target_release ?? null, + created_at: data.created ?? new Date().toISOString(), + updated_at: data.updated ?? new Date().toISOString(), + meta_yaml: content, + } +} diff --git a/mcp-server/src/store/db.ts b/mcp-server/src/store/db.ts new file mode 100644 index 0000000..49f9ff5 --- /dev/null +++ b/mcp-server/src/store/db.ts @@ -0,0 +1,37 @@ +import { Database } from 'bun:sqlite' +import { join } from 'path' + +const SCHEMA = ` +CREATE TABLE IF NOT EXISTS features ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'planning', + owner TEXT, + priority TEXT, + branch TEXT NOT NULL DEFAULT 'main', + teams TEXT DEFAULT '[]', + tags TEXT DEFAULT '[]', + blocked_by TEXT DEFAULT '[]', + target_release TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + meta_yaml TEXT, + design_md TEXT, + plan_md TEXT, + log_md TEXT, + synced_at TEXT +); + +CREATE INDEX IF NOT EXISTS idx_features_status ON features(status); +CREATE INDEX IF NOT EXISTS idx_features_branch ON features(branch); +` + +export function createDb(dbPath: string): Database { + const db = new Database(dbPath) + db.run(SCHEMA) + return db +} + +export function getDefaultDbPath(repoRoot: string): string { + return join(repoRoot, '.supercrew', '.mcp-server.db') +} diff --git a/mcp-server/src/store/feature-store.ts b/mcp-server/src/store/feature-store.ts new file mode 100644 index 0000000..b6338be --- /dev/null +++ b/mcp-server/src/store/feature-store.ts @@ -0,0 +1,183 @@ +import type { Database } from 'bun:sqlite' +import type { Feature, FeatureInput, SupercrewStatus } from '../types' + +export class FeatureStore { + private db: Database + + constructor(db: Database) { + this.db = db + } + + listAll(): Feature[] { + const stmt = this.db.query('SELECT * FROM features ORDER BY created_at DESC') + const rows = stmt.all() + return rows.map((row: any) => this.rowToFeature(row)) + } + + get(id: string): Feature | null { + const stmt = this.db.query('SELECT * FROM features WHERE id = ?') + const row = stmt.get(id) + if (!row) return null + return this.rowToFeature(row as any) + } + + create(input: FeatureInput): Feature { + const now = new Date().toISOString() + const feature: Feature = { + id: input.id, + title: input.title, + status: input.status || 'planning', + owner: input.owner || null, + priority: input.priority || null, + branch: input.branch || 'main', + teams: input.teams || [], + tags: input.tags || [], + blocked_by: [], + target_release: null, + created_at: now, + updated_at: now, + meta_yaml: null, + design_md: null, + plan_md: null, + log_md: null, + synced_at: null + } + + const stmt = this.db.query(` + INSERT INTO features ( + id, title, status, owner, priority, branch, teams, tags, blocked_by, + target_release, created_at, updated_at, meta_yaml, design_md, plan_md, log_md, synced_at + ) VALUES ( + $id, $title, $status, $owner, $priority, $branch, $teams, $tags, $blocked_by, + $target_release, $created_at, $updated_at, $meta_yaml, $design_md, $plan_md, $log_md, $synced_at + ) + `) + stmt.run(this.featureToRow(feature)) + + return feature + } + + updateStatus(id: string, status: SupercrewStatus): Feature | null { + const now = new Date().toISOString() + const stmt = this.db.query(` + UPDATE features + SET status = ?1, updated_at = ?2 + WHERE id = ?3 + `) + stmt.run(status, now, id) + + return this.get(id) + } + + updatePlan(id: string, content: string): Feature | null { + const now = new Date().toISOString() + const stmt = this.db.query(` + UPDATE features + SET plan_md = ?1, updated_at = ?2 + WHERE id = ?3 + `) + stmt.run(content, now, id) + + return this.get(id) + } + + appendLog(id: string, entry: string): Feature | null { + const feature = this.get(id) + if (!feature) return null + + const now = new Date().toISOString() + const currentLog = feature.log_md || '' + const newLog = currentLog ? `${currentLog}\n\n${entry}` : entry + + const stmt = this.db.query(` + UPDATE features + SET log_md = ?1, updated_at = ?2 + WHERE id = ?3 + `) + stmt.run(newLog, now, id) + + return this.get(id) + } + + delete(id: string): boolean { + // Check if the feature exists before deleting + const exists = this.get(id) !== null + if (!exists) return false + + const stmt = this.db.query('DELETE FROM features WHERE id = ?') + stmt.run(id) + return true + } + + upsertFromGitHub(feature: Feature): void { + const existing = this.get(feature.id) + + if (existing) { + const stmt = this.db.query(` + UPDATE features + SET title = $title, status = $status, owner = $owner, priority = $priority, + branch = $branch, teams = $teams, tags = $tags, blocked_by = $blocked_by, + target_release = $target_release, updated_at = $updated_at, + meta_yaml = $meta_yaml, design_md = $design_md, plan_md = $plan_md, + log_md = $log_md, synced_at = $synced_at + WHERE id = $id + `) + stmt.run(this.featureToRow(feature)) + } else { + const stmt = this.db.query(` + INSERT INTO features ( + id, title, status, owner, priority, branch, teams, tags, blocked_by, + target_release, created_at, updated_at, meta_yaml, design_md, plan_md, log_md, synced_at + ) VALUES ( + $id, $title, $status, $owner, $priority, $branch, $teams, $tags, $blocked_by, + $target_release, $created_at, $updated_at, $meta_yaml, $design_md, $plan_md, $log_md, $synced_at + ) + `) + stmt.run(this.featureToRow(feature)) + } + } + + private rowToFeature(row: any): Feature { + return { + id: row.id, + title: row.title, + status: row.status, + owner: row.owner, + priority: row.priority, + branch: row.branch, + teams: JSON.parse(row.teams), + tags: JSON.parse(row.tags), + blocked_by: JSON.parse(row.blocked_by), + target_release: row.target_release, + created_at: row.created_at, + updated_at: row.updated_at, + meta_yaml: row.meta_yaml, + design_md: row.design_md, + plan_md: row.plan_md, + log_md: row.log_md, + synced_at: row.synced_at + } + } + + private featureToRow(feature: Feature): any { + return { + $id: feature.id, + $title: feature.title, + $status: feature.status, + $owner: feature.owner, + $priority: feature.priority, + $branch: feature.branch, + $teams: JSON.stringify(feature.teams), + $tags: JSON.stringify(feature.tags), + $blocked_by: JSON.stringify(feature.blocked_by), + $target_release: feature.target_release, + $created_at: feature.created_at, + $updated_at: feature.updated_at, + $meta_yaml: feature.meta_yaml, + $design_md: feature.design_md, + $plan_md: feature.plan_md, + $log_md: feature.log_md, + $synced_at: feature.synced_at + } + } +} diff --git a/mcp-server/src/sync/conflict-resolver.ts b/mcp-server/src/sync/conflict-resolver.ts new file mode 100644 index 0000000..fd5677c --- /dev/null +++ b/mcp-server/src/sync/conflict-resolver.ts @@ -0,0 +1,41 @@ +import type { Feature } from '../types.js' +import type { EventBus } from '../events/event-bus.js' + +export interface ConflictResolution { + featureId: string + choice: 'local' | 'remote' +} + +export class ConflictResolver { + private pendingConflicts: Map = new Map() + + constructor(private eventBus: EventBus) {} + + detectConflict(local: Feature, remote: Feature): boolean { + // Conflict if both have been updated since last sync + if (!local.synced_at) return false + + const localUpdated = new Date(local.updated_at).getTime() + const remoteUpdated = new Date(remote.updated_at).getTime() + const lastSync = new Date(local.synced_at).getTime() + + return localUpdated > lastSync && remoteUpdated > lastSync + } + + registerConflict(local: Feature, remote: Feature) { + this.pendingConflicts.set(local.id, { local, remote }) + this.eventBus.conflictDetected(local.id, local, remote) + } + + resolveConflict(featureId: string, choice: 'local' | 'remote'): Feature | null { + const conflict = this.pendingConflicts.get(featureId) + if (!conflict) return null + + this.pendingConflicts.delete(featureId) + return choice === 'local' ? conflict.local : conflict.remote + } + + getPendingConflicts(): string[] { + return Array.from(this.pendingConflicts.keys()) + } +} diff --git a/mcp-server/src/sync/github-sync.ts b/mcp-server/src/sync/github-sync.ts new file mode 100644 index 0000000..d67f633 --- /dev/null +++ b/mcp-server/src/sync/github-sync.ts @@ -0,0 +1,165 @@ +import { execSync } from 'child_process' +import { writeFileSync, mkdirSync, existsSync, rmSync } from 'fs' +import { join } from 'path' +import yaml from 'js-yaml' +import type { Feature } from '../types.js' +import type { EventBus } from '../events/event-bus.js' + +interface SyncTask { + action: 'create' | 'update' | 'delete' + feature: Feature +} + +/** + * Validates that a feature ID is safe for use in file paths + * @param id - The feature ID to validate + * @returns true if the ID is valid + */ +function isValidFeatureId(id: string): boolean { + // Only allow alphanumeric characters, hyphens, and underscores + return /^[a-zA-Z0-9_-]+$/.test(id) +} + +/** + * Sanitizes a git commit message by escaping special characters + * @param message - The commit message to sanitize + * @returns A sanitized message safe for use in shell commands + */ +function sanitizeCommitMessage(message: string): string { + // Remove or escape characters that could break shell commands + return message.replace(/["'`$\\]/g, '') +} + +export class GitHubSyncWorker { + private queue: SyncTask[] = [] + private syncing = false + + constructor( + private repoRoot: string, + private eventBus: EventBus + ) {} + + queueCreate(feature: Feature) { + this.queue.push({ action: 'create', feature }) + this.processQueue() + } + + queueUpdate(feature: Feature) { + // Dedupe: remove pending updates for same feature + this.queue = this.queue.filter(t => t.feature.id !== feature.id) + this.queue.push({ action: 'update', feature }) + this.processQueue() + } + + queueDelete(feature: Feature) { + this.queue = this.queue.filter(t => t.feature.id !== feature.id) + this.queue.push({ action: 'delete', feature }) + this.processQueue() + } + + async flushAll() { + while (this.queue.length > 0) { + await this.processQueue() + } + } + + private async processQueue() { + if (this.syncing || this.queue.length === 0) return + + this.syncing = true + this.eventBus.syncStarted() + + let count = 0 + while (this.queue.length > 0) { + const task = this.queue.shift()! + try { + await this.executeTask(task) + count++ + } catch (e) { + console.error(`Sync failed for ${task.feature.id}:`, e) + } + } + + this.eventBus.syncCompleted(count) + this.syncing = false + } + + private async executeTask(task: SyncTask) { + const { feature, action } = task + + // Validate feature ID to prevent path traversal attacks + if (!isValidFeatureId(feature.id)) { + throw new Error(`Invalid feature ID: ${feature.id}. Only alphanumeric characters, hyphens, and underscores are allowed.`) + } + + const featureDir = join(this.repoRoot, '.supercrew', 'features', feature.id) + + switch (action) { + case 'create': + case 'update': + mkdirSync(featureDir, { recursive: true }) + this.writeFeatureFiles(featureDir, feature) + this.gitCommit(`feat: ${action} feature ${feature.id}`) + break + + case 'delete': + if (existsSync(featureDir)) { + // Use fs.rmSync instead of shell command to prevent command injection + rmSync(featureDir, { recursive: true, force: true }) + this.gitCommit(`chore: delete feature ${feature.id}`) + } + break + } + } + + private writeFeatureFiles(dir: string, feature: Feature) { + // meta.yaml + const meta = { + id: feature.id, + title: feature.title, + status: feature.status, + owner: feature.owner, + priority: feature.priority, + teams: feature.teams, + tags: feature.tags, + blocked_by: feature.blocked_by, + target_release: feature.target_release, + created: feature.created_at, + updated: feature.updated_at, + } + writeFileSync(join(dir, 'meta.yaml'), yaml.dump(meta), { encoding: 'utf-8' }) + + // design.md + if (feature.design_md) { + writeFileSync(join(dir, 'design.md'), feature.design_md, { encoding: 'utf-8' }) + } + + // plan.md + if (feature.plan_md) { + writeFileSync(join(dir, 'plan.md'), feature.plan_md, { encoding: 'utf-8' }) + } + + // log.md + if (feature.log_md) { + writeFileSync(join(dir, 'log.md'), feature.log_md, { encoding: 'utf-8' }) + } + } + + private gitCommit(message: string) { + try { + const sanitizedMessage = sanitizeCommitMessage(message) + execSync('git add .supercrew/', { cwd: this.repoRoot }) + execSync(`git commit -m "${sanitizedMessage}" --allow-empty`, { cwd: this.repoRoot }) + } catch (e) { + // Ignore if nothing to commit + } + } + + async gitPush() { + try { + execSync('git push', { cwd: this.repoRoot }) + } catch (e) { + console.error('Git push failed:', e) + } + } +} diff --git a/mcp-server/src/types.ts b/mcp-server/src/types.ts new file mode 100644 index 0000000..df21b46 --- /dev/null +++ b/mcp-server/src/types.ts @@ -0,0 +1,40 @@ +export type SupercrewStatus = + | 'planning' + | 'designing' + | 'ready' + | 'active' + | 'blocked' + | 'done' + +export type Priority = 'P0' | 'P1' | 'P2' | 'P3' + +export interface Feature { + id: string + title: string + status: SupercrewStatus + owner: string | null + priority: Priority | null + branch: string + teams: string[] + tags: string[] + blocked_by: string[] + target_release: string | null + created_at: string + updated_at: string + meta_yaml: string | null + design_md: string | null + plan_md: string | null + log_md: string | null + synced_at: string | null +} + +export interface FeatureInput { + id: string + title: string + status?: SupercrewStatus + owner?: string + priority?: Priority + branch?: string + teams?: string[] + tags?: string[] +} diff --git a/mcp-server/src/ws/server.ts b/mcp-server/src/ws/server.ts new file mode 100644 index 0000000..0bf5924 --- /dev/null +++ b/mcp-server/src/ws/server.ts @@ -0,0 +1,70 @@ +import type { EventBus } from '../events/event-bus.js' +import type { Feature } from '../types.js' + +// WebSocket message types +type WSMessage = + | { type: 'connected'; clientCount: number } + | { type: 'feature:created'; feature: Feature } + | { type: 'feature:updated'; feature: Feature } + | { type: 'feature:deleted'; featureId: string } + | { type: 'conflict:detected'; featureId: string; local: Feature; remote: Feature } + +interface WebSocketClient { + ws: WebSocket + send: (data: WSMessage) => void +} + +export class WebSocketServer { + private clients: Set = new Set() + + constructor(private eventBus: EventBus) { + this.setupEventListeners() + } + + setupEventListeners() { + this.eventBus.on('feature:created', (feature: Feature) => { + this.broadcast({ type: 'feature:created', feature }) + }) + + this.eventBus.on('feature:updated', (feature: Feature) => { + this.broadcast({ type: 'feature:updated', feature }) + }) + + this.eventBus.on('feature:deleted', (featureId: string) => { + this.broadcast({ type: 'feature:deleted', featureId }) + }) + + this.eventBus.on('conflict:detected', (featureId: string, local: Feature, remote: Feature) => { + this.broadcast({ type: 'conflict:detected', featureId, local, remote }) + }) + } + + handleConnection(ws: WebSocket) { + const client: WebSocketClient = { + ws, + send: (data: WSMessage) => ws.send(JSON.stringify(data)), + } + this.clients.add(client) + + ws.addEventListener('close', () => { + this.clients.delete(client) + }) + + // Send initial connection confirmation + client.send({ type: 'connected', clientCount: this.clients.size }) + } + + broadcast(data: WSMessage) { + for (const client of this.clients) { + try { + client.send(data) + } catch (e) { + this.clients.delete(client) + } + } + } + + get clientCount() { + return this.clients.size + } +} diff --git a/mcp-server/tsconfig.json b/mcp-server/tsconfig.json new file mode 100644 index 0000000..a1c3bb1 --- /dev/null +++ b/mcp-server/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "types": ["bun-types"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/plugins/supercrew/.claude-plugin/plugin.json b/plugins/supercrew/.claude-plugin/plugin.json index 5bd04b6..f02cfcb 100644 --- a/plugins/supercrew/.claude-plugin/plugin.json +++ b/plugins/supercrew/.claude-plugin/plugin.json @@ -1,12 +1,18 @@ { "name": "supercrew", - "description": "AI-driven feature lifecycle management. Creates and manages .supercrew/features/ directories with meta.yaml, design.md, plan.md, and log.md for structured feature development.", - "version": "0.1.0", + "description": "AI-driven feature lifecycle management with real-time MCP sync.", + "version": "0.2.0", "author": { "name": "steinsz" }, "homepage": "https://github.com/nicepkg/supercrew", "repository": "https://github.com/nicepkg/supercrew", "license": "MIT", - "keywords": ["feature-management", "kanban", "lifecycle", "planning", "tracking"] + "keywords": ["feature-management", "kanban", "lifecycle", "planning", "tracking", "mcp"], + "mcp": { + "server": { + "command": "bunx", + "args": ["@supercrew/mcp-server"] + } + } } diff --git a/plugins/supercrew/mcp/settings.json b/plugins/supercrew/mcp/settings.json new file mode 100644 index 0000000..c0b8eb0 --- /dev/null +++ b/plugins/supercrew/mcp/settings.json @@ -0,0 +1,9 @@ +{ + "mcpServers": { + "supercrew": { + "command": "bunx", + "args": ["@supercrew/mcp-server"], + "env": {} + } + } +}