/** * Session Manager Tool - Workflow session lifecycle management * Operations: init, list, read, write, update, archive, mkdir, delete, stats * Content routing via content_type + path_params */ import { z } from 'zod'; import type { ToolSchema, ToolResult } from '../types/tool.js'; import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, renameSync, rmSync, statSync, } from 'fs'; import { resolve, join, dirname } from 'path'; // Base paths for session storage const WORKFLOW_BASE = '.workflow'; const ACTIVE_BASE = '.workflow/active'; const ARCHIVE_BASE = '.workflow/archives'; const LITE_PLAN_BASE = '.workflow/.lite-plan'; const LITE_FIX_BASE = '.workflow/.lite-fix'; // Session ID validation pattern (alphanumeric, hyphen, underscore) const SESSION_ID_PATTERN = /^[a-zA-Z0-9_-]+$/; // Zod schemas - using tuple syntax for z.enum const ContentTypeEnum = z.enum([ 'session', 'plan', 'task', 'summary', 'process', 'chat', 'brainstorm', 'review-dim', 'review-iter', 'review-fix', 'todo', 'context', // Lite-specific content types 'lite-plan', 'lite-fix-plan', 'exploration', 'explorations-manifest', 'diagnosis', 'diagnoses-manifest', 'clarifications', 'execution-context', 'session-metadata' ]); const OperationEnum = z.enum(['init', 'list', 'read', 'write', 'update', 'archive', 'mkdir', 'delete', 'stats']); const LocationEnum = z.enum([ 'active', 'archived', 'both', 'lite-plan', 'lite-fix', 'all' ]); const ParamsSchema = z.object({ operation: OperationEnum, session_id: z.string().optional(), content_type: ContentTypeEnum.optional(), content: z.union([z.string(), z.record(z.string(), z.any())]).optional(), path_params: z.record(z.string(), z.string()).optional(), metadata: z.record(z.string(), z.any()).optional(), location: LocationEnum.optional(), include_metadata: z.boolean().optional(), dirs: z.array(z.string()).optional(), update_status: z.boolean().optional(), file_path: z.string().optional(), }); type Params = z.infer; type ContentType = z.infer; type Operation = z.infer; type Location = z.infer; interface SessionInfo { session_id: string; location: string; metadata?: any; } interface SessionLocation { path: string; location: string; } interface TaskStats { total: number; pending: number; in_progress: number; completed: number; blocked: number; cancelled: number; } // Cached workflow root (computed once per execution) let cachedWorkflowRoot: string | null = null; /** * Find project root by traversing up looking for .workflow directory * Falls back to cwd if not found */ function findWorkflowRoot(): string { if (cachedWorkflowRoot) return cachedWorkflowRoot; let dir = process.cwd(); const root = dirname(dir) === dir ? dir : null; // filesystem root while (dir && dir !== root) { if (existsSync(join(dir, WORKFLOW_BASE))) { cachedWorkflowRoot = dir; return dir; } const parent = dirname(dir); if (parent === dir) break; // reached filesystem root dir = parent; } // Fallback to cwd (for init operation) cachedWorkflowRoot = process.cwd(); return cachedWorkflowRoot; } /** * Validate session ID format */ function validateSessionId(sessionId: string): void { if (!sessionId || typeof sessionId !== 'string') { throw new Error('session_id must be a non-empty string'); } if (!SESSION_ID_PATTERN.test(sessionId)) { throw new Error( `Invalid session_id format: "${sessionId}". Only alphanumeric, hyphen, and underscore allowed.` ); } if (sessionId.length > 100) { throw new Error('session_id must be 100 characters or less'); } } /** * Validate path params to prevent path traversal */ function validatePathParams(pathParams: Record): void { for (const [key, value] of Object.entries(pathParams)) { if (typeof value !== 'string') continue; if (value.includes('..') || value.includes('/') || value.includes('\\')) { throw new Error(`Invalid path_params.${key}: path traversal characters not allowed`); } } } /** * Content type to file path routing * {base} is replaced with session base path * Dynamic params: {task_id}, {filename}, {dimension}, {iteration} */ const PATH_ROUTES: Record = { // Standard WFS content types session: '{base}/workflow-session.json', plan: '{base}/IMPL_PLAN.md', task: '{base}/.task/{task_id}.json', summary: '{base}/.summaries/{task_id}-summary.md', process: '{base}/.process/{filename}', chat: '{base}/.chat/{filename}', brainstorm: '{base}/.brainstorming/{filename}', 'review-dim': '{base}/.review/dimensions/{dimension}.json', 'review-iter': '{base}/.review/iterations/{iteration}.json', 'review-fix': '{base}/.review/fixes/{filename}', todo: '{base}/TODO_LIST.md', context: '{base}/context-package.json', // Lite-specific content types 'lite-plan': '{base}/plan.json', 'lite-fix-plan': '{base}/fix-plan.json', 'exploration': '{base}/exploration-{angle}.json', 'explorations-manifest': '{base}/explorations-manifest.json', 'diagnosis': '{base}/diagnosis-{angle}.json', 'diagnoses-manifest': '{base}/diagnoses-manifest.json', 'clarifications': '{base}/clarifications.json', 'execution-context': '{base}/execution-context.json', 'session-metadata': '{base}/session-metadata.json', }; /** * Resolve path with base and parameters */ function resolvePath( base: string, contentType: ContentType, pathParams: Record = {} ): string { const template = PATH_ROUTES[contentType]; if (!template) { throw new Error( `Unknown content_type: ${contentType}. Valid types: ${Object.keys(PATH_ROUTES).join(', ')}` ); } let path = template.replace('{base}', base); // Replace dynamic parameters for (const [key, value] of Object.entries(pathParams)) { path = path.replace(`{${key}}`, value); } // Check for unreplaced placeholders const unreplaced = path.match(/\{[^}]+\}/g); if (unreplaced) { throw new Error( `Missing path_params: ${unreplaced.join(', ')} for content_type "${contentType}"` ); } return resolve(findWorkflowRoot(), path); } /** * Get session base path */ function getSessionBase( sessionId: string, location: 'active' | 'archived' | 'lite-plan' | 'lite-fix' = 'active' ): string { const locationMap: Record = { 'active': ACTIVE_BASE, 'archived': ARCHIVE_BASE, 'lite-plan': LITE_PLAN_BASE, 'lite-fix': LITE_FIX_BASE, }; const basePath = locationMap[location] || ACTIVE_BASE; return resolve(findWorkflowRoot(), basePath, sessionId); } /** * Auto-detect session location by searching all known paths * Search order: active, archives, lite-plan, lite-fix */ function findSession(sessionId: string): SessionLocation | null { const root = findWorkflowRoot(); const searchPaths = [ { path: resolve(root, ACTIVE_BASE, sessionId), location: 'active' }, { path: resolve(root, ARCHIVE_BASE, sessionId), location: 'archived' }, { path: resolve(root, LITE_PLAN_BASE, sessionId), location: 'lite-plan' }, { path: resolve(root, LITE_FIX_BASE, sessionId), location: 'lite-fix' }, ]; for (const { path, location } of searchPaths) { if (existsSync(path)) { return { path, location }; } } return null; } /** * Ensure directory exists */ function ensureDir(dirPath: string): void { if (!existsSync(dirPath)) { mkdirSync(dirPath, { recursive: true }); } } /** * Read JSON file safely */ function readJsonFile(filePath: string): any { if (!existsSync(filePath)) { throw new Error(`File not found: ${filePath}`); } try { const content = readFileSync(filePath, 'utf8'); return JSON.parse(content); } catch (error) { if (error instanceof SyntaxError) { throw new Error(`Invalid JSON in ${filePath}: ${error.message}`); } throw new Error(`Failed to read ${filePath}: ${(error as Error).message}`); } } /** * Write JSON file with formatting */ function writeJsonFile(filePath: string, data: any): void { ensureDir(dirname(filePath)); const content = JSON.stringify(data, null, 2); writeFileSync(filePath, content, 'utf8'); } /** * Write text file */ function writeTextFile(filePath: string, content: string): void { ensureDir(dirname(filePath)); writeFileSync(filePath, content, 'utf8'); } // ============================================================ // Helper Functions // ============================================================ /** * List sessions in a specific directory * @param dirPath - Directory to scan * @param location - Location identifier for returned sessions * @param prefix - Optional prefix filter (e.g., 'WFS-'), null means no filter * @param includeMetadata - Whether to load metadata for each session */ function listSessionsInDir( dirPath: string, location: string, prefix: string | null, includeMetadata: boolean ): SessionInfo[] { if (!existsSync(dirPath)) return []; try { const entries = readdirSync(dirPath, { withFileTypes: true }); return entries .filter(e => e.isDirectory() && (prefix === null || e.name.startsWith(prefix))) .map(e => { const sessionInfo: SessionInfo = { session_id: e.name, location }; if (includeMetadata) { // Try multiple metadata file locations const metaPaths = [ join(dirPath, e.name, 'workflow-session.json'), join(dirPath, e.name, 'session-metadata.json'), join(dirPath, e.name, 'explorations-manifest.json'), join(dirPath, e.name, 'diagnoses-manifest.json'), ]; for (const metaPath of metaPaths) { if (existsSync(metaPath)) { try { sessionInfo.metadata = readJsonFile(metaPath); break; } catch { /* continue */ } } } } return sessionInfo; }); } catch { return []; } } // ============================================================ // Operation Handlers // ============================================================ /** * Operation: init * Create new session with directory structure * Supports both WFS sessions and lite sessions (lite-plan, lite-fix) */ function executeInit(params: Params): any { const { session_id, metadata, location } = params; if (!session_id) { throw new Error('Parameter "session_id" is required for init'); } // Validate session_id format validateSessionId(session_id); // Auto-infer location from metadata.type if location not explicitly provided // Priority: explicit location > metadata.type > default 'active' const sessionLocation: 'active' | 'archived' | 'lite-plan' | 'lite-fix' = (location === 'active' || location === 'archived' || location === 'lite-plan' || location === 'lite-fix') ? location : (metadata?.type === 'lite-plan' ? 'lite-plan' : metadata?.type === 'lite-fix' ? 'lite-fix' : 'active'); // Check if session already exists (auto-detect all locations) const existing = findSession(session_id); if (existing) { throw new Error(`Session "${session_id}" already exists in ${existing.location}`); } const sessionPath = getSessionBase(session_id, sessionLocation); // Create session directory structure based on type ensureDir(sessionPath); let directoriesCreated: string[] = []; if (sessionLocation === 'lite-plan' || sessionLocation === 'lite-fix') { // Lite sessions: minimal structure, files created by workflow // No subdirectories needed initially directoriesCreated = []; } else { // WFS sessions: standard structure ensureDir(join(sessionPath, '.task')); ensureDir(join(sessionPath, '.summaries')); ensureDir(join(sessionPath, '.process')); directoriesCreated = ['.task', '.summaries', '.process']; } // Create session metadata file if provided let sessionMetadata = null; if (metadata) { const sessionFile = sessionLocation.startsWith('lite-') ? join(sessionPath, 'session-metadata.json') // Lite sessions : join(sessionPath, 'workflow-session.json'); // WFS sessions const sessionData = { session_id, type: metadata?.type || sessionLocation, // Preserve user-specified type if provided status: 'initialized', created_at: new Date().toISOString(), ...metadata, }; writeJsonFile(sessionFile, sessionData); sessionMetadata = sessionData; } return { operation: 'init', session_id, location: sessionLocation, path: sessionPath, directories_created: directoriesCreated, metadata: sessionMetadata, message: `Session "${session_id}" initialized in ${sessionLocation}`, }; } /** * Operation: list * List sessions (active, archived, lite-plan, lite-fix, or all) */ function executeList(params: Params): any { const { location = 'both', include_metadata = false } = params; const result: { operation: string; active: SessionInfo[]; archived: SessionInfo[]; litePlan: SessionInfo[]; liteFix: SessionInfo[]; total: number; } = { operation: 'list', active: [], archived: [], litePlan: [], liteFix: [], total: 0, }; const root = findWorkflowRoot(); // Helper to check if location should be included const shouldInclude = (loc: string) => location === 'all' || location === 'both' || location === loc; // List active sessions (WFS-* prefix) if (shouldInclude('active')) { result.active = listSessionsInDir( resolve(root, ACTIVE_BASE), 'active', 'WFS-', include_metadata ); } // List archived sessions (WFS-* prefix) if (shouldInclude('archived')) { result.archived = listSessionsInDir( resolve(root, ARCHIVE_BASE), 'archived', 'WFS-', include_metadata ); } // List lite-plan sessions (no prefix filter) if (location === 'all' || location === 'lite-plan') { result.litePlan = listSessionsInDir( resolve(root, LITE_PLAN_BASE), 'lite-plan', null, include_metadata ); } // List lite-fix sessions (no prefix filter) if (location === 'all' || location === 'lite-fix') { result.liteFix = listSessionsInDir( resolve(root, LITE_FIX_BASE), 'lite-fix', null, include_metadata ); } result.total = result.active.length + result.archived.length + result.litePlan.length + result.liteFix.length; return result; } /** * Operation: read * Read file content by content_type */ function executeRead(params: Params): any { const { session_id, content_type, path_params = {} } = params; if (!session_id) { throw new Error('Parameter "session_id" is required for read'); } if (!content_type) { throw new Error('Parameter "content_type" is required for read'); } // Validate inputs validateSessionId(session_id); validatePathParams(path_params); const session = findSession(session_id); if (!session) { throw new Error(`Session "${session_id}" not found`); } const filePath = resolvePath(session.path, content_type, path_params as Record); if (!existsSync(filePath)) { throw new Error(`File not found: ${filePath}`); } // Read content const rawContent = readFileSync(filePath, 'utf8'); // Parse JSON for JSON content types const isJson = filePath.endsWith('.json'); const content = isJson ? JSON.parse(rawContent) : rawContent; return { operation: 'read', session_id, content_type, path: filePath, location: session.location, content, is_json: isJson, }; } /** * Operation: write * Write content to file by content_type */ function executeWrite(params: Params): any { const { session_id, content_type, content, path_params = {} } = params; if (!session_id) { throw new Error('Parameter "session_id" is required for write'); } if (!content_type) { throw new Error('Parameter "content_type" is required for write'); } if (content === undefined) { throw new Error('Parameter "content" is required for write'); } // Validate inputs validateSessionId(session_id); validatePathParams(path_params); const session = findSession(session_id); if (!session) { throw new Error(`Session "${session_id}" not found. Use init operation first.`); } const filePath = resolvePath(session.path, content_type, path_params as Record); const isJson = filePath.endsWith('.json'); // Write content if (isJson) { writeJsonFile(filePath, content); } else { writeTextFile(filePath, typeof content === 'string' ? content : JSON.stringify(content, null, 2)); } // Return written content for task/summary types const returnContent = content_type === 'task' || content_type === 'summary' ? content : undefined; return { operation: 'write', session_id, content_type, written_content: returnContent, path: filePath, location: session.location, message: `File written successfully`, }; } /** * Operation: update * Update existing JSON file with shallow merge */ function executeUpdate(params: Params): any { const { session_id, content_type, content, path_params = {} } = params; if (!session_id) { throw new Error('Parameter "session_id" is required for update'); } if (!content_type) { throw new Error('Parameter "content_type" is required for update'); } if (!content || typeof content !== 'object') { throw new Error('Parameter "content" must be an object for update'); } const session = findSession(session_id); if (!session) { throw new Error(`Session "${session_id}" not found`); } const filePath = resolvePath(session.path, content_type, path_params as Record); if (!filePath.endsWith('.json')) { throw new Error('Update operation only supports JSON files'); } // Read existing content or start with empty object let existing: any = {}; if (existsSync(filePath)) { existing = readJsonFile(filePath); } // Shallow merge const merged = { ...existing, ...(content as object) }; writeJsonFile(filePath, merged); return { operation: 'update', session_id, content_type, path: filePath, location: session.location, fields_updated: Object.keys(content as object), merged_data: merged, message: `File updated successfully`, }; } /** * Operation: archive * Move session from active to archives */ function executeArchive(params: Params): any { const { session_id, update_status = true } = params; if (!session_id) { throw new Error('Parameter "session_id" is required for archive'); } // Find session in any location const session = findSession(session_id); if (!session) { throw new Error(`Session "${session_id}" not found`); } // Lite sessions do not support archiving if (session.location === 'lite-plan' || session.location === 'lite-fix') { throw new Error(`Lite sessions (${session.location}) do not support archiving. Use delete operation instead.`); } // Determine archive destination based on source location let archivePath: string; if (session.location === 'active') { archivePath = getSessionBase(session_id, 'archived'); } else { // Already archived return { operation: 'archive', session_id, status: 'already_archived', path: session.path, location: session.location, message: `Session "${session_id}" is already archived`, }; } // Update status before archiving if (update_status) { const metadataFiles = [ join(session.path, 'workflow-session.json'), join(session.path, 'session-metadata.json'), join(session.path, 'explorations-manifest.json'), ]; for (const metaFile of metadataFiles) { if (existsSync(metaFile)) { try { const data = readJsonFile(metaFile); data.status = 'completed'; data.archived_at = new Date().toISOString(); writeJsonFile(metaFile, data); break; } catch { /* continue */ } } } // Update all task JSONs to completed status const taskDir = join(session.path, '.task'); if (existsSync(taskDir)) { const taskFiles = readdirSync(taskDir).filter(f => f.endsWith('.json')); for (const taskFile of taskFiles) { try { const taskPath = join(taskDir, taskFile); const taskData = readJsonFile(taskPath); if (taskData.status && taskData.status !== 'completed') { taskData.status = 'completed'; taskData.completed_at = new Date().toISOString(); writeJsonFile(taskPath, taskData); } } catch { /* skip invalid task files */ } } } } // Ensure archive directory exists ensureDir(dirname(archivePath)); // Move session directory renameSync(session.path, archivePath); // Read session metadata after archiving let sessionMetadata = null; const metadataFiles = [ join(archivePath, 'workflow-session.json'), join(archivePath, 'session-metadata.json'), join(archivePath, 'explorations-manifest.json'), ]; for (const metaFile of metadataFiles) { if (existsSync(metaFile)) { try { sessionMetadata = readJsonFile(metaFile); break; } catch { /* continue */ } } } // Update development index with archived session info if (sessionMetadata) { updateDevelopmentIndex(sessionMetadata); } return { operation: 'archive', session_id, status: 'archived', source: session.path, source_location: session.location, destination: archivePath, metadata: sessionMetadata, message: `Session "${session_id}" archived from ${session.location}`, }; } /** * Operation: mkdir * Create directory structure within session */ function executeMkdir(params: Params): any { const { session_id, dirs } = params; if (!session_id) { throw new Error('Parameter "session_id" is required for mkdir'); } if (!dirs || !Array.isArray(dirs) || dirs.length === 0) { throw new Error('Parameter "dirs" must be a non-empty array'); } const session = findSession(session_id); if (!session) { throw new Error(`Session "${session_id}" not found`); } const created: string[] = []; for (const dir of dirs) { const dirPath = join(session.path, dir); ensureDir(dirPath); created.push(dir); } return { operation: 'mkdir', session_id, location: session.location, directories_created: created, message: `Created ${created.length} directories`, }; } /** * Operation: delete * Delete a file within session (security: path traversal prevention) */ function executeDelete(params: Params): any { const { session_id, file_path } = params; if (!session_id) { throw new Error('Parameter "session_id" is required for delete'); } if (!file_path) { throw new Error('Parameter "file_path" is required for delete'); } // Validate session exists const session = findSession(session_id); if (!session) { throw new Error(`Session "${session_id}" not found`); } // Security: Prevent path traversal if (file_path.includes('..') || file_path.includes('\\')) { throw new Error('Invalid file_path: path traversal characters not allowed'); } // Construct absolute path const absolutePath = resolve(session.path, file_path); // Security: Verify path is within session directory if (!absolutePath.startsWith(session.path)) { throw new Error('Security error: file_path must be within session directory'); } // Check file exists if (!existsSync(absolutePath)) { throw new Error(`File not found: ${file_path}`); } // Delete the file rmSync(absolutePath, { force: true }); return { operation: 'delete', session_id, deleted: file_path, absolute_path: absolutePath, message: `File deleted successfully`, }; } /** * Operation: stats * Get session statistics (tasks, summaries, plan) */ function executeStats(params: Params): any { const { session_id } = params; if (!session_id) { throw new Error('Parameter "session_id" is required for stats'); } // Validate session exists const session = findSession(session_id); if (!session) { throw new Error(`Session "${session_id}" not found`); } const taskDir = join(session.path, '.task'); const summariesDir = join(session.path, '.summaries'); const planFile = join(session.path, 'IMPL_PLAN.md'); // Count tasks by status const taskStats: TaskStats = { total: 0, pending: 0, in_progress: 0, completed: 0, blocked: 0, cancelled: 0, }; if (existsSync(taskDir)) { const taskFiles = readdirSync(taskDir).filter((f) => f.endsWith('.json')); taskStats.total = taskFiles.length; for (const taskFile of taskFiles) { try { const taskPath = join(taskDir, taskFile); const taskData = readJsonFile(taskPath); const status = taskData.status || 'unknown'; if (status in taskStats) { (taskStats as any)[status]++; } } catch { // Skip invalid task files } } } // Count summaries let summariesCount = 0; if (existsSync(summariesDir)) { summariesCount = readdirSync(summariesDir).filter((f) => f.endsWith('.md')).length; } // Check for plan const hasPlan = existsSync(planFile); return { operation: 'stats', session_id, location: session.location, tasks: taskStats, summaries: summariesCount, has_plan: hasPlan, message: `Session statistics retrieved`, }; } /** * Updates the project's development index when a session is archived. * Simplified: only appends entries, does NOT manage statistics. * Dashboard aggregator handles dynamic calculation. */ function updateDevelopmentIndex(sessionMetadata: any): void { if (!sessionMetadata || !sessionMetadata.session_id) { console.warn('Skipping development index update due to missing session metadata.'); return; } const root = findWorkflowRoot(); const projectTechFile = join(root, WORKFLOW_BASE, 'project-tech.json'); if (!existsSync(projectTechFile)) { console.warn(`Skipping development index update: ${projectTechFile} not found.`); return; } try { const projectData = readJsonFile(projectTechFile); // Ensure development_index exists if (!projectData.development_index) { projectData.development_index = { feature: [], enhancement: [], bugfix: [], refactor: [], docs: [] }; } // Type inference from description const description = (sessionMetadata.description || '').toLowerCase(); let devType: 'feature' | 'enhancement' | 'bugfix' | 'refactor' | 'docs' = 'enhancement'; if (sessionMetadata.type === 'docs') { devType = 'docs'; } else if (/\b(fix|bug|resolve)\b/.test(description)) { devType = 'bugfix'; } else if (/\b(feature|implement|add|create)\b/.test(description)) { devType = 'feature'; } else if (/\b(refactor|restructure|cleanup)\b/.test(description)) { devType = 'refactor'; } const entry = { title: sessionMetadata.description || sessionMetadata.project || sessionMetadata.session_id, sessionId: sessionMetadata.session_id, type: devType, tags: sessionMetadata.tags || [], archivedAt: sessionMetadata.archived_at || new Date().toISOString(), }; // Append to correct category if (!projectData.development_index[devType]) { projectData.development_index[devType] = []; } projectData.development_index[devType].push(entry); // CRITICAL: Do NOT touch projectData.statistics // Dashboard aggregator handles dynamic calculation writeJsonFile(projectTechFile, projectData); console.log(`Development index updated for session: ${sessionMetadata.session_id}`); } catch (error) { console.error(`Failed to update development index: ${(error as Error).message}`); } } // ============================================================ // Main Execute Function // ============================================================ /** * Route to appropriate operation handler */ async function execute(params: Params): Promise { const { operation } = params; if (!operation) { throw new Error( 'Parameter "operation" is required. Valid operations: init, list, read, write, update, archive, mkdir, delete, stats' ); } switch (operation) { case 'init': return executeInit(params); case 'list': return executeList(params); case 'read': return executeRead(params); case 'write': return executeWrite(params); case 'update': return executeUpdate(params); case 'archive': return executeArchive(params); case 'mkdir': return executeMkdir(params); case 'delete': return executeDelete(params); case 'stats': return executeStats(params); default: throw new Error( `Unknown operation: ${operation}. Valid operations: init, list, read, write, update, archive, mkdir, delete, stats` ); } } // ============================================================ // Tool Definition // ============================================================ export const schema: ToolSchema = { name: 'session_manager', description: `Workflow session management. Usage: session_manager(operation="init", type="workflow", description="...") session_manager(operation="list", location="active|archived|both") session_manager(operation="read", sessionId="WFS-xxx", contentType="plan|task|summary") session_manager(operation="write", sessionId="WFS-xxx", contentType="plan", content={...}) session_manager(operation="archive", sessionId="WFS-xxx") session_manager(operation="stats", sessionId="WFS-xxx")`, inputSchema: { type: 'object', properties: { operation: { type: 'string', enum: ['init', 'list', 'read', 'write', 'update', 'archive', 'mkdir', 'delete', 'stats'], description: 'Operation to perform', }, session_id: { type: 'string', description: 'Session identifier (e.g., WFS-my-session). Required for all operations except list.', }, content_type: { type: 'string', enum: [ 'session', 'plan', 'task', 'summary', 'process', 'chat', 'brainstorm', 'review-dim', 'review-iter', 'review-fix', 'todo', 'context', ], description: 'Content type for read/write/update operations', }, content: { type: 'object', description: 'Content for write/update operations (object for JSON, string for text)', }, path_params: { type: 'object', description: 'Dynamic path parameters: task_id, filename, dimension, iteration', }, metadata: { type: 'object', description: 'Session metadata for init operation (project, type, description, etc.)', }, location: { type: 'string', enum: ['active', 'archived', 'both'], description: 'Session location filter for list operation (default: both)', }, include_metadata: { type: 'boolean', description: 'Include session metadata in list results (default: false)', }, dirs: { type: 'array', description: 'Directory paths to create for mkdir operation', }, update_status: { type: 'boolean', description: 'Update session status to completed when archiving (default: true)', }, file_path: { type: 'string', description: 'Relative file path within session for delete operation', }, }, required: ['operation'], }, }; export async function handler(params: Record): Promise { const parsed = ParamsSchema.safeParse(params); if (!parsed.success) { return { success: false, error: `Invalid params: ${parsed.error.message}` }; } try { const result = await execute(parsed.data); return { success: true, result }; } catch (error) { return { success: false, error: (error as Error).message }; } }