feat(config): add MosaicConfig schema + loader with tier auto-detection
Some checks failed
ci/woodpecker/push/ci Pipeline failed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jarvis
2026-04-02 21:03:00 -05:00
parent 626adac363
commit 04a80fb9ba
12 changed files with 270 additions and 16 deletions

View File

@@ -19,6 +19,7 @@
"@modelcontextprotocol/sdk": "^1.27.1",
"@mosaic/auth": "workspace:^",
"@mosaic/brain": "workspace:^",
"@mosaic/config": "workspace:^",
"@mosaic/coord": "workspace:^",
"@mosaic/db": "workspace:^",
"@mosaic/discord-plugin": "workspace:^",

View File

@@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { HealthController } from './health/health.controller.js';
import { ConfigModule } from './config/config.module.js';
import { DatabaseModule } from './database/database.module.js';
import { AuthModule } from './auth/auth.module.js';
import { BrainModule } from './brain/brain.module.js';
@@ -28,6 +29,7 @@ import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
@Module({
imports: [
ThrottlerModule.forRoot([{ name: 'default', ttl: 60_000, limit: 60 }]),
ConfigModule,
DatabaseModule,
AuthModule,
BrainModule,

View File

@@ -0,0 +1,16 @@
import { Global, Module } from '@nestjs/common';
import { loadConfig, type MosaicConfig } from '@mosaic/config';
export const MOSAIC_CONFIG = 'MOSAIC_CONFIG';
@Global()
@Module({
providers: [
{
provide: MOSAIC_CONFIG,
useFactory: (): MosaicConfig => loadConfig(),
},
],
exports: [MOSAIC_CONFIG],
})
export class ConfigModule {}

View File

@@ -1,19 +1,21 @@
import { Global, Inject, Module, type OnApplicationShutdown } from '@nestjs/common';
import { createDb, type Db, type DbHandle } from '@mosaic/db';
import { createStorageAdapter, type StorageAdapter } from '@mosaic/storage';
import type { MosaicConfig } from '@mosaic/config';
import { MOSAIC_CONFIG } from '../config/config.module.js';
export const DB_HANDLE = 'DB_HANDLE';
export const DB = 'DB';
export const STORAGE_ADAPTER = 'STORAGE_ADAPTER';
const DEFAULT_DATABASE_URL = 'postgresql://mosaic:mosaic@localhost:5432/mosaic';
@Global()
@Module({
providers: [
{
provide: DB_HANDLE,
useFactory: (): DbHandle => createDb(),
useFactory: (config: MosaicConfig): DbHandle =>
createDb(config.storage.type === 'postgres' ? config.storage.url : undefined),
inject: [MOSAIC_CONFIG],
},
{
provide: DB,
@@ -22,11 +24,8 @@ const DEFAULT_DATABASE_URL = 'postgresql://mosaic:mosaic@localhost:5432/mosaic';
},
{
provide: STORAGE_ADAPTER,
useFactory: (): StorageAdapter =>
createStorageAdapter({
type: 'postgres',
url: process.env['DATABASE_URL'] ?? DEFAULT_DATABASE_URL,
}),
useFactory: (config: MosaicConfig): StorageAdapter => createStorageAdapter(config.storage),
inject: [MOSAIC_CONFIG],
},
],
exports: [DB, STORAGE_ADAPTER],

View File

@@ -1,13 +1,29 @@
import { Global, Module } from '@nestjs/common';
import { createMemory, type Memory, createMemoryAdapter, type MemoryAdapter } from '@mosaic/memory';
import {
createMemory,
type Memory,
createMemoryAdapter,
type MemoryAdapter,
type MemoryConfig,
} from '@mosaic/memory';
import type { Db } from '@mosaic/db';
import { DB } from '../database/database.module.js';
import type { StorageAdapter } from '@mosaic/storage';
import type { MosaicConfig } from '@mosaic/config';
import { MOSAIC_CONFIG } from '../config/config.module.js';
import { DB, STORAGE_ADAPTER } from '../database/database.module.js';
import { MEMORY } from './memory.tokens.js';
import { MemoryController } from './memory.controller.js';
import { EmbeddingService } from './embedding.service.js';
export const MEMORY_ADAPTER = 'MEMORY_ADAPTER';
function buildMemoryConfig(config: MosaicConfig, storageAdapter: StorageAdapter): MemoryConfig {
if (config.memory.type === 'keyword') {
return { type: 'keyword', storage: storageAdapter };
}
return { type: config.memory.type };
}
@Global()
@Module({
providers: [
@@ -18,7 +34,9 @@ export const MEMORY_ADAPTER = 'MEMORY_ADAPTER';
},
{
provide: MEMORY_ADAPTER,
useFactory: (): MemoryAdapter => createMemoryAdapter({ type: 'pgvector' }),
useFactory: (config: MosaicConfig, storageAdapter: StorageAdapter): MemoryAdapter =>
createMemoryAdapter(buildMemoryConfig(config, storageAdapter)),
inject: [MOSAIC_CONFIG, STORAGE_ADAPTER],
},
EmbeddingService,
],

View File

@@ -1,5 +1,7 @@
import { Global, Module } from '@nestjs/common';
import { createQueueAdapter, type QueueAdapter } from '@mosaic/queue';
import type { MosaicConfig } from '@mosaic/config';
import { MOSAIC_CONFIG } from '../config/config.module.js';
import { QueueService } from './queue.service.js';
export const QUEUE_ADAPTER = 'QUEUE_ADAPTER';
@@ -10,11 +12,8 @@ export const QUEUE_ADAPTER = 'QUEUE_ADAPTER';
QueueService,
{
provide: QUEUE_ADAPTER,
useFactory: (): QueueAdapter =>
createQueueAdapter({
type: 'bullmq',
url: process.env['VALKEY_URL'],
}),
useFactory: (config: MosaicConfig): QueueAdapter => createQueueAdapter(config.queue),
inject: [MOSAIC_CONFIG],
},
],
exports: [QueueService, QUEUE_ADAPTER],

6
mosaic.config.json Normal file
View File

@@ -0,0 +1,6 @@
{
"tier": "local",
"storage": { "type": "sqlite", "path": ".mosaic/data.db" },
"queue": { "type": "local", "dataDir": ".mosaic/queue" },
"memory": { "type": "keyword" }
}

View File

@@ -0,0 +1,35 @@
{
"name": "@mosaic/config",
"version": "0.0.1",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"scripts": {
"build": "tsc",
"lint": "eslint src",
"typecheck": "tsc --noEmit",
"test": "vitest run --passWithNoTests"
},
"dependencies": {
"@mosaic/memory": "workspace:^",
"@mosaic/queue": "workspace:^",
"@mosaic/storage": "workspace:^"
},
"devDependencies": {
"typescript": "^5.8.0",
"vitest": "^2.0.0"
},
"publishConfig": {
"registry": "https://git.mosaicstack.dev/api/packages/mosaic/npm/",
"access": "public"
},
"files": [
"dist"
]
}

View File

@@ -0,0 +1,7 @@
export type { MosaicConfig, StorageTier, MemoryConfigRef } from './mosaic-config.js';
export {
DEFAULT_LOCAL_CONFIG,
DEFAULT_TEAM_CONFIG,
loadConfig,
validateConfig,
} from './mosaic-config.js';

View File

@@ -0,0 +1,140 @@
import { readFileSync, existsSync } from 'node:fs';
import { resolve } from 'node:path';
import type { StorageConfig } from '@mosaic/storage';
import type { QueueAdapterConfig as QueueConfig } from '@mosaic/queue';
/* ------------------------------------------------------------------ */
/* Types */
/* ------------------------------------------------------------------ */
export type StorageTier = 'local' | 'team';
export interface MemoryConfigRef {
type: 'pgvector' | 'sqlite-vec' | 'keyword';
}
export interface MosaicConfig {
tier: StorageTier;
storage: StorageConfig;
queue: QueueConfig;
memory: MemoryConfigRef;
}
/* ------------------------------------------------------------------ */
/* Defaults */
/* ------------------------------------------------------------------ */
export const DEFAULT_LOCAL_CONFIG: MosaicConfig = {
tier: 'local',
storage: { type: 'sqlite', path: '.mosaic/data.db' },
queue: { type: 'local', dataDir: '.mosaic/queue' },
memory: { type: 'keyword' },
};
export const DEFAULT_TEAM_CONFIG: MosaicConfig = {
tier: 'team',
storage: { type: 'postgres', url: 'postgresql://mosaic:mosaic@localhost:5432/mosaic' },
queue: { type: 'bullmq' },
memory: { type: 'pgvector' },
};
/* ------------------------------------------------------------------ */
/* Validation */
/* ------------------------------------------------------------------ */
const VALID_TIERS = new Set<string>(['local', 'team']);
const VALID_STORAGE_TYPES = new Set<string>(['postgres', 'sqlite', 'files']);
const VALID_QUEUE_TYPES = new Set<string>(['bullmq', 'local']);
const VALID_MEMORY_TYPES = new Set<string>(['pgvector', 'sqlite-vec', 'keyword']);
export function validateConfig(raw: unknown): MosaicConfig {
if (typeof raw !== 'object' || raw === null) {
throw new Error('MosaicConfig must be a non-null object');
}
const obj = raw as Record<string, unknown>;
// tier
const tier = obj['tier'];
if (typeof tier !== 'string' || !VALID_TIERS.has(tier)) {
throw new Error(`Invalid tier "${String(tier)}" — expected "local" or "team"`);
}
// storage
const storage = obj['storage'];
if (typeof storage !== 'object' || storage === null) {
throw new Error('config.storage must be a non-null object');
}
const storageType = (storage as Record<string, unknown>)['type'];
if (typeof storageType !== 'string' || !VALID_STORAGE_TYPES.has(storageType)) {
throw new Error(`Invalid storage.type "${String(storageType)}"`);
}
// queue
const queue = obj['queue'];
if (typeof queue !== 'object' || queue === null) {
throw new Error('config.queue must be a non-null object');
}
const queueType = (queue as Record<string, unknown>)['type'];
if (typeof queueType !== 'string' || !VALID_QUEUE_TYPES.has(queueType)) {
throw new Error(`Invalid queue.type "${String(queueType)}"`);
}
// memory
const memory = obj['memory'];
if (typeof memory !== 'object' || memory === null) {
throw new Error('config.memory must be a non-null object');
}
const memoryType = (memory as Record<string, unknown>)['type'];
if (typeof memoryType !== 'string' || !VALID_MEMORY_TYPES.has(memoryType)) {
throw new Error(`Invalid memory.type "${String(memoryType)}"`);
}
return {
tier: tier as StorageTier,
storage: storage as StorageConfig,
queue: queue as QueueConfig,
memory: memory as MemoryConfigRef,
};
}
/* ------------------------------------------------------------------ */
/* Loader */
/* ------------------------------------------------------------------ */
function detectFromEnv(): MosaicConfig {
if (process.env['DATABASE_URL']) {
return {
...DEFAULT_TEAM_CONFIG,
storage: {
type: 'postgres',
url: process.env['DATABASE_URL'],
},
queue: {
type: 'bullmq',
url: process.env['VALKEY_URL'],
},
};
}
return DEFAULT_LOCAL_CONFIG;
}
export function loadConfig(configPath?: string): MosaicConfig {
// 1. Explicit path or default location
const paths = configPath
? [resolve(configPath)]
: [
resolve(process.cwd(), 'mosaic.config.json'),
resolve(process.cwd(), '../../mosaic.config.json'), // monorepo root when cwd is apps/gateway
];
for (const p of paths) {
if (existsSync(p)) {
const raw: unknown = JSON.parse(readFileSync(p, 'utf-8'));
return validateConfig(raw);
}
}
// 2. Fall back to env-var detection
return detectFromEnv();
}

View File

@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

22
pnpm-lock.yaml generated
View File

@@ -62,6 +62,9 @@ importers:
'@mosaic/brain':
specifier: workspace:^
version: link:../../packages/brain
'@mosaic/config':
specifier: workspace:^
version: link:../../packages/config
'@mosaic/coord':
specifier: workspace:^
version: link:../../packages/coord
@@ -351,6 +354,25 @@ importers:
specifier: ^2.0.0
version: 2.1.9(@types/node@22.19.15)(jsdom@29.0.0(@noble/hashes@2.0.1))(lightningcss@1.31.1)
packages/config:
dependencies:
'@mosaic/memory':
specifier: workspace:^
version: link:../memory
'@mosaic/queue':
specifier: workspace:^
version: link:../queue
'@mosaic/storage':
specifier: workspace:^
version: link:../storage
devDependencies:
typescript:
specifier: ^5.8.0
version: 5.9.3
vitest:
specifier: ^2.0.0
version: 2.1.9(@types/node@24.12.0)(jsdom@29.0.0(@noble/hashes@2.0.1))(lightningcss@1.31.1)
packages/coord:
dependencies:
'@mosaic/types':