From 1aa11c4ee8799b1056027a0575b578b0458bb4f3 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Tue, 10 Mar 2026 20:10:12 -0500 Subject: [PATCH] feat(brain): @mosaic/brain structured data service (#10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement @mosaic/brain — typed structured data service with MCP + REST API, JSON file backend, and schema validation via Zod. Collections: tasks, projects, events, agents, tickets, appreciations, missions, mission_tasks. MCP tools: brain_tasks, brain_projects, brain_events, brain_agents, brain_tickets, brain_today, brain_stale, brain_stats, brain_search, brain_audit, brain_missions, brain_mission, brain_mission_tasks, plus mutation tools for all collections. REST API mirrors MCP 1:1 at /v1/*. Bearer token auth with timing-safe comparison. Fastify server with per-request MCP instances (stateless HTTP transport). JSON file storage with proper-lockfile for concurrent access. Also adds Brain* types to @mosaic/types. Co-Authored-By: Claude Opus 4.6 --- docs/scratchpads/brain-service.md | 40 ++ packages/brain/Dockerfile | 18 + packages/brain/package.json | 45 ++ packages/brain/src/index.ts | 83 ++++ packages/brain/src/mcp/tools.ts | 347 ++++++++++++++ packages/brain/src/middleware/auth.ts | 25 + packages/brain/src/routes/api.ts | 190 ++++++++ packages/brain/src/schemas.ts | 222 +++++++++ packages/brain/src/storage/collections.ts | 536 ++++++++++++++++++++++ packages/brain/src/storage/json-store.ts | 156 +++++++ packages/brain/tsconfig.json | 8 + packages/types/src/index.ts | 312 +++++++++++++ pnpm-lock.yaml | 364 +++++++++++++++ 13 files changed, 2346 insertions(+) create mode 100644 docs/scratchpads/brain-service.md create mode 100644 packages/brain/Dockerfile create mode 100644 packages/brain/package.json create mode 100644 packages/brain/src/index.ts create mode 100644 packages/brain/src/mcp/tools.ts create mode 100644 packages/brain/src/middleware/auth.ts create mode 100644 packages/brain/src/routes/api.ts create mode 100644 packages/brain/src/schemas.ts create mode 100644 packages/brain/src/storage/collections.ts create mode 100644 packages/brain/src/storage/json-store.ts create mode 100644 packages/brain/tsconfig.json diff --git a/docs/scratchpads/brain-service.md b/docs/scratchpads/brain-service.md new file mode 100644 index 0000000..33194c9 --- /dev/null +++ b/docs/scratchpads/brain-service.md @@ -0,0 +1,40 @@ +# Scratchpad: @mosaic/brain Structured Data Service + +**Issue:** #10 +**Branch:** feat/brain-service +**PRD:** jarvis-brain/docs/planning/MOSAIC-BRAIN-MEMORY-PRD.md + +## Objective + +Implement @mosaic/brain as a Fastify service exposing structured data (tasks, projects, events, missions, etc.) via MCP + REST with JSON file backend. + +## Plan + +1. [x] Add brain data types to @mosaic/types +2. [ ] Scaffold packages/brain/ with Fastify + MCP SDK +3. [ ] Implement JSON file storage adapter with locking +4. [ ] Implement collections: tasks, projects, events, agents, tickets, appreciations, missions, mission_tasks +5. [ ] Implement MCP tools (query + mutation + mission) +6. [ ] Implement REST API (mirrors MCP) +7. [ ] Bearer token auth middleware +8. [ ] brain_today computed endpoint +9. [ ] brain_stale, brain_stats, brain_audit, brain_search, brain_graph +10. [ ] Dockerfile + docker-compose +11. [ ] Tests +12. [ ] Code review +13. [ ] Commit, push, PR, merge + +## Progress + +- 2026-03-10: Issue #10 created, branch created, starting implementation + +## Risks + +- Types package already has `Task`/`TaskStatus`/`TaskPriority` for queue — need namespacing +- Large implementation surface — focus on core CRUD first, computed endpoints second + +## Decisions + +- Brain types exported from `@mosaic/types` under `Brain` prefix to avoid queue type conflicts +- JSON storage uses one file per collection +- In-memory index rebuilt on startup diff --git a/packages/brain/Dockerfile b/packages/brain/Dockerfile new file mode 100644 index 0000000..f262698 --- /dev/null +++ b/packages/brain/Dockerfile @@ -0,0 +1,18 @@ +FROM node:22-alpine AS builder +WORKDIR /app +COPY package.json tsconfig.json ./ +COPY src/ src/ +# In standalone mode, @mosaic/types is bundled or installed from registry +RUN npm install && npm run build + +FROM node:22-alpine +WORKDIR /app +COPY --from=builder /app/dist/ dist/ +COPY --from=builder /app/node_modules/ node_modules/ +COPY package.json ./ +ENV NODE_ENV=production +ENV PORT=8100 +ENV MOSAIC_BRAIN_DATA_DIR=/data +EXPOSE 8100 +VOLUME /data +CMD ["node", "dist/index.js"] diff --git a/packages/brain/package.json b/packages/brain/package.json new file mode 100644 index 0000000..feb559e --- /dev/null +++ b/packages/brain/package.json @@ -0,0 +1,45 @@ +{ + "name": "@mosaic/brain", + "version": "0.1.0", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "bin": { + "mosaic-brain": "./dist/index.js" + }, + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc -p tsconfig.json", + "dev": "tsx watch src/index.ts", + "start": "node dist/index.js", + "typecheck": "tsc --noEmit", + "lint": "echo 'ok'", + "test": "vitest run" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.1", + "@mosaic/types": "workspace:*", + "fastify": "^5.3.3", + "proper-lockfile": "^4.1.2", + "zod": "^3.24.4" + }, + "devDependencies": { + "@types/node": "^22", + "@types/proper-lockfile": "^4", + "tsx": "^4", + "typescript": "^5", + "vitest": "^3" + }, + "publishConfig": { + "registry": "https://git.mosaicstack.dev/api/packages/mosaic/npm", + "access": "public" + } +} diff --git a/packages/brain/src/index.ts b/packages/brain/src/index.ts new file mode 100644 index 0000000..c64405e --- /dev/null +++ b/packages/brain/src/index.ts @@ -0,0 +1,83 @@ +#!/usr/bin/env node + +import Fastify from 'fastify'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { JsonStore } from './storage/json-store.js'; +import { Collections } from './storage/collections.js'; +import { requireAuth } from './middleware/auth.js'; +import { registerRoutes } from './routes/api.js'; +import { registerMcpTools } from './mcp/tools.js'; + +const HOST = process.env['HOST'] ?? '0.0.0.0'; +const PORT = Number(process.env['PORT'] ?? 8100); +const DATA_DIR = process.env['MOSAIC_BRAIN_DATA_DIR'] ?? './data'; + +/** + * Create a fresh MCP server instance with all tools registered. + * Each HTTP request gets its own instance to avoid state leaks. + */ +function createMcpServer(collections: Collections): McpServer { + const mcp = new McpServer({ + name: 'mosaic-brain', + version: '0.1.0', + }); + registerMcpTools(mcp, collections); + return mcp; +} + +async function main(): Promise { + // --- Storage --- + const store = new JsonStore({ dataDir: DATA_DIR }); + await store.init(); + const collections = new Collections(store); + + // --- Fastify --- + const app = Fastify({ logger: true }); + + // Auth on all /v1 and /mcp routes + app.addHook('onRequest', async (req, reply) => { + if (req.url.startsWith('/v1/') || req.url === '/mcp') { + await requireAuth(req, reply); + } + }); + + // REST API + registerRoutes(app, collections); + + // MCP over HTTP (Streamable HTTP transport) + // Each request gets a fresh McpServer + transport to avoid state leaks + app.all('/mcp', async (req, reply) => { + const mcp = createMcpServer(collections); + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, // stateless + }); + await mcp.connect(transport); + + if (req.method === 'POST') { + await transport.handleRequest( + req.raw, + reply.raw, + req.body as Record, + ); + } else if (req.method === 'GET') { + await transport.handleRequest(req.raw, reply.raw); + } else if (req.method === 'DELETE') { + await transport.handleRequest(req.raw, reply.raw); + } else { + reply.code(405).send({ error: 'Method not allowed' }); + } + }); + + // --- Start --- + await app.listen({ host: HOST, port: PORT }); + console.log(`mosaic-brain listening on ${HOST}:${PORT}`); + console.log(` REST API: http://${HOST}:${PORT}/v1/`); + console.log(` MCP: http://${HOST}:${PORT}/mcp`); + console.log(` Data dir: ${DATA_DIR}`); +} + +main().catch((err) => { + console.error('Failed to start mosaic-brain:', err); + process.exit(1); +}); diff --git a/packages/brain/src/mcp/tools.ts b/packages/brain/src/mcp/tools.ts new file mode 100644 index 0000000..70c6100 --- /dev/null +++ b/packages/brain/src/mcp/tools.ts @@ -0,0 +1,347 @@ +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { z } from 'zod'; +import type { Collections } from '../storage/collections.js'; +import { + BrainTaskSchema, BrainTaskUpdateSchema, + BrainProjectSchema, BrainProjectUpdateSchema, + BrainEventSchema, BrainEventUpdateSchema, + BrainAgentUpdateSchema, + BrainMissionSchema, BrainMissionUpdateSchema, + BrainMissionTaskSchema, BrainMissionTaskUpdateSchema, + BrainDomainSchema, TaskPrioritySchema, BrainTaskStatusSchema, + BrainProjectStatusSchema, BrainEventTypeSchema, BrainEventStatusSchema, + BrainMissionStatusSchema, BrainAgentStatusSchema, +} from '../schemas.js'; + +export function registerMcpTools(mcp: McpServer, collections: Collections): void { + + // === Query Tools === + + mcp.tool('brain_tasks', + 'Query tasks with filters. Returns sorted by priority then due date.', + { + status: BrainTaskStatusSchema.optional(), + priority: TaskPrioritySchema.optional(), + domain: BrainDomainSchema.optional(), + project: z.string().optional(), + due_before: z.string().optional(), + due_after: z.string().optional(), + assignee: z.string().optional(), + limit: z.number().int().positive().optional(), + }, + async (args) => { + const tasks = await collections.getTasks(args); + return { content: [{ type: 'text' as const, text: JSON.stringify(tasks, null, 2) }] }; + }, + ); + + mcp.tool('brain_projects', + 'Query projects with filters. Returns sorted by priority.', + { + status: BrainProjectStatusSchema.optional(), + domain: BrainDomainSchema.optional(), + priority_min: z.number().int().optional(), + priority_max: z.number().int().optional(), + limit: z.number().int().positive().optional(), + }, + async (args) => { + const projects = await collections.getProjects(args); + return { content: [{ type: 'text' as const, text: JSON.stringify(projects, null, 2) }] }; + }, + ); + + mcp.tool('brain_events', + 'Query events with filters. Returns sorted by date then time.', + { + date_from: z.string().optional(), + date_to: z.string().optional(), + domain: BrainDomainSchema.optional(), + type: BrainEventTypeSchema.optional(), + status: BrainEventStatusSchema.optional(), + limit: z.number().int().positive().optional(), + }, + async (args) => { + const events = await collections.getEvents(args); + return { content: [{ type: 'text' as const, text: JSON.stringify(events, null, 2) }] }; + }, + ); + + mcp.tool('brain_agents', + 'Query active agent sessions.', + { + status: BrainAgentStatusSchema.optional(), + project: z.string().optional(), + }, + async (args) => { + const agents = await collections.getAgents(args); + return { content: [{ type: 'text' as const, text: JSON.stringify(agents, null, 2) }] }; + }, + ); + + mcp.tool('brain_tickets', + 'Query helpdesk tickets.', + { + status: z.number().int().optional(), + priority: z.number().int().optional(), + limit: z.number().int().positive().optional(), + }, + async (args) => { + const tickets = await collections.getTickets(args); + return { content: [{ type: 'text' as const, text: JSON.stringify(tickets, null, 2) }] }; + }, + ); + + mcp.tool('brain_today', + 'Daily summary: events, near-term tasks, stale items, blocked items, active missions, stats.', + { date: z.string().optional() }, + async (args) => { + const summary = await collections.getToday(args.date); + return { content: [{ type: 'text' as const, text: JSON.stringify(summary, null, 2) }] }; + }, + ); + + mcp.tool('brain_stale', + 'Find tasks and projects not updated in N+ days.', + { days: z.number().int().positive().default(7) }, + async (args) => { + const report = await collections.getStale(args.days); + return { content: [{ type: 'text' as const, text: JSON.stringify(report, null, 2) }] }; + }, + ); + + mcp.tool('brain_stats', + 'Brain statistics: counts by collection, domain, status.', + {}, + async () => { + const stats = await collections.getStats(); + return { content: [{ type: 'text' as const, text: JSON.stringify(stats, null, 2) }] }; + }, + ); + + mcp.tool('brain_search', + 'Full-text search across titles and notes (deterministic, not semantic).', + { + query: z.string().min(1), + collection: z.enum(['tasks', 'projects', 'events', 'missions']).optional(), + }, + async (args) => { + const results = await collections.search(args.query, args.collection); + return { content: [{ type: 'text' as const, text: JSON.stringify(results, null, 2) }] }; + }, + ); + + mcp.tool('brain_audit', + 'Data integrity check: orphan references, broken dependencies, missing fields, duplicate IDs.', + {}, + async () => { + const result = await collections.audit(); + return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] }; + }, + ); + + // === Mutation Tools === + + mcp.tool('brain_add_task', + 'Create a new task with schema validation.', + BrainTaskSchema.shape, + async (args) => { + try { + const task = BrainTaskSchema.parse(args); + const result = await collections.addTask(task); + return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] }; + } catch (e: unknown) { + return { content: [{ type: 'text' as const, text: JSON.stringify({ error: String(e) }) }], isError: true }; + } + }, + ); + + mcp.tool('brain_update_task', + 'Update an existing task. Auto-sets updated timestamp.', + { + id: z.string(), + ...BrainTaskUpdateSchema.shape, + }, + async ({ id, ...updates }) => { + try { + const result = await collections.updateTask(id, updates); + return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] }; + } catch (e: unknown) { + return { content: [{ type: 'text' as const, text: JSON.stringify({ error: String(e) }) }], isError: true }; + } + }, + ); + + mcp.tool('brain_add_project', + 'Create a new project with schema validation.', + BrainProjectSchema.shape, + async (args) => { + try { + const project = BrainProjectSchema.parse(args); + const result = await collections.addProject(project); + return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] }; + } catch (e: unknown) { + return { content: [{ type: 'text' as const, text: JSON.stringify({ error: String(e) }) }], isError: true }; + } + }, + ); + + mcp.tool('brain_update_project', + 'Update an existing project. Auto-sets updated timestamp.', + { + id: z.string(), + ...BrainProjectUpdateSchema.shape, + }, + async ({ id, ...updates }) => { + try { + const result = await collections.updateProject(id, updates); + return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] }; + } catch (e: unknown) { + return { content: [{ type: 'text' as const, text: JSON.stringify({ error: String(e) }) }], isError: true }; + } + }, + ); + + mcp.tool('brain_add_event', + 'Create a new event with schema validation.', + BrainEventSchema.shape, + async (args) => { + try { + const event = BrainEventSchema.parse(args); + const result = await collections.addEvent(event); + return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] }; + } catch (e: unknown) { + return { content: [{ type: 'text' as const, text: JSON.stringify({ error: String(e) }) }], isError: true }; + } + }, + ); + + mcp.tool('brain_update_event', + 'Update an existing event.', + { + id: z.string(), + ...BrainEventUpdateSchema.shape, + }, + async ({ id, ...updates }) => { + try { + const result = await collections.updateEvent(id, updates); + return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] }; + } catch (e: unknown) { + return { content: [{ type: 'text' as const, text: JSON.stringify({ error: String(e) }) }], isError: true }; + } + }, + ); + + mcp.tool('brain_update_agent', + 'Update an agent session (auto-creates if not found).', + { + id: z.string(), + ...BrainAgentUpdateSchema.shape, + }, + async ({ id, ...updates }) => { + try { + const result = await collections.updateAgent(id, updates); + return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] }; + } catch (e: unknown) { + return { content: [{ type: 'text' as const, text: JSON.stringify({ error: String(e) }) }], isError: true }; + } + }, + ); + + // === Mission Tools === + + mcp.tool('brain_missions', + 'List and filter missions.', + { + status: BrainMissionStatusSchema.optional(), + project: z.string().optional(), + limit: z.number().int().positive().optional(), + }, + async (args) => { + const missions = await collections.getMissions(args); + return { content: [{ type: 'text' as const, text: JSON.stringify(missions, null, 2) }] }; + }, + ); + + mcp.tool('brain_mission', + 'Get, create, or update a single mission. For read: provide id only. For create: provide all required fields. For update: provide id plus fields to change.', + { + id: z.string(), + title: z.string().optional(), + project: z.string().optional(), + prd_path: z.string().nullish(), + status: BrainMissionStatusSchema.optional(), + created: z.string().optional(), + updated: z.string().optional(), + notes: z.string().nullish(), + action: z.enum(['get', 'create', 'update']).default('get'), + }, + async (args) => { + try { + const { action, ...fields } = args; + if (action === 'get') { + const summary = await collections.getMissionSummary(fields.id); + if (!summary) return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'Mission not found' }) }], isError: true }; + return { content: [{ type: 'text' as const, text: JSON.stringify(summary, null, 2) }] }; + } + if (action === 'create') { + const mission = BrainMissionSchema.parse(fields); + const result = await collections.addMission(mission); + return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] }; + } + // update + const { id, ...updates } = fields; + const result = await collections.updateMission(id, updates); + return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] }; + } catch (e: unknown) { + return { content: [{ type: 'text' as const, text: JSON.stringify({ error: String(e) }) }], isError: true }; + } + }, + ); + + mcp.tool('brain_mission_tasks', + 'List, create, or update tasks within a mission. For list: provide mission_id. For create: provide all required fields. For update: provide mission_id, id, and fields to change.', + { + mission_id: z.string(), + id: z.string().optional(), + title: z.string().optional(), + phase: z.string().nullish(), + status: BrainTaskStatusSchema.optional(), + priority: TaskPrioritySchema.optional(), + dependencies: z.array(z.string()).optional(), + assigned_to: z.string().nullish(), + pr: z.string().nullish(), + order: z.number().int().optional(), + created: z.string().optional(), + updated: z.string().optional(), + completed_at: z.string().nullish(), + notes: z.string().nullish(), + action: z.enum(['list', 'create', 'update']).default('list'), + }, + async (args) => { + try { + const { action, mission_id, ...fields } = args; + if (action === 'list') { + const tasks = await collections.getMissionTasks({ + mission_id, + status: fields.status, + phase: fields.phase ?? undefined, + priority: fields.priority, + }); + return { content: [{ type: 'text' as const, text: JSON.stringify(tasks, null, 2) }] }; + } + if (action === 'create') { + const task = BrainMissionTaskSchema.parse({ ...fields, mission_id }); + const result = await collections.addMissionTask(task); + return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] }; + } + // update + if (!fields.id) return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'id required for update' }) }], isError: true }; + const { id, ...updates } = fields; + const result = await collections.updateMissionTask(mission_id, id, updates); + return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] }; + } catch (e: unknown) { + return { content: [{ type: 'text' as const, text: JSON.stringify({ error: String(e) }) }], isError: true }; + } + }, + ); +} diff --git a/packages/brain/src/middleware/auth.ts b/packages/brain/src/middleware/auth.ts new file mode 100644 index 0000000..a7f844d --- /dev/null +++ b/packages/brain/src/middleware/auth.ts @@ -0,0 +1,25 @@ +import { timingSafeEqual } from 'node:crypto'; +import type { FastifyRequest, FastifyReply } from 'fastify'; + +const API_KEY = process.env['MOSAIC_BRAIN_API_KEY'] ?? ''; + +function safeCompare(a: string, b: string): boolean { + if (a.length !== b.length) return false; + return timingSafeEqual(Buffer.from(a), Buffer.from(b)); +} + +export async function requireAuth(request: FastifyRequest, reply: FastifyReply): Promise { + if (!API_KEY) return; // No key configured = open access (dev mode) + + const authHeader = request.headers.authorization; + if (!authHeader?.startsWith('Bearer ')) { + reply.code(401).send({ error: 'Missing or invalid Authorization header', code: 'UNAUTHORIZED' }); + return reply.hijack(); + } + + const token = authHeader.slice(7); + if (!safeCompare(token, API_KEY)) { + reply.code(401).send({ error: 'Invalid API key', code: 'UNAUTHORIZED' }); + return reply.hijack(); + } +} diff --git a/packages/brain/src/routes/api.ts b/packages/brain/src/routes/api.ts new file mode 100644 index 0000000..824d803 --- /dev/null +++ b/packages/brain/src/routes/api.ts @@ -0,0 +1,190 @@ +import type { FastifyInstance } from 'fastify'; +import type { Collections } from '../storage/collections.js'; +import { + BrainTaskSchema, BrainTaskUpdateSchema, + BrainProjectSchema, BrainProjectUpdateSchema, + BrainEventSchema, BrainEventUpdateSchema, + BrainAgentUpdateSchema, + BrainMissionSchema, BrainMissionUpdateSchema, + BrainMissionTaskSchema, BrainMissionTaskUpdateSchema, +} from '../schemas.js'; + +function parseFilters(query: Record): Record { + const filters: Record = {}; + for (const [key, value] of Object.entries(query)) { + if (value === undefined) continue; + if (key === 'limit' || key === 'priority_min' || key === 'priority_max') { + filters[key] = Number(value); + } else { + filters[key] = value; + } + } + return filters; +} + +export function registerRoutes(app: FastifyInstance, collections: Collections): void { + + // === Health === + app.get('/health', async () => ({ status: 'ok', service: 'mosaic-brain', version: '0.1.0' })); + + // === Tasks === + app.get('/v1/tasks', async (req) => collections.getTasks(parseFilters(req.query as Record))); + app.get<{ Params: { id: string } }>('/v1/tasks/:id', async (req, reply) => { + const task = await collections.getTask(req.params.id); + if (!task) return reply.code(404).send({ error: 'Task not found', code: 'NOT_FOUND' }); + return task; + }); + app.post('/v1/tasks', async (req, reply) => { + try { + const parsed = BrainTaskSchema.parse(req.body); + return await collections.addTask(parsed); + } catch (e: unknown) { + return reply.code(400).send({ error: String(e), code: 'VALIDATION_ERROR' }); + } + }); + app.patch<{ Params: { id: string } }>('/v1/tasks/:id', async (req, reply) => { + try { + const parsed = BrainTaskUpdateSchema.parse(req.body); + return await collections.updateTask(req.params.id, parsed); + } catch (e: unknown) { + return reply.code(400).send({ error: String(e), code: 'VALIDATION_ERROR' }); + } + }); + + // === Projects === + app.get('/v1/projects', async (req) => collections.getProjects(parseFilters(req.query as Record))); + app.get<{ Params: { id: string } }>('/v1/projects/:id', async (req, reply) => { + const project = await collections.getProject(req.params.id); + if (!project) return reply.code(404).send({ error: 'Project not found', code: 'NOT_FOUND' }); + return project; + }); + app.post('/v1/projects', async (req, reply) => { + try { + const parsed = BrainProjectSchema.parse(req.body); + return await collections.addProject(parsed); + } catch (e: unknown) { + return reply.code(400).send({ error: String(e), code: 'VALIDATION_ERROR' }); + } + }); + app.patch<{ Params: { id: string } }>('/v1/projects/:id', async (req, reply) => { + try { + const parsed = BrainProjectUpdateSchema.parse(req.body); + return await collections.updateProject(req.params.id, parsed); + } catch (e: unknown) { + return reply.code(400).send({ error: String(e), code: 'VALIDATION_ERROR' }); + } + }); + + // === Events === + app.get('/v1/events', async (req) => collections.getEvents(parseFilters(req.query as Record))); + app.get<{ Params: { id: string } }>('/v1/events/:id', async (req, reply) => { + const event = await collections.getEvent(req.params.id); + if (!event) return reply.code(404).send({ error: 'Event not found', code: 'NOT_FOUND' }); + return event; + }); + app.post('/v1/events', async (req, reply) => { + try { + const parsed = BrainEventSchema.parse(req.body); + return await collections.addEvent(parsed); + } catch (e: unknown) { + return reply.code(400).send({ error: String(e), code: 'VALIDATION_ERROR' }); + } + }); + app.patch<{ Params: { id: string } }>('/v1/events/:id', async (req, reply) => { + try { + const parsed = BrainEventUpdateSchema.parse(req.body); + return await collections.updateEvent(req.params.id, parsed); + } catch (e: unknown) { + return reply.code(400).send({ error: String(e), code: 'VALIDATION_ERROR' }); + } + }); + + // === Agents === + app.get('/v1/agents', async (req) => collections.getAgents(parseFilters(req.query as Record))); + app.patch<{ Params: { id: string } }>('/v1/agents/:id', async (req, reply) => { + try { + const parsed = BrainAgentUpdateSchema.parse(req.body); + return await collections.updateAgent(req.params.id, parsed); + } catch (e: unknown) { + return reply.code(400).send({ error: String(e), code: 'VALIDATION_ERROR' }); + } + }); + + // === Tickets === + app.get('/v1/tickets', async (req) => { + const q = req.query as Record; + return collections.getTickets({ + status: q['status'] ? Number(q['status']) : undefined, + priority: q['priority'] ? Number(q['priority']) : undefined, + limit: q['limit'] ? Number(q['limit']) : undefined, + }); + }); + + // === Missions === + app.get('/v1/missions', async (req) => collections.getMissions(parseFilters(req.query as Record))); + app.get<{ Params: { id: string } }>('/v1/missions/:id', async (req, reply) => { + const summary = await collections.getMissionSummary(req.params.id); + if (!summary) return reply.code(404).send({ error: 'Mission not found', code: 'NOT_FOUND' }); + return summary; + }); + app.post('/v1/missions', async (req, reply) => { + try { + const parsed = BrainMissionSchema.parse(req.body); + return await collections.addMission(parsed); + } catch (e: unknown) { + return reply.code(400).send({ error: String(e), code: 'VALIDATION_ERROR' }); + } + }); + app.patch<{ Params: { id: string } }>('/v1/missions/:id', async (req, reply) => { + try { + const parsed = BrainMissionUpdateSchema.parse(req.body); + return await collections.updateMission(req.params.id, parsed); + } catch (e: unknown) { + return reply.code(400).send({ error: String(e), code: 'VALIDATION_ERROR' }); + } + }); + + // === Mission Tasks === + app.get<{ Params: { id: string } }>('/v1/missions/:id/tasks', async (req) => { + const q = req.query as Record; + return collections.getMissionTasks({ + mission_id: req.params.id, + status: q['status'] as any, + phase: q['phase'], + priority: q['priority'] as any, + }); + }); + app.post<{ Params: { id: string } }>('/v1/missions/:id/tasks', async (req, reply) => { + try { + const parsed = BrainMissionTaskSchema.parse({ ...(req.body as object), mission_id: req.params.id }); + return await collections.addMissionTask(parsed); + } catch (e: unknown) { + return reply.code(400).send({ error: String(e), code: 'VALIDATION_ERROR' }); + } + }); + app.patch<{ Params: { id: string; taskId: string } }>('/v1/missions/:id/tasks/:taskId', async (req, reply) => { + try { + const parsed = BrainMissionTaskUpdateSchema.parse(req.body); + return await collections.updateMissionTask(req.params.id, req.params.taskId, parsed); + } catch (e: unknown) { + return reply.code(400).send({ error: String(e), code: 'VALIDATION_ERROR' }); + } + }); + + // === Computed Endpoints === + app.get('/v1/today', async (req) => { + const q = req.query as Record; + return collections.getToday(q['date']); + }); + app.get('/v1/stale', async (req) => { + const q = req.query as Record; + return collections.getStale(q['days'] ? Number(q['days']) : undefined); + }); + app.get('/v1/stats', async () => collections.getStats()); + app.get('/v1/search', async (req) => { + const q = req.query as Record; + if (!q['q']) return []; + return collections.search(q['q'], q['collection']); + }); + app.get('/v1/audit', async () => collections.audit()); +} diff --git a/packages/brain/src/schemas.ts b/packages/brain/src/schemas.ts new file mode 100644 index 0000000..b19642f --- /dev/null +++ b/packages/brain/src/schemas.ts @@ -0,0 +1,222 @@ +import { z } from 'zod'; + +// === Enums === + +export const BrainDomainSchema = z.enum([ + 'work', 'software-dev', 'homelab', 'family', 'marriage', + 'finances', 'fitness', 'music', 'home-improvement', 'woodworking', + 'home', 'consulting', 'personal', +]); + +export const TaskPrioritySchema = z.enum(['critical', 'high', 'medium', 'low']); + +export const BrainTaskStatusSchema = z.enum([ + 'backlog', 'scheduled', 'in-progress', 'blocked', 'done', 'cancelled', +]); + +export const BrainProjectStatusSchema = z.enum([ + 'planning', 'active', 'paused', 'blocked', 'completed', 'archived', +]); + +export const BrainAgentStatusSchema = z.enum(['active', 'idle', 'blocked', 'completed']); + +export const BrainEventTypeSchema = z.enum([ + 'meeting', 'deadline', 'maintenance', 'event', 'recurring', + 'milestone', 'task', 'constraint', 'client-work', 'appointment', + 'reminder', 'conflict', 'time-off', +]); + +export const BrainEventStatusSchema = z.enum([ + 'scheduled', 'confirmed', 'tentative', 'completed', 'cancelled', + 'done', 'blocked', 'postponed', 'deferred', 'in-progress', + 'pending-approval', 'canceled', 'needs-resolution', +]); + +export const BrainMissionStatusSchema = z.enum([ + 'planning', 'active', 'blocked', 'completed', 'cancelled', +]); + +// === Record Schemas === + +export const BrainTaskSchema = z.object({ + id: z.string().regex(/^[a-z0-9-]+$/), + title: z.string().min(1), + domain: BrainDomainSchema, + project: z.string().nullish(), + priority: TaskPrioritySchema, + status: BrainTaskStatusSchema, + progress: z.number().int().min(0).max(100).nullish(), + due: z.string().nullish(), + blocks: z.array(z.string()).optional().default([]), + blocked_by: z.array(z.string()).optional().default([]), + related: z.array(z.string()).optional().default([]), + canonical_source: z.string().nullish(), + assignee: z.string().nullish(), + created: z.string(), + updated: z.string(), + notes: z.string().nullish(), + notes_nontechnical: z.string().nullish(), +}); + +export const BrainProjectSchema = z.object({ + id: z.string().regex(/^[a-z0-9-]+$/), + name: z.string().min(1), + description: z.string().nullish(), + domain: BrainDomainSchema, + status: BrainProjectStatusSchema, + priority: z.number().int().min(1).max(10), + progress: z.number().int().min(0).max(100).nullish(), + repo: z.string().nullish(), + branch: z.string().nullish(), + current_milestone: z.string().nullish(), + next_milestone: z.string().nullish(), + blocker: z.string().nullish(), + owner: z.string().nullish(), + docs_path: z.string().nullish(), + created: z.string(), + updated: z.string(), + notes: z.string().nullish(), + notes_nontechnical: z.string().nullish(), +}); + +export const BrainEventSchema = z.object({ + id: z.string().regex(/^[a-z0-9-]+$/), + title: z.string().min(1), + date: z.string(), + end_date: z.string().nullish(), + time: z.string().nullish(), + end_time: z.string().nullish(), + domain: BrainDomainSchema, + type: BrainEventTypeSchema, + status: BrainEventStatusSchema.optional().default('scheduled'), + priority: TaskPrioritySchema.nullish(), + recur: z.boolean().nullish(), + recur_rate: z.string().nullish(), + recur_start: z.string().nullish(), + recur_end: z.string().nullish(), + location: z.string().nullish(), + project: z.string().nullish(), + related_task: z.string().nullish(), + related_tasks: z.array(z.string()).optional().default([]), + notes: z.string().nullish(), + gcal_id: z.string().nullish(), + ics_uid: z.string().nullish(), +}); + +export const BrainAgentSchema = z.object({ + id: z.string(), + project: z.string(), + focus: z.string().nullish(), + repo: z.string().nullish(), + branch: z.string().nullish(), + status: BrainAgentStatusSchema, + workload: z.enum(['light', 'medium', 'heavy']).nullish(), + next_step: z.string().nullish(), + blocker: z.string().nullish(), + updated: z.string(), +}); + +export const BrainTicketSchema = z.object({ + id: z.string(), + title: z.string().min(1), + status: z.number().int(), + priority: z.number().int(), + urgency: z.number().int(), + impact: z.number().int(), + date_creation: z.string(), + date_mod: z.string(), + content: z.string().nullish(), + assigned_to: z.string().nullish(), +}); + +export const BrainAppreciationSchema = z.object({ + date: z.string(), + from: z.enum(['jason', 'melanie']), + to: z.enum(['jason', 'melanie']), + text: z.string().min(1), +}); + +export const BrainMissionSchema = z.object({ + id: z.string(), + title: z.string().min(1), + project: z.string(), + prd_path: z.string().nullish(), + status: BrainMissionStatusSchema, + created: z.string(), + updated: z.string(), + notes: z.string().nullish(), +}); + +export const BrainMissionTaskSchema = z.object({ + id: z.string(), + mission_id: z.string(), + title: z.string().min(1), + phase: z.string().nullish(), + status: BrainTaskStatusSchema, + priority: TaskPrioritySchema, + dependencies: z.array(z.string()).default([]), + assigned_to: z.string().nullish(), + pr: z.string().nullish(), + order: z.number().int(), + created: z.string(), + updated: z.string(), + completed_at: z.string().nullish(), + notes: z.string().nullish(), +}); + +// === Partial schemas for updates === + +export const BrainTaskUpdateSchema = BrainTaskSchema.partial().omit({ id: true }); +export const BrainProjectUpdateSchema = BrainProjectSchema.partial().omit({ id: true }); +export const BrainEventUpdateSchema = BrainEventSchema.partial().omit({ id: true }); +export const BrainAgentUpdateSchema = BrainAgentSchema.partial().omit({ id: true }); +export const BrainMissionUpdateSchema = BrainMissionSchema.partial().omit({ id: true }); +export const BrainMissionTaskUpdateSchema = BrainMissionTaskSchema.partial().omit({ id: true, mission_id: true }); + +// === Collection file schemas === + +export const TasksFileSchema = z.object({ + version: z.string(), + tasks: z.array(BrainTaskSchema), +}); + +export const ProjectsFileSchema = z.object({ + version: z.string(), + projects: z.array(BrainProjectSchema), +}); + +export const EventsFileSchema = z.object({ + version: z.string(), + domain: BrainDomainSchema, + events: z.array(BrainEventSchema), + overdue: z.array(z.object({ + id: z.string(), + title: z.string(), + original_due: z.string().optional(), + domain: z.string(), + notes: z.string().nullish(), + })).default([]), +}); + +export const AgentsFileSchema = z.object({ + version: z.string(), + agents: z.array(BrainAgentSchema), +}); + +export const MissionsFileSchema = z.object({ + version: z.string(), + missions: z.array(BrainMissionSchema), +}); + +export const MissionTasksFileSchema = z.object({ + version: z.string(), + mission_tasks: z.array(BrainMissionTaskSchema), +}); + +// Inferred types for convenience +export type BrainTaskInput = z.input; +export type BrainProjectInput = z.input; +export type BrainEventInput = z.input; +export type BrainAgentInput = z.input; +export type BrainMissionInput = z.input; +export type BrainMissionTaskInput = z.input; diff --git a/packages/brain/src/storage/collections.ts b/packages/brain/src/storage/collections.ts new file mode 100644 index 0000000..a7792a7 --- /dev/null +++ b/packages/brain/src/storage/collections.ts @@ -0,0 +1,536 @@ +import type { + BrainTask, BrainProject, BrainEvent, BrainAgent, + BrainTicket, BrainAppreciation, BrainMission, BrainMissionTask, + BrainTaskFilters, BrainProjectFilters, BrainEventFilters, + BrainMissionFilters, BrainMissionTaskFilters, + BrainMissionSummary, BrainTodaySummary, BrainStats, + BrainStaleReport, BrainAuditResult, BrainSearchResult, + TaskPriority, +} from '@mosaic/types'; +import { JsonStore } from './json-store.js'; + +function today(): string { + return new Date().toISOString().slice(0, 10); +} + +interface TasksFile { version: string; tasks: BrainTask[] } +interface ProjectsFile { version: string; projects: BrainProject[] } +interface EventsFile { version: string; events: BrainEvent[] } +interface AgentsFile { version: string; agents: BrainAgent[] } +interface TicketsFile { version: string; tickets: BrainTicket[] } +interface AppreciationsFile { version: string; appreciations: BrainAppreciation[] } +interface MissionsFile { version: string; missions: BrainMission[] } +interface MissionTasksFile { version: string; mission_tasks: BrainMissionTask[] } + +const DEFAULT_TASKS: TasksFile = { version: '2.0', tasks: [] }; +const DEFAULT_PROJECTS: ProjectsFile = { version: '2.0', projects: [] }; +const DEFAULT_EVENTS: EventsFile = { version: '2.0', events: [] }; +const DEFAULT_AGENTS: AgentsFile = { version: '2.0', agents: [] }; +const DEFAULT_TICKETS: TicketsFile = { version: '2.0', tickets: [] }; +const DEFAULT_APPRECIATIONS: AppreciationsFile = { version: '2.0', appreciations: [] }; +const DEFAULT_MISSIONS: MissionsFile = { version: '2.0', missions: [] }; +const DEFAULT_MISSION_TASKS: MissionTasksFile = { version: '2.0', mission_tasks: [] }; + +const PRIORITY_ORDER: Record = { critical: 0, high: 1, medium: 2, low: 3 }; + +function matchesFilter(item: T, filters: object): boolean { + for (const [key, value] of Object.entries(filters as Record)) { + if (value === undefined || value === null) continue; + if (key === 'limit') continue; + if (key === 'due_before') { + if (!(item as Record)['due'] || (item as Record)['due']! > value) return false; + continue; + } + if (key === 'due_after') { + if (!(item as Record)['due'] || (item as Record)['due']! < value) return false; + continue; + } + if (key === 'date_from') { + if ((item as Record)['date']! < value) return false; + continue; + } + if (key === 'date_to') { + if ((item as Record)['date']! > value) return false; + continue; + } + if (key === 'priority_min') { + if (((item as Record)['priority'] as number) < (value as number)) return false; + continue; + } + if (key === 'priority_max') { + if (((item as Record)['priority'] as number) > (value as number)) return false; + continue; + } + if ((item as Record)[key] !== value) return false; + } + return true; +} + +function applyLimit(items: T[], limit?: number): T[] { + return limit != null && limit > 0 ? items.slice(0, limit) : items; +} + +export class Collections { + constructor(private readonly store: JsonStore) {} + + // === Tasks === + + async getTasks(filters: BrainTaskFilters = {}): Promise { + const file = await this.store.read('tasks', DEFAULT_TASKS); + const filtered = file.tasks.filter(t => matchesFilter(t, filters)); + filtered.sort((a, b) => + (PRIORITY_ORDER[a.priority] ?? 9) - (PRIORITY_ORDER[b.priority] ?? 9) + || (a.due ?? '9999').localeCompare(b.due ?? '9999'), + ); + return applyLimit(filtered, filters.limit); + } + + async getTask(id: string): Promise { + const file = await this.store.read('tasks', DEFAULT_TASKS); + return file.tasks.find(t => t.id === id) ?? null; + } + + async addTask(task: BrainTask): Promise { + const result = await this.store.modify('tasks', DEFAULT_TASKS, (file) => { + if (file.tasks.some(t => t.id === task.id)) { + throw new Error(`Task '${task.id}' already exists`); + } + return { ...file, tasks: [...file.tasks, task] }; + }); + return result.tasks.find(t => t.id === task.id)!; + } + + async updateTask(id: string, updates: Partial): Promise { + let updated: BrainTask | undefined; + await this.store.modify('tasks', DEFAULT_TASKS, (file) => { + const idx = file.tasks.findIndex(t => t.id === id); + if (idx === -1) throw new Error(`Task '${id}' not found`); + const tasks = [...file.tasks]; + updated = { ...tasks[idx]!, ...updates, id, updated: today() } as BrainTask; + tasks[idx] = updated; + return { ...file, tasks }; + }); + return updated!; + } + + // === Projects === + + async getProjects(filters: BrainProjectFilters = {}): Promise { + const file = await this.store.read('projects', DEFAULT_PROJECTS); + const filtered = file.projects.filter(p => matchesFilter(p, filters)); + filtered.sort((a, b) => a.priority - b.priority); + return applyLimit(filtered, filters.limit); + } + + async getProject(id: string): Promise { + const file = await this.store.read('projects', DEFAULT_PROJECTS); + return file.projects.find(p => p.id === id) ?? null; + } + + async addProject(project: BrainProject): Promise { + const result = await this.store.modify('projects', DEFAULT_PROJECTS, (file) => { + if (file.projects.some(p => p.id === project.id)) { + throw new Error(`Project '${project.id}' already exists`); + } + return { ...file, projects: [...file.projects, project] }; + }); + return result.projects.find(p => p.id === project.id)!; + } + + async updateProject(id: string, updates: Partial): Promise { + let updated: BrainProject | undefined; + await this.store.modify('projects', DEFAULT_PROJECTS, (file) => { + const idx = file.projects.findIndex(p => p.id === id); + if (idx === -1) throw new Error(`Project '${id}' not found`); + const projects = [...file.projects]; + updated = { ...projects[idx]!, ...updates, id, updated: today() } as BrainProject; + projects[idx] = updated; + return { ...file, projects }; + }); + return updated!; + } + + // === Events === + + async getEvents(filters: BrainEventFilters = {}): Promise { + const file = await this.store.read('events', DEFAULT_EVENTS); + const filtered = file.events.filter(e => matchesFilter(e, filters)); + filtered.sort((a, b) => a.date.localeCompare(b.date) || (a.time ?? '').localeCompare(b.time ?? '')); + return applyLimit(filtered, filters.limit); + } + + async getEvent(id: string): Promise { + const file = await this.store.read('events', DEFAULT_EVENTS); + return file.events.find(e => e.id === id) ?? null; + } + + async addEvent(event: BrainEvent): Promise { + const result = await this.store.modify('events', DEFAULT_EVENTS, (file) => { + if (file.events.some(e => e.id === event.id)) { + throw new Error(`Event '${event.id}' already exists`); + } + return { ...file, events: [...file.events, event] }; + }); + return result.events.find(e => e.id === event.id)!; + } + + async updateEvent(id: string, updates: Partial): Promise { + let updated: BrainEvent | undefined; + await this.store.modify('events', DEFAULT_EVENTS, (file) => { + const idx = file.events.findIndex(e => e.id === id); + if (idx === -1) throw new Error(`Event '${id}' not found`); + const events = [...file.events]; + updated = { ...events[idx]!, ...updates, id } as BrainEvent; + events[idx] = updated; + return { ...file, events }; + }); + return updated!; + } + + // === Agents === + + async getAgents(filters: { status?: string; project?: string } = {}): Promise { + const file = await this.store.read('agents', DEFAULT_AGENTS); + return file.agents.filter(a => matchesFilter(a, filters)); + } + + async updateAgent(id: string, updates: Partial): Promise { + let updated: BrainAgent | undefined; + await this.store.modify('agents', DEFAULT_AGENTS, (file) => { + const idx = file.agents.findIndex(a => a.id === id); + if (idx === -1) { + // Auto-create agent entries + updated = { id, project: '', status: 'active', updated: new Date().toISOString(), ...updates } as BrainAgent; + return { ...file, agents: [...file.agents, updated] }; + } + const agents = [...file.agents]; + updated = { ...agents[idx]!, ...updates, id, updated: new Date().toISOString() } as BrainAgent; + agents[idx] = updated; + return { ...file, agents }; + }); + return updated!; + } + + // === Tickets === + + async getTickets(filters: { status?: number; priority?: number; limit?: number } = {}): Promise { + const file = await this.store.read('tickets', DEFAULT_TICKETS); + let filtered = file.tickets; + if (filters.status !== undefined) filtered = filtered.filter(t => t.status === filters.status); + if (filters.priority !== undefined) filtered = filtered.filter(t => t.priority === filters.priority); + return applyLimit(filtered, filters.limit); + } + + // === Appreciations === + + async getAppreciations(): Promise { + const file = await this.store.read('appreciations', DEFAULT_APPRECIATIONS); + return file.appreciations; + } + + async addAppreciation(appreciation: BrainAppreciation): Promise { + await this.store.modify('appreciations', DEFAULT_APPRECIATIONS, (file) => ({ + ...file, + appreciations: [...file.appreciations, appreciation], + })); + return appreciation; + } + + // === Missions === + + async getMissions(filters: BrainMissionFilters = {}): Promise { + const file = await this.store.read('missions', DEFAULT_MISSIONS); + const filtered = file.missions.filter(m => matchesFilter(m, filters)); + return applyLimit(filtered, filters.limit); + } + + async getMission(id: string): Promise { + const file = await this.store.read('missions', DEFAULT_MISSIONS); + return file.missions.find(m => m.id === id) ?? null; + } + + async addMission(mission: BrainMission): Promise { + const result = await this.store.modify('missions', DEFAULT_MISSIONS, (file) => { + if (file.missions.some(m => m.id === mission.id)) { + throw new Error(`Mission '${mission.id}' already exists`); + } + return { ...file, missions: [...file.missions, mission] }; + }); + return result.missions.find(m => m.id === mission.id)!; + } + + async updateMission(id: string, updates: Partial): Promise { + let updated: BrainMission | undefined; + await this.store.modify('missions', DEFAULT_MISSIONS, (file) => { + const idx = file.missions.findIndex(m => m.id === id); + if (idx === -1) throw new Error(`Mission '${id}' not found`); + const missions = [...file.missions]; + updated = { ...missions[idx]!, ...updates, id, updated: today() } as BrainMission; + missions[idx] = updated; + return { ...file, missions }; + }); + return updated!; + } + + async getMissionSummary(id: string): Promise { + const mission = await this.getMission(id); + if (!mission) return null; + + const tasks = await this.getMissionTasks({ mission_id: id }); + const completed = tasks.filter(t => t.status === 'done'); + const completedIds = new Set(completed.map(t => t.id)); + + const nextAvailable = tasks.filter(t => + t.status !== 'done' && t.status !== 'cancelled' && t.status !== 'blocked' + && t.dependencies.every(dep => completedIds.has(dep)) + && !t.assigned_to, + ); + + const blockedTasks = tasks.filter(t => + t.status === 'blocked' + || (t.status !== 'done' && t.status !== 'cancelled' + && !t.dependencies.every(dep => completedIds.has(dep))), + ); + + return { + ...mission, + task_count: tasks.length, + completed_count: completed.length, + progress: tasks.length > 0 ? Math.round((completed.length / tasks.length) * 100) : 0, + next_available: nextAvailable, + blocked_tasks: blockedTasks, + }; + } + + // === Mission Tasks === + + async getMissionTasks(filters: BrainMissionTaskFilters): Promise { + const file = await this.store.read('mission_tasks', DEFAULT_MISSION_TASKS); + const filtered = file.mission_tasks.filter(t => matchesFilter(t, filters)); + filtered.sort((a, b) => a.order - b.order); + return filtered; + } + + async addMissionTask(task: BrainMissionTask): Promise { + const result = await this.store.modify('mission_tasks', DEFAULT_MISSION_TASKS, (file) => { + if (file.mission_tasks.some(t => t.id === task.id && t.mission_id === task.mission_id)) { + throw new Error(`Mission task '${task.id}' already exists in mission '${task.mission_id}'`); + } + return { ...file, mission_tasks: [...file.mission_tasks, task] }; + }); + return result.mission_tasks.find(t => t.id === task.id && t.mission_id === task.mission_id)!; + } + + async updateMissionTask(missionId: string, taskId: string, updates: Partial): Promise { + let updated: BrainMissionTask | undefined; + await this.store.modify('mission_tasks', DEFAULT_MISSION_TASKS, (file) => { + const idx = file.mission_tasks.findIndex(t => t.id === taskId && t.mission_id === missionId); + if (idx === -1) throw new Error(`Mission task '${taskId}' not found in mission '${missionId}'`); + const tasks = [...file.mission_tasks]; + updated = { ...tasks[idx]!, ...updates, id: taskId, mission_id: missionId, updated: today() } as BrainMissionTask; + if (updates.status === 'done' && !updated.completed_at) { + updated = { ...updated, completed_at: new Date().toISOString() }; + } + tasks[idx] = updated; + return { ...file, mission_tasks: tasks }; + }); + return updated!; + } + + // === Computed: Today === + + async getToday(date?: string): Promise { + const d = date ?? today(); + const weekFromNow = new Date(Date.now() + 7 * 86400000).toISOString().slice(0, 10); + const staleCutoff = new Date(Date.now() - 7 * 86400000).toISOString().slice(0, 10); + + const [tasks, projects, events, agents, missions] = await Promise.all([ + this.getTasks(), + this.getProjects(), + this.getEvents(), + this.getAgents(), + this.getMissions({ status: 'active' }), + ]); + + const activeTasks = tasks.filter(t => t.status !== 'done' && t.status !== 'cancelled'); + const eventsToday = events.filter(e => e.date === d); + const eventsUpcoming = events.filter(e => e.date > d && e.date <= weekFromNow); + const tasksNearTerm = activeTasks.filter(t => t.due && t.due >= d && t.due <= weekFromNow); + const tasksBlocked = activeTasks.filter(t => t.status === 'blocked'); + const tasksStale = activeTasks.filter(t => t.status === 'in-progress' && t.updated < staleCutoff); + const tasksAlmostDone = activeTasks.filter(t => t.progress != null && t.progress >= 80); + + const missionSummaries: BrainMissionSummary[] = []; + for (const m of missions) { + const summary = await this.getMissionSummary(m.id); + if (summary) missionSummaries.push(summary); + } + + const tasksByStatus: Record = {}; + const tasksByDomain: Record = {}; + for (const t of tasks) { + tasksByStatus[t.status] = (tasksByStatus[t.status] ?? 0) + 1; + tasksByDomain[t.domain] = (tasksByDomain[t.domain] ?? 0) + 1; + } + const projectsByStatus: Record = {}; + for (const p of projects) { + projectsByStatus[p.status] = (projectsByStatus[p.status] ?? 0) + 1; + } + + return { + date: d, + events_today: eventsToday, + events_upcoming: eventsUpcoming, + tasks_near_term: tasksNearTerm, + tasks_blocked: tasksBlocked, + tasks_stale: tasksStale, + tasks_almost_done: tasksAlmostDone, + active_missions: missionSummaries, + stats: { + tasks: tasks.length, + projects: projects.length, + events: events.length, + agents: agents.length, + tickets: (await this.getTickets()).length, + missions: (await this.getMissions()).length, + tasks_by_status: tasksByStatus, + tasks_by_domain: tasksByDomain, + projects_by_status: projectsByStatus, + }, + }; + } + + // === Computed: Stale === + + async getStale(days: number = 7): Promise { + const cutoff = new Date(Date.now() - days * 86400000).toISOString().slice(0, 10); + const tasks = await this.getTasks(); + const projects = await this.getProjects(); + + return { + days, + tasks: tasks.filter(t => t.status === 'in-progress' && t.updated < cutoff), + projects: projects.filter(p => p.status === 'active' && p.updated < cutoff), + }; + } + + // === Computed: Stats === + + async getStats(): Promise { + const [tasks, projects, events, agents, tickets, missions] = await Promise.all([ + this.getTasks(), + this.getProjects(), + this.getEvents(), + this.getAgents(), + this.getTickets(), + this.getMissions(), + ]); + + const tasksByStatus: Record = {}; + const tasksByDomain: Record = {}; + for (const t of tasks) { + tasksByStatus[t.status] = (tasksByStatus[t.status] ?? 0) + 1; + tasksByDomain[t.domain] = (tasksByDomain[t.domain] ?? 0) + 1; + } + const projectsByStatus: Record = {}; + for (const p of projects) { + projectsByStatus[p.status] = (projectsByStatus[p.status] ?? 0) + 1; + } + + return { + tasks: tasks.length, + projects: projects.length, + events: events.length, + agents: agents.length, + tickets: tickets.length, + missions: missions.length, + tasks_by_status: tasksByStatus, + tasks_by_domain: tasksByDomain, + projects_by_status: projectsByStatus, + }; + } + + // === Computed: Search === + + async search(query: string, collection?: string): Promise { + const q = query.toLowerCase(); + const results: BrainSearchResult[] = []; + + const searchIn = (items: Array<{ id: string; title: string; notes?: string | null }>, collectionName: string) => { + for (const item of items) { + const titleMatch = item.title.toLowerCase().includes(q); + const notesMatch = item.notes?.toLowerCase().includes(q); + if (titleMatch || notesMatch) { + results.push({ + collection: collectionName, + id: item.id, + title: item.title, + match_context: titleMatch ? item.title : (item.notes?.substring(0, 200) ?? ''), + score: titleMatch ? 1.0 : 0.5, + }); + } + } + }; + + if (!collection || collection === 'tasks') searchIn(await this.getTasks(), 'tasks'); + if (!collection || collection === 'projects') { + const projects = await this.getProjects(); + for (const p of projects) { + const titleMatch = p.name.toLowerCase().includes(q); + const notesMatch = p.notes?.toLowerCase().includes(q); + if (titleMatch || notesMatch) { + results.push({ + collection: 'projects', + id: p.id, + title: p.name, + match_context: titleMatch ? p.name : (p.notes?.substring(0, 200) ?? ''), + score: titleMatch ? 1.0 : 0.5, + }); + } + } + } + if (!collection || collection === 'events') searchIn(await this.getEvents(), 'events'); + if (!collection || collection === 'missions') searchIn(await this.getMissions(), 'missions'); + + results.sort((a, b) => b.score - a.score); + return results; + } + + // === Computed: Audit === + + async audit(): Promise { + const tasks = await this.getTasks(); + const projects = await this.getProjects(); + const taskIds = new Set(tasks.map(t => t.id)); + const projectIds = new Set(projects.map(p => p.id)); + const allIds = new Set([...taskIds, ...projectIds]); + + const orphanRefs: string[] = []; + const brokenDependencies: string[] = []; + const missingRequiredFields: string[] = []; + const duplicateIds: string[] = []; + + // Check for duplicate task IDs + const seenTaskIds = new Set(); + for (const t of tasks) { + if (seenTaskIds.has(t.id)) duplicateIds.push(`task:${t.id}`); + seenTaskIds.add(t.id); + } + + // Check task references + for (const t of tasks) { + if (t.project && !projectIds.has(t.project)) { + orphanRefs.push(`task:${t.id} -> project:${t.project}`); + } + for (const dep of t.blocked_by ?? []) { + if (!allIds.has(dep)) brokenDependencies.push(`task:${t.id} blocked_by:${dep}`); + } + for (const dep of t.blocks ?? []) { + if (!allIds.has(dep)) brokenDependencies.push(`task:${t.id} blocks:${dep}`); + } + if (!t.title) missingRequiredFields.push(`task:${t.id} missing title`); + } + + return { orphan_refs: orphanRefs, broken_dependencies: brokenDependencies, missing_required_fields: missingRequiredFields, duplicate_ids: duplicateIds }; + } +} diff --git a/packages/brain/src/storage/json-store.ts b/packages/brain/src/storage/json-store.ts new file mode 100644 index 0000000..68ec4eb --- /dev/null +++ b/packages/brain/src/storage/json-store.ts @@ -0,0 +1,156 @@ +import { readFile, writeFile, mkdir } from 'node:fs/promises'; +import { existsSync } from 'node:fs'; +import { join, dirname } from 'node:path'; +import * as lockfile from 'proper-lockfile'; + +export interface JsonStoreOptions { + dataDir: string; +} + +const LOCK_OPTIONS = { retries: { retries: 5, minTimeout: 100 } }; + +/** + * JSON file-backed storage with file locking for concurrent access. + * One file per collection. In-memory cache for fast reads. + * + * Known constraint: Cache TTL means multi-instance deployments against + * the same data directory may see stale reads for up to CACHE_TTL_MS. + * Single-instance is the supported topology for Phase 1. + */ +export class JsonStore { + private readonly dataDir: string; + private readonly cache = new Map(); + private static readonly CACHE_TTL_MS = 5_000; + + constructor(options: JsonStoreOptions) { + this.dataDir = options.dataDir; + } + + async init(): Promise { + if (!existsSync(this.dataDir)) { + await mkdir(this.dataDir, { recursive: true }); + } + } + + private filePath(collection: string): string { + return join(this.dataDir, `${collection}.json`); + } + + private isCacheValid(collection: string): boolean { + const cached = this.cache.get(collection); + if (!cached) return false; + return Date.now() - cached.loadedAt < JsonStore.CACHE_TTL_MS; + } + + /** + * Ensure a file exists for locking. Uses a separate lock on the + * directory to prevent race conditions during file creation. + */ + private async ensureFile(path: string, defaultContent: string): Promise { + const dir = dirname(path); + if (!existsSync(dir)) { + await mkdir(dir, { recursive: true }); + } + if (!existsSync(path)) { + // Lock the directory to serialize file creation + let releaseDir: (() => Promise) | undefined; + try { + releaseDir = await lockfile.lock(dir, LOCK_OPTIONS); + // Double-check after acquiring lock + if (!existsSync(path)) { + await writeFile(path, defaultContent, 'utf-8'); + } + } finally { + if (releaseDir) await releaseDir(); + } + } + } + + /** + * Read a collection file. Returns parsed JSON or default if file doesn't exist. + */ + async read(collection: string, defaultValue: T): Promise { + if (this.isCacheValid(collection)) { + return this.cache.get(collection)!.data as T; + } + + const path = this.filePath(collection); + if (!existsSync(path)) { + this.cache.set(collection, { data: defaultValue, loadedAt: Date.now() }); + return defaultValue; + } + + try { + const raw = await readFile(path, 'utf-8'); + const data = JSON.parse(raw) as T; + this.cache.set(collection, { data, loadedAt: Date.now() }); + return data; + } catch { + return defaultValue; + } + } + + /** + * Write a collection file with file locking. + */ + async write(collection: string, data: unknown): Promise { + const path = this.filePath(collection); + await this.ensureFile(path, '{}'); + + let release: (() => Promise) | undefined; + try { + release = await lockfile.lock(path, LOCK_OPTIONS); + await writeFile(path, JSON.stringify(data, null, 2) + '\n', 'utf-8'); + this.cache.set(collection, { data, loadedAt: Date.now() }); + } finally { + if (release) await release(); + } + } + + /** + * Read-modify-write with file locking. The modifier function receives + * the current data and returns the updated data. + */ + async modify( + collection: string, + defaultValue: T, + modifier: (current: T) => T | Promise, + ): Promise { + const path = this.filePath(collection); + await this.ensureFile(path, JSON.stringify(defaultValue, null, 2) + '\n'); + + let release: (() => Promise) | undefined; + try { + release = await lockfile.lock(path, LOCK_OPTIONS); + + let current: T; + try { + const raw = await readFile(path, 'utf-8'); + current = JSON.parse(raw) as T; + } catch { + current = defaultValue; + } + + const updated = await modifier(current); + await writeFile(path, JSON.stringify(updated, null, 2) + '\n', 'utf-8'); + this.cache.set(collection, { data: updated, loadedAt: Date.now() }); + return updated; + } finally { + if (release) await release(); + } + } + + /** + * Invalidate cache for a collection. + */ + invalidate(collection: string): void { + this.cache.delete(collection); + } + + /** + * Invalidate all cached data. + */ + invalidateAll(): void { + this.cache.clear(); + } +} diff --git a/packages/brain/tsconfig.json b/packages/brain/tsconfig.json new file mode 100644 index 0000000..5285d28 --- /dev/null +++ b/packages/brain/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 2221509..ad33ca6 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -340,3 +340,315 @@ export interface WizardState { readonly runtimes: RuntimeState; readonly selectedSkills: readonly string[]; } + +// === Brain (Structured Data) === + +export type BrainDomain = + | 'work' + | 'software-dev' + | 'homelab' + | 'family' + | 'marriage' + | 'finances' + | 'fitness' + | 'music' + | 'home-improvement' + | 'woodworking' + | 'home' + | 'consulting' + | 'personal'; + +export type BrainTaskStatus = + | 'backlog' + | 'scheduled' + | 'in-progress' + | 'blocked' + | 'done' + | 'cancelled'; + +export type BrainProjectStatus = + | 'planning' + | 'active' + | 'paused' + | 'blocked' + | 'completed' + | 'archived'; + +export type BrainAgentStatus = 'active' | 'idle' | 'blocked' | 'completed'; + +export type BrainEventType = + | 'meeting' + | 'deadline' + | 'maintenance' + | 'event' + | 'recurring' + | 'milestone' + | 'task' + | 'constraint' + | 'client-work' + | 'appointment' + | 'reminder' + | 'conflict' + | 'time-off'; + +export type BrainEventStatus = + | 'scheduled' + | 'confirmed' + | 'tentative' + | 'completed' + | 'cancelled' + | 'done' + | 'blocked' + | 'postponed' + | 'deferred' + | 'in-progress' + | 'pending-approval' + | 'canceled' + | 'needs-resolution'; + +export type BrainMissionStatus = + | 'planning' + | 'active' + | 'blocked' + | 'completed' + | 'cancelled'; + +// --- Brain: Task --- + +export interface BrainTask { + readonly id: string; + readonly title: string; + readonly domain: BrainDomain; + readonly project?: string | null; + readonly priority: TaskPriority; + readonly status: BrainTaskStatus; + readonly progress?: number | null; + readonly due?: string | null; + readonly blocks?: readonly string[]; + readonly blocked_by?: readonly string[]; + readonly related?: readonly string[]; + readonly canonical_source?: string | null; + readonly assignee?: string | null; + readonly created: string; + readonly updated: string; + readonly notes?: string | null; + readonly notes_nontechnical?: string | null; +} + +// --- Brain: Project --- + +export interface BrainProject { + readonly id: string; + readonly name: string; + readonly description?: string | null; + readonly domain: BrainDomain; + readonly status: BrainProjectStatus; + readonly priority: number; + readonly progress?: number | null; + readonly repo?: string | null; + readonly branch?: string | null; + readonly current_milestone?: string | null; + readonly next_milestone?: string | null; + readonly blocker?: string | null; + readonly owner?: string | null; + readonly docs_path?: string | null; + readonly created: string; + readonly updated: string; + readonly notes?: string | null; + readonly notes_nontechnical?: string | null; +} + +// --- Brain: Event --- + +export interface BrainEvent { + readonly id: string; + readonly title: string; + readonly date: string; + readonly end_date?: string | null; + readonly time?: string | null; + readonly end_time?: string | null; + readonly domain: BrainDomain; + readonly type: BrainEventType; + readonly status?: BrainEventStatus; + readonly priority?: TaskPriority | null; + readonly recur?: boolean | null; + readonly recur_rate?: string | null; + readonly recur_start?: string | null; + readonly recur_end?: string | null; + readonly location?: string | null; + readonly project?: string | null; + readonly related_task?: string | null; + readonly related_tasks?: readonly string[]; + readonly notes?: string | null; + readonly gcal_id?: string | null; + readonly ics_uid?: string | null; +} + +// --- Brain: Agent --- + +export interface BrainAgent { + readonly id: string; + readonly project: string; + readonly focus?: string | null; + readonly repo?: string | null; + readonly branch?: string | null; + readonly status: BrainAgentStatus; + readonly workload?: 'light' | 'medium' | 'heavy' | null; + readonly next_step?: string | null; + readonly blocker?: string | null; + readonly updated: string; +} + +// --- Brain: Ticket --- + +export interface BrainTicket { + readonly id: string; + readonly title: string; + readonly status: number; + readonly priority: number; + readonly urgency: number; + readonly impact: number; + readonly date_creation: string; + readonly date_mod: string; + readonly content?: string | null; + readonly assigned_to?: string | null; +} + +// --- Brain: Appreciation --- + +export interface BrainAppreciation { + readonly date: string; + readonly from: 'jason' | 'melanie'; + readonly to: 'jason' | 'melanie'; + readonly text: string; +} + +// --- Brain: Mission --- + +export interface BrainMission { + readonly id: string; + readonly title: string; + readonly project: string; + readonly prd_path?: string | null; + readonly status: BrainMissionStatus; + readonly created: string; + readonly updated: string; + readonly notes?: string | null; +} + +// --- Brain: Mission Task --- + +export interface BrainMissionTask { + readonly id: string; + readonly mission_id: string; + readonly title: string; + readonly phase?: string | null; + readonly status: BrainTaskStatus; + readonly priority: TaskPriority; + readonly dependencies: readonly string[]; + readonly assigned_to?: string | null; + readonly pr?: string | null; + readonly order: number; + readonly created: string; + readonly updated: string; + readonly completed_at?: string | null; + readonly notes?: string | null; +} + +// --- Brain: Filter/Query Types --- + +export interface BrainTaskFilters { + readonly status?: BrainTaskStatus; + readonly priority?: TaskPriority; + readonly domain?: BrainDomain; + readonly project?: string; + readonly due_before?: string; + readonly due_after?: string; + readonly assignee?: string; + readonly limit?: number; +} + +export interface BrainProjectFilters { + readonly status?: BrainProjectStatus; + readonly domain?: BrainDomain; + readonly priority_min?: number; + readonly priority_max?: number; + readonly limit?: number; +} + +export interface BrainEventFilters { + readonly date_from?: string; + readonly date_to?: string; + readonly domain?: BrainDomain; + readonly type?: BrainEventType; + readonly status?: BrainEventStatus; + readonly limit?: number; +} + +export interface BrainMissionFilters { + readonly status?: BrainMissionStatus; + readonly project?: string; + readonly limit?: number; +} + +export interface BrainMissionTaskFilters { + readonly mission_id: string; + readonly status?: BrainTaskStatus; + readonly phase?: string; + readonly priority?: TaskPriority; +} + +// --- Brain: Computed Response Types --- + +export interface BrainMissionSummary extends BrainMission { + readonly task_count: number; + readonly completed_count: number; + readonly progress: number; + readonly next_available: readonly BrainMissionTask[]; + readonly blocked_tasks: readonly BrainMissionTask[]; +} + +export interface BrainTodaySummary { + readonly date: string; + readonly events_today: readonly BrainEvent[]; + readonly events_upcoming: readonly BrainEvent[]; + readonly tasks_near_term: readonly BrainTask[]; + readonly tasks_blocked: readonly BrainTask[]; + readonly tasks_stale: readonly BrainTask[]; + readonly tasks_almost_done: readonly BrainTask[]; + readonly active_missions: readonly BrainMissionSummary[]; + readonly stats: BrainStats; +} + +export interface BrainStats { + readonly tasks: number; + readonly projects: number; + readonly events: number; + readonly agents: number; + readonly tickets: number; + readonly missions: number; + readonly tasks_by_status: Readonly>; + readonly tasks_by_domain: Readonly>; + readonly projects_by_status: Readonly>; +} + +export interface BrainStaleReport { + readonly days: number; + readonly tasks: readonly BrainTask[]; + readonly projects: readonly BrainProject[]; +} + +export interface BrainAuditResult { + readonly orphan_refs: readonly string[]; + readonly broken_dependencies: readonly string[]; + readonly missing_required_fields: readonly string[]; + readonly duplicate_ids: readonly string[]; +} + +export interface BrainSearchResult { + readonly collection: string; + readonly id: string; + readonly title: string; + readonly match_context: string; + readonly score: number; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d4f57ce..f25010d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,40 @@ importers: specifier: ^5 version: 5.9.3 + packages/brain: + dependencies: + '@modelcontextprotocol/sdk': + specifier: ^1.12.1 + version: 1.27.1(zod@3.25.76) + '@mosaic/types': + specifier: workspace:* + version: link:../types + fastify: + specifier: ^5.3.3 + version: 5.8.2 + proper-lockfile: + specifier: ^4.1.2 + version: 4.1.2 + zod: + specifier: ^3.24.4 + version: 3.25.76 + devDependencies: + '@types/node': + specifier: ^22 + version: 22.19.15 + '@types/proper-lockfile': + specifier: ^4 + version: 4.1.4 + tsx: + specifier: ^4 + version: 4.21.0 + typescript: + specifier: ^5 + version: 5.9.3 + vitest: + specifier: ^3 + version: 3.2.4(@types/node@22.19.15)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + packages/cli: dependencies: '@mosaic/coord': @@ -655,6 +689,24 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@fastify/ajv-compiler@4.0.5': + resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==} + + '@fastify/error@4.2.0': + resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} + + '@fastify/fast-json-stringify-compiler@5.0.3': + resolution: {integrity: sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==} + + '@fastify/forwarded@3.0.1': + resolution: {integrity: sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==} + + '@fastify/merge-json-schemas@0.2.1': + resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + + '@fastify/proxy-addr@5.1.0': + resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} + '@hono/node-server@1.19.11': resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} engines: {node: '>=18.14.1'} @@ -736,6 +788,9 @@ packages: '@oxc-project/types@0.115.0': resolution: {integrity: sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==} + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@quansync/fs@1.0.0': resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} @@ -980,6 +1035,12 @@ packages: '@types/node@22.19.15': resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==} + '@types/proper-lockfile@4.1.4': + resolution: {integrity: sha512-uo2ABllncSqg9F1D4nugVl9v93RmjxF6LJzQLMLDdPaXCUIDPeOJ21Gbqi43xNKzBi/WQ0Q0dICqufzQbMjipQ==} + + '@types/retry@0.12.5': + resolution: {integrity: sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==} + '@typescript-eslint/eslint-plugin@8.56.1': resolution: {integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1097,6 +1158,9 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -1159,6 +1223,13 @@ packages: resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==} engines: {node: '>=20.19.0'} + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + avvio@9.2.0: + resolution: {integrity: sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1265,6 +1336,10 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + cors@2.8.6: resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} engines: {node: '>= 0.10'} @@ -1300,6 +1375,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -1455,6 +1534,9 @@ packages: extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1465,12 +1547,21 @@ packages: fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-json-stringify@6.3.0: + resolution: {integrity: sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA==} + fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fastify@5.8.2: + resolution: {integrity: sha512-lZmt3navvZG915IE+f7/TIVamxIwmBd+OMB+O9WBzcpIwOo6F0LTh0sluoMFk5VkrKTvvrwIaoJPkir4Z+jtAg==} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -1495,6 +1586,10 @@ packages: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} + find-my-way@9.5.0: + resolution: {integrity: sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ==} + engines: {node: '>=20'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -1630,6 +1725,10 @@ packages: 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'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1682,6 +1781,9 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-schema-ref-resolver@3.0.0: + resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -1704,6 +1806,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + light-my-request@6.6.0: + resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} + locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -1792,6 +1897,10 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + on-exit-leak-free@2.1.2: + 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'} @@ -1885,6 +1994,16 @@ packages: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.3.1: + resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} + hasBin: true + pkce-challenge@5.0.1: resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} engines: {node: '>=16.20.0'} @@ -1907,6 +2026,15 @@ packages: engines: {node: '>=14'} hasBin: true + process-warning@4.0.1: + resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -1928,6 +2056,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -1944,6 +2075,10 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} @@ -1967,10 +2102,21 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + ret@0.5.0: + resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} + engines: {node: '>=10'} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rolldown-plugin-dts@0.13.14: resolution: {integrity: sha512-wjNhHZz9dlN6PTIXyizB6u/mAg1wEFMW9yw7imEVe3CxHSRnNHVyycIX0yDEOVJfDNISLPbkCIPEpFpizy5+PQ==} engines: {node: '>=20.18.0'} @@ -2004,9 +2150,19 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safe-regex2@5.0.0: + resolution: {integrity: sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + semver@7.7.4: resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} @@ -2020,6 +2176,9 @@ packages: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -2050,6 +2209,9 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -2061,6 +2223,9 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -2068,6 +2233,10 @@ packages: spawndamnit@3.0.1: resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -2107,6 +2276,10 @@ packages: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} + thread-stream@4.0.0: + resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} + engines: {node: '>=20'} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -2145,6 +2318,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toad-cache@3.7.0: + resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} + engines: {node: '>=12'} + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} @@ -2815,6 +2992,29 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@fastify/ajv-compiler@4.0.5': + dependencies: + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + fast-uri: 3.1.0 + + '@fastify/error@4.2.0': {} + + '@fastify/fast-json-stringify-compiler@5.0.3': + dependencies: + fast-json-stringify: 6.3.0 + + '@fastify/forwarded@3.0.1': {} + + '@fastify/merge-json-schemas@0.2.1': + dependencies: + dequal: 2.0.3 + + '@fastify/proxy-addr@5.1.0': + dependencies: + '@fastify/forwarded': 3.0.1 + ipaddr.js: 2.3.0 + '@hono/node-server@1.19.11(hono@4.12.5)': dependencies: hono: 4.12.5 @@ -2869,6 +3069,28 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 + '@modelcontextprotocol/sdk@1.27.1(zod@3.25.76)': + 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: 3.25.76 + zod-to-json-schema: 3.25.1(zod@3.25.76) + transitivePeerDependencies: + - supports-color + '@modelcontextprotocol/sdk@1.27.1(zod@4.3.6)': dependencies: '@hono/node-server': 1.19.11(hono@4.12.5) @@ -2912,6 +3134,8 @@ snapshots: '@oxc-project/types@0.115.0': {} + '@pinojs/redact@0.4.0': {} + '@quansync/fs@1.0.0': dependencies: quansync: 1.0.0 @@ -3064,6 +3288,12 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/proper-lockfile@4.1.4': + dependencies: + '@types/retry': 0.12.5 + + '@types/retry@0.12.5': {} + '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -3237,6 +3467,8 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 + abstract-logging@2.0.1: {} + accepts@2.0.0: dependencies: mime-types: 3.0.2 @@ -3291,6 +3523,13 @@ snapshots: '@babel/parser': 7.29.0 pathe: 2.0.3 + atomic-sleep@1.0.0: {} + + avvio@9.2.0: + dependencies: + '@fastify/error': 4.2.0 + fastq: 1.20.1 + balanced-match@1.0.2: {} balanced-match@4.0.4: {} @@ -3387,6 +3626,8 @@ snapshots: cookie@0.7.2: {} + cookie@1.1.1: {} + cors@2.8.6: dependencies: object-assign: 4.1.1 @@ -3412,6 +3653,8 @@ snapshots: depd@2.0.0: {} + dequal@2.0.3: {} + detect-indent@6.1.0: {} diff@8.0.3: {} @@ -3634,6 +3877,8 @@ snapshots: extendable-error@0.1.7: {} + fast-decode-uri-component@1.0.1: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -3646,10 +3891,41 @@ snapshots: fast-json-stable-stringify@2.1.0: {} + fast-json-stringify@6.3.0: + dependencies: + '@fastify/merge-json-schemas': 0.2.1 + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + fast-uri: 3.1.0 + json-schema-ref-resolver: 3.0.0 + rfdc: 1.4.1 + fast-levenshtein@2.0.6: {} + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + fast-uri@3.1.0: {} + fastify@5.8.2: + dependencies: + '@fastify/ajv-compiler': 4.0.5 + '@fastify/error': 4.2.0 + '@fastify/fast-json-stringify-compiler': 5.0.3 + '@fastify/proxy-addr': 5.1.0 + abstract-logging: 2.0.1 + avvio: 9.2.0 + fast-json-stringify: 6.3.0 + find-my-way: 9.5.0 + light-my-request: 6.6.0 + pino: 10.3.1 + process-warning: 5.0.0 + rfdc: 1.4.1 + secure-json-parse: 4.1.0 + semver: 7.7.4 + toad-cache: 3.7.0 + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -3677,6 +3953,12 @@ snapshots: transitivePeerDependencies: - supports-color + find-my-way@9.5.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 5.0.0 + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -3817,6 +4099,8 @@ snapshots: ipaddr.js@1.9.1: {} + ipaddr.js@2.3.0: {} + is-extglob@2.1.1: {} is-glob@4.0.3: @@ -3854,6 +4138,10 @@ snapshots: json-buffer@3.0.1: {} + json-schema-ref-resolver@3.0.0: + dependencies: + dequal: 2.0.3 + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -3875,6 +4163,12 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + light-my-request@6.6.0: + dependencies: + cookie: 1.1.1 + process-warning: 4.0.1 + set-cookie-parser: 2.7.2 + locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -3938,6 +4232,8 @@ snapshots: object-inspect@1.13.4: {} + on-exit-leak-free@2.1.2: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -4013,6 +4309,26 @@ snapshots: pify@4.0.1: {} + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.1.0: {} + + pino@10.3.1: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 4.0.0 + pkce-challenge@5.0.1: {} postcss@8.5.8: @@ -4027,6 +4343,16 @@ snapshots: prettier@3.8.1: {} + process-warning@4.0.1: {} + + process-warning@5.0.0: {} + + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -4044,6 +4370,8 @@ snapshots: queue-microtask@1.2.3: {} + quick-format-unescaped@4.0.4: {} + range-parser@1.2.1: {} raw-body@3.0.2: @@ -4062,6 +4390,8 @@ snapshots: readdirp@4.1.2: {} + real-require@0.2.0: {} + redis-errors@1.2.0: {} redis-parser@3.0.0: @@ -4076,8 +4406,14 @@ snapshots: resolve-pkg-maps@1.0.0: {} + ret@0.5.0: {} + + retry@0.12.0: {} + reusify@1.1.0: {} + rfdc@1.4.1: {} + rolldown-plugin-dts@0.13.14(rolldown@1.0.0-rc.7)(typescript@5.9.3): dependencies: '@babel/generator': 7.29.1 @@ -4161,8 +4497,16 @@ snapshots: dependencies: queue-microtask: 1.2.3 + safe-regex2@5.0.0: + dependencies: + ret: 0.5.0 + + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} + secure-json-parse@4.1.0: {} + semver@7.7.4: {} send@1.2.1: @@ -4190,6 +4534,8 @@ snapshots: transitivePeerDependencies: - supports-color + set-cookie-parser@2.7.2: {} + setprototypeof@1.2.0: {} shebang-command@2.0.0: @@ -4228,12 +4574,18 @@ snapshots: siginfo@2.0.0: {} + signal-exit@3.0.7: {} + signal-exit@4.1.0: {} sisteransi@1.0.5: {} slash@3.0.0: {} + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + source-map-js@1.2.1: {} spawndamnit@3.0.1: @@ -4241,6 +4593,8 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + split2@4.2.0: {} + sprintf-js@1.0.3: {} stackback@0.0.2: {} @@ -4269,6 +4623,10 @@ snapshots: term-size@2.2.1: {} + thread-stream@4.0.0: + dependencies: + real-require: 0.2.0 + tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -4294,6 +4652,8 @@ snapshots: dependencies: is-number: 7.0.0 + toad-cache@3.7.0: {} + toidentifier@1.0.1: {} ts-api-utils@2.4.0(typescript@5.9.3): @@ -4553,6 +4913,10 @@ snapshots: yocto-queue@0.1.0: {} + zod-to-json-schema@3.25.1(zod@3.25.76): + dependencies: + zod: 3.25.76 + zod-to-json-schema@3.25.1(zod@4.3.6): dependencies: zod: 4.3.6