Compare commits
13 Commits
58ba99f3eb
...
0fe2cb79a7
| Author | SHA1 | Date | |
|---|---|---|---|
| 0fe2cb79a7 | |||
| caf058db0d | |||
| aa93c0c614 | |||
| 180604661e | |||
| 9eb48e1d9b | |||
| d83ebe65e9 | |||
| 4fe7d09e5c | |||
| e44cb7e56a | |||
| fd4b7c2ba2 | |||
| a1a1976b38 | |||
| f0d1d4bafa | |||
| 600da70960 | |||
| 780f85e0d6 |
@@ -19,6 +19,8 @@
|
||||
"@mosaic/brain": "workspace:^",
|
||||
"@mosaic/coord": "workspace:^",
|
||||
"@mosaic/db": "workspace:^",
|
||||
"@mosaic/log": "workspace:^",
|
||||
"@mosaic/memory": "workspace:^",
|
||||
"@mosaic/types": "workspace:^",
|
||||
"@nestjs/common": "^11.0.0",
|
||||
"@nestjs/core": "^11.0.0",
|
||||
@@ -38,6 +40,7 @@
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.15.1",
|
||||
"fastify": "^5.0.0",
|
||||
"node-cron": "^4.2.1",
|
||||
"reflect-metadata": "^0.2.0",
|
||||
"rxjs": "^7.8.0",
|
||||
"socket.io": "^4.8.0",
|
||||
@@ -45,6 +48,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"tsx": "^4.0.0",
|
||||
"typescript": "^5.8.0",
|
||||
|
||||
@@ -7,11 +7,15 @@ import {
|
||||
type ToolDefinition,
|
||||
} from '@mariozechner/pi-coding-agent';
|
||||
import type { Brain } from '@mosaic/brain';
|
||||
import type { Memory } from '@mosaic/memory';
|
||||
import { BRAIN } from '../brain/brain.tokens.js';
|
||||
import { MEMORY } from '../memory/memory.tokens.js';
|
||||
import { EmbeddingService } from '../memory/embedding.service.js';
|
||||
import { CoordService } from '../coord/coord.service.js';
|
||||
import { ProviderService } from './provider.service.js';
|
||||
import { createBrainTools } from './tools/brain-tools.js';
|
||||
import { createCoordTools } from './tools/coord-tools.js';
|
||||
import { createMemoryTools } from './tools/memory-tools.js';
|
||||
import type { SessionInfoDto } from './session.dto.js';
|
||||
|
||||
export interface AgentSessionOptions {
|
||||
@@ -42,9 +46,15 @@ export class AgentService implements OnModuleDestroy {
|
||||
constructor(
|
||||
@Inject(ProviderService) private readonly providerService: ProviderService,
|
||||
@Inject(BRAIN) private readonly brain: Brain,
|
||||
@Inject(MEMORY) private readonly memory: Memory,
|
||||
@Inject(EmbeddingService) private readonly embeddingService: EmbeddingService,
|
||||
@Inject(CoordService) private readonly coordService: CoordService,
|
||||
) {
|
||||
this.customTools = [...createBrainTools(brain), ...createCoordTools(coordService)];
|
||||
this.customTools = [
|
||||
...createBrainTools(brain),
|
||||
...createCoordTools(coordService),
|
||||
...createMemoryTools(memory, embeddingService.available ? embeddingService : null),
|
||||
];
|
||||
this.logger.log(`Registered ${this.customTools.length} custom tools`);
|
||||
}
|
||||
|
||||
|
||||
158
apps/gateway/src/agent/tools/memory-tools.ts
Normal file
158
apps/gateway/src/agent/tools/memory-tools.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { Type } from '@sinclair/typebox';
|
||||
import type { ToolDefinition } from '@mariozechner/pi-coding-agent';
|
||||
import type { Memory } from '@mosaic/memory';
|
||||
import type { EmbeddingProvider } from '@mosaic/memory';
|
||||
|
||||
export function createMemoryTools(
|
||||
memory: Memory,
|
||||
embeddingProvider: EmbeddingProvider | null,
|
||||
): ToolDefinition[] {
|
||||
const searchMemory: ToolDefinition = {
|
||||
name: 'memory_search',
|
||||
label: 'Search Memory',
|
||||
description:
|
||||
'Search across stored insights and knowledge using natural language. Returns semantically similar results.',
|
||||
parameters: Type.Object({
|
||||
userId: Type.String({ description: 'User ID to search memory for' }),
|
||||
query: Type.String({ description: 'Natural language search query' }),
|
||||
limit: Type.Optional(Type.Number({ description: 'Max results (default 5)' })),
|
||||
}),
|
||||
async execute(_toolCallId, params) {
|
||||
const { userId, query, limit } = params as {
|
||||
userId: string;
|
||||
query: string;
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
if (!embeddingProvider) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: 'Semantic search unavailable — no embedding provider configured',
|
||||
},
|
||||
],
|
||||
details: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const embedding = await embeddingProvider.embed(query);
|
||||
const results = await memory.insights.searchByEmbedding(userId, embedding, limit ?? 5);
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: JSON.stringify(results, null, 2) }],
|
||||
details: undefined,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const getPreferences: ToolDefinition = {
|
||||
name: 'memory_get_preferences',
|
||||
label: 'Get User Preferences',
|
||||
description: 'Retrieve stored preferences for a user.',
|
||||
parameters: Type.Object({
|
||||
userId: Type.String({ description: 'User ID' }),
|
||||
category: Type.Optional(
|
||||
Type.String({
|
||||
description: 'Filter by category: communication, coding, workflow, appearance, general',
|
||||
}),
|
||||
),
|
||||
}),
|
||||
async execute(_toolCallId, params) {
|
||||
const { userId, category } = params as { userId: string; category?: string };
|
||||
type Cat = 'communication' | 'coding' | 'workflow' | 'appearance' | 'general';
|
||||
const prefs = category
|
||||
? await memory.preferences.findByUserAndCategory(userId, category as Cat)
|
||||
: await memory.preferences.findByUser(userId);
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: JSON.stringify(prefs, null, 2) }],
|
||||
details: undefined,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const savePreference: ToolDefinition = {
|
||||
name: 'memory_save_preference',
|
||||
label: 'Save User Preference',
|
||||
description:
|
||||
'Store a learned user preference (e.g., "prefers tables over paragraphs", "timezone: America/Chicago").',
|
||||
parameters: Type.Object({
|
||||
userId: Type.String({ description: 'User ID' }),
|
||||
key: Type.String({ description: 'Preference key' }),
|
||||
value: Type.String({ description: 'Preference value (JSON string)' }),
|
||||
category: Type.Optional(
|
||||
Type.String({
|
||||
description: 'Category: communication, coding, workflow, appearance, general',
|
||||
}),
|
||||
),
|
||||
}),
|
||||
async execute(_toolCallId, params) {
|
||||
const { userId, key, value, category } = params as {
|
||||
userId: string;
|
||||
key: string;
|
||||
value: string;
|
||||
category?: string;
|
||||
};
|
||||
type Cat = 'communication' | 'coding' | 'workflow' | 'appearance' | 'general';
|
||||
let parsedValue: unknown;
|
||||
try {
|
||||
parsedValue = JSON.parse(value);
|
||||
} catch {
|
||||
parsedValue = value;
|
||||
}
|
||||
const pref = await memory.preferences.upsert({
|
||||
userId,
|
||||
key,
|
||||
value: parsedValue,
|
||||
category: (category as Cat) ?? 'general',
|
||||
source: 'agent',
|
||||
});
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: JSON.stringify(pref, null, 2) }],
|
||||
details: undefined,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const saveInsight: ToolDefinition = {
|
||||
name: 'memory_save_insight',
|
||||
label: 'Save Insight',
|
||||
description:
|
||||
'Store a learned insight, decision, or knowledge extracted from the current interaction.',
|
||||
parameters: Type.Object({
|
||||
userId: Type.String({ description: 'User ID' }),
|
||||
content: Type.String({ description: 'The insight or knowledge to store' }),
|
||||
category: Type.Optional(
|
||||
Type.String({
|
||||
description: 'Category: decision, learning, preference, fact, pattern, general',
|
||||
}),
|
||||
),
|
||||
}),
|
||||
async execute(_toolCallId, params) {
|
||||
const { userId, content, category } = params as {
|
||||
userId: string;
|
||||
content: string;
|
||||
category?: string;
|
||||
};
|
||||
type Cat = 'decision' | 'learning' | 'preference' | 'fact' | 'pattern' | 'general';
|
||||
|
||||
let embedding: number[] | null = null;
|
||||
if (embeddingProvider) {
|
||||
embedding = await embeddingProvider.embed(content);
|
||||
}
|
||||
|
||||
const insight = await memory.insights.create({
|
||||
userId,
|
||||
content,
|
||||
embedding,
|
||||
source: 'agent',
|
||||
category: (category as Cat) ?? 'learning',
|
||||
});
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: JSON.stringify(insight, null, 2) }],
|
||||
details: undefined,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
return [searchMemory, getPreferences, savePreference, saveInsight];
|
||||
}
|
||||
@@ -11,6 +11,9 @@ import { ProjectsModule } from './projects/projects.module.js';
|
||||
import { MissionsModule } from './missions/missions.module.js';
|
||||
import { TasksModule } from './tasks/tasks.module.js';
|
||||
import { CoordModule } from './coord/coord.module.js';
|
||||
import { MemoryModule } from './memory/memory.module.js';
|
||||
import { LogModule } from './log/log.module.js';
|
||||
import { SkillsModule } from './skills/skills.module.js';
|
||||
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
|
||||
|
||||
@Module({
|
||||
@@ -26,6 +29,9 @@ import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
|
||||
MissionsModule,
|
||||
TasksModule,
|
||||
CoordModule,
|
||||
MemoryModule,
|
||||
LogModule,
|
||||
SkillsModule,
|
||||
],
|
||||
controllers: [HealthController],
|
||||
providers: [
|
||||
|
||||
44
apps/gateway/src/log/cron.service.ts
Normal file
44
apps/gateway/src/log/cron.service.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Injectable, Logger, type OnModuleInit, type OnModuleDestroy } from '@nestjs/common';
|
||||
import cron from 'node-cron';
|
||||
import { SummarizationService } from './summarization.service.js';
|
||||
|
||||
@Injectable()
|
||||
export class CronService implements OnModuleInit, OnModuleDestroy {
|
||||
private readonly logger = new Logger(CronService.name);
|
||||
private readonly tasks: cron.ScheduledTask[] = [];
|
||||
|
||||
constructor(private readonly summarization: SummarizationService) {}
|
||||
|
||||
onModuleInit(): void {
|
||||
const summarizationSchedule = process.env['SUMMARIZATION_CRON'] ?? '0 */6 * * *'; // every 6 hours
|
||||
const tierManagementSchedule = process.env['TIER_MANAGEMENT_CRON'] ?? '0 3 * * *'; // daily at 3am
|
||||
|
||||
this.tasks.push(
|
||||
cron.schedule(summarizationSchedule, () => {
|
||||
this.summarization.runSummarization().catch((err) => {
|
||||
this.logger.error(`Scheduled summarization failed: ${err}`);
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
this.tasks.push(
|
||||
cron.schedule(tierManagementSchedule, () => {
|
||||
this.summarization.runTierManagement().catch((err) => {
|
||||
this.logger.error(`Scheduled tier management failed: ${err}`);
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Cron scheduled: summarization="${summarizationSchedule}", tier="${tierManagementSchedule}"`,
|
||||
);
|
||||
}
|
||||
|
||||
onModuleDestroy(): void {
|
||||
for (const task of this.tasks) {
|
||||
task.stop();
|
||||
}
|
||||
this.tasks.length = 0;
|
||||
this.logger.log('Cron tasks stopped');
|
||||
}
|
||||
}
|
||||
62
apps/gateway/src/log/log.controller.ts
Normal file
62
apps/gateway/src/log/log.controller.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Body, Controller, Get, Inject, Param, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import type { LogService } from '@mosaic/log';
|
||||
import { LOG_SERVICE } from './log.tokens.js';
|
||||
import { AuthGuard } from '../auth/auth.guard.js';
|
||||
import type { IngestLogDto, QueryLogsDto } from './log.dto.js';
|
||||
|
||||
@Controller('api/logs')
|
||||
@UseGuards(AuthGuard)
|
||||
export class LogController {
|
||||
constructor(@Inject(LOG_SERVICE) private readonly logService: LogService) {}
|
||||
|
||||
@Post()
|
||||
async ingest(@Query('userId') userId: string, @Body() dto: IngestLogDto) {
|
||||
return this.logService.logs.ingest({
|
||||
sessionId: dto.sessionId,
|
||||
userId,
|
||||
level: dto.level,
|
||||
category: dto.category,
|
||||
content: dto.content,
|
||||
metadata: dto.metadata,
|
||||
});
|
||||
}
|
||||
|
||||
@Post('batch')
|
||||
async ingestBatch(@Query('userId') userId: string, @Body() dtos: IngestLogDto[]) {
|
||||
const entries = dtos.map((dto) => ({
|
||||
sessionId: dto.sessionId,
|
||||
userId,
|
||||
level: dto.level as 'debug' | 'info' | 'warn' | 'error' | undefined,
|
||||
category: dto.category as
|
||||
| 'decision'
|
||||
| 'tool_use'
|
||||
| 'learning'
|
||||
| 'error'
|
||||
| 'general'
|
||||
| undefined,
|
||||
content: dto.content,
|
||||
metadata: dto.metadata,
|
||||
}));
|
||||
return this.logService.logs.ingestBatch(entries);
|
||||
}
|
||||
|
||||
@Get()
|
||||
async query(@Query('userId') userId: string, @Query() params: QueryLogsDto) {
|
||||
return this.logService.logs.query({
|
||||
userId,
|
||||
sessionId: params.sessionId,
|
||||
level: params.level,
|
||||
category: params.category,
|
||||
tier: params.tier,
|
||||
since: params.since ? new Date(params.since) : undefined,
|
||||
until: params.until ? new Date(params.until) : undefined,
|
||||
limit: params.limit ? Number(params.limit) : undefined,
|
||||
offset: params.offset ? Number(params.offset) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async findOne(@Param('id') id: string) {
|
||||
return this.logService.logs.findById(id);
|
||||
}
|
||||
}
|
||||
18
apps/gateway/src/log/log.dto.ts
Normal file
18
apps/gateway/src/log/log.dto.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export interface IngestLogDto {
|
||||
sessionId: string;
|
||||
level?: 'debug' | 'info' | 'warn' | 'error';
|
||||
category?: 'decision' | 'tool_use' | 'learning' | 'error' | 'general';
|
||||
content: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface QueryLogsDto {
|
||||
sessionId?: string;
|
||||
level?: 'debug' | 'info' | 'warn' | 'error';
|
||||
category?: 'decision' | 'tool_use' | 'learning' | 'error' | 'general';
|
||||
tier?: 'hot' | 'warm' | 'cold';
|
||||
since?: string;
|
||||
until?: string;
|
||||
limit?: string;
|
||||
offset?: string;
|
||||
}
|
||||
24
apps/gateway/src/log/log.module.ts
Normal file
24
apps/gateway/src/log/log.module.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { createLogService, type LogService } from '@mosaic/log';
|
||||
import type { Db } from '@mosaic/db';
|
||||
import { DB } from '../database/database.module.js';
|
||||
import { LOG_SERVICE } from './log.tokens.js';
|
||||
import { LogController } from './log.controller.js';
|
||||
import { SummarizationService } from './summarization.service.js';
|
||||
import { CronService } from './cron.service.js';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [
|
||||
{
|
||||
provide: LOG_SERVICE,
|
||||
useFactory: (db: Db): LogService => createLogService(db),
|
||||
inject: [DB],
|
||||
},
|
||||
SummarizationService,
|
||||
CronService,
|
||||
],
|
||||
controllers: [LogController],
|
||||
exports: [LOG_SERVICE, SummarizationService],
|
||||
})
|
||||
export class LogModule {}
|
||||
1
apps/gateway/src/log/log.tokens.ts
Normal file
1
apps/gateway/src/log/log.tokens.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const LOG_SERVICE = 'LOG_SERVICE';
|
||||
178
apps/gateway/src/log/summarization.service.ts
Normal file
178
apps/gateway/src/log/summarization.service.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import type { LogService } from '@mosaic/log';
|
||||
import type { Memory } from '@mosaic/memory';
|
||||
import { LOG_SERVICE } from './log.tokens.js';
|
||||
import { MEMORY } from '../memory/memory.tokens.js';
|
||||
import { EmbeddingService } from '../memory/embedding.service.js';
|
||||
import type { Db } from '@mosaic/db';
|
||||
import { sql, summarizationJobs } from '@mosaic/db';
|
||||
import { DB } from '../database/database.module.js';
|
||||
|
||||
const SUMMARIZATION_PROMPT = `You are a knowledge extraction assistant. Given the following agent interaction logs, extract the key decisions, learnings, and patterns. Output a concise summary (2-4 sentences) that captures the most important information for future reference. Focus on actionable insights, not raw events.
|
||||
|
||||
Logs:
|
||||
{logs}
|
||||
|
||||
Summary:`;
|
||||
|
||||
interface ChatCompletion {
|
||||
choices: Array<{ message: { content: string } }>;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SummarizationService {
|
||||
private readonly logger = new Logger(SummarizationService.name);
|
||||
private readonly apiKey: string | undefined;
|
||||
private readonly baseUrl: string;
|
||||
private readonly model: string;
|
||||
|
||||
constructor(
|
||||
@Inject(LOG_SERVICE) private readonly logService: LogService,
|
||||
@Inject(MEMORY) private readonly memory: Memory,
|
||||
private readonly embeddings: EmbeddingService,
|
||||
@Inject(DB) private readonly db: Db,
|
||||
) {
|
||||
this.apiKey = process.env['OPENAI_API_KEY'];
|
||||
this.baseUrl = process.env['SUMMARIZATION_API_URL'] ?? 'https://api.openai.com/v1';
|
||||
this.model = process.env['SUMMARIZATION_MODEL'] ?? 'gpt-4o-mini';
|
||||
}
|
||||
|
||||
/**
|
||||
* Run one summarization cycle:
|
||||
* 1. Find hot logs older than 24h with decision/learning/tool_use categories
|
||||
* 2. Group by session
|
||||
* 3. Summarize each group via cheap LLM
|
||||
* 4. Store as insights with embeddings
|
||||
* 5. Transition processed logs to warm tier
|
||||
*/
|
||||
async runSummarization(): Promise<{ logsProcessed: number; insightsCreated: number }> {
|
||||
const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000); // 24h ago
|
||||
|
||||
// Create job record
|
||||
const [job] = await this.db
|
||||
.insert(summarizationJobs)
|
||||
.values({ status: 'running', startedAt: new Date() })
|
||||
.returning();
|
||||
|
||||
try {
|
||||
const logs = await this.logService.logs.getLogsForSummarization(cutoff, 200);
|
||||
if (logs.length === 0) {
|
||||
await this.db
|
||||
.update(summarizationJobs)
|
||||
.set({ status: 'completed', completedAt: new Date() })
|
||||
.where(sql`id = ${job!.id}`);
|
||||
return { logsProcessed: 0, insightsCreated: 0 };
|
||||
}
|
||||
|
||||
// Group logs by session
|
||||
const bySession = new Map<string, typeof logs>();
|
||||
for (const log of logs) {
|
||||
const group = bySession.get(log.sessionId) ?? [];
|
||||
group.push(log);
|
||||
bySession.set(log.sessionId, group);
|
||||
}
|
||||
|
||||
let insightsCreated = 0;
|
||||
|
||||
for (const [sessionId, sessionLogs] of bySession) {
|
||||
const userId = sessionLogs[0]?.userId;
|
||||
if (!userId) continue;
|
||||
|
||||
const logsText = sessionLogs.map((l) => `[${l.category}] ${l.content}`).join('\n');
|
||||
|
||||
const summary = await this.summarize(logsText);
|
||||
if (!summary) continue;
|
||||
|
||||
const embedding = this.embeddings.available
|
||||
? await this.embeddings.embed(summary)
|
||||
: undefined;
|
||||
|
||||
await this.memory.insights.create({
|
||||
userId,
|
||||
content: summary,
|
||||
embedding: embedding ?? null,
|
||||
source: 'summarization',
|
||||
category: 'learning',
|
||||
metadata: { sessionId, logCount: sessionLogs.length },
|
||||
});
|
||||
insightsCreated++;
|
||||
}
|
||||
|
||||
// Transition processed logs to warm
|
||||
await this.logService.logs.promoteToWarm(cutoff);
|
||||
|
||||
await this.db
|
||||
.update(summarizationJobs)
|
||||
.set({
|
||||
status: 'completed',
|
||||
logsProcessed: logs.length,
|
||||
insightsCreated,
|
||||
completedAt: new Date(),
|
||||
})
|
||||
.where(sql`id = ${job!.id}`);
|
||||
|
||||
this.logger.log(`Summarization complete: ${logs.length} logs → ${insightsCreated} insights`);
|
||||
return { logsProcessed: logs.length, insightsCreated };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
await this.db
|
||||
.update(summarizationJobs)
|
||||
.set({ status: 'failed', errorMessage: message, completedAt: new Date() })
|
||||
.where(sql`id = ${job!.id}`);
|
||||
this.logger.error(`Summarization failed: ${message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run tier management:
|
||||
* - Warm logs older than 30 days → cold
|
||||
* - Cold logs older than 90 days → purged
|
||||
* - Decay old insight relevance scores
|
||||
*/
|
||||
async runTierManagement(): Promise<void> {
|
||||
const warmCutoff = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
||||
const coldCutoff = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
|
||||
const decayCutoff = new Date(Date.now() - 14 * 24 * 60 * 60 * 1000);
|
||||
|
||||
const promoted = await this.logService.logs.promoteToCold(warmCutoff);
|
||||
const purged = await this.logService.logs.purge(coldCutoff);
|
||||
const decayed = await this.memory.insights.decayOldInsights(decayCutoff);
|
||||
|
||||
this.logger.log(
|
||||
`Tier management: ${promoted} logs→cold, ${purged} purged, ${decayed} insights decayed`,
|
||||
);
|
||||
}
|
||||
|
||||
private async summarize(logsText: string): Promise<string | null> {
|
||||
if (!this.apiKey) {
|
||||
this.logger.warn('No API key configured — skipping summarization');
|
||||
return null;
|
||||
}
|
||||
|
||||
const prompt = SUMMARIZATION_PROMPT.replace('{logs}', logsText);
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: this.model,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
max_tokens: 300,
|
||||
temperature: 0.3,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
this.logger.error(`Summarization API error: ${response.status} ${body}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const json = (await response.json()) as ChatCompletion;
|
||||
return json.choices[0]?.message.content ?? null;
|
||||
}
|
||||
}
|
||||
69
apps/gateway/src/memory/embedding.service.ts
Normal file
69
apps/gateway/src/memory/embedding.service.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import type { EmbeddingProvider } from '@mosaic/memory';
|
||||
|
||||
const DEFAULT_MODEL = 'text-embedding-3-small';
|
||||
const DEFAULT_DIMENSIONS = 1536;
|
||||
|
||||
interface EmbeddingResponse {
|
||||
data: Array<{ embedding: number[]; index: number }>;
|
||||
model: string;
|
||||
usage: { prompt_tokens: number; total_tokens: number };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates embeddings via the OpenAI-compatible embeddings API.
|
||||
* Supports OpenAI, Azure OpenAI, and any provider with a compatible endpoint.
|
||||
*/
|
||||
@Injectable()
|
||||
export class EmbeddingService implements EmbeddingProvider {
|
||||
private readonly logger = new Logger(EmbeddingService.name);
|
||||
private readonly apiKey: string | undefined;
|
||||
private readonly baseUrl: string;
|
||||
private readonly model: string;
|
||||
|
||||
readonly dimensions = DEFAULT_DIMENSIONS;
|
||||
|
||||
constructor() {
|
||||
this.apiKey = process.env['OPENAI_API_KEY'];
|
||||
this.baseUrl = process.env['EMBEDDING_API_URL'] ?? 'https://api.openai.com/v1';
|
||||
this.model = process.env['EMBEDDING_MODEL'] ?? DEFAULT_MODEL;
|
||||
}
|
||||
|
||||
get available(): boolean {
|
||||
return !!this.apiKey;
|
||||
}
|
||||
|
||||
async embed(text: string): Promise<number[]> {
|
||||
const results = await this.embedBatch([text]);
|
||||
return results[0]!;
|
||||
}
|
||||
|
||||
async embedBatch(texts: string[]): Promise<number[][]> {
|
||||
if (!this.apiKey) {
|
||||
this.logger.warn('No OPENAI_API_KEY configured — returning zero vectors');
|
||||
return texts.map(() => new Array<number>(this.dimensions).fill(0));
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/embeddings`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: this.model,
|
||||
input: texts,
|
||||
dimensions: this.dimensions,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
this.logger.error(`Embedding API error: ${response.status} ${body}`);
|
||||
throw new Error(`Embedding API returned ${response.status}`);
|
||||
}
|
||||
|
||||
const json = (await response.json()) as EmbeddingResponse;
|
||||
return json.data.sort((a, b) => a.index - b.index).map((d) => d.embedding);
|
||||
}
|
||||
}
|
||||
126
apps/gateway/src/memory/memory.controller.ts
Normal file
126
apps/gateway/src/memory/memory.controller.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Inject,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import type { Memory } from '@mosaic/memory';
|
||||
import { MEMORY } from './memory.tokens.js';
|
||||
import { AuthGuard } from '../auth/auth.guard.js';
|
||||
import { EmbeddingService } from './embedding.service.js';
|
||||
import type { UpsertPreferenceDto, CreateInsightDto, SearchMemoryDto } from './memory.dto.js';
|
||||
|
||||
@Controller('api/memory')
|
||||
@UseGuards(AuthGuard)
|
||||
export class MemoryController {
|
||||
constructor(
|
||||
@Inject(MEMORY) private readonly memory: Memory,
|
||||
private readonly embeddings: EmbeddingService,
|
||||
) {}
|
||||
|
||||
// ─── Preferences ────────────────────────────────────────────────────
|
||||
|
||||
@Get('preferences')
|
||||
async listPreferences(@Query('userId') userId: string, @Query('category') category?: string) {
|
||||
if (category) {
|
||||
return this.memory.preferences.findByUserAndCategory(
|
||||
userId,
|
||||
category as Parameters<typeof this.memory.preferences.findByUserAndCategory>[1],
|
||||
);
|
||||
}
|
||||
return this.memory.preferences.findByUser(userId);
|
||||
}
|
||||
|
||||
@Get('preferences/:key')
|
||||
async getPreference(@Query('userId') userId: string, @Param('key') key: string) {
|
||||
const pref = await this.memory.preferences.findByUserAndKey(userId, key);
|
||||
if (!pref) throw new NotFoundException('Preference not found');
|
||||
return pref;
|
||||
}
|
||||
|
||||
@Post('preferences')
|
||||
async upsertPreference(@Query('userId') userId: string, @Body() dto: UpsertPreferenceDto) {
|
||||
return this.memory.preferences.upsert({
|
||||
userId,
|
||||
key: dto.key,
|
||||
value: dto.value,
|
||||
category: dto.category,
|
||||
source: dto.source,
|
||||
});
|
||||
}
|
||||
|
||||
@Delete('preferences/:key')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async removePreference(@Query('userId') userId: string, @Param('key') key: string) {
|
||||
const deleted = await this.memory.preferences.remove(userId, key);
|
||||
if (!deleted) throw new NotFoundException('Preference not found');
|
||||
}
|
||||
|
||||
// ─── Insights ───────────────────────────────────────────────────────
|
||||
|
||||
@Get('insights')
|
||||
async listInsights(@Query('userId') userId: string, @Query('limit') limit?: string) {
|
||||
return this.memory.insights.findByUser(userId, limit ? Number(limit) : undefined);
|
||||
}
|
||||
|
||||
@Get('insights/:id')
|
||||
async getInsight(@Param('id') id: string) {
|
||||
const insight = await this.memory.insights.findById(id);
|
||||
if (!insight) throw new NotFoundException('Insight not found');
|
||||
return insight;
|
||||
}
|
||||
|
||||
@Post('insights')
|
||||
async createInsight(@Query('userId') userId: string, @Body() dto: CreateInsightDto) {
|
||||
const embedding = this.embeddings.available
|
||||
? await this.embeddings.embed(dto.content)
|
||||
: undefined;
|
||||
|
||||
return this.memory.insights.create({
|
||||
userId,
|
||||
content: dto.content,
|
||||
source: dto.source,
|
||||
category: dto.category,
|
||||
metadata: dto.metadata,
|
||||
embedding: embedding ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
@Delete('insights/:id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async removeInsight(@Param('id') id: string) {
|
||||
const deleted = await this.memory.insights.remove(id);
|
||||
if (!deleted) throw new NotFoundException('Insight not found');
|
||||
}
|
||||
|
||||
// ─── Search ─────────────────────────────────────────────────────────
|
||||
|
||||
@Post('search')
|
||||
async searchMemory(@Query('userId') userId: string, @Body() dto: SearchMemoryDto) {
|
||||
if (!this.embeddings.available) {
|
||||
return {
|
||||
query: dto.query,
|
||||
results: [],
|
||||
message: 'Semantic search requires OPENAI_API_KEY for embeddings',
|
||||
};
|
||||
}
|
||||
|
||||
const queryEmbedding = await this.embeddings.embed(dto.query);
|
||||
const results = await this.memory.insights.searchByEmbedding(
|
||||
userId,
|
||||
queryEmbedding,
|
||||
dto.limit ?? 10,
|
||||
dto.maxDistance ?? 0.8,
|
||||
);
|
||||
|
||||
return { query: dto.query, results };
|
||||
}
|
||||
}
|
||||
19
apps/gateway/src/memory/memory.dto.ts
Normal file
19
apps/gateway/src/memory/memory.dto.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export interface UpsertPreferenceDto {
|
||||
key: string;
|
||||
value: unknown;
|
||||
category?: 'communication' | 'coding' | 'workflow' | 'appearance' | 'general';
|
||||
source?: string;
|
||||
}
|
||||
|
||||
export interface CreateInsightDto {
|
||||
content: string;
|
||||
source?: 'agent' | 'user' | 'summarization' | 'system';
|
||||
category?: 'decision' | 'learning' | 'preference' | 'fact' | 'pattern' | 'general';
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface SearchMemoryDto {
|
||||
query: string;
|
||||
limit?: number;
|
||||
maxDistance?: number;
|
||||
}
|
||||
22
apps/gateway/src/memory/memory.module.ts
Normal file
22
apps/gateway/src/memory/memory.module.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { createMemory, type Memory } from '@mosaic/memory';
|
||||
import type { Db } from '@mosaic/db';
|
||||
import { DB } from '../database/database.module.js';
|
||||
import { MEMORY } from './memory.tokens.js';
|
||||
import { MemoryController } from './memory.controller.js';
|
||||
import { EmbeddingService } from './embedding.service.js';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [
|
||||
{
|
||||
provide: MEMORY,
|
||||
useFactory: (db: Db): Memory => createMemory(db),
|
||||
inject: [DB],
|
||||
},
|
||||
EmbeddingService,
|
||||
],
|
||||
controllers: [MemoryController],
|
||||
exports: [MEMORY, EmbeddingService],
|
||||
})
|
||||
export class MemoryModule {}
|
||||
1
apps/gateway/src/memory/memory.tokens.ts
Normal file
1
apps/gateway/src/memory/memory.tokens.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const MEMORY = 'MEMORY';
|
||||
67
apps/gateway/src/skills/skills.controller.ts
Normal file
67
apps/gateway/src/skills/skills.controller.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { SkillsService } from './skills.service.js';
|
||||
import { AuthGuard } from '../auth/auth.guard.js';
|
||||
import type { CreateSkillDto, UpdateSkillDto } from './skills.dto.js';
|
||||
|
||||
@Controller('api/skills')
|
||||
@UseGuards(AuthGuard)
|
||||
export class SkillsController {
|
||||
constructor(private readonly skills: SkillsService) {}
|
||||
|
||||
@Get()
|
||||
async list() {
|
||||
return this.skills.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async findOne(@Param('id') id: string) {
|
||||
const skill = await this.skills.findById(id);
|
||||
if (!skill) throw new NotFoundException('Skill not found');
|
||||
return skill;
|
||||
}
|
||||
|
||||
@Post()
|
||||
async create(@Body() dto: CreateSkillDto) {
|
||||
return this.skills.create({
|
||||
name: dto.name,
|
||||
description: dto.description,
|
||||
version: dto.version,
|
||||
source: dto.source,
|
||||
config: dto.config,
|
||||
enabled: dto.enabled,
|
||||
});
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
async update(@Param('id') id: string, @Body() dto: UpdateSkillDto) {
|
||||
const skill = await this.skills.update(id, dto);
|
||||
if (!skill) throw new NotFoundException('Skill not found');
|
||||
return skill;
|
||||
}
|
||||
|
||||
@Patch(':id/toggle')
|
||||
async toggle(@Param('id') id: string, @Body() body: { enabled: boolean }) {
|
||||
const skill = await this.skills.toggle(id, body.enabled);
|
||||
if (!skill) throw new NotFoundException('Skill not found');
|
||||
return skill;
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async remove(@Param('id') id: string) {
|
||||
const deleted = await this.skills.remove(id);
|
||||
if (!deleted) throw new NotFoundException('Skill not found');
|
||||
}
|
||||
}
|
||||
15
apps/gateway/src/skills/skills.dto.ts
Normal file
15
apps/gateway/src/skills/skills.dto.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export interface CreateSkillDto {
|
||||
name: string;
|
||||
description?: string;
|
||||
version?: string;
|
||||
source?: 'builtin' | 'community' | 'custom';
|
||||
config?: Record<string, unknown>;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateSkillDto {
|
||||
description?: string;
|
||||
version?: string;
|
||||
config?: Record<string, unknown>;
|
||||
enabled?: boolean;
|
||||
}
|
||||
10
apps/gateway/src/skills/skills.module.ts
Normal file
10
apps/gateway/src/skills/skills.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SkillsService } from './skills.service.js';
|
||||
import { SkillsController } from './skills.controller.js';
|
||||
|
||||
@Module({
|
||||
providers: [SkillsService],
|
||||
controllers: [SkillsController],
|
||||
exports: [SkillsService],
|
||||
})
|
||||
export class SkillsModule {}
|
||||
52
apps/gateway/src/skills/skills.service.ts
Normal file
52
apps/gateway/src/skills/skills.service.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { eq, type Db, skills } from '@mosaic/db';
|
||||
import { DB } from '../database/database.module.js';
|
||||
|
||||
type Skill = typeof skills.$inferSelect;
|
||||
type NewSkill = typeof skills.$inferInsert;
|
||||
|
||||
@Injectable()
|
||||
export class SkillsService {
|
||||
constructor(@Inject(DB) private readonly db: Db) {}
|
||||
|
||||
async findAll(): Promise<Skill[]> {
|
||||
return this.db.select().from(skills);
|
||||
}
|
||||
|
||||
async findEnabled(): Promise<Skill[]> {
|
||||
return this.db.select().from(skills).where(eq(skills.enabled, true));
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Skill | undefined> {
|
||||
const rows = await this.db.select().from(skills).where(eq(skills.id, id));
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
async findByName(name: string): Promise<Skill | undefined> {
|
||||
const rows = await this.db.select().from(skills).where(eq(skills.name, name));
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
async create(data: NewSkill): Promise<Skill> {
|
||||
const rows = await this.db.insert(skills).values(data).returning();
|
||||
return rows[0]!;
|
||||
}
|
||||
|
||||
async update(id: string, data: Partial<NewSkill>): Promise<Skill | undefined> {
|
||||
const rows = await this.db
|
||||
.update(skills)
|
||||
.set({ ...data, updatedAt: new Date() })
|
||||
.where(eq(skills.id, id))
|
||||
.returning();
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<boolean> {
|
||||
const rows = await this.db.delete(skills).where(eq(skills.id, id)).returning();
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
async toggle(id: string, enabled: boolean): Promise<Skill | undefined> {
|
||||
return this.update(id, { enabled });
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@
|
||||
"@mosaic/brain": ["../../packages/brain/src/index.ts"],
|
||||
"@mosaic/coord": ["../../packages/coord/src/index.ts"],
|
||||
"@mosaic/db": ["../../packages/db/src/index.ts"],
|
||||
"@mosaic/log": ["../../packages/log/src/index.ts"],
|
||||
"@mosaic/memory": ["../../packages/memory/src/index.ts"],
|
||||
"@mosaic/types": ["../../packages/types/src/index.ts"]
|
||||
}
|
||||
}
|
||||
|
||||
2
apps/web/next-env.d.ts
vendored
2
apps/web/next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import './.next/types/routes.d.ts';
|
||||
import "./.next/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { NextConfig } from 'next';
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: 'standalone',
|
||||
transpilePackages: ['@mosaic/design-tokens'],
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
@@ -11,11 +11,17 @@
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mosaic/design-tokens": "workspace:^",
|
||||
"better-auth": "^1.5.5",
|
||||
"clsx": "^2.1.0",
|
||||
"next": "^16.0.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
"react-dom": "^19.0.0",
|
||||
"socket.io-client": "^4.8.0",
|
||||
"tailwind-merge": "^3.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.0.0",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
|
||||
8
apps/web/postcss.config.mjs
Normal file
8
apps/web/postcss.config.mjs
Normal file
@@ -0,0 +1,8 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
14
apps/web/src/app/(auth)/layout.tsx
Normal file
14
apps/web/src/app/(auth)/layout.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { GuestGuard } from '@/components/guest-guard';
|
||||
|
||||
export default function AuthLayout({ children }: { children: ReactNode }): React.ReactElement {
|
||||
return (
|
||||
<GuestGuard>
|
||||
<div className="flex min-h-screen items-center justify-center bg-surface-bg">
|
||||
<div className="w-full max-w-md rounded-xl border border-surface-border bg-surface-card p-8 shadow-lg">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</GuestGuard>
|
||||
);
|
||||
}
|
||||
97
apps/web/src/app/(auth)/login/page.tsx
Normal file
97
apps/web/src/app/(auth)/login/page.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { signIn } from '@/lib/auth-client';
|
||||
|
||||
export default function LoginPage(): React.ReactElement {
|
||||
const router = useRouter();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>): Promise<void> {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
|
||||
const form = new FormData(e.currentTarget);
|
||||
const email = form.get('email') as string;
|
||||
const password = form.get('password') as string;
|
||||
|
||||
const result = await signIn.email({ email, password });
|
||||
|
||||
if (result.error) {
|
||||
setError(result.error.message ?? 'Sign in failed');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
router.push('/chat');
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold">Sign in</h1>
|
||||
<p className="mt-1 text-sm text-text-secondary">Sign in to your Mosaic account</p>
|
||||
|
||||
{error && (
|
||||
<div
|
||||
role="alert"
|
||||
className="mt-4 rounded-lg border border-error/30 bg-error/10 px-4 py-3 text-sm text-error"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form className="mt-6 space-y-4" onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-text-secondary">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
disabled={loading}
|
||||
className="mt-1 block w-full rounded-lg border border-surface-border bg-surface-elevated px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:opacity-50"
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-text-secondary">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
disabled={loading}
|
||||
className="mt-1 block w-full rounded-lg border border-surface-border bg-surface-elevated px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:opacity-50"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-surface-card disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Signing in...' : 'Sign in'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="mt-4 text-center text-sm text-text-muted">
|
||||
Don't have an account?{' '}
|
||||
<Link href="/register" className="text-blue-400 hover:text-blue-300">
|
||||
Sign up
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
114
apps/web/src/app/(auth)/register/page.tsx
Normal file
114
apps/web/src/app/(auth)/register/page.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { signUp } from '@/lib/auth-client';
|
||||
|
||||
export default function RegisterPage(): React.ReactElement {
|
||||
const router = useRouter();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>): Promise<void> {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
|
||||
const form = new FormData(e.currentTarget);
|
||||
const name = form.get('name') as string;
|
||||
const email = form.get('email') as string;
|
||||
const password = form.get('password') as string;
|
||||
|
||||
const result = await signUp.email({ name, email, password });
|
||||
|
||||
if (result.error) {
|
||||
setError(result.error.message ?? 'Registration failed');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
router.push('/chat');
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold">Create account</h1>
|
||||
<p className="mt-1 text-sm text-text-secondary">Get started with Mosaic</p>
|
||||
|
||||
{error && (
|
||||
<div
|
||||
role="alert"
|
||||
className="mt-4 rounded-lg border border-error/30 bg-error/10 px-4 py-3 text-sm text-error"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form className="mt-6 space-y-4" onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-text-secondary">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
autoComplete="name"
|
||||
required
|
||||
disabled={loading}
|
||||
className="mt-1 block w-full rounded-lg border border-surface-border bg-surface-elevated px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:opacity-50"
|
||||
placeholder="Your name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-text-secondary">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
disabled={loading}
|
||||
className="mt-1 block w-full rounded-lg border border-surface-border bg-surface-elevated px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:opacity-50"
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-text-secondary">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
required
|
||||
disabled={loading}
|
||||
className="mt-1 block w-full rounded-lg border border-surface-border bg-surface-elevated px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:opacity-50"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-surface-card disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Creating account...' : 'Create account'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="mt-4 text-center text-sm text-text-muted">
|
||||
Already have an account?{' '}
|
||||
<Link href="/login" className="text-blue-400 hover:text-blue-300">
|
||||
Sign in
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
99
apps/web/src/app/(dashboard)/admin/page.tsx
Normal file
99
apps/web/src/app/(dashboard)/admin/page.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api } from '@/lib/api';
|
||||
|
||||
interface SessionInfo {
|
||||
id: string;
|
||||
provider: string;
|
||||
modelId: string;
|
||||
createdAt: string;
|
||||
promptCount: number;
|
||||
channels: string[];
|
||||
durationMs: number;
|
||||
}
|
||||
|
||||
interface SessionsResponse {
|
||||
sessions: SessionInfo[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export default function AdminPage(): React.ReactElement {
|
||||
const [sessions, setSessions] = useState<SessionInfo[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
api<SessionsResponse>('/api/sessions')
|
||||
.then((res) => setSessions(res.sessions))
|
||||
.catch(() => {})
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-4xl space-y-8">
|
||||
<h1 className="text-2xl font-semibold">Admin</h1>
|
||||
|
||||
{/* User Management placeholder */}
|
||||
<section>
|
||||
<h2 className="mb-4 text-lg font-medium text-text-secondary">User Management</h2>
|
||||
<div className="rounded-lg border border-surface-border bg-surface-card p-6 text-center">
|
||||
<p className="text-sm text-text-muted">
|
||||
User management will be available when the admin API is implemented
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Active Agent Sessions */}
|
||||
<section>
|
||||
<h2 className="mb-4 text-lg font-medium text-text-secondary">Active Agent Sessions</h2>
|
||||
{loading ? (
|
||||
<p className="text-sm text-text-muted">Loading sessions...</p>
|
||||
) : sessions.length === 0 ? (
|
||||
<div className="rounded-lg border border-surface-border bg-surface-card p-4">
|
||||
<p className="text-sm text-text-muted">No active sessions</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-hidden rounded-lg border border-surface-border">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-surface-border bg-surface-elevated text-left text-xs text-text-muted">
|
||||
<th className="px-4 py-2 font-medium">Session ID</th>
|
||||
<th className="px-4 py-2 font-medium">Provider</th>
|
||||
<th className="px-4 py-2 font-medium">Model</th>
|
||||
<th className="hidden px-4 py-2 font-medium md:table-cell">Prompts</th>
|
||||
<th className="hidden px-4 py-2 font-medium md:table-cell">Duration</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sessions.map((s) => (
|
||||
<tr key={s.id} className="border-b border-surface-border last:border-b-0">
|
||||
<td className="max-w-[200px] truncate px-4 py-2 font-mono text-xs text-text-primary">
|
||||
{s.id}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-xs text-text-muted">{s.provider}</td>
|
||||
<td className="px-4 py-2 text-xs text-text-muted">{s.modelId}</td>
|
||||
<td className="hidden px-4 py-2 text-xs text-text-muted md:table-cell">
|
||||
{s.promptCount}
|
||||
</td>
|
||||
<td className="hidden px-4 py-2 text-xs text-text-muted md:table-cell">
|
||||
{formatDuration(s.durationMs)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatDuration(ms: number): string {
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
if (seconds < 60) return `${seconds}s`;
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes < 60) return `${minutes}m ${seconds % 60}s`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
return `${hours}h ${minutes % 60}m`;
|
||||
}
|
||||
179
apps/web/src/app/(dashboard)/chat/page.tsx
Normal file
179
apps/web/src/app/(dashboard)/chat/page.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { api } from '@/lib/api';
|
||||
import { getSocket } from '@/lib/socket';
|
||||
import type { Conversation, Message } from '@/lib/types';
|
||||
import { ConversationList } from '@/components/chat/conversation-list';
|
||||
import { MessageBubble } from '@/components/chat/message-bubble';
|
||||
import { ChatInput } from '@/components/chat/chat-input';
|
||||
import { StreamingMessage } from '@/components/chat/streaming-message';
|
||||
|
||||
export default function ChatPage(): React.ReactElement {
|
||||
const [conversations, setConversations] = useState<Conversation[]>([]);
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [streamingText, setStreamingText] = useState('');
|
||||
const [isStreaming, setIsStreaming] = useState(false);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Load conversations on mount
|
||||
useEffect(() => {
|
||||
api<Conversation[]>('/api/conversations')
|
||||
.then(setConversations)
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
// Load messages when active conversation changes
|
||||
useEffect(() => {
|
||||
if (!activeId) {
|
||||
setMessages([]);
|
||||
return;
|
||||
}
|
||||
api<Message[]>(`/api/conversations/${activeId}/messages`)
|
||||
.then(setMessages)
|
||||
.catch(() => {});
|
||||
}, [activeId]);
|
||||
|
||||
// Auto-scroll to bottom
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [messages, streamingText]);
|
||||
|
||||
// Socket.io setup
|
||||
useEffect(() => {
|
||||
const socket = getSocket();
|
||||
socket.connect();
|
||||
|
||||
socket.on('agent:text', (data: { conversationId: string; text: string }) => {
|
||||
setStreamingText((prev) => prev + data.text);
|
||||
});
|
||||
|
||||
socket.on('agent:start', () => {
|
||||
setIsStreaming(true);
|
||||
setStreamingText('');
|
||||
});
|
||||
|
||||
socket.on('agent:end', (data: { conversationId: string }) => {
|
||||
setIsStreaming(false);
|
||||
setStreamingText('');
|
||||
// Reload messages to get the final persisted version
|
||||
api<Message[]>(`/api/conversations/${data.conversationId}/messages`)
|
||||
.then(setMessages)
|
||||
.catch(() => {});
|
||||
});
|
||||
|
||||
socket.on('error', (data: { error: string }) => {
|
||||
setIsStreaming(false);
|
||||
setStreamingText('');
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: `error-${Date.now()}`,
|
||||
conversationId: '',
|
||||
role: 'system',
|
||||
content: `Error: ${data.error}`,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.off('agent:text');
|
||||
socket.off('agent:start');
|
||||
socket.off('agent:end');
|
||||
socket.off('error');
|
||||
socket.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleNewConversation = useCallback(async () => {
|
||||
const conv = await api<Conversation>('/api/conversations', {
|
||||
method: 'POST',
|
||||
body: { title: 'New conversation' },
|
||||
});
|
||||
setConversations((prev) => [conv, ...prev]);
|
||||
setActiveId(conv.id);
|
||||
setMessages([]);
|
||||
}, []);
|
||||
|
||||
const handleSend = useCallback(
|
||||
async (content: string) => {
|
||||
let convId = activeId;
|
||||
|
||||
// Auto-create conversation if none selected
|
||||
if (!convId) {
|
||||
const conv = await api<Conversation>('/api/conversations', {
|
||||
method: 'POST',
|
||||
body: { title: content.slice(0, 50) },
|
||||
});
|
||||
setConversations((prev) => [conv, ...prev]);
|
||||
setActiveId(conv.id);
|
||||
convId = conv.id;
|
||||
}
|
||||
|
||||
// Optimistic user message
|
||||
const userMsg: Message = {
|
||||
id: `temp-${Date.now()}`,
|
||||
conversationId: convId,
|
||||
role: 'user',
|
||||
content,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
setMessages((prev) => [...prev, userMsg]);
|
||||
|
||||
// Persist user message
|
||||
await api<Message>(`/api/conversations/${convId}/messages`, {
|
||||
method: 'POST',
|
||||
body: { role: 'user', content },
|
||||
});
|
||||
|
||||
// Send to WebSocket for streaming response
|
||||
const socket = getSocket();
|
||||
socket.emit('message', { conversationId: convId, content });
|
||||
},
|
||||
[activeId],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="-m-6 flex h-[calc(100vh-3.5rem)]">
|
||||
<ConversationList
|
||||
conversations={conversations}
|
||||
activeId={activeId}
|
||||
onSelect={setActiveId}
|
||||
onNew={handleNewConversation}
|
||||
/>
|
||||
|
||||
<div className="flex flex-1 flex-col">
|
||||
{activeId ? (
|
||||
<>
|
||||
<div className="flex-1 space-y-4 overflow-y-auto p-6">
|
||||
{messages.map((msg) => (
|
||||
<MessageBubble key={msg.id} message={msg} />
|
||||
))}
|
||||
{isStreaming && <StreamingMessage text={streamingText} />}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
<ChatInput onSend={handleSend} disabled={isStreaming} />
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h2 className="text-lg font-medium text-text-secondary">Welcome to Mosaic Chat</h2>
|
||||
<p className="mt-1 text-sm text-text-muted">
|
||||
Select a conversation or start a new one
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNewConversation}
|
||||
className="mt-4 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700"
|
||||
>
|
||||
Start new conversation
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
apps/web/src/app/(dashboard)/layout.tsx
Normal file
11
apps/web/src/app/(dashboard)/layout.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { AppShell } from '@/components/layout/app-shell';
|
||||
import { AuthGuard } from '@/components/auth-guard';
|
||||
|
||||
export default function DashboardLayout({ children }: { children: ReactNode }): React.ReactElement {
|
||||
return (
|
||||
<AuthGuard>
|
||||
<AppShell>{children}</AppShell>
|
||||
</AuthGuard>
|
||||
);
|
||||
}
|
||||
96
apps/web/src/app/(dashboard)/projects/page.tsx
Normal file
96
apps/web/src/app/(dashboard)/projects/page.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { api } from '@/lib/api';
|
||||
import type { Project } from '@/lib/types';
|
||||
import { ProjectCard } from '@/components/projects/project-card';
|
||||
|
||||
export default function ProjectsPage(): React.ReactElement {
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
api<Project[]>('/api/projects')
|
||||
.then(setProjects)
|
||||
.catch(() => {})
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const handleProjectClick = useCallback((project: Project) => {
|
||||
console.log('Project clicked:', project.id);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<h1 className="text-2xl font-semibold">Projects</h1>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<p className="py-8 text-center text-sm text-text-muted">Loading projects...</p>
|
||||
) : projects.length === 0 ? (
|
||||
<div className="py-12 text-center">
|
||||
<h2 className="text-lg font-medium text-text-secondary">No projects yet</h2>
|
||||
<p className="mt-1 text-sm text-text-muted">
|
||||
Projects will appear here when created via the gateway API
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{projects.map((project) => (
|
||||
<ProjectCard key={project.id} project={project} onClick={handleProjectClick} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mission status section */}
|
||||
<MissionStatus />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MissionStatus(): React.ReactElement {
|
||||
const [mission, setMission] = useState<Record<string, unknown> | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
api<Record<string, unknown>>('/api/coord/status')
|
||||
.then(setMission)
|
||||
.catch(() => setMission(null))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="mt-8">
|
||||
<h2 className="mb-4 text-lg font-semibold">Active Mission</h2>
|
||||
{loading ? (
|
||||
<p className="text-sm text-text-muted">Loading mission status...</p>
|
||||
) : !mission ? (
|
||||
<div className="rounded-lg border border-surface-border bg-surface-card p-6 text-center">
|
||||
<p className="text-sm text-text-muted">No active mission detected</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-surface-border bg-surface-card p-4">
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<StatCard label="Mission" value={String(mission['missionId'] ?? 'Unknown')} />
|
||||
<StatCard label="Phase" value={String(mission['currentPhase'] ?? '—')} />
|
||||
<StatCard
|
||||
label="Tasks"
|
||||
value={`${mission['completedTasks'] ?? 0} / ${mission['totalTasks'] ?? 0}`}
|
||||
/>
|
||||
<StatCard label="Status" value={String(mission['status'] ?? '—')} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function StatCard({ label, value }: { label: string; value: string }): React.ReactElement {
|
||||
return (
|
||||
<div className="rounded-lg bg-surface-elevated p-3">
|
||||
<p className="text-xs text-text-muted">{label}</p>
|
||||
<p className="mt-1 text-sm font-medium text-text-primary">{value}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
150
apps/web/src/app/(dashboard)/settings/page.tsx
Normal file
150
apps/web/src/app/(dashboard)/settings/page.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api } from '@/lib/api';
|
||||
import { useSession } from '@/lib/auth-client';
|
||||
|
||||
interface ProviderInfo {
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
modelCount: number;
|
||||
}
|
||||
|
||||
interface ModelInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
provider: string;
|
||||
contextWindow: number;
|
||||
reasoning: boolean;
|
||||
cost: { input: number; output: number };
|
||||
}
|
||||
|
||||
export default function SettingsPage(): React.ReactElement {
|
||||
const { data: session } = useSession();
|
||||
const [providers, setProviders] = useState<ProviderInfo[]>([]);
|
||||
const [models, setModels] = useState<ModelInfo[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
api<ProviderInfo[]>('/api/providers').catch(() => []),
|
||||
api<ModelInfo[]>('/api/providers/models').catch(() => []),
|
||||
])
|
||||
.then(([p, m]) => {
|
||||
setProviders(p);
|
||||
setModels(m);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl space-y-8">
|
||||
<h1 className="text-2xl font-semibold">Settings</h1>
|
||||
|
||||
{/* Profile */}
|
||||
<section>
|
||||
<h2 className="mb-4 text-lg font-medium text-text-secondary">Profile</h2>
|
||||
<div className="rounded-lg border border-surface-border bg-surface-card p-4">
|
||||
{session?.user ? (
|
||||
<div className="space-y-3">
|
||||
<Field label="Name" value={session.user.name ?? '—'} />
|
||||
<Field label="Email" value={session.user.email} />
|
||||
<Field label="User ID" value={session.user.id} />
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-text-muted">Not signed in</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Providers */}
|
||||
<section>
|
||||
<h2 className="mb-4 text-lg font-medium text-text-secondary">LLM Providers</h2>
|
||||
{loading ? (
|
||||
<p className="text-sm text-text-muted">Loading providers...</p>
|
||||
) : providers.length === 0 ? (
|
||||
<div className="rounded-lg border border-surface-border bg-surface-card p-4">
|
||||
<p className="text-sm text-text-muted">No providers configured</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{providers.map((p) => (
|
||||
<div
|
||||
key={p.name}
|
||||
className="flex items-center justify-between rounded-lg border border-surface-border bg-surface-card p-4"
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-text-primary">{p.name}</p>
|
||||
<p className="text-xs text-text-muted">{p.modelCount} models available</p>
|
||||
</div>
|
||||
<span
|
||||
className={`rounded-full px-2 py-0.5 text-xs ${
|
||||
p.enabled ? 'bg-success/20 text-success' : 'bg-gray-600/20 text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{p.enabled ? 'Active' : 'Disabled'}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Models */}
|
||||
<section>
|
||||
<h2 className="mb-4 text-lg font-medium text-text-secondary">Available Models</h2>
|
||||
{loading ? (
|
||||
<p className="text-sm text-text-muted">Loading models...</p>
|
||||
) : models.length === 0 ? (
|
||||
<div className="rounded-lg border border-surface-border bg-surface-card p-4">
|
||||
<p className="text-sm text-text-muted">No models available</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-hidden rounded-lg border border-surface-border">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-surface-border bg-surface-elevated text-left text-xs text-text-muted">
|
||||
<th className="px-4 py-2 font-medium">Model</th>
|
||||
<th className="px-4 py-2 font-medium">Provider</th>
|
||||
<th className="hidden px-4 py-2 font-medium md:table-cell">Context</th>
|
||||
<th className="hidden px-4 py-2 font-medium md:table-cell">Cost (in/out)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{models.map((m) => (
|
||||
<tr
|
||||
key={`${m.provider}-${m.id}`}
|
||||
className="border-b border-surface-border last:border-b-0"
|
||||
>
|
||||
<td className="px-4 py-2 text-sm text-text-primary">
|
||||
{m.name}
|
||||
{m.reasoning && (
|
||||
<span className="ml-2 text-xs text-purple-400">reasoning</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-xs text-text-muted">{m.provider}</td>
|
||||
<td className="hidden px-4 py-2 text-xs text-text-muted md:table-cell">
|
||||
{(m.contextWindow / 1000).toFixed(0)}k
|
||||
</td>
|
||||
<td className="hidden px-4 py-2 text-xs text-text-muted md:table-cell">
|
||||
${m.cost.input} / ${m.cost.output}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Field({ label, value }: { label: string; value: string }): React.ReactElement {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-text-muted">{label}</span>
|
||||
<span className="text-sm text-text-primary">{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
72
apps/web/src/app/(dashboard)/tasks/page.tsx
Normal file
72
apps/web/src/app/(dashboard)/tasks/page.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { api } from '@/lib/api';
|
||||
import { cn } from '@/lib/cn';
|
||||
import type { Task } from '@/lib/types';
|
||||
import { KanbanBoard } from '@/components/tasks/kanban-board';
|
||||
import { TaskListView } from '@/components/tasks/task-list-view';
|
||||
|
||||
type ViewMode = 'list' | 'kanban';
|
||||
|
||||
export default function TasksPage(): React.ReactElement {
|
||||
const [tasks, setTasks] = useState<Task[]>([]);
|
||||
const [view, setView] = useState<ViewMode>('kanban');
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
api<Task[]>('/api/tasks')
|
||||
.then(setTasks)
|
||||
.catch(() => {})
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const handleTaskClick = useCallback((task: Task) => {
|
||||
// Task detail view will be added in future iteration
|
||||
console.log('Task clicked:', task.id);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<h1 className="text-2xl font-semibold">Tasks</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex rounded-lg border border-surface-border">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setView('list')}
|
||||
className={cn(
|
||||
'px-3 py-1.5 text-xs transition-colors',
|
||||
view === 'list'
|
||||
? 'bg-surface-elevated text-text-primary'
|
||||
: 'text-text-muted hover:text-text-secondary',
|
||||
)}
|
||||
>
|
||||
List
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setView('kanban')}
|
||||
className={cn(
|
||||
'px-3 py-1.5 text-xs transition-colors',
|
||||
view === 'kanban'
|
||||
? 'bg-surface-elevated text-text-primary'
|
||||
: 'text-text-muted hover:text-text-secondary',
|
||||
)}
|
||||
>
|
||||
Kanban
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<p className="py-8 text-center text-sm text-text-muted">Loading tasks...</p>
|
||||
) : view === 'kanban' ? (
|
||||
<KanbanBoard tasks={tasks} onTaskClick={handleTaskClick} />
|
||||
) : (
|
||||
<TaskListView tasks={tasks} onTaskClick={handleTaskClick} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
115
apps/web/src/app/globals.css
Normal file
115
apps/web/src/app/globals.css
Normal file
@@ -0,0 +1,115 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
/*
|
||||
* Mosaic Stack design tokens mapped to Tailwind v4 theme.
|
||||
* Source: @mosaic/design-tokens (AD-13)
|
||||
* Fonts: Outfit (sans), Fira Code (mono)
|
||||
* Palette: deep blue-grays + blue/purple/teal accents
|
||||
* Default: dark theme
|
||||
*/
|
||||
|
||||
@theme {
|
||||
/* ─── Fonts ─── */
|
||||
--font-sans: 'Outfit', system-ui, -apple-system, sans-serif;
|
||||
--font-mono: 'Fira Code', ui-monospace, Menlo, monospace;
|
||||
|
||||
/* ─── Neutral blue-gray scale ─── */
|
||||
--color-gray-50: #f0f2f5;
|
||||
--color-gray-100: #dce0e8;
|
||||
--color-gray-200: #b8c0cc;
|
||||
--color-gray-300: #8e99a9;
|
||||
--color-gray-400: #6b7a8d;
|
||||
--color-gray-500: #4e5d70;
|
||||
--color-gray-600: #3b4859;
|
||||
--color-gray-700: #2a3544;
|
||||
--color-gray-800: #1c2433;
|
||||
--color-gray-900: #111827;
|
||||
--color-gray-950: #0a0f1a;
|
||||
|
||||
/* ─── Primary — blue ─── */
|
||||
--color-blue-50: #eff4ff;
|
||||
--color-blue-100: #dae5ff;
|
||||
--color-blue-200: #bdd1ff;
|
||||
--color-blue-300: #8fb4ff;
|
||||
--color-blue-400: #5b8bff;
|
||||
--color-blue-500: #3b6cf7;
|
||||
--color-blue-600: #2551e0;
|
||||
--color-blue-700: #1d40c0;
|
||||
--color-blue-800: #1e369c;
|
||||
--color-blue-900: #1e317b;
|
||||
--color-blue-950: #162050;
|
||||
|
||||
/* ─── Accent — purple ─── */
|
||||
--color-purple-50: #f3f0ff;
|
||||
--color-purple-100: #e7dfff;
|
||||
--color-purple-200: #d2c3ff;
|
||||
--color-purple-300: #b49aff;
|
||||
--color-purple-400: #9466ff;
|
||||
--color-purple-500: #7c3aed;
|
||||
--color-purple-600: #6d28d9;
|
||||
--color-purple-700: #5b21b6;
|
||||
--color-purple-800: #4c1d95;
|
||||
--color-purple-900: #3b1578;
|
||||
--color-purple-950: #230d4d;
|
||||
|
||||
/* ─── Accent — teal ─── */
|
||||
--color-teal-50: #effcf9;
|
||||
--color-teal-100: #d0f7ef;
|
||||
--color-teal-200: #a4eddf;
|
||||
--color-teal-300: #6fddcb;
|
||||
--color-teal-400: #3ec5b2;
|
||||
--color-teal-500: #25aa99;
|
||||
--color-teal-600: #1c897e;
|
||||
--color-teal-700: #1b6e66;
|
||||
--color-teal-800: #1a5853;
|
||||
--color-teal-900: #194945;
|
||||
--color-teal-950: #082d2b;
|
||||
|
||||
/* ─── Semantic surface tokens ─── */
|
||||
--color-surface-bg: #0a0f1a;
|
||||
--color-surface-card: #111827;
|
||||
--color-surface-elevated: #1c2433;
|
||||
--color-surface-border: #2a3544;
|
||||
|
||||
/* ─── Semantic text tokens ─── */
|
||||
--color-text-primary: #f0f2f5;
|
||||
--color-text-secondary: #8e99a9;
|
||||
--color-text-muted: #6b7a8d;
|
||||
|
||||
/* ─── Status colors ─── */
|
||||
--color-success: #22c55e;
|
||||
--color-warning: #f59e0b;
|
||||
--color-error: #ef4444;
|
||||
--color-info: #3b82f6;
|
||||
|
||||
/* ─── Sidebar width ─── */
|
||||
--spacing-sidebar: 16rem;
|
||||
}
|
||||
|
||||
/* ─── Base styles ─── */
|
||||
body {
|
||||
background-color: var(--color-surface-bg);
|
||||
color: var(--color-text-primary);
|
||||
font-family: var(--font-sans);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* ─── Scrollbar styling ─── */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--color-gray-600);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-gray-500);
|
||||
}
|
||||
@@ -1,13 +1,29 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Outfit, Fira_Code } from 'next/font/google';
|
||||
import './globals.css';
|
||||
|
||||
const outfit = Outfit({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-sans',
|
||||
display: 'swap',
|
||||
weight: ['300', '400', '500', '600', '700'],
|
||||
});
|
||||
|
||||
const firaCode = Fira_Code({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-mono',
|
||||
display: 'swap',
|
||||
weight: ['400', '500', '700'],
|
||||
});
|
||||
|
||||
export const metadata = {
|
||||
title: 'Mosaic',
|
||||
description: 'Mosaic Stack Dashboard',
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: ReactNode }): ReactNode {
|
||||
export default function RootLayout({ children }: { children: ReactNode }): React.ReactElement {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="en" className={`dark ${outfit.variable} ${firaCode.variable}`}>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
export default function HomePage(): React.ReactElement {
|
||||
return (
|
||||
<main>
|
||||
<h1>Mosaic Stack</h1>
|
||||
</main>
|
||||
);
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default function HomePage(): never {
|
||||
redirect('/chat');
|
||||
}
|
||||
|
||||
34
apps/web/src/components/auth-guard.tsx
Normal file
34
apps/web/src/components/auth-guard.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
import { useSession } from '@/lib/auth-client';
|
||||
|
||||
interface AuthGuardProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function AuthGuard({ children }: AuthGuardProps): React.ReactElement | null {
|
||||
const { data: session, isPending } = useSession();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPending && !session) {
|
||||
router.replace('/login');
|
||||
}
|
||||
}, [isPending, session, router]);
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<div className="text-sm text-text-muted">Loading...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
53
apps/web/src/components/chat/chat-input.tsx
Normal file
53
apps/web/src/components/chat/chat-input.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
interface ChatInputProps {
|
||||
onSend: (content: string) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function ChatInput({ onSend, disabled }: ChatInputProps): React.ReactElement {
|
||||
const [value, setValue] = useState('');
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
function handleSubmit(e: React.FormEvent): void {
|
||||
e.preventDefault();
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed || disabled) return;
|
||||
onSend(trimmed);
|
||||
setValue('');
|
||||
textareaRef.current?.focus();
|
||||
}
|
||||
|
||||
function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>): void {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSubmit(e);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="border-t border-surface-border bg-surface-card p-4">
|
||||
<div className="flex items-end gap-3">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={disabled}
|
||||
rows={1}
|
||||
placeholder="Type a message... (Enter to send, Shift+Enter for newline)"
|
||||
className="max-h-32 min-h-[2.5rem] flex-1 resize-none rounded-lg border border-surface-border bg-surface-elevated px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:opacity-50"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={disabled || !value.trim()}
|
||||
className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
57
apps/web/src/components/chat/conversation-list.tsx
Normal file
57
apps/web/src/components/chat/conversation-list.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import { cn } from '@/lib/cn';
|
||||
import type { Conversation } from '@/lib/types';
|
||||
|
||||
interface ConversationListProps {
|
||||
conversations: Conversation[];
|
||||
activeId: string | null;
|
||||
onSelect: (id: string) => void;
|
||||
onNew: () => void;
|
||||
}
|
||||
|
||||
export function ConversationList({
|
||||
conversations,
|
||||
activeId,
|
||||
onSelect,
|
||||
onNew,
|
||||
}: ConversationListProps): React.ReactElement {
|
||||
return (
|
||||
<div className="flex h-full w-64 flex-col border-r border-surface-border bg-surface-card">
|
||||
<div className="flex items-center justify-between p-3">
|
||||
<h2 className="text-sm font-medium text-text-secondary">Conversations</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onNew}
|
||||
className="rounded-md px-2 py-1 text-xs text-blue-400 transition-colors hover:bg-surface-elevated"
|
||||
>
|
||||
+ New
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{conversations.length === 0 && (
|
||||
<p className="px-3 py-2 text-xs text-text-muted">No conversations yet</p>
|
||||
)}
|
||||
{conversations.map((conv) => (
|
||||
<button
|
||||
key={conv.id}
|
||||
type="button"
|
||||
onClick={() => onSelect(conv.id)}
|
||||
className={cn(
|
||||
'w-full px-3 py-2 text-left text-sm transition-colors',
|
||||
activeId === conv.id
|
||||
? 'bg-blue-600/20 text-blue-400'
|
||||
: 'text-text-secondary hover:bg-surface-elevated',
|
||||
)}
|
||||
>
|
||||
<span className="block truncate">{conv.title ?? 'Untitled'}</span>
|
||||
<span className="block text-xs text-text-muted">
|
||||
{new Date(conv.updatedAt).toLocaleDateString()}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
35
apps/web/src/components/chat/message-bubble.tsx
Normal file
35
apps/web/src/components/chat/message-bubble.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
'use client';
|
||||
|
||||
import { cn } from '@/lib/cn';
|
||||
import type { Message } from '@/lib/types';
|
||||
|
||||
interface MessageBubbleProps {
|
||||
message: Message;
|
||||
}
|
||||
|
||||
export function MessageBubble({ message }: MessageBubbleProps): React.ReactElement {
|
||||
const isUser = message.role === 'user';
|
||||
|
||||
return (
|
||||
<div className={cn('flex', isUser ? 'justify-end' : 'justify-start')}>
|
||||
<div
|
||||
className={cn(
|
||||
'max-w-[75%] rounded-xl px-4 py-3 text-sm',
|
||||
isUser
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'border border-surface-border bg-surface-elevated text-text-primary',
|
||||
)}
|
||||
>
|
||||
<div className="whitespace-pre-wrap break-words">{message.content}</div>
|
||||
<div
|
||||
className={cn('mt-1 text-right text-xs', isUser ? 'text-blue-200' : 'text-text-muted')}
|
||||
>
|
||||
{new Date(message.createdAt).toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
22
apps/web/src/components/chat/streaming-message.tsx
Normal file
22
apps/web/src/components/chat/streaming-message.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
'use client';
|
||||
|
||||
/** Renders an in-progress assistant message from streaming text. */
|
||||
interface StreamingMessageProps {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export function StreamingMessage({ text }: StreamingMessageProps): React.ReactElement | null {
|
||||
if (!text) return null;
|
||||
|
||||
return (
|
||||
<div className="flex justify-start">
|
||||
<div className="max-w-[75%] rounded-xl border border-surface-border bg-surface-elevated px-4 py-3 text-sm text-text-primary">
|
||||
<div className="whitespace-pre-wrap break-words">{text}</div>
|
||||
<div className="mt-1 flex items-center gap-1 text-xs text-text-muted">
|
||||
<span className="inline-block h-2 w-2 animate-pulse rounded-full bg-blue-500" />
|
||||
Thinking...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
35
apps/web/src/components/guest-guard.tsx
Normal file
35
apps/web/src/components/guest-guard.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
import { useSession } from '@/lib/auth-client';
|
||||
|
||||
interface GuestGuardProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/** Redirects authenticated users away from auth pages. */
|
||||
export function GuestGuard({ children }: GuestGuardProps): React.ReactElement | null {
|
||||
const { data: session, isPending } = useSession();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPending && session) {
|
||||
router.replace('/chat');
|
||||
}
|
||||
}, [isPending, session, router]);
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<div className="text-sm text-text-muted">Loading...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
19
apps/web/src/components/layout/app-shell.tsx
Normal file
19
apps/web/src/components/layout/app-shell.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Sidebar } from './sidebar';
|
||||
import { Topbar } from './topbar';
|
||||
|
||||
interface AppShellProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function AppShell({ children }: AppShellProps): React.ReactElement {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Sidebar />
|
||||
<div className="pl-sidebar">
|
||||
<Topbar />
|
||||
<main className="p-6">{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
60
apps/web/src/components/layout/sidebar.tsx
Normal file
60
apps/web/src/components/layout/sidebar.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { cn } from '@/lib/cn';
|
||||
|
||||
interface NavItem {
|
||||
label: string;
|
||||
href: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
const navItems: NavItem[] = [
|
||||
{ label: 'Chat', href: '/chat', icon: '💬' },
|
||||
{ label: 'Tasks', href: '/tasks', icon: '📋' },
|
||||
{ label: 'Projects', href: '/projects', icon: '📁' },
|
||||
{ label: 'Settings', href: '/settings', icon: '⚙️' },
|
||||
{ label: 'Admin', href: '/admin', icon: '🛡️' },
|
||||
];
|
||||
|
||||
export function Sidebar(): React.ReactElement {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<aside className="fixed left-0 top-0 z-30 flex h-screen w-sidebar flex-col border-r border-surface-border bg-surface-card">
|
||||
<div className="flex h-14 items-center px-4">
|
||||
<Link href="/" className="text-lg font-semibold text-text-primary">
|
||||
Mosaic
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 space-y-1 px-2 py-2">
|
||||
{navItems.map((item) => {
|
||||
const isActive = pathname === item.href || pathname.startsWith(`${item.href}/`);
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors',
|
||||
isActive
|
||||
? 'bg-blue-600/20 text-blue-400'
|
||||
: 'text-text-secondary hover:bg-surface-elevated hover:text-text-primary',
|
||||
)}
|
||||
>
|
||||
<span className="text-base" aria-hidden="true">
|
||||
{item.icon}
|
||||
</span>
|
||||
{item.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="border-t border-surface-border p-4">
|
||||
<p className="text-xs text-text-muted">Mosaic Stack v0.0.4</p>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
39
apps/web/src/components/layout/topbar.tsx
Normal file
39
apps/web/src/components/layout/topbar.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useSession, signOut } from '@/lib/auth-client';
|
||||
|
||||
export function Topbar(): React.ReactElement {
|
||||
const { data: session } = useSession();
|
||||
const router = useRouter();
|
||||
|
||||
async function handleSignOut(): Promise<void> {
|
||||
await signOut();
|
||||
router.replace('/login');
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-20 flex h-14 items-center justify-between border-b border-surface-border bg-surface-card/80 px-6 backdrop-blur-sm">
|
||||
<div />
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
{session?.user ? (
|
||||
<>
|
||||
<span className="text-sm text-text-secondary">
|
||||
{session.user.name ?? session.user.email}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSignOut}
|
||||
className="rounded-md px-3 py-1.5 text-sm text-text-muted transition-colors hover:bg-surface-elevated hover:text-text-primary"
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-sm text-text-muted">Not signed in</span>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
44
apps/web/src/components/projects/project-card.tsx
Normal file
44
apps/web/src/components/projects/project-card.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
'use client';
|
||||
|
||||
import { cn } from '@/lib/cn';
|
||||
import type { Project } from '@/lib/types';
|
||||
|
||||
interface ProjectCardProps {
|
||||
project: Project;
|
||||
onClick: (project: Project) => void;
|
||||
}
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
active: 'bg-success/20 text-success',
|
||||
paused: 'bg-warning/20 text-warning',
|
||||
completed: 'bg-blue-600/20 text-blue-400',
|
||||
archived: 'bg-gray-600/20 text-gray-400',
|
||||
};
|
||||
|
||||
export function ProjectCard({ project, onClick }: ProjectCardProps): React.ReactElement {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onClick(project)}
|
||||
className="w-full rounded-lg border border-surface-border bg-surface-card p-4 text-left transition-colors hover:border-gray-500"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<h3 className="text-sm font-medium text-text-primary">{project.name}</h3>
|
||||
<span
|
||||
className={cn(
|
||||
'rounded-full px-2 py-0.5 text-xs',
|
||||
statusColors[project.status] ?? 'bg-gray-600/20 text-gray-400',
|
||||
)}
|
||||
>
|
||||
{project.status}
|
||||
</span>
|
||||
</div>
|
||||
{project.description && (
|
||||
<p className="mt-2 line-clamp-2 text-xs text-text-muted">{project.description}</p>
|
||||
)}
|
||||
<p className="mt-3 text-xs text-text-muted">
|
||||
Created {new Date(project.createdAt).toLocaleDateString()}
|
||||
</p>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
47
apps/web/src/components/tasks/kanban-board.tsx
Normal file
47
apps/web/src/components/tasks/kanban-board.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
'use client';
|
||||
|
||||
import type { Task, TaskStatus } from '@/lib/types';
|
||||
import { TaskCard } from './task-card';
|
||||
|
||||
interface KanbanBoardProps {
|
||||
tasks: Task[];
|
||||
onTaskClick: (task: Task) => void;
|
||||
}
|
||||
|
||||
const columns: { id: TaskStatus; label: string }[] = [
|
||||
{ id: 'not-started', label: 'Not Started' },
|
||||
{ id: 'in-progress', label: 'In Progress' },
|
||||
{ id: 'blocked', label: 'Blocked' },
|
||||
{ id: 'done', label: 'Done' },
|
||||
];
|
||||
|
||||
export function KanbanBoard({ tasks, onTaskClick }: KanbanBoardProps): React.ReactElement {
|
||||
return (
|
||||
<div className="flex gap-4 overflow-x-auto pb-4">
|
||||
{columns.map((col) => {
|
||||
const columnTasks = tasks.filter((t) => t.status === col.id);
|
||||
return (
|
||||
<div
|
||||
key={col.id}
|
||||
className="flex w-72 shrink-0 flex-col rounded-lg border border-surface-border bg-surface-elevated"
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-surface-border px-3 py-2">
|
||||
<h3 className="text-sm font-medium text-text-secondary">{col.label}</h3>
|
||||
<span className="rounded-full bg-surface-card px-2 py-0.5 text-xs text-text-muted">
|
||||
{columnTasks.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 space-y-2 p-2">
|
||||
{columnTasks.length === 0 && (
|
||||
<p className="py-4 text-center text-xs text-text-muted">No tasks</p>
|
||||
)}
|
||||
{columnTasks.map((task) => (
|
||||
<TaskCard key={task.id} task={task} onClick={onTaskClick} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
57
apps/web/src/components/tasks/task-card.tsx
Normal file
57
apps/web/src/components/tasks/task-card.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import { cn } from '@/lib/cn';
|
||||
import type { Task } from '@/lib/types';
|
||||
|
||||
interface TaskCardProps {
|
||||
task: Task;
|
||||
onClick: (task: Task) => void;
|
||||
}
|
||||
|
||||
const priorityColors: Record<string, string> = {
|
||||
critical: 'text-error',
|
||||
high: 'text-warning',
|
||||
medium: 'text-blue-400',
|
||||
low: 'text-text-muted',
|
||||
};
|
||||
|
||||
const statusBadgeColors: Record<string, string> = {
|
||||
'not-started': 'bg-gray-600/20 text-gray-300',
|
||||
'in-progress': 'bg-blue-600/20 text-blue-400',
|
||||
blocked: 'bg-error/20 text-error',
|
||||
done: 'bg-success/20 text-success',
|
||||
cancelled: 'bg-gray-600/20 text-gray-500',
|
||||
};
|
||||
|
||||
export function TaskCard({ task, onClick }: TaskCardProps): React.ReactElement {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onClick(task)}
|
||||
className="w-full rounded-lg border border-surface-border bg-surface-card p-3 text-left transition-colors hover:border-gray-500"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<span className="text-sm font-medium text-text-primary">{task.title}</span>
|
||||
<span className={cn('text-xs', priorityColors[task.priority])}>{task.priority}</span>
|
||||
</div>
|
||||
{task.description && (
|
||||
<p className="mt-1 line-clamp-2 text-xs text-text-muted">{task.description}</p>
|
||||
)}
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
'rounded-full px-2 py-0.5 text-xs',
|
||||
statusBadgeColors[task.status] ?? 'bg-gray-600/20 text-gray-400',
|
||||
)}
|
||||
>
|
||||
{task.status}
|
||||
</span>
|
||||
{task.dueDate && (
|
||||
<span className="text-xs text-text-muted">
|
||||
{new Date(task.dueDate).toLocaleDateString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
67
apps/web/src/components/tasks/task-list-view.tsx
Normal file
67
apps/web/src/components/tasks/task-list-view.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
'use client';
|
||||
|
||||
import { cn } from '@/lib/cn';
|
||||
import type { Task } from '@/lib/types';
|
||||
|
||||
interface TaskListViewProps {
|
||||
tasks: Task[];
|
||||
onTaskClick: (task: Task) => void;
|
||||
}
|
||||
|
||||
const priorityColors: Record<string, string> = {
|
||||
critical: 'text-error',
|
||||
high: 'text-warning',
|
||||
medium: 'text-blue-400',
|
||||
low: 'text-text-muted',
|
||||
};
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
'not-started': 'text-gray-400',
|
||||
'in-progress': 'text-blue-400',
|
||||
blocked: 'text-error',
|
||||
done: 'text-success',
|
||||
cancelled: 'text-gray-500',
|
||||
};
|
||||
|
||||
export function TaskListView({ tasks, onTaskClick }: TaskListViewProps): React.ReactElement {
|
||||
if (tasks.length === 0) {
|
||||
return <p className="py-8 text-center text-sm text-text-muted">No tasks found</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-lg border border-surface-border">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-surface-border bg-surface-elevated text-left text-xs text-text-muted">
|
||||
<th className="px-4 py-2 font-medium">Title</th>
|
||||
<th className="px-4 py-2 font-medium">Status</th>
|
||||
<th className="px-4 py-2 font-medium">Priority</th>
|
||||
<th className="hidden px-4 py-2 font-medium md:table-cell">Due</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tasks.map((task) => (
|
||||
<tr
|
||||
key={task.id}
|
||||
onClick={() => onTaskClick(task)}
|
||||
className="cursor-pointer border-b border-surface-border transition-colors last:border-b-0 hover:bg-surface-elevated"
|
||||
>
|
||||
<td className="px-4 py-3 text-sm text-text-primary">{task.title}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={cn('text-xs', statusColors[task.status])}>{task.status}</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={cn('text-xs', priorityColors[task.priority])}>
|
||||
{task.priority}
|
||||
</span>
|
||||
</td>
|
||||
<td className="hidden px-4 py-3 text-xs text-text-muted md:table-cell">
|
||||
{task.dueDate ? new Date(task.dueDate).toLocaleDateString() : '—'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
47
apps/web/src/lib/api.ts
Normal file
47
apps/web/src/lib/api.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
const GATEWAY_URL = process.env['NEXT_PUBLIC_GATEWAY_URL'] ?? 'http://localhost:4000';
|
||||
|
||||
export interface ApiRequestInit extends Omit<RequestInit, 'body'> {
|
||||
body?: unknown;
|
||||
}
|
||||
|
||||
export interface ApiError {
|
||||
statusCode: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch wrapper for the Mosaic gateway API.
|
||||
* Sends credentials (cookies) and JSON body automatically.
|
||||
*/
|
||||
export async function api<T>(path: string, init?: ApiRequestInit): Promise<T> {
|
||||
const { body, headers: customHeaders, ...rest } = init ?? {};
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
Accept: 'application/json',
|
||||
...(customHeaders as Record<string, string>),
|
||||
};
|
||||
|
||||
if (body !== undefined) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
|
||||
const res = await fetch(`${GATEWAY_URL}${path}`, {
|
||||
credentials: 'include',
|
||||
...rest,
|
||||
headers,
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errorBody = (await res.json().catch(() => ({
|
||||
statusCode: res.status,
|
||||
message: res.statusText,
|
||||
}))) as ApiError;
|
||||
throw Object.assign(new Error(errorBody.message), {
|
||||
statusCode: errorBody.statusCode,
|
||||
});
|
||||
}
|
||||
|
||||
if (res.status === 204) return undefined as T;
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
7
apps/web/src/lib/auth-client.ts
Normal file
7
apps/web/src/lib/auth-client.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { createAuthClient } from 'better-auth/react';
|
||||
|
||||
export const authClient: ReturnType<typeof createAuthClient> = createAuthClient({
|
||||
baseURL: process.env['NEXT_PUBLIC_GATEWAY_URL'] ?? 'http://localhost:4000',
|
||||
});
|
||||
|
||||
export const { useSession, signIn, signUp, signOut } = authClient;
|
||||
7
apps/web/src/lib/cn.ts
Normal file
7
apps/web/src/lib/cn.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
/** Merge and deduplicate Tailwind class names. */
|
||||
export function cn(...inputs: ClassValue[]): string {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
15
apps/web/src/lib/socket.ts
Normal file
15
apps/web/src/lib/socket.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { io, type Socket } from 'socket.io-client';
|
||||
|
||||
const GATEWAY_URL = process.env['NEXT_PUBLIC_GATEWAY_URL'] ?? 'http://localhost:4000';
|
||||
|
||||
let socket: Socket | null = null;
|
||||
|
||||
export function getSocket(): Socket {
|
||||
if (!socket) {
|
||||
socket = io(`${GATEWAY_URL}/chat`, {
|
||||
withCredentials: true,
|
||||
autoConnect: false,
|
||||
});
|
||||
}
|
||||
return socket;
|
||||
}
|
||||
57
apps/web/src/lib/types.ts
Normal file
57
apps/web/src/lib/types.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/** Conversation returned by the gateway API. */
|
||||
export interface Conversation {
|
||||
id: string;
|
||||
userId: string;
|
||||
title: string | null;
|
||||
projectId: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/** Message within a conversation. */
|
||||
export interface Message {
|
||||
id: string;
|
||||
conversationId: string;
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
/** Task statuses. */
|
||||
export type TaskStatus = 'not-started' | 'in-progress' | 'blocked' | 'done' | 'cancelled';
|
||||
|
||||
/** Task priorities. */
|
||||
export type TaskPriority = 'critical' | 'high' | 'medium' | 'low';
|
||||
|
||||
/** Task returned by the gateway API. */
|
||||
export interface Task {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string | null;
|
||||
status: TaskStatus;
|
||||
priority: TaskPriority;
|
||||
projectId: string | null;
|
||||
missionId: string | null;
|
||||
assignee: string | null;
|
||||
tags: string[] | null;
|
||||
dueDate: string | null;
|
||||
metadata: Record<string, unknown> | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/** Project statuses. */
|
||||
export type ProjectStatus = 'active' | 'paused' | 'completed' | 'archived';
|
||||
|
||||
/** Project returned by the gateway API. */
|
||||
export interface Project {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
status: ProjectStatus;
|
||||
userId: string;
|
||||
metadata: Record<string, unknown> | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { Config } from 'tailwindcss';
|
||||
|
||||
const config: Config = {
|
||||
content: ['./src/**/*.{ts,tsx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -8,10 +8,10 @@
|
||||
**ID:** mvp-20260312
|
||||
**Statement:** Build Mosaic Stack v0.1.0 — a self-hosted, multi-user AI agent platform with web dashboard, TUI, remote control, shared memory, mission orchestration, and extensible skill/plugin architecture. All TypeScript. Pi as agent harness. Brain as knowledge layer. Queue as coordination backbone.
|
||||
**Phase:** Execution
|
||||
**Current Milestone:** Phase 3: Web Dashboard (v0.0.4)
|
||||
**Progress:** 3 / 8 milestones
|
||||
**Current Milestone:** Phase 5: Remote Control (v0.0.6)
|
||||
**Progress:** 5 / 8 milestones
|
||||
**Status:** active
|
||||
**Last Updated:** 2026-03-12 UTC
|
||||
**Last Updated:** 2026-03-13 UTC
|
||||
|
||||
## Success Criteria
|
||||
|
||||
@@ -34,8 +34,8 @@
|
||||
| 0 | ms-157 | Phase 0: Foundation (v0.0.1) | done | — | — | 2026-03-13 | 2026-03-13 |
|
||||
| 1 | ms-158 | Phase 1: Core API (v0.0.2) | done | — | — | 2026-03-13 | 2026-03-13 |
|
||||
| 2 | ms-159 | Phase 2: Agent Layer (v0.0.3) | done | — | — | 2026-03-13 | 2026-03-12 |
|
||||
| 3 | ms-160 | Phase 3: Web Dashboard (v0.0.4) | not-started | — | — | — | — |
|
||||
| 4 | ms-161 | Phase 4: Memory & Intelligence (v0.0.5) | not-started | — | — | — | — |
|
||||
| 3 | ms-160 | Phase 3: Web Dashboard (v0.0.4) | done | — | — | 2026-03-12 | 2026-03-13 |
|
||||
| 4 | ms-161 | Phase 4: Memory & Intelligence (v0.0.5) | done | — | — | 2026-03-13 | 2026-03-13 |
|
||||
| 5 | ms-162 | Phase 5: Remote Control (v0.0.6) | not-started | — | — | — | — |
|
||||
| 6 | ms-163 | Phase 6: CLI & Tools (v0.0.7) | not-started | — | — | — | — |
|
||||
| 7 | ms-164 | Phase 7: Polish & Beta (v0.1.0) | not-started | — | — | — | — |
|
||||
@@ -66,7 +66,9 @@
|
||||
| 5 | claude-opus-4-6 | 2026-03-12 | — | context limit | P1-009 |
|
||||
| 6 | claude-opus-4-6 | 2026-03-12 | — | context limit | P2-006, FIX-01 |
|
||||
| 7 | claude-opus-4-6 | 2026-03-12 | — | context limit | P2-007 |
|
||||
| 8 | claude-opus-4-6 | 2026-03-12 | — | active | Phase 2 complete |
|
||||
| 8 | claude-opus-4-6 | 2026-03-12 | — | context limit | Phase 2 complete |
|
||||
| 9 | claude-opus-4-6 | 2026-03-12 | — | context limit | P3-007 |
|
||||
| 10 | claude-opus-4-6 | 2026-03-13 | — | active | P3-008 |
|
||||
|
||||
## Scratchpad
|
||||
|
||||
|
||||
@@ -29,21 +29,21 @@
|
||||
| P2-005 | done | Phase 2 | @mosaic/coord — migrate from v0, gateway integration | #77 | #23 |
|
||||
| P2-006 | done | Phase 2 | Agent session management — tmux + monitoring | #78 | #24 |
|
||||
| P2-007 | done | Phase 2 | Verify Phase 2 — multi-provider routing works | #79 | #25 |
|
||||
| P3-001 | not-started | Phase 3 | apps/web scaffold — Next.js 16 + BetterAuth + Tailwind | — | #26 |
|
||||
| P3-002 | not-started | Phase 3 | Auth pages — login, registration, SSO redirect | — | #27 |
|
||||
| P3-003 | not-started | Phase 3 | Chat UI — conversations, messages, streaming | — | #28 |
|
||||
| P3-004 | not-started | Phase 3 | Task management — list view + kanban board | — | #29 |
|
||||
| P3-005 | not-started | Phase 3 | Project & mission views — dashboard + PRD viewer | — | #30 |
|
||||
| P3-006 | not-started | Phase 3 | Settings — provider config, profile, integrations | — | #31 |
|
||||
| P3-007 | not-started | Phase 3 | Admin panel — user management, RBAC | — | #32 |
|
||||
| P3-008 | not-started | Phase 3 | Verify Phase 3 — web dashboard functional E2E | — | #33 |
|
||||
| P4-001 | not-started | Phase 4 | @mosaic/memory — preference + insight stores | — | #34 |
|
||||
| P4-002 | not-started | Phase 4 | Semantic search — pgvector embeddings + search API | — | #35 |
|
||||
| P4-003 | not-started | Phase 4 | @mosaic/log — log ingest, parsing, tiered storage | — | #36 |
|
||||
| P4-004 | not-started | Phase 4 | Summarization pipeline — Haiku-tier LLM + cron | — | #37 |
|
||||
| P4-005 | not-started | Phase 4 | Memory integration — inject into agent sessions | — | #38 |
|
||||
| P4-006 | not-started | Phase 4 | Skill management — catalog, install, config | — | #39 |
|
||||
| P4-007 | not-started | Phase 4 | Verify Phase 4 — memory + log pipeline working | — | #40 |
|
||||
| P3-001 | done | Phase 3 | apps/web scaffold — Next.js 16 + BetterAuth + Tailwind | #82 | #26 |
|
||||
| P3-002 | done | Phase 3 | Auth pages — login, registration, SSO redirect | #83 | #27 |
|
||||
| P3-003 | done | Phase 3 | Chat UI — conversations, messages, streaming | #84 | #28 |
|
||||
| P3-004 | done | Phase 3 | Task management — list view + kanban board | #86 | #29 |
|
||||
| P3-005 | done | Phase 3 | Project & mission views — dashboard + PRD viewer | #87 | #30 |
|
||||
| P3-006 | done | Phase 3 | Settings — provider config, profile, integrations | #88 | #31 |
|
||||
| P3-007 | done | Phase 3 | Admin panel — user management, RBAC | #89 | #32 |
|
||||
| P3-008 | done | Phase 3 | Verify Phase 3 — web dashboard functional E2E | — | #33 |
|
||||
| P4-001 | done | Phase 4 | @mosaic/memory — preference + insight stores | — | #34 |
|
||||
| P4-002 | done | Phase 4 | Semantic search — pgvector embeddings + search API | — | #35 |
|
||||
| P4-003 | done | Phase 4 | @mosaic/log — log ingest, parsing, tiered storage | — | #36 |
|
||||
| P4-004 | done | Phase 4 | Summarization pipeline — Haiku-tier LLM + cron | — | #37 |
|
||||
| P4-005 | done | Phase 4 | Memory integration — inject into agent sessions | — | #38 |
|
||||
| P4-006 | done | Phase 4 | Skill management — catalog, install, config | — | #39 |
|
||||
| P4-007 | done | Phase 4 | Verify Phase 4 — memory + log pipeline working | — | #40 |
|
||||
| P5-001 | not-started | Phase 5 | Plugin host — gateway plugin loading + channel interface | — | #41 |
|
||||
| P5-002 | done | Phase 5 | @mosaic/discord-plugin — Discord bot + channel plugin | #61 | #42 |
|
||||
| P5-003 | not-started | Phase 5 | @mosaic/telegram-plugin — Telegraf bot + channel plugin | — | #43 |
|
||||
|
||||
@@ -102,3 +102,21 @@ User confirmed: start the planning gate.
|
||||
| 9 | P5-001: Plugin host (channel plugin interface) | Plugin arch works |
|
||||
| 10 | P5-002: Discord plugin (bot + channel) | Discord ↔ Gateway proven |
|
||||
| — | Then backfill: auth, brain, db, queue, OTEL, CI, web dashboard, etc. |
|
||||
|
||||
### Session 9 — Phase 3 Web Dashboard (P3-001 through P3-007)
|
||||
|
||||
| Session | Date | Milestone | Tasks Done | Outcome |
|
||||
| ------- | ---------- | --------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 9 | 2026-03-12 | Phase 3 | P3-001 through P3-007 | Full web dashboard: Next.js 16 scaffold, auth pages, chat UI, tasks (list+kanban), projects, settings, admin. PRs #82-#89 merged. |
|
||||
|
||||
### Session 10 — Phase 3 verification (P3-008)
|
||||
|
||||
| Session | Date | Milestone | Tasks Done | Outcome |
|
||||
| ------- | ---------- | --------- | ---------- | -------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 10 | 2026-03-13 | Phase 3 | P3-008 | Phase 3 verification: typecheck 18/18, lint 18/18, format clean, build green (10 routes), 10 tests pass. Phase 3 complete. |
|
||||
|
||||
### Session 10 (continued) — Phase 4 Memory & Intelligence
|
||||
|
||||
| Session | Date | Milestone | Tasks Done | Outcome |
|
||||
| ------- | ---------- | --------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 10 | 2026-03-13 | Phase 4 | P4-001 through P4-007 | Full memory + log system: DB schema (preferences, insights w/ pgvector, agent_logs, skills, summarization_jobs), @mosaic/memory + @mosaic/log packages, embedding service, summarization pipeline w/ cron, memory tools in agent sessions, skill management CRUD. All gates green. |
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
export { createDb, type Db, type DbHandle } from './client.js';
|
||||
export { runMigrations } from './migrate.js';
|
||||
export * from './schema.js';
|
||||
export { eq, and, or, desc, asc, sql, inArray, isNull, isNotNull } from 'drizzle-orm';
|
||||
export {
|
||||
eq,
|
||||
and,
|
||||
or,
|
||||
desc,
|
||||
asc,
|
||||
sql,
|
||||
inArray,
|
||||
isNull,
|
||||
isNotNull,
|
||||
gt,
|
||||
lt,
|
||||
gte,
|
||||
lte,
|
||||
} from 'drizzle-orm';
|
||||
|
||||
@@ -3,7 +3,18 @@
|
||||
* drizzle-kit reads this file directly (avoids CJS/ESM extension issues).
|
||||
*/
|
||||
|
||||
import { pgTable, text, timestamp, boolean, uuid, jsonb, index } from 'drizzle-orm/pg-core';
|
||||
import {
|
||||
pgTable,
|
||||
text,
|
||||
timestamp,
|
||||
boolean,
|
||||
uuid,
|
||||
jsonb,
|
||||
index,
|
||||
real,
|
||||
integer,
|
||||
customType,
|
||||
} from 'drizzle-orm/pg-core';
|
||||
|
||||
// ─── Auth (BetterAuth-compatible) ────────────────────────────────────────────
|
||||
|
||||
@@ -211,3 +222,152 @@ export const messages = pgTable(
|
||||
},
|
||||
(t) => [index('messages_conversation_id_idx').on(t.conversationId)],
|
||||
);
|
||||
|
||||
// ─── pgvector custom type ───────────────────────────────────────────────────
|
||||
|
||||
const vector = customType<{ data: number[]; driverParam: string; config: { dimensions: number } }>({
|
||||
dataType(config) {
|
||||
return `vector(${config?.dimensions ?? 1536})`;
|
||||
},
|
||||
fromDriver(value: unknown): number[] {
|
||||
const str = value as string;
|
||||
return str
|
||||
.slice(1, -1)
|
||||
.split(',')
|
||||
.map((v) => Number(v));
|
||||
},
|
||||
toDriver(value: number[]): string {
|
||||
return `[${value.join(',')}]`;
|
||||
},
|
||||
});
|
||||
|
||||
// ─── Memory ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export const preferences = pgTable(
|
||||
'preferences',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
key: text('key').notNull(),
|
||||
value: jsonb('value').notNull(),
|
||||
category: text('category', {
|
||||
enum: ['communication', 'coding', 'workflow', 'appearance', 'general'],
|
||||
})
|
||||
.notNull()
|
||||
.default('general'),
|
||||
source: text('source'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => [
|
||||
index('preferences_user_id_idx').on(t.userId),
|
||||
index('preferences_user_key_idx').on(t.userId, t.key),
|
||||
],
|
||||
);
|
||||
|
||||
export const insights = pgTable(
|
||||
'insights',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
content: text('content').notNull(),
|
||||
embedding: vector('embedding', { dimensions: 1536 }),
|
||||
source: text('source', {
|
||||
enum: ['agent', 'user', 'summarization', 'system'],
|
||||
})
|
||||
.notNull()
|
||||
.default('agent'),
|
||||
category: text('category', {
|
||||
enum: ['decision', 'learning', 'preference', 'fact', 'pattern', 'general'],
|
||||
})
|
||||
.notNull()
|
||||
.default('general'),
|
||||
relevanceScore: real('relevance_score').notNull().default(1.0),
|
||||
metadata: jsonb('metadata'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
decayedAt: timestamp('decayed_at', { withTimezone: true }),
|
||||
},
|
||||
(t) => [
|
||||
index('insights_user_id_idx').on(t.userId),
|
||||
index('insights_category_idx').on(t.category),
|
||||
index('insights_relevance_idx').on(t.relevanceScore),
|
||||
],
|
||||
);
|
||||
|
||||
// ─── Agent Logs ─────────────────────────────────────────────────────────────
|
||||
|
||||
export const agentLogs = pgTable(
|
||||
'agent_logs',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
sessionId: text('session_id').notNull(),
|
||||
userId: text('user_id').references(() => users.id, { onDelete: 'set null' }),
|
||||
level: text('level', { enum: ['debug', 'info', 'warn', 'error'] })
|
||||
.notNull()
|
||||
.default('info'),
|
||||
category: text('category', {
|
||||
enum: ['decision', 'tool_use', 'learning', 'error', 'general'],
|
||||
})
|
||||
.notNull()
|
||||
.default('general'),
|
||||
content: text('content').notNull(),
|
||||
metadata: jsonb('metadata'),
|
||||
tier: text('tier', { enum: ['hot', 'warm', 'cold'] })
|
||||
.notNull()
|
||||
.default('hot'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
summarizedAt: timestamp('summarized_at', { withTimezone: true }),
|
||||
archivedAt: timestamp('archived_at', { withTimezone: true }),
|
||||
},
|
||||
(t) => [
|
||||
index('agent_logs_session_id_idx').on(t.sessionId),
|
||||
index('agent_logs_user_id_idx').on(t.userId),
|
||||
index('agent_logs_tier_idx').on(t.tier),
|
||||
index('agent_logs_created_at_idx').on(t.createdAt),
|
||||
],
|
||||
);
|
||||
|
||||
// ─── Skills ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export const skills = pgTable(
|
||||
'skills',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
name: text('name').notNull().unique(),
|
||||
description: text('description'),
|
||||
version: text('version'),
|
||||
source: text('source', { enum: ['builtin', 'community', 'custom'] })
|
||||
.notNull()
|
||||
.default('custom'),
|
||||
config: jsonb('config'),
|
||||
enabled: boolean('enabled').notNull().default(true),
|
||||
installedBy: text('installed_by').references(() => users.id, { onDelete: 'set null' }),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => [index('skills_enabled_idx').on(t.enabled)],
|
||||
);
|
||||
|
||||
// ─── Summarization Jobs ─────────────────────────────────────────────────────
|
||||
|
||||
export const summarizationJobs = pgTable(
|
||||
'summarization_jobs',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
status: text('status', { enum: ['pending', 'running', 'completed', 'failed'] })
|
||||
.notNull()
|
||||
.default('pending'),
|
||||
logsProcessed: integer('logs_processed').notNull().default(0),
|
||||
insightsCreated: integer('insights_created').notNull().default(0),
|
||||
errorMessage: text('error_message'),
|
||||
startedAt: timestamp('started_at', { withTimezone: true }),
|
||||
completedAt: timestamp('completed_at', { withTimezone: true }),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => [index('summarization_jobs_status_idx').on(t.status)],
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@mosaic/design-tokens",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
|
||||
91
packages/design-tokens/src/colors.ts
Normal file
91
packages/design-tokens/src/colors.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Mosaic Stack color palette.
|
||||
* Deep blue-grays with blue/purple/teal accents.
|
||||
*/
|
||||
|
||||
export const colors = {
|
||||
/** Neutral blue-gray scale */
|
||||
gray: {
|
||||
50: '#f0f2f5',
|
||||
100: '#dce0e8',
|
||||
200: '#b8c0cc',
|
||||
300: '#8e99a9',
|
||||
400: '#6b7a8d',
|
||||
500: '#4e5d70',
|
||||
600: '#3b4859',
|
||||
700: '#2a3544',
|
||||
800: '#1c2433',
|
||||
900: '#111827',
|
||||
950: '#0a0f1a',
|
||||
},
|
||||
|
||||
/** Primary — blue */
|
||||
blue: {
|
||||
50: '#eff4ff',
|
||||
100: '#dae5ff',
|
||||
200: '#bdd1ff',
|
||||
300: '#8fb4ff',
|
||||
400: '#5b8bff',
|
||||
500: '#3b6cf7',
|
||||
600: '#2551e0',
|
||||
700: '#1d40c0',
|
||||
800: '#1e369c',
|
||||
900: '#1e317b',
|
||||
950: '#162050',
|
||||
},
|
||||
|
||||
/** Accent — purple */
|
||||
purple: {
|
||||
50: '#f3f0ff',
|
||||
100: '#e7dfff',
|
||||
200: '#d2c3ff',
|
||||
300: '#b49aff',
|
||||
400: '#9466ff',
|
||||
500: '#7c3aed',
|
||||
600: '#6d28d9',
|
||||
700: '#5b21b6',
|
||||
800: '#4c1d95',
|
||||
900: '#3b1578',
|
||||
950: '#230d4d',
|
||||
},
|
||||
|
||||
/** Accent — teal */
|
||||
teal: {
|
||||
50: '#effcf9',
|
||||
100: '#d0f7ef',
|
||||
200: '#a4eddf',
|
||||
300: '#6fddcb',
|
||||
400: '#3ec5b2',
|
||||
500: '#25aa99',
|
||||
600: '#1c897e',
|
||||
700: '#1b6e66',
|
||||
800: '#1a5853',
|
||||
900: '#194945',
|
||||
950: '#082d2b',
|
||||
},
|
||||
|
||||
/** Semantic */
|
||||
success: '#22c55e',
|
||||
warning: '#f59e0b',
|
||||
error: '#ef4444',
|
||||
info: '#3b82f6',
|
||||
|
||||
/** Surface — dark theme defaults */
|
||||
surface: {
|
||||
background: '#0a0f1a',
|
||||
card: '#111827',
|
||||
elevated: '#1c2433',
|
||||
overlay: 'rgba(0, 0, 0, 0.6)',
|
||||
border: '#2a3544',
|
||||
},
|
||||
|
||||
/** Text */
|
||||
text: {
|
||||
primary: '#f0f2f5',
|
||||
secondary: '#8e99a9',
|
||||
muted: '#6b7a8d',
|
||||
inverse: '#0a0f1a',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type Colors = typeof colors;
|
||||
33
packages/design-tokens/src/fonts.ts
Normal file
33
packages/design-tokens/src/fonts.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Mosaic Stack font definitions.
|
||||
* Outfit (sans) — headings and body.
|
||||
* Fira Code (mono) — code and terminal.
|
||||
*/
|
||||
|
||||
export const fonts = {
|
||||
sans: {
|
||||
family: 'Outfit',
|
||||
fallback: 'system-ui, -apple-system, sans-serif',
|
||||
stack: "'Outfit', system-ui, -apple-system, sans-serif",
|
||||
weights: {
|
||||
light: 300,
|
||||
regular: 400,
|
||||
medium: 500,
|
||||
semibold: 600,
|
||||
bold: 700,
|
||||
},
|
||||
},
|
||||
|
||||
mono: {
|
||||
family: 'Fira Code',
|
||||
fallback: 'ui-monospace, Menlo, monospace',
|
||||
stack: "'Fira Code', ui-monospace, Menlo, monospace",
|
||||
weights: {
|
||||
regular: 400,
|
||||
medium: 500,
|
||||
bold: 700,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type Fonts = typeof fonts;
|
||||
@@ -1 +1,5 @@
|
||||
export const VERSION = '0.0.0';
|
||||
export const VERSION = '0.0.1';
|
||||
|
||||
export { colors, type Colors } from './colors.js';
|
||||
export { fonts, type Fonts } from './fonts.js';
|
||||
export { spacing, radius, type Spacing, type Radius } from './spacing.js';
|
||||
|
||||
38
packages/design-tokens/src/spacing.ts
Normal file
38
packages/design-tokens/src/spacing.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Mosaic Stack spacing scale (in rem).
|
||||
* Based on a 4px grid (0.25rem increments).
|
||||
*/
|
||||
|
||||
export const spacing = {
|
||||
px: '1px',
|
||||
0: '0',
|
||||
0.5: '0.125rem',
|
||||
1: '0.25rem',
|
||||
1.5: '0.375rem',
|
||||
2: '0.5rem',
|
||||
2.5: '0.625rem',
|
||||
3: '0.75rem',
|
||||
4: '1rem',
|
||||
5: '1.25rem',
|
||||
6: '1.5rem',
|
||||
8: '2rem',
|
||||
10: '2.5rem',
|
||||
12: '3rem',
|
||||
16: '4rem',
|
||||
20: '5rem',
|
||||
24: '6rem',
|
||||
32: '8rem',
|
||||
} as const;
|
||||
|
||||
export const radius = {
|
||||
none: '0',
|
||||
sm: '0.25rem',
|
||||
md: '0.375rem',
|
||||
lg: '0.5rem',
|
||||
xl: '0.75rem',
|
||||
'2xl': '1rem',
|
||||
full: '9999px',
|
||||
} as const;
|
||||
|
||||
export type Spacing = typeof spacing;
|
||||
export type Radius = typeof radius;
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@mosaic/log",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
@@ -15,6 +16,10 @@
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run --passWithNoTests"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mosaic/db": "workspace:*",
|
||||
"drizzle-orm": "^0.45.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.8.0",
|
||||
"vitest": "^2.0.0"
|
||||
|
||||
117
packages/log/src/agent-logs.ts
Normal file
117
packages/log/src/agent-logs.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { eq, and, desc, lt, sql, type Db, agentLogs } from '@mosaic/db';
|
||||
|
||||
export type AgentLog = typeof agentLogs.$inferSelect;
|
||||
export type NewAgentLog = typeof agentLogs.$inferInsert;
|
||||
|
||||
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
||||
export type LogCategory = 'decision' | 'tool_use' | 'learning' | 'error' | 'general';
|
||||
export type LogTier = 'hot' | 'warm' | 'cold';
|
||||
|
||||
export interface LogQuery {
|
||||
userId?: string;
|
||||
sessionId?: string;
|
||||
level?: LogLevel;
|
||||
category?: LogCategory;
|
||||
tier?: LogTier;
|
||||
since?: Date;
|
||||
until?: Date;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export function createAgentLogsRepo(db: Db) {
|
||||
return {
|
||||
async ingest(entry: NewAgentLog): Promise<AgentLog> {
|
||||
const rows = await db.insert(agentLogs).values(entry).returning();
|
||||
return rows[0]!;
|
||||
},
|
||||
|
||||
async ingestBatch(entries: NewAgentLog[]): Promise<AgentLog[]> {
|
||||
if (entries.length === 0) return [];
|
||||
return db.insert(agentLogs).values(entries).returning();
|
||||
},
|
||||
|
||||
async query(params: LogQuery): Promise<AgentLog[]> {
|
||||
const conditions = [];
|
||||
|
||||
if (params.userId) conditions.push(eq(agentLogs.userId, params.userId));
|
||||
if (params.sessionId) conditions.push(eq(agentLogs.sessionId, params.sessionId));
|
||||
if (params.level) conditions.push(eq(agentLogs.level, params.level));
|
||||
if (params.category) conditions.push(eq(agentLogs.category, params.category));
|
||||
if (params.tier) conditions.push(eq(agentLogs.tier, params.tier));
|
||||
if (params.since) conditions.push(sql`${agentLogs.createdAt} >= ${params.since}`);
|
||||
if (params.until) conditions.push(sql`${agentLogs.createdAt} <= ${params.until}`);
|
||||
|
||||
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
||||
|
||||
return db
|
||||
.select()
|
||||
.from(agentLogs)
|
||||
.where(where)
|
||||
.orderBy(desc(agentLogs.createdAt))
|
||||
.limit(params.limit ?? 100)
|
||||
.offset(params.offset ?? 0);
|
||||
},
|
||||
|
||||
async findById(id: string): Promise<AgentLog | undefined> {
|
||||
const rows = await db.select().from(agentLogs).where(eq(agentLogs.id, id));
|
||||
return rows[0];
|
||||
},
|
||||
|
||||
/**
|
||||
* Transition hot logs older than the cutoff to warm tier.
|
||||
* Returns the number of logs transitioned.
|
||||
*/
|
||||
async promoteToWarm(olderThan: Date): Promise<number> {
|
||||
const result = await db
|
||||
.update(agentLogs)
|
||||
.set({ tier: 'warm', summarizedAt: new Date() })
|
||||
.where(and(eq(agentLogs.tier, 'hot'), lt(agentLogs.createdAt, olderThan)))
|
||||
.returning();
|
||||
return result.length;
|
||||
},
|
||||
|
||||
/**
|
||||
* Transition warm logs older than the cutoff to cold tier.
|
||||
*/
|
||||
async promoteToCold(olderThan: Date): Promise<number> {
|
||||
const result = await db
|
||||
.update(agentLogs)
|
||||
.set({ tier: 'cold', archivedAt: new Date() })
|
||||
.where(and(eq(agentLogs.tier, 'warm'), lt(agentLogs.createdAt, olderThan)))
|
||||
.returning();
|
||||
return result.length;
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete cold logs older than the retention period.
|
||||
*/
|
||||
async purge(olderThan: Date): Promise<number> {
|
||||
const result = await db
|
||||
.delete(agentLogs)
|
||||
.where(and(eq(agentLogs.tier, 'cold'), lt(agentLogs.createdAt, olderThan)))
|
||||
.returning();
|
||||
return result.length;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get hot logs ready for summarization (decisions + learnings).
|
||||
*/
|
||||
async getLogsForSummarization(olderThan: Date, limit = 100): Promise<AgentLog[]> {
|
||||
return db
|
||||
.select()
|
||||
.from(agentLogs)
|
||||
.where(
|
||||
and(
|
||||
eq(agentLogs.tier, 'hot'),
|
||||
lt(agentLogs.createdAt, olderThan),
|
||||
sql`${agentLogs.category} IN ('decision', 'learning', 'tool_use')`,
|
||||
),
|
||||
)
|
||||
.orderBy(agentLogs.createdAt)
|
||||
.limit(limit);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type AgentLogsRepo = ReturnType<typeof createAgentLogsRepo>;
|
||||
@@ -1 +1,11 @@
|
||||
export const VERSION = '0.0.0';
|
||||
export { createLogService, type LogService } from './log-service.js';
|
||||
export {
|
||||
createAgentLogsRepo,
|
||||
type AgentLogsRepo,
|
||||
type AgentLog,
|
||||
type NewAgentLog,
|
||||
type LogLevel,
|
||||
type LogCategory,
|
||||
type LogTier,
|
||||
type LogQuery,
|
||||
} from './agent-logs.js';
|
||||
|
||||
12
packages/log/src/log-service.ts
Normal file
12
packages/log/src/log-service.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { Db } from '@mosaic/db';
|
||||
import { createAgentLogsRepo, type AgentLogsRepo } from './agent-logs.js';
|
||||
|
||||
export interface LogService {
|
||||
logs: AgentLogsRepo;
|
||||
}
|
||||
|
||||
export function createLogService(db: Db): LogService {
|
||||
return {
|
||||
logs: createAgentLogsRepo(db),
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@mosaic/memory",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
@@ -16,7 +17,9 @@
|
||||
"test": "vitest run --passWithNoTests"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mosaic/types": "workspace:*"
|
||||
"@mosaic/db": "workspace:*",
|
||||
"@mosaic/types": "workspace:*",
|
||||
"drizzle-orm": "^0.45.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.8.0",
|
||||
|
||||
@@ -1 +1,15 @@
|
||||
export const VERSION = '0.0.0';
|
||||
export { createMemory, type Memory } from './memory.js';
|
||||
export {
|
||||
createPreferencesRepo,
|
||||
type PreferencesRepo,
|
||||
type Preference,
|
||||
type NewPreference,
|
||||
} from './preferences.js';
|
||||
export {
|
||||
createInsightsRepo,
|
||||
type InsightsRepo,
|
||||
type Insight,
|
||||
type NewInsight,
|
||||
type SearchResult,
|
||||
} from './insights.js';
|
||||
export type { VectorStore, VectorSearchResult, EmbeddingProvider } from './vector-store.js';
|
||||
|
||||
89
packages/memory/src/insights.ts
Normal file
89
packages/memory/src/insights.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { eq, and, desc, sql, lt, type Db, insights } from '@mosaic/db';
|
||||
|
||||
export type Insight = typeof insights.$inferSelect;
|
||||
export type NewInsight = typeof insights.$inferInsert;
|
||||
|
||||
export interface SearchResult {
|
||||
insight: Insight;
|
||||
distance: number;
|
||||
}
|
||||
|
||||
export function createInsightsRepo(db: Db) {
|
||||
return {
|
||||
async findByUser(userId: string, limit = 50): Promise<Insight[]> {
|
||||
return db
|
||||
.select()
|
||||
.from(insights)
|
||||
.where(eq(insights.userId, userId))
|
||||
.orderBy(desc(insights.createdAt))
|
||||
.limit(limit);
|
||||
},
|
||||
|
||||
async findById(id: string): Promise<Insight | undefined> {
|
||||
const rows = await db.select().from(insights).where(eq(insights.id, id));
|
||||
return rows[0];
|
||||
},
|
||||
|
||||
async create(data: NewInsight): Promise<Insight> {
|
||||
const rows = await db.insert(insights).values(data).returning();
|
||||
return rows[0]!;
|
||||
},
|
||||
|
||||
async update(id: string, data: Partial<NewInsight>): Promise<Insight | undefined> {
|
||||
const rows = await db
|
||||
.update(insights)
|
||||
.set({ ...data, updatedAt: new Date() })
|
||||
.where(eq(insights.id, id))
|
||||
.returning();
|
||||
return rows[0];
|
||||
},
|
||||
|
||||
async remove(id: string): Promise<boolean> {
|
||||
const rows = await db.delete(insights).where(eq(insights.id, id)).returning();
|
||||
return rows.length > 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Semantic search using pgvector cosine distance.
|
||||
* Requires the vector extension and an embedding for the query.
|
||||
*/
|
||||
async searchByEmbedding(
|
||||
userId: string,
|
||||
queryEmbedding: number[],
|
||||
limit = 10,
|
||||
maxDistance = 0.8,
|
||||
): Promise<SearchResult[]> {
|
||||
const embeddingStr = `[${queryEmbedding.join(',')}]`;
|
||||
const rows = await db.execute(sql`
|
||||
SELECT *,
|
||||
(embedding <=> ${embeddingStr}::vector) AS distance
|
||||
FROM insights
|
||||
WHERE user_id = ${userId}
|
||||
AND embedding IS NOT NULL
|
||||
AND (embedding <=> ${embeddingStr}::vector) < ${maxDistance}
|
||||
ORDER BY distance ASC
|
||||
LIMIT ${limit}
|
||||
`);
|
||||
|
||||
return rows as unknown as SearchResult[];
|
||||
},
|
||||
|
||||
/**
|
||||
* Decay relevance scores for old insights that haven't been accessed recently.
|
||||
*/
|
||||
async decayOldInsights(olderThan: Date, decayFactor = 0.95): Promise<number> {
|
||||
const result = await db
|
||||
.update(insights)
|
||||
.set({
|
||||
relevanceScore: sql`${insights.relevanceScore} * ${decayFactor}`,
|
||||
decayedAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(and(lt(insights.updatedAt, olderThan), sql`${insights.relevanceScore} > 0.1`))
|
||||
.returning();
|
||||
return result.length;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type InsightsRepo = ReturnType<typeof createInsightsRepo>;
|
||||
15
packages/memory/src/memory.ts
Normal file
15
packages/memory/src/memory.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { Db } from '@mosaic/db';
|
||||
import { createPreferencesRepo, type PreferencesRepo } from './preferences.js';
|
||||
import { createInsightsRepo, type InsightsRepo } from './insights.js';
|
||||
|
||||
export interface Memory {
|
||||
preferences: PreferencesRepo;
|
||||
insights: InsightsRepo;
|
||||
}
|
||||
|
||||
export function createMemory(db: Db): Memory {
|
||||
return {
|
||||
preferences: createPreferencesRepo(db),
|
||||
insights: createInsightsRepo(db),
|
||||
};
|
||||
}
|
||||
59
packages/memory/src/preferences.ts
Normal file
59
packages/memory/src/preferences.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { eq, and, type Db, preferences } from '@mosaic/db';
|
||||
|
||||
export type Preference = typeof preferences.$inferSelect;
|
||||
export type NewPreference = typeof preferences.$inferInsert;
|
||||
|
||||
export function createPreferencesRepo(db: Db) {
|
||||
return {
|
||||
async findByUser(userId: string): Promise<Preference[]> {
|
||||
return db.select().from(preferences).where(eq(preferences.userId, userId));
|
||||
},
|
||||
|
||||
async findByUserAndKey(userId: string, key: string): Promise<Preference | undefined> {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(preferences)
|
||||
.where(and(eq(preferences.userId, userId), eq(preferences.key, key)));
|
||||
return rows[0];
|
||||
},
|
||||
|
||||
async findByUserAndCategory(
|
||||
userId: string,
|
||||
category: Preference['category'],
|
||||
): Promise<Preference[]> {
|
||||
return db
|
||||
.select()
|
||||
.from(preferences)
|
||||
.where(and(eq(preferences.userId, userId), eq(preferences.category, category)));
|
||||
},
|
||||
|
||||
async upsert(data: NewPreference): Promise<Preference> {
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(preferences)
|
||||
.where(and(eq(preferences.userId, data.userId), eq(preferences.key, data.key)));
|
||||
|
||||
if (existing[0]) {
|
||||
const rows = await db
|
||||
.update(preferences)
|
||||
.set({ value: data.value, category: data.category, updatedAt: new Date() })
|
||||
.where(eq(preferences.id, existing[0].id))
|
||||
.returning();
|
||||
return rows[0]!;
|
||||
}
|
||||
|
||||
const rows = await db.insert(preferences).values(data).returning();
|
||||
return rows[0]!;
|
||||
},
|
||||
|
||||
async remove(userId: string, key: string): Promise<boolean> {
|
||||
const rows = await db
|
||||
.delete(preferences)
|
||||
.where(and(eq(preferences.userId, userId), eq(preferences.key, key)))
|
||||
.returning();
|
||||
return rows.length > 0;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type PreferencesRepo = ReturnType<typeof createPreferencesRepo>;
|
||||
39
packages/memory/src/vector-store.ts
Normal file
39
packages/memory/src/vector-store.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* VectorStore interface — abstraction over pgvector that allows future
|
||||
* swap to Qdrant, Pinecone, etc.
|
||||
*/
|
||||
export interface VectorStore {
|
||||
/** Store an embedding with an associated document ID. */
|
||||
store(documentId: string, embedding: number[], metadata?: Record<string, unknown>): Promise<void>;
|
||||
|
||||
/** Search for similar embeddings, returning document IDs and distances. */
|
||||
search(
|
||||
queryEmbedding: number[],
|
||||
limit?: number,
|
||||
filter?: Record<string, unknown>,
|
||||
): Promise<VectorSearchResult[]>;
|
||||
|
||||
/** Delete an embedding by document ID. */
|
||||
remove(documentId: string): Promise<void>;
|
||||
}
|
||||
|
||||
export interface VectorSearchResult {
|
||||
documentId: string;
|
||||
distance: number;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* EmbeddingProvider interface — generates embeddings from text.
|
||||
* Implemented by the gateway using the configured LLM provider.
|
||||
*/
|
||||
export interface EmbeddingProvider {
|
||||
/** Generate an embedding vector for the given text. */
|
||||
embed(text: string): Promise<number[]>;
|
||||
|
||||
/** Generate embeddings for multiple texts in batch. */
|
||||
embedBatch(texts: string[]): Promise<number[][]>;
|
||||
|
||||
/** The dimensionality of the embeddings this provider generates. */
|
||||
dimensions: number;
|
||||
}
|
||||
520
pnpm-lock.yaml
generated
520
pnpm-lock.yaml
generated
@@ -10,13 +10,13 @@ importers:
|
||||
devDependencies:
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: ^8.0.0
|
||||
version: 8.57.0(@typescript-eslint/parser@8.57.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)
|
||||
version: 8.57.0(@typescript-eslint/parser@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/parser':
|
||||
specifier: ^8.0.0
|
||||
version: 8.57.0(eslint@9.39.4)(typescript@5.9.3)
|
||||
version: 8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint:
|
||||
specifier: ^9.0.0
|
||||
version: 9.39.4
|
||||
version: 9.39.4(jiti@2.6.1)
|
||||
husky:
|
||||
specifier: ^9.0.0
|
||||
version: 9.1.7
|
||||
@@ -34,10 +34,10 @@ importers:
|
||||
version: 5.9.3
|
||||
typescript-eslint:
|
||||
specifier: ^8.0.0
|
||||
version: 8.57.0(eslint@9.39.4)(typescript@5.9.3)
|
||||
version: 8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
vitest:
|
||||
specifier: ^2.0.0
|
||||
version: 2.1.9(@types/node@22.19.15)
|
||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
||||
|
||||
apps/gateway:
|
||||
dependencies:
|
||||
@@ -62,6 +62,12 @@ importers:
|
||||
'@mosaic/db':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/db
|
||||
'@mosaic/log':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/log
|
||||
'@mosaic/memory':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/memory
|
||||
'@mosaic/types':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/types
|
||||
@@ -109,7 +115,7 @@ importers:
|
||||
version: 0.34.48
|
||||
better-auth:
|
||||
specifier: ^1.5.5
|
||||
version: 1.5.5(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(kysely@0.28.11)(postgres@3.4.8))(mongodb@7.1.0(socks@2.8.7))(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@2.1.9(@types/node@22.19.15))
|
||||
version: 1.5.5(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(kysely@0.28.11)(postgres@3.4.8))(mongodb@7.1.0(socks@2.8.7))(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@2.1.9(@types/node@22.19.15)(lightningcss@1.31.1))
|
||||
class-transformer:
|
||||
specifier: ^0.5.1
|
||||
version: 0.5.1
|
||||
@@ -119,6 +125,9 @@ importers:
|
||||
fastify:
|
||||
specifier: ^5.0.0
|
||||
version: 5.8.2
|
||||
node-cron:
|
||||
specifier: ^4.2.1
|
||||
version: 4.2.1
|
||||
reflect-metadata:
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.2
|
||||
@@ -135,6 +144,9 @@ importers:
|
||||
'@types/node':
|
||||
specifier: ^22.0.0
|
||||
version: 22.19.15
|
||||
'@types/node-cron':
|
||||
specifier: ^3.0.11
|
||||
version: 3.0.11
|
||||
'@types/uuid':
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.0
|
||||
@@ -146,10 +158,19 @@ importers:
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^2.0.0
|
||||
version: 2.1.9(@types/node@22.19.15)
|
||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
||||
|
||||
apps/web:
|
||||
dependencies:
|
||||
'@mosaic/design-tokens':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/design-tokens
|
||||
better-auth:
|
||||
specifier: ^1.5.5
|
||||
version: 1.5.5(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(kysely@0.28.11)(postgres@3.4.8))(mongodb@7.1.0(socks@2.8.7))(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@2.1.9(@types/node@22.19.15)(lightningcss@1.31.1))
|
||||
clsx:
|
||||
specifier: ^2.1.0
|
||||
version: 2.1.1
|
||||
next:
|
||||
specifier: ^16.0.0
|
||||
version: 16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
@@ -159,7 +180,16 @@ importers:
|
||||
react-dom:
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.4(react@19.2.4)
|
||||
socket.io-client:
|
||||
specifier: ^4.8.0
|
||||
version: 4.8.3
|
||||
tailwind-merge:
|
||||
specifier: ^3.5.0
|
||||
version: 3.5.0
|
||||
devDependencies:
|
||||
'@tailwindcss/postcss':
|
||||
specifier: ^4.0.0
|
||||
version: 4.2.1
|
||||
'@types/node':
|
||||
specifier: ^22.0.0
|
||||
version: 22.19.15
|
||||
@@ -177,7 +207,7 @@ importers:
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^2.0.0
|
||||
version: 2.1.9(@types/node@22.19.15)
|
||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
||||
|
||||
packages/agent:
|
||||
dependencies:
|
||||
@@ -190,7 +220,7 @@ importers:
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^2.0.0
|
||||
version: 2.1.9(@types/node@22.19.15)
|
||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
||||
|
||||
packages/auth:
|
||||
dependencies:
|
||||
@@ -199,7 +229,7 @@ importers:
|
||||
version: link:../db
|
||||
better-auth:
|
||||
specifier: ^1.5.5
|
||||
version: 1.5.5(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(kysely@0.28.11)(postgres@3.4.8))(mongodb@7.1.0(socks@2.8.7))(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@2.1.9(@types/node@22.19.15))
|
||||
version: 1.5.5(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(kysely@0.28.11)(postgres@3.4.8))(mongodb@7.1.0(socks@2.8.7))(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@2.1.9(@types/node@22.19.15)(lightningcss@1.31.1))
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^22.0.0
|
||||
@@ -212,7 +242,7 @@ importers:
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^2.0.0
|
||||
version: 2.1.9(@types/node@22.19.15)
|
||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
||||
|
||||
packages/brain:
|
||||
dependencies:
|
||||
@@ -228,7 +258,7 @@ importers:
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^2.0.0
|
||||
version: 2.1.9(@types/node@22.19.15)
|
||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
||||
|
||||
packages/cli:
|
||||
dependencies:
|
||||
@@ -262,7 +292,7 @@ importers:
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^2.0.0
|
||||
version: 2.1.9(@types/node@22.19.15)
|
||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
||||
|
||||
packages/coord:
|
||||
dependencies:
|
||||
@@ -278,7 +308,7 @@ importers:
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^2.0.0
|
||||
version: 2.1.9(@types/node@22.19.15)
|
||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
||||
|
||||
packages/db:
|
||||
dependencies:
|
||||
@@ -303,7 +333,7 @@ importers:
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^2.0.0
|
||||
version: 2.1.9(@types/node@22.19.15)
|
||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
||||
|
||||
packages/design-tokens:
|
||||
devDependencies:
|
||||
@@ -312,29 +342,42 @@ importers:
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^2.0.0
|
||||
version: 2.1.9(@types/node@22.19.15)
|
||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
||||
|
||||
packages/log:
|
||||
dependencies:
|
||||
'@mosaic/db':
|
||||
specifier: workspace:*
|
||||
version: link:../db
|
||||
drizzle-orm:
|
||||
specifier: ^0.45.1
|
||||
version: 0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(kysely@0.28.11)(postgres@3.4.8)
|
||||
devDependencies:
|
||||
typescript:
|
||||
specifier: ^5.8.0
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^2.0.0
|
||||
version: 2.1.9(@types/node@22.19.15)
|
||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
||||
|
||||
packages/memory:
|
||||
dependencies:
|
||||
'@mosaic/db':
|
||||
specifier: workspace:*
|
||||
version: link:../db
|
||||
'@mosaic/types':
|
||||
specifier: workspace:*
|
||||
version: link:../types
|
||||
drizzle-orm:
|
||||
specifier: ^0.45.1
|
||||
version: 0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(kysely@0.28.11)(postgres@3.4.8)
|
||||
devDependencies:
|
||||
typescript:
|
||||
specifier: ^5.8.0
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^2.0.0
|
||||
version: 2.1.9(@types/node@22.19.15)
|
||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
||||
|
||||
packages/mosaic:
|
||||
devDependencies:
|
||||
@@ -343,7 +386,7 @@ importers:
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^2.0.0
|
||||
version: 2.1.9(@types/node@22.19.15)
|
||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
||||
|
||||
packages/prdy:
|
||||
devDependencies:
|
||||
@@ -352,7 +395,7 @@ importers:
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^2.0.0
|
||||
version: 2.1.9(@types/node@22.19.15)
|
||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
||||
|
||||
packages/quality-rails:
|
||||
devDependencies:
|
||||
@@ -361,7 +404,7 @@ importers:
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^2.0.0
|
||||
version: 2.1.9(@types/node@22.19.15)
|
||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
||||
|
||||
packages/queue:
|
||||
dependencies:
|
||||
@@ -377,7 +420,7 @@ importers:
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^2.0.0
|
||||
version: 2.1.9(@types/node@22.19.15)
|
||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
||||
|
||||
packages/types:
|
||||
dependencies:
|
||||
@@ -393,7 +436,7 @@ importers:
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^2.0.0
|
||||
version: 2.1.9(@types/node@22.19.15)
|
||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
||||
|
||||
plugins/discord:
|
||||
dependencies:
|
||||
@@ -412,7 +455,7 @@ importers:
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^2.0.0
|
||||
version: 2.1.9(@types/node@22.19.15)
|
||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
||||
|
||||
plugins/telegram:
|
||||
devDependencies:
|
||||
@@ -421,7 +464,7 @@ importers:
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^2.0.0
|
||||
version: 2.1.9(@types/node@22.19.15)
|
||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -429,6 +472,10 @@ packages:
|
||||
resolution: {integrity: sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw==}
|
||||
engines: {node: '>=14.13.1'}
|
||||
|
||||
'@alloc/quick-lru@5.2.0':
|
||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
'@anthropic-ai/sdk@0.73.0':
|
||||
resolution: {integrity: sha512-URURVzhxXGJDGUGFunIOtBlSl7KWvZiAAKY/ttTkZAkXT9bTPqdk2eK0b8qqSxXpikh3QKPnPYpiyX98zf5ebw==}
|
||||
hasBin: true
|
||||
@@ -1513,9 +1560,22 @@ packages:
|
||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||
|
||||
'@jridgewell/remapping@2.3.5':
|
||||
resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2':
|
||||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5':
|
||||
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@js-sdsl/ordered-map@4.4.2':
|
||||
resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==}
|
||||
|
||||
@@ -2590,6 +2650,94 @@ packages:
|
||||
'@swc/helpers@0.5.15':
|
||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
||||
|
||||
'@tailwindcss/node@4.2.1':
|
||||
resolution: {integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==}
|
||||
|
||||
'@tailwindcss/oxide-android-arm64@4.2.1':
|
||||
resolution: {integrity: sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@tailwindcss/oxide-darwin-arm64@4.2.1':
|
||||
resolution: {integrity: sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@tailwindcss/oxide-darwin-x64@4.2.1':
|
||||
resolution: {integrity: sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@tailwindcss/oxide-freebsd-x64@4.2.1':
|
||||
resolution: {integrity: sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1':
|
||||
resolution: {integrity: sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-gnu@4.2.1':
|
||||
resolution: {integrity: sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.2.1':
|
||||
resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.2.1':
|
||||
resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.2.1':
|
||||
resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@tailwindcss/oxide-wasm32-wasi@4.2.1':
|
||||
resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
cpu: [wasm32]
|
||||
bundledDependencies:
|
||||
- '@napi-rs/wasm-runtime'
|
||||
- '@emnapi/core'
|
||||
- '@emnapi/runtime'
|
||||
- '@tybys/wasm-util'
|
||||
- '@emnapi/wasi-threads'
|
||||
- tslib
|
||||
|
||||
'@tailwindcss/oxide-win32-arm64-msvc@4.2.1':
|
||||
resolution: {integrity: sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@tailwindcss/oxide-win32-x64-msvc@4.2.1':
|
||||
resolution: {integrity: sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@tailwindcss/oxide@4.2.1':
|
||||
resolution: {integrity: sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==}
|
||||
engines: {node: '>= 20'}
|
||||
|
||||
'@tailwindcss/postcss@4.2.1':
|
||||
resolution: {integrity: sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw==}
|
||||
|
||||
'@tokenizer/inflate@0.4.1':
|
||||
resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -2627,6 +2775,9 @@ packages:
|
||||
'@types/mysql@2.15.27':
|
||||
resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==}
|
||||
|
||||
'@types/node-cron@3.0.11':
|
||||
resolution: {integrity: sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==}
|
||||
|
||||
'@types/node@22.19.15':
|
||||
resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==}
|
||||
|
||||
@@ -3051,6 +3202,10 @@ packages:
|
||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
clsx@2.1.1:
|
||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
cluster-key-slot@1.1.2:
|
||||
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -3282,6 +3437,10 @@ packages:
|
||||
resolution: {integrity: sha512-U2SN0w3OpjFRVlrc17E6TMDmH58Xl9rai1MblNjAdwWp07Kk+llmzX0hjDpQdrDGzwmvOtgM5yI+meYX6iZ2xA==}
|
||||
engines: {node: '>=10.2.0'}
|
||||
|
||||
enhanced-resolve@5.20.0:
|
||||
resolution: {integrity: sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
environment@1.1.0:
|
||||
resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -3709,6 +3868,10 @@ packages:
|
||||
jackspeak@3.4.3:
|
||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||
|
||||
jiti@2.6.1:
|
||||
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
||||
hasBin: true
|
||||
|
||||
jose@6.2.1:
|
||||
resolution: {integrity: sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==}
|
||||
|
||||
@@ -3767,6 +3930,76 @@ packages:
|
||||
light-my-request@6.6.0:
|
||||
resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==}
|
||||
|
||||
lightningcss-android-arm64@1.31.1:
|
||||
resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
lightningcss-darwin-arm64@1.31.1:
|
||||
resolution: {integrity: sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
lightningcss-darwin-x64@1.31.1:
|
||||
resolution: {integrity: sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
lightningcss-freebsd-x64@1.31.1:
|
||||
resolution: {integrity: sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
lightningcss-linux-arm-gnueabihf@1.31.1:
|
||||
resolution: {integrity: sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
lightningcss-linux-arm64-gnu@1.31.1:
|
||||
resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.31.1:
|
||||
resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.31.1:
|
||||
resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
lightningcss-linux-x64-musl@1.31.1:
|
||||
resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.31.1:
|
||||
resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
lightningcss-win32-x64-msvc@1.31.1:
|
||||
resolution: {integrity: sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
lightningcss@1.31.1:
|
||||
resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
|
||||
lilconfig@3.1.3:
|
||||
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -3976,6 +4209,10 @@ packages:
|
||||
sass:
|
||||
optional: true
|
||||
|
||||
node-cron@4.2.1:
|
||||
resolution: {integrity: sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
node-domexception@1.0.0:
|
||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||
engines: {node: '>=10.5.0'}
|
||||
@@ -4489,9 +4726,16 @@ packages:
|
||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
tailwind-merge@3.5.0:
|
||||
resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==}
|
||||
|
||||
tailwindcss@4.2.1:
|
||||
resolution: {integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==}
|
||||
|
||||
tapable@2.3.0:
|
||||
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
thenify-all@1.6.0:
|
||||
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
|
||||
engines: {node: '>=0.8'}
|
||||
@@ -4841,6 +5085,8 @@ snapshots:
|
||||
ansi-styles: 6.2.3
|
||||
is-fullwidth-code-point: 4.0.0
|
||||
|
||||
'@alloc/quick-lru@5.2.0': {}
|
||||
|
||||
'@anthropic-ai/sdk@0.73.0(zod@4.3.6)':
|
||||
dependencies:
|
||||
json-schema-to-ts: 3.1.1
|
||||
@@ -5634,9 +5880,9 @@ snapshots:
|
||||
'@esbuild/win32-x64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)':
|
||||
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))':
|
||||
dependencies:
|
||||
eslint: 9.39.4
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
eslint-visitor-keys: 3.4.3
|
||||
|
||||
'@eslint-community/regexpp@4.12.2': {}
|
||||
@@ -5860,8 +6106,25 @@ snapshots:
|
||||
wrap-ansi: 8.1.0
|
||||
wrap-ansi-cjs: wrap-ansi@7.0.0
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
|
||||
'@jridgewell/remapping@2.3.5':
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.13
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2': {}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
dependencies:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@js-sdsl/ordered-map@4.4.2': {}
|
||||
|
||||
'@lukeed/csprng@1.1.0': {}
|
||||
@@ -7227,6 +7490,75 @@ snapshots:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@tailwindcss/node@4.2.1':
|
||||
dependencies:
|
||||
'@jridgewell/remapping': 2.3.5
|
||||
enhanced-resolve: 5.20.0
|
||||
jiti: 2.6.1
|
||||
lightningcss: 1.31.1
|
||||
magic-string: 0.30.21
|
||||
source-map-js: 1.2.1
|
||||
tailwindcss: 4.2.1
|
||||
|
||||
'@tailwindcss/oxide-android-arm64@4.2.1':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-darwin-arm64@4.2.1':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-darwin-x64@4.2.1':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-freebsd-x64@4.2.1':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-gnu@4.2.1':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.2.1':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.2.1':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.2.1':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-wasm32-wasi@4.2.1':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-win32-arm64-msvc@4.2.1':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-win32-x64-msvc@4.2.1':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide@4.2.1':
|
||||
optionalDependencies:
|
||||
'@tailwindcss/oxide-android-arm64': 4.2.1
|
||||
'@tailwindcss/oxide-darwin-arm64': 4.2.1
|
||||
'@tailwindcss/oxide-darwin-x64': 4.2.1
|
||||
'@tailwindcss/oxide-freebsd-x64': 4.2.1
|
||||
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.1
|
||||
'@tailwindcss/oxide-linux-arm64-gnu': 4.2.1
|
||||
'@tailwindcss/oxide-linux-arm64-musl': 4.2.1
|
||||
'@tailwindcss/oxide-linux-x64-gnu': 4.2.1
|
||||
'@tailwindcss/oxide-linux-x64-musl': 4.2.1
|
||||
'@tailwindcss/oxide-wasm32-wasi': 4.2.1
|
||||
'@tailwindcss/oxide-win32-arm64-msvc': 4.2.1
|
||||
'@tailwindcss/oxide-win32-x64-msvc': 4.2.1
|
||||
|
||||
'@tailwindcss/postcss@4.2.1':
|
||||
dependencies:
|
||||
'@alloc/quick-lru': 5.2.0
|
||||
'@tailwindcss/node': 4.2.1
|
||||
'@tailwindcss/oxide': 4.2.1
|
||||
postcss: 8.5.8
|
||||
tailwindcss: 4.2.1
|
||||
|
||||
'@tokenizer/inflate@0.4.1':
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
@@ -7266,6 +7598,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 22.19.15
|
||||
|
||||
'@types/node-cron@3.0.11': {}
|
||||
|
||||
'@types/node@22.19.15':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
@@ -7324,15 +7658,15 @@ snapshots:
|
||||
'@types/node': 22.19.15
|
||||
optional: true
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.57.0(@typescript-eslint/parser@8.57.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)':
|
||||
'@typescript-eslint/eslint-plugin@8.57.0(@typescript-eslint/parser@8.57.0(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
|
||||
'@typescript-eslint/parser': 8.57.0(eslint@9.39.4)(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/scope-manager': 8.57.0
|
||||
'@typescript-eslint/type-utils': 8.57.0(eslint@9.39.4)(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.57.0(eslint@9.39.4)(typescript@5.9.3)
|
||||
'@typescript-eslint/type-utils': 8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/visitor-keys': 8.57.0
|
||||
eslint: 9.39.4
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
ignore: 7.0.5
|
||||
natural-compare: 1.4.0
|
||||
ts-api-utils: 2.4.0(typescript@5.9.3)
|
||||
@@ -7340,14 +7674,14 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/parser@8.57.0(eslint@9.39.4)(typescript@5.9.3)':
|
||||
'@typescript-eslint/parser@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 8.57.0
|
||||
'@typescript-eslint/types': 8.57.0
|
||||
'@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3)
|
||||
'@typescript-eslint/visitor-keys': 8.57.0
|
||||
debug: 4.4.3
|
||||
eslint: 9.39.4
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -7370,13 +7704,13 @@ snapshots:
|
||||
dependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
'@typescript-eslint/type-utils@8.57.0(eslint@9.39.4)(typescript@5.9.3)':
|
||||
'@typescript-eslint/type-utils@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.57.0
|
||||
'@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.57.0(eslint@9.39.4)(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
debug: 4.4.3
|
||||
eslint: 9.39.4
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
ts-api-utils: 2.4.0(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
@@ -7399,13 +7733,13 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/utils@8.57.0(eslint@9.39.4)(typescript@5.9.3)':
|
||||
'@typescript-eslint/utils@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4)
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1))
|
||||
'@typescript-eslint/scope-manager': 8.57.0
|
||||
'@typescript-eslint/types': 8.57.0
|
||||
'@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3)
|
||||
eslint: 9.39.4
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -7422,13 +7756,13 @@ snapshots:
|
||||
chai: 5.3.3
|
||||
tinyrainbow: 1.2.0
|
||||
|
||||
'@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.19.15))':
|
||||
'@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.19.15)(lightningcss@1.31.1))':
|
||||
dependencies:
|
||||
'@vitest/spy': 2.1.9
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 5.4.21(@types/node@22.19.15)
|
||||
vite: 5.4.21(@types/node@22.19.15)(lightningcss@1.31.1)
|
||||
|
||||
'@vitest/pretty-format@2.1.9':
|
||||
dependencies:
|
||||
@@ -7539,7 +7873,7 @@ snapshots:
|
||||
|
||||
basic-ftp@5.2.0: {}
|
||||
|
||||
better-auth@1.5.5(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(kysely@0.28.11)(postgres@3.4.8))(mongodb@7.1.0(socks@2.8.7))(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@2.1.9(@types/node@22.19.15)):
|
||||
better-auth@1.5.5(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(kysely@0.28.11)(postgres@3.4.8))(mongodb@7.1.0(socks@2.8.7))(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)):
|
||||
dependencies:
|
||||
'@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1)
|
||||
'@better-auth/drizzle-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(kysely@0.28.11)(postgres@3.4.8))
|
||||
@@ -7565,7 +7899,7 @@ snapshots:
|
||||
next: 16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
vitest: 2.1.9(@types/node@22.19.15)
|
||||
vitest: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
||||
transitivePeerDependencies:
|
||||
- '@cloudflare/workers-types'
|
||||
|
||||
@@ -7680,6 +8014,8 @@ snapshots:
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 7.0.0
|
||||
|
||||
clsx@2.1.1: {}
|
||||
|
||||
cluster-key-slot@1.1.2: {}
|
||||
|
||||
code-excerpt@4.0.0:
|
||||
@@ -7743,8 +8079,7 @@ snapshots:
|
||||
|
||||
dequal@2.0.3: {}
|
||||
|
||||
detect-libc@2.1.2:
|
||||
optional: true
|
||||
detect-libc@2.1.2: {}
|
||||
|
||||
diff@8.0.3: {}
|
||||
|
||||
@@ -7832,6 +8167,11 @@ snapshots:
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
enhanced-resolve@5.20.0:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
tapable: 2.3.0
|
||||
|
||||
environment@1.1.0: {}
|
||||
|
||||
es-module-lexer@1.7.0: {}
|
||||
@@ -7979,9 +8319,9 @@ snapshots:
|
||||
|
||||
eslint-visitor-keys@5.0.1: {}
|
||||
|
||||
eslint@9.39.4:
|
||||
eslint@9.39.4(jiti@2.6.1):
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4)
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1))
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
'@eslint/config-array': 0.21.2
|
||||
'@eslint/config-helpers': 0.4.2
|
||||
@@ -8015,6 +8355,8 @@ snapshots:
|
||||
minimatch: 3.1.5
|
||||
natural-compare: 1.4.0
|
||||
optionator: 0.9.4
|
||||
optionalDependencies:
|
||||
jiti: 2.6.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -8427,6 +8769,8 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@pkgjs/parseargs': 0.11.0
|
||||
|
||||
jiti@2.6.1: {}
|
||||
|
||||
jose@6.2.1: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
@@ -8489,6 +8833,55 @@ snapshots:
|
||||
process-warning: 4.0.1
|
||||
set-cookie-parser: 2.7.2
|
||||
|
||||
lightningcss-android-arm64@1.31.1:
|
||||
optional: true
|
||||
|
||||
lightningcss-darwin-arm64@1.31.1:
|
||||
optional: true
|
||||
|
||||
lightningcss-darwin-x64@1.31.1:
|
||||
optional: true
|
||||
|
||||
lightningcss-freebsd-x64@1.31.1:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-arm-gnueabihf@1.31.1:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-arm64-gnu@1.31.1:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-arm64-musl@1.31.1:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-x64-gnu@1.31.1:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-x64-musl@1.31.1:
|
||||
optional: true
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.31.1:
|
||||
optional: true
|
||||
|
||||
lightningcss-win32-x64-msvc@1.31.1:
|
||||
optional: true
|
||||
|
||||
lightningcss@1.31.1:
|
||||
dependencies:
|
||||
detect-libc: 2.1.2
|
||||
optionalDependencies:
|
||||
lightningcss-android-arm64: 1.31.1
|
||||
lightningcss-darwin-arm64: 1.31.1
|
||||
lightningcss-darwin-x64: 1.31.1
|
||||
lightningcss-freebsd-x64: 1.31.1
|
||||
lightningcss-linux-arm-gnueabihf: 1.31.1
|
||||
lightningcss-linux-arm64-gnu: 1.31.1
|
||||
lightningcss-linux-arm64-musl: 1.31.1
|
||||
lightningcss-linux-x64-gnu: 1.31.1
|
||||
lightningcss-linux-x64-musl: 1.31.1
|
||||
lightningcss-win32-arm64-msvc: 1.31.1
|
||||
lightningcss-win32-x64-msvc: 1.31.1
|
||||
|
||||
lilconfig@3.1.3: {}
|
||||
|
||||
lint-staged@15.5.2:
|
||||
@@ -8662,6 +9055,8 @@ snapshots:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
|
||||
node-cron@4.2.1: {}
|
||||
|
||||
node-domexception@1.0.0: {}
|
||||
|
||||
node-fetch@3.3.2:
|
||||
@@ -9213,8 +9608,12 @@ snapshots:
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
|
||||
tailwind-merge@3.5.0: {}
|
||||
|
||||
tailwindcss@4.2.1: {}
|
||||
|
||||
tapable@2.3.0: {}
|
||||
|
||||
thenify-all@1.6.0:
|
||||
dependencies:
|
||||
thenify: 3.3.1
|
||||
@@ -9308,13 +9707,13 @@ snapshots:
|
||||
|
||||
type-fest@4.41.0: {}
|
||||
|
||||
typescript-eslint@8.57.0(eslint@9.39.4)(typescript@5.9.3):
|
||||
typescript-eslint@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@typescript-eslint/eslint-plugin': 8.57.0(@typescript-eslint/parser@8.57.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.57.0(eslint@9.39.4)(typescript@5.9.3)
|
||||
'@typescript-eslint/eslint-plugin': 8.57.0(@typescript-eslint/parser@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.57.0(eslint@9.39.4)(typescript@5.9.3)
|
||||
eslint: 9.39.4
|
||||
'@typescript-eslint/utils': 8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -9343,13 +9742,13 @@ snapshots:
|
||||
|
||||
vary@1.1.2: {}
|
||||
|
||||
vite-node@2.1.9(@types/node@22.19.15):
|
||||
vite-node@2.1.9(@types/node@22.19.15)(lightningcss@1.31.1):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.4.3
|
||||
es-module-lexer: 1.7.0
|
||||
pathe: 1.1.2
|
||||
vite: 5.4.21(@types/node@22.19.15)
|
||||
vite: 5.4.21(@types/node@22.19.15)(lightningcss@1.31.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- less
|
||||
@@ -9361,7 +9760,7 @@ snapshots:
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vite@5.4.21(@types/node@22.19.15):
|
||||
vite@5.4.21(@types/node@22.19.15)(lightningcss@1.31.1):
|
||||
dependencies:
|
||||
esbuild: 0.21.5
|
||||
postcss: 8.5.8
|
||||
@@ -9369,11 +9768,12 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/node': 22.19.15
|
||||
fsevents: 2.3.3
|
||||
lightningcss: 1.31.1
|
||||
|
||||
vitest@2.1.9(@types/node@22.19.15):
|
||||
vitest@2.1.9(@types/node@22.19.15)(lightningcss@1.31.1):
|
||||
dependencies:
|
||||
'@vitest/expect': 2.1.9
|
||||
'@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.19.15))
|
||||
'@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.19.15)(lightningcss@1.31.1))
|
||||
'@vitest/pretty-format': 2.1.9
|
||||
'@vitest/runner': 2.1.9
|
||||
'@vitest/snapshot': 2.1.9
|
||||
@@ -9389,8 +9789,8 @@ snapshots:
|
||||
tinyexec: 0.3.2
|
||||
tinypool: 1.1.1
|
||||
tinyrainbow: 1.2.0
|
||||
vite: 5.4.21(@types/node@22.19.15)
|
||||
vite-node: 2.1.9(@types/node@22.19.15)
|
||||
vite: 5.4.21(@types/node@22.19.15)(lightningcss@1.31.1)
|
||||
vite-node: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 22.19.15
|
||||
|
||||
Reference in New Issue
Block a user