Compare commits
2 Commits
feat/wave3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d3d9826076 | |||
| 5adb711a67 |
40
docs/scratchpads/brain-service.md
Normal file
40
docs/scratchpads/brain-service.md
Normal file
@@ -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
|
||||
18
packages/brain/Dockerfile
Normal file
18
packages/brain/Dockerfile
Normal file
@@ -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"]
|
||||
45
packages/brain/package.json
Normal file
45
packages/brain/package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
83
packages/brain/src/index.ts
Normal file
83
packages/brain/src/index.ts
Normal file
@@ -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<void> {
|
||||
// --- 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<string, unknown>,
|
||||
);
|
||||
} 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);
|
||||
});
|
||||
347
packages/brain/src/mcp/tools.ts
Normal file
347
packages/brain/src/mcp/tools.ts
Normal file
@@ -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 };
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
25
packages/brain/src/middleware/auth.ts
Normal file
25
packages/brain/src/middleware/auth.ts
Normal file
@@ -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<void> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
190
packages/brain/src/routes/api.ts
Normal file
190
packages/brain/src/routes/api.ts
Normal file
@@ -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<string, string | undefined>): Record<string, unknown> {
|
||||
const filters: Record<string, unknown> = {};
|
||||
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<string, string>)));
|
||||
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<string, string>)));
|
||||
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<string, string>)));
|
||||
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<string, string>)));
|
||||
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<string, string>;
|
||||
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<string, string>)));
|
||||
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<string, string>;
|
||||
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<string, string>;
|
||||
return collections.getToday(q['date']);
|
||||
});
|
||||
app.get('/v1/stale', async (req) => {
|
||||
const q = req.query as Record<string, string>;
|
||||
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<string, string>;
|
||||
if (!q['q']) return [];
|
||||
return collections.search(q['q'], q['collection']);
|
||||
});
|
||||
app.get('/v1/audit', async () => collections.audit());
|
||||
}
|
||||
222
packages/brain/src/schemas.ts
Normal file
222
packages/brain/src/schemas.ts
Normal file
@@ -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<typeof BrainTaskSchema>;
|
||||
export type BrainProjectInput = z.input<typeof BrainProjectSchema>;
|
||||
export type BrainEventInput = z.input<typeof BrainEventSchema>;
|
||||
export type BrainAgentInput = z.input<typeof BrainAgentSchema>;
|
||||
export type BrainMissionInput = z.input<typeof BrainMissionSchema>;
|
||||
export type BrainMissionTaskInput = z.input<typeof BrainMissionTaskSchema>;
|
||||
536
packages/brain/src/storage/collections.ts
Normal file
536
packages/brain/src/storage/collections.ts
Normal file
@@ -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<string, number> = { critical: 0, high: 1, medium: 2, low: 3 };
|
||||
|
||||
function matchesFilter<T>(item: T, filters: object): boolean {
|
||||
for (const [key, value] of Object.entries(filters as Record<string, unknown>)) {
|
||||
if (value === undefined || value === null) continue;
|
||||
if (key === 'limit') continue;
|
||||
if (key === 'due_before') {
|
||||
if (!(item as Record<string, unknown>)['due'] || (item as Record<string, unknown>)['due']! > value) return false;
|
||||
continue;
|
||||
}
|
||||
if (key === 'due_after') {
|
||||
if (!(item as Record<string, unknown>)['due'] || (item as Record<string, unknown>)['due']! < value) return false;
|
||||
continue;
|
||||
}
|
||||
if (key === 'date_from') {
|
||||
if ((item as Record<string, unknown>)['date']! < value) return false;
|
||||
continue;
|
||||
}
|
||||
if (key === 'date_to') {
|
||||
if ((item as Record<string, unknown>)['date']! > value) return false;
|
||||
continue;
|
||||
}
|
||||
if (key === 'priority_min') {
|
||||
if (((item as Record<string, unknown>)['priority'] as number) < (value as number)) return false;
|
||||
continue;
|
||||
}
|
||||
if (key === 'priority_max') {
|
||||
if (((item as Record<string, unknown>)['priority'] as number) > (value as number)) return false;
|
||||
continue;
|
||||
}
|
||||
if ((item as Record<string, unknown>)[key] !== value) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyLimit<T>(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<BrainTask[]> {
|
||||
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<BrainTask | null> {
|
||||
const file = await this.store.read('tasks', DEFAULT_TASKS);
|
||||
return file.tasks.find(t => t.id === id) ?? null;
|
||||
}
|
||||
|
||||
async addTask(task: BrainTask): Promise<BrainTask> {
|
||||
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<BrainTask>): Promise<BrainTask> {
|
||||
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<BrainProject[]> {
|
||||
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<BrainProject | null> {
|
||||
const file = await this.store.read('projects', DEFAULT_PROJECTS);
|
||||
return file.projects.find(p => p.id === id) ?? null;
|
||||
}
|
||||
|
||||
async addProject(project: BrainProject): Promise<BrainProject> {
|
||||
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<BrainProject>): Promise<BrainProject> {
|
||||
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<BrainEvent[]> {
|
||||
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<BrainEvent | null> {
|
||||
const file = await this.store.read('events', DEFAULT_EVENTS);
|
||||
return file.events.find(e => e.id === id) ?? null;
|
||||
}
|
||||
|
||||
async addEvent(event: BrainEvent): Promise<BrainEvent> {
|
||||
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<BrainEvent>): Promise<BrainEvent> {
|
||||
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<BrainAgent[]> {
|
||||
const file = await this.store.read('agents', DEFAULT_AGENTS);
|
||||
return file.agents.filter(a => matchesFilter(a, filters));
|
||||
}
|
||||
|
||||
async updateAgent(id: string, updates: Partial<BrainAgent>): Promise<BrainAgent> {
|
||||
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<BrainTicket[]> {
|
||||
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<BrainAppreciation[]> {
|
||||
const file = await this.store.read('appreciations', DEFAULT_APPRECIATIONS);
|
||||
return file.appreciations;
|
||||
}
|
||||
|
||||
async addAppreciation(appreciation: BrainAppreciation): Promise<BrainAppreciation> {
|
||||
await this.store.modify('appreciations', DEFAULT_APPRECIATIONS, (file) => ({
|
||||
...file,
|
||||
appreciations: [...file.appreciations, appreciation],
|
||||
}));
|
||||
return appreciation;
|
||||
}
|
||||
|
||||
// === Missions ===
|
||||
|
||||
async getMissions(filters: BrainMissionFilters = {}): Promise<BrainMission[]> {
|
||||
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<BrainMission | null> {
|
||||
const file = await this.store.read('missions', DEFAULT_MISSIONS);
|
||||
return file.missions.find(m => m.id === id) ?? null;
|
||||
}
|
||||
|
||||
async addMission(mission: BrainMission): Promise<BrainMission> {
|
||||
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<BrainMission>): Promise<BrainMission> {
|
||||
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<BrainMissionSummary | null> {
|
||||
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<BrainMissionTask[]> {
|
||||
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<BrainMissionTask> {
|
||||
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<BrainMissionTask>): Promise<BrainMissionTask> {
|
||||
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<BrainTodaySummary> {
|
||||
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<string, number> = {};
|
||||
const tasksByDomain: Record<string, number> = {};
|
||||
for (const t of tasks) {
|
||||
tasksByStatus[t.status] = (tasksByStatus[t.status] ?? 0) + 1;
|
||||
tasksByDomain[t.domain] = (tasksByDomain[t.domain] ?? 0) + 1;
|
||||
}
|
||||
const projectsByStatus: Record<string, number> = {};
|
||||
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<BrainStaleReport> {
|
||||
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<BrainStats> {
|
||||
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<string, number> = {};
|
||||
const tasksByDomain: Record<string, number> = {};
|
||||
for (const t of tasks) {
|
||||
tasksByStatus[t.status] = (tasksByStatus[t.status] ?? 0) + 1;
|
||||
tasksByDomain[t.domain] = (tasksByDomain[t.domain] ?? 0) + 1;
|
||||
}
|
||||
const projectsByStatus: Record<string, number> = {};
|
||||
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<BrainSearchResult[]> {
|
||||
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<BrainAuditResult> {
|
||||
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<string>();
|
||||
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 };
|
||||
}
|
||||
}
|
||||
156
packages/brain/src/storage/json-store.ts
Normal file
156
packages/brain/src/storage/json-store.ts
Normal file
@@ -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<string, { data: unknown; loadedAt: number }>();
|
||||
private static readonly CACHE_TTL_MS = 5_000;
|
||||
|
||||
constructor(options: JsonStoreOptions) {
|
||||
this.dataDir = options.dataDir;
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
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<void> {
|
||||
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<void>) | 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<T>(collection: string, defaultValue: T): Promise<T> {
|
||||
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<void> {
|
||||
const path = this.filePath(collection);
|
||||
await this.ensureFile(path, '{}');
|
||||
|
||||
let release: (() => Promise<void>) | 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<T>(
|
||||
collection: string,
|
||||
defaultValue: T,
|
||||
modifier: (current: T) => T | Promise<T>,
|
||||
): Promise<T> {
|
||||
const path = this.filePath(collection);
|
||||
await this.ensureFile(path, JSON.stringify(defaultValue, null, 2) + '\n');
|
||||
|
||||
let release: (() => Promise<void>) | 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();
|
||||
}
|
||||
}
|
||||
8
packages/brain/tsconfig.json
Normal file
8
packages/brain/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -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<Record<string, number>>;
|
||||
readonly tasks_by_domain: Readonly<Record<string, number>>;
|
||||
readonly projects_by_status: Readonly<Record<string, number>>;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
364
pnpm-lock.yaml
generated
364
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user