diff --git a/mcp-server/package.json b/mcp-server/package.json new file mode 100644 index 00000000..ac6f5a7d --- /dev/null +++ b/mcp-server/package.json @@ -0,0 +1,31 @@ +{ + "name": "@claude-team/mcp-server", + "version": "1.0.0", + "description": "MCP server for managing Claude Agent Teams kanban board and tasks", + "type": "module", + "main": "dist/index.js", + "bin": { + "team-mcp-server": "dist/index.js" + }, + "scripts": { + "build": "tsup", + "dev": "tsx src/index.ts", + "test": "vitest run", + "test:watch": "vitest", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "fastmcp": "^3.34.0", + "zod": "^4.3.6" + }, + "devDependencies": { + "tsup": "^8.5.1", + "tsx": "^4.21.0", + "typescript": "^5.8.2", + "vitest": "^3.1.4", + "@types/node": "^22.15.18" + }, + "engines": { + "node": ">=20" + } +} diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts new file mode 100644 index 00000000..a30a5b06 --- /dev/null +++ b/mcp-server/src/index.ts @@ -0,0 +1,20 @@ +import { FastMCP } from 'fastmcp'; +import { TeamctlRunner } from './teamctl-runner.js'; +import { registerAllTools } from './tools/index.js'; + +const server = new FastMCP({ + name: 'claude-team-tools', + version: '1.0.0', + instructions: `MCP server for managing Claude Agent Teams kanban board and tasks. + +Provides 13 tools for task CRUD, kanban board management, code reviews, and team messaging. +All operations are backed by teamctl.js — the battle-tested CLI tool from Claude Agent Teams UI. + +Data is stored as JSON files in ~/.claude/tasks/{teamName}/ and ~/.claude/teams/{teamName}/.`, +}); + +const runner = new TeamctlRunner(); + +registerAllTools(server, runner); + +server.start({ transportType: 'stdio' }); diff --git a/mcp-server/src/output-parser.ts b/mcp-server/src/output-parser.ts new file mode 100644 index 00000000..e2b7c1a5 --- /dev/null +++ b/mcp-server/src/output-parser.ts @@ -0,0 +1,52 @@ +/** + * Parses teamctl stdout into structured results. + * + * teamctl outputs in three formats: + * 1. JSON — task create/get/list, attach, message send, kanban reviewers list + * 2. "OK ..." text — status changes, comments, links, kanban moves, reviews + * 3. Plain text — task briefing (multi-line human-readable report) + */ + +/** Parse JSON from teamctl stdout (task create/get/list, attach, message send) */ +export function parseJsonOutput(stdout: string): T { + const trimmed = stdout.trim(); + if (!trimmed) { + throw new Error('Empty output from teamctl (expected JSON)'); + } + try { + return JSON.parse(trimmed) as T; + } catch { + throw new Error( + `Failed to parse teamctl JSON output: ${trimmed.slice(0, 200)}`, + ); + } +} + +/** Parse "OK ..." acknowledgment lines from teamctl */ +export function parseOkOutput(stdout: string): string { + const trimmed = stdout.trim(); + if (trimmed.startsWith('OK ')) { + return trimmed.slice(3); // Strip "OK " prefix, keep the rest + } + // Some commands output just "OK\n" + if (trimmed === 'OK') { + return 'OK'; + } + // Return as-is if format is unexpected — don't throw + return trimmed; +} + +/** Return plain text as-is (briefing, help output) */ +export function parseTextOutput(stdout: string): string { + return stdout.trim(); +} + +/** + * Format teamctl stderr into a user-friendly error message. + * teamctl writes errors to stderr via `die(message)` and exits with code 1. + */ +export function formatError(stderr: string, stdout: string): string { + const msg = stderr.trim() || stdout.trim(); + if (!msg) return 'Unknown teamctl error'; + return msg; +} diff --git a/mcp-server/src/schemas.ts b/mcp-server/src/schemas.ts new file mode 100644 index 00000000..cc808e0b --- /dev/null +++ b/mcp-server/src/schemas.ts @@ -0,0 +1,115 @@ +import { z } from 'zod'; +import { sep, isAbsolute } from 'node:path'; + +// --------------------------------------------------------------------------- +// Identifiers +// --------------------------------------------------------------------------- + +/** + * Matches teamctl's `isSafePathSegment()`: + * rejects empty, '.', '..', and strings containing '/', '\\', '\0', or '..' + */ +const safePathSegment = (label: string) => + z + .string() + .min(1) + .max(128) + .refine( + (v) => + v.trim().length > 0 && + v !== '.' && + v !== '..' && + !v.includes('/') && + !v.includes('\\') && + !v.includes('\0') && + !v.includes('..'), + { message: `Invalid ${label}: must be a safe path segment` }, + ); + +/** Team name — folder inside `~/.claude/teams/` */ +export const teamNameSchema = safePathSegment('team name').describe( + 'Team name (folder in ~/.claude/teams/)', +); + +/** Numeric task ID produced by teamctl's highwatermark counter */ +export const taskIdSchema = z + .string() + .regex(/^\d{1,10}$/, 'Task ID must be a positive integer (e.g. "1", "42")') + .describe('Numeric task ID'); + +/** Team member name — folder inside inboxes, safe path segment */ +export const memberNameSchema = safePathSegment('member name').describe( + 'Team member name', +); + +// --------------------------------------------------------------------------- +// Enums — match teamctl's normalizeStatus / normalizeColumn / etc. +// --------------------------------------------------------------------------- + +export const taskStatusSchema = z.enum([ + 'pending', + 'in_progress', + 'completed', + 'deleted', +]); + +export const kanbanColumnSchema = z + .enum(['review', 'approved']) + .describe('Kanban column to move task to'); + +export const clarificationSchema = z + .enum(['lead', 'user', 'clear']) + .describe('Who needs to clarify: lead, user, or clear the flag'); + +export const linkTypeSchema = z + .enum(['blocked-by', 'blocks', 'related']) + .describe('Relationship type between tasks'); + +export const linkOperationSchema = z.enum(['link', 'unlink']); + +export const reviewDecisionSchema = z.enum(['approve', 'request-changes']); + +export const reviewerOperationSchema = z.enum(['list', 'add', 'remove']); + +// --------------------------------------------------------------------------- +// Composite schemas +// --------------------------------------------------------------------------- + +/** Comma-separated task IDs sent as a single CLI argument */ +export const taskIdsArraySchema = z + .array(taskIdSchema) + .describe('Array of task IDs (e.g. ["1", "3"])'); + +// --------------------------------------------------------------------------- +// File / attachment schemas — defence-in-depth for CLI arguments +// --------------------------------------------------------------------------- + +/** Absolute file path without traversal sequences */ +export const filePathSchema = z + .string() + .min(1) + .refine((p) => isAbsolute(p), { message: 'Path must be absolute' }) + .refine((p) => !p.split(sep).includes('..'), { + message: 'Path must not contain traversal sequences (..)', + }) + .refine((p) => !p.includes('\0'), { + message: 'Path must not contain null bytes', + }); + +/** Safe filename — no path separators, no null bytes, reasonable length */ +export const safeFilenameSchema = z + .string() + .min(1) + .max(255) + .refine( + (f) => !f.includes('/') && !f.includes('\\') && !f.includes('\0'), + { message: 'Filename must not contain path separators or null bytes' }, + ); + +/** MIME type — standard type/subtype format */ +export const mimeTypeSchema = z + .string() + .regex( + /^[a-zA-Z0-9][a-zA-Z0-9!#$&\-^_]*\/[a-zA-Z0-9][a-zA-Z0-9!#$&\-^_.+]*$/, + 'Invalid MIME type format (expected type/subtype)', + ); diff --git a/mcp-server/src/teamctl-runner.ts b/mcp-server/src/teamctl-runner.ts new file mode 100644 index 00000000..d9892106 --- /dev/null +++ b/mcp-server/src/teamctl-runner.ts @@ -0,0 +1,155 @@ +import { spawn } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { homedir } from 'node:os'; +import { join } from 'node:path'; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export interface TeamctlResult { + stdout: string; + stderr: string; + exitCode: number; +} + +export interface ITeamctlRunner { + execute(args: string[]): Promise; +} + +export interface TeamctlRunnerOptions { + /** Explicit path to teamctl.js. Falls back to TEAMCTL_PATH env, then default. */ + teamctlPath?: string; + /** Max concurrent subprocess calls (default: 5) */ + maxConcurrent?: number; + /** Subprocess timeout in ms (default: 10 000) */ + timeoutMs?: number; +} + +// --------------------------------------------------------------------------- +// Semaphore — limits concurrent subprocess spawns +// --------------------------------------------------------------------------- + +class Semaphore { + private current = 0; + private queue: Array<() => void> = []; + + constructor(private readonly max: number) {} + + async acquire(): Promise { + if (this.current < this.max) { + this.current++; + return; + } + return new Promise((resolve) => { + this.queue.push(() => { + this.current++; + resolve(); + }); + }); + } + + release(): void { + this.current--; + const next = this.queue.shift(); + if (next) next(); + } +} + +// --------------------------------------------------------------------------- +// TeamctlRunner +// --------------------------------------------------------------------------- + +export class TeamctlRunner implements ITeamctlRunner { + readonly teamctlPath: string; + private readonly timeoutMs: number; + private readonly semaphore: Semaphore; + + constructor(options?: TeamctlRunnerOptions) { + this.teamctlPath = resolveTeamctlPath(options?.teamctlPath); + this.timeoutMs = options?.timeoutMs ?? 10_000; + this.semaphore = new Semaphore(options?.maxConcurrent ?? 5); + + // Fail fast if teamctl.js doesn't exist + if (!existsSync(this.teamctlPath)) { + throw new Error( + `teamctl.js not found at ${this.teamctlPath}. ` + + 'Make sure Claude Agent Teams UI has been run at least once, ' + + 'or set the TEAMCTL_PATH environment variable.', + ); + } + } + + async execute(args: string[]): Promise { + await this.semaphore.acquire(); + try { + return await this.spawn(args); + } finally { + this.semaphore.release(); + } + } + + private spawn(args: string[]): Promise { + return new Promise((resolve, reject) => { + const child = spawn('node', [this.teamctlPath, ...args], { + stdio: ['ignore', 'pipe', 'pipe'], + timeout: this.timeoutMs, + env: { ...process.env }, + }); + + const stdoutChunks: Buffer[] = []; + const stderrChunks: Buffer[] = []; + + child.stdout.on('data', (chunk: Buffer) => stdoutChunks.push(chunk)); + child.stderr.on('data', (chunk: Buffer) => stderrChunks.push(chunk)); + + let settled = false; + + child.on('error', (err) => { + if (!settled) { + settled = true; + reject(new Error(`Failed to spawn teamctl: ${err.message}`)); + } + }); + + child.on('close', (code, signal) => { + if (settled) return; + settled = true; + + const stdout = Buffer.concat(stdoutChunks).toString('utf-8'); + const stderr = Buffer.concat(stderrChunks).toString('utf-8'); + + if (signal === 'SIGTERM') { + const partial = stdout.slice(0, 500) || stderr.slice(0, 500); + reject( + new Error( + `teamctl timed out after ${this.timeoutMs}ms` + + (partial ? `. Partial output: ${partial}` : ''), + ), + ); + return; + } + + resolve({ + stdout, + stderr, + exitCode: code ?? 1, + }); + }); + }); + } +} + +// --------------------------------------------------------------------------- +// Path resolution +// --------------------------------------------------------------------------- + +function resolveTeamctlPath(explicit?: string): string { + if (explicit) return explicit; + + const fromEnv = process.env['TEAMCTL_PATH']; + if (fromEnv) return fromEnv; + + // Default: ~/.claude/tools/teamctl.js + return join(homedir(), '.claude', 'tools', 'teamctl.js'); +} diff --git a/mcp-server/src/tools/index.ts b/mcp-server/src/tools/index.ts new file mode 100644 index 00000000..6a4da6f9 --- /dev/null +++ b/mcp-server/src/tools/index.ts @@ -0,0 +1,42 @@ +import type { FastMCP } from 'fastmcp'; +import type { ITeamctlRunner } from '../teamctl-runner.js'; + +import { register as taskCreate } from './task-create.js'; +import { register as taskSetStatus } from './task-set-status.js'; +import { register as taskSetOwner } from './task-set-owner.js'; +import { register as taskGet } from './task-get.js'; +import { register as taskList } from './task-list.js'; +import { register as taskComment } from './task-comment.js'; +import { register as taskLink } from './task-link.js'; +import { register as taskBriefing } from './task-briefing.js'; +import { register as taskAttach } from './task-attach.js'; +import { register as kanbanMove } from './kanban-move.js'; +import { register as kanbanReviewers } from './kanban-reviewers.js'; +import { register as reviewAction } from './review-action.js'; +import { register as messageSend } from './message-send.js'; + +const ALL_TOOLS = [ + taskCreate, + taskSetStatus, + taskSetOwner, + taskGet, + taskList, + taskComment, + taskLink, + taskBriefing, + taskAttach, + kanbanMove, + kanbanReviewers, + reviewAction, + messageSend, +] as const; + +/** + * Register all 13 MCP tools with the server. + * Each tool wraps a teamctl CLI command via the runner. + */ +export function registerAllTools(server: FastMCP, runner: ITeamctlRunner): void { + for (const register of ALL_TOOLS) { + register(server, runner); + } +} diff --git a/mcp-server/src/tools/kanban-move.ts b/mcp-server/src/tools/kanban-move.ts new file mode 100644 index 00000000..565a836f --- /dev/null +++ b/mcp-server/src/tools/kanban-move.ts @@ -0,0 +1,45 @@ +import { z } from 'zod'; +import { UserError } from 'fastmcp'; +import type { FastMCP } from 'fastmcp'; +import type { ITeamctlRunner } from '../teamctl-runner.js'; +import { parseOkOutput } from '../output-parser.js'; +import { teamNameSchema, taskIdSchema, kanbanColumnSchema } from '../schemas.js'; + +export function register(server: FastMCP, runner: ITeamctlRunner): void { + server.addTool({ + name: 'kanban_move', + description: `Move a task to a kanban column or clear it from the kanban board. + +Columns: "review" (awaiting code review) or "approved" (review passed). +Use operation "clear" to remove a task from the kanban board (returns to status-based display). +Moving to a kanban column also sets the task status to "completed".`, + annotations: { + readOnlyHint: false, + destructiveHint: false, + }, + parameters: z.object({ + team: teamNameSchema, + task_id: taskIdSchema, + operation: z.enum(['set-column', 'clear']).describe('"set-column" to move, "clear" to remove from kanban'), + column: kanbanColumnSchema.optional().describe('Target column (required for set-column)'), + }), + execute: async (args) => { + let cliArgs: string[]; + + if (args.operation === 'set-column') { + if (!args.column) { + throw new UserError('column is required when operation is "set-column"'); + } + cliArgs = ['--team', args.team, 'kanban', 'set-column', args.task_id, args.column]; + } else { + cliArgs = ['--team', args.team, 'kanban', 'clear', args.task_id]; + } + + const result = await runner.execute(cliArgs); + if (result.exitCode !== 0) { + throw new UserError(`Failed to update kanban: ${result.stderr.trim() || result.stdout.trim()}`); + } + return parseOkOutput(result.stdout); + }, + }); +} diff --git a/mcp-server/src/tools/kanban-reviewers.ts b/mcp-server/src/tools/kanban-reviewers.ts new file mode 100644 index 00000000..03569247 --- /dev/null +++ b/mcp-server/src/tools/kanban-reviewers.ts @@ -0,0 +1,46 @@ +import { z } from 'zod'; +import { UserError } from 'fastmcp'; +import type { FastMCP } from 'fastmcp'; +import type { ITeamctlRunner } from '../teamctl-runner.js'; +import { parseJsonOutput, parseOkOutput } from '../output-parser.js'; +import { teamNameSchema, memberNameSchema, reviewerOperationSchema } from '../schemas.js'; + +export function register(server: FastMCP, runner: ITeamctlRunner): void { + server.addTool({ + name: 'kanban_reviewers', + description: `Manage the kanban board's reviewer list. + +Operations: +- "list": returns JSON array of reviewer names +- "add": adds a reviewer (name required) +- "remove": removes a reviewer (name required)`, + annotations: { + readOnlyHint: false, + destructiveHint: false, + }, + parameters: z.object({ + team: teamNameSchema, + operation: reviewerOperationSchema.describe('"list", "add", or "remove"'), + name: memberNameSchema.optional().describe('Reviewer name (required for add/remove)'), + }), + execute: async (args) => { + if (args.operation !== 'list' && !args.name) { + throw new UserError(`name is required for "${args.operation}" operation`); + } + + const cliArgs = ['--team', args.team, 'kanban', 'reviewers', args.operation]; + if (args.name) cliArgs.push(args.name); + + const result = await runner.execute(cliArgs); + if (result.exitCode !== 0) { + throw new UserError(`Failed to manage reviewers: ${result.stderr.trim() || result.stdout.trim()}`); + } + + // "list" returns JSON array, "add"/"remove" return "OK ..." text + if (args.operation === 'list') { + return parseJsonOutput(result.stdout); + } + return parseOkOutput(result.stdout); + }, + }); +} diff --git a/mcp-server/src/tools/message-send.ts b/mcp-server/src/tools/message-send.ts new file mode 100644 index 00000000..0693bd33 --- /dev/null +++ b/mcp-server/src/tools/message-send.ts @@ -0,0 +1,39 @@ +import { z } from 'zod'; +import { UserError } from 'fastmcp'; +import type { FastMCP } from 'fastmcp'; +import type { ITeamctlRunner } from '../teamctl-runner.js'; +import { parseJsonOutput } from '../output-parser.js'; +import { teamNameSchema, memberNameSchema } from '../schemas.js'; + +export function register(server: FastMCP, runner: ITeamctlRunner): void { + server.addTool({ + name: 'message_send', + description: `Send an inbox message to a team member. Returns delivery confirmation JSON. + +Messages appear in the member's inbox and can trigger notifications. +The "source" field is automatically stripped for security — external callers cannot impersonate system notifications.`, + annotations: { + readOnlyHint: false, + destructiveHint: false, + }, + parameters: z.object({ + team: teamNameSchema, + to: memberNameSchema.describe('Recipient member name'), + text: z.string().min(1).max(10000).describe('Message text'), + summary: z.string().max(200).optional().describe('Short summary for notification preview'), + from: memberNameSchema.optional().describe('Sender name'), + }), + execute: async (args) => { + const cliArgs = ['--team', args.team, 'message', 'send', '--to', args.to, '--text', args.text]; + + if (args.summary) cliArgs.push('--summary', args.summary); + if (args.from) cliArgs.push('--from', args.from); + + const result = await runner.execute(cliArgs); + if (result.exitCode !== 0) { + throw new UserError(`Failed to send message: ${result.stderr.trim() || result.stdout.trim()}`); + } + return parseJsonOutput(result.stdout); + }, + }); +} diff --git a/mcp-server/src/tools/review-action.ts b/mcp-server/src/tools/review-action.ts new file mode 100644 index 00000000..64bad45d --- /dev/null +++ b/mcp-server/src/tools/review-action.ts @@ -0,0 +1,50 @@ +import { z } from 'zod'; +import { UserError } from 'fastmcp'; +import type { FastMCP } from 'fastmcp'; +import type { ITeamctlRunner } from '../teamctl-runner.js'; +import { parseOkOutput } from '../output-parser.js'; +import { teamNameSchema, taskIdSchema, memberNameSchema, reviewDecisionSchema } from '../schemas.js'; + +export function register(server: FastMCP, runner: ITeamctlRunner): void { + server.addTool({ + name: 'review_action', + description: `Approve a task or request changes. + +- "approve": marks the task as approved in kanban, optionally posts a review comment +- "request-changes": removes from kanban, resets status to "in_progress", notifies the task owner with the change request comment`, + annotations: { + readOnlyHint: false, + destructiveHint: false, + }, + parameters: z.object({ + team: teamNameSchema, + task_id: taskIdSchema, + decision: reviewDecisionSchema.describe('"approve" or "request-changes"'), + comment: z.string().max(5000).optional().describe('Review comment (required for request-changes)'), + from: memberNameSchema.optional().describe('Reviewer name'), + notify_owner: z.boolean().optional().describe('Notify the task owner (for approve)'), + }), + execute: async (args) => { + if (args.decision === 'request-changes' && !args.comment) { + throw new UserError('comment is required when requesting changes'); + } + + const cliArgs = ['--team', args.team, 'review', args.decision, args.task_id]; + + // approve uses --note for optional comment; request-changes uses --comment + if (args.decision === 'request-changes' && args.comment) { + cliArgs.push('--comment', args.comment); + } else if (args.decision === 'approve' && args.comment) { + cliArgs.push('--note', args.comment); + } + if (args.from) cliArgs.push('--from', args.from); + if (args.notify_owner) cliArgs.push('--notify-owner'); + + const result = await runner.execute(cliArgs); + if (result.exitCode !== 0) { + throw new UserError(`Failed to ${args.decision}: ${result.stderr.trim() || result.stdout.trim()}`); + } + return parseOkOutput(result.stdout); + }, + }); +} diff --git a/mcp-server/src/tools/task-attach.ts b/mcp-server/src/tools/task-attach.ts new file mode 100644 index 00000000..a0e9830d --- /dev/null +++ b/mcp-server/src/tools/task-attach.ts @@ -0,0 +1,42 @@ +import { z } from 'zod'; +import { UserError } from 'fastmcp'; +import type { FastMCP } from 'fastmcp'; +import type { ITeamctlRunner } from '../teamctl-runner.js'; +import { parseJsonOutput } from '../output-parser.js'; +import { teamNameSchema, taskIdSchema, memberNameSchema, filePathSchema, safeFilenameSchema, mimeTypeSchema } from '../schemas.js'; + +export function register(server: FastMCP, runner: ITeamctlRunner): void { + server.addTool({ + name: 'task_attach', + description: `Attach a file to a task. Returns attachment metadata JSON. + +Supports copy (default) or hardlink mode. MIME type is auto-detected from file content (PNG, JPEG, GIF, WebP, PDF, ZIP) with fallback to application/octet-stream. Max file size: 20 MB.`, + annotations: { + readOnlyHint: false, + destructiveHint: false, + }, + parameters: z.object({ + team: teamNameSchema, + task_id: taskIdSchema, + file: filePathSchema.describe('Absolute path to the file to attach'), + filename: safeFilenameSchema.optional().describe('Override stored filename'), + mime_type: mimeTypeSchema.optional().describe('Override MIME type (auto-detected by default)'), + mode: z.enum(['copy', 'link']).optional().describe('Storage mode: copy (default) or hardlink'), + from: memberNameSchema.optional().describe('Uploader name'), + }), + execute: async (args) => { + const cliArgs = ['--team', args.team, 'task', 'attach', args.task_id, '--file', args.file]; + + if (args.filename) cliArgs.push('--filename', args.filename); + if (args.mime_type) cliArgs.push('--mime-type', args.mime_type); + if (args.mode) cliArgs.push('--mode', args.mode); + if (args.from) cliArgs.push('--from', args.from); + + const result = await runner.execute(cliArgs); + if (result.exitCode !== 0) { + throw new UserError(`Failed to attach file: ${result.stderr.trim() || result.stdout.trim()}`); + } + return parseJsonOutput(result.stdout); + }, + }); +} diff --git a/mcp-server/src/tools/task-briefing.ts b/mcp-server/src/tools/task-briefing.ts new file mode 100644 index 00000000..9273a6d7 --- /dev/null +++ b/mcp-server/src/tools/task-briefing.ts @@ -0,0 +1,32 @@ +import { z } from 'zod'; +import { UserError } from 'fastmcp'; +import type { FastMCP } from 'fastmcp'; +import type { ITeamctlRunner } from '../teamctl-runner.js'; +import { parseTextOutput } from '../output-parser.js'; +import { teamNameSchema, memberNameSchema } from '../schemas.js'; + +export function register(server: FastMCP, runner: ITeamctlRunner): void { + server.addTool({ + name: 'task_briefing', + description: `Generate a text briefing for a team member showing their assigned tasks vs the team board. + +Returns a human-readable multi-line report. Automatically filters out internal bookkeeping tasks.`, + annotations: { + readOnlyHint: true, + destructiveHint: false, + }, + parameters: z.object({ + team: teamNameSchema, + member: memberNameSchema.describe('Member name to generate briefing for'), + }), + execute: async (args) => { + const cliArgs = ['--team', args.team, 'task', 'briefing', '--for', args.member]; + + const result = await runner.execute(cliArgs); + if (result.exitCode !== 0) { + throw new UserError(`Failed to get briefing: ${result.stderr.trim() || result.stdout.trim()}`); + } + return parseTextOutput(result.stdout); + }, + }); +} diff --git a/mcp-server/src/tools/task-comment.ts b/mcp-server/src/tools/task-comment.ts new file mode 100644 index 00000000..951ef0a9 --- /dev/null +++ b/mcp-server/src/tools/task-comment.ts @@ -0,0 +1,34 @@ +import { z } from 'zod'; +import { UserError } from 'fastmcp'; +import type { FastMCP } from 'fastmcp'; +import type { ITeamctlRunner } from '../teamctl-runner.js'; +import { parseOkOutput } from '../output-parser.js'; +import { teamNameSchema, taskIdSchema, memberNameSchema } from '../schemas.js'; + +export function register(server: FastMCP, runner: ITeamctlRunner): void { + server.addTool({ + name: 'task_comment', + description: `Add a comment to a task. Sends an inbox notification to the task owner (unless the commenter is the owner).`, + annotations: { + readOnlyHint: false, + destructiveHint: false, + }, + parameters: z.object({ + team: teamNameSchema, + task_id: taskIdSchema, + text: z.string().min(1).max(10000).describe('Comment text'), + from: memberNameSchema.optional().describe('Author name (skips self-notification)'), + }), + execute: async (args) => { + const cliArgs = ['--team', args.team, 'task', 'comment', args.task_id, '--text', args.text]; + + if (args.from) cliArgs.push('--from', args.from); + + const result = await runner.execute(cliArgs); + if (result.exitCode !== 0) { + throw new UserError(`Failed to add comment: ${result.stderr.trim() || result.stdout.trim()}`); + } + return parseOkOutput(result.stdout); + }, + }); +} diff --git a/mcp-server/src/tools/task-create.ts b/mcp-server/src/tools/task-create.ts new file mode 100644 index 00000000..5c08e4e7 --- /dev/null +++ b/mcp-server/src/tools/task-create.ts @@ -0,0 +1,52 @@ +import { z } from 'zod'; +import { UserError } from 'fastmcp'; +import type { FastMCP } from 'fastmcp'; +import type { ITeamctlRunner } from '../teamctl-runner.js'; +import { parseJsonOutput } from '../output-parser.js'; +import { teamNameSchema, memberNameSchema, taskIdsArraySchema } from '../schemas.js'; + +export function register(server: FastMCP, runner: ITeamctlRunner): void { + server.addTool({ + name: 'task_create', + description: `Create a new task in a team's task board. Returns the created task JSON. + +Behavior: +- If owner is set and no blockers → status defaults to "in_progress" +- If blocked_by specified → status defaults to "pending" (even with owner) +- If notify is true, sends an inbox notification to the assigned owner`, + annotations: { + readOnlyHint: false, + destructiveHint: false, + }, + parameters: z.object({ + team: teamNameSchema, + subject: z.string().min(1).max(500).describe('Task title'), + description: z.string().max(5000).optional().describe('Detailed task description'), + owner: memberNameSchema.optional().describe('Assign to a team member'), + blocked_by: taskIdsArraySchema.optional().describe('Task IDs that block this task'), + related: taskIdsArraySchema.optional().describe('Related (non-blocking) task IDs'), + status: z.enum(['pending', 'in_progress']).optional().describe('Initial status override'), + active_form: z.string().max(200).optional().describe('Active form hint for CLI display'), + notify: z.boolean().optional().describe('Send inbox notification to owner'), + from: memberNameSchema.optional().describe('Author name for notifications'), + }), + execute: async (args) => { + const cliArgs = ['--team', args.team, 'task', 'create', '--subject', args.subject]; + + if (args.description) cliArgs.push('--description', args.description); + if (args.owner) cliArgs.push('--owner', args.owner); + if (args.blocked_by?.length) cliArgs.push('--blocked-by', args.blocked_by.join(',')); + if (args.related?.length) cliArgs.push('--related', args.related.join(',')); + if (args.status) cliArgs.push('--status', args.status); + if (args.active_form) cliArgs.push('--activeForm', args.active_form); + if (args.notify) cliArgs.push('--notify'); + if (args.from) cliArgs.push('--from', args.from); + + const result = await runner.execute(cliArgs); + if (result.exitCode !== 0) { + throw new UserError(`Failed to create task: ${result.stderr.trim() || result.stdout.trim()}`); + } + return parseJsonOutput(result.stdout); + }, + }); +} diff --git a/mcp-server/src/tools/task-get.ts b/mcp-server/src/tools/task-get.ts new file mode 100644 index 00000000..cf56ec6d --- /dev/null +++ b/mcp-server/src/tools/task-get.ts @@ -0,0 +1,30 @@ +import { z } from 'zod'; +import { UserError } from 'fastmcp'; +import type { FastMCP } from 'fastmcp'; +import type { ITeamctlRunner } from '../teamctl-runner.js'; +import { parseJsonOutput } from '../output-parser.js'; +import { teamNameSchema, taskIdSchema } from '../schemas.js'; + +export function register(server: FastMCP, runner: ITeamctlRunner): void { + server.addTool({ + name: 'task_get', + description: `Get a single task by its ID. Returns the full task JSON including status, owner, comments, dependencies, work intervals, and status history.`, + annotations: { + readOnlyHint: true, + destructiveHint: false, + }, + parameters: z.object({ + team: teamNameSchema, + task_id: taskIdSchema, + }), + execute: async (args) => { + const cliArgs = ['--team', args.team, 'task', 'get', args.task_id]; + + const result = await runner.execute(cliArgs); + if (result.exitCode !== 0) { + throw new UserError(`Failed to get task: ${result.stderr.trim() || result.stdout.trim()}`); + } + return parseJsonOutput(result.stdout); + }, + }); +} diff --git a/mcp-server/src/tools/task-link.ts b/mcp-server/src/tools/task-link.ts new file mode 100644 index 00000000..0d43021c --- /dev/null +++ b/mcp-server/src/tools/task-link.ts @@ -0,0 +1,51 @@ +import { z } from 'zod'; +import { UserError } from 'fastmcp'; +import type { FastMCP } from 'fastmcp'; +import type { ITeamctlRunner } from '../teamctl-runner.js'; +import { parseOkOutput } from '../output-parser.js'; +import { + teamNameSchema, + taskIdSchema, + linkTypeSchema, + linkOperationSchema, +} from '../schemas.js'; + +export function register(server: FastMCP, runner: ITeamctlRunner): void { + server.addTool({ + name: 'task_link', + description: `Link or unlink task dependencies. + +Relationship types: +- "blocked-by": this task is blocked by the target +- "blocks": this task blocks the target +- "related": non-blocking relationship + +Links are bidirectional: linking A blocked-by B also sets B blocks A. +Circular dependencies are detected and rejected.`, + annotations: { + readOnlyHint: false, + destructiveHint: false, + }, + parameters: z.object({ + team: teamNameSchema, + task_id: taskIdSchema, + operation: linkOperationSchema.describe('"link" to add, "unlink" to remove'), + relationship: linkTypeSchema, + target_id: taskIdSchema.describe('The other task ID to link/unlink'), + }), + execute: async (args) => { + const cliArgs = [ + '--team', args.team, + 'task', args.operation, + args.task_id, + `--${args.relationship}`, args.target_id, + ]; + + const result = await runner.execute(cliArgs); + if (result.exitCode !== 0) { + throw new UserError(`Failed to ${args.operation} tasks: ${result.stderr.trim() || result.stdout.trim()}`); + } + return parseOkOutput(result.stdout); + }, + }); +} diff --git a/mcp-server/src/tools/task-list.ts b/mcp-server/src/tools/task-list.ts new file mode 100644 index 00000000..37c05bfd --- /dev/null +++ b/mcp-server/src/tools/task-list.ts @@ -0,0 +1,31 @@ +import { z } from 'zod'; +import { UserError } from 'fastmcp'; +import type { FastMCP } from 'fastmcp'; +import type { ITeamctlRunner } from '../teamctl-runner.js'; +import { parseJsonOutput } from '../output-parser.js'; +import { teamNameSchema } from '../schemas.js'; + +export function register(server: FastMCP, runner: ITeamctlRunner): void { + server.addTool({ + name: 'task_list', + description: `List all tasks for a team. Returns a JSON array of task objects. + +Note: includes internal bookkeeping tasks (metadata._internal). Filter client-side if needed.`, + annotations: { + readOnlyHint: true, + destructiveHint: false, + }, + parameters: z.object({ + team: teamNameSchema, + }), + execute: async (args) => { + const cliArgs = ['--team', args.team, 'task', 'list']; + + const result = await runner.execute(cliArgs); + if (result.exitCode !== 0) { + throw new UserError(`Failed to list tasks: ${result.stderr.trim() || result.stdout.trim()}`); + } + return parseJsonOutput(result.stdout); + }, + }); +} diff --git a/mcp-server/src/tools/task-set-owner.ts b/mcp-server/src/tools/task-set-owner.ts new file mode 100644 index 00000000..4cfd04bf --- /dev/null +++ b/mcp-server/src/tools/task-set-owner.ts @@ -0,0 +1,38 @@ +import { z } from 'zod'; +import { UserError } from 'fastmcp'; +import type { FastMCP } from 'fastmcp'; +import type { ITeamctlRunner } from '../teamctl-runner.js'; +import { parseOkOutput } from '../output-parser.js'; +import { teamNameSchema, taskIdSchema, memberNameSchema } from '../schemas.js'; + +export function register(server: FastMCP, runner: ITeamctlRunner): void { + server.addTool({ + name: 'task_set_owner', + description: `Assign a task to a team member or clear the assignment. + +Pass owner="clear" to unassign. Optionally sends an inbox notification to the new owner.`, + annotations: { + readOnlyHint: false, + destructiveHint: false, + }, + parameters: z.object({ + team: teamNameSchema, + task_id: taskIdSchema, + owner: z.union([memberNameSchema, z.literal('clear')]).describe('Member name to assign, or "clear" to unassign'), + notify: z.boolean().optional().describe('Send inbox notification to new owner'), + from: memberNameSchema.optional().describe('Author name for notification'), + }), + execute: async (args) => { + const cliArgs = ['--team', args.team, 'task', 'set-owner', args.task_id, args.owner]; + + if (args.notify) cliArgs.push('--notify'); + if (args.from) cliArgs.push('--from', args.from); + + const result = await runner.execute(cliArgs); + if (result.exitCode !== 0) { + throw new UserError(`Failed to set owner: ${result.stderr.trim() || result.stdout.trim()}`); + } + return parseOkOutput(result.stdout); + }, + }); +} diff --git a/mcp-server/src/tools/task-set-status.ts b/mcp-server/src/tools/task-set-status.ts new file mode 100644 index 00000000..a5b96294 --- /dev/null +++ b/mcp-server/src/tools/task-set-status.ts @@ -0,0 +1,35 @@ +import { z } from 'zod'; +import { UserError } from 'fastmcp'; +import type { FastMCP } from 'fastmcp'; +import type { ITeamctlRunner } from '../teamctl-runner.js'; +import { parseOkOutput } from '../output-parser.js'; +import { teamNameSchema, taskIdSchema, taskStatusSchema } from '../schemas.js'; + +export function register(server: FastMCP, runner: ITeamctlRunner): void { + server.addTool({ + name: 'task_set_status', + description: `Change the status of a task. + +Valid transitions: pending → in_progress → completed → deleted (and back). +Shortcuts: status "in_progress" is equivalent to "task start", "completed" to "task complete". +Records status history and tracks work intervals (time spent in_progress).`, + annotations: { + readOnlyHint: false, + destructiveHint: false, + }, + parameters: z.object({ + team: teamNameSchema, + task_id: taskIdSchema, + status: taskStatusSchema.describe('New status for the task'), + }), + execute: async (args) => { + const cliArgs = ['--team', args.team, 'task', 'set-status', args.task_id, args.status]; + + const result = await runner.execute(cliArgs); + if (result.exitCode !== 0) { + throw new UserError(`Failed to set status: ${result.stderr.trim() || result.stdout.trim()}`); + } + return parseOkOutput(result.stdout); + }, + }); +} diff --git a/mcp-server/test/output-parser.test.ts b/mcp-server/test/output-parser.test.ts new file mode 100644 index 00000000..52f8179f --- /dev/null +++ b/mcp-server/test/output-parser.test.ts @@ -0,0 +1,76 @@ +import { describe, it, expect } from 'vitest'; +import { + parseJsonOutput, + parseOkOutput, + parseTextOutput, + formatError, +} from '../src/output-parser.js'; + +describe('parseJsonOutput', () => { + it('parses valid JSON object', () => { + const input = '{"id":"42","subject":"Fix bug"}\n'; + expect(parseJsonOutput(input)).toEqual({ id: '42', subject: 'Fix bug' }); + }); + + it('parses valid JSON array', () => { + const input = '[{"id":"1"},{"id":"2"}]\n'; + expect(parseJsonOutput(input)).toEqual([{ id: '1' }, { id: '2' }]); + }); + + it('trims whitespace', () => { + const input = ' \n {"ok":true} \n '; + expect(parseJsonOutput(input)).toEqual({ ok: true }); + }); + + it('throws on empty output', () => { + expect(() => parseJsonOutput('')).toThrow('Empty output'); + expect(() => parseJsonOutput(' \n ')).toThrow('Empty output'); + }); + + it('throws on invalid JSON', () => { + expect(() => parseJsonOutput('not json')).toThrow('Failed to parse'); + }); +}); + +describe('parseOkOutput', () => { + it('strips "OK " prefix', () => { + expect(parseOkOutput('OK task #1 status=completed\n')).toBe('task #1 status=completed'); + }); + + it('handles bare "OK"', () => { + expect(parseOkOutput('OK\n')).toBe('OK'); + }); + + it('returns as-is for unexpected format', () => { + expect(parseOkOutput('Something else')).toBe('Something else'); + }); + + it('trims whitespace', () => { + expect(parseOkOutput(' OK kanban #1 cleared \n')).toBe('kanban #1 cleared'); + }); +}); + +describe('parseTextOutput', () => { + it('trims and returns text', () => { + const briefing = '=== Task Briefing for alice ===\nTask #1: Fix bug\n'; + expect(parseTextOutput(briefing)).toBe('=== Task Briefing for alice ===\nTask #1: Fix bug'); + }); + + it('handles empty string', () => { + expect(parseTextOutput('')).toBe(''); + }); +}); + +describe('formatError', () => { + it('uses stderr when available', () => { + expect(formatError('Task not found: #42\n', '')).toBe('Task not found: #42'); + }); + + it('falls back to stdout', () => { + expect(formatError('', 'Unexpected error\n')).toBe('Unexpected error'); + }); + + it('returns default for empty', () => { + expect(formatError('', '')).toBe('Unknown teamctl error'); + }); +}); diff --git a/mcp-server/test/schemas.test.ts b/mcp-server/test/schemas.test.ts new file mode 100644 index 00000000..cfd8b5f6 --- /dev/null +++ b/mcp-server/test/schemas.test.ts @@ -0,0 +1,220 @@ +import { describe, it, expect } from 'vitest'; +import { + teamNameSchema, + taskIdSchema, + memberNameSchema, + taskStatusSchema, + kanbanColumnSchema, + clarificationSchema, + linkTypeSchema, + linkOperationSchema, + reviewDecisionSchema, + reviewerOperationSchema, + taskIdsArraySchema, + filePathSchema, + safeFilenameSchema, + mimeTypeSchema, +} from '../src/schemas.js'; + +describe('teamNameSchema', () => { + it('accepts valid team names', () => { + expect(teamNameSchema.parse('acme')).toBe('acme'); + expect(teamNameSchema.parse('my-team')).toBe('my-team'); + expect(teamNameSchema.parse('My_Team')).toBe('My_Team'); + expect(teamNameSchema.parse('team123')).toBe('team123'); + }); + + it('rejects empty', () => { + expect(() => teamNameSchema.parse('')).toThrow(); + }); + + it('rejects path traversal', () => { + expect(() => teamNameSchema.parse('..')).toThrow(); + expect(() => teamNameSchema.parse('.')).toThrow(); + expect(() => teamNameSchema.parse('a/b')).toThrow(); + expect(() => teamNameSchema.parse('a\\b')).toThrow(); + expect(() => teamNameSchema.parse('a..b')).toThrow(); + }); + + it('rejects null bytes', () => { + expect(() => teamNameSchema.parse('a\0b')).toThrow(); + }); + + it('rejects too long', () => { + expect(() => teamNameSchema.parse('a'.repeat(129))).toThrow(); + }); +}); + +describe('taskIdSchema', () => { + it('accepts numeric IDs', () => { + expect(taskIdSchema.parse('1')).toBe('1'); + expect(taskIdSchema.parse('42')).toBe('42'); + expect(taskIdSchema.parse('1234567890')).toBe('1234567890'); + }); + + it('accepts zero', () => { + expect(taskIdSchema.parse('0')).toBe('0'); + }); + + it('accepts leading zeros', () => { + expect(taskIdSchema.parse('007')).toBe('007'); + }); + + it('rejects non-numeric', () => { + expect(() => taskIdSchema.parse('abc')).toThrow(); + expect(() => taskIdSchema.parse('')).toThrow(); + expect(() => taskIdSchema.parse('1.5')).toThrow(); + expect(() => taskIdSchema.parse('-1')).toThrow(); + }); + + it('rejects too long', () => { + expect(() => taskIdSchema.parse('12345678901')).toThrow(); + }); +}); + +describe('memberNameSchema', () => { + it('accepts valid member names', () => { + expect(memberNameSchema.parse('alice')).toBe('alice'); + expect(memberNameSchema.parse('bob-smith')).toBe('bob-smith'); + expect(memberNameSchema.parse('user_1')).toBe('user_1'); + }); + + it('rejects path traversal', () => { + expect(() => memberNameSchema.parse('..')).toThrow(); + expect(() => memberNameSchema.parse('a/b')).toThrow(); + expect(() => memberNameSchema.parse('a\\b')).toThrow(); + expect(() => memberNameSchema.parse('a..b')).toThrow(); + }); + + it('rejects null bytes', () => { + expect(() => memberNameSchema.parse('a\0b')).toThrow(); + }); + + it('rejects too long', () => { + expect(() => memberNameSchema.parse('a'.repeat(129))).toThrow(); + }); + + it('rejects empty', () => { + expect(() => memberNameSchema.parse('')).toThrow(); + }); +}); + +describe('enum schemas', () => { + it('taskStatusSchema accepts valid values', () => { + expect(taskStatusSchema.parse('pending')).toBe('pending'); + expect(taskStatusSchema.parse('in_progress')).toBe('in_progress'); + expect(taskStatusSchema.parse('completed')).toBe('completed'); + expect(taskStatusSchema.parse('deleted')).toBe('deleted'); + expect(() => taskStatusSchema.parse('invalid')).toThrow(); + }); + + it('kanbanColumnSchema accepts valid values', () => { + expect(kanbanColumnSchema.parse('review')).toBe('review'); + expect(kanbanColumnSchema.parse('approved')).toBe('approved'); + expect(() => kanbanColumnSchema.parse('todo')).toThrow(); + expect(() => kanbanColumnSchema.parse('')).toThrow(); + }); + + it('clarificationSchema accepts valid values', () => { + expect(clarificationSchema.parse('lead')).toBe('lead'); + expect(clarificationSchema.parse('user')).toBe('user'); + expect(clarificationSchema.parse('clear')).toBe('clear'); + expect(() => clarificationSchema.parse('nobody')).toThrow(); + }); + + it('linkTypeSchema accepts valid values', () => { + expect(linkTypeSchema.parse('blocked-by')).toBe('blocked-by'); + expect(linkTypeSchema.parse('blocks')).toBe('blocks'); + expect(linkTypeSchema.parse('related')).toBe('related'); + }); + + it('linkOperationSchema accepts valid values', () => { + expect(linkOperationSchema.parse('link')).toBe('link'); + expect(linkOperationSchema.parse('unlink')).toBe('unlink'); + }); + + it('reviewDecisionSchema accepts valid values', () => { + expect(reviewDecisionSchema.parse('approve')).toBe('approve'); + expect(reviewDecisionSchema.parse('request-changes')).toBe('request-changes'); + }); + + it('reviewerOperationSchema accepts valid values', () => { + expect(reviewerOperationSchema.parse('list')).toBe('list'); + expect(reviewerOperationSchema.parse('add')).toBe('add'); + expect(reviewerOperationSchema.parse('remove')).toBe('remove'); + }); +}); + +describe('taskIdsArraySchema', () => { + it('accepts valid arrays', () => { + expect(taskIdsArraySchema.parse(['1', '2', '3'])).toEqual(['1', '2', '3']); + expect(taskIdsArraySchema.parse([])).toEqual([]); + }); + + it('rejects arrays with invalid IDs', () => { + expect(() => taskIdsArraySchema.parse(['abc'])).toThrow(); + expect(() => taskIdsArraySchema.parse(['1', 'bad'])).toThrow(); + }); +}); + +describe('filePathSchema', () => { + it('accepts absolute paths', () => { + expect(filePathSchema.parse('/home/user/file.txt')).toBe('/home/user/file.txt'); + expect(filePathSchema.parse('/tmp/attachment.pdf')).toBe('/tmp/attachment.pdf'); + }); + + it('rejects relative paths', () => { + expect(() => filePathSchema.parse('relative/path.txt')).toThrow(); + expect(() => filePathSchema.parse('./file.txt')).toThrow(); + }); + + it('rejects path traversal', () => { + expect(() => filePathSchema.parse('/home/user/../etc/passwd')).toThrow(); + expect(() => filePathSchema.parse('/home/../../../etc/shadow')).toThrow(); + }); + + it('rejects null bytes', () => { + expect(() => filePathSchema.parse('/home/user/\0evil')).toThrow(); + }); + + it('rejects empty', () => { + expect(() => filePathSchema.parse('')).toThrow(); + }); +}); + +describe('safeFilenameSchema', () => { + it('accepts valid filenames', () => { + expect(safeFilenameSchema.parse('report.pdf')).toBe('report.pdf'); + expect(safeFilenameSchema.parse('my-file_v2.tar.gz')).toBe('my-file_v2.tar.gz'); + }); + + it('rejects path separators', () => { + expect(() => safeFilenameSchema.parse('../../evil')).toThrow(); + expect(() => safeFilenameSchema.parse('dir/file')).toThrow(); + expect(() => safeFilenameSchema.parse('dir\\file')).toThrow(); + }); + + it('rejects null bytes', () => { + expect(() => safeFilenameSchema.parse('file\0name')).toThrow(); + }); + + it('rejects too long', () => { + expect(() => safeFilenameSchema.parse('a'.repeat(256))).toThrow(); + }); +}); + +describe('mimeTypeSchema', () => { + it('accepts valid MIME types', () => { + expect(mimeTypeSchema.parse('application/pdf')).toBe('application/pdf'); + expect(mimeTypeSchema.parse('image/png')).toBe('image/png'); + expect(mimeTypeSchema.parse('text/plain')).toBe('text/plain'); + expect(mimeTypeSchema.parse('application/octet-stream')).toBe('application/octet-stream'); + }); + + it('rejects invalid formats', () => { + expect(() => mimeTypeSchema.parse('invalid')).toThrow(); + expect(() => mimeTypeSchema.parse('/pdf')).toThrow(); + expect(() => mimeTypeSchema.parse('application/')).toThrow(); + expect(() => mimeTypeSchema.parse('')).toThrow(); + }); +}); diff --git a/mcp-server/test/teamctl-runner.test.ts b/mcp-server/test/teamctl-runner.test.ts new file mode 100644 index 00000000..54b703f7 --- /dev/null +++ b/mcp-server/test/teamctl-runner.test.ts @@ -0,0 +1,78 @@ +import { describe, it, expect, vi } from 'vitest'; +import { TeamctlRunner } from '../src/teamctl-runner.js'; +import type { ITeamctlRunner, TeamctlResult } from '../src/teamctl-runner.js'; + +// We can't easily test the real subprocess without teamctl.js installed, +// so we test the interface contract and error handling. + +describe('TeamctlRunner', () => { + it('throws if teamctl.js does not exist', () => { + expect( + () => new TeamctlRunner({ teamctlPath: '/nonexistent/teamctl.js' }), + ).toThrow('teamctl.js not found'); + }); + + it('resolves path from TEAMCTL_PATH env', () => { + const original = process.env['TEAMCTL_PATH']; + try { + process.env['TEAMCTL_PATH'] = '/tmp/test-teamctl.js'; + // Will throw because file doesn't exist, but we can check the error message + expect( + () => new TeamctlRunner(), + ).toThrow('/tmp/test-teamctl.js'); + } finally { + if (original !== undefined) { + process.env['TEAMCTL_PATH'] = original; + } else { + delete process.env['TEAMCTL_PATH']; + } + } + }); +}); + +// Mock runner for tool tests +export function createMockRunner( + responses: Map | TeamctlResult, +): ITeamctlRunner { + return { + execute: vi.fn(async (args: string[]): Promise => { + if (responses instanceof Map) { + const key = args.join(' '); + const result = responses.get(key); + if (result) return result; + // Fallback: check if any key is a prefix + for (const [k, v] of responses) { + if (key.startsWith(k)) return v; + } + return { stdout: '', stderr: 'No mock for: ' + key, exitCode: 1 }; + } + return responses; + }), + }; +} + +describe('ITeamctlRunner interface', () => { + it('mock runner returns success', async () => { + const runner = createMockRunner({ + stdout: '{"id":"1","subject":"Test"}\n', + stderr: '', + exitCode: 0, + }); + + const result = await runner.execute(['--team', 'test', 'task', 'create']); + expect(result.exitCode).toBe(0); + expect(JSON.parse(result.stdout)).toHaveProperty('id', '1'); + }); + + it('mock runner returns error', async () => { + const runner = createMockRunner({ + stdout: '', + stderr: 'Task not found: #99\n', + exitCode: 1, + }); + + const result = await runner.execute(['--team', 'test', 'task', 'get', '99']); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain('Task not found'); + }); +}); diff --git a/mcp-server/test/tools/kanban-move.test.ts b/mcp-server/test/tools/kanban-move.test.ts new file mode 100644 index 00000000..e0468131 --- /dev/null +++ b/mcp-server/test/tools/kanban-move.test.ts @@ -0,0 +1,42 @@ +import { describe, it, expect } from 'vitest'; +import { register } from '../../src/tools/kanban-move.js'; +import { createMockRunner, createMockServer, ok, fail } from './test-helpers.js'; + +describe('kanban_move', () => { + function setup(response = ok('OK kanban #1 column=review\n')) { + const runner = createMockRunner(response); + const { server, tools } = createMockServer(); + register(server, runner); + return { runner, tool: tools.get('kanban_move')! }; + } + + it('builds set-column CLI args', async () => { + const { runner, tool } = setup(); + await tool.execute({ team: 'acme', task_id: '1', operation: 'set-column', column: 'review' }); + expect(runner.execute).toHaveBeenCalledWith([ + '--team', 'acme', 'kanban', 'set-column', '1', 'review', + ]); + }); + + it('builds clear CLI args', async () => { + const { runner, tool } = setup(ok('OK kanban #1 cleared\n')); + await tool.execute({ team: 'acme', task_id: '1', operation: 'clear' }); + expect(runner.execute).toHaveBeenCalledWith([ + '--team', 'acme', 'kanban', 'clear', '1', + ]); + }); + + it('throws UserError when set-column called without column', async () => { + const { tool } = setup(); + await expect( + tool.execute({ team: 'acme', task_id: '1', operation: 'set-column' }), + ).rejects.toThrow('column is required'); + }); + + it('throws on CLI failure', async () => { + const { tool } = setup(fail('task not found')); + await expect( + tool.execute({ team: 'acme', task_id: '99', operation: 'clear' }), + ).rejects.toThrow('Failed to update kanban'); + }); +}); diff --git a/mcp-server/test/tools/kanban-reviewers.test.ts b/mcp-server/test/tools/kanban-reviewers.test.ts new file mode 100644 index 00000000..ee854535 --- /dev/null +++ b/mcp-server/test/tools/kanban-reviewers.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect } from 'vitest'; +import { register } from '../../src/tools/kanban-reviewers.js'; +import { createMockRunner, createMockServer, ok, fail } from './test-helpers.js'; + +describe('kanban_reviewers', () => { + function setup(response = ok('["alice","bob"]')) { + const runner = createMockRunner(response); + const { server, tools } = createMockServer(); + register(server, runner); + return { runner, tool: tools.get('kanban_reviewers')! }; + } + + it('builds list CLI args', async () => { + const { runner, tool } = setup(); + await tool.execute({ team: 'acme', operation: 'list' }); + expect(runner.execute).toHaveBeenCalledWith([ + '--team', 'acme', 'kanban', 'reviewers', 'list', + ]); + }); + + it('returns JSON array for list', async () => { + const { tool } = setup(); + const result = await tool.execute({ team: 'acme', operation: 'list' }); + expect(result).toEqual(['alice', 'bob']); + }); + + it('builds add CLI args with name', async () => { + const { runner, tool } = setup(ok('OK reviewer added\n')); + await tool.execute({ team: 'acme', operation: 'add', name: 'charlie' }); + expect(runner.execute).toHaveBeenCalledWith([ + '--team', 'acme', 'kanban', 'reviewers', 'add', 'charlie', + ]); + }); + + it('returns OK text for add/remove', async () => { + const { tool } = setup(ok('OK reviewer added\n')); + const result = await tool.execute({ team: 'acme', operation: 'add', name: 'charlie' }); + expect(result).toBe('reviewer added'); + }); + + it('throws UserError when add called without name', async () => { + const { tool } = setup(); + await expect( + tool.execute({ team: 'acme', operation: 'add' }), + ).rejects.toThrow('name is required'); + }); + + it('throws UserError when remove called without name', async () => { + const { tool } = setup(); + await expect( + tool.execute({ team: 'acme', operation: 'remove' }), + ).rejects.toThrow('name is required'); + }); + + it('throws on CLI failure', async () => { + const { tool } = setup(fail('reviewer not found')); + await expect( + tool.execute({ team: 'acme', operation: 'remove', name: 'nobody' }), + ).rejects.toThrow('Failed to manage reviewers'); + }); +}); diff --git a/mcp-server/test/tools/message-send.test.ts b/mcp-server/test/tools/message-send.test.ts new file mode 100644 index 00000000..8bb206e8 --- /dev/null +++ b/mcp-server/test/tools/message-send.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect } from 'vitest'; +import { register } from '../../src/tools/message-send.js'; +import { createMockRunner, createMockServer, ok, fail } from './test-helpers.js'; + +describe('message_send', () => { + const deliveryJson = '{"deliveredToInbox":true,"messageId":"msg_abc"}'; + + function setup(response = ok(deliveryJson)) { + const runner = createMockRunner(response); + const { server, tools } = createMockServer(); + register(server, runner); + return { runner, tool: tools.get('message_send')! }; + } + + it('builds minimal CLI args', async () => { + const { runner, tool } = setup(); + await tool.execute({ team: 'acme', to: 'alice', text: 'Hello!' }); + expect(runner.execute).toHaveBeenCalledWith([ + '--team', 'acme', 'message', 'send', '--to', 'alice', '--text', 'Hello!', + ]); + }); + + it('includes summary and from flags', async () => { + const { runner, tool } = setup(); + await tool.execute({ + team: 'acme', to: 'alice', text: 'Task done', + summary: 'Completed task #1', from: 'bob', + }); + const args = runner.execute.mock.calls[0]![0] as string[]; + expect(args).toContain('--summary'); + expect(args[args.indexOf('--summary') + 1]).toBe('Completed task #1'); + expect(args).toContain('--from'); + expect(args[args.indexOf('--from') + 1]).toBe('bob'); + }); + + it('returns parsed JSON', async () => { + const { tool } = setup(); + const result = await tool.execute({ team: 'acme', to: 'alice', text: 'Hello!' }); + expect(result).toEqual({ deliveredToInbox: true, messageId: 'msg_abc' }); + }); + + it('throws on CLI failure', async () => { + const { tool } = setup(fail('recipient inbox not found')); + await expect( + tool.execute({ team: 'acme', to: 'nobody', text: 'Hi' }), + ).rejects.toThrow('Failed to send message'); + }); +}); diff --git a/mcp-server/test/tools/register-all.test.ts b/mcp-server/test/tools/register-all.test.ts new file mode 100644 index 00000000..d562525e --- /dev/null +++ b/mcp-server/test/tools/register-all.test.ts @@ -0,0 +1,28 @@ +import { describe, it, expect } from 'vitest'; +import { registerAllTools } from '../../src/tools/index.js'; +import { createMockRunner, createMockServer } from './test-helpers.js'; + +describe('registerAllTools', () => { + it('registers exactly 13 tools', () => { + const runner = createMockRunner({ stdout: '', stderr: '', exitCode: 0 }); + const { server, tools } = createMockServer(); + registerAllTools(server, runner); + expect(tools.size).toBe(13); + }); + + it('registers all expected tool names', () => { + const runner = createMockRunner({ stdout: '', stderr: '', exitCode: 0 }); + const { server, tools } = createMockServer(); + registerAllTools(server, runner); + + const expected = [ + 'task_create', 'task_set_status', 'task_set_owner', + 'task_get', 'task_list', 'task_comment', 'task_link', + 'task_briefing', 'task_attach', 'kanban_move', + 'kanban_reviewers', 'review_action', 'message_send', + ]; + for (const name of expected) { + expect(tools.has(name), `missing tool: ${name}`).toBe(true); + } + }); +}); diff --git a/mcp-server/test/tools/review-action.test.ts b/mcp-server/test/tools/review-action.test.ts new file mode 100644 index 00000000..952f1ae9 --- /dev/null +++ b/mcp-server/test/tools/review-action.test.ts @@ -0,0 +1,66 @@ +import { describe, it, expect } from 'vitest'; +import { register } from '../../src/tools/review-action.js'; +import { createMockRunner, createMockServer, ok, fail } from './test-helpers.js'; + +describe('review_action', () => { + function setup(response = ok('OK review #1 approved\n')) { + const runner = createMockRunner(response); + const { server, tools } = createMockServer(); + register(server, runner); + return { runner, tool: tools.get('review_action')! }; + } + + it('builds approve CLI args (no comment)', async () => { + const { runner, tool } = setup(); + await tool.execute({ team: 'acme', task_id: '1', decision: 'approve' }); + expect(runner.execute).toHaveBeenCalledWith([ + '--team', 'acme', 'review', 'approve', '1', + ]); + }); + + it('builds approve CLI args with --note (not --comment)', async () => { + const { runner, tool } = setup(); + await tool.execute({ team: 'acme', task_id: '1', decision: 'approve', comment: 'LGTM' }); + const args = runner.execute.mock.calls[0]![0] as string[]; + // approve uses --note, NOT --comment + expect(args).toContain('--note'); + expect(args[args.indexOf('--note') + 1]).toBe('LGTM'); + expect(args).not.toContain('--comment'); + }); + + it('builds request-changes CLI args with --comment (not --note)', async () => { + const { runner, tool } = setup(ok('OK review #1 requested changes\n')); + await tool.execute({ + team: 'acme', task_id: '1', decision: 'request-changes', comment: 'Fix tests', + }); + const args = runner.execute.mock.calls[0]![0] as string[]; + expect(args).toContain('--comment'); + expect(args[args.indexOf('--comment') + 1]).toBe('Fix tests'); + expect(args).not.toContain('--note'); + }); + + it('throws when request-changes has no comment', async () => { + const { tool } = setup(); + await expect( + tool.execute({ team: 'acme', task_id: '1', decision: 'request-changes' }), + ).rejects.toThrow('comment is required when requesting changes'); + }); + + it('includes from and notify_owner flags', async () => { + const { runner, tool } = setup(); + await tool.execute({ + team: 'acme', task_id: '1', decision: 'approve', + from: 'alice', notify_owner: true, + }); + const args = runner.execute.mock.calls[0]![0] as string[]; + expect(args).toContain('--from'); + expect(args).toContain('--notify-owner'); + }); + + it('throws on CLI failure', async () => { + const { tool } = setup(fail('task not in review')); + await expect( + tool.execute({ team: 'acme', task_id: '1', decision: 'approve' }), + ).rejects.toThrow('Failed to approve'); + }); +}); diff --git a/mcp-server/test/tools/task-attach.test.ts b/mcp-server/test/tools/task-attach.test.ts new file mode 100644 index 00000000..e8b63fe5 --- /dev/null +++ b/mcp-server/test/tools/task-attach.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect } from 'vitest'; +import { register } from '../../src/tools/task-attach.js'; +import { createMockRunner, createMockServer, ok, fail } from './test-helpers.js'; + +describe('task_attach', () => { + const attachJson = '{"id":"att_123","filename":"report.pdf","mimeType":"application/pdf"}'; + + function setup(response = ok(attachJson)) { + const runner = createMockRunner(response); + const { server, tools } = createMockServer(); + register(server, runner); + return { runner, tool: tools.get('task_attach')! }; + } + + it('builds minimal CLI args', async () => { + const { runner, tool } = setup(); + await tool.execute({ team: 'acme', task_id: '1', file: '/tmp/report.pdf' }); + expect(runner.execute).toHaveBeenCalledWith([ + '--team', 'acme', 'task', 'attach', '1', '--file', '/tmp/report.pdf', + ]); + }); + + it('includes all optional flags', async () => { + const { runner, tool } = setup(); + await tool.execute({ + team: 'acme', task_id: '1', file: '/tmp/file.pdf', + filename: 'renamed.pdf', mime_type: 'application/pdf', + mode: 'link', from: 'alice', + }); + const args = runner.execute.mock.calls[0]![0] as string[]; + expect(args).toContain('--filename'); + expect(args).toContain('--mime-type'); + expect(args).toContain('--mode'); + expect(args).toContain('--from'); + }); + + it('returns parsed JSON', async () => { + const { tool } = setup(); + const result = await tool.execute({ team: 'acme', task_id: '1', file: '/tmp/report.pdf' }); + expect(result).toEqual({ id: 'att_123', filename: 'report.pdf', mimeType: 'application/pdf' }); + }); + + it('throws on CLI failure', async () => { + const { tool } = setup(fail('file too large')); + await expect( + tool.execute({ team: 'acme', task_id: '1', file: '/tmp/huge.bin' }), + ).rejects.toThrow('Failed to attach file'); + }); +}); diff --git a/mcp-server/test/tools/task-briefing.test.ts b/mcp-server/test/tools/task-briefing.test.ts new file mode 100644 index 00000000..b915e994 --- /dev/null +++ b/mcp-server/test/tools/task-briefing.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect } from 'vitest'; +import { register } from '../../src/tools/task-briefing.js'; +import { createMockRunner, createMockServer, ok, fail } from './test-helpers.js'; + +describe('task_briefing', () => { + const briefingText = '=== Task Briefing for alice ===\nTask #1: Fix bug [in_progress]\n'; + + function setup(response = ok(briefingText)) { + const runner = createMockRunner(response); + const { server, tools } = createMockServer(); + register(server, runner); + return { runner, tool: tools.get('task_briefing')! }; + } + + it('builds correct CLI args', async () => { + const { runner, tool } = setup(); + await tool.execute({ team: 'acme', member: 'alice' }); + expect(runner.execute).toHaveBeenCalledWith([ + '--team', 'acme', 'task', 'briefing', '--for', 'alice', + ]); + }); + + it('returns trimmed plain text', async () => { + const { tool } = setup(); + const result = await tool.execute({ team: 'acme', member: 'alice' }); + expect(result).toBe('=== Task Briefing for alice ===\nTask #1: Fix bug [in_progress]'); + }); + + it('throws on CLI failure', async () => { + const { tool } = setup(fail('member not found')); + await expect(tool.execute({ team: 'acme', member: 'nobody' })).rejects.toThrow('Failed to get briefing'); + }); +}); diff --git a/mcp-server/test/tools/task-comment.test.ts b/mcp-server/test/tools/task-comment.test.ts new file mode 100644 index 00000000..2c340e48 --- /dev/null +++ b/mcp-server/test/tools/task-comment.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect } from 'vitest'; +import { register } from '../../src/tools/task-comment.js'; +import { createMockRunner, createMockServer, ok, fail } from './test-helpers.js'; + +describe('task_comment', () => { + function setup(response = ok('OK comment added to task #1\n')) { + const runner = createMockRunner(response); + const { server, tools } = createMockServer(); + register(server, runner); + return { runner, tool: tools.get('task_comment')! }; + } + + it('builds correct CLI args', async () => { + const { runner, tool } = setup(); + await tool.execute({ team: 'acme', task_id: '1', text: 'Looking good!' }); + expect(runner.execute).toHaveBeenCalledWith([ + '--team', 'acme', 'task', 'comment', '1', '--text', 'Looking good!', + ]); + }); + + it('includes from flag', async () => { + const { runner, tool } = setup(); + await tool.execute({ team: 'acme', task_id: '1', text: 'Done', from: 'alice' }); + const args = runner.execute.mock.calls[0]![0] as string[]; + expect(args).toContain('--from'); + expect(args[args.indexOf('--from') + 1]).toBe('alice'); + }); + + it('throws on CLI failure', async () => { + const { tool } = setup(fail('task not found')); + await expect(tool.execute({ team: 'acme', task_id: '99', text: 'Hi' })).rejects.toThrow('Failed to add comment'); + }); +}); diff --git a/mcp-server/test/tools/task-create.test.ts b/mcp-server/test/tools/task-create.test.ts new file mode 100644 index 00000000..5866e9f9 --- /dev/null +++ b/mcp-server/test/tools/task-create.test.ts @@ -0,0 +1,65 @@ +import { describe, it, expect } from 'vitest'; +import { register } from '../../src/tools/task-create.js'; +import { createMockRunner, createMockServer, ok, fail } from './test-helpers.js'; + +describe('task_create', () => { + function setup(response = ok('{"id":"1","subject":"Fix bug","status":"in_progress"}')) { + const runner = createMockRunner(response); + const { server, tools } = createMockServer(); + register(server, runner); + return { runner, tool: tools.get('task_create')! }; + } + + it('builds minimal CLI args', async () => { + const { runner, tool } = setup(); + await tool.execute({ team: 'acme', subject: 'Fix bug' }); + expect(runner.execute).toHaveBeenCalledWith([ + '--team', 'acme', 'task', 'create', '--subject', 'Fix bug', + ]); + }); + + it('includes all optional flags', async () => { + const { runner, tool } = setup(); + await tool.execute({ + team: 'acme', + subject: 'Big task', + description: 'Details here', + owner: 'alice', + blocked_by: ['1', '2'], + related: ['3'], + status: 'pending', + active_form: 'Fixing bug', + notify: true, + from: 'bob', + }); + const args = runner.execute.mock.calls[0]![0] as string[]; + expect(args).toContain('--description'); + expect(args).toContain('--owner'); + expect(args).toContain('--blocked-by'); + expect(args[args.indexOf('--blocked-by') + 1]).toBe('1,2'); + expect(args).toContain('--related'); + expect(args[args.indexOf('--related') + 1]).toBe('3'); + expect(args).toContain('--status'); + expect(args).toContain('--activeForm'); + expect(args).toContain('--notify'); + expect(args).toContain('--from'); + }); + + it('skips empty blocked_by array', async () => { + const { runner, tool } = setup(); + await tool.execute({ team: 'acme', subject: 'Task', blocked_by: [] }); + const args = runner.execute.mock.calls[0]![0] as string[]; + expect(args).not.toContain('--blocked-by'); + }); + + it('returns parsed JSON', async () => { + const { tool } = setup(); + const result = await tool.execute({ team: 'acme', subject: 'Fix bug' }); + expect(result).toEqual({ id: '1', subject: 'Fix bug', status: 'in_progress' }); + }); + + it('throws on CLI failure', async () => { + const { tool } = setup(fail('team not found')); + await expect(tool.execute({ team: 'bad', subject: 'X' })).rejects.toThrow('Failed to create task'); + }); +}); diff --git a/mcp-server/test/tools/task-get.test.ts b/mcp-server/test/tools/task-get.test.ts new file mode 100644 index 00000000..79a3d97f --- /dev/null +++ b/mcp-server/test/tools/task-get.test.ts @@ -0,0 +1,29 @@ +import { describe, it, expect } from 'vitest'; +import { register } from '../../src/tools/task-get.js'; +import { createMockRunner, createMockServer, ok, fail } from './test-helpers.js'; + +describe('task_get', () => { + function setup(response = ok('{"id":"42","subject":"Test","status":"pending"}')) { + const runner = createMockRunner(response); + const { server, tools } = createMockServer(); + register(server, runner); + return { runner, tool: tools.get('task_get')! }; + } + + it('builds correct CLI args', async () => { + const { runner, tool } = setup(); + await tool.execute({ team: 'acme', task_id: '42' }); + expect(runner.execute).toHaveBeenCalledWith(['--team', 'acme', 'task', 'get', '42']); + }); + + it('returns parsed JSON', async () => { + const { tool } = setup(); + const result = await tool.execute({ team: 'acme', task_id: '42' }); + expect(result).toEqual({ id: '42', subject: 'Test', status: 'pending' }); + }); + + it('throws on CLI failure', async () => { + const { tool } = setup(fail('Task not found: #99')); + await expect(tool.execute({ team: 'acme', task_id: '99' })).rejects.toThrow('Failed to get task'); + }); +}); diff --git a/mcp-server/test/tools/task-link.test.ts b/mcp-server/test/tools/task-link.test.ts new file mode 100644 index 00000000..7e16b802 --- /dev/null +++ b/mcp-server/test/tools/task-link.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect } from 'vitest'; +import { register } from '../../src/tools/task-link.js'; +import { createMockRunner, createMockServer, ok, fail } from './test-helpers.js'; + +describe('task_link', () => { + function setup(response = ok('OK task #1 blocked-by #2\n')) { + const runner = createMockRunner(response); + const { server, tools } = createMockServer(); + register(server, runner); + return { runner, tool: tools.get('task_link')! }; + } + + it('builds link CLI args', async () => { + const { runner, tool } = setup(); + await tool.execute({ + team: 'acme', task_id: '1', operation: 'link', + relationship: 'blocked-by', target_id: '2', + }); + expect(runner.execute).toHaveBeenCalledWith([ + '--team', 'acme', 'task', 'link', '1', '--blocked-by', '2', + ]); + }); + + it('builds unlink CLI args', async () => { + const { runner, tool } = setup(ok('OK task #1 unlinked from #2\n')); + await tool.execute({ + team: 'acme', task_id: '1', operation: 'unlink', + relationship: 'related', target_id: '3', + }); + expect(runner.execute).toHaveBeenCalledWith([ + '--team', 'acme', 'task', 'unlink', '1', '--related', '3', + ]); + }); + + it('throws on CLI failure', async () => { + const { tool } = setup(fail('circular dependency')); + await expect( + tool.execute({ + team: 'acme', task_id: '1', operation: 'link', + relationship: 'blocked-by', target_id: '1', + }), + ).rejects.toThrow('Failed to link tasks'); + }); +}); diff --git a/mcp-server/test/tools/task-list.test.ts b/mcp-server/test/tools/task-list.test.ts new file mode 100644 index 00000000..be1a25af --- /dev/null +++ b/mcp-server/test/tools/task-list.test.ts @@ -0,0 +1,29 @@ +import { describe, it, expect } from 'vitest'; +import { register } from '../../src/tools/task-list.js'; +import { createMockRunner, createMockServer, ok, fail } from './test-helpers.js'; + +describe('task_list', () => { + function setup(response = ok('[{"id":"1"},{"id":"2"}]')) { + const runner = createMockRunner(response); + const { server, tools } = createMockServer(); + register(server, runner); + return { runner, tool: tools.get('task_list')! }; + } + + it('builds correct CLI args', async () => { + const { runner, tool } = setup(); + await tool.execute({ team: 'acme' }); + expect(runner.execute).toHaveBeenCalledWith(['--team', 'acme', 'task', 'list']); + }); + + it('returns parsed JSON array', async () => { + const { tool } = setup(); + const result = await tool.execute({ team: 'acme' }); + expect(result).toEqual([{ id: '1' }, { id: '2' }]); + }); + + it('throws on CLI failure', async () => { + const { tool } = setup(fail('team dir not found')); + await expect(tool.execute({ team: 'bad' })).rejects.toThrow('Failed to list tasks'); + }); +}); diff --git a/mcp-server/test/tools/task-set-owner.test.ts b/mcp-server/test/tools/task-set-owner.test.ts new file mode 100644 index 00000000..d9859066 --- /dev/null +++ b/mcp-server/test/tools/task-set-owner.test.ts @@ -0,0 +1,42 @@ +import { describe, it, expect } from 'vitest'; +import { register } from '../../src/tools/task-set-owner.js'; +import { createMockRunner, createMockServer, ok, fail } from './test-helpers.js'; + +describe('task_set_owner', () => { + function setup(response = ok('OK task #1 owner=alice\n')) { + const runner = createMockRunner(response); + const { server, tools } = createMockServer(); + register(server, runner); + return { runner, tool: tools.get('task_set_owner')! }; + } + + it('builds args for assignment', async () => { + const { runner, tool } = setup(); + await tool.execute({ team: 'acme', task_id: '1', owner: 'alice' }); + expect(runner.execute).toHaveBeenCalledWith([ + '--team', 'acme', 'task', 'set-owner', '1', 'alice', + ]); + }); + + it('builds args for clear', async () => { + const { runner, tool } = setup(ok('OK task #1 owner=cleared\n')); + await tool.execute({ team: 'acme', task_id: '1', owner: 'clear' }); + expect(runner.execute).toHaveBeenCalledWith([ + '--team', 'acme', 'task', 'set-owner', '1', 'clear', + ]); + }); + + it('includes notify and from flags', async () => { + const { runner, tool } = setup(); + await tool.execute({ team: 'acme', task_id: '1', owner: 'alice', notify: true, from: 'bob' }); + const args = runner.execute.mock.calls[0]![0] as string[]; + expect(args).toContain('--notify'); + expect(args).toContain('--from'); + expect(args[args.indexOf('--from') + 1]).toBe('bob'); + }); + + it('throws on CLI failure', async () => { + const { tool } = setup(fail('member not found')); + await expect(tool.execute({ team: 'acme', task_id: '1', owner: 'nobody' })).rejects.toThrow('Failed to set owner'); + }); +}); diff --git a/mcp-server/test/tools/task-set-status.test.ts b/mcp-server/test/tools/task-set-status.test.ts new file mode 100644 index 00000000..0ca68d0d --- /dev/null +++ b/mcp-server/test/tools/task-set-status.test.ts @@ -0,0 +1,31 @@ +import { describe, it, expect } from 'vitest'; +import { register } from '../../src/tools/task-set-status.js'; +import { createMockRunner, createMockServer, ok, fail } from './test-helpers.js'; + +describe('task_set_status', () => { + function setup(response = ok('OK task #1 status=completed\n')) { + const runner = createMockRunner(response); + const { server, tools } = createMockServer(); + register(server, runner); + return { runner, tool: tools.get('task_set_status')! }; + } + + it('builds correct CLI args', async () => { + const { runner, tool } = setup(); + await tool.execute({ team: 'acme', task_id: '1', status: 'completed' }); + expect(runner.execute).toHaveBeenCalledWith([ + '--team', 'acme', 'task', 'set-status', '1', 'completed', + ]); + }); + + it('returns parsed OK text', async () => { + const { tool } = setup(); + const result = await tool.execute({ team: 'acme', task_id: '1', status: 'completed' }); + expect(result).toBe('task #1 status=completed'); + }); + + it('throws on CLI failure', async () => { + const { tool } = setup(fail('invalid transition')); + await expect(tool.execute({ team: 'acme', task_id: '1', status: 'deleted' })).rejects.toThrow('Failed to set status'); + }); +}); diff --git a/mcp-server/test/tools/test-helpers.ts b/mcp-server/test/tools/test-helpers.ts new file mode 100644 index 00000000..fb0a8e01 --- /dev/null +++ b/mcp-server/test/tools/test-helpers.ts @@ -0,0 +1,58 @@ +import { vi } from 'vitest'; +import type { FastMCP } from 'fastmcp'; +import type { ITeamctlRunner, TeamctlResult } from '../../src/teamctl-runner.js'; + +/** + * Creates a mock ITeamctlRunner that records calls and returns predetermined results. + */ +export function createMockRunner( + response: TeamctlResult | ((args: string[]) => TeamctlResult), +): ITeamctlRunner & { execute: ReturnType } { + return { + execute: vi.fn(async (args: string[]): Promise => { + return typeof response === 'function' ? response(args) : response; + }), + }; +} + +/** Success response helpers */ +export const ok = (stdout: string): TeamctlResult => ({ + stdout, + stderr: '', + exitCode: 0, +}); + +export const fail = (stderr: string): TeamctlResult => ({ + stdout: '', + stderr, + exitCode: 1, +}); + +/** + * Captures registered tools via a mock FastMCP-like server. + * Returns a map of tool name → { execute function, parameters schema }. + */ +export interface CapturedTool { + name: string; + execute: (args: Record) => Promise; + parameters: unknown; +} + +export function createMockServer(): { + server: FastMCP; + tools: Map; +} { + const tools = new Map(); + + const server = { + addTool: (def: { name: string; execute: CapturedTool['execute']; parameters: unknown }) => { + tools.set(def.name, { + name: def.name, + execute: def.execute, + parameters: def.parameters, + }); + }, + } as unknown as FastMCP; + + return { server, tools }; +} diff --git a/mcp-server/tsconfig.json b/mcp-server/tsconfig.json new file mode 100644 index 00000000..2bd3eb89 --- /dev/null +++ b/mcp-server/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "isolatedModules": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist", "test"] +} diff --git a/mcp-server/tsup.config.ts b/mcp-server/tsup.config.ts new file mode 100644 index 00000000..5cd234bb --- /dev/null +++ b/mcp-server/tsup.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm'], + target: 'node20', + outDir: 'dist', + clean: true, + sourcemap: true, + dts: false, + banner: { + js: '#!/usr/bin/env node', + }, +}); diff --git a/mcp-server/vitest.config.ts b/mcp-server/vitest.config.ts new file mode 100644 index 00000000..6497809d --- /dev/null +++ b/mcp-server/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['test/**/*.test.ts'], + testTimeout: 15_000, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 147ae6f3..80ad3475 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -370,6 +370,31 @@ importers: specifier: ^3.1.4 version: 3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@20.0.2)(terser@5.46.0) + mcp-server: + dependencies: + fastmcp: + specifier: ^3.34.0 + version: 3.34.0 + zod: + specifier: ^4.3.6 + version: 4.3.6 + devDependencies: + '@types/node': + specifier: ^22.15.18 + version: 22.19.15 + tsup: + specifier: ^8.5.1 + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ^5.8.2 + version: 5.9.3 + vitest: + specifier: ^3.1.4 + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.15)(happy-dom@20.0.2)(terser@5.46.0) + packages: 7zip-bin@5.2.0: @@ -479,6 +504,9 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@borewit/text-codec@0.2.1': + resolution: {integrity: sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==} + '@boundaries/elements@1.1.2': resolution: {integrity: sha512-DnGHL+v36YVMoWhWZqyJYVZ9dapNm7h4N3/P0lDPirJj0CHVPkjChMCCotj74cg6LW7iPJZFGrdEfh0X0g2bmQ==} engines: {node: '>=18.18'} @@ -1063,6 +1091,12 @@ packages: '@gar/promisify@1.1.3': resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} + '@hono/node-server@1.19.11': + resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -1203,6 +1237,16 @@ packages: '@mermaid-js/parser@1.0.0': resolution: {integrity: sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw==} + '@modelcontextprotocol/sdk@1.27.1': + resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -1893,10 +1937,20 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@sindresorhus/is@4.6.0': resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@szmarczak/http-timer@4.0.6': resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} engines: {node: '>=10'} @@ -1915,6 +1969,13 @@ packages: '@tanstack/virtual-core@3.13.18': resolution: {integrity: sha512-Mx86Hqu1k39icq2Zusq+Ey2J6dDWTjDvEv43PJtRCoEYTLyfaPnxIQ6iy7YAOK0NV/qOEmZQ/uCufrppZxTgcg==} + '@tokenizer/inflate@0.4.1': + resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} + engines: {node: '>=18'} + + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + '@tootallnate/once@2.0.0': resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} engines: {node: '>= 10'} @@ -2078,6 +2139,9 @@ packages: '@types/node@20.19.33': resolution: {integrity: sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==} + '@types/node@22.19.15': + resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==} + '@types/node@24.10.12': resolution: {integrity: sha512-68e+T28EbdmLSTkPgs3+UacC6rzmqrcWFPQs1C8mwJhI/r5Uxr0yEuQotczNRROd1gq30NGxee+fo0rSIxpyAw==} @@ -2346,6 +2410,14 @@ packages: abstract-logging@2.0.1: resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -2570,6 +2642,9 @@ packages: resolution: {integrity: sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==} engines: {node: '>=4'} + axios@1.13.6: + resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -2607,6 +2682,10 @@ packages: bluebird@3.7.2: resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + boolean@3.2.0: resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. @@ -2669,6 +2748,12 @@ packages: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} engines: {node: '>=6'} + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -2805,6 +2890,10 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} + clone-response@1.0.3: resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} @@ -2886,6 +2975,10 @@ packages: config-file-ts@0.2.8-rc1: resolution: {integrity: sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg==} + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + console-control-strings@1.1.0: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} @@ -2893,19 +2986,39 @@ packages: resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} engines: {node: '>=18'} + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} + cookies@0.9.1: + resolution: {integrity: sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==} + engines: {node: '>= 0.8'} + core-util-is@1.0.2: resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + cose-base@1.0.3: resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} @@ -3148,6 +3261,9 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} + deep-equal@1.0.1: + resolution: {integrity: sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -3176,6 +3292,10 @@ packages: delegates@1.0.0: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + depd@1.1.2: + resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} + engines: {node: '>= 0.6'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -3184,6 +3304,10 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -3251,6 +3375,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + ejs@3.1.10: resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} engines: {node: '>=0.10.0'} @@ -3301,6 +3428,10 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + encoding@0.1.13: resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} @@ -3544,9 +3675,25 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + + execa@9.6.1: + resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} + engines: {node: ^18.19.0 || >=20.5.0} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} @@ -3554,6 +3701,16 @@ packages: exponential-backoff@3.1.3: resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} + express-rate-limit@8.3.0: + resolution: {integrity: sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -3597,6 +3754,15 @@ packages: fastify@5.7.4: resolution: {integrity: sha512-e6l5NsRdaEP8rdD8VR0ErJASeyaRbzXYpmkrpr2SuvuMq6Si3lvsaVy5C+7gLanEkvjpMDzBXWE5HPeb/hgTxA==} + fastmcp@3.34.0: + resolution: {integrity: sha512-xKOXjU+MK7OZy91BY3FS5aenSiclJBCRMaZtXb3HYaKZVFbq4qYvAlFu6xYI3UU1NGLtv+h8izoStnOQ1By0BA==} + hasBin: true + peerDependencies: + jose: ^5.0.0 + peerDependenciesMeta: + jose: + optional: true + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -3615,10 +3781,18 @@ packages: picomatch: optional: true + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + file-type@21.3.0: + resolution: {integrity: sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA==} + engines: {node: '>=20'} + filelist@1.0.4: resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} @@ -3626,6 +3800,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-my-way@9.4.0: resolution: {integrity: sha512-5Ye4vHsypZRYtS01ob/iwHzGRUDELlsoCftI/OZFhcLs1M0tkGPcXldE80TAZC5yYuJMBPJQQ43UHlqbJWiX2w==} engines: {node: '>=20'} @@ -3634,6 +3812,9 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -3641,6 +3822,15 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -3658,9 +3848,21 @@ packages: engines: {node: '>=18.3.0'} hasBin: true + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -3709,6 +3911,10 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + fuse.js@7.1.0: + resolution: {integrity: sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==} + engines: {node: '>=10'} + gauge@4.0.4: resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -3746,6 +3952,10 @@ packages: resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} engines: {node: '>=8'} + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + get-symbol-description@1.1.0: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} @@ -3885,6 +4095,10 @@ packages: resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} engines: {node: '>=12.0.0'} + hono@4.12.5: + resolution: {integrity: sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==} + engines: {node: '>=16.9.0'} + hosted-git-info@4.1.0: resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} engines: {node: '>=10'} @@ -3898,9 +4112,17 @@ packages: html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + http-assert@1.5.0: + resolution: {integrity: sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==} + engines: {node: '>= 0.8'} + http-cache-semantics@4.2.0: resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + http-errors@1.8.1: + resolution: {integrity: sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==} + engines: {node: '>= 0.6'} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -3925,6 +4147,13 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + human-readable-ids@1.0.4: + resolution: {integrity: sha512-h1zwThTims8A/SpqFGWyTx+jG1+WRMJaEeZgbtPGrIpj2AZjsOgy8Y+iNzJ0yAyN669Q6F02EK66WMWcst+2FA==} + + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + humanize-ms@1.2.1: resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} @@ -3942,6 +4171,10 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + idb-keyval@6.2.2: resolution: {integrity: sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==} @@ -3996,6 +4229,10 @@ packages: resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} engines: {node: '>= 12'} + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + ipaddr.js@2.3.0: resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} engines: {node: '>= 10'} @@ -4106,6 +4343,9 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -4118,6 +4358,10 @@ packages: resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} engines: {node: '>= 0.4'} + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + is-string@1.1.1: resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} engines: {node: '>= 0.4'} @@ -4134,6 +4378,10 @@ packages: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} @@ -4207,6 +4455,13 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + jose@6.2.0: + resolution: {integrity: sha512-xsfE1TcSCbUdo6U07tR0mvhg0flGxU8tPLbF03mirl2ukGQENhUg4ubGYQnhVH0b5stLlPM+WOqDkEl1R1y5sQ==} + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -4234,6 +4489,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -4267,6 +4525,10 @@ packages: resolution: {integrity: sha512-q3N5u+1sY9Bu7T4nlXoiRBXWfwSefNGoKeOwekV+gw0cAXQlz2Ww6BLcmBxVDeXBMUDQv6fK5bcNaJLxob3ZQA==} hasBin: true + keygrip@1.1.0: + resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==} + engines: {node: '>= 0.6'} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -4281,6 +4543,21 @@ packages: '@types/node': '>=18' typescript: '>=5.0.4 <7' + knuth-shuffle@1.0.8: + resolution: {integrity: sha512-IdC4Hpp+mx53zTt6VAGsAtbGM0g4BV9fP8tTcviCosSwocHcRDw9uG5Rnv6wLWckF4r72qeXFoK9NkvV1gUJCQ==} + + koa-compose@4.1.0: + resolution: {integrity: sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==} + + koa-router@14.0.0: + resolution: {integrity: sha512-Ue8f/PRsLLNm6b7y+eS6xkqvsG2xH11d2VB1HPcfdfW6p5736kCHf2pXaq8q9XPQ01x0Dk7V/P5Il9pe+tGTxA==} + engines: {node: '>= 20'} + deprecated: 'Please use @koa/router instead, starting from v9! ' + + koa@3.1.2: + resolution: {integrity: sha512-2LOQnFKu3m0VxpE+5sb5+BRTSKrXmNxGgxVRiKwD9s5KQB1zID/FRXhtzeV7RT1L2GVpdEEAfVuclFOMGl1ikA==} + engines: {node: '>= 18'} + langium@4.2.1: resolution: {integrity: sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ==} engines: {node: '>=20.10.0', npm: '>=10.2.3'} @@ -4328,6 +4605,10 @@ packages: resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==} engines: {node: '>=20.0.0'} + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -4445,6 +4726,10 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mcp-proxy@6.4.1: + resolution: {integrity: sha512-vzKLpJEZRqkbxz9sBqYSOWo9mJq0aGYQQLJoaMe+egYF/EA1PvcKvLx9pA1/bRpV/hfguEuB67gcs0C5KJiROQ==} + hasBin: true + mdast-util-find-and-replace@3.0.2: resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} @@ -4490,6 +4775,14 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -4589,10 +4882,18 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + mime@2.6.0: resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} engines: {node: '>=4.0.0'} @@ -4723,6 +5024,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + negotiator@0.6.4: resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} engines: {node: '>= 0.6'} @@ -4789,6 +5094,10 @@ packages: resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} engines: {node: '>=10'} + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + npmlog@6.0.2: resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -4834,6 +5143,10 @@ packages: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -4893,9 +5206,17 @@ packages: parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + path-data-parser@0.1.0: resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} @@ -4911,6 +5232,10 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -4922,6 +5247,9 @@ packages: resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} engines: {node: 20 || >=22} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -4966,10 +5294,19 @@ packages: resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} hasBin: true + pipenet@1.4.0: + resolution: {integrity: sha512-Uc3EH2i8hnJUD0Eupj9z2jaZPjjAbooaiHGh0iFdExbE8/BDt6Lf0919Dtwx5VM83elHNWFzCOsvzsViTD5YZg==} + engines: {node: '>=22.0.0'} + hasBin: true + pirates@4.0.7: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -5102,6 +5439,10 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + engines: {node: '>=18'} + proc-log@5.0.0: resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==} engines: {node: ^18.17.0 || >=20.5.0} @@ -5137,6 +5478,13 @@ packages: property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + pump@3.0.3: resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} @@ -5144,6 +5492,10 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qs@6.15.0: + resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} + engines: {node: '>=0.6'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -5154,6 +5506,14 @@ packages: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + react-dom@18.3.1: resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} peerDependencies: @@ -5296,6 +5656,10 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -5354,6 +5718,10 @@ packages: roughjs@4.6.6: resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -5425,10 +5793,18 @@ packages: engines: {node: '>=10'} hasBin: true + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + serialize-error@7.0.1: resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} engines: {node: '>=10'} + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -5533,6 +5909,10 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} @@ -5569,6 +5949,10 @@ packages: resolution: {integrity: sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==} engines: {node: '>= 6'} + statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -5580,6 +5964,9 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} + strict-event-emitter-types@2.0.0: + resolution: {integrity: sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -5644,6 +6031,10 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -5655,6 +6046,10 @@ packages: strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + strtok3@10.3.4: + resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} + engines: {node: '>=18'} + style-mod@4.1.3: resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} @@ -5762,6 +6157,10 @@ packages: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} + tldjs@2.3.2: + resolution: {integrity: sha512-EORDwFMSZKrHPUVDhejCMDeAovRS5d8jZKiqALFiPp3cjKjEldPkxBY39ZSx3c45awz3RpKwJD1cCgGxEfy8/A==} + engines: {node: '>= 20'} + tmp-promise@3.0.3: resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} @@ -5781,6 +6180,14 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + token-types@6.1.2: + resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} + engines: {node: '>=14.16'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -5809,6 +6216,29 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsscmp@1.0.6: + resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} + engines: {node: '>=0.6.x'} + + tsup@8.5.1: + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + tsx@4.21.0: resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} engines: {node: '>=18.0.0'} @@ -5825,6 +6255,10 @@ packages: resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} engines: {node: '>=10'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -5861,6 +6295,10 @@ packages: engines: {node: '>=0.8.0'} hasBin: true + uint8array-extras@1.5.0: + resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} + engines: {node: '>=18'} + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -5874,6 +6312,14 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici@7.22.0: + resolution: {integrity: sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==} + engines: {node: '>=20.18.1'} + + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -5919,6 +6365,10 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} @@ -5931,6 +6381,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + uri-templates@0.2.0: + resolution: {integrity: sha512-EWkjYEN0L6KOfEoOH6Wj4ghQqU7eBZMJqRHQnxQAq+dSEzRPClkWjf8557HkWQXF6BrAUoLSAyy9i3RVTliaNg==} + use-callback-ref@1.3.3: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} @@ -5966,6 +6419,10 @@ packages: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + verror@1.10.1: resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==} engines: {node: '>=0.6.0'} @@ -6140,6 +6597,29 @@ packages: resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} engines: {node: '>=8.0'} + xsschema@0.4.3: + resolution: {integrity: sha512-8MFtONewz49S+R5ZfOGfJzEaPAmJ+Gw1ILjvuerYG0mOMVYe7OrsVQNlpiUTct4L3toAJAKD5NMwmCfmTIGYHQ==} + peerDependencies: + '@valibot/to-json-schema': ^1.0.0 + arktype: ^2.1.20 + effect: ^3.16.0 + sury: ^10.0.0 + zod: ^3.25.0 || ^4.0.0 + zod-to-json-schema: ^3.25.0 + peerDependenciesMeta: + '@valibot/to-json-schema': + optional: true + arktype: + optional: true + effect: + optional: true + sury: + optional: true + zod: + optional: true + zod-to-json-schema: + optional: true + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -6163,10 +6643,18 @@ packages: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yargs@18.0.0: + resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yauzl@2.10.0: resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} @@ -6188,10 +6676,19 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + zip-stream@4.1.1: resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} engines: {node: '>= 10'} + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + zod-validation-error@4.0.2: resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} engines: {node: '>=18.0.0'} @@ -6354,6 +6851,8 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} + '@borewit/text-codec@0.2.1': {} + '@boundaries/elements@1.1.2(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@1.21.7))': dependencies: eslint-import-resolver-node: 0.3.9 @@ -7081,6 +7580,10 @@ snapshots: '@gar/promisify@1.1.3': {} + '@hono/node-server@1.19.11(hono@4.12.5)': + dependencies: + hono: 4.12.5 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -7272,6 +7775,28 @@ snapshots: dependencies: langium: 4.2.1 + '@modelcontextprotocol/sdk@1.27.1(zod@4.3.6)': + dependencies: + '@hono/node-server': 1.19.11(hono@4.12.5) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.3.0(express@5.2.1) + hono: 4.12.5 + jose: 6.2.0 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.3.6 + zod-to-json-schema: 3.25.1(zod@4.3.6) + transitivePeerDependencies: + - supports-color + '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.8.1 @@ -7891,8 +8416,14 @@ snapshots: '@rtsao/scc@1.1.0': {} + '@sec-ant/readable-stream@0.4.1': {} + '@sindresorhus/is@4.6.0': {} + '@sindresorhus/merge-streams@4.0.0': {} + + '@standard-schema/spec@1.1.0': {} + '@szmarczak/http-timer@4.0.6': dependencies: defer-to-connect: 2.0.1 @@ -7910,6 +8441,15 @@ snapshots: '@tanstack/virtual-core@3.13.18': {} + '@tokenizer/inflate@0.4.1': + dependencies: + debug: 4.4.3 + token-types: 6.1.2 + transitivePeerDependencies: + - supports-color + + '@tokenizer/token@0.3.0': {} + '@tootallnate/once@2.0.0': {} '@tybys/wasm-util@0.10.1': @@ -8113,6 +8653,10 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/node@22.19.15': + dependencies: + undici-types: 6.21.0 + '@types/node@24.10.12': dependencies: undici-types: 7.16.0 @@ -8402,6 +8946,16 @@ snapshots: abstract-logging@2.0.1: {} + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -8720,6 +9274,14 @@ snapshots: axe-core@4.11.1: {} + axios@1.13.6(debug@4.4.3): + dependencies: + follow-redirects: 1.15.11(debug@4.4.3) + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axobject-query@4.1.0: {} bail@2.0.2: {} @@ -8750,6 +9312,20 @@ snapshots: bluebird@3.7.2: {} + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.0 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + boolean@3.2.0: optional: true @@ -8857,6 +9433,11 @@ snapshots: builtin-modules@3.3.0: {} + bundle-require@5.1.0(esbuild@0.27.2): + dependencies: + esbuild: 0.27.2 + load-tsconfig: 0.2.5 + bytes@3.1.2: {} cac@6.7.14: {} @@ -9030,6 +9611,12 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + cliui@9.0.1: + dependencies: + string-width: 7.2.0 + strip-ansi: 7.1.2 + wrap-ansi: 9.0.2 + clone-response@1.0.3: dependencies: mimic-response: 1.0.1 @@ -9102,19 +9689,37 @@ snapshots: glob: 10.5.0 typescript: 5.9.3 + consola@3.4.2: {} + console-control-strings@1.1.0: {} content-disposition@1.0.1: {} + content-type@1.0.5: {} + convert-source-map@2.0.0: {} + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + cookie@1.1.1: {} + cookies@0.9.1: + dependencies: + depd: 2.0.0 + keygrip: 1.1.0 + core-util-is@1.0.2: optional: true core-util-is@1.0.3: {} + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cose-base@1.0.3: dependencies: layout-base: 1.0.2 @@ -9379,6 +9984,8 @@ snapshots: deep-eql@5.0.2: {} + deep-equal@1.0.1: {} + deep-is@0.1.4: {} defaults@1.0.4: @@ -9407,10 +10014,14 @@ snapshots: delegates@1.0.0: {} + depd@1.1.2: {} + depd@2.0.0: {} dequal@2.0.3: {} + destroy@1.2.0: {} + detect-libc@2.1.2: {} detect-node-es@1.1.0: {} @@ -9491,6 +10102,8 @@ snapshots: eastasianwidth@0.2.0: {} + ee-first@1.1.1: {} + ejs@3.1.10: dependencies: jake: 10.9.4 @@ -9587,6 +10200,8 @@ snapshots: emoji-regex@9.2.2: {} + encodeurl@2.0.0: {} + encoding@0.1.13: dependencies: iconv-lite: 0.6.3 @@ -10019,12 +10634,73 @@ snapshots: esutils@2.0.3: {} + etag@1.8.1: {} + eventemitter3@5.0.4: {} + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + + execa@9.6.1: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.3.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.2 + expect-type@1.3.0: {} exponential-backoff@3.1.3: {} + express-rate-limit@8.3.0(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.1.0 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + extend@3.0.2: {} extract-zip@2.0.1: @@ -10091,6 +10767,30 @@ snapshots: semver: 7.7.3 toad-cache: 3.7.0 + fastmcp@3.34.0: + dependencies: + '@modelcontextprotocol/sdk': 1.27.1(zod@4.3.6) + '@standard-schema/spec': 1.1.0 + execa: 9.6.1 + file-type: 21.3.0 + fuse.js: 7.1.0 + hono: 4.12.5 + mcp-proxy: 6.4.1 + strict-event-emitter-types: 2.0.0 + undici: 7.22.0 + uri-templates: 0.2.0 + xsschema: 0.4.3(zod-to-json-schema@3.25.1(zod@4.3.6))(zod@4.3.6) + yargs: 18.0.0 + zod: 4.3.6 + zod-to-json-schema: 3.25.1(zod@4.3.6) + transitivePeerDependencies: + - '@cfworker/json-schema' + - '@valibot/to-json-schema' + - arktype + - effect + - supports-color + - sury + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -10107,10 +10807,23 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 + file-type@21.3.0: + dependencies: + '@tokenizer/inflate': 0.4.1 + strtok3: 10.3.4 + token-types: 6.1.2 + uint8array-extras: 1.5.0 + transitivePeerDependencies: + - supports-color + filelist@1.0.4: dependencies: minimatch: 5.1.7 @@ -10119,6 +10832,17 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + find-my-way@9.4.0: dependencies: fast-deep-equal: 3.1.3 @@ -10130,6 +10854,12 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.21 + mlly: 1.8.0 + rollup: 4.55.1 + flat-cache@4.0.1: dependencies: flatted: 3.3.3 @@ -10137,6 +10867,10 @@ snapshots: flatted@3.3.3: {} + follow-redirects@1.15.11(debug@4.4.3): + optionalDependencies: + debug: 4.4.3 + for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -10158,8 +10892,14 @@ snapshots: dependencies: fd-package-json: 2.0.0 + forwarded@0.2.0: {} + fraction.js@5.3.4: {} + fresh@0.5.2: {} + + fresh@2.0.0: {} + fs-constants@1.0.0: {} fs-extra@10.1.0: @@ -10215,6 +10955,8 @@ snapshots: functions-have-names@1.2.3: {} + fuse.js@7.1.0: {} + gauge@4.0.4: dependencies: aproba: 2.1.0 @@ -10258,6 +11000,11 @@ snapshots: dependencies: pump: 3.0.3 + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + get-symbol-description@1.1.0: dependencies: call-bound: 1.0.4 @@ -10478,6 +11225,8 @@ snapshots: highlight.js@11.11.1: {} + hono@4.12.5: {} + hosted-git-info@4.1.0: dependencies: lru-cache: 6.0.0 @@ -10488,8 +11237,21 @@ snapshots: html-void-elements@3.0.0: {} + http-assert@1.5.0: + dependencies: + deep-equal: 1.0.1 + http-errors: 1.8.1 + http-cache-semantics@4.2.0: {} + http-errors@1.8.1: + dependencies: + depd: 1.1.2 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 1.5.0 + toidentifier: 1.0.1 + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -10532,6 +11294,12 @@ snapshots: transitivePeerDependencies: - supports-color + human-readable-ids@1.0.4: + dependencies: + knuth-shuffle: 1.0.8 + + human-signals@8.0.1: {} + humanize-ms@1.2.1: dependencies: ms: 2.1.3 @@ -10548,6 +11316,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + idb-keyval@6.2.2: {} ieee754@1.2.1: {} @@ -10588,6 +11360,8 @@ snapshots: ip-address@10.1.0: {} + ipaddr.js@1.9.1: {} + ipaddr.js@2.3.0: {} is-alphabetical@2.0.1: {} @@ -10694,6 +11468,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-promise@4.0.0: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -10707,6 +11483,8 @@ snapshots: dependencies: call-bound: 1.0.4 + is-stream@4.0.1: {} + is-string@1.1.1: dependencies: call-bound: 1.0.4 @@ -10724,6 +11502,8 @@ snapshots: is-unicode-supported@0.1.0: {} + is-unicode-supported@2.1.0: {} + is-weakmap@2.0.2: {} is-weakref@1.1.1: @@ -10795,6 +11575,10 @@ snapshots: jiti@2.6.1: {} + jose@6.2.0: {} + + joycon@3.1.1: {} + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -10815,6 +11599,8 @@ snapshots: json-schema-traverse@1.0.0: {} + json-schema-typed@8.0.2: {} + json-stable-stringify-without-jsonify@1.0.1: {} json-stringify-safe@5.0.1: @@ -10849,6 +11635,10 @@ snapshots: dependencies: commander: 8.3.0 + keygrip@1.1.0: + dependencies: + tsscmp: 1.0.6 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -10872,6 +11662,40 @@ snapshots: typescript: 5.9.3 zod: 4.3.6 + knuth-shuffle@1.0.8: {} + + koa-compose@4.1.0: {} + + koa-router@14.0.0: + dependencies: + debug: 4.4.3 + http-errors: 2.0.1 + koa-compose: 4.1.0 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + + koa@3.1.2: + dependencies: + accepts: 1.3.8 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookies: 0.9.1 + delegates: 1.0.0 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + fresh: 0.5.2 + http-assert: 1.5.0 + http-errors: 2.0.1 + koa-compose: 4.1.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + langium@4.2.1: dependencies: chevrotain: 11.1.2 @@ -10930,6 +11754,8 @@ snapshots: rfdc: 1.4.1 wrap-ansi: 9.0.2 + load-tsconfig@0.2.5: {} + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -11064,6 +11890,12 @@ snapshots: math-intrinsics@1.1.0: {} + mcp-proxy@6.4.1: + dependencies: + pipenet: 1.4.0 + transitivePeerDependencies: + - supports-color + mdast-util-find-and-replace@3.0.2: dependencies: '@types/mdast': 4.0.4 @@ -11217,6 +12049,10 @@ snapshots: dependencies: '@types/mdast': 4.0.4 + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + merge2@1.4.1: {} mermaid@11.12.3: @@ -11440,10 +12276,16 @@ snapshots: mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35: dependencies: mime-db: 1.52.0 + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + mime@2.6.0: {} mime@3.0.0: {} @@ -11559,6 +12401,8 @@ snapshots: natural-compare@1.4.0: {} + negotiator@0.6.3: {} + negotiator@0.6.4: {} negotiator@1.0.0: {} @@ -11634,6 +12478,11 @@ snapshots: normalize-url@6.1.0: {} + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + npmlog@6.0.2: dependencies: are-we-there-yet: 3.0.1 @@ -11687,6 +12536,10 @@ snapshots: on-exit-leak-free@2.1.2: {} + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -11783,10 +12636,14 @@ snapshots: is-decimal: 2.0.1 is-hexadecimal: 2.0.1 + parse-ms@4.0.0: {} + parse5@7.3.0: dependencies: entities: 6.0.1 + parseurl@1.3.3: {} + path-data-parser@0.1.0: {} path-exists@4.0.0: {} @@ -11795,6 +12652,8 @@ snapshots: path-key@3.1.1: {} + path-key@4.0.0: {} + path-parse@1.0.7: {} path-scurry@1.11.1: @@ -11807,6 +12666,8 @@ snapshots: lru-cache: 11.2.6 minipass: 7.1.2 + path-to-regexp@8.3.0: {} + pathe@2.0.3: {} pathval@2.0.1: {} @@ -11845,8 +12706,23 @@ snapshots: sonic-boom: 4.2.1 thread-stream: 4.0.0 + pipenet@1.4.0: + dependencies: + axios: 1.13.6(debug@4.4.3) + debug: 4.4.3 + human-readable-ids: 1.0.4 + koa: 3.1.2 + koa-router: 14.0.0 + pump: 3.0.3 + tldjs: 2.3.2 + yargs: 18.0.0 + transitivePeerDependencies: + - supports-color + pirates@4.0.7: {} + pkce-challenge@5.0.1: {} + pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -11889,6 +12765,15 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 2.6.1 + postcss: 8.5.6 + tsx: 4.21.0 + yaml: 2.8.2 + postcss-nested@6.2.0(postcss@8.5.6): dependencies: postcss: 8.5.6 @@ -11920,6 +12805,10 @@ snapshots: prettier@3.8.1: {} + pretty-ms@9.3.0: + dependencies: + parse-ms: 4.0.0 + proc-log@5.0.0: {} process-nextick-args@2.0.1: {} @@ -11945,6 +12834,13 @@ snapshots: property-information@7.1.0: {} + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-from-env@1.1.0: {} + pump@3.0.3: dependencies: end-of-stream: 1.4.5 @@ -11952,12 +12848,25 @@ snapshots: punycode@2.3.1: {} + qs@6.15.0: + dependencies: + side-channel: 1.1.0 + queue-microtask@1.2.3: {} quick-format-unescaped@4.0.4: {} quick-lru@5.1.1: {} + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + react-dom@18.3.1(react@18.3.1): dependencies: loose-envify: 1.4.0 @@ -12155,6 +13064,8 @@ snapshots: resolve-from@4.0.0: {} + resolve-from@5.0.0: {} + resolve-pkg-maps@1.0.0: {} resolve@1.22.11: @@ -12245,6 +13156,16 @@ snapshots: points-on-curve: 0.2.0 points-on-path: 0.2.1 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -12313,11 +13234,36 @@ snapshots: semver@7.7.4: {} + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + serialize-error@7.0.1: dependencies: type-fest: 0.13.1 optional: true + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + set-blocking@2.0.0: {} set-cookie-parser@2.7.2: {} @@ -12448,6 +13394,8 @@ snapshots: source-map@0.6.1: {} + source-map@0.7.6: {} + space-separated-tokens@2.0.2: {} split2@4.2.0: {} @@ -12479,6 +13427,8 @@ snapshots: stat-mode@1.0.0: {} + statuses@1.5.0: {} + statuses@2.0.2: {} std-env@3.10.0: {} @@ -12488,6 +13438,8 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 + strict-event-emitter-types@2.0.0: {} + string-argv@0.3.2: {} string-width@4.2.3: @@ -12586,6 +13538,8 @@ snapshots: strip-bom@3.0.0: {} + strip-final-newline@4.0.0: {} + strip-json-comments@3.1.1: {} strip-json-comments@5.0.3: {} @@ -12594,6 +13548,10 @@ snapshots: dependencies: js-tokens: 9.0.1 + strtok3@10.3.4: + dependencies: + '@tokenizer/token': 0.3.0 + style-mod@4.1.3: {} style-to-js@1.1.21: @@ -12737,6 +13695,10 @@ snapshots: tinyspy@4.0.4: {} + tldjs@2.3.2: + dependencies: + punycode: 2.3.1 + tmp-promise@3.0.3: dependencies: tmp: 0.2.5 @@ -12751,6 +13713,14 @@ snapshots: toidentifier@1.0.1: {} + token-types@6.1.2: + dependencies: + '@borewit/text-codec': 0.2.1 + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + + tree-kill@1.2.2: {} + trim-lines@3.0.1: {} trough@2.2.0: {} @@ -12776,6 +13746,36 @@ snapshots: tslib@2.8.1: {} + tsscmp@1.0.6: {} + + tsup@8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.2) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.27.2 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2) + resolve-from: 5.0.0 + rollup: 4.55.1 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + optionalDependencies: + postcss: 8.5.6 + typescript: 5.9.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + tsx@4.21.0: dependencies: esbuild: 0.27.2 @@ -12792,6 +13792,12 @@ snapshots: type-fest@0.13.1: optional: true + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -12843,6 +13849,8 @@ snapshots: uglify-js@3.19.3: optional: true + uint8array-extras@1.5.0: {} + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -12856,6 +13864,10 @@ snapshots: undici-types@7.16.0: {} + undici@7.22.0: {} + + unicorn-magic@0.3.0: {} + unified@11.0.5: dependencies: '@types/unist': 3.0.3 @@ -12914,6 +13926,8 @@ snapshots: universalify@2.0.1: {} + unpipe@1.0.0: {} + unrs-resolver@1.11.1: dependencies: napi-postinstall: 0.3.4 @@ -12948,6 +13962,8 @@ snapshots: dependencies: punycode: 2.3.1 + uri-templates@0.2.0: {} + use-callback-ref@1.3.3(@types/react@18.3.27)(react@18.3.1): dependencies: react: 18.3.1 @@ -12973,6 +13989,8 @@ snapshots: uuid@11.1.0: {} + vary@1.1.2: {} + verror@1.10.1: dependencies: assert-plus: 1.0.0 @@ -12995,6 +14013,24 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 + vite-node@3.2.4(@types/node@22.19.15)(terser@5.46.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 5.4.21(@types/node@22.19.15)(terser@5.46.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vite-node@3.2.4(@types/node@25.0.7)(terser@5.46.0): dependencies: cac: 6.7.14 @@ -13013,6 +14049,16 @@ snapshots: - supports-color - terser + vite@5.4.21(@types/node@22.19.15)(terser@5.46.0): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.55.1 + optionalDependencies: + '@types/node': 22.19.15 + fsevents: 2.3.3 + terser: 5.46.0 + vite@5.4.21(@types/node@25.0.7)(terser@5.46.0): dependencies: esbuild: 0.21.5 @@ -13023,6 +14069,46 @@ snapshots: fsevents: 2.3.3 terser: 5.46.0 + vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.15)(happy-dom@20.0.2)(terser@5.46.0): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@5.4.21(@types/node@25.0.7)(terser@5.46.0)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 5.4.21(@types/node@22.19.15)(terser@5.46.0) + vite-node: 3.2.4(@types/node@22.19.15)(terser@5.46.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.12 + '@types/node': 22.19.15 + happy-dom: 20.0.2 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@20.0.2)(terser@5.46.0): dependencies: '@types/chai': 5.2.3 @@ -13176,6 +14262,11 @@ snapshots: xmlbuilder@15.1.1: {} + xsschema@0.4.3(zod-to-json-schema@3.25.1(zod@4.3.6))(zod@4.3.6): + optionalDependencies: + zod: 4.3.6 + zod-to-json-schema: 3.25.1(zod@4.3.6) + y18n@5.0.8: {} yallist@3.1.1: {} @@ -13188,6 +14279,8 @@ snapshots: yargs-parser@21.1.1: {} + yargs-parser@22.0.0: {} + yargs@17.7.2: dependencies: cliui: 8.0.1 @@ -13198,6 +14291,15 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yargs@18.0.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 7.2.0 + y18n: 5.0.8 + yargs-parser: 22.0.0 + yauzl@2.10.0: dependencies: buffer-crc32: 0.2.13 @@ -13213,12 +14315,18 @@ snapshots: yocto-queue@0.1.0: {} + yoctocolors@2.1.2: {} + zip-stream@4.1.1: dependencies: archiver-utils: 3.0.4 compress-commons: 4.1.2 readable-stream: 3.6.2 + zod-to-json-schema@3.25.1(zod@4.3.6): + dependencies: + zod: 4.3.6 + zod-validation-error@4.0.2(zod@4.3.6): dependencies: zod: 4.3.6 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c5739b74..0caafff1 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,4 @@ +packages: + - mcp-server ignoredBuiltDependencies: - esbuild diff --git a/src/main/services/team/TeamDataService.ts b/src/main/services/team/TeamDataService.ts index 097a8239..ecec4db3 100644 --- a/src/main/services/team/TeamDataService.ts +++ b/src/main/services/team/TeamDataService.ts @@ -1524,7 +1524,7 @@ export class TeamDataService { const tBlocks = tContent as Record[]; if (tBlocks.some((b) => b.type === 'text')) break; // next text = stop for (const b of tBlocks) { - if (b.type === 'tool_use' && typeof b.name === 'string') { + if (b.type === 'tool_use' && typeof b.name === 'string' && b.name !== 'SendMessage') { toolCounts.set(b.name, (toolCounts.get(b.name) ?? 0) + 1); } } diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index ec737a22..f9eadff4 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -2963,7 +2963,11 @@ export class TeamProvisioningService { // These counts will be attached to the next text message as toolSummary. if (run.provisioningComplete) { for (const block of content ?? []) { - if (block?.type === 'tool_use' && typeof block.name === 'string') { + if ( + block?.type === 'tool_use' && + typeof block.name === 'string' && + block.name !== 'SendMessage' + ) { run.pendingToolCounts.set( block.name as string, (run.pendingToolCounts.get(block.name as string) ?? 0) + 1 diff --git a/src/renderer/components/chat/ChatHistory.tsx b/src/renderer/components/chat/ChatHistory.tsx index 243a635e..0b5ad6ab 100644 --- a/src/renderer/components/chat/ChatHistory.tsx +++ b/src/renderer/components/chat/ChatHistory.tsx @@ -20,6 +20,7 @@ import { ChatHistoryEmptyState } from './ChatHistoryEmptyState'; import { ChatHistoryItem } from './ChatHistoryItem'; import { ChatHistoryLoadingState } from './ChatHistoryLoadingState'; +import { formatPercentOfTotal, sumContextInjectionTokens } from '@renderer/utils/contextMath'; import type { ContextInjection } from '@renderer/types/contextInjection'; /** @@ -190,6 +191,11 @@ export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => { return { allContextInjections: injections, lastAiGroupTotalTokens: totalTokens }; }, [sessionContextStats, conversation, selectedContextPhase, sessionPhaseInfo]); + const visibleContextPercentLabel = useMemo(() => { + const visibleTokens = sumContextInjectionTokens(allContextInjections); + return formatPercentOfTotal(visibleTokens, lastAiGroupTotalTokens); + }, [allContextInjections, lastAiGroupTotalTokens]); + // State for navigation highlight (blue, used for Turn navigation from CLAUDE.md panel) const [isNavigationHighlight, setIsNavigationHighlight] = useState(false); const navigationHighlightTimerRef = useRef | null>(null); @@ -828,7 +834,7 @@ export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => { : 'var(--color-text-secondary)', }} > - Context ({allContextInjections.length}) + {visibleContextPercentLabel ?? `Context (${allContextInjections.length})`} )} diff --git a/src/renderer/components/team/TeamDetailView.tsx b/src/renderer/components/team/TeamDetailView.tsx index 94f1adde..c4dffc6b 100644 --- a/src/renderer/components/team/TeamDetailView.tsx +++ b/src/renderer/components/team/TeamDetailView.tsx @@ -456,16 +456,19 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele // Keep lead-session context fresh in the background while the team tab is active. // This keeps the button value current even when the panel is closed. + // For offline teams: fetch once on mount so the percentage shows immediately. + // For alive teams: fetch on mount + periodic refresh every 30s. useEffect(() => { if (!isThisTabActive) return; if (!tabId || !projectId || !leadSessionId) return; + + void fetchSessionDetail(projectId, leadSessionId, tabId, { silent: true }); + if (!data?.isAlive) return; - const tick = (): void => { + const id = window.setInterval(() => { void fetchSessionDetail(projectId, leadSessionId, tabId, { silent: true }); - }; - tick(); - const id = window.setInterval(tick, 30_000); + }, 30_000); return () => window.clearInterval(id); }, [isThisTabActive, tabId, projectId, leadSessionId, data?.isAlive, fetchSessionDetail]); diff --git a/src/renderer/components/team/activity/ActivityTimeline.tsx b/src/renderer/components/team/activity/ActivityTimeline.tsx index 1a1a19a7..136f3f86 100644 --- a/src/renderer/components/team/activity/ActivityTimeline.tsx +++ b/src/renderer/components/team/activity/ActivityTimeline.tsx @@ -326,13 +326,11 @@ export const ActivityTimeline = ({ sessionSeparator = (
-
- - New session - -
+
+ New session +
); } @@ -413,7 +411,7 @@ export const ActivityTimeline = ({ +{hiddenCount} older - +
@@ -299,12 +328,19 @@ export const LeadThoughtsGroupRow = ({
{thought.toolSummary && ( -
- 🔧 {thought.toolSummary} -
+ + +
+ 🔧 {thought.toolSummary} +
+
+ + + +
)}
))}